@akinon/projectzero 2.0.0-beta.6 → 2.0.0-beta.8

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 (47) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/app-template/.env.example +2 -0
  3. package/app-template/.gitignore +2 -0
  4. package/app-template/CHANGELOG.md +51 -0
  5. package/app-template/README.md +6 -0
  6. package/app-template/config/prebuild-tests.json +5 -0
  7. package/app-template/next.config.ts +4 -1
  8. package/app-template/package.json +24 -22
  9. package/app-template/public/locales/en/account.json +4 -0
  10. package/app-template/public/locales/en/common.json +6 -0
  11. package/app-template/public/locales/tr/account.json +4 -0
  12. package/app-template/public/locales/tr/common.json +6 -0
  13. package/app-template/src/__tests__/middleware-matcher.test.ts +135 -0
  14. package/app-template/src/app/[commerce]/[locale]/[currency]/account/orders/[id]/page.tsx +65 -3
  15. package/app-template/src/app/[commerce]/[locale]/[currency]/error.tsx +12 -15
  16. package/app-template/src/app/[commerce]/[locale]/[currency]/xml-sitemap/[node]/route.ts +47 -1
  17. package/app-template/src/components/select.tsx +1 -1
  18. package/app-template/src/components/tabs.tsx +2 -2
  19. package/app-template/src/redux/middlewares/category.ts +1 -1
  20. package/app-template/src/redux/reducers/category.ts +1 -1
  21. package/app-template/src/utils/convert-facet-search-params.ts +1 -1
  22. package/app-template/src/views/account/orders/order-cancellation-item.tsx +0 -19
  23. package/app-template/src/views/basket/basket-item.tsx +1 -0
  24. package/app-template/src/views/category/category-active-filters.tsx +1 -1
  25. package/app-template/src/views/category/category-header.tsx +1 -1
  26. package/app-template/src/views/category/category-info.tsx +3 -3
  27. package/app-template/src/views/category/filters/index.tsx +2 -2
  28. package/app-template/src/views/checkout/steps/payment/index.tsx +1 -1
  29. package/app-template/src/views/header/action-menu.tsx +6 -3
  30. package/app-template/src/views/header/mini-basket.tsx +13 -2
  31. package/app-template/src/views/installment-options/index.tsx +1 -1
  32. package/app-template/src/views/otp-login/index.tsx +12 -14
  33. package/codemods/sentry-9/index.js +30 -0
  34. package/codemods/sentry-9/remove-sentry-configs.js +14 -0
  35. package/codemods/sentry-9/remove-sentry-dependency.js +25 -0
  36. package/codemods/sentry-9/replace-error-page.js +32 -0
  37. package/commands/codemod.ts +18 -0
  38. package/commands/index.ts +3 -1
  39. package/dist/codemods/sentry-9/templates/error.js +14 -0
  40. package/dist/commands/codemod.js +16 -0
  41. package/dist/commands/index.js +3 -1
  42. package/package.json +1 -1
  43. package/app-template/sentry.client.config.ts +0 -16
  44. package/app-template/sentry.edge.config.ts +0 -3
  45. package/app-template/sentry.properties +0 -4
  46. package/app-template/sentry.server.config.ts +0 -3
  47. package/app-template/src/app/[commerce]/[locale]/[currency]/product/[pk]/loading.tsx +0 -67
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @akinon/projectzero
2
2
 
3
+ ## 2.0.0-beta.8
4
+
5
+ ## 2.0.0-beta.7
6
+
3
7
  ## 2.0.0-beta.6
4
8
 
5
9
  ### Minor Changes
@@ -1,9 +1,11 @@
1
1
  NEXTAUTH_SECRET=PDpBb/aSJESgBbPLHw1+jveHXqyvkC7GC1Z82jvE04s=
2
+ # When using dev-ssl command, NEXTAUTH_URL should be updated to https://localhost:3000
2
3
  NEXTAUTH_URL=http://localhost:3000
3
4
  NEXT_PUBLIC_MAP_API_KEY=YOUR_MAP_API_KEY
4
5
  NEXT_PUBLIC_GTM_KEY=GTM_KEY
5
6
  NEXT_PUBLIC_URL=http://localhost:3000
6
7
  SERVICE_BACKEND_URL=https://02fde10fee4440269e695aa10707dfaf.lb.akinoncloud.com
8
+ SITEMAP_S3_BUCKET_NAME=0fb534
7
9
 
8
10
  # LOG_LEVEL_DEV=debug # For more details, please refer to the Logging documentation.
9
11
 
@@ -65,3 +65,5 @@ next.config.wizardcopy.js
65
65
 
66
66
  certificates
67
67
  public/locales/*/index.json
68
+
69
+ certificates
@@ -1,5 +1,56 @@
1
1
  # projectzeronext
2
2
 
3
+ ## 2.0.0-beta.8
4
+
5
+ ### Minor Changes
6
+
7
+ - 071d0f5: ZERO-3352: Resolve Single item size exceeds maxSize error and upgrade dependencies
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [071d0f5]
12
+ - @akinon/next@2.0.0-beta.8
13
+ - @akinon/pz-akifast@2.0.0-beta.8
14
+ - @akinon/pz-b2b@2.0.0-beta.8
15
+ - @akinon/pz-basket-gift-pack@2.0.0-beta.8
16
+ - @akinon/pz-bkm@2.0.0-beta.8
17
+ - @akinon/pz-checkout-gift-pack@2.0.0-beta.8
18
+ - @akinon/pz-click-collect@2.0.0-beta.8
19
+ - @akinon/pz-credit-payment@2.0.0-beta.8
20
+ - @akinon/pz-gpay@2.0.0-beta.8
21
+ - @akinon/pz-masterpass@2.0.0-beta.8
22
+ - @akinon/pz-one-click-checkout@2.0.0-beta.8
23
+ - @akinon/pz-otp@2.0.0-beta.8
24
+ - @akinon/pz-pay-on-delivery@2.0.0-beta.8
25
+ - @akinon/pz-saved-card@2.0.0-beta.8
26
+ - @akinon/pz-tabby-extension@2.0.0-beta.8
27
+ - @akinon/pz-tamara-extension@2.0.0-beta.8
28
+
29
+ ## 2.0.0-beta.7
30
+
31
+ ### Minor Changes
32
+
33
+ - 1bbba84: ZERO-3291: Update next.js and related dependencies to version 15.2.3
34
+
35
+ ### Patch Changes
36
+
37
+ - @akinon/next@2.0.0-beta.7
38
+ - @akinon/pz-akifast@2.0.0-beta.7
39
+ - @akinon/pz-b2b@2.0.0-beta.7
40
+ - @akinon/pz-basket-gift-pack@2.0.0-beta.7
41
+ - @akinon/pz-bkm@2.0.0-beta.7
42
+ - @akinon/pz-checkout-gift-pack@2.0.0-beta.7
43
+ - @akinon/pz-click-collect@2.0.0-beta.7
44
+ - @akinon/pz-credit-payment@2.0.0-beta.7
45
+ - @akinon/pz-gpay@2.0.0-beta.7
46
+ - @akinon/pz-masterpass@2.0.0-beta.7
47
+ - @akinon/pz-one-click-checkout@2.0.0-beta.7
48
+ - @akinon/pz-otp@2.0.0-beta.7
49
+ - @akinon/pz-pay-on-delivery@2.0.0-beta.7
50
+ - @akinon/pz-saved-card@2.0.0-beta.7
51
+ - @akinon/pz-tabby-extension@2.0.0-beta.7
52
+ - @akinon/pz-tamara-extension@2.0.0-beta.7
53
+
3
54
  ## 2.0.0-beta.6
4
55
 
5
56
  ### Minor Changes
@@ -6,6 +6,12 @@ Headless Akinon Commerce Cloud Storefront.
6
6
 
7
7
  Run `cp .env.example .env && yarn` command at project root and all dependencies should be installed.
8
8
 
9
+ ## Environment Variables
10
+
11
+ ### Required Environment Variables
12
+
13
+ - `SITEMAP_S3_BUCKET_NAME`: S3 bucket name for XML sitemaps (e.g., "0fb534"). This is required for the sitemap route to function correctly.
14
+
9
15
  ### Build
10
16
 
11
17
  To build the app, run the following command:
@@ -0,0 +1,5 @@
1
+ {
2
+ "tests": [
3
+ "test:middleware"
4
+ ]
5
+ }
@@ -26,7 +26,10 @@ const withPwaConfig = withPWA({
26
26
 
27
27
  const sentryConfig = {
28
28
  silent: true,
29
- dryRun: !process.env.SENTRY_DSN
29
+ dryRun: !process.env.SENTRY_DSN,
30
+ org: 'akinon'
31
+ // project: 'enter_your_project_name_here',
32
+ // authToken: 'enter_your_auth_token_here'
30
33
  };
31
34
 
32
35
  const enhancedConfig = withPzConfig(nextConfig);
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "projectzeronext",
3
- "version": "2.0.0-beta.6",
3
+ "version": "2.0.0-beta.8",
4
4
  "private": true,
5
5
  "license": "MIT",
6
6
  "scripts": {
7
7
  "clean": "rm -rf node_modules && rm -rf .next",
8
8
  "dev": "next dev",
9
+ "dev-ssl": "next dev --experimental-https",
9
10
  "build": "next build",
10
11
  "start": "next start -p 8008",
11
12
  "type-check": "tsc",
@@ -19,32 +20,33 @@
19
20
  "prebuild": "pz-prebuild",
20
21
  "postbuild": "pz-postbuild",
21
22
  "poststart": "pz-poststart",
22
- "prestart": "pz-prestart"
23
+ "prestart": "pz-prestart",
24
+ "test:middleware": "jest middleware-matcher.test.ts --bail"
23
25
  },
24
26
  "dependencies": {
25
- "@akinon/next": "2.0.0-beta.6",
26
- "@akinon/pz-akifast": "2.0.0-beta.6",
27
- "@akinon/pz-b2b": "2.0.0-beta.6",
28
- "@akinon/pz-basket-gift-pack": "2.0.0-beta.6",
29
- "@akinon/pz-bkm": "2.0.0-beta.6",
30
- "@akinon/pz-checkout-gift-pack": "2.0.0-beta.6",
31
- "@akinon/pz-click-collect": "2.0.0-beta.6",
32
- "@akinon/pz-credit-payment": "2.0.0-beta.6",
33
- "@akinon/pz-gpay": "2.0.0-beta.6",
34
- "@akinon/pz-masterpass": "2.0.0-beta.6",
35
- "@akinon/pz-one-click-checkout": "2.0.0-beta.6",
36
- "@akinon/pz-otp": "2.0.0-beta.6",
37
- "@akinon/pz-pay-on-delivery": "2.0.0-beta.6",
38
- "@akinon/pz-saved-card": "2.0.0-beta.6",
39
- "@akinon/pz-tabby-extension": "2.0.0-beta.6",
27
+ "@akinon/next": "2.0.0-beta.8",
28
+ "@akinon/pz-akifast": "2.0.0-beta.8",
29
+ "@akinon/pz-b2b": "2.0.0-beta.8",
30
+ "@akinon/pz-basket-gift-pack": "2.0.0-beta.8",
31
+ "@akinon/pz-bkm": "2.0.0-beta.8",
32
+ "@akinon/pz-checkout-gift-pack": "2.0.0-beta.8",
33
+ "@akinon/pz-click-collect": "2.0.0-beta.8",
34
+ "@akinon/pz-credit-payment": "2.0.0-beta.8",
35
+ "@akinon/pz-gpay": "2.0.0-beta.8",
36
+ "@akinon/pz-masterpass": "2.0.0-beta.8",
37
+ "@akinon/pz-one-click-checkout": "2.0.0-beta.8",
38
+ "@akinon/pz-otp": "2.0.0-beta.8",
39
+ "@akinon/pz-pay-on-delivery": "2.0.0-beta.8",
40
+ "@akinon/pz-saved-card": "2.0.0-beta.8",
41
+ "@akinon/pz-tabby-extension": "2.0.0-beta.8",
42
+ "@akinon/pz-tamara-extension": "2.0.0-beta.8",
40
43
  "@hookform/resolvers": "2.9.0",
41
44
  "@next/third-parties": "14.1.0",
42
45
  "@react-google-maps/api": "2.17.1",
43
- "@sentry/nextjs": "7.116.0",
44
46
  "@tailwindcss/postcss": "4.0.0",
45
47
  "dayjs": "1.11.5",
46
48
  "lossless-json": "2.0.5",
47
- "next": "15.1.3",
49
+ "next": "15.3.1",
48
50
  "next-auth": "4.24.11",
49
51
  "next-pwa": "5.6.0",
50
52
  "pino": "8.11.0",
@@ -58,11 +60,10 @@
58
60
  "react-string-replace": "1.1.0",
59
61
  "start-server-and-test": "2.0.3",
60
62
  "tailwind-merge": "1.8.0",
61
- "tailwindcss": "4.0.0",
62
63
  "yup": "0.32.11"
63
64
  },
64
65
  "devDependencies": {
65
- "@akinon/eslint-plugin-projectzero": "2.0.0-beta.6",
66
+ "@akinon/eslint-plugin-projectzero": "2.0.0-beta.8",
66
67
  "@semantic-release/changelog": "6.0.2",
67
68
  "@semantic-release/exec": "6.0.3",
68
69
  "@semantic-release/git": "10.0.1",
@@ -87,6 +88,7 @@
87
88
  "husky": "8.0.0",
88
89
  "jest": "29.7.0",
89
90
  "jest-css-modules-transform": "4.3.0",
91
+ "jest-environment-jsdom": "29.7.0",
90
92
  "lint-staged": "13.1.0",
91
93
  "prettier": "3.4.2",
92
94
  "react-number-format": "5.4.2",
@@ -98,7 +100,7 @@
98
100
  "stylelint-config-standard": "25.0.0",
99
101
  "stylelint-scss": "4.2.0",
100
102
  "stylelint-selector-bem-pattern": "2.1.1",
101
- "tailwindcss": "4.0.0",
103
+ "tailwindcss": "4.1.3",
102
104
  "ts-jest": "29.1.1",
103
105
  "ts-node": "10.7.0",
104
106
  "typescript": "5.7.2"
@@ -254,6 +254,10 @@
254
254
  "cancelled": "Cancelled",
255
255
  "cancellation_request_recieved": "Cancellation Request Recieved",
256
256
  "close_button": "Close",
257
+ "return_status": "Return Status",
258
+ "rejected": "Return Rejected",
259
+ "completed": "Return Completed",
260
+ "explanation": "Explanation",
257
261
  "success": {
258
262
  "title": "Success",
259
263
  "description": "Your request have been recieved and will be evaluated as soon as possible."
@@ -38,6 +38,12 @@
38
38
  "description": "Please try again later.",
39
39
  "link_text": "Return to home page"
40
40
  },
41
+ "client_error": {
42
+ "title": "We encountered a problem with the page.",
43
+ "description": "This appears to be a client-side issue. Please try refreshing the page or clearing your browser cache.",
44
+ "link_text": "Return to home page"
45
+ },
46
+ "try_again": "Try Again",
41
47
  "breadcrumb": {
42
48
  "homepage": "Homepage"
43
49
  },
@@ -254,6 +254,10 @@
254
254
  "cancelled": "İptal edildi",
255
255
  "cancellation_request_recieved": "İptal talebi alındı",
256
256
  "close_button": "Kapat",
257
+ "return_status": "İade Durumu",
258
+ "rejected": "İade reddedildi",
259
+ "completed": "İade tamamlandı",
260
+ "explanation": "Açıklama",
257
261
  "success": {
258
262
  "title": "Başarılı",
259
263
  "description": "Talebiniz alınmış olup en kısa sürede değerlendirilecektir."
@@ -38,6 +38,12 @@
38
38
  "description": "Lütfen daha sonra tekrar deneyiniz.",
39
39
  "link_text": "Ana sayfaya dön"
40
40
  },
41
+ "client_error": {
42
+ "title": "Sayfada bir sorunla karşılaştık.",
43
+ "description": "Bu bir tarayıcı hatası gibi görünüyor. Lütfen sayfayı yenileyin veya tarayıcı önbelleğinizi temizleyin.",
44
+ "link_text": "Ana sayfaya dön"
45
+ },
46
+ "try_again": "Tekrar Dene",
41
47
  "breadcrumb": {
42
48
  "homepage": "Anasayfa"
43
49
  },
@@ -0,0 +1,135 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ describe('Middleware matcher regex tests', () => {
5
+ const middlewareFilePath = path.resolve(__dirname, '../middleware.ts');
6
+ const middlewareContent = fs.readFileSync(middlewareFilePath, 'utf8');
7
+
8
+ let actualMatcherStrings: string[] = [];
9
+ let matcherPatterns: RegExp[] = [];
10
+
11
+ const matcherBlockRegex = middlewareContent.match(/matcher:\s*\[([\s\S]*?)\](?=\s*[,}])/);
12
+
13
+ if (matcherBlockRegex && matcherBlockRegex[1]) {
14
+ const matcherContentInsideBrackets = matcherBlockRegex[1];
15
+
16
+ actualMatcherStrings = matcherContentInsideBrackets
17
+ .split(',')
18
+ .map(line => {
19
+ const uncommentedLine = line.replace(/\/\/.*$/, '').trim();
20
+ const quoteMatch = uncommentedLine.match(/^(['"])(.*)\1$/);
21
+ return quoteMatch ? quoteMatch[2] : null;
22
+ })
23
+ .filter((pattern): pattern is string => pattern !== null && pattern !== '');
24
+
25
+ matcherPatterns = matcherContentInsideBrackets
26
+ .split(',')
27
+ .map((pattern) => pattern.trim())
28
+ .filter(Boolean)
29
+ .map((pattern) => {
30
+ let cleanPattern = pattern
31
+ .replace(/^['"]|['"]$/g, '');
32
+
33
+ try {
34
+ if (cleanPattern.includes('(?!') && cleanPattern.includes('api') && cleanPattern.includes('_next')) {
35
+ cleanPattern = '^(?!/(?:api|_next)/)(?!.*\\.\\w+$).*$';
36
+ }
37
+
38
+ return new RegExp(cleanPattern);
39
+ } catch (error) {
40
+ console.error(`Invalid simplified regex: ${cleanPattern}`, error);
41
+ return null;
42
+ }
43
+ })
44
+ .filter(Boolean) as RegExp[];
45
+ }
46
+
47
+ const testPath = (path: string): boolean => {
48
+ return matcherPatterns.some((pattern) => {
49
+ try {
50
+ const result = pattern.test(path);
51
+ return result;
52
+ } catch (error) {
53
+ console.error(
54
+ `Error testing path: ${path} | Pattern: ${pattern.toString()}`,
55
+ error
56
+ );
57
+ return false;
58
+ }
59
+ });
60
+ };
61
+
62
+ it('should NOT match api routes', () => {
63
+ const apiPaths = ['/api/products', '/api/auth/login', '/api/v1/users'];
64
+ apiPaths.forEach((path) => {
65
+ expect(testPath(path)).toBe(false);
66
+ });
67
+ });
68
+
69
+ it('should NOT match _next routes', () => {
70
+ const nextPaths = [
71
+ '/_next/static/chunks/main.js',
72
+ '/_next/image',
73
+ '/_next/data/build-id/products.json'
74
+ ];
75
+ nextPaths.forEach((path) => {
76
+ expect(testPath(path)).toBe(false);
77
+ });
78
+ });
79
+
80
+ it('should NOT match static files with extensions', () => {
81
+ const staticFiles = [
82
+ '/images/logo.png',
83
+ '/styles/main.css',
84
+ '/fonts/roboto.woff2',
85
+ '/favicon.ico',
86
+ '/manifest.json'
87
+ ];
88
+ staticFiles.forEach((path) => {
89
+ expect(testPath(path)).toBe(false);
90
+ });
91
+ });
92
+
93
+ it('should match dynamic routes and specific patterns', () => {
94
+ const validPaths = [
95
+ '/profile/settings',
96
+ '/dashboard/stats',
97
+ '/products/123'
98
+ ];
99
+ validPaths.forEach((path) => {
100
+ expect(testPath(path)).toBe(true);
101
+ });
102
+ });
103
+
104
+ it('should match checkout-with-token routes', () => {
105
+ const expectedRegexString = '\'/(.*orders\\\\/checkout-with-token.*)\'';
106
+ expect(middlewareContent.includes(expectedRegexString)).toBe(true);
107
+
108
+ const checkoutPaths = [
109
+ '/orders/checkout-with-token/123',
110
+ '/orders/checkout-with-token/abc-xyz',
111
+ '/orders/checkout-with-token'
112
+ ];
113
+ checkoutPaths.forEach((path) => {
114
+ expect(testPath(path)).toBe(true);
115
+ });
116
+ });
117
+
118
+ it('should contain the exact specific extensions regex string in the file content', () => {
119
+ const expectedRegexString = '\'/(.+\\\\.)(html|htm|aspx|asp|php)\'';
120
+ expect(middlewareContent.includes(expectedRegexString)).toBe(true);
121
+ });
122
+
123
+ it('should include the sitemap pattern specifically within the matcher array', () => {
124
+ const sitemapPattern = '/(.*sitemap\\\\.xml)';
125
+ expect(actualMatcherStrings).toContain(sitemapPattern);
126
+ });
127
+
128
+ it('should verify that api pattern is excluded in the matcher configuration', () => {
129
+ expect(/api/.test(middlewareContent)).toBe(true);
130
+ });
131
+
132
+ it('should verify that _next pattern is excluded in the matcher configuration', () => {
133
+ expect(/_next/.test(middlewareContent)).toBe(true);
134
+ });
135
+ });
@@ -208,8 +208,7 @@ const AccountOrderDetail = ({ params }) => {
208
208
  </div>
209
209
 
210
210
  {(item.is_cancellable || item.is_refundable) &&
211
- order.is_cancellable &&
212
- item.status.value == '400' && (
211
+ order.is_cancellable && (
213
212
  <div className="lg:ml-24">
214
213
  <Link
215
214
  href={`${ROUTES.ACCOUNT_ORDERS}/${order.id}/cancellation`}
@@ -227,7 +226,6 @@ const AccountOrderDetail = ({ params }) => {
227
226
  </div>
228
227
  )}
229
228
  </div>
230
-
231
229
  <div className="flex flex-col justify-center items-end lg:ml-6 lg:min-w-[7rem]">
232
230
  {parseFloat(item.retail_price) >
233
231
  parseFloat(item.price) && (
@@ -251,6 +249,70 @@ const AccountOrderDetail = ({ params }) => {
251
249
  );
252
250
  })}
253
251
  </div>
252
+
253
+ {group.map((item) =>
254
+ item.cancellationrequest_set?.map((cancellationItem) => {
255
+ const status = cancellationItem.status?.value;
256
+ const isRejected = status === 'rejected';
257
+ const isCompleted = status === 'completed';
258
+
259
+ const filteredImages =
260
+ cancellationItem.cancellation_request_image_set?.filter(
261
+ (img) =>
262
+ isRejected
263
+ ? !img.is_uploaded_by_user
264
+ : isCompleted
265
+ ? img.is_uploaded_by_user
266
+ : false
267
+ ) || [];
268
+
269
+ const statusText = isRejected
270
+ ? t('account.my_orders.return.rejected')
271
+ : isCompleted
272
+ ? t('account.my_orders.return.completed')
273
+ : null;
274
+
275
+ if (!statusText || filteredImages.length === 0) return null;
276
+
277
+ return (
278
+ <div
279
+ className="w-full px-4 lg:px-7"
280
+ key={cancellationItem.id}
281
+ >
282
+ <div className="flex flex-col py-2 gap-4 border-t border-gray">
283
+ <div className="flex flex-col">
284
+ <div className="text-sm font-semibold">
285
+ {t('account.my_orders.return.return_status')}
286
+ <span className="font-normal"> {statusText}</span>
287
+ </div>
288
+
289
+ <div className="flex gap-2 mt-2 flex-wrap">
290
+ {filteredImages.map((img) => (
291
+ <div className="flex flex-col gap-2" key={img.id}>
292
+ <Link href={img.image} target="_blank">
293
+ <Image
294
+ src={img.image}
295
+ width={112}
296
+ height={150}
297
+ alt={img.description}
298
+ />
299
+ </Link>
300
+
301
+ {img.description && (
302
+ <p className="text-xs">
303
+ {t('account.my_orders.return.explanation')}:
304
+ {img.description}
305
+ </p>
306
+ )}
307
+ </div>
308
+ ))}
309
+ </div>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ );
314
+ })
315
+ )}
254
316
  </div>
255
317
  );
256
318
  })}
@@ -1,20 +1,17 @@
1
1
  'use client';
2
2
 
3
- import { useLocalization } from '@akinon/next/hooks';
4
- import { Link } from '@theme/components';
5
- import { ROUTES } from '@theme/routes';
3
+ import { useSentryUncaughtErrors } from '@akinon/next/hooks';
4
+ import PzErrorPage from '@akinon/next/views/error-page';
6
5
 
7
- export default function Error() {
8
- const { t } = useLocalization();
6
+ export default function ErrorPage({
7
+ error,
8
+ reset
9
+ }: {
10
+ error: Error & { digest?: string; isServerError?: boolean };
11
+ reset: () => void;
12
+ }) {
13
+ // DO NOT REMOVE THIS LINE TO REPORT UNCAUGHT ERRORS TO SENTRY
14
+ useSentryUncaughtErrors(error);
9
15
 
10
- return (
11
- <section className="text-center px-6 my-14 md:px-0 md:m-14">
12
- <div className="text-7xl font-bold md:text-8xl">500</div>
13
- <h1 className="text-lg md:text-xl"> {t('common.page_500.title')} </h1>
14
- <p className="text-lg md:text-xl"> {t('common.page_500.description')} </p>
15
- <Link href={ROUTES.HOME} className="text-lg underline">
16
- {t('common.page_500.link_text')}
17
- </Link>
18
- </section>
19
- );
16
+ return <PzErrorPage error={error} reset={reset} />;
20
17
  }
@@ -1,5 +1,18 @@
1
1
  import { urlLocaleMatcherRegex } from '@akinon/next/utils';
2
2
 
3
+ /**
4
+ * XML Sitemap Route
5
+ *
6
+ * This route serves XML sitemaps from an S3 bucket.
7
+ *
8
+ * Required environment variables:
9
+ * - SITEMAP_S3_BUCKET_NAME: The name of the S3 bucket containing the sitemaps
10
+ * Example: "0fb534"
11
+ *
12
+ * If the environment variable is not set, the route will return a 503 Service Unavailable
13
+ * response with a JSON error message.
14
+ */
15
+
3
16
  export const dynamic = 'force-dynamic';
4
17
 
5
18
  export async function GET(request: Request, context: { params }) {
@@ -7,9 +20,42 @@ export async function GET(request: Request, context: { params }) {
7
20
  const url = new URL(request.url);
8
21
  const matchedLocale = url.pathname.match(urlLocaleMatcherRegex);
9
22
 
23
+ const s3BucketName = process.env.SITEMAP_S3_BUCKET_NAME;
24
+
25
+ if (!s3BucketName) {
26
+ return new Response(
27
+ JSON.stringify({
28
+ error: 'Configuration error',
29
+ message: 'Please set the SITEMAP_S3_BUCKET_NAME environment variable'
30
+ }),
31
+ {
32
+ status: 503,
33
+ headers: {
34
+ 'Content-Type': 'application/json'
35
+ }
36
+ }
37
+ );
38
+ }
39
+
10
40
  const sitemap = await fetch(
11
- `https://s3.eu-central-1.amazonaws.com/0fb534/sitemaps/sitemaps/sitemap-${node}.xml.gz`
41
+ `https://s3.eu-central-1.amazonaws.com/${s3BucketName}/sitemaps/sitemaps/sitemap-${node}.xml.gz`
12
42
  );
43
+
44
+ if (!sitemap.ok) {
45
+ return new Response(
46
+ JSON.stringify({
47
+ error: 'Sitemap not found',
48
+ message: `Failed to fetch sitemap for node: ${node}`
49
+ }),
50
+ {
51
+ status: 503,
52
+ headers: {
53
+ 'Content-Type': 'application/json'
54
+ }
55
+ }
56
+ );
57
+ }
58
+
13
59
  let sitemapContent = await sitemap.text();
14
60
 
15
61
  sitemapContent = sitemapContent.replace(
@@ -48,7 +48,7 @@ const Select = forwardRef<HTMLSelectElement, SelectProps>((props, ref) => {
48
48
  className
49
49
  )}
50
50
  >
51
- {options.map((option) => (
51
+ {options?.map((option) => (
52
52
  <option
53
53
  key={option.value}
54
54
  value={option.value}
@@ -39,7 +39,7 @@ export const Tabs = (props: Props) => {
39
39
  tabBarPosition === 'left' || tabBarPosition === 'right'
40
40
  })}
41
41
  >
42
- {children.map((item, index) => (
42
+ {children?.map((item, index) => (
43
43
  <Tab
44
44
  key={item.props.title}
45
45
  title={item.props.title}
@@ -52,7 +52,7 @@ export const Tabs = (props: Props) => {
52
52
  ))}
53
53
  </ul>
54
54
 
55
- <div className="w-full">{children[selectedTabIndex]}</div>
55
+ <div className="w-full">{children?.[selectedTabIndex]}</div>
56
56
  </div>
57
57
  );
58
58
  };
@@ -4,7 +4,7 @@ import { setSelectedFacets } from '@theme/redux/reducers/category';
4
4
 
5
5
  const getSelectedFacets = (facets: Array<Facet>) => {
6
6
  return facets
7
- .map((facet) => {
7
+ ?.map((facet) => {
8
8
  return {
9
9
  ...facet,
10
10
  data: {
@@ -32,7 +32,7 @@ const categorySlice = createSlice({
32
32
  toggleFacet(state, action) {
33
33
  const facets = JSON.parse(JSON.stringify(state.facets));
34
34
 
35
- state.selectedFacets = facets.map((facet) => {
35
+ state.selectedFacets = facets?.map((facet) => {
36
36
  if (facet.key === action.payload.facet.key) {
37
37
  facet.data.choices = facet.data.choices
38
38
  .map((choice) => {
@@ -1,7 +1,7 @@
1
1
  import { Facet } from '@akinon/next/types';
2
2
 
3
3
  const convertFacetSearchParams = (facets: Facet[]) => {
4
- const _facets: string[][] = facets.flatMap((facet) => {
4
+ const _facets: string[][] = facets?.flatMap((facet) => {
5
5
  return [
6
6
  ...facet.data.choices.map((choice) => {
7
7
  return [facet.search_key, choice.value as string];
@@ -94,25 +94,6 @@ export const OrderCancellationItem = ({
94
94
  {selectOption}
95
95
  </div>
96
96
  <div>{fileInput}</div>
97
- {item.active_cancellation_request?.cancellation_request_image_set && (
98
- <div className="flex flex-wrap gap-2">
99
- {item.active_cancellation_request?.cancellation_request_image_set?.map(
100
- (item) => {
101
- return (
102
- <div className="relative w-16 h-16" key={item.id}>
103
- <Image
104
- src={item.image}
105
- alt={item.description}
106
- aspectRatio={1}
107
- sizes="64px"
108
- fill
109
- />
110
- </div>
111
- );
112
- }
113
- )}
114
- </div>
115
- )}
116
97
  </div>
117
98
  </div>
118
99
  );
@@ -87,6 +87,7 @@ export const BasketItem = (props: Props) => {
87
87
  <li
88
88
  key={basketItem.id}
89
89
  className="flex border-b border-gray-200 py-3 relative"
90
+ data-testid="basket-item"
90
91
  >
91
92
  <div className="w-20 lg:w-[105px] mr-4 shrink-0">
92
93
  <Link href={basketItem.product.absolute_url} passHref>
@@ -69,7 +69,7 @@ const CategoryActiveFilters = () => {
69
69
 
70
70
  return (
71
71
  <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-2 mb-4">
72
- {facets.map((facet) =>
72
+ {facets?.map((facet) =>
73
73
  facet?.data?.choices
74
74
  ?.filter((choice) => choice.is_selected)
75
75
  ?.map(
@@ -121,7 +121,7 @@ export const CategoryHeader = (props: Props) => {
121
121
  </Button>
122
122
  <Select
123
123
  options={sortOptions}
124
- value={sortOptions.find(({ is_selected }) => is_selected).value}
124
+ value={sortOptions?.find(({ is_selected }) => is_selected)?.value}
125
125
  data-testid="list-sorter"
126
126
  onChange={(e) => {
127
127
  handleSelectFilter({
@@ -102,7 +102,7 @@ export default function ListPage(props: ListPageProps) {
102
102
  </div>
103
103
  )}
104
104
 
105
- {data.products.length === 0 && page > 1 && <LoaderSpinner />}
105
+ {data.products?.length === 0 && page > 1 && <LoaderSpinner />}
106
106
 
107
107
  <div
108
108
  className={clsx('grid gap-x-4 gap-y-12 grid-cols-2', {
@@ -111,7 +111,7 @@ export default function ListPage(props: ListPageProps) {
111
111
  'lg:grid-cols-3': layoutSize === 3
112
112
  })}
113
113
  >
114
- {data.products.map((product, index) => (
114
+ {data?.products?.map((product, index) => (
115
115
  <ProductItem
116
116
  key={product.pk}
117
117
  product={product}
@@ -121,7 +121,7 @@ export default function ListPage(props: ListPageProps) {
121
121
  />
122
122
  ))}
123
123
  </div>
124
- {data.products.length > 0 && (
124
+ {data?.products?.length > 0 && (
125
125
  <Pagination
126
126
  total={data.pagination.total_count}
127
127
  limit={data.pagination.page_size}
@@ -23,7 +23,7 @@ export const Filters = (props: Props) => {
23
23
  const [isPending, startTransition] = useTransition();
24
24
 
25
25
  const haveFilter = useMemo(() => {
26
- return facets.some((facet) =>
26
+ return facets?.some((facet) =>
27
27
  facet?.data?.choices?.some((choice) => choice.is_selected)
28
28
  );
29
29
  }, [facets]);
@@ -50,7 +50,7 @@ export const Filters = (props: Props) => {
50
50
  <span>{t('category.filters.ready_to_wear')}</span>
51
51
  </div>
52
52
 
53
- {facets.map((facet) => {
53
+ {facets?.map((facet) => {
54
54
  return (
55
55
  <FilterItem
56
56
  key={facet.key}
@@ -18,7 +18,7 @@ const PaymentStep = () => {
18
18
  'pointer-events-none opacity-30': isPaymentStepBusy
19
19
  })}
20
20
  >
21
- <div className="w-full mt-4 flex justify-start border border-gray-400 -mb-px z-10 md:mt-0 md:border-r-0 md:border-b-0 md:w-auto order-2 md:order-none">
21
+ <div className="w-full mt-4 flex justify-start border border-gray-400 -mb-px z-10 md:mt-0 md:border-r-0 md:border-b-0 md:w-auto order-2 md:order-none overflow-x-auto">
22
22
  <PaymentOptionButtons />
23
23
  </div>
24
24
  <div className="w-full border border-solid border-gray-400 bg-white">
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { ReactNode, useMemo, useRef, useCallback } from 'react';
4
- import { useGetBasketQuery } from '@akinon/next/data/client/basket';
4
+ import { useGetMiniBasketQuery } from '@akinon/next/data/client/basket';
5
5
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
6
6
  import {
7
7
  closeMiniBasket,
@@ -29,8 +29,11 @@ interface MenuItem {
29
29
  export default function ActionMenu() {
30
30
  const dispatch = useAppDispatch();
31
31
 
32
- const { data } = useGetBasketQuery();
33
- const totalQuantity = useMemo(() => data?.total_quantity ?? 0, [data]);
32
+ const { data: miniBasket } = useGetMiniBasketQuery();
33
+ const totalQuantity = useMemo(
34
+ () => miniBasket?.total_quantity ?? 0,
35
+ [miniBasket]
36
+ );
34
37
 
35
38
  const { open: miniBasketOpen } = useAppSelector(
36
39
  (state) => state.root.miniBasket
@@ -5,6 +5,7 @@ import clsx from 'clsx';
5
5
  import {
6
6
  basketApi,
7
7
  useGetBasketQuery,
8
+ useGetMiniBasketQuery,
8
9
  useUpdateQuantityMutation
9
10
  } from '@akinon/next/data/client/basket';
10
11
  import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
@@ -150,13 +151,23 @@ export default function MiniBasket() {
150
151
  (state) => state.root.miniBasket
151
152
  );
152
153
  const dispatch = useAppDispatch();
153
- const { data: basket, isLoading, isSuccess } = useGetBasketQuery();
154
+ const {
155
+ data: basket,
156
+ isLoading,
157
+ isSuccess
158
+ } = useGetBasketQuery(undefined, {
159
+ skip: !miniBasketOpen
160
+ });
161
+ const { data: miniBasket } = useGetMiniBasketQuery();
154
162
  const { t } = useLocalization();
155
163
  const { highlightedItem } = useAppSelector((state) => state.root.miniBasket);
156
164
  const [highlightedItemPk, setHighlightedItemPk] = useState(0);
157
165
  const [sortedBasket, setSortedBasket] = useState([]);
158
166
 
159
- const totalQuantity = useMemo(() => basket?.total_quantity ?? 0, [basket]);
167
+ const totalQuantity = useMemo(
168
+ () => miniBasket?.total_quantity ?? 0,
169
+ [miniBasket]
170
+ );
160
171
  const miniBasketList = useRef(null);
161
172
 
162
173
  useEffect(() => {
@@ -22,7 +22,7 @@ const InstallmentOptions = (props: InstallmentProps) => {
22
22
 
23
23
  useEffect(() => {
24
24
  if (isSuccess && data) {
25
- setActiveCardSlug(data.results[0].slug);
25
+ setActiveCardSlug(data.results?.[0]?.slug);
26
26
  }
27
27
  }, [isSuccess, data]);
28
28
 
@@ -10,9 +10,10 @@ import { yupResolver } from '@hookform/resolvers/yup';
10
10
  import clsx from 'clsx';
11
11
  import { useLocalization } from '@akinon/next/hooks';
12
12
  import { useOtpLoginMutation } from '@akinon/next/data/client/user';
13
- import { useAppSelector } from '@akinon/next/redux/hooks';
13
+ import { useAppDispatch, useAppSelector } from '@akinon/next/redux/hooks';
14
14
  import PluginModule, { Component } from '@akinon/next/components/plugin-module';
15
15
  import { AuthError } from '@akinon/next/types';
16
+ import { showPopup } from '@akinon/pz-otp/src/redux/reducer';
16
17
 
17
18
  const loginFormSchema = (t) =>
18
19
  yup.object().shape({
@@ -25,9 +26,9 @@ const loginFormSchema = (t) =>
25
26
  });
26
27
 
27
28
  export const OtpLogin = () => {
29
+ const dispatch = useAppDispatch();
28
30
  const { user_phone_format } = useAppSelector((state) => state.config);
29
31
  const { t, locale } = useLocalization();
30
- const [showOtpModal, setShowOtpModal] = useState(false);
31
32
  const [otpLoginMutation] = useOtpLoginMutation();
32
33
 
33
34
  const {
@@ -79,7 +80,7 @@ export const OtpLogin = () => {
79
80
  })
80
81
  .unwrap()
81
82
  .then(() => {
82
- setShowOtpModal(true);
83
+ dispatch(showPopup());
83
84
  })
84
85
  .catch((error) => {
85
86
  if (error.status === 429) {
@@ -136,17 +137,14 @@ export const OtpLogin = () => {
136
137
  </Button>
137
138
  </form>
138
139
 
139
- {showOtpModal && (
140
- <PluginModule
141
- component={Component.Otp}
142
- props={{
143
- setShowPopup: setShowOtpModal,
144
- data: getValues(),
145
- submitAction: loginHandler,
146
- error: formError
147
- }}
148
- />
149
- )}
140
+ <PluginModule
141
+ component={Component.Otp}
142
+ props={{
143
+ data: getValues(),
144
+ submitAction: loginHandler,
145
+ error: formError
146
+ }}
147
+ />
150
148
  </section>
151
149
  );
152
150
  };
@@ -0,0 +1,30 @@
1
+ const path = require('path');
2
+ const { execSync } = require('child_process');
3
+
4
+ const codemodScripts = [
5
+ path.resolve(__dirname, 'remove-sentry-dependency.js'),
6
+ path.resolve(__dirname, 'remove-sentry-configs.js'),
7
+ path.resolve(__dirname, 'replace-error-page.js')
8
+ ];
9
+
10
+ const transform = () => {
11
+ const workingDir = path.resolve(process.cwd());
12
+
13
+ codemodScripts.forEach((script) => {
14
+ try {
15
+ execSync(
16
+ `jscodeshift --ignore-pattern="**/node_modules/**" -t ${script} ${workingDir} --extensions=json,ts,tsx,js,jsx,properties,md`,
17
+ {
18
+ cwd: workingDir,
19
+ stdio: 'inherit'
20
+ }
21
+ );
22
+ } catch (e) {
23
+ console.error(e);
24
+ }
25
+ });
26
+ };
27
+
28
+ module.exports = {
29
+ transform
30
+ };
@@ -0,0 +1,14 @@
1
+ const fs = require('fs');
2
+
3
+ const transform = (fileInfo, api, options) => {
4
+ const filePath = fileInfo.path;
5
+ const regex = /sentry\.\w+\.config\.(ts|js)$|sentry\.properties$/i;
6
+
7
+ if (regex.test(filePath) && fs.existsSync(filePath)) {
8
+ fs.unlinkSync(filePath);
9
+ }
10
+
11
+ return fileInfo.source;
12
+ };
13
+
14
+ module.exports = transform;
@@ -0,0 +1,25 @@
1
+ const transform = (fileInfo, api, options) => {
2
+ if (fileInfo.path.endsWith('package.json')) {
3
+ const packageJson = JSON.parse(fileInfo.source);
4
+
5
+ if (
6
+ packageJson.dependencies &&
7
+ packageJson.dependencies['@sentry/nextjs']
8
+ ) {
9
+ delete packageJson.dependencies['@sentry/nextjs'];
10
+ }
11
+
12
+ if (
13
+ packageJson.devDependencies &&
14
+ packageJson.devDependencies['@sentry/nextjs']
15
+ ) {
16
+ delete packageJson.devDependencies['@sentry/nextjs'];
17
+ }
18
+
19
+ return JSON.stringify(packageJson, null, 2);
20
+ }
21
+
22
+ return fileInfo.source;
23
+ };
24
+
25
+ module.exports = transform;
@@ -0,0 +1,32 @@
1
+ const fs = require('fs');
2
+
3
+ const template = `
4
+ 'use client';
5
+
6
+ import { useSentryUncaughtErrors } from '@akinon/next/hooks';
7
+ import PzErrorPage from '@akinon/next/views/error-page';
8
+
9
+ export default function ErrorPage({
10
+ error,
11
+ reset
12
+ }: {
13
+ error: Error & { digest?: string; isServerError?: boolean };
14
+ reset: () => void;
15
+ }) {
16
+ // DO NOT REMOVE THIS LINE TO REPORT UNCAUGHT ERRORS TO SENTRY
17
+ useSentryUncaughtErrors(error);
18
+
19
+ return <PzErrorPage error={error} reset={reset} />;
20
+ }
21
+
22
+ `;
23
+
24
+ const transform = (fileInfo, api, options) => {
25
+ const filePath = fileInfo.path;
26
+
27
+ if (filePath.endsWith('error.tsx')) {
28
+ fs.writeFileSync(filePath, template, { encoding: 'utf8' });
29
+ }
30
+ };
31
+
32
+ module.exports = transform;
@@ -0,0 +1,18 @@
1
+ import path from 'path';
2
+
3
+ const yargs = require('yargs/yargs');
4
+ const { hideBin } = require('yargs/helpers');
5
+ const args = yargs(hideBin(process.argv)).argv;
6
+
7
+ export default () => {
8
+ const workingDir = path.resolve(process.cwd());
9
+ const codemodName = args.codemod;
10
+ const codemodPath = path.resolve(
11
+ __dirname,
12
+ `../../codemods/${codemodName}/index.js`
13
+ );
14
+
15
+ const codemod = require(codemodPath);
16
+
17
+ codemod.transform();
18
+ };
package/commands/index.ts CHANGED
@@ -4,6 +4,7 @@ import addLanguage from './add-language';
4
4
  import removeLanguage from './remove-language';
5
5
  import defaultLanguage from './default-language';
6
6
  import plugins from './plugins';
7
+ import codemod from './codemod';
7
8
 
8
9
  export default {
9
10
  commerceUrl,
@@ -11,5 +12,6 @@ export default {
11
12
  addLanguage,
12
13
  removeLanguage,
13
14
  defaultLanguage,
14
- plugins
15
+ plugins,
16
+ codemod
15
17
  };
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ 'use client';
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const hooks_1 = require("@akinon/next/hooks");
8
+ const error_page_1 = __importDefault(require("@akinon/next/views/error-page"));
9
+ function ErrorPage({ error, reset }) {
10
+ // DO NOT REMOVE THIS LINE TO REPORT UNCAUGHT ERRORS TO SENTRY
11
+ (0, hooks_1.useSentryUncaughtErrors)(error);
12
+ return React.createElement(error_page_1.default, { error: error, reset: reset });
13
+ }
14
+ exports.default = ErrorPage;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const path_1 = __importDefault(require("path"));
7
+ const yargs = require('yargs/yargs');
8
+ const { hideBin } = require('yargs/helpers');
9
+ const args = yargs(hideBin(process.argv)).argv;
10
+ exports.default = () => {
11
+ const workingDir = path_1.default.resolve(process.cwd());
12
+ const codemodName = args.codemod;
13
+ const codemodPath = path_1.default.resolve(__dirname, `../../codemods/${codemodName}/index.js`);
14
+ const codemod = require(codemodPath);
15
+ codemod.transform();
16
+ };
@@ -9,11 +9,13 @@ const add_language_1 = __importDefault(require("./add-language"));
9
9
  const remove_language_1 = __importDefault(require("./remove-language"));
10
10
  const default_language_1 = __importDefault(require("./default-language"));
11
11
  const plugins_1 = __importDefault(require("./plugins"));
12
+ const codemod_1 = __importDefault(require("./codemod"));
12
13
  exports.default = {
13
14
  commerceUrl: commerce_url_1.default,
14
15
  create: create_1.default,
15
16
  addLanguage: add_language_1.default,
16
17
  removeLanguage: remove_language_1.default,
17
18
  defaultLanguage: default_language_1.default,
18
- plugins: plugins_1.default
19
+ plugins: plugins_1.default,
20
+ codemod: codemod_1.default
19
21
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akinon/projectzero",
3
- "version": "2.0.0-beta.6",
3
+ "version": "2.0.0-beta.8",
4
4
  "private": false,
5
5
  "description": "CLI tool to manage your Project Zero Next project",
6
6
  "bin": {
@@ -1,16 +0,0 @@
1
- import { initSentry } from '@akinon/next/sentry';
2
-
3
- async function initializeSentry() {
4
- const response = await fetch('/api/sentry', { next: { revalidate: 0 } });
5
- const data = await response.json();
6
-
7
- const options = {
8
- dsn: data.dsn,
9
- integrations: [],
10
- tracesSampleRate: 1.0
11
- };
12
-
13
- initSentry('Client', options);
14
- }
15
-
16
- initializeSentry();
@@ -1,3 +0,0 @@
1
- import { initSentry } from '@akinon/next/sentry';
2
-
3
- initSentry('Edge');
@@ -1,4 +0,0 @@
1
- defaults.url=https://sentry.io/
2
- defaults.org=akinon
3
- defaults.project=console--zero-pwa
4
- cli.executable=node_modules/@sentry/cli/bin/sentry-cli
@@ -1,3 +0,0 @@
1
- import { initSentry } from '@akinon/next/sentry';
2
-
3
- initSentry('Server');
@@ -1,67 +0,0 @@
1
- import { Skeleton, SkeletonWrapper } from 'components';
2
-
3
- export default function Loading() {
4
- return (
5
- <div className="container mx-auto">
6
- <div className="max-w-5xl mx-auto my-5 px-7">
7
- <SkeletonWrapper className="md:mb-7">
8
- <Skeleton className="w-[17.25rem] h-4 lg:w-64" />
9
- </SkeletonWrapper>
10
- </div>
11
- <div className="grid max-w-5xl grid-cols-2 lg:gap-8 mx-auto px-7">
12
- <div className="col-span-2 mb-7 md:mb-0 lg:col-span-1">
13
- <div className="flex gap-1">
14
- <SkeletonWrapper className="hidden md:block md:mb-7">
15
- {Array(5)
16
- .fill(null)
17
- .map((_, index) => (
18
- <Skeleton key={index} className="w-20 h-24 mb-2" />
19
- ))}
20
- </SkeletonWrapper>
21
-
22
- <div className="flex-1">
23
- <SkeletonWrapper className="md:mb-7">
24
- <Skeleton className="w-full h-[30.375rem] md:h-[36.375rem]" />
25
- </SkeletonWrapper>
26
- </div>
27
- </div>
28
- </div>
29
- <div className="flex flex-col items-center col-span-2 lg:col-span-1">
30
- <div className="w-full">
31
- <SkeletonWrapper className="w-full md:mb-7 flex justify-center items-center">
32
- <Skeleton className="w-96 h-16 mb-9" />
33
- <Skeleton className="hidden w-36 h-14 mb-9 md:block" />
34
-
35
- <div className="flex flex-col justify-center items-center mb-9">
36
- <Skeleton className="w-36 h-4 mb-2" />
37
- <div className="flex items-center gap-2">
38
- {Array(3)
39
- .fill(null)
40
- .map((_, index) => (
41
- <Skeleton key={index} className="w-24 h-10" />
42
- ))}
43
- </div>
44
- </div>
45
-
46
- <div className="flex flex-col justify-center items-center mb-4">
47
- <Skeleton className="w-36 h-4 mb-2" />
48
- <div className="flex items-center gap-2">
49
- {Array(5)
50
- .fill(null)
51
- .map((_, index) => (
52
- <Skeleton key={index} className="w-11 h-11" />
53
- ))}
54
- </div>
55
- </div>
56
-
57
- <Skeleton className="hidden w-full h-14 mb-6 md:block" />
58
- <Skeleton className="w-40 h-10 mb-9 md:w-72" />
59
- <Skeleton className="w-24 h-10 mb-7" />
60
- <Skeleton className="w-full h-36" />
61
- </SkeletonWrapper>
62
- </div>
63
- </div>
64
- </div>
65
- </div>
66
- );
67
- }