From a5ae692cf46ba7e0304c1086c34381f4e54b56c3 Mon Sep 17 00:00:00 2001 From: Claude Integration Date: Mon, 1 Jun 2026 00:49:31 +0000 Subject: [PATCH] =?UTF-8?q?fix(admin):=20content-pages=20=C3=A9ditait=20FR?= =?UTF-8?q?=20quel=20que=20soit=20le=20lien=20cliqu=C3=A9=20=E2=80=94=20su?= =?UTF-8?q?pport=20multilang=20complet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[slug]/_components/EditorForm.tsx | 14 +- src/app/admin/content-pages/[slug]/page.tsx | 90 +++++++--- src/app/admin/content-pages/page.tsx | 160 ++++++++++++++---- 3 files changed, 204 insertions(+), 60 deletions(-) diff --git a/src/app/admin/content-pages/[slug]/_components/EditorForm.tsx b/src/app/admin/content-pages/[slug]/_components/EditorForm.tsx index 64e3818..0f2d54a 100644 --- a/src/app/admin/content-pages/[slug]/_components/EditorForm.tsx +++ b/src/app/admin/content-pages/[slug]/_components/EditorForm.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; type Page = { slug: string; + lang: string; title: string; body: string; category: string; @@ -25,11 +26,14 @@ export default function EditorForm({ page }: { page: Page }) { setMsg(null); setErr(null); try { - const res = await fetch(`/api/admin/content-pages/${encodeURIComponent(page.slug)}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ title, body, published }), - }); + const res = await fetch( + `/api/admin/content-pages/${encodeURIComponent(page.slug)}?lang=${encodeURIComponent(page.lang)}`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ title, body, published }), + }, + ); if (!res.ok) { const j = await res.json().catch(() => ({})); throw new Error(j?.error || `HTTP ${res.status}`); diff --git a/src/app/admin/content-pages/[slug]/page.tsx b/src/app/admin/content-pages/[slug]/page.tsx index d9e6b25..db82c4b 100644 --- a/src/app/admin/content-pages/[slug]/page.tsx +++ b/src/app/admin/content-pages/[slug]/page.tsx @@ -2,46 +2,90 @@ import { notFound } from "next/navigation"; import Link from "next/link"; import { requireRole } from "@/lib/authorization"; import { UserRole } from "@/generated/prisma/enums"; -import { getContentPage } from "@/lib/content-pages"; import { prisma } from "@/lib/prisma"; import EditorForm from "./_components/EditorForm"; export const dynamic = "force-dynamic"; -type PageProps = { params: Promise<{ slug: string }> }; +type PageProps = { + params: Promise<{ slug: string }>; + searchParams: Promise<{ lang?: string }>; +}; -export default async function EditContentPage({ params }: PageProps) { +function normalizeLang(v: string | undefined): string { + if (!v) return "fr"; + const l = v.toLowerCase().trim(); + return /^[a-z]{2}$/.test(l) ? l : "fr"; +} + +export default async function EditContentPage({ params, searchParams }: PageProps) { await requireRole([UserRole.ADMIN]); const { slug } = await params; - // Pas getContentPage : il filtre published=true. Ici on veut tout voir. - // Admin édite la version FR par défaut. (Édition EN = future feature.) - const row = await prisma.contentPage.findUnique({ - where: { slug_lang: { slug, lang: "fr" } }, - }); + const sp = await searchParams; + const lang = normalizeLang(sp.lang); + + const [row, siblings] = await Promise.all([ + prisma.contentPage.findUnique({ where: { slug_lang: { slug, lang } } }), + prisma.contentPage.findMany({ + where: { slug }, + select: { lang: true, title: true, published: true, updatedAt: true }, + orderBy: { lang: "asc" }, + }), + ]); if (!row) notFound(); - // Re-construction du type minimal attendu par le formulaire. + const page = { slug: row.slug, + lang: row.lang, title: row.title, body: row.body, category: row.category, published: row.published, - updatedAt: row.updatedAt, }; - // Mute eslint sur le _ = getContentPage (gardé importé pour la cohérence future). - void getContentPage; + return ( -
- - ← Toutes les pages - -

Éditer · {page.title}

-

- URL publique : /{page.slug} -

+
+
+ + ← Toutes les pages + +

+ {page.title} + + {page.lang} + +

+

+ URL publique : /{page.slug} + {page.lang !== "fr" ? ` · variante ${page.lang}` : ""} +

+ + {siblings.length > 1 ? ( + + ) : null} +
+
diff --git a/src/app/admin/content-pages/page.tsx b/src/app/admin/content-pages/page.tsx index 263e290..0f9f2ab 100644 --- a/src/app/admin/content-pages/page.tsx +++ b/src/app/admin/content-pages/page.tsx @@ -10,50 +10,146 @@ const CATEGORY_LABEL: Record = { legal: "Légales", }; +type Translation = { + lang: string; + title: string; + published: boolean; + updatedAt: Date; +}; + +type GroupedPage = { + slug: string; + category: string; + translations: Translation[]; +}; + export default async function ContentPagesAdminPage() { await requireRole([UserRole.ADMIN]); - const pages = await listContentPages(); + const rows = await listContentPages(); - const byCategory = pages.reduce>((acc, p) => { + // Regrouper par slug — chaque slug peut avoir plusieurs traductions. + const bySlug = new Map(); + for (const r of rows) { + const existing = bySlug.get(r.slug); + const t: Translation = { + lang: r.lang, + title: r.title, + published: r.published, + updatedAt: r.updatedAt, + }; + if (existing) { + existing.translations.push(t); + } else { + bySlug.set(r.slug, { slug: r.slug, category: r.category, translations: [t] }); + } + } + const pages = Array.from(bySlug.values()).sort((a, b) => a.slug.localeCompare(b.slug)); + + const byCategory = pages.reduce>((acc, p) => { (acc[p.category] ??= []).push(p); return acc; }, {}); - return ( -
-

Pages éditoriales

-

- Pages markdown affichées dans le site public. La catégorie « Général » - est gérée par le plugin content-pages, la catégorie « Légales » - par legal-pages. Désactiver le plugin dépublie ses pages - sans les supprimer. -

+ const dateFmt = new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "short", year: "2-digit" }); -
+ return ( +
+
+

Pages éditoriales

+

+ Pages markdown servies par le site public. Chaque page existe en une ou + plusieurs langues — utilisez le bouton de la langue voulue pour éditer + la bonne version. +

+
+ +
{Object.entries(byCategory).map(([cat, list]) => (
-

+

{CATEGORY_LABEL[cat] ?? cat}

-
    - {list.map((p) => ( -
  • -
    -
    {p.title}
    -
    - /{p.slug} · {p.published ? "publié" : "dépublié"} · - mis à jour le {new Date(p.updatedAt).toLocaleDateString("fr-FR")} -
    -
    - - Éditer - -
  • - ))} -
+
+ + + + + + + + + + + + {list.map((p) => { + const fr = p.translations.find((t) => t.lang === "fr"); + const others = p.translations.filter((t) => t.lang !== "fr").sort((a, b) => a.lang.localeCompare(b.lang)); + const lastUpdated = p.translations + .map((t) => t.updatedAt.getTime()) + .reduce((a, b) => Math.max(a, b), 0); + return ( + + + + + + + + ); + })} + +
SlugTitre (FR)TraductionsMAJÉditer
/{p.slug} + {fr ? ( + <> + {fr.title} + {!fr.published ? ( + + dépublié + + ) : null} + + ) : ( + — (pas de version FR) + )} + + {others.length === 0 ? ( + + ) : ( + + {others.map((t) => ( + + {t.lang} + + ))} + + )} + + {lastUpdated ? dateFmt.format(new Date(lastUpdated)) : "—"} + + + {p.translations + .sort((a, b) => (a.lang === "fr" ? -1 : b.lang === "fr" ? 1 : a.lang.localeCompare(b.lang))) + .map((t) => ( + + {t.lang} + + ))} + +
+
))}