@xiwebdesign/xicms-core 7.16.0 → 7.16.1
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/dashboard/DashboardPanel.js +17 -4
- package/dist/admin/dashboard/DashboardPanel.js.map +1 -1
- package/dist/admin/pages/PagesPanel.js +3 -2
- package/dist/admin/pages/PagesPanel.js.map +1 -1
- package/dist/admin/shop/ShopSettingsTab.js +7 -9
- package/dist/admin/shop/ShopSettingsTab.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +0 -2
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/auth/AuthProvider.js +1 -1
- package/dist/auth/AuthProvider.js.map +1 -1
- package/dist/components/EditBar.js +0 -2
- package/dist/components/EditBar.js.map +1 -1
- package/dist/styles.css +8 -8
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -20,9 +20,18 @@ import { formatCents } from "../../schema/shop.js";
|
|
|
20
20
|
import { Badge, Button, Card, Input } from "../ui/primitives.js";
|
|
21
21
|
import { useToast } from "../ui/toast.js";
|
|
22
22
|
import { useAdminNav } from "../navContext.js";
|
|
23
|
-
function
|
|
24
|
-
if (
|
|
25
|
-
|
|
23
|
+
function updatedMs(v) {
|
|
24
|
+
if (v && typeof v.toDate === "function") return v.toDate().getTime();
|
|
25
|
+
if (typeof v === "string") {
|
|
26
|
+
const t = Date.parse(v);
|
|
27
|
+
return Number.isNaN(t) ? 0 : t;
|
|
28
|
+
}
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
function fmtDatum(v) {
|
|
32
|
+
const ms = updatedMs(v);
|
|
33
|
+
if (!ms) return "\u2014";
|
|
34
|
+
return new Date(ms).toLocaleString("nl-NL", { dateStyle: "short", timeStyle: "short" });
|
|
26
35
|
}
|
|
27
36
|
function WidgetKop({ titel, tab, tabLabel }) {
|
|
28
37
|
const gaNaar = useAdminNav();
|
|
@@ -65,7 +74,11 @@ function DashboardPanel({ siteId }) {
|
|
|
65
74
|
setFormulieren([]);
|
|
66
75
|
return;
|
|
67
76
|
}
|
|
68
|
-
getDocs(
|
|
77
|
+
getDocs(collection(db, "sites", siteId, "drafts")).then(
|
|
78
|
+
(snap) => setPaginas(
|
|
79
|
+
snap.docs.map((d) => ({ id: d.id, ...d.data() })).sort((a, b) => updatedMs(b.updatedAt) - updatedMs(a.updatedAt)).slice(0, 5)
|
|
80
|
+
)
|
|
81
|
+
).catch(() => setPaginas([]));
|
|
69
82
|
const nu = /* @__PURE__ */ new Date();
|
|
70
83
|
const maandId = `${nu.getUTCFullYear()}-${String(nu.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
71
84
|
getDoc(doc(db, "sites", siteId, "stats", maandId)).then((snap) => setMaand(snap.exists() ? snap.data() : null)).catch(() => setMaand(null));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/dashboard/DashboardPanel.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Startpagina van /beheer (7.16).\n *\n * /beheer opende recht in het pagina-overzicht: een lijst technische titels als eerste indruk, en\n * nergens een plek die uitlegt hoe het systeem in elkaar zit. Deze startpagina doet drie dingen:\n * uitleggen hoe je de site bewerkt (op de site zelf, niet hier), in één oogopslag tonen wat er\n * gebeurt (bezoek, bestellingen, inzendingen, laatst bewerkt), en snelkoppelingen bieden die de\n * siteadmin zelf kan instellen.\n *\n * Bewust eenmalige reads (geen live listeners): dit is een overzicht bij binnenkomst, geen\n * monitor. Elke widget vangt zijn eigen fouten — een dichtgezette collectie (rules, module uit)\n * mag nooit de hele startpagina blokkeren.\n */\nimport { useEffect, useMemo, useState } from \"react\";\nimport {\n collection,\n doc,\n getCountFromServer,\n getDoc,\n getDocs,\n limit,\n orderBy,\n query,\n} from \"firebase/firestore\";\nimport type { Timestamp } from \"firebase/firestore\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { useAuth } from \"../../auth/AuthProvider\";\nimport { canManageSite } from \"../../auth/roles\";\nimport { useModules } from \"../../modules/context\";\nimport { formatCents } from \"../../schema/shop\";\nimport { Badge, Button, Card, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useAdminNav } from \"../navContext\";\n\ninterface RecentePagina {\n id: string;\n title?: string;\n path?: string;\n updatedAt?: Timestamp;\n}\ninterface MaandStat {\n total?: number;\n days?: Record<string, number>;\n paths?: Record<string, number>;\n}\ninterface RecenteOrder {\n id: string;\n status?: string;\n createdAt?: Timestamp;\n customer?: { name?: string };\n amount?: { totalCents?: number };\n}\ninterface FormOverzicht {\n id: string;\n name: string;\n aantal: number;\n laatste?: Timestamp;\n}\ninterface Snelkoppeling {\n label: string;\n href: string;\n}\n\nfunction fmtDatum(t?: Timestamp): string {\n if (!t?.toDate) return \"—\";\n return t.toDate().toLocaleString(\"nl-NL\", { dateStyle: \"short\", timeStyle: \"short\" });\n}\n\n/** Kop van een widget met een \"ga ernaartoe\"-pijl die naar het bijbehorende paneel springt. */\nfunction WidgetKop({ titel, tab, tabLabel }: { titel: string; tab?: string; tabLabel?: string }) {\n const gaNaar = useAdminNav();\n return (\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">{titel}</h2>\n {tab && (\n <button\n type=\"button\"\n onClick={() => gaNaar(tab)}\n className=\"shrink-0 text-xs font-medium text-brand-700 hover:underline focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n >\n {tabLabel ?? \"Bekijk alles\"} →\n </button>\n )}\n </div>\n );\n}\n\nexport function DashboardPanel({ siteId }: { siteId: string }) {\n const { role } = useAuth();\n const { activeIds } = useModules();\n const { toast } = useToast();\n const gaNaar = useAdminNav();\n const heeftShop = activeIds.has(\"shop\");\n const magInstellen = canManageSite(role);\n\n const [paginas, setPaginas] = useState<RecentePagina[] | null>(null);\n const [maand, setMaand] = useState<MaandStat | null | undefined>(undefined);\n const [orders, setOrders] = useState<RecenteOrder[] | null>(null);\n const [formulieren, setFormulieren] = useState<FormOverzicht[] | null>(null);\n const [links, setLinks] = useState<Snelkoppeling[]>([]);\n const [linksBewerken, setLinksBewerken] = useState<Snelkoppeling[] | null>(null);\n const [linksOpslaan, setLinksOpslaan] = useState(false);\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setPaginas([]);\n setMaand(null);\n setOrders([]);\n setFormulieren([]);\n return;\n }\n\n // Laatst bewerkte pagina's. orderBy laat docs zónder updatedAt weg — dat zijn alleen\n // stokoude drafts die nooit meer zijn aangeraakt, en die horen hier ook niet bovenaan.\n getDocs(query(collection(db, \"sites\", siteId, \"drafts\"), orderBy(\"updatedAt\", \"desc\"), limit(5)))\n .then((snap) => setPaginas(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<RecentePagina, \"id\">) }))))\n .catch(() => setPaginas([]));\n\n // Bezoek deze maand (zelfde bron als het Statistieken-paneel).\n const nu = new Date();\n const maandId = `${nu.getUTCFullYear()}-${String(nu.getUTCMonth() + 1).padStart(2, \"0\")}`;\n getDoc(doc(db, \"sites\", siteId, \"stats\", maandId))\n .then((snap) => setMaand(snap.exists() ? (snap.data() as MaandStat) : null))\n .catch(() => setMaand(null));\n\n // Laatste bestellingen — alleen als de shop-module actief is.\n if (heeftShop) {\n getDocs(query(collection(db, \"sites\", siteId, \"orders\"), orderBy(\"createdAt\", \"desc\"), limit(5)))\n .then((snap) => setOrders(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<RecenteOrder, \"id\">) }))))\n .catch(() => setOrders([]));\n } else {\n setOrders([]);\n }\n\n // Formulieren: naam + aantal inzendingen + laatste inzending (aggregatie-count, geen\n // volledige submissions-download). Max vier formulieren — de widget is een signaal, geen lijst.\n getDocs(collection(db, \"sites\", siteId, \"forms\"))\n .then(async (snap) => {\n const forms = snap.docs.slice(0, 4);\n const uit = await Promise.all(\n forms.map(async (f) => {\n const subs = collection(db, \"sites\", siteId, \"forms\", f.id, \"submissions\");\n const [count, laatste] = await Promise.all([\n getCountFromServer(subs).then((c) => c.data().count).catch(() => 0),\n getDocs(query(subs, orderBy(\"createdAt\", \"desc\"), limit(1)))\n .then((s) => (s.docs[0]?.data() as { createdAt?: Timestamp } | undefined)?.createdAt)\n .catch(() => undefined),\n ]);\n return { id: f.id, name: (f.data() as { name?: string }).name ?? f.id, aantal: count, laatste };\n }),\n );\n setFormulieren(uit);\n })\n .catch(() => setFormulieren([]));\n\n // Snelkoppelingen uit settings/site (editor mag lezen; instellen is siteadmin-werk).\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"site\"))\n .then((snap) => {\n const d = snap.exists() ? (snap.data() as { dashboardLinks?: Snelkoppeling[] }) : {};\n setLinks(Array.isArray(d.dashboardLinks) ? d.dashboardLinks : []);\n })\n .catch(() => setLinks([]));\n }, [siteId, heeftShop]);\n\n const vandaag = useMemo(() => {\n const dag = String(new Date().getUTCDate()).padStart(2, \"0\");\n return maand?.days?.[dag] ?? 0;\n }, [maand]);\n const topPaden = useMemo(\n () => Object.entries(maand?.paths ?? {}).sort((a, b) => b[1] - a[1]).slice(0, 3),\n [maand],\n );\n\n async function slaLinksOp() {\n if (!linksBewerken) return;\n const schoon = linksBewerken.filter((l) => l.label.trim() && l.href.trim());\n const app = getFirebaseApp();\n if (!app) return;\n setLinksOpslaan(true);\n try {\n // Zelfde route als de rest van de instellingen: de saveSiteSettings-callable (merge).\n const fn = callable(app, \"saveSiteSettings\");\n await fn({ siteId, settings: { dashboardLinks: schoon } });\n setLinks(schoon);\n setLinksBewerken(null);\n toast({ title: \"Snelkoppelingen opgeslagen\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setLinksOpslaan(false);\n }\n }\n\n return (\n <div className=\"grid gap-4\">\n {/* ── Zo werkt het ── */}\n <Card>\n <h2 className=\"text-sm font-semibold text-slate-800\">Zo werkt je site</h2>\n <div className=\"mt-2 grid gap-2 text-sm text-slate-600 md:grid-cols-3\">\n <p>\n <strong className=\"text-slate-800\">1 · Bewerk op de site zelf.</strong> Klik rechtsboven op{\" \"}\n <em>Bekijk site</em> en kies <em>Bewerken</em> in de zwarte balk. Elk blok krijgt een{\" \"}\n potlood (tekst) en een tandwiel (instellingen) — je typt gewoon in de pagina.\n </p>\n <p>\n <strong className=\"text-slate-800\">2 · Publiceer als het goed is.</strong> Alles wat je\n wijzigt is eerst een <em>werkversie</em>: bezoekers zien er niets van. Pas de knop{\" \"}\n <em>Publiceren</em> (rechtsonder) zet het live — en <em>Verwerpen</em> gooit het weg.\n </p>\n <p>\n <strong className=\"text-slate-800\">3 · De rest staat hier.</strong> Nieuwe pagina's, het\n menu, foto's, formulieren{heeftShop ? \", de webwinkel\" : \"\"} en statistieken vind je in\n de secties hiernaast. Onderaan de zijbalk staat ook een korte helptekst.\n </p>\n </div>\n </Card>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n {/* ── Statistieken ── */}\n <Card>\n <WidgetKop titel=\"Bezoek deze maand\" tab=\"stats\" tabLabel=\"Statistieken\" />\n {maand === undefined ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : maand === null ? (\n <p className=\"text-sm text-slate-500\">Nog geen bezoekersdata deze maand.</p>\n ) : (\n <div className=\"grid gap-3\">\n <div className=\"flex items-baseline gap-4\">\n <p><span className=\"text-2xl font-semibold text-slate-900\">{maand.total ?? 0}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">weergaven deze maand</span></p>\n <p><span className=\"text-lg font-semibold text-slate-700\">{vandaag}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">vandaag</span></p>\n </div>\n {topPaden.length > 0 && (\n <ul className=\"grid gap-1 text-sm text-slate-600\">\n {topPaden.map(([pad, n]) => (\n <li key={pad} className=\"flex justify-between gap-2\">\n <span className=\"truncate\" title={pad}>{pad}</span>\n <span className=\"shrink-0 text-slate-400\">{n}</span>\n </li>\n ))}\n </ul>\n )}\n </div>\n )}\n </Card>\n\n {/* ── Laatst bewerkte pagina's ── */}\n <Card>\n <WidgetKop titel=\"Laatst bewerkt\" tab=\"pages\" tabLabel=\"Pagina's\" />\n {paginas === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : paginas.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bewerkte pagina's.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {paginas.map((p) => (\n <li key={p.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"pages\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n title={`${p.title ?? p.id} — ${p.path ?? \"\"}`}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{p.title ?? p.id}</span>\n <span className=\"block truncate text-xs text-slate-400\">{p.path}</span>\n </span>\n <span className=\"shrink-0 text-xs text-slate-400\">{fmtDatum(p.updatedAt)}</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Bestellingen (alleen met shop) ── */}\n {heeftShop && (\n <Card>\n <WidgetKop titel=\"Laatste bestellingen\" tab=\"shop\" tabLabel=\"Webwinkel\" />\n {orders === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : orders.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bestellingen.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {orders.map((o) => (\n <li key={o.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"shop\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{o.customer?.name ?? \"Onbekend\"}</span>\n <span className=\"block text-xs text-slate-400\">{fmtDatum(o.createdAt)}</span>\n </span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {typeof o.amount?.totalCents === \"number\" && (\n <span className=\"text-sm font-medium text-slate-700\">{formatCents(o.amount.totalCents)}</span>\n )}\n <Badge tone={o.status === \"paid\" || o.status === \"shipped\" ? \"green\" : \"amber\"}>\n {o.status ?? \"?\"}\n </Badge>\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n )}\n\n {/* ── Formulieren ── */}\n <Card>\n <WidgetKop titel=\"Formulieren\" tab=\"forms\" tabLabel=\"Formulieren\" />\n {formulieren === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : formulieren.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen formulieren.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {formulieren.map((f) => (\n <li key={f.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"forms\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"truncate text-slate-800\" title={f.name}>{f.name}</span>\n <span className=\"shrink-0 text-xs text-slate-500\">\n {f.aantal} {f.aantal === 1 ? \"inzending\" : \"inzendingen\"}\n {f.laatste ? ` · laatste ${fmtDatum(f.laatste)}` : \"\"}\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Snelkoppelingen ── */}\n <Card>\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">Snelkoppelingen</h2>\n {magInstellen && linksBewerken === null && (\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(links.length ? [...links] : [{ label: \"\", href: \"\" }])}>\n Instellen\n </Button>\n )}\n </div>\n {linksBewerken ? (\n <div className=\"grid gap-2\">\n <div className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2 text-xs font-medium text-slate-500\">\n <span>Naam</span>\n <span>Adres</span>\n <span aria-hidden=\"true\" />\n </div>\n {linksBewerken.map((l, i) => (\n <div key={i} className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2\">\n <Input value={l.label} placeholder=\"bv. Webwinkel\" aria-label=\"Naam van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, label: e.target.value } : x)))} />\n <Input value={l.href} placeholder=\"/webwinkel of https://…\" aria-label=\"Adres van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, href: e.target.value } : x)))} />\n <Button variant=\"ghost\" size=\"sm\" aria-label=\"Verwijderen\"\n onClick={() => setLinksBewerken(linksBewerken.filter((_, j) => j !== i))}>✕</Button>\n </div>\n ))}\n <div className=\"flex items-center justify-between gap-2\">\n <Button variant=\"secondary\" size=\"sm\" disabled={linksBewerken.length >= 12}\n onClick={() => setLinksBewerken([...linksBewerken, { label: \"\", href: \"\" }])}>\n + Snelkoppeling\n </Button>\n <div className=\"flex gap-2\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(null)}>Annuleren</Button>\n <Button size=\"sm\" disabled={linksOpslaan} onClick={() => void slaLinksOp()}>\n {linksOpslaan ? \"Bezig…\" : \"Opslaan\"}\n </Button>\n </div>\n </div>\n </div>\n ) : links.length === 0 ? (\n <p className=\"text-sm text-slate-500\">\n {magInstellen\n ? \"Nog geen snelkoppelingen. Zet hier de plekken waar je vaak moet zijn — je webwinkel, een pagina, een extern systeem.\"\n : \"Nog geen snelkoppelingen ingesteld.\"}\n </p>\n ) : (\n <ul className=\"flex flex-wrap gap-2\">\n {links.map((l, i) => (\n <li key={i}>\n <a\n href={l.href}\n target={/^https?:\\/\\//i.test(l.href) ? \"_blank\" : undefined}\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-100\"\n >\n {l.label} <span aria-hidden=\"true\" className=\"text-slate-400\">↗</span>\n </a>\n </li>\n ))}\n </ul>\n )}\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/dashboard/DashboardPanel.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Startpagina van /beheer (7.16).\n *\n * /beheer opende recht in het pagina-overzicht: een lijst technische titels als eerste indruk, en\n * nergens een plek die uitlegt hoe het systeem in elkaar zit. Deze startpagina doet drie dingen:\n * uitleggen hoe je de site bewerkt (op de site zelf, niet hier), in één oogopslag tonen wat er\n * gebeurt (bezoek, bestellingen, inzendingen, laatst bewerkt), en snelkoppelingen bieden die de\n * siteadmin zelf kan instellen.\n *\n * Bewust eenmalige reads (geen live listeners): dit is een overzicht bij binnenkomst, geen\n * monitor. Elke widget vangt zijn eigen fouten — een dichtgezette collectie (rules, module uit)\n * mag nooit de hele startpagina blokkeren.\n */\nimport { useEffect, useMemo, useState } from \"react\";\nimport {\n collection,\n doc,\n getCountFromServer,\n getDoc,\n getDocs,\n limit,\n orderBy,\n query,\n} from \"firebase/firestore\";\nimport type { Timestamp } from \"firebase/firestore\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { useAuth } from \"../../auth/AuthProvider\";\nimport { canManageSite } from \"../../auth/roles\";\nimport { useModules } from \"../../modules/context\";\nimport { formatCents } from \"../../schema/shop\";\nimport { Badge, Button, Card, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useAdminNav } from \"../navContext\";\n\ninterface RecentePagina {\n id: string;\n title?: string;\n path?: string;\n /** Timestamp uit de editor, ISO-string uit import-scripts — zie updatedMs(). */\n updatedAt?: unknown;\n}\ninterface MaandStat {\n total?: number;\n days?: Record<string, number>;\n paths?: Record<string, number>;\n}\ninterface RecenteOrder {\n id: string;\n status?: string;\n createdAt?: Timestamp;\n customer?: { name?: string };\n amount?: { totalCents?: number };\n}\ninterface FormOverzicht {\n id: string;\n name: string;\n aantal: number;\n laatste?: Timestamp;\n}\ninterface Snelkoppeling {\n label: string;\n href: string;\n}\n\n/**\n * `updatedAt` is in de praktijk een Firestore-Timestamp (editor-saves) ÓF een ISO-string\n * (import-/migratiescripts schrijven via REST) — live aangetroffen op christelijkkraamcadeau,\n * dóór elkaar in dezelfde collectie. Firestore sorteert die in aparte typegroepen, dus een\n * server-side orderBy geeft dan een fout beeld; we normaliseren en sorteren daarom client-side.\n */\nfunction updatedMs(v: unknown): number {\n if (v && typeof (v as Timestamp).toDate === \"function\") return (v as Timestamp).toDate().getTime();\n if (typeof v === \"string\") {\n const t = Date.parse(v);\n return Number.isNaN(t) ? 0 : t;\n }\n return 0;\n}\n\nfunction fmtDatum(v: unknown): string {\n const ms = updatedMs(v);\n if (!ms) return \"—\";\n return new Date(ms).toLocaleString(\"nl-NL\", { dateStyle: \"short\", timeStyle: \"short\" });\n}\n\n/** Kop van een widget met een \"ga ernaartoe\"-pijl die naar het bijbehorende paneel springt. */\nfunction WidgetKop({ titel, tab, tabLabel }: { titel: string; tab?: string; tabLabel?: string }) {\n const gaNaar = useAdminNav();\n return (\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">{titel}</h2>\n {tab && (\n <button\n type=\"button\"\n onClick={() => gaNaar(tab)}\n className=\"shrink-0 text-xs font-medium text-brand-700 hover:underline focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n >\n {tabLabel ?? \"Bekijk alles\"} →\n </button>\n )}\n </div>\n );\n}\n\nexport function DashboardPanel({ siteId }: { siteId: string }) {\n const { role } = useAuth();\n const { activeIds } = useModules();\n const { toast } = useToast();\n const gaNaar = useAdminNav();\n const heeftShop = activeIds.has(\"shop\");\n const magInstellen = canManageSite(role);\n\n const [paginas, setPaginas] = useState<RecentePagina[] | null>(null);\n const [maand, setMaand] = useState<MaandStat | null | undefined>(undefined);\n const [orders, setOrders] = useState<RecenteOrder[] | null>(null);\n const [formulieren, setFormulieren] = useState<FormOverzicht[] | null>(null);\n const [links, setLinks] = useState<Snelkoppeling[]>([]);\n const [linksBewerken, setLinksBewerken] = useState<Snelkoppeling[] | null>(null);\n const [linksOpslaan, setLinksOpslaan] = useState(false);\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setPaginas([]);\n setMaand(null);\n setOrders([]);\n setFormulieren([]);\n return;\n }\n\n // Laatst bewerkte pagina's. Bewust GEEN server-side orderBy op updatedAt: dat veld is een\n // mengsel van Timestamps en ISO-strings (zie updatedMs) en Firestore sorteert per typegroep.\n // Drafts-collecties zijn klein; alles ophalen en zelf sorteren is hier de betrouwbare weg.\n getDocs(collection(db, \"sites\", siteId, \"drafts\"))\n .then((snap) =>\n setPaginas(\n snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as Omit<RecentePagina, \"id\">) }))\n .sort((a, b) => updatedMs(b.updatedAt) - updatedMs(a.updatedAt))\n .slice(0, 5),\n ),\n )\n .catch(() => setPaginas([]));\n\n // Bezoek deze maand (zelfde bron als het Statistieken-paneel).\n const nu = new Date();\n const maandId = `${nu.getUTCFullYear()}-${String(nu.getUTCMonth() + 1).padStart(2, \"0\")}`;\n getDoc(doc(db, \"sites\", siteId, \"stats\", maandId))\n .then((snap) => setMaand(snap.exists() ? (snap.data() as MaandStat) : null))\n .catch(() => setMaand(null));\n\n // Laatste bestellingen — alleen als de shop-module actief is.\n if (heeftShop) {\n getDocs(query(collection(db, \"sites\", siteId, \"orders\"), orderBy(\"createdAt\", \"desc\"), limit(5)))\n .then((snap) => setOrders(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<RecenteOrder, \"id\">) }))))\n .catch(() => setOrders([]));\n } else {\n setOrders([]);\n }\n\n // Formulieren: naam + aantal inzendingen + laatste inzending (aggregatie-count, geen\n // volledige submissions-download). Max vier formulieren — de widget is een signaal, geen lijst.\n getDocs(collection(db, \"sites\", siteId, \"forms\"))\n .then(async (snap) => {\n const forms = snap.docs.slice(0, 4);\n const uit = await Promise.all(\n forms.map(async (f) => {\n const subs = collection(db, \"sites\", siteId, \"forms\", f.id, \"submissions\");\n const [count, laatste] = await Promise.all([\n getCountFromServer(subs).then((c) => c.data().count).catch(() => 0),\n getDocs(query(subs, orderBy(\"createdAt\", \"desc\"), limit(1)))\n .then((s) => (s.docs[0]?.data() as { createdAt?: Timestamp } | undefined)?.createdAt)\n .catch(() => undefined),\n ]);\n return { id: f.id, name: (f.data() as { name?: string }).name ?? f.id, aantal: count, laatste };\n }),\n );\n setFormulieren(uit);\n })\n .catch(() => setFormulieren([]));\n\n // Snelkoppelingen uit settings/site (editor mag lezen; instellen is siteadmin-werk).\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"site\"))\n .then((snap) => {\n const d = snap.exists() ? (snap.data() as { dashboardLinks?: Snelkoppeling[] }) : {};\n setLinks(Array.isArray(d.dashboardLinks) ? d.dashboardLinks : []);\n })\n .catch(() => setLinks([]));\n }, [siteId, heeftShop]);\n\n const vandaag = useMemo(() => {\n const dag = String(new Date().getUTCDate()).padStart(2, \"0\");\n return maand?.days?.[dag] ?? 0;\n }, [maand]);\n const topPaden = useMemo(\n () => Object.entries(maand?.paths ?? {}).sort((a, b) => b[1] - a[1]).slice(0, 3),\n [maand],\n );\n\n async function slaLinksOp() {\n if (!linksBewerken) return;\n const schoon = linksBewerken.filter((l) => l.label.trim() && l.href.trim());\n const app = getFirebaseApp();\n if (!app) return;\n setLinksOpslaan(true);\n try {\n // Zelfde route als de rest van de instellingen: de saveSiteSettings-callable (merge).\n const fn = callable(app, \"saveSiteSettings\");\n await fn({ siteId, settings: { dashboardLinks: schoon } });\n setLinks(schoon);\n setLinksBewerken(null);\n toast({ title: \"Snelkoppelingen opgeslagen\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setLinksOpslaan(false);\n }\n }\n\n return (\n <div className=\"grid gap-4\">\n {/* ── Zo werkt het ── */}\n <Card>\n <h2 className=\"text-sm font-semibold text-slate-800\">Zo werkt je site</h2>\n <div className=\"mt-2 grid gap-2 text-sm text-slate-600 md:grid-cols-3\">\n <p>\n <strong className=\"text-slate-800\">1 · Bewerk op de site zelf.</strong> Klik rechtsboven op{\" \"}\n <em>Bekijk site</em> en kies <em>Bewerken</em> in de zwarte balk. Elk blok krijgt een{\" \"}\n potlood (tekst) en een tandwiel (instellingen) — je typt gewoon in de pagina.\n </p>\n <p>\n <strong className=\"text-slate-800\">2 · Publiceer als het goed is.</strong> Alles wat je\n wijzigt is eerst een <em>werkversie</em>: bezoekers zien er niets van. Pas de knop{\" \"}\n <em>Publiceren</em> (rechtsonder) zet het live — en <em>Verwerpen</em> gooit het weg.\n </p>\n <p>\n <strong className=\"text-slate-800\">3 · De rest staat hier.</strong> Nieuwe pagina's, het\n menu, foto's, formulieren{heeftShop ? \", de webwinkel\" : \"\"} en statistieken vind je in\n de secties hiernaast. Onderaan de zijbalk staat ook een korte helptekst.\n </p>\n </div>\n </Card>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n {/* ── Statistieken ── */}\n <Card>\n <WidgetKop titel=\"Bezoek deze maand\" tab=\"stats\" tabLabel=\"Statistieken\" />\n {maand === undefined ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : maand === null ? (\n <p className=\"text-sm text-slate-500\">Nog geen bezoekersdata deze maand.</p>\n ) : (\n <div className=\"grid gap-3\">\n <div className=\"flex items-baseline gap-4\">\n <p><span className=\"text-2xl font-semibold text-slate-900\">{maand.total ?? 0}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">weergaven deze maand</span></p>\n <p><span className=\"text-lg font-semibold text-slate-700\">{vandaag}</span>{\" \"}\n <span className=\"text-xs text-slate-500\">vandaag</span></p>\n </div>\n {topPaden.length > 0 && (\n <ul className=\"grid gap-1 text-sm text-slate-600\">\n {topPaden.map(([pad, n]) => (\n <li key={pad} className=\"flex justify-between gap-2\">\n <span className=\"truncate\" title={pad}>{pad}</span>\n <span className=\"shrink-0 text-slate-400\">{n}</span>\n </li>\n ))}\n </ul>\n )}\n </div>\n )}\n </Card>\n\n {/* ── Laatst bewerkte pagina's ── */}\n <Card>\n <WidgetKop titel=\"Laatst bewerkt\" tab=\"pages\" tabLabel=\"Pagina's\" />\n {paginas === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : paginas.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bewerkte pagina's.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {paginas.map((p) => (\n <li key={p.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"pages\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n title={`${p.title ?? p.id} — ${p.path ?? \"\"}`}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{p.title ?? p.id}</span>\n <span className=\"block truncate text-xs text-slate-400\">{p.path}</span>\n </span>\n <span className=\"shrink-0 text-xs text-slate-400\">{fmtDatum(p.updatedAt)}</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Bestellingen (alleen met shop) ── */}\n {heeftShop && (\n <Card>\n <WidgetKop titel=\"Laatste bestellingen\" tab=\"shop\" tabLabel=\"Webwinkel\" />\n {orders === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : orders.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen bestellingen.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {orders.map((o) => (\n <li key={o.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"shop\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate text-slate-800\">{o.customer?.name ?? \"Onbekend\"}</span>\n <span className=\"block text-xs text-slate-400\">{fmtDatum(o.createdAt)}</span>\n </span>\n <span className=\"flex shrink-0 items-center gap-2\">\n {typeof o.amount?.totalCents === \"number\" && (\n <span className=\"text-sm font-medium text-slate-700\">{formatCents(o.amount.totalCents)}</span>\n )}\n <Badge tone={o.status === \"paid\" || o.status === \"shipped\" ? \"green\" : \"amber\"}>\n {o.status ?? \"?\"}\n </Badge>\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n )}\n\n {/* ── Formulieren ── */}\n <Card>\n <WidgetKop titel=\"Formulieren\" tab=\"forms\" tabLabel=\"Formulieren\" />\n {formulieren === null ? (\n <p className=\"text-sm text-slate-500\">Laden…</p>\n ) : formulieren.length === 0 ? (\n <p className=\"text-sm text-slate-500\">Nog geen formulieren.</p>\n ) : (\n <ul className=\"grid gap-1\">\n {formulieren.map((f) => (\n <li key={f.id}>\n <button\n type=\"button\"\n onClick={() => gaNaar(\"forms\")}\n className=\"flex w-full items-center justify-between gap-2 rounded-lg px-2 py-1.5 text-left text-sm hover:bg-slate-100\"\n >\n <span className=\"truncate text-slate-800\" title={f.name}>{f.name}</span>\n <span className=\"shrink-0 text-xs text-slate-500\">\n {f.aantal} {f.aantal === 1 ? \"inzending\" : \"inzendingen\"}\n {f.laatste ? ` · laatste ${fmtDatum(f.laatste)}` : \"\"}\n </span>\n </button>\n </li>\n ))}\n </ul>\n )}\n </Card>\n\n {/* ── Snelkoppelingen ── */}\n <Card>\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h2 className=\"text-sm font-semibold text-slate-800\">Snelkoppelingen</h2>\n {magInstellen && linksBewerken === null && (\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(links.length ? [...links] : [{ label: \"\", href: \"\" }])}>\n Instellen\n </Button>\n )}\n </div>\n {linksBewerken ? (\n <div className=\"grid gap-2\">\n <div className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2 text-xs font-medium text-slate-500\">\n <span>Naam</span>\n <span>Adres</span>\n <span aria-hidden=\"true\" />\n </div>\n {linksBewerken.map((l, i) => (\n <div key={i} className=\"grid grid-cols-[1fr_1.4fr_auto] gap-2\">\n <Input value={l.label} placeholder=\"bv. Webwinkel\" aria-label=\"Naam van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, label: e.target.value } : x)))} />\n <Input value={l.href} placeholder=\"/webwinkel of https://…\" aria-label=\"Adres van de snelkoppeling\"\n onChange={(e) => setLinksBewerken(linksBewerken.map((x, j) => (j === i ? { ...x, href: e.target.value } : x)))} />\n <Button variant=\"ghost\" size=\"sm\" aria-label=\"Verwijderen\"\n onClick={() => setLinksBewerken(linksBewerken.filter((_, j) => j !== i))}>✕</Button>\n </div>\n ))}\n <div className=\"flex items-center justify-between gap-2\">\n <Button variant=\"secondary\" size=\"sm\" disabled={linksBewerken.length >= 12}\n onClick={() => setLinksBewerken([...linksBewerken, { label: \"\", href: \"\" }])}>\n + Snelkoppeling\n </Button>\n <div className=\"flex gap-2\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setLinksBewerken(null)}>Annuleren</Button>\n <Button size=\"sm\" disabled={linksOpslaan} onClick={() => void slaLinksOp()}>\n {linksOpslaan ? \"Bezig…\" : \"Opslaan\"}\n </Button>\n </div>\n </div>\n </div>\n ) : links.length === 0 ? (\n <p className=\"text-sm text-slate-500\">\n {magInstellen\n ? \"Nog geen snelkoppelingen. Zet hier de plekken waar je vaak moet zijn — je webwinkel, een pagina, een extern systeem.\"\n : \"Nog geen snelkoppelingen ingesteld.\"}\n </p>\n ) : (\n <ul className=\"flex flex-wrap gap-2\">\n {links.map((l, i) => (\n <li key={i}>\n <a\n href={l.href}\n target={/^https?:\\/\\//i.test(l.href) ? \"_blank\" : undefined}\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-slate-200 px-3 py-1.5 text-sm text-slate-700 hover:bg-slate-100\"\n >\n {l.label} <span aria-hidden=\"true\" className=\"text-slate-400\">↗</span>\n </a>\n </li>\n ))}\n </ul>\n )}\n </Card>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -20,6 +20,7 @@ const FUNCTIONELE_TYPES = /* @__PURE__ */ new Set([
|
|
|
20
20
|
"orderStatus"
|
|
21
21
|
]);
|
|
22
22
|
function isFunctioneel(p) {
|
|
23
|
+
if (p.path === "/") return false;
|
|
23
24
|
return (p.blocks ?? []).some((b) => FUNCTIONELE_TYPES.has(String(b._type)));
|
|
24
25
|
}
|
|
25
26
|
function PaginaKnop({ p, selected, onClick }) {
|
|
@@ -81,11 +82,11 @@ function PagesPanel({ siteId }) {
|
|
|
81
82
|
] }) })
|
|
82
83
|
] })
|
|
83
84
|
] }),
|
|
84
|
-
/* @__PURE__ */ jsx("ul", { className: "grid gap-1", children: gewoon.map((p) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(PaginaKnop, { p, selected: p.id === selected, onClick: () => setSelected(p.id) }) }, p.id)) }),
|
|
85
|
+
/* @__PURE__ */ jsx("ul", { className: "grid gap-1", children: gewoon.map((p) => /* @__PURE__ */ jsx("li", { className: "min-w-0", children: /* @__PURE__ */ jsx(PaginaKnop, { p, selected: p.id === selected, onClick: () => setSelected(p.id) }) }, p.id)) }),
|
|
85
86
|
functioneel.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-4 border-t border-slate-200 pt-3", children: [
|
|
86
87
|
/* @__PURE__ */ jsx("p", { className: "mb-1 px-3 text-xs font-medium tracking-wide text-slate-500 uppercase", children: "Winkel & functioneel" }),
|
|
87
88
|
/* @__PURE__ */ jsx("p", { className: "mb-2 px-3 text-xs text-slate-400", children: "Deze pagina's zijn het omhulsel van een functie. Producten en bestellingen beheer je onder Webwinkel; hier pas je alleen de tekst eromheen aan." }),
|
|
88
|
-
/* @__PURE__ */ jsx("ul", { className: "grid gap-1", children: functioneel.map((p) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(PaginaKnop, { p, selected: p.id === selected, onClick: () => setSelected(p.id) }) }, p.id)) })
|
|
89
|
+
/* @__PURE__ */ jsx("ul", { className: "grid gap-1", children: functioneel.map((p) => /* @__PURE__ */ jsx("li", { className: "min-w-0", children: /* @__PURE__ */ jsx(PaginaKnop, { p, selected: p.id === selected, onClick: () => setSelected(p.id) }) }, p.id)) })
|
|
89
90
|
] })
|
|
90
91
|
] }),
|
|
91
92
|
/* @__PURE__ */ jsx("div", { children: current ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -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\n/**\n * Bloktypes die een pagina FUNCTIONEEL maken (7.16): een afrekenpagina is \"een pagina met een\n * checkout-blok\", en die hoort in de lijst niet tussen \"Over ons\" te staan — de inhoud (producten,\n * bestellingen) beheer je onder Webwinkel, hier staat alleen het omhulsel. Er is bewust géén\n * pagina-type-veld in het schema (dat zou een tweede waarheid zijn naast de blokken zelf); we lezen\n * het af aan wat er op de pagina staat.\n */\nconst FUNCTIONELE_TYPES = new Set([\n \"shopCategories\",\n \"productGrid\",\n \"productDetail\",\n \"cart\",\n \"checkout\",\n \"donation\",\n \"orderStatus\",\n]);\nfunction isFunctioneel(p: DraftPage): boolean {\n return (p.blocks ?? []).some((b) => FUNCTIONELE_TYPES.has(String((b as { _type?: unknown })._type)));\n}\n\n/**\n * Eén rij in de paginalijst. Titel + pad onder elkaar: de kolom is 260px en een SEO-titel als\n * \"Christelijk Kraamcadeau — christelijke geschenken en kraamcadeaus\" is dan onleesbaar afgekapt\n * zónder dat je kunt zien wélke pagina het is. Het pad is kort, uniek en herkenbaar (\"/\", \"/over-\n * ons\") — precies het houvast dat de afgekapte titel mist. `title` levert de volle titel bij hover.\n */\nfunction PaginaKnop({ p, selected, onClick }: { p: DraftPage; selected: boolean; onClick: () => void }) {\n return (\n <button\n onClick={onClick}\n title={`${p.title} — ${p.path}`}\n className={`flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 text-left text-sm ${\n selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate\">{p.title}</span>\n <span className={`block truncate text-xs ${selected ? \"text-brand-700/70\" : \"text-slate-400\"}`}>{p.path}</span>\n </span>\n {p.published ? <Badge tone=\"green\">live</Badge> : <Badge tone=\"amber\">concept</Badge>}\n </button>\n );\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\n const gewoon = pages.filter((p) => !isFunctioneel(p));\n const functioneel = pages.filter(isFunctioneel);\n useEffect(() => {\n // Standaard de eerste GEWONE pagina: /beheer → Pagina's gaat over teksten, niet over de winkel.\n if (!selected && pages.length) setSelected((gewoon[0] ?? pages[0]).id);\n // eslint-disable-next-line react-hooks/exhaustive-deps\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 {gewoon.map((p) => (\n <li key={p.id}>\n <PaginaKnop p={p} selected={p.id === selected} onClick={() => setSelected(p.id)} />\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 {functioneel.length > 0 && (\n <div className=\"mt-4 border-t border-slate-200 pt-3\">\n <p className=\"mb-1 px-3 text-xs font-medium tracking-wide text-slate-500 uppercase\">Winkel & functioneel</p>\n <p className=\"mb-2 px-3 text-xs text-slate-400\">\n Deze pagina's zijn het omhulsel van een functie. Producten en bestellingen beheer je\n onder Webwinkel; hier pas je alleen de tekst eromheen aan.\n </p>\n <ul className=\"grid gap-1\">\n {functioneel.map((p) => (\n <li key={p.id}>\n <PaginaKnop p={p} selected={p.id === selected} onClick={() => setSelected(p.id)} />\n </li>\n ))}\n </ul>\n </div>\n )}\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
|
+
{"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\n/**\n * Bloktypes die een pagina FUNCTIONEEL maken (7.16): een afrekenpagina is \"een pagina met een\n * checkout-blok\", en die hoort in de lijst niet tussen \"Over ons\" te staan — de inhoud (producten,\n * bestellingen) beheer je onder Webwinkel, hier staat alleen het omhulsel. Er is bewust géén\n * pagina-type-veld in het schema (dat zou een tweede waarheid zijn naast de blokken zelf); we lezen\n * het af aan wat er op de pagina staat.\n */\nconst FUNCTIONELE_TYPES = new Set([\n \"shopCategories\",\n \"productGrid\",\n \"productDetail\",\n \"cart\",\n \"checkout\",\n \"donation\",\n \"orderStatus\",\n]);\nfunction isFunctioneel(p: DraftPage): boolean {\n // De homepage hoort ALTIJD bij de gewone pagina's: op een winkel-site staat er vaak een\n // productGrid op de home, maar de home is nooit alleen \"het omhulsel van een functie\" —\n // het is de voordeur van de site, en die moet bovenaan staan waar de klant hem zoekt.\n if (p.path === \"/\") return false;\n return (p.blocks ?? []).some((b) => FUNCTIONELE_TYPES.has(String((b as { _type?: unknown })._type)));\n}\n\n/**\n * Eén rij in de paginalijst. Titel + pad onder elkaar: de kolom is 260px en een SEO-titel als\n * \"Christelijk Kraamcadeau — christelijke geschenken en kraamcadeaus\" is dan onleesbaar afgekapt\n * zónder dat je kunt zien wélke pagina het is. Het pad is kort, uniek en herkenbaar (\"/\", \"/over-\n * ons\") — precies het houvast dat de afgekapte titel mist. `title` levert de volle titel bij hover.\n */\nfunction PaginaKnop({ p, selected, onClick }: { p: DraftPage; selected: boolean; onClick: () => void }) {\n return (\n <button\n onClick={onClick}\n title={`${p.title} — ${p.path}`}\n className={`flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 text-left text-sm ${\n selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n <span className=\"min-w-0\">\n <span className=\"block truncate\">{p.title}</span>\n <span className={`block truncate text-xs ${selected ? \"text-brand-700/70\" : \"text-slate-400\"}`}>{p.path}</span>\n </span>\n {p.published ? <Badge tone=\"green\">live</Badge> : <Badge tone=\"amber\">concept</Badge>}\n </button>\n );\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\n const gewoon = pages.filter((p) => !isFunctioneel(p));\n const functioneel = pages.filter(isFunctioneel);\n useEffect(() => {\n // Standaard de eerste GEWONE pagina: /beheer → Pagina's gaat over teksten, niet over de winkel.\n if (!selected && pages.length) setSelected((gewoon[0] ?? pages[0]).id);\n // eslint-disable-next-line react-hooks/exhaustive-deps\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 {gewoon.map((p) => (\n <li key={p.id} className=\"min-w-0\">\n <PaginaKnop p={p} selected={p.id === selected} onClick={() => setSelected(p.id)} />\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 {functioneel.length > 0 && (\n <div className=\"mt-4 border-t border-slate-200 pt-3\">\n <p className=\"mb-1 px-3 text-xs font-medium tracking-wide text-slate-500 uppercase\">Winkel & functioneel</p>\n <p className=\"mb-2 px-3 text-xs text-slate-400\">\n Deze pagina's zijn het omhulsel van een functie. Producten en bestellingen beheer je\n onder Webwinkel; hier pas je alleen de tekst eromheen aan.\n </p>\n <ul className=\"grid gap-1\">\n {functioneel.map((p) => (\n <li key={p.id} className=\"min-w-0\">\n <PaginaKnop p={p} selected={p.id === selected} onClick={() => setSelected(p.id)} />\n </li>\n ))}\n </ul>\n </div>\n )}\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,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import { doc, getDoc
|
|
5
|
-
import { getDb } from "../../firebase/client.js";
|
|
4
|
+
import { doc, getDoc } from "firebase/firestore";
|
|
5
|
+
import { getDb, getFirebaseApp } from "../../firebase/client.js";
|
|
6
|
+
import { callable } from "../../firebase/functionsClient.js";
|
|
6
7
|
import { Button, Card, Field, Input } from "../ui/primitives.js";
|
|
7
8
|
import { useToast } from "../ui/toast.js";
|
|
8
9
|
import { centsToEuroInput, eurosToCents } from "./lib.js";
|
|
@@ -25,8 +26,8 @@ function ShopSettingsTab({ siteId }) {
|
|
|
25
26
|
});
|
|
26
27
|
}, [siteId]);
|
|
27
28
|
async function save() {
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
29
|
+
const app = getFirebaseApp();
|
|
30
|
+
if (!app || !cfg) return;
|
|
30
31
|
setBusy(true);
|
|
31
32
|
try {
|
|
32
33
|
const next = {
|
|
@@ -34,11 +35,8 @@ function ShopSettingsTab({ siteId }) {
|
|
|
34
35
|
shippingFlatCents: eurosToCents(shipInput) ?? 495,
|
|
35
36
|
shippingFreeFromCents: freeInput.trim() === "" ? 0 : eurosToCents(freeInput) ?? 0
|
|
36
37
|
};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{ modules: { shop: { config: next } } },
|
|
40
|
-
{ merge: true }
|
|
41
|
-
);
|
|
38
|
+
const fn = callable(app, "saveSiteSettings");
|
|
39
|
+
await fn({ siteId, settings: { modules: { shop: { config: next } } } });
|
|
42
40
|
setCfg(next);
|
|
43
41
|
toast({ title: "Instellingen opgeslagen", tone: "success" });
|
|
44
42
|
} catch (e) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/admin/shop/ShopSettingsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { doc, getDoc
|
|
1
|
+
{"version":3,"sources":["../../../src/admin/shop/ShopSettingsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { doc, getDoc } from \"firebase/firestore\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { centsToEuroInput, eurosToCents } from \"./lib\";\n\n/**\n * Winkel-instellingen — leest/schrijft DEZELFDE module-config als Instellingen → Modules\n * (settings/public.modules.shop.config; geen tweede bron van waarheid), maar met winkel-\n * presentatie: bedragen in euro's en uitleg per veld. Siteadmin-only (rules).\n */\ninterface ShopCfg {\n shippingFlatCents?: number;\n shippingFreeFromCents?: number;\n orderPrefix?: string;\n currency?: string;\n businessName?: string;\n businessAddress?: string;\n businessZip?: string;\n businessCity?: string;\n businessKvk?: string;\n businessBtw?: string;\n [k: string]: unknown;\n}\n\nexport function ShopSettingsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const [cfg, setCfg] = useState<ShopCfg | null>(null);\n const [shipInput, setShipInput] = useState(\"\");\n const [freeInput, setFreeInput] = useState(\"\");\n const [busy, setBusy] = useState(false);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n void getDoc(doc(db, \"sites\", siteId, \"settings\", \"public\")).then((snap) => {\n const modules = (snap.data()?.modules ?? {}) as Record<string, { config?: ShopCfg } | boolean>;\n const entry = modules.shop;\n const c = typeof entry === \"object\" && entry !== null ? (entry.config ?? {}) : {};\n setCfg(c);\n setShipInput(centsToEuroInput(c.shippingFlatCents ?? 495));\n setFreeInput(c.shippingFreeFromCents ? centsToEuroInput(c.shippingFreeFromCents) : \"\");\n });\n }, [siteId]);\n\n async function save() {\n const app = getFirebaseApp();\n if (!app || !cfg) return;\n setBusy(true);\n try {\n const next: ShopCfg = {\n ...cfg,\n shippingFlatCents: eurosToCents(shipInput) ?? 495,\n shippingFreeFromCents: freeInput.trim() === \"\" ? 0 : (eurosToCents(freeInput) ?? 0),\n };\n /**\n * Via de saveSiteSettings-callable, NIET rechtstreeks naar Firestore. Deze tab deed nog een\n * directe setDoc op settings/public, maar die deur zit sinds de settings-verstrakking in de\n * rules voor iederéén dicht (ook superadmin) — de klant kreeg letterlijk \"Missing or\n * insufficient permissions\" bij het opslaan van verzendkosten. De callable is bovendien de\n * enige route die site/public consistent houdt én de cache-tag pusht, zodat de nieuwe\n * verzendkosten binnen seconden op de live winkel staan i.p.v. na het ISR-vangnet.\n * set+merge in de callable merged diep: de enabled-vlag en andere modules blijven staan.\n */\n const fn = callable(app, \"saveSiteSettings\");\n await fn({ siteId, settings: { modules: { shop: { config: next } } } });\n setCfg(next);\n toast({ title: \"Instellingen opgeslagen\", tone: \"success\" });\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 if (!cfg) return <Card><p className=\"text-sm text-slate-500\">Laden…</p></Card>;\n\n const set = (k: string, v: string) => setCfg((p) => ({ ...p, [k]: v }));\n\n return (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Card>\n <h3 className=\"mb-3 text-sm font-semibold\">Verzending & bestellen</h3>\n <div className=\"grid gap-4\">\n <Field label=\"Verzendkosten (€)\" hint=\"Vast bedrag per bestelling binnen Nederland\">\n <Input value={shipInput} onChange={(e) => setShipInput(e.target.value)} placeholder=\"4,95\" className=\"w-32\" />\n </Field>\n <Field label=\"Gratis verzending vanaf (€)\" hint=\"Leeg = nooit gratis\">\n <Input value={freeInput} onChange={(e) => setFreeInput(e.target.value)} placeholder=\"50,00\" className=\"w-32\" />\n </Field>\n <Field label=\"Ordernummer-prefix\" hint=\"Bijv. CKC → CKC-2026-0001\">\n <Input value={String(cfg.orderPrefix ?? \"ORDER\")} onChange={(e) => set(\"orderPrefix\", e.target.value.toUpperCase().slice(0, 12))} className=\"w-32\" />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-sm font-semibold\">Bedrijfsgegevens (factuur)</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Deze gegevens komen op de factuur-PDF die klanten bij hun bestelling krijgen.</p>\n <div className=\"grid gap-3\">\n <Field label=\"Bedrijfsnaam\"><Input value={String(cfg.businessName ?? \"\")} onChange={(e) => set(\"businessName\", e.target.value)} /></Field>\n <Field label=\"Adres\"><Input value={String(cfg.businessAddress ?? \"\")} onChange={(e) => set(\"businessAddress\", e.target.value)} /></Field>\n <div className=\"grid grid-cols-[120px_1fr] gap-3\">\n <Field label=\"Postcode\"><Input value={String(cfg.businessZip ?? \"\")} onChange={(e) => set(\"businessZip\", e.target.value)} /></Field>\n <Field label=\"Plaats\"><Input value={String(cfg.businessCity ?? \"\")} onChange={(e) => set(\"businessCity\", e.target.value)} /></Field>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <Field label=\"KVK-nummer\"><Input value={String(cfg.businessKvk ?? \"\")} onChange={(e) => set(\"businessKvk\", e.target.value)} /></Field>\n <Field label=\"Btw-nummer\"><Input value={String(cfg.businessBtw ?? \"\")} onChange={(e) => set(\"businessBtw\", e.target.value)} /></Field>\n </div>\n </div>\n </Card>\n\n <div className=\"lg:col-span-2\">\n <Button onClick={save} disabled={busy}>Opslaan</Button>\n <p className=\"mt-2 text-xs text-slate-500\">\n Paden en valuta van de winkel staan onder Instellingen → Modules → Webwinkel.\n </p>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
@@ -22,7 +22,6 @@ import { StyleEditor } from "../blockEditors/StyleEditor.js";
|
|
|
22
22
|
import { siteLocales, parseLocaleSlugs, localizeBlocks } from "../../i18n/overlay.js";
|
|
23
23
|
import { useEditor } from "../../editor/EditorProvider.js";
|
|
24
24
|
import { AdminModes } from "../../components/EditBar.js";
|
|
25
|
-
import { useBalkOffset } from "../../components/barOffset.js";
|
|
26
25
|
import { aantalWijzigingen, bepaalWijzigingen, GEEN_WIJZIGINGEN } from "./wijzigingen.js";
|
|
27
26
|
import { TranslationPanel } from "./TranslationPanel.js";
|
|
28
27
|
import { PatternInserter } from "../patterns/PatternInserter.js";
|
|
@@ -110,7 +109,6 @@ function VisualEditorInner({ onClose }) {
|
|
|
110
109
|
const canvasRef = useRef(null);
|
|
111
110
|
const [conflict, setConflict] = useState(false);
|
|
112
111
|
const [liveBlocks, setLiveBlocks] = useState(void 0);
|
|
113
|
-
useBalkOffset(true);
|
|
114
112
|
const { vars: themeVars, darkAuto } = useThemeVars();
|
|
115
113
|
const containerRef = useRef(null);
|
|
116
114
|
useEffect(() => {
|