@xiwebdesign/xicms-core 5.5.2 → 5.5.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.
- package/dist/modules/shop/blocks/ProductDetail.js +8 -1
- package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
- package/dist/modules/shop/blocks/ProductGrid.js +39 -4
- package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
- package/dist/modules/shop/lib.d.ts +10 -0
- package/dist/modules/shop/lib.js +26 -0
- package/dist/modules/shop/lib.js.map +1 -1
- package/package.json +1 -1
|
@@ -76,7 +76,14 @@ function ProductDetailRender({ block, siteId }) {
|
|
|
76
76
|
alt: product.name,
|
|
77
77
|
style: { width: "100%", aspectRatio: "1/1", objectFit: "cover", borderRadius: "var(--xicms-radius, 12px)" }
|
|
78
78
|
}
|
|
79
|
-
) : /* @__PURE__ */ jsx(
|
|
79
|
+
) : /* @__PURE__ */ jsx(
|
|
80
|
+
"div",
|
|
81
|
+
{
|
|
82
|
+
className: "xicms-shop-detail__placeholder",
|
|
83
|
+
"aria-hidden": "true",
|
|
84
|
+
style: { width: "100%", aspectRatio: "1/1", background: "var(--xicms-muted, #f1f1f1)", borderRadius: "var(--xicms-radius, 12px)" }
|
|
85
|
+
}
|
|
86
|
+
),
|
|
80
87
|
(product.images?.length ?? 0) > 1 && /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8, marginTop: 8, flexWrap: "wrap" }, children: product.images.map((url, i) => /* @__PURE__ */ jsx(
|
|
81
88
|
"button",
|
|
82
89
|
{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/shop/blocks/ProductDetail.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { ProductDetailBlockSchema, formatCents, productPriceCents, variantKeyFor } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { Selector } from \"@astryxdesign/core/Selector\";\nimport { cartAdd } from \"../cartStore\";\nimport { fetchProductBySlug, readQueryParam, useShopSettings, variantLabelFor, type StoredProduct } from \"../lib\";\n\ntype Blk = z.infer<typeof ProductDetailBlockSchema>;\n\n/**\n * Productdetail — één generieke pagina met dit block toont elk product via `?p=<slug>`\n * (of een vast product via `block.slug`). Optie-selects, variant-prijs/voorraad, aantal,\n * in-winkelwagen.\n */\nfunction ProductDetailRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const [product, setProduct] = useState<StoredProduct | null | undefined>(undefined);\n const [choices, setChoices] = useState<Record<number, string>>({});\n const [qty, setQty] = useState(1);\n const [imageIdx, setImageIdx] = useState(0);\n const [added, setAdded] = useState(false);\n\n useEffect(() => {\n const slug = block.slug || readQueryParam(\"p\") || \"\";\n if (!slug) {\n setProduct(null);\n return;\n }\n void fetchProductBySlug(siteId, slug).then(setProduct).catch((e) => {\n console.warn(\"[xicms] product laden mislukt:\", e);\n setProduct(null);\n });\n }, [siteId, block.slug]);\n\n const options = product?.options ?? [];\n const allChosen = options.every((_, i) => Boolean(choices[i]));\n const variantKey = product && options.length > 0 && allChosen\n ? variantKeyFor(options.map((_, i) => choices[i]))\n : undefined;\n const variant = variantKey ? product?.variants?.find((v) => v.key === variantKey) : undefined;\n\n const priceCents = useMemo(() => {\n if (!product) return 0;\n return productPriceCents(product, variantKey);\n }, [product, variantKey]);\n\n /** Uitverkocht? Alleen met voorraadbeheer aan én een expliciet gezette voorraad (undefined = onbeperkt). */\n const outOfStock = Boolean(\n product?.trackStock &&\n (options.length === 0\n ? product.stock !== undefined && product.stock <= 0\n : variant?.stock !== undefined && variant.stock <= 0),\n );\n const variantInactive = variant !== undefined && variant.active === false;\n\n if (product === undefined) return null;\n if (product === null) {\n return (\n <Card padding={4} variant=\"muted\">\n <Text>Product niet gevonden. <a href={settings.shopPath}>Terug naar de winkel</a></Text>\n </Card>\n );\n }\n\n function addToCart() {\n if (!product) return;\n cartAdd(\n siteId,\n {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n variantKey,\n variantLabel: variantKey ? variantLabelFor(options, variantKey) : undefined,\n unitPriceCents: priceCents,\n imageUrl: product.images?.[0],\n },\n qty,\n );\n setAdded(true);\n }\n\n const canOrder = (options.length === 0 || allChosen) && !outOfStock && !variantInactive;\n\n return (\n <div\n className=\"xicms-shop-detail\"\n style={{ display: \"grid\", gap: \"2rem\", gridTemplateColumns: \"repeat(auto-fit, minmax(min(320px, 100%), 1fr))\" }}\n >\n <div className=\"xicms-shop-detail__media\">\n {product.images?.[imageIdx] ? (\n <img\n src={product.images[imageIdx]}\n alt={product.name}\n style={{ width: \"100%\", aspectRatio: \"1/1\", objectFit: \"cover\", borderRadius: \"var(--xicms-radius, 12px)\" }}\n />\n ) : (\n <div style={{ width: \"100%\", aspectRatio: \"1/1\", display: \"grid\", placeItems: \"center\", background: \"var(--xicms-muted, #f1f1f1)\", borderRadius: \"var(--xicms-radius, 12px)\" }}>\n <span style={{ fontSize: \"3rem\", opacity: 0.4 }}>🖼️</span>\n </div>\n )}\n {(product.images?.length ?? 0) > 1 && (\n <div style={{ display: \"flex\", gap: 8, marginTop: 8, flexWrap: \"wrap\" }}>\n {product.images.map((url, i) => (\n <button\n key={url + i}\n type=\"button\"\n onClick={() => setImageIdx(i)}\n style={{\n padding: 0,\n border: i === imageIdx ? \"2px solid var(--xicms-primary, #333)\" : \"2px solid transparent\",\n borderRadius: 8,\n cursor: \"pointer\",\n background: \"none\",\n }}\n >\n <img src={url} alt=\"\" style={{ width: 64, height: 64, objectFit: \"cover\", borderRadius: 6, display: \"block\" }} />\n </button>\n ))}\n </div>\n )}\n </div>\n\n <div className=\"xicms-shop-detail__info\">\n <Heading level={1}>{product.name}</Heading>\n {/* Echte block-elementen (geen Astryx Text): prijs en beschrijving mogen nooit in\n elkaars inline-flow terechtkomen. */}\n <p className=\"xicms-shop-detail__price\" style={{ fontSize: \"1.35rem\", fontWeight: 700, margin: \"0.5rem 0 1rem\" }}>\n {product.compareAtCents !== undefined && product.compareAtCents > priceCents && (\n <s style={{ opacity: 0.6, marginRight: 8, fontWeight: \"normal\" }}>\n {formatCents(product.compareAtCents, settings.currency)}\n </s>\n )}\n {formatCents(priceCents, settings.currency)}\n </p>\n\n {product.description && (\n // Platte tekst met behoud van regeleinden — bewust geen HTML (zie schema/shop.ts).\n <p className=\"xicms-shop-detail__desc\" style={{ whiteSpace: \"pre-wrap\", margin: 0 }}>{product.description}</p>\n )}\n\n <div style={{ display: \"grid\", gap: \"0.75rem\", maxWidth: 420, marginTop: \"1rem\" }}>\n {options.map((opt, i) => (\n <Selector\n key={opt.name}\n label={opt.name}\n placeholder=\"Kies…\"\n options={opt.values}\n value={choices[i] ?? \"\"}\n onChange={(v) => {\n setChoices((prev) => ({ ...prev, [i]: (v as string) ?? \"\" }));\n setAdded(false);\n }}\n />\n ))}\n\n <label className=\"xicms-shop-detail__qty\" style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>Aantal</span>\n <input\n className=\"xicms-input\"\n type=\"number\"\n min={1}\n max={999}\n value={qty}\n onChange={(e) => setQty(Math.max(1, Math.min(999, Number(e.target.value) || 1)))}\n style={{ width: 90 }}\n />\n </label>\n\n {outOfStock && <Text style={{ color: \"var(--color-error, #c0392b)\" }}>Uitverkocht in deze uitvoering.</Text>}\n {variantInactive && <Text style={{ color: \"var(--color-error, #c0392b)\" }}>Deze combinatie is niet leverbaar.</Text>}\n\n {added ? (\n <div role=\"status\" style={{ display: \"grid\", gap: 8 }}>\n <Text>✓ Toegevoegd aan je winkelwagen.</Text>\n <div style={{ display: \"flex\", gap: 12, flexWrap: \"wrap\" }}>\n <a className=\"xicms-btn xicms-btn--primary\" href={settings.cartPath}>Naar winkelwagen</a>\n <a className=\"xicms-btn xicms-btn--secondary\" href={settings.shopPath}>Verder winkelen</a>\n </div>\n </div>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={!canOrder}\n onClick={addToCart}\n style={{ cursor: canOrder ? \"pointer\" : \"not-allowed\", opacity: canOrder ? 1 : 0.5, width: \"fit-content\" }}\n >\n In winkelwagen\n </button>\n )}\n </div>\n </div>\n </div>\n );\n}\n\nexport const productDetailBlock = defineBlock({\n type: \"productDetail\",\n schema: ProductDetailBlockSchema,\n Render: ProductDetailRender,\n label: \"Productdetail\",\n icon: \"🏷️\",\n defaults: () => ({}),\n});\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/shop/blocks/ProductDetail.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { ProductDetailBlockSchema, formatCents, productPriceCents, variantKeyFor } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { Selector } from \"@astryxdesign/core/Selector\";\nimport { cartAdd } from \"../cartStore\";\nimport { fetchProductBySlug, readQueryParam, useShopSettings, variantLabelFor, type StoredProduct } from \"../lib\";\n\ntype Blk = z.infer<typeof ProductDetailBlockSchema>;\n\n/**\n * Productdetail — één generieke pagina met dit block toont elk product via `?p=<slug>`\n * (of een vast product via `block.slug`). Optie-selects, variant-prijs/voorraad, aantal,\n * in-winkelwagen.\n */\nfunction ProductDetailRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const [product, setProduct] = useState<StoredProduct | null | undefined>(undefined);\n const [choices, setChoices] = useState<Record<number, string>>({});\n const [qty, setQty] = useState(1);\n const [imageIdx, setImageIdx] = useState(0);\n const [added, setAdded] = useState(false);\n\n useEffect(() => {\n const slug = block.slug || readQueryParam(\"p\") || \"\";\n if (!slug) {\n setProduct(null);\n return;\n }\n void fetchProductBySlug(siteId, slug).then(setProduct).catch((e) => {\n console.warn(\"[xicms] product laden mislukt:\", e);\n setProduct(null);\n });\n }, [siteId, block.slug]);\n\n const options = product?.options ?? [];\n const allChosen = options.every((_, i) => Boolean(choices[i]));\n const variantKey = product && options.length > 0 && allChosen\n ? variantKeyFor(options.map((_, i) => choices[i]))\n : undefined;\n const variant = variantKey ? product?.variants?.find((v) => v.key === variantKey) : undefined;\n\n const priceCents = useMemo(() => {\n if (!product) return 0;\n return productPriceCents(product, variantKey);\n }, [product, variantKey]);\n\n /** Uitverkocht? Alleen met voorraadbeheer aan én een expliciet gezette voorraad (undefined = onbeperkt). */\n const outOfStock = Boolean(\n product?.trackStock &&\n (options.length === 0\n ? product.stock !== undefined && product.stock <= 0\n : variant?.stock !== undefined && variant.stock <= 0),\n );\n const variantInactive = variant !== undefined && variant.active === false;\n\n if (product === undefined) return null;\n if (product === null) {\n return (\n <Card padding={4} variant=\"muted\">\n <Text>Product niet gevonden. <a href={settings.shopPath}>Terug naar de winkel</a></Text>\n </Card>\n );\n }\n\n function addToCart() {\n if (!product) return;\n cartAdd(\n siteId,\n {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n variantKey,\n variantLabel: variantKey ? variantLabelFor(options, variantKey) : undefined,\n unitPriceCents: priceCents,\n imageUrl: product.images?.[0],\n },\n qty,\n );\n setAdded(true);\n }\n\n const canOrder = (options.length === 0 || allChosen) && !outOfStock && !variantInactive;\n\n return (\n <div\n className=\"xicms-shop-detail\"\n style={{ display: \"grid\", gap: \"2rem\", gridTemplateColumns: \"repeat(auto-fit, minmax(min(320px, 100%), 1fr))\" }}\n >\n <div className=\"xicms-shop-detail__media\">\n {product.images?.[imageIdx] ? (\n <img\n src={product.images[imageIdx]}\n alt={product.name}\n style={{ width: \"100%\", aspectRatio: \"1/1\", objectFit: \"cover\", borderRadius: \"var(--xicms-radius, 12px)\" }}\n />\n ) : (\n <div\n className=\"xicms-shop-detail__placeholder\"\n aria-hidden=\"true\"\n style={{ width: \"100%\", aspectRatio: \"1/1\", background: \"var(--xicms-muted, #f1f1f1)\", borderRadius: \"var(--xicms-radius, 12px)\" }}\n />\n )}\n {(product.images?.length ?? 0) > 1 && (\n <div style={{ display: \"flex\", gap: 8, marginTop: 8, flexWrap: \"wrap\" }}>\n {product.images.map((url, i) => (\n <button\n key={url + i}\n type=\"button\"\n onClick={() => setImageIdx(i)}\n style={{\n padding: 0,\n border: i === imageIdx ? \"2px solid var(--xicms-primary, #333)\" : \"2px solid transparent\",\n borderRadius: 8,\n cursor: \"pointer\",\n background: \"none\",\n }}\n >\n <img src={url} alt=\"\" style={{ width: 64, height: 64, objectFit: \"cover\", borderRadius: 6, display: \"block\" }} />\n </button>\n ))}\n </div>\n )}\n </div>\n\n <div className=\"xicms-shop-detail__info\">\n <Heading level={1}>{product.name}</Heading>\n {/* Echte block-elementen (geen Astryx Text): prijs en beschrijving mogen nooit in\n elkaars inline-flow terechtkomen. */}\n <p className=\"xicms-shop-detail__price\" style={{ fontSize: \"1.35rem\", fontWeight: 700, margin: \"0.5rem 0 1rem\" }}>\n {product.compareAtCents !== undefined && product.compareAtCents > priceCents && (\n <s style={{ opacity: 0.6, marginRight: 8, fontWeight: \"normal\" }}>\n {formatCents(product.compareAtCents, settings.currency)}\n </s>\n )}\n {formatCents(priceCents, settings.currency)}\n </p>\n\n {product.description && (\n // Platte tekst met behoud van regeleinden — bewust geen HTML (zie schema/shop.ts).\n <p className=\"xicms-shop-detail__desc\" style={{ whiteSpace: \"pre-wrap\", margin: 0 }}>{product.description}</p>\n )}\n\n <div style={{ display: \"grid\", gap: \"0.75rem\", maxWidth: 420, marginTop: \"1rem\" }}>\n {options.map((opt, i) => (\n <Selector\n key={opt.name}\n label={opt.name}\n placeholder=\"Kies…\"\n options={opt.values}\n value={choices[i] ?? \"\"}\n onChange={(v) => {\n setChoices((prev) => ({ ...prev, [i]: (v as string) ?? \"\" }));\n setAdded(false);\n }}\n />\n ))}\n\n <label className=\"xicms-shop-detail__qty\" style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>Aantal</span>\n <input\n className=\"xicms-input\"\n type=\"number\"\n min={1}\n max={999}\n value={qty}\n onChange={(e) => setQty(Math.max(1, Math.min(999, Number(e.target.value) || 1)))}\n style={{ width: 90 }}\n />\n </label>\n\n {outOfStock && <Text style={{ color: \"var(--color-error, #c0392b)\" }}>Uitverkocht in deze uitvoering.</Text>}\n {variantInactive && <Text style={{ color: \"var(--color-error, #c0392b)\" }}>Deze combinatie is niet leverbaar.</Text>}\n\n {added ? (\n <div role=\"status\" style={{ display: \"grid\", gap: 8 }}>\n <Text>✓ Toegevoegd aan je winkelwagen.</Text>\n <div style={{ display: \"flex\", gap: 12, flexWrap: \"wrap\" }}>\n <a className=\"xicms-btn xicms-btn--primary\" href={settings.cartPath}>Naar winkelwagen</a>\n <a className=\"xicms-btn xicms-btn--secondary\" href={settings.shopPath}>Verder winkelen</a>\n </div>\n </div>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={!canOrder}\n onClick={addToCart}\n style={{ cursor: canOrder ? \"pointer\" : \"not-allowed\", opacity: canOrder ? 1 : 0.5, width: \"fit-content\" }}\n >\n In winkelwagen\n </button>\n )}\n </div>\n </div>\n </div>\n );\n}\n\nexport const productDetailBlock = defineBlock({\n type: \"productDetail\",\n schema: ProductDetailBlockSchema,\n Render: ProductDetailRender,\n label: \"Productdetail\",\n icon: \"🏷️\",\n defaults: () => ({}),\n});\n"],"mappings":"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","names":[]}
|
|
@@ -9,6 +9,8 @@ import { cartAdd } from "../cartStore";
|
|
|
9
9
|
import {
|
|
10
10
|
fetchActiveProducts,
|
|
11
11
|
fetchShopCategories,
|
|
12
|
+
kleurNaarCss,
|
|
13
|
+
kleurOptie,
|
|
12
14
|
readQueryParam,
|
|
13
15
|
useShopSettings
|
|
14
16
|
} from "../lib";
|
|
@@ -21,6 +23,28 @@ function PriceLine({ product, currency }) {
|
|
|
21
23
|
hasRange ? `vanaf ${formatCents(min, currency)}` : formatCents(min, currency)
|
|
22
24
|
] });
|
|
23
25
|
}
|
|
26
|
+
function ColorSwatches({ product }) {
|
|
27
|
+
const optie = kleurOptie(product);
|
|
28
|
+
if (!optie) return null;
|
|
29
|
+
const dots = optie.values.map((v) => ({ v, css: kleurNaarCss(v) })).filter((d) => d.css !== null);
|
|
30
|
+
if (dots.length === 0) return null;
|
|
31
|
+
return /* @__PURE__ */ jsx("span", { className: "xicms-shop-card__swatches", "aria-label": `Kleuren: ${optie.values.join(", ")}`, children: dots.map((d) => /* @__PURE__ */ jsx(
|
|
32
|
+
"span",
|
|
33
|
+
{
|
|
34
|
+
className: "xicms-shop-card__swatch",
|
|
35
|
+
title: d.v,
|
|
36
|
+
style: {
|
|
37
|
+
display: "inline-block",
|
|
38
|
+
width: 12,
|
|
39
|
+
height: 12,
|
|
40
|
+
borderRadius: 999,
|
|
41
|
+
background: d.css,
|
|
42
|
+
boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.18)"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
d.v
|
|
46
|
+
)) });
|
|
47
|
+
}
|
|
24
48
|
function ProductCard({
|
|
25
49
|
product,
|
|
26
50
|
siteId,
|
|
@@ -41,10 +65,21 @@ function ProductCard({
|
|
|
41
65
|
loading: "lazy",
|
|
42
66
|
style: { width: "100%", aspectRatio: "1/1", objectFit: "cover", display: "block", borderRadius: "inherit" }
|
|
43
67
|
}
|
|
44
|
-
) :
|
|
45
|
-
|
|
46
|
-
/* @__PURE__ */ jsx(
|
|
47
|
-
|
|
68
|
+
) : (
|
|
69
|
+
// Stabiele class zodat een site hier zijn eigen (CSS-)placeholder van kan maken.
|
|
70
|
+
/* @__PURE__ */ jsx(
|
|
71
|
+
"div",
|
|
72
|
+
{
|
|
73
|
+
className: "xicms-shop-card__placeholder",
|
|
74
|
+
"aria-hidden": "true",
|
|
75
|
+
style: { width: "100%", aspectRatio: "1/1", background: "var(--xicms-muted, #f1f1f1)", borderRadius: "inherit" }
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
),
|
|
79
|
+
/* @__PURE__ */ jsxs("div", { style: { padding: "0.8rem 1rem 0.25rem", display: "grid", gap: 4 }, children: [
|
|
80
|
+
/* @__PURE__ */ jsx("span", { style: { fontWeight: 700 }, children: product.name }),
|
|
81
|
+
/* @__PURE__ */ jsx("span", { style: { fontSize: "0.92em" }, children: /* @__PURE__ */ jsx(PriceLine, { product, currency }) }),
|
|
82
|
+
/* @__PURE__ */ jsx("span", { style: { display: "inline-flex", gap: 5, minHeight: 12 }, children: /* @__PURE__ */ jsx(ColorSwatches, { product }) })
|
|
48
83
|
] })
|
|
49
84
|
] }),
|
|
50
85
|
/* @__PURE__ */ jsx("div", { style: { padding: "0.5rem 1rem 1rem" }, children: simple ? added ? /* @__PURE__ */ jsx("a", { className: "xicms-shop-card__added", href: cartPath, style: { fontSize: "0.9rem" }, children: "\u2713 Toegevoegd \u2014 naar winkelwagen" }) : /* @__PURE__ */ jsx(
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/modules/shop/blocks/ProductGrid.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { ProductGridBlockSchema, formatCents } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { Card, Text } from \"../../../blocks/ui\";\nimport { Heading } from \"../../../blocks/ui\";\nimport { cartAdd } from \"../cartStore\";\nimport {\n fetchActiveProducts,\n fetchShopCategories,\n readQueryParam,\n useShopSettings,\n type StoredProduct,\n type StoredShopCategory,\n} from \"../lib\";\n\ntype Blk = z.infer<typeof ProductGridBlockSchema>;\n\n/** Prijsregel met optionele doorgestreepte \"van\"-prijs en \"vanaf\" bij variant-prijzen. */\nexport function PriceLine({ product, currency }: { product: StoredProduct; currency: string }) {\n const variantPrices = (product.variants ?? [])\n .filter((v) => v.priceCents !== undefined)\n .map((v) => v.priceCents as number);\n const min = Math.min(product.priceCents, ...variantPrices);\n const hasRange = variantPrices.some((p) => p !== product.priceCents);\n return (\n <span className=\"xicms-shop-price\">\n {product.compareAtCents !== undefined && product.compareAtCents > min && (\n <s style={{ opacity: 0.6, marginRight: 6 }}>{formatCents(product.compareAtCents, currency)}</s>\n )}\n {hasRange ? `vanaf ${formatCents(min, currency)}` : formatCents(min, currency)}\n </span>\n );\n}\n\nfunction ProductCard({\n product,\n siteId,\n currency,\n detailPath,\n cartPath,\n}: {\n product: StoredProduct;\n siteId: string;\n currency: string;\n detailPath: string;\n cartPath: string;\n}) {\n const [added, setAdded] = useState(false);\n const href = `${detailPath}?p=${encodeURIComponent(product.slug)}`;\n const simple = (product.options ?? []).length === 0;\n\n return (\n <Card padding={0} className=\"xicms-shop-card\">\n <a href={href} style={{ textDecoration: \"none\", color: \"inherit\", display: \"block\" }}>\n {product.images?.[0] ? (\n <img\n src={product.images[0]}\n alt={product.name}\n loading=\"lazy\"\n style={{ width: \"100%\", aspectRatio: \"1/1\", objectFit: \"cover\", display: \"block\", borderRadius: \"inherit\" }}\n />\n ) : (\n <div style={{ width: \"100%\", aspectRatio: \"1/1\", display: \"grid\", placeItems: \"center\", background: \"var(--xicms-muted, #f1f1f1)\", borderRadius: \"inherit\" }}>\n <span style={{ fontSize: \"2rem\", opacity: 0.4 }}>🖼️</span>\n </div>\n )}\n <div style={{ padding: \"0.8rem 1rem 0.25rem\" }}>\n <Text weight=\"bold\" display=\"block\">{product.name}</Text>\n <Text size=\"sm\" display=\"block\" style={{ marginTop: 2 }}>\n <PriceLine product={product} currency={currency} />\n </Text>\n </div>\n </a>\n <div style={{ padding: \"0.5rem 1rem 1rem\" }}>\n {simple ? (\n added ? (\n <a className=\"xicms-shop-card__added\" href={cartPath} style={{ fontSize: \"0.9rem\" }}>\n ✓ Toegevoegd — naar winkelwagen\n </a>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n style={{ cursor: \"pointer\" }}\n onClick={() => {\n cartAdd(siteId, {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n unitPriceCents: product.priceCents,\n imageUrl: product.images?.[0],\n });\n setAdded(true);\n }}\n >\n In winkelwagen\n </button>\n )\n ) : (\n <a className=\"xicms-btn xicms-btn--secondary\" href={href}>\n Bekijk opties\n </a>\n )}\n </div>\n </Card>\n );\n}\n\nfunction ProductGridRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const [products, setProducts] = useState<StoredProduct[] | null>(null);\n const [cats, setCats] = useState<StoredShopCategory[]>([]);\n const [queryCat, setQueryCat] = useState<string | null>(null);\n\n useEffect(() => {\n void fetchActiveProducts(siteId).then(setProducts).catch((e) => {\n console.warn(\"[xicms] producten laden mislukt:\", e);\n setProducts([]);\n });\n void fetchShopCategories(siteId).then(setCats).catch(() => setCats([]));\n if (block.useQueryCategory && !block.categorySlug) setQueryCat(readQueryParam(\"cat\"));\n }, [siteId, block.useQueryCategory, block.categorySlug]);\n\n const activeSlug = block.categorySlug || queryCat || null;\n const activeCat = activeSlug ? cats.find((c) => c.slug === activeSlug) ?? null : null;\n\n const visible = useMemo(() => {\n if (!products) return [];\n let list = products;\n if (activeCat) list = list.filter((p) => (p.categoryIds ?? []).includes(activeCat.id));\n if (block.featuredOnly) list = list.filter((p) => p.featured);\n const sorted = [...list];\n if (block.sort === \"name\") sorted.sort((a, b) => a.name.localeCompare(b.name, \"nl\"));\n else if (block.sort === \"priceAsc\") sorted.sort((a, b) => a.priceCents - b.priceCents);\n else if (block.sort === \"priceDesc\") sorted.sort((a, b) => b.priceCents - a.priceCents);\n else sorted.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\"));\n return sorted.slice(0, block.limit);\n }, [products, activeCat, block.featuredOnly, block.sort, block.limit]);\n\n if (products === null) return null;\n\n return (\n <div className=\"xicms-shop-grid\">\n {(block.heading || activeCat) && (\n <Heading level={2} data-xicms-edit=\"heading\">\n {activeCat?.name ?? block.heading}\n </Heading>\n )}\n {activeCat && (\n <Text size=\"sm\" display=\"block\" style={{ marginTop: 4 }}>\n <a href={settings.shopPath}>← Alle categorieën</a>\n </Text>\n )}\n {visible.length === 0 ? (\n <Card padding={4} variant=\"muted\" style={{ marginTop: \"1rem\" }}>\n <Text>Geen producten gevonden.</Text>\n </Card>\n ) : (\n <div\n className=\"xicms-shop-grid__items\"\n style={{\n display: \"grid\",\n gridTemplateColumns: `repeat(auto-fill, minmax(min(240px, calc(100%/${block.columns} - 1rem)), 1fr))`,\n gap: \"1.25rem\",\n marginTop: \"1.25rem\",\n }}\n >\n {visible.map((p) => (\n <ProductCard\n key={p.id}\n product={p}\n siteId={siteId}\n currency={settings.currency}\n detailPath={settings.detailPath}\n cartPath={settings.cartPath}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport const productGridBlock = defineBlock({\n type: \"productGrid\",\n schema: ProductGridBlockSchema,\n Render: ProductGridRender,\n label: \"Producten-overzicht\",\n icon: \"🛍️\",\n defaults: () => ({ useQueryCategory: true, columns: 3, limit: 60, sort: \"order\" }),\n});\n"],"mappings":";AA4BI,SAEI,KAFJ;AA1BJ,SAAS,WAAW,SAAS,gBAAgB;AAE7C,SAAS,wBAAwB,mBAAmB;AACpD,SAAS,mBAA0C;AACnD,SAAS,MAAM,YAAY;AAC3B,SAAS,eAAe;AACxB,SAAS,eAAe;AACxB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAKA,SAAS,UAAU,EAAE,SAAS,SAAS,GAAiD;AAC7F,QAAM,iBAAiB,QAAQ,YAAY,CAAC,GACzC,OAAO,CAAC,MAAM,EAAE,eAAe,MAAS,EACxC,IAAI,CAAC,MAAM,EAAE,UAAoB;AACpC,QAAM,MAAM,KAAK,IAAI,QAAQ,YAAY,GAAG,aAAa;AACzD,QAAM,WAAW,cAAc,KAAK,CAAC,MAAM,MAAM,QAAQ,UAAU;AACnE,SACE,qBAAC,UAAK,WAAU,oBACb;AAAA,YAAQ,mBAAmB,UAAa,QAAQ,iBAAiB,OAChE,oBAAC,OAAE,OAAO,EAAE,SAAS,KAAK,aAAa,EAAE,GAAI,sBAAY,QAAQ,gBAAgB,QAAQ,GAAE;AAAA,IAE5F,WAAW,SAAS,YAAY,KAAK,QAAQ,CAAC,KAAK,YAAY,KAAK,QAAQ;AAAA,KAC/E;AAEJ;AAEA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AACxC,QAAM,OAAO,GAAG,UAAU,MAAM,mBAAmB,QAAQ,IAAI,CAAC;AAChE,QAAM,UAAU,QAAQ,WAAW,CAAC,GAAG,WAAW;AAElD,SACE,qBAAC,QAAK,SAAS,GAAG,WAAU,mBAC1B;AAAA,yBAAC,OAAE,MAAY,OAAO,EAAE,gBAAgB,QAAQ,OAAO,WAAW,SAAS,QAAQ,GAChF;AAAA,cAAQ,SAAS,CAAC,IACjB;AAAA,QAAC;AAAA;AAAA,UACC,KAAK,QAAQ,OAAO,CAAC;AAAA,UACrB,KAAK,QAAQ;AAAA,UACb,SAAQ;AAAA,UACR,OAAO,EAAE,OAAO,QAAQ,aAAa,OAAO,WAAW,SAAS,SAAS,SAAS,cAAc,UAAU;AAAA;AAAA,MAC5G,IAEA,oBAAC,SAAI,OAAO,EAAE,OAAO,QAAQ,aAAa,OAAO,SAAS,QAAQ,YAAY,UAAU,YAAY,+BAA+B,cAAc,UAAU,GACzJ,8BAAC,UAAK,OAAO,EAAE,UAAU,QAAQ,SAAS,IAAI,GAAG,6BAAG,GACtD;AAAA,MAEF,qBAAC,SAAI,OAAO,EAAE,SAAS,sBAAsB,GAC3C;AAAA,4BAAC,QAAK,QAAO,QAAO,SAAQ,SAAS,kBAAQ,MAAK;AAAA,QAClD,oBAAC,QAAK,MAAK,MAAK,SAAQ,SAAQ,OAAO,EAAE,WAAW,EAAE,GACpD,8BAAC,aAAU,SAAkB,UAAoB,GACnD;AAAA,SACF;AAAA,OACF;AAAA,IACA,oBAAC,SAAI,OAAO,EAAE,SAAS,mBAAmB,GACvC,mBACC,QACE,oBAAC,OAAE,WAAU,0BAAyB,MAAM,UAAU,OAAO,EAAE,UAAU,SAAS,GAAG,uDAErF,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,MAAK;AAAA,QACL,WAAU;AAAA,QACV,OAAO,EAAE,QAAQ,UAAU;AAAA,QAC3B,SAAS,MAAM;AACb,kBAAQ,QAAQ;AAAA,YACd,WAAW,QAAQ;AAAA,YACnB,MAAM,QAAQ;AAAA,YACd,MAAM,QAAQ;AAAA,YACd,gBAAgB,QAAQ;AAAA,YACxB,UAAU,QAAQ,SAAS,CAAC;AAAA,UAC9B,CAAC;AACD,mBAAS,IAAI;AAAA,QACf;AAAA,QACD;AAAA;AAAA,IAED,IAGF,oBAAC,OAAE,WAAU,kCAAiC,MAAY,2BAE1D,GAEJ;AAAA,KACF;AAEJ;AAEA,SAAS,kBAAkB,EAAE,OAAO,OAAO,GAA0B;AACnE,QAAM,WAAW,gBAAgB;AACjC,QAAM,CAAC,UAAU,WAAW,IAAI,SAAiC,IAAI;AACrE,QAAM,CAAC,MAAM,OAAO,IAAI,SAA+B,CAAC,CAAC;AACzD,QAAM,CAAC,UAAU,WAAW,IAAI,SAAwB,IAAI;AAE5D,YAAU,MAAM;AACd,SAAK,oBAAoB,MAAM,EAAE,KAAK,WAAW,EAAE,MAAM,CAAC,MAAM;AAC9D,cAAQ,KAAK,oCAAoC,CAAC;AAClD,kBAAY,CAAC,CAAC;AAAA,IAChB,CAAC;AACD,SAAK,oBAAoB,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,MAAM,QAAQ,CAAC,CAAC,CAAC;AACtE,QAAI,MAAM,oBAAoB,CAAC,MAAM,aAAc,aAAY,eAAe,KAAK,CAAC;AAAA,EACtF,GAAG,CAAC,QAAQ,MAAM,kBAAkB,MAAM,YAAY,CAAC;AAEvD,QAAM,aAAa,MAAM,gBAAgB,YAAY;AACrD,QAAM,YAAY,aAAa,KAAK,KAAK,CAAC,MAAM,EAAE,SAAS,UAAU,KAAK,OAAO;AAEjF,QAAM,UAAU,QAAQ,MAAM;AAC5B,QAAI,CAAC,SAAU,QAAO,CAAC;AACvB,QAAI,OAAO;AACX,QAAI,UAAW,QAAO,KAAK,OAAO,CAAC,OAAO,EAAE,eAAe,CAAC,GAAG,SAAS,UAAU,EAAE,CAAC;AACrF,QAAI,MAAM,aAAc,QAAO,KAAK,OAAO,CAAC,MAAM,EAAE,QAAQ;AAC5D,UAAM,SAAS,CAAC,GAAG,IAAI;AACvB,QAAI,MAAM,SAAS,OAAQ,QAAO,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,MAAM,IAAI,CAAC;AAAA,aAC1E,MAAM,SAAS,WAAY,QAAO,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;AAAA,aAC5E,MAAM,SAAS,YAAa,QAAO,KAAK,CAAC,GAAG,MAAM,EAAE,aAAa,EAAE,UAAU;AAAA,QACjF,QAAO,KAAK,CAAC,GAAG,OAAO,EAAE,SAAS,MAAM,EAAE,SAAS,MAAM,EAAE,KAAK,cAAc,EAAE,MAAM,IAAI,CAAC;AAChG,WAAO,OAAO,MAAM,GAAG,MAAM,KAAK;AAAA,EACpC,GAAG,CAAC,UAAU,WAAW,MAAM,cAAc,MAAM,MAAM,MAAM,KAAK,CAAC;AAErE,MAAI,aAAa,KAAM,QAAO;AAE9B,SACE,qBAAC,SAAI,WAAU,mBACX;AAAA,WAAM,WAAW,cACjB,oBAAC,WAAQ,OAAO,GAAG,mBAAgB,WAChC,qBAAW,QAAQ,MAAM,SAC5B;AAAA,IAED,aACC,oBAAC,QAAK,MAAK,MAAK,SAAQ,SAAQ,OAAO,EAAE,WAAW,EAAE,GACpD,8BAAC,OAAE,MAAM,SAAS,UAAU,wCAAkB,GAChD;AAAA,IAED,QAAQ,WAAW,IAClB,oBAAC,QAAK,SAAS,GAAG,SAAQ,SAAQ,OAAO,EAAE,WAAW,OAAO,GAC3D,8BAAC,QAAK,sCAAwB,GAChC,IAEA;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,OAAO;AAAA,UACL,SAAS;AAAA,UACT,qBAAqB,iDAAiD,MAAM,OAAO;AAAA,UACnF,KAAK;AAAA,UACL,WAAW;AAAA,QACb;AAAA,QAEC,kBAAQ,IAAI,CAAC,MACZ;AAAA,UAAC;AAAA;AAAA,YAEC,SAAS;AAAA,YACT;AAAA,YACA,UAAU,SAAS;AAAA,YACnB,YAAY,SAAS;AAAA,YACrB,UAAU,SAAS;AAAA;AAAA,UALd,EAAE;AAAA,QAMT,CACD;AAAA;AAAA,IACH;AAAA,KAEJ;AAEJ;AAEO,MAAM,mBAAmB,YAAY;AAAA,EAC1C,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,MAAM;AAAA,EACN,UAAU,OAAO,EAAE,kBAAkB,MAAM,SAAS,GAAG,OAAO,IAAI,MAAM,QAAQ;AAClF,CAAC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/modules/shop/blocks/ProductGrid.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport type { z } from \"zod\";\nimport { ProductGridBlockSchema, formatCents } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { Card, Text } from \"../../../blocks/ui\";\nimport { Heading } from \"../../../blocks/ui\";\nimport { cartAdd } from \"../cartStore\";\nimport {\n fetchActiveProducts,\n fetchShopCategories,\n kleurNaarCss,\n kleurOptie,\n readQueryParam,\n useShopSettings,\n type StoredProduct,\n type StoredShopCategory,\n} from \"../lib\";\n\ntype Blk = z.infer<typeof ProductGridBlockSchema>;\n\n/** Prijsregel met optionele doorgestreepte \"van\"-prijs en \"vanaf\" bij variant-prijzen. */\nexport function PriceLine({ product, currency }: { product: StoredProduct; currency: string }) {\n const variantPrices = (product.variants ?? [])\n .filter((v) => v.priceCents !== undefined)\n .map((v) => v.priceCents as number);\n const min = Math.min(product.priceCents, ...variantPrices);\n const hasRange = variantPrices.some((p) => p !== product.priceCents);\n return (\n <span className=\"xicms-shop-price\">\n {product.compareAtCents !== undefined && product.compareAtCents > min && (\n <s style={{ opacity: 0.6, marginRight: 6 }}>{formatCents(product.compareAtCents, currency)}</s>\n )}\n {hasRange ? `vanaf ${formatCents(min, currency)}` : formatCents(min, currency)}\n </span>\n );\n}\n\n/** Stof-stipjes met de beschikbare kleuren van het product (optie \"Kleur\"). */\nfunction ColorSwatches({ product }: { product: StoredProduct }) {\n const optie = kleurOptie(product);\n if (!optie) return null;\n const dots = optie.values\n .map((v) => ({ v, css: kleurNaarCss(v) }))\n .filter((d): d is { v: string; css: string } => d.css !== null);\n if (dots.length === 0) return null;\n return (\n <span className=\"xicms-shop-card__swatches\" aria-label={`Kleuren: ${optie.values.join(\", \")}`}>\n {dots.map((d) => (\n <span\n key={d.v}\n className=\"xicms-shop-card__swatch\"\n title={d.v}\n style={{\n display: \"inline-block\",\n width: 12,\n height: 12,\n borderRadius: 999,\n background: d.css,\n boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\",\n }}\n />\n ))}\n </span>\n );\n}\n\nfunction ProductCard({\n product,\n siteId,\n currency,\n detailPath,\n cartPath,\n}: {\n product: StoredProduct;\n siteId: string;\n currency: string;\n detailPath: string;\n cartPath: string;\n}) {\n const [added, setAdded] = useState(false);\n const href = `${detailPath}?p=${encodeURIComponent(product.slug)}`;\n const simple = (product.options ?? []).length === 0;\n\n return (\n <Card padding={0} className=\"xicms-shop-card\">\n <a href={href} style={{ textDecoration: \"none\", color: \"inherit\", display: \"block\" }}>\n {product.images?.[0] ? (\n <img\n src={product.images[0]}\n alt={product.name}\n loading=\"lazy\"\n style={{ width: \"100%\", aspectRatio: \"1/1\", objectFit: \"cover\", display: \"block\", borderRadius: \"inherit\" }}\n />\n ) : (\n // Stabiele class zodat een site hier zijn eigen (CSS-)placeholder van kan maken.\n <div\n className=\"xicms-shop-card__placeholder\"\n aria-hidden=\"true\"\n style={{ width: \"100%\", aspectRatio: \"1/1\", background: \"var(--xicms-muted, #f1f1f1)\", borderRadius: \"inherit\" }}\n />\n )}\n <div style={{ padding: \"0.8rem 1rem 0.25rem\", display: \"grid\", gap: 4 }}>\n <span style={{ fontWeight: 700 }}>{product.name}</span>\n <span style={{ fontSize: \"0.92em\" }}>\n <PriceLine product={product} currency={currency} />\n </span>\n <span style={{ display: \"inline-flex\", gap: 5, minHeight: 12 }}>\n <ColorSwatches product={product} />\n </span>\n </div>\n </a>\n <div style={{ padding: \"0.5rem 1rem 1rem\" }}>\n {simple ? (\n added ? (\n <a className=\"xicms-shop-card__added\" href={cartPath} style={{ fontSize: \"0.9rem\" }}>\n ✓ Toegevoegd — naar winkelwagen\n </a>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n style={{ cursor: \"pointer\" }}\n onClick={() => {\n cartAdd(siteId, {\n productId: product.id,\n slug: product.slug,\n name: product.name,\n unitPriceCents: product.priceCents,\n imageUrl: product.images?.[0],\n });\n setAdded(true);\n }}\n >\n In winkelwagen\n </button>\n )\n ) : (\n <a className=\"xicms-btn xicms-btn--secondary\" href={href}>\n Bekijk opties\n </a>\n )}\n </div>\n </Card>\n );\n}\n\nfunction ProductGridRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const [products, setProducts] = useState<StoredProduct[] | null>(null);\n const [cats, setCats] = useState<StoredShopCategory[]>([]);\n const [queryCat, setQueryCat] = useState<string | null>(null);\n\n useEffect(() => {\n void fetchActiveProducts(siteId).then(setProducts).catch((e) => {\n console.warn(\"[xicms] producten laden mislukt:\", e);\n setProducts([]);\n });\n void fetchShopCategories(siteId).then(setCats).catch(() => setCats([]));\n if (block.useQueryCategory && !block.categorySlug) setQueryCat(readQueryParam(\"cat\"));\n }, [siteId, block.useQueryCategory, block.categorySlug]);\n\n const activeSlug = block.categorySlug || queryCat || null;\n const activeCat = activeSlug ? cats.find((c) => c.slug === activeSlug) ?? null : null;\n\n const visible = useMemo(() => {\n if (!products) return [];\n let list = products;\n if (activeCat) list = list.filter((p) => (p.categoryIds ?? []).includes(activeCat.id));\n if (block.featuredOnly) list = list.filter((p) => p.featured);\n const sorted = [...list];\n if (block.sort === \"name\") sorted.sort((a, b) => a.name.localeCompare(b.name, \"nl\"));\n else if (block.sort === \"priceAsc\") sorted.sort((a, b) => a.priceCents - b.priceCents);\n else if (block.sort === \"priceDesc\") sorted.sort((a, b) => b.priceCents - a.priceCents);\n else sorted.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, \"nl\"));\n return sorted.slice(0, block.limit);\n }, [products, activeCat, block.featuredOnly, block.sort, block.limit]);\n\n if (products === null) return null;\n\n return (\n <div className=\"xicms-shop-grid\">\n {(block.heading || activeCat) && (\n <Heading level={2} data-xicms-edit=\"heading\">\n {activeCat?.name ?? block.heading}\n </Heading>\n )}\n {activeCat && (\n <Text size=\"sm\" display=\"block\" style={{ marginTop: 4 }}>\n <a href={settings.shopPath}>← Alle categorieën</a>\n </Text>\n )}\n {visible.length === 0 ? (\n <Card padding={4} variant=\"muted\" style={{ marginTop: \"1rem\" }}>\n <Text>Geen producten gevonden.</Text>\n </Card>\n ) : (\n <div\n className=\"xicms-shop-grid__items\"\n style={{\n display: \"grid\",\n gridTemplateColumns: `repeat(auto-fill, minmax(min(240px, calc(100%/${block.columns} - 1rem)), 1fr))`,\n gap: \"1.25rem\",\n marginTop: \"1.25rem\",\n }}\n >\n {visible.map((p) => (\n <ProductCard\n key={p.id}\n product={p}\n siteId={siteId}\n currency={settings.currency}\n detailPath={settings.detailPath}\n cartPath={settings.cartPath}\n />\n ))}\n </div>\n )}\n </div>\n );\n}\n\nexport const productGridBlock = defineBlock({\n type: \"productGrid\",\n schema: ProductGridBlockSchema,\n Render: ProductGridRender,\n label: \"Producten-overzicht\",\n icon: \"🛍️\",\n defaults: () => ({ useQueryCategory: true, columns: 3, limit: 60, sort: \"order\" }),\n});\n"],"mappings":"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","names":[]}
|
|
@@ -23,3 +23,13 @@ export declare function variantLabelFor(options: {
|
|
|
23
23
|
}[], key: string): string;
|
|
24
24
|
/** Querystring-parameter client-side lezen (blocks zijn client components). */
|
|
25
25
|
export declare function readQueryParam(name: string): string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Zet een kleurnaam ("Roze", "Korte mouw tshirt (oker)geel") om naar een CSS-kleur voor de
|
|
28
|
+
* swatch-stipjes op productkaarten. Onbekende namen geven null (dan geen stipje).
|
|
29
|
+
*/
|
|
30
|
+
export declare function kleurNaarCss(naam: string): string | null;
|
|
31
|
+
/** De kleur-optie van een product (naam "Kleur"/"Colour"/"Color"), of null. */
|
|
32
|
+
export declare function kleurOptie(product: Pick<Product, "options">): {
|
|
33
|
+
name: string;
|
|
34
|
+
values: string[];
|
|
35
|
+
} | null;
|
package/dist/modules/shop/lib.js
CHANGED
|
@@ -53,11 +53,37 @@ function readQueryParam(name) {
|
|
|
53
53
|
if (typeof window === "undefined") return null;
|
|
54
54
|
return new URLSearchParams(window.location.search).get(name);
|
|
55
55
|
}
|
|
56
|
+
const KLEUR_CSS = [
|
|
57
|
+
[/fuchsia|fushia/, "#c2588c"],
|
|
58
|
+
[/roze|pink/, "#f2b9cd"],
|
|
59
|
+
[/blauw|blue/, "#a9c8e6"],
|
|
60
|
+
[/mint/, "#abd7c6"],
|
|
61
|
+
[/oker|geel|yellow/, "#eac57e"],
|
|
62
|
+
[/wit|white/, "#ffffff"],
|
|
63
|
+
[/creme|crème|ecru|beige|zand/, "#f1e6d2"],
|
|
64
|
+
[/grijs|gray|grey/, "#b9bec7"],
|
|
65
|
+
[/groen|green/, "#9fc79f"],
|
|
66
|
+
[/rood|red|bordeaux/, "#d97a7a"],
|
|
67
|
+
[/paars|purple|lila/, "#c3aede"],
|
|
68
|
+
[/oranje|orange/, "#f0a868"],
|
|
69
|
+
[/bruin|brown/, "#b08968"],
|
|
70
|
+
[/zwart|black|navy|donkerblauw/, "#3a4356"]
|
|
71
|
+
];
|
|
72
|
+
function kleurNaarCss(naam) {
|
|
73
|
+
const l = naam.toLowerCase();
|
|
74
|
+
for (const [re, css] of KLEUR_CSS) if (re.test(l)) return css;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function kleurOptie(product) {
|
|
78
|
+
return (product.options ?? []).find((o) => /^kleur|^colou?r/i.test(o.name)) ?? null;
|
|
79
|
+
}
|
|
56
80
|
export {
|
|
57
81
|
createOrderCall,
|
|
58
82
|
fetchActiveProducts,
|
|
59
83
|
fetchProductBySlug,
|
|
60
84
|
fetchShopCategories,
|
|
85
|
+
kleurNaarCss,
|
|
86
|
+
kleurOptie,
|
|
61
87
|
readQueryParam,
|
|
62
88
|
useShopSettings,
|
|
63
89
|
variantLabelFor
|
|
@@ -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, 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/** 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"],"mappings":";AAQA,SAAS,YAAY,SAAS,SAAS,SAAS,OAAO,aAAa;AACpE,SAAS,cAAc,qBAAqB;AAC5C,SAAS,OAAO,sBAAsB;AACtC,SAAS,uBAAuB;AAEhC,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,KAAK,MAAM;AACjB,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,KAAK,MAAM;AACjB,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,KAAK,MAAM;AACjB,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;AAGA,eAAsB,gBAAgB,OAAwE;AAC5G,QAAM,MAAM,eAAe;AAC3B,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,6BAA6B;AACvD,QAAM,KAAK,cAAc,aAAa,KAAK,gBAAgB,CAAC,GAAG,aAAa;AAC5E,QAAM,MAAM,MAAM,GAAG,KAAK;AAC1B,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;","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, 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/** 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/* ── Kleur-swatches ──────────────────────────────────────────────────────── */\n\nconst KLEUR_CSS: [RegExp, string][] = [\n [/fuchsia|fushia/, \"#c2588c\"],\n [/roze|pink/, \"#f2b9cd\"],\n [/blauw|blue/, \"#a9c8e6\"],\n [/mint/, \"#abd7c6\"],\n [/oker|geel|yellow/, \"#eac57e\"],\n [/wit|white/, \"#ffffff\"],\n [/creme|crème|ecru|beige|zand/, \"#f1e6d2\"],\n [/grijs|gray|grey/, \"#b9bec7\"],\n [/groen|green/, \"#9fc79f\"],\n [/rood|red|bordeaux/, \"#d97a7a\"],\n [/paars|purple|lila/, \"#c3aede\"],\n [/oranje|orange/, \"#f0a868\"],\n [/bruin|brown/, \"#b08968\"],\n [/zwart|black|navy|donkerblauw/, \"#3a4356\"],\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\">): { name: string; values: string[] } | null {\n return (product.options ?? []).find((o) => /^kleur|^colou?r/i.test(o.name)) ?? null;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiwebdesign/xicms-core",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.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": {
|