@akinon/next 2.0.0-beta.2 → 2.0.0-beta.20
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/.eslintrc.js +12 -0
- package/CHANGELOG.md +377 -7
- package/__tests__/next-config.test.ts +83 -0
- package/__tests__/tsconfig.json +23 -0
- package/api/auth.ts +133 -44
- package/api/barcode-search.ts +59 -0
- package/api/cache.ts +41 -5
- package/api/client.ts +21 -4
- package/api/form.ts +85 -0
- package/api/image-proxy.ts +75 -0
- package/api/product-categories.ts +53 -0
- package/api/similar-product-list.ts +63 -0
- package/api/similar-products.ts +111 -0
- package/api/virtual-try-on.ts +382 -0
- package/assets/styles/index.scss +84 -0
- package/babel.config.js +6 -0
- package/bin/pz-generate-routes.js +115 -0
- package/bin/pz-prebuild.js +1 -0
- package/bin/pz-predev.js +1 -0
- package/bin/pz-run-tests.js +99 -0
- package/bin/run-prebuild-tests.js +46 -0
- package/components/accordion.tsx +20 -5
- package/components/button.tsx +51 -36
- package/components/client-root.tsx +138 -2
- package/components/file-input.tsx +65 -3
- package/components/index.ts +1 -0
- package/components/input.tsx +1 -1
- package/components/link.tsx +46 -16
- package/components/logger-popup.tsx +213 -0
- package/components/modal.tsx +32 -16
- package/components/plugin-module.tsx +62 -3
- package/components/price.tsx +2 -2
- package/components/select.tsx +1 -1
- package/components/selected-payment-option-view.tsx +21 -0
- package/components/theme-editor/blocks/accordion-block.tsx +136 -0
- package/components/theme-editor/blocks/block-renderer-registry.tsx +77 -0
- package/components/theme-editor/blocks/button-block.tsx +593 -0
- package/components/theme-editor/blocks/counter-block.tsx +348 -0
- package/components/theme-editor/blocks/divider-block.tsx +20 -0
- package/components/theme-editor/blocks/embed-block.tsx +208 -0
- package/components/theme-editor/blocks/group-block.tsx +116 -0
- package/components/theme-editor/blocks/hotspot-block.tsx +147 -0
- package/components/theme-editor/blocks/icon-block.tsx +230 -0
- package/components/theme-editor/blocks/image-block.tsx +137 -0
- package/components/theme-editor/blocks/image-gallery-block.tsx +269 -0
- package/components/theme-editor/blocks/input-block.tsx +123 -0
- package/components/theme-editor/blocks/link-block.tsx +216 -0
- package/components/theme-editor/blocks/lottie-block.tsx +325 -0
- package/components/theme-editor/blocks/map-block.tsx +89 -0
- package/components/theme-editor/blocks/slider-block.tsx +595 -0
- package/components/theme-editor/blocks/tab-block.tsx +10 -0
- package/components/theme-editor/blocks/text-block.tsx +52 -0
- package/components/theme-editor/blocks/video-block.tsx +122 -0
- package/components/theme-editor/components/action-toolbar.tsx +305 -0
- package/components/theme-editor/components/designer-overlay.tsx +74 -0
- package/components/theme-editor/components/with-designer-features.tsx +142 -0
- package/components/theme-editor/dynamic-font-loader.tsx +79 -0
- package/components/theme-editor/hooks/use-designer-features.tsx +100 -0
- package/components/theme-editor/hooks/use-external-designer.tsx +95 -0
- package/components/theme-editor/hooks/use-native-widget-data.ts +188 -0
- package/components/theme-editor/hooks/use-visibility-context.ts +27 -0
- package/components/theme-editor/placeholder-registry.ts +31 -0
- package/components/theme-editor/sections/before-after-section.tsx +245 -0
- package/components/theme-editor/sections/contact-form-section.tsx +563 -0
- package/components/theme-editor/sections/countdown-campaign-banner-section.tsx +433 -0
- package/components/theme-editor/sections/coupon-banner-section.tsx +710 -0
- package/components/theme-editor/sections/divider-section.tsx +62 -0
- package/components/theme-editor/sections/featured-product-spotlight-section.tsx +507 -0
- package/components/theme-editor/sections/find-in-store-section.tsx +1995 -0
- package/components/theme-editor/sections/hover-showcase-section.tsx +326 -0
- package/components/theme-editor/sections/image-hotspot-section.tsx +142 -0
- package/components/theme-editor/sections/installment-options-section.tsx +1065 -0
- package/components/theme-editor/sections/notification-banner-section.tsx +173 -0
- package/components/theme-editor/sections/order-tracking-lookup-section.tsx +1379 -0
- package/components/theme-editor/sections/posts-slider-section.tsx +472 -0
- package/components/theme-editor/sections/pre-order-launch-banner-section.tsx +663 -0
- package/components/theme-editor/sections/section-renderer-registry.tsx +89 -0
- package/components/theme-editor/sections/section-wrapper.tsx +135 -0
- package/components/theme-editor/sections/shipping-threshold-progress-section.tsx +586 -0
- package/components/theme-editor/sections/stats-counter-section.tsx +486 -0
- package/components/theme-editor/sections/tabs-section.tsx +578 -0
- package/components/theme-editor/theme-block.tsx +102 -0
- package/components/theme-editor/theme-placeholder-client.tsx +218 -0
- package/components/theme-editor/theme-placeholder-wrapper.tsx +732 -0
- package/components/theme-editor/theme-placeholder.tsx +288 -0
- package/components/theme-editor/theme-section.tsx +1224 -0
- package/components/theme-editor/theme-settings-context.tsx +13 -0
- package/components/theme-editor/utils/index.ts +792 -0
- package/components/theme-editor/utils/iterator-utils.ts +234 -0
- package/components/theme-editor/utils/publish-window.ts +86 -0
- package/components/theme-editor/utils/visibility-rules.ts +188 -0
- package/data/client/account.ts +17 -2
- package/data/client/api.ts +2 -0
- package/data/client/basket.ts +66 -5
- package/data/client/checkout.ts +391 -99
- package/data/client/misc.ts +38 -2
- package/data/client/product.ts +19 -2
- package/data/client/user.ts +16 -8
- package/data/server/category.ts +11 -9
- package/data/server/flatpage.ts +11 -4
- package/data/server/form.ts +15 -4
- package/data/server/landingpage.ts +11 -4
- package/data/server/list.ts +5 -4
- package/data/server/menu.ts +11 -3
- package/data/server/product.ts +111 -55
- package/data/server/seo.ts +14 -4
- package/data/server/special-page.ts +5 -4
- package/data/server/widget.ts +90 -5
- package/data/urls.ts +16 -5
- package/hocs/client/with-segment-defaults.tsx +2 -2
- package/hocs/server/with-segment-defaults.tsx +65 -20
- package/hooks/index.ts +4 -0
- package/hooks/use-localization.ts +24 -10
- package/hooks/use-logger-context.tsx +114 -0
- package/hooks/use-logger.ts +92 -0
- package/hooks/use-loyalty-availability.ts +21 -0
- package/hooks/use-payment-options.ts +2 -1
- package/hooks/use-pz-params.ts +37 -0
- package/hooks/use-router.ts +51 -14
- package/hooks/use-sentry-uncaught-errors.ts +24 -0
- package/instrumentation/index.ts +10 -1
- package/instrumentation/node.ts +2 -20
- package/jest.config.js +25 -0
- package/lib/cache-handler.mjs +534 -16
- package/lib/cache.ts +272 -37
- package/localization/index.ts +2 -1
- package/localization/provider.tsx +2 -5
- package/middlewares/bfcache-headers.ts +18 -0
- package/middlewares/checkout-provider.ts +1 -1
- package/middlewares/complete-gpay.ts +32 -26
- package/middlewares/complete-masterpass.ts +33 -26
- package/middlewares/complete-wallet.ts +182 -0
- package/middlewares/default.ts +360 -215
- package/middlewares/index.ts +10 -2
- package/middlewares/locale.ts +34 -11
- package/middlewares/masterpass-rest-callback.ts +230 -0
- package/middlewares/oauth-login.ts +200 -57
- package/middlewares/pretty-url.ts +21 -8
- package/middlewares/redirection-payment.ts +32 -26
- package/middlewares/saved-card-redirection.ts +33 -26
- package/middlewares/three-d-redirection.ts +32 -26
- package/middlewares/url-redirection.ts +11 -1
- package/middlewares/wallet-complete-redirection.ts +206 -0
- package/package.json +25 -10
- package/plugins.d.ts +19 -4
- package/plugins.js +10 -1
- package/redux/actions.ts +47 -0
- package/redux/middlewares/checkout.ts +63 -138
- package/redux/middlewares/index.ts +14 -10
- package/redux/middlewares/pre-order/address.ts +7 -2
- package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +7 -1
- package/redux/middlewares/pre-order/data-source-shipping-option.ts +7 -1
- package/redux/middlewares/pre-order/delivery-option.ts +7 -1
- package/redux/middlewares/pre-order/index.ts +16 -10
- package/redux/middlewares/pre-order/installment-option.ts +8 -1
- package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
- package/redux/middlewares/pre-order/payment-option.ts +7 -1
- package/redux/middlewares/pre-order/pre-order-validation.ts +8 -3
- package/redux/middlewares/pre-order/redirection.ts +8 -2
- package/redux/middlewares/pre-order/set-pre-order.ts +6 -2
- package/redux/middlewares/pre-order/shipping-option.ts +7 -1
- package/redux/middlewares/pre-order/shipping-step.ts +5 -1
- package/redux/reducers/checkout.ts +23 -3
- package/redux/reducers/index.ts +11 -3
- package/redux/reducers/root.ts +7 -2
- package/redux/reducers/widget.ts +80 -0
- package/sentry/index.ts +69 -13
- package/tailwind/content.js +16 -0
- package/types/commerce/account.ts +5 -1
- package/types/commerce/checkout.ts +35 -1
- package/types/commerce/widget.ts +33 -0
- package/types/index.ts +101 -6
- package/types/next-auth.d.ts +2 -2
- package/types/widget.ts +80 -0
- package/utils/app-fetch.ts +7 -2
- package/utils/generate-commerce-search-params.ts +3 -2
- package/utils/get-checkout-path.ts +3 -0
- package/utils/get-root-hostname.ts +28 -0
- package/utils/index.ts +64 -10
- package/utils/localization.ts +4 -0
- package/utils/mobile-3d-iframe.ts +8 -2
- package/utils/override-middleware.ts +7 -12
- package/utils/pz-segments.ts +92 -0
- package/utils/redirect-ignore.ts +35 -0
- package/utils/redirect.ts +9 -3
- package/utils/redirection-iframe.ts +8 -2
- package/utils/widget-styles.ts +107 -0
- package/views/error-page.tsx +93 -0
- package/with-pz-config.js +13 -6
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import Settings from 'settings';
|
|
3
|
+
|
|
4
|
+
export async function GET(request: NextRequest) {
|
|
5
|
+
try {
|
|
6
|
+
const { searchParams } = new URL(request.url);
|
|
7
|
+
const dynamicFilter = request.headers.get('x-search-dynamic-filter');
|
|
8
|
+
const dynamicExclude = request.headers.get('x-search-dynamic-exclude');
|
|
9
|
+
|
|
10
|
+
if (!dynamicFilter && !dynamicExclude) {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{
|
|
13
|
+
error:
|
|
14
|
+
'Missing x-search-dynamic-filter or x-search-dynamic-exclude header'
|
|
15
|
+
},
|
|
16
|
+
{ status: 400 }
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (Settings.commerceUrl === 'default') {
|
|
21
|
+
return NextResponse.json(
|
|
22
|
+
{ error: 'Commerce URL is not configured' },
|
|
23
|
+
{ status: 500 }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const queryString = searchParams.toString();
|
|
28
|
+
const apiUrl = `${Settings.commerceUrl}/list${
|
|
29
|
+
queryString ? `?${queryString}` : ''
|
|
30
|
+
}`;
|
|
31
|
+
|
|
32
|
+
const headers: Record<string, string> = {
|
|
33
|
+
Accept: 'application/json',
|
|
34
|
+
'Content-Type': 'application/json'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (dynamicFilter) {
|
|
38
|
+
headers['x-search-dynamic-filter'] = dynamicFilter;
|
|
39
|
+
} else if (dynamicExclude) {
|
|
40
|
+
headers['x-search-dynamic-exclude'] = dynamicExclude;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const response = await fetch(apiUrl, {
|
|
44
|
+
method: 'GET',
|
|
45
|
+
headers
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ error: `API request failed with status: ${response.status}` },
|
|
51
|
+
{ status: response.status }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const data = await response.json();
|
|
56
|
+
return NextResponse.json(data);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return NextResponse.json(
|
|
59
|
+
{ error: (error as Error).message },
|
|
60
|
+
{ status: 500 }
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
|
|
16
|
+
if (!url) {
|
|
17
|
+
return errorResponse('URL parameter is required', 400);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (Settings.commerceUrl === 'default') {
|
|
21
|
+
return errorResponse('Commerce URL is not configured', 500);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const apiParams = new URLSearchParams();
|
|
25
|
+
apiParams.append('limit', limit);
|
|
26
|
+
apiParams.append('url', url);
|
|
27
|
+
|
|
28
|
+
if (excludedProductIds) {
|
|
29
|
+
apiParams.append('excluded_product_ids', excludedProductIds);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const apiUrl = `${IMAGE_SEARCH_API_URL}?${apiParams.toString()}`;
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(apiUrl, {
|
|
36
|
+
method: 'GET',
|
|
37
|
+
headers: {
|
|
38
|
+
Accept: 'application/json'
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (!response.ok) {
|
|
43
|
+
const errorText = await response.text();
|
|
44
|
+
return errorResponse(
|
|
45
|
+
errorText || `API request failed with status: ${response.status}`,
|
|
46
|
+
response.status
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const responseText = await response.text();
|
|
51
|
+
|
|
52
|
+
return NextResponse.json(JSON.parse(responseText));
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return errorResponse((error as Error).message, 500);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function POST(request: Request) {
|
|
59
|
+
const { searchParams } = new URL(request.url);
|
|
60
|
+
const limit = searchParams.get('limit') || '20';
|
|
61
|
+
|
|
62
|
+
if (Settings.commerceUrl === 'default') {
|
|
63
|
+
return errorResponse('Commerce URL is not configured', 500);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let requestBody;
|
|
67
|
+
try {
|
|
68
|
+
requestBody = await request.json();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return errorResponse('Invalid JSON in request body', 400);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!requestBody.image) {
|
|
74
|
+
return errorResponse('Image data is required in request body', 400);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const apiParams = new URLSearchParams();
|
|
78
|
+
apiParams.append('limit', limit);
|
|
79
|
+
|
|
80
|
+
const apiUrl = `${IMAGE_SEARCH_API_URL}?${apiParams.toString()}`;
|
|
81
|
+
|
|
82
|
+
const bodyData: any = { image: requestBody.image };
|
|
83
|
+
|
|
84
|
+
if (requestBody.excluded_product_ids) {
|
|
85
|
+
bodyData.excluded_product_ids = requestBody.excluded_product_ids;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const response = await fetch(apiUrl, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: {
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
Accept: 'application/json'
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify(bodyData)
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
const errorText = await response.text();
|
|
100
|
+
return errorResponse(
|
|
101
|
+
errorText || `API request failed with status: ${response.status}`,
|
|
102
|
+
response.status
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const responseData = await response.json();
|
|
107
|
+
return NextResponse.json(responseData);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return errorResponse((error as Error).message, 500);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import Settings from 'settings';
|
|
3
|
+
|
|
4
|
+
const VIRTUAL_TRY_ON_API_URL = process.env.NEXT_PUBLIC_VIRTUAL_TRY_ON_API_URL;
|
|
5
|
+
|
|
6
|
+
let limitedCategoriesCache: {
|
|
7
|
+
data: any;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
} | null = null;
|
|
10
|
+
|
|
11
|
+
const CACHE_TTL = 3600000;
|
|
12
|
+
|
|
13
|
+
function getClientIP(request: NextRequest): string | null {
|
|
14
|
+
const forwardedFor = request.headers.get('x-forwarded-for');
|
|
15
|
+
if (forwardedFor) {
|
|
16
|
+
const ip = forwardedFor.split(',')[0].trim();
|
|
17
|
+
if (ip === '::1' || ip === '::ffff:127.0.0.1') {
|
|
18
|
+
return '127.0.0.1';
|
|
19
|
+
}
|
|
20
|
+
return ip;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const realIP = request.headers.get('x-real-ip');
|
|
24
|
+
if (realIP) {
|
|
25
|
+
if (realIP === '::1' || realIP === '::ffff:127.0.0.1') {
|
|
26
|
+
return '127.0.0.1';
|
|
27
|
+
}
|
|
28
|
+
return realIP;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (process.env.NODE_ENV === 'development') {
|
|
32
|
+
return '127.0.0.1';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getLocaleFromRequest(request: NextRequest): string | null {
|
|
39
|
+
const cookieLocale = request.cookies.get('pz-locale')?.value;
|
|
40
|
+
|
|
41
|
+
if (cookieLocale) {
|
|
42
|
+
const currentLocale = Settings.localization.locales.find(
|
|
43
|
+
(l) => l.value === cookieLocale
|
|
44
|
+
);
|
|
45
|
+
return currentLocale?.apiValue || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function GET(request: NextRequest) {
|
|
52
|
+
try {
|
|
53
|
+
const { searchParams } = new URL(request.url);
|
|
54
|
+
const endpoint = searchParams.get('endpoint');
|
|
55
|
+
|
|
56
|
+
if (endpoint === 'limited-categories') {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
limitedCategoriesCache &&
|
|
61
|
+
now - limitedCategoriesCache.timestamp < CACHE_TTL
|
|
62
|
+
) {
|
|
63
|
+
return NextResponse.json(limitedCategoriesCache.data, {
|
|
64
|
+
status: 200,
|
|
65
|
+
headers: {
|
|
66
|
+
'Access-Control-Allow-Origin': '*',
|
|
67
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
|
|
68
|
+
'Access-Control-Allow-Headers':
|
|
69
|
+
'Content-Type, Accept, Authorization',
|
|
70
|
+
'Cache-Control':
|
|
71
|
+
'public, s-maxage=3600, stale-while-revalidate=7200',
|
|
72
|
+
'X-Virtual-Try-On-Cache-Status': 'HIT'
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const externalUrl = `${VIRTUAL_TRY_ON_API_URL}/api/v1/limited-categories`;
|
|
78
|
+
const clientIP = getClientIP(request);
|
|
79
|
+
const locale = getLocaleFromRequest(request);
|
|
80
|
+
|
|
81
|
+
const headersToSend = {
|
|
82
|
+
Accept: 'application/json',
|
|
83
|
+
...(locale && { 'Accept-Language': locale }),
|
|
84
|
+
...(request.headers.get('authorization') && {
|
|
85
|
+
Authorization: request.headers.get('authorization')!
|
|
86
|
+
}),
|
|
87
|
+
...(clientIP && {
|
|
88
|
+
'X-Forwarded-For': clientIP
|
|
89
|
+
})
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const response = await fetch(externalUrl, {
|
|
93
|
+
method: 'GET',
|
|
94
|
+
headers: headersToSend
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
let responseData: any;
|
|
98
|
+
const responseText = await response.text();
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
responseData = responseText ? JSON.parse(responseText) : {};
|
|
102
|
+
} catch (parseError) {
|
|
103
|
+
responseData = { category_ids: [] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
responseData = { category_ids: [] };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
limitedCategoriesCache = {
|
|
111
|
+
data: responseData,
|
|
112
|
+
timestamp: Date.now()
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return NextResponse.json(responseData, {
|
|
116
|
+
status: response.ok ? response.status : 200,
|
|
117
|
+
headers: {
|
|
118
|
+
'Access-Control-Allow-Origin': '*',
|
|
119
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
|
|
120
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization',
|
|
121
|
+
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
|
122
|
+
'X-Virtual-Try-On-Cache-Status': 'MISS'
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
} else if (endpoint === 'job-status') {
|
|
126
|
+
const referenceUrl = searchParams.get('reference_url');
|
|
127
|
+
if (!referenceUrl) {
|
|
128
|
+
return NextResponse.json(
|
|
129
|
+
{ error: 'reference_url is required' },
|
|
130
|
+
{ status: 400 }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const externalUrl = `${VIRTUAL_TRY_ON_API_URL}/api/async/v1/job-status?reference_url=${encodeURIComponent(
|
|
135
|
+
referenceUrl
|
|
136
|
+
)}`;
|
|
137
|
+
const clientIP = getClientIP(request);
|
|
138
|
+
const locale = getLocaleFromRequest(request);
|
|
139
|
+
|
|
140
|
+
const headersToSend = {
|
|
141
|
+
Accept: 'application/json',
|
|
142
|
+
...(locale && { 'Accept-Language': locale }),
|
|
143
|
+
...(request.headers.get('authorization') && {
|
|
144
|
+
Authorization: request.headers.get('authorization')!
|
|
145
|
+
}),
|
|
146
|
+
...(clientIP && {
|
|
147
|
+
'X-Forwarded-For': clientIP
|
|
148
|
+
})
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const response = await fetch(externalUrl, {
|
|
152
|
+
method: 'GET',
|
|
153
|
+
headers: headersToSend
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
let responseData: any;
|
|
157
|
+
const responseText = await response.text();
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
responseData = responseText ? JSON.parse(responseText) : {};
|
|
161
|
+
} catch (parseError) {
|
|
162
|
+
return NextResponse.json(
|
|
163
|
+
{ error: 'Invalid JSON response from job status API' },
|
|
164
|
+
{ status: 500 }
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return NextResponse.json(responseData, {
|
|
169
|
+
status: response.status,
|
|
170
|
+
headers: {
|
|
171
|
+
'Access-Control-Allow-Origin': '*',
|
|
172
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
|
|
173
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization'
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return NextResponse.json(
|
|
179
|
+
{ error: 'Invalid endpoint for GET method' },
|
|
180
|
+
{ status: 400 }
|
|
181
|
+
);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return NextResponse.json({ category_ids: [] }, { status: 200 });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function POST(request: NextRequest) {
|
|
188
|
+
try {
|
|
189
|
+
const { searchParams } = new URL(request.url);
|
|
190
|
+
const endpoint = searchParams.get('endpoint');
|
|
191
|
+
|
|
192
|
+
const body = await request.json();
|
|
193
|
+
|
|
194
|
+
let externalUrl: string;
|
|
195
|
+
let httpMethod = 'POST';
|
|
196
|
+
|
|
197
|
+
if (endpoint === 'feedback') {
|
|
198
|
+
if (!body.url || typeof body.url !== 'string' || !body.url.trim()) {
|
|
199
|
+
return NextResponse.json(
|
|
200
|
+
{ status: 'error', message: 'URL is required for feedback' },
|
|
201
|
+
{ status: 400 }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (typeof body.feedback !== 'boolean') {
|
|
205
|
+
return NextResponse.json(
|
|
206
|
+
{ status: 'error', message: 'Feedback must be a boolean value' },
|
|
207
|
+
{ status: 400 }
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
externalUrl = `${VIRTUAL_TRY_ON_API_URL}/api/v1/feedback`;
|
|
211
|
+
httpMethod = 'PUT';
|
|
212
|
+
} else if (endpoint === 'async-multiple-try-on') {
|
|
213
|
+
externalUrl = `${VIRTUAL_TRY_ON_API_URL}/api/async/v1/multiple-virtual-try-on`;
|
|
214
|
+
} else {
|
|
215
|
+
return NextResponse.json(
|
|
216
|
+
{ error: 'Invalid endpoint specified' },
|
|
217
|
+
{ status: 400 }
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const clientIP = getClientIP(request);
|
|
222
|
+
const locale = getLocaleFromRequest(request);
|
|
223
|
+
|
|
224
|
+
const headersToSend = {
|
|
225
|
+
'Content-Type': 'application/json',
|
|
226
|
+
...(locale && { 'Accept-Language': locale }),
|
|
227
|
+
...(httpMethod === 'POST' && { Accept: 'application/json' }),
|
|
228
|
+
...(request.headers.get('authorization') && {
|
|
229
|
+
Authorization: request.headers.get('authorization')!
|
|
230
|
+
}),
|
|
231
|
+
...(clientIP && {
|
|
232
|
+
'X-Forwarded-For': clientIP
|
|
233
|
+
})
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const fetchOptions: RequestInit = {
|
|
237
|
+
method: httpMethod,
|
|
238
|
+
headers: headersToSend
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
if (httpMethod !== 'GET') {
|
|
242
|
+
fetchOptions.body = JSON.stringify(body);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const response = await fetch(externalUrl, fetchOptions);
|
|
246
|
+
|
|
247
|
+
let responseData: Record<string, any>;
|
|
248
|
+
const responseText = await response.text();
|
|
249
|
+
|
|
250
|
+
if (endpoint === 'feedback') {
|
|
251
|
+
try {
|
|
252
|
+
responseData = responseText ? JSON.parse(responseText) : {};
|
|
253
|
+
} catch (parseError) {
|
|
254
|
+
responseData = { error: 'Invalid JSON response from feedback API' };
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
try {
|
|
258
|
+
responseData = responseText ? JSON.parse(responseText) : {};
|
|
259
|
+
} catch (parseError) {
|
|
260
|
+
responseData = { error: 'Invalid JSON response' };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (!response.ok && responseData.error) {
|
|
265
|
+
let userFriendlyMessage = responseData.error;
|
|
266
|
+
|
|
267
|
+
if (
|
|
268
|
+
typeof responseData.error === 'string' &&
|
|
269
|
+
(responseData.error.includes('duplicate key value') ||
|
|
270
|
+
responseData.error.includes('image_pk'))
|
|
271
|
+
) {
|
|
272
|
+
userFriendlyMessage =
|
|
273
|
+
'This image has already been processed. Please try with a different image.';
|
|
274
|
+
} else if (responseData.error.includes('bulk insert images')) {
|
|
275
|
+
userFriendlyMessage =
|
|
276
|
+
'There was an issue processing your image. Please try again with a different image.';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return NextResponse.json(
|
|
280
|
+
{
|
|
281
|
+
...responseData,
|
|
282
|
+
message: userFriendlyMessage
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
status: response.status,
|
|
286
|
+
headers: {
|
|
287
|
+
'Access-Control-Allow-Origin': '*',
|
|
288
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
289
|
+
'Access-Control-Allow-Headers':
|
|
290
|
+
'Content-Type, Accept, Authorization'
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return NextResponse.json(responseData, {
|
|
297
|
+
status: response.status,
|
|
298
|
+
headers: {
|
|
299
|
+
'Access-Control-Allow-Origin': '*',
|
|
300
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
301
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization'
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
} catch (error) {
|
|
305
|
+
return NextResponse.json(
|
|
306
|
+
{
|
|
307
|
+
status: 'error',
|
|
308
|
+
message:
|
|
309
|
+
'Internal server error occurred during virtual try-on processing'
|
|
310
|
+
},
|
|
311
|
+
{ status: 500 }
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export async function PUT(request: NextRequest) {
|
|
317
|
+
try {
|
|
318
|
+
const { searchParams } = new URL(request.url);
|
|
319
|
+
const endpoint = searchParams.get('endpoint');
|
|
320
|
+
|
|
321
|
+
if (endpoint !== 'feedback') {
|
|
322
|
+
return NextResponse.json(
|
|
323
|
+
{ error: 'PUT method only supports feedback endpoint' },
|
|
324
|
+
{ status: 400 }
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const body = await request.json();
|
|
329
|
+
|
|
330
|
+
const externalUrl = `${VIRTUAL_TRY_ON_API_URL}/api/v1/feedback`;
|
|
331
|
+
const clientIP = getClientIP(request);
|
|
332
|
+
const locale = getLocaleFromRequest(request);
|
|
333
|
+
|
|
334
|
+
const headersToSend = {
|
|
335
|
+
'Content-Type': 'application/json',
|
|
336
|
+
...(locale && { 'Accept-Language': locale }),
|
|
337
|
+
...(request.headers.get('authorization') && {
|
|
338
|
+
Authorization: request.headers.get('authorization')!
|
|
339
|
+
}),
|
|
340
|
+
...(clientIP && {
|
|
341
|
+
'X-Forwarded-For': clientIP
|
|
342
|
+
})
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const response = await fetch(externalUrl, {
|
|
346
|
+
method: 'PUT',
|
|
347
|
+
headers: headersToSend,
|
|
348
|
+
body: JSON.stringify(body)
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const responseData = response?.json() || {};
|
|
352
|
+
|
|
353
|
+
return NextResponse.json(responseData, {
|
|
354
|
+
status: response.status,
|
|
355
|
+
headers: {
|
|
356
|
+
'Access-Control-Allow-Origin': '*',
|
|
357
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
|
|
358
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization'
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
} catch (error) {
|
|
362
|
+
return NextResponse.json(
|
|
363
|
+
{
|
|
364
|
+
status: 'error',
|
|
365
|
+
message: 'Internal server error occurred during feedback submission',
|
|
366
|
+
error: (error as Error).message
|
|
367
|
+
},
|
|
368
|
+
{ status: 500 }
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export async function OPTIONS() {
|
|
374
|
+
return new NextResponse(null, {
|
|
375
|
+
status: 200,
|
|
376
|
+
headers: {
|
|
377
|
+
'Access-Control-Allow-Origin': '*',
|
|
378
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
|
|
379
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization'
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
}
|
package/assets/styles/index.scss
CHANGED
|
@@ -51,3 +51,87 @@
|
|
|
51
51
|
z-index: 1001;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
.container-droppable {
|
|
56
|
+
outline: 2px dashed #4482ff !important;
|
|
57
|
+
outline-offset: -2px;
|
|
58
|
+
background-color: rgba(96, 165, 250, 0.1) !important;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.container-dragover {
|
|
62
|
+
background-color: rgba(59, 130, 246, 0.1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.container-dragover::after {
|
|
66
|
+
content: '';
|
|
67
|
+
position: absolute;
|
|
68
|
+
top: 0;
|
|
69
|
+
left: 0;
|
|
70
|
+
right: 0;
|
|
71
|
+
bottom: 0;
|
|
72
|
+
border: 2px solid #4482ff;
|
|
73
|
+
pointer-events: none;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.dragging * {
|
|
77
|
+
cursor: default !important;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.dragging [data-component='true'] {
|
|
81
|
+
cursor: grabbing !important;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.dragging .drop-target {
|
|
85
|
+
cursor: grab !important;
|
|
86
|
+
outline: 2px dashed #4482ff !important;
|
|
87
|
+
outline-offset: -2px;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.dragging .no-drop-target {
|
|
91
|
+
cursor: no-drop !important;
|
|
92
|
+
outline: 2px dashed #e63946 !important;
|
|
93
|
+
outline-offset: -2px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
[data-component='true'] {
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.carousel-container {
|
|
101
|
+
li[aria-hidden='false'] {
|
|
102
|
+
div {
|
|
103
|
+
display: block;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.posts-slider-carousel {
|
|
109
|
+
width: 100%;
|
|
110
|
+
|
|
111
|
+
.react-multi-carousel-track {
|
|
112
|
+
align-items: stretch;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.posts-slider-dots {
|
|
117
|
+
position: relative;
|
|
118
|
+
display: flex;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
gap: 8px;
|
|
121
|
+
margin-top: 16px;
|
|
122
|
+
padding: 0;
|
|
123
|
+
list-style: none;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.posts-slider-dots li button {
|
|
127
|
+
width: 8px;
|
|
128
|
+
height: 8px;
|
|
129
|
+
border-radius: 9999px;
|
|
130
|
+
border: none;
|
|
131
|
+
background: rgba(0, 0, 0, 0.25);
|
|
132
|
+
cursor: pointer;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.posts-slider-dots li.react-multi-carousel-dot--active button {
|
|
136
|
+
background: rgba(0, 0, 0, 0.6);
|
|
137
|
+
}
|