diff --git a/apps/main/src/components/CustomChannelHeader.tsx b/apps/main/src/components/CustomChannelHeader.tsx index 6e2c6ec..bc7373c 100644 --- a/apps/main/src/components/CustomChannelHeader.tsx +++ b/apps/main/src/components/CustomChannelHeader.tsx @@ -6,18 +6,20 @@ interface CustomChannelHeaderProps { tablos: UserTablo[]; onToggleChannelList?: () => void; isChannelListExpanded?: boolean; + showToggleButton?: boolean; } export const CustomChannelHeader = ({ tablos, onToggleChannelList, isChannelListExpanded = false, + showToggleButton = true, }: CustomChannelHeaderProps) => { const { channel } = useChannelStateContext(); return (
- {onToggleChannelList && ( + {showToggleButton && onToggleChannelList && ( +
+ )} + + {/* File Upload Section - Only for Admins */} + {isAdmin && ( +
+
+ + + +

Ajouter un fichier

+
+ + {!selectedFile ? ( +
+ + + +
+ ) : ( +
+
+
+ + + +
+
+

+ {selectedFile.name} +

+

+ {(selectedFile.size / 1024 / 1024).toFixed(2)} MB +

+
+
+ +
+ + +
+
+ )} + +

Taille maximale: 20MB

+
+ )} + + {/* File List */} +
+

+ Liste des fichiers + {fileData?.fileNames && ( + + ({fileData.fileNames.length}) + + )} +

+ + {filesLoading ? ( +
+
+ + Chargement des fichiers... + +
+ ) : filesError ? ( +
+
+ + + + + Erreur lors du chargement des fichiers + +
+
+ ) : fileData && fileData.fileNames && fileData.fileNames.length > 0 ? ( +
+ {fileData.fileNames.map((fileName, index) => { + const fileExtension = fileName.split(".").pop()?.toLowerCase() || ""; + const isImage = ["jpg", "jpeg", "png", "gif", "webp", "svg"].includes(fileExtension); + const isPdf = fileExtension === "pdf"; + const isText = ["txt", "md", "json", "csv"].includes(fileExtension); + + return ( +
+ +
+

+ {fileName} +

+

+ {fileExtension || "Fichier"} +

+
+
+ + {isAdmin && ( + + )} +
+
+ ); + })} +
+ ) : ( +
+ + + + +

Aucun fichier dans ce tablo

+ {isAdmin && ( +

+ Ajoutez votre premier fichier ci-dessus +

+ )} +
+ )} +
+ + ); +}; diff --git a/apps/main/src/components/TabloSettingsSection.tsx b/apps/main/src/components/TabloSettingsSection.tsx new file mode 100644 index 0000000..4b860cd --- /dev/null +++ b/apps/main/src/components/TabloSettingsSection.tsx @@ -0,0 +1,254 @@ +import { TabloUpdate, UserTablo } from "@xtablo/shared/types/tablos.types"; +import { Button } from "@xtablo/ui/components/button"; +import { useEffect, useRef, useState } from "react"; +import { useInviteUser } from "../hooks/invite"; +import { useTabloMembers } from "../hooks/tablos"; +import { useUser } from "../providers/UserStoreProvider"; +import { ClickOutside } from "./ClickOutside"; +import { ImageColorPicker } from "./ImageColorPicker"; +import { StatusPicker } from "./StatusPicker"; + +type StatusType = "todo" | "in_progress" | "done"; + +interface TabloSettingsSectionProps { + tablo: UserTablo; + isAdmin: boolean; + onEdit: (updatedTablo: TabloUpdate & { id: string }) => void; +} + +export const TabloSettingsSection = ({ tablo, isAdmin, onEdit }: TabloSettingsSectionProps) => { + const currentUser = useUser(); + const [editData, setEditData] = useState(tablo); + const [isEditingName, setIsEditingName] = useState(false); + const [creationMode, setCreationMode] = useState<"image" | "color">("color"); + const [selectedColor, setSelectedColor] = useState(tablo.color || "bg-blue-500"); + const { data: members } = useTabloMembers(tablo.id); + + const [inviteEmail, setInviteEmail] = useState(""); + const { mutate: inviteUser, isPending: isInvitingUser } = useInviteUser(); + + const nameInputRef = useRef(null); + + useEffect(() => { + setEditData(tablo); + setSelectedColor(tablo.color || "bg-blue-500"); + }, [tablo]); + + // Auto-focus name input when editing + useEffect(() => { + if (isEditingName && nameInputRef.current) { + nameInputRef.current.focus(); + nameInputRef.current.select(); + } + }, [isEditingName]); + + const handleSaveEdit = () => { + if (editData && onEdit) { + const updatedTablo: TabloUpdate & { id: string } = { + id: editData.id, + name: editData.name, + color: creationMode === "color" ? selectedColor : null, + status: editData.status, + }; + onEdit(updatedTablo); + } + }; + + const handleSendInvite = () => { + if (inviteEmail.trim()) { + inviteUser({ email: inviteEmail, tablo_id: tablo.id }); + setInviteEmail(""); + } + }; + + const isEmailValid = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + }; + + const currentData = editData || tablo; + + return ( +
+
+

Paramètres

+

Configurez votre tablo et gérez les accès

+
+ + {!isAdmin ? ( + /* Read-only view for non-admins */ +
+ {/* Tablo Preview */} +
+

Aperçu

+
+ {tablo.image ? ( + {tablo.name} + ) : ( +
+

{tablo.name}

+
+ )} +
+
+ + {/* Status Display */} +
+

Statut

+
+ {currentData.status === "todo" && "À faire"} + {currentData.status === "in_progress" && "En cours"} + {currentData.status === "done" && "Terminé"} +
+
+ + {/* Access Level */} +
+

Votre rôle

+
{tablo.is_admin ? "Administrateur" : "Invité"}
+
+
+ ) : ( + /* Editable view for admins */ +
+ {/* Name Edit */} +
+

Nom du tablo

+ {isEditingName ? ( + setIsEditingName(false)}> + + setEditData((prev) => (prev ? { ...prev, name: e.target.value } : null)) + } + onKeyDown={(e) => { + if (e.key === "Enter") { + setIsEditingName(false); + } + }} + className="w-full px-3 py-2 text-lg font-medium text-foreground bg-transparent border-b-2 border-primary focus:outline-none focus:border-primary" + placeholder="Nom du tablo" + /> + + ) : ( +
setIsEditingName(true)} + > + {tablo.name} +
+ )} +
+ + {/* Color/Image Picker */} +
+

Apparence

+ +
+ + {/* Status Picker */} +
+

Statut

+ + 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-input rounded-md shadow-sm placeholder-muted-foreground focus:outline-none focus:ring-primary focus:border-primary bg-background text-foreground" + /> + {isInvitingUser ? ( +
+
+
+ ) : ( + + )} +
+
+ + {/* Save Button */} +
+ +
+
+ )} + + {/* Members List */} +
+

+ Membres + {members && ( + + ({members.length}) + + )} +

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

Aucun membre trouvé

+ )} +
+
+
+ ); +}; diff --git a/apps/main/src/lib/routes.tsx b/apps/main/src/lib/routes.tsx index 92886c1..7f5c9c7 100644 --- a/apps/main/src/lib/routes.tsx +++ b/apps/main/src/lib/routes.tsx @@ -20,6 +20,7 @@ import { ResetPasswordPage } from "../pages/reset-password"; import SettingsPage from "../pages/settings"; import { SignUpPage } from "../pages/signup"; import { TabloPage } from "../pages/tablo"; +import { TabloDetailsPage } from "../pages/tablo-details"; import ChatProvider from "../providers/ChatProvider"; export const routes: RouteObject[] = [ @@ -28,6 +29,10 @@ export const routes: RouteObject[] = [ path: "/", element: , children: [ + { + path: "tablos/:tabloId", + element: , + }, { path: "", element: , diff --git a/apps/main/src/pages/tablo-details.tsx b/apps/main/src/pages/tablo-details.tsx new file mode 100644 index 0000000..4623dec --- /dev/null +++ b/apps/main/src/pages/tablo-details.tsx @@ -0,0 +1,180 @@ +import { toast } from "@xtablo/shared"; +import { TabloUpdate, UserTablo } from "@xtablo/shared/types/tablos.types"; +import { Button } from "@xtablo/ui/components/button"; +import { ArrowLeft, FileText, MessageSquare, Settings } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { LoadingSpinner } from "../components/LoadingSpinner"; +import { TabloDiscussionSection } from "../components/TabloDiscussionSection"; +import { TabloFilesSection } from "../components/TabloFilesSection"; +import { TabloSettingsSection } from "../components/TabloSettingsSection"; +import { useTablosList, useUpdateTablo } from "../hooks/tablos"; + +type TabSection = "files" | "discussion" | "settings"; + +export const TabloDetailsPage = () => { + const { tabloId } = useParams<{ tabloId: string }>(); + const navigate = useNavigate(); + const { data: tablos, isLoading } = useTablosList(); + const { mutateAsync: updateTablo } = useUpdateTablo(); + + const [activeSection, setActiveSection] = useState("files"); + const [tablo, setTablo] = useState(null); + + useEffect(() => { + if (tablos && tabloId) { + const foundTablo = tablos.find((t) => t.id === tabloId); + if (foundTablo) { + setTablo(foundTablo); + } else { + // Tablo not found, redirect back + toast.add( + { + title: "Tablo introuvable", + description: "Le tablo demandé n'existe pas ou vous n'y avez pas accès", + type: "error", + }, + { timeout: 5000 } + ); + navigate("/tablo"); + } + } + }, [tablos, tabloId, navigate]); + + const handleEdit = async (updatedTablo: TabloUpdate & { id: string }) => { + try { + await updateTablo(updatedTablo); + toast.add( + { + title: "Tablo mis à jour", + description: "Les modifications ont été enregistrées", + type: "success", + }, + { timeout: 3000 } + ); + } catch (error) { + toast.add( + { + title: "Erreur", + description: "Impossible de mettre à jour le tablo", + type: "error", + }, + { timeout: 5000 } + ); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (!tablo) { + return null; + } + + const isAdmin = tablo.is_admin; + + const navigationItems: Array<{ + id: TabSection; + label: string; + icon: React.ReactNode; + }> = [ + { + id: "files", + label: "Fichiers", + icon: , + }, + { + id: "discussion", + label: "Discussion", + icon: , + }, + { + id: "settings", + label: "Paramètres", + icon: , + }, + ]; + + return ( +
+ {/* Left Sidebar Navigation */} + + + {/* Main Content Area */} +
+
+ {activeSection === "files" && } + {activeSection === "discussion" && ( + + )} + {activeSection === "settings" && ( + + )} +
+
+
+ ); +}; diff --git a/apps/main/src/pages/tablo.tsx b/apps/main/src/pages/tablo.tsx index c284370..c6dcca9 100644 --- a/apps/main/src/pages/tablo.tsx +++ b/apps/main/src/pages/tablo.tsx @@ -1,9 +1,8 @@ import { CreateTabloModal } from "@ui/components/CreateTabloModal"; import { DeleteTabloModal } from "@ui/components/DeleteTabloModal"; import { LoadingSpinner } from "@ui/components/LoadingSpinner"; -import { TabloModal } from "@ui/components/TabloModal"; import { TabloTutorial } from "@ui/components/TabloTutorial"; -import { TabloInsert, TabloUpdate, UserTablo } from "@xtablo/shared/types/tablos.types"; +import { TabloInsert, UserTablo } from "@xtablo/shared/types/tablos.types"; import { Button } from "@xtablo/ui/components/button"; import { Empty, @@ -57,7 +56,6 @@ export const TabloPage = () => { y: number; } | null>(null); const [isCreateModalOpen, setIsCreateModalOpen] = useState(false); - const [viewingTablo, setViewingTablo] = useState(null); const [deletingTablo, setDeletingTablo] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const [filterType, setFilterType] = useState<"all" | "todo" | "in_progress" | "done">("all"); @@ -143,15 +141,7 @@ export const TabloPage = () => { }; const openTablo = (tabloId: string) => { - if (!tablos) return; - const tablo = tablos.find((t) => t.id === tabloId); - if (tablo) { - setViewingTablo(tablo); - } - }; - - const closeTabloModal = () => { - setViewingTablo(null); + navigate(`/tablos/${tabloId}`); }; const getStatusLabel = (status: string) => { @@ -193,14 +183,6 @@ export const TabloPage = () => { } }; - const onEditTablo = (tablo: TabloUpdate & { id: string }) => { - updateTablo(tablo, { - onSuccess: () => { - closeTabloModal(); - }, - }); - }; - const handleDeleteTablo = (tabloId: string) => { if (!tablos) return; const tablo = tablos.find((t) => t.id === tabloId); @@ -921,11 +903,6 @@ export const TabloPage = () => { )} - {/* Tablo Details Modal */} - {!!viewingTablo && ( - - )} - {/* Delete Tablo Modal */} {!!deletingTablo && (