@sazito/checkout 0.1.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/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/chunks/labels-BlMZkOsV.cjs +1646 -0
- package/dist/chunks/labels-BlMZkOsV.cjs.map +1 -0
- package/dist/chunks/labels-ChywPk2i.js +1613 -0
- package/dist/chunks/labels-ChywPk2i.js.map +1 -0
- package/dist/chunks/use-checkout-Cvp_K3UW.cjs +69 -0
- package/dist/chunks/use-checkout-Cvp_K3UW.cjs.map +1 -0
- package/dist/chunks/use-checkout-E0O8HsSs.js +63 -0
- package/dist/chunks/use-checkout-E0O8HsSs.js.map +1 -0
- package/dist/core/index.cjs +40 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +482 -0
- package/dist/core/index.d.ts +482 -0
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -0
- package/dist/next/index.cjs +845 -0
- package/dist/next/index.cjs.map +1 -0
- package/dist/next/index.d.cts +427 -0
- package/dist/next/index.d.ts +427 -0
- package/dist/next/index.js +834 -0
- package/dist/next/index.js.map +1 -0
- package/dist/react/index.cjs +17 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +376 -0
- package/dist/react/index.d.ts +376 -0
- package/dist/react/index.js +7 -0
- package/dist/react/index.js.map +1 -0
- package/dist/styles.css +1696 -0
- package/package.json +98 -0
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
import { isValidElement, cloneElement, useState, useRef, useEffect, useCallback } from 'react';
|
|
4
|
+
import { u as useCheckout, b as useSazitoClient, C as CheckoutProvider } from '../chunks/use-checkout-E0O8HsSs.js';
|
|
5
|
+
export { S as SazitoProvider } from '../chunks/use-checkout-E0O8HsSs.js';
|
|
6
|
+
import { m as formatPercent, F as toPersianDigits, s as isValidIranianMobile, y as normalizeIranianPhone, z as paymentMethodInfo, l as formatNumber } from '../chunks/labels-ChywPk2i.js';
|
|
7
|
+
import '@sazito/client-sdk';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* When `asChild` is true, merges the parent's props onto the single React child
|
|
11
|
+
* element instead of rendering a wrapper element — the same pattern Radix UI /
|
|
12
|
+
* shadcn/ui uses. Class names and event handlers are merged; all other props
|
|
13
|
+
* from the parent win (child props act as defaults).
|
|
14
|
+
*/
|
|
15
|
+
function Slot({ children, ...slotProps }) {
|
|
16
|
+
if (!isValidElement(children)) {
|
|
17
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
18
|
+
console.warn('[Slot] Must receive exactly one valid React element as child.');
|
|
19
|
+
}
|
|
20
|
+
return jsx(Fragment, { children: children });
|
|
21
|
+
}
|
|
22
|
+
const child = children;
|
|
23
|
+
const childProps = child.props;
|
|
24
|
+
const mergedClassName = [slotProps.className, childProps.className].filter(Boolean).join(' ') || undefined;
|
|
25
|
+
// Merge event handlers: call both if both exist.
|
|
26
|
+
const merged = { ...childProps, ...slotProps };
|
|
27
|
+
if (mergedClassName)
|
|
28
|
+
merged.className = mergedClassName;
|
|
29
|
+
for (const key of Object.keys(childProps)) {
|
|
30
|
+
if (key.startsWith('on') && typeof childProps[key] === 'function' && typeof slotProps[key] === 'function') {
|
|
31
|
+
const slotHandler = slotProps[key];
|
|
32
|
+
const childHandler = childProps[key];
|
|
33
|
+
merged[key] = (...args) => {
|
|
34
|
+
slotHandler(...args);
|
|
35
|
+
childHandler(...args);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return cloneElement(child, merged);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function cx(...parts) {
|
|
43
|
+
return parts.filter(Boolean).join(' ');
|
|
44
|
+
}
|
|
45
|
+
function Button({ variant = 'primary', block, loading, asChild, className, children, disabled, ...rest }) {
|
|
46
|
+
const props = {
|
|
47
|
+
className: cx('szc-btn', `szc-btn--${variant}`, block && 'szc-btn--block', className),
|
|
48
|
+
disabled: disabled || loading,
|
|
49
|
+
'aria-busy': loading || undefined,
|
|
50
|
+
...rest,
|
|
51
|
+
};
|
|
52
|
+
if (asChild) {
|
|
53
|
+
return jsx(Slot, { ...props, children: children });
|
|
54
|
+
}
|
|
55
|
+
return (jsxs("button", { type: "button", ...props, children: [loading ? jsx(Spinner, {}) : null, jsx("span", { children: children })] }));
|
|
56
|
+
}
|
|
57
|
+
function Spinner({ asChild, className, children } = {}) {
|
|
58
|
+
if (asChild && children) {
|
|
59
|
+
return jsx(Slot, { className: cx('szc-spinner', className), children: children });
|
|
60
|
+
}
|
|
61
|
+
return jsx("span", { className: cx('szc-spinner', className), "aria-hidden": "true", children: children });
|
|
62
|
+
}
|
|
63
|
+
function ProductPlaceholder({ asChild, className, children } = {}) {
|
|
64
|
+
const props = { className: cx('szc-product-placeholder', className) };
|
|
65
|
+
if (asChild && children) {
|
|
66
|
+
return jsx(Slot, { ...props, children: children });
|
|
67
|
+
}
|
|
68
|
+
return (jsx("span", { ...props, children: children ?? (jsxs("svg", { viewBox: "0 0 64 64", width: "30", height: "30", fill: "none", "aria-hidden": "true", children: [jsx("path", { d: "M32 12 49 21.5v21L32 52 15 42.5v-21L32 12Z", stroke: "currentColor", strokeWidth: "2.25", strokeLinejoin: "round" }), jsx("path", { d: "m15 21.5 17 9.5 17-9.5M32 31v21", stroke: "currentColor", strokeWidth: "2.25", strokeLinecap: "round", strokeLinejoin: "round" })] })) }));
|
|
69
|
+
}
|
|
70
|
+
function FieldLabel({ label, required, optionalLabel }) {
|
|
71
|
+
return (jsxs("span", { className: "szc-field__label", children: [label, !required && optionalLabel ? (jsxs("span", { className: "szc-field__optional", children: ["(", optionalLabel, ")"] })) : null] }));
|
|
72
|
+
}
|
|
73
|
+
function Field({ label, id, className, error, required, optionalLabel, asChild, children, ...rest }) {
|
|
74
|
+
const inputProps = {
|
|
75
|
+
id,
|
|
76
|
+
required,
|
|
77
|
+
className: cx('szc-input', error && 'szc-input--error', className),
|
|
78
|
+
...rest,
|
|
79
|
+
};
|
|
80
|
+
return (jsxs("label", { className: "szc-field", htmlFor: id, children: [jsx(FieldLabel, { label: label, required: required, optionalLabel: optionalLabel }), asChild && children
|
|
81
|
+
? jsx(Slot, { ...inputProps, children: children })
|
|
82
|
+
: jsx("input", { ...inputProps }), error ? jsx("span", { className: "szc-field__error", children: error }) : null] }));
|
|
83
|
+
}
|
|
84
|
+
function SectionTitle({ children, asChild, className }) {
|
|
85
|
+
const props = { className: cx('szc-section-title', className) };
|
|
86
|
+
if (asChild && children) {
|
|
87
|
+
return jsx(Slot, { ...props, children: children });
|
|
88
|
+
}
|
|
89
|
+
return jsx("h3", { ...props, children: children });
|
|
90
|
+
}
|
|
91
|
+
function ErrorBanner({ message, asChild, children, className }) {
|
|
92
|
+
const props = {
|
|
93
|
+
className: cx('szc-error', className),
|
|
94
|
+
role: 'alert',
|
|
95
|
+
};
|
|
96
|
+
if (asChild && children) {
|
|
97
|
+
return jsx(Slot, { ...props, children: children });
|
|
98
|
+
}
|
|
99
|
+
return jsx("div", { ...props, children: message });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const STEPS = ['cart', 'shipping', 'payment', 'result'];
|
|
103
|
+
const STEP_ICONS = {
|
|
104
|
+
cart: CartIcon,
|
|
105
|
+
shipping: TruckIcon$1,
|
|
106
|
+
payment: CardIcon,
|
|
107
|
+
result: ClipboardIcon
|
|
108
|
+
};
|
|
109
|
+
function Stepper() {
|
|
110
|
+
const { state, t } = useCheckout();
|
|
111
|
+
const labels = {
|
|
112
|
+
cart: t.stepCart,
|
|
113
|
+
shipping: t.stepShipping,
|
|
114
|
+
payment: t.stepPayment,
|
|
115
|
+
result: t.stepResult
|
|
116
|
+
};
|
|
117
|
+
const currentIndex = STEPS.indexOf(state.step);
|
|
118
|
+
return (jsx("ol", { className: "szc-stepper", children: STEPS.map((step, index) => {
|
|
119
|
+
const status = index < currentIndex ? 'done' : index === currentIndex ? 'active' : 'todo';
|
|
120
|
+
const Icon = STEP_ICONS[step];
|
|
121
|
+
return (jsxs("li", { className: `szc-step szc-step--${status}`, children: [jsx("span", { className: "szc-step__dot", children: status === 'done' ? jsx(CheckIcon$2, {}) : jsx(Icon, {}) }), jsx("span", { className: "szc-step__label", children: labels[step] }), index < STEPS.length - 1 ? jsx("span", { className: "szc-step__line" }) : null] }, step));
|
|
122
|
+
}) }));
|
|
123
|
+
}
|
|
124
|
+
function CheckIcon$2() {
|
|
125
|
+
return (jsx("svg", { viewBox: "0 0 16 16", width: "15", height: "15", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "M3.5 8.5l3 3 6-6.5", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
126
|
+
}
|
|
127
|
+
function CartIcon() {
|
|
128
|
+
return (jsxs("svg", { viewBox: "0 0 16 16", width: "15", height: "15", fill: "none", "aria-hidden": "true", children: [jsx("path", { d: "M1.5 2h1.8l1.9 7.5h6.3l1.5-5H4.8", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round" }), jsx("circle", { cx: "6.5", cy: "13", r: "1", fill: "currentColor" }), jsx("circle", { cx: "11", cy: "13", r: "1", fill: "currentColor" })] }));
|
|
129
|
+
}
|
|
130
|
+
function TruckIcon$1() {
|
|
131
|
+
return (jsxs("svg", { viewBox: "0 0 16 16", width: "15", height: "15", fill: "none", "aria-hidden": "true", children: [jsx("rect", { x: "1", y: "4", width: "9", height: "7", rx: "1", stroke: "currentColor", strokeWidth: "1.6" }), jsx("path", { d: "M10 6.5h2.5l2 2.5V11h-4.5V6.5z", stroke: "currentColor", strokeWidth: "1.6", strokeLinejoin: "round" }), jsx("circle", { cx: "4", cy: "12.5", r: "1.2", stroke: "currentColor", strokeWidth: "1.3" }), jsx("circle", { cx: "12", cy: "12.5", r: "1.2", stroke: "currentColor", strokeWidth: "1.3" })] }));
|
|
132
|
+
}
|
|
133
|
+
function CardIcon() {
|
|
134
|
+
return (jsxs("svg", { viewBox: "0 0 16 16", width: "15", height: "15", fill: "none", "aria-hidden": "true", children: [jsx("rect", { x: "1.5", y: "3.5", width: "13", height: "9", rx: "1.5", stroke: "currentColor", strokeWidth: "1.6" }), jsx("path", { d: "M1.5 6.5h13", stroke: "currentColor", strokeWidth: "1.6" }), jsx("rect", { x: "3", y: "8.5", width: "3", height: "1.5", rx: ".5", fill: "currentColor" })] }));
|
|
135
|
+
}
|
|
136
|
+
function ClipboardIcon() {
|
|
137
|
+
return (jsxs("svg", { viewBox: "0 0 16 16", width: "15", height: "15", fill: "none", "aria-hidden": "true", children: [jsx("rect", { x: "2.5", y: "2.5", width: "11", height: "12", rx: "1.5", stroke: "currentColor", strokeWidth: "1.6" }), jsx("path", { d: "M5.5 2.5A1.5 1.5 0 0 1 7 1h2a1.5 1.5 0 0 1 1.5 1.5", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }), jsx("path", { d: "M5 7h6M5 9.5h4", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round" })] }));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function OrderSummary() {
|
|
141
|
+
const { state, summary, money, t } = useCheckout();
|
|
142
|
+
const percentSign = state.locale === 'fa' ? '٪' : '%';
|
|
143
|
+
const lineLabels = {
|
|
144
|
+
subtotal: t.subtotal,
|
|
145
|
+
discount: t.yourSavings,
|
|
146
|
+
shipping: t.shipping,
|
|
147
|
+
credit: t.credit,
|
|
148
|
+
vat: t.vat
|
|
149
|
+
};
|
|
150
|
+
return (jsxs("aside", { className: "szc-summary", children: [jsxs("div", { className: "szc-summary__total-head", children: [jsx("span", { className: "szc-summary__total-label", children: t.totalAmount }), jsx("span", { className: "szc-summary__total-value", children: money(summary.total) })] }), jsx("dl", { className: "szc-summary__lines", children: summary.lines.map((line) => (jsxs("div", { className: `szc-summary__line${line.key === 'discount' ? ' szc-summary__line--discount' : ''}`, children: [jsxs("dt", { children: [lineLabels[line.key], (line.percent ?? 0) > 0
|
|
151
|
+
? ` (${formatPercent(line.percent, state.locale)}${percentSign})`
|
|
152
|
+
: ''] }), jsx("dd", { className: line.negative ? 'szc-summary__line--neg' : undefined, children: line.free ? (jsx("span", { className: "szc-summary__free", children: t.free })) : (`${line.negative ? '−' : ''}${money(line.amount)}`) })] }, line.key))) }), jsxs("div", { className: "szc-summary__grand", children: [jsx("span", { children: t.total }), jsx("span", { children: money(summary.total) })] })] }));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const PERSIAN_DIGITS = '۰۱۲۳۴۵۶۷۸۹';
|
|
156
|
+
const ARABIC_DIGITS = '٠١٢٣٤٥٦٧٨٩';
|
|
157
|
+
const QUANTITY_DEBOUNCE_MS = 600;
|
|
158
|
+
function toLatinDigits(value) {
|
|
159
|
+
return value
|
|
160
|
+
.replace(/[۰-۹]/g, (digit) => String(PERSIAN_DIGITS.indexOf(digit)))
|
|
161
|
+
.replace(/[٠-٩]/g, (digit) => String(ARABIC_DIGITS.indexOf(digit)));
|
|
162
|
+
}
|
|
163
|
+
const HEX_COLOR_RE = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
|
|
164
|
+
function attrValueToString(value) {
|
|
165
|
+
if (value == null)
|
|
166
|
+
return '';
|
|
167
|
+
if (typeof value === 'object') {
|
|
168
|
+
const v = value.value;
|
|
169
|
+
return v == null ? '' : String(v);
|
|
170
|
+
}
|
|
171
|
+
return String(value);
|
|
172
|
+
}
|
|
173
|
+
function toHexColor(value) {
|
|
174
|
+
if (typeof value !== 'string')
|
|
175
|
+
return null;
|
|
176
|
+
const trimmed = value.trim();
|
|
177
|
+
return HEX_COLOR_RE.test(trimmed) ? (trimmed.startsWith('#') ? trimmed : `#${trimmed}`) : null;
|
|
178
|
+
}
|
|
179
|
+
// Hex can arrive as the raw value, or nested in `extra`/`value` of an object attribute.
|
|
180
|
+
function attrHexColor(value) {
|
|
181
|
+
if (value && typeof value === 'object') {
|
|
182
|
+
const obj = value;
|
|
183
|
+
return toHexColor(obj.extra) ?? toHexColor(obj.value);
|
|
184
|
+
}
|
|
185
|
+
return toHexColor(value);
|
|
186
|
+
}
|
|
187
|
+
function CartStep({ continueShoppingUrl, renderEmpty, }) {
|
|
188
|
+
const { state, actions, money, t } = useCheckout();
|
|
189
|
+
const cart = state.cart;
|
|
190
|
+
const busy = state.flags.updatingCart;
|
|
191
|
+
const invoiceItemsByVariant = new Map((state.invoice?.items ?? []).map((item) => [item.productVariantId, item]));
|
|
192
|
+
if (!cart || cart.items.length === 0) {
|
|
193
|
+
if (renderEmpty) {
|
|
194
|
+
return renderEmpty({ continueShoppingUrl });
|
|
195
|
+
}
|
|
196
|
+
return (jsx("section", { className: "szc-empty", role: "status", "aria-live": "polite", children: jsxs("div", { className: "szc-empty__card", children: [jsx("span", { className: "szc-empty__badge", children: jsx(CartIcon, {}) }), jsxs("div", { className: "szc-empty__copy", children: [jsx("h3", { className: "szc-empty__title", children: t.cartEmpty }), jsx("p", { className: "szc-empty__hint", children: t.cartEmptyHint })] }), continueShoppingUrl ? (jsx(Button, { className: "szc-empty__cta", variant: "primary", onClick: () => { window.location.href = continueShoppingUrl; }, children: t.continueShopping })) : null] }) }));
|
|
197
|
+
}
|
|
198
|
+
return (jsx("section", { className: "szc-step-panel", children: jsx("ul", { className: "szc-cart-list", children: cart.items.map((item) => {
|
|
199
|
+
const max = item.product.maxOrderQuantity;
|
|
200
|
+
const min = item.product.minOrderQuantity ?? 1;
|
|
201
|
+
const invoiceItem = invoiceItemsByVariant.get(item.product.variantId);
|
|
202
|
+
const rawLineTotal = (invoiceItem?.rawPrice ?? item.unitPrice) * item.quantity;
|
|
203
|
+
const itemDiscount = invoiceItem?.customerProfit
|
|
204
|
+
|| Math.max(0, rawLineTotal - item.lineTotal);
|
|
205
|
+
return (jsxs("li", { className: "szc-cart-row", children: [item.product.image?.url ? (
|
|
206
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
207
|
+
jsx("img", { className: "szc-cart-row__thumb", src: item.product.image.url, alt: "" })) : (jsx(ProductPlaceholder, {})), jsxs("div", { className: "szc-cart-row__main", children: [jsx("span", { className: "szc-cart-row__name", children: item.product.name }), item.product.attributes?.length ? (jsx("span", { className: "szc-cart-row__attrs", children: item.product.attributes.map((a, i) => {
|
|
208
|
+
const value = attrValueToString(a.value);
|
|
209
|
+
const hex = attrHexColor(a.value);
|
|
210
|
+
return (jsxs("span", { className: "szc-cart-row__attr", children: [i > 0 ? jsx("span", { className: "szc-cart-row__attr-sep", children: "\u00B7" }) : null, jsxs("span", { children: [a.name, ":"] }), hex ? (jsx("span", { className: "szc-cart-row__swatch", style: { backgroundColor: hex }, title: hex, "aria-hidden": "true" })) : null, value ? jsx("span", { children: value }) : null] }, `${a.name}-${i}`));
|
|
211
|
+
}) })) : null, jsx("span", { className: "szc-cart-row__price", children: money(item.unitPrice) })] }), jsxs("div", { className: "szc-cart-row__side", children: [jsx(QuantityControl, { value: item.quantity, min: min, max: max, label: t.quantity, onChange: (quantity) => actions.updateItemQuantity(item.id, item.product.variantId, quantity) }), itemDiscount > 0 ? (jsx("span", { className: "szc-cart-row__discount", children: t.itemDiscount(money(itemDiscount)) })) : null, jsx("span", { className: "szc-cart-row__line-total", children: money(item.lineTotal) }), jsx("button", { type: "button", className: "szc-cart-row__remove", "aria-label": t.remove, disabled: busy, onClick: () => actions.removeItem(item.id, item.product.variantId), children: jsx(TrashIcon, {}) })] })] }, item.id));
|
|
212
|
+
}) }) }));
|
|
213
|
+
}
|
|
214
|
+
function QuantityControl({ value, min, max, label, onChange }) {
|
|
215
|
+
const [draft, setDraft] = useState(() => toPersianDigits(String(value)));
|
|
216
|
+
const [pending, setPending] = useState(false);
|
|
217
|
+
const draftRef = useRef(draft);
|
|
218
|
+
const pendingValueRef = useRef(null);
|
|
219
|
+
const timerRef = useRef(null);
|
|
220
|
+
const inFlightRef = useRef(false);
|
|
221
|
+
const serverValueRef = useRef(value);
|
|
222
|
+
const onChangeRef = useRef(onChange);
|
|
223
|
+
serverValueRef.current = value;
|
|
224
|
+
onChangeRef.current = onChange;
|
|
225
|
+
const updateDraft = (next) => {
|
|
226
|
+
draftRef.current = next;
|
|
227
|
+
setDraft(next);
|
|
228
|
+
};
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (!pending || pendingValueRef.current === value) {
|
|
231
|
+
updateDraft(toPersianDigits(String(value)));
|
|
232
|
+
}
|
|
233
|
+
}, [value, pending]);
|
|
234
|
+
useEffect(() => () => {
|
|
235
|
+
if (timerRef.current)
|
|
236
|
+
clearTimeout(timerRef.current);
|
|
237
|
+
}, []);
|
|
238
|
+
const normalize = (next) => Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min, Math.trunc(next)));
|
|
239
|
+
async function flushPendingUpdate() {
|
|
240
|
+
if (inFlightRef.current)
|
|
241
|
+
return;
|
|
242
|
+
const target = pendingValueRef.current;
|
|
243
|
+
if (target == null)
|
|
244
|
+
return;
|
|
245
|
+
if (target === serverValueRef.current) {
|
|
246
|
+
pendingValueRef.current = null;
|
|
247
|
+
setPending(false);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
inFlightRef.current = true;
|
|
251
|
+
try {
|
|
252
|
+
await onChangeRef.current(target);
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
inFlightRef.current = false;
|
|
256
|
+
if (pendingValueRef.current === target) {
|
|
257
|
+
pendingValueRef.current = null;
|
|
258
|
+
setPending(false);
|
|
259
|
+
}
|
|
260
|
+
else if (timerRef.current == null) {
|
|
261
|
+
timerRef.current = setTimeout(() => {
|
|
262
|
+
timerRef.current = null;
|
|
263
|
+
void flushPendingUpdate();
|
|
264
|
+
}, QUANTITY_DEBOUNCE_MS);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const scheduleUpdate = (rawValue) => {
|
|
269
|
+
const parsed = typeof rawValue === 'number' ? rawValue : Number(toLatinDigits(rawValue));
|
|
270
|
+
const next = normalize(Number.isFinite(parsed) ? parsed : value);
|
|
271
|
+
updateDraft(toPersianDigits(String(next)));
|
|
272
|
+
if (next === serverValueRef.current && !pending && !inFlightRef.current)
|
|
273
|
+
return;
|
|
274
|
+
pendingValueRef.current = next;
|
|
275
|
+
setPending(true);
|
|
276
|
+
if (timerRef.current)
|
|
277
|
+
clearTimeout(timerRef.current);
|
|
278
|
+
timerRef.current = setTimeout(() => {
|
|
279
|
+
timerRef.current = null;
|
|
280
|
+
void flushPendingUpdate();
|
|
281
|
+
}, QUANTITY_DEBOUNCE_MS);
|
|
282
|
+
};
|
|
283
|
+
const step = (amount) => {
|
|
284
|
+
const parsedDraft = Number(toLatinDigits(draftRef.current));
|
|
285
|
+
const base = Number.isFinite(parsedDraft) ? parsedDraft : value;
|
|
286
|
+
scheduleUpdate(base + amount);
|
|
287
|
+
};
|
|
288
|
+
const parsedDraft = Number(toLatinDigits(draft));
|
|
289
|
+
const displayedValue = Number.isFinite(parsedDraft) ? normalize(parsedDraft) : value;
|
|
290
|
+
return (jsxs("div", { className: `szc-qty${pending ? ' szc-qty--pending' : ''}`, "aria-busy": pending, children: [jsx("button", { type: "button", className: "szc-qty__btn", "aria-label": `${label} −`, disabled: displayedValue <= min, onMouseDown: (event) => event.preventDefault(), onClick: () => step(-1), children: jsx(MinusIcon, {}) }), jsx("input", { className: "szc-qty__input", type: "text", inputMode: "numeric", pattern: "[\u06F0-\u06F9\u0660-\u06690-9]*", role: "spinbutton", value: draft, "aria-label": label, "aria-valuemin": min, "aria-valuemax": max, "aria-valuenow": displayedValue, onChange: (event) => {
|
|
291
|
+
const digits = toLatinDigits(event.target.value).replace(/\D/g, '');
|
|
292
|
+
if (!digits) {
|
|
293
|
+
if (timerRef.current)
|
|
294
|
+
clearTimeout(timerRef.current);
|
|
295
|
+
timerRef.current = null;
|
|
296
|
+
updateDraft('');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
scheduleUpdate(Number(digits));
|
|
300
|
+
}, onBlur: () => scheduleUpdate(draftRef.current), onKeyDown: (event) => {
|
|
301
|
+
if (event.key === 'Enter') {
|
|
302
|
+
event.currentTarget.blur();
|
|
303
|
+
}
|
|
304
|
+
else if (event.key === 'Escape') {
|
|
305
|
+
event.preventDefault();
|
|
306
|
+
if (timerRef.current)
|
|
307
|
+
clearTimeout(timerRef.current);
|
|
308
|
+
timerRef.current = null;
|
|
309
|
+
pendingValueRef.current = null;
|
|
310
|
+
setPending(false);
|
|
311
|
+
updateDraft(toPersianDigits(String(value)));
|
|
312
|
+
}
|
|
313
|
+
else if (event.key === 'ArrowUp') {
|
|
314
|
+
event.preventDefault();
|
|
315
|
+
step(1);
|
|
316
|
+
}
|
|
317
|
+
else if (event.key === 'ArrowDown') {
|
|
318
|
+
event.preventDefault();
|
|
319
|
+
step(-1);
|
|
320
|
+
}
|
|
321
|
+
} }), jsx("button", { type: "button", className: "szc-qty__btn", "aria-label": `${label} +`, disabled: max != null && displayedValue >= max, onMouseDown: (event) => event.preventDefault(), onClick: () => step(1), children: jsx(PlusIcon, {}) })] }));
|
|
322
|
+
}
|
|
323
|
+
function MinusIcon() {
|
|
324
|
+
return (jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "M3.5 8h9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }));
|
|
325
|
+
}
|
|
326
|
+
function PlusIcon() {
|
|
327
|
+
return (jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "M8 3.5v9M3.5 8h9", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }));
|
|
328
|
+
}
|
|
329
|
+
function TrashIcon() {
|
|
330
|
+
return (jsxs("svg", { viewBox: "0 0 24 24", width: "16", height: "16", fill: "none", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [jsx("path", { d: "M3 6h18" }), jsx("path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }), jsx("path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }), jsx("path", { d: "M10 11v6M14 11v6" })] }));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function validate(form, needsShipping, postalCodeMandatory, emailMandatory, t) {
|
|
334
|
+
const e = {};
|
|
335
|
+
if (!form.firstName.trim())
|
|
336
|
+
e.firstName = t.errorRequired;
|
|
337
|
+
if (!form.lastName.trim())
|
|
338
|
+
e.lastName = t.errorRequired;
|
|
339
|
+
if (!form.mobilePhone.trim()) {
|
|
340
|
+
e.mobilePhone = t.errorRequired;
|
|
341
|
+
}
|
|
342
|
+
else if (!isValidIranianMobile(form.mobilePhone)) {
|
|
343
|
+
e.mobilePhone = t.errorMobilePhone;
|
|
344
|
+
}
|
|
345
|
+
if (emailMandatory && !form.email.trim()) {
|
|
346
|
+
e.email = t.errorRequired;
|
|
347
|
+
}
|
|
348
|
+
else if (form.email.trim() && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email.trim())) {
|
|
349
|
+
e.email = t.errorEmail;
|
|
350
|
+
}
|
|
351
|
+
if (needsShipping) {
|
|
352
|
+
if (!form.regionId)
|
|
353
|
+
e.regionId = t.errorRequired;
|
|
354
|
+
if (!form.cityId)
|
|
355
|
+
e.cityId = t.errorRequired;
|
|
356
|
+
if (postalCodeMandatory && !form.postalCode.trim())
|
|
357
|
+
e.postalCode = t.errorRequired;
|
|
358
|
+
if (!form.address.trim())
|
|
359
|
+
e.address = t.errorRequired;
|
|
360
|
+
}
|
|
361
|
+
return e;
|
|
362
|
+
}
|
|
363
|
+
function ShippingStep() {
|
|
364
|
+
const { state, actions, t, digitalItems } = useCheckout();
|
|
365
|
+
const { addressForm, regions, shippingGroups } = state;
|
|
366
|
+
const [touched, setTouched] = useState({});
|
|
367
|
+
const needsShipping = state.invoice?.needsShipping ?? true;
|
|
368
|
+
const touch = useCallback((key) => {
|
|
369
|
+
setTouched((prev) => ({ ...prev, [key]: true }));
|
|
370
|
+
}, []);
|
|
371
|
+
const allErrors = validate(addressForm, needsShipping, state.postalCodeMandatory, state.emailMandatory, t);
|
|
372
|
+
const errors = Object.fromEntries(Object.entries(allErrors).filter(([k]) => touched[k]));
|
|
373
|
+
const selectedRegion = regions.find((r) => r.id === addressForm.regionId);
|
|
374
|
+
const handleBlurPhone = (key) => {
|
|
375
|
+
const raw = addressForm[key];
|
|
376
|
+
if (raw) {
|
|
377
|
+
const normalized = normalizeIranianPhone(raw);
|
|
378
|
+
if (normalized !== raw)
|
|
379
|
+
actions.setAddressField(key, normalized);
|
|
380
|
+
}
|
|
381
|
+
touch(key);
|
|
382
|
+
};
|
|
383
|
+
const busy = state.flags.savingAddress || state.flags.loadingShipping;
|
|
384
|
+
return (jsxs("section", { className: `szc-step-panel${busy ? ' szc-step-panel--loading' : ''}`, children: [jsxs("div", { className: "szc-shipping-form-card", children: [jsxs("section", { className: "szc-shipping-section", "aria-labelledby": "szc-contact-title", children: [jsx(ShippingSectionHeader, { id: "szc-contact-title", title: t.contactInfo, icon: "contact" }), jsxs("div", { className: "szc-form-grid", children: [jsx(Field, { id: "szc-first-name", label: t.firstName, required: true, optionalLabel: t.optional, value: addressForm.firstName, error: errors.firstName, onChange: (e) => actions.setAddressField('firstName', e.target.value), onBlur: () => touch('firstName') }), jsx(Field, { id: "szc-last-name", label: t.lastName, required: true, optionalLabel: t.optional, value: addressForm.lastName, error: errors.lastName, onChange: (e) => actions.setAddressField('lastName', e.target.value), onBlur: () => touch('lastName') }), jsx(Field, { id: "szc-mobile", label: t.mobilePhone, required: true, optionalLabel: t.optional, className: "szc-input--ltr", dir: "ltr", inputMode: "tel", value: addressForm.mobilePhone, error: errors.mobilePhone, onChange: (e) => actions.setAddressField('mobilePhone', e.target.value), onBlur: () => handleBlurPhone('mobilePhone') }), jsx(Field, { id: "szc-email", label: t.email, required: state.emailMandatory, optionalLabel: t.optional, className: "szc-input--ltr", dir: "ltr", inputMode: "email", value: addressForm.email, error: errors.email, onChange: (e) => actions.setAddressField('email', e.target.value), onBlur: () => touch('email') })] })] }), needsShipping ? (jsxs("section", { className: "szc-shipping-section", "aria-labelledby": "szc-address-title", children: [jsx(ShippingSectionHeader, { id: "szc-address-title", title: t.reviewAddress, icon: "address" }), jsxs("div", { className: "szc-form-grid", children: [jsx(EnhancedSelect, { id: "szc-region", label: t.region, required: true, optionalLabel: t.optional, value: addressForm.regionId ?? '', error: errors.regionId, onChange: (v) => {
|
|
385
|
+
actions.setAddressField('regionId', v ? Number(v) : null);
|
|
386
|
+
touch('regionId');
|
|
387
|
+
}, placeholder: t.selectRegion, options: regions.map((r) => ({ value: r.id, label: r.name })) }), jsx(EnhancedSelect, { id: "szc-city", label: t.city, required: true, optionalLabel: t.optional, value: addressForm.cityId ?? '', disabled: !selectedRegion, error: errors.cityId, onChange: (v) => {
|
|
388
|
+
actions.setAddressField('cityId', v ? Number(v) : null);
|
|
389
|
+
touch('cityId');
|
|
390
|
+
}, placeholder: t.selectCity, options: selectedRegion?.cities.map((c) => ({ value: c.id, label: c.name })) ?? [] }), jsx(Field, { id: "szc-postal", label: t.postalCode, required: state.postalCodeMandatory, optionalLabel: t.optional, className: "szc-input--ltr", dir: "ltr", inputMode: "numeric", value: addressForm.postalCode, error: errors.postalCode, onChange: (e) => actions.setAddressField('postalCode', e.target.value), onBlur: () => touch('postalCode') }), jsx(Field, { id: "szc-phone", label: t.phoneNumber, optionalLabel: t.optional, className: "szc-input--ltr", dir: "ltr", inputMode: "tel", value: addressForm.phoneNumber, onChange: (e) => actions.setAddressField('phoneNumber', e.target.value), onBlur: () => handleBlurPhone('phoneNumber') })] }), jsx(Field, { id: "szc-address", label: t.addressLine, required: true, optionalLabel: t.optional, value: addressForm.address, error: errors.address, onChange: (e) => actions.setAddressField('address', e.target.value), onBlur: () => touch('address') }), jsx(Field, { id: "szc-description", label: t.description, optionalLabel: t.optional, value: addressForm.description, onChange: (e) => actions.setAddressField('description', e.target.value), onBlur: () => touch('description') })] })) : null] }), shippingGroups.length > 0 ? (jsxs("section", { className: "szc-shipping-methods", "aria-labelledby": "szc-methods-title", children: [jsx(ShippingSectionHeader, { id: "szc-methods-title", title: t.shippingMethods, icon: "shipping" }), jsx("div", { className: "szc-ship-groups", children: shippingGroups.map((group) => (jsx(ShippingGroupCard, { group: group }, group.key))) })] })) : needsShipping ? (jsxs("div", { className: "szc-shipping-pending", role: "note", children: [jsx("span", { className: "szc-shipping-pending__icon", "aria-hidden": "true", children: jsx(TruckIcon, {}) }), jsxs("div", { className: "szc-shipping-pending__copy", children: [jsx("strong", { children: t.shippingMethods }), jsx("span", { children: t.shippingMethodsHint })] })] })) : null, digitalItems.length > 0 ? (jsxs("div", { className: "szc-digital-note", children: [jsx("span", { className: "szc-digital-note__icon", children: "\u2B07\uFE0E" }), jsxs("div", { children: [jsx("strong", { children: t.digitalNoShipping }), jsx("ul", { children: digitalItems.map((item) => (jsx("li", { children: item.name }, item.id))) })] })] })) : null] }));
|
|
391
|
+
}
|
|
392
|
+
function ShippingSectionHeader({ id, title, icon }) {
|
|
393
|
+
return (jsxs("header", { className: "szc-shipping-section__head", children: [jsx("span", { className: "szc-shipping-section__icon", "aria-hidden": "true", children: icon === 'contact' ? jsx(ContactIcon, {}) : icon === 'address' ? jsx(AddressIcon, {}) : jsx(TruckIcon, {}) }), jsx("h2", { id: id, className: "szc-shipping-section__title", children: title })] }));
|
|
394
|
+
}
|
|
395
|
+
function ContactIcon() {
|
|
396
|
+
return (jsxs("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", "aria-hidden": "true", children: [jsx("circle", { cx: "12", cy: "8", r: "3.25", stroke: "currentColor", strokeWidth: "1.7" }), jsx("path", { d: "M5.5 19c.65-3.35 3-5.25 6.5-5.25s5.85 1.9 6.5 5.25", stroke: "currentColor", strokeWidth: "1.7", strokeLinecap: "round" })] }));
|
|
397
|
+
}
|
|
398
|
+
function AddressIcon() {
|
|
399
|
+
return (jsxs("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", "aria-hidden": "true", children: [jsx("path", { d: "M19 10c0 4.7-7 10-7 10S5 14.7 5 10a7 7 0 1 1 14 0Z", stroke: "currentColor", strokeWidth: "1.7", strokeLinejoin: "round" }), jsx("circle", { cx: "12", cy: "10", r: "2.25", stroke: "currentColor", strokeWidth: "1.7" })] }));
|
|
400
|
+
}
|
|
401
|
+
function EnhancedSelect({ id, label, value, placeholder, options, required, optionalLabel, disabled, error, onChange }) {
|
|
402
|
+
return (jsxs("label", { className: "szc-field", htmlFor: id, children: [jsx(FieldLabel, { label: label, required: required, optionalLabel: optionalLabel }), jsxs("div", { className: "szc-select-wrap", children: [jsxs("select", { id: id, className: `szc-input szc-select-enhanced${error ? ' szc-input--error' : ''}`, value: value, required: required, disabled: disabled, onChange: (e) => onChange(e.target.value), children: [jsx("option", { value: "", children: placeholder }), options.map((opt) => (jsx("option", { value: opt.value, children: opt.label }, opt.value)))] }), jsx("span", { className: "szc-select-chevron", "aria-hidden": "true", children: jsx(ChevronIcon, {}) })] }), error ? jsx("span", { className: "szc-field__error", children: error }) : null] }));
|
|
403
|
+
}
|
|
404
|
+
function ChevronIcon() {
|
|
405
|
+
return (jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "M4 6l4 4 4-4", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Click-and-drag horizontal scrolling for a container with a hidden scrollbar.
|
|
409
|
+
* Also maps vertical mouse-wheel deltas to horizontal scroll so a plain mouse
|
|
410
|
+
* (no horizontal wheel) can still pan the list.
|
|
411
|
+
*/
|
|
412
|
+
function useDragScroll() {
|
|
413
|
+
const ref = useRef(null);
|
|
414
|
+
const drag = useRef({ down: false, startX: 0, startLeft: 0 });
|
|
415
|
+
useEffect(() => {
|
|
416
|
+
const el = ref.current;
|
|
417
|
+
if (!el)
|
|
418
|
+
return;
|
|
419
|
+
const onWheel = (e) => {
|
|
420
|
+
if (el.scrollWidth <= el.clientWidth)
|
|
421
|
+
return; // nothing to pan
|
|
422
|
+
if (Math.abs(e.deltaY) <= Math.abs(e.deltaX))
|
|
423
|
+
return; // already horizontal
|
|
424
|
+
e.preventDefault();
|
|
425
|
+
el.scrollLeft += e.deltaY;
|
|
426
|
+
};
|
|
427
|
+
el.addEventListener('wheel', onWheel, { passive: false });
|
|
428
|
+
return () => el.removeEventListener('wheel', onWheel);
|
|
429
|
+
}, []);
|
|
430
|
+
const onPointerDown = (e) => {
|
|
431
|
+
const el = ref.current;
|
|
432
|
+
if (!el)
|
|
433
|
+
return;
|
|
434
|
+
drag.current = { down: true, startX: e.clientX, startLeft: el.scrollLeft };
|
|
435
|
+
el.setPointerCapture(e.pointerId);
|
|
436
|
+
el.classList.add('szc-dragging');
|
|
437
|
+
};
|
|
438
|
+
const onPointerMove = (e) => {
|
|
439
|
+
const el = ref.current;
|
|
440
|
+
if (!el || !drag.current.down)
|
|
441
|
+
return;
|
|
442
|
+
el.scrollLeft = drag.current.startLeft - (e.clientX - drag.current.startX);
|
|
443
|
+
};
|
|
444
|
+
const onPointerUp = (e) => {
|
|
445
|
+
const el = ref.current;
|
|
446
|
+
if (!el || !drag.current.down)
|
|
447
|
+
return;
|
|
448
|
+
drag.current.down = false;
|
|
449
|
+
el.classList.remove('szc-dragging');
|
|
450
|
+
try {
|
|
451
|
+
el.releasePointerCapture(e.pointerId);
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
/* pointer already released */
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
return { ref, onPointerDown, onPointerMove, onPointerUp };
|
|
458
|
+
}
|
|
459
|
+
function ShippingGroupCard({ group }) {
|
|
460
|
+
const { actions, price, state, t } = useCheckout();
|
|
461
|
+
const busy = state.flags.selectingRate;
|
|
462
|
+
const selectedRate = group.rates.find((rate) => rate.id === group.selectedRateId);
|
|
463
|
+
const showRateSelector = group.items.length !== 1 || group.rates.length !== 1;
|
|
464
|
+
const itemsScroll = useDragScroll();
|
|
465
|
+
return (jsxs("div", { className: "szc-ship-group", children: [jsxs("header", { className: "szc-ship-group__head", children: [jsx("span", { className: "szc-ship-group__mark", style: shippingRateIconStyle(selectedRate?.color), "aria-hidden": "true", children: selectedRate ? jsx(ShippingRateIcon, { rate: selectedRate }) : jsx(TruckIcon, {}) }), jsxs("div", { className: "szc-ship-group__heading", children: [jsxs("div", { className: "szc-ship-group__title-row", children: [jsx("strong", { className: "szc-ship-group__title", children: group.title || selectedRate?.name || t.shippingMethod }), jsx("span", { className: "szc-ship-group__count", children: t.productCount(toPersianDigits(String(group.items.length))) })] }), selectedRate ? (jsxs("span", { className: "szc-ship-group__hint", children: [selectedRate.description ? (jsx("span", { className: "szc-ship-group__desc", children: selectedRate.description })) : null, jsx("span", { className: "szc-ship-group__rate-price", children: selectedRate.price > 0 ? price(selectedRate.price) : t.free })] })) : (jsx("span", { className: "szc-ship-group__hint", children: t.shippingMethods }))] })] }), jsx("div", { className: "szc-ship-group__items", ref: itemsScroll.ref, onPointerDown: itemsScroll.onPointerDown, onPointerMove: itemsScroll.onPointerMove, onPointerUp: itemsScroll.onPointerUp, onPointerCancel: itemsScroll.onPointerUp, children: group.items.map((item) => (jsxs("article", { className: "szc-ship-product", children: [jsxs("div", { className: "szc-ship-product__media", children: [item.image?.url ? (jsx("img", { src: item.image.url, alt: item.image.alt || item.name })) : (jsx(ProductPlaceholder, {})), jsx("span", { className: "szc-ship-product__quantity", children: toPersianDigits(String(item.quantity)) })] }), jsxs("div", { className: "szc-ship-product__copy", children: [jsx("strong", { className: "szc-ship-product__name", children: item.name }), item.attributes.length > 0 ? (jsx("span", { className: "szc-ship-product__attrs", children: item.attributes.map((attribute) => `${attribute.name}: ${attribute.value}`).join(' · ') })) : null] })] }, item.id))) }), !showRateSelector && selectedRate?.description?.trim() ? (jsx("div", { className: "szc-ship-group__description", children: selectedRate.description.trim() })) : null, showRateSelector ? (jsx("div", { className: "szc-rate-list", role: "radiogroup", children: group.rates.map((rate) => {
|
|
466
|
+
const selected = group.selectedRateId === rate.id;
|
|
467
|
+
const description = rate.description?.trim();
|
|
468
|
+
return (jsxs("button", { type: "button", role: "radio", "aria-checked": selected, "aria-label": description ? `${rate.name} — ${description}` : rate.name, disabled: busy, className: `szc-rate${selected ? ' szc-rate--selected' : ''}`, onClick: () => actions.selectShippingRate(group.key, rate.id), children: [jsx("span", { className: "szc-rate__icon", style: shippingRateIconStyle(rate.color), "aria-hidden": "true", children: jsx(ShippingRateIcon, { rate: rate }) }), jsxs("span", { className: "szc-rate__copy", children: [jsx("span", { className: "szc-rate__name", children: rate.name }), rate.price > 0 ? (jsx("span", { className: "szc-rate__price", children: price(rate.price) })) : null] }), jsx("span", { className: "szc-rate__radio", "aria-hidden": "true", children: selected ? jsx(CheckIcon$1, {}) : null }), description ? (jsx("span", { className: "szc-rate__description", children: description })) : null] }, rate.id));
|
|
469
|
+
}) })) : null] }));
|
|
470
|
+
}
|
|
471
|
+
const ICON_PROPS$1 = {
|
|
472
|
+
viewBox: '0 0 24 24',
|
|
473
|
+
width: 26,
|
|
474
|
+
height: 26,
|
|
475
|
+
fill: 'none',
|
|
476
|
+
stroke: 'currentColor',
|
|
477
|
+
strokeWidth: 1.8,
|
|
478
|
+
strokeLinecap: 'round',
|
|
479
|
+
strokeLinejoin: 'round',
|
|
480
|
+
'aria-hidden': true,
|
|
481
|
+
};
|
|
482
|
+
function TruckIcon() {
|
|
483
|
+
return (jsxs("svg", { ...ICON_PROPS$1, children: [jsx("path", { d: "M14 17V6a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v11a1 1 0 0 0 1 1h1" }), jsx("path", { d: "M9 18h5" }), jsx("path", { d: "M18 18h1a1 1 0 0 0 1-1v-3.28a1 1 0 0 0-.21-.62l-2.7-3.39A1 1 0 0 0 16.3 9H14" }), jsx("circle", { cx: "7", cy: "18", r: "2" }), jsx("circle", { cx: "16", cy: "18", r: "2" })] }));
|
|
484
|
+
}
|
|
485
|
+
function ShippingRateIcon({ rate }) {
|
|
486
|
+
const iconKey = shippingRateIconKey(rate);
|
|
487
|
+
switch (iconKey) {
|
|
488
|
+
case 'post':
|
|
489
|
+
return jsx(PostIcon, {});
|
|
490
|
+
case 'free-delivery':
|
|
491
|
+
return jsx(FreeDeliveryIcon, {});
|
|
492
|
+
case 'courier':
|
|
493
|
+
return jsx(CourierIcon, {});
|
|
494
|
+
case 'delivery-man':
|
|
495
|
+
return jsx(DeliveryManIcon, {});
|
|
496
|
+
default:
|
|
497
|
+
return rate.icon && isImageUrl(rate.icon) ? jsx("img", { src: rate.icon, alt: "" }) : jsx(TruckIcon, {});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function shippingRateIconKey(rate) {
|
|
501
|
+
const explicit = (rate.icon || rate.type || '').trim().toLowerCase().replace(/_/g, '-');
|
|
502
|
+
if (explicit)
|
|
503
|
+
return explicit;
|
|
504
|
+
const name = rate.name.trim().toLowerCase();
|
|
505
|
+
if (name.includes('پست') || name.includes('post'))
|
|
506
|
+
return 'post';
|
|
507
|
+
if (name.includes('رایگان') || name.includes('free'))
|
|
508
|
+
return 'free-delivery';
|
|
509
|
+
if (name.includes('پیک') || name.includes('courier'))
|
|
510
|
+
return 'courier';
|
|
511
|
+
if (name.includes('حضوری') || name.includes('delivery man'))
|
|
512
|
+
return 'delivery-man';
|
|
513
|
+
return '';
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* The API returns `color` as a palette *token* (e.g. `mainColor`, `green`) — not
|
|
517
|
+
* always a CSS color. Map the known tokens to concrete CSS colors so each rate
|
|
518
|
+
* renders its own chip color instead of every icon falling back to the accent.
|
|
519
|
+
*/
|
|
520
|
+
const SHIPPING_COLOR_TOKENS = {
|
|
521
|
+
green: '#16a34a',
|
|
522
|
+
red: '#dc2626',
|
|
523
|
+
blue: '#2563eb',
|
|
524
|
+
orange: '#ea580c',
|
|
525
|
+
yellow: '#eab308',
|
|
526
|
+
purple: '#7c3aed',
|
|
527
|
+
pink: '#db2777',
|
|
528
|
+
teal: '#0d9488',
|
|
529
|
+
cyan: '#0891b2',
|
|
530
|
+
indigo: '#4f46e5',
|
|
531
|
+
gray: '#6b7280',
|
|
532
|
+
grey: '#6b7280',
|
|
533
|
+
black: '#0f172a',
|
|
534
|
+
brown: '#92400e'
|
|
535
|
+
};
|
|
536
|
+
function resolveShippingColor(raw) {
|
|
537
|
+
const token = raw.trim().toLowerCase();
|
|
538
|
+
if (!token)
|
|
539
|
+
return undefined;
|
|
540
|
+
// The shop's "main color" is the theme accent → let the CSS fallback handle it.
|
|
541
|
+
if (token === 'maincolor' || token === 'main')
|
|
542
|
+
return undefined;
|
|
543
|
+
if (token === 'transparent' || token === 'white')
|
|
544
|
+
return '#ffffff';
|
|
545
|
+
if (SHIPPING_COLOR_TOKENS[token])
|
|
546
|
+
return SHIPPING_COLOR_TOKENS[token];
|
|
547
|
+
// Already a concrete CSS color value (hex / rgb()).
|
|
548
|
+
if (parseColorChannels(token))
|
|
549
|
+
return token;
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
function shippingRateIconStyle(color) {
|
|
553
|
+
if (!color)
|
|
554
|
+
return undefined;
|
|
555
|
+
const resolved = resolveShippingColor(color);
|
|
556
|
+
// Unknown token / unparseable → let CSS use the theme's soft accent treatment.
|
|
557
|
+
if (!resolved)
|
|
558
|
+
return undefined;
|
|
559
|
+
const rgb = parseColorChannels(resolved);
|
|
560
|
+
if (!rgb)
|
|
561
|
+
return undefined;
|
|
562
|
+
const luminance = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;
|
|
563
|
+
// A white / near-white brand color (common for "post") would vanish against
|
|
564
|
+
// the card. Render a neutral chip with a slate glyph instead.
|
|
565
|
+
if (luminance > 0.9) {
|
|
566
|
+
return {
|
|
567
|
+
'--szc-rate-color': '#eef0f5',
|
|
568
|
+
'--szc-rate-foreground': '#475569'
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const foreground = luminance > 0.62
|
|
572
|
+
? `rgb(${rgb.map((channel) => Math.round(channel * 0.58)).join(', ')})`
|
|
573
|
+
: resolved;
|
|
574
|
+
return {
|
|
575
|
+
'--szc-rate-color': `color-mix(in srgb, ${resolved} 13%, var(--szc-card))`,
|
|
576
|
+
'--szc-rate-foreground': foreground
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function parseColorChannels(value) {
|
|
580
|
+
const hex = value.match(/^#([\da-f]{3}|[\da-f]{6}|[\da-f]{8})$/i)?.[1];
|
|
581
|
+
if (hex?.length === 3) {
|
|
582
|
+
return [0, 1, 2].map((index) => parseInt(hex[index] + hex[index], 16));
|
|
583
|
+
}
|
|
584
|
+
if (hex && hex.length >= 6) {
|
|
585
|
+
return [
|
|
586
|
+
parseInt(hex.slice(0, 2), 16),
|
|
587
|
+
parseInt(hex.slice(2, 4), 16),
|
|
588
|
+
parseInt(hex.slice(4, 6), 16)
|
|
589
|
+
];
|
|
590
|
+
}
|
|
591
|
+
const channels = value.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i);
|
|
592
|
+
if (channels)
|
|
593
|
+
return [Number(channels[1]), Number(channels[2]), Number(channels[3])];
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
function isImageUrl(value) {
|
|
597
|
+
return /^(https?:)?\/\//i.test(value) || value.startsWith('/') || value.startsWith('data:');
|
|
598
|
+
}
|
|
599
|
+
function PostIcon() {
|
|
600
|
+
return (jsxs("svg", { ...ICON_PROPS$1, children: [jsx("path", { d: "M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" }), jsx("path", { d: "M3.3 7 12 12l8.7-5" }), jsx("path", { d: "M12 22V12" }), jsx("path", { d: "m7.5 4.6 9 5.2" })] }));
|
|
601
|
+
}
|
|
602
|
+
function FreeDeliveryIcon() {
|
|
603
|
+
return (jsxs("svg", { ...ICON_PROPS$1, children: [jsx("path", { d: "M21 11V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l1.5-.86" }), jsx("path", { d: "M3.3 7 12 12l8.7-5" }), jsx("path", { d: "M12 22V12" }), jsx("path", { d: "m15.5 17 2 2 4-4" })] }));
|
|
604
|
+
}
|
|
605
|
+
function CourierIcon() {
|
|
606
|
+
return (jsxs("svg", { ...ICON_PROPS$1, children: [jsx("circle", { cx: "5.5", cy: "17.5", r: "3.5" }), jsx("circle", { cx: "18.5", cy: "17.5", r: "3.5" }), jsx("circle", { cx: "15", cy: "5", r: "1" }), jsx("path", { d: "M12 17.5V14l-3-3 4-3 2 3h2" })] }));
|
|
607
|
+
}
|
|
608
|
+
function DeliveryManIcon() {
|
|
609
|
+
return (jsxs("svg", { ...ICON_PROPS$1, children: [jsx("circle", { cx: "9", cy: "4.5", r: "2.5" }), jsx("path", { d: "M5.5 21v-6.5a3.5 3.5 0 0 1 7 0V21" }), jsx("rect", { x: "14", y: "11.5", width: "6.5", height: "7", rx: "1.2" }), jsx("path", { d: "M14 15h6.5" }), jsx("path", { d: "M17.2 11.5v3.5" })] }));
|
|
610
|
+
}
|
|
611
|
+
function CheckIcon$1() {
|
|
612
|
+
return (jsx("svg", { viewBox: "0 0 16 16", width: "12", height: "12", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "m3.2 8.2 3 3 6.6-6.4", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Human line for the applied-code card: "20% off (50,000)", "Free shipping", … */
|
|
616
|
+
function describeDiscount(discount, t, money, locale) {
|
|
617
|
+
const parts = [];
|
|
618
|
+
if (discount.kind === 'percentage' && (discount.percent ?? 0) > 0) {
|
|
619
|
+
const percentOff = t.discountPercentOff(formatPercent(discount.percent, locale));
|
|
620
|
+
parts.push(discount.amount > 0 ? `${percentOff} (${money(discount.amount)})` : percentOff);
|
|
621
|
+
}
|
|
622
|
+
else if (discount.amount > 0) {
|
|
623
|
+
parts.push(t.discountAmountOff(money(discount.amount)));
|
|
624
|
+
}
|
|
625
|
+
if (discount.kind === 'free_shipping' || (discount.shippingSaved ?? 0) > 0) {
|
|
626
|
+
parts.push(t.discountFreeShipping);
|
|
627
|
+
}
|
|
628
|
+
return parts.length > 0 ? parts.join(' + ') : null;
|
|
629
|
+
}
|
|
630
|
+
function PaymentStep() {
|
|
631
|
+
const { state, actions, t, money } = useCheckout();
|
|
632
|
+
const { paymentMethods, selectedPaymentMethodId, appliedDiscount, discountCode, discountError, flags, locale } = state;
|
|
633
|
+
return (jsxs("section", { className: "szc-step-panel", children: [jsx(SectionTitle, { children: t.paymentMethod }), flags.loadingPayments ? (jsxs("p", { className: "szc-inline-loading", children: [jsx(Spinner, {}), " ", t.loading] })) : (jsx("div", { className: "szc-pay-list", role: "radiogroup", "aria-label": t.paymentMethod, children: paymentMethods.map((method) => {
|
|
634
|
+
const selected = selectedPaymentMethodId === method.id;
|
|
635
|
+
const info = paymentMethodInfo(method.code, locale);
|
|
636
|
+
// Prefer backend-provided title/description; fall back to derived labels.
|
|
637
|
+
const backendTitle = locale === 'fa' ? method.titleFa : method.title;
|
|
638
|
+
const title = backendTitle?.trim() || info.title;
|
|
639
|
+
const description = method.description?.trim() || info.description;
|
|
640
|
+
return (jsxs("button", { type: "button", role: "radio", "aria-checked": selected, className: `szc-pay${selected ? ' szc-pay--selected' : ''}`, onClick: () => actions.selectPaymentMethod(method.id), children: [jsx("span", { className: "szc-pay__icon", "aria-hidden": "true", children: jsx(PaymentIcon, { kind: info.kind }) }), jsxs("span", { className: "szc-pay__body", children: [jsx("span", { className: "szc-pay__name", children: title }), jsx("span", { className: "szc-pay__desc", children: description })] }), jsx("span", { className: "szc-pay__radio", "aria-hidden": "true", children: selected ? jsx(CheckIcon, {}) : null })] }, method.id));
|
|
641
|
+
}) })), jsx(SectionTitle, { children: t.discountCode }), appliedDiscount ? (jsxs("div", { className: "szc-discount szc-discount--applied", children: [jsx("span", { className: "szc-discount__icon", "aria-hidden": "true", children: jsx(TagIcon, {}) }), jsxs("span", { className: "szc-discount__body", children: [jsx("code", { className: "szc-discount__code", children: appliedDiscount.code }), jsx("span", { className: "szc-discount__desc", children: describeDiscount(appliedDiscount, t, money, locale) ?? t.applied })] }), jsxs("span", { className: "szc-discount__badge", children: [jsx(CheckIcon, {}), " ", t.applied] }), jsx(Button, { variant: "ghost", onClick: () => actions.removeDiscount(), loading: flags.applyingDiscount, children: t.remove })] })) : (jsxs("div", { className: "szc-discount-field", children: [jsxs("div", { className: "szc-discount", children: [jsxs("span", { className: "szc-discount__inputwrap", children: [jsx(TagIcon, {}), jsx("input", { className: `szc-input${discountError ? ' szc-input--error' : ''}`, placeholder: t.discountPlaceholder, value: discountCode, "aria-invalid": discountError ? true : undefined, onChange: (e) => actions.setDiscountCode(e.target.value), onKeyDown: (e) => {
|
|
642
|
+
if (e.key === 'Enter')
|
|
643
|
+
actions.applyDiscount();
|
|
644
|
+
} })] }), jsx(Button, { variant: "outline", onClick: () => actions.applyDiscount(), loading: flags.applyingDiscount, disabled: !discountCode.trim(), children: t.apply })] }), discountError ? (jsxs("p", { className: "szc-discount__error", role: "alert", children: [jsx(AlertIcon, {}), discountError] })) : null] }))] }));
|
|
645
|
+
}
|
|
646
|
+
function TagIcon() {
|
|
647
|
+
return (jsxs("svg", { viewBox: "0 0 20 20", width: "18", height: "18", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [jsx("path", { d: "M10.5 2.5H16v5.5l-7.5 7.5a1.8 1.8 0 0 1-2.5 0L3 13a1.8 1.8 0 0 1 0-2.5z" }), jsx("circle", { cx: "12.8", cy: "5.7", r: "1", fill: "currentColor", stroke: "none" })] }));
|
|
648
|
+
}
|
|
649
|
+
const ICON_PROPS = {
|
|
650
|
+
viewBox: '0 0 20 20',
|
|
651
|
+
width: 20,
|
|
652
|
+
height: 20,
|
|
653
|
+
fill: 'none',
|
|
654
|
+
stroke: 'currentColor',
|
|
655
|
+
strokeWidth: 1.4,
|
|
656
|
+
strokeLinecap: 'round',
|
|
657
|
+
strokeLinejoin: 'round',
|
|
658
|
+
'aria-hidden': true
|
|
659
|
+
};
|
|
660
|
+
function AlertIcon() {
|
|
661
|
+
return (jsxs("svg", { viewBox: "0 0 16 16", width: "14", height: "14", fill: "none", "aria-hidden": "true", children: [jsx("circle", { cx: "8", cy: "8", r: "6.5", stroke: "currentColor", strokeWidth: "1.4" }), jsx("path", { d: "M8 4.8v3.4", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round" }), jsx("circle", { cx: "8", cy: "11", r: "0.85", fill: "currentColor" })] }));
|
|
662
|
+
}
|
|
663
|
+
function CheckIcon() {
|
|
664
|
+
return (jsx("svg", { viewBox: "0 0 16 16", width: "12", height: "12", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "m3.2 8.2 3 3 6.6-6.4", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
|
|
665
|
+
}
|
|
666
|
+
function PaymentIcon({ kind }) {
|
|
667
|
+
switch (kind) {
|
|
668
|
+
case 'card':
|
|
669
|
+
// two cards / transfer
|
|
670
|
+
return (jsxs("svg", { ...ICON_PROPS, children: [jsx("rect", { x: "2", y: "5", width: "13", height: "9", rx: "2" }), jsx("path", { d: "M2 8h13M5.5 11.5h3" }), jsx("path", { d: "M15.5 6.5h2.5v9h-9v-1.5" })] }));
|
|
671
|
+
case 'cod':
|
|
672
|
+
// package / hand
|
|
673
|
+
return (jsxs("svg", { ...ICON_PROPS, children: [jsx("path", { d: "M10 2.5 17 6v8l-7 3.5L3 14V6z" }), jsx("path", { d: "M3 6l7 3.5L17 6M10 9.5v8" })] }));
|
|
674
|
+
case 'bnpl':
|
|
675
|
+
// calendar / installments
|
|
676
|
+
return (jsxs("svg", { ...ICON_PROPS, children: [jsx("rect", { x: "3", y: "4", width: "14", height: "13", rx: "2" }), jsx("path", { d: "M3 8h14M7 2.5v3M13 2.5v3M6.5 12h3" })] }));
|
|
677
|
+
case 'wallet':
|
|
678
|
+
return (jsxs("svg", { ...ICON_PROPS, children: [jsx("rect", { x: "2.5", y: "5", width: "15", height: "11", rx: "2.5" }), jsx("path", { d: "M2.5 9h15" }), jsx("circle", { cx: "14", cy: "12.5", r: "1.1", fill: "currentColor", stroke: "none" })] }));
|
|
679
|
+
case 'free':
|
|
680
|
+
// tag
|
|
681
|
+
return (jsxs("svg", { ...ICON_PROPS, children: [jsx("path", { d: "M10.5 2.5H16v5.5l-7.5 7.5a1.8 1.8 0 0 1-2.5 0L3 13a1.8 1.8 0 0 1 0-2.5z" }), jsx("circle", { cx: "12.8", cy: "5.7", r: "1", fill: "currentColor", stroke: "none" })] }));
|
|
682
|
+
case 'gateway':
|
|
683
|
+
default:
|
|
684
|
+
// single card / bank
|
|
685
|
+
return (jsxs("svg", { ...ICON_PROPS, children: [jsx("rect", { x: "2.5", y: "4.5", width: "15", height: "11", rx: "2" }), jsx("path", { d: "M2.5 8.5h15M5.5 12.5h4" })] }));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function ResultStep({ continueShoppingUrl }) {
|
|
690
|
+
const { state, actions, t } = useCheckout();
|
|
691
|
+
const result = state.result;
|
|
692
|
+
const status = result?.status ?? 'pending';
|
|
693
|
+
const titleMap = {
|
|
694
|
+
success: t.paymentSuccess,
|
|
695
|
+
failed: t.paymentFailed,
|
|
696
|
+
pending: t.paymentPending,
|
|
697
|
+
stock_violated: t.paymentFailed
|
|
698
|
+
};
|
|
699
|
+
return (jsxs("section", { className: `szc-result szc-result--${status}`, children: [jsx("div", { className: "szc-result__icon", children: status === 'success' ? (jsx(CheckCircle, {})) : status === 'pending' ? (jsx(Spinner, {})) : (jsx(CrossCircle, {})) }), jsx("h2", { className: "szc-result__title", children: titleMap[status] }), status === 'pending' ? jsx("p", { className: "szc-muted", children: t.paymentPendingHint }) : null, result?.message ? jsx("p", { className: "szc-muted", children: result.message }) : null, result?.order ? (jsxs("p", { className: "szc-result__order", children: [t.orderNumber, ": ", jsx("strong", { children: result.order.orderNumber })] })) : null, jsxs("div", { className: "szc-result__actions", children: [status === 'failed' || status === 'stock_violated' ? (jsx(Button, { onClick: () => actions.goToStep('payment'), children: t.tryAgain })) : null, continueShoppingUrl ? (jsx(Button, { variant: "outline", onClick: () => (window.location.href = continueShoppingUrl), children: t.continueShopping })) : null] })] }));
|
|
700
|
+
}
|
|
701
|
+
function CheckCircle() {
|
|
702
|
+
return (jsxs("svg", { viewBox: "0 0 48 48", width: "48", height: "48", fill: "none", "aria-hidden": "true", children: [jsx("circle", { cx: "24", cy: "24", r: "22", stroke: "currentColor", strokeWidth: "2.5" }), jsx("path", { d: "M15 24.5l6 6 12-13", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round" })] }));
|
|
703
|
+
}
|
|
704
|
+
function CrossCircle() {
|
|
705
|
+
return (jsxs("svg", { viewBox: "0 0 48 48", width: "48", height: "48", fill: "none", "aria-hidden": "true", children: [jsx("circle", { cx: "24", cy: "24", r: "22", stroke: "currentColor", strokeWidth: "2.5" }), jsx("path", { d: "M17 17l14 14M31 17L17 31", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round" })] }));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function themeVars(theme) {
|
|
709
|
+
const vars = {};
|
|
710
|
+
if (theme?.accent)
|
|
711
|
+
vars['--szc-accent'] = theme.accent;
|
|
712
|
+
if (theme?.accentForeground)
|
|
713
|
+
vars['--szc-accent-foreground'] = theme.accentForeground;
|
|
714
|
+
if (theme?.accentSoft)
|
|
715
|
+
vars['--szc-accent-soft'] = theme.accentSoft;
|
|
716
|
+
if (theme?.background)
|
|
717
|
+
vars['--szc-bg'] = theme.background;
|
|
718
|
+
if (theme?.foreground)
|
|
719
|
+
vars['--szc-fg'] = theme.foreground;
|
|
720
|
+
if (theme?.muted)
|
|
721
|
+
vars['--szc-muted'] = theme.muted;
|
|
722
|
+
if (theme?.mutedForeground)
|
|
723
|
+
vars['--szc-muted-fg'] = theme.mutedForeground;
|
|
724
|
+
if (theme?.border)
|
|
725
|
+
vars['--szc-border'] = theme.border;
|
|
726
|
+
if (theme?.card)
|
|
727
|
+
vars['--szc-card'] = theme.card;
|
|
728
|
+
if (theme?.summaryBackground)
|
|
729
|
+
vars['--szc-summary-bg'] = theme.summaryBackground;
|
|
730
|
+
if (theme?.danger)
|
|
731
|
+
vars['--szc-danger'] = theme.danger;
|
|
732
|
+
if (theme?.success)
|
|
733
|
+
vars['--szc-success'] = theme.success;
|
|
734
|
+
if (theme?.radius != null)
|
|
735
|
+
vars['--szc-radius'] = `${theme.radius}px`;
|
|
736
|
+
if (theme?.fontFamily)
|
|
737
|
+
vars['--szc-font'] = theme.fontFamily;
|
|
738
|
+
return vars;
|
|
739
|
+
}
|
|
740
|
+
function CheckoutSkeleton({ label }) {
|
|
741
|
+
return (jsxs("div", { className: "szc-layout szc-skeleton-layout", role: "status", "aria-label": label, children: [jsxs("main", { className: "szc-main", "aria-hidden": "true", children: [jsxs("div", { className: "szc-skeleton-cart-row", children: [jsx("span", { className: "szc-skeleton szc-skeleton--thumb" }), jsxs("div", { className: "szc-skeleton-copy", children: [jsx("span", { className: "szc-skeleton szc-skeleton--title" }), jsx("span", { className: "szc-skeleton szc-skeleton--text" }), jsx("span", { className: "szc-skeleton szc-skeleton--price" })] }), jsx("span", { className: "szc-skeleton szc-skeleton--counter" })] }), jsxs("div", { className: "szc-skeleton-cart-row", children: [jsx("span", { className: "szc-skeleton szc-skeleton--thumb" }), jsxs("div", { className: "szc-skeleton-copy", children: [jsx("span", { className: "szc-skeleton szc-skeleton--title" }), jsx("span", { className: "szc-skeleton szc-skeleton--text" }), jsx("span", { className: "szc-skeleton szc-skeleton--price" })] }), jsx("span", { className: "szc-skeleton szc-skeleton--counter" })] })] }), jsxs("div", { className: "szc-side", "aria-hidden": "true", children: [jsxs("aside", { className: "szc-summary szc-summary--skeleton", children: [jsxs("div", { className: "szc-summary__total-head", children: [jsx("span", { className: "szc-skeleton szc-skeleton--summary-label" }), jsx("span", { className: "szc-skeleton szc-skeleton--summary-total" })] }), jsxs("div", { className: "szc-skeleton-summary-lines", children: [jsxs("div", { children: [jsx("span", { className: "szc-skeleton" }), jsx("span", { className: "szc-skeleton" })] }), jsxs("div", { children: [jsx("span", { className: "szc-skeleton" }), jsx("span", { className: "szc-skeleton" })] })] }), jsxs("div", { className: "szc-skeleton-summary-grand", children: [jsx("span", { className: "szc-skeleton" }), jsx("span", { className: "szc-skeleton" })] })] }), jsx("div", { className: "szc-side__cta", children: jsx("span", { className: "szc-skeleton szc-skeleton--button" }) })] })] }));
|
|
742
|
+
}
|
|
743
|
+
function SazitoCheckout({ theme, continueShoppingUrl, className, renderNextButton, renderBackButton, renderEmptyCart, }) {
|
|
744
|
+
const { state, actions, t, summary, money } = useCheckout();
|
|
745
|
+
const { step, direction, flags, error } = state;
|
|
746
|
+
const isResult = step === 'result';
|
|
747
|
+
const bootstrapping = step === 'cart' && !error && (!state.cart || !state.invoice || flags.bootstrapping);
|
|
748
|
+
const fatal = error && !state.cart && step === 'cart';
|
|
749
|
+
const cartEmpty = !state.cart || state.cart.items.length === 0;
|
|
750
|
+
const footerVisible = (step === 'cart' && !cartEmpty) || step === 'shipping' || step === 'payment';
|
|
751
|
+
const nextBusy = state.status === 'working' ||
|
|
752
|
+
flags.savingAddress ||
|
|
753
|
+
flags.loadingShipping ||
|
|
754
|
+
flags.loadingPayments ||
|
|
755
|
+
flags.placingOrder;
|
|
756
|
+
const needsShipping = state.invoice?.needsShipping !== false;
|
|
757
|
+
const shippingSavePhase = state.addressDirty || !state.applicable;
|
|
758
|
+
const shippingReady = !needsShipping ||
|
|
759
|
+
(state.shippingGroups.length > 0 &&
|
|
760
|
+
state.shippingGroups.every((g) => g.selectedRateId != null));
|
|
761
|
+
const paymentReady = state.paymentMethods.length > 0 && state.selectedPaymentMethodId != null;
|
|
762
|
+
const nextDisabled = step === 'cart'
|
|
763
|
+
? cartEmpty
|
|
764
|
+
: step === 'shipping'
|
|
765
|
+
? !shippingSavePhase && !shippingReady
|
|
766
|
+
: step === 'payment'
|
|
767
|
+
? !paymentReady
|
|
768
|
+
: false;
|
|
769
|
+
const nextLabel = step === 'cart'
|
|
770
|
+
? t.placeOrder
|
|
771
|
+
: step === 'shipping'
|
|
772
|
+
? needsShipping && shippingSavePhase
|
|
773
|
+
? t.saveShippingDetails
|
|
774
|
+
: t.continueToPayment
|
|
775
|
+
: t.finishPurchase;
|
|
776
|
+
const backOnClick = step === 'cart' && continueShoppingUrl
|
|
777
|
+
? () => { window.location.href = continueShoppingUrl; }
|
|
778
|
+
: () => actions.back();
|
|
779
|
+
const backVisible = step !== 'cart' || !!continueShoppingUrl;
|
|
780
|
+
const nextButtonProps = {
|
|
781
|
+
loading: nextBusy,
|
|
782
|
+
disabled: nextDisabled,
|
|
783
|
+
onClick: () => actions.next(),
|
|
784
|
+
children: nextLabel,
|
|
785
|
+
};
|
|
786
|
+
const backButtonProps = {
|
|
787
|
+
loading: false,
|
|
788
|
+
disabled: false,
|
|
789
|
+
onClick: backOnClick,
|
|
790
|
+
children: t.back,
|
|
791
|
+
};
|
|
792
|
+
/* Rendered twice: under the order summary (desktop) and in the sticky
|
|
793
|
+
bottom bar (mobile). CSS shows exactly one of the two. */
|
|
794
|
+
const nextButtonNode = renderNextButton ? (renderNextButton(nextButtonProps)) : (jsx(Button, { className: "szc-footer__next", loading: nextBusy, disabled: nextDisabled, onClick: () => actions.next(), children: nextLabel }));
|
|
795
|
+
const backButtonNode = backVisible ? (renderBackButton ? (renderBackButton(backButtonProps)) : (jsxs(Button, { variant: "ghost", className: "szc-back-btn", onClick: backOnClick, children: [jsx("svg", { className: "szc-back-arrow", viewBox: "0 0 16 16", width: "15", height: "15", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "M9.5 3.5 5 8l4.5 4.5", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }), t.back] }))) : null;
|
|
796
|
+
const actionsVisible = !bootstrapping && !fatal && !isResult && footerVisible;
|
|
797
|
+
/* Mobile header (replaces the stepper on small screens): back chevron at the
|
|
798
|
+
inline start, current step title centered, "step n of m" at the end. */
|
|
799
|
+
const flowSteps = ['cart', 'shipping', 'payment'];
|
|
800
|
+
const flowIndex = flowSteps.indexOf(step);
|
|
801
|
+
const headTitle = step === 'cart'
|
|
802
|
+
? t.stepCart
|
|
803
|
+
: step === 'shipping'
|
|
804
|
+
? t.stepShippingInfo
|
|
805
|
+
: step === 'payment'
|
|
806
|
+
? t.stepPayment
|
|
807
|
+
: t.stepResult;
|
|
808
|
+
const HeadIcon = step === 'cart'
|
|
809
|
+
? CartIcon
|
|
810
|
+
: step === 'shipping'
|
|
811
|
+
? TruckIcon$1
|
|
812
|
+
: step === 'payment'
|
|
813
|
+
? CardIcon
|
|
814
|
+
: ClipboardIcon;
|
|
815
|
+
const mobileHead = (jsxs("div", { className: "szc-mobile-head", children: [footerVisible && backVisible ? (jsx("button", { type: "button", className: "szc-mobile-head__back", "aria-label": t.back, onClick: backOnClick, children: jsx("svg", { className: "szc-back-arrow", viewBox: "0 0 16 16", width: "18", height: "18", fill: "none", "aria-hidden": "true", children: jsx("path", { d: "M9.5 3.5 5 8l4.5 4.5", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round" }) }) })) : (jsx("span", {})), jsxs("span", { className: "szc-mobile-head__title", children: [jsx("span", { className: "szc-mobile-head__dot", children: jsx(HeadIcon, {}) }), headTitle] }), flowIndex >= 0 ? (jsx("span", { className: "szc-mobile-head__count", children: t.stepOf(formatNumber(flowIndex + 1, state.locale), formatNumber(flowSteps.length, state.locale)) })) : (jsx("span", {}))] }));
|
|
816
|
+
return (jsxs("div", { className: `szc-root${className ? ` ${className}` : ''}`, dir: direction, style: themeVars(theme), children: [mobileHead, jsx(Stepper, {}), bootstrapping ? (jsx(CheckoutSkeleton, { label: t.loading })) : fatal ? (jsx(ErrorBanner, { message: error.message })) : isResult ? (jsx("div", { className: "szc-result-wrap", children: jsx(ResultStep, { continueShoppingUrl: continueShoppingUrl }) })) : (jsx(Fragment, { children: jsxs("div", { className: `szc-layout${cartEmpty ? ' szc-layout--full' : ''}`, children: [jsxs("main", { className: "szc-main", children: [error ? jsx(ErrorBanner, { message: error.message }) : null, step === 'cart' ? jsx(CartStep, { continueShoppingUrl: continueShoppingUrl, renderEmpty: renderEmptyCart }) : null, step === 'shipping' ? jsx(ShippingStep, {}) : null, step === 'payment' ? jsx(PaymentStep, {}) : null, actionsVisible && backButtonNode ? (jsx("div", { className: "szc-back-row", children: backButtonNode })) : null] }), !cartEmpty ? (jsxs("div", { className: "szc-side", children: [jsx(OrderSummary, {}), actionsVisible ? jsx("div", { className: "szc-side__cta", children: nextButtonNode }) : null] })) : null] }) })), actionsVisible ? (jsx("div", { className: "szc-footer-bar", children: jsxs("div", { className: "szc-footer", children: [jsxs("div", { className: "szc-footer__total", children: [jsx("span", { className: "szc-footer__total-label", children: t.total }), jsx("span", { className: "szc-footer__total-value", children: money(summary.total) })] }), jsx("div", { className: "szc-footer__actions", children: nextButtonNode })] }) })) : null] }));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function SazitoCheckoutPage({ credentials, config, paymentReturnParams, className, renderNextButton, renderBackButton, renderEmptyCart, }) {
|
|
820
|
+
const isReturn = paymentReturnParams != null;
|
|
821
|
+
const client = useSazitoClient();
|
|
822
|
+
return (jsxs(CheckoutProvider, { client: client, credentials: credentials, config: config, autoStart: !isReturn, children: [isReturn ? jsx(ResolveReturn, { params: paymentReturnParams }) : null, jsx(SazitoCheckout, { theme: config?.theme, continueShoppingUrl: config?.continueShoppingUrl, className: className, renderNextButton: renderNextButton, renderBackButton: renderBackButton, renderEmptyCart: renderEmptyCart })] }));
|
|
823
|
+
}
|
|
824
|
+
function ResolveReturn({ params }) {
|
|
825
|
+
const { actions } = useCheckout();
|
|
826
|
+
useEffect(() => {
|
|
827
|
+
void actions.resolvePaymentReturn(params);
|
|
828
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
829
|
+
}, []);
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
export { Button, CheckoutProvider, ErrorBanner, Field, ProductPlaceholder, SazitoCheckout, SazitoCheckoutPage, SectionTitle, Spinner, useCheckout };
|
|
834
|
+
//# sourceMappingURL=index.js.map
|