@xiwebdesign/xicms-core 7.23.0 → 7.25.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/admin/AdminApp.js +2 -1
- package/dist/admin/AdminApp.js.map +1 -1
- package/dist/admin/CoreVersion.d.ts +6 -0
- package/dist/admin/CoreVersion.js +42 -1
- package/dist/admin/CoreVersion.js.map +1 -1
- package/dist/admin/SuperadminPanel.js +19 -3
- package/dist/admin/SuperadminPanel.js.map +1 -1
- package/dist/admin/dashboard/DashboardPanel.js +46 -9
- package/dist/admin/dashboard/DashboardPanel.js.map +1 -1
- package/dist/admin/forms/FormsPanel.d.ts +9 -0
- package/dist/admin/forms/FormsPanel.js +102 -16
- package/dist/admin/forms/FormsPanel.js.map +1 -1
- package/dist/admin/forms/gezien.d.ts +3 -0
- package/dist/admin/forms/gezien.js +22 -0
- package/dist/admin/forms/gezien.js.map +1 -0
- package/dist/admin/lib/kleur.d.ts +32 -0
- package/dist/admin/lib/kleur.js +60 -0
- package/dist/admin/lib/kleur.js.map +1 -0
- package/dist/admin/lib/pagesClient.d.ts +32 -0
- package/dist/admin/lib/pagesClient.js +60 -1
- package/dist/admin/lib/pagesClient.js.map +1 -1
- package/dist/admin/lib/seoHulp.d.ts +28 -0
- package/dist/admin/lib/seoHulp.js +33 -0
- package/dist/admin/lib/seoHulp.js.map +1 -0
- package/dist/admin/media/MediaManager.js +133 -51
- package/dist/admin/media/MediaManager.js.map +1 -1
- package/dist/admin/pages/PageEditor.d.ts +14 -0
- package/dist/admin/pages/PageEditor.js +192 -123
- package/dist/admin/pages/PageEditor.js.map +1 -1
- package/dist/admin/pages/PagesPanel.js +88 -40
- package/dist/admin/pages/PagesPanel.js.map +1 -1
- package/dist/admin/settings/SettingsPanel.js +100 -19
- package/dist/admin/settings/SettingsPanel.js.map +1 -1
- package/dist/admin/shop/OrdersTab.js +171 -3
- package/dist/admin/shop/OrdersTab.js.map +1 -1
- package/dist/admin/shop/ProductsTab.js +1 -1
- package/dist/admin/shop/ProductsTab.js.map +1 -1
- package/dist/admin/shop/ShopPanel.js +5 -2
- package/dist/admin/shop/ShopPanel.js.map +1 -1
- package/dist/admin/stats/StatsPanel.js +30 -2
- package/dist/admin/stats/StatsPanel.js.map +1 -1
- package/dist/admin/stats/lib.d.ts +14 -1
- package/dist/admin/stats/lib.js +46 -2
- package/dist/admin/stats/lib.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +80 -4
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/blocks/builtins/FormBlockRender.js +8 -2
- package/dist/blocks/builtins/FormBlockRender.js.map +1 -1
- package/dist/blocks/builtins/lib/submitForm.d.ts +3 -1
- package/dist/blocks/builtins/lib/submitForm.js +2 -2
- package/dist/blocks/builtins/lib/submitForm.js.map +1 -1
- package/dist/components/EditBar.d.ts +3 -1
- package/dist/components/EditBar.js +30 -4
- package/dist/components/EditBar.js.map +1 -1
- package/dist/components/News.js +8 -4
- package/dist/components/News.js.map +1 -1
- package/dist/components/PageView.d.ts +5 -0
- package/dist/components/PageView.js +24 -2
- package/dist/components/PageView.js.map +1 -1
- package/dist/components/ResponsiveImage.d.ts +1 -1
- package/dist/components/ResponsiveImage.js +3 -1
- package/dist/components/ResponsiveImage.js.map +1 -1
- package/dist/components/site/SiteNav.d.ts +29 -0
- package/dist/components/site/SiteNav.js +82 -0
- package/dist/components/site/SiteNav.js.map +1 -0
- package/dist/content/types.d.ts +8 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/media/ImageContext.d.ts +1 -6
- package/dist/media/ImageContext.js +6 -1
- package/dist/media/ImageContext.js.map +1 -1
- package/dist/releaseNotes.d.ts +15 -0
- package/dist/releaseNotes.js +28 -0
- package/dist/releaseNotes.js.map +1 -0
- package/dist/schema/mail.d.ts +20 -0
- package/dist/schema/mail.js +14 -0
- package/dist/schema/mail.js.map +1 -1
- package/dist/schema/settings.d.ts +105 -0
- package/dist/schema/settings.js +45 -5
- package/dist/schema/settings.js.map +1 -1
- package/dist/schema/shop.d.ts +2 -0
- package/dist/schema/shop.js +12 -2
- package/dist/schema/shop.js.map +1 -1
- package/dist/schema/siteConfig.d.ts +9 -0
- package/dist/server/blockData.js +6 -2
- package/dist/server/blockData.js.map +1 -1
- package/dist/server/draft.d.ts +11 -0
- package/dist/server/draft.js +20 -1
- package/dist/server/draft.js.map +1 -1
- package/dist/server/index.d.ts +3 -1
- package/dist/server/index.js +7 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/pages.js +4 -1
- package/dist/server/pages.js.map +1 -1
- package/dist/server/previewToken.d.ts +34 -0
- package/dist/server/previewToken.js +44 -0
- package/dist/server/previewToken.js.map +1 -0
- package/dist/server/redirects.d.ts +15 -0
- package/dist/server/redirects.js +15 -0
- package/dist/server/redirects.js.map +1 -0
- package/dist/styles.css +110 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -20,6 +20,11 @@ import { useToast } from "../ui/toast.js";
|
|
|
20
20
|
import { useConfirm } from "../ui/ConfirmDialog.js";
|
|
21
21
|
import { MailTemplates } from "../settings/MailTemplates.js";
|
|
22
22
|
import { FORM_TEMPLATE_KEYS } from "../../schema/mail.js";
|
|
23
|
+
import { callable } from "../../firebase/functionsClient.js";
|
|
24
|
+
import { getFirebaseApp } from "../../firebase/client.js";
|
|
25
|
+
import { useAuth } from "../../auth/AuthProvider.js";
|
|
26
|
+
import { canManageSite } from "../../auth/roles.js";
|
|
27
|
+
import { formulierenGezienSinds, markeerFormulierGezien } from "./gezien.js";
|
|
23
28
|
const FIELD_TYPES = [
|
|
24
29
|
{ value: "text", label: "Tekst" },
|
|
25
30
|
{ value: "email", label: "E-mail" },
|
|
@@ -93,6 +98,8 @@ function FormsPanel({ siteId }) {
|
|
|
93
98
|
] })
|
|
94
99
|
] });
|
|
95
100
|
}
|
|
101
|
+
let veldTeller = 0;
|
|
102
|
+
const nieuweVeldId = () => `v${Date.now().toString(36)}${(veldTeller++).toString(36)}`;
|
|
96
103
|
function FormDetail({
|
|
97
104
|
siteId,
|
|
98
105
|
form,
|
|
@@ -101,6 +108,20 @@ function FormDetail({
|
|
|
101
108
|
}) {
|
|
102
109
|
const [def, setDef] = useState(form);
|
|
103
110
|
const confirm = useConfirm();
|
|
111
|
+
const [veldIds, setVeldIds] = useState(() => form.fields.map(() => nieuweVeldId()));
|
|
112
|
+
function zetVelden(fields, ids) {
|
|
113
|
+
setDef((d) => ({ ...d, fields }));
|
|
114
|
+
setVeldIds(ids);
|
|
115
|
+
}
|
|
116
|
+
function verplaatsVeld(i, richting) {
|
|
117
|
+
const j = i + richting;
|
|
118
|
+
if (j < 0 || j >= def.fields.length) return;
|
|
119
|
+
const fields = [...def.fields];
|
|
120
|
+
const ids = [...veldIds];
|
|
121
|
+
[fields[i], fields[j]] = [fields[j], fields[i]];
|
|
122
|
+
[ids[i], ids[j]] = [ids[j], ids[i]];
|
|
123
|
+
zetVelden(fields, ids);
|
|
124
|
+
}
|
|
104
125
|
async function save() {
|
|
105
126
|
const db = getDb();
|
|
106
127
|
if (!db) return;
|
|
@@ -184,7 +205,11 @@ function FormDetail({
|
|
|
184
205
|
/* @__PURE__ */ jsx(Field, { label: "Label", children: /* @__PURE__ */ jsx(Input, { value: f.label, onChange: (e) => setField(i, { label: e.target.value }) }) }),
|
|
185
206
|
/* @__PURE__ */ jsx(Field, { label: "Type", children: /* @__PURE__ */ jsx(Select, { value: f.type, onValueChange: (v) => setField(i, { type: v }), options: FIELD_TYPES }) }),
|
|
186
207
|
/* @__PURE__ */ jsx(Switch, { checked: f.required, onCheckedChange: (v) => setField(i, { required: v }), label: "Verplicht" }),
|
|
187
|
-
/* @__PURE__ */
|
|
208
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
|
|
209
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": "Veld omhoog", disabled: i === 0, onClick: () => verplaatsVeld(i, -1), children: "\u2191" }),
|
|
210
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": "Veld omlaag", disabled: i === def.fields.length - 1, onClick: () => verplaatsVeld(i, 1), children: "\u2193" }),
|
|
211
|
+
/* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", "aria-label": "Veld verwijderen", onClick: () => zetVelden(def.fields.filter((_, idx) => idx !== i), veldIds.filter((_, idx) => idx !== i)), children: "\u{1F5D1}" })
|
|
212
|
+
] }),
|
|
188
213
|
(f.type === "select" || f.type === "radio") && /* @__PURE__ */ jsx("div", { className: "md:col-span-5", children: /* @__PURE__ */ jsx(Field, { label: "Opties (komma-gescheiden)", children: /* @__PURE__ */ jsx(
|
|
189
214
|
Input,
|
|
190
215
|
{
|
|
@@ -192,44 +217,105 @@ function FormDetail({
|
|
|
192
217
|
onChange: (e) => setField(i, { options: e.target.value.split(",").map((s) => s.trim()).filter(Boolean) })
|
|
193
218
|
}
|
|
194
219
|
) }) })
|
|
195
|
-
] }, i)) }),
|
|
220
|
+
] }, veldIds[i] ?? i)) }),
|
|
196
221
|
/* @__PURE__ */ jsx("div", { className: "mt-3", children: /* @__PURE__ */ jsx(
|
|
197
222
|
Button,
|
|
198
223
|
{
|
|
199
224
|
variant: "secondary",
|
|
200
225
|
size: "sm",
|
|
201
|
-
onClick: () =>
|
|
226
|
+
onClick: () => zetVelden([...def.fields, { name: `veld${def.fields.length + 1}`, label: "Nieuw veld", type: "text", required: false }], [...veldIds, nieuweVeldId()]),
|
|
202
227
|
children: "+ Veld toevoegen"
|
|
203
228
|
}
|
|
204
229
|
) })
|
|
205
230
|
] }),
|
|
206
|
-
/* @__PURE__ */ jsx(Submissions, { siteId, formId: form.id })
|
|
231
|
+
/* @__PURE__ */ jsx(Submissions, { siteId, formId: form.id, velden: def.fields.map((f) => f.name) })
|
|
207
232
|
] });
|
|
208
233
|
}
|
|
209
|
-
function
|
|
234
|
+
function datumVan(s) {
|
|
235
|
+
const d = s.createdAt && typeof s.createdAt.toDate === "function" ? s.createdAt.toDate() : null;
|
|
236
|
+
return d ? d.toLocaleString("nl-NL", { dateStyle: "short", timeStyle: "short" }) : "";
|
|
237
|
+
}
|
|
238
|
+
function inzendingenCsv(velden, subs) {
|
|
239
|
+
const cel = (v) => `"${String(v ?? "").replace(/"/g, '""')}"`;
|
|
240
|
+
const kop = ["datum", ...velden].map(cel).join(";");
|
|
241
|
+
const rijen = subs.map((s) => [datumVan(s), ...velden.map((k) => s.values?.[k])].map(cel).join(";"));
|
|
242
|
+
return "\uFEFF" + [kop, ...rijen].join("\r\n");
|
|
243
|
+
}
|
|
244
|
+
function Submissions({ siteId, formId, velden }) {
|
|
210
245
|
const [subs, setSubs] = useState([]);
|
|
246
|
+
const { toast } = useToast();
|
|
247
|
+
const confirm = useConfirm();
|
|
248
|
+
const { role } = useAuth();
|
|
249
|
+
const magVerwijderen = canManageSite(role);
|
|
250
|
+
const [sinds] = useState(() => formulierenGezienSinds(siteId, formId));
|
|
251
|
+
useEffect(() => () => markeerFormulierGezien(siteId, formId), [siteId, formId]);
|
|
211
252
|
useEffect(() => {
|
|
212
253
|
const db = getDb();
|
|
213
254
|
if (!db) return;
|
|
214
255
|
const q = query(collection(db, "sites", siteId, "forms", formId, "submissions"), orderBy("createdAt", "desc"));
|
|
215
256
|
return onSnapshot(q, (snap) => setSubs(snap.docs.map((d) => ({ id: d.id, ...d.data() }))));
|
|
216
257
|
}, [siteId, formId]);
|
|
258
|
+
const isNieuw = (s) => Boolean(sinds && s.createdAt && s.createdAt.toMillis() > sinds.toMillis());
|
|
259
|
+
const nieuw = subs.filter(isNieuw).length;
|
|
260
|
+
function exporteer() {
|
|
261
|
+
const blob = new Blob([inzendingenCsv(velden, subs)], { type: "text/csv;charset=utf-8" });
|
|
262
|
+
const url = URL.createObjectURL(blob);
|
|
263
|
+
const a = document.createElement("a");
|
|
264
|
+
a.href = url;
|
|
265
|
+
a.download = `inzendingen-${formId}-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}.csv`;
|
|
266
|
+
a.click();
|
|
267
|
+
URL.revokeObjectURL(url);
|
|
268
|
+
}
|
|
269
|
+
async function verwijder(s) {
|
|
270
|
+
const ok = await confirm({
|
|
271
|
+
title: "Inzending verwijderen?",
|
|
272
|
+
description: "Bijvoorbeeld op verzoek van de inzender (AVG). Dit kan niet ongedaan worden gemaakt.",
|
|
273
|
+
confirmLabel: "Verwijderen",
|
|
274
|
+
tone: "danger"
|
|
275
|
+
});
|
|
276
|
+
if (!ok) return;
|
|
277
|
+
const app = getFirebaseApp();
|
|
278
|
+
if (!app) return;
|
|
279
|
+
try {
|
|
280
|
+
await callable(app, "deleteSubmission")({ siteId, formId, submissionId: s.id });
|
|
281
|
+
toast({ title: "Inzending verwijderd" });
|
|
282
|
+
} catch (e) {
|
|
283
|
+
toast({ title: "Verwijderen mislukt", description: e.message, tone: "error" });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
217
286
|
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
218
|
-
/* @__PURE__ */ jsxs("
|
|
219
|
-
"
|
|
220
|
-
|
|
287
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-3 flex flex-wrap items-center justify-between gap-2", children: [
|
|
288
|
+
/* @__PURE__ */ jsxs("h4", { className: "text-sm font-semibold", children: [
|
|
289
|
+
"Inzendingen ",
|
|
290
|
+
/* @__PURE__ */ jsx(Badge, { children: subs.length }),
|
|
291
|
+
nieuw > 0 && /* @__PURE__ */ jsxs(Badge, { tone: "amber", children: [
|
|
292
|
+
nieuw,
|
|
293
|
+
" nieuw"
|
|
294
|
+
] })
|
|
295
|
+
] }),
|
|
296
|
+
subs.length > 0 && /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: exporteer, children: "Exporteer CSV" })
|
|
221
297
|
] }),
|
|
222
|
-
subs.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen inzendingen." }) : /* @__PURE__ */ jsx("div", { className: "grid gap-2", children: subs.map((s) => /* @__PURE__ */
|
|
223
|
-
/* @__PURE__ */ jsxs("
|
|
224
|
-
|
|
225
|
-
|
|
298
|
+
subs.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen inzendingen." }) : /* @__PURE__ */ jsx("div", { className: "grid gap-2", children: subs.map((s) => /* @__PURE__ */ jsxs("div", { className: `rounded-lg border p-3 text-sm ${isNieuw(s) ? "border-amber-300 bg-amber-50/40" : "border-slate-200"}`, children: [
|
|
299
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-between gap-2 text-xs text-slate-400", children: [
|
|
300
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
301
|
+
datumVan(s),
|
|
302
|
+
isNieuw(s) ? " \xB7 nieuw" : ""
|
|
303
|
+
] }),
|
|
304
|
+
magVerwijderen && /* @__PURE__ */ jsx("button", { type: "button", className: "text-red-600 hover:underline", onClick: () => void verwijder(s), children: "Verwijderen" })
|
|
226
305
|
] }),
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
306
|
+
Object.entries(s.values ?? {}).map(([k, v]) => /* @__PURE__ */ jsxs("div", { children: [
|
|
307
|
+
/* @__PURE__ */ jsxs("span", { className: "font-medium text-slate-600", children: [
|
|
308
|
+
k,
|
|
309
|
+
":"
|
|
310
|
+
] }),
|
|
311
|
+
" ",
|
|
312
|
+
String(v)
|
|
313
|
+
] }, k))
|
|
314
|
+
] }, s.id)) })
|
|
230
315
|
] });
|
|
231
316
|
}
|
|
232
317
|
export {
|
|
233
|
-
FormsPanel
|
|
318
|
+
FormsPanel,
|
|
319
|
+
inzendingenCsv
|
|
234
320
|
};
|
|
235
321
|
//# sourceMappingURL=FormsPanel.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/forms/FormsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport {\n addDoc,\n collection,\n deleteDoc,\n doc,\n onSnapshot,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n} from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { FormSchema, type FormDefinition, type FormField, type FieldType } from \"../../schema/forms\";\nimport { Button, Card, Field, Input, Badge } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { MailTemplates } from \"../settings/MailTemplates\";\nimport { FORM_TEMPLATE_KEYS } from \"../../schema/mail\";\n\ntype StoredForm = FormDefinition & { id: string };\n\nconst FIELD_TYPES: { value: FieldType; label: string }[] = [\n { value: \"text\", label: \"Tekst\" },\n { value: \"email\", label: \"E-mail\" },\n { value: \"tel\", label: \"Telefoon\" },\n { value: \"textarea\", label: \"Tekstvak\" },\n { value: \"select\", label: \"Keuzelijst\" },\n { value: \"radio\", label: \"Keuzerondjes\" },\n { value: \"checkbox\", label: \"Vinkje\" },\n { value: \"date\", label: \"Datum\" },\n { value: \"number\", label: \"Getal\" },\n];\n\nexport function FormsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [forms, setForms] = useState<StoredForm[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n return onSnapshot(collection(db, \"sites\", siteId, \"forms\"), (snap) =>\n setForms(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<StoredForm, \"id\">) }))),\n );\n }, [siteId]);\n\n const current = forms.find((f) => f.id === selected) ?? null;\n\n async function createForm() {\n const db = getDb();\n if (!db) return;\n const ref = await addDoc(collection(db, \"sites\", siteId, \"forms\"), {\n name: \"Nieuw formulier\",\n fields: [{ name: \"naam\", label: \"Naam\", type: \"text\", required: true }],\n submitLabel: \"Versturen\",\n successMessage: \"Bedankt! We nemen snel contact op.\",\n } satisfies FormDefinition);\n setSelected(ref.id);\n }\n\n return (\n <div className=\"grid gap-6\">\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\">\n <h3 className=\"text-sm font-semibold\">Formulieren</h3>\n <Button size=\"sm\" onClick={createForm}>+ Nieuw</Button>\n </div>\n <ul className=\"grid gap-1\">\n {forms.map((f) => (\n <li key={f.id}>\n <button\n onClick={() => setSelected(f.id)}\n className={`w-full truncate rounded-lg px-3 py-2 text-left text-sm ${\n f.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n {f.name}\n <span className=\"ml-1 text-xs text-slate-400\">#{f.id.slice(0, 5)}</span>\n </button>\n </li>\n ))}\n {forms.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Nog geen formulieren.</li>}\n </ul>\n </Card>\n {current ? (\n <FormDetail key={current.id} siteId={siteId} form={current} onDeleted={() => setSelected(null)} onToast={toast} />\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een formulier.</p>\n </Card>\n )}\n </div>\n\n <div>\n <h3 className=\"mb-1 text-base font-semibold\">E-mailteksten voor formulieren</h3>\n <p className=\"mb-3 text-sm text-slate-600\">\n Gelden voor álle formulieren: de notificatie naar jou en de bevestiging naar de invuller. De opmaak\n (huisstijl) en verzendprovider stel je site-breed in bij <strong>Instellingen → E-mail</strong>.\n </p>\n <MailTemplates siteId={siteId} keys={FORM_TEMPLATE_KEYS} />\n </div>\n </div>\n );\n}\n\nfunction FormDetail({\n siteId,\n form,\n onDeleted,\n onToast,\n}: {\n siteId: string;\n form: StoredForm;\n onDeleted: () => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n}) {\n const [def, setDef] = useState<FormDefinition>(form);\n const confirm = useConfirm();\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = FormSchema.safeParse(def);\n if (!parsed.success) {\n onToast({ title: \"Ongeldig formulier\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n await setDoc(doc(db, \"sites\", siteId, \"forms\", form.id), parsed.data, { merge: true });\n onToast({ title: \"Opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const db = getDb();\n if (!db) return;\n const ok = await confirm({ title: \"Formulier verwijderen?\", description: \"Het formulier wordt verwijderd. Inzendingen blijven bewaard.\", confirmLabel: \"Verwijderen\", tone: \"danger\" });\n if (!ok) return;\n await deleteDoc(doc(db, \"sites\", siteId, \"forms\", form.id));\n onDeleted();\n }\n\n function setField(i: number, part: Partial<FormField>) {\n const fields = [...def.fields];\n fields[i] = { ...fields[i], ...part };\n setDef({ ...def, fields });\n }\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-base font-semibold\">Formulier bewerken</h3>\n <div className=\"flex gap-2\">\n <Button variant=\"danger\" size=\"sm\" onClick={remove}>Verwijderen</Button>\n <Button size=\"sm\" onClick={save}>Opslaan</Button>\n </div>\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Naam\"><Input value={def.name} onChange={(e) => setDef({ ...def, name: e.target.value })} /></Field>\n <Field label=\"Verstuurknop\"><Input value={def.submitLabel} onChange={(e) => setDef({ ...def, submitLabel: e.target.value })} /></Field>\n <Field label=\"Bedankbericht\"><Input value={def.successMessage} onChange={(e) => setDef({ ...def, successMessage: e.target.value })} /></Field>\n <Field label=\"Bedankt-pagina (optioneel)\" hint=\"Na verzenden doorsturen naar dit pad, bv. /bedankt\">\n <Input value={def.successRedirect ?? \"\"} onChange={(e) => setDef({ ...def, successRedirect: e.target.value || undefined })} />\n </Field>\n </div>\n\n <div className=\"mt-4 rounded-lg border border-slate-200 p-4\">\n <h4 className=\"text-sm font-semibold\">Wat gebeurt er bij een inzending?</h4>\n <p className=\"mb-3 mt-0.5 text-xs text-slate-500\">\n Elke inzending wordt altijd bewaard onder <strong>Inzendingen</strong>. Daarnaast kun je een kopie\n mailen naar jezelf en/of een bevestiging naar de invuller. De opmaak en verzendprovider stel je\n site-breed in bij <strong>Instellingen → E-mail</strong>.\n </p>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Kopie van inzending naar\" hint=\"Leeg = het notificatie-adres uit Instellingen → E-mail.\">\n <Input\n type=\"email\"\n value={def.notifyTo ?? \"\"}\n onChange={(e) => setDef({ ...def, notifyTo: e.target.value || undefined })}\n placeholder=\"jij@voorbeeld.nl\"\n />\n </Field>\n <label className=\"mt-6 flex h-9 items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={Boolean(def.confirmSubmitter)}\n onChange={(e) => setDef({ ...def, confirmSubmitter: e.target.checked })}\n />\n Stuur de invuller ook een bevestiging (naar het eerste e-mailveld)\n </label>\n </div>\n </div>\n <p className=\"mt-3 text-xs text-slate-500\">Formulier-id (voor het Formulier-blok): <code>{form.id}</code></p>\n </Card>\n\n <Card>\n <h4 className=\"mb-3 text-sm font-semibold\">Velden</h4>\n <div className=\"grid gap-3\">\n {def.fields.map((f, i) => (\n <div key={i} className=\"grid items-end gap-2 rounded-lg border border-slate-200 p-3 md:grid-cols-[1fr_1fr_140px_auto_auto]\">\n <Field label=\"Naam (sleutel)\"><Input value={f.name} onChange={(e) => setField(i, { name: e.target.value })} /></Field>\n <Field label=\"Label\"><Input value={f.label} onChange={(e) => setField(i, { label: e.target.value })} /></Field>\n <Field label=\"Type\">\n <Select value={f.type} onValueChange={(v) => setField(i, { type: v as FieldType })} options={FIELD_TYPES} />\n </Field>\n <Switch checked={f.required} onCheckedChange={(v) => setField(i, { required: v })} label=\"Verplicht\" />\n <Button variant=\"ghost\" size=\"sm\" onClick={() => setDef({ ...def, fields: def.fields.filter((_, idx) => idx !== i) })}>🗑</Button>\n {(f.type === \"select\" || f.type === \"radio\") && (\n <div className=\"md:col-span-5\">\n <Field label=\"Opties (komma-gescheiden)\">\n <Input\n value={(f.options ?? []).join(\", \")}\n onChange={(e) => setField(i, { options: e.target.value.split(\",\").map((s) => s.trim()).filter(Boolean) })}\n />\n </Field>\n </div>\n )}\n </div>\n ))}\n </div>\n <div className=\"mt-3\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => setDef({ ...def, fields: [...def.fields, { name: `veld${def.fields.length + 1}`, label: \"Nieuw veld\", type: \"text\", required: false }] })}\n >\n + Veld toevoegen\n </Button>\n </div>\n </Card>\n\n <Submissions siteId={siteId} formId={form.id} />\n </div>\n );\n}\n\nfunction Submissions({ siteId, formId }: { siteId: string; formId: string }) {\n const [subs, setSubs] = useState<{ id: string; values: Record<string, unknown>; createdAt?: unknown }[]>([]);\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const q = query(collection(db, \"sites\", siteId, \"forms\", formId, \"submissions\"), orderBy(\"createdAt\", \"desc\"));\n return onSnapshot(q, (snap) => setSubs(snap.docs.map((d) => ({ id: d.id, ...(d.data() as { values: Record<string, unknown> }) }))));\n }, [siteId, formId]);\n\n return (\n <Card>\n <h4 className=\"mb-3 text-sm font-semibold\">Inzendingen <Badge>{subs.length}</Badge></h4>\n {subs.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen inzendingen.</p>\n ) : (\n <div className=\"grid gap-2\">\n {subs.map((s) => (\n <div key={s.id} className=\"rounded-lg border border-slate-200 p-3 text-sm\">\n {Object.entries(s.values ?? {}).map(([k, v]) => (\n <div key={k}>\n <span className=\"font-medium text-slate-600\">{k}:</span> {String(v)}\n </div>\n ))}\n </div>\n ))}\n </div>\n )}\n </Card>\n );\n}\n"],"mappings":";AAqEQ,SACE,KADF;AAnER,SAAS,WAAW,gBAAgB;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,kBAAuE;AAChF,SAAS,QAAQ,MAAM,OAAO,OAAO,aAAa;AAClD,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,0BAA0B;AAInC,MAAM,cAAqD;AAAA,EACzD,EAAE,OAAO,QAAQ,OAAO,QAAQ;AAAA,EAChC,EAAE,OAAO,SAAS,OAAO,SAAS;AAAA,EAClC,EAAE,OAAO,OAAO,OAAO,WAAW;AAAA,EAClC,EAAE,OAAO,YAAY,OAAO,WAAW;AAAA,EACvC,EAAE,OAAO,UAAU,OAAO,aAAa;AAAA,EACvC,EAAE,OAAO,SAAS,OAAO,eAAe;AAAA,EACxC,EAAE,OAAO,YAAY,OAAO,SAAS;AAAA,EACrC,EAAE,OAAO,QAAQ,OAAO,QAAQ;AAAA,EAChC,EAAE,OAAO,UAAU,OAAO,QAAQ;AACpC;AAEO,SAAS,WAAW,EAAE,OAAO,GAAuB;AACzD,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,CAAC,CAAC;AACnD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAwB,IAAI;AAE5D,YAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,WAAO;AAAA,MAAW,WAAW,IAAI,SAAS,QAAQ,OAAO;AAAA,MAAG,CAAC,SAC3D,SAAS,KAAK,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAA6B,EAAE,CAAC;AAAA,IACxF;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,UAAU,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ,KAAK;AAExD,iBAAe,aAAa;AAC1B,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,MAAM,MAAM,OAAO,WAAW,IAAI,SAAS,QAAQ,OAAO,GAAG;AAAA,MACjE,MAAM;AAAA,MACN,QAAQ,CAAC,EAAE,MAAM,QAAQ,OAAO,QAAQ,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,MACtE,aAAa;AAAA,MACb,gBAAgB;AAAA,IAClB,CAA0B;AAC1B,gBAAY,IAAI,EAAE;AAAA,EACpB;AAEA,SACE,qBAAC,SAAI,WAAU,cACf;AAAA,yBAAC,SAAI,WAAU,uCACb;AAAA,2BAAC,QAAK,WAAU,SACd;AAAA,6BAAC,SAAI,WAAU,0CACb;AAAA,8BAAC,QAAG,WAAU,yBAAwB,yBAAW;AAAA,UACjD,oBAAC,UAAO,MAAK,MAAK,SAAS,YAAY,qBAAO;AAAA,WAChD;AAAA,QACA,qBAAC,QAAG,WAAU,cACX;AAAA,gBAAM,IAAI,CAAC,MACV,oBAAC,QACC;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,MAAM,YAAY,EAAE,EAAE;AAAA,cAC/B,WAAW,0DACT,EAAE,OAAO,WAAW,+BAA+B,oBACrD;AAAA,cAEC;AAAA,kBAAE;AAAA,gBACH,qBAAC,UAAK,WAAU,+BAA8B;AAAA;AAAA,kBAAE,EAAE,GAAG,MAAM,GAAG,CAAC;AAAA,mBAAE;AAAA;AAAA;AAAA,UACnE,KATO,EAAE,EAUX,CACD;AAAA,UACA,MAAM,WAAW,KAAK,oBAAC,QAAG,WAAU,oCAAmC,mCAAqB;AAAA,WAC/F;AAAA,SACF;AAAA,MACC,UACC,oBAAC,cAA4B,QAAgB,MAAM,SAAS,WAAW,MAAM,YAAY,IAAI,GAAG,SAAS,SAAxF,QAAQ,EAAuF,IAEhH,oBAAC,QACC,8BAAC,OAAE,WAAU,0BAAyB,8CAAgC,GACxE;AAAA,OAEJ;AAAA,IAEE,qBAAC,SACC;AAAA,0BAAC,QAAG,WAAU,gCAA+B,4CAA8B;AAAA,MAC3E,qBAAC,OAAE,WAAU,+BAA8B;AAAA;AAAA,QAEgB,oBAAC,YAAO,wCAAqB;AAAA,QAAS;AAAA,SACjG;AAAA,MACA,oBAAC,iBAAc,QAAgB,MAAM,oBAAoB;AAAA,OAC3D;AAAA,KACF;AAEJ;AAEA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,CAAC,KAAK,MAAM,IAAI,SAAyB,IAAI;AACnD,QAAM,UAAU,WAAW;AAE3B,iBAAe,OAAO;AACpB,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,SAAS,WAAW,UAAU,GAAG;AACvC,QAAI,CAAC,OAAO,SAAS;AACnB,cAAQ,EAAE,OAAO,sBAAsB,aAAa,OAAO,MAAM,OAAO,CAAC,GAAG,SAAS,MAAM,QAAQ,CAAC;AACpG;AAAA,IACF;AACA,UAAM,OAAO,IAAI,IAAI,SAAS,QAAQ,SAAS,KAAK,EAAE,GAAG,OAAO,MAAM,EAAE,OAAO,KAAK,CAAC;AACrF,YAAQ,EAAE,OAAO,cAAc,MAAM,UAAU,CAAC;AAAA,EAClD;AAEA,iBAAe,SAAS;AACtB,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,KAAK,MAAM,QAAQ,EAAE,OAAO,0BAA0B,aAAa,gEAAgE,cAAc,eAAe,MAAM,SAAS,CAAC;AACtL,QAAI,CAAC,GAAI;AACT,UAAM,UAAU,IAAI,IAAI,SAAS,QAAQ,SAAS,KAAK,EAAE,CAAC;AAC1D,cAAU;AAAA,EACZ;AAEA,WAAS,SAAS,GAAW,MAA0B;AACrD,UAAM,SAAS,CAAC,GAAG,IAAI,MAAM;AAC7B,WAAO,CAAC,IAAI,EAAE,GAAG,OAAO,CAAC,GAAG,GAAG,KAAK;AACpC,WAAO,EAAE,GAAG,KAAK,OAAO,CAAC;AAAA,EAC3B;AAEA,SACE,qBAAC,SAAI,WAAU,cACb;AAAA,yBAAC,QACC;AAAA,2BAAC,SAAI,WAAU,0CACb;AAAA,4BAAC,QAAG,WAAU,2BAA0B,gCAAkB;AAAA,QAC1D,qBAAC,SAAI,WAAU,cACb;AAAA,8BAAC,UAAO,SAAQ,UAAS,MAAK,MAAK,SAAS,QAAQ,yBAAW;AAAA,UAC/D,oBAAC,UAAO,MAAK,MAAK,SAAS,MAAM,qBAAO;AAAA,WAC1C;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,6BACb;AAAA,4BAAC,SAAM,OAAM,QAAO,8BAAC,SAAM,OAAO,IAAI,MAAM,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,MAAM,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QACzG,oBAAC,SAAM,OAAM,gBAAe,8BAAC,SAAM,OAAO,IAAI,aAAa,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,aAAa,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QAC/H,oBAAC,SAAM,OAAM,iBAAgB,8BAAC,SAAM,OAAO,IAAI,gBAAgB,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,gBAAgB,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QACtI,oBAAC,SAAM,OAAM,8BAA6B,MAAK,sDAC7C,8BAAC,SAAM,OAAO,IAAI,mBAAmB,IAAI,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,iBAAiB,EAAE,OAAO,SAAS,OAAU,CAAC,GAAG,GAC9H;AAAA,SACF;AAAA,MAEA,qBAAC,SAAI,WAAU,+CACb;AAAA,4BAAC,QAAG,WAAU,yBAAwB,+CAAiC;AAAA,QACvE,qBAAC,OAAE,WAAU,sCAAqC;AAAA;AAAA,UACN,oBAAC,YAAO,yBAAW;AAAA,UAAS;AAAA,UAEpD,oBAAC,YAAO,wCAAqB;AAAA,UAAS;AAAA,WAC1D;AAAA,QACA,qBAAC,SAAI,WAAU,6BACb;AAAA,8BAAC,SAAM,OAAM,4BAA2B,MAAK,gEAC3C;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,OAAO,IAAI,YAAY;AAAA,cACvB,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,UAAU,EAAE,OAAO,SAAS,OAAU,CAAC;AAAA,cACzE,aAAY;AAAA;AAAA,UACd,GACF;AAAA,UACA,qBAAC,WAAM,WAAU,4CACf;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS,QAAQ,IAAI,gBAAgB;AAAA,gBACrC,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,kBAAkB,EAAE,OAAO,QAAQ,CAAC;AAAA;AAAA,YACxE;AAAA,YAAE;AAAA,aAEJ;AAAA,WACF;AAAA,SACF;AAAA,MACA,qBAAC,OAAE,WAAU,+BAA8B;AAAA;AAAA,QAAwC,oBAAC,UAAM,eAAK,IAAG;AAAA,SAAO;AAAA,OAC3G;AAAA,IAEA,qBAAC,QACC;AAAA,0BAAC,QAAG,WAAU,8BAA6B,oBAAM;AAAA,MACjD,oBAAC,SAAI,WAAU,cACZ,cAAI,OAAO,IAAI,CAAC,GAAG,MAClB,qBAAC,SAAY,WAAU,sGACrB;AAAA,4BAAC,SAAM,OAAM,kBAAiB,8BAAC,SAAM,OAAO,EAAE,MAAM,UAAU,CAAC,MAAM,SAAS,GAAG,EAAE,MAAM,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QAC9G,oBAAC,SAAM,OAAM,SAAQ,8BAAC,SAAM,OAAO,EAAE,OAAO,UAAU,CAAC,MAAM,SAAS,GAAG,EAAE,OAAO,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QACvG,oBAAC,SAAM,OAAM,QACX,8BAAC,UAAO,OAAO,EAAE,MAAM,eAAe,CAAC,MAAM,SAAS,GAAG,EAAE,MAAM,EAAe,CAAC,GAAG,SAAS,aAAa,GAC5G;AAAA,QACA,oBAAC,UAAO,SAAS,EAAE,UAAU,iBAAiB,CAAC,MAAM,SAAS,GAAG,EAAE,UAAU,EAAE,CAAC,GAAG,OAAM,aAAY;AAAA,QACrG,oBAAC,UAAO,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,OAAO,EAAE,GAAG,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,GAAG,QAAQ,QAAQ,CAAC,EAAE,CAAC,GAAG,uBAAE;AAAA,SACvH,EAAE,SAAS,YAAY,EAAE,SAAS,YAClC,oBAAC,SAAI,WAAU,iBACb,8BAAC,SAAM,OAAM,6BACX;AAAA,UAAC;AAAA;AAAA,YACC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,IAAI;AAAA,YAClC,UAAU,CAAC,MAAM,SAAS,GAAG,EAAE,SAAS,EAAE,OAAO,MAAM,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO,EAAE,CAAC;AAAA;AAAA,QAC1G,GACF,GACF;AAAA,WAhBM,CAkBV,CACD,GACH;AAAA,MACA,oBAAC,SAAI,WAAU,QACb;AAAA,QAAC;AAAA;AAAA,UACC,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,SAAS,MAAM,OAAO,EAAE,GAAG,KAAK,QAAQ,CAAC,GAAG,IAAI,QAAQ,EAAE,MAAM,OAAO,IAAI,OAAO,SAAS,CAAC,IAAI,OAAO,cAAc,MAAM,QAAQ,UAAU,MAAM,CAAC,EAAE,CAAC;AAAA,UACxJ;AAAA;AAAA,MAED,GACF;AAAA,OACF;AAAA,IAEA,oBAAC,eAAY,QAAgB,QAAQ,KAAK,IAAI;AAAA,KAChD;AAEJ;AAEA,SAAS,YAAY,EAAE,QAAQ,OAAO,GAAuC;AAC3E,QAAM,CAAC,MAAM,OAAO,IAAI,SAAiF,CAAC,CAAC;AAC3G,YAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,IAAI,MAAM,WAAW,IAAI,SAAS,QAAQ,SAAS,QAAQ,aAAa,GAAG,QAAQ,aAAa,MAAM,CAAC;AAC7G,WAAO,WAAW,GAAG,CAAC,SAAS,QAAQ,KAAK,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAA0C,EAAE,CAAC,CAAC;AAAA,EACpI,GAAG,CAAC,QAAQ,MAAM,CAAC;AAEnB,SACE,qBAAC,QACC;AAAA,yBAAC,QAAG,WAAU,8BAA6B;AAAA;AAAA,MAAY,oBAAC,SAAO,eAAK,QAAO;AAAA,OAAQ;AAAA,IAClF,KAAK,WAAW,IACf,oBAAC,OAAE,WAAU,0BAAyB,mCAAqB,IAE3D,oBAAC,SAAI,WAAU,cACZ,eAAK,IAAI,CAAC,MACT,oBAAC,SAAe,WAAU,kDACvB,iBAAO,QAAQ,EAAE,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MACxC,qBAAC,SACC;AAAA,2BAAC,UAAK,WAAU,8BAA8B;AAAA;AAAA,QAAE;AAAA,SAAC;AAAA,MAAO;AAAA,MAAE,OAAO,CAAC;AAAA,SAD1D,CAEV,CACD,KALO,EAAE,EAMZ,CACD,GACH;AAAA,KAEJ;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/forms/FormsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport {\n addDoc,\n collection,\n deleteDoc,\n doc,\n onSnapshot,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n} from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { FormSchema, type FormDefinition, type FormField, type FieldType } from \"../../schema/forms\";\nimport { Button, Card, Field, Input, Badge } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { MailTemplates } from \"../settings/MailTemplates\";\nimport { FORM_TEMPLATE_KEYS } from \"../../schema/mail\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { getFirebaseApp } from \"../../firebase/client\";\nimport { useAuth } from \"../../auth/AuthProvider\";\nimport { canManageSite } from \"../../auth/roles\";\nimport { formulierenGezienSinds, markeerFormulierGezien } from \"./gezien\";\nimport type { Timestamp } from \"firebase/firestore\";\n\ntype StoredForm = FormDefinition & { id: string };\n\nconst FIELD_TYPES: { value: FieldType; label: string }[] = [\n { value: \"text\", label: \"Tekst\" },\n { value: \"email\", label: \"E-mail\" },\n { value: \"tel\", label: \"Telefoon\" },\n { value: \"textarea\", label: \"Tekstvak\" },\n { value: \"select\", label: \"Keuzelijst\" },\n { value: \"radio\", label: \"Keuzerondjes\" },\n { value: \"checkbox\", label: \"Vinkje\" },\n { value: \"date\", label: \"Datum\" },\n { value: \"number\", label: \"Getal\" },\n];\n\nexport function FormsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [forms, setForms] = useState<StoredForm[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n return onSnapshot(collection(db, \"sites\", siteId, \"forms\"), (snap) =>\n setForms(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<StoredForm, \"id\">) }))),\n );\n }, [siteId]);\n\n const current = forms.find((f) => f.id === selected) ?? null;\n\n async function createForm() {\n const db = getDb();\n if (!db) return;\n const ref = await addDoc(collection(db, \"sites\", siteId, \"forms\"), {\n name: \"Nieuw formulier\",\n fields: [{ name: \"naam\", label: \"Naam\", type: \"text\", required: true }],\n submitLabel: \"Versturen\",\n successMessage: \"Bedankt! We nemen snel contact op.\",\n } satisfies FormDefinition);\n setSelected(ref.id);\n }\n\n return (\n <div className=\"grid gap-6\">\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\">\n <h3 className=\"text-sm font-semibold\">Formulieren</h3>\n <Button size=\"sm\" onClick={createForm}>+ Nieuw</Button>\n </div>\n <ul className=\"grid gap-1\">\n {forms.map((f) => (\n <li key={f.id}>\n <button\n onClick={() => setSelected(f.id)}\n className={`w-full truncate rounded-lg px-3 py-2 text-left text-sm ${\n f.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n {f.name}\n <span className=\"ml-1 text-xs text-slate-400\">#{f.id.slice(0, 5)}</span>\n </button>\n </li>\n ))}\n {forms.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Nog geen formulieren.</li>}\n </ul>\n </Card>\n {current ? (\n <FormDetail key={current.id} siteId={siteId} form={current} onDeleted={() => setSelected(null)} onToast={toast} />\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een formulier.</p>\n </Card>\n )}\n </div>\n\n <div>\n <h3 className=\"mb-1 text-base font-semibold\">E-mailteksten voor formulieren</h3>\n <p className=\"mb-3 text-sm text-slate-600\">\n Gelden voor álle formulieren: de notificatie naar jou en de bevestiging naar de invuller. De opmaak\n (huisstijl) en verzendprovider stel je site-breed in bij <strong>Instellingen → E-mail</strong>.\n </p>\n <MailTemplates siteId={siteId} keys={FORM_TEMPLATE_KEYS} />\n </div>\n </div>\n );\n}\n\nlet veldTeller = 0;\nconst nieuweVeldId = () => `v${Date.now().toString(36)}${(veldTeller++).toString(36)}`;\n\nfunction FormDetail({\n siteId,\n form,\n onDeleted,\n onToast,\n}: {\n siteId: string;\n form: StoredForm;\n onDeleted: () => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n}) {\n const [def, setDef] = useState<FormDefinition>(form);\n const confirm = useConfirm();\n /**\n * Stabiele React-sleutels per veld (7.24). Op index (`key={i}`) erven rijen elkaars state bij\n * verwijderen of verplaatsen — dezelfde stille corruptie die de vorige review in ArrayEditor\n * dichtte. De ids bestaan alleen hier (niet in het schema) en lopen mee met elke mutatie.\n */\n const [veldIds, setVeldIds] = useState<string[]>(() => form.fields.map(() => nieuweVeldId()));\n function zetVelden(fields: FormField[], ids: string[]) {\n setDef((d) => ({ ...d, fields }));\n setVeldIds(ids);\n }\n function verplaatsVeld(i: number, richting: -1 | 1) {\n const j = i + richting;\n if (j < 0 || j >= def.fields.length) return;\n const fields = [...def.fields];\n const ids = [...veldIds];\n [fields[i], fields[j]] = [fields[j], fields[i]];\n [ids[i], ids[j]] = [ids[j], ids[i]];\n zetVelden(fields, ids);\n }\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = FormSchema.safeParse(def);\n if (!parsed.success) {\n onToast({ title: \"Ongeldig formulier\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n await setDoc(doc(db, \"sites\", siteId, \"forms\", form.id), parsed.data, { merge: true });\n onToast({ title: \"Opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const db = getDb();\n if (!db) return;\n const ok = await confirm({ title: \"Formulier verwijderen?\", description: \"Het formulier wordt verwijderd. Inzendingen blijven bewaard.\", confirmLabel: \"Verwijderen\", tone: \"danger\" });\n if (!ok) return;\n await deleteDoc(doc(db, \"sites\", siteId, \"forms\", form.id));\n onDeleted();\n }\n\n function setField(i: number, part: Partial<FormField>) {\n const fields = [...def.fields];\n fields[i] = { ...fields[i], ...part };\n setDef({ ...def, fields });\n }\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-base font-semibold\">Formulier bewerken</h3>\n <div className=\"flex gap-2\">\n <Button variant=\"danger\" size=\"sm\" onClick={remove}>Verwijderen</Button>\n <Button size=\"sm\" onClick={save}>Opslaan</Button>\n </div>\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Naam\"><Input value={def.name} onChange={(e) => setDef({ ...def, name: e.target.value })} /></Field>\n <Field label=\"Verstuurknop\"><Input value={def.submitLabel} onChange={(e) => setDef({ ...def, submitLabel: e.target.value })} /></Field>\n <Field label=\"Bedankbericht\"><Input value={def.successMessage} onChange={(e) => setDef({ ...def, successMessage: e.target.value })} /></Field>\n <Field label=\"Bedankt-pagina (optioneel)\" hint=\"Na verzenden doorsturen naar dit pad, bv. /bedankt\">\n <Input value={def.successRedirect ?? \"\"} onChange={(e) => setDef({ ...def, successRedirect: e.target.value || undefined })} />\n </Field>\n </div>\n\n <div className=\"mt-4 rounded-lg border border-slate-200 p-4\">\n <h4 className=\"text-sm font-semibold\">Wat gebeurt er bij een inzending?</h4>\n <p className=\"mb-3 mt-0.5 text-xs text-slate-500\">\n Elke inzending wordt altijd bewaard onder <strong>Inzendingen</strong>. Daarnaast kun je een kopie\n mailen naar jezelf en/of een bevestiging naar de invuller. De opmaak en verzendprovider stel je\n site-breed in bij <strong>Instellingen → E-mail</strong>.\n </p>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Kopie van inzending naar\" hint=\"Leeg = het notificatie-adres uit Instellingen → E-mail.\">\n <Input\n type=\"email\"\n value={def.notifyTo ?? \"\"}\n onChange={(e) => setDef({ ...def, notifyTo: e.target.value || undefined })}\n placeholder=\"jij@voorbeeld.nl\"\n />\n </Field>\n <label className=\"mt-6 flex h-9 items-center gap-2 text-sm\">\n <input\n type=\"checkbox\"\n checked={Boolean(def.confirmSubmitter)}\n onChange={(e) => setDef({ ...def, confirmSubmitter: e.target.checked })}\n />\n Stuur de invuller ook een bevestiging (naar het eerste e-mailveld)\n </label>\n </div>\n </div>\n <p className=\"mt-3 text-xs text-slate-500\">Formulier-id (voor het Formulier-blok): <code>{form.id}</code></p>\n </Card>\n\n <Card>\n <h4 className=\"mb-3 text-sm font-semibold\">Velden</h4>\n <div className=\"grid gap-3\">\n {def.fields.map((f, i) => (\n <div key={veldIds[i] ?? i} className=\"grid items-end gap-2 rounded-lg border border-slate-200 p-3 md:grid-cols-[1fr_1fr_140px_auto_auto]\">\n <Field label=\"Naam (sleutel)\"><Input value={f.name} onChange={(e) => setField(i, { name: e.target.value })} /></Field>\n <Field label=\"Label\"><Input value={f.label} onChange={(e) => setField(i, { label: e.target.value })} /></Field>\n <Field label=\"Type\">\n <Select value={f.type} onValueChange={(v) => setField(i, { type: v as FieldType })} options={FIELD_TYPES} />\n </Field>\n <Switch checked={f.required} onCheckedChange={(v) => setField(i, { required: v })} label=\"Verplicht\" />\n <div className=\"flex items-center gap-1\">\n <Button variant=\"ghost\" size=\"sm\" aria-label=\"Veld omhoog\" disabled={i === 0} onClick={() => verplaatsVeld(i, -1)}>↑</Button>\n <Button variant=\"ghost\" size=\"sm\" aria-label=\"Veld omlaag\" disabled={i === def.fields.length - 1} onClick={() => verplaatsVeld(i, 1)}>↓</Button>\n <Button variant=\"ghost\" size=\"sm\" aria-label=\"Veld verwijderen\" onClick={() => zetVelden(def.fields.filter((_, idx) => idx !== i), veldIds.filter((_, idx) => idx !== i))}>🗑</Button>\n </div>\n {(f.type === \"select\" || f.type === \"radio\") && (\n <div className=\"md:col-span-5\">\n <Field label=\"Opties (komma-gescheiden)\">\n <Input\n value={(f.options ?? []).join(\", \")}\n onChange={(e) => setField(i, { options: e.target.value.split(\",\").map((s) => s.trim()).filter(Boolean) })}\n />\n </Field>\n </div>\n )}\n </div>\n ))}\n </div>\n <div className=\"mt-3\">\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => zetVelden([...def.fields, { name: `veld${def.fields.length + 1}`, label: \"Nieuw veld\", type: \"text\", required: false }], [...veldIds, nieuweVeldId()])}\n >\n + Veld toevoegen\n </Button>\n </div>\n </Card>\n\n <Submissions siteId={siteId} formId={form.id} velden={def.fields.map((f) => f.name)} />\n </div>\n );\n}\n\ntype Inzending = { id: string; values: Record<string, unknown>; createdAt?: Timestamp };\n\nfunction datumVan(s: Inzending): string {\n const d = s.createdAt && typeof s.createdAt.toDate === \"function\" ? s.createdAt.toDate() : null;\n return d ? d.toLocaleString(\"nl-NL\", { dateStyle: \"short\", timeStyle: \"short\" }) : \"\";\n}\n\n/** CSV met Excel-vriendelijke opmaak: BOM, puntkomma, alles gequoot. Kolommen = de velden van het formulier. */\nexport function inzendingenCsv(velden: string[], subs: Inzending[]): string {\n const cel = (v: unknown) => `\"${String(v ?? \"\").replace(/\"/g, '\"\"')}\"`;\n const kop = [\"datum\", ...velden].map(cel).join(\";\");\n const rijen = subs.map((s) => [datumVan(s), ...velden.map((k) => s.values?.[k])].map(cel).join(\";\"));\n return \"\\ufeff\" + [kop, ...rijen].join(\"\\r\\n\");\n}\n\nfunction Submissions({ siteId, formId, velden }: { siteId: string; formId: string; velden: string[] }) {\n const [subs, setSubs] = useState<Inzending[]>([]);\n const { toast } = useToast();\n const confirm = useConfirm();\n const { role } = useAuth();\n const magVerwijderen = canManageSite(role);\n // \"Nieuw sinds je laatste bezoek\" — het moment van openen; markeren gebeurt bij het sluiten\n // (unmount), zodat je tijdens het kijken nog ziet wát er nieuw was.\n const [sinds] = useState(() => formulierenGezienSinds(siteId, formId));\n useEffect(() => () => markeerFormulierGezien(siteId, formId), [siteId, formId]);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const q = query(collection(db, \"sites\", siteId, \"forms\", formId, \"submissions\"), orderBy(\"createdAt\", \"desc\"));\n return onSnapshot(q, (snap) => setSubs(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<Inzending, \"id\">) }))));\n }, [siteId, formId]);\n\n const isNieuw = (s: Inzending) => Boolean(sinds && s.createdAt && s.createdAt.toMillis() > sinds.toMillis());\n const nieuw = subs.filter(isNieuw).length;\n\n function exporteer() {\n const blob = new Blob([inzendingenCsv(velden, subs)], { type: \"text/csv;charset=utf-8\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `inzendingen-${formId}-${new Date().toISOString().slice(0, 10)}.csv`;\n a.click();\n URL.revokeObjectURL(url);\n }\n\n async function verwijder(s: Inzending) {\n const ok = await confirm({\n title: \"Inzending verwijderen?\",\n description: \"Bijvoorbeeld op verzoek van de inzender (AVG). Dit kan niet ongedaan worden gemaakt.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n const app = getFirebaseApp();\n if (!app) return;\n try {\n await callable(app, \"deleteSubmission\")({ siteId, formId, submissionId: s.id });\n toast({ title: \"Inzending verwijderd\" });\n } catch (e) {\n toast({ title: \"Verwijderen mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n return (\n <Card>\n <div className=\"mb-3 flex flex-wrap items-center justify-between gap-2\">\n <h4 className=\"text-sm font-semibold\">\n Inzendingen <Badge>{subs.length}</Badge>\n {nieuw > 0 && <Badge tone=\"amber\">{nieuw} nieuw</Badge>}\n </h4>\n {subs.length > 0 && (\n <Button variant=\"secondary\" size=\"sm\" onClick={exporteer}>\n Exporteer CSV\n </Button>\n )}\n </div>\n {subs.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen inzendingen.</p>\n ) : (\n <div className=\"grid gap-2\">\n {subs.map((s) => (\n <div key={s.id} className={`rounded-lg border p-3 text-sm ${isNieuw(s) ? \"border-amber-300 bg-amber-50/40\" : \"border-slate-200\"}`}>\n <div className=\"mb-1 flex items-center justify-between gap-2 text-xs text-slate-400\">\n <span>{datumVan(s)}{isNieuw(s) ? \" · nieuw\" : \"\"}</span>\n {magVerwijderen && (\n <button type=\"button\" className=\"text-red-600 hover:underline\" onClick={() => void verwijder(s)}>\n Verwijderen\n </button>\n )}\n </div>\n {Object.entries(s.values ?? {}).map(([k, v]) => (\n <div key={k}>\n <span className=\"font-medium text-slate-600\">{k}:</span> {String(v)}\n </div>\n ))}\n </div>\n ))}\n </div>\n )}\n </Card>\n );\n}\n"],"mappings":";AA2EQ,SACE,KADF;AAzER,SAAS,WAAW,gBAAgB;AACpC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AACP,SAAS,aAAa;AACtB,SAAS,kBAAuE;AAChF,SAAS,QAAQ,MAAM,OAAO,OAAO,aAAa;AAClD,SAAS,cAAc;AACvB,SAAS,cAAc;AACvB,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,0BAA0B;AACnC,SAAS,gBAAgB;AACzB,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AACxB,SAAS,qBAAqB;AAC9B,SAAS,wBAAwB,8BAA8B;AAK/D,MAAM,cAAqD;AAAA,EACzD,EAAE,OAAO,QAAQ,OAAO,QAAQ;AAAA,EAChC,EAAE,OAAO,SAAS,OAAO,SAAS;AAAA,EAClC,EAAE,OAAO,OAAO,OAAO,WAAW;AAAA,EAClC,EAAE,OAAO,YAAY,OAAO,WAAW;AAAA,EACvC,EAAE,OAAO,UAAU,OAAO,aAAa;AAAA,EACvC,EAAE,OAAO,SAAS,OAAO,eAAe;AAAA,EACxC,EAAE,OAAO,YAAY,OAAO,SAAS;AAAA,EACrC,EAAE,OAAO,QAAQ,OAAO,QAAQ;AAAA,EAChC,EAAE,OAAO,UAAU,OAAO,QAAQ;AACpC;AAEO,SAAS,WAAW,EAAE,OAAO,GAAuB;AACzD,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,CAAC,CAAC;AACnD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAwB,IAAI;AAE5D,YAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,WAAO;AAAA,MAAW,WAAW,IAAI,SAAS,QAAQ,OAAO;AAAA,MAAG,CAAC,SAC3D,SAAS,KAAK,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAA6B,EAAE,CAAC;AAAA,IACxF;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,UAAU,MAAM,KAAK,CAAC,MAAM,EAAE,OAAO,QAAQ,KAAK;AAExD,iBAAe,aAAa;AAC1B,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,MAAM,MAAM,OAAO,WAAW,IAAI,SAAS,QAAQ,OAAO,GAAG;AAAA,MACjE,MAAM;AAAA,MACN,QAAQ,CAAC,EAAE,MAAM,QAAQ,OAAO,QAAQ,MAAM,QAAQ,UAAU,KAAK,CAAC;AAAA,MACtE,aAAa;AAAA,MACb,gBAAgB;AAAA,IAClB,CAA0B;AAC1B,gBAAY,IAAI,EAAE;AAAA,EACpB;AAEA,SACE,qBAAC,SAAI,WAAU,cACf;AAAA,yBAAC,SAAI,WAAU,uCACb;AAAA,2BAAC,QAAK,WAAU,SACd;AAAA,6BAAC,SAAI,WAAU,0CACb;AAAA,8BAAC,QAAG,WAAU,yBAAwB,yBAAW;AAAA,UACjD,oBAAC,UAAO,MAAK,MAAK,SAAS,YAAY,qBAAO;AAAA,WAChD;AAAA,QACA,qBAAC,QAAG,WAAU,cACX;AAAA,gBAAM,IAAI,CAAC,MACV,oBAAC,QACC;AAAA,YAAC;AAAA;AAAA,cACC,SAAS,MAAM,YAAY,EAAE,EAAE;AAAA,cAC/B,WAAW,0DACT,EAAE,OAAO,WAAW,+BAA+B,oBACrD;AAAA,cAEC;AAAA,kBAAE;AAAA,gBACH,qBAAC,UAAK,WAAU,+BAA8B;AAAA;AAAA,kBAAE,EAAE,GAAG,MAAM,GAAG,CAAC;AAAA,mBAAE;AAAA;AAAA;AAAA,UACnE,KATO,EAAE,EAUX,CACD;AAAA,UACA,MAAM,WAAW,KAAK,oBAAC,QAAG,WAAU,oCAAmC,mCAAqB;AAAA,WAC/F;AAAA,SACF;AAAA,MACC,UACC,oBAAC,cAA4B,QAAgB,MAAM,SAAS,WAAW,MAAM,YAAY,IAAI,GAAG,SAAS,SAAxF,QAAQ,EAAuF,IAEhH,oBAAC,QACC,8BAAC,OAAE,WAAU,0BAAyB,8CAAgC,GACxE;AAAA,OAEJ;AAAA,IAEE,qBAAC,SACC;AAAA,0BAAC,QAAG,WAAU,gCAA+B,4CAA8B;AAAA,MAC3E,qBAAC,OAAE,WAAU,+BAA8B;AAAA;AAAA,QAEgB,oBAAC,YAAO,wCAAqB;AAAA,QAAS;AAAA,SACjG;AAAA,MACA,oBAAC,iBAAc,QAAgB,MAAM,oBAAoB;AAAA,OAC3D;AAAA,KACF;AAEJ;AAEA,IAAI,aAAa;AACjB,MAAM,eAAe,MAAM,IAAI,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,cAAc,SAAS,EAAE,CAAC;AAEpF,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,CAAC,KAAK,MAAM,IAAI,SAAyB,IAAI;AACnD,QAAM,UAAU,WAAW;AAM3B,QAAM,CAAC,SAAS,UAAU,IAAI,SAAmB,MAAM,KAAK,OAAO,IAAI,MAAM,aAAa,CAAC,CAAC;AAC5F,WAAS,UAAU,QAAqB,KAAe;AACrD,WAAO,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,EAAE;AAChC,eAAW,GAAG;AAAA,EAChB;AACA,WAAS,cAAc,GAAW,UAAkB;AAClD,UAAM,IAAI,IAAI;AACd,QAAI,IAAI,KAAK,KAAK,IAAI,OAAO,OAAQ;AACrC,UAAM,SAAS,CAAC,GAAG,IAAI,MAAM;AAC7B,UAAM,MAAM,CAAC,GAAG,OAAO;AACvB,KAAC,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AAC9C,KAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;AAClC,cAAU,QAAQ,GAAG;AAAA,EACvB;AAEA,iBAAe,OAAO;AACpB,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,SAAS,WAAW,UAAU,GAAG;AACvC,QAAI,CAAC,OAAO,SAAS;AACnB,cAAQ,EAAE,OAAO,sBAAsB,aAAa,OAAO,MAAM,OAAO,CAAC,GAAG,SAAS,MAAM,QAAQ,CAAC;AACpG;AAAA,IACF;AACA,UAAM,OAAO,IAAI,IAAI,SAAS,QAAQ,SAAS,KAAK,EAAE,GAAG,OAAO,MAAM,EAAE,OAAO,KAAK,CAAC;AACrF,YAAQ,EAAE,OAAO,cAAc,MAAM,UAAU,CAAC;AAAA,EAClD;AAEA,iBAAe,SAAS;AACtB,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,KAAK,MAAM,QAAQ,EAAE,OAAO,0BAA0B,aAAa,gEAAgE,cAAc,eAAe,MAAM,SAAS,CAAC;AACtL,QAAI,CAAC,GAAI;AACT,UAAM,UAAU,IAAI,IAAI,SAAS,QAAQ,SAAS,KAAK,EAAE,CAAC;AAC1D,cAAU;AAAA,EACZ;AAEA,WAAS,SAAS,GAAW,MAA0B;AACrD,UAAM,SAAS,CAAC,GAAG,IAAI,MAAM;AAC7B,WAAO,CAAC,IAAI,EAAE,GAAG,OAAO,CAAC,GAAG,GAAG,KAAK;AACpC,WAAO,EAAE,GAAG,KAAK,OAAO,CAAC;AAAA,EAC3B;AAEA,SACE,qBAAC,SAAI,WAAU,cACb;AAAA,yBAAC,QACC;AAAA,2BAAC,SAAI,WAAU,0CACb;AAAA,4BAAC,QAAG,WAAU,2BAA0B,gCAAkB;AAAA,QAC1D,qBAAC,SAAI,WAAU,cACb;AAAA,8BAAC,UAAO,SAAQ,UAAS,MAAK,MAAK,SAAS,QAAQ,yBAAW;AAAA,UAC/D,oBAAC,UAAO,MAAK,MAAK,SAAS,MAAM,qBAAO;AAAA,WAC1C;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,6BACb;AAAA,4BAAC,SAAM,OAAM,QAAO,8BAAC,SAAM,OAAO,IAAI,MAAM,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,MAAM,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QACzG,oBAAC,SAAM,OAAM,gBAAe,8BAAC,SAAM,OAAO,IAAI,aAAa,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,aAAa,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QAC/H,oBAAC,SAAM,OAAM,iBAAgB,8BAAC,SAAM,OAAO,IAAI,gBAAgB,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,gBAAgB,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QACtI,oBAAC,SAAM,OAAM,8BAA6B,MAAK,sDAC7C,8BAAC,SAAM,OAAO,IAAI,mBAAmB,IAAI,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,iBAAiB,EAAE,OAAO,SAAS,OAAU,CAAC,GAAG,GAC9H;AAAA,SACF;AAAA,MAEA,qBAAC,SAAI,WAAU,+CACb;AAAA,4BAAC,QAAG,WAAU,yBAAwB,+CAAiC;AAAA,QACvE,qBAAC,OAAE,WAAU,sCAAqC;AAAA;AAAA,UACN,oBAAC,YAAO,yBAAW;AAAA,UAAS;AAAA,UAEpD,oBAAC,YAAO,wCAAqB;AAAA,UAAS;AAAA,WAC1D;AAAA,QACA,qBAAC,SAAI,WAAU,6BACb;AAAA,8BAAC,SAAM,OAAM,4BAA2B,MAAK,gEAC3C;AAAA,YAAC;AAAA;AAAA,cACC,MAAK;AAAA,cACL,OAAO,IAAI,YAAY;AAAA,cACvB,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,UAAU,EAAE,OAAO,SAAS,OAAU,CAAC;AAAA,cACzE,aAAY;AAAA;AAAA,UACd,GACF;AAAA,UACA,qBAAC,WAAM,WAAU,4CACf;AAAA;AAAA,cAAC;AAAA;AAAA,gBACC,MAAK;AAAA,gBACL,SAAS,QAAQ,IAAI,gBAAgB;AAAA,gBACrC,UAAU,CAAC,MAAM,OAAO,EAAE,GAAG,KAAK,kBAAkB,EAAE,OAAO,QAAQ,CAAC;AAAA;AAAA,YACxE;AAAA,YAAE;AAAA,aAEJ;AAAA,WACF;AAAA,SACF;AAAA,MACA,qBAAC,OAAE,WAAU,+BAA8B;AAAA;AAAA,QAAwC,oBAAC,UAAM,eAAK,IAAG;AAAA,SAAO;AAAA,OAC3G;AAAA,IAEA,qBAAC,QACC;AAAA,0BAAC,QAAG,WAAU,8BAA6B,oBAAM;AAAA,MACjD,oBAAC,SAAI,WAAU,cACZ,cAAI,OAAO,IAAI,CAAC,GAAG,MAClB,qBAAC,SAA0B,WAAU,sGACnC;AAAA,4BAAC,SAAM,OAAM,kBAAiB,8BAAC,SAAM,OAAO,EAAE,MAAM,UAAU,CAAC,MAAM,SAAS,GAAG,EAAE,MAAM,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QAC9G,oBAAC,SAAM,OAAM,SAAQ,8BAAC,SAAM,OAAO,EAAE,OAAO,UAAU,CAAC,MAAM,SAAS,GAAG,EAAE,OAAO,EAAE,OAAO,MAAM,CAAC,GAAG,GAAE;AAAA,QACvG,oBAAC,SAAM,OAAM,QACX,8BAAC,UAAO,OAAO,EAAE,MAAM,eAAe,CAAC,MAAM,SAAS,GAAG,EAAE,MAAM,EAAe,CAAC,GAAG,SAAS,aAAa,GAC5G;AAAA,QACA,oBAAC,UAAO,SAAS,EAAE,UAAU,iBAAiB,CAAC,MAAM,SAAS,GAAG,EAAE,UAAU,EAAE,CAAC,GAAG,OAAM,aAAY;AAAA,QACrG,qBAAC,SAAI,WAAU,2BACb;AAAA,8BAAC,UAAO,SAAQ,SAAQ,MAAK,MAAK,cAAW,eAAc,UAAU,MAAM,GAAG,SAAS,MAAM,cAAc,GAAG,EAAE,GAAG,oBAAC;AAAA,UACpH,oBAAC,UAAO,SAAQ,SAAQ,MAAK,MAAK,cAAW,eAAc,UAAU,MAAM,IAAI,OAAO,SAAS,GAAG,SAAS,MAAM,cAAc,GAAG,CAAC,GAAG,oBAAC;AAAA,UACvI,oBAAC,UAAO,SAAQ,SAAQ,MAAK,MAAK,cAAW,oBAAmB,SAAS,MAAM,UAAU,IAAI,OAAO,OAAO,CAAC,GAAG,QAAQ,QAAQ,CAAC,GAAG,QAAQ,OAAO,CAAC,GAAG,QAAQ,QAAQ,CAAC,CAAC,GAAG,uBAAE;AAAA,WAC/K;AAAA,SACE,EAAE,SAAS,YAAY,EAAE,SAAS,YAClC,oBAAC,SAAI,WAAU,iBACb,8BAAC,SAAM,OAAM,6BACX;AAAA,UAAC;AAAA;AAAA,YACC,QAAQ,EAAE,WAAW,CAAC,GAAG,KAAK,IAAI;AAAA,YAClC,UAAU,CAAC,MAAM,SAAS,GAAG,EAAE,SAAS,EAAE,OAAO,MAAM,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO,EAAE,CAAC;AAAA;AAAA,QAC1G,GACF,GACF;AAAA,WApBM,QAAQ,CAAC,KAAK,CAsBxB,CACD,GACH;AAAA,MACA,oBAAC,SAAI,WAAU,QACb;AAAA,QAAC;AAAA;AAAA,UACC,SAAQ;AAAA,UACR,MAAK;AAAA,UACL,SAAS,MAAM,UAAU,CAAC,GAAG,IAAI,QAAQ,EAAE,MAAM,OAAO,IAAI,OAAO,SAAS,CAAC,IAAI,OAAO,cAAc,MAAM,QAAQ,UAAU,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,aAAa,CAAC,CAAC;AAAA,UACrK;AAAA;AAAA,MAED,GACF;AAAA,OACF;AAAA,IAEA,oBAAC,eAAY,QAAgB,QAAQ,KAAK,IAAI,QAAQ,IAAI,OAAO,IAAI,CAAC,MAAM,EAAE,IAAI,GAAG;AAAA,KACvF;AAEJ;AAIA,SAAS,SAAS,GAAsB;AACtC,QAAM,IAAI,EAAE,aAAa,OAAO,EAAE,UAAU,WAAW,aAAa,EAAE,UAAU,OAAO,IAAI;AAC3F,SAAO,IAAI,EAAE,eAAe,SAAS,EAAE,WAAW,SAAS,WAAW,QAAQ,CAAC,IAAI;AACrF;AAGO,SAAS,eAAe,QAAkB,MAA2B;AAC1E,QAAM,MAAM,CAAC,MAAe,IAAI,OAAO,KAAK,EAAE,EAAE,QAAQ,MAAM,IAAI,CAAC;AACnE,QAAM,MAAM,CAAC,SAAS,GAAG,MAAM,EAAE,IAAI,GAAG,EAAE,KAAK,GAAG;AAClD,QAAM,QAAQ,KAAK,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,GAAG,OAAO,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,GAAG,EAAE,KAAK,GAAG,CAAC;AACnG,SAAO,WAAW,CAAC,KAAK,GAAG,KAAK,EAAE,KAAK,MAAM;AAC/C;AAEA,SAAS,YAAY,EAAE,QAAQ,QAAQ,OAAO,GAAyD;AACrG,QAAM,CAAC,MAAM,OAAO,IAAI,SAAsB,CAAC,CAAC;AAChD,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,UAAU,WAAW;AAC3B,QAAM,EAAE,KAAK,IAAI,QAAQ;AACzB,QAAM,iBAAiB,cAAc,IAAI;AAGzC,QAAM,CAAC,KAAK,IAAI,SAAS,MAAM,uBAAuB,QAAQ,MAAM,CAAC;AACrE,YAAU,MAAM,MAAM,uBAAuB,QAAQ,MAAM,GAAG,CAAC,QAAQ,MAAM,CAAC;AAE9E,YAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,UAAM,IAAI,MAAM,WAAW,IAAI,SAAS,QAAQ,SAAS,QAAQ,aAAa,GAAG,QAAQ,aAAa,MAAM,CAAC;AAC7G,WAAO,WAAW,GAAG,CAAC,SAAS,QAAQ,KAAK,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAA4B,EAAE,CAAC,CAAC;AAAA,EACtH,GAAG,CAAC,QAAQ,MAAM,CAAC;AAEnB,QAAM,UAAU,CAAC,MAAiB,QAAQ,SAAS,EAAE,aAAa,EAAE,UAAU,SAAS,IAAI,MAAM,SAAS,CAAC;AAC3G,QAAM,QAAQ,KAAK,OAAO,OAAO,EAAE;AAEnC,WAAS,YAAY;AACnB,UAAM,OAAO,IAAI,KAAK,CAAC,eAAe,QAAQ,IAAI,CAAC,GAAG,EAAE,MAAM,yBAAyB,CAAC;AACxF,UAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,UAAM,IAAI,SAAS,cAAc,GAAG;AACpC,MAAE,OAAO;AACT,MAAE,WAAW,eAAe,MAAM,KAAI,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AAC3E,MAAE,MAAM;AACR,QAAI,gBAAgB,GAAG;AAAA,EACzB;AAEA,iBAAe,UAAU,GAAc;AACrC,UAAM,KAAK,MAAM,QAAQ;AAAA,MACvB,OAAO;AAAA,MACP,aAAa;AAAA,MACb,cAAc;AAAA,MACd,MAAM;AAAA,IACR,CAAC;AACD,QAAI,CAAC,GAAI;AACT,UAAM,MAAM,eAAe;AAC3B,QAAI,CAAC,IAAK;AACV,QAAI;AACF,YAAM,SAAS,KAAK,kBAAkB,EAAE,EAAE,QAAQ,QAAQ,cAAc,EAAE,GAAG,CAAC;AAC9E,YAAM,EAAE,OAAO,uBAAuB,CAAC;AAAA,IACzC,SAAS,GAAG;AACV,YAAM,EAAE,OAAO,uBAAuB,aAAc,EAAY,SAAS,MAAM,QAAQ,CAAC;AAAA,IAC1F;AAAA,EACF;AAEA,SACE,qBAAC,QACC;AAAA,yBAAC,SAAI,WAAU,0DACb;AAAA,2BAAC,QAAG,WAAU,yBAAwB;AAAA;AAAA,QACxB,oBAAC,SAAO,eAAK,QAAO;AAAA,QAC/B,QAAQ,KAAK,qBAAC,SAAM,MAAK,SAAS;AAAA;AAAA,UAAM;AAAA,WAAM;AAAA,SACjD;AAAA,MACC,KAAK,SAAS,KACb,oBAAC,UAAO,SAAQ,aAAY,MAAK,MAAK,SAAS,WAAW,2BAE1D;AAAA,OAEJ;AAAA,IACC,KAAK,WAAW,IACf,oBAAC,OAAE,WAAU,0BAAyB,mCAAqB,IAE3D,oBAAC,SAAI,WAAU,cACZ,eAAK,IAAI,CAAC,MACT,qBAAC,SAAe,WAAW,iCAAiC,QAAQ,CAAC,IAAI,oCAAoC,kBAAkB,IAC7H;AAAA,2BAAC,SAAI,WAAU,uEACb;AAAA,6BAAC,UAAM;AAAA,mBAAS,CAAC;AAAA,UAAG,QAAQ,CAAC,IAAI,gBAAa;AAAA,WAAG;AAAA,QAChD,kBACC,oBAAC,YAAO,MAAK,UAAS,WAAU,gCAA+B,SAAS,MAAM,KAAK,UAAU,CAAC,GAAG,yBAEjG;AAAA,SAEJ;AAAA,MACC,OAAO,QAAQ,EAAE,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MACxC,qBAAC,SACC;AAAA,6BAAC,UAAK,WAAU,8BAA8B;AAAA;AAAA,UAAE;AAAA,WAAC;AAAA,QAAO;AAAA,QAAE,OAAO,CAAC;AAAA,WAD1D,CAEV,CACD;AAAA,SAbO,EAAE,EAcZ,CACD,GACH;AAAA,KAEJ;AAEJ;","names":[]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Timestamp } from "firebase/firestore";
|
|
2
|
+
const sleutel = (siteId, formId) => `xicms-forms-gezien:${siteId}:${formId}`;
|
|
3
|
+
function formulierenGezienSinds(siteId, formId) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = localStorage.getItem(sleutel(siteId, formId));
|
|
6
|
+
const ms = raw ? Number(raw) : 0;
|
|
7
|
+
return ms > 0 ? Timestamp.fromMillis(ms) : null;
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function markeerFormulierGezien(siteId, formId) {
|
|
13
|
+
try {
|
|
14
|
+
localStorage.setItem(sleutel(siteId, formId), String(Date.now()));
|
|
15
|
+
} catch {
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
formulierenGezienSinds,
|
|
20
|
+
markeerFormulierGezien
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=gezien.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/forms/gezien.ts"],"sourcesContent":["import { Timestamp } from \"firebase/firestore\";\n\n/**\n * \"Nieuw sinds je laatste bezoek\" voor formulier-inzendingen (7.24).\n *\n * Het moment staat LOKAAL (localStorage, per site en formulier), niet in Firestore. Een gelezen-\n * vlag per inzending zou een write per beheerder per inzending vragen — en de rules houden\n * inzendingen bewust write-only vanaf de client (audit-trail). Lokaal is goed genoeg voor waar\n * het voor dient: de vraag \"is er iets bijgekomen sinds ik keek?\" op dít apparaat.\n */\nconst sleutel = (siteId: string, formId: string) => `xicms-forms-gezien:${siteId}:${formId}`;\n\nexport function formulierenGezienSinds(siteId: string, formId: string): Timestamp | null {\n try {\n const raw = localStorage.getItem(sleutel(siteId, formId));\n const ms = raw ? Number(raw) : 0;\n return ms > 0 ? Timestamp.fromMillis(ms) : null;\n } catch {\n return null;\n }\n}\n\nexport function markeerFormulierGezien(siteId: string, formId: string): void {\n try {\n localStorage.setItem(sleutel(siteId, formId), String(Date.now()));\n } catch {\n /* opslag geweigerd: dan blijft alles \"nieuw\" — nooit een fout hierom */\n }\n}\n"],"mappings":"AAAA,SAAS,iBAAiB;AAU1B,MAAM,UAAU,CAAC,QAAgB,WAAmB,sBAAsB,MAAM,IAAI,MAAM;AAEnF,SAAS,uBAAuB,QAAgB,QAAkC;AACvF,MAAI;AACF,UAAM,MAAM,aAAa,QAAQ,QAAQ,QAAQ,MAAM,CAAC;AACxD,UAAM,KAAK,MAAM,OAAO,GAAG,IAAI;AAC/B,WAAO,KAAK,IAAI,UAAU,WAAW,EAAE,IAAI;AAAA,EAC7C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,uBAAuB,QAAgB,QAAsB;AAC3E,MAAI;AACF,iBAAa,QAAQ,QAAQ,QAAQ,MAAM,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC;AAAA,EAClE,QAAQ;AAAA,EAER;AACF;","names":[]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kleurhulpjes voor het Huisstijl-scherm (7.24): hex ↔ getallen en het WCAG-contrast.
|
|
3
|
+
*
|
|
4
|
+
* Klanten kozen een merkkleur op gevoel en zagen pas op de site dat witte knoptekst op een
|
|
5
|
+
* lichtgele knop onleesbaar is. Het contrastcijfer erbij zetten voorkomt dat gesprek.
|
|
6
|
+
*/
|
|
7
|
+
/** "#abc" / "#aabbcc" / "aabbcc" → [r, g, b], of null bij iets wat geen hex-kleur is. */
|
|
8
|
+
export declare function hexNaarRgb(hex: string | undefined): [number, number, number] | null;
|
|
9
|
+
/** Naar een 6-cijferige hex voor `<input type="color">` (die accepteert alleen #rrggbb). */
|
|
10
|
+
export declare function naarHex6(kleur: string | undefined, fallback?: string): string;
|
|
11
|
+
/** Relatieve luminantie (WCAG 2.x). */
|
|
12
|
+
export declare function luminantie(rgb: [number, number, number]): number;
|
|
13
|
+
/** Contrastverhouding tussen twee kleuren (1 = geen contrast, 21 = zwart op wit). */
|
|
14
|
+
export declare function contrast(a: string | undefined, b: string | undefined): number | null;
|
|
15
|
+
/** Welke tekstkleur komt er op een knop in deze kleur? (Spiegelt de on-accent-afleiding in CSS.) */
|
|
16
|
+
export declare function tekstOp(kleur: string | undefined): "#ffffff" | "#0f172a";
|
|
17
|
+
/**
|
|
18
|
+
* Oordeel in gewone woorden. AA voor normale tekst is 4.5; voor grote tekst en knoppen volstaat 3.
|
|
19
|
+
* We meten tegen 3 (knoppen zijn het hoofdgebruik van de merkkleur) en waarschuwen daaronder.
|
|
20
|
+
*/
|
|
21
|
+
export declare function contrastOordeel(ratio: number | null): {
|
|
22
|
+
tekst: string;
|
|
23
|
+
toon: "goed" | "matig" | "slecht";
|
|
24
|
+
} | null;
|
|
25
|
+
/** Curated lettertype-paren voor het Huisstijl-scherm — één keuze, meteen goed. */
|
|
26
|
+
export declare const LETTERTYPE_PAREN: {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
fontFamily: string;
|
|
30
|
+
headingFont?: string;
|
|
31
|
+
fontUrl: string;
|
|
32
|
+
}[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
function hexNaarRgb(hex) {
|
|
2
|
+
if (!hex) return null;
|
|
3
|
+
const m = hex.trim().replace(/^#/, "");
|
|
4
|
+
const vol = m.length === 3 ? m.split("").map((c) => c + c).join("") : m;
|
|
5
|
+
if (!/^[0-9a-fA-F]{6}$/.test(vol)) return null;
|
|
6
|
+
return [parseInt(vol.slice(0, 2), 16), parseInt(vol.slice(2, 4), 16), parseInt(vol.slice(4, 6), 16)];
|
|
7
|
+
}
|
|
8
|
+
function naarHex6(kleur, fallback = "#000000") {
|
|
9
|
+
const rgb = hexNaarRgb(kleur);
|
|
10
|
+
if (!rgb) return fallback;
|
|
11
|
+
return `#${rgb.map((n) => n.toString(16).padStart(2, "0")).join("")}`;
|
|
12
|
+
}
|
|
13
|
+
function lineair(c) {
|
|
14
|
+
const v = c / 255;
|
|
15
|
+
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
16
|
+
}
|
|
17
|
+
function luminantie(rgb) {
|
|
18
|
+
return 0.2126 * lineair(rgb[0]) + 0.7152 * lineair(rgb[1]) + 0.0722 * lineair(rgb[2]);
|
|
19
|
+
}
|
|
20
|
+
function contrast(a, b) {
|
|
21
|
+
const ra = hexNaarRgb(a);
|
|
22
|
+
const rb = hexNaarRgb(b);
|
|
23
|
+
if (!ra || !rb) return null;
|
|
24
|
+
const la = luminantie(ra);
|
|
25
|
+
const lb = luminantie(rb);
|
|
26
|
+
const [licht, donker] = la > lb ? [la, lb] : [lb, la];
|
|
27
|
+
return (licht + 0.05) / (donker + 0.05);
|
|
28
|
+
}
|
|
29
|
+
function tekstOp(kleur) {
|
|
30
|
+
const rgb = hexNaarRgb(kleur);
|
|
31
|
+
if (!rgb) return "#ffffff";
|
|
32
|
+
return luminantie(rgb) > 0.4 ? "#0f172a" : "#ffffff";
|
|
33
|
+
}
|
|
34
|
+
function contrastOordeel(ratio) {
|
|
35
|
+
if (ratio === null) return null;
|
|
36
|
+
const r = Math.round(ratio * 10) / 10;
|
|
37
|
+
if (ratio >= 4.5) return { tekst: `Contrast ${r}:1 \u2014 goed leesbaar`, toon: "goed" };
|
|
38
|
+
if (ratio >= 3) return { tekst: `Contrast ${r}:1 \u2014 voldoende voor knoppen en koppen, krap voor kleine tekst`, toon: "matig" };
|
|
39
|
+
return { tekst: `Contrast ${r}:1 \u2014 te laag; kies een donkerdere of lichtere tint`, toon: "slecht" };
|
|
40
|
+
}
|
|
41
|
+
const LETTERTYPE_PAREN = [
|
|
42
|
+
{ id: "inter", label: "Inter \u2014 helder & neutraal", fontFamily: "'Inter', system-ui, sans-serif", fontUrl: "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" },
|
|
43
|
+
{ id: "inter-sora", label: "Inter + Sora \u2014 modern met karakter", fontFamily: "'Inter', system-ui, sans-serif", headingFont: "'Sora', 'Inter', sans-serif", fontUrl: "https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Sora:wght@600;700&display=swap" },
|
|
44
|
+
{ id: "poppins", label: "Poppins \u2014 rond & vriendelijk", fontFamily: "'Poppins', system-ui, sans-serif", fontUrl: "https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" },
|
|
45
|
+
{ id: "nunito", label: "Nunito \u2014 zacht & toegankelijk", fontFamily: "'Nunito', system-ui, sans-serif", fontUrl: "https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap" },
|
|
46
|
+
{ id: "lora-source", label: "Source Sans + Lora \u2014 klassiek & warm", fontFamily: "'Source Sans 3', system-ui, sans-serif", headingFont: "'Lora', Georgia, serif", fontUrl: "https://fonts.googleapis.com/css2?family=Lora:wght@600;700&family=Source+Sans+3:wght@400;600;700&display=swap" },
|
|
47
|
+
{ id: "playfair", label: "Inter + Playfair Display \u2014 elegant", fontFamily: "'Inter', system-ui, sans-serif", headingFont: "'Playfair Display', Georgia, serif", fontUrl: "https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Playfair+Display:wght@600;700&display=swap" },
|
|
48
|
+
{ id: "manrope", label: "Manrope \u2014 strak & technisch", fontFamily: "'Manrope', system-ui, sans-serif", fontUrl: "https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" },
|
|
49
|
+
{ id: "systeem", label: "Systeemlettertype \u2014 snelst, geen download", fontFamily: "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif", fontUrl: "" }
|
|
50
|
+
];
|
|
51
|
+
export {
|
|
52
|
+
LETTERTYPE_PAREN,
|
|
53
|
+
contrast,
|
|
54
|
+
contrastOordeel,
|
|
55
|
+
hexNaarRgb,
|
|
56
|
+
luminantie,
|
|
57
|
+
naarHex6,
|
|
58
|
+
tekstOp
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=kleur.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/lib/kleur.ts"],"sourcesContent":["/**\n * Kleurhulpjes voor het Huisstijl-scherm (7.24): hex ↔ getallen en het WCAG-contrast.\n *\n * Klanten kozen een merkkleur op gevoel en zagen pas op de site dat witte knoptekst op een\n * lichtgele knop onleesbaar is. Het contrastcijfer erbij zetten voorkomt dat gesprek.\n */\n\n/** \"#abc\" / \"#aabbcc\" / \"aabbcc\" → [r, g, b], of null bij iets wat geen hex-kleur is. */\nexport function hexNaarRgb(hex: string | undefined): [number, number, number] | null {\n if (!hex) return null;\n const m = hex.trim().replace(/^#/, \"\");\n const vol = m.length === 3 ? m.split(\"\").map((c) => c + c).join(\"\") : m;\n if (!/^[0-9a-fA-F]{6}$/.test(vol)) return null;\n return [parseInt(vol.slice(0, 2), 16), parseInt(vol.slice(2, 4), 16), parseInt(vol.slice(4, 6), 16)];\n}\n\n/** Naar een 6-cijferige hex voor `<input type=\"color\">` (die accepteert alleen #rrggbb). */\nexport function naarHex6(kleur: string | undefined, fallback = \"#000000\"): string {\n const rgb = hexNaarRgb(kleur);\n if (!rgb) return fallback;\n return `#${rgb.map((n) => n.toString(16).padStart(2, \"0\")).join(\"\")}`;\n}\n\nfunction lineair(c: number): number {\n const v = c / 255;\n return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;\n}\n\n/** Relatieve luminantie (WCAG 2.x). */\nexport function luminantie(rgb: [number, number, number]): number {\n return 0.2126 * lineair(rgb[0]) + 0.7152 * lineair(rgb[1]) + 0.0722 * lineair(rgb[2]);\n}\n\n/** Contrastverhouding tussen twee kleuren (1 = geen contrast, 21 = zwart op wit). */\nexport function contrast(a: string | undefined, b: string | undefined): number | null {\n const ra = hexNaarRgb(a);\n const rb = hexNaarRgb(b);\n if (!ra || !rb) return null;\n const la = luminantie(ra);\n const lb = luminantie(rb);\n const [licht, donker] = la > lb ? [la, lb] : [lb, la];\n return (licht + 0.05) / (donker + 0.05);\n}\n\n/** Welke tekstkleur komt er op een knop in deze kleur? (Spiegelt de on-accent-afleiding in CSS.) */\nexport function tekstOp(kleur: string | undefined): \"#ffffff\" | \"#0f172a\" {\n const rgb = hexNaarRgb(kleur);\n if (!rgb) return \"#ffffff\";\n return luminantie(rgb) > 0.4 ? \"#0f172a\" : \"#ffffff\";\n}\n\n/**\n * Oordeel in gewone woorden. AA voor normale tekst is 4.5; voor grote tekst en knoppen volstaat 3.\n * We meten tegen 3 (knoppen zijn het hoofdgebruik van de merkkleur) en waarschuwen daaronder.\n */\nexport function contrastOordeel(ratio: number | null): { tekst: string; toon: \"goed\" | \"matig\" | \"slecht\" } | null {\n if (ratio === null) return null;\n const r = Math.round(ratio * 10) / 10;\n if (ratio >= 4.5) return { tekst: `Contrast ${r}:1 — goed leesbaar`, toon: \"goed\" };\n if (ratio >= 3) return { tekst: `Contrast ${r}:1 — voldoende voor knoppen en koppen, krap voor kleine tekst`, toon: \"matig\" };\n return { tekst: `Contrast ${r}:1 — te laag; kies een donkerdere of lichtere tint`, toon: \"slecht\" };\n}\n\n/** Curated lettertype-paren voor het Huisstijl-scherm — één keuze, meteen goed. */\nexport const LETTERTYPE_PAREN: { id: string; label: string; fontFamily: string; headingFont?: string; fontUrl: string }[] = [\n { id: \"inter\", label: \"Inter — helder & neutraal\", fontFamily: \"'Inter', system-ui, sans-serif\", fontUrl: \"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap\" },\n { id: \"inter-sora\", label: \"Inter + Sora — modern met karakter\", fontFamily: \"'Inter', system-ui, sans-serif\", headingFont: \"'Sora', 'Inter', sans-serif\", fontUrl: \"https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Sora:wght@600;700&display=swap\" },\n { id: \"poppins\", label: \"Poppins — rond & vriendelijk\", fontFamily: \"'Poppins', system-ui, sans-serif\", fontUrl: \"https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap\" },\n { id: \"nunito\", label: \"Nunito — zacht & toegankelijk\", fontFamily: \"'Nunito', system-ui, sans-serif\", fontUrl: \"https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&display=swap\" },\n { id: \"lora-source\", label: \"Source Sans + Lora — klassiek & warm\", fontFamily: \"'Source Sans 3', system-ui, sans-serif\", headingFont: \"'Lora', Georgia, serif\", fontUrl: \"https://fonts.googleapis.com/css2?family=Lora:wght@600;700&family=Source+Sans+3:wght@400;600;700&display=swap\" },\n { id: \"playfair\", label: \"Inter + Playfair Display — elegant\", fontFamily: \"'Inter', system-ui, sans-serif\", headingFont: \"'Playfair Display', Georgia, serif\", fontUrl: \"https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Playfair+Display:wght@600;700&display=swap\" },\n { id: \"manrope\", label: \"Manrope — strak & technisch\", fontFamily: \"'Manrope', system-ui, sans-serif\", fontUrl: \"https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap\" },\n { id: \"systeem\", label: \"Systeemlettertype — snelst, geen download\", fontFamily: \"system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif\", fontUrl: \"\" },\n];\n"],"mappings":"AAQO,SAAS,WAAW,KAA0D;AACnF,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,IAAI,IAAI,KAAK,EAAE,QAAQ,MAAM,EAAE;AACrC,QAAM,MAAM,EAAE,WAAW,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,MAAM,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI;AACtE,MAAI,CAAC,mBAAmB,KAAK,GAAG,EAAG,QAAO;AAC1C,SAAO,CAAC,SAAS,IAAI,MAAM,GAAG,CAAC,GAAG,EAAE,GAAG,SAAS,IAAI,MAAM,GAAG,CAAC,GAAG,EAAE,GAAG,SAAS,IAAI,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC;AACrG;AAGO,SAAS,SAAS,OAA2B,WAAW,WAAmB;AAChF,QAAM,MAAM,WAAW,KAAK;AAC5B,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,IAAI,IAAI,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;AACrE;AAEA,SAAS,QAAQ,GAAmB;AAClC,QAAM,IAAI,IAAI;AACd,SAAO,KAAK,UAAU,IAAI,UAAU,IAAI,SAAS,UAAU;AAC7D;AAGO,SAAS,WAAW,KAAuC;AAChE,SAAO,SAAS,QAAQ,IAAI,CAAC,CAAC,IAAI,SAAS,QAAQ,IAAI,CAAC,CAAC,IAAI,SAAS,QAAQ,IAAI,CAAC,CAAC;AACtF;AAGO,SAAS,SAAS,GAAuB,GAAsC;AACpF,QAAM,KAAK,WAAW,CAAC;AACvB,QAAM,KAAK,WAAW,CAAC;AACvB,MAAI,CAAC,MAAM,CAAC,GAAI,QAAO;AACvB,QAAM,KAAK,WAAW,EAAE;AACxB,QAAM,KAAK,WAAW,EAAE;AACxB,QAAM,CAAC,OAAO,MAAM,IAAI,KAAK,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;AACpD,UAAQ,QAAQ,SAAS,SAAS;AACpC;AAGO,SAAS,QAAQ,OAAkD;AACxE,QAAM,MAAM,WAAW,KAAK;AAC5B,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,WAAW,GAAG,IAAI,MAAM,YAAY;AAC7C;AAMO,SAAS,gBAAgB,OAAmF;AACjH,MAAI,UAAU,KAAM,QAAO;AAC3B,QAAM,IAAI,KAAK,MAAM,QAAQ,EAAE,IAAI;AACnC,MAAI,SAAS,IAAK,QAAO,EAAE,OAAO,YAAY,CAAC,2BAAsB,MAAM,OAAO;AAClF,MAAI,SAAS,EAAG,QAAO,EAAE,OAAO,YAAY,CAAC,sEAAiE,MAAM,QAAQ;AAC5H,SAAO,EAAE,OAAO,YAAY,CAAC,2DAAsD,MAAM,SAAS;AACpG;AAGO,MAAM,mBAA+G;AAAA,EAC1H,EAAE,IAAI,SAAS,OAAO,kCAA6B,YAAY,kCAAkC,SAAS,mFAAmF;AAAA,EAC7L,EAAE,IAAI,cAAc,OAAO,2CAAsC,YAAY,kCAAkC,aAAa,+BAA+B,SAAS,wGAAwG;AAAA,EAC5Q,EAAE,IAAI,WAAW,OAAO,qCAAgC,YAAY,oCAAoC,SAAS,qFAAqF;AAAA,EACtM,EAAE,IAAI,UAAU,OAAO,sCAAiC,YAAY,mCAAmC,SAAS,oFAAoF;AAAA,EACpM,EAAE,IAAI,eAAe,OAAO,6CAAwC,YAAY,0CAA0C,aAAa,0BAA0B,SAAS,gHAAgH;AAAA,EAC1R,EAAE,IAAI,YAAY,OAAO,2CAAsC,YAAY,kCAAkC,aAAa,sCAAsC,SAAS,gHAAgH;AAAA,EACzR,EAAE,IAAI,WAAW,OAAO,oCAA+B,YAAY,oCAAoC,SAAS,yFAAyF;AAAA,EACzM,EAAE,IAAI,WAAW,OAAO,kDAA6C,YAAY,4DAA4D,SAAS,GAAG;AAC3J;","names":[]}
|
|
@@ -2,7 +2,24 @@ import type { Page } from "../../schema/pages.js";
|
|
|
2
2
|
export type DraftPage = Page & {
|
|
3
3
|
id: string;
|
|
4
4
|
published?: boolean;
|
|
5
|
+
/** Firestore-Timestamp (editor) of ISO-string (import-scripts). */
|
|
6
|
+
updatedAt?: unknown;
|
|
7
|
+
lastPublishedAt?: unknown;
|
|
8
|
+
/** 7.24: in de prullenbak; wordt na 30 dagen definitief verwijderd (purgeTrashedPages). */
|
|
9
|
+
trashed?: boolean;
|
|
10
|
+
trashedAt?: unknown;
|
|
5
11
|
};
|
|
12
|
+
/**
|
|
13
|
+
* Staat er op deze pagina iets dat nog niet live is? (7.24)
|
|
14
|
+
*
|
|
15
|
+
* Nooit gepubliceerd = concept (dat is een eigen toestand, geen "wijziging"). Gepubliceerd en
|
|
16
|
+
* daarna opnieuw opgeslagen = wijzigingen die niemand ziet. De marge van 1,5 s vangt de
|
|
17
|
+
* autosave die `publishPage` vlak vóór het publiceren zelf doet (die zet `updatedAt` een tel
|
|
18
|
+
* eerder dan `lastPublishedAt`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function heeftOngepubliceerdeWijzigingen(p: Pick<DraftPage, "published" | "updatedAt" | "lastPublishedAt">): boolean;
|
|
21
|
+
/** Leesbare toestand voor badges: live / gewijzigd / concept / prullenbak. */
|
|
22
|
+
export declare function paginaToestand(p: DraftPage): "prullenbak" | "gewijzigd" | "live" | "concept";
|
|
6
23
|
/** Live-lijst van pagina's (werkversies in `drafts/`). */
|
|
7
24
|
export declare function subscribePages(siteId: string, cb: (pages: DraftPage[]) => void): () => void;
|
|
8
25
|
export declare function subscribeVersions(siteId: string, pageId: string, cb: (versions: {
|
|
@@ -40,6 +57,21 @@ export declare function createPage(siteId: string, input: Partial<Page> & {
|
|
|
40
57
|
path: string;
|
|
41
58
|
}): Promise<string>;
|
|
42
59
|
export declare function saveDraft(siteId: string, pageId: string, partial: Partial<Page>): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Pagina dupliceren (7.24): nieuwe draft met dezelfde blokken (verse `_key`s), als concept, met
|
|
62
|
+
* "-kopie" achter pad en titel zodat hij nergens mee botst.
|
|
63
|
+
*/
|
|
64
|
+
export declare function duplicatePage(siteId: string, bron: DraftPage): Promise<string>;
|
|
65
|
+
/** Offline halen (7.24): de live versie verdwijnt, de werkversie blijft. Via de callable. */
|
|
66
|
+
export declare function unpublishPage(siteId: string, pageId: string): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* Naar de prullenbak (7.24): offline halen + markeren. Geen delete — een klant die per ongeluk
|
|
69
|
+
* op "verwijderen" drukte kon tot 7.24 alleen via een herstelpunt terug. Na 30 dagen ruimt de
|
|
70
|
+
* backend definitief op (purgeTrashedPages).
|
|
71
|
+
*/
|
|
72
|
+
export declare function trashPage(siteId: string, page: DraftPage): Promise<void>;
|
|
73
|
+
/** Terug uit de prullenbak: als concept (publiceren is een bewuste tweede stap). */
|
|
74
|
+
export declare function restorePage(siteId: string, pageId: string): Promise<void>;
|
|
43
75
|
export declare function deletePage(siteId: string, pageId: string): Promise<void>;
|
|
44
76
|
/**
|
|
45
77
|
* Herstelt een eerdere versie naar de WERKVERSIE (draft). De gebruiker moet daarna zelf
|
|
@@ -18,6 +18,26 @@ import { callable } from "../../firebase/functionsClient.js";
|
|
|
18
18
|
import { getDb, getFirebaseApp } from "../../firebase/client.js";
|
|
19
19
|
import { isDemoActive, demoIdForPath, resolveDemoPage, saveDemoDraft } from "../../demo/demoStore.js";
|
|
20
20
|
import { sanitizeDraftBlocks } from "./sanitizeDraft.js";
|
|
21
|
+
function ms(v) {
|
|
22
|
+
if (v && typeof v.toDate === "function") return v.toDate().getTime();
|
|
23
|
+
if (typeof v === "string") {
|
|
24
|
+
const t = Date.parse(v);
|
|
25
|
+
return Number.isNaN(t) ? 0 : t;
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
function heeftOngepubliceerdeWijzigingen(p) {
|
|
30
|
+
if (!p.published) return false;
|
|
31
|
+
const u = ms(p.updatedAt);
|
|
32
|
+
const l = ms(p.lastPublishedAt);
|
|
33
|
+
if (!u || !l) return false;
|
|
34
|
+
return u - l > 1500;
|
|
35
|
+
}
|
|
36
|
+
function paginaToestand(p) {
|
|
37
|
+
if (p.trashed) return "prullenbak";
|
|
38
|
+
if (heeftOngepubliceerdeWijzigingen(p)) return "gewijzigd";
|
|
39
|
+
return p.published ? "live" : "concept";
|
|
40
|
+
}
|
|
21
41
|
function subscribePages(siteId, cb) {
|
|
22
42
|
const db = getDb();
|
|
23
43
|
if (!db) {
|
|
@@ -112,6 +132,39 @@ async function saveDraft(siteId, pageId, partial) {
|
|
|
112
132
|
{ merge: true }
|
|
113
133
|
);
|
|
114
134
|
}
|
|
135
|
+
async function duplicatePage(siteId, bron) {
|
|
136
|
+
const nieuweKey = () => (globalThis.crypto?.randomUUID?.() ?? `b${Date.now()}${Math.random()}`).slice(0, 12);
|
|
137
|
+
const blocks = (bron.blocks ?? []).map((b) => ({ ...b, _key: nieuweKey() }));
|
|
138
|
+
const slug = `${bron.slug || "pagina"}-kopie`;
|
|
139
|
+
const path = bron.path === "/" ? "/home-kopie" : `${bron.path}-kopie`;
|
|
140
|
+
return createPage(siteId, {
|
|
141
|
+
title: `${bron.title} (kopie)`,
|
|
142
|
+
slug,
|
|
143
|
+
path,
|
|
144
|
+
blocks,
|
|
145
|
+
...bron.seo ? { seo: bron.seo } : {},
|
|
146
|
+
...bron.translations ? { translations: bron.translations } : {}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async function unpublishPage(siteId, pageId) {
|
|
150
|
+
if (isDemoActive()) return;
|
|
151
|
+
const app = getFirebaseApp();
|
|
152
|
+
if (!app) throw new Error("Firebase niet geconfigureerd.");
|
|
153
|
+
await callable(app, "unpublishPage")({ siteId, pageId });
|
|
154
|
+
}
|
|
155
|
+
async function trashPage(siteId, page) {
|
|
156
|
+
const db = getDb();
|
|
157
|
+
if (!db) return;
|
|
158
|
+
if (page.published) {
|
|
159
|
+
await unpublishPage(siteId, page.id).catch(() => void 0);
|
|
160
|
+
}
|
|
161
|
+
await setDoc(doc(db, "sites", siteId, "drafts", page.id), { trashed: true, trashedAt: serverTimestamp(), updatedAt: serverTimestamp() }, { merge: true });
|
|
162
|
+
}
|
|
163
|
+
async function restorePage(siteId, pageId) {
|
|
164
|
+
const db = getDb();
|
|
165
|
+
if (!db) return;
|
|
166
|
+
await setDoc(doc(db, "sites", siteId, "drafts", pageId), { trashed: false, trashedAt: null, updatedAt: serverTimestamp() }, { merge: true });
|
|
167
|
+
}
|
|
115
168
|
async function deletePage(siteId, pageId) {
|
|
116
169
|
const db = getDb();
|
|
117
170
|
if (!db) return;
|
|
@@ -154,13 +207,19 @@ async function publishPage(siteId, pageId) {
|
|
|
154
207
|
export {
|
|
155
208
|
createPage,
|
|
156
209
|
deletePage,
|
|
210
|
+
duplicatePage,
|
|
157
211
|
getDraftByPath,
|
|
158
212
|
getPublishedPage,
|
|
213
|
+
heeftOngepubliceerdeWijzigingen,
|
|
214
|
+
paginaToestand,
|
|
159
215
|
publishPage,
|
|
216
|
+
restorePage,
|
|
160
217
|
restoreVersion,
|
|
161
218
|
saveDraft,
|
|
162
219
|
subscribeDraft,
|
|
163
220
|
subscribePages,
|
|
164
|
-
subscribeVersions
|
|
221
|
+
subscribeVersions,
|
|
222
|
+
trashPage,
|
|
223
|
+
unpublishPage
|
|
165
224
|
};
|
|
166
225
|
//# sourceMappingURL=pagesClient.js.map
|