From 24fe6b89a94ac95a5473cca80eb3452e240b3ff6 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 21 Feb 2026 21:51:58 +0100 Subject: [PATCH] Redesign project detail page with header tabs and overview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace sidebar layout with top header: project icon, name, Discussion (purple) and Invite buttons - Add metadata bar: role, creation date, status badge, progress bar - Tab navigation: Aperçu, Tâches, Fichiers, Discussion, Événements, Roadmap (bientôt) - Overview tab: project description card, tasks preview (5 items), files sidebar, project info card - Other tabs delegate to existing section components (TabloTasksSection, TabloFilesSection, etc.) - Move /tablos/:tabloId route inside Layout wrapper so it gets the sidebar and TopBar - Purple #804EEC accent throughout, full dark mode support Co-Authored-By: Claude Sonnet 4.6 (1M context) --- apps/main/src/lib/routes.tsx | 8 +- apps/main/src/pages/tablo-details.tsx | 421 ++++++++++++++++---------- 2 files changed, 273 insertions(+), 156 deletions(-) diff --git a/apps/main/src/lib/routes.tsx b/apps/main/src/lib/routes.tsx index 5bf3124..63fa415 100644 --- a/apps/main/src/lib/routes.tsx +++ b/apps/main/src/lib/routes.tsx @@ -34,14 +34,14 @@ export const routes: RouteObject[] = [ path: "/", element: , children: [ - { - path: "tablos/:tabloId", - element: , - }, { path: "", element: , children: [ + { + path: "tablos/:tabloId", + element: , + }, { index: true, element: , diff --git a/apps/main/src/pages/tablo-details.tsx b/apps/main/src/pages/tablo-details.tsx index c6b37e3..b94e947 100644 --- a/apps/main/src/pages/tablo-details.tsx +++ b/apps/main/src/pages/tablo-details.tsx @@ -1,201 +1,318 @@ -import { toast } from "@xtablo/shared"; -import { UserTablo } from "@xtablo/shared/types/tablos.types"; -import { Button } from "@xtablo/ui/components/button"; +import { cn, toast } from "@xtablo/shared"; +import type { UserTablo } from "@xtablo/shared/types/tablos.types"; +import type { KanbanTask } from "@xtablo/shared-types"; +import { LoadingSpinner } from "@ui/components/LoadingSpinner"; import { - ArrowLeft, - // BookOpen, // Notes feature temporarily hidden - Calendar, - FileText, - LayoutDashboard, - ListChecks, - MessageSquare, + CalendarIcon, + CircleCheckIcon, + EllipsisVerticalIcon, + FileTextIcon, + FolderIcon, + KanbanIcon, + LayoutDashboardIcon, + MapIcon, + MessageCircleIcon, + PlusIcon, + UserPlusIcon, } from "lucide-react"; import { useEffect, useState } from "react"; -import { useNavigate, useParams, useSearchParams } from "react-router-dom"; -import { match } from "ts-pattern"; -import { LoadingSpinner } from "../components/LoadingSpinner"; +import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { TabloDiscussionSection } from "../components/TabloDiscussionSection"; import { TabloEventsSection } from "../components/TabloEventsSection"; import { TabloFilesSection } from "../components/TabloFilesSection"; -// import { TabloNotesSection } from "../components/TabloNotesSection"; // Notes feature temporarily hidden -import { TabloOverviewSection } from "../components/TabloOverviewSection"; import { TabloTasksSection } from "../components/TabloTasksSection"; +import { useAllTasks } from "../hooks/tasks"; +import { useTabloFileNames } from "../hooks/tablo_data"; import { useTablosList } from "../hooks/tablos"; -type TabSection = - | "overview" - | "files" - | "discussion" - // | "notes" // Notes feature temporarily hidden - | "events" - | "tasks"; +// ─── Status helpers ─────────────────────────────────────────────────────────── + +function getStatusConfig(status: string) { + switch (status) { + case "in_progress": + return { label: "En cours", badgeClass: "bg-yellow-50 text-yellow-700 border border-yellow-200 dark:bg-yellow-950/30 dark:text-yellow-400 dark:border-yellow-800", progress: 50 }; + case "done": + return { label: "Terminé", badgeClass: "bg-green-50 text-green-600 border border-green-200 dark:bg-green-950/30 dark:text-green-400 dark:border-green-800", progress: 100 }; + default: + return { label: "À faire", badgeClass: "bg-blue-50 text-blue-600 border border-blue-200 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-800", progress: 0 }; + } +} + +// ─── Tabs ───────────────────────────────────────────────────────────────────── + +type TabSection = "overview" | "board" | "list" | "roadmap" | "calendar" | "files" | "discussion" | "events" | "tasks"; + +const TABS: { id: TabSection; label: string; icon: React.ElementType; disabled?: boolean }[] = [ + { id: "overview", label: "Aperçu", icon: LayoutDashboardIcon }, + { id: "tasks", label: "Tâches", icon: KanbanIcon }, + { id: "files", label: "Fichiers", icon: FolderIcon }, + { id: "discussion", label: "Discussion", icon: MessageCircleIcon }, + { id: "events", label: "Événements", icon: CalendarIcon }, + { id: "roadmap", label: "Roadmap", icon: MapIcon, disabled: true }, +]; + +// ─── Page ───────────────────────────────────────────────────────────────────── export const TabloDetailsPage = () => { const { tabloId } = useParams<{ tabloId: string }>(); const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); const { data: tablos, isLoading } = useTablosList(); - const [searchParams, setSearchParams] = useSearchParams(); - const sectionParam = searchParams.get("section"); - // Notes feature temporarily hidden - redirect to overview if notes is selected + const sectionParam = searchParams.get("section") as TabSection | null; const activeSection: TabSection = - sectionParam && - sectionParam !== "notes" && - ["overview", "files", "discussion", "events", "tasks"].includes(sectionParam) - ? (sectionParam as TabSection) + sectionParam && TABS.some((t) => t.id === sectionParam && !t.disabled) + ? sectionParam : "overview"; const [tablo, setTablo] = useState(null); useEffect(() => { if (tablos && tabloId) { - const foundTablo = tablos.find((t) => t.id === tabloId); - if (foundTablo) { - setTablo(foundTablo); + const found = tablos.find((t) => t.id === tabloId); + if (found) { + setTablo(found); } 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", - }, + { title: "Projet introuvable", description: "Le projet demandé n'existe pas ou vous n'y avez pas accès", type: "error" }, { timeout: 5000 } ); - navigate("/tablo"); + navigate("/tablos"); } } }, [tablos, tabloId, navigate]); + // Tasks for this tablo (used in overview) + const { data: allTasks = [] } = useAllTasks(); + const tabloTasks = (allTasks as KanbanTask[]).filter((t) => t.tablo_id === tabloId); + + // Files for this tablo (used in overview) + const { data: filesData } = useTabloFileNames(tabloId ?? ""); + const fileNames = (filesData?.fileNames ?? []).filter((f) => !f.startsWith(".")); + if (isLoading) { return ( -
+
); } - if (!tablo) { - return null; - } + if (!tablo) return null; + const { label: statusLabel, badgeClass, progress } = getStatusConfig(tablo.status); const isAdmin = tablo.is_admin; - const navigationItems: Array<{ - id: TabSection; - label: string; - icon: React.ReactNode; - }> = [ - { - id: "overview", - label: "Vue d'ensemble", - icon: , - }, - { - id: "tasks", - label: "Tâches", - icon: , - }, - { - id: "files", - label: "Fichiers", - icon: , - }, - { - id: "discussion", - label: "Discussion", - icon: , - }, - // Notes feature temporarily hidden - // { - // id: "notes", - // label: "Notes", - // icon: , - // }, - { - id: "events", - label: "Événements", - icon: , - }, - ]; - return ( -
- {/* Left Sidebar Navigation */} - - - {/* Main Content Area */} -
-
- {match(activeSection) - .with("overview", () => ) - .with("files", () => ) - .with("discussion", () => ) - // Notes feature temporarily hidden - // .with("notes", () => ) - .with("events", () => ) - .with("tasks", () => ) - .exhaustive()} + {/* ── Metadata bar ──────────────────────────────────────────────── */} +
+
+ Rôle : + {isAdmin ? "Admin" : "Invité"} +
+
+ Créé le : + + {new Intl.DateTimeFormat("fr-FR", { year: "numeric", month: "short", day: "2-digit" }).format(new Date(tablo.created_at))} + +
+
+ Statut : + {statusLabel} +
+
+ Progression : +
+
+
+ {progress}% +
-
+
+ + {/* ── Tab navigation ──────────────────────────────────────────────── */} +
+
+
+ {TABS.map((tab) => { + const isActive = activeSection === tab.id; + return ( + + ); + })} +
+
+
+ + {/* ── Tab content ─────────────────────────────────────────────────── */} +
+ {activeSection === "overview" && ( +
+ {/* Left column */} +
+ {/* Description */} +
+

Description du projet

+

+ Ce projet regroupe les tâches, fichiers et événements associés. Utilisez les onglets ci-dessus pour naviguer entre les différentes sections. +

+
+ + {/* Tasks */} +
+
+

Mes tâches

+ +
+
+ {tabloTasks.length === 0 ? ( +
Aucune tâche
+ ) : ( + tabloTasks.slice(0, 5).map((task) => ( +
setSearchParams({ section: "tasks" })} + > + {task.status === "done" ? ( + + ) : ( +
+ )} +

+ {task.title} +

+
+ )) + )} + {tabloTasks.length > 5 && ( + + )} +
+
+
+ + {/* Right column */} +
+ {/* Files */} +
+
+

Fichiers

+ +
+
+ {fileNames.length === 0 ? ( +

Aucun fichier

+ ) : ( + fileNames.slice(0, 5).map((fileName) => ( +
+
+ +
+
+

{fileName}

+
+ +
+ )) + )} +
+
+ + {/* Info */} +
+

Informations

+
+
Tâches
{tabloTasks.length}
+
Fichiers
{fileNames.length}
+
Statut
{statusLabel}
+
Rôle
{isAdmin ? "Admin" : "Invité"}
+
+
+
+
+ )} + + {activeSection === "tasks" && } + {activeSection === "files" && } + {activeSection === "discussion" && } + {activeSection === "events" && } +
); };