@akinon/pz-theme 2.0.37-rc.0 → 2.0.38-beta.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/CHANGELOG.md +15 -31
- 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,274 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import NextLink from 'next/link';
|
|
5
|
+
import { useSession } from 'next-auth/react';
|
|
6
|
+
import { useGetOrdersQuery } from '@akinon/next/data/client/account';
|
|
7
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
8
|
+
import { getResponsiveValue, partAttrs } from '../utils';
|
|
9
|
+
import { formatDate, formatMoney } from './commerce-format';
|
|
10
|
+
import { hasItemTemplate, ItemTemplateList } from './item-template';
|
|
11
|
+
import { readToggle } from './product-block-utils';
|
|
12
|
+
import { BlockRendererProps } from './block-renderer-registry';
|
|
13
|
+
|
|
14
|
+
interface OrderLike {
|
|
15
|
+
id?: number;
|
|
16
|
+
number?: string;
|
|
17
|
+
amount?: string;
|
|
18
|
+
created_date?: string;
|
|
19
|
+
status?: { value?: string; label?: string };
|
|
20
|
+
currency?: { value?: string; label?: string };
|
|
21
|
+
orderitem_set?: unknown[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Newest/oldest first without mutating the array RTK Query handed us. */
|
|
25
|
+
const sortOrders = (orders: OrderLike[], sort: string): OrderLike[] => {
|
|
26
|
+
if (sort !== 'newest' && sort !== 'oldest') return orders;
|
|
27
|
+
|
|
28
|
+
const time = (order: OrderLike) => {
|
|
29
|
+
const parsed = new Date(order.created_date as string).getTime();
|
|
30
|
+
|
|
31
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return [...orders].sort((a, b) =>
|
|
35
|
+
sort === 'newest' ? time(b) - time(a) : time(a) - time(b)
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Functional account block: the customer's most recent orders. Order CONTENT
|
|
41
|
+
* stays commerce-driven (account orders service); the editor owns how many
|
|
42
|
+
* rows are shown, which fields the row carries, where a row links to and the
|
|
43
|
+
* styling.
|
|
44
|
+
* Properties: limit, orderHrefBase, sort, showDate, showStatus, showAmount,
|
|
45
|
+
* emptyLabel.
|
|
46
|
+
* Parts: list, item, link, date, total, status, empty.
|
|
47
|
+
*/
|
|
48
|
+
const OrderListBlock = ({
|
|
49
|
+
block,
|
|
50
|
+
placeholderId,
|
|
51
|
+
sectionId,
|
|
52
|
+
selectedBlockId = null,
|
|
53
|
+
currentBreakpoint = 'desktop',
|
|
54
|
+
isDesigner = false
|
|
55
|
+
}: BlockRendererProps) => {
|
|
56
|
+
const rawLimit = Number(
|
|
57
|
+
getResponsiveValue(block.properties?.limit, currentBreakpoint, 5)
|
|
58
|
+
);
|
|
59
|
+
const limit =
|
|
60
|
+
Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 20) : 5;
|
|
61
|
+
const orderHrefBase = String(
|
|
62
|
+
getResponsiveValue(
|
|
63
|
+
block.properties?.orderHrefBase,
|
|
64
|
+
currentBreakpoint,
|
|
65
|
+
'/account/orders'
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
const sort = String(
|
|
69
|
+
getResponsiveValue(block.properties?.sort, currentBreakpoint, 'default')
|
|
70
|
+
);
|
|
71
|
+
const showDate = readToggle(
|
|
72
|
+
block.properties?.showDate,
|
|
73
|
+
currentBreakpoint,
|
|
74
|
+
true
|
|
75
|
+
);
|
|
76
|
+
const showStatus = readToggle(
|
|
77
|
+
block.properties?.showStatus,
|
|
78
|
+
currentBreakpoint,
|
|
79
|
+
true
|
|
80
|
+
);
|
|
81
|
+
const showAmount = readToggle(
|
|
82
|
+
block.properties?.showAmount,
|
|
83
|
+
currentBreakpoint,
|
|
84
|
+
true
|
|
85
|
+
);
|
|
86
|
+
const emptyLabel = String(
|
|
87
|
+
getResponsiveValue(block.properties?.emptyLabel, currentBreakpoint, '')
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const { locale, currency: localeCurrency } = useLocalization();
|
|
91
|
+
const { status } = useSession();
|
|
92
|
+
const isAuthenticated = status === 'authenticated';
|
|
93
|
+
|
|
94
|
+
// Guests have no orders endpoint; skipping avoids a guaranteed 403 on every
|
|
95
|
+
// page that hosts this block.
|
|
96
|
+
const { data, isLoading } = useGetOrdersQuery(
|
|
97
|
+
{ limit, page: 1 },
|
|
98
|
+
{ skip: !isAuthenticated }
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const orders: OrderLike[] =
|
|
102
|
+
(data as { results?: OrderLike[] } | undefined)?.results ?? [];
|
|
103
|
+
|
|
104
|
+
// Navigating inside the designer iframe would drop the author out of the
|
|
105
|
+
// page they are editing, so the order links are inert there.
|
|
106
|
+
const guard = (event: React.MouseEvent) => {
|
|
107
|
+
if (isDesigner) event.preventDefault();
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const hrefFor = (order: OrderLike) =>
|
|
111
|
+
order.id ? `${orderHrefBase.replace(/\/$/, '')}/${order.id}` : orderHrefBase;
|
|
112
|
+
|
|
113
|
+
const visibleOrders = sortOrders(orders, sort).slice(0, limit);
|
|
114
|
+
|
|
115
|
+
const rows = visibleOrders.map((order, index) => ({
|
|
116
|
+
id: order.id ?? '',
|
|
117
|
+
key: String(order.id ?? order.number ?? index),
|
|
118
|
+
number: order.number || `#${order.id ?? ''}`,
|
|
119
|
+
name: order.number || `#${order.id ?? ''}`,
|
|
120
|
+
// Every field is published whatever the show/hide toggles say: those
|
|
121
|
+
// govern the built-in row, and a designer's own layout decides for itself.
|
|
122
|
+
date: formatDate(order.created_date, locale),
|
|
123
|
+
total: formatMoney(order.amount, {
|
|
124
|
+
locale,
|
|
125
|
+
currency: order.currency?.value || localeCurrency
|
|
126
|
+
}),
|
|
127
|
+
price: formatMoney(order.amount, {
|
|
128
|
+
locale,
|
|
129
|
+
currency: order.currency?.value || localeCurrency
|
|
130
|
+
}),
|
|
131
|
+
status: order.status?.label || '',
|
|
132
|
+
statusValue: order.status?.value || '',
|
|
133
|
+
itemCount: Array.isArray(order.orderitem_set)
|
|
134
|
+
? order.orderitem_set.length
|
|
135
|
+
: 0,
|
|
136
|
+
url: hrefFor(order),
|
|
137
|
+
href: hrefFor(order)
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
// A designer who marked a child as the row layout gets that layout repeated
|
|
141
|
+
// per order; otherwise the built-in row renders unchanged.
|
|
142
|
+
const customRows = hasItemTemplate(block, currentBreakpoint) ? (
|
|
143
|
+
<ItemTemplateList
|
|
144
|
+
block={block}
|
|
145
|
+
items={rows}
|
|
146
|
+
placeholderId={placeholderId}
|
|
147
|
+
sectionId={sectionId}
|
|
148
|
+
isDesigner={isDesigner}
|
|
149
|
+
selectedBlockId={selectedBlockId}
|
|
150
|
+
currentBreakpoint={currentBreakpoint}
|
|
151
|
+
/>
|
|
152
|
+
) : null;
|
|
153
|
+
|
|
154
|
+
if (orders.length === 0 && !customRows) {
|
|
155
|
+
if (isDesigner) {
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
data-order-list=""
|
|
159
|
+
data-order-list-state="empty"
|
|
160
|
+
{...partAttrs('empty', { empty: true })}
|
|
161
|
+
>
|
|
162
|
+
{isLoading
|
|
163
|
+
? 'Recent orders (loading)'
|
|
164
|
+
: emptyLabel || 'Recent orders (no orders)'}
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// The shopper-facing empty copy is only honest once the account query has
|
|
170
|
+
// resolved; before that (and without a message) the block stays absent,
|
|
171
|
+
// which is what every published composition renders today.
|
|
172
|
+
if (!isAuthenticated || isLoading || !emptyLabel) return null;
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div
|
|
176
|
+
data-order-list=""
|
|
177
|
+
data-order-list-state="empty"
|
|
178
|
+
{...partAttrs('empty', { empty: true })}
|
|
179
|
+
>
|
|
180
|
+
{emptyLabel}
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<ul
|
|
187
|
+
data-order-list=""
|
|
188
|
+
{...partAttrs('list')}
|
|
189
|
+
style={{
|
|
190
|
+
display: 'flex',
|
|
191
|
+
flexDirection: 'column',
|
|
192
|
+
gap: 'var(--order-list-gap, 12px)',
|
|
193
|
+
listStyle: 'none',
|
|
194
|
+
margin: 0,
|
|
195
|
+
padding: 0
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
{customRows ??
|
|
199
|
+
visibleOrders.map((order, index) => {
|
|
200
|
+
const href = hrefFor(order);
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<li
|
|
204
|
+
key={order.id ?? order.number ?? index}
|
|
205
|
+
data-order-list-item=""
|
|
206
|
+
{...partAttrs('item')}
|
|
207
|
+
style={{
|
|
208
|
+
display: 'flex',
|
|
209
|
+
alignItems: 'center',
|
|
210
|
+
justifyContent: 'space-between',
|
|
211
|
+
gap: '12px',
|
|
212
|
+
padding: '12px 0',
|
|
213
|
+
borderBottom: '1px solid var(--order-list-border, #e5e7eb)'
|
|
214
|
+
}}
|
|
215
|
+
>
|
|
216
|
+
<div style={{ minWidth: 0 }}>
|
|
217
|
+
<NextLink
|
|
218
|
+
href={isDesigner ? '#' : href}
|
|
219
|
+
onClick={guard}
|
|
220
|
+
data-order-list-number=""
|
|
221
|
+
{...partAttrs('link')}
|
|
222
|
+
style={{ color: 'inherit', textDecoration: 'none' }}
|
|
223
|
+
>
|
|
224
|
+
{order.number || `#${order.id ?? ''}`}
|
|
225
|
+
</NextLink>
|
|
226
|
+
{showDate && (
|
|
227
|
+
<div
|
|
228
|
+
data-order-list-date=""
|
|
229
|
+
{...partAttrs('date')}
|
|
230
|
+
style={{ opacity: 0.8, marginTop: '2px' }}
|
|
231
|
+
>
|
|
232
|
+
{formatDate(order.created_date, locale)}
|
|
233
|
+
</div>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div
|
|
238
|
+
style={{
|
|
239
|
+
display: 'flex',
|
|
240
|
+
flexDirection: 'column',
|
|
241
|
+
alignItems: 'flex-end'
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
{showAmount && (
|
|
245
|
+
<span data-order-list-amount="" {...partAttrs('total')}>
|
|
246
|
+
{/* The order carries the currency it was placed in; the
|
|
247
|
+
storefront currency is only the fallback. */}
|
|
248
|
+
{formatMoney(order.amount, {
|
|
249
|
+
locale,
|
|
250
|
+
currency: order.currency?.value || localeCurrency
|
|
251
|
+
})}
|
|
252
|
+
</span>
|
|
253
|
+
)}
|
|
254
|
+
{showStatus && order.status?.label && (
|
|
255
|
+
<span
|
|
256
|
+
data-order-list-status=""
|
|
257
|
+
// The raw status code is published so scoped custom CSS can
|
|
258
|
+
// paint one status differently from the rest.
|
|
259
|
+
data-order-status-value={order.status.value}
|
|
260
|
+
{...partAttrs('status')}
|
|
261
|
+
style={{ opacity: 0.8 }}
|
|
262
|
+
>
|
|
263
|
+
{order.status.label}
|
|
264
|
+
</span>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
</li>
|
|
268
|
+
);
|
|
269
|
+
})}
|
|
270
|
+
</ul>
|
|
271
|
+
);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
export default OrderListBlock;
|