From 68f37f554f222ab9eb55eb77daeeaae4bd088345 Mon Sep 17 00:00:00 2001 From: Claude Integration Date: Sun, 31 May 2026 10:12:13 +0000 Subject: [PATCH] feat(plugins): content-pages + legal-pages (Phase 4.1 + 4.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plugin content-pages : - Modèle Prisma ContentPage (slug PK, title, body markdown, category, published) - lib/content-pages.ts : helpers upsert/get/list/unpublish - lib/markdown.ts : mini-renderer markdown server-side sans deps externe (h1-h3, paragraphes, gras/italique, liens, listes ul/ol, hr, blockquote, échappement HTML) - ContentPageRenderer server component, applique le theme Guyane (font-serif) - 5 pages seedées : /a-propos, /faq, /comment-ca-marche, /pour-comites-entreprise, /devenir-loueur - Routes publiques + force-dynamic + guard requirePluginOr404 Plugin legal-pages : - Réutilise le même modèle ContentPage, catégorie 'legal' - 3 pages seedées : /cgv, /mentions-legales, /politique-de-confidentialite (contenu de base, à valider par avocat avant prod réelle) Admin : - /admin/content-pages : table par catégorie, statut publié/dépublié - /admin/content-pages/[slug] : éditeur markdown + toggle publié - PATCH /api/admin/content-pages/[slug] Hooks plugin : - onEnable seed + republish toutes les pages - onDisable dépublie toute la catégorie sans la supprimer (preserve les edits) --- .../migration.sql | 16 ++ prisma/schema.prisma | 16 ++ src/app/a-propos/page.tsx | 18 ++ .../[slug]/_components/EditorForm.tsx | 93 +++++++++ src/app/admin/content-pages/[slug]/page.tsx | 47 +++++ src/app/admin/content-pages/page.tsx | 62 ++++++ .../api/admin/content-pages/[slug]/route.ts | 39 ++++ src/app/cgv/page.tsx | 18 ++ src/app/comment-ca-marche/page.tsx | 18 ++ src/app/devenir-loueur/page.tsx | 18 ++ src/app/faq/page.tsx | 18 ++ src/app/mentions-legales/page.tsx | 18 ++ src/app/politique-de-confidentialite/page.tsx | 18 ++ src/app/pour-comites-entreprise/page.tsx | 18 ++ src/components/ContentPageRenderer.tsx | 31 +++ src/lib/content-pages.ts | 110 +++++++++++ src/lib/markdown.ts | 151 ++++++++++++++ src/lib/plugins/hooks.ts | 36 ++++ .../plugins/seeds/content-pages-default.ts | 185 +++++++++++++++++ src/lib/plugins/seeds/legal-pages-default.ts | 186 ++++++++++++++++++ 20 files changed, 1116 insertions(+) create mode 100644 prisma/migrations/20260531180000_add_content_pages/migration.sql create mode 100644 src/app/a-propos/page.tsx create mode 100644 src/app/admin/content-pages/[slug]/_components/EditorForm.tsx create mode 100644 src/app/admin/content-pages/[slug]/page.tsx create mode 100644 src/app/admin/content-pages/page.tsx create mode 100644 src/app/api/admin/content-pages/[slug]/route.ts create mode 100644 src/app/cgv/page.tsx create mode 100644 src/app/comment-ca-marche/page.tsx create mode 100644 src/app/devenir-loueur/page.tsx create mode 100644 src/app/faq/page.tsx create mode 100644 src/app/mentions-legales/page.tsx create mode 100644 src/app/politique-de-confidentialite/page.tsx create mode 100644 src/app/pour-comites-entreprise/page.tsx create mode 100644 src/components/ContentPageRenderer.tsx create mode 100644 src/lib/content-pages.ts create mode 100644 src/lib/markdown.ts create mode 100644 src/lib/plugins/seeds/content-pages-default.ts create mode 100644 src/lib/plugins/seeds/legal-pages-default.ts diff --git a/prisma/migrations/20260531180000_add_content_pages/migration.sql b/prisma/migrations/20260531180000_add_content_pages/migration.sql new file mode 100644 index 0000000..4306682 --- /dev/null +++ b/prisma/migrations/20260531180000_add_content_pages/migration.sql @@ -0,0 +1,16 @@ +-- Plugin content-pages + legal-pages : table ContentPage + +CREATE TABLE "ContentPage" ( + "slug" TEXT PRIMARY KEY, + "title" TEXT NOT NULL, + "body" TEXT NOT NULL, + "lang" TEXT NOT NULL DEFAULT 'fr', + "category" TEXT NOT NULL DEFAULT 'general', + "published" BOOLEAN NOT NULL DEFAULT true, + "lastEditedBy" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL +); + +CREATE INDEX "ContentPage_category_idx" ON "ContentPage" ("category"); +CREATE INDEX "ContentPage_published_idx" ON "ContentPage" ("published"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a7d8c9e..4fd694e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -280,3 +280,19 @@ model Plugin { @@index([category]) @@index([enabled]) } + +model ContentPage { + slug String @id + title String + body String + lang String @default("fr") + // 'general' (about, faq, ...) ou 'legal' (cgv, mentions, ...) + category String @default("general") + published Boolean @default(true) + lastEditedBy String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([category]) + @@index([published]) +} diff --git a/src/app/a-propos/page.tsx b/src/app/a-propos/page.tsx new file mode 100644 index 0000000..e4a36b1 --- /dev/null +++ b/src/app/a-propos/page.tsx @@ -0,0 +1,18 @@ +import { notFound } from "next/navigation"; +import { getContentPage } from "@/lib/content-pages"; +import { isPluginEnabled } from "@/lib/plugins/server"; +import { ContentPageRenderer } from "@/components/ContentPageRenderer"; + +export const dynamic = "force-dynamic"; + +export async function generateMetadata() { + const page = await getContentPage("a-propos"); + return { title: page?.title ?? "À propos" }; +} + +export default async function AboutPage() { + if (!(await isPluginEnabled("content-pages"))) notFound(); + const page = await getContentPage("a-propos"); + if (!page) notFound(); + return ; +} diff --git a/src/app/admin/content-pages/[slug]/_components/EditorForm.tsx b/src/app/admin/content-pages/[slug]/_components/EditorForm.tsx new file mode 100644 index 0000000..64e3818 --- /dev/null +++ b/src/app/admin/content-pages/[slug]/_components/EditorForm.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +type Page = { + slug: string; + title: string; + body: string; + category: string; + published: boolean; +}; + +export default function EditorForm({ page }: { page: Page }) { + const router = useRouter(); + const [title, setTitle] = useState(page.title); + const [body, setBody] = useState(page.body); + const [published, setPublished] = useState(page.published); + const [busy, setBusy] = useState(false); + const [msg, setMsg] = useState(null); + const [err, setErr] = useState(null); + + async function save() { + setBusy(true); + 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 }), + }); + if (!res.ok) { + const j = await res.json().catch(() => ({})); + throw new Error(j?.error || `HTTP ${res.status}`); + } + setMsg("Sauvegardé."); + router.refresh(); + } catch (e) { + setErr(e instanceof Error ? e.message : String(e)); + } finally { + setBusy(false); + } + } + + return ( +
+ + +