@akinon/pz-theme 2.0.37-rc.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +7 -31
  2. package/package.json +4 -4
  3. package/src/__tests__/__snapshots__/facet-filters-block.test.tsx.snap +11 -0
  4. package/src/__tests__/custom-css.test.ts +181 -0
  5. package/src/__tests__/designer-chrome-clipping.test.tsx +497 -0
  6. package/src/__tests__/facet-filters-block.test.tsx +707 -0
  7. package/src/__tests__/item-template.test.tsx +184 -0
  8. package/src/__tests__/listing-controls.test.tsx +961 -0
  9. package/src/__tests__/part-style-scope.test.tsx +229 -0
  10. package/src/__tests__/part-styles.test.ts +311 -0
  11. package/src/__tests__/registry-parity.test.ts +42 -0
  12. package/src/__tests__/row-visibility.test.ts +672 -0
  13. package/src/__tests__/sort-select-page-cursor.test.tsx +336 -0
  14. package/src/__tests__/theme-block-html-id.test.tsx +167 -0
  15. package/src/blocks/__tests__/__snapshots__/product-gallery-block.test.tsx.snap +15 -0
  16. package/src/blocks/__tests__/accordion-block-state.test.tsx +201 -0
  17. package/src/blocks/__tests__/facet-filters-block.test.tsx +190 -0
  18. package/src/blocks/__tests__/listing-context-card-model.test.tsx +230 -0
  19. package/src/blocks/__tests__/listing-lookup-hardening.test.ts +80 -0
  20. package/src/blocks/__tests__/product-gallery-block.test.tsx +974 -0
  21. package/src/blocks/__tests__/product-grid-composition.test.tsx +570 -0
  22. package/src/blocks/__tests__/product-grid-lookups.test.tsx +230 -0
  23. package/src/blocks/__tests__/product-price-block.test.tsx +248 -0
  24. package/src/blocks/__tests__/product-recommendations-block.test.tsx +702 -0
  25. package/src/blocks/__tests__/share-block.test.tsx +392 -0
  26. package/src/blocks/__tests__/store-locator-block.test.tsx +691 -0
  27. package/src/blocks/__tests__/variant-selector-block.test.tsx +817 -0
  28. package/src/blocks/__tests__/variant-selector-golden.json +20 -0
  29. package/src/blocks/account-menu-block.tsx +264 -0
  30. package/src/blocks/add-to-cart-block.tsx +417 -0
  31. package/src/blocks/address-list-block.tsx +348 -0
  32. package/src/blocks/applied-filters-block.tsx +288 -0
  33. package/src/blocks/auth-form-block.tsx +295 -0
  34. package/src/blocks/basket-items-block.tsx +457 -0
  35. package/src/blocks/basket-summary-block.tsx +404 -0
  36. package/src/blocks/block-renderer-registry.tsx +94 -0
  37. package/src/blocks/change-password-form-block.tsx +240 -0
  38. package/src/blocks/checkout-steps-block.tsx +288 -0
  39. package/src/blocks/checkout-summary-block.tsx +449 -0
  40. package/src/blocks/countdown-block.tsx +182 -0
  41. package/src/blocks/coupon-list-block.tsx +292 -0
  42. package/src/blocks/facet-filters-block.tsx +1686 -0
  43. package/src/blocks/faq-accordion-block.tsx +358 -0
  44. package/src/blocks/favorite-button-block.tsx +252 -0
  45. package/src/blocks/favourites-grid-block.tsx +510 -0
  46. package/src/blocks/forgot-password-block.tsx +155 -0
  47. package/src/blocks/item-template-utils.ts +131 -0
  48. package/src/blocks/item-template.tsx +164 -0
  49. package/src/blocks/listing-breadcrumb-block.tsx +175 -0
  50. package/src/blocks/listing-context.ts +850 -0
  51. package/src/blocks/listing-pagination-block.tsx +923 -0
  52. package/src/blocks/listing-result-count-block.tsx +126 -0
  53. package/src/blocks/listing-title-block.tsx +115 -0
  54. package/src/blocks/oauth-buttons-block.tsx +205 -0
  55. package/src/blocks/order-detail-block.tsx +474 -0
  56. package/src/blocks/order-list-block.tsx +274 -0
  57. package/src/blocks/order-success-block.tsx +641 -0
  58. package/src/blocks/order-tracking-block.tsx +533 -0
  59. package/src/blocks/product-attributes-block.tsx +307 -0
  60. package/src/blocks/product-block-utils.tsx +639 -0
  61. package/src/blocks/product-breadcrumb-block.tsx +215 -0
  62. package/src/blocks/product-description-block.tsx +169 -0
  63. package/src/blocks/product-gallery-block.tsx +1523 -0
  64. package/src/blocks/product-grid-block.tsx +966 -0
  65. package/src/blocks/product-price-block.tsx +373 -0
  66. package/src/blocks/product-recommendations-block.tsx +1433 -0
  67. package/src/blocks/product-title-block.tsx +129 -0
  68. package/src/blocks/profile-form-block.tsx +297 -0
  69. package/src/blocks/recently-viewed-store.ts +201 -0
  70. package/src/blocks/register-form-block.tsx +362 -0
  71. package/src/blocks/share-block.tsx +667 -0
  72. package/src/blocks/size-guide-block.tsx +313 -0
  73. package/src/blocks/sort-select-block.tsx +636 -0
  74. package/src/blocks/stock-status-block.tsx +168 -0
  75. package/src/blocks/store-locator-block.tsx +1184 -0
  76. package/src/blocks/variant-selector-block.tsx +1110 -0
  77. package/src/sections/__tests__/tablet-raw-bucket-readers.test.tsx +186 -0
  78. 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;