@xiwebdesign/xicms-core 7.2.0 → 7.3.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 +2 -8
- package/dist/XiCMSProvider.js +1 -8
- package/dist/XiCMSProvider.js.map +1 -1
- package/dist/admin/AdminApp.js +7 -12
- package/dist/admin/AdminApp.js.map +1 -1
- package/dist/admin/NewsEditor.d.ts +8 -1
- package/dist/admin/NewsEditor.js +19 -18
- package/dist/admin/NewsEditor.js.map +1 -1
- package/dist/admin/SuperadminPanel.js +6 -7
- package/dist/admin/SuperadminPanel.js.map +1 -1
- package/dist/admin/customBlocks/CustomBlocksPanel.js +3 -4
- package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -1
- package/dist/admin/lib/pagesClient.js +3 -4
- package/dist/admin/lib/pagesClient.js.map +1 -1
- package/dist/admin/lib/symbolsClient.js +3 -4
- package/dist/admin/lib/symbolsClient.js.map +1 -1
- package/dist/admin/pages/PagesPanel.js +5 -2
- package/dist/admin/pages/PagesPanel.js.map +1 -1
- package/dist/admin/settings/MailSettings.js +2 -3
- package/dist/admin/settings/MailSettings.js.map +1 -1
- package/dist/admin/settings/MailTemplates.js +2 -3
- package/dist/admin/settings/MailTemplates.js.map +1 -1
- package/dist/admin/settings/SettingsPanel.js +250 -225
- package/dist/admin/settings/SettingsPanel.js.map +1 -1
- package/dist/admin/settings/SnapshotsCard.js +3 -4
- package/dist/admin/settings/SnapshotsCard.js.map +1 -1
- package/dist/admin/shop/lib.js +2 -3
- package/dist/admin/shop/lib.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +239 -103
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/blocks/builtins/Agenda.js +6 -6
- package/dist/blocks/builtins/Agenda.js.map +1 -1
- package/dist/blocks/builtins/Banner.js +1 -1
- package/dist/blocks/builtins/Banner.js.map +1 -1
- package/dist/blocks/builtins/Breadcrumbs.js +5 -2
- package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
- package/dist/blocks/builtins/Cards.js +2 -2
- package/dist/blocks/builtins/Cards.js.map +1 -1
- package/dist/blocks/builtins/Contact.js +3 -3
- package/dist/blocks/builtins/Contact.js.map +1 -1
- package/dist/blocks/builtins/Cta.js +3 -3
- package/dist/blocks/builtins/Cta.js.map +1 -1
- package/dist/blocks/builtins/Downloads.js +5 -5
- package/dist/blocks/builtins/Downloads.js.map +1 -1
- package/dist/blocks/builtins/Elements.js +8 -5
- package/dist/blocks/builtins/Elements.js.map +1 -1
- package/dist/blocks/builtins/Hero.js +1 -1
- package/dist/blocks/builtins/Hero.js.map +1 -1
- package/dist/blocks/builtins/Jobs.js +4 -1
- package/dist/blocks/builtins/Jobs.js.map +1 -1
- package/dist/blocks/builtins/MapBlock.js +11 -11
- package/dist/blocks/builtins/MapBlock.js.map +1 -1
- package/dist/blocks/builtins/Newsletter.js +3 -2
- package/dist/blocks/builtins/Newsletter.js.map +1 -1
- package/dist/blocks/builtins/OpeningHours.js +2 -2
- package/dist/blocks/builtins/OpeningHours.js.map +1 -1
- package/dist/blocks/builtins/PdfBlock.js +2 -2
- package/dist/blocks/builtins/PdfBlock.js.map +1 -1
- package/dist/blocks/builtins/Portfolio.js +4 -4
- package/dist/blocks/builtins/Portfolio.js.map +1 -1
- package/dist/blocks/builtins/Pricing.js +3 -3
- package/dist/blocks/builtins/Pricing.js.map +1 -1
- package/dist/blocks/builtins/Sections.js +7 -7
- package/dist/blocks/builtins/Sections.js.map +1 -1
- package/dist/blocks/builtins/Team.js +3 -3
- package/dist/blocks/builtins/Team.js.map +1 -1
- package/dist/blocks/builtins/Widgets.js +28 -28
- package/dist/blocks/builtins/Widgets.js.map +1 -1
- package/dist/blocks/builtins/lib/submitForm.js +3 -4
- package/dist/blocks/builtins/lib/submitForm.js.map +1 -1
- package/dist/blocks/ui/index.d.ts +8 -1
- package/dist/blocks/ui/index.js +3 -2
- package/dist/blocks/ui/index.js.map +1 -1
- package/dist/components/EditBar.d.ts +9 -1
- package/dist/components/EditBar.js +6 -5
- package/dist/components/EditBar.js.map +1 -1
- package/dist/components/PageView.js +3 -5
- package/dist/components/PageView.js.map +1 -1
- package/dist/firebase/admin.js +4 -0
- package/dist/firebase/admin.js.map +1 -1
- package/dist/firebase/client.js +23 -5
- package/dist/firebase/client.js.map +1 -1
- package/dist/firebase/emulators.d.ts +9 -0
- package/dist/firebase/emulators.js +24 -0
- package/dist/firebase/emulators.js.map +1 -0
- package/dist/firebase/functionsClient.d.ts +18 -0
- package/dist/firebase/functionsClient.js +17 -0
- package/dist/firebase/functionsClient.js.map +1 -0
- package/dist/lock/DemoLock.js +19 -2
- package/dist/lock/DemoLock.js.map +1 -1
- package/dist/modules/shop/cartStore.js +2 -1
- package/dist/modules/shop/cartStore.js.map +1 -1
- package/dist/modules/shop/lib.js +9 -10
- package/dist/modules/shop/lib.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/pages.d.ts +11 -0
- package/dist/server/pages.js +47 -0
- package/dist/server/pages.js.map +1 -1
- package/dist/styles.css +130 -29
- package/dist/theme/color.d.ts +17 -0
- package/dist/theme/color.js +12 -0
- package/dist/theme/color.js.map +1 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -1 +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 (e) {\n // Regel/kolom uit de parserfout halen: \"position N\" zonder context was ondebugbaar bij\n // een handgetypte Field-IR — één verkeerde komma blokkeerde het hele bouwsteen-opslaan.\n const msg = (e as Error).message;\n const pos = /position (\\d+)/.exec(msg);\n let where = \"\";\n if (pos) {\n const n = Number(pos[1]);\n const before = fieldsText.slice(0, n);\n const line = before.split(\"\\n\").length;\n const col = n - before.lastIndexOf(\"\\n\");\n where = ` (regel ${line}, kolom ${col})`;\n }\n toast({ title: \"Velden zijn geen geldige JSON\", description: `${msg}${where}`, 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":[]}
|
|
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 { callable } from \"../../firebase/functionsClient\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\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 (e) {\n // Regel/kolom uit de parserfout halen: \"position N\" zonder context was ondebugbaar bij\n // een handgetypte Field-IR — één verkeerde komma blokkeerde het hele bouwsteen-opslaan.\n const msg = (e as Error).message;\n const pos = /position (\\d+)/.exec(msg);\n let where = \"\";\n if (pos) {\n const n = Number(pos[1]);\n const before = fieldsText.slice(0, n);\n const line = before.split(\"\\n\").length;\n const col = n - before.lastIndexOf(\"\\n\");\n where = ` (regel ${line}, kolom ${col})`;\n }\n toast({ title: \"Velden zijn geen geldige JSON\", description: `${msg}${where}`, 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 = callable(app, \"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 callable(app, \"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":[]}
|
|
@@ -14,9 +14,8 @@ import {
|
|
|
14
14
|
setDoc,
|
|
15
15
|
where
|
|
16
16
|
} from "firebase/firestore";
|
|
17
|
-
import {
|
|
17
|
+
import { callable } from "../../firebase/functionsClient.js";
|
|
18
18
|
import { getDb, getFirebaseApp } from "../../firebase/client.js";
|
|
19
|
-
import { functionsRegion } from "../../firebase/region.js";
|
|
20
19
|
import { isDemoActive, demoIdForPath, resolveDemoPage, saveDemoDraft } from "../../demo/demoStore.js";
|
|
21
20
|
import { sanitizeDraftBlocks } from "./sanitizeDraft.js";
|
|
22
21
|
function subscribePages(siteId, cb) {
|
|
@@ -104,7 +103,7 @@ async function deletePage(siteId, pageId) {
|
|
|
104
103
|
const app = getFirebaseApp();
|
|
105
104
|
if (!app) return;
|
|
106
105
|
try {
|
|
107
|
-
const fn =
|
|
106
|
+
const fn = callable(app, "deletePage");
|
|
108
107
|
await fn({ siteId, pageId });
|
|
109
108
|
} catch {
|
|
110
109
|
}
|
|
@@ -133,7 +132,7 @@ async function publishPage(siteId, pageId) {
|
|
|
133
132
|
}
|
|
134
133
|
const app = getFirebaseApp();
|
|
135
134
|
if (!app) throw new Error("Firebase niet geconfigureerd.");
|
|
136
|
-
const fn =
|
|
135
|
+
const fn = callable(app, "publishPage");
|
|
137
136
|
await fn({ siteId, pageId });
|
|
138
137
|
}
|
|
139
138
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/lib/pagesClient.ts"],"sourcesContent":["\"use client\";\n\nimport {\n addDoc,\n collection,\n deleteDoc,\n doc,\n getDoc,\n getDocs,\n limit,\n onSnapshot,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n where,\n} from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport type { Page } from \"../../schema/pages\";\nimport { isDemoActive, demoIdForPath, resolveDemoPage, saveDemoDraft } from \"../../demo/demoStore\";\nimport { sanitizeDraftBlocks } from \"./sanitizeDraft\";\n\nexport type DraftPage = Page & { id: string; published?: boolean };\n\n/** Live-lijst van pagina's (werkversies in `drafts/`). */\nexport function subscribePages(siteId: string, cb: (pages: DraftPage[]) => void): () => void {\n const db = getDb();\n if (!db) {\n cb([]);\n return () => {};\n }\n const q = query(collection(db, \"sites\", siteId, \"drafts\"), orderBy(\"order\", \"asc\"));\n return onSnapshot(q, (snap) => {\n cb(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<DraftPage, \"id\">) })));\n });\n}\n\nexport function subscribeVersions(\n siteId: string,\n pageId: string,\n cb: (versions: { id: string; createdAt?: unknown; kind?: string; label?: string }[]) => void,\n): () => void {\n const db = getDb();\n if (!db) {\n cb([]);\n return () => {};\n }\n const q = query(collection(db, \"sites\", siteId, \"pages\", pageId, \"versions\"), orderBy(\"createdAt\", \"desc\"));\n return onSnapshot(q, (snap) => cb(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Record<string, unknown>) }))));\n}\n\n/** Laadt de werkversie (draft) voor een pad — voor het inline bewerken op de live pagina. */\nexport async function getDraftByPath(siteId: string, path: string): Promise<DraftPage | null> {\n // Demo-modus: de werkversie komt uit de lokale browseropslag (seed + lokale bewerkingen).\n if (isDemoActive()) {\n const page = resolveDemoPage(siteId, path);\n return page ? ({ ...page, id: demoIdForPath(path) } as DraftPage) : null;\n }\n const db = getDb();\n if (!db) return null;\n const snap = await getDocs(\n query(collection(db, \"sites\", siteId, \"drafts\"), where(\"path\", \"==\", path), limit(1)),\n );\n const d = snap.docs[0];\n // Rich text uit een werkversie is nooit server-side gesaneerd (zie sanitizeDraft.ts).\n return d ? sanitizeDraftBlocks({ id: d.id, ...(d.data() as Omit<DraftPage, \"id\">) }) : null;\n}\n\n/**\n * Live-listener op één werkversie — voor conflictdetectie in de visuele editor (twee tabs of\n * twee editors tegelijk). `fromServer` is false voor de lokale echo van een eigen write\n * (hasPendingWrites), zodat de aanroeper eigen saves kan negeren.\n */\nexport function subscribeDraft(\n siteId: string,\n pageId: string,\n cb: (draft: DraftPage | null, meta: { fromServer: boolean }) => void,\n): () => void {\n if (isDemoActive()) return () => {};\n const db = getDb();\n if (!db) return () => {};\n return onSnapshot(doc(db, \"sites\", siteId, \"drafts\", pageId), (snap) => {\n cb(\n snap.exists() ? sanitizeDraftBlocks({ id: snap.id, ...(snap.data() as Omit<DraftPage, \"id\">) }) : null,\n { fromServer: !snap.metadata.hasPendingWrites },\n );\n });\n}\n\n/**\n * Firestore WEIGERT `undefined` als veldwaarde: `setDoc({ seo: undefined })` gooit\n * \"Unsupported field value: undefined\" en de hele schrijfactie mislukt. De editors geven hun\n * pagina-state als geheel door (`{ title, slug, path, seo, blocks }`), en bij een nieuwe pagina is\n * `seo` gewoon nog niet ingevuld — dus liep élke eerste publicatie van een nieuwe pagina stuk op\n * de autosave die eraan voorafgaat. Gevonden op de staging-deploy van groenhof: \"Publiceren\n * mislukt — Unsupported field value: undefined (found in field seo)\".\n *\n * Weglaten is hier ook de JUISTE semantiek: deze writes zijn `merge: true`, en \"veld niet\n * meegestuurd\" betekent daar \"laat staan wat er stond\". Een veld écht wissen doe je met\n * `deleteField()`, niet met `undefined` — dat onderscheid blijft zo intact.\n */\nfunction zonderUndefined<T extends Record<string, unknown>>(o: T): Partial<T> {\n return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined)) as Partial<T>;\n}\n\nexport async function createPage(siteId: string, input: Partial<Page> & { title: string; slug: string; path: string }) {\n const db = getDb();\n if (!db) throw new Error(\"Firebase niet geconfigureerd.\");\n const ref = await addDoc(collection(db, \"sites\", siteId, \"drafts\"), {\n parentId: null,\n order: Date.now(),\n status: \"draft\",\n blocks: [],\n published: false,\n ...zonderUndefined(input),\n updatedAt: serverTimestamp(),\n });\n return ref.id;\n}\n\nexport async function saveDraft(siteId: string, pageId: string, partial: Partial<Page>) {\n // Demo-modus: bewaar de bewerking alleen lokaal in de browser (niet in Firebase).\n if (isDemoActive()) {\n saveDemoDraft(siteId, pageId, partial);\n return;\n }\n const db = getDb();\n if (!db) return;\n await setDoc(\n doc(db, \"sites\", siteId, \"drafts\", pageId),\n { ...zonderUndefined(partial), updatedAt: serverTimestamp() },\n { merge: true },\n );\n}\n\nexport async function deletePage(siteId: string, pageId: string) {\n const db = getDb();\n if (!db) return;\n // Draft direct (client mag drafts beheren); de gepubliceerde pagina via de Cloud Function —\n // die verwijdert óók de versions-subcollectie (Firestore cascadeert niet; directe client-deletes\n // lieten wees-versiegeschiedenis achter en zijn nu in de rules dichtgezet).\n await deleteDoc(doc(db, \"sites\", siteId, \"drafts\", pageId));\n const app = getFirebaseApp();\n if (!app) return;\n try {\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"deletePage\");\n await fn({ siteId, pageId });\n } catch {\n /* gepubliceerde versie bestond niet — negeren */\n }\n}\n\n/**\n * Herstelt een eerdere versie naar de WERKVERSIE (draft). De gebruiker moet daarna zelf\n * publiceren — zo is herstellen veilig en omkeerbaar (de live site verandert pas bij Publiceren).\n * Geeft de herstelde velden terug zodat de editor zijn lokale state kan bijwerken.\n */\nexport async function restoreVersion(\n siteId: string,\n pageId: string,\n versionId: string,\n): Promise<Partial<Page>> {\n const db = getDb();\n if (!db) throw new Error(\"Firebase niet geconfigureerd.\");\n const snap = await getDoc(doc(db, \"sites\", siteId, \"pages\", pageId, \"versions\", versionId));\n if (!snap.exists()) throw new Error(\"Versie niet gevonden.\");\n const s = (snap.data().snapshot ?? {}) as Record<string, unknown>;\n const restored: Partial<Page> = {\n title: s.title as string,\n slug: s.slug as string,\n path: s.path as string,\n seo: s.seo as Page[\"seo\"],\n blocks: s.blocks as Page[\"blocks\"],\n order: s.order as number,\n };\n await saveDraft(siteId, pageId, restored);\n return restored;\n}\n\n/** Publiceren via de Cloud Function (server-side validatie + versie-snapshot + audit). */\nexport async function publishPage(siteId: string, pageId: string): Promise<void> {\n // Demo-modus: er wordt niets echt gepubliceerd; de lokale bewerking is al zichtbaar.\n if (isDemoActive()) {\n saveDemoDraft(siteId, pageId, {});\n return;\n }\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"publishPage\");\n await fn({ siteId, pageId });\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/lib/pagesClient.ts"],"sourcesContent":["\"use client\";\n\nimport {\n addDoc,\n collection,\n deleteDoc,\n doc,\n getDoc,\n getDocs,\n limit,\n onSnapshot,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n where,\n} from \"firebase/firestore\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport type { Page } from \"../../schema/pages\";\nimport { isDemoActive, demoIdForPath, resolveDemoPage, saveDemoDraft } from \"../../demo/demoStore\";\nimport { sanitizeDraftBlocks } from \"./sanitizeDraft\";\n\nexport type DraftPage = Page & { id: string; published?: boolean };\n\n/** Live-lijst van pagina's (werkversies in `drafts/`). */\nexport function subscribePages(siteId: string, cb: (pages: DraftPage[]) => void): () => void {\n const db = getDb();\n if (!db) {\n cb([]);\n return () => {};\n }\n const q = query(collection(db, \"sites\", siteId, \"drafts\"), orderBy(\"order\", \"asc\"));\n return onSnapshot(q, (snap) => {\n cb(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<DraftPage, \"id\">) })));\n });\n}\n\nexport function subscribeVersions(\n siteId: string,\n pageId: string,\n cb: (versions: { id: string; createdAt?: unknown; kind?: string; label?: string }[]) => void,\n): () => void {\n const db = getDb();\n if (!db) {\n cb([]);\n return () => {};\n }\n const q = query(collection(db, \"sites\", siteId, \"pages\", pageId, \"versions\"), orderBy(\"createdAt\", \"desc\"));\n return onSnapshot(q, (snap) => cb(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Record<string, unknown>) }))));\n}\n\n/** Laadt de werkversie (draft) voor een pad — voor het inline bewerken op de live pagina. */\nexport async function getDraftByPath(siteId: string, path: string): Promise<DraftPage | null> {\n // Demo-modus: de werkversie komt uit de lokale browseropslag (seed + lokale bewerkingen).\n if (isDemoActive()) {\n const page = resolveDemoPage(siteId, path);\n return page ? ({ ...page, id: demoIdForPath(path) } as DraftPage) : null;\n }\n const db = getDb();\n if (!db) return null;\n const snap = await getDocs(\n query(collection(db, \"sites\", siteId, \"drafts\"), where(\"path\", \"==\", path), limit(1)),\n );\n const d = snap.docs[0];\n // Rich text uit een werkversie is nooit server-side gesaneerd (zie sanitizeDraft.ts).\n return d ? sanitizeDraftBlocks({ id: d.id, ...(d.data() as Omit<DraftPage, \"id\">) }) : null;\n}\n\n/**\n * Live-listener op één werkversie — voor conflictdetectie in de visuele editor (twee tabs of\n * twee editors tegelijk). `fromServer` is false voor de lokale echo van een eigen write\n * (hasPendingWrites), zodat de aanroeper eigen saves kan negeren.\n */\nexport function subscribeDraft(\n siteId: string,\n pageId: string,\n cb: (draft: DraftPage | null, meta: { fromServer: boolean }) => void,\n): () => void {\n if (isDemoActive()) return () => {};\n const db = getDb();\n if (!db) return () => {};\n return onSnapshot(doc(db, \"sites\", siteId, \"drafts\", pageId), (snap) => {\n cb(\n snap.exists() ? sanitizeDraftBlocks({ id: snap.id, ...(snap.data() as Omit<DraftPage, \"id\">) }) : null,\n { fromServer: !snap.metadata.hasPendingWrites },\n );\n });\n}\n\n/**\n * Firestore WEIGERT `undefined` als veldwaarde: `setDoc({ seo: undefined })` gooit\n * \"Unsupported field value: undefined\" en de hele schrijfactie mislukt. De editors geven hun\n * pagina-state als geheel door (`{ title, slug, path, seo, blocks }`), en bij een nieuwe pagina is\n * `seo` gewoon nog niet ingevuld — dus liep élke eerste publicatie van een nieuwe pagina stuk op\n * de autosave die eraan voorafgaat. Gevonden op de staging-deploy van groenhof: \"Publiceren\n * mislukt — Unsupported field value: undefined (found in field seo)\".\n *\n * Weglaten is hier ook de JUISTE semantiek: deze writes zijn `merge: true`, en \"veld niet\n * meegestuurd\" betekent daar \"laat staan wat er stond\". Een veld écht wissen doe je met\n * `deleteField()`, niet met `undefined` — dat onderscheid blijft zo intact.\n */\nfunction zonderUndefined<T extends Record<string, unknown>>(o: T): Partial<T> {\n return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined)) as Partial<T>;\n}\n\nexport async function createPage(siteId: string, input: Partial<Page> & { title: string; slug: string; path: string }) {\n const db = getDb();\n if (!db) throw new Error(\"Firebase niet geconfigureerd.\");\n const ref = await addDoc(collection(db, \"sites\", siteId, \"drafts\"), {\n parentId: null,\n order: Date.now(),\n status: \"draft\",\n blocks: [],\n published: false,\n ...zonderUndefined(input),\n updatedAt: serverTimestamp(),\n });\n return ref.id;\n}\n\nexport async function saveDraft(siteId: string, pageId: string, partial: Partial<Page>) {\n // Demo-modus: bewaar de bewerking alleen lokaal in de browser (niet in Firebase).\n if (isDemoActive()) {\n saveDemoDraft(siteId, pageId, partial);\n return;\n }\n const db = getDb();\n if (!db) return;\n await setDoc(\n doc(db, \"sites\", siteId, \"drafts\", pageId),\n { ...zonderUndefined(partial), updatedAt: serverTimestamp() },\n { merge: true },\n );\n}\n\nexport async function deletePage(siteId: string, pageId: string) {\n const db = getDb();\n if (!db) return;\n // Draft direct (client mag drafts beheren); de gepubliceerde pagina via de Cloud Function —\n // die verwijdert óók de versions-subcollectie (Firestore cascadeert niet; directe client-deletes\n // lieten wees-versiegeschiedenis achter en zijn nu in de rules dichtgezet).\n await deleteDoc(doc(db, \"sites\", siteId, \"drafts\", pageId));\n const app = getFirebaseApp();\n if (!app) return;\n try {\n const fn = callable(app, \"deletePage\");\n await fn({ siteId, pageId });\n } catch {\n /* gepubliceerde versie bestond niet — negeren */\n }\n}\n\n/**\n * Herstelt een eerdere versie naar de WERKVERSIE (draft). De gebruiker moet daarna zelf\n * publiceren — zo is herstellen veilig en omkeerbaar (de live site verandert pas bij Publiceren).\n * Geeft de herstelde velden terug zodat de editor zijn lokale state kan bijwerken.\n */\nexport async function restoreVersion(\n siteId: string,\n pageId: string,\n versionId: string,\n): Promise<Partial<Page>> {\n const db = getDb();\n if (!db) throw new Error(\"Firebase niet geconfigureerd.\");\n const snap = await getDoc(doc(db, \"sites\", siteId, \"pages\", pageId, \"versions\", versionId));\n if (!snap.exists()) throw new Error(\"Versie niet gevonden.\");\n const s = (snap.data().snapshot ?? {}) as Record<string, unknown>;\n const restored: Partial<Page> = {\n title: s.title as string,\n slug: s.slug as string,\n path: s.path as string,\n seo: s.seo as Page[\"seo\"],\n blocks: s.blocks as Page[\"blocks\"],\n order: s.order as number,\n };\n await saveDraft(siteId, pageId, restored);\n return restored;\n}\n\n/** Publiceren via de Cloud Function (server-side validatie + versie-snapshot + audit). */\nexport async function publishPage(siteId: string, pageId: string): Promise<void> {\n // Demo-modus: er wordt niets echt gepubliceerd; de lokale bewerking is al zichtbaar.\n if (isDemoActive()) {\n saveDemoDraft(siteId, pageId, {});\n return;\n }\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = callable(app, \"publishPage\");\n await fn({ siteId, pageId });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { collection, doc, getDoc, onSnapshot } from "firebase/firestore";
|
|
3
|
-
import {
|
|
3
|
+
import { callable } from "../../firebase/functionsClient.js";
|
|
4
4
|
import { getDb, getFirebaseApp } from "../../firebase/client.js";
|
|
5
|
-
import { functionsRegion } from "../../firebase/region.js";
|
|
6
5
|
import { SymbolSchema } from "../../schema/symbols.js";
|
|
7
6
|
function subscribeSymbols(siteId, cb) {
|
|
8
7
|
const db = getDb();
|
|
@@ -34,13 +33,13 @@ async function getSymbolOnce(siteId, symbolId) {
|
|
|
34
33
|
async function saveSymbol(input) {
|
|
35
34
|
const app = getFirebaseApp();
|
|
36
35
|
if (!app) throw new Error("Firebase niet geconfigureerd.");
|
|
37
|
-
const fn =
|
|
36
|
+
const fn = callable(app, "saveSymbol");
|
|
38
37
|
return (await fn(input)).data;
|
|
39
38
|
}
|
|
40
39
|
async function deleteSymbol(siteId, symbolId) {
|
|
41
40
|
const app = getFirebaseApp();
|
|
42
41
|
if (!app) throw new Error("Firebase niet geconfigureerd.");
|
|
43
|
-
await
|
|
42
|
+
await callable(app, "deleteSymbol")({ siteId, symbolId });
|
|
44
43
|
}
|
|
45
44
|
export {
|
|
46
45
|
deleteSymbol,
|
|
@@ -1 +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 {
|
|
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 { callable } from \"../../firebase/functionsClient\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\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 = callable<typeof input, { id: string; message?: string }>(app, \"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 callable(app, \"deleteSymbol\")({ siteId, symbolId });\n}\n"],"mappings":";AAGA,SAAS,YAAY,KAAK,QAAQ,kBAAkB;AACpD,SAAS,gBAAgB;AACzB,SAAS,OAAO,sBAAsB;AACtC,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,SAAyD,KAAK,YAAY;AACrF,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,SAAS,KAAK,cAAc,EAAE,EAAE,QAAQ,SAAS,CAAC;AAC1D;","names":[]}
|
|
@@ -72,14 +72,17 @@ function PagesPanel({ siteId }) {
|
|
|
72
72
|
onClick: async () => {
|
|
73
73
|
const ok = await confirm({
|
|
74
74
|
title: `Pagina "${current.title}" verwijderen?`,
|
|
75
|
-
description: "De pagina en de bijbehorende werkversie worden verwijderd.
|
|
75
|
+
description: "De pagina en de bijbehorende werkversie worden verwijderd. Dit kan niet ongedaan worden gemaakt. Van de live site verdwijnt hij zodra die opnieuw is gebouwd \u2014 meestal binnen enkele minuten.",
|
|
76
76
|
confirmLabel: "Verwijderen",
|
|
77
77
|
tone: "danger"
|
|
78
78
|
});
|
|
79
79
|
if (!ok) return;
|
|
80
80
|
await deletePage(siteId, current.id);
|
|
81
81
|
setSelected(null);
|
|
82
|
-
toast({
|
|
82
|
+
toast({
|
|
83
|
+
title: "Pagina verwijderd",
|
|
84
|
+
description: "De live site laat de pagina los zodra hij opnieuw is gebouwd (meestal enkele minuten)."
|
|
85
|
+
});
|
|
83
86
|
},
|
|
84
87
|
children: "Pagina verwijderen"
|
|
85
88
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/pages/PagesPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Button, Card, Field, Input, Badge } from \"../ui/primitives\";\nimport { Dialog, DialogTrigger, DialogContent, DialogClose } from \"../ui/dialog\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { subscribePages, createPage, deletePage, type DraftPage } from \"../lib/pagesClient\";\nimport { PageEditor } from \"./PageEditor\";\n\nfunction slugify(s: string): string {\n return s\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/(^-|-$)/g, \"\");\n}\n\nexport function PagesPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [pages, setPages] = useState<DraftPage[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [newTitle, setNewTitle] = useState(\"\");\n\n useEffect(() => subscribePages(siteId, setPages), [siteId]);\n useEffect(() => {\n if (!selected && pages.length) setSelected(pages[0].id);\n }, [pages, selected]);\n\n const current = pages.find((p) => p.id === selected) ?? null;\n\n async function create() {\n const title = newTitle.trim();\n if (!title) return;\n const slug = slugify(title) || \"pagina\";\n const path = slug === \"home\" ? \"/\" : `/${slug}`;\n try {\n const id = await createPage(siteId, { title, slug, path });\n setNewTitle(\"\");\n setSelected(id);\n toast({ title: \"Pagina aangemaakt\", description: path });\n } catch (e) {\n // De dialoog sluit sowieso (DialogClose) — zonder deze toast dacht de klant dat de pagina\n // bestond terwijl de write (rules/offline) faalde.\n toast({ title: \"Pagina aanmaken mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[260px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <span className=\"text-xs font-medium tracking-wide text-slate-500 uppercase\">Overzicht</span>\n <Dialog>\n <DialogTrigger asChild>\n <Button size=\"sm\">+ Nieuw</Button>\n </DialogTrigger>\n <DialogContent title=\"Nieuwe pagina\">\n <div className=\"grid gap-3\">\n <Field label=\"Titel\">\n <Input value={newTitle} onChange={(e) => setNewTitle(e.target.value)} placeholder=\"bv. Over ons\" />\n </Field>\n <div className=\"flex justify-end gap-2\">\n <DialogClose asChild>\n <Button variant=\"secondary\">Annuleren</Button>\n </DialogClose>\n <DialogClose asChild>\n <Button onClick={create}>Aanmaken</Button>\n </DialogClose>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n </div>\n <ul className=\"grid gap-1\">\n {pages.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n <span className=\"truncate\">{p.title}</span>\n {p.published ? <Badge tone=\"green\">live</Badge> : <Badge tone=\"amber\">concept</Badge>}\n </button>\n </li>\n ))}\n {/* Geen lege-lijst-tekst hier: de lege staat rechts zegt hetzelfde én noemt de volgende\n stap. Twee keer \"Nog geen pagina's\" naast elkaar leest als een fout. */}\n </ul>\n </Card>\n\n <div>\n {current ? (\n <>\n <PageEditor siteId={siteId} page={current} allPages={pages} />\n {/* Verwijderen staat ONDER de editor en draagt geen rode vlakknop meer. Het was het\n opvallendste element op het scherm — helderrood, bovenaan, bóven \"Publiceren\" —\n waarmee de onomkeerbare actie meer aandacht kreeg dan de actie waarvoor je komt.\n Een tekstknop in de foutkleur is duidelijk genoeg; de bevestiging vangt de rest. */}\n <div className=\"mt-4 flex justify-end border-t border-slate-200 pt-4\">\n <button\n type=\"button\"\n className=\"rounded px-2 py-1 text-sm text-red-600 hover:underline focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n onClick={async () => {\n const ok = await confirm({\n title: `Pagina \"${current.title}\" verwijderen?`,\n description:\n \"De pagina en de bijbehorende werkversie worden verwijderd.
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/pages/PagesPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Button, Card, Field, Input, Badge } from \"../ui/primitives\";\nimport { Dialog, DialogTrigger, DialogContent, DialogClose } from \"../ui/dialog\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { subscribePages, createPage, deletePage, type DraftPage } from \"../lib/pagesClient\";\nimport { PageEditor } from \"./PageEditor\";\n\nfunction slugify(s: string): string {\n return s\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/(^-|-$)/g, \"\");\n}\n\nexport function PagesPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [pages, setPages] = useState<DraftPage[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [newTitle, setNewTitle] = useState(\"\");\n\n useEffect(() => subscribePages(siteId, setPages), [siteId]);\n useEffect(() => {\n if (!selected && pages.length) setSelected(pages[0].id);\n }, [pages, selected]);\n\n const current = pages.find((p) => p.id === selected) ?? null;\n\n async function create() {\n const title = newTitle.trim();\n if (!title) return;\n const slug = slugify(title) || \"pagina\";\n const path = slug === \"home\" ? \"/\" : `/${slug}`;\n try {\n const id = await createPage(siteId, { title, slug, path });\n setNewTitle(\"\");\n setSelected(id);\n toast({ title: \"Pagina aangemaakt\", description: path });\n } catch (e) {\n // De dialoog sluit sowieso (DialogClose) — zonder deze toast dacht de klant dat de pagina\n // bestond terwijl de write (rules/offline) faalde.\n toast({ title: \"Pagina aanmaken mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[260px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <span className=\"text-xs font-medium tracking-wide text-slate-500 uppercase\">Overzicht</span>\n <Dialog>\n <DialogTrigger asChild>\n <Button size=\"sm\">+ Nieuw</Button>\n </DialogTrigger>\n <DialogContent title=\"Nieuwe pagina\">\n <div className=\"grid gap-3\">\n <Field label=\"Titel\">\n <Input value={newTitle} onChange={(e) => setNewTitle(e.target.value)} placeholder=\"bv. Over ons\" />\n </Field>\n <div className=\"flex justify-end gap-2\">\n <DialogClose asChild>\n <Button variant=\"secondary\">Annuleren</Button>\n </DialogClose>\n <DialogClose asChild>\n <Button onClick={create}>Aanmaken</Button>\n </DialogClose>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n </div>\n <ul className=\"grid gap-1\">\n {pages.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n <span className=\"truncate\">{p.title}</span>\n {p.published ? <Badge tone=\"green\">live</Badge> : <Badge tone=\"amber\">concept</Badge>}\n </button>\n </li>\n ))}\n {/* Geen lege-lijst-tekst hier: de lege staat rechts zegt hetzelfde én noemt de volgende\n stap. Twee keer \"Nog geen pagina's\" naast elkaar leest als een fout. */}\n </ul>\n </Card>\n\n <div>\n {current ? (\n <>\n <PageEditor siteId={siteId} page={current} allPages={pages} />\n {/* Verwijderen staat ONDER de editor en draagt geen rode vlakknop meer. Het was het\n opvallendste element op het scherm — helderrood, bovenaan, bóven \"Publiceren\" —\n waarmee de onomkeerbare actie meer aandacht kreeg dan de actie waarvoor je komt.\n Een tekstknop in de foutkleur is duidelijk genoeg; de bevestiging vangt de rest. */}\n <div className=\"mt-4 flex justify-end border-t border-slate-200 pt-4\">\n <button\n type=\"button\"\n className=\"rounded px-2 py-1 text-sm text-red-600 hover:underline focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n onClick={async () => {\n const ok = await confirm({\n title: `Pagina \"${current.title}\" verwijderen?`,\n description:\n \"De pagina en de bijbehorende werkversie worden verwijderd. Dit kan niet ongedaan worden gemaakt. \" +\n \"Van de live site verdwijnt hij zodra die opnieuw is gebouwd — meestal binnen enkele minuten.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n await deletePage(siteId, current.id);\n setSelected(null);\n // Eerlijk over de vertraging: de pagina is wég uit het beheer, maar de live site\n // laat hem pas los na een rebuild. Beloven dat hij \"verdwenen\" is terwijl de URL\n // nog werkt, kost meer vertrouwen dan het kost om het te zeggen.\n toast({\n title: \"Pagina verwijderd\",\n description: \"De live site laat de pagina los zodra hij opnieuw is gebouwd (meestal enkele minuten).\",\n });\n }}\n >\n Pagina verwijderen\n </button>\n </div>\n </>\n ) : (\n <Card className=\"grid place-items-center py-16 text-center\">\n <div className=\"max-w-sm\">\n {/* Een lege lijst is een uitnodiging, geen mededeling: zeg wat de volgende stap is. */}\n <p className=\"text-sm font-medium text-slate-700\">\n {pages.length === 0 ? \"Nog geen pagina's\" : \"Geen pagina geselecteerd\"}\n </p>\n <p className=\"mt-1 text-sm text-slate-500\">\n {pages.length === 0\n ? \"Maak je eerste pagina met + Nieuw. Wat je maakt blijft een concept tot je publiceert.\"\n : \"Kies links een pagina om de tekst en secties aan te passen.\"}\n </p>\n </div>\n </Card>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useState } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { callable } from "../../firebase/functionsClient.js";
|
|
5
5
|
import { getFirebaseApp } from "../../firebase/client.js";
|
|
6
|
-
import { functionsRegion } from "../../firebase/region.js";
|
|
7
6
|
import { Badge, Button, Card, Field, Input } from "../ui/primitives.js";
|
|
8
7
|
import { Switch } from "../ui/switch.js";
|
|
9
8
|
import { useToast } from "../ui/toast.js";
|
|
@@ -11,7 +10,7 @@ function callFunction(name) {
|
|
|
11
10
|
return async (data) => {
|
|
12
11
|
const app = getFirebaseApp();
|
|
13
12
|
if (!app) throw new Error("Firebase niet beschikbaar.");
|
|
14
|
-
const fn =
|
|
13
|
+
const fn = callable(app, name);
|
|
15
14
|
const res = await fn(data);
|
|
16
15
|
return res.data;
|
|
17
16
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/settings/MailSettings.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { Badge, Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\n\ntype MailProvider = \"resend\" | \"postmark\" | \"smtp\";\n\ninterface MailStatus {\n provider?: MailProvider;\n configured: boolean;\n host?: string;\n port?: number;\n secure?: boolean;\n user?: string;\n fromName?: string;\n fromEmail?: string;\n notifyTo?: string;\n hasResendKey?: boolean;\n hasPostmarkToken?: boolean;\n hasSmtpPass?: boolean;\n}\n\n/**\n * Generieke callable-helper (niet shop-gebonden): roept een functie op naam aan via de\n * client-Firebase. Bewust hier lokaal zodat de algemene instellingen niet van de opt-in\n * shop-module afhangen — mail is een site-brede instelling (zie docs/SHARED-SETTINGS.md).\n */\nfunction callFunction<TIn, TOut>(name: string) {\n return async (data: TIn): Promise<TOut> => {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet beschikbaar.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), name);\n const res = await fn(data as Record<string, unknown>);\n return res.data as TOut;\n };\n}\n\n/** Segmented control voor providerkeuze. */\nfunction Segmented<T extends string>({\n value,\n options,\n onChange,\n}: {\n value: T;\n options: { value: T; label: string }[];\n onChange: (v: T) => void;\n}) {\n return (\n <div className=\"inline-flex rounded-lg border border-slate-200 bg-slate-50 p-0.5\">\n {options.map((o) => (\n <button\n key={o.value}\n type=\"button\"\n onClick={() => onChange(o.value)}\n className={`rounded-md px-3 py-1.5 text-sm font-medium transition ${\n value === o.value ? \"bg-white text-brand-700 shadow-sm\" : \"text-slate-500 hover:text-slate-700\"\n }`}\n >\n {o.label}\n </button>\n ))}\n </div>\n );\n}\n\nfunction mailProviderLabel(p: MailProvider): string {\n return p === \"smtp\" ? \"Eigen SMTP\" : p === \"resend\" ? \"Resend\" : \"Postmark\";\n}\n\n/**\n * Site-brede mailprovider-configuratie (Resend / Postmark / eigen SMTP) + afzender-identiteit.\n * Deze instelling wordt door MEERDERE modules gebruikt (formulier-notificaties én de shop), dus\n * hoort site-breed thuis in de algemene instellingen — niet in een module-paneel. Geheimen gaan\n * uitsluitend via callables naar `sites/{siteId}/private/mail` (deny-all rules); dit paneel toont\n * alleen gemaskeerde status. Alleen een siteadmin kan opslaan.\n */\nexport function MailSettings({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [status, setStatus] = useState<MailStatus | null>(null);\n const [busy, setBusy] = useState(false);\n\n const [mailProvider, setMailProvider] = useState<MailProvider>(\"smtp\");\n const [resendKey, setResendKey] = useState(\"\");\n const [postmarkToken, setPostmarkToken] = useState(\"\");\n const [smtp, setSmtp] = useState({ host: \"\", port: \"465\", secure: true, user: \"\", pass: \"\" });\n const [fromName, setFromName] = useState(\"\");\n const [fromEmail, setFromEmail] = useState(\"\");\n const [notifyTo, setNotifyTo] = useState(\"\");\n\n const refresh = useCallback(async () => {\n try {\n const m = await callFunction<{ siteId: string }, MailStatus>(\"getMailStatus\")({ siteId });\n setStatus(m);\n setMailProvider(m.provider ?? (m.host ? \"smtp\" : m.hasPostmarkToken ? \"postmark\" : m.hasResendKey ? \"resend\" : \"smtp\"));\n setSmtp((p) => ({ ...p, host: m.host ?? p.host, port: String(m.port ?? 465), secure: m.secure !== false, user: m.user ?? p.user }));\n setFromName(m.fromName ?? \"\");\n setFromEmail(m.fromEmail ?? \"\");\n setNotifyTo(m.notifyTo ?? \"\");\n } catch {\n setStatus(null);\n }\n }, [siteId]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n async function saveMail() {\n setBusy(true);\n try {\n const mail: Record<string, unknown> = { provider: mailProvider, fromName, fromEmail, notifyTo };\n if (mailProvider === \"resend\") mail.resendApiKey = resendKey.trim() || undefined;\n if (mailProvider === \"postmark\") mail.postmarkToken = postmarkToken.trim() || undefined;\n if (mailProvider === \"smtp\") {\n mail.host = smtp.host.trim();\n mail.port = Number(smtp.port) || 465;\n mail.secure = smtp.secure;\n mail.user = smtp.user.trim();\n if (smtp.pass) mail.pass = smtp.pass;\n }\n await callFunction<{ siteId: string; mail: Record<string, unknown> }, { ok: boolean }>(\"saveMailSettings\")({ siteId, mail });\n toast({ title: \"E-mailinstellingen opgeslagen\", tone: \"success\" });\n setResendKey(\"\");\n setPostmarkToken(\"\");\n setSmtp((p) => ({ ...p, pass: \"\" }));\n await refresh();\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function sendTest() {\n setBusy(true);\n try {\n // `sendShopTestMail` is een generieke testverzending (naam ongewijzigd voor compat).\n const res = await callFunction<{ siteId: string }, { to: string }>(\"sendShopTestMail\")({ siteId });\n toast({ title: \"Testmail verstuurd\", description: `Naar ${res.to} — check de inbox (en spam).`, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Testmail mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div>\n <h3 className=\"text-base font-semibold\">E-mail (formulieren & shop)</h3>\n <p className=\"text-xs text-slate-500\">\n De provider waarmee deze site e-mails verstuurt — formulier-notificaties én\n bestelbevestigingen. Een provider verstuurt alleen vanaf een geverifieerd domein.\n </p>\n </div>\n {status && (\n <Badge tone={status.configured ? \"green\" : \"amber\"}>\n {status.configured ? `${mailProviderLabel(mailProvider)} actief` : \"Nog niet ingesteld\"}\n </Badge>\n )}\n </div>\n\n <Segmented\n value={mailProvider}\n onChange={setMailProvider}\n options={[\n { value: \"smtp\", label: \"Eigen SMTP\" },\n { value: \"resend\", label: \"Resend\" },\n { value: \"postmark\", label: \"Postmark\" },\n ]}\n />\n\n {mailProvider === \"smtp\" && (\n <div className=\"grid gap-3\">\n <p className=\"text-sm text-slate-600\">\n Via je eigen mailbox: bv. het adres bij je hostingpartij, of Gmail (smtp.gmail.com, poort 465, met een{\" \"}\n <a className=\"underline\" href=\"https://myaccount.google.com/apppasswords\" target=\"_blank\" rel=\"noreferrer\">app-wachtwoord</a>).\n </p>\n <div className=\"grid gap-3 md:grid-cols-[1fr_110px]\">\n <Field label=\"SMTP-server\"><Input value={smtp.host} onChange={(e) => setSmtp((p) => ({ ...p, host: e.target.value }))} placeholder=\"smtp.gmail.com\" /></Field>\n <Field label=\"Poort\"><Input value={smtp.port} onChange={(e) => setSmtp((p) => ({ ...p, port: e.target.value }))} placeholder=\"465\" /></Field>\n </div>\n <Switch checked={smtp.secure} onCheckedChange={(v) => setSmtp((p) => ({ ...p, secure: v }))} label=\"SSL (poort 465). Uit = STARTTLS (poort 587)\" />\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Gebruikersnaam\"><Input value={smtp.user} onChange={(e) => setSmtp((p) => ({ ...p, user: e.target.value }))} placeholder=\"info@jouwdomein.nl\" /></Field>\n <Field label=\"Wachtwoord\" hint={status?.hasSmtpPass ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={smtp.pass} onChange={(e) => setSmtp((p) => ({ ...p, pass: e.target.value }))} />\n </Field>\n </div>\n </div>\n )}\n\n {mailProvider === \"resend\" && (\n <div className=\"grid gap-3\">\n <p className=\"text-sm text-slate-600\">\n <a className=\"underline\" href=\"https://resend.com\" target=\"_blank\" rel=\"noreferrer\">Resend</a> verstuurt vanaf een geverifieerd domein.\n Maak een API-sleutel (re_…) en verifieer je afzenderdomein in het Resend-dashboard.\n </p>\n <Field label=\"Resend API-sleutel (re_…)\" hint={status?.hasResendKey ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={resendKey} onChange={(e) => setResendKey(e.target.value)} placeholder=\"re_…\" />\n </Field>\n </div>\n )}\n\n {mailProvider === \"postmark\" && (\n <div className=\"grid gap-3\">\n <p className=\"text-sm text-slate-600\">\n <a className=\"underline\" href=\"https://postmarkapp.com\" target=\"_blank\" rel=\"noreferrer\">Postmark</a> verstuurt vanaf een geverifieerde afzender/domein (Sender Signature).\n Kopieer het Server-token.\n </p>\n <Field label=\"Postmark server-token\" hint={status?.hasPostmarkToken ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={postmarkToken} onChange={(e) => setPostmarkToken(e.target.value)} placeholder=\"xxxxxxxx-xxxx-…\" />\n </Field>\n </div>\n )}\n\n {/* Gedeelde afzender-velden */}\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzendernaam\"><Input value={fromName} onChange={(e) => setFromName(e.target.value)} placeholder=\"Mijn Site\" /></Field>\n <Field label=\"Afzenderadres\"><Input value={fromEmail} onChange={(e) => setFromEmail(e.target.value)} placeholder=\"info@jouwdomein.nl\" /></Field>\n </div>\n <Field label=\"Notificatie-adres\" hint=\"Hier ontvang je zelf een mail bij elk formulier/elke bestelling; ook het doel van de testmail\">\n <Input value={notifyTo} onChange={(e) => setNotifyTo(e.target.value)} placeholder=\"notificaties@jouwdomein.nl\" />\n </Field>\n\n <div className=\"flex items-center gap-2\">\n <Button onClick={saveMail} disabled={busy}>Opslaan</Button>\n <Button variant=\"secondary\" onClick={sendTest} disabled={busy || !status?.configured}>Testmail versturen</Button>\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/settings/MailSettings.tsx"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { getFirebaseApp } from \"../../firebase/client\";\nimport { Badge, Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\n\ntype MailProvider = \"resend\" | \"postmark\" | \"smtp\";\n\ninterface MailStatus {\n provider?: MailProvider;\n configured: boolean;\n host?: string;\n port?: number;\n secure?: boolean;\n user?: string;\n fromName?: string;\n fromEmail?: string;\n notifyTo?: string;\n hasResendKey?: boolean;\n hasPostmarkToken?: boolean;\n hasSmtpPass?: boolean;\n}\n\n/**\n * Generieke callable-helper (niet shop-gebonden): roept een functie op naam aan via de\n * client-Firebase. Bewust hier lokaal zodat de algemene instellingen niet van de opt-in\n * shop-module afhangen — mail is een site-brede instelling (zie docs/SHARED-SETTINGS.md).\n */\nfunction callFunction<TIn, TOut>(name: string) {\n return async (data: TIn): Promise<TOut> => {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet beschikbaar.\");\n const fn = callable(app, name);\n const res = await fn(data as Record<string, unknown>);\n return res.data as TOut;\n };\n}\n\n/** Segmented control voor providerkeuze. */\nfunction Segmented<T extends string>({\n value,\n options,\n onChange,\n}: {\n value: T;\n options: { value: T; label: string }[];\n onChange: (v: T) => void;\n}) {\n return (\n <div className=\"inline-flex rounded-lg border border-slate-200 bg-slate-50 p-0.5\">\n {options.map((o) => (\n <button\n key={o.value}\n type=\"button\"\n onClick={() => onChange(o.value)}\n className={`rounded-md px-3 py-1.5 text-sm font-medium transition ${\n value === o.value ? \"bg-white text-brand-700 shadow-sm\" : \"text-slate-500 hover:text-slate-700\"\n }`}\n >\n {o.label}\n </button>\n ))}\n </div>\n );\n}\n\nfunction mailProviderLabel(p: MailProvider): string {\n return p === \"smtp\" ? \"Eigen SMTP\" : p === \"resend\" ? \"Resend\" : \"Postmark\";\n}\n\n/**\n * Site-brede mailprovider-configuratie (Resend / Postmark / eigen SMTP) + afzender-identiteit.\n * Deze instelling wordt door MEERDERE modules gebruikt (formulier-notificaties én de shop), dus\n * hoort site-breed thuis in de algemene instellingen — niet in een module-paneel. Geheimen gaan\n * uitsluitend via callables naar `sites/{siteId}/private/mail` (deny-all rules); dit paneel toont\n * alleen gemaskeerde status. Alleen een siteadmin kan opslaan.\n */\nexport function MailSettings({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [status, setStatus] = useState<MailStatus | null>(null);\n const [busy, setBusy] = useState(false);\n\n const [mailProvider, setMailProvider] = useState<MailProvider>(\"smtp\");\n const [resendKey, setResendKey] = useState(\"\");\n const [postmarkToken, setPostmarkToken] = useState(\"\");\n const [smtp, setSmtp] = useState({ host: \"\", port: \"465\", secure: true, user: \"\", pass: \"\" });\n const [fromName, setFromName] = useState(\"\");\n const [fromEmail, setFromEmail] = useState(\"\");\n const [notifyTo, setNotifyTo] = useState(\"\");\n\n const refresh = useCallback(async () => {\n try {\n const m = await callFunction<{ siteId: string }, MailStatus>(\"getMailStatus\")({ siteId });\n setStatus(m);\n setMailProvider(m.provider ?? (m.host ? \"smtp\" : m.hasPostmarkToken ? \"postmark\" : m.hasResendKey ? \"resend\" : \"smtp\"));\n setSmtp((p) => ({ ...p, host: m.host ?? p.host, port: String(m.port ?? 465), secure: m.secure !== false, user: m.user ?? p.user }));\n setFromName(m.fromName ?? \"\");\n setFromEmail(m.fromEmail ?? \"\");\n setNotifyTo(m.notifyTo ?? \"\");\n } catch {\n setStatus(null);\n }\n }, [siteId]);\n\n useEffect(() => {\n void refresh();\n }, [refresh]);\n\n async function saveMail() {\n setBusy(true);\n try {\n const mail: Record<string, unknown> = { provider: mailProvider, fromName, fromEmail, notifyTo };\n if (mailProvider === \"resend\") mail.resendApiKey = resendKey.trim() || undefined;\n if (mailProvider === \"postmark\") mail.postmarkToken = postmarkToken.trim() || undefined;\n if (mailProvider === \"smtp\") {\n mail.host = smtp.host.trim();\n mail.port = Number(smtp.port) || 465;\n mail.secure = smtp.secure;\n mail.user = smtp.user.trim();\n if (smtp.pass) mail.pass = smtp.pass;\n }\n await callFunction<{ siteId: string; mail: Record<string, unknown> }, { ok: boolean }>(\"saveMailSettings\")({ siteId, mail });\n toast({ title: \"E-mailinstellingen opgeslagen\", tone: \"success\" });\n setResendKey(\"\");\n setPostmarkToken(\"\");\n setSmtp((p) => ({ ...p, pass: \"\" }));\n await refresh();\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function sendTest() {\n setBusy(true);\n try {\n // `sendShopTestMail` is een generieke testverzending (naam ongewijzigd voor compat).\n const res = await callFunction<{ siteId: string }, { to: string }>(\"sendShopTestMail\")({ siteId });\n toast({ title: \"Testmail verstuurd\", description: `Naar ${res.to} — check de inbox (en spam).`, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Testmail mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <div>\n <h3 className=\"text-base font-semibold\">E-mail (formulieren & shop)</h3>\n <p className=\"text-xs text-slate-500\">\n De provider waarmee deze site e-mails verstuurt — formulier-notificaties én\n bestelbevestigingen. Een provider verstuurt alleen vanaf een geverifieerd domein.\n </p>\n </div>\n {status && (\n <Badge tone={status.configured ? \"green\" : \"amber\"}>\n {status.configured ? `${mailProviderLabel(mailProvider)} actief` : \"Nog niet ingesteld\"}\n </Badge>\n )}\n </div>\n\n <Segmented\n value={mailProvider}\n onChange={setMailProvider}\n options={[\n { value: \"smtp\", label: \"Eigen SMTP\" },\n { value: \"resend\", label: \"Resend\" },\n { value: \"postmark\", label: \"Postmark\" },\n ]}\n />\n\n {mailProvider === \"smtp\" && (\n <div className=\"grid gap-3\">\n <p className=\"text-sm text-slate-600\">\n Via je eigen mailbox: bv. het adres bij je hostingpartij, of Gmail (smtp.gmail.com, poort 465, met een{\" \"}\n <a className=\"underline\" href=\"https://myaccount.google.com/apppasswords\" target=\"_blank\" rel=\"noreferrer\">app-wachtwoord</a>).\n </p>\n <div className=\"grid gap-3 md:grid-cols-[1fr_110px]\">\n <Field label=\"SMTP-server\"><Input value={smtp.host} onChange={(e) => setSmtp((p) => ({ ...p, host: e.target.value }))} placeholder=\"smtp.gmail.com\" /></Field>\n <Field label=\"Poort\"><Input value={smtp.port} onChange={(e) => setSmtp((p) => ({ ...p, port: e.target.value }))} placeholder=\"465\" /></Field>\n </div>\n <Switch checked={smtp.secure} onCheckedChange={(v) => setSmtp((p) => ({ ...p, secure: v }))} label=\"SSL (poort 465). Uit = STARTTLS (poort 587)\" />\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Gebruikersnaam\"><Input value={smtp.user} onChange={(e) => setSmtp((p) => ({ ...p, user: e.target.value }))} placeholder=\"info@jouwdomein.nl\" /></Field>\n <Field label=\"Wachtwoord\" hint={status?.hasSmtpPass ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={smtp.pass} onChange={(e) => setSmtp((p) => ({ ...p, pass: e.target.value }))} />\n </Field>\n </div>\n </div>\n )}\n\n {mailProvider === \"resend\" && (\n <div className=\"grid gap-3\">\n <p className=\"text-sm text-slate-600\">\n <a className=\"underline\" href=\"https://resend.com\" target=\"_blank\" rel=\"noreferrer\">Resend</a> verstuurt vanaf een geverifieerd domein.\n Maak een API-sleutel (re_…) en verifieer je afzenderdomein in het Resend-dashboard.\n </p>\n <Field label=\"Resend API-sleutel (re_…)\" hint={status?.hasResendKey ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={resendKey} onChange={(e) => setResendKey(e.target.value)} placeholder=\"re_…\" />\n </Field>\n </div>\n )}\n\n {mailProvider === \"postmark\" && (\n <div className=\"grid gap-3\">\n <p className=\"text-sm text-slate-600\">\n <a className=\"underline\" href=\"https://postmarkapp.com\" target=\"_blank\" rel=\"noreferrer\">Postmark</a> verstuurt vanaf een geverifieerde afzender/domein (Sender Signature).\n Kopieer het Server-token.\n </p>\n <Field label=\"Postmark server-token\" hint={status?.hasPostmarkToken ? \"Leeg laten = ongewijzigd\" : undefined}>\n <Input type=\"password\" value={postmarkToken} onChange={(e) => setPostmarkToken(e.target.value)} placeholder=\"xxxxxxxx-xxxx-…\" />\n </Field>\n </div>\n )}\n\n {/* Gedeelde afzender-velden */}\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzendernaam\"><Input value={fromName} onChange={(e) => setFromName(e.target.value)} placeholder=\"Mijn Site\" /></Field>\n <Field label=\"Afzenderadres\"><Input value={fromEmail} onChange={(e) => setFromEmail(e.target.value)} placeholder=\"info@jouwdomein.nl\" /></Field>\n </div>\n <Field label=\"Notificatie-adres\" hint=\"Hier ontvang je zelf een mail bij elk formulier/elke bestelling; ook het doel van de testmail\">\n <Input value={notifyTo} onChange={(e) => setNotifyTo(e.target.value)} placeholder=\"notificaties@jouwdomein.nl\" />\n </Field>\n\n <div className=\"flex items-center gap-2\">\n <Button onClick={saveMail} disabled={busy}>Opslaan</Button>\n <Button variant=\"secondary\" onClick={sendTest} disabled={busy || !status?.configured}>Testmail versturen</Button>\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import { doc, getDoc, setDoc } from "firebase/firestore";
|
|
5
|
-
import {
|
|
5
|
+
import { callable } from "../../firebase/functionsClient.js";
|
|
6
6
|
import { getDb, getFirebaseApp } from "../../firebase/client.js";
|
|
7
|
-
import { functionsRegion } from "../../firebase/region.js";
|
|
8
7
|
import { MAIL_TEMPLATE_DEFS } from "../../schema/mail.js";
|
|
9
8
|
import { Button, Card, Field, Input, Textarea } from "../ui/primitives.js";
|
|
10
9
|
import { useToast } from "../ui/toast.js";
|
|
@@ -12,7 +11,7 @@ function callFunction(name) {
|
|
|
12
11
|
return async (data) => {
|
|
13
12
|
const app = getFirebaseApp();
|
|
14
13
|
if (!app) throw new Error("Firebase niet beschikbaar.");
|
|
15
|
-
const fn =
|
|
14
|
+
const fn = callable(app, name);
|
|
16
15
|
const res = await fn(data);
|
|
17
16
|
return res.data;
|
|
18
17
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/settings/MailTemplates.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { doc, getDoc, setDoc } from \"firebase/firestore\";\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/settings/MailTemplates.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { doc, getDoc, setDoc } from \"firebase/firestore\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { MAIL_TEMPLATE_DEFS, type MailTemplateKey, type MailTemplates as MailTemplatesData } from \"../../schema/mail\";\nimport { Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\n\ntype MailTemplatesProps = {\n siteId: string;\n /** Welke templates tonen — bv. FORM_TEMPLATE_KEYS in Formulieren, SHOP_TEMPLATE_KEYS in de shop. */\n keys?: readonly MailTemplateKey[];\n};\n\n/** Generieke callable-helper (niet shop-gebonden) — mail is een site-brede instelling. */\nfunction callFunction<TIn, TOut>(name: string) {\n return async (data: TIn): Promise<TOut> => {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet beschikbaar.\");\n const fn = callable(app, name);\n const res = await fn(data as Record<string, unknown>);\n return res.data as TOut;\n };\n}\n\n/**\n * E-mailteksten — bewerkbare templates in `settings/mailTemplates` (site-breed, siteadmin). Een\n * leeg veld gebruikt de ingebouwde standaardtekst, dus er gaat altijd een correcte mail uit. Je\n * bewerkt platte tekst met {{invulvelden}}; de engine verpakt die in je huisstijl (kleur, logo,\n * footer) en stuurt een nette HTML-mail. Shop-templates verschijnen alleen op sites met webwinkel.\n */\nexport function MailTemplates({ siteId, keys }: MailTemplatesProps) {\n const { toast } = useToast();\n const [templates, setTemplates] = useState<MailTemplatesData | null>(null);\n const [busy, setBusy] = useState<string | null>(null);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n void getDoc(doc(db, \"sites\", siteId, \"settings\", \"mailTemplates\")).then((snap) =>\n setTemplates((snap.data() as MailTemplatesData | undefined) ?? {}),\n );\n }, [siteId]);\n\n async function save() {\n const db = getDb();\n if (!db || !templates) return;\n setBusy(\"save\");\n try {\n await setDoc(doc(db, \"sites\", siteId, \"settings\", \"mailTemplates\"), templates);\n toast({ title: \"E-mailteksten opgeslagen\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(null);\n }\n }\n\n async function preview(key: MailTemplateKey) {\n setBusy(key);\n try {\n const res = await callFunction<{ siteId: string; template: MailTemplateKey }, { to: string }>(\"sendTemplatePreview\")({\n siteId,\n template: key,\n });\n toast({ title: \"Voorbeeld verstuurd\", description: `Naar ${res.to} (met voorbeeldgegevens en je huisstijl).`, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Voorbeeld mislukt\", description: e instanceof Error ? e.message : undefined, tone: \"error\" });\n } finally {\n setBusy(null);\n }\n }\n\n if (!templates) return <p className=\"text-sm text-slate-500\">E-mailteksten laden…</p>;\n\n const defs = keys ? MAIL_TEMPLATE_DEFS.filter((d) => keys.includes(d.key as MailTemplateKey)) : MAIL_TEMPLATE_DEFS;\n\n return (\n <div className=\"grid gap-4\">\n <p className=\"text-sm text-slate-600\">\n Laat een veld leeg voor de standaardtekst. De opmaak (huisstijlkleur, logo, footer) zet de engine er\n automatisch omheen — je bewerkt alleen de tekst. Gebruik de {\"{{invulvelden}}\"} die per e-mail staan vermeld.\n </p>\n\n {defs.map((def) => {\n const key = def.key as MailTemplateKey;\n const tpl = templates[key] ?? {};\n const set = (patch: { subject?: string; bodyText?: string }) =>\n setTemplates((p) => ({ ...p, [key]: { ...p?.[key], ...patch } }));\n return (\n <Card key={key}>\n <div className=\"mb-2 flex items-start justify-between gap-3\">\n <div>\n <h4 className=\"text-sm font-semibold\">{def.label}</h4>\n <p className=\"text-xs text-slate-500\">{def.hint}</p>\n </div>\n <Button size=\"sm\" variant=\"secondary\" disabled={busy !== null} onClick={() => preview(key)}>\n {busy === key ? \"Versturen…\" : \"Stuur voorbeeld naar mij\"}\n </Button>\n </div>\n <div className=\"grid gap-3\">\n <Field label=\"Onderwerp\" hint=\"Leeg = standaard\">\n <Input value={tpl.subject ?? \"\"} onChange={(e) => set({ subject: e.target.value })} />\n </Field>\n <Field label=\"Tekst\" hint=\"Leeg = standaard\">\n <Textarea rows={6} value={tpl.bodyText ?? \"\"} onChange={(e) => set({ bodyText: e.target.value })} />\n </Field>\n <p className=\"text-xs text-slate-500\">\n Invulvelden:{\" \"}\n <code className=\"rounded bg-slate-100 px-1 py-0.5 text-[11px]\">{def.placeholders}</code>\n </p>\n </div>\n </Card>\n );\n })}\n\n <div>\n <Button onClick={save} disabled={busy !== null}>\n {busy === \"save\" ? \"Opslaan…\" : \"E-mailteksten opslaan\"}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|