@akinon/next 1.101.0-snapshot-ZERO-3615-20250924130435 → 1.101.0-snapshot-ZERO-3648-20250925195915
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 +2 -2
- package/api/virtual-try-on.ts +209 -0
- package/components/plugin-module.tsx +8 -3
- package/data/server/product.ts +49 -93
- package/middlewares/default.ts +14 -0
- package/middlewares/index.ts +1 -0
- package/middlewares/pretty-url.ts +2 -0
- package/package.json +2 -2
- package/plugins.js +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @akinon/next
|
|
2
2
|
|
|
3
|
-
## 1.101.0-snapshot-ZERO-
|
|
3
|
+
## 1.101.0-snapshot-ZERO-3648-20250925195915
|
|
4
4
|
|
|
5
5
|
### Minor Changes
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- fcbbea7: ZERO-3648: Add virtual try-on feature with localization support
|
|
8
8
|
|
|
9
9
|
## 1.100.0
|
|
10
10
|
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
const VIRTUAL_TRY_ON_API_URL = process.env.NEXT_PUBLIC_VIRTUAL_TRY_ON_API_URL;
|
|
4
|
+
|
|
5
|
+
export async function GET(request: NextRequest) {
|
|
6
|
+
try {
|
|
7
|
+
const { searchParams } = new URL(request.url);
|
|
8
|
+
const endpoint = searchParams.get('endpoint');
|
|
9
|
+
|
|
10
|
+
if (endpoint === 'limited-categories') {
|
|
11
|
+
const externalUrl = `${VIRTUAL_TRY_ON_API_URL}/api/v1/limited-categories`;
|
|
12
|
+
|
|
13
|
+
const response = await fetch(externalUrl, {
|
|
14
|
+
method: 'GET',
|
|
15
|
+
headers: {
|
|
16
|
+
Accept: 'application/json',
|
|
17
|
+
...(request.headers.get('authorization') && {
|
|
18
|
+
Authorization: request.headers.get('authorization')!
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
let responseData: any;
|
|
24
|
+
const responseText = await response.text();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
responseData = responseText ? JSON.parse(responseText) : {};
|
|
28
|
+
} catch (parseError) {
|
|
29
|
+
responseData = { category_ids: [] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
responseData = { category_ids: [] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return NextResponse.json(responseData, {
|
|
37
|
+
status: response.ok ? response.status : 200,
|
|
38
|
+
headers: {
|
|
39
|
+
'Access-Control-Allow-Origin': '*',
|
|
40
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
|
|
41
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization'
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return NextResponse.json(
|
|
47
|
+
{ error: 'Invalid endpoint for GET method' },
|
|
48
|
+
{ status: 400 }
|
|
49
|
+
);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return NextResponse.json({ category_ids: [] }, { status: 200 });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function POST(request: NextRequest) {
|
|
56
|
+
try {
|
|
57
|
+
const { searchParams } = new URL(request.url);
|
|
58
|
+
const endpoint = searchParams.get('endpoint');
|
|
59
|
+
|
|
60
|
+
const body = await request.json();
|
|
61
|
+
|
|
62
|
+
let externalUrl: string;
|
|
63
|
+
if (endpoint === 'feedback') {
|
|
64
|
+
externalUrl = `${VIRTUAL_TRY_ON_API_URL}/api/v1/feedback`;
|
|
65
|
+
} else {
|
|
66
|
+
externalUrl = `${VIRTUAL_TRY_ON_API_URL}/api/v1/virtual-try-on`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const httpMethod = endpoint === 'feedback' ? 'PUT' : 'POST';
|
|
70
|
+
|
|
71
|
+
const response = await fetch(externalUrl, {
|
|
72
|
+
method: httpMethod,
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
...(httpMethod === 'POST' && { Accept: 'application/json' }),
|
|
76
|
+
...(request.headers.get('authorization') && {
|
|
77
|
+
Authorization: request.headers.get('authorization')!
|
|
78
|
+
})
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify(body)
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
let responseData: any;
|
|
84
|
+
const responseText = await response.text();
|
|
85
|
+
|
|
86
|
+
if (endpoint === 'feedback') {
|
|
87
|
+
try {
|
|
88
|
+
responseData = responseText ? JSON.parse(responseText) : {};
|
|
89
|
+
} catch (parseError) {
|
|
90
|
+
responseData = { error: 'Invalid JSON response from feedback API' };
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
try {
|
|
94
|
+
responseData = responseText ? JSON.parse(responseText) : {};
|
|
95
|
+
} catch (parseError) {
|
|
96
|
+
responseData = { error: 'Invalid JSON response' };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!response.ok && responseData.error) {
|
|
101
|
+
let userFriendlyMessage = responseData.error;
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
typeof responseData.error === 'string' &&
|
|
105
|
+
(responseData.error.includes('duplicate key value') ||
|
|
106
|
+
responseData.error.includes('image_pk'))
|
|
107
|
+
) {
|
|
108
|
+
userFriendlyMessage =
|
|
109
|
+
'This image has already been processed. Please try with a different image.';
|
|
110
|
+
} else if (responseData.error.includes('bulk insert images')) {
|
|
111
|
+
userFriendlyMessage =
|
|
112
|
+
'There was an issue processing your image. Please try again with a different image.';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return NextResponse.json(
|
|
116
|
+
{
|
|
117
|
+
...responseData,
|
|
118
|
+
message: userFriendlyMessage
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
status: response.status,
|
|
122
|
+
headers: {
|
|
123
|
+
'Access-Control-Allow-Origin': '*',
|
|
124
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
125
|
+
'Access-Control-Allow-Headers':
|
|
126
|
+
'Content-Type, Accept, Authorization'
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return NextResponse.json(responseData, {
|
|
133
|
+
status: response.status,
|
|
134
|
+
headers: {
|
|
135
|
+
'Access-Control-Allow-Origin': '*',
|
|
136
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
137
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization'
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return NextResponse.json(
|
|
142
|
+
{
|
|
143
|
+
status: 'error',
|
|
144
|
+
message:
|
|
145
|
+
'Internal server error occurred during virtual try-on processing'
|
|
146
|
+
},
|
|
147
|
+
{ status: 500 }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function PUT(request: NextRequest) {
|
|
153
|
+
try {
|
|
154
|
+
const { searchParams } = new URL(request.url);
|
|
155
|
+
const endpoint = searchParams.get('endpoint');
|
|
156
|
+
|
|
157
|
+
if (endpoint !== 'feedback') {
|
|
158
|
+
return NextResponse.json(
|
|
159
|
+
{ error: 'PUT method only supports feedback endpoint' },
|
|
160
|
+
{ status: 400 }
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const body = await request.json();
|
|
165
|
+
|
|
166
|
+
const externalUrl = `${VIRTUAL_TRY_ON_API_URL}/api/v1/feedback`;
|
|
167
|
+
|
|
168
|
+
const response = await fetch(externalUrl, {
|
|
169
|
+
method: 'PUT',
|
|
170
|
+
headers: {
|
|
171
|
+
'Content-Type': 'application/json',
|
|
172
|
+
...(request.headers.get('authorization') && {
|
|
173
|
+
Authorization: request.headers.get('authorization')!
|
|
174
|
+
})
|
|
175
|
+
},
|
|
176
|
+
body: JSON.stringify(body)
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const responseData = await response.json();
|
|
180
|
+
|
|
181
|
+
return NextResponse.json(responseData, {
|
|
182
|
+
status: response.status,
|
|
183
|
+
headers: {
|
|
184
|
+
'Access-Control-Allow-Origin': '*',
|
|
185
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
|
|
186
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization'
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return NextResponse.json(
|
|
191
|
+
{
|
|
192
|
+
status: 'error',
|
|
193
|
+
message: 'Internal server error occurred during feedback submission'
|
|
194
|
+
},
|
|
195
|
+
{ status: 500 }
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function OPTIONS() {
|
|
201
|
+
return new NextResponse(null, {
|
|
202
|
+
status: 200,
|
|
203
|
+
headers: {
|
|
204
|
+
'Access-Control-Allow-Origin': '*',
|
|
205
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, OPTIONS',
|
|
206
|
+
'Access-Control-Allow-Headers': 'Content-Type, Accept, Authorization'
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}
|
|
@@ -21,7 +21,8 @@ enum Plugin {
|
|
|
21
21
|
Akifast = 'pz-akifast',
|
|
22
22
|
MultiBasket = 'pz-multi-basket',
|
|
23
23
|
SavedCard = 'pz-saved-card',
|
|
24
|
-
FlowPayment = 'pz-flow-payment'
|
|
24
|
+
FlowPayment = 'pz-flow-payment',
|
|
25
|
+
VirtualTryOn = 'pz-virtual-try-on'
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export enum Component {
|
|
@@ -47,7 +48,8 @@ export enum Component {
|
|
|
47
48
|
AkifastCheckoutButton = 'CheckoutButton',
|
|
48
49
|
MultiBasket = 'MultiBasket',
|
|
49
50
|
SavedCard = 'SavedCardOption',
|
|
50
|
-
FlowPayment = 'FlowPayment'
|
|
51
|
+
FlowPayment = 'FlowPayment',
|
|
52
|
+
VirtualTryOnPlugin = 'VirtualTryOnPlugin'
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
const PluginComponents = new Map([
|
|
@@ -81,7 +83,8 @@ const PluginComponents = new Map([
|
|
|
81
83
|
],
|
|
82
84
|
[Plugin.MultiBasket, [Component.MultiBasket]],
|
|
83
85
|
[Plugin.SavedCard, [Component.SavedCard]],
|
|
84
|
-
[Plugin.FlowPayment, [Component.FlowPayment]]
|
|
86
|
+
[Plugin.FlowPayment, [Component.FlowPayment]],
|
|
87
|
+
[Plugin.VirtualTryOn, [Component.VirtualTryOnPlugin]]
|
|
85
88
|
]);
|
|
86
89
|
|
|
87
90
|
const getPlugin = (component: Component) => {
|
|
@@ -148,6 +151,8 @@ export default function PluginModule({
|
|
|
148
151
|
promise = import(`${'@akinon/pz-saved-card'}`);
|
|
149
152
|
} else if (plugin === Plugin.FlowPayment) {
|
|
150
153
|
promise = import(`${'@akinon/pz-flow-payment'}`);
|
|
154
|
+
} else if (plugin === Plugin.VirtualTryOn) {
|
|
155
|
+
promise = import(`${'@akinon/pz-virtual-try-on'}`);
|
|
151
156
|
}
|
|
152
157
|
} catch (error) {
|
|
153
158
|
logger.error(error);
|
package/data/server/product.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { ServerVariables } from '../../utils/server-variables';
|
|
|
6
6
|
import logger from '../../utils/log';
|
|
7
7
|
|
|
8
8
|
type GetProduct = {
|
|
9
|
-
pk: number
|
|
9
|
+
pk: number;
|
|
10
10
|
locale?: string;
|
|
11
11
|
currency?: string;
|
|
12
12
|
searchParams?: URLSearchParams;
|
|
@@ -23,21 +23,9 @@ const getProductDataHandler = ({
|
|
|
23
23
|
headers
|
|
24
24
|
}: GetProduct) => {
|
|
25
25
|
return async function () {
|
|
26
|
-
// Convert pk to number if it's a string and validate
|
|
27
|
-
const numericPk = typeof pk === 'string' ? parseInt(pk, 10) : pk;
|
|
28
|
-
|
|
29
|
-
if (isNaN(numericPk)) {
|
|
30
|
-
logger.warn('Invalid product pk provided', {
|
|
31
|
-
handler: 'getProductDataHandler',
|
|
32
|
-
pk,
|
|
33
|
-
numericPk
|
|
34
|
-
});
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
26
|
let url = groupProduct
|
|
39
|
-
? product.getGroupProductByPk(
|
|
40
|
-
: product.getProductByPk(
|
|
27
|
+
? product.getGroupProductByPk(pk)
|
|
28
|
+
: product.getProductByPk(pk);
|
|
41
29
|
|
|
42
30
|
if (searchParams) {
|
|
43
31
|
url +=
|
|
@@ -47,81 +35,61 @@ const getProductDataHandler = ({
|
|
|
47
35
|
.join('&');
|
|
48
36
|
}
|
|
49
37
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
...(headers ?? {})
|
|
60
|
-
}
|
|
38
|
+
const data = await appFetch<ProductResult>({
|
|
39
|
+
url,
|
|
40
|
+
locale,
|
|
41
|
+
currency,
|
|
42
|
+
init: {
|
|
43
|
+
headers: {
|
|
44
|
+
Accept: 'application/json',
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
...(headers ?? {})
|
|
61
47
|
}
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// If product data is not found, return null to trigger 404
|
|
65
|
-
if (!data?.product?.pk) {
|
|
66
|
-
logger.warn('Product not found', {
|
|
67
|
-
handler: 'getProductDataHandler',
|
|
68
|
-
pk: numericPk,
|
|
69
|
-
hasData: !!data,
|
|
70
|
-
hasProduct: !!data?.product
|
|
71
|
-
});
|
|
72
|
-
return null;
|
|
73
48
|
}
|
|
49
|
+
});
|
|
74
50
|
|
|
75
|
-
|
|
51
|
+
const categoryUrl = product.categoryUrl(data.product.pk);
|
|
76
52
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
53
|
+
const productCategoryData = await appFetch<ProductCategoryResult>({
|
|
54
|
+
url: categoryUrl,
|
|
55
|
+
locale,
|
|
56
|
+
currency,
|
|
57
|
+
init: {
|
|
58
|
+
headers: {
|
|
59
|
+
Accept: 'application/json',
|
|
60
|
+
'Content-Type': 'application/json'
|
|
86
61
|
}
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const menuItemModel = productCategoryData?.results[0]?.menuitemmodel;
|
|
90
|
-
|
|
91
|
-
if (!menuItemModel) {
|
|
92
|
-
logger.warn('menuItemModel is undefined, skipping breadcrumbData fetch', {
|
|
93
|
-
handler: 'getProductDataHandler',
|
|
94
|
-
pk: numericPk
|
|
95
|
-
});
|
|
96
|
-
return { data, breadcrumbData: undefined };
|
|
97
62
|
}
|
|
63
|
+
});
|
|
98
64
|
|
|
99
|
-
|
|
65
|
+
const menuItemModel = productCategoryData?.results[0]?.menuitemmodel;
|
|
100
66
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
locale,
|
|
104
|
-
currency,
|
|
105
|
-
init: {
|
|
106
|
-
headers: {
|
|
107
|
-
Accept: 'application/json',
|
|
108
|
-
'Content-Type': 'application/json'
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
return {
|
|
114
|
-
data,
|
|
115
|
-
breadcrumbData: breadcrumbData?.menu
|
|
116
|
-
};
|
|
117
|
-
} catch (error) {
|
|
118
|
-
logger.error('Error in getProductDataHandler', {
|
|
67
|
+
if (!menuItemModel) {
|
|
68
|
+
logger.warn('menuItemModel is undefined, skipping breadcrumbData fetch', {
|
|
119
69
|
handler: 'getProductDataHandler',
|
|
120
|
-
pk
|
|
121
|
-
error: error.message
|
|
70
|
+
pk
|
|
122
71
|
});
|
|
123
|
-
return
|
|
72
|
+
return { data, breadcrumbData: undefined };
|
|
124
73
|
}
|
|
74
|
+
|
|
75
|
+
const breadcrumbUrl = product.breadcrumbUrl(menuItemModel);
|
|
76
|
+
|
|
77
|
+
const breadcrumbData = await appFetch<any>({
|
|
78
|
+
url: breadcrumbUrl,
|
|
79
|
+
locale,
|
|
80
|
+
currency,
|
|
81
|
+
init: {
|
|
82
|
+
headers: {
|
|
83
|
+
Accept: 'application/json',
|
|
84
|
+
'Content-Type': 'application/json'
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
data,
|
|
91
|
+
breadcrumbData: breadcrumbData?.menu
|
|
92
|
+
};
|
|
125
93
|
};
|
|
126
94
|
};
|
|
127
95
|
|
|
@@ -133,12 +101,9 @@ export const getProductData = async ({
|
|
|
133
101
|
groupProduct,
|
|
134
102
|
headers
|
|
135
103
|
}: GetProduct) => {
|
|
136
|
-
|
|
137
|
-
const numericPkForCache = typeof pk === 'string' ? parseInt(pk, 10) : pk;
|
|
138
|
-
|
|
139
|
-
const result = await Cache.wrap(
|
|
104
|
+
return Cache.wrap(
|
|
140
105
|
CacheKey[groupProduct ? 'GroupProduct' : 'Product'](
|
|
141
|
-
|
|
106
|
+
pk,
|
|
142
107
|
searchParams ?? new URLSearchParams()
|
|
143
108
|
),
|
|
144
109
|
locale,
|
|
@@ -155,13 +120,4 @@ export const getProductData = async ({
|
|
|
155
120
|
compressed: true
|
|
156
121
|
}
|
|
157
122
|
);
|
|
158
|
-
|
|
159
|
-
// If product data is not found, throw 404 error
|
|
160
|
-
if (!result) {
|
|
161
|
-
const error = new Error('Product not found') as Error & { status: number };
|
|
162
|
-
error.status = 404;
|
|
163
|
-
throw error;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return result;
|
|
167
123
|
};
|
package/middlewares/default.ts
CHANGED
|
@@ -214,6 +214,7 @@ const withPzDefault =
|
|
|
214
214
|
|
|
215
215
|
req.middlewareParams = {
|
|
216
216
|
commerceUrl,
|
|
217
|
+
found: true,
|
|
217
218
|
rewrites: {}
|
|
218
219
|
};
|
|
219
220
|
|
|
@@ -301,6 +302,19 @@ const withPzDefault =
|
|
|
301
302
|
)}`;
|
|
302
303
|
}
|
|
303
304
|
|
|
305
|
+
if (
|
|
306
|
+
!req.middlewareParams.found &&
|
|
307
|
+
Settings.customNotFoundEnabled
|
|
308
|
+
) {
|
|
309
|
+
const pathname = url.pathname
|
|
310
|
+
.replace(/\/+$/, '')
|
|
311
|
+
.split('/');
|
|
312
|
+
url.pathname = url.pathname.replace(
|
|
313
|
+
pathname.pop(),
|
|
314
|
+
'pz-not-found'
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
304
318
|
Settings.rewrites.forEach((rewrite) => {
|
|
305
319
|
url.pathname = url.pathname.replace(
|
|
306
320
|
rewrite.source,
|
package/middlewares/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/next",
|
|
3
3
|
"description": "Core package for Project Zero Next",
|
|
4
|
-
"version": "1.101.0-snapshot-ZERO-
|
|
4
|
+
"version": "1.101.0-snapshot-ZERO-3648-20250925195915",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"set-cookie-parser": "2.6.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@akinon/eslint-plugin-projectzero": "1.101.0-snapshot-ZERO-
|
|
38
|
+
"@akinon/eslint-plugin-projectzero": "1.101.0-snapshot-ZERO-3648-20250925195915",
|
|
39
39
|
"@babel/core": "7.26.10",
|
|
40
40
|
"@babel/preset-env": "7.26.9",
|
|
41
41
|
"@babel/preset-typescript": "7.27.0",
|