@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,174 +1,38 @@
1
- import React, { useCallback, useEffect, useRef, useState } from 'react';
2
1
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
3
- import {
4
- setCurrentStep,
5
- setSelectedShippingOptions
6
- } from '@akinon/next/redux/reducers/checkout';
2
+ import { setCurrentStep } from '@akinon/next/redux/reducers/checkout';
7
3
  import { RootState } from '@theme/redux/store';
8
- import {
9
- useSetShippingOptionMutation,
10
- useSetAttributeBasedShippingOptionsMutation,
11
- useSetDataSourceShippingOptionsMutation
12
- } from '@akinon/next/data/client/checkout';
4
+ import { useSetShippingOptionMutation } from '@akinon/next/data/client/checkout';
13
5
  import { Price, Button, Radio } from '@theme/components';
14
6
  import { CheckoutStep } from '@akinon/next/types';
15
7
  import { useLocalization } from '@akinon/next/hooks';
16
8
 
17
- const ShippingOptions: React.FC = () => {
9
+ const ShippingOptions = () => {
18
10
  const { t } = useLocalization();
19
- const dispatch = useAppDispatch();
20
- const {
21
- steps,
22
- shippingOptions,
23
- attributeBasedShippingOptions,
24
- dataSourceShippingOptions,
25
- preOrder,
26
- addressList,
27
- selectedShippingOptions
28
- } = useAppSelector((state: RootState) => state.checkout);
29
- const {
30
- shipping_option,
31
- shipping_address,
32
- attribute_based_shipping_options,
33
- data_source_shipping_options
34
- } = preOrder ?? {};
35
-
36
- const [setShippingOption] = useSetShippingOptionMutation();
37
- const [setAttributeBasedShippingOptions] =
38
- useSetAttributeBasedShippingOptionsMutation();
39
- const [setDataSourceShippingOption] =
40
- useSetDataSourceShippingOptionsMutation();
41
-
42
- const prevAttributeBasedOptionsRef = useRef(attributeBasedShippingOptions);
43
-
44
- const [selectedPks, setSelectedPks] = useState<
45
- { dataSourcePk: number; optionPk: number }[] | null
46
- >(null);
47
-
48
- const initializeSelectedOptions = useCallback(() => {
49
- if (attribute_based_shipping_options) {
50
- const newSelected = { ...selectedShippingOptions };
51
- let hasChanges = false;
52
-
53
- Object.entries(attributeBasedShippingOptions).forEach(
54
- ([color, options]) => {
55
- if (
56
- !newSelected[color] ||
57
- !options.some((opt) => opt.pk === newSelected[color])
58
- ) {
59
- newSelected[color] = options[0].pk;
60
- hasChanges = true;
61
- }
62
- }
63
- );
64
-
65
- if (hasChanges) {
66
- dispatch(setSelectedShippingOptions(newSelected));
67
- setAttributeBasedShippingOptions(newSelected);
68
- }
69
- } else if (shippingOptions.length > 0 && !shipping_option) {
70
- setShippingOption(shippingOptions[0].pk);
71
- }
72
- // eslint-disable-next-line react-hooks/exhaustive-deps
73
- }, [
74
- attributeBasedShippingOptions,
75
- selectedShippingOptions,
76
- setAttributeBasedShippingOptions,
77
- attribute_based_shipping_options,
78
- shippingOptions,
79
- shipping_option,
80
- setShippingOption
81
- ]);
82
-
83
- useEffect(() => {
84
- if (!data_source_shipping_options) return;
85
-
86
- const initialSelectedPks = data_source_shipping_options.map((option) => ({
87
- dataSourcePk: option.data_source.pk,
88
- optionPk: option.pk
89
- }));
90
-
91
- setSelectedPks(initialSelectedPks);
92
- }, [data_source_shipping_options]);
93
-
94
- useEffect(() => {
95
- if (
96
- JSON.stringify(prevAttributeBasedOptionsRef.current) !==
97
- JSON.stringify(attributeBasedShippingOptions) ||
98
- Object.keys(selectedShippingOptions).length === 0 ||
99
- (!shipping_option && shippingOptions.length > 0)
100
- ) {
101
- initializeSelectedOptions();
102
- prevAttributeBasedOptionsRef.current = attributeBasedShippingOptions;
103
- }
104
- }, [
105
- attributeBasedShippingOptions,
106
- selectedShippingOptions,
107
- initializeSelectedOptions,
108
- shipping_option,
109
- shippingOptions
110
- ]);
111
-
112
- const handleAttributeBasedOptionChange = useCallback(
113
- (color: string, newPk: number) => {
114
- const updatedOptions = { ...selectedShippingOptions, [color]: newPk };
115
- dispatch(setSelectedShippingOptions(updatedOptions));
116
- setAttributeBasedShippingOptions(updatedOptions);
117
- },
118
- // eslint-disable-next-line react-hooks/exhaustive-deps
119
- [selectedShippingOptions, setAttributeBasedShippingOptions]
11
+ const { steps, shippingOptions, preOrder, addressList } = useAppSelector(
12
+ (state: RootState) => state.checkout
120
13
  );
121
-
122
- if (addressList.length < 1) {
123
- return (
124
- <div className="w-full lg:w-2/5">
125
- <div className="border-b border-gray-400 px-8 py-4">
126
- <h2 className="text-2xl">{t('checkout.address.shipping.title')}</h2>
127
- </div>
128
- <div className="py-4 px-8">
129
- <p className="text-xs">
130
- {t('checkout.address.shipping.select_address_to_continue')}
131
- </p>
132
- </div>
133
- </div>
134
- );
135
- }
136
-
137
- const updateData = (dataSourcePk: number, newPk: number) => {
138
- const updatedSelectedPks = selectedPks?.map((item) =>
139
- item.dataSourcePk === dataSourcePk ? { ...item, optionPk: newPk } : item
140
- );
141
-
142
- if (!updatedSelectedPks) return;
143
-
144
- setSelectedPks(updatedSelectedPks);
145
-
146
- const pks = updatedSelectedPks.map((item) => item.optionPk);
147
- setDataSourceShippingOption(pks);
148
- };
149
-
150
- const handleRadioChange = (
151
- e: React.ChangeEvent<HTMLInputElement>,
152
- dataSourcePk: number
153
- ) => {
154
- const newPk = parseInt(e.currentTarget.value);
155
- updateData(dataSourcePk, newPk);
156
- };
14
+ const { shipping_option, shipping_address } = preOrder ?? {};
15
+ const [setShippingOption] = useSetShippingOptionMutation();
16
+ const dispatch = useAppDispatch();
157
17
 
158
18
  return (
159
19
  <div className="w-full lg:w-2/5">
160
20
  <div className="border-b border-gray-400 px-8 py-4">
161
21
  <h2 className="text-2xl">{t('checkout.address.shipping.title')}</h2>
162
22
  </div>
163
- <div className="py-4 px-6">
164
- <p className="text-xs border-gray-400 pb-4">
165
- {t('checkout.address.shipping.chosen_address')}:{' '}
166
- {shipping_address?.city.name}
167
- </p>
168
-
169
- {shippingOptions &&
170
- shippingOptions.length > 0 &&
171
- shippingOptions.map((option) => (
23
+ {addressList.length < 1 ? (
24
+ <div className="py-4 px-8">
25
+ <p className="text-xs">
26
+ {t('checkout.address.shipping.select_address_to_continue')}
27
+ </p>
28
+ </div>
29
+ ) : (
30
+ <div className="py-4 px-6">
31
+ <p className="text-xs border-gray-400 pb-4">
32
+ {t('checkout.address.shipping.chosen_address')}:{' '}
33
+ {shipping_address?.city.name}
34
+ </p>
35
+ {shippingOptions.map((option) => (
172
36
  <div
173
37
  key={option.pk}
174
38
  className="py-4 border-t border-gray-400 flex justify-between"
@@ -176,7 +40,9 @@ const ShippingOptions: React.FC = () => {
176
40
  <Radio
177
41
  name="shipping"
178
42
  checked={option.pk === shipping_option?.pk}
179
- onChange={() => setShippingOption(option.pk)}
43
+ onChange={() => {
44
+ setShippingOption(option.pk);
45
+ }}
180
46
  data-testid={`checkout-shipping-option-${option.pk}`}
181
47
  >
182
48
  {option.name}
@@ -186,78 +52,16 @@ const ShippingOptions: React.FC = () => {
186
52
  </span>
187
53
  </div>
188
54
  ))}
189
-
190
- {attributeBasedShippingOptions &&
191
- Object.keys(attributeBasedShippingOptions).length > 0 &&
192
- Object.entries(attributeBasedShippingOptions).map(
193
- ([color, options]) => (
194
- <div key={color}>
195
- <h3 className="text-lg font-bold">{color}</h3>
196
- {options.map((option) => (
197
- <div
198
- key={option.pk}
199
- className="py-4 border-t border-gray-400 flex justify-between"
200
- >
201
- <Radio
202
- name={`attribute-based-shipping-${color}`}
203
- checked={selectedShippingOptions[color] === option.pk}
204
- onChange={() =>
205
- handleAttributeBasedOptionChange(color, option.pk)
206
- }
207
- data-testid={`checkout-attribute-based-shipping-${option.pk}`}
208
- >
209
- {`${option.shipping_option_name}`}
210
- </Radio>
211
- <span className="text-xs">
212
- <Price value={option.shipping_amount} />
213
- </span>
214
- </div>
215
- ))}
216
- </div>
217
- )
218
- )}
219
-
220
- {dataSourceShippingOptions &&
221
- dataSourceShippingOptions.length > 0 &&
222
- dataSourceShippingOptions.map((option) => (
223
- <div key={option.pk}>
224
- <h3 className="text-lg font-bold">{option?.name}</h3>
225
- {option.data_source_shipping_options.map((opt) => (
226
- <div
227
- key={opt.pk}
228
- className="py-4 border-t border-gray-400 flex justify-between"
229
- >
230
- <Radio
231
- name={`data-source-shipping-${option.pk}`}
232
- checked={
233
- selectedPks?.some((item) => item.optionPk === opt.pk) ||
234
- false
235
- }
236
- onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
237
- handleRadioChange(e, option.pk)
238
- }
239
- value={opt.pk}
240
- data-testid={`checkout-data-source-shipping-${opt.pk}`}
241
- >
242
- {opt?.shipping_option_name}
243
- </Radio>
244
- <span className="text-xs">
245
- <Price value={opt?.shipping_amount} />
246
- </span>
247
- </div>
248
- ))}
249
- </div>
250
- ))}
251
-
252
- <Button
253
- className="mt-2 w-full"
254
- disabled={!steps.shipping.completed}
255
- onClick={() => dispatch(setCurrentStep(CheckoutStep.Payment))}
256
- data-testid="checkout-shipping-save"
257
- >
258
- {t('checkout.address.shipping.button')}
259
- </Button>
260
- </div>
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>
63
+ </div>
64
+ )}
261
65
  </div>
262
66
  );
263
67
  };
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState, useEffect, useRef } from 'react';
3
+ import { useState } 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,30 +32,9 @@ 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
-
57
35
  return (
58
36
  <>
37
+ {/* MENU OVERLAY */}
59
38
  <div
60
39
  className={clsx(
61
40
  'fixed top-0 left-0 z-30 w-screen h-screen invisible opacity-0 bg-black bg-opacity-80 transition duration-500',
@@ -63,10 +42,14 @@ export default function MobileMenu(props: MobileMenuProps) {
63
42
  '!visible !opacity-100 scroll-lock': isMobileMenuOpen
64
43
  }
65
44
  )}
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
+ }}
66
50
  />
67
-
51
+ {/* TODO: Add a way to close the menu when clicking outside of it */}
68
52
  <div
69
- ref={menuRef}
70
53
  className={clsx(
71
54
  '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',
72
55
  {
@@ -3,19 +3,7 @@
3
3
  "display": "Default",
4
4
  "compilerOptions": {
5
5
  "baseUrl": "./src",
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
- },
6
+ "paths": { "@theme/*": ["./*"] },
19
7
  "allowSyntheticDefaultImports": true,
20
8
  "composite": false,
21
9
  "declaration": true,
@@ -52,5 +40,7 @@
52
40
  ".next/types/**/*.ts",
53
41
  "../../packages/**/*"
54
42
  ],
55
- "exclude": ["node_modules", "../../packages/projectzero/app-template"]
43
+ "exclude": ["node_modules",
44
+ "../../packages/projectzero/app-template"
45
+ ]
56
46
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/projectzero",
3
- "version": "1.55.0-rc.0",
3
+ "version": "1.55.0",
4
4
  "private": false,
5
5
  "description": "CLI tool to manage your Project Zero Next project",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  "@types/temp": "0.9.4"
20
20
  },
21
21
  "dependencies": {
22
- "loading-spinner": "^1.2.1",
22
+ "loading-spinner": "1.2.1",
23
23
  "prompt-checkbox": "2.2.0",
24
24
  "semver": "7.6.2",
25
25
  "temp": "0.9.4",
@@ -1,8 +0,0 @@
1
- import Page, {
2
- dynamic,
3
- revalidate,
4
- generateMetadata
5
- } from '@akinon/next/routes/pretty-url';
6
-
7
- export { dynamic, revalidate, generateMetadata };
8
- export default Page;
@@ -1,163 +0,0 @@
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, LoaderSpinner } 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
- import { usePathname } from 'next/navigation';
11
- import { useState } from 'react';
12
-
13
- const COMPONENT_TYPES = {
14
- [WIDGET_TYPE.category]: Radio,
15
- [WIDGET_TYPE.multiselect]: Checkbox
16
- };
17
-
18
- const sizeKey = commonProductAttributes.find(
19
- (item) => item.translationKey === 'size'
20
- ).key;
21
-
22
- interface Props {
23
- facet: Facet;
24
- isPending: boolean;
25
- startTransition: (callback: () => void) => void;
26
- }
27
-
28
- const sortByPredefinedOrder = (
29
- aLabel: string,
30
- bLabel: string,
31
- order: string[]
32
- ) => {
33
- const aIndex = order.indexOf(aLabel);
34
- const bIndex = order.indexOf(bLabel);
35
-
36
- if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
37
- if (aIndex !== -1) return -1;
38
- if (bIndex !== -1) return 1;
39
-
40
- return null;
41
- };
42
-
43
- const sortByNumericValue = (aLabel: string, bLabel: string) => {
44
- const aNum = parseInt(aLabel, 10);
45
- const bNum = parseInt(bLabel, 10);
46
-
47
- if (!isNaN(aNum) && !isNaN(bNum)) return aNum - bNum;
48
- if (!isNaN(aNum)) return -1;
49
- if (!isNaN(bNum)) return 1;
50
-
51
- return null;
52
- };
53
-
54
- const sortChoices = (
55
- facetKey: string,
56
- choices: FacetChoice[]
57
- ): FacetChoice[] => {
58
- if (facetKey === sizeKey) {
59
- const order = ['xs', 's', 'm', 'l', 'xl'];
60
-
61
- return choices.sort((a, b) => {
62
- const aLabel = a.label.toLowerCase();
63
- const bLabel = b.label.toLowerCase();
64
-
65
- const orderComparison = sortByPredefinedOrder(aLabel, bLabel, order);
66
- if (orderComparison !== null) return orderComparison;
67
-
68
- const numericComparison = sortByNumericValue(aLabel, bLabel);
69
- if (numericComparison !== null) return numericComparison;
70
-
71
- return aLabel.localeCompare(bLabel);
72
- });
73
- }
74
-
75
- return choices;
76
- };
77
-
78
- const getComponentByWidgetType = (widgetType: string, facetKey: string) => {
79
- if (facetKey === sizeKey) {
80
- return SizeFilter;
81
- }
82
- return COMPONENT_TYPES[widgetType] || COMPONENT_TYPES[WIDGET_TYPE.category];
83
- };
84
-
85
- export const FilterItem = ({ facet, isPending, startTransition }: Props) => {
86
- const dispatch = useAppDispatch();
87
- const router = useRouter();
88
- const pathname = usePathname();
89
-
90
- const [pendingChoice, setPendingChoice] = useState<string | null>(null);
91
-
92
- const handleSelectFilter = ({
93
- facet,
94
- choice
95
- }: {
96
- facet: Facet;
97
- choice: FacetChoice;
98
- }) => {
99
- setPendingChoice(choice.label);
100
- startTransition(() => {
101
- if (facet.key === 'category_ids') {
102
- router.push(choice.url);
103
- } else {
104
- dispatch(toggleFacet({ facet, choice }));
105
- }
106
- setPendingChoice(null);
107
-
108
- const urlSearchParams = new URLSearchParams(window.location.search);
109
- urlSearchParams.delete(facet.search_key);
110
- router.replace(pathname + '?' + urlSearchParams.toString());
111
- });
112
- };
113
-
114
- const Component = getComponentByWidgetType(facet.widget_type, facet.key);
115
- const choices = sortChoices(facet.key, [...facet.data.choices]);
116
-
117
- return (
118
- <Accordion
119
- key={facet.key}
120
- title={facet.name}
121
- isCollapse={choices.some((choice) => choice.is_selected)}
122
- dataTestId={`filter-${facet.name}`}
123
- >
124
- <div
125
- className={clsx('flex gap-4', {
126
- 'flex-wrap flex-row': facet.key === sizeKey,
127
- 'flex-col': facet.key !== sizeKey
128
- })}
129
- >
130
- {choices.map((choice, index) => (
131
- <div key={choice.label} className="relative">
132
- <Component
133
- key={choice.label}
134
- data={choice}
135
- name={facet.key}
136
- onChange={() => handleSelectFilter({ facet, choice })}
137
- onClick={() =>
138
- facet.key === sizeKey && handleSelectFilter({ facet, choice })
139
- }
140
- checked={choice.is_selected}
141
- data-testid={`${choice.label.trim()}`}
142
- disabled={isPending}
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
- {isPending && pendingChoice === choice.label && (
154
- <div className="absolute inset-0 flex items-center justify-center z-50">
155
- <LoaderSpinner />
156
- </div>
157
- )}
158
- </div>
159
- ))}
160
- </div>
161
- </Accordion>
162
- );
163
- };