@xiwebdesign/xicms-core 4.17.1 → 4.18.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 (81) hide show
  1. package/dist/XiCMSProvider.js +3 -1
  2. package/dist/XiCMSProvider.js.map +1 -1
  3. package/dist/admin/blockEditors/index.js +47 -14
  4. package/dist/admin/blockEditors/index.js.map +1 -1
  5. package/dist/admin/lib/compressImage.d.ts +4 -2
  6. package/dist/admin/lib/compressImage.js +7 -5
  7. package/dist/admin/lib/compressImage.js.map +1 -1
  8. package/dist/admin/lib/mediaUsage.d.ts +4 -3
  9. package/dist/admin/lib/mediaUsage.js +21 -10
  10. package/dist/admin/lib/mediaUsage.js.map +1 -1
  11. package/dist/admin/lib/upload.d.ts +4 -0
  12. package/dist/admin/lib/upload.js +26 -6
  13. package/dist/admin/lib/upload.js.map +1 -1
  14. package/dist/admin/media/MediaManager.js +23 -7
  15. package/dist/admin/media/MediaManager.js.map +1 -1
  16. package/dist/admin/media/MediaPicker.js +12 -2
  17. package/dist/admin/media/MediaPicker.js.map +1 -1
  18. package/dist/admin/settings/SettingsPanel.js +4 -1
  19. package/dist/admin/settings/SettingsPanel.js.map +1 -1
  20. package/dist/admin/visual/VisualEditor.js +4 -1
  21. package/dist/admin/visual/VisualEditor.js.map +1 -1
  22. package/dist/blocks/builtins/Agenda.d.ts +6 -0
  23. package/dist/blocks/builtins/Agenda.js +14 -6
  24. package/dist/blocks/builtins/Agenda.js.map +1 -1
  25. package/dist/blocks/builtins/Breadcrumbs.js +5 -1
  26. package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
  27. package/dist/blocks/builtins/Cards.js +3 -2
  28. package/dist/blocks/builtins/Cards.js.map +1 -1
  29. package/dist/blocks/builtins/Contact.js +2 -1
  30. package/dist/blocks/builtins/Contact.js.map +1 -1
  31. package/dist/blocks/builtins/Faq.js +5 -4
  32. package/dist/blocks/builtins/Faq.js.map +1 -1
  33. package/dist/blocks/builtins/Gallery.js +2 -2
  34. package/dist/blocks/builtins/Gallery.js.map +1 -1
  35. package/dist/blocks/builtins/Jobs.d.ts +8 -1
  36. package/dist/blocks/builtins/Jobs.js +37 -21
  37. package/dist/blocks/builtins/Jobs.js.map +1 -1
  38. package/dist/blocks/builtins/MapBlock.js +31 -4
  39. package/dist/blocks/builtins/MapBlock.js.map +1 -1
  40. package/dist/blocks/builtins/PdfBlock.js +2 -3
  41. package/dist/blocks/builtins/PdfBlock.js.map +1 -1
  42. package/dist/blocks/builtins/Portfolio.js +2 -2
  43. package/dist/blocks/builtins/Portfolio.js.map +1 -1
  44. package/dist/blocks/builtins/Pricing.d.ts +2 -0
  45. package/dist/blocks/builtins/Pricing.js +18 -10
  46. package/dist/blocks/builtins/Pricing.js.map +1 -1
  47. package/dist/blocks/builtins/Sections.js +3 -2
  48. package/dist/blocks/builtins/Sections.js.map +1 -1
  49. package/dist/blocks/builtins/Slider.js +21 -3
  50. package/dist/blocks/builtins/Slider.js.map +1 -1
  51. package/dist/blocks/builtins/Testimonials.js +21 -18
  52. package/dist/blocks/builtins/Testimonials.js.map +1 -1
  53. package/dist/blocks/builtins/WhatsappChat.js +1 -1
  54. package/dist/blocks/builtins/WhatsappChat.js.map +1 -1
  55. package/dist/blocks/builtins/Widgets.js +2 -3
  56. package/dist/blocks/builtins/Widgets.js.map +1 -1
  57. package/dist/blocks/builtins/lib/ics.js +1 -1
  58. package/dist/blocks/builtins/lib/ics.js.map +1 -1
  59. package/dist/blocks/builtins/lib/openingHours.d.ts +4 -1
  60. package/dist/blocks/builtins/lib/openingHours.js +11 -5
  61. package/dist/blocks/builtins/lib/openingHours.js.map +1 -1
  62. package/dist/demo/demoStore.d.ts +11 -1
  63. package/dist/demo/demoStore.js +50 -0
  64. package/dist/demo/demoStore.js.map +1 -1
  65. package/dist/firebase/client.js +2 -1
  66. package/dist/firebase/client.js.map +1 -1
  67. package/dist/index.d.ts +1 -1
  68. package/dist/index.js +2 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/modules/builtins.js +1 -1
  71. package/dist/modules/builtins.js.map +1 -1
  72. package/dist/modules/consent/useConsent.js +15 -3
  73. package/dist/modules/consent/useConsent.js.map +1 -1
  74. package/dist/schema/blocks.d.ts +24 -3
  75. package/dist/schema/blocks.js +4 -1
  76. package/dist/schema/blocks.js.map +1 -1
  77. package/dist/server/seo.d.ts +2 -0
  78. package/dist/server/seo.js +1 -1
  79. package/dist/server/seo.js.map +1 -1
  80. package/dist/styles.css +35 -6
  81. package/package.json +1 -1
@@ -3,8 +3,10 @@
3
3
  * zijde) en exporteert als webp. Dit bespaart fors op Storage-kosten (een 5MB-foto wordt ~200KB)
4
4
  * en houdt de opgeslagen URL stabiel (we uploaden meteen het kleine bestand).
5
5
  *
6
- * Vector/animatie (SVG/GIF) en al kleine bestanden worden ongemoeid gelaten. Bij elke fout valt
7
- * de functie terug op het originele bestand — uploaden mag nooit stuklopen op compressie.
6
+ * Vector/animatie (SVG/GIF) en al kleine bestanden worden ongemoeid gelaten. Bij een fout in de
7
+ * compressie-stap valt de functie terug op het origineel — MAAR een afbeelding die de browser
8
+ * überhaupt niet kan decoderen (HEIC van een iPhone bv.) wordt geweigerd met een duidelijke fout:
9
+ * stil doorsturen zou een bestand uploaden dat bij vrijwel geen enkele bezoeker rendert.
8
10
  */
9
11
  export declare function compressImage(file: File, opts?: {
10
12
  maxSize?: number;
@@ -5,8 +5,8 @@ async function compressImage(file, opts = {}) {
5
5
  if (typeof document === "undefined") return file;
6
6
  if (!file.type.startsWith("image/")) return file;
7
7
  if (file.type === "image/svg+xml" || file.type === "image/gif") return file;
8
+ const bitmap = await loadBitmap(file);
8
9
  try {
9
- const bitmap = await loadBitmap(file);
10
10
  const scale = Math.min(1, maxSize / Math.max(bitmap.width, bitmap.height));
11
11
  const w = Math.max(1, Math.round(bitmap.width * scale));
12
12
  const h = Math.max(1, Math.round(bitmap.height * scale));
@@ -21,8 +21,10 @@ async function compressImage(file, opts = {}) {
21
21
  (resolve) => canvas.toBlob(resolve, "image/webp", quality)
22
22
  );
23
23
  if (!blob || blob.size >= file.size) return file;
24
- const name = file.name.replace(/\.[^.]+$/, "") + ".webp";
25
- return new File([blob], name, { type: "image/webp", lastModified: Date.now() });
24
+ const type = blob.type || "image/webp";
25
+ const ext = type === "image/webp" ? ".webp" : type === "image/png" ? ".png" : ".jpg";
26
+ const name = file.name.replace(/\.[^.]+$/, "") + ext;
27
+ return new File([blob], name, { type, lastModified: Date.now() });
26
28
  } catch {
27
29
  return file;
28
30
  }
@@ -30,7 +32,7 @@ async function compressImage(file, opts = {}) {
30
32
  async function loadBitmap(file) {
31
33
  if (typeof createImageBitmap === "function") {
32
34
  try {
33
- return await createImageBitmap(file);
35
+ return await createImageBitmap(file, { imageOrientation: "from-image" });
34
36
  } catch {
35
37
  }
36
38
  }
@@ -39,7 +41,7 @@ async function loadBitmap(file) {
39
41
  const img = new Image();
40
42
  await new Promise((resolve, reject) => {
41
43
  img.onload = () => resolve();
42
- img.onerror = () => reject(new Error("kon afbeelding niet laden"));
44
+ img.onerror = () => reject(new Error("Dit bestandsformaat kan de browser niet lezen (bv. HEIC) \u2014 sla de foto op als JPG of PNG en probeer opnieuw."));
43
45
  img.src = url;
44
46
  });
45
47
  return img;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/lib/compressImage.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Comprimeert een afbeelding in de browser vóór upload: schaalt naar max `maxSize` px (langste\n * zijde) en exporteert als webp. Dit bespaart fors op Storage-kosten (een 5MB-foto wordt ~200KB)\n * en houdt de opgeslagen URL stabiel (we uploaden meteen het kleine bestand).\n *\n * Vector/animatie (SVG/GIF) en al kleine bestanden worden ongemoeid gelaten. Bij elke fout valt\n * de functie terug op het originele bestand — uploaden mag nooit stuklopen op compressie.\n */\nexport async function compressImage(\n file: File,\n opts: { maxSize?: number; quality?: number } = {},\n): Promise<File> {\n const maxSize = opts.maxSize ?? 2048;\n const quality = opts.quality ?? 0.82;\n\n if (typeof document === \"undefined\") return file; // alleen client\n if (!file.type.startsWith(\"image/\")) return file;\n if (file.type === \"image/svg+xml\" || file.type === \"image/gif\") return file;\n\n try {\n const bitmap = await loadBitmap(file);\n const scale = Math.min(1, maxSize / Math.max(bitmap.width, bitmap.height));\n const w = Math.max(1, Math.round(bitmap.width * scale));\n const h = Math.max(1, Math.round(bitmap.height * scale));\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = w;\n canvas.height = h;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return file;\n ctx.drawImage(bitmap as CanvasImageSource, 0, 0, w, h);\n if (\"close\" in bitmap && typeof bitmap.close === \"function\") bitmap.close();\n\n const blob = await new Promise<Blob | null>((resolve) =>\n canvas.toBlob(resolve, \"image/webp\", quality),\n );\n if (!blob || blob.size >= file.size) return file; // compressie hielp niet\n\n const name = file.name.replace(/\\.[^.]+$/, \"\") + \".webp\";\n return new File([blob], name, { type: \"image/webp\", lastModified: Date.now() });\n } catch {\n return file;\n }\n}\n\nasync function loadBitmap(file: File): Promise<ImageBitmap | HTMLImageElement> {\n if (typeof createImageBitmap === \"function\") {\n try {\n return await createImageBitmap(file);\n } catch {\n /* val terug op <img> */\n }\n }\n const url = URL.createObjectURL(file);\n try {\n const img = new Image();\n await new Promise<void>((resolve, reject) => {\n img.onload = () => resolve();\n img.onerror = () => reject(new Error(\"kon afbeelding niet laden\"));\n img.src = url;\n });\n return img;\n } finally {\n URL.revokeObjectURL(url);\n }\n}\n"],"mappings":";AAUA,eAAsB,cACpB,MACA,OAA+C,CAAC,GACjC;AACf,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,UAAU,KAAK,WAAW;AAEhC,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,MAAI,CAAC,KAAK,KAAK,WAAW,QAAQ,EAAG,QAAO;AAC5C,MAAI,KAAK,SAAS,mBAAmB,KAAK,SAAS,YAAa,QAAO;AAEvE,MAAI;AACF,UAAM,SAAS,MAAM,WAAW,IAAI;AACpC,UAAM,QAAQ,KAAK,IAAI,GAAG,UAAU,KAAK,IAAI,OAAO,OAAO,OAAO,MAAM,CAAC;AACzE,UAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,QAAQ,KAAK,CAAC;AACtD,UAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,SAAS,KAAK,CAAC;AAEvD,UAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,WAAO,QAAQ;AACf,WAAO,SAAS;AAChB,UAAM,MAAM,OAAO,WAAW,IAAI;AAClC,QAAI,CAAC,IAAK,QAAO;AACjB,QAAI,UAAU,QAA6B,GAAG,GAAG,GAAG,CAAC;AACrD,QAAI,WAAW,UAAU,OAAO,OAAO,UAAU,WAAY,QAAO,MAAM;AAE1E,UAAM,OAAO,MAAM,IAAI;AAAA,MAAqB,CAAC,YAC3C,OAAO,OAAO,SAAS,cAAc,OAAO;AAAA,IAC9C;AACA,QAAI,CAAC,QAAQ,KAAK,QAAQ,KAAK,KAAM,QAAO;AAE5C,UAAM,OAAO,KAAK,KAAK,QAAQ,YAAY,EAAE,IAAI;AACjD,WAAO,IAAI,KAAK,CAAC,IAAI,GAAG,MAAM,EAAE,MAAM,cAAc,cAAc,KAAK,IAAI,EAAE,CAAC;AAAA,EAChF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,WAAW,MAAqD;AAC7E,MAAI,OAAO,sBAAsB,YAAY;AAC3C,QAAI;AACF,aAAO,MAAM,kBAAkB,IAAI;AAAA,IACrC,QAAQ;AAAA,IAER;AAAA,EACF;AACA,QAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,MAAI;AACF,UAAM,MAAM,IAAI,MAAM;AACtB,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,UAAI,SAAS,MAAM,QAAQ;AAC3B,UAAI,UAAU,MAAM,OAAO,IAAI,MAAM,2BAA2B,CAAC;AACjE,UAAI,MAAM;AAAA,IACZ,CAAC;AACD,WAAO;AAAA,EACT,UAAE;AACA,QAAI,gBAAgB,GAAG;AAAA,EACzB;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/lib/compressImage.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Comprimeert een afbeelding in de browser vóór upload: schaalt naar max `maxSize` px (langste\n * zijde) en exporteert als webp. Dit bespaart fors op Storage-kosten (een 5MB-foto wordt ~200KB)\n * en houdt de opgeslagen URL stabiel (we uploaden meteen het kleine bestand).\n *\n * Vector/animatie (SVG/GIF) en al kleine bestanden worden ongemoeid gelaten. Bij een fout in de\n * compressie-stap valt de functie terug op het origineel — MAAR een afbeelding die de browser\n * überhaupt niet kan decoderen (HEIC van een iPhone bv.) wordt geweigerd met een duidelijke fout:\n * stil doorsturen zou een bestand uploaden dat bij vrijwel geen enkele bezoeker rendert.\n */\nexport async function compressImage(\n file: File,\n opts: { maxSize?: number; quality?: number } = {},\n): Promise<File> {\n const maxSize = opts.maxSize ?? 2048;\n const quality = opts.quality ?? 0.82;\n\n if (typeof document === \"undefined\") return file; // alleen client\n if (!file.type.startsWith(\"image/\")) return file;\n if (file.type === \"image/svg+xml\" || file.type === \"image/gif\") return file;\n\n const bitmap = await loadBitmap(file); // gooit bij niet-decodeerbaar formaat (bv. HEIC)\n\n try {\n const scale = Math.min(1, maxSize / Math.max(bitmap.width, bitmap.height));\n const w = Math.max(1, Math.round(bitmap.width * scale));\n const h = Math.max(1, Math.round(bitmap.height * scale));\n\n const canvas = document.createElement(\"canvas\");\n canvas.width = w;\n canvas.height = h;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return file;\n ctx.drawImage(bitmap as CanvasImageSource, 0, 0, w, h);\n if (\"close\" in bitmap && typeof bitmap.close === \"function\") bitmap.close();\n\n const blob = await new Promise<Blob | null>((resolve) =>\n canvas.toBlob(resolve, \"image/webp\", quality),\n );\n if (!blob || blob.size >= file.size) return file; // compressie hielp niet\n // Safari kan stil een ander type teruggeven dan gevraagd — label het bestand dan eerlijk.\n const type = blob.type || \"image/webp\";\n const ext = type === \"image/webp\" ? \".webp\" : type === \"image/png\" ? \".png\" : \".jpg\";\n const name = file.name.replace(/\\.[^.]+$/, \"\") + ext;\n return new File([blob], name, { type, lastModified: Date.now() });\n } catch {\n return file; // compressie faalde, maar het beeld ís decodeerbaar → origineel is veilig\n }\n}\n\nasync function loadBitmap(file: File): Promise<ImageBitmap | HTMLImageElement> {\n if (typeof createImageBitmap === \"function\") {\n try {\n // EXIF-oriëntatie expliciet respecteren (default in moderne browsers; hardening voor oude).\n return await createImageBitmap(file, { imageOrientation: \"from-image\" });\n } catch {\n /* val terug op <img> */\n }\n }\n const url = URL.createObjectURL(file);\n try {\n const img = new Image();\n await new Promise<void>((resolve, reject) => {\n img.onload = () => resolve();\n img.onerror = () =>\n reject(new Error(\"Dit bestandsformaat kan de browser niet lezen (bv. HEIC) — sla de foto op als JPG of PNG en probeer opnieuw.\"));\n img.src = url;\n });\n return img;\n } finally {\n URL.revokeObjectURL(url);\n }\n}\n"],"mappings":";AAYA,eAAsB,cACpB,MACA,OAA+C,CAAC,GACjC;AACf,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,UAAU,KAAK,WAAW;AAEhC,MAAI,OAAO,aAAa,YAAa,QAAO;AAC5C,MAAI,CAAC,KAAK,KAAK,WAAW,QAAQ,EAAG,QAAO;AAC5C,MAAI,KAAK,SAAS,mBAAmB,KAAK,SAAS,YAAa,QAAO;AAEvE,QAAM,SAAS,MAAM,WAAW,IAAI;AAEpC,MAAI;AACF,UAAM,QAAQ,KAAK,IAAI,GAAG,UAAU,KAAK,IAAI,OAAO,OAAO,OAAO,MAAM,CAAC;AACzE,UAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,QAAQ,KAAK,CAAC;AACtD,UAAM,IAAI,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,SAAS,KAAK,CAAC;AAEvD,UAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,WAAO,QAAQ;AACf,WAAO,SAAS;AAChB,UAAM,MAAM,OAAO,WAAW,IAAI;AAClC,QAAI,CAAC,IAAK,QAAO;AACjB,QAAI,UAAU,QAA6B,GAAG,GAAG,GAAG,CAAC;AACrD,QAAI,WAAW,UAAU,OAAO,OAAO,UAAU,WAAY,QAAO,MAAM;AAE1E,UAAM,OAAO,MAAM,IAAI;AAAA,MAAqB,CAAC,YAC3C,OAAO,OAAO,SAAS,cAAc,OAAO;AAAA,IAC9C;AACA,QAAI,CAAC,QAAQ,KAAK,QAAQ,KAAK,KAAM,QAAO;AAE5C,UAAM,OAAO,KAAK,QAAQ;AAC1B,UAAM,MAAM,SAAS,eAAe,UAAU,SAAS,cAAc,SAAS;AAC9E,UAAM,OAAO,KAAK,KAAK,QAAQ,YAAY,EAAE,IAAI;AACjD,WAAO,IAAI,KAAK,CAAC,IAAI,GAAG,MAAM,EAAE,MAAM,cAAc,KAAK,IAAI,EAAE,CAAC;AAAA,EAClE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,WAAW,MAAqD;AAC7E,MAAI,OAAO,sBAAsB,YAAY;AAC3C,QAAI;AAEF,aAAO,MAAM,kBAAkB,MAAM,EAAE,kBAAkB,aAAa,CAAC;AAAA,IACzE,QAAQ;AAAA,IAER;AAAA,EACF;AACA,QAAM,MAAM,IAAI,gBAAgB,IAAI;AACpC,MAAI;AACF,UAAM,MAAM,IAAI,MAAM;AACtB,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,UAAI,SAAS,MAAM,QAAQ;AAC3B,UAAI,UAAU,MACZ,OAAO,IAAI,MAAM,mHAA8G,CAAC;AAClI,UAAI,MAAM;AAAA,IACZ,CAAC;AACD,WAAO;AAAA,EACT,UAAE;AACA,QAAI,gBAAgB,GAAG;AAAA,EACzB;AACF;","names":[]}
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Zoekt (goedkoop) of een media-item in gepubliceerde pagina's wordt gebruikt, zodat we kunnen
3
- * waarschuwen vóór verwijderen (anders breekt een live pagina met een dode afbeelding).
4
- * Sites hebben weinig pagina's → een JSON-substring-scan is ruim voldoende.
2
+ * Zoekt (goedkoop) of een media-item ergens in gebruik is, zodat we kunnen waarschuwen vóór
3
+ * verwijderen (anders breekt een live pagina met een dode afbeelding). Gescand worden:
4
+ * gepubliceerde pagina's, concepten (drafts), nieuwsartikelen (covers/inline beelden) en de
5
+ * site-instellingen (og-beeld e.d.). Sites zijn klein → een JSON-substring-scan is ruim voldoende.
5
6
  */
6
7
  export declare function findMediaUsage(siteId: string, item: {
7
8
  id: string;
@@ -1,22 +1,33 @@
1
1
  "use client";
2
- import { collection, getDocs } from "firebase/firestore";
2
+ import { collection, doc, getDoc, getDocs } from "firebase/firestore";
3
3
  import { getDb } from "../../firebase/client";
4
4
  async function findMediaUsage(siteId, item) {
5
5
  const db = getDb();
6
6
  if (!db) return [];
7
+ const matches = (blob) => item.url && blob.includes(item.url) || blob.includes(item.id);
8
+ const hits = [];
7
9
  try {
8
- const snap = await getDocs(collection(db, "sites", siteId, "pages"));
9
- const hits = [];
10
- for (const d of snap.docs) {
10
+ const [pages, drafts, news, settings] = await Promise.all([
11
+ getDocs(collection(db, "sites", siteId, "pages")),
12
+ getDocs(collection(db, "sites", siteId, "drafts")).catch(() => null),
13
+ getDocs(collection(db, "sites", siteId, "news")).catch(() => null),
14
+ getDoc(doc(db, "sites", siteId, "settings", "public")).catch(() => null)
15
+ ]);
16
+ for (const d of pages.docs) {
11
17
  const data = d.data();
12
- const blob = JSON.stringify(data.blocks ?? []);
13
- if (item.url && blob.includes(item.url) || blob.includes(item.id)) {
14
- hits.push(String(data.path ?? data.title ?? d.id));
15
- }
18
+ if (matches(JSON.stringify(data.blocks ?? []))) hits.push(String(data.path ?? data.title ?? d.id));
16
19
  }
17
- return hits;
20
+ for (const d of drafts?.docs ?? []) {
21
+ const data = d.data();
22
+ if (matches(JSON.stringify(data.blocks ?? []))) hits.push(`${String(data.path ?? d.id)} (concept)`);
23
+ }
24
+ for (const d of news?.docs ?? []) {
25
+ if (matches(JSON.stringify(d.data()))) hits.push(`nieuws: ${String(d.data().title ?? d.id)}`);
26
+ }
27
+ if (settings?.exists() && matches(JSON.stringify(settings.data()))) hits.push("site-instellingen");
28
+ return [...new Set(hits)];
18
29
  } catch {
19
- return [];
30
+ return hits;
20
31
  }
21
32
  }
22
33
  export {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/lib/mediaUsage.ts"],"sourcesContent":["\"use client\";\n\nimport { collection, getDocs } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\n\n/**\n * Zoekt (goedkoop) of een media-item in gepubliceerde pagina's wordt gebruikt, zodat we kunnen\n * waarschuwen vóór verwijderen (anders breekt een live pagina met een dode afbeelding).\n * Sites hebben weinig pagina's → een JSON-substring-scan is ruim voldoende.\n */\nexport async function findMediaUsage(\n siteId: string,\n item: { id: string; url?: string },\n): Promise<string[]> {\n const db = getDb();\n if (!db) return [];\n try {\n const snap = await getDocs(collection(db, \"sites\", siteId, \"pages\"));\n const hits: string[] = [];\n for (const d of snap.docs) {\n const data = d.data() as Record<string, unknown>;\n const blob = JSON.stringify(data.blocks ?? []);\n if ((item.url && blob.includes(item.url)) || blob.includes(item.id)) {\n hits.push(String(data.path ?? data.title ?? d.id));\n }\n }\n return hits;\n } catch {\n return [];\n }\n}\n"],"mappings":";AAEA,SAAS,YAAY,eAAe;AACpC,SAAS,aAAa;AAOtB,eAAsB,eACpB,QACA,MACmB;AACnB,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,QAAO,CAAC;AACjB,MAAI;AACF,UAAM,OAAO,MAAM,QAAQ,WAAW,IAAI,SAAS,QAAQ,OAAO,CAAC;AACnE,UAAM,OAAiB,CAAC;AACxB,eAAW,KAAK,KAAK,MAAM;AACzB,YAAM,OAAO,EAAE,KAAK;AACpB,YAAM,OAAO,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC;AAC7C,UAAK,KAAK,OAAO,KAAK,SAAS,KAAK,GAAG,KAAM,KAAK,SAAS,KAAK,EAAE,GAAG;AACnE,aAAK,KAAK,OAAO,KAAK,QAAQ,KAAK,SAAS,EAAE,EAAE,CAAC;AAAA,MACnD;AAAA,IACF;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/lib/mediaUsage.ts"],"sourcesContent":["\"use client\";\n\nimport { collection, doc, getDoc, getDocs } from \"firebase/firestore\";\nimport { getDb } from \"../../firebase/client\";\n\n/**\n * Zoekt (goedkoop) of een media-item ergens in gebruik is, zodat we kunnen waarschuwen vóór\n * verwijderen (anders breekt een live pagina met een dode afbeelding). Gescand worden:\n * gepubliceerde pagina's, concepten (drafts), nieuwsartikelen (covers/inline beelden) en de\n * site-instellingen (og-beeld e.d.). Sites zijn klein → een JSON-substring-scan is ruim voldoende.\n */\nexport async function findMediaUsage(\n siteId: string,\n item: { id: string; url?: string },\n): Promise<string[]> {\n const db = getDb();\n if (!db) return [];\n const matches = (blob: string) => (item.url && blob.includes(item.url)) || blob.includes(item.id);\n const hits: string[] = [];\n try {\n const [pages, drafts, news, settings] = await Promise.all([\n getDocs(collection(db, \"sites\", siteId, \"pages\")),\n getDocs(collection(db, \"sites\", siteId, \"drafts\")).catch(() => null),\n getDocs(collection(db, \"sites\", siteId, \"news\")).catch(() => null),\n getDoc(doc(db, \"sites\", siteId, \"settings\", \"public\")).catch(() => null),\n ]);\n for (const d of pages.docs) {\n const data = d.data() as Record<string, unknown>;\n if (matches(JSON.stringify(data.blocks ?? []))) hits.push(String(data.path ?? data.title ?? d.id));\n }\n for (const d of drafts?.docs ?? []) {\n const data = d.data() as Record<string, unknown>;\n if (matches(JSON.stringify(data.blocks ?? []))) hits.push(`${String(data.path ?? d.id)} (concept)`);\n }\n for (const d of news?.docs ?? []) {\n if (matches(JSON.stringify(d.data()))) hits.push(`nieuws: ${String((d.data() as { title?: string }).title ?? d.id)}`);\n }\n if (settings?.exists() && matches(JSON.stringify(settings.data()))) hits.push(\"site-instellingen\");\n return [...new Set(hits)];\n } catch {\n return hits;\n }\n}\n"],"mappings":";AAEA,SAAS,YAAY,KAAK,QAAQ,eAAe;AACjD,SAAS,aAAa;AAQtB,eAAsB,eACpB,QACA,MACmB;AACnB,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,GAAI,QAAO,CAAC;AACjB,QAAM,UAAU,CAAC,SAAkB,KAAK,OAAO,KAAK,SAAS,KAAK,GAAG,KAAM,KAAK,SAAS,KAAK,EAAE;AAChG,QAAM,OAAiB,CAAC;AACxB,MAAI;AACF,UAAM,CAAC,OAAO,QAAQ,MAAM,QAAQ,IAAI,MAAM,QAAQ,IAAI;AAAA,MACxD,QAAQ,WAAW,IAAI,SAAS,QAAQ,OAAO,CAAC;AAAA,MAChD,QAAQ,WAAW,IAAI,SAAS,QAAQ,QAAQ,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,MACnE,QAAQ,WAAW,IAAI,SAAS,QAAQ,MAAM,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,MACjE,OAAO,IAAI,IAAI,SAAS,QAAQ,YAAY,QAAQ,CAAC,EAAE,MAAM,MAAM,IAAI;AAAA,IACzE,CAAC;AACD,eAAW,KAAK,MAAM,MAAM;AAC1B,YAAM,OAAO,EAAE,KAAK;AACpB,UAAI,QAAQ,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC,CAAC,EAAG,MAAK,KAAK,OAAO,KAAK,QAAQ,KAAK,SAAS,EAAE,EAAE,CAAC;AAAA,IACnG;AACA,eAAW,KAAK,QAAQ,QAAQ,CAAC,GAAG;AAClC,YAAM,OAAO,EAAE,KAAK;AACpB,UAAI,QAAQ,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC,CAAC,EAAG,MAAK,KAAK,GAAG,OAAO,KAAK,QAAQ,EAAE,EAAE,CAAC,YAAY;AAAA,IACpG;AACA,eAAW,KAAK,MAAM,QAAQ,CAAC,GAAG;AAChC,UAAI,QAAQ,KAAK,UAAU,EAAE,KAAK,CAAC,CAAC,EAAG,MAAK,KAAK,WAAW,OAAQ,EAAE,KAAK,EAAyB,SAAS,EAAE,EAAE,CAAC,EAAE;AAAA,IACtH;AACA,QAAI,UAAU,OAAO,KAAK,QAAQ,KAAK,UAAU,SAAS,KAAK,CAAC,CAAC,EAAG,MAAK,KAAK,mBAAmB;AACjG,WAAO,CAAC,GAAG,IAAI,IAAI,IAAI,CAAC;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -1,5 +1,9 @@
1
1
  /** Maximale bestandsgrootte vóór compressie (foolproof: voorkomt vastlopen op enorme bestanden). */
2
2
  export declare const MAX_UPLOAD_BYTES: number;
3
+ /** Harde grens die de Storage-rules afdwingen (storage.rules): het GECOMPRIMEERDE bestand moet
4
+ * hieronder blijven. We checken dit client-side na compressie zodat een slecht comprimeerbaar
5
+ * bestand een nette melding geeft i.p.v. een cryptische rules-weigering ("er gebeurt niets"). */
6
+ export declare const MAX_STORED_BYTES: number;
3
7
  /**
4
8
  * Comprimeert een afbeelding (client-side, webp + resize) en uploadt 'm naar
5
9
  * `sites/{siteId}/media/...`. Registreert het media-doc (status "processing" — de sharp-Cloud-
@@ -1,26 +1,39 @@
1
1
  "use client";
2
2
  import { ref as storageRef, uploadBytes, getDownloadURL } from "firebase/storage";
3
- import { addDoc, collection, serverTimestamp } from "firebase/firestore";
3
+ import { addDoc, collection, doc, serverTimestamp, updateDoc } from "firebase/firestore";
4
4
  import { getBucket, getDb } from "../../firebase/client";
5
+ import { addDemoMedia, isDemoActive } from "../../demo/demoStore";
5
6
  import { compressImage } from "./compressImage";
6
7
  const MAX_UPLOAD_BYTES = 15 * 1024 * 1024;
8
+ const MAX_STORED_BYTES = 10 * 1024 * 1024;
7
9
  async function uploadImage(siteId, file) {
8
10
  const bucket = getBucket();
9
11
  const db = getDb();
10
- if (!bucket) throw new Error("Firebase Storage is niet geconfigureerd.");
11
12
  if (!file.type.startsWith("image/")) throw new Error("Alleen afbeeldingen zijn toegestaan.");
12
13
  if (file.size > MAX_UPLOAD_BYTES) {
13
14
  throw new Error(`Bestand te groot (max ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB).`);
14
15
  }
16
+ if (isDemoActive()) {
17
+ const optimized2 = await compressImage(file);
18
+ const dataUrl = await new Promise((resolve, reject) => {
19
+ const reader = new FileReader();
20
+ reader.onload = () => resolve(String(reader.result));
21
+ reader.onerror = () => reject(new Error("Bestand lezen mislukt."));
22
+ reader.readAsDataURL(optimized2);
23
+ });
24
+ const media = addDemoMedia(siteId, { url: dataUrl, name: optimized2.name });
25
+ return { url: media.url, mediaId: media.id, path: media.path };
26
+ }
27
+ if (!bucket) throw new Error("Firebase Storage is niet geconfigureerd.");
15
28
  const optimized = await compressImage(file);
29
+ if (optimized.size > MAX_STORED_BYTES) {
30
+ throw new Error(`Afbeelding blijft na verkleinen te groot (max ${Math.round(MAX_STORED_BYTES / 1024 / 1024)} MB). Kies een kleinere of minder gedetailleerde afbeelding.`);
31
+ }
16
32
  const path = `sites/${siteId}/media/${Date.now()}-${optimized.name.replace(/[^\w.-]+/g, "_")}`;
17
- const sref = storageRef(bucket, path);
18
- await uploadBytes(sref, optimized);
19
- const url = await getDownloadURL(sref);
20
33
  let mediaId = "";
21
34
  if (db) {
22
35
  const docRef = await addDoc(collection(db, "sites", siteId, "media"), {
23
- url,
36
+ url: "",
24
37
  path,
25
38
  name: optimized.name,
26
39
  contentType: optimized.type,
@@ -31,9 +44,16 @@ async function uploadImage(siteId, file) {
31
44
  });
32
45
  mediaId = docRef.id;
33
46
  }
47
+ const sref = storageRef(bucket, path);
48
+ await uploadBytes(sref, optimized);
49
+ const url = await getDownloadURL(sref);
50
+ if (db && mediaId) {
51
+ await updateDoc(doc(db, "sites", siteId, "media", mediaId), { url });
52
+ }
34
53
  return { url, mediaId, path };
35
54
  }
36
55
  export {
56
+ MAX_STORED_BYTES,
37
57
  MAX_UPLOAD_BYTES,
38
58
  uploadImage
39
59
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/lib/upload.ts"],"sourcesContent":["\"use client\";\n\nimport { ref as storageRef, uploadBytes, getDownloadURL } from \"firebase/storage\";\nimport { addDoc, collection, serverTimestamp } from \"firebase/firestore\";\nimport { getBucket, getDb } from \"../../firebase/client\";\nimport { compressImage } from \"./compressImage\";\n\n/** Maximale bestandsgrootte vóór compressie (foolproof: voorkomt vastlopen op enorme bestanden). */\nexport const MAX_UPLOAD_BYTES = 15 * 1024 * 1024; // 15 MB\n\n/**\n * Comprimeert een afbeelding (client-side, webp + resize) en uploadt 'm naar\n * `sites/{siteId}/media/...`. Registreert het media-doc (status \"processing\" — de sharp-Cloud-\n * Function vult srcset-varianten + zet \"ready\") en geeft de download-URL + media-id terug.\n * Door vóór upload te comprimeren bewaren we nooit het grote origineel → flinke storagebesparing.\n */\nexport async function uploadImage(\n siteId: string,\n file: File,\n): Promise<{ url: string; mediaId: string; path: string }> {\n const bucket = getBucket();\n const db = getDb();\n if (!bucket) throw new Error(\"Firebase Storage is niet geconfigureerd.\");\n if (!file.type.startsWith(\"image/\")) throw new Error(\"Alleen afbeeldingen zijn toegestaan.\");\n if (file.size > MAX_UPLOAD_BYTES) {\n throw new Error(`Bestand te groot (max ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB).`);\n }\n\n const optimized = await compressImage(file);\n const path = `sites/${siteId}/media/${Date.now()}-${optimized.name.replace(/[^\\w.-]+/g, \"_\")}`;\n const sref = storageRef(bucket, path);\n await uploadBytes(sref, optimized);\n const url = await getDownloadURL(sref);\n let mediaId = \"\";\n if (db) {\n const docRef = await addDoc(collection(db, \"sites\", siteId, \"media\"), {\n url,\n path,\n name: optimized.name,\n contentType: optimized.type,\n size: optimized.size,\n alt: \"\",\n status: \"processing\",\n createdAt: serverTimestamp(),\n });\n mediaId = docRef.id;\n }\n return { url, mediaId, path };\n}\n"],"mappings":";AAEA,SAAS,OAAO,YAAY,aAAa,sBAAsB;AAC/D,SAAS,QAAQ,YAAY,uBAAuB;AACpD,SAAS,WAAW,aAAa;AACjC,SAAS,qBAAqB;AAGvB,MAAM,mBAAmB,KAAK,OAAO;AAQ5C,eAAsB,YACpB,QACA,MACyD;AACzD,QAAM,SAAS,UAAU;AACzB,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,0CAA0C;AACvE,MAAI,CAAC,KAAK,KAAK,WAAW,QAAQ,EAAG,OAAM,IAAI,MAAM,sCAAsC;AAC3F,MAAI,KAAK,OAAO,kBAAkB;AAChC,UAAM,IAAI,MAAM,yBAAyB,KAAK,MAAM,mBAAmB,OAAO,IAAI,CAAC,OAAO;AAAA,EAC5F;AAEA,QAAM,YAAY,MAAM,cAAc,IAAI;AAC1C,QAAM,OAAO,SAAS,MAAM,UAAU,KAAK,IAAI,CAAC,IAAI,UAAU,KAAK,QAAQ,aAAa,GAAG,CAAC;AAC5F,QAAM,OAAO,WAAW,QAAQ,IAAI;AACpC,QAAM,YAAY,MAAM,SAAS;AACjC,QAAM,MAAM,MAAM,eAAe,IAAI;AACrC,MAAI,UAAU;AACd,MAAI,IAAI;AACN,UAAM,SAAS,MAAM,OAAO,WAAW,IAAI,SAAS,QAAQ,OAAO,GAAG;AAAA,MACpE;AAAA,MACA;AAAA,MACA,MAAM,UAAU;AAAA,MAChB,aAAa,UAAU;AAAA,MACvB,MAAM,UAAU;AAAA,MAChB,KAAK;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,gBAAgB;AAAA,IAC7B,CAAC;AACD,cAAU,OAAO;AAAA,EACnB;AACA,SAAO,EAAE,KAAK,SAAS,KAAK;AAC9B;","names":[]}
1
+ {"version":3,"sources":["../../../src/admin/lib/upload.ts"],"sourcesContent":["\"use client\";\n\nimport { ref as storageRef, uploadBytes, getDownloadURL } from \"firebase/storage\";\nimport { addDoc, collection, doc, serverTimestamp, updateDoc } from \"firebase/firestore\";\nimport { getBucket, getDb } from \"../../firebase/client\";\nimport { addDemoMedia, isDemoActive } from \"../../demo/demoStore\";\nimport { compressImage } from \"./compressImage\";\n\n/** Maximale bestandsgrootte vóór compressie (foolproof: voorkomt vastlopen op enorme bestanden). */\nexport const MAX_UPLOAD_BYTES = 15 * 1024 * 1024; // 15 MB\n/** Harde grens die de Storage-rules afdwingen (storage.rules): het GECOMPRIMEERDE bestand moet\n * hieronder blijven. We checken dit client-side na compressie zodat een slecht comprimeerbaar\n * bestand een nette melding geeft i.p.v. een cryptische rules-weigering (\"er gebeurt niets\"). */\nexport const MAX_STORED_BYTES = 10 * 1024 * 1024; // 10 MB — moet gelijk zijn aan storage.rules\n\n/**\n * Comprimeert een afbeelding (client-side, webp + resize) en uploadt 'm naar\n * `sites/{siteId}/media/...`. Registreert het media-doc (status \"processing\" — de sharp-Cloud-\n * Function vult srcset-varianten + zet \"ready\") en geeft de download-URL + media-id terug.\n * Door vóór upload te comprimeren bewaren we nooit het grote origineel → flinke storagebesparing.\n */\nexport async function uploadImage(\n siteId: string,\n file: File,\n): Promise<{ url: string; mediaId: string; path: string }> {\n const bucket = getBucket();\n const db = getDb();\n if (!file.type.startsWith(\"image/\")) throw new Error(\"Alleen afbeeldingen zijn toegestaan.\");\n if (file.size > MAX_UPLOAD_BYTES) {\n throw new Error(`Bestand te groot (max ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB).`);\n }\n\n // Demo-bewerkmodus: geen Firebase — de (gecomprimeerde) afbeelding wordt als data-URL in de\n // lokale demo-bibliotheek bewaard. Alles blijft in de browser, net als de demo-drafts.\n if (isDemoActive()) {\n const optimized = await compressImage(file);\n const dataUrl = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result));\n reader.onerror = () => reject(new Error(\"Bestand lezen mislukt.\"));\n reader.readAsDataURL(optimized);\n });\n const media = addDemoMedia(siteId, { url: dataUrl, name: optimized.name });\n return { url: media.url, mediaId: media.id, path: media.path };\n }\n\n if (!bucket) throw new Error(\"Firebase Storage is niet geconfigureerd.\");\n const optimized = await compressImage(file);\n if (optimized.size > MAX_STORED_BYTES) {\n throw new Error(`Afbeelding blijft na verkleinen te groot (max ${Math.round(MAX_STORED_BYTES / 1024 / 1024)} MB). Kies een kleinere of minder gedetailleerde afbeelding.`);\n }\n const path = `sites/${siteId}/media/${Date.now()}-${optimized.name.replace(/[^\\w.-]+/g, \"_\")}`;\n // Media-doc VÓÓR de upload aanmaken: de variants-Cloud-Function zoekt het doc op `path` zodra\n // het bestand binnenkomt — was het doc er dan nog niet, dan bleef het item eeuwig \"processing\".\n let mediaId = \"\";\n if (db) {\n const docRef = await addDoc(collection(db, \"sites\", siteId, \"media\"), {\n url: \"\",\n path,\n name: optimized.name,\n contentType: optimized.type,\n size: optimized.size,\n alt: \"\",\n status: \"processing\",\n createdAt: serverTimestamp(),\n });\n mediaId = docRef.id;\n }\n const sref = storageRef(bucket, path);\n await uploadBytes(sref, optimized);\n const url = await getDownloadURL(sref);\n if (db && mediaId) {\n await updateDoc(doc(db, \"sites\", siteId, \"media\", mediaId), { url });\n }\n return { url, mediaId, path };\n}\n"],"mappings":";AAEA,SAAS,OAAO,YAAY,aAAa,sBAAsB;AAC/D,SAAS,QAAQ,YAAY,KAAK,iBAAiB,iBAAiB;AACpE,SAAS,WAAW,aAAa;AACjC,SAAS,cAAc,oBAAoB;AAC3C,SAAS,qBAAqB;AAGvB,MAAM,mBAAmB,KAAK,OAAO;AAIrC,MAAM,mBAAmB,KAAK,OAAO;AAQ5C,eAAsB,YACpB,QACA,MACyD;AACzD,QAAM,SAAS,UAAU;AACzB,QAAM,KAAK,MAAM;AACjB,MAAI,CAAC,KAAK,KAAK,WAAW,QAAQ,EAAG,OAAM,IAAI,MAAM,sCAAsC;AAC3F,MAAI,KAAK,OAAO,kBAAkB;AAChC,UAAM,IAAI,MAAM,yBAAyB,KAAK,MAAM,mBAAmB,OAAO,IAAI,CAAC,OAAO;AAAA,EAC5F;AAIA,MAAI,aAAa,GAAG;AAClB,UAAMA,aAAY,MAAM,cAAc,IAAI;AAC1C,UAAM,UAAU,MAAM,IAAI,QAAgB,CAAC,SAAS,WAAW;AAC7D,YAAM,SAAS,IAAI,WAAW;AAC9B,aAAO,SAAS,MAAM,QAAQ,OAAO,OAAO,MAAM,CAAC;AACnD,aAAO,UAAU,MAAM,OAAO,IAAI,MAAM,wBAAwB,CAAC;AACjE,aAAO,cAAcA,UAAS;AAAA,IAChC,CAAC;AACD,UAAM,QAAQ,aAAa,QAAQ,EAAE,KAAK,SAAS,MAAMA,WAAU,KAAK,CAAC;AACzE,WAAO,EAAE,KAAK,MAAM,KAAK,SAAS,MAAM,IAAI,MAAM,MAAM,KAAK;AAAA,EAC/D;AAEA,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,0CAA0C;AACvE,QAAM,YAAY,MAAM,cAAc,IAAI;AAC1C,MAAI,UAAU,OAAO,kBAAkB;AACrC,UAAM,IAAI,MAAM,iDAAiD,KAAK,MAAM,mBAAmB,OAAO,IAAI,CAAC,8DAA8D;AAAA,EAC3K;AACA,QAAM,OAAO,SAAS,MAAM,UAAU,KAAK,IAAI,CAAC,IAAI,UAAU,KAAK,QAAQ,aAAa,GAAG,CAAC;AAG5F,MAAI,UAAU;AACd,MAAI,IAAI;AACN,UAAM,SAAS,MAAM,OAAO,WAAW,IAAI,SAAS,QAAQ,OAAO,GAAG;AAAA,MACpE,KAAK;AAAA,MACL;AAAA,MACA,MAAM,UAAU;AAAA,MAChB,aAAa,UAAU;AAAA,MACvB,MAAM,UAAU;AAAA,MAChB,KAAK;AAAA,MACL,QAAQ;AAAA,MACR,WAAW,gBAAgB;AAAA,IAC7B,CAAC;AACD,cAAU,OAAO;AAAA,EACnB;AACA,QAAM,OAAO,WAAW,QAAQ,IAAI;AACpC,QAAM,YAAY,MAAM,SAAS;AACjC,QAAM,MAAM,MAAM,eAAe,IAAI;AACrC,MAAI,MAAM,SAAS;AACjB,UAAM,UAAU,IAAI,IAAI,SAAS,QAAQ,SAAS,OAAO,GAAG,EAAE,IAAI,CAAC;AAAA,EACrE;AACA,SAAO,EAAE,KAAK,SAAS,KAAK;AAC9B;","names":["optimized"]}
@@ -12,6 +12,7 @@ import {
12
12
  } from "firebase/firestore";
13
13
  import { deleteObject, ref as storageRef } from "firebase/storage";
14
14
  import { getBucket, getDb } from "../../firebase/client";
15
+ import { DEMO_EVENT, isDemoActive, listDemoMediaAll } from "../../demo/demoStore";
15
16
  import { Button, Card, Badge, Input } from "../ui/primitives";
16
17
  import { useToast } from "../ui/toast";
17
18
  import { useConfirm } from "../ui/ConfirmDialog";
@@ -24,14 +25,22 @@ function MediaManager({ siteId }) {
24
25
  const [busy, setBusy] = useState(false);
25
26
  const inputRef = useRef(null);
26
27
  useEffect(() => {
28
+ if (isDemoActive()) {
29
+ const sync = () => setItems(listDemoMediaAll(siteId));
30
+ sync();
31
+ window.addEventListener(DEMO_EVENT, sync);
32
+ return () => window.removeEventListener(DEMO_EVENT, sync);
33
+ }
27
34
  const db = getDb();
28
35
  if (!db) return;
29
36
  const q = query(collection(db, "sites", siteId, "media"), orderBy("createdAt", "desc"));
30
37
  return onSnapshot(
31
38
  q,
32
- (snap) => setItems(snap.docs.map((d) => ({ id: d.id, ...d.data() })))
39
+ (snap) => setItems(snap.docs.map((d) => ({ id: d.id, ...d.data() }))),
40
+ // Leesfouten (rules/App Check) niet stil slikken — anders lijkt de bibliotheek leeg.
41
+ (err) => toast({ title: "Kon mediabibliotheek niet laden", description: err.message, tone: "error" })
33
42
  );
34
- }, [siteId]);
43
+ }, [siteId, toast]);
35
44
  async function onFiles(files) {
36
45
  setBusy(true);
37
46
  let ok = 0;
@@ -66,11 +75,18 @@ function MediaManager({ siteId }) {
66
75
  });
67
76
  if (!ok) return;
68
77
  await deleteDoc(doc(db, "sites", siteId, "media", item.id));
69
- if (bucket && item.path) {
70
- try {
71
- await deleteObject(storageRef(bucket, item.path));
72
- } catch {
73
- }
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
+ );
74
90
  }
75
91
  toast({ title: "Verwijderd" });
76
92
  }
@@ -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 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 const db = getDb();\n if (!db) return;\n const q = query(collection(db, \"sites\", siteId, \"media\"), orderBy(\"createdAt\", \"desc\"));\n return onSnapshot(q, (snap) =>\n setItems(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<MediaItem, \"id\">) }))),\n );\n }, [siteId]);\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 && item.path) {\n try {\n await deleteObject(storageRef(bucket, item.path));\n } catch {\n /* al weg — negeren */\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 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":[]}
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import { collection, onSnapshot, orderBy, query } from "firebase/firestore";
5
5
  import { getDb } from "../../firebase/client";
6
+ import { DEMO_EVENT, isDemoActive, listDemoMediaAll } from "../../demo/demoStore";
6
7
  import { Button, Input } from "../ui/primitives";
7
8
  import { Dialog, DialogContent } from "../ui/dialog";
8
9
  import { useToast } from "../ui/toast";
@@ -20,14 +21,23 @@ function MediaPickerDialog({
20
21
  const inputRef = useRef(null);
21
22
  useEffect(() => {
22
23
  if (!open) return;
24
+ if (isDemoActive()) {
25
+ const sync = () => setItems(listDemoMediaAll(siteId));
26
+ sync();
27
+ window.addEventListener(DEMO_EVENT, sync);
28
+ return () => window.removeEventListener(DEMO_EVENT, sync);
29
+ }
23
30
  const db = getDb();
24
31
  if (!db) return;
25
32
  const qy = query(collection(db, "sites", siteId, "media"), orderBy("createdAt", "desc"));
26
33
  return onSnapshot(
27
34
  qy,
28
- (snap) => setItems(snap.docs.map((d) => ({ id: d.id, ...d.data() })))
35
+ (snap) => setItems(snap.docs.map((d) => ({ id: d.id, ...d.data() }))),
36
+ // Zonder onError worden lees-/rules-/App-Check-fouten stil geslikt → de kiezer toont dan
37
+ // onterecht "Nog geen afbeeldingen". Nu een duidelijke melding.
38
+ (err) => toast({ title: "Kon mediabibliotheek niet laden", description: err.message, tone: "error" })
29
39
  );
30
- }, [open, siteId]);
40
+ }, [open, siteId, toast]);
31
41
  const filtered = useMemo(() => {
32
42
  const term = q.trim().toLowerCase();
33
43
  if (!term) return items;
@@ -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 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 const db = getDb();\n if (!db) return;\n const qy = query(collection(db, \"sites\", siteId, \"media\"), orderBy(\"createdAt\", \"desc\"));\n return onSnapshot(qy, (snap) =>\n setItems(snap.docs.map((d) => ({ id: d.id, ...(d.data() as Omit<MediaItem, \"id\">) }))),\n );\n }, [open, siteId]);\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, 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":[]}
@@ -80,7 +80,10 @@ function SettingsPanel({ siteId }) {
80
80
  if (!ok) return;
81
81
  setRestoring(true);
82
82
  try {
83
- await downloadBackup(true);
83
+ if (!await downloadBackup(true)) {
84
+ toast({ title: "Terugzetten afgebroken", description: "De veiligheidsback-up van de huidige stand mislukte; er is niets overschreven.", tone: "error" });
85
+ return;
86
+ }
84
87
  const app = getFirebaseApp();
85
88
  if (!app) throw new Error("Firebase niet geconfigureerd.");
86
89
  const fn = httpsCallable(getFunctions(app, functionsRegion()), "restoreSite");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/admin/settings/SettingsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { doc, onSnapshot, writeBatch } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { SiteSettingsSchema, toPublicSettings, type SiteSettings } from \"../../schema/settings\";\nimport { useModules } from \"../../modules/context\";\nimport type { ModulesSiteConfig } from \"../../modules/types\";\nimport { builtinThemePresets } from \"../../theme/presets\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { ArrayEditor } from \"../ui/ArrayEditor\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { ModuleSettingsForm } from \"./moduleSettingsForm\";\n\nexport function SettingsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const { defs } = useModules();\n const [s, setS] = useState<SiteSettings>({});\n const [loaded, setLoaded] = useState(false);\n const [exporting, setExporting] = useState(false);\n const [restoring, setRestoring] = useState(false);\n const restoreInput = useRef<HTMLInputElement>(null);\n\n async function downloadBackup(silent = false): Promise<boolean> {\n const app = getFirebaseApp();\n if (!app) return false;\n setExporting(true);\n try {\n const fn = httpsCallable<{ siteId: string }, Record<string, unknown>>(\n getFunctions(app, functionsRegion()),\n \"exportSite\",\n );\n const res = await fn({ siteId });\n const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `xicms-backup-${siteId}-${new Date().toISOString().slice(0, 10)}.json`;\n a.click();\n URL.revokeObjectURL(url);\n if (!silent) toast({ title: \"Back-up gedownload\", tone: \"success\" });\n return true;\n } catch (e) {\n toast({ title: \"Back-up mislukt\", description: (e as Error).message, tone: \"error\" });\n return false;\n } finally {\n setExporting(false);\n }\n }\n\n async function restoreFromFile(file: File) {\n let backup: Record<string, unknown>;\n try {\n backup = JSON.parse(await file.text()) as Record<string, unknown>;\n } catch {\n toast({ title: \"Ongeldig bestand\", description: \"Dit is geen geldig back-upbestand.\", tone: \"error\" });\n return;\n }\n if (backup._format !== \"xicms-export\") {\n toast({ title: \"Ongeldig bestand\", description: \"Dit lijkt geen XiCMS-back-up.\", tone: \"error\" });\n return;\n }\n if (backup.siteId && backup.siteId !== siteId) {\n const ok = await confirm({\n title: \"Back-up van een andere site\",\n description: `Deze back-up is van “${String(backup.siteId)}”, niet van deze site (${siteId}). Toch terugzetten?`,\n confirmLabel: \"Toch terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n }\n const ok = await confirm({\n title: \"Back-up terugzetten?\",\n description: `Dit VERVANGT alle huidige pagina's, nieuws, media en instellingen door de inhoud van de back-up${backup.exportedAt ? ` van ${new Date(String(backup.exportedAt)).toLocaleString(\"nl-NL\")}` : \"\"}. We maken eerst automatisch een back-up van de huidige stand.`,\n confirmLabel: \"Terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n setRestoring(true);\n try {\n // Veiligheid: eerst de huidige stand wegschrijven (als download) vóór we overschrijven.\n await downloadBackup(true);\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"restoreSite\");\n await fn({ siteId, backup });\n toast({ title: \"Back-up teruggezet\", description: \"Herlaad de pagina om alles bij te werken.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Terugzetten mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setRestoring(false);\n }\n }\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoaded(true);\n return;\n }\n return onSnapshot(doc(db, \"sites\", siteId, \"settings\", \"site\"), (snap) => {\n if (snap.exists()) setS(snap.data() as SiteSettings);\n setLoaded(true);\n });\n }, [siteId]);\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = SiteSettingsSchema.safeParse(s);\n if (!parsed.success) {\n toast({ title: \"Ongeldige instellingen\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Volledige (rol-only) settings + de publiek-veilige subset die de live site leest —\n // atomair in één batch, zodat `site` en `public` nooit uit sync kunnen raken.\n try {\n const batch = writeBatch(db);\n batch.set(doc(db, \"sites\", siteId, \"settings\", \"site\"), parsed.data, { merge: true });\n batch.set(doc(db, \"sites\", siteId, \"settings\", \"public\"), toPublicSettings(parsed.data), { merge: true });\n await batch.commit();\n toast({ title: \"Opgeslagen\", description: \"Wijzigingen zijn direct live.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n if (!loaded) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n const forms = s.forms ?? { mode: \"xicms\" as const };\n const theme = s.theme ?? { primary: \"#4f46e5\" };\n const footer = s.footer ?? {};\n\n // Hulpfuncties voor de module-instellingen.\n const moduleState = (id: string, defaultEnabled: boolean) => {\n const raw = (s.modules as ModulesSiteConfig | undefined)?.[id];\n if (typeof raw === \"boolean\") return { enabled: raw, config: {} as Record<string, unknown> };\n return { enabled: raw?.enabled ?? defaultEnabled, config: raw?.config ?? {} };\n };\n const setModule = (id: string, next: { enabled: boolean; config: Record<string, unknown> }) =>\n setS({ ...s, modules: { ...(s.modules as ModulesSiteConfig | undefined), [id]: next } });\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Algemeen</h3>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Sitenaam\"><Input value={s.name ?? \"\"} onChange={(e) => setS({ ...s, name: e.target.value })} /></Field>\n <Field label=\"Domein\"><Input value={s.domain ?? \"\"} onChange={(e) => setS({ ...s, domain: e.target.value })} /></Field>\n <Field label=\"Contact-e-mail\"><Input value={s.contactEmail ?? \"\"} onChange={(e) => setS({ ...s, contactEmail: e.target.value })} /></Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Huisstijl</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Thema-preset\" hint=\"Vult kleuren, lettertype en afronding in één keer in.\">\n <Select\n value=\"\"\n placeholder=\"Kies een preset…\"\n onValueChange={(id) => {\n const preset = builtinThemePresets.find((p) => p.id === id);\n if (preset) setS({ ...s, theme: { ...theme, ...preset.theme } });\n }}\n options={builtinThemePresets.map((p) => ({ value: p.id, label: p.label }))}\n />\n </Field>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Primaire kleur\"><Input value={theme.primary} onChange={(e) => setS({ ...s, theme: { ...theme, primary: e.target.value } })} /></Field>\n <Field label=\"Accentkleur\"><Input value={theme.accent ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, accent: e.target.value } })} /></Field>\n <Field label=\"Achtergrondkleur\"><Input value={theme.background ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, background: e.target.value } })} placeholder=\"#ffffff\" /></Field>\n <Field label=\"Tekstkleur\"><Input value={theme.foreground ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, foreground: e.target.value } })} placeholder=\"#0f172a\" /></Field>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">SEO (standaard)</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Standaard-omschrijving\">\n <Input value={s.seoDefaults?.description ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, description: e.target.value } })} />\n </Field>\n <Field label=\"OG-afbeelding (URL)\">\n <Input value={s.seoDefaults?.ogImage ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, ogImage: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Footer</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Kolommen met links, social-links en copyright. Wijzigingen zijn direct live.</p>\n <div className=\"grid gap-4\">\n <div>\n <span className=\"text-sm font-medium\">Kolommen</span>\n <ArrayEditor\n items={footer.columns ?? []}\n onChange={(columns) => setS({ ...s, footer: { ...footer, columns } })}\n newItem={() => ({ heading: \"\", links: [] })}\n addLabel=\"+ Kolom\"\n emptyLabel=\"Nog geen kolommen.\"\n renderRow={(col, update) => (\n <div className=\"grid gap-2\">\n <Field label=\"Kop\"><Input value={col.heading ?? \"\"} onChange={(e) => update({ heading: e.target.value })} /></Field>\n <ArrayEditor\n items={col.links ?? []}\n onChange={(links) => update({ links })}\n newItem={() => ({ label: \"\", href: \"\" })}\n addLabel=\"+ Link\"\n renderRow={(link, updL) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.label} placeholder=\"Label\" onChange={(e) => updL({ label: e.target.value })} />\n <Input value={link.href} placeholder=\"/pad of https://…\" onChange={(e) => updL({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n )}\n />\n </div>\n <div>\n <span className=\"text-sm font-medium\">Social-links</span>\n <ArrayEditor\n items={footer.social ?? []}\n onChange={(social) => setS({ ...s, footer: { ...footer, social } })}\n newItem={() => ({ platform: \"\", href: \"\" })}\n addLabel=\"+ Social-link\"\n renderRow={(link, update) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.platform} placeholder=\"bv. Instagram\" onChange={(e) => update({ platform: e.target.value })} />\n <Input value={link.href} placeholder=\"https://…\" onChange={(e) => update({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Copyright (leeg = automatisch)\">\n <Input value={footer.copyright ?? \"\"} onChange={(e) => setS({ ...s, footer: { ...footer, copyright: e.target.value } })} />\n </Field>\n <label className=\"flex items-center gap-2 self-end text-sm\">\n <Switch checked={footer.showAdminLink !== false} onCheckedChange={(v) => setS({ ...s, footer: { ...footer, showAdminLink: v } })} />\n “Beheer”-link tonen\n </label>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Modules</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Zet functies aan/uit en stel ze in. Uitgezette modules verbergen hun blokken en beheer-paneel.</p>\n <div className=\"grid gap-3\">\n {defs\n .filter((d) => !d.core)\n .map((d) => {\n const st = moduleState(d.id, d.defaultEnabled ?? true);\n return (\n <div key={d.id} className=\"rounded-lg border border-slate-200 p-3\">\n <label className=\"flex items-center justify-between gap-2\">\n <span>\n <strong className=\"text-sm\">{d.label}</strong>\n {d.description && <span className=\"block text-xs text-slate-500\">{d.description}</span>}\n </span>\n <Switch checked={st.enabled} onCheckedChange={(v) => setModule(d.id, { enabled: v, config: st.config })} />\n </label>\n {st.enabled && d.settingsSchema && (\n <div className=\"mt-3\">\n <ModuleSettingsForm\n schema={d.settingsSchema}\n value={st.config}\n onChange={(config) => setModule(d.id, { enabled: st.enabled, config })}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Formulier-e-mail</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Een provider verstuurt alleen vanaf een geverifieerd domein. Kies de XiCMS-afzender\n (werkt direct) of het eigen domein van de klant (vereist domeinverificatie).\n </p>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzender-modus\">\n <Select\n value={forms.mode}\n onValueChange={(v) => setS({ ...s, forms: { ...forms, mode: v as \"xicms\" | \"client\" } })}\n options={[\n { value: \"xicms\", label: \"XiCMS-domein (aanbevolen)\" },\n { value: \"client\", label: \"Eigen domein van de klant\" },\n ]}\n />\n </Field>\n <Field label=\"Van (from) e-mail\" hint=\"Moet een geverifieerd domein zijn.\">\n <Input value={forms.fromEmail ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromEmail: e.target.value } })} />\n </Field>\n <Field label=\"Van (naam)\"><Input value={forms.fromName ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromName: e.target.value } })} /></Field>\n <Field label=\"Notificaties naar\" hint=\"Meestal het mailadres van de klant.\">\n <Input value={forms.notifyTo ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, notifyTo: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Back-up &amp; herstel</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Sla alle content van deze site (pagina's, nieuws, formulieren, instellingen en je foto-galerij)\n op in één bestand. Bewaar dat veilig; je kunt het later weer terugzetten. De foto-bestanden\n zelf blijven in de cloud staan — de back-up bevat de teksten, instellingen en verwijzingen.\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"secondary\" onClick={() => void downloadBackup()} disabled={exporting || restoring}>\n {exporting ? \"Bezig…\" : \"Back-up downloaden\"}\n </Button>\n <Button variant=\"secondary\" onClick={() => restoreInput.current?.click()} disabled={restoring || exporting}>\n {restoring ? \"Terugzetten…\" : \"Back-up terugzetten…\"}\n </Button>\n <input\n ref={restoreInput}\n type=\"file\"\n accept=\"application/json,.json\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void restoreFromFile(f);\n e.target.value = \"\";\n }}\n />\n </div>\n <p className=\"mt-2 text-xs text-slate-400\">\n Terugzetten vervangt de huidige content door de back-up. We maken automatisch eerst een\n back-up van de huidige stand, zodat je nooit iets kwijtraakt.\n </p>\n </Card>\n\n <div className=\"flex justify-end\">\n <Button onClick={save}>Instellingen opslaan</Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["ok"]}
1
+ {"version":3,"sources":["../../../src/admin/settings/SettingsPanel.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { doc, onSnapshot, writeBatch } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\nimport { functionsRegion } from \"../../firebase/region\";\nimport { SiteSettingsSchema, toPublicSettings, type SiteSettings } from \"../../schema/settings\";\nimport { useModules } from \"../../modules/context\";\nimport type { ModulesSiteConfig } from \"../../modules/types\";\nimport { builtinThemePresets } from \"../../theme/presets\";\nimport { Button, Card, Field, Input } from \"../ui/primitives\";\nimport { Select } from \"../ui/select\";\nimport { Switch } from \"../ui/switch\";\nimport { ArrayEditor } from \"../ui/ArrayEditor\";\nimport { useToast } from \"../ui/toast\";\nimport { useConfirm } from \"../ui/ConfirmDialog\";\nimport { ModuleSettingsForm } from \"./moduleSettingsForm\";\n\nexport function SettingsPanel({ siteId }: { siteId: string }) {\n const { toast } = useToast();\n const confirm = useConfirm();\n const { defs } = useModules();\n const [s, setS] = useState<SiteSettings>({});\n const [loaded, setLoaded] = useState(false);\n const [exporting, setExporting] = useState(false);\n const [restoring, setRestoring] = useState(false);\n const restoreInput = useRef<HTMLInputElement>(null);\n\n async function downloadBackup(silent = false): Promise<boolean> {\n const app = getFirebaseApp();\n if (!app) return false;\n setExporting(true);\n try {\n const fn = httpsCallable<{ siteId: string }, Record<string, unknown>>(\n getFunctions(app, functionsRegion()),\n \"exportSite\",\n );\n const res = await fn({ siteId });\n const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: \"application/json\" });\n const url = URL.createObjectURL(blob);\n const a = document.createElement(\"a\");\n a.href = url;\n a.download = `xicms-backup-${siteId}-${new Date().toISOString().slice(0, 10)}.json`;\n a.click();\n URL.revokeObjectURL(url);\n if (!silent) toast({ title: \"Back-up gedownload\", tone: \"success\" });\n return true;\n } catch (e) {\n toast({ title: \"Back-up mislukt\", description: (e as Error).message, tone: \"error\" });\n return false;\n } finally {\n setExporting(false);\n }\n }\n\n async function restoreFromFile(file: File) {\n let backup: Record<string, unknown>;\n try {\n backup = JSON.parse(await file.text()) as Record<string, unknown>;\n } catch {\n toast({ title: \"Ongeldig bestand\", description: \"Dit is geen geldig back-upbestand.\", tone: \"error\" });\n return;\n }\n if (backup._format !== \"xicms-export\") {\n toast({ title: \"Ongeldig bestand\", description: \"Dit lijkt geen XiCMS-back-up.\", tone: \"error\" });\n return;\n }\n if (backup.siteId && backup.siteId !== siteId) {\n const ok = await confirm({\n title: \"Back-up van een andere site\",\n description: `Deze back-up is van “${String(backup.siteId)}”, niet van deze site (${siteId}). Toch terugzetten?`,\n confirmLabel: \"Toch terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n }\n const ok = await confirm({\n title: \"Back-up terugzetten?\",\n description: `Dit VERVANGT alle huidige pagina's, nieuws, media en instellingen door de inhoud van de back-up${backup.exportedAt ? ` van ${new Date(String(backup.exportedAt)).toLocaleString(\"nl-NL\")}` : \"\"}. We maken eerst automatisch een back-up van de huidige stand.`,\n confirmLabel: \"Terugzetten\",\n tone: \"danger\",\n });\n if (!ok) return;\n setRestoring(true);\n try {\n // Veiligheid: eerst de huidige stand wegschrijven (als download) vóór we overschrijven.\n // Lukt die back-up niet, dan NIET doorgaan met de destructieve restore (anders ben je de\n // huidige stand kwijt zonder vangnet). De gebruiker kan het daarna opnieuw proberen.\n if (!(await downloadBackup(true))) {\n toast({ title: \"Terugzetten afgebroken\", description: \"De veiligheidsback-up van de huidige stand mislukte; er is niets overschreven.\", tone: \"error\" });\n return;\n }\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Firebase niet geconfigureerd.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"restoreSite\");\n await fn({ siteId, backup });\n toast({ title: \"Back-up teruggezet\", description: \"Herlaad de pagina om alles bij te werken.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Terugzetten mislukt\", description: (e as Error).message, tone: \"error\" });\n } finally {\n setRestoring(false);\n }\n }\n\n useEffect(() => {\n const db = getDb();\n if (!db) {\n setLoaded(true);\n return;\n }\n return onSnapshot(doc(db, \"sites\", siteId, \"settings\", \"site\"), (snap) => {\n if (snap.exists()) setS(snap.data() as SiteSettings);\n setLoaded(true);\n });\n }, [siteId]);\n\n async function save() {\n const db = getDb();\n if (!db) return;\n const parsed = SiteSettingsSchema.safeParse(s);\n if (!parsed.success) {\n toast({ title: \"Ongeldige instellingen\", description: parsed.error.issues[0]?.message, tone: \"error\" });\n return;\n }\n // Volledige (rol-only) settings + de publiek-veilige subset die de live site leest —\n // atomair in één batch, zodat `site` en `public` nooit uit sync kunnen raken.\n try {\n const batch = writeBatch(db);\n batch.set(doc(db, \"sites\", siteId, \"settings\", \"site\"), parsed.data, { merge: true });\n batch.set(doc(db, \"sites\", siteId, \"settings\", \"public\"), toPublicSettings(parsed.data), { merge: true });\n await batch.commit();\n toast({ title: \"Opgeslagen\", description: \"Wijzigingen zijn direct live.\", tone: \"success\" });\n } catch (e) {\n toast({ title: \"Opslaan mislukt\", description: (e as Error).message, tone: \"error\" });\n }\n }\n\n if (!loaded) return <p className=\"text-sm text-slate-500\">Laden…</p>;\n\n const forms = s.forms ?? { mode: \"xicms\" as const };\n const theme = s.theme ?? { primary: \"#4f46e5\" };\n const footer = s.footer ?? {};\n\n // Hulpfuncties voor de module-instellingen.\n const moduleState = (id: string, defaultEnabled: boolean) => {\n const raw = (s.modules as ModulesSiteConfig | undefined)?.[id];\n if (typeof raw === \"boolean\") return { enabled: raw, config: {} as Record<string, unknown> };\n return { enabled: raw?.enabled ?? defaultEnabled, config: raw?.config ?? {} };\n };\n const setModule = (id: string, next: { enabled: boolean; config: Record<string, unknown> }) =>\n setS({ ...s, modules: { ...(s.modules as ModulesSiteConfig | undefined), [id]: next } });\n\n return (\n <div className=\"grid gap-4\">\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Algemeen</h3>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Sitenaam\"><Input value={s.name ?? \"\"} onChange={(e) => setS({ ...s, name: e.target.value })} /></Field>\n <Field label=\"Domein\"><Input value={s.domain ?? \"\"} onChange={(e) => setS({ ...s, domain: e.target.value })} /></Field>\n <Field label=\"Contact-e-mail\"><Input value={s.contactEmail ?? \"\"} onChange={(e) => setS({ ...s, contactEmail: e.target.value })} /></Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">Huisstijl</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Thema-preset\" hint=\"Vult kleuren, lettertype en afronding in één keer in.\">\n <Select\n value=\"\"\n placeholder=\"Kies een preset…\"\n onValueChange={(id) => {\n const preset = builtinThemePresets.find((p) => p.id === id);\n if (preset) setS({ ...s, theme: { ...theme, ...preset.theme } });\n }}\n options={builtinThemePresets.map((p) => ({ value: p.id, label: p.label }))}\n />\n </Field>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Primaire kleur\"><Input value={theme.primary} onChange={(e) => setS({ ...s, theme: { ...theme, primary: e.target.value } })} /></Field>\n <Field label=\"Accentkleur\"><Input value={theme.accent ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, accent: e.target.value } })} /></Field>\n <Field label=\"Achtergrondkleur\"><Input value={theme.background ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, background: e.target.value } })} placeholder=\"#ffffff\" /></Field>\n <Field label=\"Tekstkleur\"><Input value={theme.foreground ?? \"\"} onChange={(e) => setS({ ...s, theme: { ...theme, foreground: e.target.value } })} placeholder=\"#0f172a\" /></Field>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-3 text-base font-semibold\">SEO (standaard)</h3>\n <div className=\"grid gap-3\">\n <Field label=\"Standaard-omschrijving\">\n <Input value={s.seoDefaults?.description ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, description: e.target.value } })} />\n </Field>\n <Field label=\"OG-afbeelding (URL)\">\n <Input value={s.seoDefaults?.ogImage ?? \"\"} onChange={(e) => setS({ ...s, seoDefaults: { ...s.seoDefaults, ogImage: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Footer</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Kolommen met links, social-links en copyright. Wijzigingen zijn direct live.</p>\n <div className=\"grid gap-4\">\n <div>\n <span className=\"text-sm font-medium\">Kolommen</span>\n <ArrayEditor\n items={footer.columns ?? []}\n onChange={(columns) => setS({ ...s, footer: { ...footer, columns } })}\n newItem={() => ({ heading: \"\", links: [] })}\n addLabel=\"+ Kolom\"\n emptyLabel=\"Nog geen kolommen.\"\n renderRow={(col, update) => (\n <div className=\"grid gap-2\">\n <Field label=\"Kop\"><Input value={col.heading ?? \"\"} onChange={(e) => update({ heading: e.target.value })} /></Field>\n <ArrayEditor\n items={col.links ?? []}\n onChange={(links) => update({ links })}\n newItem={() => ({ label: \"\", href: \"\" })}\n addLabel=\"+ Link\"\n renderRow={(link, updL) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.label} placeholder=\"Label\" onChange={(e) => updL({ label: e.target.value })} />\n <Input value={link.href} placeholder=\"/pad of https://…\" onChange={(e) => updL({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n )}\n />\n </div>\n <div>\n <span className=\"text-sm font-medium\">Social-links</span>\n <ArrayEditor\n items={footer.social ?? []}\n onChange={(social) => setS({ ...s, footer: { ...footer, social } })}\n newItem={() => ({ platform: \"\", href: \"\" })}\n addLabel=\"+ Social-link\"\n renderRow={(link, update) => (\n <div className=\"grid grid-cols-2 gap-2\">\n <Input value={link.platform} placeholder=\"bv. Instagram\" onChange={(e) => update({ platform: e.target.value })} />\n <Input value={link.href} placeholder=\"https://…\" onChange={(e) => update({ href: e.target.value })} />\n </div>\n )}\n />\n </div>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Copyright (leeg = automatisch)\">\n <Input value={footer.copyright ?? \"\"} onChange={(e) => setS({ ...s, footer: { ...footer, copyright: e.target.value } })} />\n </Field>\n <label className=\"flex items-center gap-2 self-end text-sm\">\n <Switch checked={footer.showAdminLink !== false} onCheckedChange={(v) => setS({ ...s, footer: { ...footer, showAdminLink: v } })} />\n “Beheer”-link tonen\n </label>\n </div>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Modules</h3>\n <p className=\"mb-3 text-xs text-slate-500\">Zet functies aan/uit en stel ze in. Uitgezette modules verbergen hun blokken en beheer-paneel.</p>\n <div className=\"grid gap-3\">\n {defs\n .filter((d) => !d.core)\n .map((d) => {\n const st = moduleState(d.id, d.defaultEnabled ?? true);\n return (\n <div key={d.id} className=\"rounded-lg border border-slate-200 p-3\">\n <label className=\"flex items-center justify-between gap-2\">\n <span>\n <strong className=\"text-sm\">{d.label}</strong>\n {d.description && <span className=\"block text-xs text-slate-500\">{d.description}</span>}\n </span>\n <Switch checked={st.enabled} onCheckedChange={(v) => setModule(d.id, { enabled: v, config: st.config })} />\n </label>\n {st.enabled && d.settingsSchema && (\n <div className=\"mt-3\">\n <ModuleSettingsForm\n schema={d.settingsSchema}\n value={st.config}\n onChange={(config) => setModule(d.id, { enabled: st.enabled, config })}\n />\n </div>\n )}\n </div>\n );\n })}\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Formulier-e-mail</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Een provider verstuurt alleen vanaf een geverifieerd domein. Kies de XiCMS-afzender\n (werkt direct) of het eigen domein van de klant (vereist domeinverificatie).\n </p>\n <div className=\"grid gap-3 md:grid-cols-2\">\n <Field label=\"Afzender-modus\">\n <Select\n value={forms.mode}\n onValueChange={(v) => setS({ ...s, forms: { ...forms, mode: v as \"xicms\" | \"client\" } })}\n options={[\n { value: \"xicms\", label: \"XiCMS-domein (aanbevolen)\" },\n { value: \"client\", label: \"Eigen domein van de klant\" },\n ]}\n />\n </Field>\n <Field label=\"Van (from) e-mail\" hint=\"Moet een geverifieerd domein zijn.\">\n <Input value={forms.fromEmail ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromEmail: e.target.value } })} />\n </Field>\n <Field label=\"Van (naam)\"><Input value={forms.fromName ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, fromName: e.target.value } })} /></Field>\n <Field label=\"Notificaties naar\" hint=\"Meestal het mailadres van de klant.\">\n <Input value={forms.notifyTo ?? \"\"} onChange={(e) => setS({ ...s, forms: { ...forms, notifyTo: e.target.value } })} />\n </Field>\n </div>\n </Card>\n\n <Card>\n <h3 className=\"mb-1 text-base font-semibold\">Back-up &amp; herstel</h3>\n <p className=\"mb-3 text-xs text-slate-500\">\n Sla alle content van deze site (pagina's, nieuws, formulieren, instellingen en je foto-galerij)\n op in één bestand. Bewaar dat veilig; je kunt het later weer terugzetten. De foto-bestanden\n zelf blijven in de cloud staan — de back-up bevat de teksten, instellingen en verwijzingen.\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"secondary\" onClick={() => void downloadBackup()} disabled={exporting || restoring}>\n {exporting ? \"Bezig…\" : \"Back-up downloaden\"}\n </Button>\n <Button variant=\"secondary\" onClick={() => restoreInput.current?.click()} disabled={restoring || exporting}>\n {restoring ? \"Terugzetten…\" : \"Back-up terugzetten…\"}\n </Button>\n <input\n ref={restoreInput}\n type=\"file\"\n accept=\"application/json,.json\"\n hidden\n onChange={(e) => {\n const f = e.target.files?.[0];\n if (f) void restoreFromFile(f);\n e.target.value = \"\";\n }}\n />\n </div>\n <p className=\"mt-2 text-xs text-slate-400\">\n Terugzetten vervangt de huidige content door de back-up. We maken automatisch eerst een\n back-up van de huidige stand, zodat je nooit iets kwijtraakt.\n </p>\n </Card>\n\n <div className=\"flex justify-end\">\n <Button onClick={save}>Instellingen opslaan</Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["ok"]}