@xiwebdesign/xicms-core 7.7.0 → 7.8.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.
Files changed (32) hide show
  1. package/dist/admin/AdminApp.js +3 -1
  2. package/dist/admin/AdminApp.js.map +1 -1
  3. package/dist/admin/lib/pagesClient.d.ts +13 -0
  4. package/dist/admin/lib/pagesClient.js +13 -0
  5. package/dist/admin/lib/pagesClient.js.map +1 -1
  6. package/dist/admin/shop/ProductPaneel.d.ts +15 -0
  7. package/dist/admin/shop/ProductPaneel.js +55 -0
  8. package/dist/admin/shop/ProductPaneel.js.map +1 -0
  9. package/dist/admin/shop/ProductsTab.d.ts +27 -0
  10. package/dist/admin/shop/ProductsTab.js +3 -2
  11. package/dist/admin/shop/ProductsTab.js.map +1 -1
  12. package/dist/admin/shop/productClient.d.ts +32 -0
  13. package/dist/admin/shop/productClient.js +31 -0
  14. package/dist/admin/shop/productClient.js.map +1 -0
  15. package/dist/admin/visual/VisualEditor.js +296 -43
  16. package/dist/admin/visual/VisualEditor.js.map +1 -1
  17. package/dist/admin/visual/wijzigingen.d.ts +46 -0
  18. package/dist/admin/visual/wijzigingen.js +78 -0
  19. package/dist/admin/visual/wijzigingen.js.map +1 -0
  20. package/dist/components/EditBar.d.ts +30 -0
  21. package/dist/components/EditBar.js +32 -6
  22. package/dist/components/EditBar.js.map +1 -1
  23. package/dist/editor/EditorProvider.d.ts +5 -0
  24. package/dist/editor/EditorProvider.js +28 -11
  25. package/dist/editor/EditorProvider.js.map +1 -1
  26. package/dist/modules/shop/blocks/ProductDetail.js +3 -3
  27. package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
  28. package/dist/modules/shop/blocks/ProductGrid.js +5 -2
  29. package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
  30. package/dist/styles.css +163 -46
  31. package/dist/version.js +1 -1
  32. package/package.json +1 -1
@@ -42,7 +42,9 @@ function AdminApp({
42
42
  "Beheer \xB7 ",
43
43
  name
44
44
  ] }),
45
- /* @__PURE__ */ jsx(LoginForm, {})
45
+ /* @__PURE__ */ jsx(LoginForm, { onSuccess: () => {
46
+ window.location.replace("/");
47
+ } })
46
48
  ] }) }) : !canEdit(role) ? /* @__PURE__ */ jsxs(Melding, { children: [
47
49
  "Je bent ingelogd als ",
48
50
  /* @__PURE__ */ jsx("strong", { children: user.email }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/AdminApp.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { LoginForm } from \"../auth/LoginForm\";\nimport { useSiteConfig } from \"../config\";\nimport { useModules } from \"../modules/context\";\nimport { canEdit, canManageSite, type Role } from \"../auth/roles\";\nimport { Tabs, TabsContent } from \"./ui/tabs\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { ToastProvider } from \"./ui/toast\";\nimport { ConfirmHost } from \"./ui/ConfirmDialog\";\nimport { Button } from \"./ui/primitives\";\nimport { PanelIcon } from \"./ui/PanelIcon\";\nimport { builtinPanels, type PanelDefinition } from \"./panelRegistry\";\nimport { AdminHelp } from \"./AdminHelp\";\nimport { CoreVersion } from \"./CoreVersion\";\n\n/**\n * Rol-bewaakt beheer (XiCMS 3.0). De secties komen uit de paneel-registry, gefilterd op de actieve\n * modules (`SiteConfig.modules`/`features`) en de rol van de gebruiker. Een module met een eigen\n * paneel (ook klant-modules via `extraPanels`) verschijnt automatisch.\n *\n * VORM: een schermvullend werkblad met een vaste zijbalk, geen kaart in de pagina. Het beheer viel\n * hiervoor binnen de site-chrome — nav erboven, footer eronder, cookiebanner eroverheen — en las\n * daardoor als een blok ergens op de website in plaats van als gereedschap. De site-chrome leeft nu\n * in `app/(site)/layout.tsx`; /beheer valt daarbuiten.\n *\n * De zijbalk verving de horizontale tabstrip omdat die bij zeven of meer panelen al omsloeg naar een\n * tweede regel. Verticaal is er bovendien ruimte voor een icoon naast het woord, en `orientation`\n * houdt de pijltjestoetsen correct (Radix).\n *\n * Wikkel in `.xicms-admin` zodat de Tailwind-stijlen uit `@xiwebdesign/xicms-core/admin.css` scoped\n * blijven en niet naar de site lekken. Importeer die CSS alleen op de /beheer-route.\n */\nfunction roleAllows(role: Role | null, min: PanelDefinition[\"minRole\"]): boolean {\n return min === \"siteadmin\" ? canManageSite(role) : canEdit(role);\n}\n\n/** Gecentreerde boodschap voor de toestanden waarin er (nog) geen beheer te tonen is. */\nfunction Melding({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"xicms-admin__midden grid place-items-center p-6\">\n <div className=\"w-full max-w-md rounded-xl border border-slate-200 bg-white p-6 text-sm shadow-sm\">{children}</div>\n </div>\n );\n}\n\nexport function AdminApp({\n extraPanels = [],\n embedded = false,\n}: {\n extraPanels?: PanelDefinition[];\n /**\n * Draait het beheer BINNEN de site-schil (header, logo, menu, footer eromheen) in plaats van als\n * schermvullend werkblad op een kale pagina?\n *\n * Het verschil is niet cosmetisch. Los rekent het werkblad met het VENSTER: `min-h-screen`, en een\n * zijbalk die met `sticky top-0` aan de bovenkant van het scherm plakt. Zet je daar een\n * site-header boven, dan loopt de pagina 100vh + de hoogte van die header, en plakt de zijbalk\n * onder de header vast in plaats van erlangs. Ingebed laat de zijbalk daarom los van het venster\n * en wordt hij een balk over de volle breedte — dat vecht niet met de navigatie erboven.\n */\n embedded?: boolean;\n}) {\n const { user, role, loading, configured, signOut } = useAuth();\n const { name, siteId } = useSiteConfig();\n const { activeIds } = useModules();\n\n // Panelen van actieve modules waar de rol toegang toe heeft, gesorteerd op volgorde.\n const panels = [...builtinPanels, ...extraPanels]\n .filter((p) => activeIds.has(p.moduleId) && roleAllows(role, p.minRole))\n .sort((a, b) => a.order - b.order);\n /** Sleutel van een paneel in de tabbladen: eigen `id` als die er is, anders de module-id. */\n const sleutel = (p: PanelDefinition) => p.id ?? p.moduleId;\n const firstTab = panels[0] && sleutel(panels[0]);\n // Gecontroleerd i.p.v. `defaultValue`: het werkblad toont de naam van de sectie waar je in zit.\n // Zonder die kop lees je alleen aan de zijbalk-markering af waar je bent, en dat is te weinig\n // houvast op een breed scherm waar de zijbalk ver van je blik af staat.\n const [actief, setActief] = useState<string | undefined>(firstTab);\n const actievePanel = panels.find((p) => sleutel(p) === (actief ?? firstTab));\n\n return (\n <div className={embedded ? \"xicms-admin xicms-admin--ingebed\" : \"xicms-admin\"}>\n <ToastProvider>\n <ConfirmHost>\n {!configured ? (\n <Melding>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>-variabelen in\n om het beheer aan te zetten.\n </Melding>\n ) : loading ? (\n <Melding>\n <span className=\"text-slate-500\">Bezig met laden…</span>\n </Melding>\n ) : !user ? (\n <div className=\"xicms-admin__midden grid place-items-center p-6\">\n <div className=\"w-full max-w-sm\">\n <p className=\"mb-4 text-center text-sm text-slate-500\">Beheer · {name}</p>\n <LoginForm />\n </div>\n </div>\n ) : !canEdit(role) ? (\n <Melding>\n Je bent ingelogd als <strong>{user.email}</strong>, maar hebt geen beheerrechten voor deze site.\n <div className=\"mt-3\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>Uitloggen</Button>\n </div>\n </Melding>\n ) : panels.length === 0 ? (\n <Melding>Geen actieve beheer-modules voor deze site.</Melding>\n ) : (\n <Tabs value={actief ?? firstTab} onValueChange={setActief} orientation=\"vertical\" className=\"xicms-admin__raster md:grid md:grid-cols-[248px_1fr]\">\n {/* ── Zijbalk ─────────────────────────────────────────────────────────── */}\n <div\n className=\"xicms-admin__rail flex flex-col gap-4 border-b border-slate-200 p-4 md:sticky md:top-0 md:h-screen md:border-r md:border-b-0\"\n style={{ background: \"var(--admin-sidebar)\" }}\n >\n <div className=\"min-w-0 px-2 pt-1\">\n <p className=\"truncate text-sm font-semibold\">{name}</p>\n <p className=\"text-xs text-slate-500\">Beheer</p>\n </div>\n\n <TabsPrimitive.List aria-label=\"Beheer-secties\" className=\"xicms-admin__lijst -mx-1 flex gap-1 overflow-x-auto md:flex-col md:overflow-visible\">\n {panels.map((p) => (\n <TabsPrimitive.Trigger\n key={sleutel(p)}\n value={sleutel(p)}\n className=\"flex shrink-0 items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-200 hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none data-[state=active]:bg-slate-100 data-[state=active]:text-slate-900 data-[state=active]:shadow-[inset_2px_0_0_0_var(--color-brand-500)]\"\n >\n <PanelIcon id={p.moduleId} />\n {p.label}\n </TabsPrimitive.Trigger>\n ))}\n </TabsPrimitive.List>\n\n {/* Voet van de zijbalk: wie ben ik, welke engine draait er, en eruit. Stond eerder als\n vier losse regels op elkaar gepakt — e-mail, rol, versie en een onderstreepte link\n liepen visueel in elkaar over. Nu drie duidelijke lagen met lucht ertussen, en\n Uitloggen als een echte knop in plaats van een tekstlink. */}\n <div className=\"xicms-admin__voet mt-auto hidden min-w-0 flex-col gap-3 border-t border-slate-200 pt-3 md:flex\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-xs font-medium text-slate-600\" title={user.email ?? undefined}>{user.email}</p>\n <p className=\"text-xs text-slate-400\">{role}</p>\n </div>\n <CoreVersion />\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>\n Uitloggen\n </Button>\n </div>\n </div>\n\n {/* ── Werkblad ────────────────────────────────────────────────────────── */}\n <div className=\"min-w-0\">\n <header className=\"xicms-admin__kop sticky top-0 z-10 flex items-center justify-between gap-4 border-b border-slate-200 px-4 py-3 md:px-8\" style={{ background: \"var(--admin-canvas)\" }}>\n <h1 className=\"truncate text-base font-semibold\">{actievePanel?.label ?? \"Beheer\"}</h1>\n <a\n className=\"inline-flex h-8 shrink-0 items-center rounded-lg border border-slate-200 px-3 text-sm hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n href=\"/\"\n >\n Bekijk site ↗\n </a>\n </header>\n <div className=\"grid gap-6 p-4 md:p-8\">\n {panels.map((p) => (\n <TabsContent key={sleutel(p)} value={sleutel(p)} className=\"pt-0\">\n <p.Panel siteId={siteId} />\n </TabsContent>\n ))}\n <AdminHelp />\n </div>\n </div>\n </Tabs>\n )}\n </ConfirmHost>\n </ToastProvider>\n </div>\n );\n}\n"],"mappings":";AA2CM,cA4CI,YA5CJ;AAzCN,SAAS,gBAAgB;AACzB,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAC1B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,SAAS,qBAAgC;AAClD,SAAS,MAAM,mBAAmB;AAClC,YAAY,mBAAmB;AAC/B,SAAS,qBAAqB;AAC9B,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AACvB,SAAS,iBAAiB;AAC1B,SAAS,qBAA2C;AACpD,SAAS,iBAAiB;AAC1B,SAAS,mBAAmB;AAmB5B,SAAS,WAAW,MAAmB,KAA0C;AAC/E,SAAO,QAAQ,cAAc,cAAc,IAAI,IAAI,QAAQ,IAAI;AACjE;AAGA,SAAS,QAAQ,EAAE,SAAS,GAAkC;AAC5D,SACE,oBAAC,SAAI,WAAU,mDACb,8BAAC,SAAI,WAAU,qFAAqF,UAAS,GAC/G;AAEJ;AAEO,SAAS,SAAS;AAAA,EACvB,cAAc,CAAC;AAAA,EACf,WAAW;AACb,GAaG;AACD,QAAM,EAAE,MAAM,MAAM,SAAS,YAAY,QAAQ,IAAI,QAAQ;AAC7D,QAAM,EAAE,MAAM,OAAO,IAAI,cAAc;AACvC,QAAM,EAAE,UAAU,IAAI,WAAW;AAGjC,QAAM,SAAS,CAAC,GAAG,eAAe,GAAG,WAAW,EAC7C,OAAO,CAAC,MAAM,UAAU,IAAI,EAAE,QAAQ,KAAK,WAAW,MAAM,EAAE,OAAO,CAAC,EACtE,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAEnC,QAAM,UAAU,CAAC,MAAuB,EAAE,MAAM,EAAE;AAClD,QAAM,WAAW,OAAO,CAAC,KAAK,QAAQ,OAAO,CAAC,CAAC;AAI/C,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA6B,QAAQ;AACjE,QAAM,eAAe,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC,OAAO,UAAU,SAAS;AAE3E,SACE,oBAAC,SAAI,WAAW,WAAW,qCAAqC,eAC9D,8BAAC,iBACA,8BAAC,eACC,WAAC,aACA,qBAAC,WAAQ;AAAA;AAAA,IACqC,oBAAC,UAAK,oCAAsB;AAAA,IAAO;AAAA,KAEjF,IACE,UACF,oBAAC,WACC,8BAAC,UAAK,WAAU,kBAAiB,mCAAgB,GACnD,IACE,CAAC,OACH,oBAAC,SAAI,WAAU,mDACb,+BAAC,SAAI,WAAU,mBACb;AAAA,yBAAC,OAAE,WAAU,2CAA0C;AAAA;AAAA,MAAU;AAAA,OAAK;AAAA,IACtE,oBAAC,aAAU;AAAA,KACb,GACF,IACE,CAAC,QAAQ,IAAI,IACf,qBAAC,WAAQ;AAAA;AAAA,IACc,oBAAC,YAAQ,eAAK,OAAM;AAAA,IAAS;AAAA,IAClD,oBAAC,SAAI,WAAU,QACb,8BAAC,UAAO,SAAQ,aAAY,MAAK,MAAK,SAAS,MAAM,KAAK,QAAQ,GAAG,uBAAS,GAChF;AAAA,KACF,IACE,OAAO,WAAW,IACpB,oBAAC,WAAQ,yDAA2C,IAEpD,qBAAC,QAAK,OAAO,UAAU,UAAU,eAAe,WAAW,aAAY,YAAW,WAAU,wDAE1F;AAAA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,OAAO,EAAE,YAAY,uBAAuB;AAAA,QAE5C;AAAA,+BAAC,SAAI,WAAU,qBACb;AAAA,gCAAC,OAAE,WAAU,kCAAkC,gBAAK;AAAA,YACpD,oBAAC,OAAE,WAAU,0BAAyB,oBAAM;AAAA,aAC9C;AAAA,UAEA,oBAAC,cAAc,MAAd,EAAmB,cAAW,kBAAiB,WAAU,uFACvD,iBAAO,IAAI,CAAC,MACX;AAAA,YAAC,cAAc;AAAA,YAAd;AAAA,cAEC,OAAO,QAAQ,CAAC;AAAA,cAChB,WAAU;AAAA,cAEV;AAAA,oCAAC,aAAU,IAAI,EAAE,UAAU;AAAA,gBAC1B,EAAE;AAAA;AAAA;AAAA,YALE,QAAQ,CAAC;AAAA,UAMhB,CACD,GACH;AAAA,UAMA,qBAAC,SAAI,WAAU,kGACb;AAAA,iCAAC,SAAI,WAAU,WACb;AAAA,kCAAC,OAAE,WAAU,+CAA8C,OAAO,KAAK,SAAS,QAAY,eAAK,OAAM;AAAA,cACvG,oBAAC,OAAE,WAAU,0BAA0B,gBAAK;AAAA,eAC9C;AAAA,YACA,oBAAC,eAAY;AAAA,YACb,oBAAC,UAAO,SAAQ,aAAY,MAAK,MAAK,SAAS,MAAM,KAAK,QAAQ,GAAG,uBAErE;AAAA,aACF;AAAA;AAAA;AAAA,IACF;AAAA,IAGA,qBAAC,SAAI,WAAU,WACb;AAAA,2BAAC,YAAO,WAAU,0HAAyH,OAAO,EAAE,YAAY,sBAAsB,GACpL;AAAA,4BAAC,QAAG,WAAU,oCAAoC,wBAAc,SAAS,UAAS;AAAA,QAClF;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,MAAK;AAAA,YACN;AAAA;AAAA,QAED;AAAA,SACF;AAAA,MACA,qBAAC,SAAI,WAAU,yBACZ;AAAA,eAAO,IAAI,CAAC,MACX,oBAAC,eAA6B,OAAO,QAAQ,CAAC,GAAG,WAAU,QACzD,8BAAC,EAAE,OAAF,EAAQ,QAAgB,KADT,QAAQ,CAAC,CAE3B,CACD;AAAA,QACD,oBAAC,aAAU;AAAA,SACb;AAAA,OACF;AAAA,KACF,GAEH,GACD,GACF;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/admin/AdminApp.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { useAuth } from \"../auth/AuthProvider\";\nimport { LoginForm } from \"../auth/LoginForm\";\nimport { useSiteConfig } from \"../config\";\nimport { useModules } from \"../modules/context\";\nimport { canEdit, canManageSite, type Role } from \"../auth/roles\";\nimport { Tabs, TabsContent } from \"./ui/tabs\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\nimport { ToastProvider } from \"./ui/toast\";\nimport { ConfirmHost } from \"./ui/ConfirmDialog\";\nimport { Button } from \"./ui/primitives\";\nimport { PanelIcon } from \"./ui/PanelIcon\";\nimport { builtinPanels, type PanelDefinition } from \"./panelRegistry\";\nimport { AdminHelp } from \"./AdminHelp\";\nimport { CoreVersion } from \"./CoreVersion\";\n\n/**\n * Rol-bewaakt beheer (XiCMS 3.0). De secties komen uit de paneel-registry, gefilterd op de actieve\n * modules (`SiteConfig.modules`/`features`) en de rol van de gebruiker. Een module met een eigen\n * paneel (ook klant-modules via `extraPanels`) verschijnt automatisch.\n *\n * VORM: een schermvullend werkblad met een vaste zijbalk, geen kaart in de pagina. Het beheer viel\n * hiervoor binnen de site-chrome — nav erboven, footer eronder, cookiebanner eroverheen — en las\n * daardoor als een blok ergens op de website in plaats van als gereedschap. De site-chrome leeft nu\n * in `app/(site)/layout.tsx`; /beheer valt daarbuiten.\n *\n * De zijbalk verving de horizontale tabstrip omdat die bij zeven of meer panelen al omsloeg naar een\n * tweede regel. Verticaal is er bovendien ruimte voor een icoon naast het woord, en `orientation`\n * houdt de pijltjestoetsen correct (Radix).\n *\n * Wikkel in `.xicms-admin` zodat de Tailwind-stijlen uit `@xiwebdesign/xicms-core/admin.css` scoped\n * blijven en niet naar de site lekken. Importeer die CSS alleen op de /beheer-route.\n */\nfunction roleAllows(role: Role | null, min: PanelDefinition[\"minRole\"]): boolean {\n return min === \"siteadmin\" ? canManageSite(role) : canEdit(role);\n}\n\n/** Gecentreerde boodschap voor de toestanden waarin er (nog) geen beheer te tonen is. */\nfunction Melding({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"xicms-admin__midden grid place-items-center p-6\">\n <div className=\"w-full max-w-md rounded-xl border border-slate-200 bg-white p-6 text-sm shadow-sm\">{children}</div>\n </div>\n );\n}\n\nexport function AdminApp({\n extraPanels = [],\n embedded = false,\n}: {\n extraPanels?: PanelDefinition[];\n /**\n * Draait het beheer BINNEN de site-schil (header, logo, menu, footer eromheen) in plaats van als\n * schermvullend werkblad op een kale pagina?\n *\n * Het verschil is niet cosmetisch. Los rekent het werkblad met het VENSTER: `min-h-screen`, en een\n * zijbalk die met `sticky top-0` aan de bovenkant van het scherm plakt. Zet je daar een\n * site-header boven, dan loopt de pagina 100vh + de hoogte van die header, en plakt de zijbalk\n * onder de header vast in plaats van erlangs. Ingebed laat de zijbalk daarom los van het venster\n * en wordt hij een balk over de volle breedte — dat vecht niet met de navigatie erboven.\n */\n embedded?: boolean;\n}) {\n const { user, role, loading, configured, signOut } = useAuth();\n const { name, siteId } = useSiteConfig();\n const { activeIds } = useModules();\n\n // Panelen van actieve modules waar de rol toegang toe heeft, gesorteerd op volgorde.\n const panels = [...builtinPanels, ...extraPanels]\n .filter((p) => activeIds.has(p.moduleId) && roleAllows(role, p.minRole))\n .sort((a, b) => a.order - b.order);\n /** Sleutel van een paneel in de tabbladen: eigen `id` als die er is, anders de module-id. */\n const sleutel = (p: PanelDefinition) => p.id ?? p.moduleId;\n const firstTab = panels[0] && sleutel(panels[0]);\n // Gecontroleerd i.p.v. `defaultValue`: het werkblad toont de naam van de sectie waar je in zit.\n // Zonder die kop lees je alleen aan de zijbalk-markering af waar je bent, en dat is te weinig\n // houvast op een breed scherm waar de zijbalk ver van je blik af staat.\n const [actief, setActief] = useState<string | undefined>(firstTab);\n const actievePanel = panels.find((p) => sleutel(p) === (actief ?? firstTab));\n\n return (\n <div className={embedded ? \"xicms-admin xicms-admin--ingebed\" : \"xicms-admin\"}>\n <ToastProvider>\n <ConfirmHost>\n {!configured ? (\n <Melding>\n Firebase is nog niet geconfigureerd. Vul de <code>NEXT_PUBLIC_FIREBASE_*</code>-variabelen in\n om het beheer aan te zetten.\n </Melding>\n ) : loading ? (\n <Melding>\n <span className=\"text-slate-500\">Bezig met laden…</span>\n </Melding>\n ) : !user ? (\n <div className=\"xicms-admin__midden grid place-items-center p-6\">\n <div className=\"w-full max-w-sm\">\n <p className=\"mb-4 text-center text-sm text-slate-500\">Beheer · {name}</p>\n {/*\n Na inloggen naar de HOMEPAGINA, niet naar dit scherm.\n\n Dat is een sturende keuze, geen detail. Wie op het beheer uitkomt gaat daar\n rondkijken en bewerkt zijn teksten in formulieren. Wie op zijn eigen site uitkomt\n ziet de beheerbalk over de pagina heen en past de tekst aan op de plek waar hij\n staat — en dat is waar XiCMS voor gemaakt is. Het beheer blijft één klik weg, via\n \"Beheren\" in die balk, voor het werk dat er wél thuishoort.\n\n Ook hier en niet alleen op /login: je komt op /beheer terecht via een bladwijzer of\n een oude link, en dan gold die keuze ineens niet.\n */}\n <LoginForm onSuccess={() => { window.location.replace(\"/\"); }} />\n </div>\n </div>\n ) : !canEdit(role) ? (\n <Melding>\n Je bent ingelogd als <strong>{user.email}</strong>, maar hebt geen beheerrechten voor deze site.\n <div className=\"mt-3\">\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>Uitloggen</Button>\n </div>\n </Melding>\n ) : panels.length === 0 ? (\n <Melding>Geen actieve beheer-modules voor deze site.</Melding>\n ) : (\n <Tabs value={actief ?? firstTab} onValueChange={setActief} orientation=\"vertical\" className=\"xicms-admin__raster md:grid md:grid-cols-[248px_1fr]\">\n {/* ── Zijbalk ─────────────────────────────────────────────────────────── */}\n <div\n className=\"xicms-admin__rail flex flex-col gap-4 border-b border-slate-200 p-4 md:sticky md:top-0 md:h-screen md:border-r md:border-b-0\"\n style={{ background: \"var(--admin-sidebar)\" }}\n >\n <div className=\"min-w-0 px-2 pt-1\">\n <p className=\"truncate text-sm font-semibold\">{name}</p>\n <p className=\"text-xs text-slate-500\">Beheer</p>\n </div>\n\n <TabsPrimitive.List aria-label=\"Beheer-secties\" className=\"xicms-admin__lijst -mx-1 flex gap-1 overflow-x-auto md:flex-col md:overflow-visible\">\n {panels.map((p) => (\n <TabsPrimitive.Trigger\n key={sleutel(p)}\n value={sleutel(p)}\n className=\"flex shrink-0 items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-200 hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none data-[state=active]:bg-slate-100 data-[state=active]:text-slate-900 data-[state=active]:shadow-[inset_2px_0_0_0_var(--color-brand-500)]\"\n >\n <PanelIcon id={p.moduleId} />\n {p.label}\n </TabsPrimitive.Trigger>\n ))}\n </TabsPrimitive.List>\n\n {/* Voet van de zijbalk: wie ben ik, welke engine draait er, en eruit. Stond eerder als\n vier losse regels op elkaar gepakt — e-mail, rol, versie en een onderstreepte link\n liepen visueel in elkaar over. Nu drie duidelijke lagen met lucht ertussen, en\n Uitloggen als een echte knop in plaats van een tekstlink. */}\n <div className=\"xicms-admin__voet mt-auto hidden min-w-0 flex-col gap-3 border-t border-slate-200 pt-3 md:flex\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-xs font-medium text-slate-600\" title={user.email ?? undefined}>{user.email}</p>\n <p className=\"text-xs text-slate-400\">{role}</p>\n </div>\n <CoreVersion />\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void signOut()}>\n Uitloggen\n </Button>\n </div>\n </div>\n\n {/* ── Werkblad ────────────────────────────────────────────────────────── */}\n <div className=\"min-w-0\">\n <header className=\"xicms-admin__kop sticky top-0 z-10 flex items-center justify-between gap-4 border-b border-slate-200 px-4 py-3 md:px-8\" style={{ background: \"var(--admin-canvas)\" }}>\n <h1 className=\"truncate text-base font-semibold\">{actievePanel?.label ?? \"Beheer\"}</h1>\n <a\n className=\"inline-flex h-8 shrink-0 items-center rounded-lg border border-slate-200 px-3 text-sm hover:bg-slate-100 focus-visible:ring-2 focus-visible:ring-brand-500 focus-visible:outline-none\"\n href=\"/\"\n >\n Bekijk site ↗\n </a>\n </header>\n <div className=\"grid gap-6 p-4 md:p-8\">\n {panels.map((p) => (\n <TabsContent key={sleutel(p)} value={sleutel(p)} className=\"pt-0\">\n <p.Panel siteId={siteId} />\n </TabsContent>\n ))}\n <AdminHelp />\n </div>\n </div>\n </Tabs>\n )}\n </ConfirmHost>\n </ToastProvider>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -13,6 +13,19 @@ export declare function subscribeVersions(siteId: string, pageId: string, cb: (v
13
13
  }[]) => void): () => void;
14
14
  /** Laadt de werkversie (draft) voor een pad — voor het inline bewerken op de live pagina. */
15
15
  export declare function getDraftByPath(siteId: string, path: string): Promise<DraftPage | null>;
16
+ /**
17
+ * De GEPUBLICEERDE versie van één pagina — dat wat een gewone bezoeker ziet.
18
+ *
19
+ * De editor heeft dit nodig om te kunnen zeggen wát er veranderd is. Zonder deze tweede lezing
20
+ * weet hij alleen of de werkversie sinds de laatste autosave nog is aangeraakt, en dat is een
21
+ * andere vraag: je kunt een uur geleden iets hebben gewijzigd, keurig opgeslagen, en nog steeds
22
+ * niets live hebben staan. "Opgeslagen" en "gepubliceerd" zijn twee toestanden, en de knop
23
+ * rechtsonder gaat over de tweede.
24
+ *
25
+ * Publieke leesrechten volstaan: `sites/{id}/pages/{id}` is precies de content die ook in de
26
+ * HTML van de live pagina staat. Geen werkversies, geen inzendingen.
27
+ */
28
+ export declare function getPublishedPage(siteId: string, pageId: string): Promise<Page | null>;
16
29
  /**
17
30
  * Live-listener op één werkversie — voor conflictdetectie in de visuele editor (twee tabs of
18
31
  * twee editors tegelijk). `fromServer` is false voor de lokale echo van een eigen write
@@ -53,6 +53,13 @@ async function getDraftByPath(siteId, path) {
53
53
  const d = snap.docs[0];
54
54
  return d ? sanitizeDraftBlocks({ id: d.id, ...d.data() }) : null;
55
55
  }
56
+ async function getPublishedPage(siteId, pageId) {
57
+ if (isDemoActive()) return null;
58
+ const db = getDb();
59
+ if (!db) return null;
60
+ const snap = await getDoc(doc(db, "sites", siteId, "pages", pageId));
61
+ return snap.exists() ? snap.data() : null;
62
+ }
56
63
  function subscribeDraft(siteId, pageId, cb) {
57
64
  if (isDemoActive()) return () => {
58
65
  };
@@ -70,6 +77,11 @@ function zonderUndefined(o) {
70
77
  return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
71
78
  }
72
79
  async function createPage(siteId, input) {
80
+ if (isDemoActive()) {
81
+ const id = demoIdForPath(input.path);
82
+ saveDemoDraft(siteId, id, { blocks: [], status: "draft", ...zonderUndefined(input) });
83
+ return id;
84
+ }
73
85
  const db = getDb();
74
86
  if (!db) throw new Error("Firebase niet geconfigureerd.");
75
87
  const ref = await addDoc(collection(db, "sites", siteId, "drafts"), {
@@ -139,6 +151,7 @@ export {
139
151
  createPage,
140
152
  deletePage,
141
153
  getDraftByPath,
154
+ getPublishedPage,
142
155
  publishPage,
143
156
  restoreVersion,
144
157
  saveDraft,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/lib/pagesClient.ts"],"sourcesContent":["\"use client\";\n\nimport {\n addDoc,\n collection,\n deleteDoc,\n doc,\n getDoc,\n getDocs,\n limit,\n onSnapshot,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n where,\n} from \"firebase/firestore\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport type { Page } from \"../../schema/pages\";\nimport { isDemoActive, demoIdForPath, resolveDemoPage, saveDemoDraft } from \"../../demo/demoStore\";\nimport { sanitizeDraftBlocks } from \"./sanitizeDraft\";\n\nexport type DraftPage = Page & { id: string; published?: boolean };\n\n/** Live-lijst van pagina's (werkversies in `drafts/`). */\nexport function subscribePages(siteId: string, cb: (pages: DraftPage[]) => void): () => void {\n const db = getDb();\n if (!db) {\n cb([]);\n return () => {};\n }\n const q = query(collection(db, \"sites\", siteId, \"drafts\"), orderBy(\"order\", \"asc\"));\n return onSnapshot(q, (snap) => {\n cb(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<DraftPage, \"id\">) })));\n });\n}\n\nexport function subscribeVersions(\n siteId: string,\n pageId: string,\n cb: (versions: { id: string; createdAt?: unknown; kind?: string; label?: string }[]) => void,\n): () => void {\n const db = getDb();\n if (!db) {\n cb([]);\n return () => {};\n }\n const q = query(collection(db, \"sites\", siteId, \"pages\", pageId, \"versions\"), orderBy(\"createdAt\", \"desc\"));\n return onSnapshot(q, (snap) => cb(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Record<string, unknown>) }))));\n}\n\n/** Laadt de werkversie (draft) voor een pad — voor het inline bewerken op de live pagina. */\nexport async function getDraftByPath(siteId: string, path: string): Promise<DraftPage | null> {\n // Demo-modus: de werkversie komt uit de lokale browseropslag (seed + lokale bewerkingen).\n if (isDemoActive()) {\n const page = resolveDemoPage(siteId, path);\n return page ? ({ ...page, id: demoIdForPath(path) } as DraftPage) : null;\n }\n const db = getDb();\n if (!db) return null;\n const snap = await getDocs(\n query(collection(db, \"sites\", siteId, \"drafts\"), where(\"path\", \"==\", path), limit(1)),\n );\n const d = snap.docs[0];\n // Rich text uit een werkversie is nooit server-side gesaneerd (zie sanitizeDraft.ts).\n return d ? sanitizeDraftBlocks({ id: d.id, ...(d.data() as Omit<DraftPage, \"id\">) }) : null;\n}\n\n/**\n * Live-listener op één werkversie — voor conflictdetectie in de visuele editor (twee tabs of\n * twee editors tegelijk). `fromServer` is false voor de lokale echo van een eigen write\n * (hasPendingWrites), zodat de aanroeper eigen saves kan negeren.\n */\nexport function subscribeDraft(\n siteId: string,\n pageId: string,\n cb: (draft: DraftPage | null, meta: { fromServer: boolean }) => void,\n): () => void {\n if (isDemoActive()) return () => {};\n const db = getDb();\n if (!db) return () => {};\n return onSnapshot(doc(db, \"sites\", siteId, \"drafts\", pageId), (snap) => {\n cb(\n snap.exists() ? sanitizeDraftBlocks({ id: snap.id, ...(snap.data() as Omit<DraftPage, \"id\">) }) : null,\n { fromServer: !snap.metadata.hasPendingWrites },\n );\n });\n}\n\n/**\n * Firestore WEIGERT `undefined` als veldwaarde: `setDoc({ seo: undefined })` gooit\n * \"Unsupported field value: undefined\" en de hele schrijfactie mislukt. De editors geven hun\n * pagina-state als geheel door (`{ title, slug, path, seo, blocks }`), en bij een nieuwe pagina is\n * `seo` gewoon nog niet ingevuld — dus liep élke eerste publicatie van een nieuwe pagina stuk op\n * de autosave die eraan voorafgaat. Gevonden op de staging-deploy van groenhof: \"Publiceren\n * mislukt — Unsupported field value: undefined (found in field seo)\".\n *\n * Weglaten is hier ook de JUISTE semantiek: deze writes zijn `merge: true`, en \"veld niet\n * meegestuurd\" betekent daar \"laat staan wat er stond\". Een veld écht wissen doe je met\n * `deleteField()`, niet met `undefined` — dat onderscheid blijft zo intact.\n */\nfunction zonderUndefined<T extends Record<string, unknown>>(o: T): Partial<T> {\n return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined)) as Partial<T>;\n}\n\nexport async function createPage(siteId: string, input: Partial<Page> & { title: string; slug: string; path: string }) {\n const db = getDb();\n if (!db) throw new Error(\"Firebase niet geconfigureerd.\");\n const ref = await addDoc(collection(db, \"sites\", siteId, \"drafts\"), {\n parentId: null,\n order: Date.now(),\n status: \"draft\",\n blocks: [],\n published: false,\n ...zonderUndefined(input),\n updatedAt: serverTimestamp(),\n });\n return ref.id;\n}\n\nexport async function saveDraft(siteId: string, pageId: string, partial: Partial<Page>) {\n // Demo-modus: bewaar de bewerking alleen lokaal in de browser (niet in Firebase).\n if (isDemoActive()) {\n saveDemoDraft(siteId, pageId, partial);\n return;\n }\n const db = getDb();\n if (!db) return;\n await setDoc(\n doc(db, \"sites\", siteId, \"drafts\", pageId),\n { ...zonderUndefined(partial), updatedAt: serverTimestamp() },\n { merge: true },\n );\n}\n\nexport async function deletePage(siteId: string, pageId: string) {\n const db = getDb();\n if (!db) return;\n // Draft direct (client mag drafts beheren); de gepubliceerde pagina via de Cloud Function —\n // die verwijdert óók de versions-subcollectie (Firestore cascadeert niet; directe client-deletes\n // lieten wees-versiegeschiedenis achter en zijn nu in de rules dichtgezet).\n await deleteDoc(doc(db, \"sites\", siteId, \"drafts\", pageId));\n const app = getFirebaseApp();\n if (!app) return;\n try {\n const fn = callable(app, \"deletePage\");\n await fn({ siteId, pageId });\n } catch {\n /* gepubliceerde versie bestond niet — negeren */\n }\n}\n\n/**\n * Herstelt een eerdere versie naar de WERKVERSIE (draft). De gebruiker moet daarna zelf\n * publiceren — zo is herstellen veilig en omkeerbaar (de live site verandert pas bij Publiceren).\n * Geeft de herstelde velden terug zodat de editor zijn lokale state kan bijwerken.\n */\nexport async function restoreVersion(\n siteId: string,\n pageId: string,\n versionId: string,\n): Promise<Partial<Page>> {\n const db = getDb();\n if (!db) throw new Error(\"Firebase niet geconfigureerd.\");\n const snap = await getDoc(doc(db, \"sites\", siteId, \"pages\", pageId, \"versions\", versionId));\n if (!snap.exists()) throw new Error(\"Versie niet gevonden.\");\n const s = (snap.data().snapshot ?? {}) as Record<string, unknown>;\n const restored: Partial<Page> = {\n title: s.title as string,\n slug: s.slug as string,\n path: s.path as string,\n seo: s.seo as Page[\"seo\"],\n blocks: s.blocks as Page[\"blocks\"],\n order: s.order as number,\n };\n await saveDraft(siteId, pageId, restored);\n return restored;\n}\n\n/** Publiceren via de Cloud Function (server-side validatie + versie-snapshot + audit). */\nexport async function publishPage(siteId: string, pageId: string): Promise<void> {\n // Demo-modus: er wordt niets echt gepubliceerd; de lokale bewerking is al zichtbaar.\n if (isDemoActive()) {\n saveDemoDraft(siteId, pageId, {});\n return;\n }\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = callable(app, \"publishPage\");\n await fn({ siteId, pageId });\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/lib/pagesClient.ts"],"sourcesContent":["\"use client\";\n\nimport {\n addDoc,\n collection,\n deleteDoc,\n doc,\n getDoc,\n getDocs,\n limit,\n onSnapshot,\n orderBy,\n query,\n serverTimestamp,\n setDoc,\n where,\n} from \"firebase/firestore\";\nimport { callable } from \"../../firebase/functionsClient\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport type { Page } from \"../../schema/pages\";\nimport { isDemoActive, demoIdForPath, resolveDemoPage, saveDemoDraft } from \"../../demo/demoStore\";\nimport { sanitizeDraftBlocks } from \"./sanitizeDraft\";\n\nexport type DraftPage = Page & { id: string; published?: boolean };\n\n/** Live-lijst van pagina's (werkversies in `drafts/`). */\nexport function subscribePages(siteId: string, cb: (pages: DraftPage[]) => void): () => void {\n const db = getDb();\n if (!db) {\n cb([]);\n return () => {};\n }\n const q = query(collection(db, \"sites\", siteId, \"drafts\"), orderBy(\"order\", \"asc\"));\n return onSnapshot(q, (snap) => {\n cb(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<DraftPage, \"id\">) })));\n });\n}\n\nexport function subscribeVersions(\n siteId: string,\n pageId: string,\n cb: (versions: { id: string; createdAt?: unknown; kind?: string; label?: string }[]) => void,\n): () => void {\n const db = getDb();\n if (!db) {\n cb([]);\n return () => {};\n }\n const q = query(collection(db, \"sites\", siteId, \"pages\", pageId, \"versions\"), orderBy(\"createdAt\", \"desc\"));\n return onSnapshot(q, (snap) => cb(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Record<string, unknown>) }))));\n}\n\n/** Laadt de werkversie (draft) voor een pad — voor het inline bewerken op de live pagina. */\nexport async function getDraftByPath(siteId: string, path: string): Promise<DraftPage | null> {\n // Demo-modus: de werkversie komt uit de lokale browseropslag (seed + lokale bewerkingen).\n if (isDemoActive()) {\n const page = resolveDemoPage(siteId, path);\n return page ? ({ ...page, id: demoIdForPath(path) } as DraftPage) : null;\n }\n const db = getDb();\n if (!db) return null;\n const snap = await getDocs(\n query(collection(db, \"sites\", siteId, \"drafts\"), where(\"path\", \"==\", path), limit(1)),\n );\n const d = snap.docs[0];\n // Rich text uit een werkversie is nooit server-side gesaneerd (zie sanitizeDraft.ts).\n return d ? sanitizeDraftBlocks({ id: d.id, ...(d.data() as Omit<DraftPage, \"id\">) }) : null;\n}\n\n/**\n * De GEPUBLICEERDE versie van één pagina — dat wat een gewone bezoeker ziet.\n *\n * De editor heeft dit nodig om te kunnen zeggen wát er veranderd is. Zonder deze tweede lezing\n * weet hij alleen of de werkversie sinds de laatste autosave nog is aangeraakt, en dat is een\n * andere vraag: je kunt een uur geleden iets hebben gewijzigd, keurig opgeslagen, en nog steeds\n * niets live hebben staan. \"Opgeslagen\" en \"gepubliceerd\" zijn twee toestanden, en de knop\n * rechtsonder gaat over de tweede.\n *\n * Publieke leesrechten volstaan: `sites/{id}/pages/{id}` is precies de content die ook in de\n * HTML van de live pagina staat. Geen werkversies, geen inzendingen.\n */\nexport async function getPublishedPage(siteId: string, pageId: string): Promise<Page | null> {\n // In de demo is \"live\" wat er in de seed stond; daar bestaat geen aparte gepubliceerde kopie.\n if (isDemoActive()) return null;\n const db = getDb();\n if (!db) return null;\n const snap = await getDoc(doc(db, \"sites\", siteId, \"pages\", pageId));\n return snap.exists() ? (snap.data() as Page) : null;\n}\n\n/**\n * Live-listener op één werkversie — voor conflictdetectie in de visuele editor (twee tabs of\n * twee editors tegelijk). `fromServer` is false voor de lokale echo van een eigen write\n * (hasPendingWrites), zodat de aanroeper eigen saves kan negeren.\n */\nexport function subscribeDraft(\n siteId: string,\n pageId: string,\n cb: (draft: DraftPage | null, meta: { fromServer: boolean }) => void,\n): () => void {\n if (isDemoActive()) return () => {};\n const db = getDb();\n if (!db) return () => {};\n return onSnapshot(doc(db, \"sites\", siteId, \"drafts\", pageId), (snap) => {\n cb(\n snap.exists() ? sanitizeDraftBlocks({ id: snap.id, ...(snap.data() as Omit<DraftPage, \"id\">) }) : null,\n { fromServer: !snap.metadata.hasPendingWrites },\n );\n });\n}\n\n/**\n * Firestore WEIGERT `undefined` als veldwaarde: `setDoc({ seo: undefined })` gooit\n * \"Unsupported field value: undefined\" en de hele schrijfactie mislukt. De editors geven hun\n * pagina-state als geheel door (`{ title, slug, path, seo, blocks }`), en bij een nieuwe pagina is\n * `seo` gewoon nog niet ingevuld — dus liep élke eerste publicatie van een nieuwe pagina stuk op\n * de autosave die eraan voorafgaat. Gevonden op de staging-deploy van groenhof: \"Publiceren\n * mislukt — Unsupported field value: undefined (found in field seo)\".\n *\n * Weglaten is hier ook de JUISTE semantiek: deze writes zijn `merge: true`, en \"veld niet\n * meegestuurd\" betekent daar \"laat staan wat er stond\". Een veld écht wissen doe je met\n * `deleteField()`, niet met `undefined` — dat onderscheid blijft zo intact.\n */\nfunction zonderUndefined<T extends Record<string, unknown>>(o: T): Partial<T> {\n return Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined)) as Partial<T>;\n}\n\nexport async function createPage(siteId: string, input: Partial<Page> & { title: string; slug: string; path: string }) {\n /**\n * In de demo bestaat er geen Firestore om naar te schrijven.\n *\n * Deze functie was als enige van dit bestand de demo-tak vergeten — `getDraftByPath`,\n * `saveDraft` en `publishPage` hebben hem alle drie. Gevolg: wie in een demo op\n * \"+ Pagina aanmaken\" drukte, kreeg \"Missing or insufficient permissions\" van de echte\n * database. Op een site die bestaat om te laten zien hoe prettig het bewerken werkt.\n */\n if (isDemoActive()) {\n const id = demoIdForPath(input.path);\n saveDemoDraft(siteId, id, { blocks: [], status: \"draft\", ...zonderUndefined(input) } as Partial<Page>);\n return id;\n }\n const db = getDb();\n if (!db) throw new Error(\"Firebase niet geconfigureerd.\");\n const ref = await addDoc(collection(db, \"sites\", siteId, \"drafts\"), {\n parentId: null,\n order: Date.now(),\n status: \"draft\",\n blocks: [],\n published: false,\n ...zonderUndefined(input),\n updatedAt: serverTimestamp(),\n });\n return ref.id;\n}\n\nexport async function saveDraft(siteId: string, pageId: string, partial: Partial<Page>) {\n // Demo-modus: bewaar de bewerking alleen lokaal in de browser (niet in Firebase).\n if (isDemoActive()) {\n saveDemoDraft(siteId, pageId, partial);\n return;\n }\n const db = getDb();\n if (!db) return;\n await setDoc(\n doc(db, \"sites\", siteId, \"drafts\", pageId),\n { ...zonderUndefined(partial), updatedAt: serverTimestamp() },\n { merge: true },\n );\n}\n\nexport async function deletePage(siteId: string, pageId: string) {\n const db = getDb();\n if (!db) return;\n // Draft direct (client mag drafts beheren); de gepubliceerde pagina via de Cloud Function —\n // die verwijdert óók de versions-subcollectie (Firestore cascadeert niet; directe client-deletes\n // lieten wees-versiegeschiedenis achter en zijn nu in de rules dichtgezet).\n await deleteDoc(doc(db, \"sites\", siteId, \"drafts\", pageId));\n const app = getFirebaseApp();\n if (!app) return;\n try {\n const fn = callable(app, \"deletePage\");\n await fn({ siteId, pageId });\n } catch {\n /* gepubliceerde versie bestond niet — negeren */\n }\n}\n\n/**\n * Herstelt een eerdere versie naar de WERKVERSIE (draft). De gebruiker moet daarna zelf\n * publiceren — zo is herstellen veilig en omkeerbaar (de live site verandert pas bij Publiceren).\n * Geeft de herstelde velden terug zodat de editor zijn lokale state kan bijwerken.\n */\nexport async function restoreVersion(\n siteId: string,\n pageId: string,\n versionId: string,\n): Promise<Partial<Page>> {\n const db = getDb();\n if (!db) throw new Error(\"Firebase niet geconfigureerd.\");\n const snap = await getDoc(doc(db, \"sites\", siteId, \"pages\", pageId, \"versions\", versionId));\n if (!snap.exists()) throw new Error(\"Versie niet gevonden.\");\n const s = (snap.data().snapshot ?? {}) as Record<string, unknown>;\n const restored: Partial<Page> = {\n title: s.title as string,\n slug: s.slug as string,\n path: s.path as string,\n seo: s.seo as Page[\"seo\"],\n blocks: s.blocks as Page[\"blocks\"],\n order: s.order as number,\n };\n await saveDraft(siteId, pageId, restored);\n return restored;\n}\n\n/** Publiceren via de Cloud Function (server-side validatie + versie-snapshot + audit). */\nexport async function publishPage(siteId: string, pageId: string): Promise<void> {\n // Demo-modus: er wordt niets echt gepubliceerd; de lokale bewerking is al zichtbaar.\n if (isDemoActive()) {\n saveDemoDraft(siteId, pageId, {});\n return;\n }\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = callable(app, \"publishPage\");\n await fn({ siteId, pageId });\n}\n"],"mappings":";AAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB;AACzB,SAAS,OAAO,sBAAsB;AAEtC,SAAS,cAAc,eAAe,iBAAiB,qBAAqB;AAC5E,SAAS,2BAA2B;AAK7B,SAAS,eAAe,QAAgB,IAA8C;AAC3F,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,IAAI;AACP,OAAG,CAAC,CAAC;AACL,WAAO,MAAM;AAAA,IAAC;AAAA,EAChB;AACA,QAAM,IAAI,MAAM,WAAW,IAAI,SAAS,QAAQ,QAAQ,GAAG,QAAQ,SAAS,KAAK,CAAC;AAClF,SAAO,WAAW,GAAG,CAAC,SAAS;AAC7B,OAAG,KAAK,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAA4B,EAAE,CAAC;AAAA,EACjF,CAAC;AACH;AAEO,SAAS,kBACd,QACA,QACA,IACY;AACZ,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,IAAI;AACP,OAAG,CAAC,CAAC;AACL,WAAO,MAAM;AAAA,IAAC;AAAA,EAChB;AACA,QAAM,IAAI,MAAM,WAAW,IAAI,SAAS,QAAQ,SAAS,QAAQ,UAAU,GAAG,QAAQ,aAAa,MAAM,CAAC;AAC1G,SAAO,WAAW,GAAG,CAAC,SAAS,GAAG,KAAK,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAA8B,EAAE,CAAC,CAAC;AACnH;AAGA,eAAsB,eAAe,QAAgB,MAAyC;AAE5F,MAAI,aAAa,GAAG;AAClB,UAAM,OAAO,gBAAgB,QAAQ,IAAI;AACzC,WAAO,OAAQ,EAAE,GAAG,MAAM,IAAI,cAAc,IAAI,EAAE,IAAkB;AAAA,EACtE;AACA,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,OAAO,MAAM;AAAA,IACjB,MAAM,WAAW,IAAI,SAAS,QAAQ,QAAQ,GAAG,MAAM,QAAQ,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC;AAAA,EACtF;AACA,QAAM,IAAI,KAAK,KAAK,CAAC;AAErB,SAAO,IAAI,oBAAoB,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAA4B,CAAC,IAAI;AACzF;AAcA,eAAsB,iBAAiB,QAAgB,QAAsC;AAE3F,MAAI,aAAa,EAAG,QAAO;AAC3B,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,QAAO;AAChB,QAAM,OAAO,MAAM,OAAO,IAAI,IAAI,SAAS,QAAQ,SAAS,MAAM,CAAC;AACnE,SAAO,KAAK,OAAO,IAAK,KAAK,KAAK,IAAa;AACjD;AAOO,SAAS,eACd,QACA,QACA,IACY;AACZ,MAAI,aAAa,EAAG,QAAO,MAAM;AAAA,EAAC;AAClC,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,QAAO,MAAM;AAAA,EAAC;AACvB,SAAO,WAAW,IAAI,IAAI,SAAS,QAAQ,UAAU,MAAM,GAAG,CAAC,SAAS;AACtE;AAAA,MACE,KAAK,OAAO,IAAI,oBAAoB,EAAE,IAAI,KAAK,IAAI,GAAI,KAAK,KAAK,EAA4B,CAAC,IAAI;AAAA,MAClG,EAAE,YAAY,CAAC,KAAK,SAAS,iBAAiB;AAAA,IAChD;AAAA,EACF,CAAC;AACH;AAcA,SAAS,gBAAmD,GAAkB;AAC5E,SAAO,OAAO,YAAY,OAAO,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,MAAM,MAAS,CAAC;AAChF;AAEA,eAAsB,WAAW,QAAgB,OAAsE;AASrH,MAAI,aAAa,GAAG;AAClB,UAAM,KAAK,cAAc,MAAM,IAAI;AACnC,kBAAc,QAAQ,IAAI,EAAE,QAAQ,CAAC,GAAG,QAAQ,SAAS,GAAG,gBAAgB,KAAK,EAAE,CAAkB;AACrG,WAAO;AAAA,EACT;AACA,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,OAAM,IAAI,MAAM,+BAA+B;AACxD,QAAM,MAAM,MAAM,OAAO,WAAW,IAAI,SAAS,QAAQ,QAAQ,GAAG;AAAA,IAClE,UAAU;AAAA,IACV,OAAO,KAAK,IAAI;AAAA,IAChB,QAAQ;AAAA,IACR,QAAQ,CAAC;AAAA,IACT,WAAW;AAAA,IACX,GAAG,gBAAgB,KAAK;AAAA,IACxB,WAAW,gBAAgB;AAAA,EAC7B,CAAC;AACD,SAAO,IAAI;AACb;AAEA,eAAsB,UAAU,QAAgB,QAAgB,SAAwB;AAEtF,MAAI,aAAa,GAAG;AAClB,kBAAc,QAAQ,QAAQ,OAAO;AACrC;AAAA,EACF;AACA,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI;AACT,QAAM;AAAA,IACJ,IAAI,IAAI,SAAS,QAAQ,UAAU,MAAM;AAAA,IACzC,EAAE,GAAG,gBAAgB,OAAO,GAAG,WAAW,gBAAgB,EAAE;AAAA,IAC5D,EAAE,OAAO,KAAK;AAAA,EAChB;AACF;AAEA,eAAsB,WAAW,QAAgB,QAAgB;AAC/D,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI;AAIT,QAAM,UAAU,IAAI,IAAI,SAAS,QAAQ,UAAU,MAAM,CAAC;AAC1D,QAAM,MAAM,eAAe;AAC3B,MAAI,CAAC,IAAK;AACV,MAAI;AACF,UAAM,KAAK,SAAS,KAAK,YAAY;AACrC,UAAM,GAAG,EAAE,QAAQ,OAAO,CAAC;AAAA,EAC7B,QAAQ;AAAA,EAER;AACF;AAOA,eAAsB,eACpB,QACA,QACA,WACwB;AACxB,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,OAAM,IAAI,MAAM,+BAA+B;AACxD,QAAM,OAAO,MAAM,OAAO,IAAI,IAAI,SAAS,QAAQ,SAAS,QAAQ,YAAY,SAAS,CAAC;AAC1F,MAAI,CAAC,KAAK,OAAO,EAAG,OAAM,IAAI,MAAM,uBAAuB;AAC3D,QAAM,IAAK,KAAK,KAAK,EAAE,YAAY,CAAC;AACpC,QAAM,WAA0B;AAAA,IAC9B,OAAO,EAAE;AAAA,IACT,MAAM,EAAE;AAAA,IACR,MAAM,EAAE;AAAA,IACR,KAAK,EAAE;AAAA,IACP,QAAQ,EAAE;AAAA,IACV,OAAO,EAAE;AAAA,EACX;AACA,QAAM,UAAU,QAAQ,QAAQ,QAAQ;AACxC,SAAO;AACT;AAGA,eAAsB,YAAY,QAAgB,QAA+B;AAE/E,MAAI,aAAa,GAAG;AAClB,kBAAc,QAAQ,QAAQ,CAAC,CAAC;AAChC;AAAA,EACF;AACA,QAAM,MAAM,eAAe;AAC3B,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,+BAA+B;AACzD,QAAM,KAAK,SAAS,KAAK,aAAa;AACtC,QAAM,GAAG,EAAE,QAAQ,OAAO,CAAC;AAC7B;","names":[]}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Het productformulier in het zijpaneel van de bewerkmodus.
3
+ *
4
+ * Het tandwiel bij een product deed niets — en dat was geen bug maar een gat in het ontwerp: het
5
+ * blok waar het tandwiel bij hoort (`productDetail`, `productGrid`) heeft nauwelijks eigen velden.
6
+ * Alles wat je op het scherm ziet staan — naam, prijs, foto's, varianten — komt uit het product.
7
+ * Je klikte dus op een tandwiel bij een productfoto en kreeg een paneel met "welk product" en
8
+ * "toon ook gerelateerde producten". Formeel juist, in de praktijk onbruikbaar.
9
+ *
10
+ * Nu opent hetzelfde formulier als in het Webwinkel-paneel, met het product dat je aanwees al open.
11
+ */
12
+ export declare function ProductPaneel({ siteId, productId }: {
13
+ siteId: string;
14
+ productId: string;
15
+ }): import("react").JSX.Element;
@@ -0,0 +1,55 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { collection, onSnapshot } from "firebase/firestore";
5
+ import { getDb } from "../../firebase/client.js";
6
+ import { ProductForm } from "./ProductsTab.js";
7
+ import { subscribeProduct } from "./productClient.js";
8
+ import { useToast } from "../ui/toast.js";
9
+ import { useConfirm } from "../ui/ConfirmDialog.js";
10
+ function ProductPaneel({ siteId, productId }) {
11
+ const { toast } = useToast();
12
+ const confirm = useConfirm();
13
+ const [product, setProduct] = useState(void 0);
14
+ const [cats, setCats] = useState([]);
15
+ useEffect(() => subscribeProduct(siteId, productId, setProduct), [siteId, productId]);
16
+ useEffect(() => {
17
+ const db = getDb();
18
+ if (!db) return;
19
+ return onSnapshot(
20
+ collection(db, "sites", siteId, "shopCategories"),
21
+ (snap) => setCats(snap.docs.map((d) => ({ id: d.id, ...d.data() })))
22
+ );
23
+ }, [siteId]);
24
+ if (product === void 0) return /* @__PURE__ */ jsx("p", { className: "xicms-ve__hint", children: "Product laden\u2026" });
25
+ if (product === null) {
26
+ return /* @__PURE__ */ jsx("p", { className: "xicms-ve__hint", children: "Dit product bestaat niet meer. Ververs de pagina, of beheer de webwinkel via Beheren \u2192 Webwinkel." });
27
+ }
28
+ return /* @__PURE__ */ jsxs("div", { className: "grid gap-3", children: [
29
+ /* @__PURE__ */ jsxs("h3", { className: "text-sm font-semibold", children: [
30
+ "Product \u2014 ",
31
+ product.name
32
+ ] }),
33
+ /* @__PURE__ */ jsxs("p", { className: "xicms-ve__hint", children: [
34
+ "Wijzigingen aan een product zijn ",
35
+ /* @__PURE__ */ jsx("strong", { children: "meteen live" }),
36
+ " \u2014 ook op de cataloguspagina. Publiceren is hier niet nodig en geldt alleen voor de tekst van de pagina zelf."
37
+ ] }),
38
+ /* @__PURE__ */ jsx(
39
+ ProductForm,
40
+ {
41
+ siteId,
42
+ product,
43
+ categories: cats,
44
+ onDeleted: () => setProduct(null),
45
+ onToast: toast,
46
+ confirm
47
+ },
48
+ product.id
49
+ )
50
+ ] });
51
+ }
52
+ export {
53
+ ProductPaneel
54
+ };
55
+ //# sourceMappingURL=ProductPaneel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/admin/shop/ProductPaneel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { collection, onSnapshot } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport type { ShopCategory } from \"../../schema/shop\";\nimport { ProductForm } from \"./ProductsTab\";\nimport { subscribeProduct, type StoredProduct } from \"./productClient\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\n\n/**\n * Het productformulier in het zijpaneel van de bewerkmodus.\n *\n * Het tandwiel bij een product deed niets — en dat was geen bug maar een gat in het ontwerp: het\n * blok waar het tandwiel bij hoort (`productDetail`, `productGrid`) heeft nauwelijks eigen velden.\n * Alles wat je op het scherm ziet staan — naam, prijs, foto's, varianten — komt uit het product.\n * Je klikte dus op een tandwiel bij een productfoto en kreeg een paneel met \"welk product\" en\n * \"toon ook gerelateerde producten\". Formeel juist, in de praktijk onbruikbaar.\n *\n * Nu opent hetzelfde formulier als in het Webwinkel-paneel, met het product dat je aanwees al open.\n */\nexport function ProductPaneel({ siteId, productId }: { siteId: string; productId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [product, setProduct] = useState<StoredProduct | null | undefined>(undefined);\n const [cats, setCats] = useState<(ShopCategory & { id: string })[]>([]);\n\n useEffect(() => subscribeProduct(siteId, productId, setProduct), [siteId, productId]);\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n return onSnapshot(collection(db, \"sites\", siteId, \"shopCategories\"), (snap) =>\n setCats(snap.docs.map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))),\n );\n }, [siteId]);\n\n if (product === undefined) return <p className=\"xicms-ve__hint\">Product laden…</p>;\n if (product === null) {\n return (\n <p className=\"xicms-ve__hint\">\n Dit product bestaat niet meer. Ververs de pagina, of beheer de webwinkel via Beheren → Webwinkel.\n </p>\n );\n }\n\n return (\n <div className=\"grid gap-3\">\n <h3 className=\"text-sm font-semibold\">Product — {product.name}</h3>\n {/*\n Producten kennen geen werkversie: opslaan is meteen live, op élke pagina die dit product\n toont. Dat staat hier omdat de rest van deze editor precies andersom werkt, en een\n verwachting die je op de ene plek opbouwt neem je mee naar de andere.\n */}\n <p className=\"xicms-ve__hint\">\n Wijzigingen aan een product zijn <strong>meteen live</strong> — ook op de cataloguspagina.\n Publiceren is hier niet nodig en geldt alleen voor de tekst van de pagina zelf.\n </p>\n <ProductForm\n key={product.id}\n siteId={siteId}\n product={product}\n categories={cats}\n onDeleted={() => setProduct(null)}\n onToast={toast}\n confirm={confirm}\n />\n </div>\n );\n}\n"],"mappings":";AAsCoC,cAW9B,YAX8B;AApCpC,SAAS,WAAW,gBAAgB;AACpC,SAAS,YAAY,kBAAkB;AACvC,SAAS,aAAa;AAEtB,SAAS,mBAAmB;AAC5B,SAAS,wBAA4C;AACrD,SAAS,gBAAgB;AACzB,SAAS,kBAAkB;AAapB,SAAS,cAAc,EAAE,QAAQ,UAAU,GAA0C;AAC1F,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,UAAU,WAAW;AAC3B,QAAM,CAAC,SAAS,UAAU,IAAI,SAA2C,MAAS;AAClF,QAAM,CAAC,MAAM,OAAO,IAAI,SAA4C,CAAC,CAAC;AAEtE,YAAU,MAAM,iBAAiB,QAAQ,WAAW,UAAU,GAAG,CAAC,QAAQ,SAAS,CAAC;AAEpF,YAAU,MAAM;AACd,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AACT,WAAO;AAAA,MAAW,WAAW,IAAI,SAAS,QAAQ,gBAAgB;AAAA,MAAG,CAAC,SACpE,QAAQ,KAAK,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAAmB,EAAE,CAAC;AAAA,IAC7E;AAAA,EACF,GAAG,CAAC,MAAM,CAAC;AAEX,MAAI,YAAY,OAAW,QAAO,oBAAC,OAAE,WAAU,kBAAiB,iCAAc;AAC9E,MAAI,YAAY,MAAM;AACpB,WACE,oBAAC,OAAE,WAAU,kBAAiB,oHAE9B;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,cACb;AAAA,yBAAC,QAAG,WAAU,yBAAwB;AAAA;AAAA,MAAW,QAAQ;AAAA,OAAK;AAAA,IAM9D,qBAAC,OAAE,WAAU,kBAAiB;AAAA;AAAA,MACK,oBAAC,YAAO,yBAAW;AAAA,MAAS;AAAA,OAE/D;AAAA,IACA;AAAA,MAAC;AAAA;AAAA,QAEC;AAAA,QACA;AAAA,QACA,YAAY;AAAA,QACZ,WAAW,MAAM,WAAW,IAAI;AAAA,QAChC,SAAS;AAAA,QACT;AAAA;AAAA,MANK,QAAQ;AAAA,IAOf;AAAA,KACF;AAEJ;","names":[]}
@@ -1,3 +1,30 @@
1
+ import { type Product, type ShopCategory } from "../../schema/shop.js";
2
+ import { type ConfirmOptions } from "../ui/ConfirmDialog.js";
3
+ type StoredProduct = Product & {
4
+ id: string;
5
+ };
6
+ type StoredCategory = ShopCategory & {
7
+ id: string;
8
+ };
1
9
  export declare function ProductsTab({ siteId }: {
2
10
  siteId: string;
3
11
  }): import("react").JSX.Element;
12
+ /**
13
+ * Het volledige productformulier. Sinds 7.8 ook buiten dit tabblad in gebruik: het tandwiel bij een
14
+ * product op de pagina opent hetzelfde formulier in een zijpaneel. Bewust hergebruik en geen tweede,
15
+ * uitgeklede versie — twee formulieren voor één product lopen gegarandeerd uit elkaar, en dan hangt
16
+ * het van je route af welke velden je kunt bereiken.
17
+ */
18
+ export declare function ProductForm({ siteId, product, categories, onDeleted, onToast, confirm, }: {
19
+ siteId: string;
20
+ product: StoredProduct;
21
+ categories: StoredCategory[];
22
+ onDeleted: () => void;
23
+ onToast: (t: {
24
+ title: string;
25
+ description?: string;
26
+ tone?: "default" | "success" | "error";
27
+ }) => void;
28
+ confirm: (opts: ConfirmOptions) => Promise<boolean>;
29
+ }): import("react").JSX.Element;
30
+ export {};
@@ -95,7 +95,7 @@ function ProductsTab({ siteId }) {
95
95
  ] })
96
96
  ] }),
97
97
  current ? /* @__PURE__ */ jsx(
98
- ProductDetail,
98
+ ProductForm,
99
99
  {
100
100
  siteId,
101
101
  product: current,
@@ -108,7 +108,7 @@ function ProductsTab({ siteId }) {
108
108
  ) : /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Selecteer of maak een product." }) })
109
109
  ] });
110
110
  }
111
- function ProductDetail({
111
+ function ProductForm({
112
112
  siteId,
113
113
  product,
114
114
  categories,
@@ -516,6 +516,7 @@ function ProductDetail({
516
516
  ] }) });
517
517
  }
518
518
  export {
519
+ ProductForm,
519
520
  ProductsTab
520
521
  };
521
522
  //# sourceMappingURL=ProductsTab.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/shop/ProductsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { addDoc, collection, deleteDoc, doc, onSnapshot, setDoc } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport {\n ProductSchema,\n formatCents,\n type PersonalizationField,\n type Product,\n type ProductOption,\n type ProductVariant,\n type ShopCategory,\n} from \"../../schema/shop\";\nimport { Badge, Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm, type ConfirmOptions } from \"../ui/ConfirmDialog\";\nimport { MediaPickerButton } from \"../media/MediaPicker\";\nimport { slugify } from \"../lib/slugify\";\nimport { allVariantKeys, centsToEuroInput, eurosToCents } from \"./lib\";\n\ntype StoredProduct = Product & { id: string };\ntype StoredCategory = ShopCategory & { id: string };\n\nconst STATUS_OPTIONS = [\n { value: \"draft\", label: \"Concept\" },\n { value: \"active\", label: \"Actief\" },\n { value: \"archived\", label: \"Gearchiveerd\" },\n];\n\nexport function ProductsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [products, setProducts] = useState<StoredProduct[]>([]);\n const [cats, setCats] = useState<StoredCategory[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [q, setQ] = useState(\"\");\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const un1 = onSnapshot(collection(db, \"sites\", siteId, \"products\"), (snap) =>\n setProducts(\n snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as Product) }))\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\")),\n ),\n );\n const un2 = onSnapshot(collection(db, \"sites\", siteId, \"shopCategories\"), (snap) =>\n setCats(snap.docs.map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))),\n );\n return () => {\n un1();\n un2();\n };\n }, [siteId]);\n\n async function createProduct() {\n const db = getDb();\n if (!db) return;\n const ref = await addDoc(collection(db, \"sites\", siteId, \"products\"), {\n name: \"Nieuw product\",\n slug: `product-${products.length + 1}`,\n description: \"\",\n categoryIds: [],\n images: [],\n priceCents: 0,\n vatRate: 21,\n options: [],\n variants: [],\n trackStock: false,\n status: \"draft\",\n order: products.length,\n featured: false,\n } satisfies Product);\n setSelected(ref.id);\n }\n\n const filtered = useMemo(() => {\n const needle = q.trim().toLowerCase();\n if (!needle) return products;\n return products.filter((p) => p.name.toLowerCase().includes(needle) || p.slug.includes(needle));\n }, [products, q]);\n\n const current = products.find((p) => p.id === selected) ?? null;\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[300px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Producten</h3>\n <Button size=\"sm\" onClick={createProduct}>+ Nieuw</Button>\n </div>\n <Input placeholder=\"Zoeken…\" value={q} onChange={(e) => setQ(e.target.value)} className=\"mb-2\" />\n <ul className=\"grid max-h-[60vh] gap-1 overflow-y-auto\">\n {filtered.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n {p.images?.[0] && <img src={p.images[0]} alt=\"\" className=\"h-8 w-8 rounded object-cover\" />}\n <span className=\"min-w-0 flex-1 truncate\">{p.name}</span>\n <Badge tone={p.status === \"active\" ? \"green\" : p.status === \"draft\" ? \"amber\" : \"slate\"}>\n {STATUS_OPTIONS.find((s) => s.value === p.status)?.label ?? p.status}\n </Badge>\n </button>\n </li>\n ))}\n {filtered.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Geen producten.</li>}\n </ul>\n </Card>\n {current ? (\n <ProductDetail\n key={current.id}\n siteId={siteId}\n product={current}\n categories={cats}\n onDeleted={() => setSelected(null)}\n onToast={toast}\n confirm={confirm}\n />\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een product.</p>\n </Card>\n )}\n </div>\n );\n}\n\nfunction ProductDetail({\n siteId,\n product,\n categories,\n onDeleted,\n onToast,\n confirm,\n}: {\n siteId: string;\n product: StoredProduct;\n categories: StoredCategory[];\n onDeleted: () => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n confirm: (opts: ConfirmOptions) => Promise<boolean>;\n}) {\n const [def, setDef] = useState<Product>(product);\n const [priceInput, setPriceInput] = useState(centsToEuroInput(product.priceCents));\n const [compareInput, setCompareInput] = useState(centsToEuroInput(product.compareAtCents));\n\n const variantKeys = useMemo(() => allVariantKeys(def.options ?? []), [def.options]);\n\n function setOption(i: number, patch: Partial<ProductOption>) {\n setDef((p) => {\n const options = [...(p.options ?? [])];\n options[i] = { ...options[i], ...patch };\n return { ...p, options };\n });\n }\n\n function variantFor(key: string): ProductVariant | undefined {\n return (def.variants ?? []).find((v) => v.key === key);\n }\n\n function patchVariant(key: string, patch: Partial<ProductVariant>) {\n setDef((p) => {\n const variants = [...(p.variants ?? [])];\n const idx = variants.findIndex((v) => v.key === key);\n if (idx >= 0) variants[idx] = { ...variants[idx], ...patch };\n else variants.push({ key, active: true, ...patch });\n return { ...p, variants };\n });\n }\n\n async function save() {\n const db = getDb();\n if (!db) return;\n // Alleen varianten bewaren die iets toevoegen (afwijkende prijs/voorraad/sku of inactief),\n // en geen wees-varianten van verwijderde optiewaarden.\n const validKeys = new Set(variantKeys);\n const cleanVariants = (def.variants ?? []).filter(\n (v) =>\n validKeys.has(v.key) &&\n (v.priceCents !== undefined || v.stock !== undefined || v.sku !== undefined || v.active === false),\n );\n // Onvolledige personalisatie-regels (zonder label) niet opslaan.\n const cleanPersonalization = (def.personalization ?? []).filter((f) => f.label.trim().length > 0);\n const parsed = ProductSchema.safeParse({\n ...def,\n variants: cleanVariants,\n personalization: cleanPersonalization.length > 0 ? cleanPersonalization : undefined,\n });\n if (!parsed.success) {\n onToast({ title: \"Ongeldig product\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n await setDoc(doc(db, \"sites\", siteId, \"products\", product.id), parsed.data);\n onToast({ title: \"Product opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const db = getDb();\n if (!db) return;\n const ok = await confirm({\n title: `\"${product.name}\" verwijderen?`,\n description: \"Bestaande bestellingen behouden hun eigen kopie van naam en prijs.\",\n tone: \"danger\",\n });\n if (!ok) return;\n await deleteDoc(doc(db, \"sites\", siteId, \"products\", product.id));\n onDeleted();\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Field label=\"Naam\">\n <Input\n value={def.name}\n onChange={(e) => setDef((p) => ({ ...p, name: e.target.value, slug: slugify(e.target.value) || p.slug }))}\n />\n </Field>\n <Field label=\"Slug\" hint=\"Product-URL: /product?p=<slug>\">\n <Input value={def.slug} onChange={(e) => setDef((p) => ({ ...p, slug: slugify(e.target.value) }))} />\n </Field>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <Field label=\"Prijs (€, incl. btw)\">\n <Input\n value={priceInput}\n onChange={(e) => {\n setPriceInput(e.target.value);\n const c = eurosToCents(e.target.value);\n if (c !== null) setDef((p) => ({ ...p, priceCents: c }));\n }}\n />\n </Field>\n <Field label=\"Van-prijs (€, optioneel)\" hint=\"Doorgestreept naast de prijs\">\n <Input\n value={compareInput}\n onChange={(e) => {\n setCompareInput(e.target.value);\n const c = eurosToCents(e.target.value);\n setDef((p) => ({ ...p, compareAtCents: c ?? undefined }));\n }}\n />\n </Field>\n <Field label=\"Btw-tarief\">\n <Select\n value={String(def.vatRate ?? 21)}\n onValueChange={(v) => setDef((p) => ({ ...p, vatRate: Number(v) as 21 | 9 | 0 }))}\n options={[\n { value: \"21\", label: \"21%\" },\n { value: \"9\", label: \"9%\" },\n { value: \"0\", label: \"0%\" },\n ]}\n />\n </Field>\n </div>\n\n <Field label=\"Omschrijving\">\n <Textarea rows={4} value={def.description ?? \"\"} onChange={(e) => setDef((p) => ({ ...p, description: e.target.value }))} />\n </Field>\n\n <Field label=\"Afbeeldingen\" hint=\"Eerste afbeelding is het hoofdbeeld\">\n <div className=\"flex flex-wrap items-center gap-3\">\n {(def.images ?? []).map((url, i) => (\n <div key={url + i} className=\"relative\">\n <img src={url} alt=\"\" className=\"h-16 w-16 rounded-lg object-cover\" />\n <button\n type=\"button\"\n aria-label=\"Verwijder afbeelding\"\n className=\"absolute -right-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white\"\n onClick={() => setDef((p) => ({ ...p, images: (p.images ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </button>\n </div>\n ))}\n <MediaPickerButton\n siteId={siteId}\n label=\"+ Afbeelding\"\n onSelect={(m) => setDef((p) => ({ ...p, images: [...(p.images ?? []), m.url] }))}\n />\n </div>\n </Field>\n\n <Field label=\"Categorieën\">\n <div className=\"flex flex-wrap gap-2\">\n {categories.map((c) => {\n const on = (def.categoryIds ?? []).includes(c.id);\n return (\n <button\n key={c.id}\n type=\"button\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n categoryIds: on ? (p.categoryIds ?? []).filter((x) => x !== c.id) : [...(p.categoryIds ?? []), c.id],\n }))\n }\n className={`rounded-full border px-3 py-1 text-sm ${\n on ? \"border-brand-500 bg-brand-50 text-brand-700\" : \"border-slate-200 hover:bg-slate-50\"\n }`}\n >\n {on ? \"✓ \" : \"\"}\n {c.name}\n </button>\n );\n })}\n {categories.length === 0 && <span className=\"text-sm text-slate-500\">Maak eerst een categorie aan.</span>}\n </div>\n </Field>\n\n {/* Opties & varianten */}\n <Field\n label=\"Opties (bv. Maat, Kleur)\"\n hint=\"Max. 3. Waarden gescheiden door komma's. Combinaties kun je hieronder per stuk aanpassen.\"\n >\n <div className=\"grid gap-2\">\n {(def.options ?? []).map((opt, i) => (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2\">\n <div className=\"flex items-center gap-2\">\n <Input\n className=\"w-36\"\n placeholder=\"Naam (bv. Maat)\"\n value={opt.name}\n onChange={(e) => setOption(i, { name: e.target.value })}\n />\n <Input\n placeholder=\"Waarden, bv: 56, 62, 68, 74\"\n value={opt.values.join(\", \")}\n onChange={(e) =>\n setOption(i, { values: e.target.value.split(\",\").map((s) => s.trim()).filter(Boolean) })\n }\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, options: (p.options ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n {/* Afbeelding per waarde (5.6) — vooral voor de kleur-as: de detailpagina\n wisselt het hoofdbeeld mee met de gekozen kleur. */}\n {/^kleur|^colou?r/i.test(opt.name) && opt.values.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {opt.values.map((value) => {\n const url = opt.images?.[value];\n return (\n <span key={value} className=\"flex items-center gap-1 rounded-full border border-slate-200 px-2 py-1 text-xs\">\n {url && <img src={url} alt=\"\" className=\"h-6 w-6 rounded object-cover\" />}\n {value}\n <MediaPickerButton\n siteId={siteId}\n label={url ? \"↻\" : \"+ foto\"}\n onSelect={(m) =>\n setOption(i, { images: { ...(opt.images ?? {}), [value]: m.url } })\n }\n />\n {url && (\n <button\n type=\"button\"\n aria-label={`Verwijder foto van ${value}`}\n className=\"text-slate-400 hover:text-slate-700\"\n onClick={() => {\n const images = { ...(opt.images ?? {}) };\n delete images[value];\n setOption(i, { images });\n }}\n >\n ✕\n </button>\n )}\n </span>\n );\n })}\n </div>\n )}\n </div>\n ))}\n {(def.options ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() => setDef((p) => ({ ...p, options: [...(p.options ?? []), { name: \"\", values: [] }] }))}\n >\n + Optie\n </Button>\n )}\n </div>\n </Field>\n\n {variantKeys.length > 0 && variantKeys.length <= 60 && (\n <Field label=\"Varianten\" hint=\"Leeg = standaardprijs / onbeperkte voorraad\">\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-slate-500\">\n <th className=\"py-1 pr-2\">Combinatie</th>\n <th className=\"py-1 pr-2\">Prijs (€)</th>\n {def.trackStock && <th className=\"py-1 pr-2\">Voorraad</th>}\n <th className=\"py-1 pr-2\">Leverbaar</th>\n </tr>\n </thead>\n <tbody>\n {variantKeys.map((key) => {\n const v = variantFor(key);\n return (\n <tr key={key} className=\"border-t border-slate-100\">\n <td className=\"py-1 pr-2\">{key.split(\"|\").join(\" · \")}</td>\n <td className=\"py-1 pr-2\">\n {/* Native uncontrolled inputs: geen cursor-gedoe bij \"13,\" tijdens typen. */}\n <input\n className=\"w-24 rounded-lg border border-slate-200 px-2 py-1\"\n placeholder={centsToEuroInput(def.priceCents)}\n defaultValue={centsToEuroInput(v?.priceCents)}\n onChange={(e) => {\n const c = eurosToCents(e.target.value);\n patchVariant(key, { priceCents: c ?? undefined });\n }}\n />\n </td>\n {def.trackStock && (\n <td className=\"py-1 pr-2\">\n <input\n className=\"w-20 rounded-lg border border-slate-200 px-2 py-1\"\n type=\"number\"\n min={0}\n placeholder=\"∞\"\n defaultValue={v?.stock ?? \"\"}\n onChange={(e) => {\n const n = e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0);\n patchVariant(key, { stock: n });\n }}\n />\n </td>\n )}\n <td className=\"py-1 pr-2\">\n <Switch checked={v?.active !== false} onCheckedChange={(on) => patchVariant(key, { active: on })} />\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n </Field>\n )}\n {variantKeys.length > 60 && (\n <p className=\"text-sm text-amber-600\">\n {variantKeys.length} combinaties — te veel om per stuk te tonen. Beperk het aantal optie-waarden.\n </p>\n )}\n\n {/* Personalisatie (5.6): eigen naam/logo van de klant, met optionele meerprijs. */}\n <Field\n label=\"Personalisatie (eigen tekst of afbeelding)\"\n hint=\"Max. 3 velden. De klant vult dit in op de productpagina; de meerprijs telt automatisch mee.\"\n >\n <div className=\"grid gap-2\">\n {(def.personalization ?? []).map((f, i) => {\n const setPers = (patch: Partial<PersonalizationField>) =>\n setDef((p) => {\n const personalization = [...(p.personalization ?? [])];\n personalization[i] = { ...personalization[i], ...patch };\n return { ...p, personalization };\n });\n return (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2 md:grid-cols-[1fr_130px_90px_110px_auto]\">\n <Input\n placeholder='Label (bv. \"Naam van het kindje\")'\n value={f.label}\n onChange={(e) =>\n setPers({\n label: e.target.value,\n key: f.key || e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, \"_\").replace(/^_+|_+$/g, \"\").slice(0, 40) || `veld${i + 1}`,\n })\n }\n />\n <Select\n value={f.type}\n onValueChange={(v) => setPers({ type: v as \"text\" | \"image\" })}\n options={[\n { value: \"text\", label: \"Tekst\" },\n { value: \"image\", label: \"Afbeelding\" },\n ]}\n />\n {f.type === \"text\" ? (\n <Input\n type=\"number\"\n title=\"Max. tekens\"\n min={1}\n max={200}\n value={f.maxLength ?? 40}\n onChange={(e) => setPers({ maxLength: Math.max(1, Math.min(200, Number(e.target.value) || 40)) })}\n />\n ) : (\n <span className=\"self-center text-xs text-slate-400\">jpg/png/webp</span>\n )}\n <input\n className=\"rounded-lg border border-slate-200 px-2 py-1\"\n placeholder=\"Meerprijs €\"\n defaultValue={f.priceCents ? centsToEuroInput(f.priceCents) : \"\"}\n onChange={(e) => setPers({ priceCents: eurosToCents(e.target.value) ?? 0 })}\n />\n <div className=\"flex items-center gap-2\">\n <Switch checked={f.required === true} onCheckedChange={(v) => setPers({ required: v })} label=\"Verplicht\" />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, personalization: (p.personalization ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n </div>\n );\n })}\n {(def.personalization ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n personalization: [\n ...(p.personalization ?? []),\n { key: `veld${(p.personalization ?? []).length + 1}`, label: \"\", type: \"text\", required: false, maxLength: 40, priceCents: 0 },\n ],\n }))\n }\n >\n + Personalisatie-veld\n </Button>\n )}\n </div>\n </Field>\n\n <div className=\"flex flex-wrap items-center gap-6\">\n <Field label=\"Status\">\n <Select\n value={def.status ?? \"draft\"}\n onValueChange={(v) => setDef((p) => ({ ...p, status: v as Product[\"status\"] }))}\n options={STATUS_OPTIONS}\n />\n </Field>\n <Field label=\"Volgorde\">\n <Input\n type=\"number\"\n className=\"w-24\"\n value={def.order ?? 0}\n onChange={(e) => setDef((p) => ({ ...p, order: Math.max(0, Number(e.target.value) || 0) }))}\n />\n </Field>\n <Switch checked={def.featured === true} onCheckedChange={(v) => setDef((p) => ({ ...p, featured: v }))} label=\"Uitgelicht\" />\n <Switch checked={def.trackStock === true} onCheckedChange={(v) => setDef((p) => ({ ...p, trackStock: v }))} label=\"Voorraad bijhouden\" />\n {def.trackStock && (def.options ?? []).length === 0 && (\n <Field label=\"Voorraad\">\n <Input\n type=\"number\"\n className=\"w-24\"\n placeholder=\"∞\"\n value={def.stock ?? \"\"}\n onChange={(e) =>\n setDef((p) => ({ ...p, stock: e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0) }))\n }\n />\n </Field>\n )}\n </div>\n\n <div className=\"flex items-center justify-between border-t border-slate-100 pt-4\">\n <div className=\"flex items-center gap-3\">\n <Button onClick={save}>Opslaan</Button>\n <span className=\"text-sm text-slate-500\">{formatCents(def.priceCents)}</span>\n </div>\n <Button variant=\"danger\" onClick={remove}>Verwijderen</Button>\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/shop/ProductsTab.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport { addDoc, collection, deleteDoc, doc, onSnapshot, setDoc } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport {\n ProductSchema,\n formatCents,\n type PersonalizationField,\n type Product,\n type ProductOption,\n type ProductVariant,\n type ShopCategory,\n} from \"../../schema/shop\";\nimport { Badge, Button, Card, Field, Input, Textarea } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm, type ConfirmOptions } from \"../ui/ConfirmDialog\";\nimport { MediaPickerButton } from \"../media/MediaPicker\";\nimport { slugify } from \"../lib/slugify\";\nimport { allVariantKeys, centsToEuroInput, eurosToCents } from \"./lib\";\n\ntype StoredProduct = Product & { id: string };\ntype StoredCategory = ShopCategory & { id: string };\n\nconst STATUS_OPTIONS = [\n { value: \"draft\", label: \"Concept\" },\n { value: \"active\", label: \"Actief\" },\n { value: \"archived\", label: \"Gearchiveerd\" },\n];\n\nexport function ProductsTab({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [products, setProducts] = useState<StoredProduct[]>([]);\n const [cats, setCats] = useState<StoredCategory[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [q, setQ] = useState(\"\");\n\n useEffect(() => {\n const db = getDb();\n if (!db) return;\n const un1 = onSnapshot(collection(db, \"sites\", siteId, \"products\"), (snap) =>\n setProducts(\n snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as Product) }))\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\")),\n ),\n );\n const un2 = onSnapshot(collection(db, \"sites\", siteId, \"shopCategories\"), (snap) =>\n setCats(snap.docs.map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))),\n );\n return () => {\n un1();\n un2();\n };\n }, [siteId]);\n\n async function createProduct() {\n const db = getDb();\n if (!db) return;\n const ref = await addDoc(collection(db, \"sites\", siteId, \"products\"), {\n name: \"Nieuw product\",\n slug: `product-${products.length + 1}`,\n description: \"\",\n categoryIds: [],\n images: [],\n priceCents: 0,\n vatRate: 21,\n options: [],\n variants: [],\n trackStock: false,\n status: \"draft\",\n order: products.length,\n featured: false,\n } satisfies Product);\n setSelected(ref.id);\n }\n\n const filtered = useMemo(() => {\n const needle = q.trim().toLowerCase();\n if (!needle) return products;\n return products.filter((p) => p.name.toLowerCase().includes(needle) || p.slug.includes(needle));\n }, [products, q]);\n\n const current = products.find((p) => p.id === selected) ?? null;\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[300px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Producten</h3>\n <Button size=\"sm\" onClick={createProduct}>+ Nieuw</Button>\n </div>\n <Input placeholder=\"Zoeken…\" value={q} onChange={(e) => setQ(e.target.value)} className=\"mb-2\" />\n <ul className=\"grid max-h-[60vh] gap-1 overflow-y-auto\">\n {filtered.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n {p.images?.[0] && <img src={p.images[0]} alt=\"\" className=\"h-8 w-8 rounded object-cover\" />}\n <span className=\"min-w-0 flex-1 truncate\">{p.name}</span>\n <Badge tone={p.status === \"active\" ? \"green\" : p.status === \"draft\" ? \"amber\" : \"slate\"}>\n {STATUS_OPTIONS.find((s) => s.value === p.status)?.label ?? p.status}\n </Badge>\n </button>\n </li>\n ))}\n {filtered.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Geen producten.</li>}\n </ul>\n </Card>\n {current ? (\n <ProductForm\n key={current.id}\n siteId={siteId}\n product={current}\n categories={cats}\n onDeleted={() => setSelected(null)}\n onToast={toast}\n confirm={confirm}\n />\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een product.</p>\n </Card>\n )}\n </div>\n );\n}\n\n/**\n * Het volledige productformulier. Sinds 7.8 ook buiten dit tabblad in gebruik: het tandwiel bij een\n * product op de pagina opent hetzelfde formulier in een zijpaneel. Bewust hergebruik en geen tweede,\n * uitgeklede versie — twee formulieren voor één product lopen gegarandeerd uit elkaar, en dan hangt\n * het van je route af welke velden je kunt bereiken.\n */\nexport function ProductForm({\n siteId,\n product,\n categories,\n onDeleted,\n onToast,\n confirm,\n}: {\n siteId: string;\n product: StoredProduct;\n categories: StoredCategory[];\n onDeleted: () => void;\n onToast: (t: { title: string; description?: string; tone?: \"default\" | \"success\" | \"error\" }) => void;\n confirm: (opts: ConfirmOptions) => Promise<boolean>;\n}) {\n const [def, setDef] = useState<Product>(product);\n const [priceInput, setPriceInput] = useState(centsToEuroInput(product.priceCents));\n const [compareInput, setCompareInput] = useState(centsToEuroInput(product.compareAtCents));\n\n const variantKeys = useMemo(() => allVariantKeys(def.options ?? []), [def.options]);\n\n function setOption(i: number, patch: Partial<ProductOption>) {\n setDef((p) => {\n const options = [...(p.options ?? [])];\n options[i] = { ...options[i], ...patch };\n return { ...p, options };\n });\n }\n\n function variantFor(key: string): ProductVariant | undefined {\n return (def.variants ?? []).find((v) => v.key === key);\n }\n\n function patchVariant(key: string, patch: Partial<ProductVariant>) {\n setDef((p) => {\n const variants = [...(p.variants ?? [])];\n const idx = variants.findIndex((v) => v.key === key);\n if (idx >= 0) variants[idx] = { ...variants[idx], ...patch };\n else variants.push({ key, active: true, ...patch });\n return { ...p, variants };\n });\n }\n\n async function save() {\n const db = getDb();\n if (!db) return;\n // Alleen varianten bewaren die iets toevoegen (afwijkende prijs/voorraad/sku of inactief),\n // en geen wees-varianten van verwijderde optiewaarden.\n const validKeys = new Set(variantKeys);\n const cleanVariants = (def.variants ?? []).filter(\n (v) =>\n validKeys.has(v.key) &&\n (v.priceCents !== undefined || v.stock !== undefined || v.sku !== undefined || v.active === false),\n );\n // Onvolledige personalisatie-regels (zonder label) niet opslaan.\n const cleanPersonalization = (def.personalization ?? []).filter((f) => f.label.trim().length > 0);\n const parsed = ProductSchema.safeParse({\n ...def,\n variants: cleanVariants,\n personalization: cleanPersonalization.length > 0 ? cleanPersonalization : undefined,\n });\n if (!parsed.success) {\n onToast({ title: \"Ongeldig product\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n await setDoc(doc(db, \"sites\", siteId, \"products\", product.id), parsed.data);\n onToast({ title: \"Product opgeslagen\", tone: \"success\" });\n }\n\n async function remove() {\n const db = getDb();\n if (!db) return;\n const ok = await confirm({\n title: `\"${product.name}\" verwijderen?`,\n description: \"Bestaande bestellingen behouden hun eigen kopie van naam en prijs.\",\n tone: \"danger\",\n });\n if (!ok) return;\n await deleteDoc(doc(db, \"sites\", siteId, \"products\", product.id));\n onDeleted();\n }\n\n return (\n <Card>\n <div className=\"grid gap-4\">\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Field label=\"Naam\">\n <Input\n value={def.name}\n onChange={(e) => setDef((p) => ({ ...p, name: e.target.value, slug: slugify(e.target.value) || p.slug }))}\n />\n </Field>\n <Field label=\"Slug\" hint=\"Product-URL: /product?p=<slug>\">\n <Input value={def.slug} onChange={(e) => setDef((p) => ({ ...p, slug: slugify(e.target.value) }))} />\n </Field>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <Field label=\"Prijs (€, incl. btw)\">\n <Input\n value={priceInput}\n onChange={(e) => {\n setPriceInput(e.target.value);\n const c = eurosToCents(e.target.value);\n if (c !== null) setDef((p) => ({ ...p, priceCents: c }));\n }}\n />\n </Field>\n <Field label=\"Van-prijs (€, optioneel)\" hint=\"Doorgestreept naast de prijs\">\n <Input\n value={compareInput}\n onChange={(e) => {\n setCompareInput(e.target.value);\n const c = eurosToCents(e.target.value);\n setDef((p) => ({ ...p, compareAtCents: c ?? undefined }));\n }}\n />\n </Field>\n <Field label=\"Btw-tarief\">\n <Select\n value={String(def.vatRate ?? 21)}\n onValueChange={(v) => setDef((p) => ({ ...p, vatRate: Number(v) as 21 | 9 | 0 }))}\n options={[\n { value: \"21\", label: \"21%\" },\n { value: \"9\", label: \"9%\" },\n { value: \"0\", label: \"0%\" },\n ]}\n />\n </Field>\n </div>\n\n <Field label=\"Omschrijving\">\n <Textarea rows={4} value={def.description ?? \"\"} onChange={(e) => setDef((p) => ({ ...p, description: e.target.value }))} />\n </Field>\n\n <Field label=\"Afbeeldingen\" hint=\"Eerste afbeelding is het hoofdbeeld\">\n <div className=\"flex flex-wrap items-center gap-3\">\n {(def.images ?? []).map((url, i) => (\n <div key={url + i} className=\"relative\">\n <img src={url} alt=\"\" className=\"h-16 w-16 rounded-lg object-cover\" />\n <button\n type=\"button\"\n aria-label=\"Verwijder afbeelding\"\n className=\"absolute -right-2 -top-2 grid h-5 w-5 place-items-center rounded-full bg-slate-700 text-xs text-white\"\n onClick={() => setDef((p) => ({ ...p, images: (p.images ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </button>\n </div>\n ))}\n <MediaPickerButton\n siteId={siteId}\n label=\"+ Afbeelding\"\n onSelect={(m) => setDef((p) => ({ ...p, images: [...(p.images ?? []), m.url] }))}\n />\n </div>\n </Field>\n\n <Field label=\"Categorieën\">\n <div className=\"flex flex-wrap gap-2\">\n {categories.map((c) => {\n const on = (def.categoryIds ?? []).includes(c.id);\n return (\n <button\n key={c.id}\n type=\"button\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n categoryIds: on ? (p.categoryIds ?? []).filter((x) => x !== c.id) : [...(p.categoryIds ?? []), c.id],\n }))\n }\n className={`rounded-full border px-3 py-1 text-sm ${\n on ? \"border-brand-500 bg-brand-50 text-brand-700\" : \"border-slate-200 hover:bg-slate-50\"\n }`}\n >\n {on ? \"✓ \" : \"\"}\n {c.name}\n </button>\n );\n })}\n {categories.length === 0 && <span className=\"text-sm text-slate-500\">Maak eerst een categorie aan.</span>}\n </div>\n </Field>\n\n {/* Opties & varianten */}\n <Field\n label=\"Opties (bv. Maat, Kleur)\"\n hint=\"Max. 3. Waarden gescheiden door komma's. Combinaties kun je hieronder per stuk aanpassen.\"\n >\n <div className=\"grid gap-2\">\n {(def.options ?? []).map((opt, i) => (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2\">\n <div className=\"flex items-center gap-2\">\n <Input\n className=\"w-36\"\n placeholder=\"Naam (bv. Maat)\"\n value={opt.name}\n onChange={(e) => setOption(i, { name: e.target.value })}\n />\n <Input\n placeholder=\"Waarden, bv: 56, 62, 68, 74\"\n value={opt.values.join(\", \")}\n onChange={(e) =>\n setOption(i, { values: e.target.value.split(\",\").map((s) => s.trim()).filter(Boolean) })\n }\n />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, options: (p.options ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n {/* Afbeelding per waarde (5.6) — vooral voor de kleur-as: de detailpagina\n wisselt het hoofdbeeld mee met de gekozen kleur. */}\n {/^kleur|^colou?r/i.test(opt.name) && opt.values.length > 0 && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {opt.values.map((value) => {\n const url = opt.images?.[value];\n return (\n <span key={value} className=\"flex items-center gap-1 rounded-full border border-slate-200 px-2 py-1 text-xs\">\n {url && <img src={url} alt=\"\" className=\"h-6 w-6 rounded object-cover\" />}\n {value}\n <MediaPickerButton\n siteId={siteId}\n label={url ? \"↻\" : \"+ foto\"}\n onSelect={(m) =>\n setOption(i, { images: { ...(opt.images ?? {}), [value]: m.url } })\n }\n />\n {url && (\n <button\n type=\"button\"\n aria-label={`Verwijder foto van ${value}`}\n className=\"text-slate-400 hover:text-slate-700\"\n onClick={() => {\n const images = { ...(opt.images ?? {}) };\n delete images[value];\n setOption(i, { images });\n }}\n >\n ✕\n </button>\n )}\n </span>\n );\n })}\n </div>\n )}\n </div>\n ))}\n {(def.options ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() => setDef((p) => ({ ...p, options: [...(p.options ?? []), { name: \"\", values: [] }] }))}\n >\n + Optie\n </Button>\n )}\n </div>\n </Field>\n\n {variantKeys.length > 0 && variantKeys.length <= 60 && (\n <Field label=\"Varianten\" hint=\"Leeg = standaardprijs / onbeperkte voorraad\">\n <div className=\"overflow-x-auto\">\n <table className=\"w-full text-sm\">\n <thead>\n <tr className=\"text-left text-xs text-slate-500\">\n <th className=\"py-1 pr-2\">Combinatie</th>\n <th className=\"py-1 pr-2\">Prijs (€)</th>\n {def.trackStock && <th className=\"py-1 pr-2\">Voorraad</th>}\n <th className=\"py-1 pr-2\">Leverbaar</th>\n </tr>\n </thead>\n <tbody>\n {variantKeys.map((key) => {\n const v = variantFor(key);\n return (\n <tr key={key} className=\"border-t border-slate-100\">\n <td className=\"py-1 pr-2\">{key.split(\"|\").join(\" · \")}</td>\n <td className=\"py-1 pr-2\">\n {/* Native uncontrolled inputs: geen cursor-gedoe bij \"13,\" tijdens typen. */}\n <input\n className=\"w-24 rounded-lg border border-slate-200 px-2 py-1\"\n placeholder={centsToEuroInput(def.priceCents)}\n defaultValue={centsToEuroInput(v?.priceCents)}\n onChange={(e) => {\n const c = eurosToCents(e.target.value);\n patchVariant(key, { priceCents: c ?? undefined });\n }}\n />\n </td>\n {def.trackStock && (\n <td className=\"py-1 pr-2\">\n <input\n className=\"w-20 rounded-lg border border-slate-200 px-2 py-1\"\n type=\"number\"\n min={0}\n placeholder=\"∞\"\n defaultValue={v?.stock ?? \"\"}\n onChange={(e) => {\n const n = e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0);\n patchVariant(key, { stock: n });\n }}\n />\n </td>\n )}\n <td className=\"py-1 pr-2\">\n <Switch checked={v?.active !== false} onCheckedChange={(on) => patchVariant(key, { active: on })} />\n </td>\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n </Field>\n )}\n {variantKeys.length > 60 && (\n <p className=\"text-sm text-amber-600\">\n {variantKeys.length} combinaties — te veel om per stuk te tonen. Beperk het aantal optie-waarden.\n </p>\n )}\n\n {/* Personalisatie (5.6): eigen naam/logo van de klant, met optionele meerprijs. */}\n <Field\n label=\"Personalisatie (eigen tekst of afbeelding)\"\n hint=\"Max. 3 velden. De klant vult dit in op de productpagina; de meerprijs telt automatisch mee.\"\n >\n <div className=\"grid gap-2\">\n {(def.personalization ?? []).map((f, i) => {\n const setPers = (patch: Partial<PersonalizationField>) =>\n setDef((p) => {\n const personalization = [...(p.personalization ?? [])];\n personalization[i] = { ...personalization[i], ...patch };\n return { ...p, personalization };\n });\n return (\n <div key={i} className=\"grid gap-2 rounded-lg border border-slate-100 p-2 md:grid-cols-[1fr_130px_90px_110px_auto]\">\n <Input\n placeholder='Label (bv. \"Naam van het kindje\")'\n value={f.label}\n onChange={(e) =>\n setPers({\n label: e.target.value,\n key: f.key || e.target.value.toLowerCase().replace(/[^a-z0-9]+/g, \"_\").replace(/^_+|_+$/g, \"\").slice(0, 40) || `veld${i + 1}`,\n })\n }\n />\n <Select\n value={f.type}\n onValueChange={(v) => setPers({ type: v as \"text\" | \"image\" })}\n options={[\n { value: \"text\", label: \"Tekst\" },\n { value: \"image\", label: \"Afbeelding\" },\n ]}\n />\n {f.type === \"text\" ? (\n <Input\n type=\"number\"\n title=\"Max. tekens\"\n min={1}\n max={200}\n value={f.maxLength ?? 40}\n onChange={(e) => setPers({ maxLength: Math.max(1, Math.min(200, Number(e.target.value) || 40)) })}\n />\n ) : (\n <span className=\"self-center text-xs text-slate-400\">jpg/png/webp</span>\n )}\n <input\n className=\"rounded-lg border border-slate-200 px-2 py-1\"\n placeholder=\"Meerprijs €\"\n defaultValue={f.priceCents ? centsToEuroInput(f.priceCents) : \"\"}\n onChange={(e) => setPers({ priceCents: eurosToCents(e.target.value) ?? 0 })}\n />\n <div className=\"flex items-center gap-2\">\n <Switch checked={f.required === true} onCheckedChange={(v) => setPers({ required: v })} label=\"Verplicht\" />\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setDef((p) => ({ ...p, personalization: (p.personalization ?? []).filter((_, j) => j !== i) }))}\n >\n ✕\n </Button>\n </div>\n </div>\n );\n })}\n {(def.personalization ?? []).length < 3 && (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n onClick={() =>\n setDef((p) => ({\n ...p,\n personalization: [\n ...(p.personalization ?? []),\n { key: `veld${(p.personalization ?? []).length + 1}`, label: \"\", type: \"text\", required: false, maxLength: 40, priceCents: 0 },\n ],\n }))\n }\n >\n + Personalisatie-veld\n </Button>\n )}\n </div>\n </Field>\n\n <div className=\"flex flex-wrap items-center gap-6\">\n <Field label=\"Status\">\n <Select\n value={def.status ?? \"draft\"}\n onValueChange={(v) => setDef((p) => ({ ...p, status: v as Product[\"status\"] }))}\n options={STATUS_OPTIONS}\n />\n </Field>\n <Field label=\"Volgorde\">\n <Input\n type=\"number\"\n className=\"w-24\"\n value={def.order ?? 0}\n onChange={(e) => setDef((p) => ({ ...p, order: Math.max(0, Number(e.target.value) || 0) }))}\n />\n </Field>\n <Switch checked={def.featured === true} onCheckedChange={(v) => setDef((p) => ({ ...p, featured: v }))} label=\"Uitgelicht\" />\n <Switch checked={def.trackStock === true} onCheckedChange={(v) => setDef((p) => ({ ...p, trackStock: v }))} label=\"Voorraad bijhouden\" />\n {def.trackStock && (def.options ?? []).length === 0 && (\n <Field label=\"Voorraad\">\n <Input\n type=\"number\"\n className=\"w-24\"\n placeholder=\"∞\"\n value={def.stock ?? \"\"}\n onChange={(e) =>\n setDef((p) => ({ ...p, stock: e.target.value === \"\" ? undefined : Math.max(0, Number(e.target.value) || 0) }))\n }\n />\n </Field>\n )}\n </div>\n\n <div className=\"flex items-center justify-between border-t border-slate-100 pt-4\">\n <div className=\"flex items-center gap-3\">\n <Button onClick={save}>Opslaan</Button>\n <span className=\"text-sm text-slate-500\">{formatCents(def.priceCents)}</span>\n </div>\n <Button variant=\"danger\" onClick={remove}>Verwijderen</Button>\n </div>\n </div>\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,32 @@
1
+ import type { Product } from "../../schema/shop.js";
2
+ export type StoredProduct = Product & {
3
+ id: string;
4
+ };
5
+ /**
6
+ * Eén product, live — voor het paneel dat de bewerkmodus opent bij een productblok.
7
+ *
8
+ * Een abonnement en geen eenmalige lezing: hetzelfde product kan tegelijk openstaan in het
9
+ * Webwinkel-paneel of in een tweede tabblad, en dan hoort dit paneel niet met een oude prijs te
10
+ * blijven staan.
11
+ */
12
+ export declare function subscribeProduct(siteId: string, productId: string, cb: (p: StoredProduct | null) => void): () => void;
13
+ /**
14
+ * Schrijf één veld van een product weg — de inline-tegenhanger van het volledige formulier.
15
+ *
16
+ * ── WAAROM DIT ANDERS IS DAN EEN TEKST OP EEN PAGINA ──────────────────────────────────────
17
+ *
18
+ * Een blokveld gaat naar de WERKVERSIE van die ene pagina en wordt pas zichtbaar als je
19
+ * publiceert. Een product heeft geen werkversie: het staat in de productencollectie en wordt door
20
+ * elke pagina gelezen die het toont. Wie hier de naam wijzigt, wijzigt hem dus METEEN en OVERAL —
21
+ * op de cataloguspagina, in de zoekresultaten, op de bestelbevestiging.
22
+ *
23
+ * Dat verschil is niet weg te poetsen zonder een tweede publicatiestroom voor producten te bouwen,
24
+ * en dat zou de webwinkel ingewikkelder maken dan hij is. Het staat daarom expliciet in de tekst
25
+ * bij het potlood, zodat het een keuze is en geen verrassing.
26
+ *
27
+ * `updateDoc` en niet `setDoc`: een product heeft velden die dit pad niet kent (varianten,
28
+ * voorraad, personalisatie). Een hele-document-schrijfactie zou die stilzwijgend wissen.
29
+ */
30
+ export declare function setProductField(siteId: string, productId: string, field: string, value: unknown): Promise<void>;
31
+ /** Eenmalige lezing — voor het moment waarop de editor moet weten wat er nú staat. */
32
+ export declare function getProduct(siteId: string, productId: string): Promise<StoredProduct | null>;
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { doc, getDoc, onSnapshot, updateDoc } from "firebase/firestore";
3
+ import { getDb } from "../../firebase/client.js";
4
+ function subscribeProduct(siteId, productId, cb) {
5
+ const db = getDb();
6
+ if (!db) {
7
+ cb(null);
8
+ return () => {
9
+ };
10
+ }
11
+ return onSnapshot(doc(db, "sites", siteId, "products", productId), (snap) => {
12
+ cb(snap.exists() ? { id: snap.id, ...snap.data() } : null);
13
+ });
14
+ }
15
+ async function setProductField(siteId, productId, field, value) {
16
+ const db = getDb();
17
+ if (!db) throw new Error("Firebase niet geconfigureerd.");
18
+ await updateDoc(doc(db, "sites", siteId, "products", productId), { [field]: value });
19
+ }
20
+ async function getProduct(siteId, productId) {
21
+ const db = getDb();
22
+ if (!db) return null;
23
+ const snap = await getDoc(doc(db, "sites", siteId, "products", productId));
24
+ return snap.exists() ? { id: snap.id, ...snap.data() } : null;
25
+ }
26
+ export {
27
+ getProduct,
28
+ setProductField,
29
+ subscribeProduct
30
+ };
31
+ //# sourceMappingURL=productClient.js.map