@xiwebdesign/xicms-core 4.17.1 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/XiCMSProvider.d.ts +7 -0
- package/dist/XiCMSProvider.js +32 -7
- package/dist/XiCMSProvider.js.map +1 -1
- package/dist/admin/blockEditors/GenericEditor.d.ts +22 -0
- package/dist/admin/blockEditors/GenericEditor.js +201 -0
- package/dist/admin/blockEditors/GenericEditor.js.map +1 -0
- package/dist/admin/blockEditors/SymbolRefEditor.d.ts +2 -0
- package/dist/admin/blockEditors/SymbolRefEditor.js +55 -0
- package/dist/admin/blockEditors/SymbolRefEditor.js.map +1 -0
- package/dist/admin/blockEditors/index.d.ts +11 -6
- package/dist/admin/blockEditors/index.js +49 -620
- package/dist/admin/blockEditors/index.js.map +1 -1
- package/dist/admin/customBlocks/CustomBlocksPanel.d.ts +3 -0
- package/dist/admin/customBlocks/CustomBlocksPanel.js +145 -0
- package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -0
- package/dist/admin/lib/compressImage.d.ts +4 -2
- package/dist/admin/lib/compressImage.js +7 -5
- package/dist/admin/lib/compressImage.js.map +1 -1
- package/dist/admin/lib/mediaUsage.d.ts +4 -3
- package/dist/admin/lib/mediaUsage.js +21 -10
- package/dist/admin/lib/mediaUsage.js.map +1 -1
- package/dist/admin/lib/symbolsClient.d.ts +13 -0
- package/dist/admin/lib/symbolsClient.js +51 -0
- package/dist/admin/lib/symbolsClient.js.map +1 -0
- package/dist/admin/lib/upload.d.ts +4 -0
- package/dist/admin/lib/upload.js +26 -6
- package/dist/admin/lib/upload.js.map +1 -1
- package/dist/admin/lib/zodFields.d.ts +18 -0
- package/dist/admin/lib/zodFields.js +65 -2
- package/dist/admin/lib/zodFields.js.map +1 -1
- package/dist/admin/media/MediaManager.js +23 -7
- package/dist/admin/media/MediaManager.js.map +1 -1
- package/dist/admin/media/MediaPicker.js +12 -2
- package/dist/admin/media/MediaPicker.js.map +1 -1
- package/dist/admin/panelRegistry.js +3 -0
- package/dist/admin/panelRegistry.js.map +1 -1
- package/dist/admin/settings/SettingsPanel.js +6 -1
- package/dist/admin/settings/SettingsPanel.js.map +1 -1
- package/dist/admin/settings/SnapshotsCard.d.ts +3 -0
- package/dist/admin/settings/SnapshotsCard.js +98 -0
- package/dist/admin/settings/SnapshotsCard.js.map +1 -0
- package/dist/admin/visual/TranslationPanel.d.ts +13 -0
- package/dist/admin/visual/TranslationPanel.js +73 -0
- package/dist/admin/visual/TranslationPanel.js.map +1 -0
- package/dist/admin/visual/VisualEditor.js +146 -18
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/agentFiles/catalog.d.ts +21 -0
- package/dist/agentFiles/catalog.js +78 -0
- package/dist/agentFiles/catalog.js.map +1 -0
- package/dist/agentFiles/files.d.ts +59 -0
- package/dist/agentFiles/files.js +109 -0
- package/dist/agentFiles/files.js.map +1 -0
- package/dist/blocks/builtins/Agenda.d.ts +6 -0
- package/dist/blocks/builtins/Agenda.js +14 -6
- package/dist/blocks/builtins/Agenda.js.map +1 -1
- package/dist/blocks/builtins/BeforeAfter.js +1 -0
- package/dist/blocks/builtins/BeforeAfter.js.map +1 -1
- package/dist/blocks/builtins/Breadcrumbs.js +5 -1
- package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
- package/dist/blocks/builtins/Cards.js +4 -2
- package/dist/blocks/builtins/Cards.js.map +1 -1
- package/dist/blocks/builtins/Contact.js +2 -1
- package/dist/blocks/builtins/Contact.js.map +1 -1
- package/dist/blocks/builtins/Cta.js +1 -0
- package/dist/blocks/builtins/Cta.js.map +1 -1
- package/dist/blocks/builtins/Elements.js +1 -0
- package/dist/blocks/builtins/Elements.js.map +1 -1
- package/dist/blocks/builtins/Faq.js +5 -4
- package/dist/blocks/builtins/Faq.js.map +1 -1
- package/dist/blocks/builtins/Gallery.js +2 -2
- package/dist/blocks/builtins/Gallery.js.map +1 -1
- package/dist/blocks/builtins/Hero.js +2 -2
- package/dist/blocks/builtins/Hero.js.map +1 -1
- package/dist/blocks/builtins/Jobs.d.ts +8 -1
- package/dist/blocks/builtins/Jobs.js +38 -21
- package/dist/blocks/builtins/Jobs.js.map +1 -1
- package/dist/blocks/builtins/MapBlock.js +31 -4
- package/dist/blocks/builtins/MapBlock.js.map +1 -1
- package/dist/blocks/builtins/PdfBlock.js +2 -3
- package/dist/blocks/builtins/PdfBlock.js.map +1 -1
- package/dist/blocks/builtins/Portfolio.js +2 -2
- package/dist/blocks/builtins/Portfolio.js.map +1 -1
- package/dist/blocks/builtins/Pricing.d.ts +2 -0
- package/dist/blocks/builtins/Pricing.js +18 -10
- package/dist/blocks/builtins/Pricing.js.map +1 -1
- package/dist/blocks/builtins/Sections.js +4 -2
- package/dist/blocks/builtins/Sections.js.map +1 -1
- package/dist/blocks/builtins/Slider.js +21 -3
- package/dist/blocks/builtins/Slider.js.map +1 -1
- package/dist/blocks/builtins/SymbolRef.d.ts +2 -0
- package/dist/blocks/builtins/SymbolRef.js +61 -0
- package/dist/blocks/builtins/SymbolRef.js.map +1 -0
- package/dist/blocks/builtins/Testimonials.js +21 -18
- package/dist/blocks/builtins/Testimonials.js.map +1 -1
- package/dist/blocks/builtins/WhatsappChat.js +1 -1
- package/dist/blocks/builtins/WhatsappChat.js.map +1 -1
- package/dist/blocks/builtins/Widgets.js +2 -3
- package/dist/blocks/builtins/Widgets.js.map +1 -1
- package/dist/blocks/builtins/lib/ics.js +1 -1
- package/dist/blocks/builtins/lib/ics.js.map +1 -1
- package/dist/blocks/builtins/lib/openingHours.d.ts +4 -1
- package/dist/blocks/builtins/lib/openingHours.js +11 -5
- package/dist/blocks/builtins/lib/openingHours.js.map +1 -1
- package/dist/blocks/customTemplate/definition.d.ts +7 -0
- package/dist/blocks/customTemplate/definition.js +34 -0
- package/dist/blocks/customTemplate/definition.js.map +1 -0
- package/dist/blocks/customTemplate/interpret.d.ts +10 -0
- package/dist/blocks/customTemplate/interpret.js +136 -0
- package/dist/blocks/customTemplate/interpret.js.map +1 -0
- package/dist/blocks/registry.d.ts +8 -0
- package/dist/blocks/registry.js.map +1 -1
- package/dist/demo/demoStore.d.ts +11 -1
- package/dist/demo/demoStore.js +50 -0
- package/dist/demo/demoStore.js.map +1 -1
- package/dist/firebase/client.js +2 -1
- package/dist/firebase/client.js.map +1 -1
- package/dist/i18n/overlay.d.ts +61 -0
- package/dist/i18n/overlay.js +90 -0
- package/dist/i18n/overlay.js.map +1 -0
- package/dist/index.d.ts +11 -9
- package/dist/index.js +34 -9
- package/dist/index.js.map +1 -1
- package/dist/modules/builtins.js +3 -2
- package/dist/modules/builtins.js.map +1 -1
- package/dist/modules/consent/useConsent.js +15 -3
- package/dist/modules/consent/useConsent.js.map +1 -1
- package/dist/schema/blocks.d.ts +183 -3
- package/dist/schema/blocks.js +12 -1
- package/dist/schema/blocks.js.map +1 -1
- package/dist/schema/customBlocks.d.ts +47 -0
- package/dist/schema/customBlocks.js +32 -0
- package/dist/schema/customBlocks.js.map +1 -0
- package/dist/schema/fields.d.ts +122 -0
- package/dist/schema/fields.js +120 -0
- package/dist/schema/fields.js.map +1 -0
- package/dist/schema/forms.d.ts +6 -6
- package/dist/schema/pages.d.ts +15 -1
- package/dist/schema/pages.js +6 -2
- package/dist/schema/pages.js.map +1 -1
- package/dist/schema/siteConfig.d.ts +14 -8
- package/dist/schema/siteConfig.js +2 -1
- package/dist/schema/siteConfig.js.map +1 -1
- package/dist/schema/symbols.d.ts +205 -0
- package/dist/schema/symbols.js +18 -0
- package/dist/schema/symbols.js.map +1 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +24 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/pages.d.ts +21 -0
- package/dist/server/pages.js +77 -0
- package/dist/server/pages.js.map +1 -1
- package/dist/server/seo.d.ts +12 -3
- package/dist/server/seo.js +23 -11
- package/dist/server/seo.js.map +1 -1
- package/dist/styles.css +62 -6
- package/package.json +1 -1
- package/dist/components/EditableImage.d.ts +0 -18
- package/dist/components/EditableImage.js +0 -62
- package/dist/components/EditableImage.js.map +0 -1
- package/dist/components/EditableRichText.d.ts +0 -14
- package/dist/components/EditableRichText.js +0 -26
- package/dist/components/EditableRichText.js.map +0 -1
- package/dist/components/EditableText.d.ts +0 -21
- package/dist/components/EditableText.js +0 -25
- package/dist/components/EditableText.js.map +0 -1
- package/dist/content/ContentProvider.d.ts +0 -23
- package/dist/content/ContentProvider.js +0 -86
- package/dist/content/ContentProvider.js.map +0 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { collection, limit, onSnapshot, orderBy, query } from "firebase/firestore";
|
|
5
|
+
import { getFunctions, httpsCallable } from "firebase/functions";
|
|
6
|
+
import { getDb, getFirebaseApp } from "../../firebase/client";
|
|
7
|
+
import { functionsRegion } from "../../firebase/region";
|
|
8
|
+
import { Button, Card, Input } from "../ui/primitives";
|
|
9
|
+
import { useToast } from "../ui/toast";
|
|
10
|
+
import { useConfirm } from "../ui/ConfirmDialog";
|
|
11
|
+
function formatDate(createdAt) {
|
|
12
|
+
const date = createdAt && typeof createdAt.toDate === "function" ? createdAt.toDate() : null;
|
|
13
|
+
return date ? date.toLocaleString("nl-NL", { dateStyle: "short", timeStyle: "short" }) : "";
|
|
14
|
+
}
|
|
15
|
+
function SnapshotsCard({ siteId }) {
|
|
16
|
+
const { toast } = useToast();
|
|
17
|
+
const confirm = useConfirm();
|
|
18
|
+
const [snapshots, setSnapshots] = useState([]);
|
|
19
|
+
const [label, setLabel] = useState("");
|
|
20
|
+
const [busy, setBusy] = useState(null);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const db = getDb();
|
|
23
|
+
if (!db) return;
|
|
24
|
+
const q = query(collection(db, "sites", siteId, "snapshots"), orderBy("createdAt", "desc"), limit(20));
|
|
25
|
+
return onSnapshot(
|
|
26
|
+
q,
|
|
27
|
+
(snap) => setSnapshots(snap.docs.map((d) => ({ id: d.id, ...d.data() }))),
|
|
28
|
+
() => setSnapshots([])
|
|
29
|
+
);
|
|
30
|
+
}, [siteId]);
|
|
31
|
+
async function create() {
|
|
32
|
+
const app = getFirebaseApp();
|
|
33
|
+
if (!app) return;
|
|
34
|
+
setBusy("create");
|
|
35
|
+
try {
|
|
36
|
+
const fn = httpsCallable(getFunctions(app, functionsRegion()), "createSnapshot");
|
|
37
|
+
await fn({ siteId, label: label.trim() || void 0 });
|
|
38
|
+
setLabel("");
|
|
39
|
+
toast({ title: "Herstelpunt aangemaakt", tone: "success" });
|
|
40
|
+
} catch (e) {
|
|
41
|
+
toast({ title: "Herstelpunt mislukt", description: e.message, tone: "error" });
|
|
42
|
+
} finally {
|
|
43
|
+
setBusy(null);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function restore(s) {
|
|
47
|
+
const ok = await confirm({
|
|
48
|
+
title: "Herstelpunt terugzetten?",
|
|
49
|
+
description: `De site wordt teruggezet naar "${s.label ?? s.id}" (${formatDate(s.createdAt)}). Er wordt eerst automatisch een nieuw herstelpunt van de huidige stand gemaakt.`,
|
|
50
|
+
confirmLabel: "Terugzetten",
|
|
51
|
+
tone: "danger"
|
|
52
|
+
});
|
|
53
|
+
if (!ok) return;
|
|
54
|
+
const app = getFirebaseApp();
|
|
55
|
+
if (!app) return;
|
|
56
|
+
setBusy(s.id);
|
|
57
|
+
try {
|
|
58
|
+
const fn = httpsCallable(getFunctions(app, functionsRegion()), "restoreSnapshot");
|
|
59
|
+
await fn({ siteId, snapshotId: s.id });
|
|
60
|
+
toast({ title: "Herstelpunt teruggezet", description: "Herlaad de pagina om alles bij te werken.", tone: "success" });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
toast({ title: "Herstellen mislukt", description: e.message, tone: "error" });
|
|
63
|
+
} finally {
|
|
64
|
+
setBusy(null);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
68
|
+
/* @__PURE__ */ jsx("h3", { className: "mb-1 text-base font-semibold", children: "Herstelpunten" }),
|
|
69
|
+
/* @__PURE__ */ jsx("p", { className: "mb-3 text-xs text-slate-500", children: "Leg de huidige stand van de hele site vast (pagina's, instellingen, symbolen, bouwstenen) en zet hem later met \xE9\xE9n klik terug \u2014 zonder bestanden te downloaden. V\xF3\xF3r elk herstel wordt automatisch een nieuw herstelpunt gemaakt, dus terugzetten is altijd omkeerbaar." }),
|
|
70
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-3 flex flex-wrap items-center gap-2", children: [
|
|
71
|
+
/* @__PURE__ */ jsx(
|
|
72
|
+
Input,
|
|
73
|
+
{
|
|
74
|
+
value: label,
|
|
75
|
+
placeholder: "Naam (bv. v\xF3\xF3r grote wijziging)",
|
|
76
|
+
onChange: (e) => setLabel(e.target.value),
|
|
77
|
+
className: "max-w-xs"
|
|
78
|
+
}
|
|
79
|
+
),
|
|
80
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", disabled: busy !== null, onClick: () => void create(), children: busy === "create" ? "Bezig\u2026" : "Herstelpunt maken" })
|
|
81
|
+
] }),
|
|
82
|
+
snapshots.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-xs text-slate-400", children: "Nog geen herstelpunten." }) : /* @__PURE__ */ jsx("ul", { className: "grid gap-1 text-sm", children: snapshots.map((s) => /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between gap-2 rounded border border-slate-200 px-2 py-1.5", children: [
|
|
83
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
84
|
+
s.kind === "auto" ? "\u{1F6DF} " : "\u{1F4CC} ",
|
|
85
|
+
s.label ?? "Herstelpunt",
|
|
86
|
+
/* @__PURE__ */ jsxs("span", { className: "text-slate-400", children: [
|
|
87
|
+
" \xB7 ",
|
|
88
|
+
formatDate(s.createdAt)
|
|
89
|
+
] })
|
|
90
|
+
] }),
|
|
91
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", disabled: busy !== null, onClick: () => void restore(s), children: busy === s.id ? "Bezig\u2026" : "Terugzetten" })
|
|
92
|
+
] }, s.id)) })
|
|
93
|
+
] });
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
SnapshotsCard
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=SnapshotsCard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/settings/SnapshotsCard.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * \"Herstelpunten\" (5.0, primo-concept site_snapshots): met één klik de volledige content-stand\n * van de site vastleggen in Firestore, en later terugzetten. Herstellen maakt automatisch eerst\n * een nieuw herstelpunt van de huidige stand (server-side), dus het is altijd omkeerbaar.\n * Aanvulling op de download-back-up: geen bestand nodig, en de automatische vangnetten\n * (vóór restore/push-site) verschijnen hier ook.\n */\nimport { useEffect, useState } from \"react\";\nimport { collection, limit, onSnapshot, orderBy, query } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { Button, Card, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\n\ninterface SnapshotMeta {\n id: string;\n label?: string;\n kind?: string;\n createdAt?: { toDate?: () => Date };\n}\n\nfunction formatDate(createdAt: SnapshotMeta[\"createdAt\"]): string {\n const date = createdAt && typeof createdAt.toDate === \"function\" ? createdAt.toDate() : null;\n return date ? date.toLocaleString(\"nl-NL\", { dateStyle: \"short\", timeStyle: \"short\" }) : \"\";\n}\n\nexport function SnapshotsCard({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [snapshots, setSnapshots] = useState<SnapshotMeta[]>([]);\n const [label, setLabel] = useState(\"\");\n const [busy, setBusy] = useState<string | null>(null);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const q = query(collection(db, \"sites\", siteId, \"snapshots\"), orderBy(\"createdAt\", \"desc\"), limit(20));\n return onSnapshot(\n q,\n (snap) => setSnapshots(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<SnapshotMeta, \"id\">) }))),\n () => setSnapshots([]),\n );\n }, [siteId]);\n\n async function create() {\n const app = getFirebaseApp();\n if (!app) return;\n setBusy(\"create\");\n try {\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"createSnapshot\");\n await fn({ siteId, label: label.trim() || undefined });\n setLabel(\"\");\n toast({ title: \"Herstelpunt aangemaakt\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Herstelpunt mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setBusy(null);\n }\n }\n\n async function restore(s: SnapshotMeta) {\n const ok = await confirm({\n title: \"Herstelpunt terugzetten?\",\n description: `De site wordt teruggezet naar \"${s.label ?? s.id}\" (${formatDate(s.createdAt)}). Er wordt eerst automatisch een nieuw herstelpunt van de huidige stand gemaakt.`,\n confirmLabel: \"Terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n const app = getFirebaseApp();\n if (!app) return;\n setBusy(s.id);\n try {\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"restoreSnapshot\");\n await fn({ siteId, snapshotId: s.id });\n toast({ title: \"Herstelpunt teruggezet\", description: \"Herlaad de pagina om alles bij te werken.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Herstellen mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setBusy(null);\n }\n }\n\n return (\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Herstelpunten</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Leg de huidige stand van de hele site vast (pagina's, instellingen, symbolen, bouwstenen) en\n zet hem later met één klik terug — zonder bestanden te downloaden. Vóór elk herstel wordt\n automatisch een nieuw herstelpunt gemaakt, dus terugzetten is altijd omkeerbaar.\n </p>\n <div className=\"mb-3 flex flex-wrap items-center gap-2\">\n <Input\n value={label}\n placeholder=\"Naam (bv. vóór grote wijziging)\"\n onChange={(e) => setLabel(e.target.value)}\n className=\"max-w-xs\"\n />\n <Button variant=\"secondary\" disabled={busy !== null} onClick={() => void create()}>\n {busy === \"create\" ? \"Bezig…\" : \"Herstelpunt maken\"}\n </Button>\n </div>\n {snapshots.length === 0 ? (\n <p className=\"text-xs text-slate-400\">Nog geen herstelpunten.</p>\n ) : (\n <ul className=\"grid gap-1 text-sm\">\n {snapshots.map((s) => (\n <li key={s.id} className=\"flex items-center justify-between gap-2 rounded border border-slate-200 px-2 py-1.5\">\n <span>\n {s.kind === \"auto\" ? \"🛟 \" : \"📌 \"}\n {s.label ?? \"Herstelpunt\"}\n <span className=\"text-slate-400\"> · {formatDate(s.createdAt)}</span>\n </span>\n <Button variant=\"ghost\" size=\"sm\" disabled={busy !== null} onClick={() => void restore(s)}>\n {busy === s.id ? \"Bezig…\" : \"Terugzetten\"}\n </Button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n );\n}\n"],"mappings":";AAwFM,cAMA,YANA;AA/EN,SAAS,WAAW,gBAAgB;AACpC,SAAS,YAAY,OAAO,YAAY,SAAS,aAAa;AAC9D,SAAS,cAAc,qBAAqB;AAC5C,SAAS,OAAO,sBAAsB;AACtC,SAAS,uBAAuB;AAChC,SAAS,QAAQ,MAAM,aAAa;AACpC,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAS3B,SAAS,WAAW,WAA8C;AAChE,QAAM,OAAO,aAAa,OAAO,UAAU,WAAW,aAAa,UAAU,OAAO,IAAI;AACxF,SAAO,OAAO,KAAK,eAAe,SAAS,EAAE,WAAW,SAAS,WAAW,QAAQ,CAAC,IAAI;AAC3F;AAEO,SAAS,cAAc,EAAE,OAAO,GAAuB;AAC5D,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,UAAU,WAAW;AAC3B,QAAM,CAAC,WAAW,YAAY,IAAI,SAAyB,CAAC,CAAC;AAC7D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,EAAE;AACrC,QAAM,CAAC,MAAM,OAAO,IAAI,SAAwB,IAAI;AAEpD,YAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,IAAI,MAAM,WAAW,IAAI,SAAS,QAAQ,WAAW,GAAG,QAAQ,aAAa,MAAM,GAAG,MAAM,EAAE,CAAC;AACrG,WAAO;AAAA,MACL;AAAA,MACA,CAAC,SAAS,aAAa,KAAK,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAA+B,EAAE,CAAC;AAAA,MACtG,MAAM,aAAa,CAAC,CAAC;AAAA,IACvB;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,iBAAe,SAAS;AACtB,UAAM,MAAM,eAAe;AAC3B,QAAI,CAAC,IAAK;AACV,YAAQ,QAAQ;AAChB,QAAI;AACF,YAAM,KAAK,cAAc,aAAa,KAAK,gBAAgB,CAAC,GAAG,gBAAgB;AAC/E,YAAM,GAAG,EAAE,QAAQ,OAAO,MAAM,KAAK,KAAK,OAAU,CAAC;AACrD,eAAS,EAAE;AACX,YAAM,EAAE,OAAO,0BAA0B,MAAM,UAAU,CAAC;AAAA,IAC5D,SAAS,GAAG;AACV,YAAM,EAAE,OAAO,uBAAuB,aAAc,EAAY,SAAS,MAAM,QAAQ,CAAC;AAAA,IAC1F,UAAE;AACA,cAAQ,IAAI;AAAA,IACd;AAAA,EACF;AAEA,iBAAe,QAAQ,GAAiB;AACtC,UAAM,KAAK,MAAM,QAAQ;AAAA,MACvB,OAAO;AAAA,MACP,aAAa,kCAAkC,EAAE,SAAS,EAAE,EAAE,MAAM,WAAW,EAAE,SAAS,CAAC;AAAA,MAC3F,cAAc;AAAA,MACd,MAAM;AAAA,IACR,CAAC;AACD,QAAI,CAAC,GAAI;AACT,UAAM,MAAM,eAAe;AAC3B,QAAI,CAAC,IAAK;AACV,YAAQ,EAAE,EAAE;AACZ,QAAI;AACF,YAAM,KAAK,cAAc,aAAa,KAAK,gBAAgB,CAAC,GAAG,iBAAiB;AAChF,YAAM,GAAG,EAAE,QAAQ,YAAY,EAAE,GAAG,CAAC;AACrC,YAAM,EAAE,OAAO,0BAA0B,aAAa,6CAA6C,MAAM,UAAU,CAAC;AAAA,IACtH,SAAS,GAAG;AACV,YAAM,EAAE,OAAO,sBAAsB,aAAc,EAAY,SAAS,MAAM,QAAQ,CAAC;AAAA,IACzF,UAAE;AACA,cAAQ,IAAI;AAAA,IACd;AAAA,EACF;AAEA,SACE,qBAAC,QACC;AAAA,wBAAC,QAAG,WAAU,gCAA+B,2BAAa;AAAA,IAC1D,oBAAC,OAAE,WAAU,+BAA8B,sSAI3C;AAAA,IACA,qBAAC,SAAI,WAAU,0CACb;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO;AAAA,UACP,aAAY;AAAA,UACZ,UAAU,CAAC,MAAM,SAAS,EAAE,OAAO,KAAK;AAAA,UACxC,WAAU;AAAA;AAAA,MACZ;AAAA,MACA,oBAAC,UAAO,SAAQ,aAAY,UAAU,SAAS,MAAM,SAAS,MAAM,KAAK,OAAO,GAC7E,mBAAS,WAAW,gBAAW,qBAClC;AAAA,OACF;AAAA,IACC,UAAU,WAAW,IACpB,oBAAC,OAAE,WAAU,0BAAyB,qCAAuB,IAE7D,oBAAC,QAAG,WAAU,sBACX,oBAAU,IAAI,CAAC,MACd,qBAAC,QAAc,WAAU,uFACvB;AAAA,2BAAC,UACE;AAAA,UAAE,SAAS,SAAS,eAAQ;AAAA,QAC5B,EAAE,SAAS;AAAA,QACZ,qBAAC,UAAK,WAAU,kBAAiB;AAAA;AAAA,UAAI,WAAW,EAAE,SAAS;AAAA,WAAE;AAAA,SAC/D;AAAA,MACA,oBAAC,UAAO,SAAQ,SAAQ,MAAK,MAAK,UAAU,SAAS,MAAM,SAAS,MAAM,KAAK,QAAQ,CAAC,GACrF,mBAAS,EAAE,KAAK,gBAAW,eAC9B;AAAA,SARO,EAAE,EASX,CACD,GACH;AAAA,KAEJ;AAEJ;","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BlockRegistry } from "../../blocks/registry";
|
|
2
|
+
export declare function TranslationPanel({ block, siteId, registry, locale, overlay, onTranslate, }: {
|
|
3
|
+
block: Record<string, unknown> & {
|
|
4
|
+
_key: string;
|
|
5
|
+
_type: string;
|
|
6
|
+
};
|
|
7
|
+
siteId: string;
|
|
8
|
+
registry: BlockRegistry;
|
|
9
|
+
locale: string;
|
|
10
|
+
/** De overlay van de actieve taal (gepunte sleutels → vertaalde tekst). */
|
|
11
|
+
overlay: Record<string, string>;
|
|
12
|
+
onTranslate: (key: string, value: string) => void;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { introspectToFields } from "../lib/zodFields";
|
|
5
|
+
import { RichTextEditor } from "../editors/RichTextEditor";
|
|
6
|
+
import { Field as FieldRow, Input, Textarea } from "../ui/primitives";
|
|
7
|
+
function collectEntries(fields, value, prefix = "", labelPrefix = "") {
|
|
8
|
+
const out = [];
|
|
9
|
+
for (const f of fields) {
|
|
10
|
+
const v = value?.[f.key];
|
|
11
|
+
const label = `${labelPrefix}${f.label || f.key}`;
|
|
12
|
+
switch (f.type) {
|
|
13
|
+
case "text":
|
|
14
|
+
case "textarea":
|
|
15
|
+
case "richtext":
|
|
16
|
+
out.push({ path: `${prefix}${f.key}`, label, kind: f.type, canonical: typeof v === "string" ? v : "" });
|
|
17
|
+
break;
|
|
18
|
+
case "link": {
|
|
19
|
+
const lbl = v?.label;
|
|
20
|
+
out.push({ path: `${prefix}${f.key}.label`, label: `${label} (tekst)`, kind: "text", canonical: typeof lbl === "string" ? lbl : "" });
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
case "group":
|
|
24
|
+
out.push(...collectEntries(f.fields, v ?? {}, `${prefix}${f.key}.`, `${label} \xB7 `));
|
|
25
|
+
break;
|
|
26
|
+
case "repeater": {
|
|
27
|
+
const items = Array.isArray(v) ? v : [];
|
|
28
|
+
items.forEach((item, i) => {
|
|
29
|
+
out.push(...collectEntries(f.fields, item ?? {}, `${prefix}${f.key}.${i}.`, `${f.itemLabel ?? label} ${i + 1} \xB7 `));
|
|
30
|
+
});
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
default:
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
function TranslationPanel({
|
|
40
|
+
block,
|
|
41
|
+
siteId,
|
|
42
|
+
registry,
|
|
43
|
+
locale,
|
|
44
|
+
overlay,
|
|
45
|
+
onTranslate
|
|
46
|
+
}) {
|
|
47
|
+
const def = registry.get(block._type);
|
|
48
|
+
const entries = useMemo(() => {
|
|
49
|
+
if (!def) return [];
|
|
50
|
+
const custom = def.customFields;
|
|
51
|
+
const fields = custom ?? introspectToFields(def.schema, def.fields);
|
|
52
|
+
return collectEntries(fields, block);
|
|
53
|
+
}, [def, block]);
|
|
54
|
+
if (entries.length === 0) {
|
|
55
|
+
return /* @__PURE__ */ jsx("p", { className: "xicms-ve__hint", children: "Dit blok heeft geen vertaalbare tekstvelden \u2014 de canonieke inhoud wordt in elke taal getoond." });
|
|
56
|
+
}
|
|
57
|
+
return /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
58
|
+
/* @__PURE__ */ jsxs("p", { className: "text-xs text-slate-500", children: [
|
|
59
|
+
"Vertaling ",
|
|
60
|
+
/* @__PURE__ */ jsx("strong", { children: locale }),
|
|
61
|
+
" \u2014 leeg laten = de originele tekst gebruiken. Blokken, afbeeldingen en links wijzig je in de standaardtaal."
|
|
62
|
+
] }),
|
|
63
|
+
entries.map((e) => {
|
|
64
|
+
const key = `${block._key}.${e.path}`;
|
|
65
|
+
const value = overlay[key] ?? "";
|
|
66
|
+
return /* @__PURE__ */ jsx(FieldRow, { label: e.label, hint: e.canonical ? `Origineel: ${e.canonical.replace(/<[^>]+>/g, "").slice(0, 120)}` : void 0, children: e.kind === "richtext" ? /* @__PURE__ */ jsx(RichTextEditor, { siteId, value: value || e.canonical, onChange: (html) => onTranslate(key, html) }) : e.kind === "textarea" ? /* @__PURE__ */ jsx(Textarea, { rows: 3, value, placeholder: e.canonical, onChange: (ev) => onTranslate(key, ev.target.value) }) : /* @__PURE__ */ jsx(Input, { value, placeholder: e.canonical, onChange: (ev) => onTranslate(key, ev.target.value) }) }, key);
|
|
67
|
+
})
|
|
68
|
+
] });
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
TranslationPanel
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=TranslationPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/visual/TranslationPanel.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Vertaalpaneel (5.0): het zijpaneel van de visuele editor in een NIET-default taal. Toont per\n * blok alleen de vertaalbare velden (text/textarea/richtext + link-labels, afgeleid uit de\n * Field-IR en de canonieke waarden — repeater-items dus stuk voor stuk) en schrijft naar de\n * taal-overlay (`translations[locale][\"<blokKey>.<veldpad>\"]`). Leegmaken = terugvallen op de\n * canonieke tekst. Structuurwijzigingen (blokken, afbeeldingen, links) blijven canoniek.\n */\nimport { useMemo } from \"react\";\nimport type { BlockRegistry } from \"../../blocks/registry\";\nimport type { Field } from \"../../schema/fields\";\nimport { introspectToFields } from \"../lib/zodFields\";\nimport { RichTextEditor } from \"../editors/RichTextEditor\";\nimport { Field as FieldRow, Input, Textarea } from \"../ui/primitives\";\n\ninterface Entry {\n /** Veldpad binnen het blok, bv. \"items.0.title\". */\n path: string;\n label: string;\n kind: \"text\" | \"textarea\" | \"richtext\";\n canonical: string;\n}\n\nfunction collectEntries(fields: Field[], value: Record<string, unknown>, prefix = \"\", labelPrefix = \"\"): Entry[] {\n const out: Entry[] = [];\n for (const f of fields) {\n const v = value?.[f.key];\n const label = `${labelPrefix}${f.label || f.key}`;\n switch (f.type) {\n case \"text\":\n case \"textarea\":\n case \"richtext\":\n out.push({ path: `${prefix}${f.key}`, label, kind: f.type, canonical: typeof v === \"string\" ? v : \"\" });\n break;\n case \"link\": {\n const lbl = (v as { label?: unknown } | undefined)?.label;\n out.push({ path: `${prefix}${f.key}.label`, label: `${label} (tekst)`, kind: \"text\", canonical: typeof lbl === \"string\" ? lbl : \"\" });\n break;\n }\n case \"group\":\n out.push(...collectEntries(f.fields, (v as Record<string, unknown>) ?? {}, `${prefix}${f.key}.`, `${label} · `));\n break;\n case \"repeater\": {\n const items = Array.isArray(v) ? (v as Record<string, unknown>[]) : [];\n items.forEach((item, i) => {\n out.push(...collectEntries(f.fields, item ?? {}, `${prefix}${f.key}.${i}.`, `${f.itemLabel ?? label} ${i + 1} · `));\n });\n break;\n }\n default:\n break;\n }\n }\n return out;\n}\n\nexport function TranslationPanel({\n block,\n siteId,\n registry,\n locale,\n overlay,\n onTranslate,\n}: {\n block: Record<string, unknown> & { _key: string; _type: string };\n siteId: string;\n registry: BlockRegistry;\n locale: string;\n /** De overlay van de actieve taal (gepunte sleutels → vertaalde tekst). */\n overlay: Record<string, string>;\n onTranslate: (key: string, value: string) => void;\n}) {\n const def = registry.get(block._type);\n const entries = useMemo(() => {\n if (!def) return [];\n const custom = (def as { customFields?: Field[] }).customFields;\n const fields = custom ?? introspectToFields(def.schema, def.fields);\n return collectEntries(fields, block);\n }, [def, block]);\n\n if (entries.length === 0) {\n return <p className=\"xicms-ve__hint\">Dit blok heeft geen vertaalbare tekstvelden — de canonieke inhoud wordt in elke taal getoond.</p>;\n }\n\n return (\n <div className=\"grid gap-3\">\n <p className=\"text-xs text-slate-500\">\n Vertaling <strong>{locale}</strong> — leeg laten = de originele tekst gebruiken. Blokken,\n afbeeldingen en links wijzig je in de standaardtaal.\n </p>\n {entries.map((e) => {\n const key = `${block._key}.${e.path}`;\n const value = overlay[key] ?? \"\";\n return (\n <FieldRow key={key} label={e.label} hint={e.canonical ? `Origineel: ${e.canonical.replace(/<[^>]+>/g, \"\").slice(0, 120)}` : undefined}>\n {e.kind === \"richtext\" ? (\n <RichTextEditor siteId={siteId} value={value || e.canonical} onChange={(html) => onTranslate(key, html)} />\n ) : e.kind === \"textarea\" ? (\n <Textarea rows={3} value={value} placeholder={e.canonical} onChange={(ev) => onTranslate(key, ev.target.value)} />\n ) : (\n <Input value={value} placeholder={e.canonical} onChange={(ev) => onTranslate(key, ev.target.value)} />\n )}\n </FieldRow>\n );\n })}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -4,6 +4,7 @@ import "@xiwebdesign/xicms-core/admin.css";
|
|
|
4
4
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { useSiteConfig } from "../../config";
|
|
6
6
|
import { useRegistry } from "../../blocks/context";
|
|
7
|
+
import { introspectToFields } from "../lib/zodFields";
|
|
7
8
|
import { BlockErrorBoundary } from "../../blocks/BlockErrorBoundary";
|
|
8
9
|
import { InlineImageEditProvider } from "../../media/ImageContext";
|
|
9
10
|
import { MediaPickerDialog } from "../media/MediaPicker";
|
|
@@ -13,22 +14,29 @@ import { getDraftByPath, saveDraft, publishPage, subscribeDraft, subscribeVersio
|
|
|
13
14
|
import { isDemoActive } from "../../demo/demoStore";
|
|
14
15
|
import { builtinBlockEditors, GenericEditor } from "../blockEditors";
|
|
15
16
|
import { StyleEditor } from "../blockEditors/StyleEditor";
|
|
17
|
+
import { siteLocales, parseLocaleSlugs, localizeBlocks } from "../../i18n/overlay";
|
|
18
|
+
import { TranslationPanel } from "./TranslationPanel";
|
|
16
19
|
import { reorderBlocks } from "./reorder";
|
|
17
20
|
import { PatternInserter } from "../patterns/PatternInserter";
|
|
18
21
|
import { Button, Field, Input, Textarea } from "../ui/primitives";
|
|
19
22
|
import { Select } from "../ui/select";
|
|
20
23
|
import { ToastProvider, useToast } from "../ui/toast";
|
|
21
24
|
import { ConfirmHost, useConfirm } from "../ui/ConfirmDialog";
|
|
22
|
-
const INLINE_FIELDS = {
|
|
23
|
-
hero: [
|
|
24
|
-
{ selector: ".xicms-hero__title", field: "title" },
|
|
25
|
-
{ selector: ".xicms-hero__sub", field: "subtitle" }
|
|
26
|
-
]
|
|
27
|
-
};
|
|
28
25
|
const GLOBAL_INLINE = [
|
|
29
26
|
{ selector: ".xicms-richtext", field: "html", html: true },
|
|
30
27
|
{ selector: "[data-xicms-edit]", field: "", html: false, fromData: true }
|
|
31
28
|
];
|
|
29
|
+
const inlineCoverageWarned = /* @__PURE__ */ new Set();
|
|
30
|
+
function warnInlineCoverage(type, def, host) {
|
|
31
|
+
if (process.env.NODE_ENV === "production" || !def || inlineCoverageWarned.has(type)) return;
|
|
32
|
+
inlineCoverageWarned.add(type);
|
|
33
|
+
const missing = introspectToFields(def.schema, def.fields).filter((f) => f.type === "text" || f.type === "textarea").map((f) => f.key).filter((key) => !host.querySelector(`[data-xicms-edit="${key}"], [data-xicms-edit$=".${key}"]`));
|
|
34
|
+
if (missing.length > 0) {
|
|
35
|
+
console.warn(
|
|
36
|
+
`[xicms] blok "${type}": tekstveld(en) zonder inline-haak: ${missing.join(", ")} \u2014 voeg data-xicms-edit="<veld>" toe aan de Render zodat ze direct op de pagina bewerkbaar zijn.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
32
40
|
function getPath(obj, path) {
|
|
33
41
|
return path.split(".").reduce((o, k) => o == null ? void 0 : o[k], obj);
|
|
34
42
|
}
|
|
@@ -59,11 +67,20 @@ function VisualEditor({ onClose }) {
|
|
|
59
67
|
return /* @__PURE__ */ jsx("div", { className: "xicms-admin xicms-ve", children: /* @__PURE__ */ jsx(ToastProvider, { children: /* @__PURE__ */ jsx(ConfirmHost, { children: /* @__PURE__ */ jsx(VisualEditorInner, { onClose }) }) }) });
|
|
60
68
|
}
|
|
61
69
|
function VisualEditorInner({ onClose }) {
|
|
62
|
-
const
|
|
70
|
+
const config = useSiteConfig();
|
|
71
|
+
const { siteId, name } = config;
|
|
63
72
|
const registry = useRegistry();
|
|
64
73
|
const { toast } = useToast();
|
|
65
74
|
const confirm = useConfirm();
|
|
66
|
-
const
|
|
75
|
+
const localeCfg = useMemo(() => siteLocales(config), [config]);
|
|
76
|
+
const { path, urlLocale } = useMemo(() => {
|
|
77
|
+
const raw = typeof window !== "undefined" ? window.location.pathname || "/" : "/";
|
|
78
|
+
const { locale: locale2, slugs } = parseLocaleSlugs(raw.split("/").filter(Boolean), localeCfg);
|
|
79
|
+
const p = "/" + slugs.join("/");
|
|
80
|
+
return { path: p === "/" ? "/" : p.replace(/\/$/, ""), urlLocale: locale2 };
|
|
81
|
+
}, [localeCfg]);
|
|
82
|
+
const [locale, setLocale] = useState(urlLocale);
|
|
83
|
+
const isTranslating = locale !== localeCfg.defaultLocale;
|
|
67
84
|
const [draft, setDraft] = useState(null);
|
|
68
85
|
const [status, setStatus] = useState("loading");
|
|
69
86
|
const [selectedKey, setSelectedKey] = useState(null);
|
|
@@ -138,7 +155,9 @@ function VisualEditorInner({ onClose }) {
|
|
|
138
155
|
title: d.title,
|
|
139
156
|
slug: d.slug,
|
|
140
157
|
path: d.path,
|
|
141
|
-
seo: d.seo
|
|
158
|
+
seo: d.seo,
|
|
159
|
+
// Alleen meesturen wanneer aanwezig: `undefined` weigert de Firestore-client.
|
|
160
|
+
...d.translations ? { translations: d.translations } : {}
|
|
142
161
|
});
|
|
143
162
|
const flushSave = useCallback(async () => {
|
|
144
163
|
const d = draftRef.current;
|
|
@@ -160,7 +179,7 @@ function VisualEditorInner({ onClose }) {
|
|
|
160
179
|
if (!active) return;
|
|
161
180
|
if (d) {
|
|
162
181
|
setDraft(d);
|
|
163
|
-
lastSavedRef.current = stableStringify(
|
|
182
|
+
lastSavedRef.current = stableStringify(draftPayload(d));
|
|
164
183
|
setStatus("idle");
|
|
165
184
|
} else {
|
|
166
185
|
setStatus("none");
|
|
@@ -175,13 +194,18 @@ function VisualEditorInner({ onClose }) {
|
|
|
175
194
|
if (!draftId) return;
|
|
176
195
|
return subscribeDraft(siteId, draftId, (remote, meta) => {
|
|
177
196
|
if (!meta.fromServer || !remote) return;
|
|
178
|
-
const remotePayload = stableStringify(
|
|
197
|
+
const remotePayload = stableStringify(draftPayload(remote));
|
|
179
198
|
const local = draftRef.current;
|
|
180
199
|
const localPayload = local ? stableStringify(draftPayload(local)) : null;
|
|
181
200
|
if (remotePayload !== lastSavedRef.current && remotePayload !== localPayload) setConflict(true);
|
|
182
201
|
});
|
|
183
202
|
}, [siteId, draftId]);
|
|
184
203
|
const blocks = draft?.blocks ?? [];
|
|
204
|
+
const overlay = draft?.translations?.[locale];
|
|
205
|
+
const displayBlocks = useMemo(
|
|
206
|
+
() => isTranslating ? localizeBlocks(blocks, overlay) : blocks,
|
|
207
|
+
[blocks, isTranslating, overlay]
|
|
208
|
+
);
|
|
185
209
|
const commitInline = useCallback(
|
|
186
210
|
(key, field, value) => {
|
|
187
211
|
mutateDraft(
|
|
@@ -198,12 +222,34 @@ function VisualEditorInner({ onClose }) {
|
|
|
198
222
|
},
|
|
199
223
|
[mutateDraft]
|
|
200
224
|
);
|
|
225
|
+
const setTranslation = useCallback(
|
|
226
|
+
(tkey, value) => {
|
|
227
|
+
mutateDraft(
|
|
228
|
+
(d) => {
|
|
229
|
+
const overlay2 = { ...d.translations?.[locale] ?? {} };
|
|
230
|
+
if (value.trim() === "" || value === void 0) delete overlay2[tkey];
|
|
231
|
+
else overlay2[tkey] = value;
|
|
232
|
+
return { ...d, translations: { ...d.translations ?? {}, [locale]: overlay2 } };
|
|
233
|
+
},
|
|
234
|
+
{ coalesce: true }
|
|
235
|
+
);
|
|
236
|
+
},
|
|
237
|
+
[mutateDraft, locale]
|
|
238
|
+
);
|
|
239
|
+
const commitText = useCallback(
|
|
240
|
+
(key, field, value) => {
|
|
241
|
+
if (isTranslating) setTranslation(`${key}.${field}`, value);
|
|
242
|
+
else commitInline(key, field, value);
|
|
243
|
+
},
|
|
244
|
+
[isTranslating, setTranslation, commitInline]
|
|
245
|
+
);
|
|
201
246
|
useEffect(() => {
|
|
202
247
|
if (!selectedKey) return;
|
|
203
248
|
const host = canvasRef.current?.querySelector(`[data-ve-content="${selectedKey}"]`);
|
|
204
249
|
if (!host) return;
|
|
205
250
|
const type = blocks.find((b) => b._key === selectedKey)?._type ?? "";
|
|
206
|
-
const specs = [...
|
|
251
|
+
const specs = [...GLOBAL_INLINE];
|
|
252
|
+
warnInlineCoverage(String(type), registry.get(String(type)), host);
|
|
207
253
|
const bound = /* @__PURE__ */ new Set();
|
|
208
254
|
const bind = (el, field, html) => {
|
|
209
255
|
if (el.dataset.veInline === "1") return;
|
|
@@ -211,7 +257,7 @@ function VisualEditorInner({ onClose }) {
|
|
|
211
257
|
el.setAttribute("contenteditable", "true");
|
|
212
258
|
el.setAttribute("spellcheck", "true");
|
|
213
259
|
el.classList.add("xicms-ve-inline");
|
|
214
|
-
el.addEventListener("blur", () =>
|
|
260
|
+
el.addEventListener("blur", () => commitText(selectedKey, field, html ? el.innerHTML : el.textContent ?? ""));
|
|
215
261
|
el.addEventListener("keydown", (e) => {
|
|
216
262
|
if (e.key === "Escape") el.blur();
|
|
217
263
|
else if (!html && e.key === "Enter") {
|
|
@@ -241,7 +287,7 @@ function VisualEditorInner({ onClose }) {
|
|
|
241
287
|
delete el.dataset.veInline;
|
|
242
288
|
});
|
|
243
289
|
};
|
|
244
|
-
}, [selectedKey, blocks,
|
|
290
|
+
}, [selectedKey, blocks, commitText]);
|
|
245
291
|
const dirtySinceRef = useRef(null);
|
|
246
292
|
useEffect(() => {
|
|
247
293
|
if (!draft) return;
|
|
@@ -280,7 +326,10 @@ function VisualEditorInner({ onClose }) {
|
|
|
280
326
|
};
|
|
281
327
|
const onKeyDown = (e) => {
|
|
282
328
|
if (e.key === "Escape") {
|
|
283
|
-
|
|
329
|
+
const t2 = e.target;
|
|
330
|
+
const inField2 = !!t2 && (t2.isContentEditable || t2.tagName === "INPUT" || t2.tagName === "TEXTAREA" || t2.tagName === "SELECT");
|
|
331
|
+
const dialogOpen = document.querySelector('[role="dialog"][data-state="open"], [role="alertdialog"][data-state="open"]');
|
|
332
|
+
if (!inField2 && !dialogOpen) handleClose();
|
|
284
333
|
return;
|
|
285
334
|
}
|
|
286
335
|
const t = e.target;
|
|
@@ -324,6 +373,28 @@ function VisualEditorInner({ onClose }) {
|
|
|
324
373
|
copy.splice(i + 1, 0, { ...blocks[i], _key: newKey() });
|
|
325
374
|
setBlocks(copy);
|
|
326
375
|
}
|
|
376
|
+
async function detachSymbol(i) {
|
|
377
|
+
const blk = blocks[i];
|
|
378
|
+
if (blk._type !== "symbolRef" || !blk.symbolId) return;
|
|
379
|
+
const ok = await confirm({
|
|
380
|
+
title: "Symbool loskoppelen?",
|
|
381
|
+
description: "Dit blok wordt een losse kopie en beweegt niet meer mee met het gedeelde symbool.",
|
|
382
|
+
confirmLabel: "Loskoppelen"
|
|
383
|
+
});
|
|
384
|
+
if (!ok) return;
|
|
385
|
+
const { getSymbolOnce } = await import("../lib/symbolsClient");
|
|
386
|
+
const sym = await getSymbolOnce(siteId, blk.symbolId);
|
|
387
|
+
if (!sym) {
|
|
388
|
+
toast({ title: "Symbool niet gevonden", tone: "error" });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const clones = sym.blocks.map((b) => ({ ...b, _key: newKey() }));
|
|
392
|
+
const copy = [...blocks];
|
|
393
|
+
copy.splice(i, 1, ...clones);
|
|
394
|
+
setBlocks(copy);
|
|
395
|
+
if (selectedKey === blk._key) setSelectedKey(null);
|
|
396
|
+
toast({ title: "Losgekoppeld", description: "Dit is nu een losse kopie.", tone: "success" });
|
|
397
|
+
}
|
|
327
398
|
async function remove(i) {
|
|
328
399
|
const ok = await confirm({ title: "Blok verwijderen?", confirmLabel: "Verwijderen", tone: "danger" });
|
|
329
400
|
if (!ok) return;
|
|
@@ -350,7 +421,7 @@ function VisualEditorInner({ onClose }) {
|
|
|
350
421
|
if (!ok) return;
|
|
351
422
|
setPublishing(true);
|
|
352
423
|
try {
|
|
353
|
-
await saveDraft(siteId, draft.id,
|
|
424
|
+
await saveDraft(siteId, draft.id, draftPayload(draft));
|
|
354
425
|
await publishPage(siteId, draft.id);
|
|
355
426
|
toast({ title: "Gepubliceerd", description: "Herlaad de pagina om het live resultaat te zien.", tone: "success" });
|
|
356
427
|
} catch (e) {
|
|
@@ -381,6 +452,17 @@ function VisualEditorInner({ onClose }) {
|
|
|
381
452
|
name
|
|
382
453
|
] }),
|
|
383
454
|
/* @__PURE__ */ jsx("span", { className: "xicms-ve__path", children: path }),
|
|
455
|
+
localeCfg.locales.length > 1 && /* @__PURE__ */ jsxs("span", { className: "xicms-ve__locale", title: "Taal \u2014 in een andere taal dan de standaard bewerk je vertalingen", children: [
|
|
456
|
+
"\u{1F310}",
|
|
457
|
+
/* @__PURE__ */ jsx(
|
|
458
|
+
Select,
|
|
459
|
+
{
|
|
460
|
+
value: locale,
|
|
461
|
+
onValueChange: setLocale,
|
|
462
|
+
options: localeCfg.locales.map((l) => ({ value: l, label: l === localeCfg.defaultLocale ? `${l} (standaard)` : l }))
|
|
463
|
+
}
|
|
464
|
+
)
|
|
465
|
+
] }),
|
|
384
466
|
/* @__PURE__ */ jsx("button", { type: "button", className: "xicms-ve__histbtn", disabled: !canUndo, onClick: undo, title: "Ongedaan maken (Ctrl+Z)", "aria-label": "Ongedaan maken (Ctrl+Z)", children: "\u21B6" }),
|
|
385
467
|
/* @__PURE__ */ jsx("button", { type: "button", className: "xicms-ve__histbtn", disabled: !canRedo, onClick: redo, title: "Opnieuw (Ctrl+Shift+Z)", "aria-label": "Opnieuw (Ctrl+Shift+Z)", children: "\u21B7" }),
|
|
386
468
|
/* @__PURE__ */ jsx("span", { className: "xicms-ve__status", "aria-live": "polite", children: status === "saving" ? "Opslaan\u2026" : status === "saved" ? "\u2713 Opgeslagen" : status === "error" ? "\u26A0 Niet opgeslagen" : "" }),
|
|
@@ -408,7 +490,7 @@ function VisualEditorInner({ onClose }) {
|
|
|
408
490
|
),
|
|
409
491
|
draft && /* @__PURE__ */ jsxs("div", { className: "container", ref: canvasRef, children: [
|
|
410
492
|
blocks.length === 0 && /* @__PURE__ */ jsx(EmptyState, { icon: "\u{1F9F1}", title: "Nog geen blokken", description: "Voeg hieronder je eerste blok toe." }),
|
|
411
|
-
|
|
493
|
+
displayBlocks.map((b, i) => {
|
|
412
494
|
const def = registry.get(b._type);
|
|
413
495
|
const Render = def?.Render;
|
|
414
496
|
const safe = (() => {
|
|
@@ -459,6 +541,7 @@ function VisualEditorInner({ onClose }) {
|
|
|
459
541
|
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => setSelectedKey(b._key), "aria-label": `Bewerk ${def?.label ?? b._type}`, title: "Bewerken", children: "\u270F\uFE0F" }),
|
|
460
542
|
/* @__PURE__ */ jsx("button", { type: "button", disabled: i === 0, onClick: () => move(i, -1), "aria-label": "Omhoog", children: "\u2191" }),
|
|
461
543
|
/* @__PURE__ */ jsx("button", { type: "button", disabled: i === blocks.length - 1, onClick: () => move(i, 1), "aria-label": "Omlaag", children: "\u2193" }),
|
|
544
|
+
b._type === "symbolRef" && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => void detachSymbol(i), "aria-label": "Symbool loskoppelen", title: "Loskoppelen (losse kopie)", children: "\u{1F517}\u2702" }),
|
|
462
545
|
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => duplicate(i), "aria-label": "Dupliceren", children: "\u2398" }),
|
|
463
546
|
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => void remove(i), "aria-label": "Verwijderen", children: "\u{1F5D1}" })
|
|
464
547
|
] }),
|
|
@@ -483,7 +566,33 @@ function VisualEditorInner({ onClose }) {
|
|
|
483
566
|
] })
|
|
484
567
|
] })
|
|
485
568
|
] }),
|
|
486
|
-
/* @__PURE__ */ jsx("aside", { className: "xicms-ve__panel", children: selected
|
|
569
|
+
/* @__PURE__ */ jsx("aside", { className: "xicms-ve__panel", children: selected && isTranslating ? /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
570
|
+
/* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold", children: [
|
|
571
|
+
registry.get(selected._type)?.label ?? selected._type,
|
|
572
|
+
" \u2014 vertaling (",
|
|
573
|
+
locale,
|
|
574
|
+
")"
|
|
575
|
+
] }),
|
|
576
|
+
/* @__PURE__ */ jsx(
|
|
577
|
+
TranslationPanel,
|
|
578
|
+
{
|
|
579
|
+
block: selected,
|
|
580
|
+
siteId,
|
|
581
|
+
registry,
|
|
582
|
+
locale,
|
|
583
|
+
overlay: overlay ?? {},
|
|
584
|
+
onTranslate: setTranslation
|
|
585
|
+
},
|
|
586
|
+
`${selected._key}:${locale}`
|
|
587
|
+
)
|
|
588
|
+
] }) : selected ? /* @__PURE__ */ jsx(SelectedEditor, { block: selected, siteId, registry, onChange: (next) => updateByKey(selected._key, next) }, selected._key) : draft && isTranslating ? /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
|
|
589
|
+
/* @__PURE__ */ jsxs("p", { className: "xicms-ve__hint", children: [
|
|
590
|
+
"Je bewerkt de vertaling ",
|
|
591
|
+
/* @__PURE__ */ jsx("strong", { children: locale }),
|
|
592
|
+
". Klik op een blok om zijn teksten te vertalen; lege velden vallen terug op de standaardtaal. Blokken toevoegen/verplaatsen doe je in de standaardtaal."
|
|
593
|
+
] }),
|
|
594
|
+
/* @__PURE__ */ jsx(PageTranslationSettings, { draft, locale, onTranslate: setTranslation })
|
|
595
|
+
] }) : draft ? /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
|
|
487
596
|
/* @__PURE__ */ jsx("p", { className: "xicms-ve__hint", children: "Klik op een blok om het te bewerken." }),
|
|
488
597
|
/* @__PURE__ */ jsx(PageSettings, { draft, onChange: (part) => mutateDraft((d) => ({ ...d, ...part }), { coalesce: true }) }),
|
|
489
598
|
!isDemoActive() && /* @__PURE__ */ jsx(
|
|
@@ -529,6 +638,25 @@ function PageSettings({
|
|
|
529
638
|
] })
|
|
530
639
|
] });
|
|
531
640
|
}
|
|
641
|
+
function PageTranslationSettings({
|
|
642
|
+
draft,
|
|
643
|
+
locale,
|
|
644
|
+
onTranslate
|
|
645
|
+
}) {
|
|
646
|
+
const ov = draft.translations?.[locale] ?? {};
|
|
647
|
+
return /* @__PURE__ */ jsxs("details", { className: "xicms-ve__section", open: true, children: [
|
|
648
|
+
/* @__PURE__ */ jsxs("summary", { className: "text-sm font-semibold", children: [
|
|
649
|
+
"Pagina-instellingen (",
|
|
650
|
+
locale,
|
|
651
|
+
")"
|
|
652
|
+
] }),
|
|
653
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-2 grid gap-3", children: [
|
|
654
|
+
/* @__PURE__ */ jsx(Field, { label: "Paginatitel", hint: `Origineel: ${draft.title ?? ""}`, children: /* @__PURE__ */ jsx(Input, { value: ov["title"] ?? "", placeholder: draft.title ?? "", onChange: (e) => onTranslate("title", e.target.value) }) }),
|
|
655
|
+
/* @__PURE__ */ jsx(Field, { label: "SEO-titel", children: /* @__PURE__ */ jsx(Input, { value: ov["seo.title"] ?? "", placeholder: draft.seo?.title ?? "", onChange: (e) => onTranslate("seo.title", e.target.value) }) }),
|
|
656
|
+
/* @__PURE__ */ jsx(Field, { label: "SEO-omschrijving", children: /* @__PURE__ */ jsx(Textarea, { rows: 3, value: ov["seo.description"] ?? "", placeholder: draft.seo?.description ?? "", onChange: (e) => onTranslate("seo.description", e.target.value) }) })
|
|
657
|
+
] })
|
|
658
|
+
] });
|
|
659
|
+
}
|
|
532
660
|
function VersionsPanel({
|
|
533
661
|
siteId,
|
|
534
662
|
pageId,
|