@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
@@ -1,27 +1,56 @@
1
1
  'use client';
2
2
 
3
3
  import clsx from 'clsx';
4
- import { Button, Icon } from '@theme/components';
5
- import { useLocalization } from '@akinon/next/hooks';
4
+ import { Button, Icon, Select } from '@theme/components';
5
+ import { useRouter, useLocalization } from '@akinon/next/hooks';
6
6
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
7
7
  import { resetSelectedFacets } from '@theme/redux/reducers/category';
8
8
  import CategoryActiveFilters from '@theme/views/category/category-active-filters';
9
- import { useMemo, useTransition } from 'react';
9
+ import { useMemo, useTransition, useState, CSSProperties } from 'react';
10
10
  import { FilterItem } from './filter-item';
11
+ import { SortOption } from '@akinon/next/types';
12
+ import { usePathname, useSearchParams } from 'next/navigation';
13
+ import { useProductList } from '../product-list-registrar';
14
+ import { useNativeWidget } from '../native-widget-context';
11
15
 
12
16
  interface Props {
13
17
  isMenuOpen: boolean;
14
18
  setIsMenuOpen: (isMenuOpen: boolean) => void;
19
+ totalCount?: number;
20
+ sortOptions: SortOption[];
15
21
  }
16
22
 
17
23
  export const Filters = (props: Props) => {
18
24
  const facets = useAppSelector((state) => state.category.facets);
19
25
  const dispatch = useAppDispatch();
20
26
  const { t } = useLocalization();
21
- const { isMenuOpen, setIsMenuOpen } = props;
27
+ const { isMenuOpen, setIsMenuOpen, totalCount, sortOptions } = props;
28
+ const [openDropdown, setOpenDropdown] = useState<string | null>(null);
22
29
 
23
30
  const [isPending, startTransition] = useTransition();
24
31
 
32
+ const router = useRouter();
33
+ const searchParams = useSearchParams();
34
+ const pathname = usePathname();
35
+
36
+ // Get styles and properties from both contexts
37
+ const {
38
+ isDesigner,
39
+ filterStyles: registrarStyles,
40
+ filterProperties: registrarProps
41
+ } = useProductList();
42
+ const { filterStyles: widgetStyles, filterProperties: widgetProps } =
43
+ useNativeWidget();
44
+
45
+ // Use registrar data in designer mode, widget data otherwise
46
+ const filterStyles = isDesigner ? registrarStyles : widgetStyles;
47
+
48
+ // Merge properties - registrar props override widget props for live updates
49
+ const filterProperties = {
50
+ ...widgetProps,
51
+ ...registrarProps
52
+ };
53
+
25
54
  const haveFilter = useMemo(() => {
26
55
  return facets?.some((facet) =>
27
56
  facet?.data?.choices?.some((choice) => choice.is_selected)
@@ -32,50 +61,181 @@ export const Filters = (props: Props) => {
32
61
  dispatch(resetSelectedFacets());
33
62
  };
34
63
 
35
- return (
36
- <div
37
- className={clsx(
38
- 'w-9/10 fixed left-0 top-0 bottom-0 bg-white z-20 p-6 transition-all ease-in duration-300 lg:static lg:block lg:mr-16 lg:text-sm lg:p-0 lg:pt-4',
39
- isMenuOpen
40
- ? 'flex flex-col opacity-100 overflow-auto'
41
- : 'opacity-0 invisible absolute -translate-x-full lg:opacity-100 lg:visible lg:translate-x-0'
42
- )}
43
- >
44
- <div className="flex justify-between mb-6 lg:hidden">
45
- <h3 className="text-2xl">{t('category.filters.title')}</h3>
46
- <Icon name="close" size={22} onClick={() => setIsMenuOpen(false)} />
47
- </div>
48
- <div className="flex justify-between items-center mb-6 lg:hidden">
49
- <span className="text-sm">1 {t('category.filters.results')}</span>
50
- <span>{t('category.filters.ready_to_wear')}</span>
51
- </div>
52
-
53
- {facets?.map((facet) => {
54
- return (
55
- <FilterItem
56
- key={facet.key}
57
- facet={facet}
58
- isPending={isPending}
59
- startTransition={startTransition}
64
+ const handleSelectFilter = ({
65
+ key,
66
+ value
67
+ }: {
68
+ key: string;
69
+ value: string;
70
+ }) => {
71
+ const urlSearchParams = new URLSearchParams(searchParams.toString());
72
+
73
+ urlSearchParams.set(key, value);
74
+
75
+ router.push(pathname + '?' + urlSearchParams.toString());
76
+ };
77
+
78
+ // Get layout from properties
79
+ const layout = (filterProperties.layout as string) || 'top';
80
+ const containerStyles = filterStyles as CSSProperties;
81
+
82
+ // Parse item properties (they come as strings like '14px' from Theme Editor)
83
+ const itemPaddingY =
84
+ typeof filterProperties['item-padding-y'] === 'number'
85
+ ? filterProperties['item-padding-y']
86
+ : typeof filterProperties['item-padding-y'] === 'string'
87
+ ? parseInt(filterProperties['item-padding-y'], 10)
88
+ : 6;
89
+ const itemPaddingX =
90
+ typeof filterProperties['item-padding-x'] === 'number'
91
+ ? filterProperties['item-padding-x']
92
+ : typeof filterProperties['item-padding-x'] === 'string'
93
+ ? parseInt(filterProperties['item-padding-x'], 10)
94
+ : 0;
95
+ const itemMarginBottom =
96
+ typeof filterProperties['item-margin-bottom'] === 'number'
97
+ ? filterProperties['item-margin-bottom']
98
+ : typeof filterProperties['item-margin-bottom'] === 'string'
99
+ ? parseInt(filterProperties['item-margin-bottom'], 10)
100
+ : 0;
101
+
102
+ // Sidebar layout (desktop + mobile)
103
+ if (layout === 'sidebar') {
104
+ return (
105
+ <>
106
+ {/* Mobile overlay backdrop */}
107
+ {isMenuOpen && (
108
+ <div
109
+ className="fixed inset-0 bg-black/80 z-40 lg:hidden transition-opacity duration-300"
110
+ onClick={() => setIsMenuOpen(false)}
60
111
  />
61
- );
62
- })}
63
-
64
- <div className="lg:hidden">
65
- <CategoryActiveFilters />
66
- </div>
67
-
68
- {haveFilter && (
69
- <div className="lg:hidden">
70
- <Button
71
- onClick={handleResetFilter}
72
- appearance="outlined"
73
- className="w-full mt-4 lg:hidden"
74
- >
75
- {t('category.filters.clear_all')}
76
- </Button>
112
+ )}
113
+
114
+ {/* Mobile slide-in menu */}
115
+ <div
116
+ className={clsx(
117
+ 'w-11/12 fixed left-0 top-0 bottom-0 bg-white z-50 transition-all ease-in duration-300 lg:hidden',
118
+ isMenuOpen
119
+ ? 'flex flex-col opacity-100 overflow-auto'
120
+ : 'opacity-0 invisible absolute -translate-x-full'
121
+ )}
122
+ style={containerStyles}
123
+ data-section-id="filters-section"
124
+ >
125
+ <div className="relative flex items-center justify-center mb-6 border-b border-black-650 pt-2 pb-3">
126
+ <div className="flex flex-col items-center text-center gap-1 text-black-750">
127
+ <h3 className="text-sm">{t('category.filters.mobile_title')}</h3>
128
+ <span className="text-[10px]">
129
+ {totalCount} {t('category.header.results')}
130
+ </span>
131
+ </div>
132
+ <Button
133
+ type="button"
134
+ appearance="ghost"
135
+ aria-label="Close Filter Menu"
136
+ onClick={() => setIsMenuOpen(false)}
137
+ className="absolute right-4 p-0 text-black bg-transparent hover:bg-transparent hover:text-black"
138
+ >
139
+ <Icon name="close" size={16} />
140
+ </Button>
141
+ </div>
142
+
143
+ <div className="px-4">
144
+ {facets?.map((facet) => {
145
+ return (
146
+ <FilterItem
147
+ key={facet.key}
148
+ facet={facet}
149
+ isPending={isPending}
150
+ startTransition={startTransition}
151
+ itemPaddingY={itemPaddingY}
152
+ itemPaddingX={itemPaddingX}
153
+ itemMarginBottom={itemMarginBottom}
154
+ />
155
+ );
156
+ })}
157
+ </div>
158
+
159
+ <div className="px-4 flex items-center justify-between text-black-750 text-sm">
160
+ {t('category.header.sort_by')}:
161
+ <Select
162
+ options={sortOptions}
163
+ value={sortOptions?.find(({ is_selected }) => is_selected)?.value}
164
+ data-testid="list-sorter"
165
+ className="border-0 text-black-750 text-sm"
166
+ onChange={(e) => {
167
+ handleSelectFilter({
168
+ key: 'sorter',
169
+ value: e.currentTarget.value
170
+ });
171
+ }}
172
+ borderless={false}
173
+ />
174
+ </div>
175
+
176
+ <div className="p-8">
177
+ <CategoryActiveFilters />
178
+ </div>
179
+
180
+ {haveFilter && (
181
+ <div className="px-4">
182
+ <Button
183
+ onClick={handleResetFilter}
184
+ appearance="filled"
185
+ className="w-full mt-4"
186
+ data-section-id="filters-section"
187
+ >
188
+ {t('category.filters.clear_all')}
189
+ </Button>
190
+ </div>
191
+ )}
77
192
  </div>
78
- )}
79
- </div>
80
- );
193
+
194
+ {/* Desktop sidebar */}
195
+ <aside
196
+ className="hidden lg:block lg:col-span-1"
197
+ style={containerStyles}
198
+ data-section-id="filters-section"
199
+ >
200
+ <div className="sticky top-24">
201
+ <h3 className="text-lg font-medium mb-4">
202
+ {t('category.filters.title')}
203
+ </h3>
204
+
205
+ <div>
206
+ {facets?.map((facet) => {
207
+ return (
208
+ <FilterItem
209
+ key={facet.key}
210
+ facet={facet}
211
+ isPending={isPending}
212
+ startTransition={startTransition}
213
+ itemPaddingY={itemPaddingY}
214
+ itemPaddingX={itemPaddingX}
215
+ itemMarginBottom={itemMarginBottom}
216
+ />
217
+ );
218
+ })}
219
+ </div>
220
+
221
+ {haveFilter && (
222
+ <div className="mt-6">
223
+ <Button
224
+ onClick={handleResetFilter}
225
+ appearance="outlined"
226
+ className="w-full"
227
+ data-section-id="filters-section"
228
+ >
229
+ {t('category.filters.clear_all')}
230
+ </Button>
231
+ </div>
232
+ )}
233
+ </div>
234
+ </aside>
235
+ </>
236
+ );
237
+ }
238
+
239
+ // Top layout - return null, handled in category-header
240
+ return null;
81
241
  };
@@ -1,8 +1,10 @@
1
1
  import clsx from 'clsx';
2
2
  import { BreadcrumbResultType, GetCategoryResponse } from '@akinon/next/types';
3
+ import ThemePlaceholder from '@akinon/next/components/theme-editor/theme-placeholder';
3
4
  import Breadcrumb from '@theme/views/breadcrumb';
4
5
  import { CategoryBanner } from '@theme/views/category/category-banner';
5
6
  import ListPage from '@theme/views/category/category-info';
7
+ import ProductListRegistrar from '@theme/views/category/product-list-registrar';
6
8
 
7
9
  export default async function Layout({
8
10
  data,
@@ -14,7 +16,8 @@ export default async function Layout({
14
16
  breadcrumbData?: BreadcrumbResultType[];
15
17
  }) {
16
18
  return (
17
- <>
19
+ <ProductListRegistrar>
20
+ <ThemePlaceholder slug="list-page-body-new" />
18
21
  <div
19
22
  className={clsx(
20
23
  data?.category?.attributes?.category_banner
@@ -25,16 +28,16 @@ export default async function Layout({
25
28
  {children}
26
29
  <div
27
30
  className={clsx(
28
- 'my-4 lg:mt-7',
31
+ 'my-10 lg:my-16',
29
32
  data?.category?.attributes?.category_banner &&
30
33
  'lg:absolute lg:inset-x-0 z-10 container lg:my-4 mx-auto'
31
34
  )}
32
35
  >
33
36
  <Breadcrumb breadcrumbList={breadcrumbData} />
34
37
  </div>
35
- <CategoryBanner {...data?.category?.attributes?.category_banner} />
38
+ <CategoryBanner {...data?.category?.attributes?.banner} />
36
39
  </div>
37
40
  <ListPage data={data} />
38
- </>
41
+ </ProductListRegistrar>
39
42
  );
40
43
  }
@@ -0,0 +1,257 @@
1
+ 'use client';
2
+
3
+ import {
4
+ createContext,
5
+ useContext,
6
+ useEffect,
7
+ useState,
8
+ PropsWithChildren
9
+ } from 'react';
10
+ import { useGetWidgetQuery } from '@akinon/next/data/client/misc';
11
+
12
+ // Constants
13
+ export const PLACEHOLDER_ID = 'product-list-page';
14
+ export const FILTERS_SECTION_ID = 'filters-section';
15
+ export const PRODUCTS_SECTION_ID = 'products-section';
16
+ export const PRODUCT_ITEM_SECTION_ID = 'product-item-section';
17
+ export const PAGINATION_SECTION_ID = 'pagination-section';
18
+ export const WIDGET_SLUG = 'product-list-page-styles';
19
+
20
+ interface NativeWidgetContextValue {
21
+ filterStyles: Record<string, unknown>;
22
+ filterProperties: Record<string, unknown>;
23
+ productsProperties: Record<string, unknown>;
24
+ productsStyles: Record<string, unknown>;
25
+ productItemProperties: Record<string, unknown>;
26
+ productItemStyles: Record<string, unknown>;
27
+ paginationProperties: Record<string, unknown>;
28
+ paginationStyles: Record<string, unknown>;
29
+ }
30
+
31
+ const NativeWidgetContext = createContext<NativeWidgetContextValue>({
32
+ filterStyles: {},
33
+ filterProperties: {
34
+ layout: 'top',
35
+ 'item-padding-y': 6,
36
+ 'item-padding-x': 0,
37
+ 'item-margin-bottom': 0
38
+ },
39
+ productsProperties: {
40
+ 'show-layout-buttons': true,
41
+ 'available-layouts': ['2', '3', '4']
42
+ },
43
+ productsStyles: {},
44
+ productItemProperties: {
45
+ 'show-fav-button': true,
46
+ 'fav-button-position': 'top-right'
47
+ },
48
+ productItemStyles: {},
49
+ paginationProperties: {
50
+ type: 'list'
51
+ },
52
+ paginationStyles: {}
53
+ });
54
+
55
+ export function NativeWidgetProvider({ children }: PropsWithChildren) {
56
+ const [filterStyles, setFilterStyles] = useState<Record<string, unknown>>({});
57
+ const [filterProperties, setFilterProperties] = useState<
58
+ Record<string, unknown>
59
+ >({
60
+ layout: 'top',
61
+ 'item-padding-y': 6,
62
+ 'item-padding-x': 0,
63
+ 'item-margin-bottom': 0
64
+ });
65
+ const [productsProperties, setProductsProperties] = useState<
66
+ Record<string, unknown>
67
+ >({
68
+ 'show-layout-buttons': true,
69
+ 'available-layouts': ['2', '3', '4']
70
+ });
71
+ const [productsStyles, setProductsStyles] = useState<Record<string, unknown>>(
72
+ {}
73
+ );
74
+ const [productItemProperties, setProductItemProperties] = useState<
75
+ Record<string, unknown>
76
+ >({
77
+ 'show-fav-button': true,
78
+ 'fav-button-position': 'top-right'
79
+ });
80
+ const [productItemStyles, setProductItemStyles] = useState<
81
+ Record<string, unknown>
82
+ >({});
83
+ const [paginationProperties, setPaginationProperties] = useState<
84
+ Record<string, unknown>
85
+ >({
86
+ type: 'list'
87
+ });
88
+ const [paginationStyles, setPaginationStyles] = useState<
89
+ Record<string, unknown>
90
+ >({});
91
+
92
+ // Fetch styles from widget
93
+ const { data: widgetData } = useGetWidgetQuery(WIDGET_SLUG);
94
+
95
+ // Load styles from widget (initial load)
96
+ useEffect(() => {
97
+ if (!widgetData) return;
98
+
99
+ const attributes = widgetData.attributes || {};
100
+
101
+ // Load filters section
102
+ const filtersData = attributes[FILTERS_SECTION_ID];
103
+ if (filtersData) {
104
+ let parsedData: unknown;
105
+ if (typeof filtersData === 'string') {
106
+ try {
107
+ parsedData = JSON.parse(filtersData);
108
+ } catch {
109
+ // ignore
110
+ }
111
+ } else if (typeof filtersData === 'object' && 'value' in filtersData) {
112
+ try {
113
+ parsedData = JSON.parse((filtersData as { value: string }).value);
114
+ } catch {
115
+ // ignore
116
+ }
117
+ } else {
118
+ parsedData = filtersData;
119
+ }
120
+
121
+ if (parsedData && typeof parsedData === 'object') {
122
+ const data = parsedData as Record<string, unknown>;
123
+ setFilterStyles((data.styles as Record<string, string>) || {});
124
+ setFilterProperties(
125
+ (data.properties as Record<string, unknown>) || {
126
+ layout: 'top',
127
+ 'item-padding-y': 6,
128
+ 'item-padding-x': 0,
129
+ 'item-margin-bottom': 0
130
+ }
131
+ );
132
+ }
133
+ }
134
+
135
+ // Load products section
136
+ const productsData = attributes[PRODUCTS_SECTION_ID];
137
+ if (productsData) {
138
+ let parsedData: unknown;
139
+ if (typeof productsData === 'string') {
140
+ try {
141
+ parsedData = JSON.parse(productsData);
142
+ } catch {
143
+ // ignore
144
+ }
145
+ } else if (typeof productsData === 'object' && 'value' in productsData) {
146
+ try {
147
+ parsedData = JSON.parse((productsData as { value: string }).value);
148
+ } catch {
149
+ // ignore
150
+ }
151
+ } else {
152
+ parsedData = productsData;
153
+ }
154
+
155
+ if (parsedData && typeof parsedData === 'object') {
156
+ const data = parsedData as Record<string, unknown>;
157
+ setProductsStyles((data.styles as Record<string, string>) || {});
158
+ setProductsProperties(
159
+ (data.properties as Record<string, unknown>) || {
160
+ 'show-layout-buttons': true,
161
+ 'available-layouts': ['2', '3', '4']
162
+ }
163
+ );
164
+ }
165
+ }
166
+
167
+ // Load product-item section
168
+ const productItemData = attributes[PRODUCT_ITEM_SECTION_ID];
169
+ if (productItemData) {
170
+ let parsedData: unknown;
171
+ if (typeof productItemData === 'string') {
172
+ try {
173
+ parsedData = JSON.parse(productItemData);
174
+ } catch {
175
+ // ignore
176
+ }
177
+ } else if (
178
+ typeof productItemData === 'object' &&
179
+ 'value' in productItemData
180
+ ) {
181
+ try {
182
+ parsedData = JSON.parse((productItemData as { value: string }).value);
183
+ } catch {
184
+ // ignore
185
+ }
186
+ } else {
187
+ parsedData = productItemData;
188
+ }
189
+
190
+ if (parsedData && typeof parsedData === 'object') {
191
+ const data = parsedData as Record<string, unknown>;
192
+ setProductItemStyles((data.styles as Record<string, string>) || {});
193
+ setProductItemProperties(
194
+ (data.properties as Record<string, unknown>) || {
195
+ 'show-fav-button': true,
196
+ 'fav-button-position': 'top-right'
197
+ }
198
+ );
199
+ }
200
+ }
201
+
202
+ // Load pagination section
203
+ const paginationData = attributes[PAGINATION_SECTION_ID];
204
+ if (paginationData) {
205
+ let parsedData: unknown;
206
+ if (typeof paginationData === 'string') {
207
+ try {
208
+ parsedData = JSON.parse(paginationData);
209
+ } catch {
210
+ // ignore
211
+ }
212
+ } else if (
213
+ typeof paginationData === 'object' &&
214
+ 'value' in paginationData
215
+ ) {
216
+ try {
217
+ parsedData = JSON.parse((paginationData as { value: string }).value);
218
+ } catch {
219
+ // ignore
220
+ }
221
+ } else {
222
+ parsedData = paginationData;
223
+ }
224
+
225
+ if (parsedData && typeof parsedData === 'object') {
226
+ const data = parsedData as Record<string, unknown>;
227
+ setPaginationStyles((data.styles as Record<string, string>) || {});
228
+ setPaginationProperties(
229
+ (data.properties as Record<string, unknown>) || {
230
+ type: 'list'
231
+ }
232
+ );
233
+ }
234
+ }
235
+ }, [widgetData]);
236
+
237
+ const value: NativeWidgetContextValue = {
238
+ filterStyles,
239
+ filterProperties,
240
+ productsProperties,
241
+ productsStyles,
242
+ productItemProperties,
243
+ productItemStyles,
244
+ paginationProperties,
245
+ paginationStyles
246
+ };
247
+
248
+ return (
249
+ <NativeWidgetContext.Provider value={value}>
250
+ {children}
251
+ </NativeWidgetContext.Provider>
252
+ );
253
+ }
254
+
255
+ export function useNativeWidget() {
256
+ return useContext(NativeWidgetContext);
257
+ }