@akinon/next 1.99.0-snapshot-ZERO-3640-20250919140935 → 1.100.0-rc.71
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 +1337 -51
- package/__tests__/next-config.test.ts +1 -10
- package/__tests__/redirect.test.ts +319 -0
- package/api/cache.ts +41 -5
- package/api/form.ts +84 -0
- package/api/image-proxy.ts +75 -0
- package/api/similar-product-list.ts +84 -0
- package/api/similar-products.ts +120 -0
- package/bin/pz-generate-routes.js +105 -0
- package/bin/pz-prebuild.js +1 -0
- package/bin/pz-predev.js +1 -0
- package/components/accordion.tsx +20 -5
- package/components/file-input.tsx +65 -3
- package/components/input.tsx +2 -0
- package/components/link.tsx +16 -12
- package/components/modal.tsx +32 -16
- package/components/plugin-module.tsx +32 -4
- package/data/client/checkout.ts +4 -2
- package/data/server/basket.ts +72 -0
- package/data/server/category.ts +48 -26
- package/data/server/flatpage.ts +20 -13
- package/data/server/form.ts +4 -1
- package/data/server/landingpage.ts +20 -13
- package/data/server/list.ts +25 -14
- package/data/server/menu.ts +4 -1
- package/data/server/product.ts +68 -40
- package/data/server/seo.ts +4 -1
- package/data/server/special-page.ts +18 -13
- package/data/server/widget.ts +4 -1
- package/data/urls.ts +6 -2
- package/hocs/server/with-segment-defaults.tsx +5 -2
- package/hooks/use-localization.ts +2 -3
- package/jest.config.js +7 -1
- package/lib/cache-handler.mjs +365 -87
- package/lib/cache.ts +254 -25
- package/middlewares/complete-gpay.ts +2 -1
- package/middlewares/complete-masterpass.ts +2 -1
- package/middlewares/default.ts +50 -13
- package/middlewares/locale.ts +9 -1
- package/middlewares/pretty-url.ts +21 -6
- package/middlewares/redirection-payment.ts +2 -1
- package/middlewares/saved-card-redirection.ts +2 -1
- package/middlewares/three-d-redirection.ts +2 -2
- package/middlewares/url-redirection.ts +8 -14
- package/package.json +4 -3
- package/plugins.d.ts +8 -5
- package/plugins.js +3 -1
- package/redux/middlewares/checkout.ts +5 -1
- package/types/commerce/order.ts +1 -0
- package/types/index.ts +35 -1
- package/utils/app-fetch.ts +7 -2
- package/utils/redirect-ignore.ts +35 -0
- package/utils/redirect.ts +31 -6
- package/with-pz-config.js +1 -5
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import Settings from 'settings';
|
|
3
|
+
|
|
4
|
+
const IMAGE_SEARCH_API_URL = Settings.commerceUrl + '/image-search/';
|
|
5
|
+
|
|
6
|
+
const errorResponse = (message: string, status: number) => {
|
|
7
|
+
return NextResponse.json({ error: message }, { status });
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export async function GET(request: Request) {
|
|
11
|
+
const { searchParams } = new URL(request.url);
|
|
12
|
+
const limit = searchParams.get('limit') || '20';
|
|
13
|
+
const url = searchParams.get('url');
|
|
14
|
+
const excludedProductIds = searchParams.get('excluded_product_ids');
|
|
15
|
+
const text = searchParams.get('text');
|
|
16
|
+
|
|
17
|
+
if (!url) {
|
|
18
|
+
return errorResponse('URL parameter is required', 400);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (Settings.commerceUrl === 'default') {
|
|
22
|
+
return errorResponse('Commerce URL is not configured', 500);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const apiParams = new URLSearchParams();
|
|
26
|
+
apiParams.append('limit', limit);
|
|
27
|
+
apiParams.append('url', url);
|
|
28
|
+
|
|
29
|
+
if (excludedProductIds) {
|
|
30
|
+
apiParams.append('excluded_product_ids', excludedProductIds);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (text) {
|
|
34
|
+
apiParams.append('text', text);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const apiUrl = `${IMAGE_SEARCH_API_URL}?${apiParams.toString()}`;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(apiUrl, {
|
|
41
|
+
method: 'GET',
|
|
42
|
+
headers: {
|
|
43
|
+
Accept: 'application/json'
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const errorText = await response.text();
|
|
49
|
+
return errorResponse(
|
|
50
|
+
errorText || `API request failed with status: ${response.status}`,
|
|
51
|
+
response.status
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const responseText = await response.text();
|
|
56
|
+
|
|
57
|
+
return NextResponse.json(JSON.parse(responseText));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return errorResponse((error as Error).message, 500);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function POST(request: Request) {
|
|
64
|
+
const { searchParams } = new URL(request.url);
|
|
65
|
+
const limit = searchParams.get('limit') || '20';
|
|
66
|
+
|
|
67
|
+
if (Settings.commerceUrl === 'default') {
|
|
68
|
+
return errorResponse('Commerce URL is not configured', 500);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let requestBody;
|
|
72
|
+
try {
|
|
73
|
+
requestBody = await request.json();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return errorResponse('Invalid JSON in request body', 400);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!requestBody.image) {
|
|
79
|
+
return errorResponse('Image data is required in request body', 400);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const apiParams = new URLSearchParams();
|
|
83
|
+
apiParams.append('limit', limit);
|
|
84
|
+
|
|
85
|
+
const apiUrl = `${IMAGE_SEARCH_API_URL}?${apiParams.toString()}`;
|
|
86
|
+
|
|
87
|
+
const bodyData: any = { image: requestBody.image };
|
|
88
|
+
|
|
89
|
+
if (requestBody.excluded_product_ids) {
|
|
90
|
+
bodyData.excluded_product_ids = requestBody.excluded_product_ids;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (requestBody.text) {
|
|
94
|
+
bodyData.text = requestBody.text;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(apiUrl, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: {
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
Accept: 'application/json'
|
|
103
|
+
},
|
|
104
|
+
body: JSON.stringify(bodyData)
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
const errorText = await response.text();
|
|
109
|
+
return errorResponse(
|
|
110
|
+
errorText || `API request failed with status: ${response.status}`,
|
|
111
|
+
response.status
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const responseData = await response.json();
|
|
116
|
+
return NextResponse.json(responseData);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return errorResponse((error as Error).message, 500);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const findBaseDir = require('../utils/find-base-dir');
|
|
6
|
+
|
|
7
|
+
const generateRoutes = () => {
|
|
8
|
+
const baseDir = findBaseDir();
|
|
9
|
+
const srcDir = path.join(baseDir, 'src');
|
|
10
|
+
const appDir = path.join(srcDir, 'app');
|
|
11
|
+
|
|
12
|
+
const routesDir = path.join(srcDir, 'routes');
|
|
13
|
+
const jsOutputPath = path.join(routesDir, 'generated-routes.js');
|
|
14
|
+
|
|
15
|
+
if (!fs.existsSync(routesDir)) {
|
|
16
|
+
fs.mkdirSync(routesDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(appDir)) {
|
|
20
|
+
console.error(`Error: app directory not found at ${appDir}`);
|
|
21
|
+
console.error('Make sure you have an app directory in your project');
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const routes = [];
|
|
26
|
+
const excludedDirs = ['api', 'pz-not-found'];
|
|
27
|
+
|
|
28
|
+
const skipSegments = ['[commerce]', '[locale]', '[currency]'];
|
|
29
|
+
const skipCatchAllRoutes = ['[...prettyurl]'];
|
|
30
|
+
|
|
31
|
+
const walkDirectory = (dir, basePath = '') => {
|
|
32
|
+
const files = fs.readdirSync(dir);
|
|
33
|
+
|
|
34
|
+
files.forEach((file) => {
|
|
35
|
+
const filePath = path.join(dir, file);
|
|
36
|
+
const stat = fs.statSync(filePath);
|
|
37
|
+
|
|
38
|
+
if (stat.isDirectory()) {
|
|
39
|
+
if (excludedDirs.includes(file)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (skipCatchAllRoutes.includes(file.toLowerCase())) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let routePath = basePath;
|
|
48
|
+
if (skipSegments.includes(file.toLowerCase())) {
|
|
49
|
+
routePath = basePath;
|
|
50
|
+
} else if (file.startsWith('[') && file.endsWith(']')) {
|
|
51
|
+
if (file.startsWith('[...')) {
|
|
52
|
+
routePath = `${basePath}/.+`;
|
|
53
|
+
} else {
|
|
54
|
+
routePath = `${basePath}/[^/]+`;
|
|
55
|
+
}
|
|
56
|
+
} else if (file.startsWith('(') && file.endsWith(')')) {
|
|
57
|
+
routePath = basePath;
|
|
58
|
+
} else {
|
|
59
|
+
routePath = `${basePath}/${file}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const pageFiles = ['page.tsx'];
|
|
63
|
+
const hasPage = pageFiles.some((pageFile) =>
|
|
64
|
+
fs.existsSync(path.join(filePath, pageFile))
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (hasPage && routePath) {
|
|
68
|
+
routes.push(routePath === '' ? '/' : routePath);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
walkDirectory(filePath, routePath);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
walkDirectory(appDir);
|
|
77
|
+
|
|
78
|
+
routes.push('/');
|
|
79
|
+
|
|
80
|
+
const cleanedRoutes = routes
|
|
81
|
+
.map((route) => {
|
|
82
|
+
return route.replace(/\/+/g, '/');
|
|
83
|
+
})
|
|
84
|
+
.filter((route, index, self) => {
|
|
85
|
+
return self.indexOf(route) === index;
|
|
86
|
+
})
|
|
87
|
+
.sort();
|
|
88
|
+
|
|
89
|
+
const jsContent = `// Generated by pz-generate-routes script
|
|
90
|
+
// Do not edit manually
|
|
91
|
+
module.exports = ${JSON.stringify(cleanedRoutes, null, 2)};`;
|
|
92
|
+
|
|
93
|
+
fs.writeFileSync(jsOutputPath, jsContent);
|
|
94
|
+
|
|
95
|
+
console.info(
|
|
96
|
+
`Routes generated successfully. (Found ${cleanedRoutes.length} routes)`
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
generateRoutes();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Error generating routes:', error);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
package/bin/pz-prebuild.js
CHANGED
package/bin/pz-predev.js
CHANGED
package/components/accordion.tsx
CHANGED
|
@@ -7,15 +7,19 @@ import { AccordionProps } from '../types';
|
|
|
7
7
|
|
|
8
8
|
export const Accordion = ({
|
|
9
9
|
isCollapse = false,
|
|
10
|
+
collapseClassName,
|
|
10
11
|
title,
|
|
11
12
|
subTitle,
|
|
12
13
|
icons = ['chevron-up', 'chevron-down'],
|
|
13
14
|
iconSize = 16,
|
|
14
15
|
iconColor = 'fill-[#000000]',
|
|
15
16
|
children,
|
|
17
|
+
headerClassName,
|
|
16
18
|
className,
|
|
17
19
|
titleClassName,
|
|
18
|
-
|
|
20
|
+
subTitleClassName,
|
|
21
|
+
dataTestId,
|
|
22
|
+
contentClassName
|
|
19
23
|
}: AccordionProps) => {
|
|
20
24
|
const [collapse, setCollapse] = useState(isCollapse);
|
|
21
25
|
|
|
@@ -27,15 +31,22 @@ export const Accordion = ({
|
|
|
27
31
|
)}
|
|
28
32
|
>
|
|
29
33
|
<div
|
|
30
|
-
className=
|
|
34
|
+
className={twMerge(
|
|
35
|
+
'flex items-center justify-between cursor-pointer',
|
|
36
|
+
headerClassName
|
|
37
|
+
)}
|
|
31
38
|
onClick={() => setCollapse(!collapse)}
|
|
32
39
|
data-testid={dataTestId}
|
|
33
40
|
>
|
|
34
|
-
<div className=
|
|
41
|
+
<div className={twMerge('flex flex-col', contentClassName)}>
|
|
35
42
|
{title && (
|
|
36
43
|
<h3 className={twMerge('text-sm', titleClassName)}>{title}</h3>
|
|
37
44
|
)}
|
|
38
|
-
{subTitle &&
|
|
45
|
+
{subTitle && (
|
|
46
|
+
<h4 className={twMerge('text-xs text-gray-700', subTitleClassName)}>
|
|
47
|
+
{subTitle}
|
|
48
|
+
</h4>
|
|
49
|
+
)}
|
|
39
50
|
</div>
|
|
40
51
|
|
|
41
52
|
{icons && (
|
|
@@ -46,7 +57,11 @@ export const Accordion = ({
|
|
|
46
57
|
/>
|
|
47
58
|
)}
|
|
48
59
|
</div>
|
|
49
|
-
{collapse &&
|
|
60
|
+
{collapse && (
|
|
61
|
+
<div className={twMerge('mt-3 text-sm', collapseClassName)}>
|
|
62
|
+
{children}
|
|
63
|
+
</div>
|
|
64
|
+
)}
|
|
50
65
|
</div>
|
|
51
66
|
);
|
|
52
67
|
};
|
|
@@ -1,8 +1,70 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
1
2
|
import { forwardRef } from 'react';
|
|
2
|
-
import {
|
|
3
|
+
import { useLocalization } from '@akinon/next/hooks';
|
|
4
|
+
import { twMerge } from 'tailwind-merge';
|
|
5
|
+
import { FileInputProps } from '../types';
|
|
3
6
|
|
|
4
7
|
export const FileInput = forwardRef<HTMLInputElement, FileInputProps>(
|
|
5
|
-
function
|
|
6
|
-
|
|
8
|
+
function FileInput(
|
|
9
|
+
{
|
|
10
|
+
buttonClassName,
|
|
11
|
+
onChange,
|
|
12
|
+
fileClassName,
|
|
13
|
+
fileNameWrapperClassName,
|
|
14
|
+
fileInputClassName,
|
|
15
|
+
...props
|
|
16
|
+
},
|
|
17
|
+
ref
|
|
18
|
+
) {
|
|
19
|
+
const { t } = useLocalization();
|
|
20
|
+
const [fileNames, setFileNames] = useState<string[]>([]);
|
|
21
|
+
|
|
22
|
+
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
23
|
+
const files = Array.from(event.target.files || []);
|
|
24
|
+
setFileNames(files.map((file) => file.name));
|
|
25
|
+
|
|
26
|
+
if (onChange) {
|
|
27
|
+
onChange(event);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="relative">
|
|
33
|
+
<input
|
|
34
|
+
type="file"
|
|
35
|
+
{...props}
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={twMerge(
|
|
38
|
+
'absolute inset-0 w-full h-full opacity-0 cursor-pointer',
|
|
39
|
+
fileInputClassName
|
|
40
|
+
)}
|
|
41
|
+
onChange={handleFileChange}
|
|
42
|
+
/>
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
className={twMerge(
|
|
46
|
+
'bg-primary text-white py-2 px-4 text-sm',
|
|
47
|
+
buttonClassName
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
{t('common.file_input.select_file')}
|
|
51
|
+
</button>
|
|
52
|
+
<div
|
|
53
|
+
className={twMerge('mt-1 text-gray-500', fileNameWrapperClassName)}
|
|
54
|
+
>
|
|
55
|
+
{fileNames.length > 0 ? (
|
|
56
|
+
<ul className={twMerge('list-disc pl-4 text-xs', fileClassName)}>
|
|
57
|
+
{fileNames.map((name, index) => (
|
|
58
|
+
<li key={index}>{name}</li>
|
|
59
|
+
))}
|
|
60
|
+
</ul>
|
|
61
|
+
) : (
|
|
62
|
+
<span className={twMerge('text-xs', fileClassName)}>
|
|
63
|
+
{t('common.file_input.no_file')}
|
|
64
|
+
</span>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
7
69
|
}
|
|
8
70
|
);
|
package/components/input.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import { forwardRef, FocusEvent, useState, Ref } from 'react';
|
|
3
3
|
import { Controller } from 'react-hook-form';
|
|
4
|
+
|
|
5
|
+
// @ts-ignore
|
|
4
6
|
import { PatternFormat, PatternFormatProps } from 'react-number-format';
|
|
5
7
|
import { InputProps } from '../types';
|
|
6
8
|
import { twMerge } from 'tailwind-merge';
|
package/components/link.tsx
CHANGED
|
@@ -10,7 +10,9 @@ type LinkProps = Omit<
|
|
|
10
10
|
React.AnchorHTMLAttributes<HTMLAnchorElement>,
|
|
11
11
|
keyof NextLinkProps
|
|
12
12
|
> &
|
|
13
|
-
NextLinkProps
|
|
13
|
+
NextLinkProps & {
|
|
14
|
+
href: string;
|
|
15
|
+
};
|
|
14
16
|
|
|
15
17
|
export const Link = ({ children, href, ...rest }: LinkProps) => {
|
|
16
18
|
const { locale, defaultLocaleValue, localeUrlStrategy } = useLocalization();
|
|
@@ -26,19 +28,21 @@ export const Link = ({ children, href, ...rest }: LinkProps) => {
|
|
|
26
28
|
return href;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
if (typeof href === 'string' && !href.startsWith('http')) {
|
|
32
|
+
const pathnameWithoutLocale = href.replace(urlLocaleMatcherRegex, '');
|
|
33
|
+
const hrefWithLocale = `/${locale}${pathnameWithoutLocale}`;
|
|
34
|
+
|
|
35
|
+
if (localeUrlStrategy === LocaleUrlStrategy.ShowAllLocales) {
|
|
36
|
+
return hrefWithLocale;
|
|
37
|
+
} else if (
|
|
38
|
+
localeUrlStrategy === LocaleUrlStrategy.HideDefaultLocale &&
|
|
39
|
+
locale !== defaultLocaleValue
|
|
40
|
+
) {
|
|
41
|
+
return hrefWithLocale;
|
|
42
|
+
}
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
return href
|
|
45
|
+
return href;
|
|
42
46
|
}, [href, defaultLocaleValue, locale, localeUrlStrategy]);
|
|
43
47
|
|
|
44
48
|
return (
|
package/components/modal.tsx
CHANGED
|
@@ -4,16 +4,7 @@ import { ReactPortal } from './react-portal';
|
|
|
4
4
|
import { Icon } from './icon';
|
|
5
5
|
import { twMerge } from 'tailwind-merge';
|
|
6
6
|
import { useEffect } from 'react';
|
|
7
|
-
|
|
8
|
-
export interface ModalProps {
|
|
9
|
-
portalId: string;
|
|
10
|
-
children?: React.ReactNode;
|
|
11
|
-
open?: boolean;
|
|
12
|
-
setOpen?: (open: boolean) => void;
|
|
13
|
-
title?: React.ReactNode;
|
|
14
|
-
showCloseButton?: React.ReactNode;
|
|
15
|
-
className?: string;
|
|
16
|
-
}
|
|
7
|
+
import { ModalProps } from '../types';
|
|
17
8
|
|
|
18
9
|
export const Modal = (props: ModalProps) => {
|
|
19
10
|
const {
|
|
@@ -23,7 +14,14 @@ export const Modal = (props: ModalProps) => {
|
|
|
23
14
|
setOpen,
|
|
24
15
|
title = '',
|
|
25
16
|
showCloseButton = true,
|
|
26
|
-
className
|
|
17
|
+
className,
|
|
18
|
+
overlayClassName,
|
|
19
|
+
headerWrapperClassName,
|
|
20
|
+
titleClassName,
|
|
21
|
+
closeButtonClassName,
|
|
22
|
+
iconName = 'close',
|
|
23
|
+
iconSize = 16,
|
|
24
|
+
iconClassName
|
|
27
25
|
} = props;
|
|
28
26
|
|
|
29
27
|
useEffect(() => {
|
|
@@ -38,7 +36,12 @@ export const Modal = (props: ModalProps) => {
|
|
|
38
36
|
|
|
39
37
|
return (
|
|
40
38
|
<ReactPortal wrapperId={portalId}>
|
|
41
|
-
<div
|
|
39
|
+
<div
|
|
40
|
+
className={twMerge(
|
|
41
|
+
'fixed top-0 left-0 w-screen h-screen bg-primary bg-opacity-60 z-50',
|
|
42
|
+
overlayClassName
|
|
43
|
+
)}
|
|
44
|
+
/>
|
|
42
45
|
<section
|
|
43
46
|
className={twMerge(
|
|
44
47
|
'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-50 bg-white',
|
|
@@ -46,15 +49,28 @@ export const Modal = (props: ModalProps) => {
|
|
|
46
49
|
)}
|
|
47
50
|
>
|
|
48
51
|
{(showCloseButton || title) && (
|
|
49
|
-
<div
|
|
50
|
-
|
|
52
|
+
<div
|
|
53
|
+
className={twMerge(
|
|
54
|
+
'flex px-6 py-4 border-b border-gray-400',
|
|
55
|
+
headerWrapperClassName
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{title && (
|
|
59
|
+
<h3 className={twMerge('text-lg font-light', titleClassName)}>
|
|
60
|
+
{title}
|
|
61
|
+
</h3>
|
|
62
|
+
)}
|
|
51
63
|
{showCloseButton && (
|
|
52
64
|
<button
|
|
53
65
|
type="button"
|
|
54
66
|
onClick={() => setOpen(false)}
|
|
55
|
-
className=
|
|
67
|
+
className={twMerge('ml-auto', closeButtonClassName)}
|
|
56
68
|
>
|
|
57
|
-
<Icon
|
|
69
|
+
<Icon
|
|
70
|
+
name={iconName}
|
|
71
|
+
size={iconSize}
|
|
72
|
+
className={iconClassName}
|
|
73
|
+
/>
|
|
58
74
|
</button>
|
|
59
75
|
)}
|
|
60
76
|
</div>
|
|
@@ -21,7 +21,9 @@ enum Plugin {
|
|
|
21
21
|
Akifast = 'pz-akifast',
|
|
22
22
|
MultiBasket = 'pz-multi-basket',
|
|
23
23
|
SavedCard = 'pz-saved-card',
|
|
24
|
-
|
|
24
|
+
Hepsipay = 'pz-hepsipay',
|
|
25
|
+
FlowPayment = 'pz-flow-payment',
|
|
26
|
+
SimilarProducts = 'pz-similar-products'
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export enum Component {
|
|
@@ -47,7 +49,16 @@ export enum Component {
|
|
|
47
49
|
AkifastCheckoutButton = 'CheckoutButton',
|
|
48
50
|
MultiBasket = 'MultiBasket',
|
|
49
51
|
SavedCard = 'SavedCardOption',
|
|
50
|
-
|
|
52
|
+
Hepsipay = 'Hepsipay',
|
|
53
|
+
FlowPayment = 'FlowPayment',
|
|
54
|
+
SimilarProductsModal = 'SimilarProductsModal',
|
|
55
|
+
SimilarProductsFilterSidebar = 'SimilarProductsFilterSidebar',
|
|
56
|
+
SimilarProductsResultsGrid = 'SimilarProductsResultsGrid',
|
|
57
|
+
SimilarProductsPlugin = 'SimilarProductsPlugin',
|
|
58
|
+
ProductImageSearchFeature = 'ProductImageSearchFeature',
|
|
59
|
+
ImageSearchButton = 'ImageSearchButton',
|
|
60
|
+
HeaderImageSearchFeature = 'HeaderImageSearchFeature',
|
|
61
|
+
IyzicoSavedCard = 'IyzicoSavedCardOption'
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
const PluginComponents = new Map([
|
|
@@ -80,8 +91,21 @@ const PluginComponents = new Map([
|
|
|
80
91
|
[Component.AkifastQuickLoginButton, Component.AkifastCheckoutButton]
|
|
81
92
|
],
|
|
82
93
|
[Plugin.MultiBasket, [Component.MultiBasket]],
|
|
83
|
-
[Plugin.SavedCard, [Component.SavedCard]],
|
|
84
|
-
[Plugin.
|
|
94
|
+
[Plugin.SavedCard, [Component.SavedCard, Component.IyzicoSavedCard]],
|
|
95
|
+
[Plugin.Hepsipay, [Component.Hepsipay]],
|
|
96
|
+
[Plugin.FlowPayment, [Component.FlowPayment]],
|
|
97
|
+
[
|
|
98
|
+
Plugin.SimilarProducts,
|
|
99
|
+
[
|
|
100
|
+
Component.SimilarProductsModal,
|
|
101
|
+
Component.SimilarProductsFilterSidebar,
|
|
102
|
+
Component.SimilarProductsResultsGrid,
|
|
103
|
+
Component.SimilarProductsPlugin,
|
|
104
|
+
Component.ProductImageSearchFeature,
|
|
105
|
+
Component.ImageSearchButton,
|
|
106
|
+
Component.HeaderImageSearchFeature
|
|
107
|
+
]
|
|
108
|
+
]
|
|
85
109
|
]);
|
|
86
110
|
|
|
87
111
|
const getPlugin = (component: Component) => {
|
|
@@ -146,8 +170,12 @@ export default function PluginModule({
|
|
|
146
170
|
promise = import(`${'@akinon/pz-multi-basket'}`);
|
|
147
171
|
} else if (plugin === Plugin.SavedCard) {
|
|
148
172
|
promise = import(`${'@akinon/pz-saved-card'}`);
|
|
173
|
+
} else if (plugin === Plugin.Hepsipay) {
|
|
174
|
+
promise = import(`${'@akinon/pz-hepsipay'}`);
|
|
149
175
|
} else if (plugin === Plugin.FlowPayment) {
|
|
150
176
|
promise = import(`${'@akinon/pz-flow-payment'}`);
|
|
177
|
+
} else if (plugin === Plugin.SimilarProducts) {
|
|
178
|
+
promise = import(`${'@akinon/pz-similar-products'}`);
|
|
151
179
|
}
|
|
152
180
|
} catch (error) {
|
|
153
181
|
logger.error(error);
|
package/data/client/checkout.ts
CHANGED
|
@@ -35,8 +35,10 @@ import {
|
|
|
35
35
|
|
|
36
36
|
interface CheckoutResponse {
|
|
37
37
|
pre_order?: PreOrder;
|
|
38
|
-
errors
|
|
39
|
-
non_field_errors
|
|
38
|
+
errors?: {
|
|
39
|
+
non_field_errors?: string;
|
|
40
|
+
sample_products?: string[];
|
|
41
|
+
[key: string]: string | string[] | undefined;
|
|
40
42
|
};
|
|
41
43
|
context_list?: CheckoutContext[];
|
|
42
44
|
template_name?: string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Cache, CacheKey } from '../../lib/cache';
|
|
2
|
+
import { basket } from '../../data/urls';
|
|
3
|
+
import { Basket } from '../../types';
|
|
4
|
+
import appFetch from '../../utils/app-fetch';
|
|
5
|
+
import { ServerVariables } from '../../utils/server-variables';
|
|
6
|
+
import logger from '../../utils/log';
|
|
7
|
+
|
|
8
|
+
type GetBasketParams = {
|
|
9
|
+
locale?: string;
|
|
10
|
+
currency?: string;
|
|
11
|
+
namespace?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const getBasketDataHandler = ({
|
|
15
|
+
locale,
|
|
16
|
+
currency,
|
|
17
|
+
namespace
|
|
18
|
+
}: GetBasketParams) => {
|
|
19
|
+
return async function () {
|
|
20
|
+
try {
|
|
21
|
+
const url = namespace
|
|
22
|
+
? basket.getBasketDetail(namespace)
|
|
23
|
+
: basket.getBasket;
|
|
24
|
+
|
|
25
|
+
const basketData = await appFetch<{ basket: Basket }>({
|
|
26
|
+
url,
|
|
27
|
+
locale,
|
|
28
|
+
currency,
|
|
29
|
+
init: {
|
|
30
|
+
headers: {
|
|
31
|
+
Accept: 'application/json',
|
|
32
|
+
'Content-Type': 'application/json'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!basketData?.basket) {
|
|
38
|
+
logger.warn('Basket data is undefined', {
|
|
39
|
+
handler: 'getBasketDataHandler',
|
|
40
|
+
namespace
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return basketData;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.error('Error fetching basket data', {
|
|
47
|
+
handler: 'getBasketDataHandler',
|
|
48
|
+
error,
|
|
49
|
+
namespace
|
|
50
|
+
});
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const getBasketData = async ({
|
|
57
|
+
locale = ServerVariables.locale,
|
|
58
|
+
currency = ServerVariables.currency,
|
|
59
|
+
namespace
|
|
60
|
+
}: GetBasketParams = {}) => {
|
|
61
|
+
return Cache.wrap(
|
|
62
|
+
CacheKey.Basket(namespace),
|
|
63
|
+
locale,
|
|
64
|
+
getBasketDataHandler({ locale, currency, namespace }),
|
|
65
|
+
{
|
|
66
|
+
expire: 0,
|
|
67
|
+
cache: false
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
|