@akinon/projectzero 1.55.0-rc.0 → 1.55.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +2 -34
  2. package/app-template/.gitignore +0 -2
  3. package/app-template/CHANGELOG.md +80 -1871
  4. package/app-template/package.json +18 -17
  5. package/app-template/public/locales/en/account.json +4 -4
  6. package/app-template/public/locales/tr/account.json +1 -1
  7. package/app-template/src/app/[commerce]/[locale]/[currency]/account/coupons/page.tsx +4 -4
  8. package/app-template/src/app/[commerce]/[locale]/[currency]/account/profile/page.tsx +0 -1
  9. package/app-template/src/app/[commerce]/[locale]/[currency]/category/[pk]/page.tsx +2 -5
  10. package/app-template/src/app/[commerce]/[locale]/[currency]/orders/completed/[token]/page.tsx +8 -12
  11. package/app-template/src/components/checkbox.tsx +2 -2
  12. package/app-template/src/components/input.tsx +7 -19
  13. package/app-template/src/components/pagination.tsx +18 -13
  14. package/app-template/src/components/price.tsx +4 -9
  15. package/app-template/src/redux/reducers/category.ts +1 -7
  16. package/app-template/src/settings.js +1 -6
  17. package/app-template/src/views/account/address-form.tsx +2 -12
  18. package/app-template/src/views/account/contact-form.tsx +6 -23
  19. package/app-template/src/views/account/favourite-products/favourite-products-list.tsx +1 -5
  20. package/app-template/src/views/breadcrumb.tsx +1 -4
  21. package/app-template/src/views/category/category-active-filters.tsx +6 -16
  22. package/app-template/src/views/category/category-info.tsx +17 -31
  23. package/app-template/src/views/category/filters/index.tsx +108 -16
  24. package/app-template/src/views/category/layout.tsx +3 -5
  25. package/app-template/src/views/checkout/steps/payment/options/redirection.tsx +37 -43
  26. package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +3 -19
  27. package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +34 -230
  28. package/app-template/src/views/header/mobile-menu.tsx +8 -25
  29. package/app-template/tsconfig.json +4 -14
  30. package/package.json +2 -2
  31. package/app-template/src/app/[commerce]/[locale]/[currency]/[...prettyurl]/page.tsx +0 -8
  32. package/app-template/src/views/category/filters/filter-item.tsx +0 -163
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useMemo } from 'react';
3
+ import { useEffect, useMemo, useState } from 'react';
4
4
  import clsx from 'clsx';
5
5
  import { useSearchParams } from 'next/navigation';
6
6
  import { CategoryHeader } from './category-header';
@@ -8,14 +8,13 @@ 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, useAppSelector } from '@akinon/next/redux/hooks';
12
- import { setFacets, setMenuOpen } from '@theme/redux/reducers/category';
11
+ import { useAppDispatch } from '@akinon/next/redux/hooks';
12
+ import { setFacets } 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';
19
18
 
20
19
  interface ListPageProps {
21
20
  data: GetCategoryResponse;
@@ -23,16 +22,13 @@ interface ListPageProps {
23
22
 
24
23
  export default function ListPage(props: ListPageProps) {
25
24
  const { data } = props;
26
- const dispatch = useAppDispatch();
27
- const isMenuOpen = useAppSelector(
28
- (state: RootState) => state.category.isMenuOpen
29
- );
25
+ const [isMenuOpen, setIsMenuOpen] = useState(false); // TODO: Move to redux
30
26
 
31
27
  const searchParams = useSearchParams();
32
28
  const router = useRouter();
33
29
 
34
30
  const layoutSize = useMemo(
35
- () => Number(searchParams.get('layout') ?? 3),
31
+ () => searchParams.get('layout') ?? 3,
36
32
  [searchParams]
37
33
  );
38
34
 
@@ -41,24 +37,16 @@ export default function ListPage(props: ListPageProps) {
41
37
  [searchParams]
42
38
  );
43
39
 
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
-
54
40
  useEffect(() => {
55
41
  if (page > 1 && data.products?.length === 0) {
56
42
  const newUrl = new URL(window.location.href);
43
+
57
44
  newUrl.searchParams.delete('page');
58
45
  router.push(newUrl.pathname + newUrl.search, undefined);
59
46
  }
60
- }, [searchParams, data.products, page]);
47
+ }, [searchParams, data.products, page]); // eslint-disable-line react-hooks/exhaustive-deps
61
48
 
49
+ const dispatch = useAppDispatch();
62
50
  const { t } = useLocalization();
63
51
 
64
52
  useEffect(() => {
@@ -70,12 +58,9 @@ export default function ListPage(props: ListPageProps) {
70
58
  <>
71
59
  <div className="container px-4 mx-auto lg:px-0 lg:my-4">
72
60
  <div className="grid grid-cols-[19rem_1fr]">
73
- <Filters
74
- isMenuOpen={isMenuOpen}
75
- setIsMenuOpen={(open) => dispatch(setMenuOpen(open))}
76
- />
61
+ <Filters isMenuOpen={isMenuOpen} setIsMenuOpen={setIsMenuOpen} />
77
62
  <div
78
- onClick={() => dispatch(setMenuOpen(false))}
63
+ onClick={() => setIsMenuOpen(false)}
79
64
  className={clsx(
80
65
  'transition-opacity duration-300 ease-linear lg:hidden',
81
66
  isMenuOpen
@@ -86,7 +71,7 @@ export default function ListPage(props: ListPageProps) {
86
71
  <div className="flex flex-col items-center lg:items-stretch col-span-2 lg:col-span-1">
87
72
  <CategoryHeader
88
73
  totalCount={data.pagination?.total_count}
89
- setMenuStatus={() => dispatch(setMenuOpen(true))}
74
+ setMenuStatus={() => setIsMenuOpen(true)}
90
75
  sortOptions={data.sorters}
91
76
  />
92
77
  <div className="hidden lg:block">
@@ -106,17 +91,18 @@ export default function ListPage(props: ListPageProps) {
106
91
 
107
92
  <div
108
93
  className={clsx('grid gap-x-4 gap-y-12 grid-cols-2', {
109
- 'md:grid-cols-3': layoutSize === 3,
110
- 'lg:grid-cols-2': layoutSize === 2,
111
- 'lg:grid-cols-3': layoutSize === 3
94
+ 'md:grid-cols-3': Number(layoutSize) === 3,
95
+ 'lg:grid-cols-2': Number(layoutSize) === 2,
96
+ 'lg:grid-cols-3': Number(layoutSize) === 3
112
97
  })}
113
98
  >
114
99
  {data.products.map((product, index) => (
115
100
  <ProductItem
116
101
  key={product.pk}
117
102
  product={product}
118
- width={itemDimensions.width}
119
- height={itemDimensions.height}
103
+ // TODO: Find a better way to handle layout size
104
+ width={340}
105
+ height={510}
120
106
  index={index}
121
107
  />
122
108
  ))}
@@ -1,13 +1,26 @@
1
1
  'use client';
2
2
 
3
+ import { WIDGET_TYPE } from '@theme/types';
3
4
  import clsx from 'clsx';
4
- import { Button, Icon } from '@theme/components';
5
- import { useLocalization } from '@akinon/next/hooks';
5
+
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';
6
11
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
7
- import { resetSelectedFacets } from '@theme/redux/reducers/category';
12
+ import {
13
+ resetSelectedFacets,
14
+ toggleFacet
15
+ } from '@theme/redux/reducers/category';
8
16
  import CategoryActiveFilters from '@theme/views/category/category-active-filters';
9
- import { useMemo, useState, useTransition } from 'react';
10
- import { FilterItem } from './filter-item';
17
+ 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
24
 
12
25
  interface Props {
13
26
  isMenuOpen: boolean;
@@ -15,16 +28,37 @@ interface Props {
15
28
  }
16
29
 
17
30
  export const Filters = (props: Props) => {
31
+ const router = useRouter();
18
32
  const facets = useAppSelector((state) => state.category.facets);
19
33
  const dispatch = useAppDispatch();
20
34
  const { t } = useLocalization();
21
35
  const { isMenuOpen, setIsMenuOpen } = props;
22
36
 
23
- const [isPending, startTransition] = useTransition();
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
+ };
24
55
 
25
56
  const haveFilter = useMemo(() => {
26
- return facets.some((facet) =>
27
- facet.data.choices.some((choice) => choice.is_selected)
57
+ return (
58
+ facets.filter(
59
+ (facet) =>
60
+ facet.data.choices.filter((choice) => choice.is_selected).length > 0
61
+ ).length > 0
28
62
  );
29
63
  }, [facets]);
30
64
 
@@ -32,6 +66,10 @@ export const Filters = (props: Props) => {
32
66
  dispatch(resetSelectedFacets());
33
67
  };
34
68
 
69
+ const sizeKey = commonProductAttributes.find(
70
+ (item) => item.translationKey === 'size'
71
+ ).key;
72
+
35
73
  return (
36
74
  <div
37
75
  className={clsx(
@@ -49,22 +87,76 @@ export const Filters = (props: Props) => {
49
87
  <span className="text-sm">1 {t('category.filters.results')}</span>
50
88
  <span>{t('category.filters.ready_to_wear')}</span>
51
89
  </div>
52
-
53
90
  {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
+
54
111
  return (
55
- <FilterItem
112
+ <Accordion
56
113
  key={facet.key}
57
- facet={facet}
58
- isPending={isPending}
59
- startTransition={startTransition}
60
- />
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>
61
155
  );
62
156
  })}
63
-
64
157
  <div className="lg:hidden">
65
158
  <CategoryActiveFilters />
66
159
  </div>
67
-
68
160
  {haveFilter && (
69
161
  <div className="lg:hidden">
70
162
  <Button
@@ -1,17 +1,15 @@
1
1
  import clsx from 'clsx';
2
- import { BreadcrumbResultType, GetCategoryResponse } from '@akinon/next/types';
2
+ import { GetCategoryResponse } from '@akinon/next/types';
3
3
  import Breadcrumb from '@theme/views/breadcrumb';
4
4
  import { CategoryBanner } from '@theme/views/category/category-banner';
5
5
  import ListPage from '@theme/views/category/category-info';
6
6
 
7
7
  export default async function Layout({
8
8
  data,
9
- children,
10
- breadcrumbData
9
+ children
11
10
  }: {
12
11
  data: GetCategoryResponse;
13
12
  children?: React.ReactNode;
14
- breadcrumbData?: BreadcrumbResultType[];
15
13
  }) {
16
14
  return (
17
15
  <>
@@ -30,7 +28,7 @@ export default async function Layout({
30
28
  'lg:absolute lg:inset-x-0 z-10 container lg:my-4 mx-auto'
31
29
  )}
32
30
  >
33
- <Breadcrumb breadcrumbList={breadcrumbData} />
31
+ <Breadcrumb />
34
32
  </div>
35
33
  <CategoryBanner {...data.category?.attributes?.category_banner} />
36
34
  </div>
@@ -9,7 +9,6 @@ import { twMerge } from 'tailwind-merge';
9
9
  import * as yup from 'yup';
10
10
  import { useEffect, useState } from 'react';
11
11
  import { getPosError } from '@akinon/next/utils';
12
- import { useMessageListener } from '@akinon/next/hooks';
13
12
 
14
13
  interface FormValues {
15
14
  agreement: boolean;
@@ -26,6 +25,7 @@ const formSchema = () =>
26
25
  export default function RedirectionPayment() {
27
26
  const { payment_option } = useAppSelector((state) => state.checkout.preOrder);
28
27
  const [formError, setFormError] = useState(null);
28
+
29
29
  const {
30
30
  register,
31
31
  handleSubmit,
@@ -34,12 +34,11 @@ export default function RedirectionPayment() {
34
34
  resolver: yupResolver(formSchema())
35
35
  });
36
36
  const [completeRedirectionPayment] = useCompleteRedirectionPaymentMutation();
37
+
37
38
  const onSubmit = async () => {
38
39
  completeRedirectionPayment();
39
40
  };
40
41
 
41
- useMessageListener();
42
-
43
42
  useEffect(() => {
44
43
  const posErrors = getPosError();
45
44
 
@@ -49,49 +48,44 @@ export default function RedirectionPayment() {
49
48
  }, []);
50
49
 
51
50
  return (
52
- <div className="checkout-redirection-payment-wrapper">
53
- <form
54
- onSubmit={handleSubmit(onSubmit)}
55
- className="lg-5 space-y-5 lg:p-10"
56
- >
57
- <h1 className="text-2xl font-bold px-4 md:px-0">
58
- Pay With {payment_option.name}
59
- </h1>
51
+ <form onSubmit={handleSubmit(onSubmit)} className="lg-5 space-y-5 lg:p-10">
52
+ <h1 className="text-2xl font-bold px-4 md:px-0">
53
+ Pay With {payment_option.name}
54
+ </h1>
60
55
 
61
- <p className="px-4 md:px-0">
62
- You can quickly and easily pay and complete your order with{' '}
63
- {payment_option.name}.
64
- </p>
56
+ <p className="px-4 md:px-0">
57
+ You can quickly and easily pay and complete your order with{' '}
58
+ {payment_option.name}.
59
+ </p>
65
60
 
66
- <Checkbox
67
- className="px-4 md:px-0"
68
- {...register('agreement')}
69
- error={errors.agreement}
70
- >
71
- Check here to indicate that you have read and agree to the all terms.
72
- </Checkbox>
61
+ <Checkbox
62
+ className="px-4 md:px-0"
63
+ {...register('agreement')}
64
+ error={errors.agreement}
65
+ >
66
+ Check here to indicate that you have read and agree to the all terms.
67
+ </Checkbox>
73
68
 
74
- {formError?.non_field_errors && (
75
- <div
76
- className="w-full text-xs text-start px-1 mt-3 text-error"
77
- data-testid="checkout-form-error"
78
- >
79
- {formError.non_field_errors}
80
- </div>
81
- )}
82
- {formError?.status && (
83
- <div
84
- className="w-full text-xs text-start px-1 mt-3 text-error"
85
- data-testid="checkout-form-error"
86
- >
87
- {formError.status}
88
- </div>
89
- )}
69
+ {formError?.non_field_errors && (
70
+ <div
71
+ className="w-full text-xs text-start px-1 mt-3 text-error"
72
+ data-testid="checkout-form-error"
73
+ >
74
+ {formError.non_field_errors}
75
+ </div>
76
+ )}
77
+ {formError?.status && (
78
+ <div
79
+ className="w-full text-xs text-start px-1 mt-3 text-error"
80
+ data-testid="checkout-form-error"
81
+ >
82
+ {formError.status}
83
+ </div>
84
+ )}
90
85
 
91
- <Button className={twMerge('w-full md:w-36 px-4 md:px-0')}>
92
- {payment_option.name}
93
- </Button>
94
- </form>
95
- </div>
86
+ <Button className={twMerge('w-full md:w-36 px-4 md:px-0')}>
87
+ {payment_option.name}
88
+ </Button>
89
+ </form>
96
90
  );
97
91
  }
@@ -5,12 +5,9 @@ import { useSetPaymentOptionMutation } from '@akinon/next/data/client/checkout';
5
5
  import { CheckoutPaymentOption } from '@akinon/next/types';
6
6
  import { Radio } from '@theme/components';
7
7
  import { usePaymentOptions } from '@akinon/next/hooks/use-payment-options';
8
- import { useMemo } from 'react';
9
8
 
10
9
  const PaymentOptionButtons = () => {
11
- const { preOrder, attributeBasedShippingOptions } = useAppSelector(
12
- (state: RootState) => state.checkout
13
- );
10
+ const { preOrder } = useAppSelector((state: RootState) => state.checkout);
14
11
  const [setPaymentOption] = useSetPaymentOptionMutation();
15
12
  const { filteredPaymentOptions } = usePaymentOptions();
16
13
 
@@ -25,23 +22,10 @@ const PaymentOptionButtons = () => {
25
22
  });
26
23
  };
27
24
 
28
- const displayedPaymentOptions = useMemo(() => {
29
- if (
30
- attributeBasedShippingOptions &&
31
- Object.keys(attributeBasedShippingOptions).length > 0
32
- ) {
33
- return filteredPaymentOptions.filter(
34
- (option) => option.slug.toLowerCase() !== 'pay-on-delivery'
35
- );
36
- }
37
-
38
- return filteredPaymentOptions;
39
- }, [filteredPaymentOptions, attributeBasedShippingOptions]);
40
-
41
25
  return (
42
26
  <>
43
27
  <div className="w-full space-y-4 px-4 flex flex-col mb-8 md:hidden">
44
- {displayedPaymentOptions.map((option) => (
28
+ {filteredPaymentOptions.map((option) => (
45
29
  <label
46
30
  key={`payment-option-${option.pk}`}
47
31
  className="border px-4 py-3 mt-3 flex h-12"
@@ -63,7 +47,7 @@ const PaymentOptionButtons = () => {
63
47
  </div>
64
48
 
65
49
  <div className="hidden md:flex">
66
- {displayedPaymentOptions.map((option) => (
50
+ {filteredPaymentOptions.map((option) => (
67
51
  <button
68
52
  key={`payment-option-${option.pk}`}
69
53
  onClick={() => onClickHandler(option)}