@xiwebdesign/xicms-core 4.18.0 → 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 +29 -6
- 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 +4 -608
- 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/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/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/panelRegistry.js +3 -0
- package/dist/admin/panelRegistry.js.map +1 -1
- package/dist/admin/settings/SettingsPanel.js +2 -0
- 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 +142 -17
- 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/BeforeAfter.js +1 -0
- package/dist/blocks/builtins/BeforeAfter.js.map +1 -1
- package/dist/blocks/builtins/Cards.js +1 -0
- package/dist/blocks/builtins/Cards.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/Hero.js +2 -2
- package/dist/blocks/builtins/Hero.js.map +1 -1
- package/dist/blocks/builtins/Jobs.js +1 -0
- package/dist/blocks/builtins/Jobs.js.map +1 -1
- package/dist/blocks/builtins/Sections.js +1 -0
- package/dist/blocks/builtins/Sections.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/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/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 +10 -8
- package/dist/index.js +32 -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/schema/blocks.d.ts +159 -0
- package/dist/schema/blocks.js +8 -0
- 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 +10 -3
- package/dist/server/seo.js +22 -10
- package/dist/server/seo.js.map +1 -1
- package/dist/styles.css +27 -0
- 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,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { collection, onSnapshot } 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 { CustomBlockSchema } from "../../schema/customBlocks";
|
|
9
|
+
import { FieldsSchema } from "../../schema/fields";
|
|
10
|
+
import { Button, Field, Input, Textarea } from "../ui/primitives";
|
|
11
|
+
import { useToast } from "../ui/toast";
|
|
12
|
+
import { useConfirm } from "../ui/ConfirmDialog";
|
|
13
|
+
const EMPTY = {
|
|
14
|
+
type: "custom-",
|
|
15
|
+
label: "",
|
|
16
|
+
icon: "\u{1F9E9}",
|
|
17
|
+
fields: [],
|
|
18
|
+
template: "",
|
|
19
|
+
css: "",
|
|
20
|
+
version: 1
|
|
21
|
+
};
|
|
22
|
+
const FIELDS_VOORBEELD = `[
|
|
23
|
+
{ "key": "heading", "label": "Kop", "type": "text" },
|
|
24
|
+
{ "key": "items", "label": "Items", "type": "repeater", "fields": [
|
|
25
|
+
{ "key": "title", "label": "Titel", "type": "text" },
|
|
26
|
+
{ "key": "bodyHtml", "label": "Tekst", "type": "richtext" }
|
|
27
|
+
] }
|
|
28
|
+
]`;
|
|
29
|
+
function CustomBlocksPanel({ siteId }) {
|
|
30
|
+
const { toast } = useToast();
|
|
31
|
+
const confirm = useConfirm();
|
|
32
|
+
const [blocks, setBlocks] = useState([]);
|
|
33
|
+
const [editing, setEditing] = useState(null);
|
|
34
|
+
const [fieldsText, setFieldsText] = useState("[]");
|
|
35
|
+
const [busy, setBusy] = useState(false);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const db = getDb();
|
|
38
|
+
if (!db) return;
|
|
39
|
+
return onSnapshot(collection(db, "sites", siteId, "customBlocks"), (snap) => {
|
|
40
|
+
const out = [];
|
|
41
|
+
snap.forEach((d) => {
|
|
42
|
+
const res = CustomBlockSchema.safeParse({ ...d.data(), type: d.id });
|
|
43
|
+
if (res.success) out.push(res.data);
|
|
44
|
+
});
|
|
45
|
+
setBlocks(out.sort((a, b) => a.label.localeCompare(b.label)));
|
|
46
|
+
});
|
|
47
|
+
}, [siteId]);
|
|
48
|
+
function open(def) {
|
|
49
|
+
setEditing(def);
|
|
50
|
+
setFieldsText(JSON.stringify(def.fields, null, 2));
|
|
51
|
+
}
|
|
52
|
+
async function save() {
|
|
53
|
+
if (!editing) return;
|
|
54
|
+
let fields;
|
|
55
|
+
try {
|
|
56
|
+
fields = JSON.parse(fieldsText);
|
|
57
|
+
} catch {
|
|
58
|
+
toast({ title: "Velden zijn geen geldige JSON", tone: "error" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const parsedFields = FieldsSchema.safeParse(fields);
|
|
62
|
+
if (!parsedFields.success) {
|
|
63
|
+
toast({ title: "Velden ongeldig", description: parsedFields.error.issues[0]?.message, tone: "error" });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const candidate = CustomBlockSchema.safeParse({ ...editing, fields: parsedFields.data });
|
|
67
|
+
if (!candidate.success) {
|
|
68
|
+
toast({ title: "Bouwsteen ongeldig", description: candidate.error.issues[0]?.message, tone: "error" });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const app = getFirebaseApp();
|
|
72
|
+
if (!app) return;
|
|
73
|
+
setBusy(true);
|
|
74
|
+
try {
|
|
75
|
+
const fn = httpsCallable(getFunctions(app, functionsRegion()), "saveCustomBlock");
|
|
76
|
+
const res = await fn({ siteId, ...candidate.data });
|
|
77
|
+
toast({ title: "Opgeslagen", description: res.data.message, tone: "success" });
|
|
78
|
+
setEditing(null);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
toast({ title: "Opslaan mislukt", description: e.message, tone: "error" });
|
|
81
|
+
} finally {
|
|
82
|
+
setBusy(false);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function remove(type) {
|
|
86
|
+
const ok = await confirm({ title: "Bouwsteen verwijderen?", description: "Pagina's die dit blok gebruiken slaan het daarna over.", confirmLabel: "Verwijderen", tone: "danger" });
|
|
87
|
+
if (!ok) return;
|
|
88
|
+
const app = getFirebaseApp();
|
|
89
|
+
if (!app) return;
|
|
90
|
+
try {
|
|
91
|
+
await httpsCallable(getFunctions(app, functionsRegion()), "deleteCustomBlock")({ siteId, type });
|
|
92
|
+
toast({ title: "Verwijderd", tone: "success" });
|
|
93
|
+
} catch (e) {
|
|
94
|
+
toast({ title: "Verwijderen mislukt", description: e.message, tone: "error" });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (editing) {
|
|
98
|
+
return /* @__PURE__ */ jsxs("div", { className: "grid max-w-3xl gap-3", children: [
|
|
99
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-bold", children: editing.label || editing.type }),
|
|
100
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
|
|
101
|
+
/* @__PURE__ */ jsx(Field, { label: "Bloktype", hint: 'begint met "custom-"; niet meer te wijzigen na aanmaken', children: /* @__PURE__ */ jsx(Input, { value: editing.type, onChange: (e) => setEditing({ ...editing, type: e.target.value }), disabled: blocks.some((b) => b.type === editing.type) }) }),
|
|
102
|
+
/* @__PURE__ */ jsx(Field, { label: "Naam in het menu", children: /* @__PURE__ */ jsx(Input, { value: editing.label, onChange: (e) => setEditing({ ...editing, label: e.target.value }) }) })
|
|
103
|
+
] }),
|
|
104
|
+
/* @__PURE__ */ jsx(Field, { label: "Icoon (emoji)", children: /* @__PURE__ */ jsx(Input, { value: editing.icon, maxLength: 16, onChange: (e) => setEditing({ ...editing, icon: e.target.value }), className: "w-24" }) }),
|
|
105
|
+
/* @__PURE__ */ jsx(Field, { label: "Velden (Field-IR, JSON)", hint: `Veldtypes: text, textarea, richtext (sleutel eindigt op Html), number, switch, select, date, image, url, link, group, repeater. Voorbeeld:
|
|
106
|
+
${FIELDS_VOORBEELD}`, children: /* @__PURE__ */ jsx(Textarea, { rows: 10, value: fieldsText, onChange: (e) => setFieldsText(e.target.value), spellCheck: false, className: "font-mono text-xs" }) }),
|
|
107
|
+
/* @__PURE__ */ jsx(Field, { label: "Template", hint: "Mustache-subset: {{veld}}, {{{veldHtml}}}, {{#each items}}\u2026{{/each}}, {{#if veld}}\u2026{{else}}\u2026{{/if}}. Geen scripts \u2014 die worden bij opslaan gestript.", children: /* @__PURE__ */ jsx(Textarea, { rows: 10, value: editing.template, onChange: (e) => setEditing({ ...editing, template: e.target.value }), spellCheck: false, className: "font-mono text-xs" }) }),
|
|
108
|
+
/* @__PURE__ */ jsx(Field, { label: "CSS", hint: 'Automatisch gescoped op dit blok ([data-xicms-cb="\u2026"]).', children: /* @__PURE__ */ jsx(Textarea, { rows: 8, value: editing.css, onChange: (e) => setEditing({ ...editing, css: e.target.value }), spellCheck: false, className: "font-mono text-xs" }) }),
|
|
109
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
110
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "primary", disabled: busy, onClick: () => void save(), children: busy ? "Opslaan\u2026" : "Opslaan" }),
|
|
111
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", onClick: () => setEditing(null), children: "Annuleren" })
|
|
112
|
+
] })
|
|
113
|
+
] });
|
|
114
|
+
}
|
|
115
|
+
return /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
|
|
116
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
117
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-bold", children: "Bouwstenen" }),
|
|
118
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => open(EMPTY), children: "+ Nieuwe bouwsteen" })
|
|
119
|
+
] }),
|
|
120
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Eigen bloktypes voor deze site \u2014 zonder release. Definieer velden, een veilige template en CSS; het blok verschijnt direct in het toevoeg-menu van de pagina-editor." }),
|
|
121
|
+
blocks.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen bouwstenen." }),
|
|
122
|
+
/* @__PURE__ */ jsx("ul", { className: "grid gap-2", children: blocks.map((b) => /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between rounded-lg border border-slate-200 p-3", children: [
|
|
123
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
124
|
+
/* @__PURE__ */ jsx("span", { className: "mr-2", children: b.icon }),
|
|
125
|
+
/* @__PURE__ */ jsx("strong", { children: b.label }),
|
|
126
|
+
" ",
|
|
127
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-slate-500", children: [
|
|
128
|
+
"(",
|
|
129
|
+
b.type,
|
|
130
|
+
" \xB7 v",
|
|
131
|
+
b.version,
|
|
132
|
+
")"
|
|
133
|
+
] })
|
|
134
|
+
] }),
|
|
135
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
136
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => open(b), children: "Bewerken" }),
|
|
137
|
+
/* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => void remove(b.type), children: "Verwijderen" })
|
|
138
|
+
] })
|
|
139
|
+
] }, b.type)) })
|
|
140
|
+
] });
|
|
141
|
+
}
|
|
142
|
+
export {
|
|
143
|
+
CustomBlocksPanel
|
|
144
|
+
};
|
|
145
|
+
//# sourceMappingURL=CustomBlocksPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/customBlocks/CustomBlocksPanel.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Beheer-paneel \"Bouwstenen\" (5.0): custom blocks — per site gedefinieerde bloktypes zonder\n * deploy (primo-concept, veilige template-variant). Voor de developer-/agent-rol: velden als\n * Field-IR (JSON, gevalideerd vóór opslaan), een veilige mustache-subset-template en scoped CSS.\n * Opslaan loopt via de saveCustomBlock-callable (sanitize + audit); het blok verschijnt direct\n * in het toevoeg-menu van de editor (live registry-sync in XiCMSProvider).\n */\nimport { useEffect, useState } from \"react\";\nimport { collection, onSnapshot } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { CustomBlockSchema, type CustomBlock } from \"../../schema/customBlocks\";\nimport { FieldsSchema } from \"../../schema/fields\";\nimport { Button, Field, Input, Textarea } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\n\nconst EMPTY: CustomBlock = {\n type: \"custom-\",\n label: \"\",\n icon: \"🧩\",\n fields: [],\n template: \"\",\n css: \"\",\n version: 1,\n};\n\nconst FIELDS_VOORBEELD = `[\n { \"key\": \"heading\", \"label\": \"Kop\", \"type\": \"text\" },\n { \"key\": \"items\", \"label\": \"Items\", \"type\": \"repeater\", \"fields\": [\n { \"key\": \"title\", \"label\": \"Titel\", \"type\": \"text\" },\n { \"key\": \"bodyHtml\", \"label\": \"Tekst\", \"type\": \"richtext\" }\n ] }\n]`;\n\nexport function CustomBlocksPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [blocks, setBlocks] = useState<CustomBlock[]>([]);\n const [editing, setEditing] = useState<CustomBlock | null>(null);\n const [fieldsText, setFieldsText] = useState(\"[]\");\n const [busy, setBusy] = useState(false);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n return onSnapshot(collection(db, \"sites\", siteId, \"customBlocks\"), (snap) => {\n const out: CustomBlock[] = [];\n snap.forEach((d) => {\n const res = CustomBlockSchema.safeParse({ ...(d.data() as Record<string, unknown>), type: d.id });\n if (res.success) out.push(res.data);\n });\n setBlocks(out.sort((a, b) => a.label.localeCompare(b.label)));\n });\n }, [siteId]);\n\n function open(def: CustomBlock) {\n setEditing(def);\n setFieldsText(JSON.stringify(def.fields, null, 2));\n }\n\n async function save() {\n if (!editing) return;\n let fields: unknown;\n try {\n fields = JSON.parse(fieldsText);\n } catch {\n toast({ title: \"Velden zijn geen geldige JSON\", tone: \"error\" });\n return;\n }\n const parsedFields = FieldsSchema.safeParse(fields);\n if (!parsedFields.success) {\n toast({ title: \"Velden ongeldig\", description: parsedFields.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n const candidate = CustomBlockSchema.safeParse({ ...editing, fields: parsedFields.data });\n if (!candidate.success) {\n toast({ title: \"Bouwsteen ongeldig\", description: candidate.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n const app = getFirebaseApp();\n if (!app) return;\n setBusy(true);\n try {\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"saveCustomBlock\");\n const res = await fn({ siteId, ...candidate.data });\n toast({ title: \"Opgeslagen\", description: (res.data as { message?: string }).message, tone: \"success\" });\n setEditing(null);\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function remove(type: string) {\n const ok = await confirm({ title: \"Bouwsteen verwijderen?\", description: \"Pagina's die dit blok gebruiken slaan het daarna over.\", confirmLabel: \"Verwijderen\", tone: \"danger\" });\n if (!ok) return;\n const app = getFirebaseApp();\n if (!app) return;\n try {\n await httpsCallable(getFunctions(app, functionsRegion()), \"deleteCustomBlock\")({ siteId, type });\n toast({ title: \"Verwijderd\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Verwijderen mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n if (editing) {\n return (\n <div className=\"grid max-w-3xl gap-3\">\n <h2 className=\"text-lg font-bold\">{editing.label || editing.type}</h2>\n <div className=\"grid grid-cols-2 gap-2\">\n <Field label=\"Bloktype\" hint='begint met \"custom-\"; niet meer te wijzigen na aanmaken'>\n <Input value={editing.type} onChange={(e) => setEditing({ ...editing, type: e.target.value })} disabled={blocks.some((b) => b.type === editing.type)} />\n </Field>\n <Field label=\"Naam in het menu\">\n <Input value={editing.label} onChange={(e) => setEditing({ ...editing, label: e.target.value })} />\n </Field>\n </div>\n <Field label=\"Icoon (emoji)\">\n <Input value={editing.icon} maxLength={16} onChange={(e) => setEditing({ ...editing, icon: e.target.value })} className=\"w-24\" />\n </Field>\n <Field label=\"Velden (Field-IR, JSON)\" hint={`Veldtypes: text, textarea, richtext (sleutel eindigt op Html), number, switch, select, date, image, url, link, group, repeater. Voorbeeld:\\n${FIELDS_VOORBEELD}`}>\n <Textarea rows={10} value={fieldsText} onChange={(e) => setFieldsText(e.target.value)} spellCheck={false} className=\"font-mono text-xs\" />\n </Field>\n <Field label=\"Template\" hint=\"Mustache-subset: {{veld}}, {{{veldHtml}}}, {{#each items}}…{{/each}}, {{#if veld}}…{{else}}…{{/if}}. Geen scripts — die worden bij opslaan gestript.\">\n <Textarea rows={10} value={editing.template} onChange={(e) => setEditing({ ...editing, template: e.target.value })} spellCheck={false} className=\"font-mono text-xs\" />\n </Field>\n <Field label=\"CSS\" hint='Automatisch gescoped op dit blok ([data-xicms-cb=\"…\"]).'>\n <Textarea rows={8} value={editing.css} onChange={(e) => setEditing({ ...editing, css: e.target.value })} spellCheck={false} className=\"font-mono text-xs\" />\n </Field>\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"primary\" disabled={busy} onClick={() => void save()}>{busy ? \"Opslaan…\" : \"Opslaan\"}</Button>\n <Button type=\"button\" variant=\"ghost\" onClick={() => setEditing(null)}>Annuleren</Button>\n </div>\n </div>\n );\n }\n\n return (\n <div className=\"grid gap-3\">\n <div className=\"flex items-center justify-between\">\n <h2 className=\"text-lg font-bold\">Bouwstenen</h2>\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={() => open(EMPTY)}>+ Nieuwe bouwsteen</Button>\n </div>\n <p className=\"text-sm text-slate-500\">\n Eigen bloktypes voor deze site — zonder release. Definieer velden, een veilige template en CSS;\n het blok verschijnt direct in het toevoeg-menu van de pagina-editor.\n </p>\n {blocks.length === 0 && <p className=\"text-sm text-slate-500\">Nog geen bouwstenen.</p>}\n <ul className=\"grid gap-2\">\n {blocks.map((b) => (\n <li key={b.type} className=\"flex items-center justify-between rounded-lg border border-slate-200 p-3\">\n <div>\n <span className=\"mr-2\">{b.icon}</span>\n <strong>{b.label}</strong> <span className=\"text-xs text-slate-500\">({b.type} · v{b.version})</span>\n </div>\n <div className=\"flex gap-2\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => open(b)}>Bewerken</Button>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => void remove(b.type)}>Verwijderen</Button>\n </div>\n </li>\n ))}\n </ul>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type StoredSymbol } from "../../schema/symbols";
|
|
2
|
+
export declare function subscribeSymbols(siteId: string, cb: (symbols: StoredSymbol[]) => void): () => void;
|
|
3
|
+
export declare function getSymbolOnce(siteId: string, symbolId: string): Promise<StoredSymbol | null>;
|
|
4
|
+
export declare function saveSymbol(input: {
|
|
5
|
+
siteId: string;
|
|
6
|
+
symbolId?: string;
|
|
7
|
+
name: string;
|
|
8
|
+
blocks: Record<string, unknown>[];
|
|
9
|
+
}): Promise<{
|
|
10
|
+
id: string;
|
|
11
|
+
message?: string;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function deleteSymbol(siteId: string, symbolId: string): Promise<void>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { collection, doc, getDoc, onSnapshot } from "firebase/firestore";
|
|
3
|
+
import { getFunctions, httpsCallable } from "firebase/functions";
|
|
4
|
+
import { getDb, getFirebaseApp } from "../../firebase/client";
|
|
5
|
+
import { functionsRegion } from "../../firebase/region";
|
|
6
|
+
import { SymbolSchema } from "../../schema/symbols";
|
|
7
|
+
function subscribeSymbols(siteId, cb) {
|
|
8
|
+
const db = getDb();
|
|
9
|
+
if (!db) {
|
|
10
|
+
cb([]);
|
|
11
|
+
return () => void 0;
|
|
12
|
+
}
|
|
13
|
+
return onSnapshot(
|
|
14
|
+
collection(db, "sites", siteId, "symbols"),
|
|
15
|
+
(snap) => {
|
|
16
|
+
const out = [];
|
|
17
|
+
snap.forEach((d) => {
|
|
18
|
+
const res = SymbolSchema.safeParse(d.data());
|
|
19
|
+
if (res.success) out.push({ id: d.id, ...res.data });
|
|
20
|
+
});
|
|
21
|
+
cb(out.sort((a, b) => a.name.localeCompare(b.name)));
|
|
22
|
+
},
|
|
23
|
+
() => cb([])
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
async function getSymbolOnce(siteId, symbolId) {
|
|
27
|
+
const db = getDb();
|
|
28
|
+
if (!db) return null;
|
|
29
|
+
const snap = await getDoc(doc(db, "sites", siteId, "symbols", symbolId));
|
|
30
|
+
if (!snap.exists()) return null;
|
|
31
|
+
const res = SymbolSchema.safeParse(snap.data());
|
|
32
|
+
return res.success ? { id: snap.id, ...res.data } : null;
|
|
33
|
+
}
|
|
34
|
+
async function saveSymbol(input) {
|
|
35
|
+
const app = getFirebaseApp();
|
|
36
|
+
if (!app) throw new Error("Firebase niet geconfigureerd.");
|
|
37
|
+
const fn = httpsCallable(getFunctions(app, functionsRegion()), "saveSymbol");
|
|
38
|
+
return (await fn(input)).data;
|
|
39
|
+
}
|
|
40
|
+
async function deleteSymbol(siteId, symbolId) {
|
|
41
|
+
const app = getFirebaseApp();
|
|
42
|
+
if (!app) throw new Error("Firebase niet geconfigureerd.");
|
|
43
|
+
await httpsCallable(getFunctions(app, functionsRegion()), "deleteSymbol")({ siteId, symbolId });
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
deleteSymbol,
|
|
47
|
+
getSymbolOnce,
|
|
48
|
+
saveSymbol,
|
|
49
|
+
subscribeSymbols
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=symbolsClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/lib/symbolsClient.ts"],"sourcesContent":["\"use client\";\n\n/** Dataclient voor symbolen (5.0): live lijst + opslaan/verwijderen via de callables. */\nimport { collection, doc, getDoc, onSnapshot } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { SymbolSchema, type StoredSymbol } from \"../../schema/symbols\";\n\nexport function subscribeSymbols(siteId: string, cb: (symbols: StoredSymbol[]) => void): () => void {\n const db = getDb();\n if (!db) {\n cb([]);\n return () => undefined;\n }\n return onSnapshot(\n collection(db, \"sites\", siteId, \"symbols\"),\n (snap) => {\n const out: StoredSymbol[] = [];\n snap.forEach((d) => {\n const res = SymbolSchema.safeParse(d.data());\n if (res.success) out.push({ id: d.id, ...res.data });\n });\n cb(out.sort((a, b) => a.name.localeCompare(b.name)));\n },\n () => cb([]),\n );\n}\n\nexport async function getSymbolOnce(siteId: string, symbolId: string): Promise<StoredSymbol | null> {\n const db = getDb();\n if (!db) return null;\n const snap = await getDoc(doc(db, \"sites\", siteId, \"symbols\", symbolId));\n if (!snap.exists()) return null;\n const res = SymbolSchema.safeParse(snap.data());\n return res.success ? { id: snap.id, ...res.data } : null;\n}\n\nexport async function saveSymbol(input: {\n siteId: string;\n symbolId?: string;\n name: string;\n blocks: Record<string, unknown>[];\n}): Promise<{ id: string; message?: string }> {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = httpsCallable<typeof input, { id: string; message?: string }>(getFunctions(app, functionsRegion()), \"saveSymbol\");\n return (await fn(input)).data;\n}\n\nexport async function deleteSymbol(siteId: string, symbolId: string): Promise<void> {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n await httpsCallable(getFunctions(app, functionsRegion()), \"deleteSymbol\")({ siteId, symbolId });\n}\n"],"mappings":";AAGA,SAAS,YAAY,KAAK,QAAQ,kBAAkB;AACpD,SAAS,cAAc,qBAAqB;AAC5C,SAAS,OAAO,sBAAsB;AACtC,SAAS,uBAAuB;AAChC,SAAS,oBAAuC;AAEzC,SAAS,iBAAiB,QAAgB,IAAmD;AAClG,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,IAAI;AACP,OAAG,CAAC,CAAC;AACL,WAAO,MAAM;AAAA,EACf;AACA,SAAO;AAAA,IACL,WAAW,IAAI,SAAS,QAAQ,SAAS;AAAA,IACzC,CAAC,SAAS;AACR,YAAM,MAAsB,CAAC;AAC7B,WAAK,QAAQ,CAAC,MAAM;AAClB,cAAM,MAAM,aAAa,UAAU,EAAE,KAAK,CAAC;AAC3C,YAAI,IAAI,QAAS,KAAI,KAAK,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,CAAC;AAAA,MACrD,CAAC;AACD,SAAG,IAAI,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC,CAAC;AAAA,IACrD;AAAA,IACA,MAAM,GAAG,CAAC,CAAC;AAAA,EACb;AACF;AAEA,eAAsB,cAAc,QAAgB,UAAgD;AAClG,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,OAAO,MAAM,OAAO,IAAI,IAAI,SAAS,QAAQ,WAAW,QAAQ,CAAC;AACvE,MAAI,CAAC,KAAK,OAAO,EAAG,QAAO;AAC3B,QAAM,MAAM,aAAa,UAAU,KAAK,KAAK,CAAC;AAC9C,SAAO,IAAI,UAAU,EAAE,IAAI,KAAK,IAAI,GAAG,IAAI,KAAK,IAAI;AACtD;AAEA,eAAsB,WAAW,OAKa;AAC5C,QAAM,MAAM,eAAe;AAC3B,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,+BAA+B;AACzD,QAAM,KAAK,cAA8D,aAAa,KAAK,gBAAgB,CAAC,GAAG,YAAY;AAC3H,UAAQ,MAAM,GAAG,KAAK,GAAG;AAC3B;AAEA,eAAsB,aAAa,QAAgB,UAAiC;AAClF,QAAM,MAAM,eAAe;AAC3B,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,+BAA+B;AACzD,QAAM,cAAc,aAAa,KAAK,gBAAgB,CAAC,GAAG,cAAc,EAAE,EAAE,QAAQ,SAAS,CAAC;AAChG;","names":[]}
|
|
@@ -3,10 +3,17 @@
|
|
|
3
3
|
* - de `GenericEditor` (fallback-editor voor blokken zonder eigen `Editor`), en
|
|
4
4
|
* - het `ModuleSettingsForm` (instellingen van een module in /beheer).
|
|
5
5
|
*
|
|
6
|
+
* Twee lagen:
|
|
7
|
+
* - `introspectObjectSchema` (legacy, plat): simpele kinds — blijft bestaan voor module-settings.
|
|
8
|
+
* - `introspectToFields` (5.0): levert de rijke **Field-IR** (schema/fields.ts, primo-port) met
|
|
9
|
+
* échte repeaters/groepen (recursief), richtext-/image-/link-detectie en optionele per-block
|
|
10
|
+
* hints (`defineBlock({ fields })`). De GenericEditor 2.0 rendert deze IR.
|
|
11
|
+
*
|
|
6
12
|
* Een veld krijgt zijn label uit `.describe("…")` op het schema; zonder beschrijving valt het
|
|
7
13
|
* terug op de veldsleutel. Puur (geen React) — apart testbaar.
|
|
8
14
|
*/
|
|
9
15
|
import type { ZodTypeAny } from "zod";
|
|
16
|
+
import type { Field } from "../../schema/fields";
|
|
10
17
|
export interface SchemaField {
|
|
11
18
|
key: string;
|
|
12
19
|
kind: "number" | "boolean" | "string" | "enum" | "json";
|
|
@@ -25,3 +32,14 @@ export interface SchemaField {
|
|
|
25
32
|
* aanbieden; een aanroeper die alleen simpele velden wil, filtert op `kind !== "json"`.
|
|
26
33
|
*/
|
|
27
34
|
export declare function introspectObjectSchema(schema: ZodTypeAny): SchemaField[];
|
|
35
|
+
export type { FieldHints } from "../../schema/fields";
|
|
36
|
+
import type { FieldHints } from "../../schema/fields";
|
|
37
|
+
/**
|
|
38
|
+
* Leidt de rijke Field-IR af uit een Zod-objectschema (5.0). Verschillen met de platte laag:
|
|
39
|
+
* - `z.array(z.object)` → **repeater** met recursief geïntrospecteerde subvelden;
|
|
40
|
+
* - genest `z.object` → **group** (of **link** als de vorm exact `{label, href}` is);
|
|
41
|
+
* - stringvelden `html`/`*Html` → **richtext**, beeld-sleutels → **image**, URL-sleutels → **url**;
|
|
42
|
+
* - al het overige (records, scalar-arrays, unions) → **json** (laatste redmiddel).
|
|
43
|
+
* `hints` verfijnt per (dotted) sleutel: type-overrides (date/slider/…) en `showIf`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function introspectToFields(schema: ZodTypeAny, hints?: FieldHints, prefix?: string): Field[];
|
|
@@ -5,7 +5,7 @@ function unwrap(schema) {
|
|
|
5
5
|
s = s._def.innerType;
|
|
6
6
|
description = description ?? s?._def?.description;
|
|
7
7
|
}
|
|
8
|
-
return { def: s?._def, description };
|
|
8
|
+
return { def: s?._def, description, inner: s };
|
|
9
9
|
}
|
|
10
10
|
function introspectObjectSchema(schema) {
|
|
11
11
|
const def = schema._def;
|
|
@@ -42,7 +42,70 @@ function introspectObjectSchema(schema) {
|
|
|
42
42
|
}
|
|
43
43
|
return out;
|
|
44
44
|
}
|
|
45
|
+
const IMAGE_KEY = /image|photo|foto|logo|cover|avatar|poster/i;
|
|
46
|
+
const URL_KEY = /^(href|url|link)$|Url$|Href$/;
|
|
47
|
+
function introspectToFields(schema, hints, prefix = "") {
|
|
48
|
+
const def = schema._def;
|
|
49
|
+
if (def?.typeName !== "ZodObject" || typeof def.shape !== "function") return [];
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const [key, raw] of Object.entries(def.shape())) {
|
|
52
|
+
if (key.startsWith("_")) continue;
|
|
53
|
+
const { def: inner, description, inner: innerSchema } = unwrap(raw);
|
|
54
|
+
const label = description ?? key;
|
|
55
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
56
|
+
const hint = hints?.[path];
|
|
57
|
+
const push = (f) => out.push({ ...f, ...hint, key, label: hint?.label ?? label });
|
|
58
|
+
switch (inner?.typeName) {
|
|
59
|
+
case "ZodNumber":
|
|
60
|
+
push({ key, label, type: "number" });
|
|
61
|
+
break;
|
|
62
|
+
case "ZodBoolean":
|
|
63
|
+
push({ key, label, type: "switch" });
|
|
64
|
+
break;
|
|
65
|
+
case "ZodEnum":
|
|
66
|
+
push({ key, label, type: "select", options: (inner.values ?? []).map((v) => ({ value: v, label: v })) });
|
|
67
|
+
break;
|
|
68
|
+
case "ZodString": {
|
|
69
|
+
const max = inner.checks?.find((c) => c.kind === "max")?.value;
|
|
70
|
+
if (key === "html" || key.endsWith("Html")) push({ key, label, type: "richtext" });
|
|
71
|
+
else if (IMAGE_KEY.test(key)) push({ key, label, type: "image" });
|
|
72
|
+
else if (URL_KEY.test(key)) push({ key, label, type: "url" });
|
|
73
|
+
else if ((max ?? 0) > 500) push({ key, label, type: "textarea", maxLength: max });
|
|
74
|
+
else push({ key, label, type: "text", maxLength: max });
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case "ZodObject": {
|
|
78
|
+
const sub = introspectToFields(innerSchema, hints, path);
|
|
79
|
+
const isLink = sub.length === 2 && sub.some((f) => f.key === "label") && sub.some((f) => f.key === "href");
|
|
80
|
+
if (isLink) push({ key, label, type: "link" });
|
|
81
|
+
else if (sub.length > 0) push({ key, label, type: "group", fields: sub });
|
|
82
|
+
else push({ key, label, type: "json" });
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "ZodArray": {
|
|
86
|
+
const el = inner.type ?? inner.element;
|
|
87
|
+
const { def: elDef, inner: elSchema } = unwrap(el);
|
|
88
|
+
if (elDef?.typeName === "ZodObject") {
|
|
89
|
+
const sub = introspectToFields(elSchema, hints, path);
|
|
90
|
+
if (sub.length > 0) {
|
|
91
|
+
push({ key, label, type: "repeater", fields: sub });
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
push({ key, label, type: "json" });
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
case "ZodRecord":
|
|
99
|
+
push({ key, label, type: "json" });
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
45
107
|
export {
|
|
46
|
-
introspectObjectSchema
|
|
108
|
+
introspectObjectSchema,
|
|
109
|
+
introspectToFields
|
|
47
110
|
};
|
|
48
111
|
//# sourceMappingURL=zodFields.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/lib/zodFields.ts"],"sourcesContent":["/**\n * Zod-introspectie voor automatisch gegenereerde formulieren. Gedeeld door:\n * - de `GenericEditor` (fallback-editor voor blokken zonder eigen `Editor`), en\n * - het `ModuleSettingsForm` (instellingen van een module in /beheer).\n *\n * Een veld krijgt zijn label uit `.describe(\"…\")` op het schema; zonder beschrijving valt het\n * terug op de veldsleutel. Puur (geen React) — apart testbaar.\n */\nimport type { ZodTypeAny } from \"zod\";\n\nexport interface SchemaField {\n key: string;\n kind: \"number\" | \"boolean\" | \"string\" | \"enum\" | \"json\";\n /** Leesbaar label: `.describe()` van het schema, anders de veldsleutel. */\n label: string;\n options?: string[];\n /** Max-lengte van een string (bepaalt Input vs Textarea in de UI). */\n maxLength?: number;\n /** Het volledige veld-schema — voor validatie van json-velden vóór commit. */\n schema: ZodTypeAny;\n}\n\ninterface AnyDef {\n typeName?: string;\n innerType?: unknown;\n values?: string[];\n description?: string;\n checks?: { kind: string; value?: number }[];\n shape?: () => Record<string, unknown>;\n}\n\n/** Pakt het binnenste type uit ZodDefault/ZodOptional/ZodNullable; bewaart de buitenste beschrijving. */\nfunction unwrap(schema: unknown): { def?: AnyDef; description?: string } {\n let s = schema as { _def?: AnyDef };\n let description = s?._def?.description;\n while (\n s?._def?.typeName === \"ZodDefault\" ||\n s?._def?.typeName === \"ZodOptional\" ||\n s?._def?.typeName === \"ZodNullable\"\n ) {\n s = s._def.innerType as typeof s;\n description = description ?? s?._def?.description;\n }\n return { def: s?._def, description };\n}\n\n/**\n * Leidt een formulier-beschrijving af uit een Zod-objectschema. Interne velden (`_key`/`_type`/\n * `_style`) worden overgeslagen — die beheren de editor-chrome en de StyleEditor. Complexe velden\n * (array/object/record) worden als `json` teruggegeven zodat een fallback-editor ze tóch kan\n * aanbieden; een aanroeper die alleen simpele velden wil, filtert op `kind !== \"json\"`.\n */\nexport function introspectObjectSchema(schema: ZodTypeAny): SchemaField[] {\n const def = (schema as { _def?: AnyDef })._def;\n if (def?.typeName !== \"ZodObject\" || typeof def.shape !== \"function\") return [];\n const out: SchemaField[] = [];\n for (const [key, field] of Object.entries(def.shape())) {\n if (key.startsWith(\"_\")) continue;\n const { def: inner, description } = unwrap(field);\n const label = description ?? key;\n const fieldSchema = field as ZodTypeAny;\n switch (inner?.typeName) {\n case \"ZodNumber\":\n out.push({ key, kind: \"number\", label, schema: fieldSchema });\n break;\n case \"ZodBoolean\":\n out.push({ key, kind: \"boolean\", label, schema: fieldSchema });\n break;\n case \"ZodEnum\":\n out.push({ key, kind: \"enum\", label, options: inner.values, schema: fieldSchema });\n break;\n case \"ZodString\": {\n const max = inner.checks?.find((c) => c.kind === \"max\")?.value;\n out.push({ key, kind: \"string\", label, maxLength: max, schema: fieldSchema });\n break;\n }\n case \"ZodArray\":\n case \"ZodObject\":\n case \"ZodRecord\":\n out.push({ key, kind: \"json\", label, schema: fieldSchema });\n break;\n default:\n // ZodLiteral (bv. _type), unions e.d.: geen zinvol generiek veld.\n break;\n }\n }\n return out;\n}\n"],"mappings":"AAgCA,SAAS,OAAO,QAAyD;AACvE,MAAI,IAAI;AACR,MAAI,cAAc,GAAG,MAAM;AAC3B,SACE,GAAG,MAAM,aAAa,gBACtB,GAAG,MAAM,aAAa,iBACtB,GAAG,MAAM,aAAa,eACtB;AACA,QAAI,EAAE,KAAK;AACX,kBAAc,eAAe,GAAG,MAAM;AAAA,EACxC;AACA,SAAO,EAAE,KAAK,GAAG,MAAM,YAAY;AACrC;AAQO,SAAS,uBAAuB,QAAmC;AACxE,QAAM,MAAO,OAA6B;AAC1C,MAAI,KAAK,aAAa,eAAe,OAAO,IAAI,UAAU,WAAY,QAAO,CAAC;AAC9E,QAAM,MAAqB,CAAC;AAC5B,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,IAAI,MAAM,CAAC,GAAG;AACtD,QAAI,IAAI,WAAW,GAAG,EAAG;AACzB,UAAM,EAAE,KAAK,OAAO,YAAY,IAAI,OAAO,KAAK;AAChD,UAAM,QAAQ,eAAe;AAC7B,UAAM,cAAc;AACpB,YAAQ,OAAO,UAAU;AAAA,MACvB,KAAK;AACH,YAAI,KAAK,EAAE,KAAK,MAAM,UAAU,OAAO,QAAQ,YAAY,CAAC;AAC5D;AAAA,MACF,KAAK;AACH,YAAI,KAAK,EAAE,KAAK,MAAM,WAAW,OAAO,QAAQ,YAAY,CAAC;AAC7D;AAAA,MACF,KAAK;AACH,YAAI,KAAK,EAAE,KAAK,MAAM,QAAQ,OAAO,SAAS,MAAM,QAAQ,QAAQ,YAAY,CAAC;AACjF;AAAA,MACF,KAAK,aAAa;AAChB,cAAM,MAAM,MAAM,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,KAAK,GAAG;AACzD,YAAI,KAAK,EAAE,KAAK,MAAM,UAAU,OAAO,WAAW,KAAK,QAAQ,YAAY,CAAC;AAC5E;AAAA,MACF;AAAA,MACA,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,YAAI,KAAK,EAAE,KAAK,MAAM,QAAQ,OAAO,QAAQ,YAAY,CAAC;AAC1D;AAAA,MACF;AAEE;AAAA,IACJ;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/lib/zodFields.ts"],"sourcesContent":["/**\n * Zod-introspectie voor automatisch gegenereerde formulieren. Gedeeld door:\n * - de `GenericEditor` (fallback-editor voor blokken zonder eigen `Editor`), en\n * - het `ModuleSettingsForm` (instellingen van een module in /beheer).\n *\n * Twee lagen:\n * - `introspectObjectSchema` (legacy, plat): simpele kinds — blijft bestaan voor module-settings.\n * - `introspectToFields` (5.0): levert de rijke **Field-IR** (schema/fields.ts, primo-port) met\n * échte repeaters/groepen (recursief), richtext-/image-/link-detectie en optionele per-block\n * hints (`defineBlock({ fields })`). De GenericEditor 2.0 rendert deze IR.\n *\n * Een veld krijgt zijn label uit `.describe(\"…\")` op het schema; zonder beschrijving valt het\n * terug op de veldsleutel. Puur (geen React) — apart testbaar.\n */\nimport type { ZodTypeAny } from \"zod\";\nimport type { Field } from \"../../schema/fields\";\n\nexport interface SchemaField {\n key: string;\n kind: \"number\" | \"boolean\" | \"string\" | \"enum\" | \"json\";\n /** Leesbaar label: `.describe()` van het schema, anders de veldsleutel. */\n label: string;\n options?: string[];\n /** Max-lengte van een string (bepaalt Input vs Textarea in de UI). */\n maxLength?: number;\n /** Het volledige veld-schema — voor validatie van json-velden vóór commit. */\n schema: ZodTypeAny;\n}\n\ninterface AnyDef {\n typeName?: string;\n innerType?: unknown;\n values?: string[];\n description?: string;\n checks?: { kind: string; value?: number }[];\n shape?: () => Record<string, unknown>;\n}\n\n/** Pakt het binnenste type uit ZodDefault/ZodOptional/ZodNullable; bewaart de buitenste beschrijving. */\nfunction unwrap(schema: unknown): { def?: AnyDef; description?: string; inner?: unknown } {\n let s = schema as { _def?: AnyDef };\n let description = s?._def?.description;\n while (\n s?._def?.typeName === \"ZodDefault\" ||\n s?._def?.typeName === \"ZodOptional\" ||\n s?._def?.typeName === \"ZodNullable\"\n ) {\n s = s._def.innerType as typeof s;\n description = description ?? s?._def?.description;\n }\n return { def: s?._def, description, inner: s };\n}\n\n/**\n * Leidt een formulier-beschrijving af uit een Zod-objectschema. Interne velden (`_key`/`_type`/\n * `_style`) worden overgeslagen — die beheren de editor-chrome en de StyleEditor. Complexe velden\n * (array/object/record) worden als `json` teruggegeven zodat een fallback-editor ze tóch kan\n * aanbieden; een aanroeper die alleen simpele velden wil, filtert op `kind !== \"json\"`.\n */\nexport function introspectObjectSchema(schema: ZodTypeAny): SchemaField[] {\n const def = (schema as { _def?: AnyDef })._def;\n if (def?.typeName !== \"ZodObject\" || typeof def.shape !== \"function\") return [];\n const out: SchemaField[] = [];\n for (const [key, field] of Object.entries(def.shape())) {\n if (key.startsWith(\"_\")) continue;\n const { def: inner, description } = unwrap(field);\n const label = description ?? key;\n const fieldSchema = field as ZodTypeAny;\n switch (inner?.typeName) {\n case \"ZodNumber\":\n out.push({ key, kind: \"number\", label, schema: fieldSchema });\n break;\n case \"ZodBoolean\":\n out.push({ key, kind: \"boolean\", label, schema: fieldSchema });\n break;\n case \"ZodEnum\":\n out.push({ key, kind: \"enum\", label, options: inner.values, schema: fieldSchema });\n break;\n case \"ZodString\": {\n const max = inner.checks?.find((c) => c.kind === \"max\")?.value;\n out.push({ key, kind: \"string\", label, maxLength: max, schema: fieldSchema });\n break;\n }\n case \"ZodArray\":\n case \"ZodObject\":\n case \"ZodRecord\":\n out.push({ key, kind: \"json\", label, schema: fieldSchema });\n break;\n default:\n // ZodLiteral (bv. _type), unions e.d.: geen zinvol generiek veld.\n break;\n }\n }\n return out;\n}\n\nexport type { FieldHints } from \"../../schema/fields\";\nimport type { FieldHints } from \"../../schema/fields\";\n\n/** Herkent een beeld-URL-veld aan de sleutelnaam (zelfde heuristiek als de oude GenericEditor). */\nconst IMAGE_KEY = /image|photo|foto|logo|cover|avatar|poster/i;\n/** Herkent een URL-veld aan de sleutelnaam. */\nconst URL_KEY = /^(href|url|link)$|Url$|Href$/;\n\n/**\n * Leidt de rijke Field-IR af uit een Zod-objectschema (5.0). Verschillen met de platte laag:\n * - `z.array(z.object)` → **repeater** met recursief geïntrospecteerde subvelden;\n * - genest `z.object` → **group** (of **link** als de vorm exact `{label, href}` is);\n * - stringvelden `html`/`*Html` → **richtext**, beeld-sleutels → **image**, URL-sleutels → **url**;\n * - al het overige (records, scalar-arrays, unions) → **json** (laatste redmiddel).\n * `hints` verfijnt per (dotted) sleutel: type-overrides (date/slider/…) en `showIf`.\n */\nexport function introspectToFields(schema: ZodTypeAny, hints?: FieldHints, prefix = \"\"): Field[] {\n const def = (schema as { _def?: AnyDef })._def;\n if (def?.typeName !== \"ZodObject\" || typeof def.shape !== \"function\") return [];\n const out: Field[] = [];\n for (const [key, raw] of Object.entries(def.shape())) {\n if (key.startsWith(\"_\")) continue;\n const { def: inner, description, inner: innerSchema } = unwrap(raw);\n const label = description ?? key;\n const path = prefix ? `${prefix}.${key}` : key;\n const hint = hints?.[path];\n const push = (f: Field) => out.push({ ...f, ...(hint as Partial<Field>), key, label: (hint?.label as string) ?? label } as Field);\n switch (inner?.typeName) {\n case \"ZodNumber\":\n push({ key, label, type: \"number\" });\n break;\n case \"ZodBoolean\":\n push({ key, label, type: \"switch\" });\n break;\n case \"ZodEnum\":\n push({ key, label, type: \"select\", options: (inner.values ?? []).map((v) => ({ value: v, label: v })) });\n break;\n case \"ZodString\": {\n const max = inner.checks?.find((c) => c.kind === \"max\")?.value;\n if (key === \"html\" || key.endsWith(\"Html\")) push({ key, label, type: \"richtext\" });\n else if (IMAGE_KEY.test(key)) push({ key, label, type: \"image\" });\n else if (URL_KEY.test(key)) push({ key, label, type: \"url\" });\n else if ((max ?? 0) > 500) push({ key, label, type: \"textarea\", maxLength: max });\n else push({ key, label, type: \"text\", maxLength: max });\n break;\n }\n case \"ZodObject\": {\n const sub = introspectToFields(innerSchema as ZodTypeAny, hints, path);\n const isLink = sub.length === 2 && sub.some((f) => f.key === \"label\") && sub.some((f) => f.key === \"href\");\n if (isLink) push({ key, label, type: \"link\" });\n else if (sub.length > 0) push({ key, label, type: \"group\", fields: sub });\n else push({ key, label, type: \"json\" });\n break;\n }\n case \"ZodArray\": {\n const el = (inner as { type?: unknown }).type ?? (inner as AnyDef & { element?: unknown }).element;\n const { def: elDef, inner: elSchema } = unwrap(el);\n if (elDef?.typeName === \"ZodObject\") {\n const sub = introspectToFields(elSchema as ZodTypeAny, hints, path);\n if (sub.length > 0) {\n push({ key, label, type: \"repeater\", fields: sub });\n break;\n }\n }\n push({ key, label, type: \"json\" });\n break;\n }\n case \"ZodRecord\":\n push({ key, label, type: \"json\" });\n break;\n default:\n break;\n }\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -6,6 +6,7 @@ import { FormsPanel } from "./forms/FormsPanel";
|
|
|
6
6
|
import { StatsPanel } from "./stats/StatsPanel";
|
|
7
7
|
import { SettingsPanel } from "./settings/SettingsPanel";
|
|
8
8
|
import { NewsEditor } from "./NewsEditor";
|
|
9
|
+
import { CustomBlocksPanel } from "./customBlocks/CustomBlocksPanel";
|
|
9
10
|
function NewsPanel(_) {
|
|
10
11
|
return /* @__PURE__ */ jsx(NewsEditor, {});
|
|
11
12
|
}
|
|
@@ -15,6 +16,8 @@ const builtinPanels = [
|
|
|
15
16
|
{ moduleId: "media", label: "Media", order: 30, minRole: "editor", Panel: MediaManager },
|
|
16
17
|
{ moduleId: "forms", label: "Formulieren", order: 40, minRole: "editor", Panel: FormsPanel },
|
|
17
18
|
{ moduleId: "stats", label: "Statistieken", order: 50, minRole: "editor", Panel: StatsPanel },
|
|
19
|
+
// Bouwstenen (5.0): custom blocks zonder deploy — hangt aan de altijd-actieve content-module.
|
|
20
|
+
{ moduleId: "content", label: "Bouwstenen", order: 60, minRole: "siteadmin", Panel: CustomBlocksPanel },
|
|
18
21
|
{ moduleId: "settings", label: "Instellingen", order: 100, minRole: "siteadmin", Panel: SettingsPanel }
|
|
19
22
|
];
|
|
20
23
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/admin/panelRegistry.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Paneel-registry — koppelt een beheer-paneel aan een module-id. `AdminApp` rendert de tabs\n * hieruit (gesorteerd op `order`, gated op rol én actieve-module-set) i.p.v. hardgecodeerd. Een\n * nieuwe module met een eigen paneel hoeft `AdminApp` dus niet meer aan te raken.\n */\nimport type { ComponentType } from \"react\";\nimport { PagesPanel } from \"./pages/PagesPanel\";\nimport { MediaManager } from \"./media/MediaManager\";\nimport { FormsPanel } from \"./forms/FormsPanel\";\nimport { StatsPanel } from \"./stats/StatsPanel\";\nimport { SettingsPanel } from \"./settings/SettingsPanel\";\nimport { NewsEditor } from \"./NewsEditor\";\n\nexport type PanelMinRole = \"editor\" | \"siteadmin\";\n\nexport interface PanelDefinition {\n /** Moet overeenkomen met de module-`id`; bepaalt of het paneel actief is. */\n moduleId: string;\n label: string;\n /** Sorteervolgorde (laag = links). Instellingen staat vast achteraan (100). */\n order: number;\n minRole: PanelMinRole;\n Panel: ComponentType<{ siteId: string }>;\n}\n\n/** Adapter: de NewsEditor leest zelf siteId via context; wrap voor een uniforme prop-signatuur. */\nfunction NewsPanel(_: { siteId: string }) {\n return <NewsEditor />;\n}\n\n/** De panelen van de ingebouwde modules. */\nexport const builtinPanels: PanelDefinition[] = [\n { moduleId: \"pages\", label: \"Pagina's\", order: 10, minRole: \"editor\", Panel: PagesPanel },\n { moduleId: \"news\", label: \"Nieuws\", order: 20, minRole: \"editor\", Panel: NewsPanel },\n { moduleId: \"media\", label: \"Media\", order: 30, minRole: \"editor\", Panel: MediaManager },\n { moduleId: \"forms\", label: \"Formulieren\", order: 40, minRole: \"editor\", Panel: FormsPanel },\n { moduleId: \"stats\", label: \"Statistieken\", order: 50, minRole: \"editor\", Panel: StatsPanel },\n { moduleId: \"settings\", label: \"Instellingen\", order: 100, minRole: \"siteadmin\", Panel: SettingsPanel },\n];\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/admin/panelRegistry.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Paneel-registry — koppelt een beheer-paneel aan een module-id. `AdminApp` rendert de tabs\n * hieruit (gesorteerd op `order`, gated op rol én actieve-module-set) i.p.v. hardgecodeerd. Een\n * nieuwe module met een eigen paneel hoeft `AdminApp` dus niet meer aan te raken.\n */\nimport type { ComponentType } from \"react\";\nimport { PagesPanel } from \"./pages/PagesPanel\";\nimport { MediaManager } from \"./media/MediaManager\";\nimport { FormsPanel } from \"./forms/FormsPanel\";\nimport { StatsPanel } from \"./stats/StatsPanel\";\nimport { SettingsPanel } from \"./settings/SettingsPanel\";\nimport { NewsEditor } from \"./NewsEditor\";\nimport { CustomBlocksPanel } from \"./customBlocks/CustomBlocksPanel\";\n\nexport type PanelMinRole = \"editor\" | \"siteadmin\";\n\nexport interface PanelDefinition {\n /** Moet overeenkomen met de module-`id`; bepaalt of het paneel actief is. */\n moduleId: string;\n label: string;\n /** Sorteervolgorde (laag = links). Instellingen staat vast achteraan (100). */\n order: number;\n minRole: PanelMinRole;\n Panel: ComponentType<{ siteId: string }>;\n}\n\n/** Adapter: de NewsEditor leest zelf siteId via context; wrap voor een uniforme prop-signatuur. */\nfunction NewsPanel(_: { siteId: string }) {\n return <NewsEditor />;\n}\n\n/** De panelen van de ingebouwde modules. */\nexport const builtinPanels: PanelDefinition[] = [\n { moduleId: \"pages\", label: \"Pagina's\", order: 10, minRole: \"editor\", Panel: PagesPanel },\n { moduleId: \"news\", label: \"Nieuws\", order: 20, minRole: \"editor\", Panel: NewsPanel },\n { moduleId: \"media\", label: \"Media\", order: 30, minRole: \"editor\", Panel: MediaManager },\n { moduleId: \"forms\", label: \"Formulieren\", order: 40, minRole: \"editor\", Panel: FormsPanel },\n { moduleId: \"stats\", label: \"Statistieken\", order: 50, minRole: \"editor\", Panel: StatsPanel },\n // Bouwstenen (5.0): custom blocks zonder deploy — hangt aan de altijd-actieve content-module.\n { moduleId: \"content\", label: \"Bouwstenen\", order: 60, minRole: \"siteadmin\", Panel: CustomBlocksPanel },\n { moduleId: \"settings\", label: \"Instellingen\", order: 100, minRole: \"siteadmin\", Panel: SettingsPanel },\n];\n"],"mappings":";AA8BS;AAtBT,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAelC,SAAS,UAAU,GAAuB;AACxC,SAAO,oBAAC,cAAW;AACrB;AAGO,MAAM,gBAAmC;AAAA,EAC9C,EAAE,UAAU,SAAS,OAAO,YAAY,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA,EACxF,EAAE,UAAU,QAAQ,OAAO,UAAU,OAAO,IAAI,SAAS,UAAU,OAAO,UAAU;AAAA,EACpF,EAAE,UAAU,SAAS,OAAO,SAAS,OAAO,IAAI,SAAS,UAAU,OAAO,aAAa;AAAA,EACvF,EAAE,UAAU,SAAS,OAAO,eAAe,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA,EAC3F,EAAE,UAAU,SAAS,OAAO,gBAAgB,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA;AAAA,EAE5F,EAAE,UAAU,WAAW,OAAO,cAAc,OAAO,IAAI,SAAS,aAAa,OAAO,kBAAkB;AAAA,EACtG,EAAE,UAAU,YAAY,OAAO,gBAAgB,OAAO,KAAK,SAAS,aAAa,OAAO,cAAc;AACxG;","names":[]}
|
|
@@ -15,6 +15,7 @@ import { ArrayEditor } from "../ui/ArrayEditor";
|
|
|
15
15
|
import { useToast } from "../ui/toast";
|
|
16
16
|
import { useConfirm } from "../ui/ConfirmDialog";
|
|
17
17
|
import { ModuleSettingsForm } from "./moduleSettingsForm";
|
|
18
|
+
import { SnapshotsCard } from "./SnapshotsCard";
|
|
18
19
|
function SettingsPanel({ siteId }) {
|
|
19
20
|
const { toast } = useToast();
|
|
20
21
|
const confirm = useConfirm();
|
|
@@ -298,6 +299,7 @@ function SettingsPanel({ siteId }) {
|
|
|
298
299
|
] }),
|
|
299
300
|
/* @__PURE__ */ jsx("p", { className: "mt-2 text-xs text-slate-400", children: "Terugzetten vervangt de huidige content door de back-up. We maken automatisch eerst een back-up van de huidige stand, zodat je nooit iets kwijtraakt." })
|
|
300
301
|
] }),
|
|
302
|
+
/* @__PURE__ */ jsx(SnapshotsCard, { siteId }),
|
|
301
303
|
/* @__PURE__ */ jsx("div", { className: "flex justify-end", children: /* @__PURE__ */ jsx(Button, { onClick: save, children: "Instellingen opslaan" }) })
|
|
302
304
|
] });
|
|
303
305
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/settings/SettingsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { doc, onSnapshot, writeBatch } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { SiteSettingsSchema, toPublicSettings, type SiteSettings } from \"../../schema/settings\";\nimport { useModules } from \"../../modules/context\";\nimport type { ModulesSiteConfig } from \"../../modules/types\";\nimport { builtinThemePresets } from \"../../theme/presets\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { ArrayEditor } from \"../ui/ArrayEditor\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { ModuleSettingsForm } from \"./moduleSettingsForm\";\n\nexport function SettingsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const { defs } = useModules();\n const [s, setS] = useState<SiteSettings>({});\n const [loaded, setLoaded] = useState(false);\n const [exporting, setExporting] = useState(false);\n const [restoring, setRestoring] = useState(false);\n const restoreInput = useRef<HTMLInputElement>(null);\n\n async function downloadBackup(silent = false): Promise<boolean> {\n const app = getFirebaseApp();\n if (!app) return false;\n setExporting(true);\n try {\n const fn = httpsCallable<{ siteId: string }, Record<string, unknown>>(\n getFunctions(app, functionsRegion()),\n \"exportSite\",\n );\n const res = await fn({ siteId });\n const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `xicms-backup-${siteId}-${new Date().toISOString().slice(0, 10)}.json`;\n a.click();\n URL.revokeObjectURL(url);\n if (!silent) toast({ title: \"Back-up gedownload\", tone: \"success\" });\n return true;\n } catch (e) {\n toast({ title: \"Back-up mislukt\", description: (e as Error).message, tone: \"error\" });\n return false;\n } finally {\n setExporting(false);\n }\n }\n\n async function restoreFromFile(file: File) {\n let backup: Record<string, unknown>;\n try {\n backup = JSON.parse(await file.text()) as Record<string, unknown>;\n } catch {\n toast({ title: \"Ongeldig bestand\", description: \"Dit is geen geldig back-upbestand.\", tone: \"error\" });\n return;\n }\n if (backup._format !== \"xicms-export\") {\n toast({ title: \"Ongeldig bestand\", description: \"Dit lijkt geen XiCMS-back-up.\", tone: \"error\" });\n return;\n }\n if (backup.siteId && backup.siteId !== siteId) {\n const ok = await confirm({\n title: \"Back-up van een andere site\",\n description: `Deze back-up is van “${String(backup.siteId)}”, niet van deze site (${siteId}). Toch terugzetten?`,\n confirmLabel: \"Toch terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n }\n const ok = await confirm({\n title: \"Back-up terugzetten?\",\n description: `Dit VERVANGT alle huidige pagina's, nieuws, media en instellingen door de inhoud van de back-up${backup.exportedAt ? ` van ${new Date(String(backup.exportedAt)).toLocaleString(\"nl-NL\")}` : \"\"}. We maken eerst automatisch een back-up van de huidige stand.`,\n confirmLabel: \"Terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n setRestoring(true);\n try {\n // Veiligheid: eerst de huidige stand wegschrijven (als download) vóór we overschrijven.\n // Lukt die back-up niet, dan NIET doorgaan met de destructieve restore (anders ben je de\n // huidige stand kwijt zonder vangnet). De gebruiker kan het daarna opnieuw proberen.\n if (!(await downloadBackup(true))) {\n toast({ title: \"Terugzetten afgebroken\", description: \"De veiligheidsback-up van de huidige stand mislukte; er is niets overschreven.\", tone: \"error\" });\n return;\n }\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"restoreSite\");\n await fn({ siteId, backup });\n toast({ title: \"Back-up teruggezet\", description: \"Herlaad de pagina om alles bij te werken.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Terugzetten mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setRestoring(false);\n }\n }\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoaded(true);\n return;\n }\n return onSnapshot(doc(db, \"sites\", siteId, \"settings\", \"site\"), (snap) => {\n if (snap.exists()) setS(snap.data() as SiteSettings);\n setLoaded(true);\n });\n }, [siteId]);\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = SiteSettingsSchema.safeParse(s);\n if (!parsed.success) {\n toast({ title: \"Ongeldige instellingen\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Volledige (rol-only) settings + de publiek-veilige subset die de live site leest —\n // atomair in één batch, zodat `site` en `public` nooit uit sync kunnen raken.\n try {\n const batch = writeBatch(db);\n batch.set(doc(db, \"sites\", siteId, \"settings\", \"site\"), parsed.data, { merge: true });\n batch.set(doc(db, \"sites\", siteId, \"settings\", \"public\"), toPublicSettings(parsed.data), { merge: true });\n await batch.commit();\n toast({ title: \"Opgeslagen\", description: \"Wijzigingen zijn direct live.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n if (!loaded) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n const forms = s.forms ?? { mode: \"xicms\" as const };\n const theme = s.theme ?? { primary: \"#4f46e5\" };\n const footer = s.footer ?? {};\n\n // Hulpfuncties voor de module-instellingen.\n const moduleState = (id: string, defaultEnabled: boolean) => {\n const raw = (s.modules as ModulesSiteConfig | undefined)?.[id];\n if (typeof raw === \"boolean\") return { enabled: raw, config: {} as Record<string, unknown> };\n return { enabled: raw?.enabled ?? defaultEnabled, config: raw?.config ?? {} };\n };\n const setModule = (id: string, next: { enabled: boolean; config: Record<string, unknown> }) =>\n setS({ ...s, modules: { ...(s.modules as ModulesSiteConfig | undefined), [id]: next } });\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Algemeen</h3>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Sitenaam\"><Input value={s.name ?? \"\"} onChange={(e) => setS({ ...s, name: e.target.value })} /></Field>\n <Field label=\"Domein\"><Input value={s.domain ?? \"\"} onChange={(e) => setS({ ...s, domain: e.target.value })} /></Field>\n <Field label=\"Contact-e-mail\"><Input value={s.contactEmail ?? \"\"} onChange={(e) => setS({ ...s, contactEmail: e.target.value })} /></Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Huisstijl</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Thema-preset\" hint=\"Vult kleuren, lettertype en afronding in één keer in.\">\n <Select\n value=\"\"\n placeholder=\"Kies een preset…\"\n onValueChange={(id) => {\n const preset = builtinThemePresets.find((p) => p.id === id);\n if (preset) setS({ ...s, theme: { ...theme, ...preset.theme } });\n }}\n options={builtinThemePresets.map((p) => ({ value: p.id, label: p.label }))}\n />\n </Field>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Primaire kleur\"><Input value={theme.primary} onChange={(e) => setS({ ...s, theme: { ...theme, primary: e.target.value } })} /></Field>\n <Field label=\"Accentkleur\"><Input value={theme.accent ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, accent: e.target.value } })} /></Field>\n <Field label=\"Achtergrondkleur\"><Input value={theme.background ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, background: e.target.value } })} placeholder=\"#ffffff\" /></Field>\n <Field label=\"Tekstkleur\"><Input value={theme.foreground ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, foreground: e.target.value } })} placeholder=\"#0f172a\" /></Field>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">SEO (standaard)</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Standaard-omschrijving\">\n <Input value={s.seoDefaults?.description ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, description: e.target.value } })} />\n </Field>\n <Field label=\"OG-afbeelding (URL)\">\n <Input value={s.seoDefaults?.ogImage ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, ogImage: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Footer</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Kolommen met links, social-links en copyright. Wijzigingen zijn direct live.</p>\n <div className=\"grid gap-4\">\n <div>\n <span className=\"text-sm font-medium\">Kolommen</span>\n <ArrayEditor\n items={footer.columns ?? []}\n onChange={(columns) => setS({ ...s, footer: { ...footer, columns } })}\n newItem={() => ({ heading: \"\", links: [] })}\n addLabel=\"+ Kolom\"\n emptyLabel=\"Nog geen kolommen.\"\n renderRow={(col, update) => (\n <div className=\"grid gap-2\">\n <Field label=\"Kop\"><Input value={col.heading ?? \"\"} onChange={(e) => update({ heading: e.target.value })} /></Field>\n <ArrayEditor\n items={col.links ?? []}\n onChange={(links) => update({ links })}\n newItem={() => ({ label: \"\", href: \"\" })}\n addLabel=\"+ Link\"\n renderRow={(link, updL) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.label} placeholder=\"Label\" onChange={(e) => updL({ label: e.target.value })} />\n <Input value={link.href} placeholder=\"/pad of https://…\" onChange={(e) => updL({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n )}\n />\n </div>\n <div>\n <span className=\"text-sm font-medium\">Social-links</span>\n <ArrayEditor\n items={footer.social ?? []}\n onChange={(social) => setS({ ...s, footer: { ...footer, social } })}\n newItem={() => ({ platform: \"\", href: \"\" })}\n addLabel=\"+ Social-link\"\n renderRow={(link, update) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.platform} placeholder=\"bv. Instagram\" onChange={(e) => update({ platform: e.target.value })} />\n <Input value={link.href} placeholder=\"https://…\" onChange={(e) => update({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Copyright (leeg = automatisch)\">\n <Input value={footer.copyright ?? \"\"} onChange={(e) => setS({ ...s, footer: { ...footer, copyright: e.target.value } })} />\n </Field>\n <label className=\"flex items-center gap-2 self-end text-sm\">\n <Switch checked={footer.showAdminLink !== false} onCheckedChange={(v) => setS({ ...s, footer: { ...footer, showAdminLink: v } })} />\n “Beheer”-link tonen\n </label>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Modules</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Zet functies aan/uit en stel ze in. Uitgezette modules verbergen hun blokken en beheer-paneel.</p>\n <div className=\"grid gap-3\">\n {defs\n .filter((d) => !d.core)\n .map((d) => {\n const st = moduleState(d.id, d.defaultEnabled ?? true);\n return (\n <div key={d.id} className=\"rounded-lg border border-slate-200 p-3\">\n <label className=\"flex items-center justify-between gap-2\">\n <span>\n <strong className=\"text-sm\">{d.label}</strong>\n {d.description && <span className=\"block text-xs text-slate-500\">{d.description}</span>}\n </span>\n <Switch checked={st.enabled} onCheckedChange={(v) => setModule(d.id, { enabled: v, config: st.config })} />\n </label>\n {st.enabled && d.settingsSchema && (\n <div className=\"mt-3\">\n <ModuleSettingsForm\n schema={d.settingsSchema}\n value={st.config}\n onChange={(config) => setModule(d.id, { enabled: st.enabled, config })}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Formulier-e-mail</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Een provider verstuurt alleen vanaf een geverifieerd domein. Kies de XiCMS-afzender\n (werkt direct) of het eigen domein van de klant (vereist domeinverificatie).\n </p>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzender-modus\">\n <Select\n value={forms.mode}\n onValueChange={(v) => setS({ ...s, forms: { ...forms, mode: v as \"xicms\" | \"client\" } })}\n options={[\n { value: \"xicms\", label: \"XiCMS-domein (aanbevolen)\" },\n { value: \"client\", label: \"Eigen domein van de klant\" },\n ]}\n />\n </Field>\n <Field label=\"Van (from) e-mail\" hint=\"Moet een geverifieerd domein zijn.\">\n <Input value={forms.fromEmail ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromEmail: e.target.value } })} />\n </Field>\n <Field label=\"Van (naam)\"><Input value={forms.fromName ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromName: e.target.value } })} /></Field>\n <Field label=\"Notificaties naar\" hint=\"Meestal het mailadres van de klant.\">\n <Input value={forms.notifyTo ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, notifyTo: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Back-up & herstel</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Sla alle content van deze site (pagina's, nieuws, formulieren, instellingen en je foto-galerij)\n op in één bestand. Bewaar dat veilig; je kunt het later weer terugzetten. De foto-bestanden\n zelf blijven in de cloud staan — de back-up bevat de teksten, instellingen en verwijzingen.\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"secondary\" onClick={() => void downloadBackup()} disabled={exporting || restoring}>\n {exporting ? \"Bezig…\" : \"Back-up downloaden\"}\n </Button>\n <Button variant=\"secondary\" onClick={() => restoreInput.current?.click()} disabled={restoring || exporting}>\n {restoring ? \"Terugzetten…\" : \"Back-up terugzetten…\"}\n </Button>\n <input\n ref={restoreInput}\n type=\"file\"\n accept=\"application/json,.json\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void restoreFromFile(f);\n e.target.value = \"\";\n }}\n />\n </div>\n <p className=\"mt-2 text-xs text-slate-400\">\n Terugzetten vervangt de huidige content door de back-up. We maken automatisch eerst een\n back-up van de huidige stand, zodat je nooit iets kwijtraakt.\n </p>\n </Card>\n\n <div className=\"flex justify-end\">\n <Button onClick={save}>Instellingen opslaan</Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["ok"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/settings/SettingsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { doc, onSnapshot, writeBatch } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { SiteSettingsSchema, toPublicSettings, type SiteSettings } from \"../../schema/settings\";\nimport { useModules } from \"../../modules/context\";\nimport type { ModulesSiteConfig } from \"../../modules/types\";\nimport { builtinThemePresets } from \"../../theme/presets\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { ArrayEditor } from \"../ui/ArrayEditor\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { ModuleSettingsForm } from \"./moduleSettingsForm\";\nimport { SnapshotsCard } from \"./SnapshotsCard\";\n\nexport function SettingsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const { defs } = useModules();\n const [s, setS] = useState<SiteSettings>({});\n const [loaded, setLoaded] = useState(false);\n const [exporting, setExporting] = useState(false);\n const [restoring, setRestoring] = useState(false);\n const restoreInput = useRef<HTMLInputElement>(null);\n\n async function downloadBackup(silent = false): Promise<boolean> {\n const app = getFirebaseApp();\n if (!app) return false;\n setExporting(true);\n try {\n const fn = httpsCallable<{ siteId: string }, Record<string, unknown>>(\n getFunctions(app, functionsRegion()),\n \"exportSite\",\n );\n const res = await fn({ siteId });\n const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `xicms-backup-${siteId}-${new Date().toISOString().slice(0, 10)}.json`;\n a.click();\n URL.revokeObjectURL(url);\n if (!silent) toast({ title: \"Back-up gedownload\", tone: \"success\" });\n return true;\n } catch (e) {\n toast({ title: \"Back-up mislukt\", description: (e as Error).message, tone: \"error\" });\n return false;\n } finally {\n setExporting(false);\n }\n }\n\n async function restoreFromFile(file: File) {\n let backup: Record<string, unknown>;\n try {\n backup = JSON.parse(await file.text()) as Record<string, unknown>;\n } catch {\n toast({ title: \"Ongeldig bestand\", description: \"Dit is geen geldig back-upbestand.\", tone: \"error\" });\n return;\n }\n if (backup._format !== \"xicms-export\") {\n toast({ title: \"Ongeldig bestand\", description: \"Dit lijkt geen XiCMS-back-up.\", tone: \"error\" });\n return;\n }\n if (backup.siteId && backup.siteId !== siteId) {\n const ok = await confirm({\n title: \"Back-up van een andere site\",\n description: `Deze back-up is van “${String(backup.siteId)}”, niet van deze site (${siteId}). Toch terugzetten?`,\n confirmLabel: \"Toch terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n }\n const ok = await confirm({\n title: \"Back-up terugzetten?\",\n description: `Dit VERVANGT alle huidige pagina's, nieuws, media en instellingen door de inhoud van de back-up${backup.exportedAt ? ` van ${new Date(String(backup.exportedAt)).toLocaleString(\"nl-NL\")}` : \"\"}. We maken eerst automatisch een back-up van de huidige stand.`,\n confirmLabel: \"Terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n setRestoring(true);\n try {\n // Veiligheid: eerst de huidige stand wegschrijven (als download) vóór we overschrijven.\n // Lukt die back-up niet, dan NIET doorgaan met de destructieve restore (anders ben je de\n // huidige stand kwijt zonder vangnet). De gebruiker kan het daarna opnieuw proberen.\n if (!(await downloadBackup(true))) {\n toast({ title: \"Terugzetten afgebroken\", description: \"De veiligheidsback-up van de huidige stand mislukte; er is niets overschreven.\", tone: \"error\" });\n return;\n }\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"restoreSite\");\n await fn({ siteId, backup });\n toast({ title: \"Back-up teruggezet\", description: \"Herlaad de pagina om alles bij te werken.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Terugzetten mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setRestoring(false);\n }\n }\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoaded(true);\n return;\n }\n return onSnapshot(doc(db, \"sites\", siteId, \"settings\", \"site\"), (snap) => {\n if (snap.exists()) setS(snap.data() as SiteSettings);\n setLoaded(true);\n });\n }, [siteId]);\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = SiteSettingsSchema.safeParse(s);\n if (!parsed.success) {\n toast({ title: \"Ongeldige instellingen\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Volledige (rol-only) settings + de publiek-veilige subset die de live site leest —\n // atomair in één batch, zodat `site` en `public` nooit uit sync kunnen raken.\n try {\n const batch = writeBatch(db);\n batch.set(doc(db, \"sites\", siteId, \"settings\", \"site\"), parsed.data, { merge: true });\n batch.set(doc(db, \"sites\", siteId, \"settings\", \"public\"), toPublicSettings(parsed.data), { merge: true });\n await batch.commit();\n toast({ title: \"Opgeslagen\", description: \"Wijzigingen zijn direct live.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n if (!loaded) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n const forms = s.forms ?? { mode: \"xicms\" as const };\n const theme = s.theme ?? { primary: \"#4f46e5\" };\n const footer = s.footer ?? {};\n\n // Hulpfuncties voor de module-instellingen.\n const moduleState = (id: string, defaultEnabled: boolean) => {\n const raw = (s.modules as ModulesSiteConfig | undefined)?.[id];\n if (typeof raw === \"boolean\") return { enabled: raw, config: {} as Record<string, unknown> };\n return { enabled: raw?.enabled ?? defaultEnabled, config: raw?.config ?? {} };\n };\n const setModule = (id: string, next: { enabled: boolean; config: Record<string, unknown> }) =>\n setS({ ...s, modules: { ...(s.modules as ModulesSiteConfig | undefined), [id]: next } });\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Algemeen</h3>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Sitenaam\"><Input value={s.name ?? \"\"} onChange={(e) => setS({ ...s, name: e.target.value })} /></Field>\n <Field label=\"Domein\"><Input value={s.domain ?? \"\"} onChange={(e) => setS({ ...s, domain: e.target.value })} /></Field>\n <Field label=\"Contact-e-mail\"><Input value={s.contactEmail ?? \"\"} onChange={(e) => setS({ ...s, contactEmail: e.target.value })} /></Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Huisstijl</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Thema-preset\" hint=\"Vult kleuren, lettertype en afronding in één keer in.\">\n <Select\n value=\"\"\n placeholder=\"Kies een preset…\"\n onValueChange={(id) => {\n const preset = builtinThemePresets.find((p) => p.id === id);\n if (preset) setS({ ...s, theme: { ...theme, ...preset.theme } });\n }}\n options={builtinThemePresets.map((p) => ({ value: p.id, label: p.label }))}\n />\n </Field>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Primaire kleur\"><Input value={theme.primary} onChange={(e) => setS({ ...s, theme: { ...theme, primary: e.target.value } })} /></Field>\n <Field label=\"Accentkleur\"><Input value={theme.accent ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, accent: e.target.value } })} /></Field>\n <Field label=\"Achtergrondkleur\"><Input value={theme.background ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, background: e.target.value } })} placeholder=\"#ffffff\" /></Field>\n <Field label=\"Tekstkleur\"><Input value={theme.foreground ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, foreground: e.target.value } })} placeholder=\"#0f172a\" /></Field>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">SEO (standaard)</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Standaard-omschrijving\">\n <Input value={s.seoDefaults?.description ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, description: e.target.value } })} />\n </Field>\n <Field label=\"OG-afbeelding (URL)\">\n <Input value={s.seoDefaults?.ogImage ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, ogImage: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Footer</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Kolommen met links, social-links en copyright. Wijzigingen zijn direct live.</p>\n <div className=\"grid gap-4\">\n <div>\n <span className=\"text-sm font-medium\">Kolommen</span>\n <ArrayEditor\n items={footer.columns ?? []}\n onChange={(columns) => setS({ ...s, footer: { ...footer, columns } })}\n newItem={() => ({ heading: \"\", links: [] })}\n addLabel=\"+ Kolom\"\n emptyLabel=\"Nog geen kolommen.\"\n renderRow={(col, update) => (\n <div className=\"grid gap-2\">\n <Field label=\"Kop\"><Input value={col.heading ?? \"\"} onChange={(e) => update({ heading: e.target.value })} /></Field>\n <ArrayEditor\n items={col.links ?? []}\n onChange={(links) => update({ links })}\n newItem={() => ({ label: \"\", href: \"\" })}\n addLabel=\"+ Link\"\n renderRow={(link, updL) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.label} placeholder=\"Label\" onChange={(e) => updL({ label: e.target.value })} />\n <Input value={link.href} placeholder=\"/pad of https://…\" onChange={(e) => updL({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n )}\n />\n </div>\n <div>\n <span className=\"text-sm font-medium\">Social-links</span>\n <ArrayEditor\n items={footer.social ?? []}\n onChange={(social) => setS({ ...s, footer: { ...footer, social } })}\n newItem={() => ({ platform: \"\", href: \"\" })}\n addLabel=\"+ Social-link\"\n renderRow={(link, update) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.platform} placeholder=\"bv. Instagram\" onChange={(e) => update({ platform: e.target.value })} />\n <Input value={link.href} placeholder=\"https://…\" onChange={(e) => update({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Copyright (leeg = automatisch)\">\n <Input value={footer.copyright ?? \"\"} onChange={(e) => setS({ ...s, footer: { ...footer, copyright: e.target.value } })} />\n </Field>\n <label className=\"flex items-center gap-2 self-end text-sm\">\n <Switch checked={footer.showAdminLink !== false} onCheckedChange={(v) => setS({ ...s, footer: { ...footer, showAdminLink: v } })} />\n “Beheer”-link tonen\n </label>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Modules</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Zet functies aan/uit en stel ze in. Uitgezette modules verbergen hun blokken en beheer-paneel.</p>\n <div className=\"grid gap-3\">\n {defs\n .filter((d) => !d.core)\n .map((d) => {\n const st = moduleState(d.id, d.defaultEnabled ?? true);\n return (\n <div key={d.id} className=\"rounded-lg border border-slate-200 p-3\">\n <label className=\"flex items-center justify-between gap-2\">\n <span>\n <strong className=\"text-sm\">{d.label}</strong>\n {d.description && <span className=\"block text-xs text-slate-500\">{d.description}</span>}\n </span>\n <Switch checked={st.enabled} onCheckedChange={(v) => setModule(d.id, { enabled: v, config: st.config })} />\n </label>\n {st.enabled && d.settingsSchema && (\n <div className=\"mt-3\">\n <ModuleSettingsForm\n schema={d.settingsSchema}\n value={st.config}\n onChange={(config) => setModule(d.id, { enabled: st.enabled, config })}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Formulier-e-mail</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Een provider verstuurt alleen vanaf een geverifieerd domein. Kies de XiCMS-afzender\n (werkt direct) of het eigen domein van de klant (vereist domeinverificatie).\n </p>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzender-modus\">\n <Select\n value={forms.mode}\n onValueChange={(v) => setS({ ...s, forms: { ...forms, mode: v as \"xicms\" | \"client\" } })}\n options={[\n { value: \"xicms\", label: \"XiCMS-domein (aanbevolen)\" },\n { value: \"client\", label: \"Eigen domein van de klant\" },\n ]}\n />\n </Field>\n <Field label=\"Van (from) e-mail\" hint=\"Moet een geverifieerd domein zijn.\">\n <Input value={forms.fromEmail ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromEmail: e.target.value } })} />\n </Field>\n <Field label=\"Van (naam)\"><Input value={forms.fromName ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromName: e.target.value } })} /></Field>\n <Field label=\"Notificaties naar\" hint=\"Meestal het mailadres van de klant.\">\n <Input value={forms.notifyTo ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, notifyTo: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Back-up & herstel</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Sla alle content van deze site (pagina's, nieuws, formulieren, instellingen en je foto-galerij)\n op in één bestand. Bewaar dat veilig; je kunt het later weer terugzetten. De foto-bestanden\n zelf blijven in de cloud staan — de back-up bevat de teksten, instellingen en verwijzingen.\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"secondary\" onClick={() => void downloadBackup()} disabled={exporting || restoring}>\n {exporting ? \"Bezig…\" : \"Back-up downloaden\"}\n </Button>\n <Button variant=\"secondary\" onClick={() => restoreInput.current?.click()} disabled={restoring || exporting}>\n {restoring ? \"Terugzetten…\" : \"Back-up terugzetten…\"}\n </Button>\n <input\n ref={restoreInput}\n type=\"file\"\n accept=\"application/json,.json\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void restoreFromFile(f);\n e.target.value = \"\";\n }}\n />\n </div>\n <p className=\"mt-2 text-xs text-slate-400\">\n Terugzetten vervangt de huidige content door de back-up. We maken automatisch eerst een\n back-up van de huidige stand, zodat je nooit iets kwijtraakt.\n </p>\n </Card>\n\n <SnapshotsCard siteId={siteId} />\n\n <div className=\"flex justify-end\">\n <Button onClick={save}>Instellingen opslaan</Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["ok"]}
|