@akinon/next 2.0.0-beta.11 → 2.0.0-beta.13

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +286 -27
  2. package/api/auth.ts +99 -77
  3. package/api/cache.ts +41 -5
  4. package/api/client.ts +3 -3
  5. package/api/form.ts +85 -0
  6. package/api/image-proxy.ts +75 -0
  7. package/api/product-categories.ts +53 -0
  8. package/api/similar-product-list.ts +63 -0
  9. package/api/similar-products.ts +111 -0
  10. package/api/virtual-try-on.ts +382 -0
  11. package/bin/pz-generate-routes.js +105 -0
  12. package/bin/pz-prebuild.js +1 -1
  13. package/bin/pz-predev.js +1 -0
  14. package/components/accordion.tsx +21 -6
  15. package/components/button.tsx +1 -1
  16. package/components/file-input.tsx +65 -3
  17. package/components/input.tsx +2 -2
  18. package/components/modal.tsx +32 -16
  19. package/components/plugin-module.tsx +61 -3
  20. package/components/select.tsx +2 -2
  21. package/components/selected-payment-option-view.tsx +21 -0
  22. package/data/client/checkout.ts +130 -74
  23. package/data/server/category.ts +11 -9
  24. package/data/server/flatpage.ts +4 -1
  25. package/data/server/form.ts +4 -1
  26. package/data/server/landingpage.ts +4 -1
  27. package/data/server/list.ts +5 -4
  28. package/data/server/menu.ts +4 -1
  29. package/data/server/product.ts +97 -52
  30. package/data/server/seo.ts +4 -1
  31. package/data/server/special-page.ts +5 -4
  32. package/data/server/widget.ts +4 -1
  33. package/data/urls.ts +3 -2
  34. package/hocs/client/with-segment-defaults.tsx +2 -2
  35. package/hocs/server/with-segment-defaults.tsx +65 -20
  36. package/hooks/index.ts +1 -0
  37. package/hooks/use-loyalty-availability.ts +21 -0
  38. package/hooks/use-payment-options.ts +2 -1
  39. package/hooks/use-pz-params.ts +37 -0
  40. package/instrumentation/index.ts +0 -1
  41. package/instrumentation/node.ts +2 -20
  42. package/jest.config.js +7 -1
  43. package/lib/cache-handler.mjs +527 -15
  44. package/lib/cache.ts +260 -31
  45. package/localization/provider.tsx +2 -5
  46. package/middlewares/checkout-provider.ts +1 -1
  47. package/middlewares/complete-gpay.ts +33 -26
  48. package/middlewares/complete-masterpass.ts +34 -26
  49. package/middlewares/complete-wallet.ts +183 -0
  50. package/middlewares/default.ts +346 -235
  51. package/middlewares/index.ts +8 -2
  52. package/middlewares/locale.ts +0 -1
  53. package/middlewares/masterpass-rest-callback.ts +220 -0
  54. package/middlewares/pretty-url.ts +21 -8
  55. package/middlewares/redirection-payment.ts +33 -26
  56. package/middlewares/saved-card-redirection.ts +34 -26
  57. package/middlewares/three-d-redirection.ts +33 -26
  58. package/middlewares/url-redirection.ts +9 -15
  59. package/middlewares/wallet-complete-redirection.ts +207 -0
  60. package/package.json +20 -11
  61. package/plugins.d.ts +19 -4
  62. package/plugins.js +9 -1
  63. package/redux/actions.ts +47 -0
  64. package/redux/middlewares/checkout.ts +20 -8
  65. package/redux/middlewares/index.ts +12 -10
  66. package/redux/middlewares/pre-order/address.ts +1 -1
  67. package/redux/middlewares/pre-order/attribute-based-shipping-option.ts +1 -1
  68. package/redux/middlewares/pre-order/data-source-shipping-option.ts +1 -1
  69. package/redux/middlewares/pre-order/delivery-option.ts +1 -1
  70. package/redux/middlewares/pre-order/index.ts +3 -1
  71. package/redux/middlewares/pre-order/installment-option.ts +2 -1
  72. package/redux/middlewares/pre-order/payment-option-reset.ts +37 -0
  73. package/redux/middlewares/pre-order/payment-option.ts +1 -1
  74. package/redux/middlewares/pre-order/pre-order-validation.ts +4 -3
  75. package/redux/middlewares/pre-order/redirection.ts +2 -2
  76. package/redux/middlewares/pre-order/set-pre-order.ts +2 -2
  77. package/redux/middlewares/pre-order/shipping-option.ts +1 -1
  78. package/redux/middlewares/pre-order/shipping-step.ts +1 -1
  79. package/redux/reducers/checkout.ts +9 -1
  80. package/redux/reducers/index.ts +5 -1
  81. package/sentry/index.ts +54 -17
  82. package/types/commerce/checkout.ts +11 -1
  83. package/types/index.ts +96 -6
  84. package/types/next-auth.d.ts +2 -2
  85. package/utils/app-fetch.ts +2 -2
  86. package/utils/generate-commerce-search-params.ts +3 -2
  87. package/utils/get-checkout-path.ts +3 -0
  88. package/utils/index.ts +38 -11
  89. package/utils/override-middleware.ts +1 -0
  90. package/utils/pz-segments.ts +92 -0
  91. package/utils/redirect-ignore.ts +35 -0
  92. package/utils/redirect.ts +9 -3
  93. package/with-pz-config.js +10 -4
@@ -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
+ }
@@ -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 = ['[pz]', '[commerce]', '[locale]', '[currency]'];
29
+ const skipCatchAllRoutes = ['[...prettyurl]', '[...not_found]'];
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
+ }
@@ -2,7 +2,7 @@
2
2
 
3
3
  const runScript = require('./run-script');
4
4
 
5
- runScript('pz-run-tests.js');
6
5
  runScript('pz-install-theme.js');
7
6
  runScript('pz-pre-check-dist.js');
8
7
  runScript('pz-generate-translations.js');
8
+ runScript('pz-generate-routes.js');
package/bin/pz-predev.js CHANGED
@@ -6,3 +6,4 @@ runScript('pz-install-extensions.js');
6
6
  runScript('pz-check-env.js');
7
7
  runScript('pz-install-theme.js');
8
8
  runScript('pz-generate-translations.js');
9
+ runScript('pz-generate-routes.js');
@@ -7,35 +7,46 @@ 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
- dataTestId
20
+ subTitleClassName,
21
+ dataTestId,
22
+ contentClassName
19
23
  }: AccordionProps) => {
20
24
  const [collapse, setCollapse] = useState(isCollapse);
21
25
 
22
26
  return (
23
27
  <div
24
28
  className={twMerge(
25
- 'flex flex-col justify-center border-b border-gray-200 pb-4 mb-4 last:border-none',
29
+ 'flex flex-col justify-center border-b pb-4 mb-4 last:border-none',
26
30
  className
27
31
  )}
28
32
  >
29
33
  <div
30
- className="flex items-center justify-between cursor-pointer"
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="flex flex-col">
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 && <h4 className="text-xs text-gray-700">{subTitle}</h4>}
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 && <div className="mt-3 text-sm">{children}</div>}
60
+ {collapse && (
61
+ <div className={twMerge('mt-3 text-sm', collapseClassName)}>
62
+ {children}
63
+ </div>
64
+ )}
50
65
  </div>
51
66
  );
52
67
  };
@@ -35,7 +35,7 @@ export const Button = (props: ButtonProps) => {
35
35
 
36
36
  const buttonClasses = twMerge(
37
37
  clsx(
38
- 'px-4 text-xs transition-all duration-200 cursor-pointer',
38
+ 'px-4 text-xs transition-all duration-200',
39
39
  'inline-flex gap-2 justify-center items-center',
40
40
  variants[appearance],
41
41
  sizes[size],
@@ -1,8 +1,70 @@
1
+ import { useState } from 'react';
1
2
  import { forwardRef } from 'react';
2
- import { FileInputProps } from '../types/index';
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 fileInput(props, ref) {
6
- return <input type="file" {...props} ref={ref} />;
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
  );
@@ -40,7 +40,7 @@ export const Input = forwardRef<
40
40
  const inputClass = twMerge(
41
41
  clsx(
42
42
  'text-xs border px-2.5 h-10 placeholder:text-gray-600 peer',
43
- 'focus-visible:outline-hidden', // disable outline on focus
43
+ 'focus-visible:outline-none', // disable outline on focus
44
44
  error
45
45
  ? 'border-error focus:border-error'
46
46
  : 'border-gray-500 hover:border-black focus:border-black'
@@ -112,7 +112,7 @@ export const Input = forwardRef<
112
112
  )}
113
113
  </div>
114
114
  {error && (
115
- <span className="mt-1 text-sm text-error">{error.message}</span>
115
+ <span className="mt-1 text-sm text-error">{String(error.message)}</span>
116
116
  )}
117
117
  </div>
118
118
  );