member-list.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. "use client";
  2. import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
  3. interface Member {
  4. user_id: string;
  5. role: "admin" | "member";
  6. joined_at: string;
  7. users: { display_name: string; avatar_color: string | null } | null;
  8. }
  9. interface MembersResponse {
  10. members: Member[];
  11. currentUserRole: "admin" | "member";
  12. }
  13. export function MemberList({
  14. groupId,
  15. onTransferRequest,
  16. }: {
  17. groupId: string;
  18. onTransferRequest: (userId: string, displayName: string) => void;
  19. }) {
  20. const queryClient = useQueryClient();
  21. const { data, isLoading, error } = useQuery<MembersResponse>({
  22. queryKey: ["groups", groupId, "members"],
  23. queryFn: async () => {
  24. const res = await fetch(`/api/groups/${groupId}/members`);
  25. if (!res.ok) throw new Error("Failed to fetch members");
  26. return res.json();
  27. },
  28. staleTime: 30_000,
  29. });
  30. const removeMember = useMutation({
  31. mutationFn: async (userId: string) => {
  32. const res = await fetch(`/api/groups/${groupId}/members`, {
  33. method: "DELETE",
  34. headers: { "Content-Type": "application/json" },
  35. body: JSON.stringify({ user_id: userId }),
  36. });
  37. if (!res.ok) {
  38. const data = await res.json();
  39. throw new Error(data.error || "Failed to remove member");
  40. }
  41. return res.json();
  42. },
  43. onSuccess: () => {
  44. queryClient.invalidateQueries({ queryKey: ["groups", groupId, "members"] });
  45. },
  46. });
  47. if (isLoading) return <p className="text-sm text-gray-500">Loading members...</p>;
  48. if (error) return <p className="text-sm text-red-500">Failed to load members</p>;
  49. if (!data) return null;
  50. const isAdmin = data.currentUserRole === "admin";
  51. return (
  52. <div>
  53. <h3 className="text-sm font-medium mb-2">Members ({data.members.length})</h3>
  54. <ul className="space-y-2">
  55. {data.members.map((member) => (
  56. <li
  57. key={member.user_id}
  58. className="flex items-center justify-between rounded-md border border-gray-200 p-2 dark:border-gray-700"
  59. >
  60. <div className="flex items-center gap-2">
  61. <span
  62. className="inline-block h-6 w-6 rounded-full"
  63. style={{ backgroundColor: member.users?.avatar_color ?? "#6b7280" }}
  64. aria-hidden="true"
  65. />
  66. <span className="text-sm">{member.users?.display_name ?? "Unknown"}</span>
  67. {member.role === "admin" && (
  68. <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">
  69. Admin
  70. </span>
  71. )}
  72. </div>
  73. {isAdmin && member.role !== "admin" && (
  74. <div className="flex gap-1">
  75. <button
  76. onClick={() =>
  77. onTransferRequest(member.user_id, member.users?.display_name ?? "this member")
  78. }
  79. 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"
  80. >
  81. Make Admin
  82. </button>
  83. <button
  84. onClick={() => removeMember.mutate(member.user_id)}
  85. disabled={removeMember.isPending}
  86. 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"
  87. >
  88. Remove
  89. </button>
  90. </div>
  91. )}
  92. </li>
  93. ))}
  94. </ul>
  95. {removeMember.error && (
  96. <p className="mt-2 text-sm text-red-500" role="alert">
  97. {removeMember.error.message}
  98. </p>
  99. )}
  100. </div>
  101. );
  102. }