From 7662922a8bffa08feb87119594a1e5b55ddb7e34 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Thu, 26 Feb 2026 08:38:51 +0100 Subject: [PATCH] feat: add templates page with list and diff comparison views Server-first: page.tsx renders list and compare views as server components, with
/ accordions and URL-param navigation. Only the two template selects require a client component (TemplateCompareSelects). Diff highlighting uses a subtle cyan underline; layout is 2-col on desktop. Co-Authored-By: Claude Sonnet 4.6 --- src/app/templates/page.tsx | 283 ++++++++++++++++++++++ src/components/Header.tsx | 6 + src/components/TemplateCompareSelects.tsx | 52 ++++ 3 files changed, 341 insertions(+) create mode 100644 src/app/templates/page.tsx create mode 100644 src/components/TemplateCompareSelects.tsx diff --git a/src/app/templates/page.tsx b/src/app/templates/page.tsx new file mode 100644 index 0000000..75dce36 --- /dev/null +++ b/src/app/templates/page.tsx @@ -0,0 +1,283 @@ +import Link from "next/link"; +import { redirect } from "next/navigation"; +import { auth } from "@/auth"; +import { getTemplates } from "@/lib/server-data"; +import { parseRubric, parseQuestions } from "@/lib/export-utils"; +import { TemplateCompareSelects } from "@/components/TemplateCompareSelects"; + +interface PageProps { + searchParams: Promise<{ mode?: string; left?: string; right?: string; diffs?: string }>; +} + +// ── Types ──────────────────────────────────────────────────────────────────── + +type TemplateDimension = Awaited>[number]["dimensions"][number]; +type Template = Awaited>[number]; + +// ── Sub-components (server) ─────────────────────────────────────────────────── + +function DimensionAccordion({ dim, index }: { dim: TemplateDimension; index: number }) { + const rubricLabels = parseRubric(dim.rubric); + const questions = parseQuestions(dim.suggestedQuestions); + + return ( +
+ +
+ {index + 1}. + {dim.title} +
+ + + +
+ +
+ {questions.length > 0 && ( +
+

Questions suggérées

+
    + {questions.map((q, i) => ( +
  1. {q}
  2. + ))} +
+
+ )} +
+ {rubricLabels.map((label, i) => ( +
+ {i + 1} {label} +
+ ))} +
+
+
+ ); +} + +function ListView({ templates }: { templates: Template[] }) { + if (templates.length === 0) { + return ( +
+ Aucun template disponible. +
+ ); + } + + return ( +
+ {templates.map((t) => ( +
+
+

{t.name}

+ {t.dimensions.length} dim. +
+
+ {t.dimensions.map((dim, i) => ( + + ))} +
+
+ ))} +
+ ); +} + +function CompareView({ + templates, + leftId, + rightId, + onlyDiffs, +}: { + templates: Template[]; + leftId: string; + rightId: string; + onlyDiffs: boolean; +}) { + const leftTemplate = templates.find((t) => t.id === leftId); + const rightTemplate = templates.find((t) => t.id === rightId); + + // Collect all unique slugs, preserving order + const slugOrder = new Map(); + for (const t of [leftTemplate, rightTemplate]) { + if (!t) continue; + for (const dim of t.dimensions) { + if (!slugOrder.has(dim.slug)) slugOrder.set(dim.slug, dim.orderIndex); + } + } + const slugs = Array.from(slugOrder.keys()).sort( + (a, b) => (slugOrder.get(a) ?? 0) - (slugOrder.get(b) ?? 0) + ); + + // Pre-compute diffs + const diffsBySlugs = new Map(); + let totalDiffDims = 0; + for (const slug of slugs) { + const l = leftTemplate?.dimensions.find((d) => d.slug === slug); + const r = rightTemplate?.dimensions.find((d) => d.slug === slug); + const ll = l ? parseRubric(l.rubric) : []; + const rl = r ? parseRubric(r.rubric) : []; + const diff = [0, 1, 2, 3, 4].filter((i) => ll[i] !== rl[i]); + diffsBySlugs.set(slug, diff); + if (diff.length > 0) totalDiffDims++; + } + + const visibleSlugs = onlyDiffs ? slugs.filter((s) => (diffsBySlugs.get(s)?.length ?? 0) > 0) : slugs; + + const base = `?mode=compare&left=${leftId}&right=${rightId}`; + const diffsHref = onlyDiffs ? base : `${base}&diffs=1`; + + return ( +
+ {templates.length > 2 && ( + ({ id: t.id, name: t.name }))} + leftId={leftId} + rightId={rightId} + onlyDiffs={onlyDiffs} + /> + )} + + {/* Summary + filter */} +
+

+ {totalDiffDims} + {" / "} + {slugs.length} dimensions modifiées +

+ + △ uniquement les différences + +
+ + {/* Column headers */} +
+
+ {leftTemplate?.name ?? "—"} +
+
+ {rightTemplate?.name ?? "—"} +
+
+ +
+ {visibleSlugs.map((slug, idx) => { + const leftDim = leftTemplate?.dimensions.find((d) => d.slug === slug); + const rightDim = rightTemplate?.dimensions.find((d) => d.slug === slug); + const leftLabels = leftDim ? parseRubric(leftDim.rubric) : []; + const rightLabels = rightDim ? parseRubric(rightDim.rubric) : []; + const title = (leftDim ?? rightDim)?.title ?? slug; + const diffLevels = diffsBySlugs.get(slug) ?? []; + const hasDiff = diffLevels.length > 0; + + return ( +
+
+ + {idx + 1}. + {title} + + {hasDiff && ( + + {diffLevels.length} Δ + + )} +
+
+ {[ + { dim: leftDim, labels: leftLabels }, + { dim: rightDim, labels: rightLabels }, + ].map(({ dim, labels }, col) => ( +
+ {dim ? ( + labels.map((label, i) => { + const differs = diffLevels.includes(i); + return ( +
+ + {i + 1} + + + {label} + +
+ ); + }) + ) : ( + absent + )} +
+ ))} +
+
+ ); + })} +
+
+ ); +} + +// ── Page ───────────────────────────────────────────────────────────────────── + +export default async function TemplatesPage({ searchParams }: PageProps) { + const session = await auth(); + if (!session?.user) redirect("/auth/login"); + + const { mode, left, right, diffs } = await searchParams; + const templates = await getTemplates(); + + const isCompare = mode === "compare"; + const leftId = left ?? templates[0]?.id ?? ""; + const rightId = right ?? templates[1]?.id ?? templates[0]?.id ?? ""; + const onlyDiffs = diffs === "1"; + + const compareHref = `?mode=compare&left=${leftId}&right=${rightId}`; + + return ( +
+
+

Templates

+
+ + liste + + + comparer + +
+
+ + {isCompare ? ( + + ) : ( + + )} +
+ ); +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 39e6d9d..6d8240f 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -28,6 +28,12 @@ export function Header({ session }: { session: Session | null }) { > /new + + /templates + {session.user.role === "admin" && ( { + const params = new URLSearchParams({ mode: "compare", left, right }); + if (onlyDiffs) params.set("diffs", "1"); + router.push(`/templates?${params.toString()}`); + }; + + return ( +
+
+ Gauche : + +
+
+ Droite : + +
+
+ ); +}