@akinon/pz-theme 2.0.26-beta.0 → 2.0.26
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 +3 -3
- package/package.json +3 -3
- package/src/sections/featured-product-spotlight-section.tsx +3 -1
- package/src/sections/find-in-store-section.tsx +3 -1
- package/src/sections/installment-options-section.tsx +4 -2
- package/src/sections/pre-order-launch-banner-section.tsx +15 -5
- package/src/sections/section-wrapper.tsx +26 -1
- package/src/theme-placeholder-client.tsx +9 -2
- package/src/theme-placeholder-wrapper.tsx +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.26
|
|
3
|
+
## 2.0.26
|
|
4
4
|
|
|
5
5
|
### Patch Changes
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- @akinon/next@2.0.26
|
|
7
|
+
- 40bc4a3: ZERO-4545: Refactor product query hooks to use currentData for better state management across args
|
|
8
|
+
- @akinon/next@2.0.26
|
|
9
9
|
|
|
10
10
|
## 2.0.25
|
|
11
11
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.26
|
|
4
|
+
"version": "2.0.26",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"peerDependencies": {
|
|
8
|
-
"@akinon/next": "2.0.26
|
|
8
|
+
"@akinon/next": "2.0.26",
|
|
9
9
|
"react": "^18.0.0 || ^19.0.0",
|
|
10
10
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
11
11
|
},
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"tailwind-merge": "^2.5.4"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@akinon/next": "2.0.26
|
|
18
|
+
"@akinon/next": "2.0.26",
|
|
19
19
|
"@types/node": "^18.7.8",
|
|
20
20
|
"@types/react": "^18.0.17",
|
|
21
21
|
"@types/react-dom": "^18.0.6",
|
|
@@ -257,7 +257,9 @@ export default function FeaturedProductSpotlightSection({
|
|
|
257
257
|
const fallbackProductPk = parsePositiveInt(
|
|
258
258
|
getResponsiveValue(section.properties?.['product-pk'], currentBreakpoint, '')
|
|
259
259
|
);
|
|
260
|
-
|
|
260
|
+
// currentData (not data): data is sticky across args, so a cleared
|
|
261
|
+
// product-pk would keep rendering the previously fetched product.
|
|
262
|
+
const { currentData: fallbackProductResponse } = useGetProductByPkQuery(
|
|
261
263
|
fallbackProductPk as number,
|
|
262
264
|
{
|
|
263
265
|
skip: products.length > 0 || !fallbackProductPk
|
|
@@ -593,7 +593,9 @@ export default function FindInStoreSection({
|
|
|
593
593
|
currentBreakpoint
|
|
594
594
|
);
|
|
595
595
|
|
|
596
|
-
|
|
596
|
+
// currentData (not data): data is sticky across args, so a cleared
|
|
597
|
+
// product-pk would keep rendering the previously fetched product.
|
|
598
|
+
const { currentData: productResponse, isLoading: productLoading } =
|
|
597
599
|
useGetProductByPkQuery(resolvedProductPk as number, {
|
|
598
600
|
skip: !resolvedProductPk
|
|
599
601
|
});
|
|
@@ -565,13 +565,15 @@ export default function InstallmentOptionsSection({
|
|
|
565
565
|
const resolvedProductPk = parseProductPk(collectionProduct?.pk) ?? propertyProductPk;
|
|
566
566
|
|
|
567
567
|
const shouldFetchProduct = !collectionProduct && resolvedProductPk !== null;
|
|
568
|
-
|
|
568
|
+
// currentData (not data): data is sticky across args, so a cleared
|
|
569
|
+
// product-pk would keep rendering the previous product/installments.
|
|
570
|
+
const { currentData: productResponse } = useGetProductByPkQuery(
|
|
569
571
|
resolvedProductPk as number,
|
|
570
572
|
{
|
|
571
573
|
skip: !shouldFetchProduct
|
|
572
574
|
}
|
|
573
575
|
);
|
|
574
|
-
const {
|
|
576
|
+
const { currentData: installmentsData, isLoading } = useGetInstallmentsQuery(
|
|
575
577
|
resolvedProductPk as number,
|
|
576
578
|
{
|
|
577
579
|
skip: resolvedProductPk === null
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import { useGetProductByPkQuery } from '@akinon/next/data/client/product';
|
|
5
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
5
6
|
|
|
6
7
|
import ThemeBlock, { Block } from '../theme-block';
|
|
7
8
|
import { useThemeSettingsContext } from '../theme-settings-context';
|
|
@@ -216,10 +217,16 @@ const getRemainingTime = (
|
|
|
216
217
|
};
|
|
217
218
|
};
|
|
218
219
|
|
|
219
|
-
|
|
220
|
+
// Locale must be explicit: Intl with `undefined` resolves to the runtime
|
|
221
|
+
// default, which differs between the SSR node process and the browser and
|
|
222
|
+
// causes hydration mismatches on the rendered date text.
|
|
223
|
+
const formatLaunchDate = (
|
|
224
|
+
value: Date | null,
|
|
225
|
+
locale?: string
|
|
226
|
+
): string | null => {
|
|
220
227
|
if (!value) return null;
|
|
221
228
|
|
|
222
|
-
return new Intl.DateTimeFormat(
|
|
229
|
+
return new Intl.DateTimeFormat(locale || 'en', {
|
|
223
230
|
month: 'long',
|
|
224
231
|
day: 'numeric',
|
|
225
232
|
year: 'numeric'
|
|
@@ -304,6 +311,7 @@ export default function PreOrderLaunchBannerSection({
|
|
|
304
311
|
selectedBlockId = null
|
|
305
312
|
}: PreOrderLaunchBannerSectionProps) {
|
|
306
313
|
const themeSettings = useThemeSettingsContext();
|
|
314
|
+
const { locale } = useLocalization();
|
|
307
315
|
|
|
308
316
|
const maxWidth = getResponsiveValue(
|
|
309
317
|
section.styles?.['max-width'],
|
|
@@ -362,7 +370,9 @@ export default function PreOrderLaunchBannerSection({
|
|
|
362
370
|
''
|
|
363
371
|
)
|
|
364
372
|
);
|
|
365
|
-
|
|
373
|
+
// currentData (not data): data is sticky across args, so a cleared
|
|
374
|
+
// product-pk would keep rendering the previously fetched product.
|
|
375
|
+
const { currentData: fallbackProductResponse } = useGetProductByPkQuery(
|
|
366
376
|
fallbackProductPk as number,
|
|
367
377
|
{
|
|
368
378
|
skip: products.length > 0 || !fallbackProductPk
|
|
@@ -385,8 +395,8 @@ export default function PreOrderLaunchBannerSection({
|
|
|
385
395
|
[product, propertyLaunchDate]
|
|
386
396
|
);
|
|
387
397
|
const launchDateText = useMemo(
|
|
388
|
-
() => formatLaunchDate(launchDate),
|
|
389
|
-
[launchDate]
|
|
398
|
+
() => formatLaunchDate(launchDate, locale),
|
|
399
|
+
[launchDate, locale]
|
|
390
400
|
);
|
|
391
401
|
|
|
392
402
|
const showCountdown = parseBoolean(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
3
|
+
import React, { useEffect, useRef } from 'react';
|
|
4
4
|
import ActionToolbar from '../components/action-toolbar';
|
|
5
5
|
import { twMerge } from 'tailwind-merge';
|
|
6
6
|
import clsx from 'clsx';
|
|
@@ -58,6 +58,30 @@ export default function SectionWrapper({
|
|
|
58
58
|
section.properties?.['error-message']
|
|
59
59
|
).trim();
|
|
60
60
|
|
|
61
|
+
const sectionRef = useRef<HTMLElement>(null);
|
|
62
|
+
|
|
63
|
+
// Custom-renderer sections render through this wrapper instead of the
|
|
64
|
+
// default <section> in theme-section, so the editor's SCROLL_TO_SECTION
|
|
65
|
+
// must be handled here too.
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!isDesigner) return;
|
|
68
|
+
|
|
69
|
+
const handleMessage = (event: MessageEvent) => {
|
|
70
|
+
if (
|
|
71
|
+
event.data?.type === 'SCROLL_TO_SECTION' &&
|
|
72
|
+
event.data?.data?.sectionId === section.id
|
|
73
|
+
) {
|
|
74
|
+
sectionRef.current?.scrollIntoView({
|
|
75
|
+
behavior: 'smooth',
|
|
76
|
+
block: 'center'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
window.addEventListener('message', handleMessage);
|
|
82
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
83
|
+
}, [section.id, isDesigner]);
|
|
84
|
+
|
|
61
85
|
const handleClick = (e: React.MouseEvent) => {
|
|
62
86
|
if (isDesigner && onSelect) {
|
|
63
87
|
e.stopPropagation();
|
|
@@ -80,6 +104,7 @@ export default function SectionWrapper({
|
|
|
80
104
|
|
|
81
105
|
return (
|
|
82
106
|
<section
|
|
107
|
+
ref={sectionRef}
|
|
83
108
|
data-section-id={section.id}
|
|
84
109
|
data-newsletter-signup={isNewsletterSection ? 'true' : undefined}
|
|
85
110
|
data-newsletter-endpoint={
|
|
@@ -10,10 +10,14 @@ import { useVisibilityContext } from './hooks/use-visibility-context';
|
|
|
10
10
|
import { applyVisibilityRulesToSections } from './utils/visibility-rules';
|
|
11
11
|
import { isPublishWindowVisible } from './utils/publish-window';
|
|
12
12
|
|
|
13
|
-
// Global
|
|
13
|
+
// Global flags that must survive component remounts (e.g. in-iframe route
|
|
14
|
+
// changes): IFRAME_READY is sent once per document, and the designer flag
|
|
15
|
+
// arrives once from the editor in response — remounted components must not
|
|
16
|
+
// reset to non-designer state.
|
|
14
17
|
declare global {
|
|
15
18
|
interface Window {
|
|
16
19
|
__iframeReadySent?: boolean;
|
|
20
|
+
__themeEditorDesigner?: boolean;
|
|
17
21
|
}
|
|
18
22
|
}
|
|
19
23
|
|
|
@@ -36,7 +40,9 @@ export default function ThemePlaceholderClient({
|
|
|
36
40
|
);
|
|
37
41
|
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
|
38
42
|
const [currentBreakpoint, setCurrentBreakpoint] = useState<string>('desktop');
|
|
39
|
-
const [isDesigner, setIsDesigner] = useState(
|
|
43
|
+
const [isDesigner, setIsDesigner] = useState(
|
|
44
|
+
() => typeof window !== 'undefined' && window.__themeEditorDesigner === true
|
|
45
|
+
);
|
|
40
46
|
const visibilityContext = useVisibilityContext(currentBreakpoint);
|
|
41
47
|
const renderedSections = applyVisibilityRulesToSections(
|
|
42
48
|
sections,
|
|
@@ -73,6 +79,7 @@ export default function ThemePlaceholderClient({
|
|
|
73
79
|
switch (event.data?.type) {
|
|
74
80
|
case 'SET_THEME_EDITOR_COOKIE':
|
|
75
81
|
if (event.data.data) {
|
|
82
|
+
window.__themeEditorDesigner = true;
|
|
76
83
|
setIsDesigner(true);
|
|
77
84
|
}
|
|
78
85
|
break;
|
|
@@ -297,7 +297,13 @@ export default function ThemePlaceholderWrapper({
|
|
|
297
297
|
);
|
|
298
298
|
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
|
299
299
|
const [currentBreakpoint, setCurrentBreakpoint] = useState<string>('desktop');
|
|
300
|
-
|
|
300
|
+
// Designer flag survives remounts via the window global; the editor sends
|
|
301
|
+
// SET_THEME_EDITOR_COOKIE only once per document (IFRAME_READY guard).
|
|
302
|
+
const [isDesigner, setIsDesigner] = useState(
|
|
303
|
+
() =>
|
|
304
|
+
isDesignMode ||
|
|
305
|
+
(typeof window !== 'undefined' && window.__themeEditorDesigner === true)
|
|
306
|
+
);
|
|
301
307
|
const [dataSources, setDataSources] = useState<any[]>(initialDataSources);
|
|
302
308
|
const [themeSettings, setThemeSettings] = useState<Record<
|
|
303
309
|
string,
|
|
@@ -502,6 +508,7 @@ export default function ThemePlaceholderWrapper({
|
|
|
502
508
|
switch (event.data?.type) {
|
|
503
509
|
case 'SET_THEME_EDITOR_COOKIE':
|
|
504
510
|
if (event.data.data) {
|
|
511
|
+
window.__themeEditorDesigner = true;
|
|
505
512
|
setIsDesigner(true);
|
|
506
513
|
}
|
|
507
514
|
break;
|