@xiwebdesign/xicms-core 5.8.1 → 5.8.3

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.
@@ -6,13 +6,15 @@ import { defineBlock } from "../../../blocks/registry";
6
6
  import { Card, Heading, Text } from "../../../blocks/ui";
7
7
  import { useBlockData } from "../../../blocks/context";
8
8
  import { useShopStrings } from "../strings";
9
- import { fetchShopCategories, useShopSettings } from "../lib";
9
+ import { fetchShopCategories, readQueryParam, useShopSettings } from "../lib";
10
10
  function ShopCategoriesRender({ block, siteId }) {
11
11
  const settings = useShopSettings();
12
12
  const t = useShopStrings();
13
13
  const serverData = useBlockData("shop");
14
14
  const [cats, setCats] = useState(serverData?.categories ?? null);
15
+ const [activeSlug, setActiveSlug] = useState(null);
15
16
  useEffect(() => {
17
+ setActiveSlug(readQueryParam("cat"));
16
18
  if (serverData?.categories) return;
17
19
  void fetchShopCategories(siteId).then(setCats).catch((e) => {
18
20
  console.warn("[xicms] shop-categorie\xEBn laden mislukt:", e);
@@ -20,6 +22,22 @@ function ShopCategoriesRender({ block, siteId }) {
20
22
  });
21
23
  }, [siteId]);
22
24
  if (cats === null) return null;
25
+ const active = activeSlug ? cats.find((c) => c.slug === activeSlug) : void 0;
26
+ if (active) {
27
+ return /* @__PURE__ */ jsxs("nav", { className: "xicms-shop-cat-chips", "aria-label": "Categorie\xEBn", children: [
28
+ /* @__PURE__ */ jsx("a", { className: "xicms-shop-cat-chip", href: settings.shopPath, children: t("allCategories") }),
29
+ cats.map((c) => /* @__PURE__ */ jsx(
30
+ "a",
31
+ {
32
+ className: `xicms-shop-cat-chip${c.slug === activeSlug ? " xicms-shop-cat-chip--active" : ""}`,
33
+ href: `${settings.shopPath}?cat=${encodeURIComponent(c.slug)}`,
34
+ "aria-current": c.slug === activeSlug ? "page" : void 0,
35
+ children: c.name
36
+ },
37
+ c.id
38
+ ))
39
+ ] });
40
+ }
23
41
  if (cats.length === 0) {
24
42
  return /* @__PURE__ */ jsx(Card, { padding: 4, variant: "muted", children: /* @__PURE__ */ jsx(Text, { children: t("noCategories") }) });
25
43
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/shop/blocks/ShopCategories.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { ShopCategoriesBlockSchema } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { useBlockData } from \"../../../blocks/context\";\nimport { useShopStrings } from \"../strings\";\nimport { fetchShopCategories, useShopSettings, type StoredShopCategory } from \"../lib\";\nimport type { ShopPageData } from \"../types\";\n\ntype Blk = z.infer<typeof ShopCategoriesBlockSchema>;\n\n/** Categorie-tegels — linkt naar de winkelpagina met `?cat=<slug>` (productGrid filtert daarop). */\nfunction ShopCategoriesRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [cats, setCats] = useState<StoredShopCategory[] | null>(serverData?.categories ?? null);\n\n useEffect(() => {\n if (serverData?.categories) return; // SSR-data aanwezig — geen client-fetch\n void fetchShopCategories(siteId).then(setCats).catch((e) => {\n console.warn(\"[xicms] shop-categorieën laden mislukt:\", e);\n setCats([]);\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [siteId]);\n\n if (cats === null) return null;\n if (cats.length === 0) {\n return (\n <Card padding={4} variant=\"muted\">\n <Text>{t(\"noCategories\")}</Text>\n </Card>\n );\n }\n\n return (\n <div className=\"xicms-shop-categories\">\n {block.heading && (\n <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>\n )}\n <div\n className=\"xicms-shop-categories__grid\"\n style={{\n display: \"grid\",\n gridTemplateColumns: `repeat(auto-fill, minmax(min(260px, calc(100%/${block.columns} - 1rem)), 1fr))`,\n gap: \"1rem\",\n marginTop: block.heading ? \"1rem\" : 0,\n }}\n >\n {cats.map((c) => (\n <a\n key={c.id}\n className=\"xicms-shop-categories__item\"\n href={`${settings.shopPath}?cat=${encodeURIComponent(c.slug)}`}\n style={{ textDecoration: \"none\", color: \"inherit\" }}\n >\n <Card padding={0}>\n {c.imageUrl && (\n <img\n src={c.imageUrl}\n alt={c.name}\n loading=\"lazy\"\n style={{ width: \"100%\", aspectRatio: \"4/3\", objectFit: \"cover\", display: \"block\", borderRadius: \"inherit\" }}\n />\n )}\n <div style={{ padding: \"0.9rem 1rem\" }}>\n <Text weight=\"bold\" display=\"block\">{c.name}</Text>\n {c.description && (\n <Text size=\"sm\" display=\"block\" style={{ opacity: 0.75, marginTop: 4 }}>{c.description}</Text>\n )}\n </div>\n </Card>\n </a>\n ))}\n </div>\n </div>\n );\n}\n\nexport const shopCategoriesBlock = defineBlock({\n type: \"shopCategories\",\n schema: ShopCategoriesBlockSchema,\n Render: ShopCategoriesRender,\n label: \"Winkel-categorieën\",\n icon: \"🗂️\",\n defaults: () => ({ columns: 3 }),\n});\n"],"mappings":";AAkCQ,cAmCM,YAnCN;AAhCR,SAAS,WAAW,gBAAgB;AAEpC,SAAS,iCAAiC;AAC1C,SAAS,mBAA0C;AACnD,SAAS,MAAM,SAAS,YAAY;AACpC,SAAS,oBAAoB;AAC7B,SAAS,sBAAsB;AAC/B,SAAS,qBAAqB,uBAAgD;AAM9E,SAAS,qBAAqB,EAAE,OAAO,OAAO,GAA0B;AACtE,QAAM,WAAW,gBAAgB;AACjC,QAAM,IAAI,eAAe;AACzB,QAAM,aAAa,aAA2B,MAAM;AACpD,QAAM,CAAC,MAAM,OAAO,IAAI,SAAsC,YAAY,cAAc,IAAI;AAE5F,YAAU,MAAM;AACd,QAAI,YAAY,WAAY;AAC5B,SAAK,oBAAoB,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,CAAC,MAAM;AAC1D,cAAQ,KAAK,8CAA2C,CAAC;AACzD,cAAQ,CAAC,CAAC;AAAA,IACZ,CAAC;AAAA,EAEH,GAAG,CAAC,MAAM,CAAC;AAEX,MAAI,SAAS,KAAM,QAAO;AAC1B,MAAI,KAAK,WAAW,GAAG;AACrB,WACE,oBAAC,QAAK,SAAS,GAAG,SAAQ,SACxB,8BAAC,QAAM,YAAE,cAAc,GAAE,GAC3B;AAAA,EAEJ;AAEA,SACE,qBAAC,SAAI,WAAU,yBACZ;AAAA,UAAM,WACL,oBAAC,WAAQ,OAAO,GAAG,mBAAgB,WAAW,gBAAM,SAAQ;AAAA,IAE9D;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,OAAO;AAAA,UACL,SAAS;AAAA,UACT,qBAAqB,iDAAiD,MAAM,OAAO;AAAA,UACnF,KAAK;AAAA,UACL,WAAW,MAAM,UAAU,SAAS;AAAA,QACtC;AAAA,QAEC,eAAK,IAAI,CAAC,MACT;AAAA,UAAC;AAAA;AAAA,YAEC,WAAU;AAAA,YACV,MAAM,GAAG,SAAS,QAAQ,QAAQ,mBAAmB,EAAE,IAAI,CAAC;AAAA,YAC5D,OAAO,EAAE,gBAAgB,QAAQ,OAAO,UAAU;AAAA,YAElD,+BAAC,QAAK,SAAS,GACZ;AAAA,gBAAE,YACD;AAAA,gBAAC;AAAA;AAAA,kBACC,KAAK,EAAE;AAAA,kBACP,KAAK,EAAE;AAAA,kBACP,SAAQ;AAAA,kBACR,OAAO,EAAE,OAAO,QAAQ,aAAa,OAAO,WAAW,SAAS,SAAS,SAAS,cAAc,UAAU;AAAA;AAAA,cAC5G;AAAA,cAEF,qBAAC,SAAI,OAAO,EAAE,SAAS,cAAc,GACnC;AAAA,oCAAC,QAAK,QAAO,QAAO,SAAQ,SAAS,YAAE,MAAK;AAAA,gBAC3C,EAAE,eACD,oBAAC,QAAK,MAAK,MAAK,SAAQ,SAAQ,OAAO,EAAE,SAAS,MAAM,WAAW,EAAE,GAAI,YAAE,aAAY;AAAA,iBAE3F;AAAA,eACF;AAAA;AAAA,UApBK,EAAE;AAAA,QAqBT,CACD;AAAA;AAAA,IACH;AAAA,KACF;AAEJ;AAEO,MAAM,sBAAsB,YAAY;AAAA,EAC7C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,SAAS,EAAE;AAChC,CAAC;","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/shop/blocks/ShopCategories.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { ShopCategoriesBlockSchema } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { useBlockData } from \"../../../blocks/context\";\nimport { useShopStrings } from \"../strings\";\nimport { fetchShopCategories, readQueryParam, useShopSettings, type StoredShopCategory } from \"../lib\";\nimport type { ShopPageData } from \"../types\";\n\ntype Blk = z.infer<typeof ShopCategoriesBlockSchema>;\n\n/** Categorie-tegels — linkt naar de winkelpagina met `?cat=<slug>` (productGrid filtert daarop). */\nfunction ShopCategoriesRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [cats, setCats] = useState<StoredShopCategory[] | null>(serverData?.categories ?? null);\n // Actieve categorie uit de URL (client-side; op de server tonen we altijd de volle tegels).\n const [activeSlug, setActiveSlug] = useState<string | null>(null);\n\n useEffect(() => {\n setActiveSlug(readQueryParam(\"cat\"));\n if (serverData?.categories) return; // SSR-data aanwezig — geen client-fetch\n void fetchShopCategories(siteId).then(setCats).catch((e) => {\n console.warn(\"[xicms] shop-categorieën laden mislukt:\", e);\n setCats([]);\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [siteId]);\n\n if (cats === null) return null;\n\n // Ingeklapt: zodra er een categorie gekozen is, tonen we geen schermvullende tegels meer maar\n // een compacte, horizontaal scrollbare chip-rij (actieve gemarkeerd). Zo staan de producten\n // meteen in beeld i.p.v. onder de tegels — vooral op mobiel. \"Alles\" schakelt het filter uit.\n const active = activeSlug ? cats.find((c) => c.slug === activeSlug) : undefined;\n if (active) {\n return (\n <nav className=\"xicms-shop-cat-chips\" aria-label=\"Categorieën\">\n <a className=\"xicms-shop-cat-chip\" href={settings.shopPath}>{t(\"allCategories\")}</a>\n {cats.map((c) => (\n <a\n key={c.id}\n className={`xicms-shop-cat-chip${c.slug === activeSlug ? \" xicms-shop-cat-chip--active\" : \"\"}`}\n href={`${settings.shopPath}?cat=${encodeURIComponent(c.slug)}`}\n aria-current={c.slug === activeSlug ? \"page\" : undefined}\n >\n {c.name}\n </a>\n ))}\n </nav>\n );\n }\n\n if (cats.length === 0) {\n return (\n <Card padding={4} variant=\"muted\">\n <Text>{t(\"noCategories\")}</Text>\n </Card>\n );\n }\n\n return (\n <div className=\"xicms-shop-categories\">\n {block.heading && (\n <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>\n )}\n <div\n className=\"xicms-shop-categories__grid\"\n style={{\n display: \"grid\",\n gridTemplateColumns: `repeat(auto-fill, minmax(min(260px, calc(100%/${block.columns} - 1rem)), 1fr))`,\n gap: \"1rem\",\n marginTop: block.heading ? \"1rem\" : 0,\n }}\n >\n {cats.map((c) => (\n <a\n key={c.id}\n className=\"xicms-shop-categories__item\"\n href={`${settings.shopPath}?cat=${encodeURIComponent(c.slug)}`}\n style={{ textDecoration: \"none\", color: \"inherit\" }}\n >\n <Card padding={0}>\n {c.imageUrl && (\n <img\n src={c.imageUrl}\n alt={c.name}\n loading=\"lazy\"\n style={{ width: \"100%\", aspectRatio: \"4/3\", objectFit: \"cover\", display: \"block\", borderRadius: \"inherit\" }}\n />\n )}\n <div style={{ padding: \"0.9rem 1rem\" }}>\n <Text weight=\"bold\" display=\"block\">{c.name}</Text>\n {c.description && (\n <Text size=\"sm\" display=\"block\" style={{ opacity: 0.75, marginTop: 4 }}>{c.description}</Text>\n )}\n </div>\n </Card>\n </a>\n ))}\n </div>\n </div>\n );\n}\n\nexport const shopCategoriesBlock = defineBlock({\n type: \"shopCategories\",\n schema: ShopCategoriesBlockSchema,\n Render: ShopCategoriesRender,\n label: \"Winkel-categorieën\",\n icon: \"🗂️\",\n defaults: () => ({ columns: 3 }),\n});\n"],"mappings":"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","names":[]}
@@ -11,7 +11,6 @@ export type StoredShopCategory = ShopCategory & {
11
11
  export declare function fetchActiveProducts(siteId: string): Promise<StoredProduct[]>;
12
12
  export declare function fetchProductBySlug(siteId: string, slug: string): Promise<StoredProduct | null>;
13
13
  export declare function fetchShopCategories(siteId: string): Promise<StoredShopCategory[]>;
14
- /** Start de checkout: order + Stripe-sessie server-side; geeft de redirect-URL terug. */
15
14
  export declare function createOrderCall(input: CreateOrderInput): Promise<{
16
15
  url: string;
17
16
  orderNumber: string;
@@ -38,11 +38,25 @@ async function fetchShopCategories(siteId) {
38
38
  const snap = await getDocs(query(collection(db, "sites", siteId, "shopCategories"), fsLimit(100)));
39
39
  return snap.docs.map((d) => ({ id: d.id, ...d.data() })).filter((c) => c.visible !== false).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
40
40
  }
41
+ function stripEmpty(value) {
42
+ if (Array.isArray(value)) {
43
+ return value.map((v) => stripEmpty(v));
44
+ }
45
+ if (value && typeof value === "object") {
46
+ const out = {};
47
+ for (const [k, v] of Object.entries(value)) {
48
+ if (v === void 0 || v === null) continue;
49
+ out[k] = stripEmpty(v);
50
+ }
51
+ return out;
52
+ }
53
+ return value;
54
+ }
41
55
  async function createOrderCall(input) {
42
56
  const app = getFirebaseApp();
43
57
  if (!app) throw new Error("Webwinkel niet beschikbaar.");
44
58
  const fn = httpsCallable(getFunctions(app, functionsRegion()), "createOrder");
45
- const res = await fn(input);
59
+ const res = await fn(stripEmpty(input));
46
60
  return res.data;
47
61
  }
48
62
  async function validateDiscountCall(siteId, code, itemsCents) {
@@ -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 */\nimport { collection, getDocs, limit as fsLimit, query, where } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\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 = getDb();\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 = getDb();\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 = getDb();\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. */\nexport async function createOrderCall(input: CreateOrderInput): Promise<{ url: string; orderNumber: string }> {\n const app = getFirebaseApp();\n if (!app) throw new Error(\"Webwinkel niet beschikbaar.\");\n const fn = httpsCallable(getFunctions(app, functionsRegion()), \"createOrder\");\n const res = await fn(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 = getFirebaseApp();\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 = getFirebaseApp();\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 = getFirebaseApp();\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 */\nimport { collection, getDocs, limit as fsLimit, query, where } from \"firebase/firestore\";\nimport { getFunctions, httpsCallable } from \"firebase/functions\";\nimport { getDb, getFirebaseApp } from \"../../firebase/client\";\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 = getDb();\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 = getDb();\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 = getDb();\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 = getFirebaseApp();\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 = getFirebaseApp();\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 = getFirebaseApp();\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 = getFirebaseApp();\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":[]}
@@ -28,7 +28,7 @@ const NL = {
28
28
  pay: "Betalen \u2014 {bedrag}",
29
29
  busy: "Bezig\u2026",
30
30
  checkoutFailed: "Afrekenen is niet gelukt. Controleer je gegevens en probeer het opnieuw.",
31
- secureNote: "Je rekent veilig af via Stripe (o.a. iDEAL). Na betaling ontvang je een bevestiging per e-mail.",
31
+ secureNote: "Je rekent veilig af via een beveiligde betaalpagina (o.a. iDEAL). Na betaling ontvang je een bevestiging per e-mail.",
32
32
  yourOrder: "Je bestelling",
33
33
  fieldName: "Naam",
34
34
  fieldEmail: "E-mail",
@@ -88,7 +88,7 @@ const EN = {
88
88
  pay: "Pay \u2014 {bedrag}",
89
89
  busy: "Working\u2026",
90
90
  checkoutFailed: "Checkout failed. Check your details and try again.",
91
- secureNote: "You pay securely via Stripe. You will receive a confirmation email after payment.",
91
+ secureNote: "You pay securely via a protected checkout page (incl. iDEAL). You will receive a confirmation email after payment.",
92
92
  yourOrder: "Your order",
93
93
  fieldName: "Name",
94
94
  fieldEmail: "Email",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/modules/shop/strings.ts"],"sourcesContent":["\"use client\";\n\n/**\n * UI-teksten van de shop-blocks, gelokaliseerd op `SiteConfig.lang` (nl = default, en).\n * Interpolatie via `{naam}`-placeholders. Bewust een platte tabel — geen i18n-framework;\n * de shop-strings zijn de enige plek in de blocks met vaste UI-tekst.\n */\nimport { useSiteConfig } from \"../../config\";\n\nconst NL = {\n addToCart: \"In winkelwagen\",\n added: \"✓ Toegevoegd aan je winkelwagen.\",\n addedGoToCart: \"✓ In winkelwagen — bekijk\",\n view: \"Bekijk\",\n toCart: \"Naar winkelwagen\",\n continueShopping: \"Verder winkelen\",\n checkout: \"Afrekenen\",\n cartEmpty: \"Je winkelwagen is leeg.\",\n toShop: \"Naar de winkel\",\n subtotal: \"Subtotaal\",\n shippingCosts: \"Verzendkosten\",\n free: \"gratis\",\n total: \"Totaal\",\n perPiece: \"p/st\",\n qty: \"Aantal\",\n outOfStock: \"Uitverkocht in deze uitvoering.\",\n variantUnavailable: \"Deze combinatie is niet leverbaar.\",\n stockLeft: \"Nog {n} beschikbaar\",\n soldOut: \"Uitverkocht\",\n productNotFound: \"Product niet gevonden.\",\n backToShop: \"Terug naar de winkel\",\n noProducts: \"Geen producten gevonden.\",\n noCategories: \"Nog geen categorieën.\",\n allCategories: \"Alle categorieën\",\n pay: \"Betalen — {bedrag}\",\n busy: \"Bezig…\",\n checkoutFailed: \"Afrekenen is niet gelukt. Controleer je gegevens en probeer het opnieuw.\",\n secureNote: \"Je rekent veilig af via Stripe (o.a. iDEAL). Na betaling ontvang je een bevestiging per e-mail.\",\n yourOrder: \"Je bestelling\",\n fieldName: \"Naam\",\n fieldEmail: \"E-mail\",\n fieldPhone: \"Telefoon (optioneel)\",\n fieldAddress: \"Straat + huisnummer\",\n fieldZip: \"Postcode\",\n fieldCity: \"Plaats\",\n fieldNote: \"Opmerking (optioneel)\",\n choose: \"Kies…\",\n discountCode: \"Kortingscode\",\n discountApply: \"Toepassen\",\n discount: \"Korting\",\n discountInvalid: \"Deze code is niet (meer) geldig.\",\n discountApplied: \"Korting toegepast: {code}\",\n related: \"Ook leuk\",\n filterAll: \"Alles\",\n sortLabel: \"Sorteren\",\n sortDefault: \"Aanbevolen\",\n sortName: \"Naam\",\n sortPriceAsc: \"Prijs (laag-hoog)\",\n sortPriceDesc: \"Prijs (hoog-laag)\",\n personalizationUpload: \"Kies bestand…\",\n personalizationUploading: \"Uploaden…\",\n personalizationUploaded: \"✓ Geüpload\",\n personalizationTooBig: \"Bestand te groot (max 5 MB).\",\n personalizationFailed: \"Uploaden mislukt — probeer het opnieuw.\",\n personalizationSurcharge: \"+ {bedrag}\",\n donationFailed: \"Doneren is niet gelukt. Probeer het later opnieuw.\",\n donationCustomAmount: \"Ander bedrag\",\n donationAmountLabel: \"Bedrag in euro's (min. {min})\",\n};\n\nconst EN: typeof NL = {\n addToCart: \"Add to cart\",\n added: \"✓ Added to your cart.\",\n addedGoToCart: \"✓ In cart — view\",\n view: \"View\",\n toCart: \"Go to cart\",\n continueShopping: \"Continue shopping\",\n checkout: \"Checkout\",\n cartEmpty: \"Your cart is empty.\",\n toShop: \"Go to the shop\",\n subtotal: \"Subtotal\",\n shippingCosts: \"Shipping\",\n free: \"free\",\n total: \"Total\",\n perPiece: \"each\",\n qty: \"Quantity\",\n outOfStock: \"Sold out in this variant.\",\n variantUnavailable: \"This combination is unavailable.\",\n stockLeft: \"Only {n} left\",\n soldOut: \"Sold out\",\n productNotFound: \"Product not found.\",\n backToShop: \"Back to the shop\",\n noProducts: \"No products found.\",\n noCategories: \"No categories yet.\",\n allCategories: \"All categories\",\n pay: \"Pay — {bedrag}\",\n busy: \"Working…\",\n checkoutFailed: \"Checkout failed. Check your details and try again.\",\n secureNote: \"You pay securely via Stripe. You will receive a confirmation email after payment.\",\n yourOrder: \"Your order\",\n fieldName: \"Name\",\n fieldEmail: \"Email\",\n fieldPhone: \"Phone (optional)\",\n fieldAddress: \"Street + number\",\n fieldZip: \"Postal code\",\n fieldCity: \"City\",\n fieldNote: \"Note (optional)\",\n choose: \"Choose…\",\n discountCode: \"Discount code\",\n discountApply: \"Apply\",\n discount: \"Discount\",\n discountInvalid: \"This code is not (or no longer) valid.\",\n discountApplied: \"Discount applied: {code}\",\n related: \"You may also like\",\n filterAll: \"All\",\n sortLabel: \"Sort\",\n sortDefault: \"Recommended\",\n sortName: \"Name\",\n sortPriceAsc: \"Price (low-high)\",\n sortPriceDesc: \"Price (high-low)\",\n personalizationUpload: \"Choose file…\",\n personalizationUploading: \"Uploading…\",\n personalizationUploaded: \"✓ Uploaded\",\n personalizationTooBig: \"File too large (max 5 MB).\",\n personalizationFailed: \"Upload failed — please try again.\",\n personalizationSurcharge: \"+ {bedrag}\",\n donationFailed: \"Donation failed. Please try again later.\",\n donationCustomAmount: \"Custom amount\",\n donationAmountLabel: \"Amount in euros (min. {min})\",\n};\n\nexport type ShopStringKey = keyof typeof NL;\nconst TABLES: Record<string, typeof NL> = { nl: NL, en: EN };\n\nexport type ShopT = (key: ShopStringKey, vars?: Record<string, string | number>) => string;\n\n/** Vertaalfunctie voor shop-blocks: `t(\"addToCart\")`, `t(\"stockLeft\", { n: 3 })`. */\nexport function useShopStrings(): ShopT {\n const lang = (useSiteConfig().lang ?? \"nl\").slice(0, 2).toLowerCase();\n const table = TABLES[lang] ?? NL;\n return (key, vars) => {\n let s = table[key] ?? NL[key] ?? key;\n if (vars) for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, String(v));\n return s;\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/modules/shop/strings.ts"],"sourcesContent":["\"use client\";\n\n/**\n * UI-teksten van de shop-blocks, gelokaliseerd op `SiteConfig.lang` (nl = default, en).\n * Interpolatie via `{naam}`-placeholders. Bewust een platte tabel — geen i18n-framework;\n * de shop-strings zijn de enige plek in de blocks met vaste UI-tekst.\n */\nimport { useSiteConfig } from \"../../config\";\n\nconst NL = {\n addToCart: \"In winkelwagen\",\n added: \"✓ Toegevoegd aan je winkelwagen.\",\n addedGoToCart: \"✓ In winkelwagen — bekijk\",\n view: \"Bekijk\",\n toCart: \"Naar winkelwagen\",\n continueShopping: \"Verder winkelen\",\n checkout: \"Afrekenen\",\n cartEmpty: \"Je winkelwagen is leeg.\",\n toShop: \"Naar de winkel\",\n subtotal: \"Subtotaal\",\n shippingCosts: \"Verzendkosten\",\n free: \"gratis\",\n total: \"Totaal\",\n perPiece: \"p/st\",\n qty: \"Aantal\",\n outOfStock: \"Uitverkocht in deze uitvoering.\",\n variantUnavailable: \"Deze combinatie is niet leverbaar.\",\n stockLeft: \"Nog {n} beschikbaar\",\n soldOut: \"Uitverkocht\",\n productNotFound: \"Product niet gevonden.\",\n backToShop: \"Terug naar de winkel\",\n noProducts: \"Geen producten gevonden.\",\n noCategories: \"Nog geen categorieën.\",\n allCategories: \"Alle categorieën\",\n pay: \"Betalen — {bedrag}\",\n busy: \"Bezig…\",\n checkoutFailed: \"Afrekenen is niet gelukt. Controleer je gegevens en probeer het opnieuw.\",\n secureNote: \"Je rekent veilig af via een beveiligde betaalpagina (o.a. iDEAL). Na betaling ontvang je een bevestiging per e-mail.\",\n yourOrder: \"Je bestelling\",\n fieldName: \"Naam\",\n fieldEmail: \"E-mail\",\n fieldPhone: \"Telefoon (optioneel)\",\n fieldAddress: \"Straat + huisnummer\",\n fieldZip: \"Postcode\",\n fieldCity: \"Plaats\",\n fieldNote: \"Opmerking (optioneel)\",\n choose: \"Kies…\",\n discountCode: \"Kortingscode\",\n discountApply: \"Toepassen\",\n discount: \"Korting\",\n discountInvalid: \"Deze code is niet (meer) geldig.\",\n discountApplied: \"Korting toegepast: {code}\",\n related: \"Ook leuk\",\n filterAll: \"Alles\",\n sortLabel: \"Sorteren\",\n sortDefault: \"Aanbevolen\",\n sortName: \"Naam\",\n sortPriceAsc: \"Prijs (laag-hoog)\",\n sortPriceDesc: \"Prijs (hoog-laag)\",\n personalizationUpload: \"Kies bestand…\",\n personalizationUploading: \"Uploaden…\",\n personalizationUploaded: \"✓ Geüpload\",\n personalizationTooBig: \"Bestand te groot (max 5 MB).\",\n personalizationFailed: \"Uploaden mislukt — probeer het opnieuw.\",\n personalizationSurcharge: \"+ {bedrag}\",\n donationFailed: \"Doneren is niet gelukt. Probeer het later opnieuw.\",\n donationCustomAmount: \"Ander bedrag\",\n donationAmountLabel: \"Bedrag in euro's (min. {min})\",\n};\n\nconst EN: typeof NL = {\n addToCart: \"Add to cart\",\n added: \"✓ Added to your cart.\",\n addedGoToCart: \"✓ In cart — view\",\n view: \"View\",\n toCart: \"Go to cart\",\n continueShopping: \"Continue shopping\",\n checkout: \"Checkout\",\n cartEmpty: \"Your cart is empty.\",\n toShop: \"Go to the shop\",\n subtotal: \"Subtotal\",\n shippingCosts: \"Shipping\",\n free: \"free\",\n total: \"Total\",\n perPiece: \"each\",\n qty: \"Quantity\",\n outOfStock: \"Sold out in this variant.\",\n variantUnavailable: \"This combination is unavailable.\",\n stockLeft: \"Only {n} left\",\n soldOut: \"Sold out\",\n productNotFound: \"Product not found.\",\n backToShop: \"Back to the shop\",\n noProducts: \"No products found.\",\n noCategories: \"No categories yet.\",\n allCategories: \"All categories\",\n pay: \"Pay — {bedrag}\",\n busy: \"Working…\",\n checkoutFailed: \"Checkout failed. Check your details and try again.\",\n secureNote: \"You pay securely via a protected checkout page (incl. iDEAL). You will receive a confirmation email after payment.\",\n yourOrder: \"Your order\",\n fieldName: \"Name\",\n fieldEmail: \"Email\",\n fieldPhone: \"Phone (optional)\",\n fieldAddress: \"Street + number\",\n fieldZip: \"Postal code\",\n fieldCity: \"City\",\n fieldNote: \"Note (optional)\",\n choose: \"Choose…\",\n discountCode: \"Discount code\",\n discountApply: \"Apply\",\n discount: \"Discount\",\n discountInvalid: \"This code is not (or no longer) valid.\",\n discountApplied: \"Discount applied: {code}\",\n related: \"You may also like\",\n filterAll: \"All\",\n sortLabel: \"Sort\",\n sortDefault: \"Recommended\",\n sortName: \"Name\",\n sortPriceAsc: \"Price (low-high)\",\n sortPriceDesc: \"Price (high-low)\",\n personalizationUpload: \"Choose file…\",\n personalizationUploading: \"Uploading…\",\n personalizationUploaded: \"✓ Uploaded\",\n personalizationTooBig: \"File too large (max 5 MB).\",\n personalizationFailed: \"Upload failed — please try again.\",\n personalizationSurcharge: \"+ {bedrag}\",\n donationFailed: \"Donation failed. Please try again later.\",\n donationCustomAmount: \"Custom amount\",\n donationAmountLabel: \"Amount in euros (min. {min})\",\n};\n\nexport type ShopStringKey = keyof typeof NL;\nconst TABLES: Record<string, typeof NL> = { nl: NL, en: EN };\n\nexport type ShopT = (key: ShopStringKey, vars?: Record<string, string | number>) => string;\n\n/** Vertaalfunctie voor shop-blocks: `t(\"addToCart\")`, `t(\"stockLeft\", { n: 3 })`. */\nexport function useShopStrings(): ShopT {\n const lang = (useSiteConfig().lang ?? \"nl\").slice(0, 2).toLowerCase();\n const table = TABLES[lang] ?? NL;\n return (key, vars) => {\n let s = table[key] ?? NL[key] ?? key;\n if (vars) for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, String(v));\n return s;\n };\n}\n"],"mappings":"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","names":[]}
package/dist/styles.css CHANGED
@@ -2835,6 +2835,38 @@
2835
2835
  border-color: var(--xicms-primary, #4f46e5);
2836
2836
  color: var(--xicms-on-primary, #fff);
2837
2837
  }
2838
+
2839
+ /* Ingeklapte categorieën: compacte, horizontaal scrollbare chip-rij (na keuze van een categorie).
2840
+ Neemt één regel i.p.v. de schermvullende tegels, zodat de producten meteen in beeld staan. */
2841
+ .xicms-shop-cat-chips {
2842
+ display: flex;
2843
+ gap: 0.4rem;
2844
+ overflow-x: auto;
2845
+ -webkit-overflow-scrolling: touch;
2846
+ scrollbar-width: none;
2847
+ padding-bottom: 0.15rem;
2848
+ }
2849
+ .xicms-shop-cat-chips::-webkit-scrollbar {
2850
+ display: none;
2851
+ }
2852
+ .xicms-shop-cat-chip {
2853
+ flex: 0 0 auto;
2854
+ display: inline-flex;
2855
+ align-items: center;
2856
+ padding: 0.32rem 0.8rem;
2857
+ border-radius: 999px;
2858
+ border: 1px solid var(--xicms-border-strong, rgba(0, 0, 0, 0.18));
2859
+ background: var(--xicms-surface, #fff);
2860
+ color: inherit;
2861
+ font-size: 0.85rem;
2862
+ text-decoration: none;
2863
+ white-space: nowrap;
2864
+ }
2865
+ .xicms-shop-cat-chip--active {
2866
+ background: var(--xicms-primary, #4f46e5);
2867
+ border-color: var(--xicms-primary, #4f46e5);
2868
+ color: var(--xicms-on-primary, #fff);
2869
+ }
2838
2870
  .xicms-shop-filters__sort {
2839
2871
  display: inline-flex;
2840
2872
  align-items: center;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiwebdesign/xicms-core",
3
- "version": "5.8.1",
3
+ "version": "5.8.3",
4
4
  "description": "XiCMS engine: gestructureerde content, pagina's, draft/publish, admin (TipTap+Radix), SEO en beveiliging voor klantsites.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {