@akinon/projectzero 1.104.0 → 1.105.0-rc.84
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 +234 -4
- package/app-template/.env.example +1 -0
- package/app-template/CHANGELOG.md +4968 -315
- package/app-template/README.md +25 -1
- package/app-template/package.json +19 -19
- package/app-template/public/locales/en/checkout.json +6 -0
- package/app-template/public/locales/en/common.json +42 -1
- package/app-template/public/locales/tr/checkout.json +6 -0
- package/app-template/public/locales/tr/common.json +42 -1
- package/app-template/src/app/[commerce]/[locale]/[currency]/basket/page.tsx +9 -82
- package/app-template/src/app/[commerce]/[locale]/[currency]/landing-page/[pk]/page.tsx +12 -1
- package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/loading.tsx +67 -0
- package/app-template/src/app/api/image-proxy/route.ts +1 -0
- package/app-template/src/app/api/similar-product-list/route.ts +1 -0
- package/app-template/src/app/api/similar-products/route.ts +1 -0
- package/app-template/src/assets/fonts/pz-icon.css +3 -0
- package/app-template/src/components/__tests__/link.test.tsx +2 -0
- package/app-template/src/components/accordion.tsx +22 -19
- package/app-template/src/components/file-input.tsx +27 -7
- package/app-template/src/components/input.tsx +9 -2
- package/app-template/src/components/modal.tsx +32 -16
- package/app-template/src/components/pagination.tsx +1 -0
- package/app-template/src/components/price.tsx +1 -1
- package/app-template/src/components/select.tsx +38 -26
- package/app-template/src/components/types/index.ts +25 -1
- package/app-template/src/hooks/index.ts +2 -0
- package/app-template/src/hooks/use-product-cart.ts +77 -0
- package/app-template/src/hooks/use-stock-alert.ts +74 -0
- package/app-template/src/plugins.js +3 -1
- package/app-template/src/settings.js +6 -1
- package/app-template/src/types/index.ts +17 -0
- package/app-template/src/utils/variant-validation.ts +41 -0
- package/app-template/src/views/account/address-form.tsx +8 -4
- package/app-template/src/views/account/contact-form.tsx +1 -1
- package/app-template/src/views/account/content-header.tsx +2 -2
- package/app-template/src/views/account/faq/faq-tabs.tsx +8 -2
- package/app-template/src/views/basket/basket-content.tsx +106 -0
- package/app-template/src/views/basket/basket-item.tsx +22 -14
- package/app-template/src/views/basket/summary.tsx +10 -7
- package/app-template/src/views/breadcrumb.tsx +2 -2
- package/app-template/src/views/category/category-info.tsx +1 -0
- package/app-template/src/views/category/filters/index.tsx +1 -1
- package/app-template/src/views/checkout/steps/payment/options/store-credit.tsx +121 -0
- package/app-template/src/views/checkout/summary.tsx +10 -0
- package/app-template/src/views/guest-login/index.tsx +6 -1
- package/app-template/src/views/header/search/index.tsx +17 -5
- package/app-template/src/views/product/product-actions.tsx +165 -0
- package/app-template/src/views/product/product-info.tsx +62 -263
- package/app-template/src/views/product/product-share.tsx +56 -0
- package/app-template/src/views/product/product-variants.tsx +26 -0
- package/app-template/src/views/product/slider.tsx +86 -73
- package/app-template/src/widgets/footer-menu.tsx +6 -2
- package/commands/plugins.ts +63 -16
- package/dist/commands/plugins.js +57 -16
- package/package.json +1 -1
|
@@ -1,177 +1,73 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import clsx from 'clsx';
|
|
4
|
-
import { Button, Icon, Modal } from '@theme/components';
|
|
5
|
-
import { useAddProductToBasket } from '../../hooks';
|
|
6
4
|
import React, { useEffect, useState } from 'react';
|
|
7
|
-
import {
|
|
8
|
-
import { pushAddToCart, pushProductViewed } from '@theme/utils/gtm';
|
|
9
|
-
import { PriceWrapper, Variant } from '@theme/views/product';
|
|
10
|
-
import Share from '@theme/views/share';
|
|
5
|
+
import { PriceWrapper } from '@theme/views/product';
|
|
11
6
|
import { ProductPageProps } from './layout';
|
|
12
7
|
import MiscButtons from './misc-buttons';
|
|
13
|
-
import {
|
|
14
|
-
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
15
|
-
import { Trans } from '@akinon/next/components/trans';
|
|
8
|
+
import { pushProductViewed } from '@theme/utils/gtm';
|
|
16
9
|
import { useSession } from 'next-auth/react';
|
|
10
|
+
import { isVariantSelectionComplete } from '../../utils/variant-validation';
|
|
11
|
+
import { useProductCart } from '../../hooks/use-product-cart';
|
|
12
|
+
import { useStockAlert } from '../../hooks/use-stock-alert';
|
|
13
|
+
import { ProductVariants } from './product-variants';
|
|
14
|
+
import { ProductActions } from './product-actions';
|
|
15
|
+
import { ProductShare } from './product-share';
|
|
17
16
|
|
|
18
17
|
export default function ProductInfo({ data }: ProductPageProps) {
|
|
19
|
-
const { t } = useLocalization();
|
|
20
18
|
const { data: session } = useSession();
|
|
21
|
-
const [currentUrl, setCurrentUrl] = useState(null);
|
|
22
|
-
const [productError, setProductError] = useState(null);
|
|
23
|
-
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
24
|
-
const [stockAlertResponseMessage, setStockAlertResponseMessage] =
|
|
25
|
-
useState(null);
|
|
26
19
|
const [isVariantLoading, setIsVariantLoading] = useState(false);
|
|
27
20
|
|
|
28
|
-
const [addProduct, { isLoading: isAddToCartLoading }] =
|
|
29
|
-
useAddProductToBasket();
|
|
30
|
-
const [addStockAlert, { isLoading: isAddToStockAlertLoading }] =
|
|
31
|
-
useAddStockAlertMutation();
|
|
32
21
|
const inStock = data.selected_variant !== null || data.product.in_stock;
|
|
33
22
|
|
|
34
|
-
|
|
35
|
-
|
|
23
|
+
const {
|
|
24
|
+
addProductToCart,
|
|
25
|
+
productError: cartError,
|
|
26
|
+
clearProductError: clearCartError,
|
|
27
|
+
isAddToCartLoading
|
|
28
|
+
} = useProductCart({
|
|
29
|
+
product: data.product,
|
|
30
|
+
variants: data.variants
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const {
|
|
34
|
+
addProductToStockAlertList,
|
|
35
|
+
isModalOpen,
|
|
36
|
+
stockAlertResponseMessage,
|
|
37
|
+
productError: stockError,
|
|
38
|
+
isAddToStockAlertLoading,
|
|
39
|
+
closeModal,
|
|
40
|
+
clearError: clearStockError
|
|
41
|
+
} = useStockAlert({
|
|
42
|
+
productPk: data.product.pk,
|
|
43
|
+
userEmail: session?.user?.email
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const productError = cartError || stockError;
|
|
47
|
+
const clearProductError = () => {
|
|
48
|
+
clearCartError();
|
|
49
|
+
clearStockError();
|
|
50
|
+
};
|
|
36
51
|
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
isVariantSelectionComplete(data.variants) && setIsVariantLoading(false);
|
|
37
54
|
!inStock && setIsVariantLoading(false);
|
|
38
|
-
}, [data]);
|
|
55
|
+
}, [data, inStock]);
|
|
39
56
|
|
|
40
57
|
useEffect(() => {
|
|
41
58
|
if (isVariantLoading) {
|
|
42
|
-
|
|
59
|
+
clearProductError();
|
|
43
60
|
}
|
|
61
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
44
62
|
}, [isVariantLoading]);
|
|
45
63
|
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
setCurrentUrl(window.location.href);
|
|
48
|
-
}, [currentUrl]);
|
|
49
|
-
|
|
50
64
|
useEffect(() => {
|
|
51
65
|
pushProductViewed(data?.product);
|
|
52
66
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
53
67
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
await addProduct({
|
|
61
|
-
product: data.product.pk,
|
|
62
|
-
quantity: 1,
|
|
63
|
-
attributes: {}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
pushAddToCart(data?.product);
|
|
67
|
-
} catch (error) {
|
|
68
|
-
setProductError(
|
|
69
|
-
error?.data?.non_field_errors ||
|
|
70
|
-
Object.keys(error?.data).map(
|
|
71
|
-
(key) => `${key}: ${error?.data[key].join(', ')}`
|
|
72
|
-
)
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const variantsSelectionCheck = () => {
|
|
78
|
-
const unselectedVariant = data.variants.find((variant) =>
|
|
79
|
-
variant.options.every((opt) => !opt.is_selected)
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
if (unselectedVariant) {
|
|
83
|
-
setProductError(() => (
|
|
84
|
-
<Trans
|
|
85
|
-
i18nKey="product.please_select_variant"
|
|
86
|
-
components={{
|
|
87
|
-
VariantName: <span>{unselectedVariant.attribute_name}</span>
|
|
88
|
-
}}
|
|
89
|
-
/>
|
|
90
|
-
));
|
|
91
|
-
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return true;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const isVariantSelectionComplete = () => {
|
|
99
|
-
return data?.variants.every((variant) =>
|
|
100
|
-
variant?.options.some((opt) => opt.is_selected)
|
|
101
|
-
);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const addProductToStockAlertList = async () => {
|
|
105
|
-
try {
|
|
106
|
-
await addStockAlert({
|
|
107
|
-
productPk: data.product.pk,
|
|
108
|
-
email: session?.user?.email
|
|
109
|
-
})
|
|
110
|
-
.unwrap()
|
|
111
|
-
.then(handleSuccess)
|
|
112
|
-
.catch((err) => handleError(err));
|
|
113
|
-
} catch (error) {
|
|
114
|
-
setProductError(error?.data?.non_field_errors || null);
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const handleModalClick = () => {
|
|
119
|
-
setIsModalOpen(false);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const handleSuccess = () => {
|
|
123
|
-
setStockAlertResponseMessage(() => (
|
|
124
|
-
<Trans
|
|
125
|
-
i18nKey="product.stock_alert.success_description"
|
|
126
|
-
components={{
|
|
127
|
-
Email: <span>{session?.user?.email}</span>
|
|
128
|
-
}}
|
|
129
|
-
/>
|
|
130
|
-
));
|
|
131
|
-
setIsModalOpen(true);
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const handleError = (err) => {
|
|
135
|
-
if (err.status !== 401) {
|
|
136
|
-
setStockAlertResponseMessage(
|
|
137
|
-
t('product.stock_alert.error_description').toString()
|
|
138
|
-
);
|
|
139
|
-
setIsModalOpen(true);
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const checkoutProviderProps = {
|
|
144
|
-
product: data.product,
|
|
145
|
-
clearBasket: true,
|
|
146
|
-
addBeforeClick: variantsSelectionCheck,
|
|
147
|
-
openMiniBasket: false,
|
|
148
|
-
className: clsx([
|
|
149
|
-
'py-2.5',
|
|
150
|
-
'bg-black',
|
|
151
|
-
'relative',
|
|
152
|
-
'hover:bg-black',
|
|
153
|
-
'before:content-[""]',
|
|
154
|
-
'before:w-6',
|
|
155
|
-
'before:h-6',
|
|
156
|
-
'before:bg-white',
|
|
157
|
-
'before:absolute',
|
|
158
|
-
'before:rounded-r-[18px]',
|
|
159
|
-
'before:left-0',
|
|
160
|
-
'after:content-[""]',
|
|
161
|
-
'after:absolute',
|
|
162
|
-
'after:w-3',
|
|
163
|
-
'after:h-3',
|
|
164
|
-
'after:bg-[#d02c2f]',
|
|
165
|
-
'after:rounded-xl',
|
|
166
|
-
'after:left-1'
|
|
167
|
-
]),
|
|
168
|
-
onError: (error) =>
|
|
169
|
-
setProductError(
|
|
170
|
-
error?.data?.non_field_errors ||
|
|
171
|
-
Object.keys(error?.data).map(
|
|
172
|
-
(key) => `${key}: ${error?.data[key].join(', ')}`
|
|
173
|
-
)
|
|
174
|
-
)
|
|
68
|
+
const handleVariantChange = () => {
|
|
69
|
+
clearProductError();
|
|
70
|
+
setIsVariantLoading(true);
|
|
175
71
|
};
|
|
176
72
|
|
|
177
73
|
return (
|
|
@@ -187,72 +83,26 @@ export default function ProductInfo({ data }: ProductPageProps) {
|
|
|
187
83
|
retailPrice={data.product.retail_price}
|
|
188
84
|
/>
|
|
189
85
|
</div>
|
|
190
|
-
<div className="flex flex-col">
|
|
191
|
-
{data.variants.map((variant) => (
|
|
192
|
-
<Variant
|
|
193
|
-
key={variant.attribute_key}
|
|
194
|
-
{...variant}
|
|
195
|
-
className="items-center mt-8"
|
|
196
|
-
onChange={() => {
|
|
197
|
-
setProductError(null);
|
|
198
|
-
setIsVariantLoading(true);
|
|
199
|
-
}}
|
|
200
|
-
/>
|
|
201
|
-
))}
|
|
202
|
-
</div>
|
|
203
|
-
|
|
204
|
-
{productError && (
|
|
205
|
-
<div className="mt-4 text-xs text-center text-error">
|
|
206
|
-
{productError}
|
|
207
|
-
</div>
|
|
208
|
-
)}
|
|
209
86
|
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
className={clsx(
|
|
215
|
-
'fixed bottom-0 right-0 w-1/2 h-14 z-[20] flex items-center justify-center fill-primary-foreground',
|
|
216
|
-
'hover:fill-primary sm:relative sm:w-full sm:mt-3 sm:font-semibold sm:h-12'
|
|
217
|
-
)}
|
|
218
|
-
onClick={() => {
|
|
219
|
-
setProductError(null);
|
|
220
|
-
|
|
221
|
-
if (inStock) {
|
|
222
|
-
addProductToCart();
|
|
223
|
-
} else {
|
|
224
|
-
addProductToStockAlertList();
|
|
225
|
-
}
|
|
226
|
-
}}
|
|
227
|
-
data-testid="product-add-to-cart"
|
|
228
|
-
>
|
|
229
|
-
{isVariantLoading ? (
|
|
230
|
-
<Icon
|
|
231
|
-
name="spinner"
|
|
232
|
-
size={20}
|
|
233
|
-
className="animate-spin mr-4 fill-primary"
|
|
234
|
-
/>
|
|
235
|
-
) : inStock ? (
|
|
236
|
-
<span>{t('product.add_to_cart')}</span>
|
|
237
|
-
) : (
|
|
238
|
-
<>
|
|
239
|
-
<Icon name="bell" size={20} className="mr-4" />
|
|
240
|
-
<span>{t('product.add_stock_alert')}</span>
|
|
241
|
-
</>
|
|
242
|
-
)}
|
|
243
|
-
</Button>
|
|
244
|
-
|
|
245
|
-
<PluginModule
|
|
246
|
-
component={Component.AkifastCheckoutButton}
|
|
247
|
-
props={{
|
|
248
|
-
...checkoutProviderProps,
|
|
249
|
-
isPdp: true
|
|
250
|
-
}}
|
|
87
|
+
<ProductVariants
|
|
88
|
+
variants={data.variants}
|
|
89
|
+
onVariantChange={handleVariantChange}
|
|
251
90
|
/>
|
|
252
91
|
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
92
|
+
<ProductActions
|
|
93
|
+
product={data.product}
|
|
94
|
+
variants={data.variants}
|
|
95
|
+
inStock={inStock}
|
|
96
|
+
isVariantLoading={isVariantLoading}
|
|
97
|
+
onAddToCart={addProductToCart}
|
|
98
|
+
onAddToStockAlert={addProductToStockAlertList}
|
|
99
|
+
onClearError={clearProductError}
|
|
100
|
+
isAddToCartLoading={isAddToCartLoading}
|
|
101
|
+
isAddToStockAlertLoading={isAddToStockAlertLoading}
|
|
102
|
+
productError={productError}
|
|
103
|
+
isModalOpen={isModalOpen}
|
|
104
|
+
stockAlertResponseMessage={stockAlertResponseMessage}
|
|
105
|
+
onCloseModal={closeModal}
|
|
256
106
|
/>
|
|
257
107
|
|
|
258
108
|
<MiscButtons
|
|
@@ -261,58 +111,7 @@ export default function ProductInfo({ data }: ProductPageProps) {
|
|
|
261
111
|
variants={data.variants}
|
|
262
112
|
/>
|
|
263
113
|
|
|
264
|
-
<
|
|
265
|
-
className="my-2 sm:mb-4"
|
|
266
|
-
buttonText={t('product.share')}
|
|
267
|
-
items={[
|
|
268
|
-
{
|
|
269
|
-
href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
|
|
270
|
-
currentUrl
|
|
271
|
-
)}`,
|
|
272
|
-
iconName: 'facebook',
|
|
273
|
-
iconSize: 22
|
|
274
|
-
},
|
|
275
|
-
{
|
|
276
|
-
href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
|
277
|
-
currentUrl
|
|
278
|
-
)}`,
|
|
279
|
-
iconName: 'twitter',
|
|
280
|
-
iconSize: 22
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
href: `https://api.whatsapp.com/send?text=${
|
|
284
|
-
data.product.name
|
|
285
|
-
}%20${encodeURIComponent(currentUrl)}`,
|
|
286
|
-
iconName: 'whatsapp',
|
|
287
|
-
iconSize: 22
|
|
288
|
-
}
|
|
289
|
-
]}
|
|
290
|
-
/>
|
|
291
|
-
|
|
292
|
-
<Modal
|
|
293
|
-
portalId="stock-alert-modal"
|
|
294
|
-
open={isModalOpen}
|
|
295
|
-
setOpen={setIsModalOpen}
|
|
296
|
-
showCloseButton={false}
|
|
297
|
-
className="w-5/6 md:max-w-md"
|
|
298
|
-
>
|
|
299
|
-
<div className="flex flex-col items-center justify-center gap-4 px-6 py-9">
|
|
300
|
-
<Icon name="bell" size={48} />
|
|
301
|
-
<h2 className="text-xl font-semibold">
|
|
302
|
-
{t('product.stock_alert.title')}
|
|
303
|
-
</h2>
|
|
304
|
-
<div className="max-w-40 text-xs text-center leading-4">
|
|
305
|
-
<p>{stockAlertResponseMessage}</p>
|
|
306
|
-
</div>
|
|
307
|
-
<Button
|
|
308
|
-
onClick={handleModalClick}
|
|
309
|
-
appearance="outlined"
|
|
310
|
-
className="font-semibold px-10 h-12"
|
|
311
|
-
>
|
|
312
|
-
{t('product.stock_alert.close_button')}
|
|
313
|
-
</Button>
|
|
314
|
-
</div>
|
|
315
|
-
</Modal>
|
|
114
|
+
<ProductShare productName={data.product.name} className="my-2 sm:mb-4" />
|
|
316
115
|
</>
|
|
317
116
|
);
|
|
318
117
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import Share from '@theme/views/share';
|
|
3
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
4
|
+
|
|
5
|
+
interface ProductShareProps {
|
|
6
|
+
productName: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ProductShare: React.FC<ProductShareProps> = ({
|
|
11
|
+
productName,
|
|
12
|
+
className
|
|
13
|
+
}) => {
|
|
14
|
+
const { t } = useLocalization();
|
|
15
|
+
const [currentUrl, setCurrentUrl] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
setCurrentUrl(window.location.href);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
if (!currentUrl) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const shareItems = [
|
|
26
|
+
{
|
|
27
|
+
href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
|
|
28
|
+
currentUrl
|
|
29
|
+
)}`,
|
|
30
|
+
iconName: 'facebook' as const,
|
|
31
|
+
iconSize: 22
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
href: `https://twitter.com/intent/tweet?text=${encodeURIComponent(
|
|
35
|
+
currentUrl
|
|
36
|
+
)}`,
|
|
37
|
+
iconName: 'twitter' as const,
|
|
38
|
+
iconSize: 22
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
href: `https://api.whatsapp.com/send?text=${productName}%20${encodeURIComponent(
|
|
42
|
+
currentUrl
|
|
43
|
+
)}`,
|
|
44
|
+
iconName: 'whatsapp' as const,
|
|
45
|
+
iconSize: 22
|
|
46
|
+
}
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Share
|
|
51
|
+
className={className}
|
|
52
|
+
buttonText={t('product.share')}
|
|
53
|
+
items={shareItems}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Variant } from '@theme/views/product';
|
|
3
|
+
import { VariantType } from '@akinon/next/types';
|
|
4
|
+
|
|
5
|
+
interface ProductVariantsProps {
|
|
6
|
+
variants: VariantType[];
|
|
7
|
+
onVariantChange: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const ProductVariants: React.FC<ProductVariantsProps> = ({
|
|
11
|
+
variants,
|
|
12
|
+
onVariantChange
|
|
13
|
+
}) => {
|
|
14
|
+
return (
|
|
15
|
+
<div className="flex flex-col">
|
|
16
|
+
{variants.map((variant) => (
|
|
17
|
+
<Variant
|
|
18
|
+
key={variant.attribute_key}
|
|
19
|
+
{...variant}
|
|
20
|
+
className="items-center mt-8"
|
|
21
|
+
onChange={onVariantChange}
|
|
22
|
+
/>
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -7,6 +7,7 @@ import { Product } from '@akinon/next/types';
|
|
|
7
7
|
import { Image } from '@akinon/next/components/image';
|
|
8
8
|
import useFavButton from '../../hooks/use-fav-button';
|
|
9
9
|
import { twMerge } from 'tailwind-merge';
|
|
10
|
+
import PluginModule, { Component } from '@akinon/next/components/plugin-module';
|
|
10
11
|
|
|
11
12
|
type ProductSliderItem = {
|
|
12
13
|
product: Product;
|
|
@@ -35,90 +36,102 @@ export default function ProductInfoSlider({ product }: ProductSliderItem) {
|
|
|
35
36
|
carouselRef.current?.next();
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
const handleThumbnailClick = (index) => {
|
|
39
|
+
const handleThumbnailClick = (index: number) => {
|
|
39
40
|
setActiveIndex(index);
|
|
40
41
|
carouselRef.current?.goToSlide(index);
|
|
41
42
|
};
|
|
42
43
|
|
|
43
44
|
return (
|
|
44
|
-
|
|
45
|
-
<div className="lg:
|
|
46
|
-
<div className="
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
<>
|
|
46
|
+
<div className="lg:grid lg:grid-cols-6">
|
|
47
|
+
<div className="lg:col-span-1">
|
|
48
|
+
<div className="flex flex-col items-center justify-center md:mr-[6px]">
|
|
49
|
+
<button
|
|
50
|
+
onClick={goToPrev}
|
|
51
|
+
className={twMerge(
|
|
52
|
+
'hidden justify-center p-2 mb-3 border border-gray-100 rounded-full cursor-pointer lg:block',
|
|
53
|
+
[activeIndex === 0 && 'cursor-not-allowed opacity-45']
|
|
54
|
+
)}
|
|
55
|
+
disabled={activeIndex === 0}
|
|
56
|
+
>
|
|
57
|
+
<Icon name="chevron-up" size={15} className="fill-[#000000]" />
|
|
58
|
+
</button>
|
|
59
|
+
<div className="hidden flex-col items-center overflow-scroll w-[80px] max-h-[620px] lg:block">
|
|
60
|
+
{product?.productimage_set?.map((item, index) => (
|
|
61
|
+
<Image
|
|
62
|
+
key={index}
|
|
63
|
+
src={item.image}
|
|
64
|
+
alt={`Thumbnail ${index}`}
|
|
65
|
+
width={80}
|
|
66
|
+
height={128}
|
|
67
|
+
aspectRatio={80 / 128}
|
|
68
|
+
className={twMerge('cursor-pointer', [
|
|
69
|
+
activeIndex === index && 'border-2 border-primary'
|
|
70
|
+
])}
|
|
71
|
+
onClick={() => handleThumbnailClick(index)}
|
|
72
|
+
/>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
<button
|
|
76
|
+
onClick={goToNext}
|
|
77
|
+
className={twMerge(
|
|
78
|
+
'hidden justify-center p-2 mt-3 border border-gray-100 rounded-full cursor-pointer lg:block',
|
|
79
|
+
[
|
|
80
|
+
activeIndex === product.productimage_set.length - 1 &&
|
|
81
|
+
'cursor-not-allowed opacity-45'
|
|
82
|
+
]
|
|
83
|
+
)}
|
|
84
|
+
disabled={activeIndex === product.productimage_set.length - 1}
|
|
85
|
+
>
|
|
86
|
+
<Icon name="chevron-down" size={15} className="fill-[#000000]" />
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="relative lg:col-span-5">
|
|
92
|
+
<FavButton className="absolute right-8 top-6 z-[20] sm:hidden" />
|
|
93
|
+
|
|
94
|
+
<PluginModule
|
|
95
|
+
component={Component.ProductImageSearchFeature}
|
|
96
|
+
props={{
|
|
97
|
+
product,
|
|
98
|
+
activeIndex,
|
|
99
|
+
showResetButton: true
|
|
100
|
+
}}
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
<CarouselCore
|
|
104
|
+
responsive={{
|
|
105
|
+
all: {
|
|
106
|
+
breakpoint: { max: 5000, min: 0 },
|
|
107
|
+
items: 1
|
|
108
|
+
}
|
|
109
|
+
}}
|
|
110
|
+
arrows={false}
|
|
111
|
+
swipeable={true}
|
|
112
|
+
ref={carouselRef}
|
|
113
|
+
afterChange={(previousSlide, { currentSlide }) => {
|
|
114
|
+
setActiveIndex(currentSlide);
|
|
115
|
+
}}
|
|
116
|
+
containerAspectRatio={{ mobile: 520 / 798, desktop: 484 / 726 }}
|
|
54
117
|
>
|
|
55
|
-
|
|
56
|
-
</button>
|
|
57
|
-
<div className="hidden flex-col items-center overflow-scroll w-[80px] max-h-[620px] lg:block">
|
|
58
|
-
{product?.productimage_set?.map((item, index) => (
|
|
118
|
+
{product?.productimage_set?.map((item, i) => (
|
|
59
119
|
<Image
|
|
60
|
-
key={
|
|
120
|
+
key={i}
|
|
61
121
|
src={item.image}
|
|
62
|
-
alt={
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
122
|
+
alt={product?.name || 'Product image'}
|
|
123
|
+
draggable={false}
|
|
124
|
+
aspectRatio={484 / 726}
|
|
125
|
+
sizes="(min-width: 425px) 512px,
|
|
126
|
+
(min-width: 601px) 576px,
|
|
127
|
+
(min-width: 768px) 336px,
|
|
128
|
+
(min-width: 1024px) 484px, 368px"
|
|
129
|
+
fill
|
|
69
130
|
/>
|
|
70
131
|
))}
|
|
71
|
-
</
|
|
72
|
-
<button
|
|
73
|
-
onClick={goToNext}
|
|
74
|
-
className={twMerge(
|
|
75
|
-
'hidden justify-center p-2 mt-3 border border-gray-100 rounded-full cursor-pointer lg:block',
|
|
76
|
-
[
|
|
77
|
-
activeIndex === product.productimage_set.length - 1 &&
|
|
78
|
-
'cursor-not-allowed opacity-45'
|
|
79
|
-
]
|
|
80
|
-
)}
|
|
81
|
-
disabled={activeIndex === product.productimage_set.length - 1}
|
|
82
|
-
>
|
|
83
|
-
<Icon name="chevron-down" size={15} className="fill-[#000000]" />
|
|
84
|
-
</button>
|
|
132
|
+
</CarouselCore>
|
|
85
133
|
</div>
|
|
86
134
|
</div>
|
|
87
|
-
|
|
88
|
-
<div className="relative lg:col-span-5">
|
|
89
|
-
<FavButton className="absolute right-8 top-6 z-[20] sm:hidden" />
|
|
90
|
-
|
|
91
|
-
<CarouselCore
|
|
92
|
-
responsive={{
|
|
93
|
-
all: {
|
|
94
|
-
breakpoint: { max: 5000, min: 0 },
|
|
95
|
-
items: 1
|
|
96
|
-
}
|
|
97
|
-
}}
|
|
98
|
-
arrows={false}
|
|
99
|
-
swipeable={true}
|
|
100
|
-
ref={carouselRef}
|
|
101
|
-
afterChange={(previousSlide, { currentSlide }) => {
|
|
102
|
-
setActiveIndex(currentSlide);
|
|
103
|
-
}}
|
|
104
|
-
containerAspectRatio={{ mobile: 520 / 798, desktop: 484 / 726 }}
|
|
105
|
-
>
|
|
106
|
-
{product?.productimage_set?.map((item, i) => (
|
|
107
|
-
<Image
|
|
108
|
-
key={i}
|
|
109
|
-
src={item.image}
|
|
110
|
-
alt={product.name}
|
|
111
|
-
draggable={false}
|
|
112
|
-
aspectRatio={484 / 726}
|
|
113
|
-
sizes="(min-width: 425px) 512px,
|
|
114
|
-
(min-width: 601px) 576px,
|
|
115
|
-
(min-width: 768px) 336px,
|
|
116
|
-
(min-width: 1024px) 484px, 368px"
|
|
117
|
-
fill
|
|
118
|
-
/>
|
|
119
|
-
))}
|
|
120
|
-
</CarouselCore>
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
135
|
+
</>
|
|
123
136
|
);
|
|
124
137
|
}
|
|
@@ -2,6 +2,7 @@ import 'server-only';
|
|
|
2
2
|
|
|
3
3
|
import { Link, Accordion } from '@theme/components';
|
|
4
4
|
import { getWidgetData } from '@akinon/next/data/server';
|
|
5
|
+
import { ServerVariables } from '@akinon/next/utils/server-variables';
|
|
5
6
|
|
|
6
7
|
type SideItem = {
|
|
7
8
|
value: string;
|
|
@@ -47,6 +48,7 @@ type FooterMenuType = {
|
|
|
47
48
|
|
|
48
49
|
export default async function FooterMenu() {
|
|
49
50
|
const data = await getWidgetData<FooterMenuType>({ slug: 'footer-menu' });
|
|
51
|
+
const { locale } = ServerVariables;
|
|
50
52
|
|
|
51
53
|
return (
|
|
52
54
|
<div className="flex-1">
|
|
@@ -72,7 +74,7 @@ export default async function FooterMenu() {
|
|
|
72
74
|
: '_self'
|
|
73
75
|
}
|
|
74
76
|
data-testid={`footer-categories-${item?.value?.name
|
|
75
|
-
?.toLocaleLowerCase()
|
|
77
|
+
?.toLocaleLowerCase(locale)
|
|
76
78
|
.split(' ')
|
|
77
79
|
.join('')}`}
|
|
78
80
|
>
|
|
@@ -96,7 +98,9 @@ export default async function FooterMenu() {
|
|
|
96
98
|
? '_blank'
|
|
97
99
|
: '_self'
|
|
98
100
|
}
|
|
99
|
-
data-testid={`footer-categories-${item?.value?.name?.toLocaleLowerCase(
|
|
101
|
+
data-testid={`footer-categories-${item?.value?.name?.toLocaleLowerCase(
|
|
102
|
+
locale
|
|
103
|
+
)}`}
|
|
100
104
|
>
|
|
101
105
|
{item?.value?.name}
|
|
102
106
|
</Link>
|