@akinon/projectzero 2.0.0-beta.1 → 2.0.0-beta.11
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 +41 -0
- package/app-template/.env.example +5 -0
- package/app-template/.gitignore +2 -0
- package/app-template/CHANGELOG.md +251 -0
- package/app-template/README.md +6 -0
- package/app-template/config/prebuild-tests.json +5 -0
- package/app-template/docs/plugins.md +60 -25
- package/app-template/jest.config.ts +2 -2
- package/app-template/{next.config.mjs → next.config.ts} +6 -3
- package/app-template/package.json +30 -27
- package/app-template/postcss.config.mjs +8 -0
- package/app-template/public/locales/en/account.json +4 -0
- package/app-template/public/locales/en/common.json +10 -0
- package/app-template/public/locales/tr/account.json +4 -0
- package/app-template/public/locales/tr/common.json +10 -0
- package/app-template/src/__tests__/middleware-matcher.test.ts +135 -0
- package/app-template/src/app/[commerce]/[locale]/[currency]/account/orders/[id]/cancellation/page.tsx +99 -7
- package/app-template/src/app/[commerce]/[locale]/[currency]/account/orders/[id]/page.tsx +112 -47
- package/app-template/src/app/[commerce]/[locale]/[currency]/account/page.tsx +1 -1
- package/app-template/src/app/[commerce]/[locale]/[currency]/address/stores/page.tsx +2 -2
- package/app-template/src/app/[commerce]/[locale]/[currency]/auth/page.tsx +1 -1
- package/app-template/src/app/[commerce]/[locale]/[currency]/basket/page.tsx +2 -2
- package/app-template/src/app/[commerce]/[locale]/[currency]/error.tsx +12 -15
- package/app-template/src/app/[commerce]/[locale]/[currency]/forms/[pk]/generate/page.tsx +1 -1
- package/app-template/src/app/[commerce]/[locale]/[currency]/{pz-not-found/page.tsx → not-found.tsx} +2 -2
- package/app-template/src/app/[commerce]/[locale]/[currency]/orders/checkout/page.tsx +7 -4
- package/app-template/src/app/[commerce]/[locale]/[currency]/xml-sitemap/[node]/route.ts +47 -1
- package/app-template/src/assets/globals.scss +162 -34
- package/app-template/src/components/__tests__/badge.test.tsx +2 -2
- package/app-template/src/components/accordion.tsx +1 -1
- package/app-template/src/components/button.tsx +50 -35
- package/app-template/src/components/checkbox.tsx +1 -0
- package/app-template/src/components/file-input.tsx +44 -2
- package/app-template/src/components/input.tsx +3 -3
- package/app-template/src/components/modal.tsx +1 -1
- package/app-template/src/components/select.tsx +2 -2
- package/app-template/src/components/shimmer.tsx +1 -1
- package/app-template/src/components/tabs.tsx +2 -2
- package/app-template/src/components/types/index.ts +4 -1
- package/app-template/src/middleware.ts +1 -0
- package/app-template/src/plugins.js +2 -1
- package/app-template/src/redux/middlewares/category.ts +1 -1
- package/app-template/src/redux/reducers/category.ts +1 -1
- package/app-template/src/redux/store.ts +4 -3
- package/app-template/src/utils/convert-facet-search-params.ts +1 -1
- package/app-template/src/views/account/contact-form.tsx +3 -8
- package/app-template/src/views/account/content-header.tsx +2 -3
- package/app-template/src/views/account/order.tsx +11 -9
- package/app-template/src/views/account/orders/order-cancellation-item.tsx +5 -4
- package/app-template/src/views/anonymous-tracking/order-detail/index.tsx +45 -38
- package/app-template/src/views/basket/basket-item.tsx +1 -0
- package/app-template/src/views/category/category-active-filters.tsx +1 -1
- package/app-template/src/views/category/category-header.tsx +12 -6
- package/app-template/src/views/category/category-info.tsx +4 -4
- package/app-template/src/views/category/filters/index.tsx +2 -2
- package/app-template/src/views/checkout/auth.tsx +1 -1
- package/app-template/src/views/checkout/layout/header.tsx +1 -1
- package/app-template/src/views/checkout/steps/payment/index.tsx +1 -1
- package/app-template/src/views/checkout/steps/payment/options/credit-card/index.tsx +1 -1
- package/app-template/src/views/checkout/steps/payment/options/redirection.tsx +5 -1
- package/app-template/src/views/checkout/steps/payment/payment-option-buttons.tsx +4 -4
- package/app-template/src/views/checkout/steps/shipping/address-box.tsx +3 -3
- package/app-template/src/views/checkout/steps/shipping/addresses.tsx +1 -1
- package/app-template/src/views/checkout/summary.tsx +2 -2
- package/app-template/src/views/header/action-menu.tsx +11 -4
- package/app-template/src/views/header/band.tsx +2 -2
- package/app-template/src/views/header/mini-basket.tsx +15 -4
- package/app-template/src/views/header/mobile-menu.tsx +6 -6
- package/app-template/src/views/header/navbar.tsx +1 -1
- package/app-template/src/views/header/pwa-back-button.tsx +1 -1
- package/app-template/src/views/header/search/index.tsx +16 -4
- package/app-template/src/views/header/search/results.tsx +1 -1
- package/app-template/src/views/header/user-menu.tsx +3 -1
- package/app-template/src/views/installment-options/index.tsx +1 -1
- package/app-template/src/views/login/index.tsx +30 -6
- package/app-template/src/views/otp-login/index.tsx +12 -14
- package/app-template/src/views/product/price-wrapper.tsx +7 -2
- package/app-template/src/views/product/product-info.tsx +35 -5
- package/app-template/src/views/product/slider.tsx +1 -1
- package/app-template/src/views/product-pointer-banner-item.tsx +1 -1
- package/app-template/src/views/register/index.tsx +29 -4
- package/app-template/src/views/sales-contract-modal/index.tsx +17 -17
- package/app-template/src/widgets/footer-info.tsx +1 -1
- package/app-template/src/widgets/footer-menu.tsx +1 -1
- package/app-template/src/widgets/footer-subscription/index.tsx +1 -1
- package/app-template/src/widgets/home-stories-eng.tsx +1 -1
- package/app-template/tailwind.config.js +2 -134
- package/codemods/sentry-9/index.js +30 -0
- package/codemods/sentry-9/remove-sentry-configs.js +14 -0
- package/codemods/sentry-9/remove-sentry-dependency.js +25 -0
- package/codemods/sentry-9/replace-error-page.js +32 -0
- package/codemods/update-tailwind-config/index.js +30 -0
- package/codemods/update-tailwind-config/transform.js +102 -0
- package/commands/codemod.ts +17 -0
- package/commands/index.ts +3 -1
- package/commands/plugins.ts +24 -30
- package/dist/codemods/sentry-9/templates/error.js +14 -0
- package/dist/commands/codemod.js +15 -0
- package/dist/commands/index.js +3 -1
- package/dist/commands/plugins.js +23 -20
- package/package.json +3 -2
- package/app-template/postcss.config.js +0 -6
- package/app-template/sentry.client.config.ts +0 -16
- package/app-template/sentry.edge.config.ts +0 -3
- package/app-template/sentry.properties +0 -4
- package/app-template/sentry.server.config.ts +0 -3
- package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/loading.tsx +0 -67
|
@@ -130,7 +130,7 @@ const AddressBox = ({
|
|
|
130
130
|
onClick={() => handleAddressClick(addressType, address)}
|
|
131
131
|
key={address.pk}
|
|
132
132
|
className={clsx(
|
|
133
|
-
'cursor-pointer relative w-full border shadow p-4 min-h-[8rem]',
|
|
133
|
+
'cursor-pointer relative w-full border border-gray-200 shadow-sm p-4 min-h-[8rem]',
|
|
134
134
|
"hover:after:content-[''] hover:after:border-4 hover:after:opacity-30 hover:after:transition-opacity",
|
|
135
135
|
'after:border-secondary-400 after:absolute after:inset-0 after:duration-150 after:-z-10',
|
|
136
136
|
{
|
|
@@ -167,7 +167,7 @@ const AddressBox = ({
|
|
|
167
167
|
<div className="text-xs flex justify-between">
|
|
168
168
|
<Button
|
|
169
169
|
appearance="ghost"
|
|
170
|
-
className="italic underline hover:text-secondary-500 hover
|
|
170
|
+
className="italic underline hover:text-secondary-500 hover:bg-white! hover:border-white! p-0 h-auto"
|
|
171
171
|
onClick={() => setIsEditAddressModalOpen(true)}
|
|
172
172
|
data-testid="checkout-address-edit"
|
|
173
173
|
>
|
|
@@ -193,7 +193,7 @@ const AddressBox = ({
|
|
|
193
193
|
</Modal>
|
|
194
194
|
<Button
|
|
195
195
|
appearance="ghost"
|
|
196
|
-
className="italic underline hover:text-secondary-500 hover
|
|
196
|
+
className="italic underline hover:text-secondary-500 hover:bg-white! hover:border-white! p-0 h-auto"
|
|
197
197
|
onClick={() => setRemoveAddressModalOpen(true)}
|
|
198
198
|
data-testid="checkout-address-remove"
|
|
199
199
|
>
|
|
@@ -153,7 +153,7 @@ const Addresses = () => {
|
|
|
153
153
|
role="button"
|
|
154
154
|
onClick={() => setIsModalOpen(true)}
|
|
155
155
|
className={clsx(
|
|
156
|
-
'relative cursor-pointer w-full min-h-[8rem] border shadow p-4',
|
|
156
|
+
'relative cursor-pointer w-full min-h-[8rem] border border-gray-200 shadow-sm p-4',
|
|
157
157
|
"hover:after:content-[''] hover:after:border-4 hover:after:opacity-30 hover:after:transition-opacity",
|
|
158
158
|
'after:border-secondary-400 after:absolute after:inset-0 after:opacity-0 after:duration-150 after:-z-10',
|
|
159
159
|
{
|
|
@@ -149,7 +149,7 @@ export const Summary = () => {
|
|
|
149
149
|
</div>
|
|
150
150
|
</div>
|
|
151
151
|
<div className="flex flex-col py-4 px-4 text-black-800 text-xs sm:px-5">
|
|
152
|
-
<div className="w-full overflow-hidden
|
|
152
|
+
<div className="w-full overflow-hidden text-ellipsis mb-1 last:mb-0">
|
|
153
153
|
<Trans
|
|
154
154
|
i18nKey="checkout.summary.info"
|
|
155
155
|
components={{
|
|
@@ -162,7 +162,7 @@ export const Summary = () => {
|
|
|
162
162
|
}}
|
|
163
163
|
/>
|
|
164
164
|
</div>
|
|
165
|
-
<div className="w-full overflow-hidden
|
|
165
|
+
<div className="w-full overflow-hidden text-ellipsis mb-1 last:mb-0">
|
|
166
166
|
{preOrder.shipping_address?.line}{' '}
|
|
167
167
|
{preOrder.shipping_address?.postcode}{' '}
|
|
168
168
|
{preOrder.shipping_address?.district && (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { ReactNode, useMemo, useRef, useCallback } from 'react';
|
|
4
|
-
import {
|
|
4
|
+
import { useGetMiniBasketQuery } from '@akinon/next/data/client/basket';
|
|
5
5
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
6
6
|
import {
|
|
7
7
|
closeMiniBasket,
|
|
@@ -29,8 +29,11 @@ interface MenuItem {
|
|
|
29
29
|
export default function ActionMenu() {
|
|
30
30
|
const dispatch = useAppDispatch();
|
|
31
31
|
|
|
32
|
-
const { data } =
|
|
33
|
-
const totalQuantity = useMemo(
|
|
32
|
+
const { data: miniBasket } = useGetMiniBasketQuery();
|
|
33
|
+
const totalQuantity = useMemo(
|
|
34
|
+
() => miniBasket?.total_quantity ?? 0,
|
|
35
|
+
[miniBasket]
|
|
36
|
+
);
|
|
34
37
|
|
|
35
38
|
const { open: miniBasketOpen } = useAppSelector(
|
|
36
39
|
(state) => state.root.miniBasket
|
|
@@ -89,7 +92,11 @@ export default function ActionMenu() {
|
|
|
89
92
|
ref={menu.miniBasket ? miniBasketRef : null}
|
|
90
93
|
>
|
|
91
94
|
{menu.action ? (
|
|
92
|
-
<button
|
|
95
|
+
<button
|
|
96
|
+
onClick={menu.action}
|
|
97
|
+
data-testid={menu.dataTestId}
|
|
98
|
+
className="cursor-pointer"
|
|
99
|
+
>
|
|
93
100
|
<Icon name={menu.icon} size={24} />
|
|
94
101
|
{menu.badge}
|
|
95
102
|
</button>
|
|
@@ -16,13 +16,13 @@ export default function HeaderBand() {
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
18
|
<div className="header-grid-area-nav bg-gray-100 h-auto p-3 sm:header-grid-area-band-m sm:h-9 sm:py-0">
|
|
19
|
-
<div className="text-center
|
|
19
|
+
<div className="text-center text-ellipsis line-clamp-2 h-full flex items-center justify-center">
|
|
20
20
|
<HeaderBandText />
|
|
21
21
|
</div>
|
|
22
22
|
</div>
|
|
23
23
|
|
|
24
24
|
<div className="header-grid-area-main-r h-full pr-4 sm:header-grid-area-band-r sm:pr-0">
|
|
25
|
-
<div className="flex items-center justify-end h-full">
|
|
25
|
+
<div className="flex items-center justify-end h-full gap-10">
|
|
26
26
|
<UserMenu isMobile={false} />
|
|
27
27
|
<ActionMenu />
|
|
28
28
|
</div>
|
|
@@ -5,6 +5,7 @@ import clsx from 'clsx';
|
|
|
5
5
|
import {
|
|
6
6
|
basketApi,
|
|
7
7
|
useGetBasketQuery,
|
|
8
|
+
useGetMiniBasketQuery,
|
|
8
9
|
useUpdateQuantityMutation
|
|
9
10
|
} from '@akinon/next/data/client/basket';
|
|
10
11
|
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
@@ -150,13 +151,23 @@ export default function MiniBasket() {
|
|
|
150
151
|
(state) => state.root.miniBasket
|
|
151
152
|
);
|
|
152
153
|
const dispatch = useAppDispatch();
|
|
153
|
-
const {
|
|
154
|
+
const {
|
|
155
|
+
data: basket,
|
|
156
|
+
isLoading,
|
|
157
|
+
isSuccess
|
|
158
|
+
} = useGetBasketQuery(undefined, {
|
|
159
|
+
skip: !miniBasketOpen
|
|
160
|
+
});
|
|
161
|
+
const { data: miniBasket } = useGetMiniBasketQuery();
|
|
154
162
|
const { t } = useLocalization();
|
|
155
163
|
const { highlightedItem } = useAppSelector((state) => state.root.miniBasket);
|
|
156
164
|
const [highlightedItemPk, setHighlightedItemPk] = useState(0);
|
|
157
165
|
const [sortedBasket, setSortedBasket] = useState([]);
|
|
158
166
|
|
|
159
|
-
const totalQuantity = useMemo(
|
|
167
|
+
const totalQuantity = useMemo(
|
|
168
|
+
() => miniBasket?.total_quantity ?? 0,
|
|
169
|
+
[miniBasket]
|
|
170
|
+
);
|
|
160
171
|
const miniBasketList = useRef(null);
|
|
161
172
|
|
|
162
173
|
useEffect(() => {
|
|
@@ -192,7 +203,7 @@ export default function MiniBasket() {
|
|
|
192
203
|
miniBasketOpen
|
|
193
204
|
? 'opacity-100 visible lg:invisible'
|
|
194
205
|
: 'opacity-0 invisible',
|
|
195
|
-
'fixed top-0 left-0 z-50 w-screen h-screen bg-black
|
|
206
|
+
'fixed top-0 left-0 z-50 w-screen h-screen bg-black/80 transition-all duration-300'
|
|
196
207
|
)}
|
|
197
208
|
onClick={() => {
|
|
198
209
|
dispatch(closeMiniBasket());
|
|
@@ -206,7 +217,7 @@ export default function MiniBasket() {
|
|
|
206
217
|
'fixed lg:absolute bottom-0 lg:-bottom-1 right-0 w-80 h-screen lg:h-auto bg-white lg:border-l lg:border-t lg:border-r-2 lg:border-b-2 lg:border-gray-500 p-5 z-50 transition-all duration-300'
|
|
207
218
|
)}
|
|
208
219
|
>
|
|
209
|
-
<header className="flex items-center gap-2 pb-3 border-b">
|
|
220
|
+
<header className="flex items-center gap-2 pb-3 border-b border-gray-200">
|
|
210
221
|
<h3 className="text-xl font-bold">
|
|
211
222
|
{t('basket.mini_basket.my_bag')}
|
|
212
223
|
</h3>
|
|
@@ -58,9 +58,9 @@ export default function MobileMenu(props: MobileMenuProps) {
|
|
|
58
58
|
<>
|
|
59
59
|
<div
|
|
60
60
|
className={clsx(
|
|
61
|
-
'fixed top-0 left-0 z-30 w-screen h-screen invisible opacity-0 bg-black
|
|
61
|
+
'fixed top-0 left-0 z-30 w-screen h-screen invisible opacity-0 bg-black/80 transition duration-500',
|
|
62
62
|
{
|
|
63
|
-
'!
|
|
63
|
+
'visible! opacity-100! scroll-lock': isMobileMenuOpen
|
|
64
64
|
}
|
|
65
65
|
)}
|
|
66
66
|
/>
|
|
@@ -70,7 +70,7 @@ export default function MobileMenu(props: MobileMenuProps) {
|
|
|
70
70
|
className={clsx(
|
|
71
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',
|
|
72
72
|
{
|
|
73
|
-
'!
|
|
73
|
+
'visible! opacity-100! translate-x-0': isMobileMenuOpen
|
|
74
74
|
}
|
|
75
75
|
)}
|
|
76
76
|
>
|
|
@@ -106,15 +106,15 @@ export default function MobileMenu(props: MobileMenuProps) {
|
|
|
106
106
|
className={clsx(
|
|
107
107
|
'absolute top-0 left-0 right-0 px-8 bg-white invisible opacity-0 transition duration-500 transform translate-x-full',
|
|
108
108
|
{
|
|
109
|
-
'!
|
|
109
|
+
'visible! opacity-100! translate-x-0!': selectedSubMenu
|
|
110
110
|
}
|
|
111
111
|
)}
|
|
112
112
|
>
|
|
113
|
-
<header className="flex items-center justify-between border-b h-[61px] py-4 mb-4">
|
|
113
|
+
<header className="flex items-center justify-between border-b border-gray-200 h-[61px] py-4 mb-4">
|
|
114
114
|
<Button
|
|
115
115
|
appearance="ghost"
|
|
116
116
|
onClick={() => setSelectedSubMenu(null)}
|
|
117
|
-
className="underline text-xs font-semibold flex items-center gap-2
|
|
117
|
+
className="underline text-xs font-semibold flex items-center gap-2 p-0!"
|
|
118
118
|
>
|
|
119
119
|
<Icon name="chevron-start" size={12} className="shrink-0" />
|
|
120
120
|
{t('common.mobile_menu.back')}
|
|
@@ -34,7 +34,7 @@ export const PwaBackButton = () => {
|
|
|
34
34
|
return (
|
|
35
35
|
<div className="relative z-10 md:top-0 md:left-0 md:fixed">
|
|
36
36
|
<button
|
|
37
|
-
className="bg-secondary-600 text-white flex items-center justify-center
|
|
37
|
+
className="bg-secondary-600 text-white flex items-center justify-center shrink-0 w-12 h-12 md:w-10 md:h-9 active:bg-secondary-700"
|
|
38
38
|
onClick={() => router.back()}
|
|
39
39
|
>
|
|
40
40
|
<svg
|
|
@@ -22,19 +22,31 @@ export default function Search() {
|
|
|
22
22
|
if (isSearchOpen) {
|
|
23
23
|
inputRef.current?.focus();
|
|
24
24
|
document.body.style.overflow = 'hidden';
|
|
25
|
+
|
|
26
|
+
const handleEscKey = (e: KeyboardEvent) => {
|
|
27
|
+
if (e.key === 'Escape') {
|
|
28
|
+
dispatch(closeSearch());
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
document.addEventListener('keydown', handleEscKey);
|
|
33
|
+
return () => {
|
|
34
|
+
document.removeEventListener('keydown', handleEscKey);
|
|
35
|
+
document.body.style.overflow = 'auto';
|
|
36
|
+
};
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
return () => {
|
|
28
40
|
document.body.style.overflow = 'auto';
|
|
29
41
|
};
|
|
30
|
-
}, [isSearchOpen]);
|
|
42
|
+
}, [isSearchOpen, dispatch]);
|
|
31
43
|
|
|
32
44
|
return (
|
|
33
45
|
<>
|
|
34
46
|
<div
|
|
35
47
|
className={clsx(
|
|
36
48
|
// 177px is the height of the header
|
|
37
|
-
'absolute bg-black
|
|
49
|
+
'absolute bg-black/75 w-screen h-screen transition duration-500 left-0 bottom-0 translate-y-full z-30',
|
|
38
50
|
isSearchOpen && searchText
|
|
39
51
|
? 'visible opacity-100'
|
|
40
52
|
: 'invisible opacity-0'
|
|
@@ -48,7 +60,7 @@ export default function Search() {
|
|
|
48
60
|
isSearchOpen ? 'visible opacity-100' : 'invisible opacity-0'
|
|
49
61
|
)}
|
|
50
62
|
>
|
|
51
|
-
<div className="max-w-
|
|
63
|
+
<div className="max-w-(--breakpoint-2xl) mx-auto flex flex-col gap-12">
|
|
52
64
|
<div className="border-b border-gray-400 flex flex-col py-1.5 gap-2 self-center items-center md:flex-row">
|
|
53
65
|
<span className="text-xl lg:text-2xl">
|
|
54
66
|
{t('common.search.results_for')}
|
|
@@ -62,7 +74,7 @@ export default function Search() {
|
|
|
62
74
|
router.push(`${ROUTES.LIST}/?search_text=${searchText}`);
|
|
63
75
|
}
|
|
64
76
|
}}
|
|
65
|
-
className="border-0 text-2xl outline-
|
|
77
|
+
className="border-0 text-2xl outline-hidden text-secondary placeholder:text-xl lg:placeholder:text-2xl"
|
|
66
78
|
placeholder={t('common.search.placeholder')}
|
|
67
79
|
ref={inputRef}
|
|
68
80
|
/>
|
|
@@ -80,7 +80,7 @@ export default function Results(props: ResultsProps) {
|
|
|
80
80
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-8">
|
|
81
81
|
{products.map((product, index) => (
|
|
82
82
|
<Link href={product?.url} key={index} className="flex flex-col">
|
|
83
|
-
<div className="relative aspect-
|
|
83
|
+
<div className="relative aspect-315/448">
|
|
84
84
|
{product.extra.image ? (
|
|
85
85
|
<Image
|
|
86
86
|
src={product.extra.image}
|
|
@@ -36,7 +36,9 @@ export const UserMenu = (props: UserMenuProps) => {
|
|
|
36
36
|
<ul
|
|
37
37
|
className={clsx(
|
|
38
38
|
'items-center divide-x divide-black',
|
|
39
|
-
isMobile
|
|
39
|
+
isMobile
|
|
40
|
+
? 'flex pt-2 text-sm pb-6 border-b border-gray-200 mx-8'
|
|
41
|
+
: 'hidden sm:flex'
|
|
40
42
|
)}
|
|
41
43
|
id="user-menu"
|
|
42
44
|
>
|
|
@@ -103,10 +103,34 @@ export const Login = () => {
|
|
|
103
103
|
)?.data as string[];
|
|
104
104
|
|
|
105
105
|
fieldErrors?.forEach((item) => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
let parsedValue: Record<string, string[]> | string[] = [];
|
|
107
|
+
|
|
108
|
+
if (typeof item.value === 'string') {
|
|
109
|
+
try {
|
|
110
|
+
parsedValue = JSON.parse(item.value);
|
|
111
|
+
} catch {
|
|
112
|
+
parsedValue = [item.value];
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
parsedValue = item.value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (Array.isArray(parsedValue)) {
|
|
119
|
+
setError(item.name as keyof LoginFormType, {
|
|
120
|
+
type: 'custom',
|
|
121
|
+
message: parsedValue.join(', '),
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
Object.keys(parsedValue).forEach((key) => {
|
|
125
|
+
const fieldName = key as keyof LoginFormType;
|
|
126
|
+
const errorMessages = parsedValue[key] as string[];
|
|
127
|
+
|
|
128
|
+
setError(fieldName, {
|
|
129
|
+
type: 'custom',
|
|
130
|
+
message: errorMessages.join(', '),
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
110
134
|
});
|
|
111
135
|
|
|
112
136
|
if (nonFieldErrors?.length) {
|
|
@@ -195,7 +219,7 @@ export const Login = () => {
|
|
|
195
219
|
{t('auth.login.form.submit')}
|
|
196
220
|
</Button>
|
|
197
221
|
|
|
198
|
-
<p className="relative text-gray-600 text-center my-4 before:absolute before:h-[1px] before:w-5/12 before:bg-gray-600
|
|
222
|
+
<p className="relative text-gray-600 text-center my-4 before:absolute before:h-[1px] before:w-5/12 before:bg-gray-600/25 before:top-1/2 before:left-0 after:absolute after:h-[1px] after:w-5/12 after:bg-gray-600/25 after:top-1/2 after:right-0">
|
|
199
223
|
{t('auth.login.form.or')}
|
|
200
224
|
</p>
|
|
201
225
|
|
|
@@ -224,7 +248,7 @@ export const Login = () => {
|
|
|
224
248
|
alt={provider.label}
|
|
225
249
|
width={provider.label === 'Facebook' ? 10 : 18}
|
|
226
250
|
height={18}
|
|
227
|
-
className="
|
|
251
|
+
className="shrink-0"
|
|
228
252
|
/>
|
|
229
253
|
)}
|
|
230
254
|
|
|
@@ -10,9 +10,10 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
|
|
10
10
|
import clsx from 'clsx';
|
|
11
11
|
import { useLocalization } from '@akinon/next/hooks';
|
|
12
12
|
import { useOtpLoginMutation } from '@akinon/next/data/client/user';
|
|
13
|
-
import { useAppSelector } from '@akinon/next/redux/hooks';
|
|
13
|
+
import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
|
|
14
14
|
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
15
15
|
import { AuthError } from '@akinon/next/types';
|
|
16
|
+
import { showPopup } from '@akinon/pz-otp/src/redux/reducer';
|
|
16
17
|
|
|
17
18
|
const loginFormSchema = (t) =>
|
|
18
19
|
yup.object().shape({
|
|
@@ -25,9 +26,9 @@ const loginFormSchema = (t) =>
|
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
export const OtpLogin = () => {
|
|
29
|
+
const dispatch = useAppDispatch();
|
|
28
30
|
const { user_phone_format } = useAppSelector((state) => state.config);
|
|
29
31
|
const { t, locale } = useLocalization();
|
|
30
|
-
const [showOtpModal, setShowOtpModal] = useState(false);
|
|
31
32
|
const [otpLoginMutation] = useOtpLoginMutation();
|
|
32
33
|
|
|
33
34
|
const {
|
|
@@ -79,7 +80,7 @@ export const OtpLogin = () => {
|
|
|
79
80
|
})
|
|
80
81
|
.unwrap()
|
|
81
82
|
.then(() => {
|
|
82
|
-
|
|
83
|
+
dispatch(showPopup());
|
|
83
84
|
})
|
|
84
85
|
.catch((error) => {
|
|
85
86
|
if (error.status === 429) {
|
|
@@ -136,17 +137,14 @@ export const OtpLogin = () => {
|
|
|
136
137
|
</Button>
|
|
137
138
|
</form>
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}}
|
|
148
|
-
/>
|
|
149
|
-
)}
|
|
140
|
+
<PluginModule
|
|
141
|
+
component={Component.Otp}
|
|
142
|
+
props={{
|
|
143
|
+
data: getValues(),
|
|
144
|
+
submitAction: loginHandler,
|
|
145
|
+
error: formError
|
|
146
|
+
}}
|
|
147
|
+
/>
|
|
150
148
|
</section>
|
|
151
149
|
);
|
|
152
150
|
};
|
|
@@ -13,7 +13,8 @@ export interface PriceProps {
|
|
|
13
13
|
export default function PriceWrapper(props: PriceProps) {
|
|
14
14
|
const { t } = useLocalization();
|
|
15
15
|
const { price, retailPrice } = props;
|
|
16
|
-
const hasRetailPrice =
|
|
16
|
+
const hasRetailPrice =
|
|
17
|
+
parseFloat(retailPrice || '0') > parseFloat(price || '0');
|
|
17
18
|
|
|
18
19
|
return (
|
|
19
20
|
<div className="flex items-center gap-3 justify-center h-full">
|
|
@@ -31,7 +32,11 @@ export default function PriceWrapper(props: PriceProps) {
|
|
|
31
32
|
{hasRetailPrice && (
|
|
32
33
|
<div className="flex flex-col items-center w-9 py-0.5 text-xs text-white bg-secondary">
|
|
33
34
|
<span className="font-bold">
|
|
34
|
-
{Math.round(
|
|
35
|
+
{Math.round(
|
|
36
|
+
100 -
|
|
37
|
+
(parseInt(price || '0') / parseInt(retailPrice || '1')) * 100
|
|
38
|
+
)}
|
|
39
|
+
%
|
|
35
40
|
</span>
|
|
36
41
|
<span>{t('product.off')}</span>
|
|
37
42
|
</div>
|
|
@@ -23,6 +23,7 @@ export default function ProductInfo({ data }: ProductPageProps) {
|
|
|
23
23
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
24
24
|
const [stockAlertResponseMessage, setStockAlertResponseMessage] =
|
|
25
25
|
useState(null);
|
|
26
|
+
const [isVariantLoading, setIsVariantLoading] = useState(false);
|
|
26
27
|
|
|
27
28
|
const [addProduct, { isLoading: isAddToCartLoading }] =
|
|
28
29
|
useAddProductToBasket();
|
|
@@ -30,6 +31,18 @@ export default function ProductInfo({ data }: ProductPageProps) {
|
|
|
30
31
|
useAddStockAlertMutation();
|
|
31
32
|
const inStock = data.selected_variant !== null || data.product.in_stock;
|
|
32
33
|
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
isVariantSelectionComplete() && setIsVariantLoading(false);
|
|
36
|
+
|
|
37
|
+
!inStock && setIsVariantLoading(false);
|
|
38
|
+
}, [data]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (isVariantLoading) {
|
|
42
|
+
setProductError(null);
|
|
43
|
+
}
|
|
44
|
+
}, [isVariantLoading]);
|
|
45
|
+
|
|
33
46
|
useEffect(() => {
|
|
34
47
|
setCurrentUrl(window.location.href);
|
|
35
48
|
}, [currentUrl]);
|
|
@@ -82,6 +95,12 @@ export default function ProductInfo({ data }: ProductPageProps) {
|
|
|
82
95
|
return true;
|
|
83
96
|
};
|
|
84
97
|
|
|
98
|
+
const isVariantSelectionComplete = () => {
|
|
99
|
+
return data?.variants.every((variant) =>
|
|
100
|
+
variant?.options.some((opt) => opt.is_selected)
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
85
104
|
const addProductToStockAlertList = async () => {
|
|
86
105
|
try {
|
|
87
106
|
await addStockAlert({
|
|
@@ -159,7 +178,7 @@ export default function ProductInfo({ data }: ProductPageProps) {
|
|
|
159
178
|
<>
|
|
160
179
|
<div
|
|
161
180
|
className={clsx(
|
|
162
|
-
'fixed bottom-0 left-0 w-1/2 h-14 z-
|
|
181
|
+
'fixed bottom-0 left-0 w-1/2 h-14 z-20 bg-white mt-0 border-t border-gray-500 items-center justify-center',
|
|
163
182
|
'sm:relative sm:flex sm:items-center sm:mt-5 sm:border-none'
|
|
164
183
|
)}
|
|
165
184
|
>
|
|
@@ -174,7 +193,10 @@ export default function ProductInfo({ data }: ProductPageProps) {
|
|
|
174
193
|
key={variant.attribute_key}
|
|
175
194
|
{...variant}
|
|
176
195
|
className="items-center mt-8"
|
|
177
|
-
onChange={() =>
|
|
196
|
+
onChange={() => {
|
|
197
|
+
setProductError(null);
|
|
198
|
+
setIsVariantLoading(true);
|
|
199
|
+
}}
|
|
178
200
|
/>
|
|
179
201
|
))}
|
|
180
202
|
</div>
|
|
@@ -186,9 +208,11 @@ export default function ProductInfo({ data }: ProductPageProps) {
|
|
|
186
208
|
)}
|
|
187
209
|
|
|
188
210
|
<Button
|
|
189
|
-
disabled={
|
|
211
|
+
disabled={
|
|
212
|
+
isAddToCartLoading || isAddToStockAlertLoading || isVariantLoading
|
|
213
|
+
}
|
|
190
214
|
className={clsx(
|
|
191
|
-
'fixed bottom-0 right-0 w-1/2 h-14 z-
|
|
215
|
+
'fixed bottom-0 right-0 w-1/2 h-14 z-20 flex items-center justify-center fill-primary-foreground',
|
|
192
216
|
'hover:fill-primary sm:relative sm:w-full sm:mt-3 sm:font-semibold sm:h-12'
|
|
193
217
|
)}
|
|
194
218
|
onClick={() => {
|
|
@@ -202,7 +226,13 @@ export default function ProductInfo({ data }: ProductPageProps) {
|
|
|
202
226
|
}}
|
|
203
227
|
data-testid="product-add-to-cart"
|
|
204
228
|
>
|
|
205
|
-
{
|
|
229
|
+
{isVariantLoading ? (
|
|
230
|
+
<Icon
|
|
231
|
+
name="spinner"
|
|
232
|
+
size={20}
|
|
233
|
+
className="animate-spin mr-4 fill-primary"
|
|
234
|
+
/>
|
|
235
|
+
) : inStock ? (
|
|
206
236
|
<span>{t('product.add_to_cart')}</span>
|
|
207
237
|
) : (
|
|
208
238
|
<>
|
|
@@ -86,7 +86,7 @@ export default function ProductInfoSlider({ product }: ProductSliderItem) {
|
|
|
86
86
|
</div>
|
|
87
87
|
|
|
88
88
|
<div className="relative lg:col-span-5">
|
|
89
|
-
<FavButton className="absolute right-8 top-6 z-
|
|
89
|
+
<FavButton className="absolute right-8 top-6 z-20 sm:hidden" />
|
|
90
90
|
|
|
91
91
|
<CarouselCore
|
|
92
92
|
responsive={{
|
|
@@ -109,7 +109,7 @@ const ProductPointerWidget = (props: ProductPointerWidgetProps) => {
|
|
|
109
109
|
hidden: buttonStatus
|
|
110
110
|
})}
|
|
111
111
|
>
|
|
112
|
-
<div className="w-full h-full flex items-center gap-2
|
|
112
|
+
<div className="w-full h-full flex items-center gap-2 shrink-0">
|
|
113
113
|
<Image
|
|
114
114
|
src={productItem?.kwargs?.value?.card_image?.url}
|
|
115
115
|
alt={productItem?.value?.alt}
|
|
@@ -143,6 +143,7 @@ export const Register = () => {
|
|
|
143
143
|
if (registerResponse.error) {
|
|
144
144
|
const errors: AuthError[] = JSON.parse(registerResponse.error);
|
|
145
145
|
|
|
146
|
+
|
|
146
147
|
if (errors.find((error) => error.type === 'captcha')) {
|
|
147
148
|
if (await validateCaptcha()) {
|
|
148
149
|
onSubmit(data);
|
|
@@ -164,10 +165,34 @@ export const Register = () => {
|
|
|
164
165
|
)?.data as string[];
|
|
165
166
|
|
|
166
167
|
fieldErrors?.forEach((item) => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
let parsedValue: Record<string, string[]> | string[] = [];
|
|
169
|
+
|
|
170
|
+
if (typeof item.value === 'string') {
|
|
171
|
+
try {
|
|
172
|
+
parsedValue = JSON.parse(item.value);
|
|
173
|
+
} catch {
|
|
174
|
+
parsedValue = [item.value];
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
parsedValue = item.value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (Array.isArray(parsedValue)) {
|
|
181
|
+
setError(item.name as keyof RegisterFormType, {
|
|
182
|
+
type: 'custom',
|
|
183
|
+
message: parsedValue.join(', '),
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
Object.keys(parsedValue).forEach((key) => {
|
|
187
|
+
const fieldName = key as keyof RegisterFormType;
|
|
188
|
+
const errorMessages = parsedValue[key] as string[];
|
|
189
|
+
|
|
190
|
+
setError(fieldName, {
|
|
191
|
+
type: 'custom',
|
|
192
|
+
message: errorMessages.join(', '),
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
171
196
|
});
|
|
172
197
|
|
|
173
198
|
if (nonFieldErrors?.length) {
|