@xiwebdesign/xicms-core 7.1.0 → 7.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/XiCMSProvider.d.ts +2 -8
- package/dist/XiCMSProvider.js +1 -8
- package/dist/XiCMSProvider.js.map +1 -1
- package/dist/admin/AdminApp.js +7 -12
- package/dist/admin/AdminApp.js.map +1 -1
- package/dist/admin/NewsEditor.d.ts +8 -1
- package/dist/admin/NewsEditor.js +19 -18
- package/dist/admin/NewsEditor.js.map +1 -1
- package/dist/admin/SuperadminPanel.js +6 -7
- package/dist/admin/SuperadminPanel.js.map +1 -1
- package/dist/admin/customBlocks/CustomBlocksPanel.js +3 -4
- package/dist/admin/customBlocks/CustomBlocksPanel.js.map +1 -1
- package/dist/admin/lib/pagesClient.js +3 -4
- package/dist/admin/lib/pagesClient.js.map +1 -1
- package/dist/admin/lib/symbolsClient.js +3 -4
- package/dist/admin/lib/symbolsClient.js.map +1 -1
- package/dist/admin/pages/PagesPanel.js +5 -2
- package/dist/admin/pages/PagesPanel.js.map +1 -1
- package/dist/admin/settings/MailSettings.js +2 -3
- package/dist/admin/settings/MailSettings.js.map +1 -1
- package/dist/admin/settings/MailTemplates.js +2 -3
- package/dist/admin/settings/MailTemplates.js.map +1 -1
- package/dist/admin/settings/SettingsPanel.js +250 -225
- package/dist/admin/settings/SettingsPanel.js.map +1 -1
- package/dist/admin/settings/SnapshotsCard.js +3 -4
- package/dist/admin/settings/SnapshotsCard.js.map +1 -1
- package/dist/admin/shop/lib.js +2 -3
- package/dist/admin/shop/lib.js.map +1 -1
- package/dist/admin/visual/VisualEditor.js +239 -103
- package/dist/admin/visual/VisualEditor.js.map +1 -1
- package/dist/admin.css +1 -1
- package/dist/blocks/builtins/Agenda.js +6 -6
- package/dist/blocks/builtins/Agenda.js.map +1 -1
- package/dist/blocks/builtins/Banner.js +1 -1
- package/dist/blocks/builtins/Banner.js.map +1 -1
- package/dist/blocks/builtins/Breadcrumbs.js +5 -2
- package/dist/blocks/builtins/Breadcrumbs.js.map +1 -1
- package/dist/blocks/builtins/Cards.js +2 -2
- package/dist/blocks/builtins/Cards.js.map +1 -1
- package/dist/blocks/builtins/Contact.js +3 -3
- package/dist/blocks/builtins/Contact.js.map +1 -1
- package/dist/blocks/builtins/Cta.js +3 -3
- package/dist/blocks/builtins/Cta.js.map +1 -1
- package/dist/blocks/builtins/Downloads.js +5 -5
- package/dist/blocks/builtins/Downloads.js.map +1 -1
- package/dist/blocks/builtins/Elements.js +8 -5
- package/dist/blocks/builtins/Elements.js.map +1 -1
- package/dist/blocks/builtins/Hero.js +1 -1
- package/dist/blocks/builtins/Hero.js.map +1 -1
- package/dist/blocks/builtins/Jobs.js +4 -1
- package/dist/blocks/builtins/Jobs.js.map +1 -1
- package/dist/blocks/builtins/MapBlock.js +11 -11
- package/dist/blocks/builtins/MapBlock.js.map +1 -1
- package/dist/blocks/builtins/Newsletter.js +3 -2
- package/dist/blocks/builtins/Newsletter.js.map +1 -1
- package/dist/blocks/builtins/OpeningHours.js +2 -2
- package/dist/blocks/builtins/OpeningHours.js.map +1 -1
- package/dist/blocks/builtins/PdfBlock.js +2 -2
- package/dist/blocks/builtins/PdfBlock.js.map +1 -1
- package/dist/blocks/builtins/Portfolio.js +4 -4
- package/dist/blocks/builtins/Portfolio.js.map +1 -1
- package/dist/blocks/builtins/Pricing.js +3 -3
- package/dist/blocks/builtins/Pricing.js.map +1 -1
- package/dist/blocks/builtins/Sections.js +7 -7
- package/dist/blocks/builtins/Sections.js.map +1 -1
- package/dist/blocks/builtins/Team.js +3 -3
- package/dist/blocks/builtins/Team.js.map +1 -1
- package/dist/blocks/builtins/Widgets.js +28 -28
- package/dist/blocks/builtins/Widgets.js.map +1 -1
- package/dist/blocks/builtins/lib/submitForm.js +3 -4
- package/dist/blocks/builtins/lib/submitForm.js.map +1 -1
- package/dist/blocks/ui/index.d.ts +8 -1
- package/dist/blocks/ui/index.js +3 -2
- package/dist/blocks/ui/index.js.map +1 -1
- package/dist/components/EditBar.d.ts +9 -1
- package/dist/components/EditBar.js +6 -5
- package/dist/components/EditBar.js.map +1 -1
- package/dist/components/PageView.js +3 -5
- package/dist/components/PageView.js.map +1 -1
- package/dist/firebase/admin.js +4 -0
- package/dist/firebase/admin.js.map +1 -1
- package/dist/firebase/client.js +23 -5
- package/dist/firebase/client.js.map +1 -1
- package/dist/firebase/emulators.d.ts +9 -0
- package/dist/firebase/emulators.js +24 -0
- package/dist/firebase/emulators.js.map +1 -0
- package/dist/firebase/functionsClient.d.ts +18 -0
- package/dist/firebase/functionsClient.js +17 -0
- package/dist/firebase/functionsClient.js.map +1 -0
- package/dist/lock/DemoLock.js +19 -2
- package/dist/lock/DemoLock.js.map +1 -1
- package/dist/modules/shop/cartStore.js +2 -1
- package/dist/modules/shop/cartStore.js.map +1 -1
- package/dist/modules/shop/lib.js +9 -10
- package/dist/modules/shop/lib.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/pages.d.ts +11 -0
- package/dist/server/pages.js +47 -0
- package/dist/server/pages.js.map +1 -1
- package/dist/server/version.d.ts +7 -0
- package/dist/server/version.js +2 -1
- package/dist/server/version.js.map +1 -1
- package/dist/styles.css +130 -29
- package/dist/theme/color.d.ts +17 -0
- package/dist/theme/color.js +12 -0
- package/dist/theme/color.js.map +1 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/firebase/client.js
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { initializeApp, getApps, getApp } from "firebase/app";
|
|
3
|
-
import { getAuth } from "firebase/auth";
|
|
4
|
-
import { getFirestore } from "firebase/firestore";
|
|
5
|
-
import { getStorage } from "firebase/storage";
|
|
3
|
+
import { getAuth, connectAuthEmulator } from "firebase/auth";
|
|
4
|
+
import { getFirestore, connectFirestoreEmulator } from "firebase/firestore";
|
|
5
|
+
import { getStorage, connectStorageEmulator } from "firebase/storage";
|
|
6
6
|
import { isFirebaseConfigured, readFirebaseConfig } from "./config.js";
|
|
7
|
+
import { emulatorHosts } from "./emulators.js";
|
|
7
8
|
const readConfig = readFirebaseConfig;
|
|
9
|
+
let emulatorsVerbonden = false;
|
|
10
|
+
function verbindEmulators(app) {
|
|
11
|
+
if (emulatorsVerbonden) return;
|
|
12
|
+
const em = emulatorHosts();
|
|
13
|
+
if (!em) return;
|
|
14
|
+
emulatorsVerbonden = true;
|
|
15
|
+
connectAuthEmulator(getAuth(app), `http://${em.host}:${em.auth}`, { disableWarnings: true });
|
|
16
|
+
connectFirestoreEmulator(getFirestore(app), em.host, em.firestore);
|
|
17
|
+
if (readConfig().storageBucket) connectStorageEmulator(getStorage(app), em.host, em.storage);
|
|
18
|
+
console.info(`[xicms] Firebase-emulators actief op ${em.host} (auth ${em.auth}, firestore ${em.firestore}).`);
|
|
19
|
+
}
|
|
8
20
|
function getFirebaseApp() {
|
|
9
21
|
if (!isFirebaseConfigured()) return null;
|
|
10
|
-
if (getApps().length)
|
|
11
|
-
|
|
22
|
+
if (getApps().length) {
|
|
23
|
+
const bestaand = getApp();
|
|
24
|
+
verbindEmulators(bestaand);
|
|
25
|
+
return bestaand;
|
|
26
|
+
}
|
|
27
|
+
const app = initializeApp(readConfig());
|
|
28
|
+
verbindEmulators(app);
|
|
29
|
+
return app;
|
|
12
30
|
}
|
|
13
31
|
function getFirebaseAuth() {
|
|
14
32
|
const app = getFirebaseApp();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/firebase/client.ts"],"sourcesContent":["\"use client\";\n\nimport { initializeApp, getApps, getApp, type FirebaseApp } from \"firebase/app\";\nimport { getAuth, type Auth } from \"firebase/auth\";\nimport { getFirestore, type Firestore } from \"firebase/firestore\";\nimport { getStorage, type FirebaseStorage } from \"firebase/storage\";\nimport { isFirebaseConfigured, readFirebaseConfig } from \"./config\";\n\n/**\n * Client-side Firebase, volledig env-gedreven (NEXT_PUBLIC_*). Als de env niet is ingevuld\n * geven alle helpers `null` terug, zodat een site ook zónder Firebase blijft werken (dan\n * toont hij alleen de standaardteksten en is bewerken uitgeschakeld).\n */\nconst readConfig = readFirebaseConfig;\n\n// Blijft hier geëxporteerd zodat bestaande imports werken; de SDK-vrije bron staat in ./config.\nexport { isFirebaseConfigured };\n\nexport function getFirebaseApp(): FirebaseApp | null {\n if (!isFirebaseConfigured()) return null;\n if (getApps().length)
|
|
1
|
+
{"version":3,"sources":["../../src/firebase/client.ts"],"sourcesContent":["\"use client\";\n\nimport { initializeApp, getApps, getApp, type FirebaseApp } from \"firebase/app\";\nimport { getAuth, connectAuthEmulator, type Auth } from \"firebase/auth\";\nimport { getFirestore, connectFirestoreEmulator, type Firestore } from \"firebase/firestore\";\nimport { getStorage, connectStorageEmulator, type FirebaseStorage } from \"firebase/storage\";\nimport { isFirebaseConfigured, readFirebaseConfig } from \"./config\";\nimport { emulatorHosts } from \"./emulators\";\n\n/**\n * Client-side Firebase, volledig env-gedreven (NEXT_PUBLIC_*). Als de env niet is ingevuld\n * geven alle helpers `null` terug, zodat een site ook zónder Firebase blijft werken (dan\n * toont hij alleen de standaardteksten en is bewerken uitgeschakeld).\n */\nconst readConfig = readFirebaseConfig;\n\n// Blijft hier geëxporteerd zodat bestaande imports werken; de SDK-vrije bron staat in ./config.\nexport { isFirebaseConfigured };\n\n/**\n * Verbind auth/firestore/storage met de lokale emulators. Eén keer per app-instantie: de SDK gooit\n * bij een tweede aanroep, en een hot reload komt hier opnieuw langs.\n *\n * `firebase/functions` staat hier bewust NIET tussen — dat pakket wordt overal dynamisch geladen om\n * het uit de publieke bundel te houden (zie PageView/submitForm). De callables verbinden daarom\n * langs `functionsClient.ts`, met dezelfde instelling.\n */\nlet emulatorsVerbonden = false;\nfunction verbindEmulators(app: FirebaseApp): void {\n if (emulatorsVerbonden) return;\n const em = emulatorHosts();\n if (!em) return;\n emulatorsVerbonden = true;\n connectAuthEmulator(getAuth(app), `http://${em.host}:${em.auth}`, { disableWarnings: true });\n connectFirestoreEmulator(getFirestore(app), em.host, em.firestore);\n if (readConfig().storageBucket) connectStorageEmulator(getStorage(app), em.host, em.storage);\n console.info(`[xicms] Firebase-emulators actief op ${em.host} (auth ${em.auth}, firestore ${em.firestore}).`);\n}\n\nexport function getFirebaseApp(): FirebaseApp | null {\n if (!isFirebaseConfigured()) return null;\n if (getApps().length) {\n const bestaand = getApp();\n verbindEmulators(bestaand);\n return bestaand;\n }\n const app = initializeApp(readConfig() as Record<string, string>);\n verbindEmulators(app);\n return app;\n}\n\nexport function getFirebaseAuth(): Auth | null {\n const app = getFirebaseApp();\n return app ? getAuth(app) : null;\n}\n\nexport function getDb(): Firestore | null {\n const app = getFirebaseApp();\n return app ? getFirestore(app) : null;\n}\n\nexport function getBucket(): FirebaseStorage | null {\n const app = getFirebaseApp();\n // Zonder storageBucket geeft getStorage() een client zónder default bucket terug, waarna\n // storageRef() een cryptisch `storage/no-default-bucket` gooit (of de upload stil hangt). Geef\n // hier `null` zodat de aanroeper de nette \"Storage niet geconfigureerd\"-melding kan tonen.\n if (!app || !readConfig().storageBucket) return null;\n return getStorage(app);\n}\n"],"mappings":";AAEA,SAAS,eAAe,SAAS,cAAgC;AACjE,SAAS,SAAS,2BAAsC;AACxD,SAAS,cAAc,gCAAgD;AACvE,SAAS,YAAY,8BAAoD;AACzE,SAAS,sBAAsB,0BAA0B;AACzD,SAAS,qBAAqB;AAO9B,MAAM,aAAa;AAanB,IAAI,qBAAqB;AACzB,SAAS,iBAAiB,KAAwB;AAChD,MAAI,mBAAoB;AACxB,QAAM,KAAK,cAAc;AACzB,MAAI,CAAC,GAAI;AACT,uBAAqB;AACrB,sBAAoB,QAAQ,GAAG,GAAG,UAAU,GAAG,IAAI,IAAI,GAAG,IAAI,IAAI,EAAE,iBAAiB,KAAK,CAAC;AAC3F,2BAAyB,aAAa,GAAG,GAAG,GAAG,MAAM,GAAG,SAAS;AACjE,MAAI,WAAW,EAAE,cAAe,wBAAuB,WAAW,GAAG,GAAG,GAAG,MAAM,GAAG,OAAO;AAC3F,UAAQ,KAAK,wCAAwC,GAAG,IAAI,UAAU,GAAG,IAAI,eAAe,GAAG,SAAS,IAAI;AAC9G;AAEO,SAAS,iBAAqC;AACnD,MAAI,CAAC,qBAAqB,EAAG,QAAO;AACpC,MAAI,QAAQ,EAAE,QAAQ;AACpB,UAAM,WAAW,OAAO;AACxB,qBAAiB,QAAQ;AACzB,WAAO;AAAA,EACT;AACA,QAAM,MAAM,cAAc,WAAW,CAA2B;AAChE,mBAAiB,GAAG;AACpB,SAAO;AACT;AAEO,SAAS,kBAA+B;AAC7C,QAAM,MAAM,eAAe;AAC3B,SAAO,MAAM,QAAQ,GAAG,IAAI;AAC9B;AAEO,SAAS,QAA0B;AACxC,QAAM,MAAM,eAAe;AAC3B,SAAO,MAAM,aAAa,GAAG,IAAI;AACnC;AAEO,SAAS,YAAoC;AAClD,QAAM,MAAM,eAAe;AAI3B,MAAI,CAAC,OAAO,CAAC,WAAW,EAAE,cAAe,QAAO;AAChD,SAAO,WAAW,GAAG;AACvB;","names":[]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface EmulatorHosts {
|
|
2
|
+
host: string;
|
|
3
|
+
auth: number;
|
|
4
|
+
firestore: number;
|
|
5
|
+
storage: number;
|
|
6
|
+
functions: number;
|
|
7
|
+
}
|
|
8
|
+
/** De emulator-instelling, of `null` wanneer er (terecht) niet tegen een emulator gepraat wordt. */
|
|
9
|
+
export declare function emulatorHosts(): EmulatorHosts | null;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const LOKAAL = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "0.0.0.0"]);
|
|
2
|
+
function emulatorHosts() {
|
|
3
|
+
const vlag = process.env.NEXT_PUBLIC_FIREBASE_EMULATORS;
|
|
4
|
+
if (vlag !== "1" && vlag !== "true") return null;
|
|
5
|
+
if (typeof window === "undefined") return null;
|
|
6
|
+
if (!LOKAAL.has(window.location.hostname)) {
|
|
7
|
+
console.warn(
|
|
8
|
+
"[xicms] NEXT_PUBLIC_FIREBASE_EMULATORS staat aan, maar dit is geen localhost \u2014 genegeerd. Deze vlag hoort alleen in .env.local te staan, nooit in een build die je uitrolt."
|
|
9
|
+
);
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const poort = (waarde, standaard) => Number(waarde) || standaard;
|
|
13
|
+
return {
|
|
14
|
+
host: "127.0.0.1",
|
|
15
|
+
auth: poort(process.env.NEXT_PUBLIC_FIREBASE_EMULATOR_AUTH_PORT, 9099),
|
|
16
|
+
firestore: poort(process.env.NEXT_PUBLIC_FIREBASE_EMULATOR_FIRESTORE_PORT, 8080),
|
|
17
|
+
storage: poort(process.env.NEXT_PUBLIC_FIREBASE_EMULATOR_STORAGE_PORT, 9199),
|
|
18
|
+
functions: poort(process.env.NEXT_PUBLIC_FIREBASE_EMULATOR_FUNCTIONS_PORT, 5001)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
emulatorHosts
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=emulators.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/firebase/emulators.ts"],"sourcesContent":["/**\n * Lokale ontwikkeling tegen de Firebase-emulators (`npm run emulators`).\n *\n * SDK-VRIJ, net als `config.ts`: dit bestand leest alleen env en mag daarom overal geïmporteerd\n * worden zonder de Firebase-SDK de publieke bundel in te trekken (zie de uitleg in `config.ts`).\n *\n * Aanzetten met `NEXT_PUBLIC_FIREBASE_EMULATORS=1` in `.env.local`. De poorten volgen\n * `firebase/firebase.json`; wijk je daarvan af, dan zijn ze per stuk te overschrijven.\n *\n * TWEE VEILIGHEIDSGRENZEN, allebei bewust:\n *\n * 1. **Alleen op localhost.** Een `NEXT_PUBLIC_*`-waarde wordt bij het bouwen ingebakken. Zou zo'n\n * build per ongeluk naar Vercel gaan, dan praatte de site tegen een emulator die daar niet\n * bestaat — alles kapot, zonder duidelijke oorzaak. De hostname-eis maakt dat onmogelijk.\n * 2. **Alles of niets.** Auth, Firestore, Storage én Functions gaan samen naar de emulator. Zou\n * alleen Firestore lokaal draaien en een callable naar het ECHTE project gaan, dan schrijf je\n * productiedata weg terwijl je denkt dat je lokaal speelt — precies het soort fout dat stil\n * faalt. `callable()` in `functionsClient.ts` verbindt daarom langs dezelfde weg.\n */\nconst LOKAAL = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"0.0.0.0\"]);\n\nexport interface EmulatorHosts {\n host: string;\n auth: number;\n firestore: number;\n storage: number;\n functions: number;\n}\n\n/** De emulator-instelling, of `null` wanneer er (terecht) niet tegen een emulator gepraat wordt. */\nexport function emulatorHosts(): EmulatorHosts | null {\n const vlag = process.env.NEXT_PUBLIC_FIREBASE_EMULATORS;\n if (vlag !== \"1\" && vlag !== \"true\") return null;\n // Server-side rendering: geen window, dus geen hostname om op te controleren. De browser doet de\n // verbinding; hier gebeurt niets.\n if (typeof window === \"undefined\") return null;\n if (!LOKAAL.has(window.location.hostname)) {\n console.warn(\n \"[xicms] NEXT_PUBLIC_FIREBASE_EMULATORS staat aan, maar dit is geen localhost — genegeerd. \" +\n \"Deze vlag hoort alleen in .env.local te staan, nooit in een build die je uitrolt.\",\n );\n return null;\n }\n // LETTERLIJK uitschrijven, geen `process.env[naam]`: Next vervangt alleen exact geschreven\n // `process.env.NEXT_PUBLIC_X` door zijn waarde. Met een berekende sleutel blijft het in de\n // browser `undefined` en val je stilzwijgend terug op de standaardpoort — je verbindt dan met\n // wat daar toevallig luistert.\n const poort = (waarde: string | undefined, standaard: number) => Number(waarde) || standaard;\n return {\n host: \"127.0.0.1\",\n auth: poort(process.env.NEXT_PUBLIC_FIREBASE_EMULATOR_AUTH_PORT, 9099),\n firestore: poort(process.env.NEXT_PUBLIC_FIREBASE_EMULATOR_FIRESTORE_PORT, 8080),\n storage: poort(process.env.NEXT_PUBLIC_FIREBASE_EMULATOR_STORAGE_PORT, 9199),\n functions: poort(process.env.NEXT_PUBLIC_FIREBASE_EMULATOR_FUNCTIONS_PORT, 5001),\n };\n}\n"],"mappings":"AAmBA,MAAM,SAAS,oBAAI,IAAI,CAAC,aAAa,aAAa,SAAS,SAAS,CAAC;AAW9D,SAAS,gBAAsC;AACpD,QAAM,OAAO,QAAQ,IAAI;AACzB,MAAI,SAAS,OAAO,SAAS,OAAQ,QAAO;AAG5C,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,MAAI,CAAC,OAAO,IAAI,OAAO,SAAS,QAAQ,GAAG;AACzC,YAAQ;AAAA,MACN;AAAA,IAEF;AACA,WAAO;AAAA,EACT;AAKA,QAAM,QAAQ,CAAC,QAA4B,cAAsB,OAAO,MAAM,KAAK;AACnF,SAAO;AAAA,IACL,MAAM;AAAA,IACN,MAAM,MAAM,QAAQ,IAAI,yCAAyC,IAAI;AAAA,IACrE,WAAW,MAAM,QAAQ,IAAI,8CAA8C,IAAI;AAAA,IAC/E,SAAS,MAAM,QAAQ,IAAI,4CAA4C,IAAI;AAAA,IAC3E,WAAW,MAAM,QAAQ,IAAI,8CAA8C,IAAI;AAAA,EACjF;AACF;","names":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eén plek waar een Cloud-Function-callable vandaan komt.
|
|
3
|
+
*
|
|
4
|
+
* Hiervoor stond op eenentwintig plekken hetzelfde regeltje
|
|
5
|
+
* `httpsCallable(getFunctions(app, functionsRegion()), "naam")`. Dat was al duplicatie, maar het
|
|
6
|
+
* werd een risico toen de emulators erbij kwamen: verbind je Firestore lokaal en laat je de
|
|
7
|
+
* callables ongemoeid, dan lees je uit de emulator terwijl je SCHRIJFT naar het echte project —
|
|
8
|
+
* en dat merk je pas als de productiedata al veranderd is. Door de callables hier te maken kan
|
|
9
|
+
* die scheefstand niet ontstaan.
|
|
10
|
+
*
|
|
11
|
+
* Dit bestand importeert `firebase/functions` statisch, en wordt daarom zélf overal dynamisch
|
|
12
|
+
* geïmporteerd waar het publieke pad langskomt (PageView, submitForm) — zo blijft de SDK uit de
|
|
13
|
+
* bundel van een gewone bezoeker.
|
|
14
|
+
*/
|
|
15
|
+
import type { FirebaseApp } from "firebase/app";
|
|
16
|
+
import { type HttpsCallable } from "firebase/functions";
|
|
17
|
+
/** Een getypeerde callable voor `naam`, in de juiste regio en (lokaal) tegen de emulator. */
|
|
18
|
+
export declare function callable<Req = unknown, Res = unknown>(app: FirebaseApp, naam: string): HttpsCallable<Req, Res>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getFunctions, httpsCallable, connectFunctionsEmulator } from "firebase/functions";
|
|
2
|
+
import { functionsRegion } from "./region.js";
|
|
3
|
+
import { emulatorHosts } from "./emulators.js";
|
|
4
|
+
const verbonden = /* @__PURE__ */ new WeakSet();
|
|
5
|
+
function callable(app, naam) {
|
|
6
|
+
const fns = getFunctions(app, functionsRegion());
|
|
7
|
+
if (!verbonden.has(fns)) {
|
|
8
|
+
verbonden.add(fns);
|
|
9
|
+
const em = emulatorHosts();
|
|
10
|
+
if (em) connectFunctionsEmulator(fns, em.host, em.functions);
|
|
11
|
+
}
|
|
12
|
+
return httpsCallable(fns, naam);
|
|
13
|
+
}
|
|
14
|
+
export {
|
|
15
|
+
callable
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=functionsClient.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/firebase/functionsClient.ts"],"sourcesContent":["/**\n * Eén plek waar een Cloud-Function-callable vandaan komt.\n *\n * Hiervoor stond op eenentwintig plekken hetzelfde regeltje\n * `httpsCallable(getFunctions(app, functionsRegion()), \"naam\")`. Dat was al duplicatie, maar het\n * werd een risico toen de emulators erbij kwamen: verbind je Firestore lokaal en laat je de\n * callables ongemoeid, dan lees je uit de emulator terwijl je SCHRIJFT naar het echte project —\n * en dat merk je pas als de productiedata al veranderd is. Door de callables hier te maken kan\n * die scheefstand niet ontstaan.\n *\n * Dit bestand importeert `firebase/functions` statisch, en wordt daarom zélf overal dynamisch\n * geïmporteerd waar het publieke pad langskomt (PageView, submitForm) — zo blijft de SDK uit de\n * bundel van een gewone bezoeker.\n */\nimport type { FirebaseApp } from \"firebase/app\";\nimport { getFunctions, httpsCallable, connectFunctionsEmulator, type HttpsCallable } from \"firebase/functions\";\nimport { functionsRegion } from \"./region\";\nimport { emulatorHosts } from \"./emulators\";\n\n// `getFunctions(app, regio)` geeft per app+regio dezelfde instantie terug, dus één verbinding volstaat.\nconst verbonden = new WeakSet<object>();\n\n/** Een getypeerde callable voor `naam`, in de juiste regio en (lokaal) tegen de emulator. */\nexport function callable<Req = unknown, Res = unknown>(app: FirebaseApp, naam: string): HttpsCallable<Req, Res> {\n const fns = getFunctions(app, functionsRegion());\n if (!verbonden.has(fns)) {\n verbonden.add(fns);\n const em = emulatorHosts();\n if (em) connectFunctionsEmulator(fns, em.host, em.functions);\n }\n return httpsCallable<Req, Res>(fns, naam);\n}\n"],"mappings":"AAeA,SAAS,cAAc,eAAe,gCAAoD;AAC1F,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAG9B,MAAM,YAAY,oBAAI,QAAgB;AAG/B,SAAS,SAAuC,KAAkB,MAAuC;AAC9G,QAAM,MAAM,aAAa,KAAK,gBAAgB,CAAC;AAC/C,MAAI,CAAC,UAAU,IAAI,GAAG,GAAG;AACvB,cAAU,IAAI,GAAG;AACjB,UAAM,KAAK,cAAc;AACzB,QAAI,GAAI,0BAAyB,KAAK,GAAG,MAAM,GAAG,SAAS;AAAA,EAC7D;AACA,SAAO,cAAwB,KAAK,IAAI;AAC1C;","names":[]}
|
package/dist/lock/DemoLock.js
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { useSiteConfig } from "../config.js";
|
|
5
5
|
function DemoLock() {
|
|
6
6
|
const { mode, contactEmail, name } = useSiteConfig();
|
|
7
7
|
const [open, setOpen] = useState(true);
|
|
8
|
+
const bannerRef = useRef(null);
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
const el = bannerRef.current;
|
|
11
|
+
const root = document.documentElement;
|
|
12
|
+
if (!el) {
|
|
13
|
+
root.style.removeProperty("--xicms-bottombar");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const meet = () => root.style.setProperty("--xicms-bottombar", `${el.offsetHeight}px`);
|
|
17
|
+
meet();
|
|
18
|
+
const ro = new ResizeObserver(meet);
|
|
19
|
+
ro.observe(el);
|
|
20
|
+
return () => {
|
|
21
|
+
ro.disconnect();
|
|
22
|
+
root.style.removeProperty("--xicms-bottombar");
|
|
23
|
+
};
|
|
24
|
+
}, [open, mode]);
|
|
8
25
|
if (mode !== "demo") return null;
|
|
9
26
|
const mailto = contactEmail ? `mailto:${contactEmail}?subject=${encodeURIComponent(`Demo ${name} live zetten`)}` : "https://www.xiwebdesign.nl/#contact";
|
|
10
27
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
11
28
|
/* @__PURE__ */ jsx("div", { className: "xicms-demo-ribbon", children: "DEMO" }),
|
|
12
|
-
open && /* @__PURE__ */ jsxs("div", { className: "xicms-demo-banner", role: "status", children: [
|
|
29
|
+
open && /* @__PURE__ */ jsxs("div", { className: "xicms-demo-banner", role: "status", ref: bannerRef, children: [
|
|
13
30
|
/* @__PURE__ */ jsxs("span", { children: [
|
|
14
31
|
"Dit is een ",
|
|
15
32
|
/* @__PURE__ */ jsx("strong", { children: "demo" }),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lock/DemoLock.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState } from \"react\";\nimport { useSiteConfig } from \"../config\";\n\n/**\n * Demo-markering die wordt getoond zolang `mode: \"demo\"`. Niet-blokkerend (de demo blijft\n * bekijkbaar), maar duidelijk gemarkeerd met een CTA om de site live te laten zetten.\n */\nexport function DemoLock() {\n const { mode, contactEmail, name } = useSiteConfig();\n const [open, setOpen] = useState(true);\n\n if (mode !== \"demo\") return null;\n\n const mailto = contactEmail\n ? `mailto:${contactEmail}?subject=${encodeURIComponent(`Demo ${name} live zetten`)}`\n : \"https://www.xiwebdesign.nl/#contact\";\n\n return (\n <>\n <div className=\"xicms-demo-ribbon\">DEMO</div>\n {open && (\n <div className=\"xicms-demo-banner\" role=\"status\">\n <span>\n Dit is een <strong>demo</strong> gemaakt door Xi Webdesign. Wil je deze site echt\n live?\n </span>\n <a href={mailto} className=\"xicms-btn\">Zet live</a>\n <button\n type=\"button\"\n aria-label=\"Sluiten\"\n className=\"xicms-btn xicms-btn--ghost\"\n onClick={() => setOpen(false)}\n >\n ✕\n </button>\n </div>\n )}\n </>\n );\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../../src/lock/DemoLock.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\nimport { useSiteConfig } from \"../config\";\n\n/**\n * Demo-markering die wordt getoond zolang `mode: \"demo\"`. Niet-blokkerend (de demo blijft\n * bekijkbaar), maar duidelijk gemarkeerd met een CTA om de site live te laten zetten.\n */\nexport function DemoLock() {\n const { mode, contactEmail, name } = useSiteConfig();\n const [open, setOpen] = useState(true);\n const bannerRef = useRef<HTMLDivElement>(null);\n\n // De demo-balk beslaat de onderrand van het scherm, en daar zweeft ook de bewerkbalk. Die twee\n // lagen over elkaar heen — juist op de demo, wat een prospect als eerste ziet. De balk meldt\n // daarom zijn hoogte als CSS-variabele; de bewerkbalk gaat er precies bovenop staan. Meten in\n // plaats van een vast getal, want de balk breekt op smalle schermen naar twee regels.\n useEffect(() => {\n const el = bannerRef.current;\n const root = document.documentElement;\n if (!el) {\n root.style.removeProperty(\"--xicms-bottombar\");\n return;\n }\n const meet = () => root.style.setProperty(\"--xicms-bottombar\", `${el.offsetHeight}px`);\n meet();\n const ro = new ResizeObserver(meet);\n ro.observe(el);\n return () => {\n ro.disconnect();\n root.style.removeProperty(\"--xicms-bottombar\");\n };\n }, [open, mode]);\n\n if (mode !== \"demo\") return null;\n\n const mailto = contactEmail\n ? `mailto:${contactEmail}?subject=${encodeURIComponent(`Demo ${name} live zetten`)}`\n : \"https://www.xiwebdesign.nl/#contact\";\n\n return (\n <>\n <div className=\"xicms-demo-ribbon\">DEMO</div>\n {open && (\n <div className=\"xicms-demo-banner\" role=\"status\" ref={bannerRef}>\n <span>\n Dit is een <strong>demo</strong> gemaakt door Xi Webdesign. Wil je deze site echt\n live?\n </span>\n <a href={mailto} className=\"xicms-btn\">Zet live</a>\n <button\n type=\"button\"\n aria-label=\"Sluiten\"\n className=\"xicms-btn xicms-btn--ghost\"\n onClick={() => setOpen(false)}\n >\n ✕\n </button>\n </div>\n )}\n </>\n );\n}\n"],"mappings":";AA0CI,mBACE,KAGI,YAJN;AAxCJ,SAAS,WAAW,QAAQ,gBAAgB;AAC5C,SAAS,qBAAqB;AAMvB,SAAS,WAAW;AACzB,QAAM,EAAE,MAAM,cAAc,KAAK,IAAI,cAAc;AACnD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAS,IAAI;AACrC,QAAM,YAAY,OAAuB,IAAI;AAM7C,YAAU,MAAM;AACd,UAAM,KAAK,UAAU;AACrB,UAAM,OAAO,SAAS;AACtB,QAAI,CAAC,IAAI;AACP,WAAK,MAAM,eAAe,mBAAmB;AAC7C;AAAA,IACF;AACA,UAAM,OAAO,MAAM,KAAK,MAAM,YAAY,qBAAqB,GAAG,GAAG,YAAY,IAAI;AACrF,SAAK;AACL,UAAM,KAAK,IAAI,eAAe,IAAI;AAClC,OAAG,QAAQ,EAAE;AACb,WAAO,MAAM;AACX,SAAG,WAAW;AACd,WAAK,MAAM,eAAe,mBAAmB;AAAA,IAC/C;AAAA,EACF,GAAG,CAAC,MAAM,IAAI,CAAC;AAEf,MAAI,SAAS,OAAQ,QAAO;AAE5B,QAAM,SAAS,eACX,UAAU,YAAY,YAAY,mBAAmB,QAAQ,IAAI,cAAc,CAAC,KAChF;AAEJ,SACE,iCACE;AAAA,wBAAC,SAAI,WAAU,qBAAoB,kBAAI;AAAA,IACtC,QACC,qBAAC,SAAI,WAAU,qBAAoB,MAAK,UAAS,KAAK,WACpD;AAAA,2BAAC,UAAK;AAAA;AAAA,QACO,oBAAC,YAAO,kBAAI;AAAA,QAAS;AAAA,SAElC;AAAA,MACA,oBAAC,OAAE,MAAM,QAAQ,WAAU,aAAY,sBAAQ;AAAA,MAC/C;AAAA,QAAC;AAAA;AAAA,UACC,MAAK;AAAA,UACL,cAAW;AAAA,UACX,WAAU;AAAA,UACV,SAAS,MAAM,QAAQ,KAAK;AAAA,UAC7B;AAAA;AAAA,MAED;AAAA,OACF;AAAA,KAEJ;AAEJ;","names":[]}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { useSyncExternalStore } from "react";
|
|
3
3
|
const listeners = /* @__PURE__ */ new Set();
|
|
4
4
|
const cache = /* @__PURE__ */ new Map();
|
|
5
|
+
const LEGE_CART = Object.freeze([]);
|
|
5
6
|
function storageKey(siteId) {
|
|
6
7
|
return `xicms:cart:${siteId}`;
|
|
7
8
|
}
|
|
@@ -72,7 +73,7 @@ function cartClear(siteId) {
|
|
|
72
73
|
writeLines(siteId, []);
|
|
73
74
|
}
|
|
74
75
|
function useCart(siteId) {
|
|
75
|
-
const lines = useSyncExternalStore(subscribe, () => readLines(siteId), () =>
|
|
76
|
+
const lines = useSyncExternalStore(subscribe, () => readLines(siteId), () => LEGE_CART);
|
|
76
77
|
return {
|
|
77
78
|
lines,
|
|
78
79
|
count: lines.reduce((n, l) => n + l.qty, 0),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/modules/shop/cartStore.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Winkelwagen-store — puur client-side (localStorage), geen Firestore-writes vóór checkout.\n * Extern store + `useSyncExternalStore` zodat élk shop-block (grid, cart-badge, checkout) dezelfde\n * state ziet zonder dat er een provider gemount hoeft te worden. Cross-tab sync via `storage`.\n *\n * Prijzen in de cart zijn indicatief voor weergave; bij `createOrder` herrekent de server alles\n * tegen de opgeslagen product-docs (client wordt nooit vertrouwd).\n */\nimport { useSyncExternalStore } from \"react\";\n\nexport interface CartLine {\n productId: string;\n slug: string;\n name: string;\n /** Variant-key (\"68|Roze\") + leesbaar label (\"Maat 68, Roze\"). */\n variantKey?: string;\n variantLabel?: string;\n /** Incl. personalisatie-meerprijs (indicatief; server herrekent). */\n unitPriceCents: number;\n qty: number;\n imageUrl?: string;\n /** 5.6: personalisatie per veld-key ({ naam: { text: \"Boaz\" } }). */\n personalization?: Record<string, { text?: string; imagePath?: string; fileName?: string }>;\n /** Leesbare samenvatting voor cart/checkout (\"Naam: Boaz\"). */\n personalizationLabel?: string;\n}\n\nconst listeners = new Set<() => void>();\n/** Cache per siteId zodat getSnapshot een stabiele referentie teruggeeft (useSyncExternalStore-eis). */\nconst cache = new Map<string, { raw: string; lines: CartLine[] }>();\n\nfunction storageKey(siteId: string): string {\n return `xicms:cart:${siteId}`;\n}\n\nfunction readLines(siteId: string): CartLine[] {\n if (typeof window === \"undefined\") return [];\n let raw = \"\";\n try {\n raw = window.localStorage.getItem(storageKey(siteId)) ?? \"[]\";\n } catch {\n raw = \"[]\";\n }\n const cached = cache.get(siteId);\n if (cached && cached.raw === raw) return cached.lines;\n let lines: CartLine[] = [];\n try {\n const parsed = JSON.parse(raw);\n if (Array.isArray(parsed)) lines = parsed.filter((l) => l && typeof l === \"object\" && typeof l.productId === \"string\");\n } catch {\n lines = [];\n }\n cache.set(siteId, { raw, lines });\n return lines;\n}\n\nfunction writeLines(siteId: string, lines: CartLine[]): void {\n try {\n window.localStorage.setItem(storageKey(siteId), JSON.stringify(lines));\n } catch {\n // localStorage vol/geblokkeerd: cart werkt dan alleen in-memory via de cache.\n cache.set(siteId, { raw: `mem:${Date.now()}`, lines });\n }\n emit();\n}\n\nfunction emit(): void {\n for (const l of listeners) l();\n}\n\nfunction subscribe(cb: () => void): () => void {\n listeners.add(cb);\n // Cross-tab: een andere tab die de cart wijzigt triggert hier een re-render.\n const onStorage = (e: StorageEvent) => {\n if (e.key?.startsWith(\"xicms:cart:\")) cb();\n };\n window.addEventListener(\"storage\", onStorage);\n return () => {\n listeners.delete(cb);\n window.removeEventListener(\"storage\", onStorage);\n };\n}\n\nfunction lineId(l: Pick<CartLine, \"productId\" | \"variantKey\" | \"personalization\">): string {\n // Personalisatie maakt de regel uniek: twee rompertjes met verschillende namen zijn\n // aparte regels (anders verhoogt \"nog één toevoegen\" stilletjes de verkeerde).\n const pers = l.personalization ? JSON.stringify(l.personalization) : \"\";\n return `${l.productId}::${l.variantKey ?? \"\"}::${pers}`;\n}\n\nexport function cartAdd(siteId: string, line: Omit<CartLine, \"qty\">, qty = 1): void {\n const lines = [...readLines(siteId)];\n const idx = lines.findIndex((l) => lineId(l) === lineId(line));\n if (idx >= 0) lines[idx] = { ...lines[idx], qty: Math.min(999, lines[idx].qty + qty) };\n else lines.push({ ...line, qty: Math.min(999, Math.max(1, qty)) });\n writeLines(siteId, lines);\n}\n\nexport function cartSetQty(\n siteId: string,\n line: Pick<CartLine, \"productId\" | \"variantKey\" | \"personalization\">,\n qty: number,\n): void {\n const lines = readLines(siteId)\n .map((l) => (lineId(l) === lineId(line) ? { ...l, qty: Math.min(999, qty) } : l))\n .filter((l) => l.qty > 0);\n writeLines(siteId, lines);\n}\n\nexport function cartRemove(siteId: string, line: Pick<CartLine, \"productId\" | \"variantKey\" | \"personalization\">): void {\n writeLines(siteId, readLines(siteId).filter((l) => lineId(l) !== lineId(line)));\n}\n\n/** Stabiele sleutel voor React-keys in cart-weergaven. */\nexport function cartLineKey(l: Pick<CartLine, \"productId\" | \"variantKey\" | \"personalization\">): string {\n return lineId(l);\n}\n\nexport function cartClear(siteId: string): void {\n writeLines(siteId, []);\n}\n\nexport interface CartState {\n lines: CartLine[];\n count: number;\n itemsCents: number;\n}\n\n/** Reactieve cart-state voor blocks/klantcomponenten (bv. een cart-badge in de header). */\nexport function useCart(siteId: string): CartState {\n const lines = useSyncExternalStore(subscribe, () => readLines(siteId), () =>
|
|
1
|
+
{"version":3,"sources":["../../../src/modules/shop/cartStore.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Winkelwagen-store — puur client-side (localStorage), geen Firestore-writes vóór checkout.\n * Extern store + `useSyncExternalStore` zodat élk shop-block (grid, cart-badge, checkout) dezelfde\n * state ziet zonder dat er een provider gemount hoeft te worden. Cross-tab sync via `storage`.\n *\n * Prijzen in de cart zijn indicatief voor weergave; bij `createOrder` herrekent de server alles\n * tegen de opgeslagen product-docs (client wordt nooit vertrouwd).\n */\nimport { useSyncExternalStore } from \"react\";\n\nexport interface CartLine {\n productId: string;\n slug: string;\n name: string;\n /** Variant-key (\"68|Roze\") + leesbaar label (\"Maat 68, Roze\"). */\n variantKey?: string;\n variantLabel?: string;\n /** Incl. personalisatie-meerprijs (indicatief; server herrekent). */\n unitPriceCents: number;\n qty: number;\n imageUrl?: string;\n /** 5.6: personalisatie per veld-key ({ naam: { text: \"Boaz\" } }). */\n personalization?: Record<string, { text?: string; imagePath?: string; fileName?: string }>;\n /** Leesbare samenvatting voor cart/checkout (\"Naam: Boaz\"). */\n personalizationLabel?: string;\n}\n\nconst listeners = new Set<() => void>();\n/** Cache per siteId zodat getSnapshot een stabiele referentie teruggeeft (useSyncExternalStore-eis). */\nconst cache = new Map<string, { raw: string; lines: CartLine[] }>();\n/**\n * Dezelfde eis geldt voor `getServerSnapshot`, en dáár stond hij niet: die gaf `() => []` terug,\n * dus elke render een nieuwe referentie. React meldde daarop bij élke pagina-load van een\n * shop-site: \"The result of getServerSnapshot should be cached to avoid an infinite loop\".\n * Eén gedeelde, bevroren lege lijst lost dat op — de cart is server-side per definitie leeg\n * (localStorage bestaat daar niet), dus één instantie volstaat.\n */\nconst LEGE_CART: readonly CartLine[] = Object.freeze([]);\n\nfunction storageKey(siteId: string): string {\n return `xicms:cart:${siteId}`;\n}\n\nfunction readLines(siteId: string): CartLine[] {\n if (typeof window === \"undefined\") return [];\n let raw = \"\";\n try {\n raw = window.localStorage.getItem(storageKey(siteId)) ?? \"[]\";\n } catch {\n raw = \"[]\";\n }\n const cached = cache.get(siteId);\n if (cached && cached.raw === raw) return cached.lines;\n let lines: CartLine[] = [];\n try {\n const parsed = JSON.parse(raw);\n if (Array.isArray(parsed)) lines = parsed.filter((l) => l && typeof l === \"object\" && typeof l.productId === \"string\");\n } catch {\n lines = [];\n }\n cache.set(siteId, { raw, lines });\n return lines;\n}\n\nfunction writeLines(siteId: string, lines: CartLine[]): void {\n try {\n window.localStorage.setItem(storageKey(siteId), JSON.stringify(lines));\n } catch {\n // localStorage vol/geblokkeerd: cart werkt dan alleen in-memory via de cache.\n cache.set(siteId, { raw: `mem:${Date.now()}`, lines });\n }\n emit();\n}\n\nfunction emit(): void {\n for (const l of listeners) l();\n}\n\nfunction subscribe(cb: () => void): () => void {\n listeners.add(cb);\n // Cross-tab: een andere tab die de cart wijzigt triggert hier een re-render.\n const onStorage = (e: StorageEvent) => {\n if (e.key?.startsWith(\"xicms:cart:\")) cb();\n };\n window.addEventListener(\"storage\", onStorage);\n return () => {\n listeners.delete(cb);\n window.removeEventListener(\"storage\", onStorage);\n };\n}\n\nfunction lineId(l: Pick<CartLine, \"productId\" | \"variantKey\" | \"personalization\">): string {\n // Personalisatie maakt de regel uniek: twee rompertjes met verschillende namen zijn\n // aparte regels (anders verhoogt \"nog één toevoegen\" stilletjes de verkeerde).\n const pers = l.personalization ? JSON.stringify(l.personalization) : \"\";\n return `${l.productId}::${l.variantKey ?? \"\"}::${pers}`;\n}\n\nexport function cartAdd(siteId: string, line: Omit<CartLine, \"qty\">, qty = 1): void {\n const lines = [...readLines(siteId)];\n const idx = lines.findIndex((l) => lineId(l) === lineId(line));\n if (idx >= 0) lines[idx] = { ...lines[idx], qty: Math.min(999, lines[idx].qty + qty) };\n else lines.push({ ...line, qty: Math.min(999, Math.max(1, qty)) });\n writeLines(siteId, lines);\n}\n\nexport function cartSetQty(\n siteId: string,\n line: Pick<CartLine, \"productId\" | \"variantKey\" | \"personalization\">,\n qty: number,\n): void {\n const lines = readLines(siteId)\n .map((l) => (lineId(l) === lineId(line) ? { ...l, qty: Math.min(999, qty) } : l))\n .filter((l) => l.qty > 0);\n writeLines(siteId, lines);\n}\n\nexport function cartRemove(siteId: string, line: Pick<CartLine, \"productId\" | \"variantKey\" | \"personalization\">): void {\n writeLines(siteId, readLines(siteId).filter((l) => lineId(l) !== lineId(line)));\n}\n\n/** Stabiele sleutel voor React-keys in cart-weergaven. */\nexport function cartLineKey(l: Pick<CartLine, \"productId\" | \"variantKey\" | \"personalization\">): string {\n return lineId(l);\n}\n\nexport function cartClear(siteId: string): void {\n writeLines(siteId, []);\n}\n\nexport interface CartState {\n lines: CartLine[];\n count: number;\n itemsCents: number;\n}\n\n/** Reactieve cart-state voor blocks/klantcomponenten (bv. een cart-badge in de header). */\nexport function useCart(siteId: string): CartState {\n const lines = useSyncExternalStore(subscribe, () => readLines(siteId), () => LEGE_CART as CartLine[]);\n return {\n lines,\n count: lines.reduce((n, l) => n + l.qty, 0),\n itemsCents: lines.reduce((n, l) => n + l.unitPriceCents * l.qty, 0),\n };\n}\n"],"mappings":";AAUA,SAAS,4BAA4B;AAmBrC,MAAM,YAAY,oBAAI,IAAgB;AAEtC,MAAM,QAAQ,oBAAI,IAAgD;AAQlE,MAAM,YAAiC,OAAO,OAAO,CAAC,CAAC;AAEvD,SAAS,WAAW,QAAwB;AAC1C,SAAO,cAAc,MAAM;AAC7B;AAEA,SAAS,UAAU,QAA4B;AAC7C,MAAI,OAAO,WAAW,YAAa,QAAO,CAAC;AAC3C,MAAI,MAAM;AACV,MAAI;AACF,UAAM,OAAO,aAAa,QAAQ,WAAW,MAAM,CAAC,KAAK;AAAA,EAC3D,QAAQ;AACN,UAAM;AAAA,EACR;AACA,QAAM,SAAS,MAAM,IAAI,MAAM;AAC/B,MAAI,UAAU,OAAO,QAAQ,IAAK,QAAO,OAAO;AAChD,MAAI,QAAoB,CAAC;AACzB,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,MAAM,QAAQ,MAAM,EAAG,SAAQ,OAAO,OAAO,CAAC,MAAM,KAAK,OAAO,MAAM,YAAY,OAAO,EAAE,cAAc,QAAQ;AAAA,EACvH,QAAQ;AACN,YAAQ,CAAC;AAAA,EACX;AACA,QAAM,IAAI,QAAQ,EAAE,KAAK,MAAM,CAAC;AAChC,SAAO;AACT;AAEA,SAAS,WAAW,QAAgB,OAAyB;AAC3D,MAAI;AACF,WAAO,aAAa,QAAQ,WAAW,MAAM,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,EACvE,QAAQ;AAEN,UAAM,IAAI,QAAQ,EAAE,KAAK,OAAO,KAAK,IAAI,CAAC,IAAI,MAAM,CAAC;AAAA,EACvD;AACA,OAAK;AACP;AAEA,SAAS,OAAa;AACpB,aAAW,KAAK,UAAW,GAAE;AAC/B;AAEA,SAAS,UAAU,IAA4B;AAC7C,YAAU,IAAI,EAAE;AAEhB,QAAM,YAAY,CAAC,MAAoB;AACrC,QAAI,EAAE,KAAK,WAAW,aAAa,EAAG,IAAG;AAAA,EAC3C;AACA,SAAO,iBAAiB,WAAW,SAAS;AAC5C,SAAO,MAAM;AACX,cAAU,OAAO,EAAE;AACnB,WAAO,oBAAoB,WAAW,SAAS;AAAA,EACjD;AACF;AAEA,SAAS,OAAO,GAA2E;AAGzF,QAAM,OAAO,EAAE,kBAAkB,KAAK,UAAU,EAAE,eAAe,IAAI;AACrE,SAAO,GAAG,EAAE,SAAS,KAAK,EAAE,cAAc,EAAE,KAAK,IAAI;AACvD;AAEO,SAAS,QAAQ,QAAgB,MAA6B,MAAM,GAAS;AAClF,QAAM,QAAQ,CAAC,GAAG,UAAU,MAAM,CAAC;AACnC,QAAM,MAAM,MAAM,UAAU,CAAC,MAAM,OAAO,CAAC,MAAM,OAAO,IAAI,CAAC;AAC7D,MAAI,OAAO,EAAG,OAAM,GAAG,IAAI,EAAE,GAAG,MAAM,GAAG,GAAG,KAAK,KAAK,IAAI,KAAK,MAAM,GAAG,EAAE,MAAM,GAAG,EAAE;AAAA,MAChF,OAAM,KAAK,EAAE,GAAG,MAAM,KAAK,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,CAAC;AACjE,aAAW,QAAQ,KAAK;AAC1B;AAEO,SAAS,WACd,QACA,MACA,KACM;AACN,QAAM,QAAQ,UAAU,MAAM,EAC3B,IAAI,CAAC,MAAO,OAAO,CAAC,MAAM,OAAO,IAAI,IAAI,EAAE,GAAG,GAAG,KAAK,KAAK,IAAI,KAAK,GAAG,EAAE,IAAI,CAAE,EAC/E,OAAO,CAAC,MAAM,EAAE,MAAM,CAAC;AAC1B,aAAW,QAAQ,KAAK;AAC1B;AAEO,SAAS,WAAW,QAAgB,MAA4E;AACrH,aAAW,QAAQ,UAAU,MAAM,EAAE,OAAO,CAAC,MAAM,OAAO,CAAC,MAAM,OAAO,IAAI,CAAC,CAAC;AAChF;AAGO,SAAS,YAAY,GAA2E;AACrG,SAAO,OAAO,CAAC;AACjB;AAEO,SAAS,UAAU,QAAsB;AAC9C,aAAW,QAAQ,CAAC,CAAC;AACvB;AASO,SAAS,QAAQ,QAA2B;AACjD,QAAM,QAAQ,qBAAqB,WAAW,MAAM,UAAU,MAAM,GAAG,MAAM,SAAuB;AACpG,SAAO;AAAA,IACL;AAAA,IACA,OAAO,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,KAAK,CAAC;AAAA,IAC1C,YAAY,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,EAAE,iBAAiB,EAAE,KAAK,CAAC;AAAA,EACpE;AACF;","names":[]}
|
package/dist/modules/shop/lib.js
CHANGED
|
@@ -4,10 +4,9 @@ async function fsSdk() {
|
|
|
4
4
|
return { db: client.getDb(), ...sdk };
|
|
5
5
|
}
|
|
6
6
|
async function fnSdk() {
|
|
7
|
-
const [client, sdk] = await Promise.all([import("../../firebase/client.js"), import("firebase/
|
|
7
|
+
const [client, sdk] = await Promise.all([import("../../firebase/client.js"), import("../../firebase/functionsClient.js")]);
|
|
8
8
|
return { app: client.getFirebaseApp(), ...sdk };
|
|
9
9
|
}
|
|
10
|
-
import { functionsRegion } from "../../firebase/region.js";
|
|
11
10
|
import { useModuleSettings } from "../context.js";
|
|
12
11
|
import { defaultShopSettings } from "./settings.js";
|
|
13
12
|
function useShopSettings() {
|
|
@@ -58,21 +57,21 @@ function stripEmpty(value) {
|
|
|
58
57
|
return value;
|
|
59
58
|
}
|
|
60
59
|
async function createOrderCall(input) {
|
|
61
|
-
const { app,
|
|
60
|
+
const { app, callable } = await fnSdk();
|
|
62
61
|
if (!app) throw new Error("Webwinkel niet beschikbaar.");
|
|
63
|
-
const fn =
|
|
62
|
+
const fn = callable(app, "createOrder");
|
|
64
63
|
const res = await fn(stripEmpty(input));
|
|
65
64
|
return res.data;
|
|
66
65
|
}
|
|
67
66
|
async function validateDiscountCall(siteId, code, itemsCents) {
|
|
68
|
-
const { app,
|
|
67
|
+
const { app, callable } = await fnSdk();
|
|
69
68
|
if (!app) throw new Error("Webwinkel niet beschikbaar.");
|
|
70
|
-
const fn =
|
|
69
|
+
const fn = callable(app, "validateDiscountCode");
|
|
71
70
|
const res = await fn({ siteId, code, itemsCents });
|
|
72
71
|
return res.data;
|
|
73
72
|
}
|
|
74
73
|
async function uploadPersonalizationCall(siteId, productId, file) {
|
|
75
|
-
const { app,
|
|
74
|
+
const { app, callable } = await fnSdk();
|
|
76
75
|
if (!app) throw new Error("Webwinkel niet beschikbaar.");
|
|
77
76
|
if (file.size > 5 * 1024 * 1024) throw new Error("TOO_BIG");
|
|
78
77
|
const bytesBase64 = await new Promise((resolve, reject) => {
|
|
@@ -81,7 +80,7 @@ async function uploadPersonalizationCall(siteId, productId, file) {
|
|
|
81
80
|
reader.onerror = () => reject(new Error("Bestand lezen mislukt."));
|
|
82
81
|
reader.readAsDataURL(file);
|
|
83
82
|
});
|
|
84
|
-
const fn =
|
|
83
|
+
const fn = callable(app, "uploadPersonalization");
|
|
85
84
|
const res = await fn({ siteId, productId, fileName: file.name, contentType: file.type, bytesBase64 });
|
|
86
85
|
return res.data;
|
|
87
86
|
}
|
|
@@ -94,9 +93,9 @@ function readQueryParam(name) {
|
|
|
94
93
|
return new URLSearchParams(window.location.search).get(name);
|
|
95
94
|
}
|
|
96
95
|
async function getOrderReturnStatusCall(siteId, orderId) {
|
|
97
|
-
const { app,
|
|
96
|
+
const { app, callable } = await fnSdk();
|
|
98
97
|
if (!app) return { status: "unknown" };
|
|
99
|
-
const fn =
|
|
98
|
+
const fn = callable(app, "getOrderReturnStatus");
|
|
100
99
|
const res = await fn({ siteId, orderId });
|
|
101
100
|
return res.data;
|
|
102
101
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/modules/shop/lib.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Client-helpers voor de shop-blocks: producten/categorieën lezen (publieke rules) en de\n * `createOrder`-callable aanroepen. Queries bewust simpel gehouden (één `where` op status,\n * filteren/sorteren client-side) zodat er géén composite-indexes nodig zijn — assortimenten\n * van XiCMS-sites zijn klein (tientallen tot honderden producten).\n */\n\n/**\n * Firestore/Functions worden hier DYNAMISCH geladen. De shop-blocks zitten in de publieke bundel,\n * dus een statische import zette de SDK op élke pagina — ook op sites zonder webshop.\n */\nasync function fsSdk() {\n const [client, sdk] = await Promise.all([import(\"../../firebase/client\"), import(\"firebase/firestore\")]);\n return { db: client.getDb(), ...sdk };\n}\nasync function fnSdk() {\n const [client, sdk] = await Promise.all([import(\"../../firebase/client\"), import(\"firebase/functions\")]);\n return { app: client.getFirebaseApp(), ...sdk };\n}\nimport { functionsRegion } from \"../../firebase/region\";\nimport type { CreateOrderInput, Product, ProductOption, ShopCategory } from \"../../schema/shop\";\nimport { useModuleSettings } from \"../context\";\nimport { defaultShopSettings, type ShopSettings } from \"./settings\";\n\n/** Publieke shop-instellingen met defaults (paden, verzendkosten, valuta). */\nexport function useShopSettings(): ShopSettings {\n const raw = useModuleSettings<Partial<ShopSettings>>(\"shop\");\n return { ...defaultShopSettings, ...raw };\n}\n\nexport type StoredProduct = Product & { id: string };\nexport type StoredShopCategory = ShopCategory & { id: string };\n\nexport async function fetchActiveProducts(siteId: string): Promise<StoredProduct[]> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db) return [];\n const snap = await getDocs(\n query(collection(db, \"sites\", siteId, \"products\"), where(\"status\", \"==\", \"active\"), fsLimit(500)),\n );\n return snap.docs.map((d) => ({ id: d.id, ...(d.data() as Product) }));\n}\n\nexport async function fetchProductBySlug(siteId: string, slug: string): Promise<StoredProduct | null> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db || !slug) return null;\n // Het status-filter is verplicht: de rules staan publieke reads alleen toe voor actieve\n // producten, en Firestore weigert query's die niet aantoonbaar binnen die regel blijven.\n // Twee equality-filters → geen composite index nodig (index-merging).\n const snap = await getDocs(\n query(\n collection(db, \"sites\", siteId, \"products\"),\n where(\"slug\", \"==\", slug),\n where(\"status\", \"==\", \"active\"),\n fsLimit(1),\n ),\n );\n const doc = snap.docs[0];\n if (!doc) return null;\n return { id: doc.id, ...(doc.data() as Product) };\n}\n\nexport async function fetchShopCategories(siteId: string): Promise<StoredShopCategory[]> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db) return [];\n const snap = await getDocs(query(collection(db, \"sites\", siteId, \"shopCategories\"), fsLimit(100)));\n return snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))\n .filter((c) => c.visible !== false)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n}\n\n/** Start de checkout: order + Stripe-sessie server-side; geeft de redirect-URL terug. */\n/**\n * Verwijdert `undefined`/`null`-velden diep uit een payload vóór een callable-aanroep.\n * De Firebase-callable-SDK serialiseert `undefined` namelijk als `null` op de wire, terwijl de\n * server-side Zod-schema's optionele velden met `.optional()` valideren — dat wijst `null` af en\n * geeft \"Ongeldige invoer\". Door lege velden hier weg te laten komen ze als afwezig binnen (= ok).\n */\nfunction stripEmpty<T>(value: T): T {\n if (Array.isArray(value)) {\n return value.map((v) => stripEmpty(v)) as unknown as T;\n }\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (v === undefined || v === null) continue;\n out[k] = stripEmpty(v);\n }\n return out as T;\n }\n return value;\n}\n\nexport async function createOrderCall(input: CreateOrderInput): Promise<{ url: string; orderNumber: string }> {\n const { app, getFunctions, httpsCallable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"createOrder\");\n const res = await fn(stripEmpty(input));\n return res.data as { url: string; orderNumber: string };\n}\n\n/** Valideert een kortingscode server-side; geeft de verwachte korting terug (preview). */\nexport interface DiscountPreview {\n valid: boolean;\n code?: string;\n discountCents?: number;\n freeShipping?: boolean;\n reason?: string;\n}\nexport async function validateDiscountCall(siteId: string, code: string, itemsCents: number): Promise<DiscountPreview> {\n const { app, getFunctions, httpsCallable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"validateDiscountCode\");\n const res = await fn({ siteId, code, itemsCents });\n return res.data as DiscountPreview;\n}\n\n/**\n * Upload een personalisatie-afbeelding (eigen logo e.d.) via de begrensde callable —\n * geeft het Storage-pad terug dat op de orderregel meegaat. Max 5 MB, jpeg/png/webp.\n */\nexport async function uploadPersonalizationCall(\n siteId: string,\n productId: string,\n file: File,\n): Promise<{ path: string }> {\n const { app, getFunctions, httpsCallable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n if (file.size > 5 * 1024 * 1024) throw new Error(\"TOO_BIG\");\n const bytesBase64 = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result).split(\",\")[1] ?? \"\");\n reader.onerror = () => reject(new Error(\"Bestand lezen mislukt.\"));\n reader.readAsDataURL(file);\n });\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"uploadPersonalization\");\n const res = await fn({ siteId, productId, fileName: file.name, contentType: file.type, bytesBase64 });\n return res.data as { path: string };\n}\n\n/** Leesbaar variantlabel: opties [\"Maat\",\"Kleur\"] + key \"68|Roze\" → \"Maat 68 · Roze\". */\nexport function variantLabelFor(options: { name: string; values: string[] }[], key: string): string {\n const parts = key.split(\"|\");\n return options.map((o, i) => `${o.name} ${parts[i] ?? \"?\"}`).join(\" · \");\n}\n\n/** Querystring-parameter client-side lezen (blocks zijn client components). */\nexport function readQueryParam(name: string): string | null {\n if (typeof window === \"undefined\") return null;\n return new URLSearchParams(window.location.search).get(name);\n}\n\n/**\n * Echte orderstatus opvragen (voor de bedankt-pagina). Nodig bij Mollie: die heeft één\n * redirect-URL voor alle uitkomsten, dus de status moet server-side gecheckt worden.\n */\nexport async function getOrderReturnStatusCall(\n siteId: string,\n orderId: string,\n): Promise<{ status: string; number?: string }> {\n const { app, getFunctions, httpsCallable } = await fnSdk();\n if (!app) return { status: \"unknown\" };\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"getOrderReturnStatus\");\n const res = await fn({ siteId, orderId });\n return res.data as { status: string; number?: string };\n}\n\n/* ── Kleur-swatches ──────────────────────────────────────────────────────── */\n\n// Zachte, pastelachtige swatch-tinten — informatief maar rustig (geen felle vlekken op de\n// kaart). Bewust gedempt t.o.v. de echte stofkleuren; de foto toont de precieze kleur.\nconst KLEUR_CSS: [RegExp, string][] = [\n [/fuchsia|fushia/, \"#dd8fb4\"],\n [/roze|pink/, \"#f2c3d3\"],\n [/blauw|blue/, \"#bcd3ea\"],\n [/mint/, \"#bfe0d2\"],\n [/oker|geel|yellow/, \"#eccf90\"],\n [/wit|white/, \"#f6f3ec\"],\n [/creme|crème|ecru|beige|zand/, \"#f0e7d6\"],\n [/grijs|gray|grey/, \"#c7ccd3\"],\n [/groen|green/, \"#b3d1b3\"],\n [/rood|red|bordeaux/, \"#e0a0a0\"],\n [/paars|purple|lila/, \"#cdbce6\"],\n [/oranje|orange/, \"#f2bd94\"],\n [/bruin|brown/, \"#c3a488\"],\n [/zwart|black|navy|donkerblauw/, \"#6b7488\"],\n];\n\n/**\n * Zet een kleurnaam (\"Roze\", \"Korte mouw tshirt (oker)geel\") om naar een CSS-kleur voor de\n * swatch-stipjes op productkaarten. Onbekende namen geven null (dan geen stipje).\n */\nexport function kleurNaarCss(naam: string): string | null {\n const l = naam.toLowerCase();\n for (const [re, css] of KLEUR_CSS) if (re.test(l)) return css;\n return null;\n}\n\n/** De kleur-optie van een product (naam \"Kleur\"/\"Colour\"/\"Color\"), of null. */\nexport function kleurOptie(product: Pick<Product, \"options\">): ProductOption | null {\n return (product.options ?? []).find((o) => /^kleur|^colou?r/i.test(o.name)) ?? null;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/modules/shop/lib.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Client-helpers voor de shop-blocks: producten/categorieën lezen (publieke rules) en de\n * `createOrder`-callable aanroepen. Queries bewust simpel gehouden (één `where` op status,\n * filteren/sorteren client-side) zodat er géén composite-indexes nodig zijn — assortimenten\n * van XiCMS-sites zijn klein (tientallen tot honderden producten).\n */\n\n/**\n * Firestore/Functions worden hier DYNAMISCH geladen. De shop-blocks zitten in de publieke bundel,\n * dus een statische import zette de SDK op élke pagina — ook op sites zonder webshop.\n */\nasync function fsSdk() {\n const [client, sdk] = await Promise.all([import(\"../../firebase/client\"), import(\"firebase/firestore\")]);\n return { db: client.getDb(), ...sdk };\n}\nasync function fnSdk() {\n const [client, sdk] = await Promise.all([import(\"../../firebase/client\"), import(\"../../firebase/functionsClient\")]);\n return { app: client.getFirebaseApp(), ...sdk };\n}\nimport type { CreateOrderInput, Product, ProductOption, ShopCategory } from \"../../schema/shop\";\nimport { useModuleSettings } from \"../context\";\nimport { defaultShopSettings, type ShopSettings } from \"./settings\";\n\n/** Publieke shop-instellingen met defaults (paden, verzendkosten, valuta). */\nexport function useShopSettings(): ShopSettings {\n const raw = useModuleSettings<Partial<ShopSettings>>(\"shop\");\n return { ...defaultShopSettings, ...raw };\n}\n\nexport type StoredProduct = Product & { id: string };\nexport type StoredShopCategory = ShopCategory & { id: string };\n\nexport async function fetchActiveProducts(siteId: string): Promise<StoredProduct[]> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db) return [];\n const snap = await getDocs(\n query(collection(db, \"sites\", siteId, \"products\"), where(\"status\", \"==\", \"active\"), fsLimit(500)),\n );\n return snap.docs.map((d) => ({ id: d.id, ...(d.data() as Product) }));\n}\n\nexport async function fetchProductBySlug(siteId: string, slug: string): Promise<StoredProduct | null> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db || !slug) return null;\n // Het status-filter is verplicht: de rules staan publieke reads alleen toe voor actieve\n // producten, en Firestore weigert query's die niet aantoonbaar binnen die regel blijven.\n // Twee equality-filters → geen composite index nodig (index-merging).\n const snap = await getDocs(\n query(\n collection(db, \"sites\", siteId, \"products\"),\n where(\"slug\", \"==\", slug),\n where(\"status\", \"==\", \"active\"),\n fsLimit(1),\n ),\n );\n const doc = snap.docs[0];\n if (!doc) return null;\n return { id: doc.id, ...(doc.data() as Product) };\n}\n\nexport async function fetchShopCategories(siteId: string): Promise<StoredShopCategory[]> {\n const { db, collection, getDocs, limit: fsLimit, query, where } = await fsSdk();\n if (!db) return [];\n const snap = await getDocs(query(collection(db, \"sites\", siteId, \"shopCategories\"), fsLimit(100)));\n return snap.docs\n .map((d) => ({ id: d.id, ...(d.data() as ShopCategory) }))\n .filter((c) => c.visible !== false)\n .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));\n}\n\n/** Start de checkout: order + Stripe-sessie server-side; geeft de redirect-URL terug. */\n/**\n * Verwijdert `undefined`/`null`-velden diep uit een payload vóór een callable-aanroep.\n * De Firebase-callable-SDK serialiseert `undefined` namelijk als `null` op de wire, terwijl de\n * server-side Zod-schema's optionele velden met `.optional()` valideren — dat wijst `null` af en\n * geeft \"Ongeldige invoer\". Door lege velden hier weg te laten komen ze als afwezig binnen (= ok).\n */\nfunction stripEmpty<T>(value: T): T {\n if (Array.isArray(value)) {\n return value.map((v) => stripEmpty(v)) as unknown as T;\n }\n if (value && typeof value === \"object\") {\n const out: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(value as Record<string, unknown>)) {\n if (v === undefined || v === null) continue;\n out[k] = stripEmpty(v);\n }\n return out as T;\n }\n return value;\n}\n\nexport async function createOrderCall(input: CreateOrderInput): Promise<{ url: string; orderNumber: string }> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n const fn = callable(app, \"createOrder\");\n const res = await fn(stripEmpty(input));\n return res.data as { url: string; orderNumber: string };\n}\n\n/** Valideert een kortingscode server-side; geeft de verwachte korting terug (preview). */\nexport interface DiscountPreview {\n valid: boolean;\n code?: string;\n discountCents?: number;\n freeShipping?: boolean;\n reason?: string;\n}\nexport async function validateDiscountCall(siteId: string, code: string, itemsCents: number): Promise<DiscountPreview> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n const fn = callable(app, \"validateDiscountCode\");\n const res = await fn({ siteId, code, itemsCents });\n return res.data as DiscountPreview;\n}\n\n/**\n * Upload een personalisatie-afbeelding (eigen logo e.d.) via de begrensde callable —\n * geeft het Storage-pad terug dat op de orderregel meegaat. Max 5 MB, jpeg/png/webp.\n */\nexport async function uploadPersonalizationCall(\n siteId: string,\n productId: string,\n file: File,\n): Promise<{ path: string }> {\n const { app, callable } = await fnSdk();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n if (file.size > 5 * 1024 * 1024) throw new Error(\"TOO_BIG\");\n const bytesBase64 = await new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onload = () => resolve(String(reader.result).split(\",\")[1] ?? \"\");\n reader.onerror = () => reject(new Error(\"Bestand lezen mislukt.\"));\n reader.readAsDataURL(file);\n });\n const fn = callable(app, \"uploadPersonalization\");\n const res = await fn({ siteId, productId, fileName: file.name, contentType: file.type, bytesBase64 });\n return res.data as { path: string };\n}\n\n/** Leesbaar variantlabel: opties [\"Maat\",\"Kleur\"] + key \"68|Roze\" → \"Maat 68 · Roze\". */\nexport function variantLabelFor(options: { name: string; values: string[] }[], key: string): string {\n const parts = key.split(\"|\");\n return options.map((o, i) => `${o.name} ${parts[i] ?? \"?\"}`).join(\" · \");\n}\n\n/** Querystring-parameter client-side lezen (blocks zijn client components). */\nexport function readQueryParam(name: string): string | null {\n if (typeof window === \"undefined\") return null;\n return new URLSearchParams(window.location.search).get(name);\n}\n\n/**\n * Echte orderstatus opvragen (voor de bedankt-pagina). Nodig bij Mollie: die heeft één\n * redirect-URL voor alle uitkomsten, dus de status moet server-side gecheckt worden.\n */\nexport async function getOrderReturnStatusCall(\n siteId: string,\n orderId: string,\n): Promise<{ status: string; number?: string }> {\n const { app, callable } = await fnSdk();\n if (!app) return { status: \"unknown\" };\n const fn = callable(app, \"getOrderReturnStatus\");\n const res = await fn({ siteId, orderId });\n return res.data as { status: string; number?: string };\n}\n\n/* ── Kleur-swatches ──────────────────────────────────────────────────────── */\n\n// Zachte, pastelachtige swatch-tinten — informatief maar rustig (geen felle vlekken op de\n// kaart). Bewust gedempt t.o.v. de echte stofkleuren; de foto toont de precieze kleur.\nconst KLEUR_CSS: [RegExp, string][] = [\n [/fuchsia|fushia/, \"#dd8fb4\"],\n [/roze|pink/, \"#f2c3d3\"],\n [/blauw|blue/, \"#bcd3ea\"],\n [/mint/, \"#bfe0d2\"],\n [/oker|geel|yellow/, \"#eccf90\"],\n [/wit|white/, \"#f6f3ec\"],\n [/creme|crème|ecru|beige|zand/, \"#f0e7d6\"],\n [/grijs|gray|grey/, \"#c7ccd3\"],\n [/groen|green/, \"#b3d1b3\"],\n [/rood|red|bordeaux/, \"#e0a0a0\"],\n [/paars|purple|lila/, \"#cdbce6\"],\n [/oranje|orange/, \"#f2bd94\"],\n [/bruin|brown/, \"#c3a488\"],\n [/zwart|black|navy|donkerblauw/, \"#6b7488\"],\n];\n\n/**\n * Zet een kleurnaam (\"Roze\", \"Korte mouw tshirt (oker)geel\") om naar een CSS-kleur voor de\n * swatch-stipjes op productkaarten. Onbekende namen geven null (dan geen stipje).\n */\nexport function kleurNaarCss(naam: string): string | null {\n const l = naam.toLowerCase();\n for (const [re, css] of KLEUR_CSS) if (re.test(l)) return css;\n return null;\n}\n\n/** De kleur-optie van een product (naam \"Kleur\"/\"Colour\"/\"Color\"), of null. */\nexport function kleurOptie(product: Pick<Product, \"options\">): ProductOption | null {\n return (product.options ?? []).find((o) => /^kleur|^colou?r/i.test(o.name)) ?? null;\n}\n"],"mappings":";AAaA,eAAe,QAAQ;AACrB,QAAM,CAAC,QAAQ,GAAG,IAAI,MAAM,QAAQ,IAAI,CAAC,OAAO,uBAAuB,GAAG,OAAO,oBAAoB,CAAC,CAAC;AACvG,SAAO,EAAE,IAAI,OAAO,MAAM,GAAG,GAAG,IAAI;AACtC;AACA,eAAe,QAAQ;AACrB,QAAM,CAAC,QAAQ,GAAG,IAAI,MAAM,QAAQ,IAAI,CAAC,OAAO,uBAAuB,GAAG,OAAO,gCAAgC,CAAC,CAAC;AACnH,SAAO,EAAE,KAAK,OAAO,eAAe,GAAG,GAAG,IAAI;AAChD;AAEA,SAAS,yBAAyB;AAClC,SAAS,2BAA8C;AAGhD,SAAS,kBAAgC;AAC9C,QAAM,MAAM,kBAAyC,MAAM;AAC3D,SAAO,EAAE,GAAG,qBAAqB,GAAG,IAAI;AAC1C;AAKA,eAAsB,oBAAoB,QAA0C;AAClF,QAAM,EAAE,IAAI,YAAY,SAAS,OAAO,SAAS,OAAO,MAAM,IAAI,MAAM,MAAM;AAC9E,MAAI,CAAC,GAAI,QAAO,CAAC;AACjB,QAAM,OAAO,MAAM;AAAA,IACjB,MAAM,WAAW,IAAI,SAAS,QAAQ,UAAU,GAAG,MAAM,UAAU,MAAM,QAAQ,GAAG,QAAQ,GAAG,CAAC;AAAA,EAClG;AACA,SAAO,KAAK,KAAK,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAAc,EAAE;AACtE;AAEA,eAAsB,mBAAmB,QAAgB,MAA6C;AACpG,QAAM,EAAE,IAAI,YAAY,SAAS,OAAO,SAAS,OAAO,MAAM,IAAI,MAAM,MAAM;AAC9E,MAAI,CAAC,MAAM,CAAC,KAAM,QAAO;AAIzB,QAAM,OAAO,MAAM;AAAA,IACjB;AAAA,MACE,WAAW,IAAI,SAAS,QAAQ,UAAU;AAAA,MAC1C,MAAM,QAAQ,MAAM,IAAI;AAAA,MACxB,MAAM,UAAU,MAAM,QAAQ;AAAA,MAC9B,QAAQ,CAAC;AAAA,IACX;AAAA,EACF;AACA,QAAM,MAAM,KAAK,KAAK,CAAC;AACvB,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,EAAE,IAAI,IAAI,IAAI,GAAI,IAAI,KAAK,EAAc;AAClD;AAEA,eAAsB,oBAAoB,QAA+C;AACvF,QAAM,EAAE,IAAI,YAAY,SAAS,OAAO,SAAS,OAAO,MAAM,IAAI,MAAM,MAAM;AAC9E,MAAI,CAAC,GAAI,QAAO,CAAC;AACjB,QAAM,OAAO,MAAM,QAAQ,MAAM,WAAW,IAAI,SAAS,QAAQ,gBAAgB,GAAG,QAAQ,GAAG,CAAC,CAAC;AACjG,SAAO,KAAK,KACT,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,IAAI,GAAI,EAAE,KAAK,EAAmB,EAAE,EACxD,OAAO,CAAC,MAAM,EAAE,YAAY,KAAK,EACjC,KAAK,CAAC,GAAG,OAAO,EAAE,SAAS,MAAM,EAAE,SAAS,EAAE;AACnD;AASA,SAAS,WAAc,OAAa;AAClC,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,CAAC,MAAM,WAAW,CAAC,CAAC;AAAA,EACvC;AACA,MAAI,SAAS,OAAO,UAAU,UAAU;AACtC,UAAM,MAA+B,CAAC;AACtC,eAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,KAAgC,GAAG;AACrE,UAAI,MAAM,UAAa,MAAM,KAAM;AACnC,UAAI,CAAC,IAAI,WAAW,CAAC;AAAA,IACvB;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,eAAsB,gBAAgB,OAAwE;AAC5G,QAAM,EAAE,KAAK,SAAS,IAAI,MAAM,MAAM;AACtC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,6BAA6B;AACvD,QAAM,KAAK,SAAS,KAAK,aAAa;AACtC,QAAM,MAAM,MAAM,GAAG,WAAW,KAAK,CAAC;AACtC,SAAO,IAAI;AACb;AAUA,eAAsB,qBAAqB,QAAgB,MAAc,YAA8C;AACrH,QAAM,EAAE,KAAK,SAAS,IAAI,MAAM,MAAM;AACtC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,6BAA6B;AACvD,QAAM,KAAK,SAAS,KAAK,sBAAsB;AAC/C,QAAM,MAAM,MAAM,GAAG,EAAE,QAAQ,MAAM,WAAW,CAAC;AACjD,SAAO,IAAI;AACb;AAMA,eAAsB,0BACpB,QACA,WACA,MAC2B;AAC3B,QAAM,EAAE,KAAK,SAAS,IAAI,MAAM,MAAM;AACtC,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,6BAA6B;AACvD,MAAI,KAAK,OAAO,IAAI,OAAO,KAAM,OAAM,IAAI,MAAM,SAAS;AAC1D,QAAM,cAAc,MAAM,IAAI,QAAgB,CAAC,SAAS,WAAW;AACjE,UAAM,SAAS,IAAI,WAAW;AAC9B,WAAO,SAAS,MAAM,QAAQ,OAAO,OAAO,MAAM,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,EAAE;AACvE,WAAO,UAAU,MAAM,OAAO,IAAI,MAAM,wBAAwB,CAAC;AACjE,WAAO,cAAc,IAAI;AAAA,EAC3B,CAAC;AACD,QAAM,KAAK,SAAS,KAAK,uBAAuB;AAChD,QAAM,MAAM,MAAM,GAAG,EAAE,QAAQ,WAAW,UAAU,KAAK,MAAM,aAAa,KAAK,MAAM,YAAY,CAAC;AACpG,SAAO,IAAI;AACb;AAGO,SAAS,gBAAgB,SAA+C,KAAqB;AAClG,QAAM,QAAQ,IAAI,MAAM,GAAG;AAC3B,SAAO,QAAQ,IAAI,CAAC,GAAG,MAAM,GAAG,EAAE,IAAI,IAAI,MAAM,CAAC,KAAK,GAAG,EAAE,EAAE,KAAK,QAAK;AACzE;AAGO,SAAS,eAAe,MAA6B;AAC1D,MAAI,OAAO,WAAW,YAAa,QAAO;AAC1C,SAAO,IAAI,gBAAgB,OAAO,SAAS,MAAM,EAAE,IAAI,IAAI;AAC7D;AAMA,eAAsB,yBACpB,QACA,SAC8C;AAC9C,QAAM,EAAE,KAAK,SAAS,IAAI,MAAM,MAAM;AACtC,MAAI,CAAC,IAAK,QAAO,EAAE,QAAQ,UAAU;AACrC,QAAM,KAAK,SAAS,KAAK,sBAAsB;AAC/C,QAAM,MAAM,MAAM,GAAG,EAAE,QAAQ,QAAQ,CAAC;AACxC,SAAO,IAAI;AACb;AAMA,MAAM,YAAgC;AAAA,EACpC,CAAC,kBAAkB,SAAS;AAAA,EAC5B,CAAC,aAAa,SAAS;AAAA,EACvB,CAAC,cAAc,SAAS;AAAA,EACxB,CAAC,QAAQ,SAAS;AAAA,EAClB,CAAC,oBAAoB,SAAS;AAAA,EAC9B,CAAC,aAAa,SAAS;AAAA,EACvB,CAAC,+BAA+B,SAAS;AAAA,EACzC,CAAC,mBAAmB,SAAS;AAAA,EAC7B,CAAC,eAAe,SAAS;AAAA,EACzB,CAAC,qBAAqB,SAAS;AAAA,EAC/B,CAAC,qBAAqB,SAAS;AAAA,EAC/B,CAAC,iBAAiB,SAAS;AAAA,EAC3B,CAAC,eAAe,SAAS;AAAA,EACzB,CAAC,gCAAgC,SAAS;AAC5C;AAMO,SAAS,aAAa,MAA6B;AACxD,QAAM,IAAI,KAAK,YAAY;AAC3B,aAAW,CAAC,IAAI,GAAG,KAAK,UAAW,KAAI,GAAG,KAAK,CAAC,EAAG,QAAO;AAC1D,SAAO;AACT;AAGO,SAAS,WAAW,SAAyD;AAClF,UAAQ,QAAQ,WAAW,CAAC,GAAG,KAAK,CAAC,MAAM,mBAAmB,KAAK,EAAE,IAAI,CAAC,KAAK;AACjF;","names":[]}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -11,6 +11,6 @@ export { validateEnv, assertEnv, type EnvReport } from "./env.js";
|
|
|
11
11
|
export { revalidatePOST } from "./revalidate.js";
|
|
12
12
|
export { draftPOST } from "./draft.js";
|
|
13
13
|
export { versionGET } from "./version.js";
|
|
14
|
-
export { pathFromSlugs, listPublishedPages, getPublishedPageByPath, getPublishedPageByPathExpanded, getSiteSymbols, expandSymbols, getSiteCustomBlocks, getPublicSiteSettings, getDraftPageById, getDraftPageByPath, type StoredPage, } from "./pages.js";
|
|
14
|
+
export { pathFromSlugs, listPublishedPages, listPublishedNews, getPublishedPageByPath, getPublishedPageByPathExpanded, getSiteSymbols, expandSymbols, getSiteCustomBlocks, getPublicSiteSettings, getDraftPageById, getDraftPageByPath, type StoredPage, } from "./pages.js";
|
|
15
15
|
export { getBundle, getBundlePage, getBundleNews, getNewsPageData, type PublishedBundle, type BundleItem, } from "./bundle.js";
|
|
16
16
|
export { siteLocales, parseLocaleSlugs, localizedPath, localizePage, localizeBlocks, pruneTranslations, TranslationsSchema, type Translations, type LocaleConfig, } from "../i18n/overlay.js";
|
package/dist/server/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { versionGET } from "./version.js";
|
|
|
23
23
|
import {
|
|
24
24
|
pathFromSlugs,
|
|
25
25
|
listPublishedPages,
|
|
26
|
+
listPublishedNews,
|
|
26
27
|
getPublishedPageByPath,
|
|
27
28
|
getPublishedPageByPathExpanded,
|
|
28
29
|
getSiteSymbols,
|
|
@@ -74,6 +75,7 @@ export {
|
|
|
74
75
|
getSiteCustomBlocks,
|
|
75
76
|
getSiteSymbols,
|
|
76
77
|
jsonLdHtml,
|
|
78
|
+
listPublishedNews,
|
|
77
79
|
listPublishedPages,
|
|
78
80
|
localizeBlocks,
|
|
79
81
|
localizePage,
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["/**\n * @xiwebdesign/xicms-core/server — server-only helpers (firebase-admin, sanitization, SEO,\n * pagina-reads). NOOIT importeren in client-componenten.\n */\nimport \"server-only\";\n\n// `getAdminAuth` zit BEWUST niet hier maar achter de subpath `@xiwebdesign/xicms-core/server/auth`\n// (zie ./auth.ts): firebase-admin/auth sloopt via jwks-rsa/jose élke server-render op Vercel, en\n// hoort dus niet in de bundel van sites die alleen pagina's lezen.\nexport { getAdminApp, getAdminDb } from \"../firebase/admin\";\nexport { sanitizeRichText } from \"./sanitize\";\n// Veilige JSON-LD-serialisatie voor <script type=\"application/ld+json\"> in consumer-routes\n// (escapet `<` zodat klant-tekst met </script> de pagina niet kan breken).\nexport { jsonLdHtml } from \"../blocks/builtins/jsonld\";\nexport {\n buildMetadata,\n buildJsonLd,\n buildSitemap,\n buildRobots,\n siteOrigin,\n businessJsonLd,\n breadcrumbListJsonLd,\n articleJsonLd,\n productJsonLd,\n openingHoursJsonLd,\n galleryJsonLd,\n videoObjectJsonLd,\n type BusinessInfo,\n} from \"./seo\";\nexport { validateEnv, assertEnv, type EnvReport } from \"./env\";\n// On-demand revalidation (6.5): POST-handler voor `app/api/revalidate/route.ts` in de klantsite.\n// De backend roept die route na een publicatie aan → wijziging binnen seconden live, zonder rebuild.\nexport { revalidatePOST } from \"./revalidate\";\n// Preview (Draft Mode): POST-handler voor `app/api/draft/route.ts` in de klantsite. Zet de\n// bypass-cookie zodat een ingelogde editor de content-bundel + ISR-cache overslaat, terwijl de\n// pagina-route voor publiek statisch blijft.\nexport { draftPOST } from \"./draft\";\n// Versie-endpoint: GET-handler voor `app/api/version/route.ts`, zodat het superbeheer kan zien op\n// welke engine een DEPLOYMENT draait (en niet alleen wanneer er voor het laatst is ingelogd).\nexport { versionGET } from \"./version\";\n// Shop-SSR (6.0): opt-in via subpath `@xiwebdesign/xicms-core/server/shop` (readers + product-SEO).\n// Niet meer onvoorwaardelijk her-geëxporteerd, zodat shop-servercode niet in elke site landt.\nexport {\n pathFromSlugs,\n listPublishedPages,\n getPublishedPageByPath,\n getPublishedPageByPathExpanded,\n getSiteSymbols,\n expandSymbols,\n getSiteCustomBlocks,\n getPublicSiteSettings,\n getDraftPageById,\n getDraftPageByPath,\n type StoredPage,\n} from \"./pages\";\n// Content-bundle (5.7): build-time reader van `content/published.bundle.json`. De pagina-/settings-/\n// symbol-/customBlock-readers hierboven zijn al bundle-first; deze exports zijn voor directe\n// consumptie (bv. nieuws-blockData in de klant-routes).\nexport {\n getBundle,\n getBundlePage,\n getBundleNews,\n getNewsPageData,\n type PublishedBundle,\n type BundleItem,\n} from \"./bundle\";\n// Localisatie (5.0) — server-safe overlay-helpers, ook bruikbaar in route-handlers/layouts.\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n type Translations,\n type LocaleConfig,\n} from \"../i18n/overlay\";\n"],"mappings":"AAIA,OAAO;AAKP,SAAS,aAAa,kBAAkB;AACxC,SAAS,wBAAwB;AAGjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,aAAa,iBAAiC;AAGvD,SAAS,sBAAsB;AAI/B,SAAS,iBAAiB;AAG1B,SAAS,kBAAkB;AAG3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAIP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/index.ts"],"sourcesContent":["/**\n * @xiwebdesign/xicms-core/server — server-only helpers (firebase-admin, sanitization, SEO,\n * pagina-reads). NOOIT importeren in client-componenten.\n */\nimport \"server-only\";\n\n// `getAdminAuth` zit BEWUST niet hier maar achter de subpath `@xiwebdesign/xicms-core/server/auth`\n// (zie ./auth.ts): firebase-admin/auth sloopt via jwks-rsa/jose élke server-render op Vercel, en\n// hoort dus niet in de bundel van sites die alleen pagina's lezen.\nexport { getAdminApp, getAdminDb } from \"../firebase/admin\";\nexport { sanitizeRichText } from \"./sanitize\";\n// Veilige JSON-LD-serialisatie voor <script type=\"application/ld+json\"> in consumer-routes\n// (escapet `<` zodat klant-tekst met </script> de pagina niet kan breken).\nexport { jsonLdHtml } from \"../blocks/builtins/jsonld\";\nexport {\n buildMetadata,\n buildJsonLd,\n buildSitemap,\n buildRobots,\n siteOrigin,\n businessJsonLd,\n breadcrumbListJsonLd,\n articleJsonLd,\n productJsonLd,\n openingHoursJsonLd,\n galleryJsonLd,\n videoObjectJsonLd,\n type BusinessInfo,\n} from \"./seo\";\nexport { validateEnv, assertEnv, type EnvReport } from \"./env\";\n// On-demand revalidation (6.5): POST-handler voor `app/api/revalidate/route.ts` in de klantsite.\n// De backend roept die route na een publicatie aan → wijziging binnen seconden live, zonder rebuild.\nexport { revalidatePOST } from \"./revalidate\";\n// Preview (Draft Mode): POST-handler voor `app/api/draft/route.ts` in de klantsite. Zet de\n// bypass-cookie zodat een ingelogde editor de content-bundel + ISR-cache overslaat, terwijl de\n// pagina-route voor publiek statisch blijft.\nexport { draftPOST } from \"./draft\";\n// Versie-endpoint: GET-handler voor `app/api/version/route.ts`, zodat het superbeheer kan zien op\n// welke engine een DEPLOYMENT draait (en niet alleen wanneer er voor het laatst is ingelogd).\nexport { versionGET } from \"./version\";\n// Shop-SSR (6.0): opt-in via subpath `@xiwebdesign/xicms-core/server/shop` (readers + product-SEO).\n// Niet meer onvoorwaardelijk her-geëxporteerd, zodat shop-servercode niet in elke site landt.\nexport {\n pathFromSlugs,\n listPublishedPages,\n listPublishedNews,\n getPublishedPageByPath,\n getPublishedPageByPathExpanded,\n getSiteSymbols,\n expandSymbols,\n getSiteCustomBlocks,\n getPublicSiteSettings,\n getDraftPageById,\n getDraftPageByPath,\n type StoredPage,\n} from \"./pages\";\n// Content-bundle (5.7): build-time reader van `content/published.bundle.json`. De pagina-/settings-/\n// symbol-/customBlock-readers hierboven zijn al bundle-first; deze exports zijn voor directe\n// consumptie (bv. nieuws-blockData in de klant-routes).\nexport {\n getBundle,\n getBundlePage,\n getBundleNews,\n getNewsPageData,\n type PublishedBundle,\n type BundleItem,\n} from \"./bundle\";\n// Localisatie (5.0) — server-safe overlay-helpers, ook bruikbaar in route-handlers/layouts.\nexport {\n siteLocales,\n parseLocaleSlugs,\n localizedPath,\n localizePage,\n localizeBlocks,\n pruneTranslations,\n TranslationsSchema,\n type Translations,\n type LocaleConfig,\n} from \"../i18n/overlay\";\n"],"mappings":"AAIA,OAAO;AAKP,SAAS,aAAa,kBAAkB;AACxC,SAAS,wBAAwB;AAGjC,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,aAAa,iBAAiC;AAGvD,SAAS,sBAAsB;AAI/B,SAAS,iBAAiB;AAG1B,SAAS,kBAAkB;AAG3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AAIP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;","names":[]}
|
package/dist/server/pages.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { type Page } from "../schema/pages.js";
|
|
|
3
3
|
import { type StoredSymbol } from "../schema/symbols.js";
|
|
4
4
|
import { type CustomBlock } from "../schema/customBlocks.js";
|
|
5
5
|
import { type PublicSiteSettings } from "../schema/settings.js";
|
|
6
|
+
import type { NewsArticle } from "../content/types.js";
|
|
6
7
|
/**
|
|
7
8
|
* BUNDLE-FIRST, Firestore-fallback (5.7).
|
|
8
9
|
*
|
|
@@ -49,6 +50,16 @@ declare function getPublishedPageByPathUncached(siteId: string, path: string, op
|
|
|
49
50
|
export declare function getDraftPageById(siteId: string, pageId: string): Promise<StoredPage | null>;
|
|
50
51
|
/** Draft op pad (preview). */
|
|
51
52
|
export declare function getDraftPageByPath(siteId: string, path: string): Promise<StoredPage | null>;
|
|
53
|
+
declare function listPublishedNewsUncached(siteId: string): Promise<NewsArticle[]>;
|
|
54
|
+
/**
|
|
55
|
+
* Gepubliceerde nieuwsartikelen, nieuwste eerst. Live-first (bundle als statische fallback) of
|
|
56
|
+
* bundle-first — exact hetzelfde contract als `listPublishedPages`.
|
|
57
|
+
*
|
|
58
|
+
* Gebruik dit in de publieke nieuwsroutes. `getNewsPageData` blijft bestaan en doet precies wat de
|
|
59
|
+
* naam zegt: de bundle lezen, synchroon — handig waar je gegarandeerd build-time data wilt (sitemap
|
|
60
|
+
* bij een statische build), maar het is niet de verse bron.
|
|
61
|
+
*/
|
|
62
|
+
export declare const listPublishedNews: typeof listPublishedNewsUncached;
|
|
52
63
|
/** Alle symbolen van een site (gevalideerd; ongeldige overgeslagen met waarschuwing). Live-first of bundle-first. */
|
|
53
64
|
declare function getSiteSymbolsUncached(siteId: string): Promise<StoredSymbol[]>;
|
|
54
65
|
/**
|
package/dist/server/pages.js
CHANGED
|
@@ -7,6 +7,7 @@ import { CustomBlockSchema } from "../schema/customBlocks.js";
|
|
|
7
7
|
import { PublicSiteSettingsSchema } from "../schema/settings.js";
|
|
8
8
|
import { localizePage, localizeBlocks } from "../i18n/overlay.js";
|
|
9
9
|
import { getBundle, getBundlePage } from "./bundle.js";
|
|
10
|
+
import { sanitizeRichText } from "./sanitize.js";
|
|
10
11
|
const ISR_FALLBACK = Number(process.env.XICMS_ISR_FALLBACK) || 3600;
|
|
11
12
|
const ISR_NO_PUSH = Number(process.env.XICMS_ISR_FALLBACK_NOPUSH) || 300;
|
|
12
13
|
const cacheTags = {
|
|
@@ -269,6 +270,51 @@ async function readCollection(siteId, collectionId) {
|
|
|
269
270
|
return [];
|
|
270
271
|
}
|
|
271
272
|
}
|
|
273
|
+
function isPublishedNews(data) {
|
|
274
|
+
return data.published === true || data.status === "published";
|
|
275
|
+
}
|
|
276
|
+
function plain(v) {
|
|
277
|
+
if (v == null || typeof v !== "object") return v;
|
|
278
|
+
if (Array.isArray(v)) return v.map(plain);
|
|
279
|
+
if (v instanceof Date) return v.toISOString();
|
|
280
|
+
const kandidaat = v;
|
|
281
|
+
if (typeof kandidaat.toDate === "function") return kandidaat.toDate().toISOString();
|
|
282
|
+
const proto = Object.getPrototypeOf(v);
|
|
283
|
+
if (proto !== Object.prototype && proto !== null) return void 0;
|
|
284
|
+
const uit = {};
|
|
285
|
+
for (const [k, x] of Object.entries(v)) {
|
|
286
|
+
const w = plain(x);
|
|
287
|
+
if (w !== void 0) uit[k] = w;
|
|
288
|
+
}
|
|
289
|
+
return uit;
|
|
290
|
+
}
|
|
291
|
+
function toPublishedNews(rows) {
|
|
292
|
+
return rows.filter((r) => isPublishedNews(r.data)).map((r) => {
|
|
293
|
+
const raw = r.data.bodyHtml;
|
|
294
|
+
const data = { id: r.id, ...plain(r.data) };
|
|
295
|
+
if (typeof raw === "string") data.bodyHtml = sanitizeRichText(raw);
|
|
296
|
+
return data;
|
|
297
|
+
}).sort((a, b) => String(b.date ?? "").localeCompare(String(a.date ?? "")));
|
|
298
|
+
}
|
|
299
|
+
function bundleNewsRows(siteId) {
|
|
300
|
+
const bundle = getBundle();
|
|
301
|
+
return bundle && bundle.siteId === siteId ? bundle.news.map((n) => ({ id: n.id, data: n })) : [];
|
|
302
|
+
}
|
|
303
|
+
async function listPublishedNewsUncached(siteId) {
|
|
304
|
+
let rows;
|
|
305
|
+
if (LIVE_READS) {
|
|
306
|
+
rows = await liveOrBundle(
|
|
307
|
+
`listPublishedNews ${siteId}`,
|
|
308
|
+
() => readCollectionThrowing(siteId, "news"),
|
|
309
|
+
() => bundleNewsRows(siteId)
|
|
310
|
+
);
|
|
311
|
+
} else {
|
|
312
|
+
const bundle = getBundle();
|
|
313
|
+
rows = bundle && bundle.siteId === siteId ? bundleNewsRows(siteId) : await readCollection(siteId, "news");
|
|
314
|
+
}
|
|
315
|
+
return toPublishedNews(rows);
|
|
316
|
+
}
|
|
317
|
+
const listPublishedNews = cache(listPublishedNewsUncached);
|
|
272
318
|
function bundleSymbolRows(siteId) {
|
|
273
319
|
const bundle = getBundle();
|
|
274
320
|
return bundle && bundle.siteId === siteId ? bundle.symbols.map((s) => ({ id: s.id, data: s })) : [];
|
|
@@ -407,6 +453,7 @@ export {
|
|
|
407
453
|
getPublishedPageByPathExpanded,
|
|
408
454
|
getSiteCustomBlocks,
|
|
409
455
|
getSiteSymbols,
|
|
456
|
+
listPublishedNews,
|
|
410
457
|
listPublishedPages,
|
|
411
458
|
liveReadsActive,
|
|
412
459
|
pathFromSlugs
|