@akinon/projectzero 1.62.0 → 1.64.0

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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # @akinon/projectzero
2
2
 
3
+ ## 1.64.0
4
+
5
+ ## 1.63.0
6
+
7
+ ### Minor Changes
8
+
9
+ - 9b6bf91: ZERO-2660: Change campaings to campaigns
10
+
3
11
  ## 1.62.0
4
12
 
5
13
  ## 1.61.0
@@ -1,5 +1,67 @@
1
1
  # projectzeronext
2
2
 
3
+ ## 1.64.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c53ea3e: ZERO-2609: Reset additional form fields when selectedFormType is not company
8
+ - 27a5296: ZERO-2631:Fix Checkbox Click
9
+ - e665a0a: ZERO-2625: Add click outside functionality to close mobile menu
10
+ - d3474c6: ZERO-2655: Add data source shipping option
11
+ - 7a4bb76: ZERO-2610:Refactor FilterItem component for better readability and efficiency
12
+ - fac2e5b: ZERO-2622: Add isMenuOpen state to Category reducer and update CategoryInfo component
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies [c53ea3e]
17
+ - Updated dependencies [d3474c6]
18
+ - @akinon/next@1.64.0
19
+ - @akinon/pz-akifast@1.64.0
20
+ - @akinon/pz-b2b@1.64.0
21
+ - @akinon/pz-basket-gift-pack@1.64.0
22
+ - @akinon/pz-bkm@1.64.0
23
+ - @akinon/pz-checkout-gift-pack@1.64.0
24
+ - @akinon/pz-click-collect@1.64.0
25
+ - @akinon/pz-credit-payment@1.64.0
26
+ - @akinon/pz-gpay@1.64.0
27
+ - @akinon/pz-masterpass@1.64.0
28
+ - @akinon/pz-one-click-checkout@1.64.0
29
+ - @akinon/pz-otp@1.64.0
30
+ - @akinon/pz-pay-on-delivery@1.64.0
31
+ - @akinon/pz-saved-card@1.64.0
32
+ - @akinon/pz-tabby-extension@1.64.0
33
+
34
+ ## 1.63.0
35
+
36
+ ### Minor Changes
37
+
38
+ - 0d3a913: ZERO-2725: Update decimal scale in Price component
39
+ - 9b6bf91: ZERO-2660: Change campaings to campaigns
40
+
41
+ ### Patch Changes
42
+
43
+ - Updated dependencies [a4c8d6a]
44
+ - Updated dependencies [fda5b92]
45
+ - Updated dependencies [0d3a913]
46
+ - Updated dependencies [c45b62c]
47
+ - Updated dependencies [d409996]
48
+ - Updated dependencies [dcc8a15]
49
+ - @akinon/next@1.63.0
50
+ - @akinon/pz-b2b@1.63.0
51
+ - @akinon/pz-akifast@1.63.0
52
+ - @akinon/pz-basket-gift-pack@1.63.0
53
+ - @akinon/pz-bkm@1.63.0
54
+ - @akinon/pz-checkout-gift-pack@1.63.0
55
+ - @akinon/pz-click-collect@1.63.0
56
+ - @akinon/pz-credit-payment@1.63.0
57
+ - @akinon/pz-gpay@1.63.0
58
+ - @akinon/pz-masterpass@1.63.0
59
+ - @akinon/pz-one-click-checkout@1.63.0
60
+ - @akinon/pz-otp@1.63.0
61
+ - @akinon/pz-pay-on-delivery@1.63.0
62
+ - @akinon/pz-saved-card@1.63.0
63
+ - @akinon/pz-tabby-extension@1.63.0
64
+
3
65
  ## 1.62.0
4
66
 
5
67
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "projectzeronext",
3
- "version": "1.62.0",
3
+ "version": "1.64.0",
4
4
  "private": true,
5
5
  "license": "MIT",
6
6
  "scripts": {
@@ -22,20 +22,21 @@
22
22
  "prestart": "pz-prestart"
23
23
  },
24
24
  "dependencies": {
25
- "@akinon/next": "1.62.0",
26
- "@akinon/pz-akifast": "1.62.0",
27
- "@akinon/pz-b2b": "1.62.0",
28
- "@akinon/pz-basket-gift-pack": "1.62.0",
29
- "@akinon/pz-bkm": "1.62.0",
30
- "@akinon/pz-checkout-gift-pack": "1.62.0",
31
- "@akinon/pz-click-collect": "1.62.0",
32
- "@akinon/pz-credit-payment": "1.62.0",
33
- "@akinon/pz-gpay": "1.62.0",
34
- "@akinon/pz-masterpass": "1.62.0",
35
- "@akinon/pz-one-click-checkout": "1.62.0",
36
- "@akinon/pz-otp": "1.62.0",
37
- "@akinon/pz-pay-on-delivery": "1.62.0",
38
- "@akinon/pz-tabby-extension": "1.62.0",
25
+ "@akinon/next": "1.64.0",
26
+ "@akinon/pz-akifast": "1.64.0",
27
+ "@akinon/pz-b2b": "1.64.0",
28
+ "@akinon/pz-basket-gift-pack": "1.64.0",
29
+ "@akinon/pz-bkm": "1.64.0",
30
+ "@akinon/pz-checkout-gift-pack": "1.64.0",
31
+ "@akinon/pz-click-collect": "1.64.0",
32
+ "@akinon/pz-credit-payment": "1.64.0",
33
+ "@akinon/pz-gpay": "1.64.0",
34
+ "@akinon/pz-masterpass": "1.64.0",
35
+ "@akinon/pz-one-click-checkout": "1.64.0",
36
+ "@akinon/pz-otp": "1.64.0",
37
+ "@akinon/pz-pay-on-delivery": "1.64.0",
38
+ "@akinon/pz-saved-card": "1.64.0",
39
+ "@akinon/pz-tabby-extension": "1.64.0",
39
40
  "@hookform/resolvers": "2.9.0",
40
41
  "@next/third-parties": "14.1.0",
41
42
  "@react-google-maps/api": "2.17.1",
@@ -60,7 +61,7 @@
60
61
  "yup": "0.32.11"
61
62
  },
62
63
  "devDependencies": {
63
- "@akinon/eslint-plugin-projectzero": "1.62.0",
64
+ "@akinon/eslint-plugin-projectzero": "1.64.0",
64
65
  "@semantic-release/changelog": "6.0.2",
65
66
  "@semantic-release/exec": "6.0.3",
66
67
  "@semantic-release/git": "10.0.1",
@@ -286,11 +286,11 @@
286
286
  "empty_coupon": "You don't have any coupons"
287
287
  },
288
288
  "title": {
289
- "campaings": {
289
+ "campaigns": {
290
290
  "active": "Active Campaigns",
291
- "to_be_active": "Campaings to be Active",
292
- "expired": "Expired Campaings",
293
- "used": "Used Campaings"
291
+ "to_be_active": "Campaigns to be Active",
292
+ "expired": "Expired Campaigns",
293
+ "used": "Used Campaigns"
294
294
  },
295
295
  "coupons": {
296
296
  "active": "Active Coupons",
@@ -286,7 +286,7 @@
286
286
  "empty_coupon": "Herhangi bir kuponunuz yok"
287
287
  },
288
288
  "title": {
289
- "campaings": {
289
+ "campaigns": {
290
290
  "active": "Aktif Kampanyalar",
291
291
  "to_be_active": "Aktif Olacak Kampanyalar",
292
292
  "expired": "Süresi Dolmuş Kampanyalar",
@@ -51,7 +51,7 @@ export default function Page() {
51
51
  {basketOffersLoading && <LoaderSpinner className="mb-8" />}
52
52
  {basketOffersSuccess && (
53
53
  <CouponItem
54
- mainTitle={t('account.my_vouchers.title.campaings.active')}
54
+ mainTitle={t('account.my_vouchers.title.campaigns.active')}
55
55
  subTitles={[
56
56
  t('account.my_vouchers.card.campaign_name'),
57
57
  t('account.my_vouchers.card.starting_date'),
@@ -68,7 +68,7 @@ export default function Page() {
68
68
  {futureBasketOffersLoading && <LoaderSpinner className="mb-8" />}
69
69
  {futureBasketOffersSuccess && (
70
70
  <CouponItem
71
- mainTitle={t('account.my_vouchers.title.campaings.to_be_active')}
71
+ mainTitle={t('account.my_vouchers.title.campaigns.to_be_active')}
72
72
  subTitles={[
73
73
  t('account.my_vouchers.card.campaign_name'),
74
74
  t('account.my_vouchers.card.starting_date'),
@@ -85,7 +85,7 @@ export default function Page() {
85
85
  {expiredBasketOffersLoading && <LoaderSpinner className="mb-8" />}
86
86
  {expiredBasketOffersSuccess && (
87
87
  <CouponItem
88
- mainTitle={t('account.my_vouchers.title.campaings.expired')}
88
+ mainTitle={t('account.my_vouchers.title.campaigns.expired')}
89
89
  subTitles={[
90
90
  t('account.my_vouchers.card.campaign_name'),
91
91
  t('account.my_vouchers.card.starting_date'),
@@ -102,7 +102,7 @@ export default function Page() {
102
102
  {discountItemsLoading && <LoaderSpinner className="mb-8" />}
103
103
  {discountItemsSuccess && (
104
104
  <CouponItem
105
- mainTitle={t('account.my_vouchers.title.campaings.used')}
105
+ mainTitle={t('account.my_vouchers.title.campaigns.used')}
106
106
  subTitles={[
107
107
  t('account.my_vouchers.card.campaign_name'),
108
108
  t('account.my_vouchers.card.starting_date'),
@@ -3,7 +3,7 @@ import { CheckboxProps } from '@theme/components/types';
3
3
  import { twMerge } from 'tailwind-merge';
4
4
 
5
5
  const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => {
6
- const { children, checked, error, ...rest } = props;
6
+ const { children, checked = false, error, ...rest } = props;
7
7
 
8
8
  return (
9
9
  <label className={twMerge('flex flex-col text-xs', props.className)}>
@@ -12,7 +12,7 @@ const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => {
12
12
  type="checkbox"
13
13
  {...rest}
14
14
  ref={ref}
15
- checked={checked ?? false}
15
+ defaultChecked={checked}
16
16
  className="w-4 h-4 shrink-0"
17
17
  />
18
18
  {children && <span className="ml-2">{children}</span>}
@@ -3,6 +3,7 @@ import NumberFormat, { NumberFormatProps } from 'react-number-format';
3
3
  import { getCurrency } from '@akinon/next/utils';
4
4
  import { PriceProps } from '@theme/types';
5
5
  import { useLocalization } from '@akinon/next/hooks';
6
+ import Settings from '@theme/settings';
6
7
 
7
8
  export const Price = (props: NumberFormatProps & PriceProps) => {
8
9
  const {
@@ -37,6 +38,10 @@ export const Price = (props: NumberFormatProps & PriceProps) => {
37
38
  [currencyCode_, useCurrencySymbol, useCurrencyAfterPrice, useCurrencySpace]
38
39
  );
39
40
 
41
+ const currentCurrencyDecimalScale = Settings.localization.currencies.find(
42
+ (currency) => currency.code === currencyCode_
43
+ ).decimalScale;
44
+
40
45
  return (
41
46
  <NumberFormat
42
47
  value={useNegative ? `-${useNegativeSpace}${_value}` : _value}
@@ -45,7 +50,7 @@ export const Price = (props: NumberFormatProps & PriceProps) => {
45
50
  }}
46
51
  displayType={displayType}
47
52
  thousandSeparator={thousandSeparator}
48
- decimalScale={decimalScale}
53
+ decimalScale={currentCurrencyDecimalScale ?? decimalScale}
49
54
  decimalSeparator={decimalSeparator}
50
55
  fixedDecimalScale={fixedDecimalScale}
51
56
  {...rest}
@@ -7,11 +7,13 @@ import { WIDGET_TYPE } from '@theme/types';
7
7
  export interface CategoryState {
8
8
  facets: Facet[];
9
9
  selectedFacets: Facet[];
10
+ isMenuOpen: boolean;
10
11
  }
11
12
 
12
13
  const initialState: CategoryState = {
13
14
  facets: [],
14
- selectedFacets: []
15
+ selectedFacets: [],
16
+ isMenuOpen: false
15
17
  };
16
18
 
17
19
  const categorySlice = createSlice({
@@ -24,6 +26,9 @@ const categorySlice = createSlice({
24
26
  setSelectedFacets(state, action) {
25
27
  state.selectedFacets = action.payload;
26
28
  },
29
+ setMenuOpen(state, action) {
30
+ state.isMenuOpen = action.payload;
31
+ },
27
32
  toggleFacet(state, action) {
28
33
  const facets = JSON.parse(JSON.stringify(state.facets));
29
34
 
@@ -81,6 +86,7 @@ const categorySlice = createSlice({
81
86
  export const {
82
87
  setFacets,
83
88
  setSelectedFacets,
89
+ setMenuOpen,
84
90
  toggleFacet,
85
91
  removeCategoryFacet,
86
92
  resetSelectedFacets
@@ -183,12 +183,22 @@ export const AddressForm = (props: Props) => {
183
183
  if (data && country) {
184
184
  reset({
185
185
  ...data,
186
- is_corporate:
187
- String(data.is_corporate) === AddressType.company ? 'true' : 'false' // TODO: Fix this! This hack for radio buttons can't be set to boolean value
186
+ is_corporate: String(data.is_corporate)
188
187
  });
189
188
  }
190
189
  }, [data, country, reset]);
191
190
 
191
+ useEffect(() => {
192
+ if (selectedFormType !== AddressType.company) {
193
+ reset({
194
+ ...watch(),
195
+ company_name: '',
196
+ tax_office: '',
197
+ tax_no: ''
198
+ });
199
+ }
200
+ }, [selectedFormType, reset, watch]);
201
+
192
202
  return (
193
203
  <form
194
204
  onSubmit={handleSubmit(onSubmit)}
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useMemo, useState } from 'react';
3
+ import { useEffect, useMemo } from 'react';
4
4
  import clsx from 'clsx';
5
5
  import { useSearchParams } from 'next/navigation';
6
6
  import { CategoryHeader } from './category-header';
@@ -8,13 +8,14 @@ import { Filters } from './filters';
8
8
  import { Pagination } from '@theme/components';
9
9
  import { ProductItem } from '@theme/views/product-item';
10
10
  import { GetCategoryResponse } from '@akinon/next/types';
11
- import { useAppDispatch } from '@akinon/next/redux/hooks';
12
- import { setFacets } from '@theme/redux/reducers/category';
11
+ import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
12
+ import { setFacets, setMenuOpen } from '@theme/redux/reducers/category';
13
13
  import CategoryActiveFilters from '@theme/views/category/category-active-filters';
14
14
  import { useLocalization } from '@akinon/next/hooks';
15
15
  import { Link, LoaderSpinner } from '@akinon/next/components';
16
16
  import { ROUTES } from '@theme/routes';
17
17
  import { useRouter } from '@akinon/next/hooks';
18
+ import { RootState } from '@theme/redux/store';
18
19
 
19
20
  interface ListPageProps {
20
21
  data: GetCategoryResponse;
@@ -22,13 +23,16 @@ interface ListPageProps {
22
23
 
23
24
  export default function ListPage(props: ListPageProps) {
24
25
  const { data } = props;
25
- const [isMenuOpen, setIsMenuOpen] = useState(false); // TODO: Move to redux
26
+ const dispatch = useAppDispatch();
27
+ const isMenuOpen = useAppSelector(
28
+ (state: RootState) => state.category.isMenuOpen
29
+ );
26
30
 
27
31
  const searchParams = useSearchParams();
28
32
  const router = useRouter();
29
33
 
30
34
  const layoutSize = useMemo(
31
- () => searchParams.get('layout') ?? 3,
35
+ () => Number(searchParams.get('layout') ?? 3),
32
36
  [searchParams]
33
37
  );
34
38
 
@@ -37,16 +41,24 @@ export default function ListPage(props: ListPageProps) {
37
41
  [searchParams]
38
42
  );
39
43
 
44
+ const itemDimensions = useMemo(() => {
45
+ switch (layoutSize) {
46
+ case 2:
47
+ return { width: 510, height: 765 };
48
+ case 3:
49
+ default:
50
+ return { width: 340, height: 510 };
51
+ }
52
+ }, [layoutSize]);
53
+
40
54
  useEffect(() => {
41
55
  if (page > 1 && data.products?.length === 0) {
42
56
  const newUrl = new URL(window.location.href);
43
-
44
57
  newUrl.searchParams.delete('page');
45
58
  router.push(newUrl.pathname + newUrl.search, undefined);
46
59
  }
47
- }, [searchParams, data.products, page]); // eslint-disable-line react-hooks/exhaustive-deps
60
+ }, [searchParams, data.products, page]);
48
61
 
49
- const dispatch = useAppDispatch();
50
62
  const { t } = useLocalization();
51
63
 
52
64
  useEffect(() => {
@@ -58,9 +70,12 @@ export default function ListPage(props: ListPageProps) {
58
70
  <>
59
71
  <div className="container px-4 mx-auto lg:px-0 lg:my-4">
60
72
  <div className="grid grid-cols-[19rem_1fr]">
61
- <Filters isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} />
73
+ <Filters
74
+ isMenuOpen={isMenuOpen}
75
+ setIsMenuOpen={(open) => dispatch(setMenuOpen(open))}
76
+ />
62
77
  <div
63
- onClick={() => setIsMenuOpen(false)}
78
+ onClick={() => dispatch(setMenuOpen(false))}
64
79
  className={clsx(
65
80
  'transition-opacity duration-300 ease-linear lg:hidden',
66
81
  isMenuOpen
@@ -71,7 +86,7 @@ export default function ListPage(props: ListPageProps) {
71
86
  <div className="flex flex-col items-center lg:items-stretch col-span-2 lg:col-span-1">
72
87
  <CategoryHeader
73
88
  totalCount={data.pagination?.total_count}
74
- setMenuStatus={() => setIsMenuOpen(true)}
89
+ setMenuStatus={() => dispatch(setMenuOpen(true))}
75
90
  sortOptions={data.sorters}
76
91
  />
77
92
  <div className="hidden lg:block">
@@ -91,18 +106,17 @@ export default function ListPage(props: ListPageProps) {
91
106
 
92
107
  <div
93
108
  className={clsx('grid gap-x-4 gap-y-12 grid-cols-2', {
94
- 'md:grid-cols-3': Number(layoutSize) === 3,
95
- 'lg:grid-cols-2': Number(layoutSize) === 2,
96
- 'lg:grid-cols-3': Number(layoutSize) === 3
109
+ 'md:grid-cols-3': layoutSize === 3,
110
+ 'lg:grid-cols-2': layoutSize === 2,
111
+ 'lg:grid-cols-3': layoutSize === 3
97
112
  })}
98
113
  >
99
114
  {data.products.map((product, index) => (
100
115
  <ProductItem
101
116
  key={product.pk}
102
117
  product={product}
103
- // TODO: Find a better way to handle layout size
104
- width={340}
105
- height={510}
118
+ width={itemDimensions.width}
119
+ height={itemDimensions.height}
106
120
  index={index}
107
121
  />
108
122
  ))}
@@ -0,0 +1,131 @@
1
+ import clsx from 'clsx';
2
+ import { useAppDispatch } from '@akinon/next/redux/hooks';
3
+ import { Facet, FacetChoice } from '@akinon/next/types';
4
+ import { Accordion, Radio, Checkbox } from '../../../components';
5
+ import { WIDGET_TYPE } from '../../../types';
6
+ import { SizeFilter } from './size-filter';
7
+ import { toggleFacet } from '@theme/redux/reducers/category';
8
+ import { commonProductAttributes } from '@theme/settings';
9
+ import { useRouter } from '@akinon/next/hooks';
10
+
11
+ const COMPONENT_TYPES = {
12
+ [WIDGET_TYPE.category]: Radio,
13
+ [WIDGET_TYPE.multiselect]: Checkbox
14
+ };
15
+
16
+ const sizeKey = commonProductAttributes.find(
17
+ (item) => item.translationKey === 'size'
18
+ ).key;
19
+
20
+ interface Props {
21
+ facet: Facet;
22
+ }
23
+
24
+ const sortByPredefinedOrder = (
25
+ aLabel: string,
26
+ bLabel: string,
27
+ order: string[]
28
+ ) => {
29
+ const aIndex = order.indexOf(aLabel);
30
+ const bIndex = order.indexOf(bLabel);
31
+
32
+ if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
33
+ if (aIndex !== -1) return -1;
34
+ if (bIndex !== -1) return 1;
35
+
36
+ return null;
37
+ };
38
+
39
+ const sortByNumericValue = (aLabel: string, bLabel: string) => {
40
+ const aNum = parseInt(aLabel, 10);
41
+ const bNum = parseInt(bLabel, 10);
42
+
43
+ if (!isNaN(aNum) && !isNaN(bNum)) return aNum - bNum;
44
+ if (!isNaN(aNum)) return -1;
45
+ if (!isNaN(bNum)) return 1;
46
+
47
+ return null;
48
+ };
49
+
50
+ const sortChoices = (
51
+ facetKey: string,
52
+ choices: FacetChoice[]
53
+ ): FacetChoice[] => {
54
+ if (facetKey === sizeKey) {
55
+ const order = ['xs', 's', 'm', 'l', 'xl'];
56
+
57
+ return choices.sort((a, b) => {
58
+ const aLabel = a.label.toLowerCase();
59
+ const bLabel = b.label.toLowerCase();
60
+
61
+ const orderComparison = sortByPredefinedOrder(aLabel, bLabel, order);
62
+ if (orderComparison !== null) return orderComparison;
63
+
64
+ const numericComparison = sortByNumericValue(aLabel, bLabel);
65
+ if (numericComparison !== null) return numericComparison;
66
+
67
+ return aLabel.localeCompare(bLabel);
68
+ });
69
+ }
70
+
71
+ return choices;
72
+ };
73
+
74
+ const getComponentByWidgetType = (widgetType: string, facetKey: string) => {
75
+ if (facetKey === sizeKey) {
76
+ return SizeFilter;
77
+ }
78
+ return COMPONENT_TYPES[widgetType] || COMPONENT_TYPES[WIDGET_TYPE.category];
79
+ };
80
+
81
+ export const FilterItem = ({ facet }: Props) => {
82
+ const dispatch = useAppDispatch();
83
+ const router = useRouter();
84
+
85
+ const handleSelectFilter = (choice: FacetChoice) => {
86
+ if (facet.key === 'category_ids') {
87
+ router.push(choice.url);
88
+ } else {
89
+ dispatch(toggleFacet({ facet, choice }));
90
+ }
91
+ };
92
+
93
+ const Component = getComponentByWidgetType(facet.widget_type, facet.key);
94
+ const choices = sortChoices(facet.key, [...facet.data.choices]);
95
+
96
+ return (
97
+ <Accordion
98
+ key={facet.key}
99
+ title={facet.name}
100
+ isCollapse={choices.some((choice) => choice.is_selected)}
101
+ dataTestId={`filter-${facet.name}`}
102
+ >
103
+ <div
104
+ className={clsx('flex gap-4', {
105
+ 'flex-wrap flex-row': facet.key === sizeKey,
106
+ 'flex-col': facet.key !== sizeKey
107
+ })}
108
+ >
109
+ {choices.map((choice, index) => (
110
+ <Component
111
+ key={choice.label}
112
+ data={choice}
113
+ name={facet.key}
114
+ onChange={() => facet.key !== sizeKey && handleSelectFilter(choice)}
115
+ onClick={() => facet.key === sizeKey && handleSelectFilter(choice)}
116
+ checked={choice.is_selected}
117
+ data-testid={`${choice.label.trim()}`}
118
+ >
119
+ {choice.label} (
120
+ <span
121
+ data-testid={`filter-count-${facet.name.toLowerCase()}-${index}`}
122
+ >
123
+ {choice.quantity}
124
+ </span>
125
+ )
126
+ </Component>
127
+ ))}
128
+ </div>
129
+ </Accordion>
130
+ );
131
+ };
@@ -1,26 +1,14 @@
1
1
  'use client';
2
2
 
3
- import { WIDGET_TYPE } from '@theme/types';
4
3
  import clsx from 'clsx';
5
4
 
6
- import { Accordion, Button, Checkbox, Icon, Radio } from '@theme/components';
7
- import { SizeFilter } from './size-filter';
8
-
9
- import { useLocalization, useRouter } from '@akinon/next/hooks';
10
- import { Facet, FacetChoice } from '@akinon/next/types';
5
+ import { Button, Icon } from '@theme/components';
6
+ import { useLocalization } from '@akinon/next/hooks';
11
7
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
12
- import {
13
- resetSelectedFacets,
14
- toggleFacet
15
- } from '@theme/redux/reducers/category';
8
+ import { resetSelectedFacets } from '@theme/redux/reducers/category';
16
9
  import CategoryActiveFilters from '@theme/views/category/category-active-filters';
17
10
  import { useMemo } from 'react';
18
- import { commonProductAttributes } from '@theme/settings';
19
-
20
- const COMPONENT_TYPES = {
21
- [WIDGET_TYPE.category]: Radio,
22
- [WIDGET_TYPE.multiselect]: Checkbox
23
- };
11
+ import { FilterItem } from './filter-item';
24
12
 
25
13
  interface Props {
26
14
  isMenuOpen: boolean;
@@ -28,31 +16,11 @@ interface Props {
28
16
  }
29
17
 
30
18
  export const Filters = (props: Props) => {
31
- const router = useRouter();
32
19
  const facets = useAppSelector((state) => state.category.facets);
33
20
  const dispatch = useAppDispatch();
34
21
  const { t } = useLocalization();
35
22
  const { isMenuOpen, setIsMenuOpen } = props;
36
23
 
37
- const handleSelectFilter = ({
38
- facet,
39
- choice
40
- }: {
41
- facet: Facet;
42
- choice: FacetChoice;
43
- }) => {
44
- if (facet.key === 'category_ids') {
45
- router.push(choice.url);
46
- } else {
47
- dispatch(
48
- toggleFacet({
49
- facet,
50
- choice
51
- })
52
- );
53
- }
54
- };
55
-
56
24
  const haveFilter = useMemo(() => {
57
25
  return (
58
26
  facets.filter(
@@ -66,10 +34,6 @@ export const Filters = (props: Props) => {
66
34
  dispatch(resetSelectedFacets());
67
35
  };
68
36
 
69
- const sizeKey = commonProductAttributes.find(
70
- (item) => item.translationKey === 'size'
71
- ).key;
72
-
73
37
  return (
74
38
  <div
75
39
  className={clsx(
@@ -88,71 +52,7 @@ export const Filters = (props: Props) => {
88
52
  <span>{t('category.filters.ready_to_wear')}</span>
89
53
  </div>
90
54
  {facets.map((facet) => {
91
- let Component = null;
92
- const choices = [...facet.data.choices];
93
-
94
- if (facet.key === sizeKey) {
95
- // If it's a size facet, use the custom size filter component
96
- Component = SizeFilter;
97
-
98
- const order = ['xs', 's', 'm', 'l', 'xl'];
99
- choices.sort((a, b) => {
100
- return (
101
- order.indexOf(a.label.toLowerCase()) -
102
- order.indexOf(b.label.toLowerCase())
103
- );
104
- });
105
- } else {
106
- Component =
107
- COMPONENT_TYPES[facet.widget_type] ||
108
- COMPONENT_TYPES[WIDGET_TYPE.category];
109
- }
110
-
111
- return (
112
- <Accordion
113
- key={facet.key}
114
- title={facet.name}
115
- isCollapse={choices.some((choice) => choice.is_selected)}
116
- dataTestId={`filter-${facet.name}`}
117
- >
118
- <div
119
- className={clsx(
120
- 'flex gap-4 flex-wrap',
121
- facet.key === sizeKey ? 'flex-row' : 'flex-col' // TODO: This condition must be refactor to a better way
122
- )}
123
- >
124
- {choices.map((choice, index) => (
125
- <Component // TODO: This dynamic component can be a hook or higher order component so it props can be standardized
126
- key={choice.label}
127
- data={choice}
128
- name={facet.key}
129
- onChange={() => {
130
- if (facet.key !== sizeKey) {
131
- // TODO: This condition must be refactor to a better way
132
- handleSelectFilter({ facet, choice });
133
- }
134
- }}
135
- onClick={() => {
136
- if (facet.key === sizeKey) {
137
- // TODO: This condition must be refactor to a better way
138
- handleSelectFilter({ facet, choice });
139
- }
140
- }}
141
- checked={choice.is_selected}
142
- data-testid={`${choice.label.trim()}`}
143
- >
144
- {choice.label} (
145
- <span
146
- data-testid={`filter-count-${facet.name.toLowerCase()}-${index}`}
147
- >
148
- {choice.quantity}
149
- </span>
150
- )
151
- </Component>
152
- ))}
153
- </div>
154
- </Accordion>
155
- );
55
+ return <FilterItem key={facet.key} facet={facet} />;
156
56
  })}
157
57
  <div className="lg:hidden">
158
58
  <CategoryActiveFilters />
@@ -1,20 +1,69 @@
1
1
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
2
2
  import { setCurrentStep } from '@akinon/next/redux/reducers/checkout';
3
3
  import { RootState } from '@theme/redux/store';
4
- import { useSetShippingOptionMutation } from '@akinon/next/data/client/checkout';
4
+ import {
5
+ useSetShippingOptionMutation,
6
+ useSetDataSourceShippingOptionsMutation
7
+ } from '@akinon/next/data/client/checkout';
5
8
  import { Price, Button, Radio } from '@theme/components';
6
9
  import { CheckoutStep } from '@akinon/next/types';
7
10
  import { useLocalization } from '@akinon/next/hooks';
11
+ import { useEffect, useState } from 'react';
8
12
 
9
13
  const ShippingOptions = () => {
10
14
  const { t } = useLocalization();
11
- const { steps, shippingOptions, preOrder, addressList } = useAppSelector(
12
- (state: RootState) => state.checkout
13
- );
14
- const { shipping_option, shipping_address } = preOrder ?? {};
15
+ const {
16
+ steps,
17
+ shippingOptions,
18
+ dataSourceShippingOptions,
19
+ preOrder,
20
+ addressList
21
+ } = useAppSelector((state: RootState) => state.checkout);
22
+ const { shipping_option, shipping_address, data_source_shipping_options } =
23
+ preOrder ?? {};
24
+
25
+ const [selectedPks, setSelectedPks] = useState<
26
+ { dataSourcePk: number; optionPk: number }[] | null
27
+ >(null);
28
+
15
29
  const [setShippingOption] = useSetShippingOptionMutation();
30
+ const [setDataSourceShippingOption] =
31
+ useSetDataSourceShippingOptionsMutation();
32
+
16
33
  const dispatch = useAppDispatch();
17
34
 
35
+ useEffect(() => {
36
+ if (!data_source_shipping_options) return;
37
+
38
+ const initialSelectedPks = data_source_shipping_options.map((option) => ({
39
+ dataSourcePk: option.data_source.pk,
40
+ optionPk: option.pk
41
+ }));
42
+
43
+ setSelectedPks(initialSelectedPks);
44
+ }, [data_source_shipping_options]);
45
+
46
+ const updateData = (dataSourcePk: number, newPk: number) => {
47
+ const updatedSelectedPks = selectedPks?.map((item) =>
48
+ item.dataSourcePk === dataSourcePk ? { ...item, optionPk: newPk } : item
49
+ );
50
+
51
+ if (!updatedSelectedPks) return;
52
+
53
+ setSelectedPks(updatedSelectedPks);
54
+
55
+ const pks = updatedSelectedPks.map((item) => item.optionPk);
56
+ setDataSourceShippingOption(pks);
57
+ };
58
+
59
+ const handleRadioChange = (
60
+ e: React.ChangeEvent<HTMLInputElement>,
61
+ dataSourcePk: number
62
+ ) => {
63
+ const newPk = parseInt(e.currentTarget.value);
64
+ updateData(dataSourcePk, newPk);
65
+ };
66
+
18
67
  return (
19
68
  <div className="w-full lg:w-2/5">
20
69
  <div className="border-b border-gray-400 px-8 py-4">
@@ -32,34 +81,83 @@ const ShippingOptions = () => {
32
81
  {t('checkout.address.shipping.chosen_address')}:{' '}
33
82
  {shipping_address?.city.name}
34
83
  </p>
35
- {shippingOptions.map((option) => (
36
- <div
37
- key={option.pk}
38
- className="py-4 border-t border-gray-400 flex justify-between"
39
- >
40
- <Radio
41
- name="shipping"
42
- checked={option.pk === shipping_option?.pk}
43
- onChange={() => {
44
- setShippingOption(option.pk);
45
- }}
46
- data-testid={`checkout-shipping-option-${option.pk}`}
84
+ {shippingOptions && shippingOptions.length > 0 && (
85
+ <>
86
+ {shippingOptions.map((option) => (
87
+ <div
88
+ key={option.pk}
89
+ className="py-4 border-t border-gray-400 flex justify-between"
90
+ >
91
+ <Radio
92
+ name="shipping"
93
+ checked={option.pk === shipping_option?.pk}
94
+ onChange={() => {
95
+ setShippingOption(option.pk);
96
+ }}
97
+ onClick={() => {
98
+ setShippingOption(option?.pk);
99
+ }}
100
+ data-testid={`checkout-shipping-option-${option.pk}`}
101
+ >
102
+ {option.name}
103
+ </Radio>
104
+ <span className="text-xs">
105
+ <Price value={option.shipping_amount} />
106
+ </span>
107
+ </div>
108
+ ))}
109
+ <Button
110
+ className="mt-2 w-full"
111
+ disabled={!steps.shipping.completed}
112
+ onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
113
+ data-testid="checkout-shipping-save"
114
+ >
115
+ {t('checkout.address.shipping.button')}
116
+ </Button>
117
+ </>
118
+ )}
119
+ {dataSourceShippingOptions && dataSourceShippingOptions.length > 0 && (
120
+ <>
121
+ {dataSourceShippingOptions.map((option) => (
122
+ <div key={option.pk}>
123
+ <h3 className="text-lg font-bold">{option?.name}</h3>
124
+ {option.data_source_shipping_options.map((opt) => (
125
+ <div
126
+ key={opt.pk}
127
+ className="py-4 border-t border-gray-400 flex justify-between"
128
+ >
129
+ <Radio
130
+ name={`data-source-shipping-${option.pk}`}
131
+ checked={
132
+ selectedPks?.some(
133
+ (item) => item.optionPk === opt.pk
134
+ ) || false
135
+ }
136
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
137
+ handleRadioChange(e, option.pk)
138
+ }
139
+ value={opt.pk}
140
+ data-testid={`checkout-data-source-shipping-${opt.pk}`}
141
+ >
142
+ {opt?.shipping_option_name}
143
+ </Radio>
144
+ <span className="text-xs">
145
+ <Price value={opt?.shipping_amount} />
146
+ </span>
147
+ </div>
148
+ ))}
149
+ </div>
150
+ ))}
151
+ <Button
152
+ className="mt-2 w-full"
153
+ disabled={!steps.shipping.completed}
154
+ onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
155
+ data-testid="checkout-shipping-save"
47
156
  >
48
- {option.name}
49
- </Radio>
50
- <span className="text-xs">
51
- <Price value={option.shipping_amount} />
52
- </span>
53
- </div>
54
- ))}
55
- <Button
56
- className="mt-2 w-full"
57
- disabled={!steps.shipping.completed}
58
- onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
59
- data-testid="checkout-shipping-save"
60
- >
61
- {t('checkout.address.shipping.button')}
62
- </Button>
157
+ {t('checkout.address.shipping.button')}
158
+ </Button>
159
+ </>
160
+ )}
63
161
  </div>
64
162
  )}
65
163
  </div>
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState } from 'react';
3
+ import { useState, useEffect, useRef } from 'react';
4
4
  import { MenuItemType } from '@akinon/next/types';
5
5
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
6
6
  import { closeMobileMenu } from '@akinon/next/redux/reducers/header';
@@ -32,9 +32,30 @@ export default function MobileMenu(props: MobileMenuProps) {
32
32
  (state) => state.header.isMobileMenuOpen
33
33
  );
34
34
 
35
+ const menuRef = useRef<HTMLDivElement>(null);
36
+
37
+ useEffect(() => {
38
+ function handleClickOutside(event: MouseEvent) {
39
+ if (
40
+ isMobileMenuOpen &&
41
+ menuRef.current &&
42
+ !menuRef.current.contains(event.target as Node)
43
+ ) {
44
+ dispatch(closeMobileMenu());
45
+ setSelectedSubMenu(null);
46
+ }
47
+ }
48
+
49
+ document.addEventListener('mousedown', handleClickOutside);
50
+
51
+ return () => {
52
+ document.removeEventListener('mousedown', handleClickOutside);
53
+ };
54
+ // eslint-disable-next-line react-hooks/exhaustive-deps
55
+ }, [isMobileMenuOpen]);
56
+
35
57
  return (
36
58
  <>
37
- {/* MENU OVERLAY */}
38
59
  <div
39
60
  className={clsx(
40
61
  'fixed top-0 left-0 z-30 w-screen h-screen invisible opacity-0 bg-black bg-opacity-80 transition duration-500',
@@ -42,14 +63,10 @@ export default function MobileMenu(props: MobileMenuProps) {
42
63
  '!visible !opacity-100 scroll-lock': isMobileMenuOpen
43
64
  }
44
65
  )}
45
- // TODO: Remove this after we have a better solution for clicking outside of the menu
46
- onClick={() => {
47
- dispatch(closeMobileMenu());
48
- setSelectedSubMenu(null);
49
- }}
50
66
  />
51
- {/* TODO: Add a way to close the menu when clicking outside of it */}
67
+
52
68
  <div
69
+ ref={menuRef}
53
70
  className={clsx(
54
71
  'fixed top-0 left-0 z-50 flex flex-col bg-white w-72 pt-4 h-screen invisible opacity-0 transition duration-500 transform -translate-x-72',
55
72
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/projectzero",
3
- "version": "1.62.0",
3
+ "version": "1.64.0",
4
4
  "private": false,
5
5
  "description": "CLI tool to manage your Project Zero Next project",
6
6
  "bin": {