@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.
- 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 -102
- 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 +151 -29
- 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 +24 -1
- package/dist/modules/shop/lib.js +49 -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 +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 {
|
|
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 {
|
|
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
|
|
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 [
|
|
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
|
|
37
|
-
|
|
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
|
-
"
|
|
44
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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":[]}
|