@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.
Files changed (223) hide show
  1. package/CHANGELOG.md +9 -7
  2. package/app-template/CHANGELOG.md +251 -204
  3. package/app-template/akinon.json +1 -1
  4. package/app-template/package.json +28 -28
  5. package/app-template/public/amex.svg +12 -0
  6. package/app-template/public/apple-pay.svg +16 -0
  7. package/app-template/public/assets/images/product-placeholder-1.jpg +0 -0
  8. package/app-template/public/assets/images/product-placeholder-2.jpg +0 -0
  9. package/app-template/public/assets/images/product-placeholder-3.jpg +0 -0
  10. package/app-template/public/assets/images/product-placeholder-4.jpg +0 -0
  11. package/app-template/public/google-pay.svg +16 -0
  12. package/app-template/public/locales/en/account.json +6 -3
  13. package/app-template/public/locales/en/auth.json +6 -7
  14. package/app-template/public/locales/en/basket.json +6 -6
  15. package/app-template/public/locales/en/blog.json +7 -0
  16. package/app-template/public/locales/en/category.json +3 -1
  17. package/app-template/public/locales/en/checkout.json +5 -4
  18. package/app-template/public/locales/en/common.json +11 -2
  19. package/app-template/public/locales/en/forgot_password.json +6 -7
  20. package/app-template/public/locales/en/product.json +4 -3
  21. package/app-template/public/locales/tr/account.json +6 -3
  22. package/app-template/public/locales/tr/auth.json +16 -17
  23. package/app-template/public/locales/tr/basket.json +4 -4
  24. package/app-template/public/locales/tr/blog.json +7 -0
  25. package/app-template/public/locales/tr/category.json +3 -1
  26. package/app-template/public/locales/tr/checkout.json +39 -38
  27. package/app-template/public/locales/tr/common.json +10 -1
  28. package/app-template/public/locales/tr/forgot_password.json +12 -13
  29. package/app-template/public/locales/tr/product.json +1 -0
  30. package/app-template/public/logo.svg +3 -27
  31. package/app-template/public/mastercard.svg +14 -0
  32. package/app-template/public/promotion-banner.jpg +0 -0
  33. package/app-template/public/shop-pay.svg +12 -0
  34. package/app-template/public/visa.svg +12 -0
  35. package/app-template/src/app/[commerce]/[locale]/[currency]/blog/[slug]/page.tsx +118 -0
  36. package/app-template/src/app/[commerce]/[locale]/[currency]/pages/[slug]/page.tsx +15 -0
  37. package/app-template/src/app/api/theme-settings/route.ts +12 -0
  38. package/app-template/src/assets/fonts/pz-icon.css +211 -49
  39. package/app-template/src/assets/fonts/pz-icon.eot +0 -0
  40. package/app-template/src/assets/fonts/pz-icon.html +486 -0
  41. package/app-template/src/assets/fonts/pz-icon.scss +373 -49
  42. package/app-template/src/assets/fonts/pz-icon.svg +215 -53
  43. package/app-template/src/assets/fonts/pz-icon.ttf +0 -0
  44. package/app-template/src/assets/fonts/pz-icon.woff +0 -0
  45. package/app-template/src/assets/fonts/pz-icon.woff2 +0 -0
  46. package/app-template/src/assets/globals.scss +4 -0
  47. package/app-template/src/assets/icons/arrow-right.svg +3 -0
  48. package/app-template/src/assets/icons/cart.svg +4 -12
  49. package/app-template/src/assets/icons/check.svg +2 -18
  50. package/app-template/src/assets/icons/chevron-down.svg +2 -7
  51. package/app-template/src/assets/icons/delete.svg +3 -0
  52. package/app-template/src/assets/icons/facebook.svg +2 -8
  53. package/app-template/src/assets/icons/fav-off.svg +5 -0
  54. package/app-template/src/assets/icons/fav-on.svg +5 -0
  55. package/app-template/src/assets/icons/filter-and-sort.svg +3 -0
  56. package/app-template/src/assets/icons/heart.svg +3 -0
  57. package/app-template/src/assets/icons/instagram.svg +2 -13
  58. package/app-template/src/assets/icons/materials.svg +3 -0
  59. package/app-template/src/assets/icons/person.svg +4 -0
  60. package/app-template/src/assets/icons/pinterest.svg +5 -11
  61. package/app-template/src/assets/icons/ruler.svg +3 -0
  62. package/app-template/src/assets/icons/search.svg +8 -11
  63. package/app-template/src/assets/icons/share.svg +2 -9
  64. package/app-template/src/assets/icons/snapchat.svg +3 -0
  65. package/app-template/src/assets/icons/tiktok.svg +3 -0
  66. package/app-template/src/assets/icons/tumblr.svg +6 -0
  67. package/app-template/src/assets/icons/twitter.svg +2 -10
  68. package/app-template/src/assets/icons/vimeo.svg +3 -0
  69. package/app-template/src/assets/icons/youtube.svg +3 -0
  70. package/app-template/src/assets/icons/zoom.svg +8 -0
  71. package/app-template/src/components/accordion.tsx +33 -11
  72. package/app-template/src/components/action-tooltip.tsx +160 -0
  73. package/app-template/src/components/currency-select.tsx +149 -4
  74. package/app-template/src/components/icon.tsx +5 -6
  75. package/app-template/src/components/index.ts +4 -1
  76. package/app-template/src/components/language-select.tsx +88 -2
  77. package/app-template/src/components/pagination.tsx +132 -20
  78. package/app-template/src/components/quantity-input.tsx +63 -0
  79. package/app-template/src/components/quantity-selector.tsx +203 -0
  80. package/app-template/src/components/route-handler.tsx +50 -0
  81. package/app-template/src/components/select.tsx +89 -69
  82. package/app-template/src/components/types/index.ts +26 -0
  83. package/app-template/src/components/widget-content.tsx +323 -0
  84. package/app-template/src/data/server/theme.ts +70 -0
  85. package/app-template/src/hooks/use-fav-button.tsx +5 -2
  86. package/app-template/src/hooks/use-product-cart.ts +11 -8
  87. package/app-template/src/hooks/use-theme-settings.ts +42 -0
  88. package/app-template/src/lib/fonts.ts +149 -0
  89. package/app-template/src/settings.js +2 -2
  90. package/app-template/src/types/hookform-resolvers-yup.d.ts +28 -0
  91. package/app-template/src/types/widget.ts +169 -0
  92. package/app-template/src/utils/formatDate.ts +48 -0
  93. package/app-template/src/utils/styles.ts +71 -0
  94. package/app-template/src/views/account/contact-form.tsx +147 -130
  95. package/app-template/src/views/basket/basket-item.tsx +691 -107
  96. package/app-template/src/views/basket/basket-summary-context.tsx +560 -0
  97. package/app-template/src/views/basket/designer-context.tsx +617 -0
  98. package/app-template/src/views/basket/index.ts +2 -0
  99. package/app-template/src/views/basket/summary.tsx +496 -75
  100. package/app-template/src/views/breadcrumb/breadcrumb-client.tsx +190 -0
  101. package/app-template/src/views/breadcrumb/breadcrumb-registrar.tsx +286 -0
  102. package/app-template/src/views/breadcrumb/constants.ts +15 -0
  103. package/app-template/src/views/breadcrumb/index.tsx +127 -0
  104. package/app-template/src/views/breadcrumb.tsx +13 -38
  105. package/app-template/src/views/category/category-banner.tsx +4 -23
  106. package/app-template/src/views/category/category-header.tsx +289 -66
  107. package/app-template/src/views/category/category-info.tsx +173 -24
  108. package/app-template/src/views/category/filters/filter-item.tsx +138 -42
  109. package/app-template/src/views/category/filters/index.tsx +208 -48
  110. package/app-template/src/views/category/layout.tsx +7 -4
  111. package/app-template/src/views/category/native-widget-context.tsx +257 -0
  112. package/app-template/src/views/category/product-list-registrar.tsx +665 -0
  113. package/app-template/src/views/checkout/auth.tsx +64 -40
  114. package/app-template/src/views/checkout/checkout-address-registrar.tsx +254 -0
  115. package/app-template/src/views/checkout/checkout-buttons-registrar.tsx +183 -0
  116. package/app-template/src/views/checkout/checkout-delivery-method-registrar.tsx +259 -0
  117. package/app-template/src/views/checkout/checkout-payment-options-registrar.tsx +253 -0
  118. package/app-template/src/views/checkout/checkout-summary-registrar.tsx +183 -0
  119. package/app-template/src/views/checkout/constants.ts +5 -0
  120. package/app-template/src/views/checkout/index.tsx +5 -0
  121. package/app-template/src/views/checkout/layout/header.tsx +9 -5
  122. package/app-template/src/views/checkout/steps/payment/index.tsx +5 -2
  123. package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +72 -1
  124. package/app-template/src/views/checkout/steps/payment/options/masterpass-rest.tsx +15 -0
  125. package/app-template/src/views/checkout/steps/payment/options/saved-card.tsx +18 -0
  126. package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +171 -40
  127. package/app-template/src/views/checkout/steps/shipping/address-box.tsx +74 -12
  128. package/app-template/src/views/checkout/steps/shipping/addresses.tsx +128 -45
  129. package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +232 -27
  130. package/app-template/src/views/checkout/summary.tsx +303 -29
  131. package/app-template/src/views/footer/footer-app-banner-context.tsx +326 -0
  132. package/app-template/src/views/footer/footer-bottom-context.tsx +215 -0
  133. package/app-template/src/views/footer/footer-bottom-wrapper.tsx +74 -0
  134. package/app-template/src/views/footer/footer-layout-constants.ts +35 -0
  135. package/app-template/src/views/footer/footer-layout-registrar.tsx +342 -0
  136. package/app-template/src/views/footer/footer-layout-switcher.tsx +110 -0
  137. package/app-template/src/views/footer/footer-menu-context.tsx +211 -0
  138. package/app-template/src/views/footer/footer-native-widgets.tsx +60 -0
  139. package/app-template/src/views/footer/footer-social-context.tsx +254 -0
  140. package/app-template/src/views/footer/footer-subscription-context.tsx +210 -0
  141. package/app-template/src/views/footer/footer-utils.ts +43 -0
  142. package/app-template/src/views/footer/footer-value-props-context.tsx +326 -0
  143. package/app-template/src/views/footer/logo-settings.ts +183 -0
  144. package/app-template/src/views/footer/native-widget-config.ts +262 -0
  145. package/app-template/src/views/footer/subscription-settings.ts +122 -0
  146. package/app-template/src/views/footer/use-footer-logo.ts +162 -0
  147. package/app-template/src/views/footer.tsx +415 -13
  148. package/app-template/src/views/guest-login/index.tsx +62 -58
  149. package/app-template/src/views/header/action-menu.tsx +277 -45
  150. package/app-template/src/views/header/band.tsx +6 -21
  151. package/app-template/src/views/header/designer-context.tsx +261 -0
  152. package/app-template/src/views/header/header-announcement-registrar.tsx +267 -0
  153. package/app-template/src/views/header/header-client-wrapper.tsx +496 -0
  154. package/app-template/src/views/header/header-content.tsx +1026 -0
  155. package/app-template/src/views/header/header-currency-registrar.tsx +348 -0
  156. package/app-template/src/views/header/header-icons-context.tsx +262 -0
  157. package/app-template/src/views/header/header-language-registrar.tsx +348 -0
  158. package/app-template/src/views/header/header-layout-context.tsx +143 -0
  159. package/app-template/src/views/header/header-layout-registrar.tsx +658 -0
  160. package/app-template/src/views/header/header-logo-context.tsx +228 -0
  161. package/app-template/src/views/header/header-logo.tsx +118 -0
  162. package/app-template/src/views/header/header-mini-basket-context.tsx +524 -0
  163. package/app-template/src/views/header/header-search-registrar.tsx +511 -0
  164. package/app-template/src/views/header/header-text-slider-registrar.tsx +382 -0
  165. package/app-template/src/views/header/index.tsx +109 -47
  166. package/app-template/src/views/header/inline-search.tsx +262 -0
  167. package/app-template/src/views/header/mini-basket.tsx +819 -44
  168. package/app-template/src/views/header/mobile-hamburger-button.tsx +5 -8
  169. package/app-template/src/views/header/mobile-menu.tsx +12 -0
  170. package/app-template/src/views/header/navbar-menu-context.tsx +219 -0
  171. package/app-template/src/views/header/navbar.tsx +178 -111
  172. package/app-template/src/views/header/search/index.tsx +71 -32
  173. package/app-template/src/views/header/search/results.tsx +127 -65
  174. package/app-template/src/views/header/search/search-input.tsx +61 -0
  175. package/app-template/src/views/header/server-settings-parser.ts +1105 -0
  176. package/app-template/src/views/header/use-header-icons.ts +241 -0
  177. package/app-template/src/views/header/use-header-logo.ts +213 -0
  178. package/app-template/src/views/header/use-navbar-menu.ts +179 -0
  179. package/app-template/src/views/login/index.tsx +54 -46
  180. package/app-template/src/views/product/accordion-section.tsx +61 -0
  181. package/app-template/src/views/product/accordion-wrapper.tsx +135 -43
  182. package/app-template/src/views/product/custom-button-group.tsx +69 -0
  183. package/app-template/src/views/product/favorites-button-section.tsx +69 -0
  184. package/app-template/src/views/product/find-in-store-section.tsx +60 -0
  185. package/app-template/src/views/product/index.ts +1 -0
  186. package/app-template/src/views/product/layout.tsx +6 -5
  187. package/app-template/src/views/product/misc-buttons.tsx +339 -25
  188. package/app-template/src/views/product/price-wrapper.tsx +3 -29
  189. package/app-template/src/views/product/product-actions.tsx +137 -8
  190. package/app-template/src/views/product/product-info-section.tsx +140 -0
  191. package/app-template/src/views/product/product-info.tsx +69 -31
  192. package/app-template/src/views/product/product-share.tsx +13 -8
  193. package/app-template/src/views/product/product-variants.tsx +2 -2
  194. package/app-template/src/views/product/quantity-section.tsx +73 -0
  195. package/app-template/src/views/product/sale-tag.tsx +10 -0
  196. package/app-template/src/views/product/share-section.tsx +357 -0
  197. package/app-template/src/views/product/slider.tsx +117 -79
  198. package/app-template/src/views/product/variant.tsx +69 -41
  199. package/app-template/src/views/product/variants-section.tsx +126 -0
  200. package/app-template/src/views/product-detail/constants.ts +272 -0
  201. package/app-template/src/views/product-detail/index.ts +10 -0
  202. package/app-template/src/views/product-detail/product-detail-registrar.tsx +616 -0
  203. package/app-template/src/views/product-item/index.tsx +119 -46
  204. package/app-template/src/views/register/index.tsx +14 -25
  205. package/app-template/src/views/share/index.tsx +9 -6
  206. package/app-template/src/views/widgets/home-hero-slider-content.tsx +41 -39
  207. package/app-template/src/widgets/flatpages/about-us/index.tsx +78 -0
  208. package/app-template/src/widgets/flatpages/blog-list/index.tsx +129 -0
  209. package/app-template/src/widgets/footer-app-banner.tsx +444 -0
  210. package/app-template/src/widgets/footer-bottom.tsx +127 -0
  211. package/app-template/src/widgets/footer-menu-compact.tsx +238 -0
  212. package/app-template/src/widgets/footer-menu-two.tsx +298 -0
  213. package/app-template/src/widgets/footer-social-client.tsx +251 -0
  214. package/app-template/src/widgets/footer-social.tsx +47 -16
  215. package/app-template/src/widgets/footer-subscription/footer-subscription-form.tsx +17 -14
  216. package/app-template/src/widgets/footer-subscription/index.tsx +183 -17
  217. package/app-template/src/widgets/footer-value-props.tsx +201 -0
  218. package/app-template/src/widgets/index.ts +7 -0
  219. package/app-template/src/widgets/schemas/about-us.json +46 -0
  220. package/app-template/src/widgets/schemas/blog-list.json +37 -0
  221. package/app-template/src/widgets/schemas/blog.json +29 -0
  222. package/app-template/tailwind.config.js +18 -2
  223. package/package.json +1 -1
@@ -11,24 +11,21 @@ export default function MobileHamburgerButton() {
11
11
  const dispatch = useAppDispatch();
12
12
 
13
13
  return (
14
- <div className="flex row-start-2 sm:hidden ">
14
+ <div className="flex sm:hidden">
15
15
  <PwaBackButton />
16
16
  <Button
17
17
  className={clsx([
18
- 'w-12',
19
- 'h-12',
20
- 'bg-secondary',
21
- 'text-white',
18
+ 'h-auto',
19
+ 'bg-transparent',
22
20
  'flex',
23
21
  'items-center',
24
22
  'justify-center',
25
23
  'px-0',
26
- 'border-none',
27
- 'hover:bg-secondary-hover'
24
+ 'border-none'
28
25
  ])}
29
26
  onClick={() => dispatch(toggleMobileMenu())}
30
27
  >
31
- <Icon name="hamburger" size={18} className="fill-white" />
28
+ <Icon name="hamburger" size={18} className="text-black" />
32
29
  </Button>
33
30
  </div>
34
31
  );
@@ -90,10 +90,22 @@ export default function MobileMenu(props: MobileMenuProps) {
90
90
  onClick={(e) => {
91
91
  if (item.children.length > 0) {
92
92
  e.preventDefault();
93
+ e.stopPropagation();
93
94
  setSelectedSubMenu(item);
95
+
96
+ window?.parent?.postMessage(
97
+ {
98
+ type: 'MENU_TOGGLE',
99
+ isMenuAction: true
100
+ },
101
+ '*'
102
+ );
94
103
  }
95
104
  }}
96
105
  className="flex items-center justify-between"
106
+ data-is-menu-toggle={
107
+ item.children.length > 0 ? 'true' : undefined
108
+ }
97
109
  >
98
110
  <span>{item.label}</span>
99
111
  <Icon name="chevron-end" size={14} />
@@ -0,0 +1,219 @@
1
+ 'use client';
2
+
3
+ import {
4
+ createContext,
5
+ PropsWithChildren,
6
+ useCallback,
7
+ useContext,
8
+ useEffect,
9
+ useRef,
10
+ useState
11
+ } from 'react';
12
+ import { useExternalDesigner } from '@akinon/next/components/theme-editor/hooks/use-external-designer';
13
+ import { useNativeWidgetData } from '@akinon/next/components/theme-editor/hooks/use-native-widget-data';
14
+
15
+ export const NAVBAR_MENU_PLACEHOLDER_ID = 'header';
16
+ export const NAVBAR_MENU_SECTION_ID = 'header-nav';
17
+ export const NAVBAR_MENU_WIDGET_SLUG = 'header-nav-settings-2';
18
+
19
+ export const NAVBAR_MENU_BLOCK = {
20
+ id: 'header-nav-menu-item',
21
+ type: 'text',
22
+ label: 'Menu Items'
23
+ } as const;
24
+
25
+ const BLOCK_META = [NAVBAR_MENU_BLOCK];
26
+
27
+ interface ThemeBlock {
28
+ id: string;
29
+ type: string;
30
+ label: string;
31
+ styles?: Record<string, unknown>;
32
+ properties?: Record<string, unknown>;
33
+ value?: unknown;
34
+ }
35
+
36
+ interface ThemeSection {
37
+ id: string;
38
+ blocks: ThemeBlock[];
39
+ }
40
+
41
+ interface ThemePlaceholder {
42
+ slug: string;
43
+ sections: ThemeSection[];
44
+ }
45
+
46
+ interface NavbarMenuContextValue {
47
+ isDesigner: boolean;
48
+ selectedBlockId: string | null;
49
+ getBlockStyles: (blockId: string) => Record<string, unknown> | undefined;
50
+ getBlockProperties: (blockId: string) => Record<string, unknown> | undefined;
51
+ }
52
+
53
+ const NavbarMenuContext = createContext<NavbarMenuContextValue>({
54
+ isDesigner: false,
55
+ selectedBlockId: null,
56
+ getBlockStyles: () => undefined,
57
+ getBlockProperties: () => undefined
58
+ });
59
+
60
+ export const NavbarMenuProvider = ({ children }: PropsWithChildren) => {
61
+ const designerState = useExternalDesigner({
62
+ placeholderId: NAVBAR_MENU_PLACEHOLDER_ID
63
+ });
64
+
65
+ const isDesignerRef = useRef(false);
66
+ const [isDesignerChecked, setIsDesignerChecked] = useState(false);
67
+
68
+ useEffect(() => {
69
+ if (typeof window === 'undefined') return;
70
+ isDesignerRef.current = window.self !== window.top;
71
+ setIsDesignerChecked(true);
72
+ }, []);
73
+
74
+ const isDesigner = isDesignerRef.current;
75
+
76
+ const [themeBlocks, setThemeBlocks] = useState<Map<string, ThemeBlock>>(
77
+ new Map()
78
+ );
79
+ const hasReceivedThemeStyles = useRef(false);
80
+ const hasRegisteredNativeWidget = useRef(false);
81
+ const themeBlocksRef = useRef<Map<string, ThemeBlock>>(new Map());
82
+
83
+ const widgetData = useNativeWidgetData({
84
+ widgetSlug: NAVBAR_MENU_WIDGET_SLUG,
85
+ sectionId: NAVBAR_MENU_SECTION_ID,
86
+ skip: !isDesignerChecked || isDesigner,
87
+ blockMeta: BLOCK_META
88
+ });
89
+
90
+ const getBlockWithStyles = useCallback(
91
+ (blockDef: typeof NAVBAR_MENU_BLOCK) => {
92
+ const existingBlock = themeBlocksRef.current.get(blockDef.id);
93
+ return {
94
+ id: blockDef.id,
95
+ type: blockDef.type,
96
+ label: blockDef.label,
97
+ properties: existingBlock?.properties || {},
98
+ styles: existingBlock?.styles || {}
99
+ };
100
+ },
101
+ []
102
+ );
103
+
104
+ useEffect(() => {
105
+ if (hasRegisteredNativeWidget.current) return;
106
+
107
+ const isInIframe =
108
+ typeof window !== 'undefined' && window.self !== window.top;
109
+ if (!isInIframe || !window.parent) return;
110
+
111
+ const nativeWidgetConfig = {
112
+ placeholderId: NAVBAR_MENU_PLACEHOLDER_ID,
113
+ section: {
114
+ id: NAVBAR_MENU_SECTION_ID,
115
+ type: 'native',
116
+ label: 'Navigation Menu',
117
+ blocks: [getBlockWithStyles(NAVBAR_MENU_BLOCK)]
118
+ }
119
+ };
120
+
121
+ window.parent.postMessage(
122
+ {
123
+ type: 'REGISTER_NATIVE_WIDGETS',
124
+ data: { widgets: [nativeWidgetConfig] }
125
+ },
126
+ '*'
127
+ );
128
+
129
+ hasRegisteredNativeWidget.current = true;
130
+ }, [getBlockWithStyles]);
131
+
132
+ useEffect(() => {
133
+ if (!isDesignerChecked || isDesigner || widgetData.isLoading) return;
134
+ if (hasReceivedThemeStyles.current) return;
135
+
136
+ const blockMap = new Map<string, ThemeBlock>();
137
+ widgetData.blocks.forEach((block) => {
138
+ blockMap.set(block.id, {
139
+ id: block.id,
140
+ type: block.type || 'text',
141
+ label: block.label || block.id,
142
+ styles: block.styles,
143
+ properties: block.properties,
144
+ value: block.value
145
+ });
146
+ });
147
+
148
+ if (blockMap.size > 0) {
149
+ setThemeBlocks(blockMap);
150
+ themeBlocksRef.current = blockMap;
151
+ }
152
+ }, [isDesignerChecked, isDesigner, widgetData.isLoading, widgetData.blocks]);
153
+
154
+ const indexBlocks = useCallback(
155
+ (blocks: ThemeBlock[], map: Map<string, ThemeBlock>) => {
156
+ blocks.forEach((block) => {
157
+ map.set(block.id, block);
158
+ });
159
+ },
160
+ []
161
+ );
162
+
163
+ useEffect(() => {
164
+ const handleMessage = (event: MessageEvent) => {
165
+ const { type, data } = event.data || {};
166
+
167
+ if (
168
+ (type === 'UPDATE_THEME' || type === 'LOAD_THEME') &&
169
+ data?.theme?.placeholders
170
+ ) {
171
+ const placeholder = data.theme.placeholders?.find(
172
+ (p: ThemePlaceholder) => p.slug === NAVBAR_MENU_PLACEHOLDER_ID
173
+ );
174
+
175
+ const navSection = placeholder?.sections?.find(
176
+ (s: ThemeSection) => s.id === NAVBAR_MENU_SECTION_ID
177
+ );
178
+
179
+ if (navSection?.blocks) {
180
+ const blockMap = new Map<string, ThemeBlock>();
181
+ indexBlocks(navSection.blocks, blockMap);
182
+
183
+ if (blockMap.size > 0) {
184
+ hasReceivedThemeStyles.current = true;
185
+ setThemeBlocks(blockMap);
186
+ themeBlocksRef.current = blockMap;
187
+ }
188
+ }
189
+ }
190
+ };
191
+
192
+ window.addEventListener('message', handleMessage);
193
+ return () => window.removeEventListener('message', handleMessage);
194
+ }, [indexBlocks]);
195
+
196
+ const getBlockStyles = useCallback(
197
+ (blockId: string) => themeBlocks.get(blockId)?.styles,
198
+ [themeBlocks]
199
+ );
200
+
201
+ const getBlockProperties = useCallback(
202
+ (blockId: string) => themeBlocks.get(blockId)?.properties,
203
+ [themeBlocks]
204
+ );
205
+
206
+ return (
207
+ <NavbarMenuContext.Provider
208
+ value={{
209
+ ...designerState,
210
+ getBlockStyles,
211
+ getBlockProperties
212
+ }}
213
+ >
214
+ {children}
215
+ </NavbarMenuContext.Provider>
216
+ );
217
+ };
218
+
219
+ export const useNavbarMenuDesigner = () => useContext(NavbarMenuContext);
@@ -1,115 +1,181 @@
1
1
  'use client';
2
2
 
3
+ import { useState } from 'react';
3
4
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
4
- import { openSearch, setOpenedMenu } from '@akinon/next/redux/reducers/header';
5
+ import { setOpenedMenu } from '@akinon/next/redux/reducers/header';
5
6
  import clsx from 'clsx';
6
7
  import { MenuItemType } from '@akinon/next/types';
7
8
 
8
9
  import { Icon, Link } from '@theme/components';
9
10
  import Search from './search';
10
- import { useLocalization } from '@akinon/next/hooks';
11
11
  import { Image } from '@akinon/next/components/image';
12
+ import {
13
+ useNavbarMenuSettings,
14
+ NavbarMenuSettings
15
+ } from './use-navbar-menu';
16
+ import { useDesignerFeatures } from '@akinon/next/components/theme-editor/hooks/use-designer-features';
17
+ import {
18
+ NavbarMenuProvider,
19
+ useNavbarMenuDesigner,
20
+ NAVBAR_MENU_PLACEHOLDER_ID,
21
+ NAVBAR_MENU_SECTION_ID,
22
+ NAVBAR_MENU_BLOCK
23
+ } from './navbar-menu-context';
12
24
 
13
25
  interface NavbarProps {
14
26
  menu: MenuItemType[];
27
+ initialNavSettings?: NavbarMenuSettings;
15
28
  }
16
29
 
17
- export default function Navbar(props: NavbarProps) {
18
- const { menu } = props;
30
+ interface NavbarContentProps {
31
+ menu: MenuItemType[];
32
+ initialNavSettings?: NavbarMenuSettings;
33
+ }
34
+
35
+ function NavbarContent({ menu, initialNavSettings }: NavbarContentProps) {
19
36
  const dispatch = useAppDispatch();
20
37
  const { isSearchOpen, openedMenu } = useAppSelector((state) => state.header);
21
38
 
22
- const { t } = useLocalization();
39
+ const navSettings = useNavbarMenuSettings(initialNavSettings);
40
+ const { isDesigner, selectedBlockId } = useNavbarMenuDesigner();
41
+ const [hoveredItemIndex, setHoveredItemIndex] = useState<number | null>(null);
42
+
43
+ const { handleClick } = useDesignerFeatures({
44
+ blockId: NAVBAR_MENU_BLOCK.id,
45
+ placeholderId: NAVBAR_MENU_PLACEHOLDER_ID,
46
+ sectionId: NAVBAR_MENU_SECTION_ID,
47
+ isDesigner,
48
+ blockInfo: {
49
+ id: NAVBAR_MENU_BLOCK.id,
50
+ type: NAVBAR_MENU_BLOCK.type,
51
+ label: NAVBAR_MENU_BLOCK.label
52
+ }
53
+ });
54
+
55
+ const isSelected = selectedBlockId === NAVBAR_MENU_BLOCK.id;
56
+
57
+ const getMenuItemStyle = (isHovered: boolean): React.CSSProperties => {
58
+ const baseStyle: React.CSSProperties = {
59
+ fontSize: navSettings.fontSize,
60
+ color: navSettings.color,
61
+ fontWeight: navSettings.fontWeight as React.CSSProperties['fontWeight'],
62
+ textTransform: navSettings.textTransform as React.CSSProperties['textTransform'],
63
+ letterSpacing: navSettings.letterSpacing,
64
+ transition: 'color 0.2s ease'
65
+ };
66
+
67
+ if (isHovered && navSettings.hoverColor) {
68
+ return { ...baseStyle, color: navSettings.hoverColor };
69
+ }
70
+
71
+ return baseStyle;
72
+ };
23
73
 
24
74
  return (
25
75
  <>
26
- <nav className="relative flex-wrap items-center justify-center hidden header-grid-area-nav sm:flex justify-items-center">
27
- <ul className="flex flex-wrap items-center justify-center gap-x-8 gap-y-4 mt-8 justify-items-center">
28
- {menu.map(
29
- (item, index) =>
30
- item.label != null && (
31
- <li
32
- key={index}
33
- className="flex items-center h-full group"
34
- onMouseEnter={() => {
35
- dispatch(setOpenedMenu(item.pk));
36
- }}
37
- onMouseLeave={() => {
38
- dispatch(setOpenedMenu(null));
39
- }}
76
+ <nav
77
+ onClick={isDesigner ? handleClick : undefined}
78
+ className={clsx(
79
+ isDesigner && 'cursor-pointer',
80
+ isSelected && 'ring-2 ring-blue-500 ring-offset-2 rounded'
81
+ )}
82
+ >
83
+ <ul className="hidden flex-wrap items-center justify-center gap-x-8 gap-y-4 justify-items-center sm:flex">
84
+ {menu.map((item, index) => {
85
+ if (item.label == null) {
86
+ return null;
87
+ }
88
+
89
+ const hasChildren = Boolean(item.children?.length);
90
+ const isItemHovered = hoveredItemIndex === index;
91
+
92
+ return (
93
+ <li
94
+ key={index}
95
+ className="flex items-center h-full group py-7"
96
+ onMouseEnter={() => {
97
+ dispatch(setOpenedMenu(item.pk));
98
+ setHoveredItemIndex(index);
99
+ }}
100
+ onMouseLeave={() => {
101
+ dispatch(setOpenedMenu(null));
102
+ setHoveredItemIndex(null);
103
+ }}
104
+ >
105
+ <Link
106
+ href={item.url}
107
+ className="flex items-center"
108
+ style={getMenuItemStyle(isItemHovered)}
109
+ data-testid="navbar-category"
40
110
  >
41
- <Link
42
- href={item.url}
43
- className="flex items-center p-2.5 text-sm uppercase transition hover:text-secondary"
44
- data-testid="navbar-category"
45
- >
46
- {item.label}
47
- </Link>
48
-
49
- {/*
50
- Performance Note:
51
- The submenu content in this Navbar component is rendered based on hover-triggered state changes.
52
- This approach is adopted for performance optimization reasons. It ensures that:
53
- 1. Submenu data is only loaded and rendered when necessary, reducing initial load times and resource usage.
54
- 2. Unnecessary renders are avoided, enhancing the responsiveness and efficiency of the navigation bar.
55
- Please be cautious about altering this logic, as changes could negatively impact the performance and user experience of the Navbar.
56
- */}
57
-
58
- {openedMenu === item.pk && item.children.length > 0 && (
59
- <div
111
+ <span>{item.label}</span>
112
+ {hasChildren && (
113
+ <Icon
114
+ name="chevron-down"
115
+ size={20}
60
116
  className={clsx(
61
- [
62
- 'container',
63
- 'absolute',
64
- 'bottom-0',
65
- 'left-0',
66
- 'z-30',
67
- 'flex',
68
- 'justify-between',
69
- 'invisible',
70
- 'opacity-0',
71
- 'bg-gray',
72
- 'border-x-2',
73
- 'border-gray',
74
- 'pt-20',
75
- 'pb-16',
76
- 'transform',
77
- 'translate-y-full',
78
- 'transition'
79
- ],
80
- [
81
- 'before:left-0',
82
- 'before:-translate-x-full',
83
- 'before:content-[""]',
84
- 'before:w-1/2',
85
- 'before:h-full',
86
- 'before:block',
87
- 'before:absolute',
88
- 'before:top-0',
89
- 'before:transform',
90
- 'before:bg-gray'
91
- ],
92
- [
93
- 'after:right-0',
94
- 'after:translate-x-full',
95
- 'after:content-[""]',
96
- 'after:w-1/2',
97
- 'after:h-full',
98
- 'after:block',
99
- 'after:absolute',
100
- 'after:top-0',
101
- 'after:transform',
102
- 'after:bg-gray'
103
- ],
117
+ 'ml-2 transition-transform duration-200 ease-in-out group-hover:rotate-180',
104
118
  {
105
- '!visible !opacity-100 delay-500':
106
- openedMenu === item.pk
119
+ 'rotate-180': openedMenu === item.pk
107
120
  }
108
121
  )}
109
- >
122
+ />
123
+ )}
124
+ </Link>
125
+
126
+ {openedMenu === item.pk && hasChildren && (
127
+ <div
128
+ className={clsx(
129
+ [
130
+ 'absolute',
131
+ 'bottom-0',
132
+ 'left-0',
133
+ 'z-30',
134
+ 'w-full',
135
+ 'invisible',
136
+ 'opacity-0',
137
+ 'bg-gray',
138
+ 'border-x-2',
139
+ 'border-gray',
140
+ 'pt-20',
141
+ 'pb-16',
142
+ 'transform',
143
+ 'translate-y-full',
144
+ 'transition'
145
+ ],
146
+ [
147
+ 'before:left-0',
148
+ 'before:-translate-x-full',
149
+ 'before:content-[""]',
150
+ 'before:w-1/2',
151
+ 'before:h-full',
152
+ 'before:block',
153
+ 'before:absolute',
154
+ 'before:top-0',
155
+ 'before:transform',
156
+ 'before:bg-gray'
157
+ ],
158
+ [
159
+ 'after:right-0',
160
+ 'after:translate-x-full',
161
+ 'after:content-[""]',
162
+ 'after:w-1/2',
163
+ 'after:h-full',
164
+ 'after:block',
165
+ 'after:absolute',
166
+ 'after:top-0',
167
+ 'after:transform',
168
+ 'after:bg-gray'
169
+ ],
170
+ {
171
+ '!visible !opacity-100 delay-500': openedMenu === item.pk
172
+ }
173
+ )}
174
+ >
175
+ <div className="container mx-auto flex justify-between">
110
176
  <div className="flex space-x-8 lg:space-x-0">
111
- {item.children.map((child, index) => (
112
- <div key={index}>
177
+ {item.children?.map((child, childIndex) => (
178
+ <div key={childIndex}>
113
179
  <Link
114
180
  onClick={() => {
115
181
  dispatch(setOpenedMenu(null));
@@ -119,10 +185,10 @@ export default function Navbar(props: NavbarProps) {
119
185
  >
120
186
  {child.label}
121
187
  </Link>
122
- {child.children && (
188
+ {child.children && child.children.length > 0 && (
123
189
  <ul>
124
- {child.children.map((grandChild, index) => (
125
- <li key={index}>
190
+ {child.children?.map((grandChild, grandChildIndex) => (
191
+ <li key={grandChildIndex}>
126
192
  <Link
127
193
  onClick={() => {
128
194
  dispatch(setOpenedMenu(null));
@@ -139,25 +205,24 @@ export default function Navbar(props: NavbarProps) {
139
205
  </div>
140
206
  ))}
141
207
  </div>
142
- {item.extra_context.attributes.images && (
208
+ {item.extra_context?.attributes?.images && (
143
209
  <div className="flex">
144
210
  {item.extra_context.attributes.images.map(
145
- (image, index) =>
146
- image.kwargs.value.image && (
147
- <Link href={image.value.url} key={index}>
148
- {/* TODO: There is no image. It should be checked. May need fix. */}
211
+ (image, imageIndex) =>
212
+ image.kwargs?.value?.image && (
213
+ <Link href={image.value?.url} key={imageIndex}>
149
214
  <Image
150
215
  src={image.kwargs.value.image?.url}
151
- alt={image.value.title}
152
- title={image.value.title}
216
+ alt={image.value?.title}
217
+ title={image.value?.title}
153
218
  width={265}
154
219
  height={323}
155
220
  />
156
221
  <span className="block mt-4">
157
- {image.value.title}
222
+ {image.value?.title}
158
223
  </span>
159
224
  <span className="inline-block mt-2 text-xs uppercase border-b border-gray-500">
160
- {image.value.link_text}
225
+ {image.value?.link_text}
161
226
  </span>
162
227
  </Link>
163
228
  )
@@ -165,22 +230,24 @@ export default function Navbar(props: NavbarProps) {
165
230
  </div>
166
231
  )}
167
232
  </div>
168
- )}
169
- </li>
170
- )
171
- )}
172
- <li>
173
- <button
174
- onClick={() => dispatch(openSearch())}
175
- className="flex items-center gap-2 p-2 text-sm uppercase transition hover:text-secondary cursor-pointer"
176
- data-testid="header-nav-search"
177
- >
178
- <Icon name="search" size={24} /> {t('common.navbar.search')}
179
- </button>
180
- </li>
233
+ </div>
234
+ )}
235
+ </li>
236
+ );
237
+ })}
181
238
  </ul>
182
239
  </nav>
183
240
  {isSearchOpen && <Search />}
184
241
  </>
185
242
  );
186
243
  }
244
+
245
+ export default function Navbar({ menu, initialNavSettings }: NavbarProps) {
246
+ return (
247
+ <NavbarMenuProvider>
248
+ <NavbarContent menu={menu} initialNavSettings={initialNavSettings} />
249
+ </NavbarMenuProvider>
250
+ );
251
+ }
252
+
253
+ export type { NavbarMenuSettings };