@akinon/projectzero 1.41.0 → 1.42.0-rc.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 (29) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +3 -2
  3. package/app-template/CHANGELOG.md +357 -7
  4. package/app-template/docs/basic-setup.md +1 -1
  5. package/app-template/docs/plugins.md +7 -7
  6. package/app-template/package-lock.json +29303 -0
  7. package/app-template/package.json +21 -19
  8. package/app-template/public/locales/en/account.json +4 -4
  9. package/app-template/public/locales/tr/account.json +1 -1
  10. package/app-template/src/app/[commerce]/[locale]/[currency]/[...prettyurl]/page.tsx +8 -0
  11. package/app-template/src/app/[commerce]/[locale]/[currency]/account/coupons/page.tsx +4 -4
  12. package/app-template/src/app/[commerce]/[locale]/[currency]/account/profile/page.tsx +1 -0
  13. package/app-template/src/app/[commerce]/[locale]/[currency]/orders/completed/[token]/page.tsx +12 -8
  14. package/app-template/src/components/checkbox.tsx +2 -2
  15. package/app-template/src/components/input.tsx +19 -7
  16. package/app-template/src/components/price.tsx +9 -4
  17. package/app-template/src/views/account/address-form.tsx +22 -7
  18. package/app-template/src/views/account/contact-form.tsx +23 -6
  19. package/app-template/src/views/account/favourite-products/favourite-products-list.tsx +5 -1
  20. package/app-template/src/views/category/filters/filter-item.tsx +131 -0
  21. package/app-template/src/views/category/filters/index.tsx +5 -105
  22. package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +33 -4
  23. package/app-template/src/views/checkout/steps/payment/options/redirection.tsx +43 -37
  24. package/app-template/src/views/checkout/steps/shipping/shipping-options.tsx +128 -35
  25. package/app-template/tsconfig.json +14 -4
  26. package/app-template/yarn.lock +1824 -1953
  27. package/commands/create.ts +29 -5
  28. package/dist/commands/create.js +25 -2
  29. package/package.json +2 -2
@@ -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 />
@@ -23,13 +23,29 @@ import { PaymentOption } from '@akinon/next/types';
23
23
  const creditCardFormSchema = (
24
24
  t,
25
25
  payment_option: PaymentOption,
26
- isMasterpassDirectPurchase?: boolean
26
+ isMasterpassDirectPurchase?: boolean,
27
+ isMasterpassCvcRequired?: boolean
27
28
  ) => {
28
29
  if (
29
30
  payment_option?.payment_type === 'masterpass' &&
30
31
  isMasterpassDirectPurchase === false
31
32
  ) {
32
33
  return yup.object().shape({
34
+ card_cvv: yup
35
+ .string()
36
+ .transform((value: string) => value.replace(/_/g, '').replace(/ /g, ''))
37
+ .when('*', (_, schema) => {
38
+ if (isMasterpassCvcRequired) {
39
+ return schema
40
+ .length(
41
+ 3,
42
+ t('checkout.payment.credit_card.form.error.cvv_length')
43
+ )
44
+ .required(t('checkout.payment.credit_card.form.error.required'));
45
+ }
46
+
47
+ return schema;
48
+ }),
33
49
  agreement: yup
34
50
  .boolean()
35
51
  .oneOf([true], t('checkout.payment.credit_card.form.error.required'))
@@ -87,10 +103,16 @@ const CheckoutCreditCard = () => {
87
103
  control,
88
104
  formState: { errors },
89
105
  setError,
90
- getValues
106
+ getValues,
107
+ clearErrors
91
108
  } = useForm<CreditCardForm>({
92
109
  resolver: yupResolver(
93
- creditCardFormSchema(t, payment_option, masterpass?.isDirectPurchase)
110
+ creditCardFormSchema(
111
+ t,
112
+ payment_option,
113
+ masterpass?.isDirectPurchase,
114
+ masterpass?.cvcRequired
115
+ )
94
116
  )
95
117
  });
96
118
  const [months, setMonths] = useState([]);
@@ -317,7 +339,14 @@ const CheckoutCreditCard = () => {
317
339
  <PluginModule
318
340
  component={Component.MasterpassCardList}
319
341
  props={{
320
- className: 'p-10'
342
+ className: 'p-10',
343
+ form: {
344
+ control,
345
+ register,
346
+ errors,
347
+ setFormValue,
348
+ clearErrors
349
+ }
321
350
  }}
322
351
  />
323
352
 
@@ -9,6 +9,7 @@ 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';
12
13
 
13
14
  interface FormValues {
14
15
  agreement: boolean;
@@ -25,7 +26,6 @@ const formSchema = () =>
25
26
  export default function RedirectionPayment() {
26
27
  const { payment_option } = useAppSelector((state) => state.checkout.preOrder);
27
28
  const [formError, setFormError] = useState(null);
28
-
29
29
  const {
30
30
  register,
31
31
  handleSubmit,
@@ -34,11 +34,12 @@ export default function RedirectionPayment() {
34
34
  resolver: yupResolver(formSchema())
35
35
  });
36
36
  const [completeRedirectionPayment] = useCompleteRedirectionPaymentMutation();
37
-
38
37
  const onSubmit = async () => {
39
38
  completeRedirectionPayment();
40
39
  };
41
40
 
41
+ useMessageListener();
42
+
42
43
  useEffect(() => {
43
44
  const posErrors = getPosError();
44
45
 
@@ -48,44 +49,49 @@ export default function RedirectionPayment() {
48
49
  }, []);
49
50
 
50
51
  return (
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>
55
-
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>
60
-
61
- <Checkbox
62
- className="px-4 md:px-0"
63
- {...register('agreement')}
64
- error={errors.agreement}
52
+ <div className="checkout-redirection-payment-wrapper">
53
+ <form
54
+ onSubmit={handleSubmit(onSubmit)}
55
+ className="lg-5 space-y-5 lg:p-10"
65
56
  >
66
- Check here to indicate that you have read and agree to the all terms.
67
- </Checkbox>
57
+ <h1 className="text-2xl font-bold px-4 md:px-0">
58
+ Pay With {payment_option.name}
59
+ </h1>
68
60
 
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"
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>
65
+
66
+ <Checkbox
67
+ className="px-4 md:px-0"
68
+ {...register('agreement')}
69
+ error={errors.agreement}
81
70
  >
82
- {formError.status}
83
- </div>
84
- )}
71
+ Check here to indicate that you have read and agree to the all terms.
72
+ </Checkbox>
73
+
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
+ )}
85
90
 
86
- <Button className={twMerge('w-full md:w-36 px-4 md:px-0')}>
87
- {payment_option.name}
88
- </Button>
89
- </form>
91
+ <Button className={twMerge('w-full md:w-36 px-4 md:px-0')}>
92
+ {payment_option.name}
93
+ </Button>
94
+ </form>
95
+ </div>
90
96
  );
91
97
  }
@@ -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,37 +81,81 @@ 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
- onClick={() => {
47
- setShippingOption(option?.pk);
48
- }}
49
- data-testid={`checkout-shipping-option-${option.pk}`}
84
+ {}
85
+ {shippingOptions && shippingOptions.length > 0 && (
86
+ <>
87
+ {shippingOptions.map((option) => (
88
+ <div
89
+ key={option.pk}
90
+ className="py-4 border-t border-gray-400 flex justify-between"
91
+ >
92
+ <Radio
93
+ name="shipping"
94
+ checked={option.pk === shipping_option?.pk}
95
+ onChange={() => {
96
+ setShippingOption(option.pk);
97
+ }}
98
+ data-testid={`checkout-shipping-option-${option.pk}`}
99
+ >
100
+ {option.name}
101
+ </Radio>
102
+ <span className="text-xs">
103
+ <Price value={option.shipping_amount} />
104
+ </span>
105
+ </div>
106
+ ))}
107
+ <Button
108
+ className="mt-2 w-full"
109
+ disabled={!steps.shipping.completed}
110
+ onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
111
+ data-testid="checkout-shipping-save"
112
+ >
113
+ {t('checkout.address.shipping.button')}
114
+ </Button>
115
+ </>
116
+ )}
117
+ {dataSourceShippingOptions && dataSourceShippingOptions.length > 0 && (
118
+ <>
119
+ {dataSourceShippingOptions.map((option) => (
120
+ <div key={option.pk}>
121
+ <h3 className="text-lg font-bold">{option?.name}</h3>
122
+ {option.data_source_shipping_options.map((opt) => (
123
+ <div
124
+ key={opt.pk}
125
+ className="py-4 border-t border-gray-400 flex justify-between"
126
+ >
127
+ <Radio
128
+ name={`data-source-shipping-${option.pk}`}
129
+ checked={
130
+ selectedPks?.some(
131
+ (item) => item.optionPk === opt.pk
132
+ ) || false
133
+ }
134
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
135
+ handleRadioChange(e, option.pk)
136
+ }
137
+ value={opt.pk}
138
+ data-testid={`checkout-data-source-shipping-${opt.pk}`}
139
+ >
140
+ {opt?.shipping_option_name}
141
+ </Radio>
142
+ <span className="text-xs">
143
+ <Price value={opt?.shipping_amount} />
144
+ </span>
145
+ </div>
146
+ ))}
147
+ </div>
148
+ ))}
149
+ <Button
150
+ className="mt-2 w-full"
151
+ disabled={!steps.shipping.completed}
152
+ onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
153
+ data-testid="checkout-shipping-save"
50
154
  >
51
- {option.name}
52
- </Radio>
53
- <span className="text-xs">
54
- <Price value={option.shipping_amount} />
55
- </span>
56
- </div>
57
- ))}
58
- <Button
59
- className="mt-2 w-full"
60
- disabled={!steps.shipping.completed}
61
- onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
62
- data-testid="checkout-shipping-save"
63
- >
64
- {t('checkout.address.shipping.button')}
65
- </Button>
155
+ {t('checkout.address.shipping.button')}
156
+ </Button>
157
+ </>
158
+ )}
66
159
  </div>
67
160
  )}
68
161
  </div>
@@ -3,7 +3,19 @@
3
3
  "display": "Default",
4
4
  "compilerOptions": {
5
5
  "baseUrl": "./src",
6
- "paths": { "@theme/*": ["./*"] },
6
+ "paths": {
7
+ "@theme/*": ["./*"],
8
+ "@root/*": ["./app/[commerce]/[locale]/[currency]/*"],
9
+ "@product/*": ["./app/[commerce]/[locale]/[currency]/product/*"],
10
+ "@group-product/*": [
11
+ "./app/[commerce]/[locale]/[currency]/group-product/*"
12
+ ],
13
+ "@category/*": ["./app/[commerce]/[locale]/[currency]/category/*"],
14
+ "@special-page/*": [
15
+ "./app/[commerce]/[locale]/[currency]/special-page/*"
16
+ ],
17
+ "@flat-page/*": ["./app/[commerce]/[locale]/[currency]/flat-page/*"]
18
+ },
7
19
  "allowSyntheticDefaultImports": true,
8
20
  "composite": false,
9
21
  "declaration": true,
@@ -40,7 +52,5 @@
40
52
  ".next/types/**/*.ts",
41
53
  "../../packages/**/*"
42
54
  ],
43
- "exclude": ["node_modules",
44
- "../../packages/projectzero/app-template"
45
- ]
55
+ "exclude": ["node_modules", "../../packages/projectzero/app-template"]
46
56
  }