From aca3d3249898fa786c107ec72e40592721fd20c9 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 6 Jul 2025 17:23:44 +0200 Subject: [PATCH] Show members of a tablo --- api/src/tablo.ts | 32 ++++ sql/10_create_tablo_access_table.sql | 4 + ui/src/components/TabloModal.tsx | 252 +++++++++++++++++++++------ ui/src/hooks/tablos.ts | 19 ++ ui/src/pages/tablo.tsx | 42 ++--- 5 files changed, 277 insertions(+), 72 deletions(-) diff --git a/api/src/tablo.ts b/api/src/tablo.ts index 97f8320..eb02ca8 100644 --- a/api/src/tablo.ts +++ b/api/src/tablo.ts @@ -230,3 +230,35 @@ tabloRouter.post("/join", async (c) => { return c.json({ message: "Tablo joined successfully" }); }); + +tabloRouter.get("/members/:tablo_id", async (c) => { + const user = c.get("user"); + const supabase = c.get("supabase"); + const { tablo_id } = c.req.param(); + + const { data: tabloData, error: tabloError } = await supabase + .from("user_tablos") + .select("*") + .eq("id", tablo_id) + .eq("user_id", user.id); + + if (!tabloData || tabloData.length === 0) { + return c.json({ error: "You are not a member of this tablo" }, 403); + } + + if (tabloError) { + return c.json({ error: "Internal server error" }, 500); + } + + const { data, error } = await supabase + .from("tablo_access") + .select("profiles(id, name)") + .eq("tablo_id", tablo_id) + .eq("is_active", true); + + if (error) { + return c.json({ error: error.message }, 500); + } + + return c.json({ members: data.map((member) => member.profiles) }); +}); diff --git a/sql/10_create_tablo_access_table.sql b/sql/10_create_tablo_access_table.sql index 03a7b6a..c0a2d78 100644 --- a/sql/10_create_tablo_access_table.sql +++ b/sql/10_create_tablo_access_table.sql @@ -19,6 +19,10 @@ CREATE TABLE IF NOT EXISTS tablo_access ( -- Foreign key constraint to users table (auth.users) CONSTRAINT fk_tablo_access_user_id FOREIGN KEY (user_id) REFERENCES auth.users(id) ON DELETE CASCADE, + + -- Foreign key constraint to profiles table + CONSTRAINT fk_tablo_access_user_id_from_profiles + FOREIGN KEY (user_id) REFERENCES profiles(id) ); -- Create indexes for performance diff --git a/ui/src/components/TabloModal.tsx b/ui/src/components/TabloModal.tsx index f1fbfc6..1dd0406 100644 --- a/ui/src/components/TabloModal.tsx +++ b/ui/src/components/TabloModal.tsx @@ -4,6 +4,8 @@ import { ImageColorPicker } from "./ImageColorPicker"; import { StatusPicker } from "./StatusPicker"; import { useInviteUser } from "@ui/hooks/invite"; import { TabloUpdate, UserTablo } from "@ui/types/tablos.types"; +import { useTabloMembers } from "@ui/hooks/tablos"; +import { useUser } from "@ui/providers/UserStoreProvider"; type StatusType = "todo" | "in_progress" | "done"; @@ -11,9 +13,17 @@ interface TabloModalProps { tablo: UserTablo | null; onEdit: (updatedTablo: TabloUpdate & { id: string }) => void; onClose: () => void; + readOnly?: boolean; } -export const TabloModal = ({ tablo, onClose, onEdit }: TabloModalProps) => { +export const TabloModal = ({ + tablo, + onClose, + onEdit, + readOnly = false, +}: TabloModalProps) => { + const currentUser = useUser(); + const [editData, setEditData] = useState(tablo); const [isEditingName, setIsEditingName] = useState(false); @@ -22,6 +32,9 @@ export const TabloModal = ({ tablo, onClose, onEdit }: TabloModalProps) => { tablo?.color || "bg-blue-500" ); + const { data: members } = useTabloMembers(tablo?.id ?? ""); + const [showMembers, setShowMembers] = useState(false); + const [inviteEmail, setInviteEmail] = useState(""); const inviteUser = useInviteUser(); @@ -60,7 +73,7 @@ export const TabloModal = ({ tablo, onClose, onEdit }: TabloModalProps) => {
{/* Header */}
- {isEditingName ? ( + {!readOnly && isEditingName ? ( setIsEditingName(false)}> { ) : (

setIsEditingName(true)} + className={`text-2xl font-bold text-gray-900 dark:text-white ${ + !readOnly + ? "cursor-text hover:text-blue-600 dark:hover:text-blue-400 transition-colors" + : "" + }`} + onClick={!readOnly ? () => setIsEditingName(true) : undefined} > {tablo.name}

@@ -91,68 +108,201 @@ export const TabloModal = ({ tablo, onClose, onEdit }: TabloModalProps) => { {/* Content - Scrollable */}
- + {readOnly ? ( + /* Read-only content */ +
+ {/* Tablo Preview */} +
+ {tablo.image ? ( + {tablo.name} + ) : ( +
+

+ {tablo.name} +

+
+ )} +
- {/* Details */} -
-
- - setEditData((prev) => (prev ? { ...prev, status } : null)) - } - /> -
+ {/* Status Display */} +
+ +
+ {currentData.status === "todo" && "À faire"} + {currentData.status === "in_progress" && "En cours"} + {currentData.status === "done" && "Terminé"} +
+
- {/* Invite User Section */} -
-

- Inviter un utilisateur -

-
- setInviteEmail(e.target.value)} - placeholder="Email de l'utilisateur à inviter" - className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" - /> - + {/* Access Level */} +
+ +
+ {tablo.is_admin ? "Administrateur" : "Invité"} +
+
+ + {/* Members placeholder - can be expanded later */} +
+

+ Membres +

+
+ Vous avez accès à ce tablo en tant qu'invité +
+ ) : ( + /* Editable content */ + <> + + + {/* Details */} +
+
+ + setEditData((prev) => + prev ? { ...prev, status } : null + ) + } + /> +
+ + {/* Invite User Section */} +
+

+ Inviter un utilisateur +

+
+ setInviteEmail(e.target.value)} + placeholder="Email de l'utilisateur à inviter" + className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white" + /> + +
+
+
+ + )} +
+ + {/* Members Section */} +
+
+

+ Membres +

+
+ + {showMembers && ( +
+ {members && members.length > 0 ? ( + members.map((member, index) => ( +
+
+ {member.name.charAt(0).toUpperCase()} +
+ + {member.name} + + {member.id === currentUser?.id && ( + + (Vous) + + )} +
+ )) + ) : ( +

+ Aucun membre trouvé +

+ )} +
+ )}
{/* Footer */}
- <> + {readOnly ? ( - - + ) : ( + <> + + + + )}
diff --git a/ui/src/hooks/tablos.ts b/ui/src/hooks/tablos.ts index cc5386f..f21fb40 100644 --- a/ui/src/hooks/tablos.ts +++ b/ui/src/hooks/tablos.ts @@ -49,6 +49,25 @@ export const useTablo = (id: string) => { }); }; +// Fetch tablo members +export const useTabloMembers = (tabloId: string) => { + const { session } = useSession(); + const { data, isLoading, error } = useQuery({ + queryKey: ["tablo-members", tabloId], + queryFn: async () => { + const { data } = await api.get<{ + members: { id: string; name: string }[]; + }>(`/api/v1/tablos/members/${tabloId}`, { + headers: { + Authorization: `Bearer ${session?.access_token}`, + }, + }); + return data.members; + }, + }); + + return { data, isLoading, error }; +}; // Create new tablo export const useCreateTablo = () => { const { session } = useSession(); diff --git a/ui/src/pages/tablo.tsx b/ui/src/pages/tablo.tsx index a3bfd89..c682c0b 100644 --- a/ui/src/pages/tablo.tsx +++ b/ui/src/pages/tablo.tsx @@ -73,6 +73,7 @@ export const TabloPage = () => { { name: "Notes", action: (tabloId: string) => navigate(`/tablo/${tabloId}/notes`), + isDisabled: true, }, ]; @@ -300,16 +301,12 @@ export const TabloPage = () => { className={`bg-white dark:bg-gray-800 rounded-lg shadow-lg transition-all duration-300 w-64 overflow-hidden border border-gray-200 dark:border-gray-700 ${ isAdmin ? "hover:shadow-xl cursor-pointer" - : "cursor-default opacity-75" + : "hover:shadow-xl cursor-pointer opacity-75" }`} - onClick={ - isAdmin - ? (e) => { - e.stopPropagation(); - openTablo(tablo.id); - } - : undefined - } + onClick={(e) => { + e.stopPropagation(); + openTablo(tablo.id); + }} > {/* Image or Color */}
@@ -431,18 +428,20 @@ export const TabloPage = () => { onClick={(e) => e.stopPropagation()} > {/* Regular menu items - always show */} - {menuItems.map((item, index) => ( - - ))} + {menuItems + .filter((item) => !item.isDisabled) + .map((item, index) => ( + + ))} {/*
*/} @@ -650,6 +649,7 @@ export const TabloPage = () => { tablo={viewingTablo} onEdit={onEditTablo} onClose={closeTabloModal} + readOnly={!viewingTablo.is_admin} /> )}