@akinon/pz-theme 2.0.37-beta.0 → 2.0.37
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/CHANGELOG.md +4 -4
- package/package.json +4 -4
- package/src/__tests__/__snapshots__/facet-filters-block.test.tsx.snap +11 -0
- package/src/__tests__/custom-css.test.ts +181 -0
- package/src/__tests__/designer-chrome-clipping.test.tsx +497 -0
- package/src/__tests__/facet-filters-block.test.tsx +707 -0
- package/src/__tests__/item-template.test.tsx +184 -0
- package/src/__tests__/listing-controls.test.tsx +961 -0
- package/src/__tests__/part-style-scope.test.tsx +229 -0
- package/src/__tests__/part-styles.test.ts +311 -0
- package/src/__tests__/registry-parity.test.ts +42 -0
- package/src/__tests__/row-visibility.test.ts +672 -0
- package/src/__tests__/sort-select-page-cursor.test.tsx +336 -0
- package/src/__tests__/theme-block-html-id.test.tsx +167 -0
- package/src/blocks/__tests__/__snapshots__/product-gallery-block.test.tsx.snap +15 -0
- package/src/blocks/__tests__/accordion-block-state.test.tsx +201 -0
- package/src/blocks/__tests__/facet-filters-block.test.tsx +190 -0
- package/src/blocks/__tests__/listing-context-card-model.test.tsx +230 -0
- package/src/blocks/__tests__/listing-lookup-hardening.test.ts +80 -0
- package/src/blocks/__tests__/product-gallery-block.test.tsx +974 -0
- package/src/blocks/__tests__/product-grid-composition.test.tsx +570 -0
- package/src/blocks/__tests__/product-grid-lookups.test.tsx +230 -0
- package/src/blocks/__tests__/product-price-block.test.tsx +248 -0
- package/src/blocks/__tests__/product-recommendations-block.test.tsx +702 -0
- package/src/blocks/__tests__/share-block.test.tsx +392 -0
- package/src/blocks/__tests__/store-locator-block.test.tsx +691 -0
- package/src/blocks/__tests__/variant-selector-block.test.tsx +817 -0
- package/src/blocks/__tests__/variant-selector-golden.json +20 -0
- package/src/blocks/account-menu-block.tsx +264 -0
- package/src/blocks/add-to-cart-block.tsx +417 -0
- package/src/blocks/address-list-block.tsx +348 -0
- package/src/blocks/applied-filters-block.tsx +288 -0
- package/src/blocks/auth-form-block.tsx +295 -0
- package/src/blocks/basket-items-block.tsx +457 -0
- package/src/blocks/basket-summary-block.tsx +404 -0
- package/src/blocks/block-renderer-registry.tsx +94 -0
- package/src/blocks/change-password-form-block.tsx +240 -0
- package/src/blocks/checkout-steps-block.tsx +288 -0
- package/src/blocks/checkout-summary-block.tsx +449 -0
- package/src/blocks/countdown-block.tsx +182 -0
- package/src/blocks/coupon-list-block.tsx +292 -0
- package/src/blocks/facet-filters-block.tsx +1686 -0
- package/src/blocks/faq-accordion-block.tsx +358 -0
- package/src/blocks/favorite-button-block.tsx +252 -0
- package/src/blocks/favourites-grid-block.tsx +510 -0
- package/src/blocks/forgot-password-block.tsx +155 -0
- package/src/blocks/item-template-utils.ts +131 -0
- package/src/blocks/item-template.tsx +164 -0
- package/src/blocks/listing-breadcrumb-block.tsx +175 -0
- package/src/blocks/listing-context.ts +850 -0
- package/src/blocks/listing-pagination-block.tsx +923 -0
- package/src/blocks/listing-result-count-block.tsx +126 -0
- package/src/blocks/listing-title-block.tsx +115 -0
- package/src/blocks/oauth-buttons-block.tsx +205 -0
- package/src/blocks/order-detail-block.tsx +474 -0
- package/src/blocks/order-list-block.tsx +274 -0
- package/src/blocks/order-success-block.tsx +641 -0
- package/src/blocks/order-tracking-block.tsx +533 -0
- package/src/blocks/product-attributes-block.tsx +307 -0
- package/src/blocks/product-block-utils.tsx +639 -0
- package/src/blocks/product-breadcrumb-block.tsx +215 -0
- package/src/blocks/product-description-block.tsx +169 -0
- package/src/blocks/product-gallery-block.tsx +1523 -0
- package/src/blocks/product-grid-block.tsx +966 -0
- package/src/blocks/product-price-block.tsx +373 -0
- package/src/blocks/product-recommendations-block.tsx +1433 -0
- package/src/blocks/product-title-block.tsx +129 -0
- package/src/blocks/profile-form-block.tsx +297 -0
- package/src/blocks/recently-viewed-store.ts +201 -0
- package/src/blocks/register-form-block.tsx +362 -0
- package/src/blocks/share-block.tsx +667 -0
- package/src/blocks/size-guide-block.tsx +313 -0
- package/src/blocks/sort-select-block.tsx +636 -0
- package/src/blocks/stock-status-block.tsx +168 -0
- package/src/blocks/store-locator-block.tsx +1184 -0
- package/src/blocks/variant-selector-block.tsx +1110 -0
- package/src/sections/__tests__/tablet-raw-bucket-readers.test.tsx +186 -0
- package/src/utils/__tests__/part-ownership-guard.test.tsx +29 -4
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { buildClientRequestUrl, getCookie } from '@akinon/next/utils';
|
|
5
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
6
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
7
|
+
import { formatDate, formatMoney } from './commerce-format';
|
|
8
|
+
import { hasItemTemplate, ItemTemplateList } from './item-template';
|
|
9
|
+
import { readToggle } from './product-block-utils';
|
|
10
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Anonymous lookup endpoint. Commerce exposes it under /users/, and the core
|
|
14
|
+
* `account` url map (`anonymousOrderTracking`) is not imported here because
|
|
15
|
+
* that module pulls in brand `settings` at import time. Brands that mount it
|
|
16
|
+
* elsewhere override it through the `endpoint` property.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_ENDPOINT = '/users/orders/anonymous/';
|
|
19
|
+
|
|
20
|
+
interface TrackedOrderItem {
|
|
21
|
+
id?: number;
|
|
22
|
+
price?: string;
|
|
23
|
+
status?: { label?: string };
|
|
24
|
+
product?: { pk?: number; name?: string; image?: string } | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface TrackedOrder {
|
|
28
|
+
id?: number;
|
|
29
|
+
number?: string;
|
|
30
|
+
amount?: string;
|
|
31
|
+
created_date?: string;
|
|
32
|
+
currency?: { value?: string };
|
|
33
|
+
status?: { label?: string };
|
|
34
|
+
orderitem_set?: TrackedOrderItem[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ResultRow {
|
|
38
|
+
key: string;
|
|
39
|
+
label: string;
|
|
40
|
+
value: string;
|
|
41
|
+
type: 'number' | 'date' | 'status' | 'total';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type LookupState = 'idle' | 'busy' | 'found' | 'error';
|
|
45
|
+
|
|
46
|
+
const readCount = (value: unknown, fallback: number): number => {
|
|
47
|
+
const numeric = Math.round(Number(value));
|
|
48
|
+
|
|
49
|
+
return Number.isFinite(numeric) && numeric >= 0 ? numeric : fallback;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Anonymous order lookup: order number plus e-mail in, order status out.
|
|
54
|
+
* Mirrors the brand's anonymous-orders view — a CSRF-protected POST to the
|
|
55
|
+
* commerce anonymous order endpoint through the client proxy, which returns a
|
|
56
|
+
* list whose first entry is the matching order.
|
|
57
|
+
*
|
|
58
|
+
* Read-only by construction: it can display an order but has no cancel,
|
|
59
|
+
* return or re-order affordance, so nothing here mutates an order.
|
|
60
|
+
*
|
|
61
|
+
* Parts: root, title, form, field, label, input, submit, error, result, row,
|
|
62
|
+
* row-label, row-value, number, status, list, item, image, content, name,
|
|
63
|
+
* item-status, line-total, reset. A child marked as the item template replaces
|
|
64
|
+
* the built-in order line.
|
|
65
|
+
*/
|
|
66
|
+
const OrderTrackingBlock = ({
|
|
67
|
+
block,
|
|
68
|
+
placeholderId,
|
|
69
|
+
sectionId,
|
|
70
|
+
selectedBlockId = null,
|
|
71
|
+
currentBreakpoint = 'desktop',
|
|
72
|
+
isDesigner = false
|
|
73
|
+
}: BlockRendererProps) => {
|
|
74
|
+
const prop = (key: string, fallback: string) =>
|
|
75
|
+
String(
|
|
76
|
+
getResponsiveValue(block.properties?.[key], currentBreakpoint, fallback)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const boolProp = (key: string, fallback: boolean) =>
|
|
80
|
+
readToggle(block.properties?.[key], currentBreakpoint, fallback);
|
|
81
|
+
|
|
82
|
+
const title = prop('title', 'Track your order');
|
|
83
|
+
const numberLabel = prop('numberLabel', 'Order number');
|
|
84
|
+
const emailLabel = prop('emailLabel', 'E-mail');
|
|
85
|
+
const submitLabel = prop('submitLabel', 'Find my order');
|
|
86
|
+
const submittingLabel = prop('submittingLabel', 'Searching…');
|
|
87
|
+
const notFoundLabel = prop(
|
|
88
|
+
'notFoundLabel',
|
|
89
|
+
'No order matches that number and e-mail.'
|
|
90
|
+
);
|
|
91
|
+
const dateLabel = prop('dateLabel', 'Order date');
|
|
92
|
+
const statusLabel = prop('statusLabel', 'Status');
|
|
93
|
+
const totalLabel = prop('totalLabel', 'Total');
|
|
94
|
+
const resetLabel = prop('resetLabel', '').trim();
|
|
95
|
+
const numberPlaceholder = prop('numberPlaceholder', '');
|
|
96
|
+
const emailPlaceholder = prop('emailPlaceholder', '');
|
|
97
|
+
const endpoint = prop('endpoint', DEFAULT_ENDPOINT).trim() || DEFAULT_ENDPOINT;
|
|
98
|
+
const showLineItems = boolProp('showLineItems', true);
|
|
99
|
+
const showItemImage = boolProp('showItemImage', true);
|
|
100
|
+
const showItemStatus = boolProp('showItemStatus', true);
|
|
101
|
+
const showOrderDate = boolProp('showOrderDate', true);
|
|
102
|
+
const showOrderStatus = boolProp('showOrderStatus', true);
|
|
103
|
+
const showTotal = boolProp('showTotal', true);
|
|
104
|
+
|
|
105
|
+
const imageWidth = readCount(
|
|
106
|
+
getResponsiveValue(block.properties?.imageWidth, currentBreakpoint, 56),
|
|
107
|
+
56
|
|
108
|
+
);
|
|
109
|
+
const imageHeight = readCount(
|
|
110
|
+
getResponsiveValue(block.properties?.imageHeight, currentBreakpoint, 75),
|
|
111
|
+
75
|
|
112
|
+
);
|
|
113
|
+
const maxItems = readCount(
|
|
114
|
+
getResponsiveValue(block.properties?.maxItems, currentBreakpoint, 0),
|
|
115
|
+
0
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const { locale, currency: localeCurrency, locales } = useLocalization();
|
|
119
|
+
|
|
120
|
+
const [values, setValues] = useState({ number: '', email: '' });
|
|
121
|
+
const [state, setState] = useState<LookupState>('idle');
|
|
122
|
+
const [order, setOrder] = useState<TrackedOrder | null>(null);
|
|
123
|
+
|
|
124
|
+
const setField = (key: keyof typeof values) => (
|
|
125
|
+
event: React.ChangeEvent<HTMLInputElement>
|
|
126
|
+
) => {
|
|
127
|
+
const nextValue = event.target.value;
|
|
128
|
+
setValues((current) => ({ ...current, [key]: nextValue }));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const submit = async (event: React.FormEvent) => {
|
|
132
|
+
event.preventDefault();
|
|
133
|
+
|
|
134
|
+
// A lookup from inside the designer would send a real shopper's e-mail to
|
|
135
|
+
// commerce from the editor iframe, so the form is inert there.
|
|
136
|
+
if (isDesigner || state === 'busy') return;
|
|
137
|
+
|
|
138
|
+
setState('busy');
|
|
139
|
+
setOrder(null);
|
|
140
|
+
|
|
141
|
+
// Every other client request goes through RTK Query's `customBaseQuery`
|
|
142
|
+
// (akinon-next/data/client/api.ts), whose `prepareHeaders` derives the
|
|
143
|
+
// shopper's locale and currency from the pz cookies. This raw fetch has to
|
|
144
|
+
// derive them the same way: without the two headers commerce answers in the
|
|
145
|
+
// backend default locale and currency, while the result below formats the
|
|
146
|
+
// amounts with the shopper's currency — so a USD shopper reads TRY numbers
|
|
147
|
+
// labelled USD.
|
|
148
|
+
const activeLocale = getCookie('pz-locale') || locale;
|
|
149
|
+
// Commerce expects the locale's `apiValue` ("en-us"), not the pz locale
|
|
150
|
+
// value ("en"), which is why the settings locale list is looked up.
|
|
151
|
+
const localeApiValue = locales?.find(
|
|
152
|
+
(entry) => entry.value === activeLocale
|
|
153
|
+
)?.apiValue;
|
|
154
|
+
const activeCurrency = getCookie('pz-currency') || localeCurrency;
|
|
155
|
+
|
|
156
|
+
const headers: Record<string, string> = {
|
|
157
|
+
// Commerce rejects the POST without the Django CSRF header; the
|
|
158
|
+
// cookie is set by the storefront on first render.
|
|
159
|
+
'X-CSRFToken': getCookie('csrftoken') || ''
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Only sent once resolved: a literal "undefined" header value would make
|
|
163
|
+
// commerce reject the negotiation instead of falling back on its default.
|
|
164
|
+
if (localeApiValue) headers['Accept-Language'] = localeApiValue;
|
|
165
|
+
if (activeCurrency) headers['x-currency'] = activeCurrency;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const response = await fetch(
|
|
169
|
+
buildClientRequestUrl(endpoint, {
|
|
170
|
+
contentType: 'application/json',
|
|
171
|
+
responseType: 'json'
|
|
172
|
+
}),
|
|
173
|
+
{
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers,
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
email: values.email.trim(),
|
|
178
|
+
number: values.number.trim()
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
setState('error');
|
|
185
|
+
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const payload = await response.json();
|
|
190
|
+
// The endpoint answers with a list; an empty list means "no match",
|
|
191
|
+
// which is a not-found, not a transport failure.
|
|
192
|
+
const found = Array.isArray(payload) ? payload[0] : payload;
|
|
193
|
+
|
|
194
|
+
if (!found) {
|
|
195
|
+
setState('error');
|
|
196
|
+
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
setOrder(found as TrackedOrder);
|
|
201
|
+
setState('found');
|
|
202
|
+
} catch {
|
|
203
|
+
setState('error');
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// The lookup never runs in the designer, so the author would only ever see
|
|
208
|
+
// the idle form. `previewState` lets them open the other two states to style
|
|
209
|
+
// them; on the storefront the real state always wins.
|
|
210
|
+
const previewState = prop('previewState', 'idle') as LookupState;
|
|
211
|
+
const activeState: LookupState = isDesigner ? previewState : state;
|
|
212
|
+
|
|
213
|
+
const currency = order?.currency?.value || localeCurrency || '';
|
|
214
|
+
const allItems = Array.isArray(order?.orderitem_set) ? order.orderitem_set : [];
|
|
215
|
+
const items = maxItems > 0 ? allItems.slice(0, maxItems) : allItems;
|
|
216
|
+
|
|
217
|
+
const resultRows: ResultRow[] = [
|
|
218
|
+
{
|
|
219
|
+
key: 'number',
|
|
220
|
+
label: numberLabel,
|
|
221
|
+
value: order?.number || (order?.id ? `#${order.id}` : ''),
|
|
222
|
+
type: 'number'
|
|
223
|
+
}
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
if (showOrderDate && order?.created_date) {
|
|
227
|
+
resultRows.push({
|
|
228
|
+
key: 'date',
|
|
229
|
+
label: dateLabel,
|
|
230
|
+
value: formatDate(order.created_date, locale),
|
|
231
|
+
type: 'date'
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (showOrderStatus && order?.status?.label) {
|
|
236
|
+
resultRows.push({
|
|
237
|
+
key: 'status',
|
|
238
|
+
label: statusLabel,
|
|
239
|
+
value: order.status.label,
|
|
240
|
+
type: 'status'
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (showTotal) {
|
|
245
|
+
resultRows.push({
|
|
246
|
+
key: 'total',
|
|
247
|
+
label: totalLabel,
|
|
248
|
+
value: formatMoney(order?.amount, { locale, currency }),
|
|
249
|
+
type: 'total'
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Plain objects so a designer binds item.name / item.image / item.price /
|
|
254
|
+
// item.status in their own row layout.
|
|
255
|
+
const templateItems = items.map((item, index) => ({
|
|
256
|
+
key: String(item.id ?? item.product?.pk ?? index),
|
|
257
|
+
name: item.product?.name || '',
|
|
258
|
+
image: item.product?.image || '',
|
|
259
|
+
status: item.status?.label || '',
|
|
260
|
+
price: formatMoney(item.price, { locale, currency })
|
|
261
|
+
}));
|
|
262
|
+
|
|
263
|
+
const customRows = hasItemTemplate(block, currentBreakpoint) ? (
|
|
264
|
+
<ItemTemplateList
|
|
265
|
+
block={block}
|
|
266
|
+
items={templateItems as unknown as Record<string, unknown>[]}
|
|
267
|
+
placeholderId={placeholderId}
|
|
268
|
+
sectionId={sectionId}
|
|
269
|
+
isDesigner={isDesigner}
|
|
270
|
+
selectedBlockId={selectedBlockId}
|
|
271
|
+
currentBreakpoint={currentBreakpoint}
|
|
272
|
+
/>
|
|
273
|
+
) : null;
|
|
274
|
+
|
|
275
|
+
const inputStyle: React.CSSProperties = {
|
|
276
|
+
width: '100%',
|
|
277
|
+
border: '1px solid var(--order-tracking-border, #d1d5db)',
|
|
278
|
+
background: 'transparent',
|
|
279
|
+
color: 'inherit',
|
|
280
|
+
padding: '10px 12px',
|
|
281
|
+
outline: 'none'
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const rowStyle: React.CSSProperties = {
|
|
285
|
+
display: 'flex',
|
|
286
|
+
justifyContent: 'space-between',
|
|
287
|
+
gap: '12px',
|
|
288
|
+
padding: '4px 0'
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const isBusy = activeState === 'busy';
|
|
292
|
+
const hasResult = activeState === 'found' && (order !== null || isDesigner);
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<div
|
|
296
|
+
data-order-tracking=""
|
|
297
|
+
data-order-tracking-state={activeState}
|
|
298
|
+
{...partAttrs('root', { loading: isBusy })}
|
|
299
|
+
style={{
|
|
300
|
+
display: 'flex',
|
|
301
|
+
flexDirection: 'column',
|
|
302
|
+
gap: 'var(--order-tracking-gap, 16px)'
|
|
303
|
+
}}
|
|
304
|
+
>
|
|
305
|
+
{title && (
|
|
306
|
+
<div
|
|
307
|
+
data-order-tracking-title=""
|
|
308
|
+
{...partAttrs('title')}
|
|
309
|
+
style={{ fontWeight: 600 }}
|
|
310
|
+
>
|
|
311
|
+
{title}
|
|
312
|
+
</div>
|
|
313
|
+
)}
|
|
314
|
+
|
|
315
|
+
{activeState !== 'found' && (
|
|
316
|
+
<form
|
|
317
|
+
data-order-tracking-form=""
|
|
318
|
+
onSubmit={submit}
|
|
319
|
+
{...partAttrs('form', { loading: isBusy })}
|
|
320
|
+
style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}
|
|
321
|
+
>
|
|
322
|
+
<label
|
|
323
|
+
data-order-tracking-field="number"
|
|
324
|
+
{...partAttrs('field')}
|
|
325
|
+
style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}
|
|
326
|
+
>
|
|
327
|
+
<span {...partAttrs('label')}>{numberLabel}</span>
|
|
328
|
+
<input
|
|
329
|
+
type="text"
|
|
330
|
+
name="number"
|
|
331
|
+
value={values.number}
|
|
332
|
+
onChange={setField('number')}
|
|
333
|
+
autoComplete="off"
|
|
334
|
+
required
|
|
335
|
+
placeholder={numberPlaceholder || undefined}
|
|
336
|
+
{...partAttrs('input', { invalid: activeState === 'error' })}
|
|
337
|
+
style={inputStyle}
|
|
338
|
+
/>
|
|
339
|
+
</label>
|
|
340
|
+
|
|
341
|
+
<label
|
|
342
|
+
data-order-tracking-field="email"
|
|
343
|
+
{...partAttrs('field')}
|
|
344
|
+
style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}
|
|
345
|
+
>
|
|
346
|
+
<span {...partAttrs('label')}>{emailLabel}</span>
|
|
347
|
+
<input
|
|
348
|
+
type="email"
|
|
349
|
+
name="email"
|
|
350
|
+
value={values.email}
|
|
351
|
+
onChange={setField('email')}
|
|
352
|
+
autoComplete="email"
|
|
353
|
+
required
|
|
354
|
+
placeholder={emailPlaceholder || undefined}
|
|
355
|
+
{...partAttrs('input', { invalid: activeState === 'error' })}
|
|
356
|
+
style={inputStyle}
|
|
357
|
+
/>
|
|
358
|
+
</label>
|
|
359
|
+
|
|
360
|
+
{activeState === 'error' && (
|
|
361
|
+
<div
|
|
362
|
+
data-order-tracking-error=""
|
|
363
|
+
role="alert"
|
|
364
|
+
{...partAttrs('error', { invalid: true })}
|
|
365
|
+
>
|
|
366
|
+
{notFoundLabel}
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
|
|
370
|
+
<button
|
|
371
|
+
type="submit"
|
|
372
|
+
data-order-tracking-submit=""
|
|
373
|
+
disabled={isBusy}
|
|
374
|
+
aria-busy={isBusy}
|
|
375
|
+
{...partAttrs('submit', { disabled: isBusy, loading: isBusy })}
|
|
376
|
+
style={{
|
|
377
|
+
padding: '12px 20px',
|
|
378
|
+
background: 'var(--order-tracking-cta-bg, #111827)',
|
|
379
|
+
color: 'var(--order-tracking-cta-color, #ffffff)',
|
|
380
|
+
border: 'none',
|
|
381
|
+
cursor: isBusy ? 'not-allowed' : 'pointer',
|
|
382
|
+
opacity: isBusy ? 0.7 : 1
|
|
383
|
+
}}
|
|
384
|
+
>
|
|
385
|
+
{isBusy ? submittingLabel : submitLabel}
|
|
386
|
+
</button>
|
|
387
|
+
</form>
|
|
388
|
+
)}
|
|
389
|
+
|
|
390
|
+
{hasResult && (
|
|
391
|
+
<div data-order-tracking-result="" {...partAttrs('result')}>
|
|
392
|
+
{resultRows.map((row) =>
|
|
393
|
+
row.type === 'total' ? (
|
|
394
|
+
<div
|
|
395
|
+
key={row.key}
|
|
396
|
+
style={{ ...rowStyle, fontWeight: 600 }}
|
|
397
|
+
data-order-tracking-row="total"
|
|
398
|
+
{...partAttrs('row')}
|
|
399
|
+
>
|
|
400
|
+
<span {...partAttrs('row-label')}>{row.label}</span>
|
|
401
|
+
<span {...partAttrs('row-value')}>{row.value}</span>
|
|
402
|
+
</div>
|
|
403
|
+
) : (
|
|
404
|
+
<div
|
|
405
|
+
key={row.key}
|
|
406
|
+
style={rowStyle}
|
|
407
|
+
data-order-tracking-row={row.type}
|
|
408
|
+
{...partAttrs('row')}
|
|
409
|
+
>
|
|
410
|
+
<span {...partAttrs('row-label')}>{row.label}</span>
|
|
411
|
+
<span
|
|
412
|
+
{...(row.type === 'number'
|
|
413
|
+
? { 'data-order-tracking-number': '' }
|
|
414
|
+
: null)}
|
|
415
|
+
{...(row.type === 'status'
|
|
416
|
+
? { 'data-order-tracking-status': '' }
|
|
417
|
+
: null)}
|
|
418
|
+
{...partAttrs(row.type === 'status' ? 'status' : 'row-value')}
|
|
419
|
+
>
|
|
420
|
+
{row.value}
|
|
421
|
+
</span>
|
|
422
|
+
</div>
|
|
423
|
+
)
|
|
424
|
+
)}
|
|
425
|
+
|
|
426
|
+
{showLineItems && (items.length > 0 || customRows) && (
|
|
427
|
+
<ul
|
|
428
|
+
data-order-tracking-items=""
|
|
429
|
+
{...partAttrs('list')}
|
|
430
|
+
style={{
|
|
431
|
+
display: 'flex',
|
|
432
|
+
flexDirection: 'column',
|
|
433
|
+
gap: '12px',
|
|
434
|
+
listStyle: 'none',
|
|
435
|
+
margin: '12px 0 0',
|
|
436
|
+
padding: 0
|
|
437
|
+
}}
|
|
438
|
+
>
|
|
439
|
+
{customRows ??
|
|
440
|
+
items.map((item, index) => (
|
|
441
|
+
<li
|
|
442
|
+
key={item.id ?? item.product?.pk ?? index}
|
|
443
|
+
data-order-tracking-item=""
|
|
444
|
+
{...partAttrs('item')}
|
|
445
|
+
style={{
|
|
446
|
+
display: 'flex',
|
|
447
|
+
alignItems: 'flex-start',
|
|
448
|
+
gap: '12px'
|
|
449
|
+
}}
|
|
450
|
+
>
|
|
451
|
+
{showItemImage && item.product?.image && (
|
|
452
|
+
// Plain img: order thumbnails come from arbitrary commerce
|
|
453
|
+
// CDNs that the brand next.config may not allow.
|
|
454
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
455
|
+
<img
|
|
456
|
+
src={item.product.image}
|
|
457
|
+
alt={item.product?.name || ''}
|
|
458
|
+
width={imageWidth}
|
|
459
|
+
height={imageHeight}
|
|
460
|
+
{...partAttrs('image')}
|
|
461
|
+
style={{
|
|
462
|
+
objectFit: 'cover',
|
|
463
|
+
display: 'block',
|
|
464
|
+
flex: '0 0 auto'
|
|
465
|
+
}}
|
|
466
|
+
/>
|
|
467
|
+
)}
|
|
468
|
+
|
|
469
|
+
<div
|
|
470
|
+
{...partAttrs('content')}
|
|
471
|
+
style={{ flex: '1 1 auto', minWidth: 0 }}
|
|
472
|
+
>
|
|
473
|
+
<span
|
|
474
|
+
data-order-tracking-item-name=""
|
|
475
|
+
{...partAttrs('name')}
|
|
476
|
+
>
|
|
477
|
+
{item.product?.name || ''}
|
|
478
|
+
</span>
|
|
479
|
+
{showItemStatus && item.status?.label && (
|
|
480
|
+
<div
|
|
481
|
+
data-order-tracking-item-status=""
|
|
482
|
+
{...partAttrs('item-status')}
|
|
483
|
+
style={{ marginTop: '2px', opacity: 0.8 }}
|
|
484
|
+
>
|
|
485
|
+
{item.status.label}
|
|
486
|
+
</div>
|
|
487
|
+
)}
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
<span
|
|
491
|
+
data-order-tracking-item-price=""
|
|
492
|
+
{...partAttrs('line-total')}
|
|
493
|
+
style={{ flex: '0 0 auto' }}
|
|
494
|
+
>
|
|
495
|
+
{formatMoney(item.price, { locale, currency })}
|
|
496
|
+
</span>
|
|
497
|
+
</li>
|
|
498
|
+
))}
|
|
499
|
+
</ul>
|
|
500
|
+
)}
|
|
501
|
+
|
|
502
|
+
{resetLabel && (
|
|
503
|
+
<button
|
|
504
|
+
type="button"
|
|
505
|
+
data-order-tracking-reset=""
|
|
506
|
+
onClick={() => {
|
|
507
|
+
// Designer clicks must not clear a preview the author is styling.
|
|
508
|
+
if (isDesigner) return;
|
|
509
|
+
|
|
510
|
+
setOrder(null);
|
|
511
|
+
setState('idle');
|
|
512
|
+
}}
|
|
513
|
+
{...partAttrs('reset')}
|
|
514
|
+
style={{
|
|
515
|
+
marginTop: '12px',
|
|
516
|
+
background: 'none',
|
|
517
|
+
border: 'none',
|
|
518
|
+
color: 'inherit',
|
|
519
|
+
cursor: 'pointer',
|
|
520
|
+
textDecoration: 'underline',
|
|
521
|
+
padding: 0
|
|
522
|
+
}}
|
|
523
|
+
>
|
|
524
|
+
{resetLabel}
|
|
525
|
+
</button>
|
|
526
|
+
)}
|
|
527
|
+
</div>
|
|
528
|
+
)}
|
|
529
|
+
</div>
|
|
530
|
+
);
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
export default OrderTrackingBlock;
|