@xiwebdesign/xicms-core 5.5.3 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/shop/ConnectionsTab.js +1 -1
- package/dist/admin/shop/ConnectionsTab.js.map +1 -1
- package/dist/admin/shop/DiscountsTab.d.ts +4 -0
- package/dist/admin/shop/DiscountsTab.js +168 -0
- package/dist/admin/shop/DiscountsTab.js.map +1 -0
- package/dist/admin/shop/EmailsTab.d.ts +8 -0
- package/dist/admin/shop/EmailsTab.js +88 -0
- package/dist/admin/shop/EmailsTab.js.map +1 -0
- package/dist/admin/shop/OrdersTab.js +271 -139
- package/dist/admin/shop/OrdersTab.js.map +1 -1
- package/dist/admin/shop/ProductsTab.js +152 -28
- package/dist/admin/shop/ProductsTab.js.map +1 -1
- package/dist/admin/shop/ShopPanel.d.ts +4 -1
- package/dist/admin/shop/ShopPanel.js +9 -0
- package/dist/admin/shop/ShopPanel.js.map +1 -1
- package/dist/admin/shop/ShopSettingsTab.d.ts +3 -0
- package/dist/admin/shop/ShopSettingsTab.js +86 -0
- package/dist/admin/shop/ShopSettingsTab.js.map +1 -0
- package/dist/admin.css +1 -1
- package/dist/blocks/context.d.ts +6 -0
- package/dist/blocks/context.js +15 -1
- package/dist/blocks/context.js.map +1 -1
- package/dist/modules/shop/blocks/Cart.js +22 -14
- package/dist/modules/shop/blocks/Cart.js.map +1 -1
- package/dist/modules/shop/blocks/Checkout.js +75 -23
- package/dist/modules/shop/blocks/Checkout.js.map +1 -1
- package/dist/modules/shop/blocks/Donation.js +8 -7
- package/dist/modules/shop/blocks/Donation.js.map +1 -1
- package/dist/modules/shop/blocks/ProductDetail.js +296 -109
- package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
- package/dist/modules/shop/blocks/ProductGrid.d.ts +9 -0
- package/dist/modules/shop/blocks/ProductGrid.js +125 -38
- package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
- package/dist/modules/shop/blocks/ShopCategories.js +7 -2
- package/dist/modules/shop/blocks/ShopCategories.js.map +1 -1
- package/dist/modules/shop/cartStore.d.ts +13 -2
- package/dist/modules/shop/cartStore.js +10 -5
- package/dist/modules/shop/cartStore.js.map +1 -1
- package/dist/modules/shop/lib.d.ts +18 -5
- package/dist/modules/shop/lib.js +23 -0
- package/dist/modules/shop/lib.js.map +1 -1
- package/dist/modules/shop/module.js +6 -1
- package/dist/modules/shop/module.js.map +1 -1
- package/dist/modules/shop/settings.d.ts +18 -0
- package/dist/modules/shop/settings.js +8 -1
- package/dist/modules/shop/settings.js.map +1 -1
- package/dist/modules/shop/strings.d.ts +65 -0
- package/dist/modules/shop/strings.js +136 -0
- package/dist/modules/shop/strings.js.map +1 -0
- package/dist/modules/shop/types.d.ts +18 -0
- package/dist/modules/shop/types.js +1 -0
- package/dist/modules/shop/types.js.map +1 -0
- package/dist/render/PublishedPage.d.ts +6 -1
- package/dist/render/PublishedPage.js +4 -3
- package/dist/render/PublishedPage.js.map +1 -1
- package/dist/schema/shop.d.ts +413 -2
- package/dist/schema/shop.js +95 -6
- package/dist/schema/shop.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +14 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/shop.d.ts +46 -0
- package/dist/server/shop.js +181 -0
- package/dist/server/shop.js.map +1 -0
- package/dist/styles.css +76 -0
- package/package.json +1 -1
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
+
import { type ShopT } from "../strings";
|
|
2
3
|
import { type StoredProduct } from "../lib";
|
|
3
4
|
/** Prijsregel met optionele doorgestreepte "van"-prijs en "vanaf" bij variant-prijzen. */
|
|
4
5
|
export declare function PriceLine({ product, currency }: {
|
|
5
6
|
product: StoredProduct;
|
|
6
7
|
currency: string;
|
|
7
8
|
}): import("react").JSX.Element;
|
|
9
|
+
export declare function ProductCard({ product, siteId, currency, detailPath, cartPath, t, }: {
|
|
10
|
+
product: StoredProduct;
|
|
11
|
+
siteId: string;
|
|
12
|
+
currency: string;
|
|
13
|
+
detailPath: string;
|
|
14
|
+
cartPath: string;
|
|
15
|
+
t: ShopT;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
8
17
|
export declare const productGridBlock: import("../../..").BlockDefinition<z.ZodTypeAny>;
|
|
@@ -3,9 +3,10 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useMemo, useState } from "react";
|
|
4
4
|
import { ProductGridBlockSchema, formatCents } from "../../../schema/shop";
|
|
5
5
|
import { defineBlock } from "../../../blocks/registry";
|
|
6
|
-
import {
|
|
7
|
-
import { Heading } from "../../../blocks/ui";
|
|
6
|
+
import { useBlockData } from "../../../blocks/context";
|
|
7
|
+
import { Card, Heading, Text } from "../../../blocks/ui";
|
|
8
8
|
import { cartAdd } from "../cartStore";
|
|
9
|
+
import { useShopStrings } from "../strings";
|
|
9
10
|
import {
|
|
10
11
|
fetchActiveProducts,
|
|
11
12
|
fetchShopCategories,
|
|
@@ -23,16 +24,21 @@ function PriceLine({ product, currency }) {
|
|
|
23
24
|
hasRange ? `vanaf ${formatCents(min, currency)}` : formatCents(min, currency)
|
|
24
25
|
] });
|
|
25
26
|
}
|
|
26
|
-
function
|
|
27
|
+
function isSoldOut(p) {
|
|
28
|
+
return p.trackStock === true && (p.options ?? []).length === 0 && p.stock !== void 0 && p.stock <= 0;
|
|
29
|
+
}
|
|
30
|
+
function ColorSwatches({ product, onHover }) {
|
|
27
31
|
const optie = kleurOptie(product);
|
|
28
32
|
if (!optie) return null;
|
|
29
|
-
const dots = optie.values.map((v) => ({ v, css: kleurNaarCss(v) })).filter((d) => d.css !== null);
|
|
33
|
+
const dots = optie.values.map((v) => ({ v, css: kleurNaarCss(v), img: optie.images?.[v] })).filter((d) => d.css !== null);
|
|
30
34
|
if (dots.length === 0) return null;
|
|
31
35
|
return /* @__PURE__ */ jsx("span", { className: "xicms-shop-card__swatches", "aria-label": `Kleuren: ${optie.values.join(", ")}`, children: dots.map((d) => /* @__PURE__ */ jsx(
|
|
32
36
|
"span",
|
|
33
37
|
{
|
|
34
38
|
className: "xicms-shop-card__swatch",
|
|
35
39
|
title: d.v,
|
|
40
|
+
onMouseEnter: d.img && onHover ? () => onHover(d.img ?? null) : void 0,
|
|
41
|
+
onMouseLeave: d.img && onHover ? () => onHover(null) : void 0,
|
|
36
42
|
style: {
|
|
37
43
|
display: "inline-block",
|
|
38
44
|
width: 12,
|
|
@@ -50,44 +56,47 @@ function ProductCard({
|
|
|
50
56
|
siteId,
|
|
51
57
|
currency,
|
|
52
58
|
detailPath,
|
|
53
|
-
cartPath
|
|
59
|
+
cartPath,
|
|
60
|
+
t
|
|
54
61
|
}) {
|
|
55
62
|
const [added, setAdded] = useState(false);
|
|
63
|
+
const [hoverImage, setHoverImage] = useState(null);
|
|
56
64
|
const href = `${detailPath}?p=${encodeURIComponent(product.slug)}`;
|
|
57
|
-
const simple = (product.options ?? []).length === 0;
|
|
65
|
+
const simple = (product.options ?? []).length === 0 && !(product.personalization ?? []).length;
|
|
66
|
+
const soldOut = isSoldOut(product);
|
|
67
|
+
const image = hoverImage ?? product.images?.[0];
|
|
58
68
|
return /* @__PURE__ */ jsxs(Card, { padding: 0, className: "xicms-shop-card", children: [
|
|
59
|
-
/* @__PURE__ */ jsxs("a", { href, style: { textDecoration: "none", color: "inherit", display: "block" }, children: [
|
|
60
|
-
|
|
69
|
+
/* @__PURE__ */ jsxs("a", { href, style: { textDecoration: "none", color: "inherit", display: "block", position: "relative" }, children: [
|
|
70
|
+
image ? /* @__PURE__ */ jsx(
|
|
61
71
|
"img",
|
|
62
72
|
{
|
|
63
|
-
src:
|
|
73
|
+
src: image,
|
|
64
74
|
alt: product.name,
|
|
65
75
|
loading: "lazy",
|
|
66
76
|
style: { width: "100%", aspectRatio: "1/1", objectFit: "cover", display: "block", borderRadius: "inherit" }
|
|
67
77
|
}
|
|
68
|
-
) : (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
style: { width: "100%", aspectRatio: "1/1", background: "var(--xicms-muted, #f1f1f1)", borderRadius: "inherit" }
|
|
76
|
-
}
|
|
77
|
-
)
|
|
78
|
+
) : /* @__PURE__ */ jsx(
|
|
79
|
+
"div",
|
|
80
|
+
{
|
|
81
|
+
className: "xicms-shop-card__placeholder",
|
|
82
|
+
"aria-hidden": "true",
|
|
83
|
+
style: { width: "100%", aspectRatio: "1/1", background: "var(--xicms-muted, #f1f1f1)", borderRadius: "inherit" }
|
|
84
|
+
}
|
|
78
85
|
),
|
|
86
|
+
soldOut && /* @__PURE__ */ jsx("span", { className: "xicms-shop-card__soldout", children: t("soldOut") }),
|
|
79
87
|
/* @__PURE__ */ jsxs("div", { style: { padding: "0.8rem 1rem 0.25rem", display: "grid", gap: 4 }, children: [
|
|
80
88
|
/* @__PURE__ */ jsx("span", { style: { fontWeight: 700 }, children: product.name }),
|
|
81
89
|
/* @__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 }) })
|
|
90
|
+
/* @__PURE__ */ jsx("span", { style: { display: "inline-flex", gap: 5, minHeight: 12 }, children: /* @__PURE__ */ jsx(ColorSwatches, { product, onHover: setHoverImage }) })
|
|
83
91
|
] })
|
|
84
92
|
] }),
|
|
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: "
|
|
93
|
+
/* @__PURE__ */ jsx("div", { className: "xicms-shop-card__actions", style: { padding: "0.5rem 1rem 1rem" }, children: simple ? added ? /* @__PURE__ */ jsx("a", { className: "xicms-shop-card__added", href: cartPath, style: { fontSize: "0.9rem" }, children: t("addedGoToCart") }) : /* @__PURE__ */ jsx(
|
|
86
94
|
"button",
|
|
87
95
|
{
|
|
88
96
|
type: "button",
|
|
89
97
|
className: "xicms-btn xicms-btn--primary",
|
|
90
|
-
|
|
98
|
+
disabled: soldOut,
|
|
99
|
+
style: { cursor: soldOut ? "not-allowed" : "pointer" },
|
|
91
100
|
onClick: () => {
|
|
92
101
|
cartAdd(siteId, {
|
|
93
102
|
productId: product.id,
|
|
@@ -98,46 +107,122 @@ function ProductCard({
|
|
|
98
107
|
});
|
|
99
108
|
setAdded(true);
|
|
100
109
|
},
|
|
101
|
-
children: "
|
|
110
|
+
children: t("addToCart")
|
|
102
111
|
}
|
|
103
|
-
) : /* @__PURE__ */ jsx("a", { className: "xicms-btn xicms-btn--secondary", href, children: "
|
|
112
|
+
) : /* @__PURE__ */ jsx("a", { className: "xicms-btn xicms-btn--secondary", href, children: t("view") }) })
|
|
104
113
|
] });
|
|
105
114
|
}
|
|
106
115
|
function ProductGridRender({ block, siteId }) {
|
|
107
116
|
const settings = useShopSettings();
|
|
108
|
-
const
|
|
109
|
-
const
|
|
117
|
+
const t = useShopStrings();
|
|
118
|
+
const serverData = useBlockData("shop");
|
|
119
|
+
const [products, setProducts] = useState(serverData?.products ?? null);
|
|
120
|
+
const [cats, setCats] = useState(serverData?.categories ?? []);
|
|
110
121
|
const [queryCat, setQueryCat] = useState(null);
|
|
122
|
+
const [filterColor, setFilterColor] = useState(null);
|
|
123
|
+
const [filterSize, setFilterSize] = useState(null);
|
|
124
|
+
const [sort, setSort] = useState(block.sort);
|
|
111
125
|
useEffect(() => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
if (!serverData?.products) {
|
|
127
|
+
void fetchActiveProducts(siteId).then(setProducts).catch((e) => {
|
|
128
|
+
console.warn("[xicms] producten laden mislukt:", e);
|
|
129
|
+
setProducts([]);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (!serverData?.categories) {
|
|
133
|
+
void fetchShopCategories(siteId).then(setCats).catch(() => setCats([]));
|
|
134
|
+
}
|
|
117
135
|
if (block.useQueryCategory && !block.categorySlug) setQueryCat(readQueryParam("cat"));
|
|
118
136
|
}, [siteId, block.useQueryCategory, block.categorySlug]);
|
|
119
137
|
const activeSlug = block.categorySlug || queryCat || null;
|
|
120
138
|
const activeCat = activeSlug ? cats.find((c) => c.slug === activeSlug) ?? null : null;
|
|
139
|
+
const filterValues = useMemo(() => {
|
|
140
|
+
if (!block.showFilters || !products) return { colors: [], sizes: [] };
|
|
141
|
+
const colors = /* @__PURE__ */ new Set();
|
|
142
|
+
const sizes = /* @__PURE__ */ new Set();
|
|
143
|
+
for (const p of products) {
|
|
144
|
+
for (const o of p.options ?? []) {
|
|
145
|
+
if (/^kleur|^colou?r/i.test(o.name)) o.values.forEach((v) => colors.add(v));
|
|
146
|
+
if (/^maat|^size/i.test(o.name)) o.values.forEach((v) => sizes.add(v));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { colors: [...colors], sizes: [...sizes].sort((a, b) => Number(a) - Number(b) || a.localeCompare(b)) };
|
|
150
|
+
}, [products, block.showFilters]);
|
|
121
151
|
const visible = useMemo(() => {
|
|
122
152
|
if (!products) return [];
|
|
123
153
|
let list = products;
|
|
124
154
|
if (activeCat) list = list.filter((p) => (p.categoryIds ?? []).includes(activeCat.id));
|
|
125
155
|
if (block.featuredOnly) list = list.filter((p) => p.featured);
|
|
156
|
+
if (filterColor) {
|
|
157
|
+
list = list.filter(
|
|
158
|
+
(p) => (p.options ?? []).some((o) => /^kleur|^colou?r/i.test(o.name) && o.values.includes(filterColor))
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (filterSize) {
|
|
162
|
+
list = list.filter(
|
|
163
|
+
(p) => (p.options ?? []).some((o) => /^maat|^size/i.test(o.name) && o.values.includes(filterSize))
|
|
164
|
+
);
|
|
165
|
+
}
|
|
126
166
|
const sorted = [...list];
|
|
127
|
-
if (
|
|
128
|
-
else if (
|
|
129
|
-
else if (
|
|
167
|
+
if (sort === "name") sorted.sort((a, b) => a.name.localeCompare(b.name, "nl"));
|
|
168
|
+
else if (sort === "priceAsc") sorted.sort((a, b) => a.priceCents - b.priceCents);
|
|
169
|
+
else if (sort === "priceDesc") sorted.sort((a, b) => b.priceCents - a.priceCents);
|
|
130
170
|
else sorted.sort((a, b) => (a.order ?? 0) - (b.order ?? 0) || a.name.localeCompare(b.name, "nl"));
|
|
131
171
|
return sorted.slice(0, block.limit);
|
|
132
|
-
}, [products, activeCat, block.featuredOnly, block.sort,
|
|
172
|
+
}, [products, activeCat, block.featuredOnly, block.limit, sort, filterColor, filterSize]);
|
|
133
173
|
if (products === null) return null;
|
|
174
|
+
const chip = (label, active, onClick, swatch) => /* @__PURE__ */ jsxs(
|
|
175
|
+
"button",
|
|
176
|
+
{
|
|
177
|
+
type: "button",
|
|
178
|
+
className: `xicms-shop-filterchip${active ? " xicms-shop-filterchip--active" : ""}`,
|
|
179
|
+
"aria-pressed": active,
|
|
180
|
+
onClick,
|
|
181
|
+
children: [
|
|
182
|
+
swatch && /* @__PURE__ */ jsx(
|
|
183
|
+
"span",
|
|
184
|
+
{
|
|
185
|
+
"aria-hidden": true,
|
|
186
|
+
style: { display: "inline-block", width: 10, height: 10, borderRadius: 999, background: swatch, boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.18)", marginRight: 5 }
|
|
187
|
+
}
|
|
188
|
+
),
|
|
189
|
+
label
|
|
190
|
+
]
|
|
191
|
+
},
|
|
192
|
+
label
|
|
193
|
+
);
|
|
134
194
|
return /* @__PURE__ */ jsxs("div", { className: "xicms-shop-grid", children: [
|
|
135
195
|
(block.heading || activeCat) && /* @__PURE__ */ jsx(Heading, { level: 2, "data-xicms-edit": "heading", children: activeCat?.name ?? block.heading }),
|
|
136
|
-
activeCat && /* @__PURE__ */ jsx(Text, { size: "sm", display: "block", style: { marginTop: 4 }, children: /* @__PURE__ */
|
|
137
|
-
|
|
196
|
+
activeCat && /* @__PURE__ */ jsx(Text, { size: "sm", display: "block", style: { marginTop: 4 }, children: /* @__PURE__ */ jsxs("a", { href: settings.shopPath, children: [
|
|
197
|
+
"\u2190 ",
|
|
198
|
+
t("allCategories")
|
|
199
|
+
] }) }),
|
|
200
|
+
block.showFilters && (filterValues.colors.length > 0 || filterValues.sizes.length > 0) && /* @__PURE__ */ jsxs("div", { className: "xicms-shop-filters", children: [
|
|
201
|
+
filterValues.colors.length > 0 && /* @__PURE__ */ jsxs("div", { className: "xicms-shop-filters__row", role: "group", "aria-label": "Kleur", children: [
|
|
202
|
+
chip(t("filterAll"), filterColor === null, () => setFilterColor(null)),
|
|
203
|
+
filterValues.colors.map(
|
|
204
|
+
(c) => chip(c, filterColor === c, () => setFilterColor(filterColor === c ? null : c), kleurNaarCss(c))
|
|
205
|
+
)
|
|
206
|
+
] }),
|
|
207
|
+
filterValues.sizes.length > 0 && /* @__PURE__ */ jsxs("div", { className: "xicms-shop-filters__row", role: "group", "aria-label": "Maat", children: [
|
|
208
|
+
chip(t("filterAll"), filterSize === null, () => setFilterSize(null)),
|
|
209
|
+
filterValues.sizes.map((s) => chip(s, filterSize === s, () => setFilterSize(filterSize === s ? null : s)))
|
|
210
|
+
] }),
|
|
211
|
+
/* @__PURE__ */ jsxs("label", { className: "xicms-shop-filters__sort", children: [
|
|
212
|
+
/* @__PURE__ */ jsx("span", { children: t("sortLabel") }),
|
|
213
|
+
/* @__PURE__ */ jsxs("select", { className: "xicms-input", value: sort, onChange: (e) => setSort(e.target.value), children: [
|
|
214
|
+
/* @__PURE__ */ jsx("option", { value: "order", children: t("sortDefault") }),
|
|
215
|
+
/* @__PURE__ */ jsx("option", { value: "name", children: t("sortName") }),
|
|
216
|
+
/* @__PURE__ */ jsx("option", { value: "priceAsc", children: t("sortPriceAsc") }),
|
|
217
|
+
/* @__PURE__ */ jsx("option", { value: "priceDesc", children: t("sortPriceDesc") })
|
|
218
|
+
] })
|
|
219
|
+
] })
|
|
220
|
+
] }),
|
|
221
|
+
visible.length === 0 ? /* @__PURE__ */ jsx(Card, { padding: 4, variant: "muted", style: { marginTop: "1rem" }, children: /* @__PURE__ */ jsx(Text, { children: t("noProducts") }) }) : /* @__PURE__ */ jsx(
|
|
138
222
|
"div",
|
|
139
223
|
{
|
|
140
224
|
className: "xicms-shop-grid__items",
|
|
225
|
+
"data-columns": block.columns,
|
|
141
226
|
style: {
|
|
142
227
|
display: "grid",
|
|
143
228
|
gridTemplateColumns: `repeat(auto-fill, minmax(min(240px, calc(100%/${block.columns} - 1rem)), 1fr))`,
|
|
@@ -151,7 +236,8 @@ function ProductGridRender({ block, siteId }) {
|
|
|
151
236
|
siteId,
|
|
152
237
|
currency: settings.currency,
|
|
153
238
|
detailPath: settings.detailPath,
|
|
154
|
-
cartPath: settings.cartPath
|
|
239
|
+
cartPath: settings.cartPath,
|
|
240
|
+
t
|
|
155
241
|
},
|
|
156
242
|
p.id
|
|
157
243
|
))
|
|
@@ -169,6 +255,7 @@ const productGridBlock = defineBlock({
|
|
|
169
255
|
});
|
|
170
256
|
export {
|
|
171
257
|
PriceLine,
|
|
258
|
+
ProductCard,
|
|
172
259
|
productGridBlock
|
|
173
260
|
};
|
|
174
261
|
//# sourceMappingURL=ProductGrid.js.map
|
|
@@ -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 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":";AA8BI,SAEI,KAFJ;AA5BJ,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,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;AAGA,SAAS,cAAc,EAAE,QAAQ,GAA+B;AAC9D,QAAM,QAAQ,WAAW,OAAO;AAChC,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,MAAM,OAChB,IAAI,CAAC,OAAO,EAAE,GAAG,KAAK,aAAa,CAAC,EAAE,EAAE,EACxC,OAAO,CAAC,MAAuC,EAAE,QAAQ,IAAI;AAChE,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,SACE,oBAAC,UAAK,WAAU,6BAA4B,cAAY,YAAY,MAAM,OAAO,KAAK,IAAI,CAAC,IACxF,eAAK,IAAI,CAAC,MACT;AAAA,IAAC;AAAA;AAAA,MAEC,WAAU;AAAA,MACV,OAAO,EAAE;AAAA,MACT,OAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,cAAc;AAAA,QACd,YAAY,EAAE;AAAA,QACd,WAAW;AAAA,MACb;AAAA;AAAA,IAVK,EAAE;AAAA,EAWT,CACD,GACH;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;AAAA;AAAA,QAGA;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,eAAY;AAAA,YACZ,OAAO,EAAE,OAAO,QAAQ,aAAa,OAAO,YAAY,+BAA+B,cAAc,UAAU;AAAA;AAAA,QACjH;AAAA;AAAA,MAEF,qBAAC,SAAI,OAAO,EAAE,SAAS,uBAAuB,SAAS,QAAQ,KAAK,EAAE,GACpE;AAAA,4BAAC,UAAK,OAAO,EAAE,YAAY,IAAI,GAAI,kBAAQ,MAAK;AAAA,QAChD,oBAAC,UAAK,OAAO,EAAE,UAAU,SAAS,GAChC,8BAAC,aAAU,SAAkB,UAAoB,GACnD;AAAA,QACA,oBAAC,UAAK,OAAO,EAAE,SAAS,eAAe,KAAK,GAAG,WAAW,GAAG,GAC3D,8BAAC,iBAAc,SAAkB,GACnC;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 { useBlockData } from \"../../../blocks/context\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { cartAdd } from \"../cartStore\";\nimport { useShopStrings, type ShopT } from \"../strings\";\nimport {\n fetchActiveProducts,\n fetchShopCategories,\n kleurNaarCss,\n kleurOptie,\n readQueryParam,\n useShopSettings,\n type StoredProduct,\n type StoredShopCategory,\n} from \"../lib\";\nimport type { ShopPageData } from \"../types\";\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/** Uitverkocht (alleen betrouwbaar vast te stellen zonder opties: expliciete voorraad 0). */\nfunction isSoldOut(p: StoredProduct): boolean {\n return p.trackStock === true && (p.options ?? []).length === 0 && p.stock !== undefined && p.stock <= 0;\n}\n\n/** Stof-stipjes; hover wisselt de kaartfoto naar de per-kleur-afbeelding (als die er is). */\nfunction ColorSwatches({ product, onHover }: { product: StoredProduct; onHover?: (url: string | null) => void }) {\n const optie = kleurOptie(product);\n if (!optie) return null;\n const dots = optie.values\n .map((v) => ({ v, css: kleurNaarCss(v), img: optie.images?.[v] }))\n .filter((d): d is { v: string; css: string; img: string | undefined } => 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 onMouseEnter={d.img && onHover ? () => onHover(d.img ?? null) : undefined}\n onMouseLeave={d.img && onHover ? () => onHover(null) : undefined}\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\nexport function ProductCard({\n product,\n siteId,\n currency,\n detailPath,\n cartPath,\n t,\n}: {\n product: StoredProduct;\n siteId: string;\n currency: string;\n detailPath: string;\n cartPath: string;\n t: ShopT;\n}) {\n const [added, setAdded] = useState(false);\n const [hoverImage, setHoverImage] = useState<string | null>(null);\n const href = `${detailPath}?p=${encodeURIComponent(product.slug)}`;\n const simple = (product.options ?? []).length === 0 && !(product.personalization ?? []).length;\n const soldOut = isSoldOut(product);\n const image = hoverImage ?? product.images?.[0];\n\n return (\n <Card padding={0} className=\"xicms-shop-card\">\n <a href={href} style={{ textDecoration: \"none\", color: \"inherit\", display: \"block\", position: \"relative\" }}>\n {image ? (\n <img\n src={image}\n alt={product.name}\n loading=\"lazy\"\n style={{ width: \"100%\", aspectRatio: \"1/1\", objectFit: \"cover\", display: \"block\", borderRadius: \"inherit\" }}\n />\n ) : (\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 {soldOut && <span className=\"xicms-shop-card__soldout\">{t(\"soldOut\")}</span>}\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} onHover={setHoverImage} />\n </span>\n </div>\n </a>\n <div className=\"xicms-shop-card__actions\" style={{ padding: \"0.5rem 1rem 1rem\" }}>\n {simple ? (\n added ? (\n <a className=\"xicms-shop-card__added\" href={cartPath} style={{ fontSize: \"0.9rem\" }}>\n {t(\"addedGoToCart\")}\n </a>\n ) : (\n <button\n type=\"button\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={soldOut}\n style={{ cursor: soldOut ? \"not-allowed\" : \"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 {t(\"addToCart\")}\n </button>\n )\n ) : (\n <a className=\"xicms-btn xicms-btn--secondary\" href={href}>\n {t(\"view\")}\n </a>\n )}\n </div>\n </Card>\n );\n}\n\nfunction ProductGridRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [products, setProducts] = useState<StoredProduct[] | null>(serverData?.products ?? null);\n const [cats, setCats] = useState<StoredShopCategory[]>(serverData?.categories ?? []);\n const [queryCat, setQueryCat] = useState<string | null>(null);\n const [filterColor, setFilterColor] = useState<string | null>(null);\n const [filterSize, setFilterSize] = useState<string | null>(null);\n const [sort, setSort] = useState<Blk[\"sort\"]>(block.sort);\n\n useEffect(() => {\n // Server-data aanwezig (SSR/ISR)? Dan geen client-fetch — geen flits, minder reads.\n if (!serverData?.products) {\n void fetchActiveProducts(siteId).then(setProducts).catch((e) => {\n console.warn(\"[xicms] producten laden mislukt:\", e);\n setProducts([]);\n });\n }\n if (!serverData?.categories) {\n void fetchShopCategories(siteId).then(setCats).catch(() => setCats([]));\n }\n if (block.useQueryCategory && !block.categorySlug) setQueryCat(readQueryParam(\"cat\"));\n // eslint-disable-next-line react-hooks/exhaustive-deps\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 /** Beschikbare filterwaarden uit de geladen set (client-side; geen extra reads). */\n const filterValues = useMemo(() => {\n if (!block.showFilters || !products) return { colors: [] as string[], sizes: [] as string[] };\n const colors = new Set<string>();\n const sizes = new Set<string>();\n for (const p of products) {\n for (const o of p.options ?? []) {\n if (/^kleur|^colou?r/i.test(o.name)) o.values.forEach((v) => colors.add(v));\n if (/^maat|^size/i.test(o.name)) o.values.forEach((v) => sizes.add(v));\n }\n }\n return { colors: [...colors], sizes: [...sizes].sort((a, b) => Number(a) - Number(b) || a.localeCompare(b)) };\n }, [products, block.showFilters]);\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 if (filterColor) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^kleur|^colou?r/i.test(o.name) && o.values.includes(filterColor)),\n );\n }\n if (filterSize) {\n list = list.filter((p) =>\n (p.options ?? []).some((o) => /^maat|^size/i.test(o.name) && o.values.includes(filterSize)),\n );\n }\n const sorted = [...list];\n if (sort === \"name\") sorted.sort((a, b) => a.name.localeCompare(b.name, \"nl\"));\n else if (sort === \"priceAsc\") sorted.sort((a, b) => a.priceCents - b.priceCents);\n else if (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.limit, sort, filterColor, filterSize]);\n\n if (products === null) return null;\n\n const chip = (label: string, active: boolean, onClick: () => void, swatch?: string | null) => (\n <button\n key={label}\n type=\"button\"\n className={`xicms-shop-filterchip${active ? \" xicms-shop-filterchip--active\" : \"\"}`}\n aria-pressed={active}\n onClick={onClick}\n >\n {swatch && (\n <span\n aria-hidden\n style={{ display: \"inline-block\", width: 10, height: 10, borderRadius: 999, background: swatch, boxShadow: \"inset 0 0 0 1px rgba(0,0,0,0.18)\", marginRight: 5 }}\n />\n )}\n {label}\n </button>\n );\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}>← {t(\"allCategories\")}</a>\n </Text>\n )}\n\n {block.showFilters && (filterValues.colors.length > 0 || filterValues.sizes.length > 0) && (\n <div className=\"xicms-shop-filters\">\n {filterValues.colors.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Kleur\">\n {chip(t(\"filterAll\"), filterColor === null, () => setFilterColor(null))}\n {filterValues.colors.map((c) =>\n chip(c, filterColor === c, () => setFilterColor(filterColor === c ? null : c), kleurNaarCss(c)),\n )}\n </div>\n )}\n {filterValues.sizes.length > 0 && (\n <div className=\"xicms-shop-filters__row\" role=\"group\" aria-label=\"Maat\">\n {chip(t(\"filterAll\"), filterSize === null, () => setFilterSize(null))}\n {filterValues.sizes.map((s) => chip(s, filterSize === s, () => setFilterSize(filterSize === s ? null : s)))}\n </div>\n )}\n <label className=\"xicms-shop-filters__sort\">\n <span>{t(\"sortLabel\")}</span>\n <select className=\"xicms-input\" value={sort} onChange={(e) => setSort(e.target.value as Blk[\"sort\"])}>\n <option value=\"order\">{t(\"sortDefault\")}</option>\n <option value=\"name\">{t(\"sortName\")}</option>\n <option value=\"priceAsc\">{t(\"sortPriceAsc\")}</option>\n <option value=\"priceDesc\">{t(\"sortPriceDesc\")}</option>\n </select>\n </label>\n </div>\n )}\n\n {visible.length === 0 ? (\n <Card padding={4} variant=\"muted\" style={{ marginTop: \"1rem\" }}>\n <Text>{t(\"noProducts\")}</Text>\n </Card>\n ) : (\n <div\n className=\"xicms-shop-grid__items\"\n data-columns={block.columns}\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 t={t}\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":[]}
|
|
@@ -4,11 +4,16 @@ import { useEffect, useState } from "react";
|
|
|
4
4
|
import { ShopCategoriesBlockSchema } from "../../../schema/shop";
|
|
5
5
|
import { defineBlock } from "../../../blocks/registry";
|
|
6
6
|
import { Card, Heading, Text } from "../../../blocks/ui";
|
|
7
|
+
import { useBlockData } from "../../../blocks/context";
|
|
8
|
+
import { useShopStrings } from "../strings";
|
|
7
9
|
import { fetchShopCategories, useShopSettings } from "../lib";
|
|
8
10
|
function ShopCategoriesRender({ block, siteId }) {
|
|
9
11
|
const settings = useShopSettings();
|
|
10
|
-
const
|
|
12
|
+
const t = useShopStrings();
|
|
13
|
+
const serverData = useBlockData("shop");
|
|
14
|
+
const [cats, setCats] = useState(serverData?.categories ?? null);
|
|
11
15
|
useEffect(() => {
|
|
16
|
+
if (serverData?.categories) return;
|
|
12
17
|
void fetchShopCategories(siteId).then(setCats).catch((e) => {
|
|
13
18
|
console.warn("[xicms] shop-categorie\xEBn laden mislukt:", e);
|
|
14
19
|
setCats([]);
|
|
@@ -16,7 +21,7 @@ function ShopCategoriesRender({ block, siteId }) {
|
|
|
16
21
|
}, [siteId]);
|
|
17
22
|
if (cats === null) return null;
|
|
18
23
|
if (cats.length === 0) {
|
|
19
|
-
return /* @__PURE__ */ jsx(Card, { padding: 4, variant: "muted", children: /* @__PURE__ */ jsx(Text, { children: "
|
|
24
|
+
return /* @__PURE__ */ jsx(Card, { padding: 4, variant: "muted", children: /* @__PURE__ */ jsx(Text, { children: t("noCategories") }) });
|
|
20
25
|
}
|
|
21
26
|
return /* @__PURE__ */ jsxs("div", { className: "xicms-shop-categories", children: [
|
|
22
27
|
block.heading && /* @__PURE__ */ jsx(Heading, { level: 2, "data-xicms-edit": "heading", children: block.heading }),
|
|
@@ -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 { fetchShopCategories, useShopSettings, type StoredShopCategory } from \"../lib\";\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 [cats, setCats] = useState<StoredShopCategory[] | null>(null);\n\n useEffect(() => {\n void fetchShopCategories(siteId).then(setCats).catch((e) => {\n console.warn(\"[xicms] shop-categorieën laden mislukt:\", e);\n setCats([]);\n });\n }, [siteId]);\n\n if (cats === null) return null;\n if (cats.length === 0) {\n return (\n <Card padding={4} variant=\"muted\">\n <Text>
|
|
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":[]}
|
|
@@ -5,13 +5,24 @@ export interface CartLine {
|
|
|
5
5
|
/** Variant-key ("68|Roze") + leesbaar label ("Maat 68, Roze"). */
|
|
6
6
|
variantKey?: string;
|
|
7
7
|
variantLabel?: string;
|
|
8
|
+
/** Incl. personalisatie-meerprijs (indicatief; server herrekent). */
|
|
8
9
|
unitPriceCents: number;
|
|
9
10
|
qty: number;
|
|
10
11
|
imageUrl?: string;
|
|
12
|
+
/** 5.6: personalisatie per veld-key ({ naam: { text: "Boaz" } }). */
|
|
13
|
+
personalization?: Record<string, {
|
|
14
|
+
text?: string;
|
|
15
|
+
imagePath?: string;
|
|
16
|
+
fileName?: string;
|
|
17
|
+
}>;
|
|
18
|
+
/** Leesbare samenvatting voor cart/checkout ("Naam: Boaz"). */
|
|
19
|
+
personalizationLabel?: string;
|
|
11
20
|
}
|
|
12
21
|
export declare function cartAdd(siteId: string, line: Omit<CartLine, "qty">, qty?: number): void;
|
|
13
|
-
export declare function cartSetQty(siteId: string,
|
|
14
|
-
export declare function cartRemove(siteId: string,
|
|
22
|
+
export declare function cartSetQty(siteId: string, line: Pick<CartLine, "productId" | "variantKey" | "personalization">, qty: number): void;
|
|
23
|
+
export declare function cartRemove(siteId: string, line: Pick<CartLine, "productId" | "variantKey" | "personalization">): void;
|
|
24
|
+
/** Stabiele sleutel voor React-keys in cart-weergaven. */
|
|
25
|
+
export declare function cartLineKey(l: Pick<CartLine, "productId" | "variantKey" | "personalization">): string;
|
|
15
26
|
export declare function cartClear(siteId: string): void;
|
|
16
27
|
export interface CartState {
|
|
17
28
|
lines: CartLine[];
|
|
@@ -48,7 +48,8 @@ function subscribe(cb) {
|
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
function lineId(l) {
|
|
51
|
-
|
|
51
|
+
const pers = l.personalization ? JSON.stringify(l.personalization) : "";
|
|
52
|
+
return `${l.productId}::${l.variantKey ?? ""}::${pers}`;
|
|
52
53
|
}
|
|
53
54
|
function cartAdd(siteId, line, qty = 1) {
|
|
54
55
|
const lines = [...readLines(siteId)];
|
|
@@ -57,12 +58,15 @@ function cartAdd(siteId, line, qty = 1) {
|
|
|
57
58
|
else lines.push({ ...line, qty: Math.min(999, Math.max(1, qty)) });
|
|
58
59
|
writeLines(siteId, lines);
|
|
59
60
|
}
|
|
60
|
-
function cartSetQty(siteId,
|
|
61
|
-
const lines = readLines(siteId).map((l) => lineId(l) === lineId(
|
|
61
|
+
function cartSetQty(siteId, line, qty) {
|
|
62
|
+
const lines = readLines(siteId).map((l) => lineId(l) === lineId(line) ? { ...l, qty: Math.min(999, qty) } : l).filter((l) => l.qty > 0);
|
|
62
63
|
writeLines(siteId, lines);
|
|
63
64
|
}
|
|
64
|
-
function cartRemove(siteId,
|
|
65
|
-
writeLines(siteId, readLines(siteId).filter((l) => lineId(l) !== lineId(
|
|
65
|
+
function cartRemove(siteId, line) {
|
|
66
|
+
writeLines(siteId, readLines(siteId).filter((l) => lineId(l) !== lineId(line)));
|
|
67
|
+
}
|
|
68
|
+
function cartLineKey(l) {
|
|
69
|
+
return lineId(l);
|
|
66
70
|
}
|
|
67
71
|
function cartClear(siteId) {
|
|
68
72
|
writeLines(siteId, []);
|
|
@@ -78,6 +82,7 @@ function useCart(siteId) {
|
|
|
78
82
|
export {
|
|
79
83
|
cartAdd,
|
|
80
84
|
cartClear,
|
|
85
|
+
cartLineKey,
|
|
81
86
|
cartRemove,
|
|
82
87
|
cartSetQty,
|
|
83
88
|
useCart
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/modules/shop/cartStore.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Winkelwagen-store — puur client-side (localStorage), geen Firestore-writes vóór checkout.\n * Extern store + `useSyncExternalStore` zodat élk shop-block (grid, cart-badge, checkout) dezelfde\n * state ziet zonder dat er een provider gemount hoeft te worden. Cross-tab sync via `storage`.\n *\n * Prijzen in de cart zijn indicatief voor weergave; bij `createOrder` herrekent de server alles\n * tegen de opgeslagen product-docs (client wordt nooit vertrouwd).\n */\nimport { useSyncExternalStore } from \"react\";\n\nexport interface CartLine {\n productId: string;\n slug: string;\n name: string;\n /** Variant-key (\"68|Roze\") + leesbaar label (\"Maat 68, Roze\"). */\n variantKey?: string;\n variantLabel?: string;\n unitPriceCents: number;\n qty: number;\n imageUrl?: string;\n}\n\nconst listeners = new Set<() => void>();\n/** Cache per siteId zodat getSnapshot een stabiele referentie teruggeeft (useSyncExternalStore-eis). */\nconst cache = new Map<string, { raw: string; lines: CartLine[] }>();\n\nfunction storageKey(siteId: string): string {\n return `xicms:cart:${siteId}`;\n}\n\nfunction readLines(siteId: string): CartLine[] {\n if (typeof window === \"undefined\") return [];\n let raw = \"\";\n try {\n raw = window.localStorage.getItem(storageKey(siteId)) ?? \"[]\";\n } catch {\n raw = \"[]\";\n }\n const cached = cache.get(siteId);\n if (cached && cached.raw === raw) return cached.lines;\n let lines: CartLine[] = [];\n try {\n const parsed = JSON.parse(raw);\n if (Array.isArray(parsed)) lines = parsed.filter((l) => l && typeof l === \"object\" && typeof l.productId === \"string\");\n } catch {\n lines = [];\n }\n cache.set(siteId, { raw, lines });\n return lines;\n}\n\nfunction writeLines(siteId: string, lines: CartLine[]): void {\n try {\n window.localStorage.setItem(storageKey(siteId), JSON.stringify(lines));\n } catch {\n // localStorage vol/geblokkeerd: cart werkt dan alleen in-memory via de cache.\n cache.set(siteId, { raw: `mem:${Date.now()}`, lines });\n }\n emit();\n}\n\nfunction emit(): void {\n for (const l of listeners) l();\n}\n\nfunction subscribe(cb: () => void): () => void {\n listeners.add(cb);\n // Cross-tab: een andere tab die de cart wijzigt triggert hier een re-render.\n const onStorage = (e: StorageEvent) => {\n if (e.key?.startsWith(\"xicms:cart:\")) cb();\n };\n window.addEventListener(\"storage\", onStorage);\n return () => {\n listeners.delete(cb);\n window.removeEventListener(\"storage\", onStorage);\n };\n}\n\nfunction lineId(l: Pick<CartLine, \"productId\" | \"variantKey\">): string {\n return `${l.productId}::${l.variantKey ?? \"\"}`;\n}\n\nexport function cartAdd(siteId: string, line: Omit<CartLine, \"qty\">, qty = 1): void {\n const lines = [...readLines(siteId)];\n const idx = lines.findIndex((l) => lineId(l) === lineId(line));\n if (idx >= 0) lines[idx] = { ...lines[idx], qty: Math.min(999, lines[idx].qty + qty) };\n else lines.push({ ...line, qty: Math.min(999, Math.max(1, qty)) });\n writeLines(siteId, lines);\n}\n\nexport function cartSetQty(siteId: string, productId
|
|
1
|
+
{"version":3,"sources":["../../../src/modules/shop/cartStore.ts"],"sourcesContent":["\"use client\";\n\n/**\n * Winkelwagen-store — puur client-side (localStorage), geen Firestore-writes vóór checkout.\n * Extern store + `useSyncExternalStore` zodat élk shop-block (grid, cart-badge, checkout) dezelfde\n * state ziet zonder dat er een provider gemount hoeft te worden. Cross-tab sync via `storage`.\n *\n * Prijzen in de cart zijn indicatief voor weergave; bij `createOrder` herrekent de server alles\n * tegen de opgeslagen product-docs (client wordt nooit vertrouwd).\n */\nimport { useSyncExternalStore } from \"react\";\n\nexport interface CartLine {\n productId: string;\n slug: string;\n name: string;\n /** Variant-key (\"68|Roze\") + leesbaar label (\"Maat 68, Roze\"). */\n variantKey?: string;\n variantLabel?: string;\n /** Incl. personalisatie-meerprijs (indicatief; server herrekent). */\n unitPriceCents: number;\n qty: number;\n imageUrl?: string;\n /** 5.6: personalisatie per veld-key ({ naam: { text: \"Boaz\" } }). */\n personalization?: Record<string, { text?: string; imagePath?: string; fileName?: string }>;\n /** Leesbare samenvatting voor cart/checkout (\"Naam: Boaz\"). */\n personalizationLabel?: string;\n}\n\nconst listeners = new Set<() => void>();\n/** Cache per siteId zodat getSnapshot een stabiele referentie teruggeeft (useSyncExternalStore-eis). */\nconst cache = new Map<string, { raw: string; lines: CartLine[] }>();\n\nfunction storageKey(siteId: string): string {\n return `xicms:cart:${siteId}`;\n}\n\nfunction readLines(siteId: string): CartLine[] {\n if (typeof window === \"undefined\") return [];\n let raw = \"\";\n try {\n raw = window.localStorage.getItem(storageKey(siteId)) ?? \"[]\";\n } catch {\n raw = \"[]\";\n }\n const cached = cache.get(siteId);\n if (cached && cached.raw === raw) return cached.lines;\n let lines: CartLine[] = [];\n try {\n const parsed = JSON.parse(raw);\n if (Array.isArray(parsed)) lines = parsed.filter((l) => l && typeof l === \"object\" && typeof l.productId === \"string\");\n } catch {\n lines = [];\n }\n cache.set(siteId, { raw, lines });\n return lines;\n}\n\nfunction writeLines(siteId: string, lines: CartLine[]): void {\n try {\n window.localStorage.setItem(storageKey(siteId), JSON.stringify(lines));\n } catch {\n // localStorage vol/geblokkeerd: cart werkt dan alleen in-memory via de cache.\n cache.set(siteId, { raw: `mem:${Date.now()}`, lines });\n }\n emit();\n}\n\nfunction emit(): void {\n for (const l of listeners) l();\n}\n\nfunction subscribe(cb: () => void): () => void {\n listeners.add(cb);\n // Cross-tab: een andere tab die de cart wijzigt triggert hier een re-render.\n const onStorage = (e: StorageEvent) => {\n if (e.key?.startsWith(\"xicms:cart:\")) cb();\n };\n window.addEventListener(\"storage\", onStorage);\n return () => {\n listeners.delete(cb);\n window.removeEventListener(\"storage\", onStorage);\n };\n}\n\nfunction lineId(l: Pick<CartLine, \"productId\" | \"variantKey\" | \"personalization\">): string {\n // Personalisatie maakt de regel uniek: twee rompertjes met verschillende namen zijn\n // aparte regels (anders verhoogt \"nog één toevoegen\" stilletjes de verkeerde).\n const pers = l.personalization ? JSON.stringify(l.personalization) : \"\";\n return `${l.productId}::${l.variantKey ?? \"\"}::${pers}`;\n}\n\nexport function cartAdd(siteId: string, line: Omit<CartLine, \"qty\">, qty = 1): void {\n const lines = [...readLines(siteId)];\n const idx = lines.findIndex((l) => lineId(l) === lineId(line));\n if (idx >= 0) lines[idx] = { ...lines[idx], qty: Math.min(999, lines[idx].qty + qty) };\n else lines.push({ ...line, qty: Math.min(999, Math.max(1, qty)) });\n writeLines(siteId, lines);\n}\n\nexport function cartSetQty(\n siteId: string,\n line: Pick<CartLine, \"productId\" | \"variantKey\" | \"personalization\">,\n qty: number,\n): void {\n const lines = readLines(siteId)\n .map((l) => (lineId(l) === lineId(line) ? { ...l, qty: Math.min(999, qty) } : l))\n .filter((l) => l.qty > 0);\n writeLines(siteId, lines);\n}\n\nexport function cartRemove(siteId: string, line: Pick<CartLine, \"productId\" | \"variantKey\" | \"personalization\">): void {\n writeLines(siteId, readLines(siteId).filter((l) => lineId(l) !== lineId(line)));\n}\n\n/** Stabiele sleutel voor React-keys in cart-weergaven. */\nexport function cartLineKey(l: Pick<CartLine, \"productId\" | \"variantKey\" | \"personalization\">): string {\n return lineId(l);\n}\n\nexport function cartClear(siteId: string): void {\n writeLines(siteId, []);\n}\n\nexport interface CartState {\n lines: CartLine[];\n count: number;\n itemsCents: number;\n}\n\n/** Reactieve cart-state voor blocks/klantcomponenten (bv. een cart-badge in de header). */\nexport function useCart(siteId: string): CartState {\n const lines = useSyncExternalStore(subscribe, () => readLines(siteId), () => [] as CartLine[]);\n return {\n lines,\n count: lines.reduce((n, l) => n + l.qty, 0),\n itemsCents: lines.reduce((n, l) => n + l.unitPriceCents * l.qty, 0),\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreateOrderInput, Product, ShopCategory } from "../../schema/shop";
|
|
1
|
+
import type { CreateOrderInput, Product, ProductOption, ShopCategory } from "../../schema/shop";
|
|
2
2
|
import { type ShopSettings } from "./settings";
|
|
3
3
|
/** Publieke shop-instellingen met defaults (paden, verzendkosten, valuta). */
|
|
4
4
|
export declare function useShopSettings(): ShopSettings;
|
|
@@ -16,6 +16,22 @@ export declare function createOrderCall(input: CreateOrderInput): Promise<{
|
|
|
16
16
|
url: string;
|
|
17
17
|
orderNumber: string;
|
|
18
18
|
}>;
|
|
19
|
+
/** Valideert een kortingscode server-side; geeft de verwachte korting terug (preview). */
|
|
20
|
+
export interface DiscountPreview {
|
|
21
|
+
valid: boolean;
|
|
22
|
+
code?: string;
|
|
23
|
+
discountCents?: number;
|
|
24
|
+
freeShipping?: boolean;
|
|
25
|
+
reason?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function validateDiscountCall(siteId: string, code: string, itemsCents: number): Promise<DiscountPreview>;
|
|
28
|
+
/**
|
|
29
|
+
* Upload een personalisatie-afbeelding (eigen logo e.d.) via de begrensde callable —
|
|
30
|
+
* geeft het Storage-pad terug dat op de orderregel meegaat. Max 5 MB, jpeg/png/webp.
|
|
31
|
+
*/
|
|
32
|
+
export declare function uploadPersonalizationCall(siteId: string, productId: string, file: File): Promise<{
|
|
33
|
+
path: string;
|
|
34
|
+
}>;
|
|
19
35
|
/** Leesbaar variantlabel: opties ["Maat","Kleur"] + key "68|Roze" → "Maat 68 · Roze". */
|
|
20
36
|
export declare function variantLabelFor(options: {
|
|
21
37
|
name: string;
|
|
@@ -29,7 +45,4 @@ export declare function readQueryParam(name: string): string | null;
|
|
|
29
45
|
*/
|
|
30
46
|
export declare function kleurNaarCss(naam: string): string | null;
|
|
31
47
|
/** 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;
|
|
48
|
+
export declare function kleurOptie(product: Pick<Product, "options">): ProductOption | null;
|
package/dist/modules/shop/lib.js
CHANGED
|
@@ -45,6 +45,27 @@ async function createOrderCall(input) {
|
|
|
45
45
|
const res = await fn(input);
|
|
46
46
|
return res.data;
|
|
47
47
|
}
|
|
48
|
+
async function validateDiscountCall(siteId, code, itemsCents) {
|
|
49
|
+
const app = getFirebaseApp();
|
|
50
|
+
if (!app) throw new Error("Webwinkel niet beschikbaar.");
|
|
51
|
+
const fn = httpsCallable(getFunctions(app, functionsRegion()), "validateDiscountCode");
|
|
52
|
+
const res = await fn({ siteId, code, itemsCents });
|
|
53
|
+
return res.data;
|
|
54
|
+
}
|
|
55
|
+
async function uploadPersonalizationCall(siteId, productId, file) {
|
|
56
|
+
const app = getFirebaseApp();
|
|
57
|
+
if (!app) throw new Error("Webwinkel niet beschikbaar.");
|
|
58
|
+
if (file.size > 5 * 1024 * 1024) throw new Error("TOO_BIG");
|
|
59
|
+
const bytesBase64 = await new Promise((resolve, reject) => {
|
|
60
|
+
const reader = new FileReader();
|
|
61
|
+
reader.onload = () => resolve(String(reader.result).split(",")[1] ?? "");
|
|
62
|
+
reader.onerror = () => reject(new Error("Bestand lezen mislukt."));
|
|
63
|
+
reader.readAsDataURL(file);
|
|
64
|
+
});
|
|
65
|
+
const fn = httpsCallable(getFunctions(app, functionsRegion()), "uploadPersonalization");
|
|
66
|
+
const res = await fn({ siteId, productId, fileName: file.name, contentType: file.type, bytesBase64 });
|
|
67
|
+
return res.data;
|
|
68
|
+
}
|
|
48
69
|
function variantLabelFor(options, key) {
|
|
49
70
|
const parts = key.split("|");
|
|
50
71
|
return options.map((o, i) => `${o.name} ${parts[i] ?? "?"}`).join(" \xB7 ");
|
|
@@ -85,7 +106,9 @@ export {
|
|
|
85
106
|
kleurNaarCss,
|
|
86
107
|
kleurOptie,
|
|
87
108
|
readQueryParam,
|
|
109
|
+
uploadPersonalizationCall,
|
|
88
110
|
useShopSettings,
|
|
111
|
+
validateDiscountCall,
|
|
89
112
|
variantLabelFor
|
|
90
113
|
};
|
|
91
114
|
//# sourceMappingURL=lib.js.map
|