@xiwebdesign/xicms-core 5.5.2 → 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.
Files changed (66) hide show
  1. package/dist/admin/shop/ConnectionsTab.js +1 -1
  2. package/dist/admin/shop/ConnectionsTab.js.map +1 -1
  3. package/dist/admin/shop/DiscountsTab.d.ts +4 -0
  4. package/dist/admin/shop/DiscountsTab.js +168 -0
  5. package/dist/admin/shop/DiscountsTab.js.map +1 -0
  6. package/dist/admin/shop/EmailsTab.d.ts +8 -0
  7. package/dist/admin/shop/EmailsTab.js +88 -0
  8. package/dist/admin/shop/EmailsTab.js.map +1 -0
  9. package/dist/admin/shop/OrdersTab.js +271 -139
  10. package/dist/admin/shop/OrdersTab.js.map +1 -1
  11. package/dist/admin/shop/ProductsTab.js +152 -28
  12. package/dist/admin/shop/ProductsTab.js.map +1 -1
  13. package/dist/admin/shop/ShopPanel.d.ts +4 -1
  14. package/dist/admin/shop/ShopPanel.js +9 -0
  15. package/dist/admin/shop/ShopPanel.js.map +1 -1
  16. package/dist/admin/shop/ShopSettingsTab.d.ts +3 -0
  17. package/dist/admin/shop/ShopSettingsTab.js +86 -0
  18. package/dist/admin/shop/ShopSettingsTab.js.map +1 -0
  19. package/dist/admin.css +1 -1
  20. package/dist/blocks/context.d.ts +6 -0
  21. package/dist/blocks/context.js +15 -1
  22. package/dist/blocks/context.js.map +1 -1
  23. package/dist/modules/shop/blocks/Cart.js +22 -14
  24. package/dist/modules/shop/blocks/Cart.js.map +1 -1
  25. package/dist/modules/shop/blocks/Checkout.js +75 -23
  26. package/dist/modules/shop/blocks/Checkout.js.map +1 -1
  27. package/dist/modules/shop/blocks/Donation.js +8 -7
  28. package/dist/modules/shop/blocks/Donation.js.map +1 -1
  29. package/dist/modules/shop/blocks/ProductDetail.js +296 -102
  30. package/dist/modules/shop/blocks/ProductDetail.js.map +1 -1
  31. package/dist/modules/shop/blocks/ProductGrid.d.ts +9 -0
  32. package/dist/modules/shop/blocks/ProductGrid.js +151 -29
  33. package/dist/modules/shop/blocks/ProductGrid.js.map +1 -1
  34. package/dist/modules/shop/blocks/ShopCategories.js +7 -2
  35. package/dist/modules/shop/blocks/ShopCategories.js.map +1 -1
  36. package/dist/modules/shop/cartStore.d.ts +13 -2
  37. package/dist/modules/shop/cartStore.js +10 -5
  38. package/dist/modules/shop/cartStore.js.map +1 -1
  39. package/dist/modules/shop/lib.d.ts +24 -1
  40. package/dist/modules/shop/lib.js +49 -0
  41. package/dist/modules/shop/lib.js.map +1 -1
  42. package/dist/modules/shop/module.js +6 -1
  43. package/dist/modules/shop/module.js.map +1 -1
  44. package/dist/modules/shop/settings.d.ts +18 -0
  45. package/dist/modules/shop/settings.js +8 -1
  46. package/dist/modules/shop/settings.js.map +1 -1
  47. package/dist/modules/shop/strings.d.ts +65 -0
  48. package/dist/modules/shop/strings.js +136 -0
  49. package/dist/modules/shop/strings.js.map +1 -0
  50. package/dist/modules/shop/types.d.ts +18 -0
  51. package/dist/modules/shop/types.js +1 -0
  52. package/dist/modules/shop/types.js.map +1 -0
  53. package/dist/render/PublishedPage.d.ts +6 -1
  54. package/dist/render/PublishedPage.js +4 -3
  55. package/dist/render/PublishedPage.js.map +1 -1
  56. package/dist/schema/shop.d.ts +413 -2
  57. package/dist/schema/shop.js +95 -6
  58. package/dist/schema/shop.js.map +1 -1
  59. package/dist/server/index.d.ts +1 -0
  60. package/dist/server/index.js +14 -0
  61. package/dist/server/index.js.map +1 -1
  62. package/dist/server/shop.d.ts +46 -0
  63. package/dist/server/shop.js +181 -0
  64. package/dist/server/shop.js.map +1 -0
  65. package/dist/styles.css +76 -0
  66. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/modules/shop/blocks/Donation.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState, type FormEvent } from \"react\";\nimport type { z } from \"zod\";\nimport { DonationBlockSchema, formatCents } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { createOrderCall, useShopSettings } from \"../lib\";\n\ntype Blk = z.infer<typeof DonationBlockSchema>;\n\n/**\n * Donatie — bedragknoppen + vrij bedrag, naam/e-mail, betaling via dezelfde `createOrder`-flow\n * (orderregel kind:\"donation\", geen verzendadres/verzendkosten). Het gekozen bedrag is de enige\n * client-invoer die de server accepteert — binnen de min/max van dit block.\n */\nfunction DonationRender({ block, siteId, mode }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const [amountCents, setAmountCents] = useState<number | null>(block.amountsCents[0] ?? null);\n const [customEuro, setCustomEuro] = useState(\"\");\n const [custom, setCustom] = useState(false);\n const [v, setV] = useState({ name: \"\", email: \"\" });\n const [hp, setHp] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const effectiveCents = custom\n ? Math.round((Number(customEuro.replace(\",\", \".\")) || 0) * 100)\n : amountCents ?? 0;\n const valid =\n effectiveCents >= (custom ? block.customMinCents : 50) &&\n effectiveCents <= block.customMaxCents;\n\n async function submit(e: FormEvent) {\n e.preventDefault();\n if (mode === \"preview\" || busy || !valid) return;\n setBusy(true);\n setError(null);\n try {\n const { url } = await createOrderCall({\n siteId,\n items: [{ qty: 1, donationCents: effectiveCents }],\n customer: { email: v.email, name: v.name },\n successPath: settings.successPath,\n cancelPath: typeof window !== \"undefined\" ? window.location.pathname : \"/\",\n _hp: hp || undefined,\n });\n window.location.assign(url);\n } catch (err) {\n console.error(\"[xicms] donatie mislukt:\", err);\n setError(\"Doneren is niet gelukt. Probeer het later opnieuw.\");\n setBusy(false);\n }\n }\n\n return (\n <Card padding={4} className=\"xicms-shop-donation\">\n <form onSubmit={submit} style={{ display: \"grid\", gap: \"1rem\" }}>\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n {block.introText && <Text data-xicms-edit=\"introText\">{block.introText}</Text>}\n\n <div style={{ display: \"flex\", gap: 8, flexWrap: \"wrap\" }} role=\"group\" aria-label=\"Kies een bedrag\">\n {block.amountsCents.map((c) => {\n const active = !custom && amountCents === c;\n return (\n <button\n key={c}\n type=\"button\"\n className=\"xicms-btn\"\n aria-pressed={active}\n onClick={() => {\n setCustom(false);\n setAmountCents(c);\n }}\n style={{\n cursor: \"pointer\",\n border: active ? \"2px solid var(--xicms-primary, #333)\" : \"2px solid rgba(0,0,0,0.15)\",\n background: active ? \"var(--xicms-primary, #333)\" : \"transparent\",\n color: active ? \"var(--xicms-primary-contrast, #fff)\" : \"inherit\",\n borderRadius: 999,\n padding: \"0.5rem 1.1rem\",\n }}\n >\n {formatCents(c, settings.currency)}\n </button>\n );\n })}\n {block.allowCustom && (\n <button\n type=\"button\"\n className=\"xicms-btn\"\n aria-pressed={custom}\n onClick={() => setCustom(true)}\n style={{\n cursor: \"pointer\",\n border: custom ? \"2px solid var(--xicms-primary, #333)\" : \"2px solid rgba(0,0,0,0.15)\",\n background: \"transparent\",\n borderRadius: 999,\n padding: \"0.5rem 1.1rem\",\n }}\n >\n Ander bedrag\n </button>\n )}\n </div>\n\n {custom && (\n <TextInput\n label={`Bedrag in euro's (min. ${formatCents(block.customMinCents, settings.currency)})`}\n isRequired\n placeholder=\"10,00\"\n value={customEuro}\n onChange={setCustomEuro}\n />\n )}\n\n <div style={{ display: \"grid\", gap: \"0.9rem\", gridTemplateColumns: \"repeat(auto-fit, minmax(min(220px, 100%), 1fr))\" }}>\n <TextInput label=\"Naam\" isRequired value={v.name} onChange={(x) => setV((p) => ({ ...p, name: x }))} />\n <TextInput label=\"E-mail\" type=\"email\" isRequired value={v.email} onChange={(x) => setV((p) => ({ ...p, email: x }))} />\n </div>\n\n {/* Honeypot */}\n <input\n type=\"text\"\n tabIndex={-1}\n autoComplete=\"off\"\n value={hp}\n onChange={(e) => setHp(e.target.value)}\n style={{ position: \"absolute\", left: \"-9999px\", width: 1, height: 1 }}\n aria-hidden\n />\n\n {error && <Text role=\"alert\" style={{ color: \"var(--color-error, #c0392b)\" }}>{error}</Text>}\n\n <button\n type=\"submit\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={busy || !valid}\n style={{ cursor: busy ? \"wait\" : \"pointer\", width: \"fit-content\", opacity: valid ? 1 : 0.5 }}\n >\n {busy ? \"Bezig…\" : `${block.buttonLabel} ${valid ? formatCents(effectiveCents, settings.currency) : \"\"}`}\n </button>\n </form>\n </Card>\n );\n}\n\nexport const donationBlock = defineBlock({\n type: \"donation\",\n schema: DonationBlockSchema,\n Render: DonationRender,\n label: \"Donatie\",\n icon: \"💛\",\n defaults: () => ({\n heading: \"Steun ons\",\n amountsCents: [500, 1000, 2500],\n allowCustom: true,\n customMinCents: 100,\n customMaxCents: 100_000,\n buttonLabel: \"Doneer\",\n }),\n});\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../../src/modules/shop/blocks/Donation.tsx"],"sourcesContent":["\"use client\";\n\nimport { useState, type FormEvent } from \"react\";\nimport type { z } from \"zod\";\nimport { DonationBlockSchema, formatCents } from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { TextInput } from \"@astryxdesign/core/TextInput\";\nimport { createOrderCall, useShopSettings } from \"../lib\";\nimport { useShopStrings } from \"../strings\";\n\ntype Blk = z.infer<typeof DonationBlockSchema>;\n\n/**\n * Donatie — bedragknoppen + vrij bedrag, naam/e-mail, betaling via dezelfde `createOrder`-flow\n * (orderregel kind:\"donation\", geen verzendadres/verzendkosten). Het gekozen bedrag is de enige\n * client-invoer die de server accepteert — binnen de min/max van dit block.\n */\nfunction DonationRender({ block, siteId, mode }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const [amountCents, setAmountCents] = useState<number | null>(block.amountsCents[0] ?? null);\n const [customEuro, setCustomEuro] = useState(\"\");\n const [custom, setCustom] = useState(false);\n const [v, setV] = useState({ name: \"\", email: \"\" });\n const [hp, setHp] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const effectiveCents = custom\n ? Math.round((Number(customEuro.replace(\",\", \".\")) || 0) * 100)\n : amountCents ?? 0;\n const valid =\n effectiveCents >= (custom ? block.customMinCents : 50) &&\n effectiveCents <= block.customMaxCents;\n\n async function submit(e: FormEvent) {\n e.preventDefault();\n if (mode === \"preview\" || busy || !valid) return;\n setBusy(true);\n setError(null);\n try {\n const { url } = await createOrderCall({\n siteId,\n items: [{ qty: 1, donationCents: effectiveCents }],\n customer: { email: v.email, name: v.name },\n successPath: settings.successPath,\n cancelPath: typeof window !== \"undefined\" ? window.location.pathname : \"/\",\n _hp: hp || undefined,\n });\n window.location.assign(url);\n } catch (err) {\n console.error(\"[xicms] donatie mislukt:\", err);\n setError(t(\"donationFailed\"));\n setBusy(false);\n }\n }\n\n return (\n <Card padding={4} className=\"xicms-shop-donation\">\n <form onSubmit={submit} style={{ display: \"grid\", gap: \"1rem\" }}>\n {block.heading && <Heading level={2} data-xicms-edit=\"heading\">{block.heading}</Heading>}\n {block.introText && <Text data-xicms-edit=\"introText\">{block.introText}</Text>}\n\n <div style={{ display: \"flex\", gap: 8, flexWrap: \"wrap\" }} role=\"group\" aria-label=\"Kies een bedrag\">\n {block.amountsCents.map((c) => {\n const active = !custom && amountCents === c;\n return (\n <button\n key={c}\n type=\"button\"\n className=\"xicms-btn\"\n aria-pressed={active}\n onClick={() => {\n setCustom(false);\n setAmountCents(c);\n }}\n style={{\n cursor: \"pointer\",\n border: active ? \"2px solid var(--xicms-primary, #333)\" : \"2px solid rgba(0,0,0,0.15)\",\n background: active ? \"var(--xicms-primary, #333)\" : \"transparent\",\n color: active ? \"var(--xicms-primary-contrast, #fff)\" : \"inherit\",\n borderRadius: 999,\n padding: \"0.5rem 1.1rem\",\n }}\n >\n {formatCents(c, settings.currency)}\n </button>\n );\n })}\n {block.allowCustom && (\n <button\n type=\"button\"\n className=\"xicms-btn\"\n aria-pressed={custom}\n onClick={() => setCustom(true)}\n style={{\n cursor: \"pointer\",\n border: custom ? \"2px solid var(--xicms-primary, #333)\" : \"2px solid rgba(0,0,0,0.15)\",\n background: \"transparent\",\n borderRadius: 999,\n padding: \"0.5rem 1.1rem\",\n }}\n >\n {t(\"donationCustomAmount\")}\n </button>\n )}\n </div>\n\n {custom && (\n <TextInput\n label={`${t(\"donationAmountLabel\", { min: formatCents(block.customMinCents, settings.currency) })} *`}\n placeholder=\"10,00\"\n value={customEuro}\n onChange={setCustomEuro}\n />\n )}\n\n <div style={{ display: \"grid\", gap: \"0.9rem\", gridTemplateColumns: \"repeat(auto-fit, minmax(min(220px, 100%), 1fr))\" }}>\n <TextInput label={`${t(\"fieldName\")} *`} value={v.name} onChange={(x) => setV((p) => ({ ...p, name: x }))} />\n <TextInput label={`${t(\"fieldEmail\")} *`} type=\"email\" value={v.email} onChange={(x) => setV((p) => ({ ...p, email: x }))} />\n </div>\n\n {/* Honeypot */}\n <input\n type=\"text\"\n tabIndex={-1}\n autoComplete=\"off\"\n value={hp}\n onChange={(e) => setHp(e.target.value)}\n style={{ position: \"absolute\", left: \"-9999px\", width: 1, height: 1 }}\n aria-hidden\n />\n\n {error && <Text role=\"alert\" style={{ color: \"var(--color-error, #c0392b)\" }}>{error}</Text>}\n\n <button\n type=\"submit\"\n className=\"xicms-btn xicms-btn--primary\"\n disabled={busy || !valid}\n style={{ cursor: busy ? \"wait\" : \"pointer\", width: \"fit-content\", opacity: valid ? 1 : 0.5 }}\n >\n {busy ? t(\"busy\") : `${block.buttonLabel} ${valid ? formatCents(effectiveCents, settings.currency) : \"\"}`}\n </button>\n </form>\n </Card>\n );\n}\n\nexport const donationBlock = defineBlock({\n type: \"donation\",\n schema: DonationBlockSchema,\n Render: DonationRender,\n label: \"Donatie\",\n icon: \"💛\",\n defaults: () => ({\n heading: \"Steun ons\",\n amountsCents: [500, 1000, 2500],\n allowCustom: true,\n customMinCents: 100,\n customMaxCents: 100_000,\n buttonLabel: \"Doneer\",\n }),\n});\n"],"mappings":"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","names":[]}
@@ -1,20 +1,119 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useMemo, useState } from "react";
4
- import { ProductDetailBlockSchema, formatCents, productPriceCents, variantKeyFor } from "../../../schema/shop";
4
+ import {
5
+ ProductDetailBlockSchema,
6
+ formatCents,
7
+ productPriceCents,
8
+ variantKeyFor
9
+ } from "../../../schema/shop";
5
10
  import { defineBlock } from "../../../blocks/registry";
11
+ import { useBlockData } from "../../../blocks/context";
6
12
  import { Card, Heading, Text } from "../../../blocks/ui";
7
13
  import { Selector } from "@astryxdesign/core/Selector";
8
14
  import { cartAdd } from "../cartStore";
9
- import { fetchProductBySlug, readQueryParam, useShopSettings, variantLabelFor } from "../lib";
15
+ import { useShopStrings } from "../strings";
16
+ import {
17
+ fetchActiveProducts,
18
+ fetchProductBySlug,
19
+ kleurOptie,
20
+ readQueryParam,
21
+ uploadPersonalizationCall,
22
+ useShopSettings,
23
+ variantLabelFor
24
+ } from "../lib";
25
+ import { ProductCard } from "./ProductGrid";
26
+ function PersonalizationInputs({
27
+ fields,
28
+ siteId,
29
+ productId,
30
+ value,
31
+ currency,
32
+ onChange
33
+ }) {
34
+ const t = useShopStrings();
35
+ const [uploading, setUploading] = useState(null);
36
+ const [error, setError] = useState(null);
37
+ return /* @__PURE__ */ jsxs("div", { className: "xicms-shop-personalization", style: { display: "grid", gap: "0.75rem" }, children: [
38
+ fields.map((f) => {
39
+ const v = value[f.key] ?? {};
40
+ const label = /* @__PURE__ */ jsxs("span", { children: [
41
+ f.label,
42
+ f.required ? " *" : "",
43
+ f.priceCents > 0 && /* @__PURE__ */ jsx("span", { style: { opacity: 0.7, marginLeft: 6, fontSize: "0.9em" }, children: t("personalizationSurcharge", { bedrag: formatCents(f.priceCents, currency) }) })
44
+ ] });
45
+ if (f.type === "text") {
46
+ return /* @__PURE__ */ jsxs("label", { style: { display: "grid", gap: 4 }, children: [
47
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "0.9rem" }, children: label }),
48
+ /* @__PURE__ */ jsx(
49
+ "input",
50
+ {
51
+ className: "xicms-input",
52
+ type: "text",
53
+ maxLength: f.maxLength,
54
+ value: v.text ?? "",
55
+ onChange: (e) => onChange({ ...value, [f.key]: { text: e.target.value } })
56
+ }
57
+ ),
58
+ /* @__PURE__ */ jsxs("span", { style: { fontSize: "0.75rem", opacity: 0.6 }, children: [
59
+ f.helpText ? `${f.helpText} \xB7 ` : "",
60
+ (v.text ?? "").length,
61
+ "/",
62
+ f.maxLength
63
+ ] })
64
+ ] }, f.key);
65
+ }
66
+ return /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 4 }, children: [
67
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "0.9rem" }, children: label }),
68
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }, children: [
69
+ /* @__PURE__ */ jsxs("label", { className: "xicms-btn xicms-btn--secondary", style: { cursor: "pointer" }, children: [
70
+ uploading === f.key ? t("personalizationUploading") : v.imagePath ? t("personalizationUploaded") : t("personalizationUpload"),
71
+ /* @__PURE__ */ jsx(
72
+ "input",
73
+ {
74
+ type: "file",
75
+ accept: "image/jpeg,image/png,image/webp",
76
+ style: { display: "none" },
77
+ onChange: async (e) => {
78
+ const file = e.target.files?.[0];
79
+ if (!file) return;
80
+ setError(null);
81
+ setUploading(f.key);
82
+ try {
83
+ const { path } = await uploadPersonalizationCall(siteId, productId, file);
84
+ onChange({ ...value, [f.key]: { imagePath: path, fileName: file.name } });
85
+ } catch (err) {
86
+ setError(err instanceof Error && err.message === "TOO_BIG" ? t("personalizationTooBig") : t("personalizationFailed"));
87
+ } finally {
88
+ setUploading(null);
89
+ }
90
+ }
91
+ }
92
+ )
93
+ ] }),
94
+ v.fileName && /* @__PURE__ */ jsx("span", { style: { fontSize: "0.85rem", opacity: 0.75 }, children: v.fileName })
95
+ ] }),
96
+ f.helpText && /* @__PURE__ */ jsx("span", { style: { fontSize: "0.75rem", opacity: 0.6 }, children: f.helpText })
97
+ ] }, f.key);
98
+ }),
99
+ error && /* @__PURE__ */ jsx(Text, { role: "alert", style: { color: "var(--color-error, #c0392b)" }, children: error })
100
+ ] });
101
+ }
10
102
  function ProductDetailRender({ block, siteId }) {
11
103
  const settings = useShopSettings();
12
- const [product, setProduct] = useState(void 0);
104
+ const t = useShopStrings();
105
+ const serverData = useBlockData("shop");
106
+ const [product, setProduct] = useState(
107
+ serverData && "product" in serverData ? serverData.product : void 0
108
+ );
109
+ const [related, setRelated] = useState([]);
13
110
  const [choices, setChoices] = useState({});
111
+ const [personalization, setPersonalization] = useState({});
14
112
  const [qty, setQty] = useState(1);
15
- const [imageIdx, setImageIdx] = useState(0);
113
+ const [manualImage, setManualImage] = useState(null);
16
114
  const [added, setAdded] = useState(false);
17
115
  useEffect(() => {
116
+ if (serverData && "product" in serverData) return;
18
117
  const slug = block.slug || readQueryParam("p") || "";
19
118
  if (!slug) {
20
119
  setProduct(null);
@@ -25,27 +124,68 @@ function ProductDetailRender({ block, siteId }) {
25
124
  setProduct(null);
26
125
  });
27
126
  }, [siteId, block.slug]);
127
+ useEffect(() => {
128
+ if (!block.showRelated || !product) return;
129
+ const pick = (all) => setRelated(
130
+ all.filter((x) => x.id !== product.id && x.categoryIds?.some((c) => product.categoryIds?.includes(c))).slice(0, 4)
131
+ );
132
+ if (serverData?.products) pick(serverData.products);
133
+ else void fetchActiveProducts(siteId).then(pick).catch(() => setRelated([]));
134
+ }, [product?.id, block.showRelated, siteId]);
28
135
  const options = product?.options ?? [];
136
+ const persFields = product?.personalization ?? [];
29
137
  const allChosen = options.every((_, i) => Boolean(choices[i]));
30
138
  const variantKey = product && options.length > 0 && allChosen ? variantKeyFor(options.map((_, i) => choices[i])) : void 0;
31
139
  const variant = variantKey ? product?.variants?.find((v) => v.key === variantKey) : void 0;
140
+ const persSurcharge = useMemo(
141
+ () => persFields.reduce((sum, f) => {
142
+ const v = personalization[f.key];
143
+ return v && (v.text || v.imagePath) ? sum + f.priceCents : sum;
144
+ }, 0),
145
+ [persFields, personalization]
146
+ );
32
147
  const priceCents = useMemo(() => {
33
148
  if (!product) return 0;
34
- return productPriceCents(product, variantKey);
35
- }, [product, variantKey]);
36
- const outOfStock = Boolean(
37
- product?.trackStock && (options.length === 0 ? product.stock !== void 0 && product.stock <= 0 : variant?.stock !== void 0 && variant.stock <= 0)
38
- );
149
+ return productPriceCents(product, variantKey) + persSurcharge;
150
+ }, [product, variantKey, persSurcharge]);
151
+ const kleur = product ? kleurOptie(product) : null;
152
+ const kleurIdx = kleur && product ? (product.options ?? []).findIndex((o) => o === kleur) : -1;
153
+ const gekozenKleur = kleurIdx >= 0 ? choices[kleurIdx] : void 0;
154
+ const kleurImage = gekozenKleur ? kleur?.images?.[gekozenKleur] : void 0;
155
+ const mainImage = manualImage ?? kleurImage ?? product?.images?.[0];
156
+ const gallery = useMemo(() => {
157
+ if (!product) return [];
158
+ const out = (product.images ?? []).map((url) => ({ url }));
159
+ for (const [value, url] of Object.entries(kleur?.images ?? {})) {
160
+ if (!out.some((g) => g.url === url)) out.push({ url, label: value });
161
+ }
162
+ return out;
163
+ }, [product, kleur]);
164
+ const stockValue = options.length === 0 ? product?.stock : variant?.stock;
165
+ const outOfStock = Boolean(product?.trackStock && stockValue !== void 0 && stockValue <= 0);
166
+ const lowStock = Boolean(product?.trackStock && stockValue !== void 0 && stockValue > 0 && stockValue <= 5);
39
167
  const variantInactive = variant !== void 0 && variant.active === false;
40
168
  if (product === void 0) return null;
41
169
  if (product === null) {
42
170
  return /* @__PURE__ */ jsx(Card, { padding: 4, variant: "muted", children: /* @__PURE__ */ jsxs(Text, { children: [
43
- "Product niet gevonden. ",
44
- /* @__PURE__ */ jsx("a", { href: settings.shopPath, children: "Terug naar de winkel" })
171
+ t("productNotFound"),
172
+ " ",
173
+ /* @__PURE__ */ jsx("a", { href: settings.shopPath, children: t("backToShop") })
45
174
  ] }) });
46
175
  }
176
+ const persComplete = persFields.every((f) => {
177
+ if (!f.required) return true;
178
+ const v = personalization[f.key];
179
+ return Boolean(v && (v.text?.trim() || v.imagePath));
180
+ });
47
181
  function addToCart() {
48
182
  if (!product) return;
183
+ const filled = {};
184
+ for (const f of persFields) {
185
+ const v = personalization[f.key];
186
+ if (v && (v.text?.trim() || v.imagePath)) filled[f.key] = v;
187
+ }
188
+ const persLabel = persFields.filter((f) => filled[f.key]).map((f) => `${f.label}: ${filled[f.key].text ?? filled[f.key].fileName ?? "\u2713"}`).join(" \xB7 ");
49
189
  cartAdd(
50
190
  siteId,
51
191
  {
@@ -55,107 +195,161 @@ function ProductDetailRender({ block, siteId }) {
55
195
  variantKey,
56
196
  variantLabel: variantKey ? variantLabelFor(options, variantKey) : void 0,
57
197
  unitPriceCents: priceCents,
58
- imageUrl: product.images?.[0]
198
+ imageUrl: mainImage ?? product.images?.[0],
199
+ personalization: Object.keys(filled).length > 0 ? filled : void 0,
200
+ personalizationLabel: persLabel || void 0
59
201
  },
60
202
  qty
61
203
  );
62
204
  setAdded(true);
63
205
  }
64
- const canOrder = (options.length === 0 || allChosen) && !outOfStock && !variantInactive;
65
- return /* @__PURE__ */ jsxs(
66
- "div",
67
- {
68
- className: "xicms-shop-detail",
69
- style: { display: "grid", gap: "2rem", gridTemplateColumns: "repeat(auto-fit, minmax(min(320px, 100%), 1fr))" },
70
- children: [
71
- /* @__PURE__ */ jsxs("div", { className: "xicms-shop-detail__media", children: [
72
- product.images?.[imageIdx] ? /* @__PURE__ */ jsx(
73
- "img",
74
- {
75
- src: product.images[imageIdx],
76
- alt: product.name,
77
- style: { width: "100%", aspectRatio: "1/1", objectFit: "cover", borderRadius: "var(--xicms-radius, 12px)" }
78
- }
79
- ) : /* @__PURE__ */ jsx("div", { style: { width: "100%", aspectRatio: "1/1", display: "grid", placeItems: "center", background: "var(--xicms-muted, #f1f1f1)", borderRadius: "var(--xicms-radius, 12px)" }, children: /* @__PURE__ */ jsx("span", { style: { fontSize: "3rem", opacity: 0.4 }, children: "\u{1F5BC}\uFE0F" }) }),
80
- (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
- "button",
82
- {
83
- type: "button",
84
- onClick: () => setImageIdx(i),
85
- style: {
86
- padding: 0,
87
- border: i === imageIdx ? "2px solid var(--xicms-primary, #333)" : "2px solid transparent",
88
- borderRadius: 8,
89
- cursor: "pointer",
90
- background: "none"
206
+ const canOrder = (options.length === 0 || allChosen) && !outOfStock && !variantInactive && persComplete;
207
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
208
+ /* @__PURE__ */ jsxs(
209
+ "div",
210
+ {
211
+ className: "xicms-shop-detail",
212
+ style: { display: "grid", gap: "2rem", gridTemplateColumns: "repeat(auto-fit, minmax(min(320px, 100%), 1fr))" },
213
+ children: [
214
+ /* @__PURE__ */ jsxs("div", { className: "xicms-shop-detail__media", children: [
215
+ mainImage ? /* @__PURE__ */ jsx(
216
+ "img",
217
+ {
218
+ src: mainImage,
219
+ alt: product.name,
220
+ style: { width: "100%", aspectRatio: "1/1", objectFit: "cover", borderRadius: "var(--xicms-radius, 12px)" }
221
+ }
222
+ ) : /* @__PURE__ */ jsx(
223
+ "div",
224
+ {
225
+ className: "xicms-shop-detail__placeholder",
226
+ "aria-hidden": "true",
227
+ style: { width: "100%", aspectRatio: "1/1", background: "var(--xicms-muted, #f1f1f1)", borderRadius: "var(--xicms-radius, 12px)" }
228
+ }
229
+ ),
230
+ gallery.length > 1 && /* @__PURE__ */ jsx("div", { style: { display: "flex", gap: 8, marginTop: 8, flexWrap: "wrap" }, children: gallery.map((g) => /* @__PURE__ */ jsx(
231
+ "button",
232
+ {
233
+ type: "button",
234
+ title: g.label,
235
+ onClick: () => setManualImage(g.url),
236
+ style: {
237
+ padding: 0,
238
+ border: g.url === mainImage ? "2px solid var(--xicms-primary, #333)" : "2px solid transparent",
239
+ borderRadius: 8,
240
+ cursor: "pointer",
241
+ background: "none"
242
+ },
243
+ children: /* @__PURE__ */ jsx("img", { src: g.url, alt: g.label ?? "", style: { width: 64, height: 64, objectFit: "cover", borderRadius: 6, display: "block" } })
91
244
  },
92
- children: /* @__PURE__ */ jsx("img", { src: url, alt: "", style: { width: 64, height: 64, objectFit: "cover", borderRadius: 6, display: "block" } })
93
- },
94
- url + i
95
- )) })
96
- ] }),
97
- /* @__PURE__ */ jsxs("div", { className: "xicms-shop-detail__info", children: [
98
- /* @__PURE__ */ jsx(Heading, { level: 1, children: product.name }),
99
- /* @__PURE__ */ jsxs("p", { className: "xicms-shop-detail__price", style: { fontSize: "1.35rem", fontWeight: 700, margin: "0.5rem 0 1rem" }, children: [
100
- product.compareAtCents !== void 0 && product.compareAtCents > priceCents && /* @__PURE__ */ jsx("s", { style: { opacity: 0.6, marginRight: 8, fontWeight: "normal" }, children: formatCents(product.compareAtCents, settings.currency) }),
101
- formatCents(priceCents, settings.currency)
245
+ g.url
246
+ )) })
102
247
  ] }),
103
- product.description && // Platte tekst met behoud van regeleinden — bewust geen HTML (zie schema/shop.ts).
104
- /* @__PURE__ */ jsx("p", { className: "xicms-shop-detail__desc", style: { whiteSpace: "pre-wrap", margin: 0 }, children: product.description }),
105
- /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: "0.75rem", maxWidth: 420, marginTop: "1rem" }, children: [
106
- options.map((opt, i) => /* @__PURE__ */ jsx(
107
- Selector,
108
- {
109
- label: opt.name,
110
- placeholder: "Kies\u2026",
111
- options: opt.values,
112
- value: choices[i] ?? "",
113
- onChange: (v) => {
114
- setChoices((prev) => ({ ...prev, [i]: v ?? "" }));
115
- setAdded(false);
248
+ /* @__PURE__ */ jsxs("div", { className: "xicms-shop-detail__info", children: [
249
+ /* @__PURE__ */ jsx(Heading, { level: 1, children: product.name }),
250
+ /* @__PURE__ */ jsxs("p", { className: "xicms-shop-detail__price", style: { fontSize: "1.35rem", fontWeight: 700, margin: "0.5rem 0 1rem" }, children: [
251
+ product.compareAtCents !== void 0 && product.compareAtCents > priceCents && /* @__PURE__ */ jsx("s", { style: { opacity: 0.6, marginRight: 8, fontWeight: "normal" }, children: formatCents(product.compareAtCents, settings.currency) }),
252
+ formatCents(priceCents, settings.currency)
253
+ ] }),
254
+ product.description && /* @__PURE__ */ jsx("p", { className: "xicms-shop-detail__desc", style: { whiteSpace: "pre-wrap", margin: 0 }, children: product.description }),
255
+ /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: "0.75rem", maxWidth: 420, marginTop: "1rem" }, children: [
256
+ options.map((opt, i) => /* @__PURE__ */ jsx(
257
+ Selector,
258
+ {
259
+ label: opt.name,
260
+ placeholder: t("choose"),
261
+ options: opt.values,
262
+ value: choices[i] ?? "",
263
+ onChange: (v) => {
264
+ setChoices((prev) => ({ ...prev, [i]: v ?? "" }));
265
+ setManualImage(null);
266
+ setAdded(false);
267
+ }
268
+ },
269
+ opt.name
270
+ )),
271
+ persFields.length > 0 && /* @__PURE__ */ jsx(
272
+ PersonalizationInputs,
273
+ {
274
+ fields: persFields,
275
+ siteId,
276
+ productId: product.id,
277
+ value: personalization,
278
+ currency: settings.currency,
279
+ onChange: (v) => {
280
+ setPersonalization(v);
281
+ setAdded(false);
282
+ }
116
283
  }
117
- },
118
- opt.name
119
- )),
120
- /* @__PURE__ */ jsxs("label", { className: "xicms-shop-detail__qty", style: { display: "grid", gap: 4 }, children: [
121
- /* @__PURE__ */ jsx("span", { style: { fontSize: "0.9rem" }, children: "Aantal" }),
122
- /* @__PURE__ */ jsx(
123
- "input",
284
+ ),
285
+ /* @__PURE__ */ jsxs("label", { className: "xicms-shop-detail__qty", style: { display: "grid", gap: 4 }, children: [
286
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "0.9rem" }, children: t("qty") }),
287
+ /* @__PURE__ */ jsx(
288
+ "input",
289
+ {
290
+ className: "xicms-input",
291
+ type: "number",
292
+ min: 1,
293
+ max: 999,
294
+ value: qty,
295
+ onChange: (e) => setQty(Math.max(1, Math.min(999, Number(e.target.value) || 1))),
296
+ style: { width: 90 }
297
+ }
298
+ )
299
+ ] }),
300
+ outOfStock && /* @__PURE__ */ jsx(Text, { style: { color: "var(--color-error, #c0392b)" }, children: t("outOfStock") }),
301
+ variantInactive && /* @__PURE__ */ jsx(Text, { style: { color: "var(--color-error, #c0392b)" }, children: t("variantUnavailable") }),
302
+ lowStock && !outOfStock && /* @__PURE__ */ jsx(Text, { style: { color: "var(--xicms-primary, #4d7fb0)", fontWeight: 600 }, children: t("stockLeft", { n: stockValue }) }),
303
+ added ? /* @__PURE__ */ jsxs("div", { role: "status", style: { display: "grid", gap: 8 }, children: [
304
+ /* @__PURE__ */ jsx(Text, { children: t("added") }),
305
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 12, flexWrap: "wrap" }, children: [
306
+ /* @__PURE__ */ jsx("a", { className: "xicms-btn xicms-btn--primary", href: settings.cartPath, children: t("toCart") }),
307
+ /* @__PURE__ */ jsx("a", { className: "xicms-btn xicms-btn--secondary", href: settings.shopPath, children: t("continueShopping") })
308
+ ] })
309
+ ] }) : /* @__PURE__ */ jsx(
310
+ "button",
124
311
  {
125
- className: "xicms-input",
126
- type: "number",
127
- min: 1,
128
- max: 999,
129
- value: qty,
130
- onChange: (e) => setQty(Math.max(1, Math.min(999, Number(e.target.value) || 1))),
131
- style: { width: 90 }
312
+ type: "button",
313
+ className: "xicms-btn xicms-btn--primary",
314
+ disabled: !canOrder,
315
+ onClick: addToCart,
316
+ style: { cursor: canOrder ? "pointer" : "not-allowed", opacity: canOrder ? 1 : 0.5, width: "fit-content" },
317
+ children: t("addToCart")
132
318
  }
133
319
  )
134
- ] }),
135
- outOfStock && /* @__PURE__ */ jsx(Text, { style: { color: "var(--color-error, #c0392b)" }, children: "Uitverkocht in deze uitvoering." }),
136
- variantInactive && /* @__PURE__ */ jsx(Text, { style: { color: "var(--color-error, #c0392b)" }, children: "Deze combinatie is niet leverbaar." }),
137
- added ? /* @__PURE__ */ jsxs("div", { role: "status", style: { display: "grid", gap: 8 }, children: [
138
- /* @__PURE__ */ jsx(Text, { children: "\u2713 Toegevoegd aan je winkelwagen." }),
139
- /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 12, flexWrap: "wrap" }, children: [
140
- /* @__PURE__ */ jsx("a", { className: "xicms-btn xicms-btn--primary", href: settings.cartPath, children: "Naar winkelwagen" }),
141
- /* @__PURE__ */ jsx("a", { className: "xicms-btn xicms-btn--secondary", href: settings.shopPath, children: "Verder winkelen" })
142
- ] })
143
- ] }) : /* @__PURE__ */ jsx(
144
- "button",
145
- {
146
- type: "button",
147
- className: "xicms-btn xicms-btn--primary",
148
- disabled: !canOrder,
149
- onClick: addToCart,
150
- style: { cursor: canOrder ? "pointer" : "not-allowed", opacity: canOrder ? 1 : 0.5, width: "fit-content" },
151
- children: "In winkelwagen"
152
- }
153
- )
320
+ ] })
154
321
  ] })
155
- ] })
156
- ]
157
- }
158
- );
322
+ ]
323
+ }
324
+ ),
325
+ block.showRelated && related.length > 0 && /* @__PURE__ */ jsxs("div", { className: "xicms-shop-related", style: { marginTop: "3rem" }, children: [
326
+ /* @__PURE__ */ jsx(Heading, { level: 2, children: t("related") }),
327
+ /* @__PURE__ */ jsx(
328
+ "div",
329
+ {
330
+ className: "xicms-shop-grid__items",
331
+ style: {
332
+ display: "grid",
333
+ gridTemplateColumns: "repeat(auto-fill, minmax(min(200px, calc(50% - 1rem)), 1fr))",
334
+ gap: "1.25rem",
335
+ marginTop: "1rem"
336
+ },
337
+ children: related.map((p) => /* @__PURE__ */ jsx(
338
+ ProductCard,
339
+ {
340
+ product: p,
341
+ siteId,
342
+ currency: settings.currency,
343
+ detailPath: settings.detailPath,
344
+ cartPath: settings.cartPath,
345
+ t
346
+ },
347
+ p.id
348
+ ))
349
+ }
350
+ )
351
+ ] })
352
+ ] });
159
353
  }
160
354
  const productDetailBlock = defineBlock({
161
355
  type: "productDetail",
@@ -163,7 +357,7 @@ const productDetailBlock = defineBlock({
163
357
  Render: ProductDetailRender,
164
358
  label: "Productdetail",
165
359
  icon: "\u{1F3F7}\uFE0F",
166
- defaults: () => ({})
360
+ defaults: () => ({ showRelated: true })
167
361
  });
168
362
  export {
169
363
  productDetailBlock
@@ -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 {\n ProductDetailBlockSchema,\n formatCents,\n productPriceCents,\n variantKeyFor,\n type PersonalizationField,\n} from \"../../../schema/shop\";\nimport { defineBlock, type BlockRenderProps } from \"../../../blocks/registry\";\nimport { useBlockData } from \"../../../blocks/context\";\nimport { Card, Heading, Text } from \"../../../blocks/ui\";\nimport { Selector } from \"@astryxdesign/core/Selector\";\nimport { cartAdd } from \"../cartStore\";\nimport { useShopStrings } from \"../strings\";\nimport {\n fetchActiveProducts,\n fetchProductBySlug,\n kleurOptie,\n readQueryParam,\n uploadPersonalizationCall,\n useShopSettings,\n variantLabelFor,\n type StoredProduct,\n} from \"../lib\";\nimport type { ShopPageData } from \"../types\";\nimport { ProductCard } from \"./ProductGrid\";\n\ntype Blk = z.infer<typeof ProductDetailBlockSchema>;\ntype PersValue = Record<string, { text?: string; imagePath?: string; fileName?: string }>;\n\n/** Personalisatie-invoer (eigen tekst of afbeelding) onder de optie-selects. */\nfunction PersonalizationInputs({\n fields,\n siteId,\n productId,\n value,\n currency,\n onChange,\n}: {\n fields: PersonalizationField[];\n siteId: string;\n productId: string;\n value: PersValue;\n currency: string;\n onChange: (next: PersValue) => void;\n}) {\n const t = useShopStrings();\n const [uploading, setUploading] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n return (\n <div className=\"xicms-shop-personalization\" style={{ display: \"grid\", gap: \"0.75rem\" }}>\n {fields.map((f) => {\n const v = value[f.key] ?? {};\n const label = (\n <span>\n {f.label}\n {f.required ? \" *\" : \"\"}\n {f.priceCents > 0 && (\n <span style={{ opacity: 0.7, marginLeft: 6, fontSize: \"0.9em\" }}>\n {t(\"personalizationSurcharge\", { bedrag: formatCents(f.priceCents, currency) })}\n </span>\n )}\n </span>\n );\n if (f.type === \"text\") {\n return (\n <label key={f.key} style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{label}</span>\n <input\n className=\"xicms-input\"\n type=\"text\"\n maxLength={f.maxLength}\n value={v.text ?? \"\"}\n onChange={(e) => onChange({ ...value, [f.key]: { text: e.target.value } })}\n />\n <span style={{ fontSize: \"0.75rem\", opacity: 0.6 }}>\n {f.helpText ? `${f.helpText} · ` : \"\"}\n {(v.text ?? \"\").length}/{f.maxLength}\n </span>\n </label>\n );\n }\n return (\n <div key={f.key} style={{ display: \"grid\", gap: 4 }}>\n <span style={{ fontSize: \"0.9rem\" }}>{label}</span>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 10, flexWrap: \"wrap\" }}>\n <label className=\"xicms-btn xicms-btn--secondary\" style={{ cursor: \"pointer\" }}>\n {uploading === f.key\n ? t(\"personalizationUploading\")\n : v.imagePath\n ? t(\"personalizationUploaded\")\n : t(\"personalizationUpload\")}\n <input\n type=\"file\"\n accept=\"image/jpeg,image/png,image/webp\"\n style={{ display: \"none\" }}\n onChange={async (e) => {\n const file = e.target.files?.[0];\n if (!file) return;\n setError(null);\n setUploading(f.key);\n try {\n const { path } = await uploadPersonalizationCall(siteId, productId, file);\n onChange({ ...value, [f.key]: { imagePath: path, fileName: file.name } });\n } catch (err) {\n setError(err instanceof Error && err.message === \"TOO_BIG\" ? t(\"personalizationTooBig\") : t(\"personalizationFailed\"));\n } finally {\n setUploading(null);\n }\n }}\n />\n </label>\n {v.fileName && <span style={{ fontSize: \"0.85rem\", opacity: 0.75 }}>{v.fileName}</span>}\n </div>\n {f.helpText && <span style={{ fontSize: \"0.75rem\", opacity: 0.6 }}>{f.helpText}</span>}\n </div>\n );\n })}\n {error && <Text role=\"alert\" style={{ color: \"var(--color-error, #c0392b)\" }}>{error}</Text>}\n </div>\n );\n}\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 met per-kleur-beeldwissel,\n * personalisatie (eigen naam/logo), voorraad-signalen en \"Ook leuk\"-rij.\n */\nfunction ProductDetailRender({ block, siteId }: BlockRenderProps<Blk>) {\n const settings = useShopSettings();\n const t = useShopStrings();\n const serverData = useBlockData<ShopPageData>(\"shop\");\n const [product, setProduct] = useState<StoredProduct | null | undefined>(\n serverData && \"product\" in serverData ? serverData.product : undefined,\n );\n const [related, setRelated] = useState<StoredProduct[]>([]);\n const [choices, setChoices] = useState<Record<number, string>>({});\n const [personalization, setPersonalization] = useState<PersValue>({});\n const [qty, setQty] = useState(1);\n const [manualImage, setManualImage] = useState<string | null>(null);\n const [added, setAdded] = useState(false);\n\n useEffect(() => {\n if (serverData && \"product\" in serverData) return; // SSR-data aanwezig\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 // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [siteId, block.slug]);\n\n // \"Ook leuk\": producten uit dezelfde categorie (server-set hergebruiken waar mogelijk).\n useEffect(() => {\n if (!block.showRelated || !product) return;\n const pick = (all: StoredProduct[]) =>\n setRelated(\n all\n .filter((x) => x.id !== product.id && x.categoryIds?.some((c) => product.categoryIds?.includes(c)))\n .slice(0, 4),\n );\n if (serverData?.products) pick(serverData.products);\n else void fetchActiveProducts(siteId).then(pick).catch(() => setRelated([]));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [product?.id, block.showRelated, siteId]);\n\n const options = product?.options ?? [];\n const persFields = product?.personalization ?? [];\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 /** Meerprijs van ingevulde personalisatie (per stuk). */\n const persSurcharge = useMemo(\n () =>\n persFields.reduce((sum, f) => {\n const v = personalization[f.key];\n return v && (v.text || v.imagePath) ? sum + f.priceCents : sum;\n }, 0),\n [persFields, personalization],\n );\n\n const priceCents = useMemo(() => {\n if (!product) return 0;\n return productPriceCents(product, variantKey) + persSurcharge;\n }, [product, variantKey, persSurcharge]);\n\n /** Hoofdbeeld: handmatige thumbnail-keuze > per-kleur-afbeelding > eerste productfoto. */\n const kleur = product ? kleurOptie(product) : null;\n const kleurIdx = kleur && product ? (product.options ?? []).findIndex((o) => o === kleur) : -1;\n const gekozenKleur = kleurIdx >= 0 ? choices[kleurIdx] : undefined;\n const kleurImage = gekozenKleur ? kleur?.images?.[gekozenKleur] : undefined;\n const mainImage = manualImage ?? kleurImage ?? product?.images?.[0];\n\n /** Alle toonbare afbeeldingen: productfoto's + unieke per-kleur-afbeeldingen. */\n const gallery = useMemo(() => {\n if (!product) return [] as { url: string; label?: string }[];\n const out: { url: string; label?: string }[] = (product.images ?? []).map((url) => ({ url }));\n for (const [value, url] of Object.entries(kleur?.images ?? {})) {\n if (!out.some((g) => g.url === url)) out.push({ url, label: value });\n }\n return out;\n }, [product, kleur]);\n\n const stockValue = options.length === 0 ? product?.stock : variant?.stock;\n const outOfStock = Boolean(product?.trackStock && stockValue !== undefined && stockValue <= 0);\n const lowStock = Boolean(product?.trackStock && stockValue !== undefined && stockValue > 0 && stockValue <= 5);\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>\n {t(\"productNotFound\")} <a href={settings.shopPath}>{t(\"backToShop\")}</a>\n </Text>\n </Card>\n );\n }\n\n const persComplete = persFields.every((f) => {\n if (!f.required) return true;\n const v = personalization[f.key];\n return Boolean(v && (v.text?.trim() || v.imagePath));\n });\n\n function addToCart() {\n if (!product) return;\n const filled: PersValue = {};\n for (const f of persFields) {\n const v = personalization[f.key];\n if (v && (v.text?.trim() || v.imagePath)) filled[f.key] = v;\n }\n const persLabel = persFields\n .filter((f) => filled[f.key])\n .map((f) => `${f.label}: ${filled[f.key].text ?? filled[f.key].fileName ?? \"✓\"}`)\n .join(\" · \");\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: mainImage ?? product.images?.[0],\n personalization: Object.keys(filled).length > 0 ? filled : undefined,\n personalizationLabel: persLabel || undefined,\n },\n qty,\n );\n setAdded(true);\n }\n\n const canOrder = (options.length === 0 || allChosen) && !outOfStock && !variantInactive && persComplete;\n\n return (\n <>\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 {mainImage ? (\n <img\n src={mainImage}\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 {gallery.length > 1 && (\n <div style={{ display: \"flex\", gap: 8, marginTop: 8, flexWrap: \"wrap\" }}>\n {gallery.map((g) => (\n <button\n key={g.url}\n type=\"button\"\n title={g.label}\n onClick={() => setManualImage(g.url)}\n style={{\n padding: 0,\n border: g.url === mainImage ? \"2px solid var(--xicms-primary, #333)\" : \"2px solid transparent\",\n borderRadius: 8,\n cursor: \"pointer\",\n background: \"none\",\n }}\n >\n <img src={g.url} alt={g.label ?? \"\"} 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 <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 <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={t(\"choose\")}\n options={opt.values}\n value={choices[i] ?? \"\"}\n onChange={(v) => {\n setChoices((prev) => ({ ...prev, [i]: (v as string) ?? \"\" }));\n setManualImage(null);\n setAdded(false);\n }}\n />\n ))}\n\n {persFields.length > 0 && (\n <PersonalizationInputs\n fields={persFields}\n siteId={siteId}\n productId={product.id}\n value={personalization}\n currency={settings.currency}\n onChange={(v) => {\n setPersonalization(v);\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\" }}>{t(\"qty\")}</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)\" }}>{t(\"outOfStock\")}</Text>}\n {variantInactive && <Text style={{ color: \"var(--color-error, #c0392b)\" }}>{t(\"variantUnavailable\")}</Text>}\n {lowStock && !outOfStock && (\n <Text style={{ color: \"var(--xicms-primary, #4d7fb0)\", fontWeight: 600 }}>\n {t(\"stockLeft\", { n: stockValue as number })}\n </Text>\n )}\n\n {added ? (\n <div role=\"status\" style={{ display: \"grid\", gap: 8 }}>\n <Text>{t(\"added\")}</Text>\n <div style={{ display: \"flex\", gap: 12, flexWrap: \"wrap\" }}>\n <a className=\"xicms-btn xicms-btn--primary\" href={settings.cartPath}>{t(\"toCart\")}</a>\n <a className=\"xicms-btn xicms-btn--secondary\" href={settings.shopPath}>{t(\"continueShopping\")}</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 {t(\"addToCart\")}\n </button>\n )}\n </div>\n </div>\n </div>\n\n {block.showRelated && related.length > 0 && (\n <div className=\"xicms-shop-related\" style={{ marginTop: \"3rem\" }}>\n <Heading level={2}>{t(\"related\")}</Heading>\n <div\n className=\"xicms-shop-grid__items\"\n style={{\n display: \"grid\",\n gridTemplateColumns: \"repeat(auto-fill, minmax(min(200px, calc(50% - 1rem)), 1fr))\",\n gap: \"1.25rem\",\n marginTop: \"1rem\",\n }}\n >\n {related.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 </div>\n )}\n </>\n );\n}\n\nexport const productDetailBlock = defineBlock({\n type: \"productDetail\",\n schema: ProductDetailBlockSchema,\n Render: ProductDetailRender,\n label: \"Productdetail\",\n icon: \"🏷️\",\n defaults: () => ({ showRelated: true }),\n});\n"],"mappings":"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","names":[]}