| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111 |
- "use client";
- import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
- interface Member {
- user_id: string;
- role: "admin" | "member";
- joined_at: string;
- users: { display_name: string; avatar_color: string | null } | null;
- }
- interface MembersResponse {
- members: Member[];
- currentUserRole: "admin" | "member";
- }
- export function MemberList({
- groupId,
- onTransferRequest,
- }: {
- groupId: string;
- onTransferRequest: (userId: string, displayName: string) => void;
- }) {
- const queryClient = useQueryClient();
- const { data, isLoading, error } = useQuery<MembersResponse>({
- queryKey: ["groups", groupId, "members"],
- queryFn: async () => {
- const res = await fetch(`/api/groups/${groupId}/members`);
- if (!res.ok) throw new Error("Failed to fetch members");
- return res.json();
- },
- staleTime: 30_000,
- });
- const removeMember = useMutation({
- mutationFn: async (userId: string) => {
- const res = await fetch(`/api/groups/${groupId}/members`, {
- method: "DELETE",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ user_id: userId }),
- });
- if (!res.ok) {
- const data = await res.json();
- throw new Error(data.error || "Failed to remove member");
- }
- return res.json();
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ["groups", groupId, "members"] });
- },
- });
- if (isLoading) return <p className="text-sm text-gray-500">Loading members...</p>;
- if (error) return <p className="text-sm text-red-500">Failed to load members</p>;
- if (!data) return null;
- const isAdmin = data.currentUserRole === "admin";
- return (
- <div>
- <h3 className="text-sm font-medium mb-2">Members ({data.members.length})</h3>
- <ul className="space-y-2">
- {data.members.map((member) => (
- <li
- key={member.user_id}
- className="flex items-center justify-between rounded-md border border-gray-200 p-2 dark:border-gray-700"
- >
- <div className="flex items-center gap-2">
- <span
- className="inline-block h-6 w-6 rounded-full"
- style={{ backgroundColor: member.users?.avatar_color ?? "#6b7280" }}
- aria-hidden="true"
- />
- <span className="text-sm">{member.users?.display_name ?? "Unknown"}</span>
- {member.role === "admin" && (
- <span className="rounded bg-blue-100 px-1.5 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900/30 dark:text-blue-300">
- Admin
- </span>
- )}
- </div>
- {isAdmin && member.role !== "admin" && (
- <div className="flex gap-1">
- <button
- onClick={() =>
- onTransferRequest(member.user_id, member.users?.display_name ?? "this member")
- }
- className="rounded px-2 py-1 text-xs text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20"
- >
- Make Admin
- </button>
- <button
- onClick={() => removeMember.mutate(member.user_id)}
- disabled={removeMember.isPending}
- className="rounded px-2 py-1 text-xs text-red-600 hover:bg-red-50 disabled:opacity-50 dark:text-red-400 dark:hover:bg-red-900/20"
- >
- Remove
- </button>
- </div>
- )}
- </li>
- ))}
- </ul>
- {removeMember.error && (
- <p className="mt-2 text-sm text-red-500" role="alert">
- {removeMember.error.message}
- </p>
- )}
- </div>
- );
- }
|