From 203f808a68453c16aaf78a54ee6eef0e39ed6451 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 21 Feb 2026 19:57:05 +0100 Subject: [PATCH] Build tablos page with card/list views and project management - Card view: 4-column grid with status badge, project icon, date, progress bar, delete action - List view: table with project icon/name, status, date, progress, delete - Status filter tabs (all/todo/in_progress/done) - Search wired to TopBar ?q= param - Create project button opens CreateTabloModal - Delete with DeleteTabloModal confirmation - Full dark mode support Co-Authored-By: Claude Sonnet 4.6 (1M context) --- apps/main/src/pages/tablos.tsx | 375 ++++++++++++++++++++++++++++++++- 1 file changed, 372 insertions(+), 3 deletions(-) diff --git a/apps/main/src/pages/tablos.tsx b/apps/main/src/pages/tablos.tsx index 0bd3e3d..52f6bc7 100644 --- a/apps/main/src/pages/tablos.tsx +++ b/apps/main/src/pages/tablos.tsx @@ -1,7 +1,376 @@ -export function TablosPage() { +import { cn } from "@xtablo/shared"; +import type { UserTablo } from "@xtablo/shared/types/tablos.types"; +import { LoadingSpinner } from "@ui/components/LoadingSpinner"; +import { + CalendarIcon, + FilterIcon, + Grid3x3Icon, + ListIcon, + PlusIcon, + SearchIcon, + Trash2Icon, +} from "lucide-react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { CreateTabloModal } from "../components/CreateTabloModal"; +import { DeleteTabloModal } from "../components/DeleteTabloModal"; +import { useCreateTablo, useDeleteTablo, useTablosList } from "../hooks/tablos"; + +// ─── Status helpers ─────────────────────────────────────────────────────────── + +function getStatusConfig(status: string) { + switch (status) { + case "in_progress": + return { + label: "En cours", + badgeClass: "bg-[#FFF4E2] text-[#DB9729] border border-[#DB9729]", + 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, + }; + } +} + +function formatDate(dateStr: string) { + return new Intl.DateTimeFormat("fr-FR", { + month: "short", + day: "2-digit", + year: "numeric", + }).format(new Date(dateStr)); +} + +// ─── Card view ──────────────────────────────────────────────────────────────── + +function TabloCard({ + tablo, + onClick, + onDelete, +}: { + tablo: UserTablo; + onClick: (id: string) => void; + onDelete: (id: string) => void; +}) { + const { t } = useTranslation("pages"); + const { label, badgeClass, progress } = getStatusConfig(tablo.status); + return ( -
-

Tablos

+
onClick(tablo.id)} + > + {/* Status + delete */} +
+ + {label} + + +
+ + {/* Icon + name */} +
+
+ {tablo.image ? ( + {tablo.name} + ) : ( + tablo.name.charAt(0).toUpperCase() + )} +
+

+ {tablo.name} +

+
+ + {/* Date */} +
+ + {formatDate(tablo.created_at)} +
+ + {/* Progress */} +
+
+ {t("tablo.card.progress")} : + {progress}% +
+
+
+
+
+ + {/* Footer */} +
+ + Créé le {formatDate(tablo.created_at)} + +
+
+ ); +} + +// ─── List row ───────────────────────────────────────────────────────────────── + +function TabloRow({ + tablo, + onClick, + onDelete, +}: { + tablo: UserTablo; + onClick: (id: string) => void; + onDelete: (id: string) => void; +}) { + const { label, badgeClass, progress } = getStatusConfig(tablo.status); + + return ( + onClick(tablo.id)} + > + +
+
+ {tablo.image ? ( + {tablo.name} + ) : ( + tablo.name.charAt(0).toUpperCase() + )} +
+ {tablo.name} +
+ + + {label} + + +
+ + {formatDate(tablo.created_at)} +
+ + +
+
+
+
+ {progress}% +
+ + + + + + ); +} + +// ─── Page ───────────────────────────────────────────────────────────────────── + +export function TablosPage() { + const { t } = useTranslation("pages"); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const searchQuery = searchParams.get("q")?.toLowerCase() ?? ""; + + const [viewMode, setViewMode] = useState<"card" | "list">("card"); + const [statusFilter, setStatusFilter] = useState("all"); + const [showCreateModal, setShowCreateModal] = useState(false); + const [deleteTabloId, setDeleteTabloId] = useState(null); + + const { data: tablos = [], isLoading } = useTablosList(); + const createTablo = useCreateTablo(); + const { mutateAsync: deleteTablo, isPending: isDeleting } = useDeleteTablo(); + + const deleteTarget = tablos.find((t) => t.id === deleteTabloId) ?? null; + + const filteredTablos = tablos.filter((tablo) => { + const matchesSearch = !searchQuery || tablo.name.toLowerCase().includes(searchQuery); + const matchesStatus = statusFilter === "all" || tablo.status === statusFilter; + return matchesSearch && matchesStatus; + }); + + const statusFilters = [ + { value: "all", label: t("tablo.filter.all") }, + { value: "todo", label: t("tablo.filter.todo") }, + { value: "in_progress", label: t("tablo.filter.inProgress") }, + { value: "done", label: t("tablo.filter.done") }, + ]; + + return ( +
+ {/* Header */} +
+

+ {t("tablo.projectList.title")} +

+ +
+ + {/* View tabs */} +
+ {[ + { id: "card" as const, label: t("tablo.view.grid"), Icon: Grid3x3Icon }, + { id: "list" as const, label: t("tablo.view.list"), Icon: ListIcon }, + ].map(({ id, label, Icon }) => ( + + ))} +
+ + {/* Search + status filter */} +
+
+ + +
+
+ {statusFilters.map((f) => ( + + ))} +
+
+ + {/* Content */} + {isLoading ? ( +
+ +
+ ) : filteredTablos.length === 0 ? ( +
+
+ +
+

Aucun projet trouvé

+

+ {searchQuery ? "Essayez un autre terme de recherche" : "Créez votre premier projet"} +

+
+ ) : viewMode === "card" ? ( +
+ {filteredTablos.map((tablo) => ( + navigate(`/tablos/${id}`)} + onDelete={setDeleteTabloId} + /> + ))} +
+ ) : ( +
+ + + + + + + + + + + {filteredTablos.map((tablo) => ( + navigate(`/tablos/${id}`)} + onDelete={setDeleteTabloId} + /> + ))} + +
ProjetStatutCréé leProgression +
+
+ )} + + {/* Create modal */} + {showCreateModal && ( + setShowCreateModal(false)} + onCreate={(tabloData) => { + createTablo.mutate({ ...tabloData, status: "todo" }); + setShowCreateModal(false); + }} + /> + )} + + {/* Delete modal */} + setDeleteTabloId(null)} + onConfirm={async (id) => { + await deleteTablo(id); + setDeleteTabloId(null); + }} + isDeleting={isDeleting} + />
); }