@xiwebdesign/xicms-core 5.3.0 → 5.5.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 (135) hide show
  1. package/dist/XiCMSProvider.d.ts +11 -2
  2. package/dist/XiCMSProvider.js +10 -5
  3. package/dist/XiCMSProvider.js.map +1 -1
  4. package/dist/admin/blockEditors/GenericEditor.js +18 -8
  5. package/dist/admin/blockEditors/GenericEditor.js.map +1 -1
  6. package/dist/admin/customBlocks/CustomBlocksPanel.js +12 -2
  7. package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -1
  8. package/dist/admin/lib/pagesClient.js +4 -1
  9. package/dist/admin/lib/pagesClient.js.map +1 -1
  10. package/dist/admin/lib/upload.js +14 -7
  11. package/dist/admin/lib/upload.js.map +1 -1
  12. package/dist/admin/media/MediaManager.js +60 -28
  13. package/dist/admin/media/MediaManager.js.map +1 -1
  14. package/dist/admin/media/MediaPicker.js +25 -15
  15. package/dist/admin/media/MediaPicker.js.map +1 -1
  16. package/dist/admin/pages/PageEditor.d.ts +2 -1
  17. package/dist/admin/pages/PageEditor.js +16 -2
  18. package/dist/admin/pages/PageEditor.js.map +1 -1
  19. package/dist/admin/pages/PagesPanel.js +9 -5
  20. package/dist/admin/pages/PagesPanel.js.map +1 -1
  21. package/dist/admin/panelRegistry.js +3 -0
  22. package/dist/admin/panelRegistry.js.map +1 -1
  23. package/dist/admin/shop/CategoriesTab.d.ts +3 -0
  24. package/dist/admin/shop/CategoriesTab.js +140 -0
  25. package/dist/admin/shop/CategoriesTab.js.map +1 -0
  26. package/dist/admin/shop/ConnectionsTab.d.ts +9 -0
  27. package/dist/admin/shop/ConnectionsTab.js +158 -0
  28. package/dist/admin/shop/ConnectionsTab.js.map +1 -0
  29. package/dist/admin/shop/OrdersTab.d.ts +3 -0
  30. package/dist/admin/shop/OrdersTab.js +236 -0
  31. package/dist/admin/shop/OrdersTab.js.map +1 -0
  32. package/dist/admin/shop/ProductsTab.d.ts +3 -0
  33. package/dist/admin/shop/ProductsTab.js +397 -0
  34. package/dist/admin/shop/ProductsTab.js.map +1 -0
  35. package/dist/admin/shop/ShopPanel.d.ts +4 -0
  36. package/dist/admin/shop/ShopPanel.js +25 -0
  37. package/dist/admin/shop/ShopPanel.js.map +1 -0
  38. package/dist/admin/shop/lib.d.ts +16 -0
  39. package/dist/admin/shop/lib.js +46 -0
  40. package/dist/admin/shop/lib.js.map +1 -0
  41. package/dist/admin/visual/VisualEditor.js +11 -2
  42. package/dist/admin/visual/VisualEditor.js.map +1 -1
  43. package/dist/admin.css +1 -1
  44. package/dist/agentFiles/files.d.ts +15 -3
  45. package/dist/agentFiles/files.js +57 -0
  46. package/dist/agentFiles/files.js.map +1 -1
  47. package/dist/auth/AuthProvider.js +3 -3
  48. package/dist/auth/AuthProvider.js.map +1 -1
  49. package/dist/blocks/builtins/Columns.js +1 -1
  50. package/dist/blocks/builtins/Columns.js.map +1 -1
  51. package/dist/blocks/builtins/Gallery.js +2 -2
  52. package/dist/blocks/builtins/Gallery.js.map +1 -1
  53. package/dist/blocks/builtins/Hero.js +1 -1
  54. package/dist/blocks/builtins/Hero.js.map +1 -1
  55. package/dist/blocks/builtins/SymbolRef.js.map +1 -1
  56. package/dist/components/PageView.js +4 -3
  57. package/dist/components/PageView.js.map +1 -1
  58. package/dist/demo/index.d.ts +7 -0
  59. package/dist/demo/index.js +23 -0
  60. package/dist/demo/index.js.map +1 -0
  61. package/dist/index.d.ts +2 -4
  62. package/dist/index.js +0 -20
  63. package/dist/index.js.map +1 -1
  64. package/dist/modules/builtins.d.ts +1 -0
  65. package/dist/modules/builtins.js +4 -0
  66. package/dist/modules/builtins.js.map +1 -1
  67. package/dist/modules/index.d.ts +4 -0
  68. package/dist/modules/index.js +12 -0
  69. package/dist/modules/index.js.map +1 -1
  70. package/dist/modules/registry.d.ts +3 -4
  71. package/dist/modules/registry.js +2 -3
  72. package/dist/modules/registry.js.map +1 -1
  73. package/dist/modules/shop/blocks/Cart.d.ts +2 -0
  74. package/dist/modules/shop/blocks/Cart.js +91 -0
  75. package/dist/modules/shop/blocks/Cart.js.map +1 -0
  76. package/dist/modules/shop/blocks/Checkout.d.ts +2 -0
  77. package/dist/modules/shop/blocks/Checkout.js +126 -0
  78. package/dist/modules/shop/blocks/Checkout.js.map +1 -0
  79. package/dist/modules/shop/blocks/Donation.d.ts +2 -0
  80. package/dist/modules/shop/blocks/Donation.js +145 -0
  81. package/dist/modules/shop/blocks/Donation.js.map +1 -0
  82. package/dist/modules/shop/blocks/OrderStatus.d.ts +2 -0
  83. package/dist/modules/shop/blocks/OrderStatus.js +40 -0
  84. package/dist/modules/shop/blocks/OrderStatus.js.map +1 -0
  85. package/dist/modules/shop/blocks/ProductDetail.d.ts +2 -0
  86. package/dist/modules/shop/blocks/ProductDetail.js +168 -0
  87. package/dist/modules/shop/blocks/ProductDetail.js.map +1 -0
  88. package/dist/modules/shop/blocks/ProductGrid.d.ts +8 -0
  89. package/dist/modules/shop/blocks/ProductGrid.js +136 -0
  90. package/dist/modules/shop/blocks/ProductGrid.js.map +1 -0
  91. package/dist/modules/shop/blocks/ShopCategories.d.ts +2 -0
  92. package/dist/modules/shop/blocks/ShopCategories.js +69 -0
  93. package/dist/modules/shop/blocks/ShopCategories.js.map +1 -0
  94. package/dist/modules/shop/cartStore.d.ts +22 -0
  95. package/dist/modules/shop/cartStore.js +85 -0
  96. package/dist/modules/shop/cartStore.js.map +1 -0
  97. package/dist/modules/shop/lib.d.ts +25 -0
  98. package/dist/modules/shop/lib.js +61 -0
  99. package/dist/modules/shop/lib.js.map +1 -0
  100. package/dist/modules/shop/module.d.ts +1 -0
  101. package/dist/modules/shop/module.js +39 -0
  102. package/dist/modules/shop/module.js.map +1 -0
  103. package/dist/modules/shop/settings.d.ts +41 -0
  104. package/dist/modules/shop/settings.js +18 -0
  105. package/dist/modules/shop/settings.js.map +1 -0
  106. package/dist/schema/blocks.d.ts +27 -12
  107. package/dist/schema/blocks.js +3 -0
  108. package/dist/schema/blocks.js.map +1 -1
  109. package/dist/schema/index.d.ts +1 -0
  110. package/dist/schema/index.js +1 -0
  111. package/dist/schema/index.js.map +1 -1
  112. package/dist/schema/settings.d.ts +1 -34
  113. package/dist/schema/settings.js +2 -8
  114. package/dist/schema/settings.js.map +1 -1
  115. package/dist/schema/shop.d.ts +939 -0
  116. package/dist/schema/shop.js +209 -0
  117. package/dist/schema/shop.js.map +1 -0
  118. package/dist/schema/siteConfig.d.ts +3 -71
  119. package/dist/schema/siteConfig.js +3 -8
  120. package/dist/schema/siteConfig.js.map +1 -1
  121. package/dist/schema/symbols.d.ts +2 -2
  122. package/dist/schema/symbols.js.map +1 -1
  123. package/dist/server/index.d.ts +1 -1
  124. package/dist/server/index.js +2 -0
  125. package/dist/server/index.js.map +1 -1
  126. package/dist/server/pages.d.ts +23 -3
  127. package/dist/server/pages.js +37 -3
  128. package/dist/server/pages.js.map +1 -1
  129. package/dist/server/seo.js +5 -3
  130. package/dist/server/seo.js.map +1 -1
  131. package/dist/settings/useSiteSettings.d.ts +15 -3
  132. package/dist/settings/useSiteSettings.js +26 -5
  133. package/dist/settings/useSiteSettings.js.map +1 -1
  134. package/dist/styles.css +14 -0
  135. package/package.json +6 -2
@@ -5,6 +5,7 @@ import {
5
5
  collection,
6
6
  deleteDoc,
7
7
  doc,
8
+ limit,
8
9
  onSnapshot,
9
10
  orderBy,
10
11
  query,
@@ -18,11 +19,14 @@ import { useToast } from "../ui/toast";
18
19
  import { useConfirm } from "../ui/ConfirmDialog";
19
20
  import { uploadImage } from "../lib/upload";
20
21
  import { findMediaUsage } from "../lib/mediaUsage";
22
+ const PAGE_SIZE = 48;
21
23
  function MediaManager({ siteId }) {
22
24
  const { toast } = useToast();
23
25
  const confirm = useConfirm();
24
26
  const [items, setItems] = useState([]);
25
27
  const [busy, setBusy] = useState(false);
28
+ const [pageCount, setPageCount] = useState(1);
29
+ const [hasMore, setHasMore] = useState(false);
26
30
  const inputRef = useRef(null);
27
31
  useEffect(() => {
28
32
  if (isDemoActive()) {
@@ -33,14 +37,18 @@ function MediaManager({ siteId }) {
33
37
  }
34
38
  const db = getDb();
35
39
  if (!db) return;
36
- const q = query(collection(db, "sites", siteId, "media"), orderBy("createdAt", "desc"));
40
+ const max = pageCount * PAGE_SIZE;
41
+ const q = query(collection(db, "sites", siteId, "media"), orderBy("createdAt", "desc"), limit(max + 1));
37
42
  return onSnapshot(
38
43
  q,
39
- (snap) => setItems(snap.docs.map((d) => ({ id: d.id, ...d.data() }))),
44
+ (snap) => {
45
+ setHasMore(snap.docs.length > max);
46
+ setItems(snap.docs.slice(0, max).map((d) => ({ id: d.id, ...d.data() })));
47
+ },
40
48
  // Leesfouten (rules/App Check) niet stil slikken — anders lijkt de bibliotheek leeg.
41
49
  (err) => toast({ title: "Kon mediabibliotheek niet laden", description: err.message, tone: "error" })
42
50
  );
43
- }, [siteId, toast]);
51
+ }, [siteId, toast, pageCount]);
44
52
  async function onFiles(files) {
45
53
  setBusy(true);
46
54
  let ok = 0;
@@ -60,7 +68,13 @@ function MediaManager({ siteId }) {
60
68
  async function saveAlt(item, alt) {
61
69
  const db = getDb();
62
70
  if (!db) return;
63
- await updateDoc(doc(db, "sites", siteId, "media", item.id), { alt });
71
+ if ((item.alt ?? "") === alt) return;
72
+ try {
73
+ await updateDoc(doc(db, "sites", siteId, "media", item.id), { alt });
74
+ toast({ title: "Alt-tekst opgeslagen", tone: "success" });
75
+ } catch (e) {
76
+ toast({ title: "Alt-tekst opslaan mislukt", description: e.message, tone: "error" });
77
+ }
64
78
  }
65
79
  async function remove(item) {
66
80
  const db = getDb();
@@ -74,21 +88,25 @@ function MediaManager({ siteId }) {
74
88
  tone: "danger"
75
89
  });
76
90
  if (!ok) return;
77
- await deleteDoc(doc(db, "sites", siteId, "media", item.id));
78
- if (bucket) {
79
- const paths = [
80
- item.path,
81
- item.variants?.webp?.path,
82
- ...(item.variants?.sizes ?? []).map((v) => v.path)
83
- ].filter((p) => Boolean(p));
84
- await Promise.all(
85
- paths.map((p) => deleteObject(storageRef(bucket, p)).catch(
86
- () => void 0
87
- /* al weg */
88
- ))
89
- );
91
+ try {
92
+ await deleteDoc(doc(db, "sites", siteId, "media", item.id));
93
+ if (bucket) {
94
+ const paths = [
95
+ item.path,
96
+ item.variants?.webp?.path,
97
+ ...(item.variants?.sizes ?? []).map((v) => v.path)
98
+ ].filter((p) => Boolean(p));
99
+ await Promise.all(
100
+ paths.map((p) => deleteObject(storageRef(bucket, p)).catch(
101
+ () => void 0
102
+ /* al weg */
103
+ ))
104
+ );
105
+ }
106
+ toast({ title: "Verwijderd" });
107
+ } catch (e) {
108
+ toast({ title: "Verwijderen mislukt", description: e.message, tone: "error" });
90
109
  }
91
- toast({ title: "Verwijderd" });
92
110
  }
93
111
  return /* @__PURE__ */ jsxs("div", { className: "grid gap-4", children: [
94
112
  /* @__PURE__ */ jsxs("div", { children: [
@@ -124,24 +142,38 @@ function MediaManager({ siteId }) {
124
142
  ] }),
125
143
  m.status === "processing" ? /* @__PURE__ */ jsx(Badge, { tone: "amber", children: "verwerken" }) : /* @__PURE__ */ jsx(Badge, { tone: "green", children: "klaar" })
126
144
  ] }),
127
- /* @__PURE__ */ jsx(
128
- Input,
129
- {
130
- defaultValue: m.alt ?? "",
131
- placeholder: "Alt-tekst",
132
- className: "mb-2 h-8 text-xs",
133
- onBlur: (e) => void saveAlt(m, e.target.value)
134
- }
135
- ),
145
+ /* @__PURE__ */ jsx(AltInput, { item: m, onSave: saveAlt }),
136
146
  /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
137
147
  /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", className: "text-xs", onClick: () => void navigator.clipboard?.writeText(m.url), children: "Kopieer URL" }),
138
148
  /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", className: "text-xs", onClick: () => void remove(m), children: "Verwijder" })
139
149
  ] })
140
150
  ] }, m.id)),
141
151
  items.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Nog geen media." })
142
- ] })
152
+ ] }),
153
+ hasMore && /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: () => setPageCount((c) => c + 1), children: "Meer laden\u2026" }) })
143
154
  ] });
144
155
  }
156
+ function AltInput({ item, onSave }) {
157
+ const [value, setValue] = useState(item.alt ?? "");
158
+ const [focused, setFocused] = useState(false);
159
+ useEffect(() => {
160
+ if (!focused) setValue(item.alt ?? "");
161
+ }, [item.alt, focused]);
162
+ return /* @__PURE__ */ jsx(
163
+ Input,
164
+ {
165
+ value,
166
+ placeholder: "Alt-tekst",
167
+ className: "mb-2 h-8 text-xs",
168
+ onChange: (e) => setValue(e.target.value),
169
+ onFocus: () => setFocused(true),
170
+ onBlur: () => {
171
+ setFocused(false);
172
+ void onSave(item, value);
173
+ }
174
+ }
175
+ );
176
+ }
145
177
  export {
146
178
  MediaManager
147
179
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/media/MediaManager.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n collection,\n deleteDoc,\n doc,\n onSnapshot,\n orderBy,\n query,\n updateDoc,\n} from \"firebase/firestore\";\nimport { deleteObject, ref as storageRef } from \"firebase/storage\";\nimport { getBucket, getDb } from \"../../firebase/client\";\nimport { DEMO_EVENT, isDemoActive, listDemoMediaAll } from \"../../demo/demoStore\";\nimport type { MediaItem } from \"../../content/types\";\nimport { Button, Card, Badge, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { uploadImage } from \"../lib/upload\";\nimport { findMediaUsage } from \"../lib/mediaUsage\";\n\nexport function MediaManager({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [items, setItems] = useState<MediaItem[]>([]);\n const [busy, setBusy] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n // Demo-modus: lokale bibliotheek (geseede foto's + eigen \"uploads\") i.p.v. Firestore.\n if (isDemoActive()) {\n const sync = () => setItems(listDemoMediaAll(siteId));\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }\n const db = getDb();\n if (!db) return;\n const q = query(collection(db, \"sites\", siteId, \"media\"), orderBy(\"createdAt\", \"desc\"));\n return onSnapshot(\n q,\n (snap) => setItems(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<MediaItem, \"id\">) }))),\n // Leesfouten (rules/App Check) niet stil slikken — anders lijkt de bibliotheek leeg.\n (err) => toast({ title: \"Kon mediabibliotheek niet laden\", description: err.message, tone: \"error\" }),\n );\n }, [siteId, toast]);\n\n async function onFiles(files: FileList) {\n setBusy(true);\n let ok = 0;\n const errors: string[] = [];\n for (const file of Array.from(files)) {\n try {\n await uploadImage(siteId, file);\n ok++;\n } catch (e) {\n errors.push(`${file.name}: ${(e as Error).message}`);\n }\n }\n if (ok > 0) toast({ title: `${ok} foto('s) geüpload`, description: \"Automatisch verkleind; varianten worden op de achtergrond gemaakt.\", tone: \"success\" });\n if (errors.length) toast({ title: \"Sommige uploads mislukten\", description: errors.join(\" · \"), tone: \"error\" });\n setBusy(false);\n }\n\n async function saveAlt(item: MediaItem, alt: string) {\n const db = getDb();\n if (!db) return;\n await updateDoc(doc(db, \"sites\", siteId, \"media\", item.id), { alt });\n }\n\n async function remove(item: MediaItem) {\n const db = getDb();\n const bucket = getBucket();\n if (!db) return;\n const usedOn = await findMediaUsage(siteId, item);\n const ok = await confirm({\n title: \"Afbeelding verwijderen?\",\n description:\n usedOn.length > 0\n ? `Let op: deze afbeelding wordt gebruikt op ${usedOn.length} gepubliceerde pagina('s) (${usedOn.join(\", \")}). Daar verschijnt dan een gebroken afbeelding.`\n : \"De afbeelding wordt definitief verwijderd.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n await deleteDoc(doc(db, \"sites\", siteId, \"media\", item.id));\n if (bucket) {\n // Ook de srcset-varianten opruimen die de Cloud Function heeft gemaakt — anders blijven\n // die (publieke) bestanden voor altijd in Storage staan.\n const paths = [\n item.path,\n item.variants?.webp?.path,\n ...(item.variants?.sizes ?? []).map((v) => v.path),\n ].filter((p): p is string => Boolean(p));\n await Promise.all(\n paths.map((p) => deleteObject(storageRef(bucket, p)).catch(() => undefined /* al weg */)),\n );\n }\n toast({ title: \"Verwijderd\" });\n }\n\n return (\n <div className=\"grid gap-4\">\n <div>\n <Button disabled={busy} onClick={() => inputRef.current?.click()}>\n {busy ? \"Uploaden…\" : \"Foto's uploaden\"}\n </Button>\n <p className=\"mt-1 text-xs text-slate-500\">\n Je kunt meerdere foto&apos;s tegelijk kiezen. Ze worden automatisch verkleind en geoptimaliseerd\n (webp) — dat houdt je site snel en bespaart opslag. Max. 15 MB per bestand.\n </p>\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n hidden\n onChange={(e) => {\n if (e.target.files?.length) void onFiles(e.target.files);\n e.target.value = \"\";\n }}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4\">\n {items.map((m) => (\n <Card key={m.id} className=\"p-3\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={m.url} alt={m.alt ?? m.name} className=\"mb-2 h-28 w-full rounded-md object-cover\" />\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <span className=\"truncate text-xs text-slate-500\">\n {m.name}\n {(m as { width?: number; height?: number }).width && (\n <span className=\"text-slate-400\"> · {(m as { width?: number }).width}×{(m as { height?: number }).height}</span>\n )}\n </span>\n {m.status === \"processing\" ? <Badge tone=\"amber\">verwerken</Badge> : <Badge tone=\"green\">klaar</Badge>}\n </div>\n <Input\n defaultValue={m.alt ?? \"\"}\n placeholder=\"Alt-tekst\"\n className=\"mb-2 h-8 text-xs\"\n onBlur={(e) => void saveAlt(m, e.target.value)}\n />\n <div className=\"flex gap-1\">\n <Button variant=\"ghost\" size=\"sm\" className=\"text-xs\" onClick={() => void navigator.clipboard?.writeText(m.url)}>\n Kopieer URL\n </Button>\n <Button variant=\"ghost\" size=\"sm\" className=\"text-xs\" onClick={() => void remove(m)}>\n Verwijder\n </Button>\n </div>\n </Card>\n ))}\n {items.length === 0 && <p className=\"text-sm text-slate-500\">Nog geen media.</p>}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/media/MediaManager.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n collection,\n deleteDoc,\n doc,\n limit,\n onSnapshot,\n orderBy,\n query,\n updateDoc,\n} from \"firebase/firestore\";\nimport { deleteObject, ref as storageRef } from \"firebase/storage\";\nimport { getBucket, getDb } from \"../../firebase/client\";\nimport { DEMO_EVENT, isDemoActive, listDemoMediaAll } from \"../../demo/demoStore\";\nimport type { MediaItem } from \"../../content/types\";\nimport { Button, Card, Badge, Input } from \"../ui/primitives\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { uploadImage } from \"../lib/upload\";\nimport { findMediaUsage } from \"../lib/mediaUsage\";\n\n/** Paginagrootte van de bibliotheek: begrensd laden i.p.v. álles (kosten + geheugen bij 100+ foto's). */\nconst PAGE_SIZE = 48;\n\nexport function MediaManager({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [items, setItems] = useState<MediaItem[]>([]);\n const [busy, setBusy] = useState(false);\n const [pageCount, setPageCount] = useState(1);\n const [hasMore, setHasMore] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n // Demo-modus: lokale bibliotheek (geseede foto's + eigen \"uploads\") i.p.v. Firestore.\n if (isDemoActive()) {\n const sync = () => setItems(listDemoMediaAll(siteId));\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }\n const db = getDb();\n if (!db) return;\n // limit + \"meer laden\" (pageCount groeit): geen volledige collectie-read per open.\n const max = pageCount * PAGE_SIZE;\n const q = query(collection(db, \"sites\", siteId, \"media\"), orderBy(\"createdAt\", \"desc\"), limit(max + 1));\n return onSnapshot(\n q,\n (snap) => {\n setHasMore(snap.docs.length > max);\n setItems(snap.docs.slice(0, max).map((d) => ({ id: d.id, ...(d.data() as Omit<MediaItem, \"id\">) })));\n },\n // Leesfouten (rules/App Check) niet stil slikken — anders lijkt de bibliotheek leeg.\n (err) => toast({ title: \"Kon mediabibliotheek niet laden\", description: err.message, tone: \"error\" }),\n );\n }, [siteId, toast, pageCount]);\n\n async function onFiles(files: FileList) {\n setBusy(true);\n let ok = 0;\n const errors: string[] = [];\n for (const file of Array.from(files)) {\n try {\n await uploadImage(siteId, file);\n ok++;\n } catch (e) {\n errors.push(`${file.name}: ${(e as Error).message}`);\n }\n }\n if (ok > 0) toast({ title: `${ok} foto('s) geüpload`, description: \"Automatisch verkleind; varianten worden op de achtergrond gemaakt.\", tone: \"success\" });\n if (errors.length) toast({ title: \"Sommige uploads mislukten\", description: errors.join(\" · \"), tone: \"error\" });\n setBusy(false);\n }\n\n async function saveAlt(item: MediaItem, alt: string) {\n const db = getDb();\n if (!db) return;\n if ((item.alt ?? \"\") === alt) return; // niets gewijzigd → geen write/toast\n try {\n await updateDoc(doc(db, \"sites\", siteId, \"media\", item.id), { alt });\n toast({ title: \"Alt-tekst opgeslagen\", tone: \"success\" });\n } catch (e) {\n // Niet stil slikken: de klant dacht anders dat de alt-tekst opgeslagen was.\n toast({ title: \"Alt-tekst opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n async function remove(item: MediaItem) {\n const db = getDb();\n const bucket = getBucket();\n if (!db) return;\n const usedOn = await findMediaUsage(siteId, item);\n const ok = await confirm({\n title: \"Afbeelding verwijderen?\",\n description:\n usedOn.length > 0\n ? `Let op: deze afbeelding wordt gebruikt op ${usedOn.length} gepubliceerde pagina('s) (${usedOn.join(\", \")}). Daar verschijnt dan een gebroken afbeelding.`\n : \"De afbeelding wordt definitief verwijderd.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n try {\n await deleteDoc(doc(db, \"sites\", siteId, \"media\", item.id));\n if (bucket) {\n // Ook de srcset-varianten opruimen die de Cloud Function heeft gemaakt — anders blijven\n // die (publieke) bestanden voor altijd in Storage staan.\n const paths = [\n item.path,\n item.variants?.webp?.path,\n ...(item.variants?.sizes ?? []).map((v) => v.path),\n ].filter((p): p is string => Boolean(p));\n await Promise.all(\n paths.map((p) => deleteObject(storageRef(bucket, p)).catch(() => undefined /* al weg */)),\n );\n }\n toast({ title: \"Verwijderd\" });\n } catch (e) {\n // Rules/offline-fout niet stil slikken — anders lijkt verwijderen \"niets te doen\".\n toast({ title: \"Verwijderen mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n return (\n <div className=\"grid gap-4\">\n <div>\n <Button disabled={busy} onClick={() => inputRef.current?.click()}>\n {busy ? \"Uploaden…\" : \"Foto's uploaden\"}\n </Button>\n <p className=\"mt-1 text-xs text-slate-500\">\n Je kunt meerdere foto&apos;s tegelijk kiezen. Ze worden automatisch verkleind en geoptimaliseerd\n (webp) — dat houdt je site snel en bespaart opslag. Max. 15 MB per bestand.\n </p>\n <input\n ref={inputRef}\n type=\"file\"\n accept=\"image/*\"\n multiple\n hidden\n onChange={(e) => {\n if (e.target.files?.length) void onFiles(e.target.files);\n e.target.value = \"\";\n }}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4\">\n {items.map((m) => (\n <Card key={m.id} className=\"p-3\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={m.url} alt={m.alt ?? m.name} className=\"mb-2 h-28 w-full rounded-md object-cover\" />\n <div className=\"mb-2 flex items-center justify-between gap-1\">\n <span className=\"truncate text-xs text-slate-500\">\n {m.name}\n {(m as { width?: number; height?: number }).width && (\n <span className=\"text-slate-400\"> · {(m as { width?: number }).width}×{(m as { height?: number }).height}</span>\n )}\n </span>\n {m.status === \"processing\" ? <Badge tone=\"amber\">verwerken</Badge> : <Badge tone=\"green\">klaar</Badge>}\n </div>\n <AltInput item={m} onSave={saveAlt} />\n <div className=\"flex gap-1\">\n <Button variant=\"ghost\" size=\"sm\" className=\"text-xs\" onClick={() => void navigator.clipboard?.writeText(m.url)}>\n Kopieer URL\n </Button>\n <Button variant=\"ghost\" size=\"sm\" className=\"text-xs\" onClick={() => void remove(m)}>\n Verwijder\n </Button>\n </div>\n </Card>\n ))}\n {items.length === 0 && <p className=\"text-sm text-slate-500\">Nog geen media.</p>}\n </div>\n {hasMore && (\n <div>\n <Button variant=\"secondary\" size=\"sm\" onClick={() => setPageCount((c) => c + 1)}>\n Meer laden…\n </Button>\n </div>\n )}\n </div>\n );\n}\n\n/** Controlled alt-veld: volgt externe updates (realtime snapshot) en slaat alleen échte wijzigingen op. */\nfunction AltInput({ item, onSave }: { item: MediaItem; onSave: (item: MediaItem, alt: string) => Promise<void> }) {\n const [value, setValue] = useState(item.alt ?? \"\");\n // Extern gewijzigd (andere tab/verwerking) → veld bijtrekken, behalve terwijl de klant zelf typt.\n const [focused, setFocused] = useState(false);\n useEffect(() => {\n if (!focused) setValue(item.alt ?? \"\");\n }, [item.alt, focused]);\n return (\n <Input\n value={value}\n placeholder=\"Alt-tekst\"\n className=\"mb-2 h-8 text-xs\"\n onChange={(e) => setValue(e.target.value)}\n onFocus={() => setFocused(true)}\n onBlur={() => {\n setFocused(false);\n void onSave(item, value);\n }}\n />\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,13 +1,14 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useRef, useState } from "react";
4
- import { collection, onSnapshot, orderBy, query } from "firebase/firestore";
4
+ import { collection, limit, onSnapshot, orderBy, query } from "firebase/firestore";
5
5
  import { getDb } from "../../firebase/client";
6
6
  import { DEMO_EVENT, isDemoActive, listDemoMediaAll } from "../../demo/demoStore";
7
7
  import { Button, Input } from "../ui/primitives";
8
8
  import { Dialog, DialogContent } from "../ui/dialog";
9
9
  import { useToast } from "../ui/toast";
10
10
  import { uploadImage } from "../lib/upload";
11
+ const PICKER_PAGE_SIZE = 48;
11
12
  function MediaPickerDialog({
12
13
  siteId,
13
14
  open,
@@ -18,6 +19,8 @@ function MediaPickerDialog({
18
19
  const [items, setItems] = useState([]);
19
20
  const [q, setQ] = useState("");
20
21
  const [busy, setBusy] = useState(false);
22
+ const [pageCount, setPageCount] = useState(1);
23
+ const [hasMore, setHasMore] = useState(false);
21
24
  const inputRef = useRef(null);
22
25
  useEffect(() => {
23
26
  if (!open) return;
@@ -29,15 +32,19 @@ function MediaPickerDialog({
29
32
  }
30
33
  const db = getDb();
31
34
  if (!db) return;
32
- const qy = query(collection(db, "sites", siteId, "media"), orderBy("createdAt", "desc"));
35
+ const max = pageCount * PICKER_PAGE_SIZE;
36
+ const qy = query(collection(db, "sites", siteId, "media"), orderBy("createdAt", "desc"), limit(max + 1));
33
37
  return onSnapshot(
34
38
  qy,
35
- (snap) => setItems(snap.docs.map((d) => ({ id: d.id, ...d.data() }))),
39
+ (snap) => {
40
+ setHasMore(snap.docs.length > max);
41
+ setItems(snap.docs.slice(0, max).map((d) => ({ id: d.id, ...d.data() })));
42
+ },
36
43
  // Zonder onError worden lees-/rules-/App-Check-fouten stil geslikt → de kiezer toont dan
37
44
  // onterecht "Nog geen afbeeldingen". Nu een duidelijke melding.
38
45
  (err) => toast({ title: "Kon mediabibliotheek niet laden", description: err.message, tone: "error" })
39
46
  );
40
- }, [open, siteId, toast]);
47
+ }, [open, siteId, toast, pageCount]);
41
48
  const filtered = useMemo(() => {
42
49
  const term = q.trim().toLowerCase();
43
50
  if (!term) return items;
@@ -70,17 +77,20 @@ function MediaPickerDialog({
70
77
  e.target.value = "";
71
78
  } })
72
79
  ] }),
73
- /* @__PURE__ */ jsx("div", { className: "max-h-[55vh] overflow-y-auto", children: filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "py-8 text-center text-sm text-slate-500", children: items.length === 0 ? "Nog geen afbeeldingen \u2014 upload er een." : "Geen resultaten." }) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2 sm:grid-cols-4", children: filtered.map((m) => /* @__PURE__ */ jsx(
74
- "button",
75
- {
76
- type: "button",
77
- onClick: () => pick(m),
78
- title: m.alt || m.name,
79
- className: "group relative overflow-hidden rounded-md border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500",
80
- children: /* @__PURE__ */ jsx("img", { src: m.url, alt: m.alt ?? m.name, className: "aspect-square w-full object-cover transition group-hover:opacity-80" })
81
- },
82
- m.id
83
- )) }) })
80
+ /* @__PURE__ */ jsxs("div", { className: "max-h-[55vh] overflow-y-auto", children: [
81
+ filtered.length === 0 ? /* @__PURE__ */ jsx("p", { className: "py-8 text-center text-sm text-slate-500", children: items.length === 0 ? "Nog geen afbeeldingen \u2014 upload er een." : "Geen resultaten." }) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-3 gap-2 sm:grid-cols-4", children: filtered.map((m) => /* @__PURE__ */ jsx(
82
+ "button",
83
+ {
84
+ type: "button",
85
+ onClick: () => pick(m),
86
+ title: m.alt || m.name,
87
+ className: "group relative overflow-hidden rounded-md border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500",
88
+ children: /* @__PURE__ */ jsx("img", { src: m.url, alt: m.alt ?? m.name, className: "aspect-square w-full object-cover transition group-hover:opacity-80" })
89
+ },
90
+ m.id
91
+ )) }),
92
+ hasMore && !q.trim() && /* @__PURE__ */ jsx("p", { className: "mt-2 text-center", children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setPageCount((c) => c + 1), children: "Meer laden\u2026" }) })
93
+ ] })
84
94
  ] }) });
85
95
  }
86
96
  function MediaPickerButton({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/media/MediaPicker.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { collection, onSnapshot, orderBy, query } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { DEMO_EVENT, isDemoActive, listDemoMediaAll } from \"../../demo/demoStore\";\nimport type { MediaItem } from \"../../content/types\";\nimport { Button, Input } from \"../ui/primitives\";\nimport { Dialog, DialogContent } from \"../ui/dialog\";\nimport { useToast } from \"../ui/toast\";\nimport { uploadImage } from \"../lib/upload\";\n\nexport interface PickedMedia {\n url: string;\n mediaId: string;\n alt?: string;\n}\n\n/** Gecontroleerde mediabibliotheek-kiezer (open/onOpenChange extern) — zoek + klik om te kiezen, of\n * upload direct. Gebruikt door de knop hieronder én door de inline beeld-vervangen in de editor. */\nexport function MediaPickerDialog({\n siteId,\n open,\n onOpenChange,\n onSelect,\n}: {\n siteId: string;\n open: boolean;\n onOpenChange: (v: boolean) => void;\n onSelect: (m: PickedMedia) => void;\n}) {\n const { toast } = useToast();\n const [items, setItems] = useState<MediaItem[]>([]);\n const [q, setQ] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n if (!open) return;\n // Demo-modus: lokale bibliotheek (geseede foto's + eigen \"uploads\") i.p.v. Firestore.\n if (isDemoActive()) {\n const sync = () => setItems(listDemoMediaAll(siteId));\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }\n const db = getDb();\n if (!db) return;\n const qy = query(collection(db, \"sites\", siteId, \"media\"), orderBy(\"createdAt\", \"desc\"));\n return onSnapshot(\n qy,\n (snap) => setItems(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<MediaItem, \"id\">) }))),\n // Zonder onError worden lees-/rules-/App-Check-fouten stil geslikt → de kiezer toont dan\n // onterecht \"Nog geen afbeeldingen\". Nu een duidelijke melding.\n (err) => toast({ title: \"Kon mediabibliotheek niet laden\", description: err.message, tone: \"error\" }),\n );\n }, [open, siteId, toast]);\n\n const filtered = useMemo(() => {\n const term = q.trim().toLowerCase();\n if (!term) return items;\n return items.filter((m) => `${m.name ?? \"\"} ${m.alt ?? \"\"}`.toLowerCase().includes(term));\n }, [items, q]);\n\n function pick(m: MediaItem) {\n onSelect({ url: m.url, mediaId: m.id, alt: m.alt });\n onOpenChange(false);\n }\n\n async function onUpload(files: FileList | null) {\n const f = files?.[0];\n if (!f) return;\n setBusy(true);\n try {\n const { url, mediaId } = await uploadImage(siteId, f);\n onSelect({ url, mediaId, alt: \"\" });\n onOpenChange(false);\n } catch (err) {\n toast({ title: \"Upload mislukt\", description: (err as Error).message, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent title=\"Afbeelding kiezen\" className=\"max-w-2xl\">\n <div className=\"mb-3 flex items-center gap-2\">\n <Input value={q} onChange={(e) => setQ(e.target.value)} placeholder=\"Zoek in bibliotheek…\" className=\"h-9\" />\n <Button type=\"button\" disabled={busy} onClick={() => inputRef.current?.click()}>\n {busy ? \"Uploaden…\" : \"Upload nieuwe\"}\n </Button>\n <input ref={inputRef} type=\"file\" accept=\"image/*\" hidden onChange={(e) => { void onUpload(e.target.files); e.target.value = \"\"; }} />\n </div>\n <div className=\"max-h-[55vh] overflow-y-auto\">\n {filtered.length === 0 ? (\n <p className=\"py-8 text-center text-sm text-slate-500\">\n {items.length === 0 ? \"Nog geen afbeeldingen — upload er een.\" : \"Geen resultaten.\"}\n </p>\n ) : (\n <div className=\"grid grid-cols-3 gap-2 sm:grid-cols-4\">\n {filtered.map((m) => (\n <button\n key={m.id}\n type=\"button\"\n onClick={() => pick(m)}\n title={m.alt || m.name}\n className=\"group relative overflow-hidden rounded-md border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500\"\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={m.url} alt={m.alt ?? m.name} className=\"aspect-square w-full object-cover transition group-hover:opacity-80\" />\n </button>\n ))}\n </div>\n )}\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n\n/**\n * Knop die de mediabibliotheek-kiezer opent: zoek + klik om een bestaande afbeelding te kiezen, of\n * upload er direct een nieuwe in. Vervangt het plakken van losse URL's.\n */\nexport function MediaPickerButton({\n siteId,\n onSelect,\n label = \"Kies / upload\",\n size = \"sm\",\n variant = \"secondary\",\n}: {\n siteId: string;\n onSelect: (m: PickedMedia) => void;\n label?: string;\n size?: \"sm\" | \"md\";\n variant?: \"primary\" | \"secondary\" | \"ghost\";\n}) {\n const [open, setOpen] = useState(false);\n return (\n <>\n <Button type=\"button\" variant={variant} size={size} onClick={() => setOpen(true)}>{label}</Button>\n <MediaPickerDialog siteId={siteId} open={open} onOpenChange={setOpen} onSelect={onSelect} />\n </>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/media/MediaPicker.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { collection, limit, onSnapshot, orderBy, query } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\nimport { DEMO_EVENT, isDemoActive, listDemoMediaAll } from \"../../demo/demoStore\";\nimport type { MediaItem } from \"../../content/types\";\nimport { Button, Input } from \"../ui/primitives\";\nimport { Dialog, DialogContent } from \"../ui/dialog\";\nimport { useToast } from \"../ui/toast\";\nimport { uploadImage } from \"../lib/upload\";\n\nexport interface PickedMedia {\n url: string;\n mediaId: string;\n alt?: string;\n}\n\n/** Paginagrootte: begrensd laden i.p.v. de hele bibliotheek bij elke kiezer-open. */\nconst PICKER_PAGE_SIZE = 48;\n\n/** Gecontroleerde mediabibliotheek-kiezer (open/onOpenChange extern) — zoek + klik om te kiezen, of\n * upload direct. Gebruikt door de knop hieronder én door de inline beeld-vervangen in de editor. */\nexport function MediaPickerDialog({\n siteId,\n open,\n onOpenChange,\n onSelect,\n}: {\n siteId: string;\n open: boolean;\n onOpenChange: (v: boolean) => void;\n onSelect: (m: PickedMedia) => void;\n}) {\n const { toast } = useToast();\n const [items, setItems] = useState<MediaItem[]>([]);\n const [q, setQ] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [pageCount, setPageCount] = useState(1);\n const [hasMore, setHasMore] = useState(false);\n const inputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => {\n if (!open) return;\n // Demo-modus: lokale bibliotheek (geseede foto's + eigen \"uploads\") i.p.v. Firestore.\n if (isDemoActive()) {\n const sync = () => setItems(listDemoMediaAll(siteId));\n sync();\n window.addEventListener(DEMO_EVENT, sync);\n return () => window.removeEventListener(DEMO_EVENT, sync);\n }\n const db = getDb();\n if (!db) return;\n // Begrensd laden (+\"meer laden\"): geen volledige collectie-read bij elke kiezer-open.\n const max = pageCount * PICKER_PAGE_SIZE;\n const qy = query(collection(db, \"sites\", siteId, \"media\"), orderBy(\"createdAt\", \"desc\"), limit(max + 1));\n return onSnapshot(\n qy,\n (snap) => {\n setHasMore(snap.docs.length > max);\n setItems(snap.docs.slice(0, max).map((d) => ({ id: d.id, ...(d.data() as Omit<MediaItem, \"id\">) })));\n },\n // Zonder onError worden lees-/rules-/App-Check-fouten stil geslikt → de kiezer toont dan\n // onterecht \"Nog geen afbeeldingen\". Nu een duidelijke melding.\n (err) => toast({ title: \"Kon mediabibliotheek niet laden\", description: err.message, tone: \"error\" }),\n );\n }, [open, siteId, toast, pageCount]);\n\n const filtered = useMemo(() => {\n const term = q.trim().toLowerCase();\n if (!term) return items;\n return items.filter((m) => `${m.name ?? \"\"} ${m.alt ?? \"\"}`.toLowerCase().includes(term));\n }, [items, q]);\n\n function pick(m: MediaItem) {\n onSelect({ url: m.url, mediaId: m.id, alt: m.alt });\n onOpenChange(false);\n }\n\n async function onUpload(files: FileList | null) {\n const f = files?.[0];\n if (!f) return;\n setBusy(true);\n try {\n const { url, mediaId } = await uploadImage(siteId, f);\n onSelect({ url, mediaId, alt: \"\" });\n onOpenChange(false);\n } catch (err) {\n toast({ title: \"Upload mislukt\", description: (err as Error).message, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent title=\"Afbeelding kiezen\" className=\"max-w-2xl\">\n <div className=\"mb-3 flex items-center gap-2\">\n <Input value={q} onChange={(e) => setQ(e.target.value)} placeholder=\"Zoek in bibliotheek…\" className=\"h-9\" />\n <Button type=\"button\" disabled={busy} onClick={() => inputRef.current?.click()}>\n {busy ? \"Uploaden…\" : \"Upload nieuwe\"}\n </Button>\n <input ref={inputRef} type=\"file\" accept=\"image/*\" hidden onChange={(e) => { void onUpload(e.target.files); e.target.value = \"\"; }} />\n </div>\n <div className=\"max-h-[55vh] overflow-y-auto\">\n {filtered.length === 0 ? (\n <p className=\"py-8 text-center text-sm text-slate-500\">\n {items.length === 0 ? \"Nog geen afbeeldingen — upload er een.\" : \"Geen resultaten.\"}\n </p>\n ) : (\n <div className=\"grid grid-cols-3 gap-2 sm:grid-cols-4\">\n {filtered.map((m) => (\n <button\n key={m.id}\n type=\"button\"\n onClick={() => pick(m)}\n title={m.alt || m.name}\n className=\"group relative overflow-hidden rounded-md border border-slate-200 focus:outline-none focus:ring-2 focus:ring-indigo-500\"\n >\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={m.url} alt={m.alt ?? m.name} className=\"aspect-square w-full object-cover transition group-hover:opacity-80\" />\n </button>\n ))}\n </div>\n )}\n {hasMore && !q.trim() && (\n <p className=\"mt-2 text-center\">\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => setPageCount((c) => c + 1)}>\n Meer laden…\n </Button>\n </p>\n )}\n </div>\n </DialogContent>\n </Dialog>\n );\n}\n\n/**\n * Knop die de mediabibliotheek-kiezer opent: zoek + klik om een bestaande afbeelding te kiezen, of\n * upload er direct een nieuwe in. Vervangt het plakken van losse URL's.\n */\nexport function MediaPickerButton({\n siteId,\n onSelect,\n label = \"Kies / upload\",\n size = \"sm\",\n variant = \"secondary\",\n}: {\n siteId: string;\n onSelect: (m: PickedMedia) => void;\n label?: string;\n size?: \"sm\" | \"md\";\n variant?: \"primary\" | \"secondary\" | \"ghost\";\n}) {\n const [open, setOpen] = useState(false);\n return (\n <>\n <Button type=\"button\" variant={variant} size={size} onClick={() => setOpen(true)}>{label}</Button>\n <MediaPickerDialog siteId={siteId} open={open} onOpenChange={setOpen} onSelect={onSelect} />\n </>\n );\n}\n"],"mappings":";AAiGQ,SA4DJ,UA3DM,KADF;AA/FR,SAAS,WAAW,SAAS,QAAQ,gBAAgB;AACrD,SAAS,YAAY,OAAO,YAAY,SAAS,aAAa;AAC9D,SAAS,aAAa;AACtB,SAAS,YAAY,cAAc,wBAAwB;AAE3D,SAAS,QAAQ,aAAa;AAC9B,SAAS,QAAQ,qBAAqB;AACtC,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAS5B,MAAM,mBAAmB;AAIlB,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,QAAM,EAAE,MAAM,IAAI,SAAS;AAC3B,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAsB,CAAC,CAAC;AAClD,QAAM,CAAC,GAAG,IAAI,IAAI,SAAS,EAAE;AAC7B,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,QAAM,CAAC,WAAW,YAAY,IAAI,SAAS,CAAC;AAC5C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,KAAK;AAC5C,QAAM,WAAW,OAAyB,IAAI;AAE9C,YAAU,MAAM;AACd,QAAI,CAAC,KAAM;AAEX,QAAI,aAAa,GAAG;AAClB,YAAM,OAAO,MAAM,SAAS,iBAAiB,MAAM,CAAC;AACpD,WAAK;AACL,aAAO,iBAAiB,YAAY,IAAI;AACxC,aAAO,MAAM,OAAO,oBAAoB,YAAY,IAAI;AAAA,IAC1D;AACA,UAAM,KAAK,MAAM;AACjB,QAAI,CAAC,GAAI;AAET,UAAM,MAAM,YAAY;AACxB,UAAM,KAAK,MAAM,WAAW,IAAI,SAAS,QAAQ,OAAO,GAAG,QAAQ,aAAa,MAAM,GAAG,MAAM,MAAM,CAAC,CAAC;AACvG,WAAO;AAAA,MACL;AAAA,MACA,CAAC,SAAS;AACR,mBAAW,KAAK,KAAK,SAAS,GAAG;AACjC,iBAAS,KAAK,KAAK,MAAM,GAAG,GAAG,EAAE,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAA4B,EAAE,CAAC;AAAA,MACrG;AAAA;AAAA;AAAA,MAGA,CAAC,QAAQ,MAAM,EAAE,OAAO,mCAAmC,aAAa,IAAI,SAAS,MAAM,QAAQ,CAAC;AAAA,IACtG;AAAA,EACF,GAAG,CAAC,MAAM,QAAQ,OAAO,SAAS,CAAC;AAEnC,QAAM,WAAW,QAAQ,MAAM;AAC7B,UAAM,OAAO,EAAE,KAAK,EAAE,YAAY;AAClC,QAAI,CAAC,KAAM,QAAO;AAClB,WAAO,MAAM,OAAO,CAAC,MAAM,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,YAAY,EAAE,SAAS,IAAI,CAAC;AAAA,EAC1F,GAAG,CAAC,OAAO,CAAC,CAAC;AAEb,WAAS,KAAK,GAAc;AAC1B,aAAS,EAAE,KAAK,EAAE,KAAK,SAAS,EAAE,IAAI,KAAK,EAAE,IAAI,CAAC;AAClD,iBAAa,KAAK;AAAA,EACpB;AAEA,iBAAe,SAAS,OAAwB;AAC9C,UAAM,IAAI,QAAQ,CAAC;AACnB,QAAI,CAAC,EAAG;AACR,YAAQ,IAAI;AACZ,QAAI;AACF,YAAM,EAAE,KAAK,QAAQ,IAAI,MAAM,YAAY,QAAQ,CAAC;AACpD,eAAS,EAAE,KAAK,SAAS,KAAK,GAAG,CAAC;AAClC,mBAAa,KAAK;AAAA,IACpB,SAAS,KAAK;AACZ,YAAM,EAAE,OAAO,kBAAkB,aAAc,IAAc,SAAS,MAAM,QAAQ,CAAC;AAAA,IACvF,UAAE;AACA,cAAQ,KAAK;AAAA,IACf;AAAA,EACF;AAEA,SACE,oBAAC,UAAO,MAAY,cAClB,+BAAC,iBAAc,OAAM,qBAAoB,WAAU,aACjD;AAAA,yBAAC,SAAI,WAAU,gCACb;AAAA,0BAAC,SAAM,OAAO,GAAG,UAAU,CAAC,MAAM,KAAK,EAAE,OAAO,KAAK,GAAG,aAAY,6BAAuB,WAAU,OAAM;AAAA,MAC3G,oBAAC,UAAO,MAAK,UAAS,UAAU,MAAM,SAAS,MAAM,SAAS,SAAS,MAAM,GAC1E,iBAAO,mBAAc,iBACxB;AAAA,MACA,oBAAC,WAAM,KAAK,UAAU,MAAK,QAAO,QAAO,WAAU,QAAM,MAAC,UAAU,CAAC,MAAM;AAAE,aAAK,SAAS,EAAE,OAAO,KAAK;AAAG,UAAE,OAAO,QAAQ;AAAA,MAAI,GAAG;AAAA,OACtI;AAAA,IACA,qBAAC,SAAI,WAAU,gCACZ;AAAA,eAAS,WAAW,IACnB,oBAAC,OAAE,WAAU,2CACV,gBAAM,WAAW,IAAI,gDAA2C,oBACnE,IAEA,oBAAC,SAAI,WAAU,yCACZ,mBAAS,IAAI,CAAC,MACb;AAAA,QAAC;AAAA;AAAA,UAEC,MAAK;AAAA,UACL,SAAS,MAAM,KAAK,CAAC;AAAA,UACrB,OAAO,EAAE,OAAO,EAAE;AAAA,UAClB,WAAU;AAAA,UAGV,8BAAC,SAAI,KAAK,EAAE,KAAK,KAAK,EAAE,OAAO,EAAE,MAAM,WAAU,uEAAsE;AAAA;AAAA,QAPlH,EAAE;AAAA,MAQT,CACD,GACH;AAAA,MAED,WAAW,CAAC,EAAE,KAAK,KAClB,oBAAC,OAAE,WAAU,oBACX,8BAAC,UAAO,MAAK,UAAS,SAAQ,SAAQ,MAAK,MAAK,SAAS,MAAM,aAAa,CAAC,MAAM,IAAI,CAAC,GAAG,8BAE3F,GACF;AAAA,OAEJ;AAAA,KACF,GACF;AAEJ;AAMO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,UAAU;AACZ,GAMG;AACD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,KAAK;AACtC,SACE,iCACE;AAAA,wBAAC,UAAO,MAAK,UAAS,SAAkB,MAAY,SAAS,MAAM,QAAQ,IAAI,GAAI,iBAAM;AAAA,IACzF,oBAAC,qBAAkB,QAAgB,MAAY,cAAc,SAAS,UAAoB;AAAA,KAC5F;AAEJ;","names":[]}
@@ -1,5 +1,6 @@
1
1
  import { type DraftPage } from "../lib/pagesClient";
2
- export declare function PageEditor({ siteId, page }: {
2
+ export declare function PageEditor({ siteId, page, allPages }: {
3
3
  siteId: string;
4
4
  page: DraftPage;
5
+ allPages?: DraftPage[];
5
6
  }): import("react").JSX.Element;
@@ -22,7 +22,7 @@ function formatVersionDate(createdAt) {
22
22
  if (!d || Number.isNaN(d.getTime())) return "";
23
23
  return d.toLocaleString("nl-NL", { dateStyle: "medium", timeStyle: "short" });
24
24
  }
25
- function PageEditor({ siteId, page }) {
25
+ function PageEditor({ siteId, page, allPages }) {
26
26
  const registry = useRegistry();
27
27
  const { activeIds, defs } = useModules();
28
28
  const { toast } = useToast();
@@ -176,7 +176,21 @@ function PageEditor({ siteId, page }) {
176
176
  saveState === "error" && /* @__PURE__ */ jsx("div", { className: "mb-3 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700", children: "Niet alle wijzigingen zijn opgeslagen. Controleer je verbinding en klik \u201COpnieuw opslaan\u201D." }),
177
177
  /* @__PURE__ */ jsxs("div", { className: "grid gap-3 md:grid-cols-2", children: [
178
178
  /* @__PURE__ */ jsx(Field, { label: "Titel", children: /* @__PURE__ */ jsx(Input, { value: draft.title, onChange: (e) => setTitle(e.target.value) }) }),
179
- /* @__PURE__ */ jsx(Field, { label: "Pad", hint: "bv. / of /over-ons", children: /* @__PURE__ */ jsx(Input, { value: draft.path, onChange: (e) => update({ ...draft, path: e.target.value }) }) }),
179
+ /* @__PURE__ */ jsxs(Field, { label: "Pad", hint: "bv. / of /over-ons", children: [
180
+ /* @__PURE__ */ jsx(
181
+ Input,
182
+ {
183
+ value: draft.path,
184
+ onChange: (e) => update({ ...draft, path: e.target.value }),
185
+ onBlur: () => {
186
+ const parts = draft.path.split("/").map((s) => slugify(s)).filter(Boolean);
187
+ const normalized = "/" + parts.join("/");
188
+ if (normalized !== draft.path) update({ ...draft, path: normalized });
189
+ }
190
+ }
191
+ ),
192
+ allPages?.some((p) => p.id !== draft.id && p.path === draft.path) && /* @__PURE__ */ jsx("p", { className: "mt-1 text-xs text-red-600", role: "alert", children: "\u26A0 Er bestaat al een pagina met dit pad \u2014 \xE9\xE9n van beide wordt dan onbereikbaar." })
193
+ ] }),
180
194
  /* @__PURE__ */ jsx(Field, { label: "SEO-titel", children: /* @__PURE__ */ jsx(Input, { value: draft.seo?.title ?? "", onChange: (e) => update({ ...draft, seo: { ...draft.seo, title: e.target.value } }) }) }),
181
195
  /* @__PURE__ */ jsx(Field, { label: "SEO-omschrijving", children: /* @__PURE__ */ jsx(Textarea, { rows: 2, value: draft.seo?.description ?? "", onChange: (e) => update({ ...draft, seo: { ...draft.seo, description: e.target.value } }) }) })
182
196
  ] })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/pages/PageEditor.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState, type ComponentType } from \"react\";\nimport { useRegistry } from \"../../blocks/context\";\nimport { buildBlockAddOptions } from \"../../blocks/registry\";\nimport { useModules } from \"../../modules/context\";\nimport { moduleIdForBlockType } from \"../../modules/registry\";\nimport { Button, Card, Field, Input, Textarea, Badge } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { builtinBlockEditors, GenericEditor, type BlockEditorProps } from \"../blockEditors\";\nimport { StyleEditor } from \"../blockEditors/StyleEditor\";\nimport { PatternInserter } from \"../patterns/PatternInserter\";\nimport type { BlockStyle } from \"../../schema/blocks\";\nimport { saveDraft, publishPage, subscribeVersions, restoreVersion, type DraftPage } from \"../lib/pagesClient\";\nimport { slugify } from \"../lib/slugify\";\n\ntype Block = Record<string, unknown> & { _key: string; _type: string };\ntype SaveState = \"idle\" | \"saving\" | \"saved\" | \"error\";\n\ninterface VersionRow {\n id: string;\n createdAt?: unknown;\n kind?: string;\n label?: string;\n}\n\nfunction newKey() {\n return (globalThis.crypto?.randomUUID?.() ?? `b${Date.now()}${Math.random()}`).slice(0, 12);\n}\n\nfunction formatVersionDate(createdAt: unknown): string {\n const d =\n createdAt && typeof (createdAt as { toDate?: () => Date }).toDate === \"function\"\n ? (createdAt as { toDate: () => Date }).toDate()\n : typeof createdAt === \"string\"\n ? new Date(createdAt)\n : null;\n if (!d || Number.isNaN(d.getTime())) return \"\";\n return d.toLocaleString(\"nl-NL\", { dateStyle: \"medium\", timeStyle: \"short\" });\n}\n\nexport function PageEditor({ siteId, page }: { siteId: string; page: DraftPage }) {\n const registry = useRegistry();\n const { activeIds, defs } = useModules();\n const { toast } = useToast();\n const confirm = useConfirm();\n const [draft, setDraft] = useState<DraftPage>(page);\n const [busy, setBusy] = useState(false);\n const [saveState, setSaveState] = useState<SaveState>(\"idle\");\n const [versions, setVersions] = useState<VersionRow[]>([]);\n const firstRender = useRef(true);\n const slugTouched = useRef(Boolean(page.slug));\n\n // Wissel van pagina → herlaad lokale state.\n useEffect(() => {\n setDraft(page);\n firstRender.current = true;\n slugTouched.current = Boolean(page.slug);\n }, [page.id]); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => subscribeVersions(siteId, page.id, (v) => setVersions(v)), [siteId, page.id]);\n\n function persist(d: DraftPage) {\n setSaveState(\"saving\");\n return saveDraft(siteId, d.id, {\n title: d.title,\n slug: d.slug,\n path: d.path,\n seo: d.seo,\n blocks: d.blocks,\n order: d.order,\n })\n .then(() => setSaveState(\"saved\"))\n .catch(() => {\n setSaveState(\"error\");\n toast({ title: \"Opslaan mislukt\", description: \"Wijzigingen zijn nog niet opgeslagen.\", tone: \"error\" });\n });\n }\n\n // Autosave (debounced) bij wijzigingen.\n useEffect(() => {\n if (firstRender.current) {\n firstRender.current = false;\n return;\n }\n const t = setTimeout(() => void persist(draft), 800);\n return () => clearTimeout(t);\n }, [draft]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Waarschuw bij verlaten van de pagina met niet-opgeslagen/mislukte wijzigingen.\n useEffect(() => {\n const onBeforeUnload = (e: BeforeUnloadEvent) => {\n if (saveState === \"saving\" || saveState === \"error\") {\n e.preventDefault();\n e.returnValue = \"\";\n }\n };\n window.addEventListener(\"beforeunload\", onBeforeUnload);\n return () => window.removeEventListener(\"beforeunload\", onBeforeUnload);\n }, [saveState]);\n\n const blocks = (draft.blocks ?? []) as Block[];\n\n function update(next: DraftPage) {\n setDraft(next);\n }\n function setBlocks(next: Block[]) {\n update({ ...draft, blocks: next });\n }\n function setTitle(title: string) {\n // Auto-slug zolang de gebruiker de slug niet handmatig heeft aangeraakt.\n const slug = slugTouched.current ? draft.slug : slugify(title);\n update({ ...draft, title, slug });\n }\n function addBlock(type: string) {\n const def = registry.get(type);\n const base = def?.defaults?.() ?? {};\n setBlocks([...blocks, { _key: newKey(), _type: type, ...base } as Block]);\n }\n function addBlocks(newBlocks: Array<Record<string, unknown>>) {\n setBlocks([...blocks, ...(newBlocks as Block[])]);\n }\n function updateBlock(i: number, next: Record<string, unknown>) {\n const copy = [...blocks];\n copy[i] = { ...(next as Block), _key: blocks[i]._key, _type: blocks[i]._type };\n setBlocks(copy);\n }\n async function removeBlock(i: number) {\n const ok = await confirm({\n title: \"Blok verwijderen?\",\n description: \"Dit blok wordt uit de pagina verwijderd.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n setBlocks(blocks.filter((_, idx) => idx !== i));\n }\n function move(i: number, dir: -1 | 1) {\n const j = i + dir;\n if (j < 0 || j >= blocks.length) return;\n const copy = [...blocks];\n [copy[i], copy[j]] = [copy[j], copy[i]];\n setBlocks(copy);\n }\n\n async function onPublish() {\n const ok = await confirm({\n title: \"Pagina publiceren?\",\n description: `De gepubliceerde site wordt bijgewerkt (${draft.path}).`,\n confirmLabel: \"Publiceren\",\n });\n if (!ok) return;\n setBusy(true);\n try {\n await saveDraft(siteId, draft.id, { title: draft.title, slug: draft.slug, path: draft.path, seo: draft.seo, blocks: draft.blocks });\n await publishPage(siteId, draft.id);\n toast({ title: \"Gepubliceerd\", description: draft.path, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Publiceren mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function onRestore(versionId: string) {\n const ok = await confirm({\n title: \"Versie herstellen?\",\n description: \"De huidige werkversie wordt overschreven met deze versie. Publiceer daarna om het live te zetten.\",\n confirmLabel: \"Herstellen\",\n });\n if (!ok) return;\n try {\n const restored = await restoreVersion(siteId, draft.id, versionId);\n setDraft({ ...draft, ...restored });\n firstRender.current = true; // herstelde versie is al opgeslagen; geen extra autosave nodig\n toast({ title: \"Versie hersteld\", description: \"Controleer en publiceer om live te zetten.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Herstellen mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n const addOptions = useMemo(() => buildBlockAddOptions(registry.values()), [registry]);\n\n // Blocks waarvan de module is uitgeschakeld → tonen niet op de live site.\n const disabledBlocks = useMemo(() => {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const b of blocks) {\n if (registry.get(b._type)) continue; // actief block-type → ok\n const modId = moduleIdForBlockType(defs, b._type);\n if (modId && !activeIds.has(modId) && !seen.has(b._type)) {\n seen.add(b._type);\n out.push(b._type);\n }\n }\n return out;\n }, [blocks, registry, defs, activeIds]);\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <div className=\"mb-3 flex flex-wrap items-center justify-between gap-2\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"text-base font-semibold\">Pagina-instellingen</h3>\n {draft.published ? <Badge tone=\"green\">gepubliceerd</Badge> : <Badge tone=\"amber\">concept</Badge>}\n </div>\n <div className=\"flex items-center gap-3\">\n <span className=\"text-xs text-slate-500\" aria-live=\"polite\">\n {saveState === \"saving\" ? \"Opslaan…\" : saveState === \"saved\" ? \"✓ Opgeslagen\" : \"\"}\n </span>\n {saveState === \"error\" && (\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void persist(draft)}>\n Opnieuw opslaan\n </Button>\n )}\n <Button onClick={onPublish} disabled={busy}>\n {busy ? \"Bezig…\" : \"Publiceren\"}\n </Button>\n </div>\n </div>\n {saveState === \"error\" && (\n <div className=\"mb-3 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700\">\n Niet alle wijzigingen zijn opgeslagen. Controleer je verbinding en klik “Opnieuw opslaan”.\n </div>\n )}\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Titel\">\n <Input value={draft.title} onChange={(e) => setTitle(e.target.value)} />\n </Field>\n <Field label=\"Pad\" hint=\"bv. / of /over-ons\">\n <Input value={draft.path} onChange={(e) => update({ ...draft, path: e.target.value })} />\n </Field>\n <Field label=\"SEO-titel\">\n <Input value={draft.seo?.title ?? \"\"} onChange={(e) => update({ ...draft, seo: { ...draft.seo, title: e.target.value } })} />\n </Field>\n <Field label=\"SEO-omschrijving\">\n <Textarea rows={2} value={draft.seo?.description ?? \"\"} onChange={(e) => update({ ...draft, seo: { ...draft.seo, description: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n {disabledBlocks.length > 0 && (\n <div className=\"rounded-lg border border-amber-300 bg-amber-50 p-3 text-sm text-amber-800\">\n Deze pagina bevat blokken van uitgeschakelde modules ({disabledBlocks.join(\", \")}). Ze worden\n <strong> niet</strong> op de live site getoond. Zet de module aan onder Instellingen → Modules,\n of verwijder de blokken.\n </div>\n )}\n\n <div className=\"grid gap-3\">\n {blocks.map((b, i) => {\n const def = registry.get(b._type);\n const Editor: ComponentType<BlockEditorProps> =\n (def?.Editor as ComponentType<BlockEditorProps> | undefined) ?? builtinBlockEditors[b._type] ?? GenericEditor;\n return (\n <Card key={b._key}>\n <div className=\"mb-3 flex items-center justify-between\">\n <span className=\"text-sm font-semibold\">{def?.label ?? b._type}</span>\n <div className=\"flex items-center gap-1\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => move(i, -1)} aria-label=\"Omhoog\">↑</Button>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => move(i, 1)} aria-label=\"Omlaag\">↓</Button>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => void removeBlock(i)} aria-label=\"Verwijderen\">🗑</Button>\n </div>\n </div>\n <Editor block={b} siteId={siteId} onChange={(next) => updateBlock(i, next)} />\n <div className=\"mt-3\">\n <StyleEditor\n value={(b as Block & { _style?: BlockStyle })._style}\n onChange={(style) => updateBlock(i, { ...b, _style: style })}\n />\n </div>\n </Card>\n );\n })}\n {blocks.length === 0 && <p className=\"text-sm text-slate-500\">Nog geen blokken. Voeg er hieronder een toe.</p>}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-slate-600\">Toevoegen:</span>\n <div className=\"w-56\">\n <Select value=\"\" placeholder=\"Kies een blok…\" onValueChange={(v) => v && addBlock(v)} options={addOptions} />\n </div>\n <PatternInserter onInsert={addBlocks} />\n </div>\n\n {versions.length > 0 && (\n <Card>\n <h4 className=\"mb-2 text-sm font-semibold\">Versiegeschiedenis</h4>\n <ul className=\"grid gap-1 text-sm text-slate-600\">\n {versions.slice(0, 10).map((v) => (\n <li key={v.id} className=\"flex items-center justify-between border-b border-slate-100 py-1 last:border-0\">\n <span>\n {v.label ?? v.kind ?? \"snapshot\"}\n {formatVersionDate(v.createdAt) && <span className=\"text-slate-400\"> · {formatVersionDate(v.createdAt)}</span>}\n </span>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => void onRestore(v.id)}>\n Herstellen\n </Button>\n </li>\n ))}\n </ul>\n </Card>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/pages/PageEditor.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState, type ComponentType } from \"react\";\nimport { useRegistry } from \"../../blocks/context\";\nimport { buildBlockAddOptions } from \"../../blocks/registry\";\nimport { useModules } from \"../../modules/context\";\nimport { moduleIdForBlockType } from \"../../modules/registry\";\nimport { Button, Card, Field, Input, Textarea, Badge } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { builtinBlockEditors, GenericEditor, type BlockEditorProps } from \"../blockEditors\";\nimport { StyleEditor } from \"../blockEditors/StyleEditor\";\nimport { PatternInserter } from \"../patterns/PatternInserter\";\nimport type { BlockStyle } from \"../../schema/blocks\";\nimport { saveDraft, publishPage, subscribeVersions, restoreVersion, type DraftPage } from \"../lib/pagesClient\";\nimport { slugify } from \"../lib/slugify\";\n\ntype Block = Record<string, unknown> & { _key: string; _type: string };\ntype SaveState = \"idle\" | \"saving\" | \"saved\" | \"error\";\n\ninterface VersionRow {\n id: string;\n createdAt?: unknown;\n kind?: string;\n label?: string;\n}\n\nfunction newKey() {\n return (globalThis.crypto?.randomUUID?.() ?? `b${Date.now()}${Math.random()}`).slice(0, 12);\n}\n\nfunction formatVersionDate(createdAt: unknown): string {\n const d =\n createdAt && typeof (createdAt as { toDate?: () => Date }).toDate === \"function\"\n ? (createdAt as { toDate: () => Date }).toDate()\n : typeof createdAt === \"string\"\n ? new Date(createdAt)\n : null;\n if (!d || Number.isNaN(d.getTime())) return \"\";\n return d.toLocaleString(\"nl-NL\", { dateStyle: \"medium\", timeStyle: \"short\" });\n}\n\nexport function PageEditor({ siteId, page, allPages }: { siteId: string; page: DraftPage; allPages?: DraftPage[] }) {\n const registry = useRegistry();\n const { activeIds, defs } = useModules();\n const { toast } = useToast();\n const confirm = useConfirm();\n const [draft, setDraft] = useState<DraftPage>(page);\n const [busy, setBusy] = useState(false);\n const [saveState, setSaveState] = useState<SaveState>(\"idle\");\n const [versions, setVersions] = useState<VersionRow[]>([]);\n const firstRender = useRef(true);\n const slugTouched = useRef(Boolean(page.slug));\n\n // Wissel van pagina → herlaad lokale state.\n useEffect(() => {\n setDraft(page);\n firstRender.current = true;\n slugTouched.current = Boolean(page.slug);\n }, [page.id]); // eslint-disable-line react-hooks/exhaustive-deps\n\n useEffect(() => subscribeVersions(siteId, page.id, (v) => setVersions(v)), [siteId, page.id]);\n\n function persist(d: DraftPage) {\n setSaveState(\"saving\");\n return saveDraft(siteId, d.id, {\n title: d.title,\n slug: d.slug,\n path: d.path,\n seo: d.seo,\n blocks: d.blocks,\n order: d.order,\n })\n .then(() => setSaveState(\"saved\"))\n .catch(() => {\n setSaveState(\"error\");\n toast({ title: \"Opslaan mislukt\", description: \"Wijzigingen zijn nog niet opgeslagen.\", tone: \"error\" });\n });\n }\n\n // Autosave (debounced) bij wijzigingen.\n useEffect(() => {\n if (firstRender.current) {\n firstRender.current = false;\n return;\n }\n const t = setTimeout(() => void persist(draft), 800);\n return () => clearTimeout(t);\n }, [draft]); // eslint-disable-line react-hooks/exhaustive-deps\n\n // Waarschuw bij verlaten van de pagina met niet-opgeslagen/mislukte wijzigingen.\n useEffect(() => {\n const onBeforeUnload = (e: BeforeUnloadEvent) => {\n if (saveState === \"saving\" || saveState === \"error\") {\n e.preventDefault();\n e.returnValue = \"\";\n }\n };\n window.addEventListener(\"beforeunload\", onBeforeUnload);\n return () => window.removeEventListener(\"beforeunload\", onBeforeUnload);\n }, [saveState]);\n\n const blocks = (draft.blocks ?? []) as Block[];\n\n function update(next: DraftPage) {\n setDraft(next);\n }\n function setBlocks(next: Block[]) {\n update({ ...draft, blocks: next });\n }\n function setTitle(title: string) {\n // Auto-slug zolang de gebruiker de slug niet handmatig heeft aangeraakt.\n const slug = slugTouched.current ? draft.slug : slugify(title);\n update({ ...draft, title, slug });\n }\n function addBlock(type: string) {\n const def = registry.get(type);\n const base = def?.defaults?.() ?? {};\n setBlocks([...blocks, { _key: newKey(), _type: type, ...base } as Block]);\n }\n function addBlocks(newBlocks: Array<Record<string, unknown>>) {\n setBlocks([...blocks, ...(newBlocks as Block[])]);\n }\n function updateBlock(i: number, next: Record<string, unknown>) {\n const copy = [...blocks];\n copy[i] = { ...(next as Block), _key: blocks[i]._key, _type: blocks[i]._type };\n setBlocks(copy);\n }\n async function removeBlock(i: number) {\n const ok = await confirm({\n title: \"Blok verwijderen?\",\n description: \"Dit blok wordt uit de pagina verwijderd.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n setBlocks(blocks.filter((_, idx) => idx !== i));\n }\n function move(i: number, dir: -1 | 1) {\n const j = i + dir;\n if (j < 0 || j >= blocks.length) return;\n const copy = [...blocks];\n [copy[i], copy[j]] = [copy[j], copy[i]];\n setBlocks(copy);\n }\n\n async function onPublish() {\n const ok = await confirm({\n title: \"Pagina publiceren?\",\n description: `De gepubliceerde site wordt bijgewerkt (${draft.path}).`,\n confirmLabel: \"Publiceren\",\n });\n if (!ok) return;\n setBusy(true);\n try {\n await saveDraft(siteId, draft.id, { title: draft.title, slug: draft.slug, path: draft.path, seo: draft.seo, blocks: draft.blocks });\n await publishPage(siteId, draft.id);\n toast({ title: \"Gepubliceerd\", description: draft.path, tone: \"success\" });\n } catch (e) {\n toast({ title: \"Publiceren mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setBusy(false);\n }\n }\n\n async function onRestore(versionId: string) {\n const ok = await confirm({\n title: \"Versie herstellen?\",\n description: \"De huidige werkversie wordt overschreven met deze versie. Publiceer daarna om het live te zetten.\",\n confirmLabel: \"Herstellen\",\n });\n if (!ok) return;\n try {\n const restored = await restoreVersion(siteId, draft.id, versionId);\n setDraft({ ...draft, ...restored });\n firstRender.current = true; // herstelde versie is al opgeslagen; geen extra autosave nodig\n toast({ title: \"Versie hersteld\", description: \"Controleer en publiceer om live te zetten.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Herstellen mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n const addOptions = useMemo(() => buildBlockAddOptions(registry.values()), [registry]);\n\n // Blocks waarvan de module is uitgeschakeld → tonen niet op de live site.\n const disabledBlocks = useMemo(() => {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const b of blocks) {\n if (registry.get(b._type)) continue; // actief block-type → ok\n const modId = moduleIdForBlockType(defs, b._type);\n if (modId && !activeIds.has(modId) && !seen.has(b._type)) {\n seen.add(b._type);\n out.push(b._type);\n }\n }\n return out;\n }, [blocks, registry, defs, activeIds]);\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <div className=\"mb-3 flex flex-wrap items-center justify-between gap-2\">\n <div className=\"flex items-center gap-2\">\n <h3 className=\"text-base font-semibold\">Pagina-instellingen</h3>\n {draft.published ? <Badge tone=\"green\">gepubliceerd</Badge> : <Badge tone=\"amber\">concept</Badge>}\n </div>\n <div className=\"flex items-center gap-3\">\n <span className=\"text-xs text-slate-500\" aria-live=\"polite\">\n {saveState === \"saving\" ? \"Opslaan…\" : saveState === \"saved\" ? \"✓ Opgeslagen\" : \"\"}\n </span>\n {saveState === \"error\" && (\n <Button variant=\"secondary\" size=\"sm\" onClick={() => void persist(draft)}>\n Opnieuw opslaan\n </Button>\n )}\n <Button onClick={onPublish} disabled={busy}>\n {busy ? \"Bezig…\" : \"Publiceren\"}\n </Button>\n </div>\n </div>\n {saveState === \"error\" && (\n <div className=\"mb-3 rounded-lg border border-red-200 bg-red-50 p-3 text-sm text-red-700\">\n Niet alle wijzigingen zijn opgeslagen. Controleer je verbinding en klik “Opnieuw opslaan”.\n </div>\n )}\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Titel\">\n <Input value={draft.title} onChange={(e) => setTitle(e.target.value)} />\n </Field>\n <Field label=\"Pad\" hint=\"bv. / of /over-ons\">\n {/* Vrije tekst brak routing stil (spaties, ontbrekende /, duplicaat-pad → één van de twee\n pagina's onbereikbaar). Normaliseren op blur + waarschuwen bij een botsing. */}\n <Input\n value={draft.path}\n onChange={(e) => update({ ...draft, path: e.target.value })}\n onBlur={() => {\n const parts = draft.path.split(\"/\").map((s) => slugify(s)).filter(Boolean);\n const normalized = \"/\" + parts.join(\"/\");\n if (normalized !== draft.path) update({ ...draft, path: normalized });\n }}\n />\n {allPages?.some((p) => p.id !== draft.id && p.path === draft.path) && (\n <p className=\"mt-1 text-xs text-red-600\" role=\"alert\">\n ⚠ Er bestaat al een pagina met dit pad — één van beide wordt dan onbereikbaar.\n </p>\n )}\n </Field>\n <Field label=\"SEO-titel\">\n <Input value={draft.seo?.title ?? \"\"} onChange={(e) => update({ ...draft, seo: { ...draft.seo, title: e.target.value } })} />\n </Field>\n <Field label=\"SEO-omschrijving\">\n <Textarea rows={2} value={draft.seo?.description ?? \"\"} onChange={(e) => update({ ...draft, seo: { ...draft.seo, description: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n {disabledBlocks.length > 0 && (\n <div className=\"rounded-lg border border-amber-300 bg-amber-50 p-3 text-sm text-amber-800\">\n Deze pagina bevat blokken van uitgeschakelde modules ({disabledBlocks.join(\", \")}). Ze worden\n <strong> niet</strong> op de live site getoond. Zet de module aan onder Instellingen → Modules,\n of verwijder de blokken.\n </div>\n )}\n\n <div className=\"grid gap-3\">\n {blocks.map((b, i) => {\n const def = registry.get(b._type);\n const Editor: ComponentType<BlockEditorProps> =\n (def?.Editor as ComponentType<BlockEditorProps> | undefined) ?? builtinBlockEditors[b._type] ?? GenericEditor;\n return (\n <Card key={b._key}>\n <div className=\"mb-3 flex items-center justify-between\">\n <span className=\"text-sm font-semibold\">{def?.label ?? b._type}</span>\n <div className=\"flex items-center gap-1\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => move(i, -1)} aria-label=\"Omhoog\">↑</Button>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => move(i, 1)} aria-label=\"Omlaag\">↓</Button>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => void removeBlock(i)} aria-label=\"Verwijderen\">🗑</Button>\n </div>\n </div>\n <Editor block={b} siteId={siteId} onChange={(next) => updateBlock(i, next)} />\n <div className=\"mt-3\">\n <StyleEditor\n value={(b as Block & { _style?: BlockStyle })._style}\n onChange={(style) => updateBlock(i, { ...b, _style: style })}\n />\n </div>\n </Card>\n );\n })}\n {blocks.length === 0 && <p className=\"text-sm text-slate-500\">Nog geen blokken. Voeg er hieronder een toe.</p>}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-slate-600\">Toevoegen:</span>\n <div className=\"w-56\">\n <Select value=\"\" placeholder=\"Kies een blok…\" onValueChange={(v) => v && addBlock(v)} options={addOptions} />\n </div>\n <PatternInserter onInsert={addBlocks} />\n </div>\n\n {versions.length > 0 && (\n <Card>\n <h4 className=\"mb-2 text-sm font-semibold\">Versiegeschiedenis</h4>\n <ul className=\"grid gap-1 text-sm text-slate-600\">\n {versions.slice(0, 10).map((v) => (\n <li key={v.id} className=\"flex items-center justify-between border-b border-slate-100 py-1 last:border-0\">\n <span>\n {v.label ?? v.kind ?? \"snapshot\"}\n {formatVersionDate(v.createdAt) && <span className=\"text-slate-400\"> · {formatVersionDate(v.createdAt)}</span>}\n </span>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => void onRestore(v.id)}>\n Herstellen\n </Button>\n </li>\n ))}\n </ul>\n </Card>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -26,10 +26,14 @@ function PagesPanel({ siteId }) {
26
26
  if (!title) return;
27
27
  const slug = slugify(title) || "pagina";
28
28
  const path = slug === "home" ? "/" : `/${slug}`;
29
- const id = await createPage(siteId, { title, slug, path });
30
- setNewTitle("");
31
- setSelected(id);
32
- toast({ title: "Pagina aangemaakt", description: path });
29
+ try {
30
+ const id = await createPage(siteId, { title, slug, path });
31
+ setNewTitle("");
32
+ setSelected(id);
33
+ toast({ title: "Pagina aangemaakt", description: path });
34
+ } catch (e) {
35
+ toast({ title: "Pagina aanmaken mislukt", description: e.message, tone: "error" });
36
+ }
33
37
  }
34
38
  return /* @__PURE__ */ jsxs("div", { className: "grid gap-4 md:grid-cols-[260px_1fr]", children: [
35
39
  /* @__PURE__ */ jsxs(Card, { className: "h-fit", children: [
@@ -82,7 +86,7 @@ function PagesPanel({ siteId }) {
82
86
  children: "Verwijderen"
83
87
  }
84
88
  ) }),
85
- /* @__PURE__ */ jsx(PageEditor, { siteId, page: current })
89
+ /* @__PURE__ */ jsx(PageEditor, { siteId, page: current, allPages: pages })
86
90
  ] }) : /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx("p", { className: "text-sm text-slate-500", children: "Selecteer of maak een pagina om te bewerken." }) }) })
87
91
  ] });
88
92
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/pages/PagesPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Button, Card, Field, Input, Badge } from \"../ui/primitives\";\nimport { Dialog, DialogTrigger, DialogContent, DialogClose } from \"../ui/dialog\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { subscribePages, createPage, deletePage, type DraftPage } from \"../lib/pagesClient\";\nimport { PageEditor } from \"./PageEditor\";\n\nfunction slugify(s: string): string {\n return s\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/(^-|-$)/g, \"\");\n}\n\nexport function PagesPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [pages, setPages] = useState<DraftPage[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [newTitle, setNewTitle] = useState(\"\");\n\n useEffect(() => subscribePages(siteId, setPages), [siteId]);\n useEffect(() => {\n if (!selected && pages.length) setSelected(pages[0].id);\n }, [pages, selected]);\n\n const current = pages.find((p) => p.id === selected) ?? null;\n\n async function create() {\n const title = newTitle.trim();\n if (!title) return;\n const slug = slugify(title) || \"pagina\";\n const path = slug === \"home\" ? \"/\" : `/${slug}`;\n const id = await createPage(siteId, { title, slug, path });\n setNewTitle(\"\");\n setSelected(id);\n toast({ title: \"Pagina aangemaakt\", description: path });\n }\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[260px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Pagina's</h3>\n <Dialog>\n <DialogTrigger asChild>\n <Button size=\"sm\">+ Nieuw</Button>\n </DialogTrigger>\n <DialogContent title=\"Nieuwe pagina\">\n <div className=\"grid gap-3\">\n <Field label=\"Titel\">\n <Input value={newTitle} onChange={(e) => setNewTitle(e.target.value)} placeholder=\"bv. Over ons\" />\n </Field>\n <div className=\"flex justify-end gap-2\">\n <DialogClose asChild>\n <Button variant=\"secondary\">Annuleren</Button>\n </DialogClose>\n <DialogClose asChild>\n <Button onClick={create}>Aanmaken</Button>\n </DialogClose>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n </div>\n <ul className=\"grid gap-1\">\n {pages.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n <span className=\"truncate\">{p.title}</span>\n {p.published ? <Badge tone=\"green\">live</Badge> : <Badge tone=\"amber\">concept</Badge>}\n </button>\n </li>\n ))}\n {pages.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Nog geen pagina's.</li>}\n </ul>\n </Card>\n\n <div>\n {current ? (\n <>\n <div className=\"mb-3 flex justify-end\">\n <Button\n variant=\"danger\"\n size=\"sm\"\n onClick={async () => {\n const ok = await confirm({\n title: `Pagina \"${current.title}\" verwijderen?`,\n description:\n \"De pagina en de bijbehorende werkversie worden verwijderd. Als de pagina gepubliceerd is, verdwijnt hij ook van de live site. Dit kan niet ongedaan worden gemaakt.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n await deletePage(siteId, current.id);\n setSelected(null);\n toast({ title: \"Pagina verwijderd\" });\n }}\n >\n Verwijderen\n </Button>\n </div>\n <PageEditor siteId={siteId} page={current} />\n </>\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een pagina om te bewerken.</p>\n </Card>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/pages/PagesPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Button, Card, Field, Input, Badge } from \"../ui/primitives\";\nimport { Dialog, DialogTrigger, DialogContent, DialogClose } from \"../ui/dialog\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { subscribePages, createPage, deletePage, type DraftPage } from \"../lib/pagesClient\";\nimport { PageEditor } from \"./PageEditor\";\n\nfunction slugify(s: string): string {\n return s\n .toLowerCase()\n .normalize(\"NFD\")\n .replace(/[̀-ͯ]/g, \"\")\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/(^-|-$)/g, \"\");\n}\n\nexport function PagesPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const [pages, setPages] = useState<DraftPage[]>([]);\n const [selected, setSelected] = useState<string | null>(null);\n const [newTitle, setNewTitle] = useState(\"\");\n\n useEffect(() => subscribePages(siteId, setPages), [siteId]);\n useEffect(() => {\n if (!selected && pages.length) setSelected(pages[0].id);\n }, [pages, selected]);\n\n const current = pages.find((p) => p.id === selected) ?? null;\n\n async function create() {\n const title = newTitle.trim();\n if (!title) return;\n const slug = slugify(title) || \"pagina\";\n const path = slug === \"home\" ? \"/\" : `/${slug}`;\n try {\n const id = await createPage(siteId, { title, slug, path });\n setNewTitle(\"\");\n setSelected(id);\n toast({ title: \"Pagina aangemaakt\", description: path });\n } catch (e) {\n // De dialoog sluit sowieso (DialogClose) — zonder deze toast dacht de klant dat de pagina\n // bestond terwijl de write (rules/offline) faalde.\n toast({ title: \"Pagina aanmaken mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n return (\n <div className=\"grid gap-4 md:grid-cols-[260px_1fr]\">\n <Card className=\"h-fit\">\n <div className=\"mb-3 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold\">Pagina's</h3>\n <Dialog>\n <DialogTrigger asChild>\n <Button size=\"sm\">+ Nieuw</Button>\n </DialogTrigger>\n <DialogContent title=\"Nieuwe pagina\">\n <div className=\"grid gap-3\">\n <Field label=\"Titel\">\n <Input value={newTitle} onChange={(e) => setNewTitle(e.target.value)} placeholder=\"bv. Over ons\" />\n </Field>\n <div className=\"flex justify-end gap-2\">\n <DialogClose asChild>\n <Button variant=\"secondary\">Annuleren</Button>\n </DialogClose>\n <DialogClose asChild>\n <Button onClick={create}>Aanmaken</Button>\n </DialogClose>\n </div>\n </div>\n </DialogContent>\n </Dialog>\n </div>\n <ul className=\"grid gap-1\">\n {pages.map((p) => (\n <li key={p.id}>\n <button\n onClick={() => setSelected(p.id)}\n className={`flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm ${\n p.id === selected ? \"bg-brand-50 text-brand-700\" : \"hover:bg-slate-100\"\n }`}\n >\n <span className=\"truncate\">{p.title}</span>\n {p.published ? <Badge tone=\"green\">live</Badge> : <Badge tone=\"amber\">concept</Badge>}\n </button>\n </li>\n ))}\n {pages.length === 0 && <li className=\"px-3 py-2 text-sm text-slate-500\">Nog geen pagina's.</li>}\n </ul>\n </Card>\n\n <div>\n {current ? (\n <>\n <div className=\"mb-3 flex justify-end\">\n <Button\n variant=\"danger\"\n size=\"sm\"\n onClick={async () => {\n const ok = await confirm({\n title: `Pagina \"${current.title}\" verwijderen?`,\n description:\n \"De pagina en de bijbehorende werkversie worden verwijderd. Als de pagina gepubliceerd is, verdwijnt hij ook van de live site. Dit kan niet ongedaan worden gemaakt.\",\n confirmLabel: \"Verwijderen\",\n tone: \"danger\",\n });\n if (!ok) return;\n await deletePage(siteId, current.id);\n setSelected(null);\n toast({ title: \"Pagina verwijderd\" });\n }}\n >\n Verwijderen\n </Button>\n </div>\n <PageEditor siteId={siteId} page={current} allPages={pages} />\n </>\n ) : (\n <Card>\n <p className=\"text-sm text-slate-500\">Selecteer of maak een pagina om te bewerken.</p>\n </Card>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -7,6 +7,7 @@ import { StatsPanel } from "./stats/StatsPanel";
7
7
  import { SettingsPanel } from "./settings/SettingsPanel";
8
8
  import { NewsEditor } from "./NewsEditor";
9
9
  import { CustomBlocksPanel } from "./customBlocks/CustomBlocksPanel";
10
+ import { ShopPanel } from "./shop/ShopPanel";
10
11
  function NewsPanel(_) {
11
12
  return /* @__PURE__ */ jsx(NewsEditor, {});
12
13
  }
@@ -16,6 +17,8 @@ const builtinPanels = [
16
17
  { moduleId: "media", label: "Media", order: 30, minRole: "editor", Panel: MediaManager },
17
18
  { moduleId: "forms", label: "Formulieren", order: 40, minRole: "editor", Panel: FormsPanel },
18
19
  { moduleId: "stats", label: "Statistieken", order: 50, minRole: "editor", Panel: StatsPanel },
20
+ // Webwinkel (5.5): alleen zichtbaar als de shop-module aanstaat.
21
+ { moduleId: "shop", label: "Webwinkel", order: 45, minRole: "editor", Panel: ShopPanel },
19
22
  // Bouwstenen (5.0): custom blocks zonder deploy — hangt aan de altijd-actieve content-module.
20
23
  { moduleId: "content", label: "Bouwstenen", order: 60, minRole: "siteadmin", Panel: CustomBlocksPanel },
21
24
  { moduleId: "settings", label: "Instellingen", order: 100, minRole: "siteadmin", Panel: SettingsPanel }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/admin/panelRegistry.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Paneel-registry — koppelt een beheer-paneel aan een module-id. `AdminApp` rendert de tabs\n * hieruit (gesorteerd op `order`, gated op rol én actieve-module-set) i.p.v. hardgecodeerd. Een\n * nieuwe module met een eigen paneel hoeft `AdminApp` dus niet meer aan te raken.\n */\nimport type { ComponentType } from \"react\";\nimport { PagesPanel } from \"./pages/PagesPanel\";\nimport { MediaManager } from \"./media/MediaManager\";\nimport { FormsPanel } from \"./forms/FormsPanel\";\nimport { StatsPanel } from \"./stats/StatsPanel\";\nimport { SettingsPanel } from \"./settings/SettingsPanel\";\nimport { NewsEditor } from \"./NewsEditor\";\nimport { CustomBlocksPanel } from \"./customBlocks/CustomBlocksPanel\";\n\nexport type PanelMinRole = \"editor\" | \"siteadmin\";\n\nexport interface PanelDefinition {\n /** Moet overeenkomen met de module-`id`; bepaalt of het paneel actief is. */\n moduleId: string;\n label: string;\n /** Sorteervolgorde (laag = links). Instellingen staat vast achteraan (100). */\n order: number;\n minRole: PanelMinRole;\n Panel: ComponentType<{ siteId: string }>;\n}\n\n/** Adapter: de NewsEditor leest zelf siteId via context; wrap voor een uniforme prop-signatuur. */\nfunction NewsPanel(_: { siteId: string }) {\n return <NewsEditor />;\n}\n\n/** De panelen van de ingebouwde modules. */\nexport const builtinPanels: PanelDefinition[] = [\n { moduleId: \"pages\", label: \"Pagina's\", order: 10, minRole: \"editor\", Panel: PagesPanel },\n { moduleId: \"news\", label: \"Nieuws\", order: 20, minRole: \"editor\", Panel: NewsPanel },\n { moduleId: \"media\", label: \"Media\", order: 30, minRole: \"editor\", Panel: MediaManager },\n { moduleId: \"forms\", label: \"Formulieren\", order: 40, minRole: \"editor\", Panel: FormsPanel },\n { moduleId: \"stats\", label: \"Statistieken\", order: 50, minRole: \"editor\", Panel: StatsPanel },\n // Bouwstenen (5.0): custom blocks zonder deploy — hangt aan de altijd-actieve content-module.\n { moduleId: \"content\", label: \"Bouwstenen\", order: 60, minRole: \"siteadmin\", Panel: CustomBlocksPanel },\n { moduleId: \"settings\", label: \"Instellingen\", order: 100, minRole: \"siteadmin\", Panel: SettingsPanel },\n];\n"],"mappings":";AA8BS;AAtBT,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAelC,SAAS,UAAU,GAAuB;AACxC,SAAO,oBAAC,cAAW;AACrB;AAGO,MAAM,gBAAmC;AAAA,EAC9C,EAAE,UAAU,SAAS,OAAO,YAAY,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA,EACxF,EAAE,UAAU,QAAQ,OAAO,UAAU,OAAO,IAAI,SAAS,UAAU,OAAO,UAAU;AAAA,EACpF,EAAE,UAAU,SAAS,OAAO,SAAS,OAAO,IAAI,SAAS,UAAU,OAAO,aAAa;AAAA,EACvF,EAAE,UAAU,SAAS,OAAO,eAAe,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA,EAC3F,EAAE,UAAU,SAAS,OAAO,gBAAgB,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA;AAAA,EAE5F,EAAE,UAAU,WAAW,OAAO,cAAc,OAAO,IAAI,SAAS,aAAa,OAAO,kBAAkB;AAAA,EACtG,EAAE,UAAU,YAAY,OAAO,gBAAgB,OAAO,KAAK,SAAS,aAAa,OAAO,cAAc;AACxG;","names":[]}
1
+ {"version":3,"sources":["../../src/admin/panelRegistry.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * Paneel-registry — koppelt een beheer-paneel aan een module-id. `AdminApp` rendert de tabs\n * hieruit (gesorteerd op `order`, gated op rol én actieve-module-set) i.p.v. hardgecodeerd. Een\n * nieuwe module met een eigen paneel hoeft `AdminApp` dus niet meer aan te raken.\n */\nimport type { ComponentType } from \"react\";\nimport { PagesPanel } from \"./pages/PagesPanel\";\nimport { MediaManager } from \"./media/MediaManager\";\nimport { FormsPanel } from \"./forms/FormsPanel\";\nimport { StatsPanel } from \"./stats/StatsPanel\";\nimport { SettingsPanel } from \"./settings/SettingsPanel\";\nimport { NewsEditor } from \"./NewsEditor\";\nimport { CustomBlocksPanel } from \"./customBlocks/CustomBlocksPanel\";\nimport { ShopPanel } from \"./shop/ShopPanel\";\n\nexport type PanelMinRole = \"editor\" | \"siteadmin\";\n\nexport interface PanelDefinition {\n /** Moet overeenkomen met de module-`id`; bepaalt of het paneel actief is. */\n moduleId: string;\n label: string;\n /** Sorteervolgorde (laag = links). Instellingen staat vast achteraan (100). */\n order: number;\n minRole: PanelMinRole;\n Panel: ComponentType<{ siteId: string }>;\n}\n\n/** Adapter: de NewsEditor leest zelf siteId via context; wrap voor een uniforme prop-signatuur. */\nfunction NewsPanel(_: { siteId: string }) {\n return <NewsEditor />;\n}\n\n/** De panelen van de ingebouwde modules. */\nexport const builtinPanels: PanelDefinition[] = [\n { moduleId: \"pages\", label: \"Pagina's\", order: 10, minRole: \"editor\", Panel: PagesPanel },\n { moduleId: \"news\", label: \"Nieuws\", order: 20, minRole: \"editor\", Panel: NewsPanel },\n { moduleId: \"media\", label: \"Media\", order: 30, minRole: \"editor\", Panel: MediaManager },\n { moduleId: \"forms\", label: \"Formulieren\", order: 40, minRole: \"editor\", Panel: FormsPanel },\n { moduleId: \"stats\", label: \"Statistieken\", order: 50, minRole: \"editor\", Panel: StatsPanel },\n // Webwinkel (5.5): alleen zichtbaar als de shop-module aanstaat.\n { moduleId: \"shop\", label: \"Webwinkel\", order: 45, minRole: \"editor\", Panel: ShopPanel },\n // Bouwstenen (5.0): custom blocks zonder deploy — hangt aan de altijd-actieve content-module.\n { moduleId: \"content\", label: \"Bouwstenen\", order: 60, minRole: \"siteadmin\", Panel: CustomBlocksPanel },\n { moduleId: \"settings\", label: \"Instellingen\", order: 100, minRole: \"siteadmin\", Panel: SettingsPanel },\n];\n"],"mappings":";AA+BS;AAvBT,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B,SAAS,kBAAkB;AAC3B,SAAS,kBAAkB;AAC3B,SAAS,qBAAqB;AAC9B,SAAS,kBAAkB;AAC3B,SAAS,yBAAyB;AAClC,SAAS,iBAAiB;AAe1B,SAAS,UAAU,GAAuB;AACxC,SAAO,oBAAC,cAAW;AACrB;AAGO,MAAM,gBAAmC;AAAA,EAC9C,EAAE,UAAU,SAAS,OAAO,YAAY,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA,EACxF,EAAE,UAAU,QAAQ,OAAO,UAAU,OAAO,IAAI,SAAS,UAAU,OAAO,UAAU;AAAA,EACpF,EAAE,UAAU,SAAS,OAAO,SAAS,OAAO,IAAI,SAAS,UAAU,OAAO,aAAa;AAAA,EACvF,EAAE,UAAU,SAAS,OAAO,eAAe,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA,EAC3F,EAAE,UAAU,SAAS,OAAO,gBAAgB,OAAO,IAAI,SAAS,UAAU,OAAO,WAAW;AAAA;AAAA,EAE5F,EAAE,UAAU,QAAQ,OAAO,aAAa,OAAO,IAAI,SAAS,UAAU,OAAO,UAAU;AAAA;AAAA,EAEvF,EAAE,UAAU,WAAW,OAAO,cAAc,OAAO,IAAI,SAAS,aAAa,OAAO,kBAAkB;AAAA,EACtG,EAAE,UAAU,YAAY,OAAO,gBAAgB,OAAO,KAAK,SAAS,aAAa,OAAO,cAAc;AACxG;","names":[]}
@@ -0,0 +1,3 @@
1
+ export declare function CategoriesTab({ siteId }: {
2
+ siteId: string;
3
+ }): import("react").JSX.Element;