diff --git a/prisma/migrations/20260601120000_translation_overrides/migration.sql b/prisma/migrations/20260601120000_translation_overrides/migration.sql new file mode 100644 index 0000000..5f3bfb5 --- /dev/null +++ b/prisma/migrations/20260601120000_translation_overrides/migration.sql @@ -0,0 +1,9 @@ +CREATE TABLE "Translation" ( + "key" TEXT NOT NULL, + "lang" TEXT NOT NULL, + "value" TEXT NOT NULL, + "updatedAt" TIMESTAMP(3) NOT NULL, + "updatedBy" TEXT, + CONSTRAINT "Translation_pkey" PRIMARY KEY ("key", "lang") +); +CREATE INDEX "Translation_lang_idx" ON "Translation"("lang"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d0cc722..caa7314 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -348,3 +348,14 @@ model Setting { updatedAt DateTime @updatedAt updatedBy String? } + +model Translation { + key String + lang String + value String + updatedAt DateTime @updatedAt + updatedBy String? + + @@id([key, lang]) + @@index([lang]) +} diff --git a/src/app/admin/home/_components/HomeTranslationsForm.tsx b/src/app/admin/home/_components/HomeTranslationsForm.tsx new file mode 100644 index 0000000..e0bc7c0 --- /dev/null +++ b/src/app/admin/home/_components/HomeTranslationsForm.tsx @@ -0,0 +1,169 @@ +"use client"; + +import { useMemo, useState, useTransition } from "react"; +import { saveHomeTranslationsAction } from "../actions"; + +type Row = { + key: string; + baseFr: string; + baseEn: string; + overrideFr: string | null; + overrideEn: string | null; +}; + +type Section = { + id: string; + label: string; + description: string; + rows: Row[]; +}; + +type Props = { + sections: Section[]; +}; + +function autoRows(text: string): number { + const lines = text.split("\n").length; + return Math.min(8, Math.max(1, lines)); +} + +export function HomeTranslationsForm({ sections }: Props) { + const [pending, startTransition] = useTransition(); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + + // État local : on garde uniquement la valeur courante (initialisée avec override ?? base). + // Le baseValue est posé en input caché et sert au backend pour décider override vs reset. + const initial = useMemo(() => { + const m = new Map(); + for (const s of sections) { + for (const r of s.rows) { + m.set(r.key, { fr: r.overrideFr ?? r.baseFr, en: r.overrideEn ?? r.baseEn }); + } + } + return m; + }, [sections]); + + function onSubmit(formData: FormData) { + setError(null); + setSuccess(null); + startTransition(async () => { + const res = await saveHomeTranslationsAction(formData); + if (res.ok === false) { + setError(res.error); + } else { + const parts: string[] = []; + if (res.saved) parts.push(`${res.saved} sauvegardé${res.saved > 1 ? "s" : ""}`); + if (res.reset) parts.push(`${res.reset} réinitialisé${res.reset > 1 ? "s" : ""} (valeur de base)`); + setSuccess(parts.length > 0 ? parts.join(" · ") : "Aucun changement."); + } + }); + } + + // On crée un seul formulaire global qui contient toutes les sections. + let counter = 0; + + return ( +
+
+ {sections.map((section) => ( +
+
+

+ {section.label} +

+

{section.description}

+
+ +
+ {section.rows.map((r) => { + const idxFr = counter++; + const idxEn = counter++; + const init = initial.get(r.key)!; + const hasOverrideFr = r.overrideFr !== null; + const hasOverrideEn = r.overrideEn !== null; + return ( +
+
+ {r.key} + + {hasOverrideFr ? ( + + FR modifié + + ) : null} + {hasOverrideEn ? ( + + EN modifié + + ) : null} + +
+ +
+