@akinon/projectzero 2.0.0-beta.19 → 2.0.0-beta.20
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 +9 -7
- package/app-template/CHANGELOG.md +251 -204
- package/app-template/akinon.json +1 -1
- package/app-template/package.json +28 -28
- package/app-template/public/amex.svg +12 -0
- package/app-template/public/apple-pay.svg +16 -0
- package/app-template/public/assets/images/product-placeholder-1.jpg +0 -0
- package/app-template/public/assets/images/product-placeholder-2.jpg +0 -0
- package/app-template/public/assets/images/product-placeholder-3.jpg +0 -0
- package/app-template/public/assets/images/product-placeholder-4.jpg +0 -0
- package/app-template/public/google-pay.svg +16 -0
- package/app-template/public/locales/en/account.json +6 -3
- package/app-template/public/locales/en/auth.json +6 -7
- package/app-template/public/locales/en/basket.json +6 -6
- package/app-template/public/locales/en/blog.json +7 -0
- package/app-template/public/locales/en/category.json +3 -1
- package/app-template/public/locales/en/checkout.json +5 -4
- package/app-template/public/locales/en/common.json +11 -2
- package/app-template/public/locales/en/forgot_password.json +6 -7
- package/app-template/public/locales/en/product.json +4 -3
- package/app-template/public/locales/tr/account.json +6 -3
- package/app-template/public/locales/tr/auth.json +16 -17
- package/app-template/public/locales/tr/basket.json +4 -4
- package/app-template/public/locales/tr/blog.json +7 -0
- package/app-template/public/locales/tr/category.json +3 -1
- package/app-template/public/locales/tr/checkout.json +39 -38
- package/app-template/public/locales/tr/common.json +10 -1
- package/app-template/public/locales/tr/forgot_password.json +12 -13
- package/app-template/public/locales/tr/product.json +1 -0
- package/app-template/public/logo.svg +3 -27
- package/app-template/public/mastercard.svg +14 -0
- package/app-template/public/promotion-banner.jpg +0 -0
- package/app-template/public/shop-pay.svg +12 -0
- package/app-template/public/visa.svg +12 -0
- package/app-template/src/app/[commerce]/[locale]/[currency]/blog/[slug]/page.tsx +118 -0
- package/app-template/src/app/[commerce]/[locale]/[currency]/pages/[slug]/page.tsx +15 -0
- package/app-template/src/app/api/theme-settings/route.ts +12 -0
- package/app-template/src/assets/fonts/pz-icon.css +211 -49
- package/app-template/src/assets/fonts/pz-icon.eot +0 -0
- package/app-template/src/assets/fonts/pz-icon.html +486 -0
- package/app-template/src/assets/fonts/pz-icon.scss +373 -49
- package/app-template/src/assets/fonts/pz-icon.svg +215 -53
- package/app-template/src/assets/fonts/pz-icon.ttf +0 -0
- package/app-template/src/assets/fonts/pz-icon.woff +0 -0
- package/app-template/src/assets/fonts/pz-icon.woff2 +0 -0
- package/app-template/src/assets/globals.scss +4 -0
- package/app-template/src/assets/icons/arrow-right.svg +3 -0
- package/app-template/src/assets/icons/cart.svg +4 -12
- package/app-template/src/assets/icons/check.svg +2 -18
- package/app-template/src/assets/icons/chevron-down.svg +2 -7
- package/app-template/src/assets/icons/delete.svg +3 -0
- package/app-template/src/assets/icons/facebook.svg +2 -8
- package/app-template/src/assets/icons/fav-off.svg +5 -0
- package/app-template/src/assets/icons/fav-on.svg +5 -0
- package/app-template/src/assets/icons/filter-and-sort.svg +3 -0
- package/app-template/src/assets/icons/heart.svg +3 -0
- package/app-template/src/assets/icons/instagram.svg +2 -13
- package/app-template/src/assets/icons/materials.svg +3 -0
- package/app-template/src/assets/icons/person.svg +4 -0
- package/app-template/src/assets/icons/pinterest.svg +5 -11
- package/app-template/src/assets/icons/ruler.svg +3 -0
- package/app-template/src/assets/icons/search.svg +8 -11
- package/app-template/src/assets/icons/share.svg +2 -9
- package/app-template/src/assets/icons/snapchat.svg +3 -0
- package/app-template/src/assets/icons/tiktok.svg +3 -0
- package/app-template/src/assets/icons/tumblr.svg +6 -0
- package/app-template/src/assets/icons/twitter.svg +2 -10
- package/app-template/src/assets/icons/vimeo.svg +3 -0
- package/app-template/src/assets/icons/youtube.svg +3 -0
- package/app-template/src/assets/icons/zoom.svg +8 -0
- package/app-template/src/components/accordion.tsx +33 -11
- package/app-template/src/components/action-tooltip.tsx +160 -0
- package/app-template/src/components/currency-select.tsx +149 -4
- package/app-template/src/components/icon.tsx +5 -6
- package/app-template/src/components/index.ts +4 -1
- package/app-template/src/components/language-select.tsx +88 -2
- package/app-template/src/components/pagination.tsx +132 -20
- package/app-template/src/components/quantity-input.tsx +63 -0
- package/app-template/src/components/quantity-selector.tsx +203 -0
- package/app-template/src/components/route-handler.tsx +50 -0
- package/app-template/src/components/select.tsx +89 -69
- package/app-template/src/components/types/index.ts +26 -0
- package/app-template/src/components/widget-content.tsx +323 -0
- package/app-template/src/data/server/theme.ts +70 -0
- package/app-template/src/hooks/use-fav-button.tsx +5 -2
- package/app-template/src/hooks/use-product-cart.ts +11 -8
- package/app-template/src/hooks/use-theme-settings.ts +42 -0
- package/app-template/src/lib/fonts.ts +149 -0
- package/app-template/src/settings.js +2 -2
- package/app-template/src/types/hookform-resolvers-yup.d.ts +28 -0
- package/app-template/src/types/widget.ts +169 -0
- package/app-template/src/utils/formatDate.ts +48 -0
- package/app-template/src/utils/styles.ts +71 -0
- package/app-template/src/views/account/contact-form.tsx +147 -130
- package/app-template/src/views/basket/basket-item.tsx +691 -107
- package/app-template/src/views/basket/basket-summary-context.tsx +560 -0
- package/app-template/src/views/basket/designer-context.tsx +617 -0
- package/app-template/src/views/basket/index.ts +2 -0
- package/app-template/src/views/basket/summary.tsx +496 -75
- package/app-template/src/views/breadcrumb/breadcrumb-client.tsx +190 -0
- package/app-template/src/views/breadcrumb/breadcrumb-registrar.tsx +286 -0
- package/app-template/src/views/breadcrumb/constants.ts +15 -0
- package/app-template/src/views/breadcrumb/index.tsx +127 -0
- package/app-template/src/views/breadcrumb.tsx +13 -38
- package/app-template/src/views/category/category-banner.tsx +4 -23
- package/app-template/src/views/category/category-header.tsx +289 -66
- package/app-template/src/views/category/category-info.tsx +173 -24
- package/app-template/src/views/category/filters/filter-item.tsx +138 -42
- package/app-template/src/views/category/filters/index.tsx +208 -48
- package/app-template/src/views/category/layout.tsx +7 -4
- package/app-template/src/views/category/native-widget-context.tsx +257 -0
- package/app-template/src/views/category/product-list-registrar.tsx +665 -0
- package/app-template/src/views/checkout/auth.tsx +64 -40
- package/app-template/src/views/checkout/checkout-address-registrar.tsx +254 -0
- package/app-template/src/views/checkout/checkout-buttons-registrar.tsx +183 -0
- package/app-template/src/views/checkout/checkout-delivery-method-registrar.tsx +259 -0
- package/app-template/src/views/checkout/checkout-payment-options-registrar.tsx +253 -0
- package/app-template/src/views/checkout/checkout-summary-registrar.tsx +183 -0
- package/app-template/src/views/checkout/constants.ts +5 -0
- package/app-template/src/views/checkout/index.tsx +5 -0
- package/app-template/src/views/checkout/layout/header.tsx +9 -5
- package/app-template/src/views/checkout/steps/payment/index.tsx +5 -2
- package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +72 -1
- package/app-template/src/views/checkout/steps/payment/options/masterpass-rest.tsx +15 -0
- package/app-template/src/views/checkout/steps/payment/options/saved-card.tsx +18 -0
- package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +171 -40
- package/app-template/src/views/checkout/steps/shipping/address-box.tsx +74 -12
- package/app-template/src/views/checkout/steps/shipping/addresses.tsx +128 -45
- package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +232 -27
- package/app-template/src/views/checkout/summary.tsx +303 -29
- package/app-template/src/views/footer/footer-app-banner-context.tsx +326 -0
- package/app-template/src/views/footer/footer-bottom-context.tsx +215 -0
- package/app-template/src/views/footer/footer-bottom-wrapper.tsx +74 -0
- package/app-template/src/views/footer/footer-layout-constants.ts +35 -0
- package/app-template/src/views/footer/footer-layout-registrar.tsx +342 -0
- package/app-template/src/views/footer/footer-layout-switcher.tsx +110 -0
- package/app-template/src/views/footer/footer-menu-context.tsx +211 -0
- package/app-template/src/views/footer/footer-native-widgets.tsx +60 -0
- package/app-template/src/views/footer/footer-social-context.tsx +254 -0
- package/app-template/src/views/footer/footer-subscription-context.tsx +210 -0
- package/app-template/src/views/footer/footer-utils.ts +43 -0
- package/app-template/src/views/footer/footer-value-props-context.tsx +326 -0
- package/app-template/src/views/footer/logo-settings.ts +183 -0
- package/app-template/src/views/footer/native-widget-config.ts +262 -0
- package/app-template/src/views/footer/subscription-settings.ts +122 -0
- package/app-template/src/views/footer/use-footer-logo.ts +162 -0
- package/app-template/src/views/footer.tsx +415 -13
- package/app-template/src/views/guest-login/index.tsx +62 -58
- package/app-template/src/views/header/action-menu.tsx +277 -45
- package/app-template/src/views/header/band.tsx +6 -21
- package/app-template/src/views/header/designer-context.tsx +261 -0
- package/app-template/src/views/header/header-announcement-registrar.tsx +267 -0
- package/app-template/src/views/header/header-client-wrapper.tsx +496 -0
- package/app-template/src/views/header/header-content.tsx +1026 -0
- package/app-template/src/views/header/header-currency-registrar.tsx +348 -0
- package/app-template/src/views/header/header-icons-context.tsx +262 -0
- package/app-template/src/views/header/header-language-registrar.tsx +348 -0
- package/app-template/src/views/header/header-layout-context.tsx +143 -0
- package/app-template/src/views/header/header-layout-registrar.tsx +658 -0
- package/app-template/src/views/header/header-logo-context.tsx +228 -0
- package/app-template/src/views/header/header-logo.tsx +118 -0
- package/app-template/src/views/header/header-mini-basket-context.tsx +524 -0
- package/app-template/src/views/header/header-search-registrar.tsx +511 -0
- package/app-template/src/views/header/header-text-slider-registrar.tsx +382 -0
- package/app-template/src/views/header/index.tsx +109 -47
- package/app-template/src/views/header/inline-search.tsx +262 -0
- package/app-template/src/views/header/mini-basket.tsx +819 -44
- package/app-template/src/views/header/mobile-hamburger-button.tsx +5 -8
- package/app-template/src/views/header/mobile-menu.tsx +12 -0
- package/app-template/src/views/header/navbar-menu-context.tsx +219 -0
- package/app-template/src/views/header/navbar.tsx +178 -111
- package/app-template/src/views/header/search/index.tsx +71 -32
- package/app-template/src/views/header/search/results.tsx +127 -65
- package/app-template/src/views/header/search/search-input.tsx +61 -0
- package/app-template/src/views/header/server-settings-parser.ts +1105 -0
- package/app-template/src/views/header/use-header-icons.ts +241 -0
- package/app-template/src/views/header/use-header-logo.ts +213 -0
- package/app-template/src/views/header/use-navbar-menu.ts +179 -0
- package/app-template/src/views/login/index.tsx +54 -46
- package/app-template/src/views/product/accordion-section.tsx +61 -0
- package/app-template/src/views/product/accordion-wrapper.tsx +135 -43
- package/app-template/src/views/product/custom-button-group.tsx +69 -0
- package/app-template/src/views/product/favorites-button-section.tsx +69 -0
- package/app-template/src/views/product/find-in-store-section.tsx +60 -0
- package/app-template/src/views/product/index.ts +1 -0
- package/app-template/src/views/product/layout.tsx +6 -5
- package/app-template/src/views/product/misc-buttons.tsx +339 -25
- package/app-template/src/views/product/price-wrapper.tsx +3 -29
- package/app-template/src/views/product/product-actions.tsx +137 -8
- package/app-template/src/views/product/product-info-section.tsx +140 -0
- package/app-template/src/views/product/product-info.tsx +69 -31
- package/app-template/src/views/product/product-share.tsx +13 -8
- package/app-template/src/views/product/product-variants.tsx +2 -2
- package/app-template/src/views/product/quantity-section.tsx +73 -0
- package/app-template/src/views/product/sale-tag.tsx +10 -0
- package/app-template/src/views/product/share-section.tsx +357 -0
- package/app-template/src/views/product/slider.tsx +117 -79
- package/app-template/src/views/product/variant.tsx +69 -41
- package/app-template/src/views/product/variants-section.tsx +126 -0
- package/app-template/src/views/product-detail/constants.ts +272 -0
- package/app-template/src/views/product-detail/index.ts +10 -0
- package/app-template/src/views/product-detail/product-detail-registrar.tsx +616 -0
- package/app-template/src/views/product-item/index.tsx +119 -46
- package/app-template/src/views/register/index.tsx +14 -25
- package/app-template/src/views/share/index.tsx +9 -6
- package/app-template/src/views/widgets/home-hero-slider-content.tsx +41 -39
- package/app-template/src/widgets/flatpages/about-us/index.tsx +78 -0
- package/app-template/src/widgets/flatpages/blog-list/index.tsx +129 -0
- package/app-template/src/widgets/footer-app-banner.tsx +444 -0
- package/app-template/src/widgets/footer-bottom.tsx +127 -0
- package/app-template/src/widgets/footer-menu-compact.tsx +238 -0
- package/app-template/src/widgets/footer-menu-two.tsx +298 -0
- package/app-template/src/widgets/footer-social-client.tsx +251 -0
- package/app-template/src/widgets/footer-social.tsx +47 -16
- package/app-template/src/widgets/footer-subscription/footer-subscription-form.tsx +17 -14
- package/app-template/src/widgets/footer-subscription/index.tsx +183 -17
- package/app-template/src/widgets/footer-value-props.tsx +201 -0
- package/app-template/src/widgets/index.ts +7 -0
- package/app-template/src/widgets/schemas/about-us.json +46 -0
- package/app-template/src/widgets/schemas/blog-list.json +37 -0
- package/app-template/src/widgets/schemas/blog.json +29 -0
- package/app-template/tailwind.config.js +18 -2
- package/package.json +1 -1
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Footer Layout Section Registrar
|
|
5
|
+
*
|
|
6
|
+
* This component registers the "Layout" section for the footer placeholder.
|
|
7
|
+
* It's a minimal client component that only handles native widget registration
|
|
8
|
+
* and applies selection highlight to the footer when Layout section is selected.
|
|
9
|
+
*
|
|
10
|
+
* When the Layout section is selected in Theme Editor, the entire footer
|
|
11
|
+
* gets highlighted.
|
|
12
|
+
*
|
|
13
|
+
* It also provides the current layout type to child components via context.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
createContext,
|
|
18
|
+
useContext,
|
|
19
|
+
useEffect,
|
|
20
|
+
useRef,
|
|
21
|
+
useState,
|
|
22
|
+
useCallback,
|
|
23
|
+
ReactNode
|
|
24
|
+
} from 'react';
|
|
25
|
+
|
|
26
|
+
// Import constants for use within this component
|
|
27
|
+
import {
|
|
28
|
+
FOOTER_LAYOUT_PLACEHOLDER_ID,
|
|
29
|
+
FOOTER_LAYOUT_SECTION_ID,
|
|
30
|
+
FOOTER_LAYOUT_BLOCKS,
|
|
31
|
+
type FooterLayoutType
|
|
32
|
+
} from './footer-layout-constants';
|
|
33
|
+
|
|
34
|
+
// Re-export constants from shared file for backward compatibility
|
|
35
|
+
export {
|
|
36
|
+
FOOTER_LAYOUT_PLACEHOLDER_ID,
|
|
37
|
+
FOOTER_LAYOUT_SECTION_ID,
|
|
38
|
+
FOOTER_LAYOUT_WIDGET_SLUG,
|
|
39
|
+
FOOTER_LAYOUT_BLOCKS,
|
|
40
|
+
FOOTER_LAYOUT_BLOCK_IDS,
|
|
41
|
+
type FooterLayoutType
|
|
42
|
+
} from './footer-layout-constants';
|
|
43
|
+
|
|
44
|
+
// Global flag to track if registration has been done (survives component remount)
|
|
45
|
+
declare global {
|
|
46
|
+
interface Window {
|
|
47
|
+
__footerLayoutRegistered?: boolean;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface FooterLayoutProperties {
|
|
52
|
+
layout?: FooterLayoutType | Record<string, string>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Block styles type
|
|
56
|
+
type BlockStyles = Record<string, Record<string, unknown>>;
|
|
57
|
+
|
|
58
|
+
// Context for sharing layout type and block styles with other components
|
|
59
|
+
interface FooterLayoutContextValue {
|
|
60
|
+
layout: FooterLayoutType;
|
|
61
|
+
isDesigner: boolean;
|
|
62
|
+
selectedBlockId: string | null;
|
|
63
|
+
getBlockStyles: (blockId: string) => Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const FooterLayoutContext = createContext<FooterLayoutContextValue>({
|
|
67
|
+
layout: 'default',
|
|
68
|
+
isDesigner: false,
|
|
69
|
+
selectedBlockId: null,
|
|
70
|
+
getBlockStyles: () => ({})
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export const useFooterLayout = () => useContext(FooterLayoutContext);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* FooterLayoutRegistrar
|
|
77
|
+
*
|
|
78
|
+
* Registers the Layout native section with Theme Editor so it appears in the sidebar,
|
|
79
|
+
* handles footer highlight when Layout section is selected, and provides
|
|
80
|
+
* layout type to children via context.
|
|
81
|
+
*/
|
|
82
|
+
interface FooterLayoutRegistrarProps {
|
|
83
|
+
children?: ReactNode;
|
|
84
|
+
/** Initial layout from server to avoid layout flash */
|
|
85
|
+
initialLayout?: FooterLayoutType;
|
|
86
|
+
/** Initial block styles from server */
|
|
87
|
+
initialBlockStyles?: BlockStyles;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default function FooterLayoutRegistrar({
|
|
91
|
+
children,
|
|
92
|
+
initialLayout = 'default',
|
|
93
|
+
initialBlockStyles = {}
|
|
94
|
+
}: FooterLayoutRegistrarProps) {
|
|
95
|
+
// Initialize with server-provided value to avoid flash
|
|
96
|
+
const [sectionProperties, setSectionProperties] =
|
|
97
|
+
useState<FooterLayoutProperties>({ layout: initialLayout });
|
|
98
|
+
const [blockStyles, setBlockStyles] =
|
|
99
|
+
useState<BlockStyles>(initialBlockStyles);
|
|
100
|
+
const [isLayoutSelected, setIsLayoutSelected] = useState(false);
|
|
101
|
+
const [selectedBlockId, setSelectedBlockId] = useState<string | null>(null);
|
|
102
|
+
const [isDesigner, setIsDesigner] = useState(false);
|
|
103
|
+
const hasReceivedThemeProps = useRef(false);
|
|
104
|
+
|
|
105
|
+
// Register native widget with Theme Editor - ONLY ONCE per page session
|
|
106
|
+
// Properties are managed by Theme Editor, not sent with registration
|
|
107
|
+
// Uses window flag to survive component remount
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
// Check if in iframe (designer mode)
|
|
110
|
+
const isInIframe =
|
|
111
|
+
typeof window !== 'undefined' && window.self !== window.top;
|
|
112
|
+
setIsDesigner(isInIframe);
|
|
113
|
+
|
|
114
|
+
// Skip if already registered in this page session
|
|
115
|
+
if (typeof window !== 'undefined' && window.__footerLayoutRegistered) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!isInIframe || !window.parent) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Send native widget registration to Theme Editor
|
|
124
|
+
// Includes row blocks for styling (MAIN_ROW, BOTTOM_ROW)
|
|
125
|
+
// Properties are NOT included - Theme Editor manages them
|
|
126
|
+
const nativeWidgetConfig = {
|
|
127
|
+
placeholderId: FOOTER_LAYOUT_PLACEHOLDER_ID,
|
|
128
|
+
section: {
|
|
129
|
+
id: FOOTER_LAYOUT_SECTION_ID,
|
|
130
|
+
type: 'native',
|
|
131
|
+
label: 'Layout',
|
|
132
|
+
blocks: [
|
|
133
|
+
{
|
|
134
|
+
id: FOOTER_LAYOUT_BLOCKS.MAIN_ROW.id,
|
|
135
|
+
type: FOOTER_LAYOUT_BLOCKS.MAIN_ROW.type,
|
|
136
|
+
label: FOOTER_LAYOUT_BLOCKS.MAIN_ROW.label
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: FOOTER_LAYOUT_BLOCKS.BOTTOM_ROW.id,
|
|
140
|
+
type: FOOTER_LAYOUT_BLOCKS.BOTTOM_ROW.type,
|
|
141
|
+
label: FOOTER_LAYOUT_BLOCKS.BOTTOM_ROW.label
|
|
142
|
+
}
|
|
143
|
+
]
|
|
144
|
+
// Don't send properties - Theme Editor already has them
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
window.parent.postMessage(
|
|
149
|
+
{
|
|
150
|
+
type: 'REGISTER_NATIVE_WIDGETS',
|
|
151
|
+
data: { widgets: [nativeWidgetConfig] }
|
|
152
|
+
},
|
|
153
|
+
'*'
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Mark as registered in window to survive component remount
|
|
157
|
+
window.__footerLayoutRegistered = true;
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
// Apply highlight style to footer when Layout section is selected
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (typeof window === 'undefined') return;
|
|
163
|
+
|
|
164
|
+
const footerElement = document.querySelector('footer');
|
|
165
|
+
if (!footerElement) return;
|
|
166
|
+
|
|
167
|
+
if (isLayoutSelected) {
|
|
168
|
+
// Apply selection highlight
|
|
169
|
+
footerElement.style.outline = '2px solid #3b82f6';
|
|
170
|
+
footerElement.style.outlineOffset = '-2px';
|
|
171
|
+
} else {
|
|
172
|
+
// Remove selection highlight
|
|
173
|
+
footerElement.style.outline = '';
|
|
174
|
+
footerElement.style.outlineOffset = '';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return () => {
|
|
178
|
+
// Cleanup on unmount
|
|
179
|
+
footerElement.style.outline = '';
|
|
180
|
+
footerElement.style.outlineOffset = '';
|
|
181
|
+
};
|
|
182
|
+
}, [isLayoutSelected]);
|
|
183
|
+
|
|
184
|
+
// Listen for theme updates and selection changes from Theme Editor
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (typeof window === 'undefined') return;
|
|
187
|
+
|
|
188
|
+
const handleMessage = (event: MessageEvent) => {
|
|
189
|
+
const { type, data } = event.data || {};
|
|
190
|
+
|
|
191
|
+
// Handle theme updates
|
|
192
|
+
if (
|
|
193
|
+
(type === 'UPDATE_THEME' || type === 'LOAD_THEME') &&
|
|
194
|
+
data?.theme?.placeholders
|
|
195
|
+
) {
|
|
196
|
+
const placeholder = data.theme.placeholders?.find(
|
|
197
|
+
(p: { slug: string }) => p.slug === FOOTER_LAYOUT_PLACEHOLDER_ID
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const layoutSection = placeholder?.sections?.find(
|
|
201
|
+
(s: { id: string }) => s.id === FOOTER_LAYOUT_SECTION_ID
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
if (layoutSection) {
|
|
205
|
+
hasReceivedThemeProps.current = true;
|
|
206
|
+
if (layoutSection.properties) {
|
|
207
|
+
setSectionProperties(layoutSection.properties);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Extract block styles from section blocks
|
|
211
|
+
if (layoutSection.blocks) {
|
|
212
|
+
const newBlockStyles: BlockStyles = {};
|
|
213
|
+
layoutSection.blocks.forEach(
|
|
214
|
+
(block: { id: string; styles?: Record<string, unknown> }) => {
|
|
215
|
+
if (block.styles) {
|
|
216
|
+
newBlockStyles[block.id] = block.styles;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
setBlockStyles(newBlockStyles);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Handle property updates (when user changes dropdown in Theme Editor)
|
|
226
|
+
if (type === 'UPDATE_SECTION_PROPERTY' || type === 'UPDATE_PROPERTY') {
|
|
227
|
+
const { sectionId, placeholderId, key, value, properties } = data || {};
|
|
228
|
+
|
|
229
|
+
// Check if this is for our section
|
|
230
|
+
if (
|
|
231
|
+
sectionId === FOOTER_LAYOUT_SECTION_ID ||
|
|
232
|
+
placeholderId === FOOTER_LAYOUT_PLACEHOLDER_ID
|
|
233
|
+
) {
|
|
234
|
+
// If we get individual key/value
|
|
235
|
+
if (key && value !== undefined) {
|
|
236
|
+
setSectionProperties((prev) => ({
|
|
237
|
+
...prev,
|
|
238
|
+
[key]: value
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
241
|
+
// If we get full properties object
|
|
242
|
+
if (properties) {
|
|
243
|
+
setSectionProperties(properties);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Handle selection changes
|
|
249
|
+
if (type === 'SELECT_SECTION') {
|
|
250
|
+
const { placeholderId, sectionId } = data || {};
|
|
251
|
+
|
|
252
|
+
// Check if Layout section is selected
|
|
253
|
+
const isSelected =
|
|
254
|
+
placeholderId === FOOTER_LAYOUT_PLACEHOLDER_ID &&
|
|
255
|
+
sectionId === FOOTER_LAYOUT_SECTION_ID;
|
|
256
|
+
|
|
257
|
+
setIsLayoutSelected(isSelected);
|
|
258
|
+
if (isSelected) {
|
|
259
|
+
setSelectedBlockId(null);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Handle block selection
|
|
264
|
+
if (type === 'SELECT_BLOCK') {
|
|
265
|
+
const { sectionId, blockId } = data || {};
|
|
266
|
+
|
|
267
|
+
// If block in our section is selected
|
|
268
|
+
if (sectionId === FOOTER_LAYOUT_SECTION_ID) {
|
|
269
|
+
setSelectedBlockId(blockId);
|
|
270
|
+
setIsLayoutSelected(false);
|
|
271
|
+
} else {
|
|
272
|
+
// If a block is selected in another section, deselect our blocks
|
|
273
|
+
setSelectedBlockId(null);
|
|
274
|
+
setIsLayoutSelected(false);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Handle deselection
|
|
279
|
+
if (type === 'DESELECT' || type === 'CLEAR_SELECTION') {
|
|
280
|
+
setIsLayoutSelected(false);
|
|
281
|
+
setSelectedBlockId(null);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
window.addEventListener('message', handleMessage);
|
|
286
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
287
|
+
}, []);
|
|
288
|
+
|
|
289
|
+
// Helper to extract layout value from potentially responsive property
|
|
290
|
+
const extractLayoutValue = (layoutProp: unknown): FooterLayoutType => {
|
|
291
|
+
if (!layoutProp) return 'default';
|
|
292
|
+
|
|
293
|
+
// If it's a direct string value
|
|
294
|
+
if (typeof layoutProp === 'string') {
|
|
295
|
+
return layoutProp as FooterLayoutType;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// If it's a responsive object (e.g., { desktop: 'compact' })
|
|
299
|
+
if (typeof layoutProp === 'object' && layoutProp !== null) {
|
|
300
|
+
const obj = layoutProp as Record<string, string>;
|
|
301
|
+
// Try desktop first, then mobile, then any first value
|
|
302
|
+
return (obj.desktop ||
|
|
303
|
+
obj.mobile ||
|
|
304
|
+
Object.values(obj)[0] ||
|
|
305
|
+
'default') as FooterLayoutType;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return 'default';
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Get block styles helper
|
|
312
|
+
const getBlockStyles = useCallback(
|
|
313
|
+
(blockId: string): Record<string, unknown> => {
|
|
314
|
+
return blockStyles[blockId] || {};
|
|
315
|
+
},
|
|
316
|
+
[blockStyles]
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Get the current layout type
|
|
320
|
+
const currentLayout: FooterLayoutType = extractLayoutValue(
|
|
321
|
+
sectionProperties.layout
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// If no children, just return null (registration-only mode)
|
|
325
|
+
if (!children) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Provide layout context to children
|
|
330
|
+
return (
|
|
331
|
+
<FooterLayoutContext.Provider
|
|
332
|
+
value={{
|
|
333
|
+
layout: currentLayout,
|
|
334
|
+
isDesigner,
|
|
335
|
+
selectedBlockId,
|
|
336
|
+
getBlockStyles
|
|
337
|
+
}}
|
|
338
|
+
>
|
|
339
|
+
{children}
|
|
340
|
+
</FooterLayoutContext.Provider>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Footer Layout Switcher
|
|
5
|
+
*
|
|
6
|
+
* A client component that switches between pre-rendered footer layouts
|
|
7
|
+
* based on the current layout setting from FooterLayoutRegistrar.
|
|
8
|
+
*
|
|
9
|
+
* This component receives the layouts as React nodes (pre-rendered by the server)
|
|
10
|
+
* and simply shows/hides them based on the layout context.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { ReactNode, useMemo, useCallback } from 'react';
|
|
14
|
+
import clsx from 'clsx';
|
|
15
|
+
import {
|
|
16
|
+
useFooterLayout,
|
|
17
|
+
FOOTER_LAYOUT_BLOCKS,
|
|
18
|
+
FOOTER_LAYOUT_PLACEHOLDER_ID,
|
|
19
|
+
FOOTER_LAYOUT_SECTION_ID
|
|
20
|
+
} from './footer-layout-registrar';
|
|
21
|
+
import { useDesignerFeatures } from '@akinon/next/components/theme-editor/hooks/use-designer-features';
|
|
22
|
+
import { convertBlockStyles } from './footer-utils';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Selectable Row Container
|
|
26
|
+
* Wraps row content with theme editor selection support
|
|
27
|
+
*/
|
|
28
|
+
interface SelectableRowProps {
|
|
29
|
+
blockId: string;
|
|
30
|
+
blockLabel: string;
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
className?: string;
|
|
33
|
+
style?: React.CSSProperties;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function SelectableRow({
|
|
37
|
+
blockId,
|
|
38
|
+
blockLabel,
|
|
39
|
+
children,
|
|
40
|
+
className,
|
|
41
|
+
style
|
|
42
|
+
}: SelectableRowProps) {
|
|
43
|
+
const { isDesigner, selectedBlockId, getBlockStyles } = useFooterLayout();
|
|
44
|
+
|
|
45
|
+
const { handleClick } = useDesignerFeatures({
|
|
46
|
+
blockId,
|
|
47
|
+
placeholderId: FOOTER_LAYOUT_PLACEHOLDER_ID,
|
|
48
|
+
sectionId: FOOTER_LAYOUT_SECTION_ID,
|
|
49
|
+
isDesigner,
|
|
50
|
+
blockInfo: {
|
|
51
|
+
id: blockId,
|
|
52
|
+
type: 'container',
|
|
53
|
+
label: blockLabel
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const isSelected = selectedBlockId === blockId;
|
|
58
|
+
const blockStyles = getBlockStyles(blockId);
|
|
59
|
+
const computedStyles = useMemo(
|
|
60
|
+
() => convertBlockStyles(blockStyles),
|
|
61
|
+
[blockStyles]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const handleContainerClick = useCallback(
|
|
65
|
+
(e: React.MouseEvent) => {
|
|
66
|
+
if (isDesigner) {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
e.stopPropagation();
|
|
69
|
+
handleClick(e);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[isDesigner, handleClick]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
data-block-id={blockId}
|
|
78
|
+
onClick={handleContainerClick}
|
|
79
|
+
className={clsx(
|
|
80
|
+
className,
|
|
81
|
+
isDesigner && 'cursor-pointer',
|
|
82
|
+
isSelected && 'ring-2 ring-blue-500 ring-inset'
|
|
83
|
+
)}
|
|
84
|
+
style={{ ...style, ...computedStyles }}
|
|
85
|
+
>
|
|
86
|
+
{children}
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface FooterLayoutSwitcherProps {
|
|
92
|
+
defaultLayout: ReactNode;
|
|
93
|
+
compactLayout: ReactNode;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default function FooterLayoutSwitcher({
|
|
97
|
+
defaultLayout,
|
|
98
|
+
compactLayout
|
|
99
|
+
}: FooterLayoutSwitcherProps) {
|
|
100
|
+
const { layout } = useFooterLayout();
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<SelectableRow
|
|
104
|
+
blockId={FOOTER_LAYOUT_BLOCKS.MAIN_ROW.id}
|
|
105
|
+
blockLabel={FOOTER_LAYOUT_BLOCKS.MAIN_ROW.label}
|
|
106
|
+
>
|
|
107
|
+
{layout === 'compact' ? compactLayout : defaultLayout}
|
|
108
|
+
</SelectableRow>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
type PropsWithChildren
|
|
12
|
+
} from 'react';
|
|
13
|
+
import { useExternalDesigner } from '@akinon/next/components/theme-editor/hooks/use-external-designer';
|
|
14
|
+
import { useNativeWidgetData } from '@akinon/next/components/theme-editor/hooks/use-native-widget-data';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
FOOTER_PLACEHOLDER_ID,
|
|
18
|
+
FOOTER_MENU_SECTION_ID,
|
|
19
|
+
FOOTER_MENU_WIDGET_SLUG,
|
|
20
|
+
FOOTER_MENU_HEADING_BLOCK_ID,
|
|
21
|
+
FOOTER_MENU_LINK_BLOCK_ID,
|
|
22
|
+
type FooterNativeWidgetBlock
|
|
23
|
+
} from './native-widget-config';
|
|
24
|
+
|
|
25
|
+
export { FOOTER_MENU_HEADING_BLOCK_ID, FOOTER_MENU_LINK_BLOCK_ID };
|
|
26
|
+
|
|
27
|
+
type FooterMenuBlockState = FooterNativeWidgetBlock;
|
|
28
|
+
|
|
29
|
+
interface FooterMenuContextValue {
|
|
30
|
+
isDesigner: boolean;
|
|
31
|
+
selectedBlockId: string | null;
|
|
32
|
+
getBlock: (blockId: string) => FooterMenuBlockState | undefined;
|
|
33
|
+
blockVersion: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const FooterMenuContext = createContext<FooterMenuContextValue>({
|
|
37
|
+
isDesigner: false,
|
|
38
|
+
selectedBlockId: null,
|
|
39
|
+
getBlock: () => undefined,
|
|
40
|
+
blockVersion: 0
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const BLOCK_META = [
|
|
44
|
+
{ id: FOOTER_MENU_HEADING_BLOCK_ID, type: 'text', label: 'Menu Headings' },
|
|
45
|
+
{ id: FOOTER_MENU_LINK_BLOCK_ID, type: 'text', label: 'Menu Links' }
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const toBlockState = (
|
|
49
|
+
block: Partial<FooterNativeWidgetBlock>
|
|
50
|
+
): FooterMenuBlockState => {
|
|
51
|
+
const fallback = BLOCK_META.find((meta) => meta.id === block.id);
|
|
52
|
+
return {
|
|
53
|
+
id: block.id ?? fallback?.id ?? '',
|
|
54
|
+
type: block.type ?? fallback?.type,
|
|
55
|
+
label: block.label ?? fallback?.label,
|
|
56
|
+
styles: block.styles,
|
|
57
|
+
properties: block.properties,
|
|
58
|
+
value: block.value
|
|
59
|
+
} as FooterMenuBlockState;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const mapFromSnapshot = (
|
|
63
|
+
blocks?: FooterNativeWidgetBlock[]
|
|
64
|
+
): Map<string, FooterMenuBlockState> => {
|
|
65
|
+
const map = new Map<string, FooterMenuBlockState>();
|
|
66
|
+
blocks?.forEach((block) => {
|
|
67
|
+
map.set(block.id, toBlockState(block));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
BLOCK_META.forEach((meta) => {
|
|
71
|
+
if (!map.has(meta.id)) {
|
|
72
|
+
map.set(meta.id, toBlockState(meta));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return map;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
interface FooterMenuProviderProps {
|
|
80
|
+
initialBlocks?: FooterNativeWidgetBlock[];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function FooterMenuProvider({
|
|
84
|
+
initialBlocks,
|
|
85
|
+
children
|
|
86
|
+
}: PropsWithChildren<FooterMenuProviderProps>) {
|
|
87
|
+
const designerState = useExternalDesigner({
|
|
88
|
+
placeholderId: FOOTER_PLACEHOLDER_ID
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const [blockMap, setBlockMap] = useState(() =>
|
|
92
|
+
mapFromSnapshot(initialBlocks)
|
|
93
|
+
);
|
|
94
|
+
const blockMapRef = useRef(blockMap);
|
|
95
|
+
const [blockVersion, setBlockVersion] = useState(0);
|
|
96
|
+
|
|
97
|
+
const isDesignerRef = useRef(false);
|
|
98
|
+
const [isDesignerChecked, setIsDesignerChecked] = useState(false);
|
|
99
|
+
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
if (typeof window === 'undefined') return;
|
|
102
|
+
isDesignerRef.current = window.self !== window.top;
|
|
103
|
+
setIsDesignerChecked(true);
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
const isDesigner = isDesignerRef.current;
|
|
107
|
+
|
|
108
|
+
const widgetData = useNativeWidgetData({
|
|
109
|
+
widgetSlug: FOOTER_MENU_WIDGET_SLUG,
|
|
110
|
+
sectionId: FOOTER_MENU_SECTION_ID,
|
|
111
|
+
skip: !isDesignerChecked || isDesigner,
|
|
112
|
+
blockMeta: BLOCK_META
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const mergeBlocks = useCallback(
|
|
116
|
+
(blocks: Partial<FooterNativeWidgetBlock>[] | undefined) => {
|
|
117
|
+
if (!blocks?.length) return;
|
|
118
|
+
|
|
119
|
+
setBlockMap((prev) => {
|
|
120
|
+
const next = new Map(prev);
|
|
121
|
+
blocks.forEach((block) => {
|
|
122
|
+
if (!block.id) return;
|
|
123
|
+
const existing = next.get(block.id);
|
|
124
|
+
|
|
125
|
+
next.set(
|
|
126
|
+
block.id,
|
|
127
|
+
toBlockState({
|
|
128
|
+
...existing,
|
|
129
|
+
...block,
|
|
130
|
+
styles:
|
|
131
|
+
block.styles && Object.keys(block.styles).length > 0
|
|
132
|
+
? block.styles
|
|
133
|
+
: existing?.styles,
|
|
134
|
+
properties:
|
|
135
|
+
block.properties && Object.keys(block.properties).length > 0
|
|
136
|
+
? block.properties
|
|
137
|
+
: existing?.properties,
|
|
138
|
+
value: block.value !== undefined ? block.value : existing?.value
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
blockMapRef.current = next;
|
|
143
|
+
return next;
|
|
144
|
+
});
|
|
145
|
+
setBlockVersion((prev) => prev + 1);
|
|
146
|
+
},
|
|
147
|
+
[]
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!isDesignerChecked || isDesigner || widgetData.isLoading) return;
|
|
152
|
+
|
|
153
|
+
const blocksToMerge: Partial<FooterNativeWidgetBlock>[] = [];
|
|
154
|
+
widgetData.blocks.forEach((block) => {
|
|
155
|
+
blocksToMerge.push(block as FooterNativeWidgetBlock);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (blocksToMerge.length > 0) {
|
|
159
|
+
mergeBlocks(blocksToMerge);
|
|
160
|
+
}
|
|
161
|
+
}, [
|
|
162
|
+
isDesigner,
|
|
163
|
+
isDesignerChecked,
|
|
164
|
+
widgetData.isLoading,
|
|
165
|
+
widgetData.blocks,
|
|
166
|
+
mergeBlocks
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
const handleMessage = (event: MessageEvent) => {
|
|
171
|
+
const { type, data } = event.data || {};
|
|
172
|
+
if (
|
|
173
|
+
(type === 'UPDATE_THEME' || type === 'LOAD_THEME') &&
|
|
174
|
+
data?.theme?.placeholders
|
|
175
|
+
) {
|
|
176
|
+
const placeholder = data.theme.placeholders.find(
|
|
177
|
+
(p: { slug: string }) => p.slug === FOOTER_PLACEHOLDER_ID
|
|
178
|
+
);
|
|
179
|
+
const section = placeholder?.sections?.find(
|
|
180
|
+
(s: { id: string }) => s.id === FOOTER_MENU_SECTION_ID
|
|
181
|
+
);
|
|
182
|
+
mergeBlocks(section?.blocks);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
window.addEventListener('message', handleMessage);
|
|
187
|
+
return () => window.removeEventListener('message', handleMessage);
|
|
188
|
+
}, [mergeBlocks]);
|
|
189
|
+
|
|
190
|
+
const getBlock = useCallback(
|
|
191
|
+
(blockId: string) => blockMapRef.current.get(blockId),
|
|
192
|
+
[]
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const contextValue = useMemo(
|
|
196
|
+
() => ({
|
|
197
|
+
...designerState,
|
|
198
|
+
getBlock,
|
|
199
|
+
blockVersion
|
|
200
|
+
}),
|
|
201
|
+
[designerState, getBlock, blockVersion]
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<FooterMenuContext.Provider value={contextValue}>
|
|
206
|
+
{children}
|
|
207
|
+
</FooterMenuContext.Provider>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export const useFooterMenuDesigner = () => useContext(FooterMenuContext);
|