Ver código fonte

Merge branch 'master' into zzf-home

zhangzf 1 semana atrás
pai
commit
0596b2a588
73 arquivos alterados com 1138 adições e 1511 exclusões
  1. 23 1
      next.config.ts
  2. 5 3
      package.json
  3. 169 114
      pnpm-lock.yaml
  4. 3 2
      src/app/(checkout)/checkout/_components/CheckoutProducts/CheckoutProducts.tsx
  5. 165 186
      src/app/(checkout)/checkout/cart/_components/CartWrapper.tsx
  6. 31 12
      src/app/(checkout)/checkout/cart/_components/EmptyCart.tsx
  7. 5 5
      src/app/(checkout)/checkout/cart/_components/PromiseInCart.tsx
  8. 5 4
      src/app/(checkout)/checkout/cart/_components/UnpaidOrderSection.tsx
  9. 11 12
      src/app/(checkout)/checkout/cart/page.tsx
  10. 1 1
      src/app/(public)/_components/CategoryTabSwiper .tsx
  11. 13 1
      src/app/(public)/_components/HomeContent.tsx
  12. 0 1
      src/app/(public)/_components/TopImageSwiper.tsx
  13. 0 5
      src/app/(public)/_components/css/swiper-custom.css
  14. 0 5
      src/app/(public)/category/[slug]/[id]/page.tsx
  15. 0 1
      src/app/(public)/customer/account/_components/AccountBottomHrefSwiper.tsx
  16. 0 2
      src/app/(public)/customer/account/_components/AccountVipPopup.tsx
  17. 5 5
      src/app/(public)/customer/forget-password/_components/ForgetPasswordForm.tsx
  18. 1 1
      src/app/(public)/customer/forget-password/page.tsx
  19. 13 13
      src/app/(public)/customer/login/_components/LoginPageWrapper.tsx
  20. 4 4
      src/app/(public)/customer/reset-password/_components/ResetPasswordWrapper.tsx
  21. 1 1
      src/app/(public)/customer/reset-password/page.tsx
  22. 3 5
      src/app/(public)/product/_components/ProductMedia.tsx
  23. 1 2
      src/app/(public)/product/_components/review/ReviewImageGalleryModal.tsx
  24. 1 3
      src/app/(public)/product/_components/youmaylike/Recommend.tsx
  25. 1 1
      src/app/(public)/search/page.tsx
  26. 7 8
      src/app/api/revalidate/route.ts
  27. 404 143
      src/app/globals.css
  28. 10 13
      src/app/layout.tsx
  29. 3 2
      src/app/robots.ts
  30. 0 305
      src/components/cart/CartModal.tsx
  31. 0 23
      src/components/cart/OpenCart.tsx
  32. 0 21
      src/components/cart/index.tsx
  33. 2 2
      src/components/catalog/product/NewProductGridItems.tsx
  34. 2 2
      src/components/catalog/product/ProductGridItems.tsx
  35. 3 3
      src/components/catalog/product/ProductsSection.tsx
  36. 37 30
      src/components/common/LoginModal/LoginModal.tsx
  37. 0 31
      src/components/common/skeleton/AuthPlaceHolder.tsx
  38. 1 1
      src/components/customer/accountSwiper.tsx
  39. 2 1
      src/components/error/ErrorBoundary.tsx
  40. 0 1
      src/components/home/HomeImageBanner.tsx
  41. 2 1
      src/components/home/ImageCarousel.tsx
  42. 2 4
      src/components/home/ProductCarouselTheme.tsx
  43. 35 4
      src/components/layout/footer/index.tsx
  44. 3 45
      src/components/layout/navbar/MobileSearch.tsx
  45. 1 1
      src/components/layout/navbar/NavbarActions.tsx
  46. 1 1
      src/components/layout/navbar/Search.tsx
  47. 1 1
      src/components/layout/navbar/SearchPop.tsx
  48. 2 4
      src/components/theme/ui/CommonModal.tsx
  49. 70 0
      src/env.ts
  50. 1 0
      src/graphql/cart/mutations/AddProductToCart.ts
  51. 15 14
      src/graphql/cart/mutations/CreateApplyCoupon.ts
  52. 1 0
      src/graphql/cart/mutations/CreateMergeCart.ts
  53. 1 0
      src/graphql/cart/mutations/CreateRemoveCoupon.ts
  54. 1 0
      src/graphql/cart/mutations/GetCartItem.ts
  55. 1 0
      src/graphql/cart/mutations/RemoveCartItem.ts
  56. 1 0
      src/graphql/cart/mutations/UpdateCartItems.ts
  57. 1 0
      src/graphql/checkout/mutations/CreateSaveCheckoutCart.ts
  58. 2 1
      src/lib/Airwallex/airwallexInit.ts
  59. 2 4
      src/lib/ApolloClientServer.ts
  60. 0 130
      src/lib/graphql-fetch.ts
  61. 2 1
      src/providers/PaymentSDKProvider.tsx
  62. 2 1
      src/providers/PaypalWrapper.tsx
  63. 2 1
      src/proxy/auth.ts
  64. 4 3
      src/proxy/currency.ts
  65. 12 0
      src/types/cart/type.ts
  66. 0 4
      src/types/graphqlFetch/type.ts
  67. 2 1
      src/utils/auth.ts
  68. 10 63
      src/utils/bagisto/index.ts
  69. 15 1
      src/utils/cartDetailTools.ts
  70. 4 4
      src/utils/constants.ts
  71. 6 102
      src/utils/constants/server.ts
  72. 4 10
      src/utils/helper.ts
  73. 0 134
      src/utils/hooks/useCache.ts

+ 23 - 1
next.config.ts

@@ -1,7 +1,29 @@
-import { configHeader } from '@/utils/constants/server';
 import type { NextConfig } from "next";
+import "./src/env";
 
+const configHeader = [
+  // Security headers for all routes
+  {
+    source: "/:path*",
+    headers: [
+      { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
+      //{ key: "X-Frame-Options", value: "DENY" },
+      { key: "X-Content-Type-Options", value: "nosniff" },
+      // { key: "X-XSS-Protection", value: "1; mode=block" },
+      // {
+      //   key: "Permissions-Policy",
+      //   value:
+      //     "camera=(), microphone=(), geolocation=(), browsing-topics=()",
+      // },
+    ],
+  },
+  // Cache dynamic pages - shorter cache for frequently updated content
+  // Next.js static assets - long cache with immutable
+  // Public folder assets - long cache
+];
 const nextConfig: NextConfig = {
+  output: "standalone",
+  transpilePackages: ["@t3-oss/env-nextjs", "@t3-oss/env-core"],
   devIndicators: false,
   reactStrictMode: true,
   typescript: {

+ 5 - 3
package.json

@@ -3,9 +3,9 @@
   "version": "0.1.0",
   "private": true,
   "scripts": {
-    "dev": "next dev -p 3000",
+    "dev": "next dev",
     "build": "eslint . && next build",
-    "start": "next start -p 3000",
+    "start": "next start",
     "lint": "eslint .",
     "lint:fix": "eslint . --fix",
     "package-version": "npx npm-check-updates"
@@ -38,6 +38,7 @@
     "@react-aria/visually-hidden": "^3.8.31",
     "@react-types/shared": "^3.33.1",
     "@reduxjs/toolkit": "^2.10.1",
+    "@t3-oss/env-nextjs": "^0.13.11",
     "big.js": "^7.0.1",
     "clsx": "^2.1.1",
     "framer-motion": "^12.23.24",
@@ -51,7 +52,8 @@
     "react-hook-form": "^7.66.1",
     "react-redux": "^9.2.0",
     "rxjs": "^7.8.2",
-    "swiper": "^12.1.3"
+    "swiper": "^12.1.3",
+    "zod": "^4.6.5"
   },
   "devDependencies": {
     "@tailwindcss/postcss": "^4",

+ 169 - 114
pnpm-lock.yaml

@@ -20,7 +20,7 @@ importers:
         version: 4.2.0(graphql@16.14.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2)
       '@apollo/client-integration-nextjs':
         specifier: ^0.14.5
-        version: 0.14.5(@apollo/client@4.2.0(graphql@16.14.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(next@16.3.4(@babel/core@7.29.0)(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2)
+        version: 0.14.5(@apollo/client@4.2.0(graphql@16.14.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(next@16.3.4(@babel/core@7.29.0(supports-color@7.2.0))(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2)
       '@heroicons/react':
         specifier: ^2.2.0
         version: 2.2.0(react@19.2.5)
@@ -93,6 +93,9 @@ importers:
       '@reduxjs/toolkit':
         specifier: ^2.10.1
         version: 2.12.0(react-redux@9.3.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1))(react@19.2.5)
+      '@t3-oss/env-nextjs':
+        specifier: ^0.13.11
+        version: 0.13.11(typescript@5.9.3)(zod@4.6.5)
       big.js:
         specifier: ^7.0.1
         version: 7.0.1
@@ -113,10 +116,10 @@ importers:
         version: 0.563.0(react@19.2.5)
       next:
         specifier: 16.3.4
-        version: 16.3.4(@babel/core@7.29.0)(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+        version: 16.3.4(@babel/core@7.29.0(supports-color@7.2.0))(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
       next-auth:
         specifier: ^4.24.13
-        version: 4.24.14(next@16.3.4(@babel/core@7.29.0)(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+        version: 4.24.14(next@16.3.4(@babel/core@7.29.0(supports-color@7.2.0))(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
       react:
         specifier: 19.2.5
         version: 19.2.5
@@ -135,6 +138,9 @@ importers:
       swiper:
         specifier: ^12.1.3
         version: 12.1.4
+      zod:
+        specifier: ^4.6.5
+        version: 4.6.5
     devDependencies:
       '@tailwindcss/postcss':
         specifier: ^4
@@ -156,10 +162,10 @@ importers:
         version: 10.1.0
       eslint:
         specifier: ^9
-        version: 9.39.4(jiti@2.7.0)
+        version: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
       eslint-config-next:
         specifier: 16.3.4
-        version: 16.3.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
+        version: 16.3.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
       tailwindcss:
         specifier: ^4
         version: 4.3.0
@@ -1637,6 +1643,40 @@ packages:
   '@swc/helpers@0.5.23':
     resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==}
 
+  '@t3-oss/env-core@0.13.11':
+    resolution: {integrity: sha512-sM7GYY+KL7H/Hl0BE0inWfk3nRHZOLhmVn7sHGxaZt9FAR6KqREXAE+6TqKfiavfXmpRxO/OZ2QgKRd+oiBYRQ==}
+    peerDependencies:
+      arktype: ^2.1.0
+      typescript: '>=5.0.0'
+      valibot: ^1.0.0-beta.7 || ^1.0.0
+      zod: ^3.24.0 || ^4.0.0
+    peerDependenciesMeta:
+      arktype:
+        optional: true
+      typescript:
+        optional: true
+      valibot:
+        optional: true
+      zod:
+        optional: true
+
+  '@t3-oss/env-nextjs@0.13.11':
+    resolution: {integrity: sha512-NC+3j7YWgpzdFu1t5y/8wqibTK0lm5RS4bjXA1n8uwik3wIR4iZM4Fa+U2BaMa5k3Qk8RZiYhoAIX0WogmGkzg==}
+    peerDependencies:
+      arktype: ^2.1.0
+      typescript: '>=5.0.0'
+      valibot: ^1.0.0-beta.7 || ^1.0.0
+      zod: ^3.24.0 || ^4.0.0
+    peerDependenciesMeta:
+      arktype:
+        optional: true
+      typescript:
+        optional: true
+      valibot:
+        optional: true
+      zod:
+        optional: true
+
   '@tailwindcss/node@4.3.0':
     resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==}
 
@@ -3540,8 +3580,8 @@ packages:
     peerDependencies:
       zod: ^3.25.0 || ^4.0.0
 
-  zod@4.4.3:
-    resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
+  zod@4.6.5:
+    resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==}
 
 snapshots:
 
@@ -3580,11 +3620,11 @@ snapshots:
 
   '@alloc/quick-lru@5.2.0': {}
 
-  '@apollo/client-integration-nextjs@0.14.5(@apollo/client@4.2.0(graphql@16.14.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(next@16.3.4(@babel/core@7.29.0)(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2)':
+  '@apollo/client-integration-nextjs@0.14.5(@apollo/client@4.2.0(graphql@16.14.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(next@16.3.4(@babel/core@7.29.0(supports-color@7.2.0))(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2)':
     dependencies:
       '@apollo/client': 4.2.0(graphql@16.14.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2)
       '@apollo/client-react-streaming': 0.14.5(@apollo/client@4.2.0(graphql@16.14.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2))(@types/react@19.2.14)(graphql@16.14.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rxjs@7.8.2)
-      next: 16.3.4(@babel/core@7.29.0)(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+      next: 16.3.4(@babel/core@7.29.0(supports-color@7.2.0))(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
       react: 19.2.5
       rxjs: 7.8.2
     transitivePeerDependencies:
@@ -3627,20 +3667,20 @@ snapshots:
 
   '@babel/compat-data@7.29.3': {}
 
-  '@babel/core@7.29.0':
+  '@babel/core@7.29.0(supports-color@7.2.0)':
     dependencies:
       '@babel/code-frame': 7.29.0
       '@babel/generator': 7.29.1
       '@babel/helper-compilation-targets': 7.28.6
-      '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
+      '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0)
       '@babel/helpers': 7.29.2
       '@babel/parser': 7.29.3
       '@babel/template': 7.28.6
-      '@babel/traverse': 7.29.0
+      '@babel/traverse': 7.29.0(supports-color@7.2.0)
       '@babel/types': 7.29.0
       '@jridgewell/remapping': 2.3.5
       convert-source-map: 2.0.0
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@7.2.0)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       semver: 6.3.1
@@ -3665,19 +3705,19 @@ snapshots:
 
   '@babel/helper-globals@7.28.0': {}
 
-  '@babel/helper-module-imports@7.28.6':
+  '@babel/helper-module-imports@7.28.6(supports-color@7.2.0)':
     dependencies:
-      '@babel/traverse': 7.29.0
+      '@babel/traverse': 7.29.0(supports-color@7.2.0)
       '@babel/types': 7.29.0
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)':
+  '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0(supports-color@7.2.0))(supports-color@7.2.0)':
     dependencies:
-      '@babel/core': 7.29.0
-      '@babel/helper-module-imports': 7.28.6
+      '@babel/core': 7.29.0(supports-color@7.2.0)
+      '@babel/helper-module-imports': 7.28.6(supports-color@7.2.0)
       '@babel/helper-validator-identifier': 7.28.5
-      '@babel/traverse': 7.29.0
+      '@babel/traverse': 7.29.0(supports-color@7.2.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -3704,7 +3744,7 @@ snapshots:
       '@babel/parser': 7.29.3
       '@babel/types': 7.29.0
 
-  '@babel/traverse@7.29.0':
+  '@babel/traverse@7.29.0(supports-color@7.2.0)':
     dependencies:
       '@babel/code-frame': 7.29.0
       '@babel/generator': 7.29.1
@@ -3712,7 +3752,7 @@ snapshots:
       '@babel/parser': 7.29.3
       '@babel/template': 7.28.6
       '@babel/types': 7.29.0
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@7.2.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -3748,17 +3788,17 @@ snapshots:
 
   '@epic-web/invariant@1.0.0': {}
 
-  '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))':
+  '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))':
     dependencies:
-      eslint: 9.39.4(jiti@2.7.0)
+      eslint: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
       eslint-visitor-keys: 3.4.3
 
   '@eslint-community/regexpp@4.12.2': {}
 
-  '@eslint/config-array@0.21.2':
+  '@eslint/config-array@0.21.2(supports-color@7.2.0)':
     dependencies:
       '@eslint/object-schema': 2.1.7
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@7.2.0)
       minimatch: 3.1.5
     transitivePeerDependencies:
       - supports-color
@@ -3771,10 +3811,10 @@ snapshots:
     dependencies:
       '@types/json-schema': 7.0.15
 
-  '@eslint/eslintrc@3.3.5':
+  '@eslint/eslintrc@3.3.5(supports-color@7.2.0)':
     dependencies:
       ajv: 6.15.0
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@7.2.0)
       espree: 10.4.0
       globals: 14.0.0
       ignore: 5.3.2
@@ -4609,9 +4649,9 @@ snapshots:
 
   '@next/env@16.3.4': {}
 
-  '@next/eslint-plugin-next@16.3.4(eslint@9.39.4(jiti@2.7.0))':
+  '@next/eslint-plugin-next@16.3.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))':
     dependencies:
-      '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0))
+      '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))
       fast-glob: 3.3.1
     transitivePeerDependencies:
       - eslint
@@ -5581,6 +5621,18 @@ snapshots:
     dependencies:
       tslib: 2.8.1
 
+  '@t3-oss/env-core@0.13.11(typescript@5.9.3)(zod@4.6.5)':
+    optionalDependencies:
+      typescript: 5.9.3
+      zod: 4.6.5
+
+  '@t3-oss/env-nextjs@0.13.11(typescript@5.9.3)(zod@4.6.5)':
+    dependencies:
+      '@t3-oss/env-core': 0.13.11(typescript@5.9.3)(zod@4.6.5)
+    optionalDependencies:
+      typescript: 5.9.3
+      zod: 4.6.5
+
   '@tailwindcss/node@4.3.0':
     dependencies:
       '@jridgewell/remapping': 2.3.5
@@ -5693,15 +5745,15 @@ snapshots:
 
   '@types/use-sync-external-store@0.0.6': {}
 
-  '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
+  '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)':
     dependencies:
       '@eslint-community/regexpp': 4.12.2
-      '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
+      '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
       '@typescript-eslint/scope-manager': 8.59.4
-      '@typescript-eslint/type-utils': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
-      '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
+      '@typescript-eslint/type-utils': 8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
       '@typescript-eslint/visitor-keys': 8.59.4
-      eslint: 9.39.4(jiti@2.7.0)
+      eslint: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
       ignore: 7.0.5
       natural-compare: 1.4.0
       ts-api-utils: 2.5.0(typescript@5.9.3)
@@ -5709,23 +5761,23 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
+  '@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)':
     dependencies:
       '@typescript-eslint/scope-manager': 8.59.4
       '@typescript-eslint/types': 8.59.4
-      '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3)
+      '@typescript-eslint/typescript-estree': 8.59.4(supports-color@7.2.0)(typescript@5.9.3)
       '@typescript-eslint/visitor-keys': 8.59.4
-      debug: 4.4.3
-      eslint: 9.39.4(jiti@2.7.0)
+      debug: 4.4.3(supports-color@7.2.0)
+      eslint: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
       typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/project-service@8.59.4(typescript@5.9.3)':
+  '@typescript-eslint/project-service@8.59.4(supports-color@7.2.0)(typescript@5.9.3)':
     dependencies:
       '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@5.9.3)
       '@typescript-eslint/types': 8.59.4
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@7.2.0)
       typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
@@ -5739,13 +5791,13 @@ snapshots:
     dependencies:
       typescript: 5.9.3
 
-  '@typescript-eslint/type-utils@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
+  '@typescript-eslint/type-utils@8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)':
     dependencies:
       '@typescript-eslint/types': 8.59.4
-      '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3)
-      '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
-      debug: 4.4.3
-      eslint: 9.39.4(jiti@2.7.0)
+      '@typescript-eslint/typescript-estree': 8.59.4(supports-color@7.2.0)(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
+      debug: 4.4.3(supports-color@7.2.0)
+      eslint: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
       ts-api-utils: 2.5.0(typescript@5.9.3)
       typescript: 5.9.3
     transitivePeerDependencies:
@@ -5753,28 +5805,28 @@ snapshots:
 
   '@typescript-eslint/types@8.59.4': {}
 
-  '@typescript-eslint/typescript-estree@8.59.4(typescript@5.9.3)':
+  '@typescript-eslint/typescript-estree@8.59.4(supports-color@7.2.0)(typescript@5.9.3)':
     dependencies:
-      '@typescript-eslint/project-service': 8.59.4(typescript@5.9.3)
+      '@typescript-eslint/project-service': 8.59.4(supports-color@7.2.0)(typescript@5.9.3)
       '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@5.9.3)
       '@typescript-eslint/types': 8.59.4
       '@typescript-eslint/visitor-keys': 8.59.4
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@7.2.0)
       minimatch: 10.2.5
-      semver: 7.8.1
+      semver: 7.8.5
       tinyglobby: 0.2.16
       ts-api-utils: 2.5.0(typescript@5.9.3)
       typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/utils@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
+  '@typescript-eslint/utils@8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0))
+      '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))
       '@typescript-eslint/scope-manager': 8.59.4
       '@typescript-eslint/types': 8.59.4
-      '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3)
-      eslint: 9.39.4(jiti@2.7.0)
+      '@typescript-eslint/typescript-estree': 8.59.4(supports-color@7.2.0)(typescript@5.9.3)
+      eslint: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
       typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
@@ -6100,13 +6152,17 @@ snapshots:
       es-errors: 1.3.0
       is-data-view: 1.0.2
 
-  debug@3.2.7:
+  debug@3.2.7(supports-color@7.2.0):
     dependencies:
       ms: 2.1.3
+    optionalDependencies:
+      supports-color: 7.2.0
 
-  debug@4.4.3:
+  debug@4.4.3(supports-color@7.2.0):
     dependencies:
       ms: 2.1.3
+    optionalDependencies:
+      supports-color: 7.2.0
 
   decimal.js@10.6.0: {}
 
@@ -6259,18 +6315,18 @@ snapshots:
 
   escape-string-regexp@4.0.0: {}
 
-  eslint-config-next@16.3.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3):
+  eslint-config-next@16.3.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3):
     dependencies:
-      '@next/eslint-plugin-next': 16.3.4(eslint@9.39.4(jiti@2.7.0))
-      eslint: 9.39.4(jiti@2.7.0)
-      eslint-import-resolver-node: 0.3.10
-      eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0))
-      eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0))
-      eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0))
-      eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0))
-      eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0))
+      '@next/eslint-plugin-next': 16.3.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))
+      eslint: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
+      eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
+      eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
+      eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
+      eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))
+      eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))
+      eslint-plugin-react-hooks: 7.1.1(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
       globals: 16.4.0
-      typescript-eslint: 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
+      typescript-eslint: 8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
     optionalDependencies:
       typescript: 5.9.3
     transitivePeerDependencies:
@@ -6279,52 +6335,52 @@ snapshots:
       - eslint-plugin-import-x
       - supports-color
 
-  eslint-import-resolver-node@0.3.10:
+  eslint-import-resolver-node@0.3.10(supports-color@7.2.0):
     dependencies:
-      debug: 3.2.7
+      debug: 3.2.7(supports-color@7.2.0)
       is-core-module: 2.16.2
       resolve: 2.0.0-next.7
     transitivePeerDependencies:
       - supports-color
 
-  eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)):
+  eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
     dependencies:
       '@nolyfill/is-core-module': 1.0.39
-      debug: 4.4.3
-      eslint: 9.39.4(jiti@2.7.0)
+      debug: 4.4.3(supports-color@7.2.0)
+      eslint: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
       get-tsconfig: 4.14.0
       is-bun-module: 2.0.0
       stable-hash: 0.0.5
       tinyglobby: 0.2.16
       unrs-resolver: 1.12.2
     optionalDependencies:
-      eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0))
+      eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
     transitivePeerDependencies:
       - supports-color
 
-  eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)):
+  eslint-module-utils@2.12.1(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
     dependencies:
-      debug: 3.2.7
+      debug: 3.2.7(supports-color@7.2.0)
     optionalDependencies:
-      '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
-      eslint: 9.39.4(jiti@2.7.0)
-      eslint-import-resolver-node: 0.3.10
-      eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0))
+      '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
+      eslint: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
+      eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
+      eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
     transitivePeerDependencies:
       - supports-color
 
-  eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)):
+  eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
     dependencies:
       '@rtsao/scc': 1.1.0
       array-includes: 3.1.9
       array.prototype.findlastindex: 1.2.6
       array.prototype.flat: 1.3.3
       array.prototype.flatmap: 1.3.3
-      debug: 3.2.7
+      debug: 3.2.7(supports-color@7.2.0)
       doctrine: 2.1.0
-      eslint: 9.39.4(jiti@2.7.0)
-      eslint-import-resolver-node: 0.3.10
-      eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0))
+      eslint: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
+      eslint-import-resolver-node: 0.3.10(supports-color@7.2.0)
+      eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)
       hasown: 2.0.3
       is-core-module: 2.16.2
       is-glob: 4.0.3
@@ -6336,13 +6392,13 @@ snapshots:
       string.prototype.trimend: 1.0.9
       tsconfig-paths: 3.15.0
     optionalDependencies:
-      '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
+      '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
     transitivePeerDependencies:
       - eslint-import-resolver-typescript
       - eslint-import-resolver-webpack
       - supports-color
 
-  eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.7.0)):
+  eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0)):
     dependencies:
       aria-query: 5.3.2
       array-includes: 3.1.9
@@ -6352,7 +6408,7 @@ snapshots:
       axobject-query: 4.1.0
       damerau-levenshtein: 1.0.8
       emoji-regex: 9.2.2
-      eslint: 9.39.4(jiti@2.7.0)
+      eslint: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
       hasown: 2.0.3
       jsx-ast-utils: 3.3.5
       language-tags: 1.0.9
@@ -6361,18 +6417,18 @@ snapshots:
       safe-regex-test: 1.1.0
       string.prototype.includes: 2.0.1
 
-  eslint-plugin-react-hooks@7.1.1(eslint@9.39.4(jiti@2.7.0)):
+  eslint-plugin-react-hooks@7.1.1(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0):
     dependencies:
-      '@babel/core': 7.29.0
+      '@babel/core': 7.29.0(supports-color@7.2.0)
       '@babel/parser': 7.29.3
-      eslint: 9.39.4(jiti@2.7.0)
+      eslint: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
       hermes-parser: 0.25.1
-      zod: 4.4.3
-      zod-validation-error: 4.0.2(zod@4.4.3)
+      zod: 4.6.5
+      zod-validation-error: 4.0.2(zod@4.6.5)
     transitivePeerDependencies:
       - supports-color
 
-  eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.7.0)):
+  eslint-plugin-react@7.37.5(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0)):
     dependencies:
       array-includes: 3.1.9
       array.prototype.findlast: 1.2.5
@@ -6380,7 +6436,7 @@ snapshots:
       array.prototype.tosorted: 1.1.4
       doctrine: 2.1.0
       es-iterator-helpers: 1.3.2
-      eslint: 9.39.4(jiti@2.7.0)
+      eslint: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
       estraverse: 5.3.0
       hasown: 2.0.3
       jsx-ast-utils: 3.3.5
@@ -6405,14 +6461,14 @@ snapshots:
 
   eslint-visitor-keys@5.0.1: {}
 
-  eslint@9.39.4(jiti@2.7.0):
+  eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0):
     dependencies:
-      '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0))
+      '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))
       '@eslint-community/regexpp': 4.12.2
-      '@eslint/config-array': 0.21.2
+      '@eslint/config-array': 0.21.2(supports-color@7.2.0)
       '@eslint/config-helpers': 0.4.2
       '@eslint/core': 0.17.0
-      '@eslint/eslintrc': 3.3.5
+      '@eslint/eslintrc': 3.3.5(supports-color@7.2.0)
       '@eslint/js': 9.39.4
       '@eslint/plugin-kit': 0.4.1
       '@humanfs/node': 0.16.8
@@ -6422,7 +6478,7 @@ snapshots:
       ajv: 6.15.0
       chalk: 4.1.2
       cross-spawn: 7.0.6
-      debug: 4.4.3
+      debug: 4.4.3(supports-color@7.2.0)
       escape-string-regexp: 4.0.0
       eslint-scope: 8.4.0
       eslint-visitor-keys: 4.2.1
@@ -6937,13 +6993,13 @@ snapshots:
 
   natural-compare@1.4.0: {}
 
-  next-auth@4.24.14(next@16.3.4(@babel/core@7.29.0)(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+  next-auth@4.24.14(next@16.3.4(@babel/core@7.29.0(supports-color@7.2.0))(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
     dependencies:
       '@babel/runtime': 7.29.2
       '@panva/hkdf': 1.2.1
       cookie: 0.7.2
       jose: 4.15.9
-      next: 16.3.4(@babel/core@7.29.0)(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
+      next: 16.3.4(@babel/core@7.29.0(supports-color@7.2.0))(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
       oauth: 0.9.15
       openid-client: 5.7.1
       preact: 10.29.2
@@ -6952,7 +7008,7 @@ snapshots:
       react-dom: 19.2.5(react@19.2.5)
       uuid: 8.3.2
 
-  next@16.3.4(@babel/core@7.29.0)(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
+  next@16.3.4(@babel/core@7.29.0(supports-color@7.2.0))(@types/node@20.19.41)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
     dependencies:
       '@next/env': 16.3.4
       '@swc/helpers': 0.5.23
@@ -6961,7 +7017,7 @@ snapshots:
       postcss: 8.5.23
       react: 19.2.5
       react-dom: 19.2.5(react@19.2.5)
-      styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.5)
+      styled-jsx: 5.1.6(@babel/core@7.29.0(supports-color@7.2.0))(react@19.2.5)
     optionalDependencies:
       '@next/swc-darwin-arm64': 16.3.4
       '@next/swc-darwin-x64': 16.3.4
@@ -7279,8 +7335,7 @@ snapshots:
 
   semver@7.8.1: {}
 
-  semver@7.8.5:
-    optional: true
+  semver@7.8.5: {}
 
   server-only@0.0.1: {}
 
@@ -7441,12 +7496,12 @@ snapshots:
 
   strip-json-comments@3.1.1: {}
 
-  styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.5):
+  styled-jsx@5.1.6(@babel/core@7.29.0(supports-color@7.2.0))(react@19.2.5):
     dependencies:
       client-only: 0.0.1
       react: 19.2.5
     optionalDependencies:
-      '@babel/core': 7.29.0
+      '@babel/core': 7.29.0(supports-color@7.2.0)
 
   supports-color@7.2.0:
     dependencies:
@@ -7545,13 +7600,13 @@ snapshots:
       possible-typed-array-names: 1.1.0
       reflect.getprototypeof: 1.0.10
 
-  typescript-eslint@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3):
+  typescript-eslint@8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3):
     dependencies:
-      '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
-      '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
-      '@typescript-eslint/typescript-estree': 8.59.4(typescript@5.9.3)
-      '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
-      eslint: 9.39.4(jiti@2.7.0)
+      '@typescript-eslint/eslint-plugin': 8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
+      '@typescript-eslint/parser': 8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
+      '@typescript-eslint/typescript-estree': 8.59.4(supports-color@7.2.0)(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.59.4(eslint@9.39.4(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@5.9.3)
+      eslint: 9.39.4(jiti@2.7.0)(supports-color@7.2.0)
       typescript: 5.9.3
     transitivePeerDependencies:
       - supports-color
@@ -7686,8 +7741,8 @@ snapshots:
 
   yocto-queue@0.1.0: {}
 
-  zod-validation-error@4.0.2(zod@4.4.3):
+  zod-validation-error@4.0.2(zod@4.6.5):
     dependencies:
-      zod: 4.4.3
+      zod: 4.6.5
 
-  zod@4.4.3: {}
+  zod@4.6.5: {}

+ 3 - 2
src/app/(checkout)/checkout/_components/CheckoutProducts/CheckoutProducts.tsx

@@ -6,7 +6,7 @@ import type {CartDetail, CartItem} from "@/types/cart/type";
 import { useAddProduct } from "@utils/hooks/useAddToCart";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import {useConfig} from "@/utils/hooks/useConfig";
-import { formatCartDetail } from "@/utils/cartDetailTools";
+import { formatCartDetail, formatCartProductOptions } from "@/utils/cartDetailTools";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 
@@ -107,6 +107,7 @@ export default function CheckoutProducts({
             <div className="relative box-border w-full h-auto">
                 {productList.map((product) => {
                     const baseImage = product.baseImage ? JSON.parse(product.baseImage) : {};
+                    const productOptions = product.options.edges.map((edge) => edge.node);
                     return (
                         <div key={product.id} className="w-full flex gap-2 mt-4 pt-4 border-t-1 border-t-[#f0f0f0]">
                             <div className="relative w-33 h-44 flex-none">
@@ -123,7 +124,7 @@ export default function CheckoutProducts({
                                         </button>
                                     </div>
                                     <div className="w-full mt-2 text-ly-14 border-1 rounded-sm border-[#d9d9d9] px-2.5 py-1.5 leading-5">
-                                        {product.sku}
+                                        {formatCartProductOptions(productOptions)}
                                     </div>
                                 </div>
                                 <div className="flex items-center justify-between">

Diferenças do arquivo suprimidas por serem muito extensas
+ 165 - 186
src/app/(checkout)/checkout/cart/_components/CartWrapper.tsx


+ 31 - 12
src/app/(checkout)/checkout/cart/_components/EmptyCart.tsx

@@ -1,40 +1,59 @@
 "use client";
 
+import { useState } from "react";
 import Image from "next/image";
+import clsx from "clsx";
 import { useRouter } from 'next/navigation';
+import type { OrderDetails } from "@/types/customer/type";
+import { UnpaidOrderSection } from "./UnpaidOrderSection";
 
-
-export function EmptyCart({isGuest}:{isGuest: boolean;}) { 
+export function EmptyCart({
+    isGuest,
+    hasUnPaidOrder,
+    unpaidOrder
+}:{
+    isGuest: boolean;
+    hasUnPaidOrder: boolean;
+    unpaidOrder: Omit<OrderDetails, 'addresses'> | null;
+}) { 
 
     const router = useRouter();
+    const [showTips, setShowTips] = useState(true);
     return (
-        <div className="w-full box-border">
-            <div className="w-37.5 h-37.5 mt-6 mx-auto">
+        <div className="w-full box-border mt-12">
+            {(hasUnPaidOrder && unpaidOrder !== null) &&
+                <div className="w-130 mx-auto">
+                    <UnpaidOrderSection 
+                        orderDetail={unpaidOrder}
+                    />
+                </div>
+            }
+            <div className="w-50 h-50 mx-auto mt-6">
                 <Image src="/image/empty-cart.webp"
-                    width={150}
-                    height={150}
+                    width={200}
+                    height={200}
                     alt="empty cart"
                 />
             </div>
-            <p className="text-center text-ly-16">Your Cart Is Empty</p>
+            <p className="text-center text-ly-18 mt-6">Your Cart Is Empty</p>
             {isGuest && <>
-                <p className="mt-2 text-center text-ly-12 leading-5 text-ly-placeholder">Have an account? Sign in to view your cart</p>
-                <div className="relative border-box w-full px-4 mt-15">
+                <p className="mt-2 text-center text-ly-14 leading-5 text-ly-placeholder">Have an account? Sign in to view your cart</p>
+                <div className="relative border-box w-85.75 px-4 mt-15 mx-auto">
                     <button className="flex items-center justify-center w-full bg-ly-middlegreen text-ly-16 text-white h-12 rounded-3xl"
                         onClick={() => {router.replace('/customer/login')}}
                     >
                         Sign In
                     </button>
-                    <button className="mt-3 flex items-center justify-center w-full  text-ly-16 h-12 rounded-3xl border-1"
+                    <button className="mt-4 flex items-center justify-center w-full text-ly-16 h-12 rounded-3xl border-1"
                         onClick={() => {router.replace('/')}}
                     >
                         Back To Shop
                     </button>
-                    <div className="text-ly-12 absolute -top-4 left-1/2 -translate-x-1/2 -translate-y-full flex items-center bg-[#ffe033] w-max h-7 px-2 rounded-sm">
+                    <div className={clsx("text-ly-12 absolute -top-4 left-1/2 -translate-x-1/2 -translate-y-full flex items-center bg-[#ffe033] w-max h-7 px-2 rounded-sm", !showTips && "hidden")}>
                         <span className="absolute left-12.5 bottom-0 translate-y-2/5 bg-[#ffe033] w-3 h-3 rotate-z-45 rotate-y-30"></span>
                         <span className="font-bold px-1 leading-5 rounded-xs bg-white mr-2">$200+</span>
                         Log In To Unlock VIP CouponPack
-                        <button className="ml-1 w-6 h-6">
+                        <button className="ml-1 w-6 h-6" onClick={() => setShowTips(false)}>
                             <svg xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24" fill="none"><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M7 7L17 17"></path><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M7 17L17 7"></path></svg>
                         </button>
                         

+ 5 - 5
src/app/(checkout)/checkout/cart/_components/PromiseInCart.tsx

@@ -5,19 +5,19 @@
 export function PromiseInCart() { 
 
 
-    return (
-        <div className="flex items-center justify-center w-full box-border bg-[#EAE9E1] h-10">
+    return (<div className="w-full box-border bg-[#EAE9E1]">
+        <div className="flex items-center justify-between box-border max-w-248 px-4 h-10 mx-auto">
             <div className="flex-none text-center text-ly-14 leading-4">
                 30 Days Free Return
             </div>
-            <span className="h-5 border-[#cccccc] border-l-1 mx-32.5"></span>
+            <span className="h-5 border-[#cccccc] border-l-1"></span>
             <div className="flex-none text-center text-ly-14 leading-4 ">
                 24-48 Hrs Free Fast Shipping
             </div>
-            <span className="h-5 border-[#cccccc] border-l-1 mx-32.5"></span>
+            <span className="h-5 border-[#cccccc] border-l-1"></span>
             <div className="flex-none text-center text-ly-14 leading-4">
                 Buy Now & Pay Later
             </div>
         </div>
-    );
+    </div>);
 }

+ 5 - 4
src/app/(checkout)/checkout/cart/_components/UnpaidOrderSection.tsx

@@ -9,6 +9,7 @@ import { useCustomToast } from "@/utils/hooks/useToast";
 import {useConfig} from "@/utils/hooks/useConfig";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
+import {getProductAdditionalInfo} from "@/utils/orderDetailTools";
 import type { OrderDetails, ProductsInOrderDetails } from "@/types/customer/type";
 import CommonModal from "@/components/theme/ui/CommonModal";
 const CountDown = dynamic(() => import("@/components/common/CountDown"), {
@@ -82,7 +83,7 @@ export function UnpaidOrderSection({
         setShowDetails(false);
     };
     if(!isShowUpaidOrder) return null; 
-    return (<div className="w-full box-border p-4">
+    return (<>
         <div className="relative w-full box-border p-3 bg-ly-lightgray rounded-lg">
             <div className="flex items-center w-full">
                 Unpaid Order ({totalItemTxt})
@@ -95,7 +96,7 @@ export function UnpaidOrderSection({
                     />
                 </div>
             </div>
-            <div className="w-full mt-3 overflow-x-scroll" onClick={openProductsModal}>
+            <div className="w-full mt-3 overflow-x-auto scrollbar-thin scrollbar-thumb-gray-200" onClick={openProductsModal}>
                 <div className="w-full flex gap-3">
                     {productsData.map((product) => {
                         const baseImage = JSON.parse(product.baseImage);
@@ -176,7 +177,7 @@ export function UnpaidOrderSection({
                                             </div>
                                         </div>
                                         <div className="w-full mt-2 text-ly-12 border-1 rounded-sm border-[#d9d9d9] px-2.5 py-1.5 leading-5">
-                                            {product.sku}
+                                            {getProductAdditionalInfo(JSON.parse(product.additional))}
                                         </div>
                                     </div>
                                     <div className="flex items-center justify-between">
@@ -284,5 +285,5 @@ export function UnpaidOrderSection({
             }
         />
 
-    </div>);
+    </>);
 }

+ 11 - 12
src/app/(checkout)/checkout/cart/page.tsx

@@ -7,7 +7,7 @@ import type { OrderListData, OrderListDataVariables, OrderDetails } from "@/type
 import { EmptyCart } from "./_components/EmptyCart";
 import { PromiseInCart } from "./_components/PromiseInCart";
 import { CartWrapper } from "./_components/CartWrapper";
-import { UnpaidOrderSection } from "./_components/UnpaidOrderSection";
+
 /**
  * 购物车页 
  * 登录用户的话,获取用户地址列表 从auth session 中获取email
@@ -55,28 +55,27 @@ export default async function CheckoutPage() {
         cartDatahash = createHash('sha256').update(JSON.stringify(cartDetails)).digest('hex');
     }
 
-    return (
-        <>
-            <PromiseInCart />
-            {(unpaidOrder && !unpaidOrderExpiried) &&
-
-                <UnpaidOrderSection 
-                    orderDetail={unpaidOrder}
-                />
+    const hasUnPaidOrder = (unpaidOrder !== null && !unpaidOrderExpiried);
 
-            }
+    return (<>
+        <PromiseInCart />
+        <div className="mt-6 mb-30 max-w-312 mx-auto px-6">
             
             {!cartDetails && 
                 <EmptyCart 
                     isGuest={isGuest}
+                    hasUnPaidOrder={hasUnPaidOrder}
+                    unpaidOrder={unpaidOrder}
                 />
             }
             {cartDetails &&
                 <CartWrapper key={cartDatahash}
                     cartDetailData={cartDetails}
+                    hasUnPaidOrder={hasUnPaidOrder}
+                    unpaidOrder={unpaidOrder}
                 />
             }
-        </>
-    );
+        </div>
+    </>);
   
 }

+ 1 - 1
src/app/(public)/_components/CategoryTabSwiper .tsx

@@ -3,7 +3,7 @@
 import Link from "next/link";
 import { Swiper, SwiperSlide } from "swiper/react";
 import { FreeMode } from "swiper/modules";
-import "swiper/css";
+
 
 // 标签数据,可自由增删修改文字、跳转路由
 const tabList = [

+ 13 - 1
src/app/(public)/_components/HomeContent.tsx

@@ -1,6 +1,7 @@
 "use client";
 // import { Swiper, SwiperSlide } from "swiper/react";
 // import { Navigation } from "swiper/modules";
+import {useState} from "react";
 import JinGangSwiper from "./JinGangSwiper";
 import TopImageSwiper from "./TopImageSwiper"
 import HomeBestSellers from "./HomeBestSellers"
@@ -14,6 +15,7 @@ import "swiper/css";
 import "swiper/css/navigation";
 import type {HomeApiData,HomeProduct } from "@/types/home/type"
 import {CustomerService} from "@/components/common/CustomerService/CustomerService";
+// import AddToCartModalNew from "@/components/common/AddToCartModal/AddToCartModalNew";
 // 定义组件入参类型
 interface HomeContentProps {
   homeData: HomeApiData;
@@ -123,7 +125,12 @@ export default function HomeContent({ homeData }: HomeContentProps) {
  
    // 4. Recommended
   const recommendedSection = sections.find(sec => sec.name === "Recommended_For_You");
- 
+  // const recommended: HomeProduct[] = recommendedSection?.products ?? [];
+
+  const [isOpen, setIsOpen] = useState(false);
+  const closeHandler = () => {
+    setIsOpen(false);
+  } 
   return (<>
     <div className="min-h-screen pt-[162px] bg-white font-sans">
       {/* 顶部Banner */}
@@ -152,5 +159,10 @@ export default function HomeContent({ homeData }: HomeContentProps) {
       {/* <CategoryTabSwiper /> */}
     </div>
     <CustomerService />
+    <button onClick={() => setIsOpen(true)}>test</button>
+    {/* <AddToCartModalNew
+        isOpen={isOpen}
+        onClose={closeHandler}
+    /> */}
   </>);
 }

+ 0 - 1
src/app/(public)/_components/TopImageSwiper.tsx

@@ -1,6 +1,5 @@
 
 import { Swiper, SwiperSlide } from 'swiper/react';
-import 'swiper/css';
 // import { Navigation, Pagination, Mousewheel, Keyboard } from 'swiper/modules';
 // 定义图片类型
 type BannerImageItem = {

+ 0 - 5
src/app/(public)/_components/css/swiper-custom.css

@@ -1,5 +0,0 @@
-/* swiper-custom.css */
-.swiper-horizontal > .swiper-pagination-progressbar, .swiper-pagination-progressbar.swiper-pagination-horizontal{
-    bottom: -16px ;
-    top: unset;
-}

+ 0 - 5
src/app/(public)/category/[slug]/[id]/page.tsx

@@ -17,11 +17,6 @@ import {
   GET_CATEGORY_ATTR_FILTERS,
   CATEGORY_PRODUCTS,
 } from "@/graphql";
-import {
-  // cachedGraphQLRequest,
-  // cachedCategoryRequest,
-  // getFilterAttributes,
-} from "@/utils/hooks/useCache";
 import { newSortByFields } from "@utils/constants";
 // import { CategoryDetail } from "@components/theme/search/CategoryDetail";
 import CategoryDesc from "./_components/CategoryDesc";

+ 0 - 1
src/app/(public)/customer/account/_components/AccountBottomHrefSwiper.tsx

@@ -5,7 +5,6 @@ import { Swiper, SwiperSlide } from "swiper/react";
 // 只导入 Autoplay 即可!loop 内置了
 import { Autoplay } from "swiper/modules";
 
-import "swiper/css";
 
 const AccountBottomHrefSwiper = () => {
   const swiperRef = useRef<any>(null);

+ 0 - 2
src/app/(public)/customer/account/_components/AccountVipPopup.tsx

@@ -6,8 +6,6 @@ import { Navigation, Scrollbar } from "swiper/modules";
 import type { Swiper as SwiperType } from "swiper/types";
 import { useEffect, useState } from "react";
 import Image from "next/image";
-import "swiper/css";
-import "swiper/css/navigation";
 import { clientFetch } from "@/lib/restApiClient";
 
 // 模拟你的每一页数据,方便维护

+ 5 - 5
src/app/(public)/customer/forget-password/_components/ForgetPasswordForm.tsx

@@ -53,7 +53,7 @@ export default function ForgetPasswordForm() {
 
     return (<>{!emailValue ?
 
-        <div className="w-full box-border px-3">
+        <div className="w-full box-border">
             <h2 className="text-center text-ly-18 font-bold">
                 Retrieve Your Password Here
             </h2>
@@ -66,7 +66,7 @@ export default function ForgetPasswordForm() {
                 className="w-full  mt-9"
                 onSubmit={handleSubmit(onSubmit)}
             >
-                <label className="text-ly-12 block mb-1 font-semibold">
+                <label className="text-ly-16 block mb-1">
                     *Email Address
                 </label>
                 <InputText
@@ -101,8 +101,8 @@ export default function ForgetPasswordForm() {
                 </div>
             </form>
         </div>
-    :
-        <div className="w-full box-border px-3 mt-6">
+        : 
+        <div className="w-full box-border mt-20">
 
             <h2 className="text-center text-ly-18 font-bold">
                 Password Reset
@@ -113,7 +113,7 @@ export default function ForgetPasswordForm() {
                 with a link to resetyour password.<br/>
                 Please check your mailbox and follow theinstruction.
             </p>
-            <p className="relative pl-4 mt-6 text-ly-16 leading-6">
+            <p className="relative pl-4 mt-4 text-ly-16 leading-6">
                 <span className="absolute left-0 top-2 w-1.5 h-1.5 rounded-sm bg-[#00BAAD] mr-2.5"></span>
                 Emails may experience few-minute delays.
             </p>

+ 1 - 1
src/app/(public)/customer/forget-password/page.tsx

@@ -8,7 +8,7 @@ export async function generateMetadata() {
 }
 
 export default function ForgetPasswordPage() {
-    return (<div className="w-full mt-6 box-border">
+    return (<div className="w-87.5 mx-auto mt-20 mb-30 box-border">
         <ForgetPasswordForm />
 
     </div>);

+ 13 - 13
src/app/(public)/customer/login/_components/LoginPageWrapper.tsx

@@ -140,7 +140,7 @@ export function LoginPageWrapper({
     };
 
     return (
-        <div className="box-border w-full mt-6 pb-8">
+        <div className="box-border w-87.5 mt-6 mb-8 mx-auto">
             <div className="flex justify-center items-center">
                 <button 
                     className={clsx('text-ly-18',{
@@ -165,12 +165,12 @@ export function LoginPageWrapper({
                 </button>
             </div>
 
-            <form className={clsx(["relative px-4 pt-2", (activeTab === 'login' ? 'block' : 'hidden')])}
+            <form className={clsx(["relative mt-6", (activeTab === 'login' ? 'block' : 'hidden')])}
                 onSubmit={loginForm.handleSubmit(loginSubmit)}
             >
                                 
                 <div className="w-full">
-                    <label className="text-ly-12 block mb-3 font-semibold">
+                    <label className="text-ly-16 block mb-1 font-semibold">
                         Email *
                     </label>
                     <InputText
@@ -187,7 +187,7 @@ export function LoginPageWrapper({
                     />
                 </div>
                 <div className="w-full mt-4">
-                    <label className="text-ly-12 block mb-3 font-semibold">
+                    <label className="text-ly-16 block mb-1 font-semibold">
                         Password *
                     </label>
                     <PasswordInput
@@ -211,7 +211,7 @@ export function LoginPageWrapper({
                     />
                 </div>
                 
-                <button className="flex items-center justify-center mt-6 w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold">Log In</button>
+                <button className="flex items-center justify-center mt-6 w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16">Log In</button>
                 
                 <div className="flex justify-end mt-4">
                     <Link href="/customer/forget-password"
@@ -221,13 +221,13 @@ export function LoginPageWrapper({
             </form>
 
             <form 
-                className={clsx(["relative px-4 pt-2 pb-6",(activeTab === 'register' ? 'block' : 'hidden')])} 
+                className={clsx(["relative mt-6",(activeTab === 'register' ? 'block' : 'hidden')])} 
                 onSubmit={registerForm.handleSubmit(registerSubmit)}
             >
                                     
                 <div className="w-full flex justify-between">
                     <div className="w-41.25 flex-none">
-                        <label className="text-ly-12 block mb-3 font-semibold">
+                        <label className="text-ly-16 block mb-1 font-semibold">
                             First Name *
                         </label>
                         <InputText
@@ -244,7 +244,7 @@ export function LoginPageWrapper({
                         />
                     </div>
                     <div className="w-41.25 flex-none">
-                        <label className="text-ly-12 block mb-3 font-semibold">
+                        <label className="text-ly-16 block mb-1 font-semibold">
                             Last Name *
                         </label>
                         <InputText
@@ -263,7 +263,7 @@ export function LoginPageWrapper({
 
                 </div>
                 <div className="w-full mt-4">
-                    <label className="text-ly-12 block mb-3 font-semibold">
+                    <label className="text-ly-16 block mb-1 font-semibold">
                         Email *
                     </label>
                     <InputText
@@ -281,7 +281,7 @@ export function LoginPageWrapper({
                 </div>
 
                 <div className="w-full mt-4">
-                    <label className="text-ly-12 block mb-3 font-semibold">
+                    <label className="text-ly-16 block mb-1 font-semibold">
                         Password *
                     </label>
                     <PasswordInput
@@ -306,7 +306,7 @@ export function LoginPageWrapper({
                 </div>
 
                 <div className="w-full mt-4">
-                    <label className="text-ly-12 block mb-3 font-semibold">
+                    <label className="text-ly-16 block mb-1 font-semibold">
                         Confirm Password *
                     </label>
                     <PasswordInput
@@ -333,7 +333,7 @@ export function LoginPageWrapper({
                     />
                 </div>
                 <div className="w-full mt-4"> 
-                    <label className="text-ly-12 block mb-3 font-semibold">Phone number *</label>
+                    <label className="text-ly-16 block mb-1 font-semibold">Phone number *</label>
                     <Controller
                         name="phone"
                         control={registerForm.control}
@@ -363,7 +363,7 @@ export function LoginPageWrapper({
                     
                 </div>
 
-                <button className="flex items-center justify-center mt-6 w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold">
+                <button className="flex items-center justify-center mt-6 w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16">
                     Register
                 </button>
                 <div className="relative w-full mt-4">

+ 4 - 4
src/app/(public)/customer/reset-password/_components/ResetPasswordWrapper.tsx

@@ -80,14 +80,14 @@ export default function ResetPasswordWrapper({
 
     return (
 
-        <div className="w-full box-border px-3 mt-6">
+        <div className="w-87.5 box-border mx-auto mt-20 mb-30">
             <h2 className="text-center text-ly-18 font-bold">
                 Change Password
             </h2>
 
             <form className="w-full mt-6" onSubmit={handleSubmit(onSubmit)}>
                 <div className="w-full">
-                    <label className="text-ly-12 block mb-1 font-semibold">
+                    <label className="text-ly-16 block mb-1 font-semibold">
                         *New Password
                     </label>
                     <PasswordInput
@@ -111,7 +111,7 @@ export default function ResetPasswordWrapper({
                     />
                 </div>
                 <div className="w-full mt-4">
-                    <label className="text-ly-12 block mb-1 font-semibold">
+                    <label className="text-ly-16 block mb-1 font-semibold">
                         *Confirm Password
                     </label>
 
@@ -140,7 +140,7 @@ export default function ResetPasswordWrapper({
                 </div>
 
 
-                <div className="w-full mt-9">
+                <div className="w-full mt-6">
                     <button type="submit" 
                         title="Reset Password"
                         className="flex items-center justify-center w-full h-12 bg-ly-middlegreen text-ly-16 text-white rounded-3xl"

+ 1 - 1
src/app/(public)/customer/reset-password/page.tsx

@@ -16,7 +16,7 @@ export default async function ResetPasswordPage({
 
     return (<>
         {errorMsg ?
-            <p className="mt-6 text-center text-ly-14">{errorMsg}</p>
+            <p className="mt-20 text-center text-ly-14">{errorMsg}</p>
         :
             <ResetPasswordWrapper 
                 token={resolvedParams.token}

+ 3 - 5
src/app/(public)/product/_components/ProductMedia.tsx

@@ -1,14 +1,12 @@
 "use client";
 
 import Image from "next/image";
-import "swiper/css";
-import "swiper/css/pagination";
 import { Swiper, SwiperSlide } from "swiper/react";
 import { Pagination } from "swiper/modules";
 import type { Swiper as SwiperType } from "swiper";
 import type { PaginationOptions } from "swiper/types";
 import type { ProductMediaType } from "@/components/catalog/type";
-import { baseUrl, getImageUrl } from "@/utils/constants";
+import { getImageUrl } from "@/utils/constants";
 import { useState, useRef } from "react";
 
 export function ProductMedia({
@@ -60,7 +58,7 @@ export function ProductMedia({
                 }}
               >
                 <Image
-                  src={getImageUrl(img.publicPath, baseUrl)}
+                  src={getImageUrl(img.publicPath)}
                   width={72}
                   height={96}
                   loading="eager"
@@ -91,7 +89,7 @@ export function ProductMedia({
             <SwiperSlide className="relative w-full h-full" key={img.id}>
               <Image
                 className="block w-full h-full object-cover"
-                src={getImageUrl(img.publicPath, baseUrl)}
+                src={getImageUrl(img.publicPath)}
                 // width={528}
                 // height={704}
                 fill

+ 1 - 2
src/app/(public)/product/_components/review/ReviewImageGalleryModal.tsx

@@ -4,8 +4,7 @@ import { Swiper, SwiperSlide } from "swiper/react";
 import { Navigation } from "swiper/modules";
 import Image from "next/image";
 import type { GalleryItem } from "@/types/products/review";
-import "swiper/css";
-import "swiper/css/navigation";
+
 
 interface GalleryModalProps {
   open: boolean;

+ 1 - 3
src/app/(public)/product/_components/youmaylike/Recommend.tsx

@@ -3,9 +3,7 @@
 import { Swiper, SwiperSlide } from "swiper/react";
 import { Navigation } from "swiper/modules";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
-// 导入Swiper基础样式
-import "swiper/css";
-import "swiper/css/navigation";
+
 
 // 模拟产品数据(12个产品,分2块,每块6个)
 const productData = [

+ 1 - 1
src/app/(public)/search/page.tsx

@@ -153,7 +153,7 @@ export default async function SearchPage({
       <MobileSearchBar />
       <HotSearch />
       {isArray(products) ? (
-        <div className="flex items-center justify-between gap-4 pt-0 pb-6 md:hidden  mx-auto w-full max-w-screen-2xl px-4 xss:px-7.5">
+        <div className="flex items-center justify-between gap-4 pt-0 pb-6 mx-auto w-full max-w-screen-2xl px-4 xss:px-7.5">
           {/* <MobileFilter filterAttributes={filterAttributes} /> */}
           <div>{totalCount||0} items</div>
           <SortOrder sortOrders={SortByFields} title="Sort by" />

+ 7 - 8
src/app/api/revalidate/route.ts

@@ -1,9 +1,10 @@
-import { type NextRequest, NextResponse } from "next/server";
+import { NextResponse } from "next/server";
 import { headers } from "next/headers";
 import { revalidatePath } from "next/cache";
 
 
-async function revalidate(req: NextRequest): Promise<NextResponse> {
+
+async function revalidate(): Promise<NextResponse> {
   const collectionWebhooks = [
     "collections/create",
     "collections/delete",
@@ -15,13 +16,11 @@ async function revalidate(req: NextRequest): Promise<NextResponse> {
     "products/update",
   ];
   const topic = (await headers()).get("x-bagisto-topic") || "unknown";
-  const secret = req.nextUrl.searchParams.get("secret");
+
   const isCollectionUpdate = collectionWebhooks.includes(topic);
   const isProductUpdate = productWebhooks.includes(topic);
 
-  if (!secret || secret !== process.env.BAGISTO_REVALIDATION_SECRET) {
-    return NextResponse.json({ status: 200 });
-  }
+
 
   if (!isCollectionUpdate && !isProductUpdate) {
     return NextResponse.json({ status: 200, message: "No action needed" });
@@ -42,6 +41,6 @@ async function revalidate(req: NextRequest): Promise<NextResponse> {
 }
 
 
-export async function POST(req: NextRequest): Promise<NextResponse> {
-  return revalidate(req);
+export async function POST(): Promise<NextResponse> {
+  return revalidate();
 }

+ 404 - 143
src/app/globals.css

@@ -1,68 +1,88 @@
+/* =========================================================
+ * Cascade layer order
+ *
+ * theme
+ *   ↓
+ * base
+ *   ↓
+ * vendor.swiper
+ *   ↓
+ * components
+ *   ↓
+ * utilities
+ * ======================================================= */
+
+@layer theme, base, vendor, components, utilities;
+@layer vendor.swiper;
+
+
+/* =========================================================
+ * Framework / vendor imports
+ *
+ * 注意:
+ * @import 必须位于普通 CSS rule 之前。
+ * Swiper 按需引入,不建议无脑使用 swiper/css/bundle。
+ * ======================================================= */
+
 @import "tailwindcss";
-@plugin '../../hero.ts';
-@source '../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
-@custom-variant dark (&:is(.dark *));
 
-html,
-body {
-  scrollbar-width: thin;
-  scrollbar-color: #6b7280 transparent;
-}
+@import "swiper/css" layer(vendor.swiper);
+@import "swiper/css/navigation" layer(vendor.swiper);
+@import "swiper/css/pagination" layer(vendor.swiper);
+@import 'swiper/css/free-mode' layer(vendor.swiper);
+@import 'swiper/css/thumbs' layer(vendor.swiper);
 
 
-/**
- * 移动端适配核心:clamp() 根字号  在390px设计稿下 16px ≈ 4.1025641vw = 1rem
- * 比如 390px设计稿,有一个div宽度是120px,那么可以写 w-30,下面是换算原理:
- * tailwindcss中单位用的是rem, 默认根字号为16px,也就是在tailwindcss中,0.25rem = 4px,
- * 而对于类名 w-1 来说,这里的1就是0.25rem,即4px,那么w-1就是4px,w-2就是8px,以此类推
- * 
- * 
-html {
-  font-size: clamp(10px, 4.1025641vw, 40px);
-}
-*/
-/* GLOBAL VARIABLES :root中定义常规变量,不被映射到工具类*/
-:root {
-  /* Font families */
-  font-family: var(--font-outfit-variable), system-ui, sans-serif;
-  --background: #ffffff;
-  --foreground: #000;
-  --color-selected-black: #00000099;
-
-  --hero-background: #ffffff;
-  --hero-foreground: #000;
-  --dark-grey: #404040;
-  --selected-color: #e3ecff;
-  --selected-color-dark: #555b69;
-  --selected-bottom-dark: #95b6ff;
-  --selected-bg-bottom-dark: #314779;
-  --input-color: #b3b3b3;
-
-  /* 字体尺寸 */
-  --font-size-xxs: 13px;
-
-  /* 间距变量(供自定义类使用) */
-  --space-30: 120px;
-  --space-7_5: 30px;
-  --space-92_5: 23.125rem;
-}
+/* =========================================================
+ * HeroUI v2
+ * ======================================================= */
+
+@plugin "../../hero.ts";
+
+@source "../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}";
+
+@custom-variant dark (&:is(.dark *));
+
+
+/* =========================================================
+ * Tailwind theme tokens
+ *
+ * @theme 中的变量会映射到 Tailwind utilities。
+ *
+ * 例如:
+ * --color-ly-gray
+ *   → text-ly-gray
+ *   → bg-ly-gray
+ *   → border-ly-gray
+ *
+ * --text-ly-16
+ *   → text-ly-16
+ *
+ * --leading-ly-24
+ *   → leading-ly-24
+ * ======================================================= */
 
-/*theme中定义的变量可以被映射到实用程序类(需要注意映射规则,不是所有变量都能映射)*/
 @theme {
-  /*自定义颜色*/
+  /* ---------- Colors ---------- */
+
   --color-ly-inputborder: #e5e5e5;
-  --color-ly-placeholder: #A6A6A6;
-  --color-ly-errorcolor: #FF0000;
+  --color-ly-placeholder: #a6a6a6;
+  --color-ly-errorcolor: #ff0000;
+
   --color-ly-lightgray: #f9f9f9;
   --color-ly-middlegray: #f2f2f2;
-  --color-ly-gray: #d3d3d3; /*可被映射为text-ly-gray,bg-ly-gray等相关的颜色类目*/
-  --color-ly-deepgray: #ACACAC;
-  --color-ly-gold: #ffd700;
+  --color-ly-gray: #d3d3d3;
+  --color-ly-deepgray: #acacac;
+
   --color-ly-gold: #ffd700;
+
   --color-ly-green: #01754a;
   --color-ly-middlegreen: #036141;
   --color-ly-deepgreen: #1e3932;
-  /*自定义字体大小 前缀 ly*/
+
+
+  /* ---------- Font sizes ---------- */
+
   --text-ly-12: 0.75rem;
   --text-ly-13: 0.8125rem;
   --text-ly-14: 0.875rem;
@@ -71,11 +91,13 @@ html {
   --text-ly-20: 1.25rem;
   --text-ly-22: 1.375rem;
   --text-ly-24: 1.5rem;
-  --text-ly-30: 1.875rem; /* 1.875rem (30px) */ 
-	--text-ly-36: 2.25rem; /* 2.25rem (36px) */ 	
-  --text-ly-48: 3rem; /* 3rem (48px) */ 
+  --text-ly-30: 1.875rem;
+  --text-ly-36: 2.25rem;
+  --text-ly-48: 3rem;
+
+
+  /* ---------- Line heights ---------- */
 
-  /*自定义行高 前缀 ly*/
   --leading-ly-16: 1rem;
   --leading-ly-18: 1.125rem;
   --leading-ly-20: 1.25rem;
@@ -87,48 +109,161 @@ html {
 }
 
 
-.dark {
-  --background: #171717;
-  --foreground: #ffffff;
-
-  --hero-background: #171717;
-  --hero-foreground: #ffffff;
-}
+/* =========================================================
+ * Runtime variables -> Tailwind theme
+ *
+ * inline 用于引用其它 CSS variables。
+ *
+ * 例如:
+ *
+ * --background
+ *      ↓
+ * --color-background
+ *      ↓
+ * bg-background
+ *
+ * 当 .dark 修改 --background 后,
+ * bg-background 会自动跟随,不需要额外写:
+ *
+ * bg-white dark:bg-background
+ * ======================================================= */
 
-/*@theme指令中的 inline 表示定义引用其他变量的主题变量
-  比如 定义了一个变量--color-background,--color-background引用的是另一个变量
-*/
 @theme inline {
-  --font-outfit: var(--font-outfit-variable), system-ui, sans-serif;
+  --font-outfit:
+    var(--font-outfit-variable),
+    system-ui,
+    sans-serif;
+
   --color-background: var(--background);
   --color-foreground: var(--foreground);
-  --color-font-color: var(--font-color);
-  --color-selected-black: var(--color-selected-black);
+
+  --color-selected-black: var(--selected-black);
 
   --color-dark-grey: var(--dark-grey);
+
   --color-selected-color: var(--selected-color);
   --color-selected-color-dark: var(--selected-color-dark);
+
   --color-selected-bottom-dark: var(--selected-bottom-dark);
   --color-selected-bg-bottom-dark: var(--selected-bg-bottom-dark);
+
   --color-input-color: var(--input-color);
 }
 
-body {
-  background: var(--background);
-  color: var(--foreground);
-  min-width: 768px;
+
+/* =========================================================
+ * Base
+ *
+ * 只放:
+ * - 全局 CSS variables
+ * - html/body
+ * - document-level defaults
+ *
+ * 不放具体业务组件样式。
+ * ======================================================= */
+
+@layer base {
+  :root {
+    /* ---------- Theme ---------- */
+
+    --background: #ffffff;
+    --foreground: #000000;
+
+    --selected-black: rgb(0 0 0 / 60%);
+
+    --hero-background: #ffffff;
+    --hero-foreground: #000000;
+
+    --dark-grey: #404040;
+
+    --selected-color: #e3ecff;
+    --selected-color-dark: #555b69;
+
+    --selected-bottom-dark: #95b6ff;
+    --selected-bg-bottom-dark: #314779;
+
+    --input-color: #b3b3b3;
+  }
+
+
+  /* ---------- Dark theme ---------- */
+
+  .dark {
+    --background: #171717;
+    --foreground: #ffffff;
+
+    --hero-background: #171717;
+    --hero-foreground: #ffffff;
+  }
+
+
+  /* ---------- Document ---------- */
+
+  html {
+    font-family:
+      var(--font-outfit-variable),
+      system-ui,
+      sans-serif;
+
+    /*
+     * PC 项目推荐保留稳定 scrollbar gutter,
+     * modal / drawer 锁定 body 时减少页面左右跳动。
+     */
+    scrollbar-gutter: stable;
+
+    scrollbar-width: thin;
+    scrollbar-color: #6b7280 transparent;
+  }
+
+  body {
+    /*
+     * PC 项目:
+     * 保留你原来的最小页面宽度。
+     *
+     * 如果你的产品实际设计基准是 1024 / 1200,
+     * 可以后续根据设计稿调整。
+     */
+    min-width: 768px;
+
+    background-color: var(--background);
+    color: var(--foreground);
+  }
 }
 
+
+/* =========================================================
+ * Components
+ *
+ * 放项目自己的复合样式和第三方组件 overrides。
+ *
+ * vendor.swiper 在 components 之前,因此这里的
+ * Swiper override 不需要通过 !important 去抢优先级。
+ * ======================================================= */
+
 @layer components {
-  /* Hide scrollbar for drawer content */
+  /* =======================================================
+   * Scrollbar
+   * ===================================================== */
+
   .drawer-scrollbar-hidden {
     scrollbar-width: none;
-    -ms-overflow-style: none;
   }
+
   .drawer-scrollbar-hidden::-webkit-scrollbar {
     display: none;
   }
 
+
+  /* =======================================================
+   * Placeholder
+   *
+   * 兼容旧代码。
+   *
+   * 新代码优先考虑直接使用:
+   * placeholder:text-input-color
+   * dark:placeholder:text-selected-color-dark
+   * ===================================================== */
+
   .placeholder-input-color::placeholder {
     color: var(--input-color);
   }
@@ -137,124 +272,250 @@ body {
     color: var(--selected-color-dark);
   }
 
+
+  /* =======================================================
+   * Sticky heading
+   * ===================================================== */
+
   .mobile-heading h2 {
-    @apply sticky bg-white z-50 dark:bg-background top-0;
+    /*
+     * bg-background 本身已经会跟随 --background,
+     * 不需要:
+     *
+     * bg-white dark:bg-background
+     */
+    @apply sticky top-0 z-50 bg-background;
+  }
+
+
+  /* =======================================================
+   * Scroll lock
+   *
+   * PC 项目不需要通过 position: fixed + 100vw
+   * 处理 iOS Safari body scroll。
+   *
+   * scrollbar-gutter: stable 可以减少 scrollbar
+   * 消失造成的页面横向跳动。
+   * ===================================================== */
+
+  .scroll-locked {
+    overflow: hidden !important;
   }
 
-  .prose h1 {
-    margin: 12px 0 !important;
-    font-family: Outfit, sans-serif !important;
-    font-size: 24px !important;
-    font-weight: 600 !important;
+
+  /* =======================================================
+   * Disabled stroke
+   * ===================================================== */
+
+  .strokeline-bg-disabled {
+    background-image:
+      linear-gradient(
+        to bottom right,
+        transparent calc(50% - 1px),
+        #acacac calc(50% - 1px),
+        #acacac calc(50% + 1px),
+        transparent calc(50% + 1px)
+      );
   }
 
-  .prose p {
-    color: var(--color-selected-black) !important;
-    font-weight: 300 !important;
+
+  /* =======================================================
+   * Input
+   * ===================================================== */
+
+  .ly-input {
+    @apply
+      block
+      h-14
+      w-full
+      box-border
+      border
+      border-ly-inputborder
+      px-4
+      text-ly-16
+      leading-4
+      placeholder:text-ly-placeholder
+      focus:outline-none
+      focus-visible:border-ly-green
+      focus-visible:ring-2
+      focus-visible:ring-ly-green/20;
   }
 
-  .dark .prose p {
-    color: white !important;
-    font-weight: 300 !important;
+
+  /* =======================================================
+   * Swiper overrides
+   *
+   * Swiper 原始 CSS:
+   * vendor.swiper
+   *
+   * 项目 override:
+   * components
+   *
+   * 所以这里无需 !important。
+   * ===================================================== */
+
+  .addtocartmodal-main-swiper-prev-btn,
+  .addtocartmodal-main-swiper-next-btn {
+    @apply bg-white;
   }
 
-  .margin-t {
-    margin-top: -9px;
+  .addtocartmodal-main-swiper-prev-btn.disable,
+  .addtocartmodal-main-swiper-next-btn.disable {
+    @apply bg-black/50;
   }
 
-  @keyframes shimmer {
-    0% {
-      transform: translateX(-100%);
-    }
-    100% {
-      transform: translateX(100%);
-    }
+
+  .addtocartmodal-main-swiper-prev-btn svg,
+  .addtocartmodal-main-swiper-next-btn svg {
+    @apply stroke-black;
+  }
+
+  .addtocartmodal-main-swiper-prev-btn.disable svg,
+  .addtocartmodal-main-swiper-next-btn.disable svg {
+    @apply stroke-white;
   }
 
+
+  .addtocartmodal-main-swiper .swiper-pagination-fraction {
+    @apply
+      absolute
+      bottom-4
+      left-1/2
+      z-1
+      flex
+      h-6
+      w-auto
+      -translate-x-1/2
+      items-center
+      rounded-xl
+      px-3
+      text-ly-14
+      text-white;
+  }
+
+
+  /* =======================================================
+   * Image
+   * ===================================================== */
+
   .image-carousel-aspect {
     aspect-ratio: 1.72;
   }
 
 
-  /* Prevent body scrolling when drawer is open */
-  .scroll-locked {
-    overflow: hidden !important;
-    height: 100vh !important;
-    width: 100vw !important;
-    position: fixed !important;
-    top: 0;
-    left: 0;
-    touch-action: none;
-    -webkit-overflow-scrolling: none;
+  /* =======================================================
+   * Animation
+   * ===================================================== */
+
+  .fade-in {
+    animation: fade-in 0.3s ease-in-out forwards;
   }
-  /*禁用按钮样式*/
-  .strokeline-bg-disabled {
-    background-image:linear-gradient(to bottom right,transparent,transparent calc(50% - 1px),#acacac 50%,#acacac 0,transparent calc(50% + 1px));
+
+  .fade-out {
+    animation: fade-out 0.3s ease-in-out forwards;
   }
-  /*输入框样式*/
-  .ly-input{
-    @apply block w-full box-border text-ly-16 px-4 leading-4 border-1 border-ly-inputborder h-14 placeholder:text-ly-placeholder focus:outline-none
+
+  .slide-bottom-in {
+    animation: slide-bottom-in 0.3s ease-in-out forwards;
   }
 
-  .fade-in{
-    animation: fade-in 0.3s ease-in-out forwards;
+  .slide-bottom-out {
+    animation: slide-bottom-out 0.3s ease-in-out forwards;
   }
-  .fade-out{
-    animation: fade-out 0.3s ease-in-out forwards;
+
+
+  @keyframes shimmer {
+    from {
+      transform: translateX(-100%);
+    }
+
+    to {
+      transform: translateX(100%);
+    }
   }
-  @keyframes fade-in{
-    0%{
+
+  @keyframes fade-in {
+    from {
       opacity: 0;
     }
-    100%{
+
+    to {
       opacity: 1;
     }
   }
-  @keyframes fade-out{
-    0%{
+
+  @keyframes fade-out {
+    from {
       opacity: 1;
     }
-    100%{
+
+    to {
       opacity: 0;
     }
   }
-  .slide-bottom-in{
-    animation: slide-bottom-in 0.3s ease-in-out forwards;
-  }
-  .slide-bottom-out{
-    animation: slide-bottom-out 0.3s ease-in-out forwards;
-  }
-  @keyframes slide-bottom-in{
-    0%{
+
+  @keyframes slide-bottom-in {
+    from {
       transform: translateY(100%);
     }
-    100%{
+
+    to {
       transform: translateY(0);
     }
   }
-  @keyframes slide-bottom-out{
-    0%{
-      transform: translateY(0%);
+
+  @keyframes slide-bottom-out {
+    from {
+      transform: translateY(0);
     }
-    100%{
+
+    to {
       transform: translateY(100%);
     }
   }
 
-  .ly-thin-shadow{
-      box-shadow: 0 0px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
+
+  /* =======================================================
+   * Project styles
+   * ===================================================== */
+
+  .ly-thin-shadow {
+    box-shadow:
+      0 0 3px rgb(0 0 0 / 10%),
+      0 1px 2px rgb(0 0 0 / 6%);
   }
-  .ly-vipplus-bg{
-    background: linear-gradient(128.05deg, rgba(250, 231, 200, 1) 0%, rgba(249, 255, 235, 1) 27.29%, rgba(242, 215, 172, 1) 64.42%, rgba(232, 202, 153, 1) 100%);
+
+
+  .ly-vipplus-bg {
+    background:
+      linear-gradient(
+        128.05deg,
+        rgb(250 231 200) 0%,
+        rgb(249 255 235) 27.29%,
+        rgb(242 215 172) 64.42%,
+        rgb(232 202 153) 100%
+      );
   }
-}
 
-apple-pay-button {
+
+  /* =======================================================
+   * Apple Pay
+   * ===================================================== */
+
+  apple-pay-button {
     --apple-pay-button-box-sizing: border-box;
     --apple-pay-button-width: 300px;
     --apple-pay-button-height: 48px;
     --apple-pay-button-border-radius: 24px;
-    --apple-pay-button-padding: 6px 0px;
+    --apple-pay-button-padding: 6px 0;
+
     display: block;
     width: 100%;
-}
+  }
+
+  .middle-image-swiper.swiper-horizontal > .swiper-pagination-progressbar, 
+  .middle-image-swiper.swiper-horizontal .swiper-pagination-progressbar.swiper-pagination-horizontal{
+      bottom: -16px ;
+      top: unset;
+  }
+}

+ 10 - 13
src/app/layout.tsx

@@ -3,6 +3,7 @@ import type { Viewport } from 'next';
 import { cookies } from "next/headers";
 import Script from "next/script";
 import "./globals.css";
+import { env } from "@/env";
 import { GlobalProviders,ReduxProvider,PayPalWrapper,ConfigProvider } from "@/providers";
 import { generateMetadataForPage } from "@utils/helper";
 import { staticSeo } from "@utils/metadata";
@@ -68,21 +69,17 @@ export default async function RootLayout({
   children: React.ReactNode;
 }>) {
 
-  let paypalClientId = process.env.PAYPAL_PRODUCTION_CLIENT_ID;
-  if (process.env.APP_ENV !== "production") {
-    paypalClientId = process.env.PAYPAL_SANDBOX_CLIENT_ID;
-  }
-  if(paypalClientId === undefined) {
-    throw new Error('Paypal Client Id is not defined');
+  let paypalClientId = env.PAYPAL_PRODUCTION_CLIENT_ID;
+  if (env.APP_ENV !== "production") {
+    paypalClientId = env.PAYPAL_SANDBOX_CLIENT_ID;
   }
+
   // DEFAULT_CHANNEL DEFAULT_CURRENCY DEFAULT_LOCAL 同时也会写入cookie 
-  const defaultChannel = process.env.DEFAULT_CHANNEL;
-  const defaultCurrency = process.env.DEFAULT_CURRENCY;
-  const defaultLocale = process.env.DEFAULT_LOCAL;
-  const storeName = process.env.STORE_NAME;
-  if(!defaultChannel || !defaultCurrency || !defaultLocale ||!storeName) {
-    throw new Error('Default Channel or Default Currency or Default Locale or store name is not defined');
-  }
+  const defaultChannel = env.DEFAULT_CHANNEL;
+  const defaultCurrency = env.DEFAULT_CURRENCY;
+  const defaultLocale = env.DEFAULT_LOCAL;
+  const storeName = env.STORE_NAME;
+
 
   const cookieStore = await cookies();
   ////////eruda调试/////////

+ 3 - 2
src/app/robots.ts

@@ -1,4 +1,5 @@
-import type { MetadataRoute } from 'next'
+import type { MetadataRoute } from 'next';
+import { env } from "@/env";
  
 // src/app/robots.ts 是 Next.js App Router 中用于生成 `robots.txt 文件的特殊路由文件。
 // 它的作用是告诉 搜索引擎爬虫(Googlebot、Bingbot 等)哪些页面可以抓取,哪些页面不要抓取,同时可以声明网站的 sitemap 地址。
@@ -9,6 +10,6 @@ export default function robots(): MetadataRoute.Robots {
       allow: '/',
       disallow:  ['/customer/*', '/checkout'],
     },
-    sitemap: `${process.env.NEXT_PUBLIC_SITE_URL}/sitemap.xml`,
+    sitemap: `${env.NEXT_PUBLIC_SITE_URL}/sitemap.xml`,
   }
 }

+ 0 - 305
src/components/cart/CartModal.tsx

@@ -1,305 +0,0 @@
-"use client";
-import clsx from "clsx";
-import { useDisclosure } from "@heroui/use-disclosure";
-import { AnimatePresence, motion } from "framer-motion";
-import { ShoppingCartIcon } from "@heroicons/react/24/outline";
-import { DEFAULT_OPTION } from "@/utils/constants";
-import OpenCart from "./OpenCart";
-import { Price } from "../theme/ui/Price";
-import { DeleteItemButton } from "../common/icons/cart/DeleteItemButton";
-import { EditItemQuantityButton } from "../common/icons/cart/EditItemQuantityButton";
-import { useAppSelector } from "@/store/hooks";
-import Image from "next/image";
-import { NOT_IMAGE } from "@utils/constants";
-import LoadingDots from "@components/common/icons/LoadingDots";
-import { useFormStatus } from "react-dom";
-import { redirectToCheckout } from "@/utils/actions";
-import Link from "next/link";
-import { createUrl, safeParse } from "@utils/helper";
-import { useBodyScrollLock } from "@utils/hooks/useBodyScrollLock";
-import { useConfig } from "@utils/hooks/useConfig";
-
-
-type MerchandiseSearchParams = {
-  [key: string]: string;
-};
-export default function CartModal({
-  children,
-  className,
-  onOpen,
-  onClose,
-  isOpen,
-}: {
-  children?: React.ReactNode;
-  className?: string;
-  onOpen?: () => void;
-  onClose?: () => void;
-  isOpen?: boolean;
-}) {
-  const {store} = useConfig();
-  const {
-    isOpen: internalIsOpen,
-    onOpen: internalOnOpen,
-    onClose: internalOnClose,
-  } = useDisclosure();
-
-  const isControlled = isOpen !== undefined;
-  const finalIsOpen = isControlled ? isOpen : internalIsOpen;
-  const finalOnOpen = isControlled ? onOpen : internalOnOpen;
-  const finalOnClose = isControlled ? onClose : internalOnClose;
-
-  // const { isLoading, cartData: cartDetail } = useCartDetail();
-  const cartState= useAppSelector((state) => state.cartDetail);
-  const {cart:cartDetail, loading: isLoading} = cartState;
-  const cart = Array.isArray(cartDetail?.items?.edges)
-    ? cartDetail?.items?.edges
-    : [];
-
- 
-
-  useBodyScrollLock(finalIsOpen);
-
-  // const handleOpenChange = (open: boolean) => {
-  //   if (!open) {
-  //     finalOnClose?.();
-  //   }
-  // };
-
-  return (
-    <>
-      <button
-        type="button"
-        aria-label="Open cart"
-        className={clsx(
-          className,
-          isLoading ? "cursor-wait" : "cursor-pointer",
-        )}
-        disabled={isLoading}
-        onClick={finalOnOpen}
-      >
-        {children ? (
-          children
-        ) : (
-          <OpenCart quantity={cartDetail?.itemsQty} />
-        )}
-      </button>
-
-      
-        <AnimatePresence>
-          {finalIsOpen && (
-            <>
-              <motion.div
-                initial={{ opacity: 0 }}
-                animate={{ opacity: 1 }}
-                exit={{ opacity: 0 }}
-                onClick={finalOnClose}
-                className="fixed inset-0 z-40 bg-transparent lg:hidden transition-opacity"
-                style={{ top: "68px", bottom: "64px" }}
-              />
-
-              <motion.div
-                initial={{ x: "100%" }}
-                animate={{ x: 0 }}
-                exit={{ x: "100%" }}
-                transition={{
-                  type: "spring",
-                  damping: 30,
-                  stiffness: 300,
-                  mass: 0.8,
-                }}
-                className="fixed right-0 z-50 flex flex-col bg-white dark:bg-black w-full max-w-[448px] border-l border-neutral-200 dark:border-neutral-800 lg:hidden"
-                style={{
-                  top: "68px",
-                  bottom: "64px",
-                  width: "100%",
-                  maxWidth: "448px",
-                  height: "calc(var(--visual-viewport-height) - 132px)",
-                }}
-              >
-                <div className="flex flex-col gap-1 p-4">
-                  <div className="flex items-center justify-between">
-                    <p
-                      className={clsx(
-                        "font-semibold",
-                        "text-xl",
-                      )}
-                    >
-                      My Cart
-                    </p>
-                 
-                      <button
-                        aria-label="Close cart"
-                        className="cursor-pointer"
-                        onClick={finalOnClose}
-                      >
-                        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon" className="h-6 transition-all ease-in-out hover:scale-110"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12"></path></svg>
-                      </button>
-                    
-                  </div>
-                </div>
-
-                <div
-                  className={clsx(
-                    "flex-1 overflow-y-auto px-4 py-0 drawer-scrollbar-hidden",
-                    "!px-2",
-                  )}
-                >
-                  {cart?.length === 0 ? (
-                    <div className="mt-20 flex w-full flex-col items-center justify-center overflow-hidden">
-                      <ShoppingCartIcon className="h-16" />
-                      <p className="mt-6 text-center text-2xl font-bold">
-                        Your cart is empty.
-                      </p>
-                    </div>
-                  ) : (
-                    <div className="flex h-full flex-col justify-between">
-                      <ul className="my-0 flex-grow overflow-auto py-0">
-                        {Array.isArray(cart) &&
-                          cart?.map((item: any, i: number) => {
-                            const merchandiseSearchParams =
-                              {} as MerchandiseSearchParams;
-                            const merchandiseUrl = createUrl(
-                              `/product/${item?.node.productUrlKey}`,
-                              new URLSearchParams(merchandiseSearchParams),
-                            );
-                            const baseImage: any = safeParse(
-                              item?.node?.baseImage,
-                            );
-
-                            return (
-                              <li key={i} className="flex w-full flex-col">
-                                <div
-                                  className={clsx(
-                                    "flex w-full flex-row justify-between py-4 px-1",
-                                    "gap-1 xxs:gap-3",
-                                  )}
-                                >
-                                  <Link
-                                    className="z-30 flex flex-row space-x-4"
-                                    aria-label={`${item?.node?.name}`}
-                                    href={merchandiseUrl}
-                                    onClick={finalOnClose}
-                                  >
-                                    <div className="relative h-16 w-16 cursor-pointer overflow-hidden rounded-md border border-neutral-300 bg-neutral-300 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800">
-                                      <Image
-                                        alt={
-                                          item?.node?.baseImage ||
-                                          item?.product?.name
-                                        }
-                                        className="h-full w-full object-cover"
-                                        height={64}
-                                        src={baseImage?.small_image_url || ""}
-                                        width={74}
-                                        onError={(e) =>
-                                          (e.currentTarget.src = NOT_IMAGE)
-                                        }
-                                      />
-                                    </div>
-                                    <div className="flex flex-1 flex-col text-base">
-                                      <span className="line-clamp-1 font-outfit text-base font-medium">
-                                        {item?.node?.name}
-                                      </span>
-                                      {item.name !== DEFAULT_OPTION && (
-                                        <p className="text-sm lowercase line-clamp-1 text-black dark:text-neutral-400">
-                                          {item?.node?.sku}
-                                        </p>
-                                      )}
-                                    </div>
-                                  </Link>
-                                  <div className="flex h-16 flex-col justify-between">
-                                    <Price
-                                      amount={item?.node?.price}
-                                      className="flex justify-end space-y-2 text-right font-outfit text-base font-medium"
-                                      currencyCode={store.currentCurrency}
-                                    />
-                                    <div className="flex items-center gap-x-2">
-                                      <DeleteItemButton item={item} />
-                                      <div className="ml-auto flex h-9 flex-row items-center rounded-full border border-neutral-200 dark:border-neutral-700">
-                                        <EditItemQuantityButton
-                                          item={item}
-                                          type="minus"
-                                        />
-                                        <p className="w-6 text-center">
-                                          <span className="w-full text-sm">
-                                            {item?.node?.quantity}
-                                          </span>
-                                        </p>
-                                        <EditItemQuantityButton
-                                          item={item}
-                                          type="plus"
-                                        />
-                                      </div>
-                                    </div>
-                                  </div>
-                                </div>
-                              </li>
-                            );
-                          })}
-                      </ul>
-
-                      <div className="border-0 border-t border-solid border-neutral-200 dark:border-dark-grey py-4 text-sm text-neutral-500 dark:text-neutral-400">
-                        {(cartDetail as any)?.taxAmount > 0 && (
-                          <div className="mb-3 flex items-center justify-between">
-                            <p className="text-base font-normal text-black/[60%] dark:text-white">
-                              Taxes
-                            </p>
-                            <Price
-                              amount={(cartDetail as any)?.taxAmount}
-                              className="text-right text-base font-medium text-black dark:text-white"
-                              currencyCode={store.currentCurrency}
-                            />
-                          </div>
-                        )}
-                        <div className="mb-3 flex items-center justify-between pb-1">
-                          <p className="text-base font-normal text-black/[60%] dark:text-white">
-                            Total
-                          </p>
-                          <Price
-                            amount={(cartDetail as any)?.grandTotal}
-                            className="text-right text-base font-medium text-black dark:text-white"
-                            currencyCode={store.currentCurrency}
-                          />
-                        </div>
-
-                        <form action={redirectToCheckout}>
-                          <CheckoutButton />
-                        </form>
-                      </div>
-                    </div>
-                  )}
-                </div>
-
-                <div className="p-4" />
-              </motion.div>
-            </>
-          )}
-        </AnimatePresence>
-      
-    </>
-  );
-}
-
-function CheckoutButton() {
-  const { pending } = useFormStatus();
-
-
-  return (
-    <>
-      <input
-        name="url"
-        type="hidden"
-        value="/checkout"
-      />
-      <button
-        className={clsx(
-          "block w-full rounded-full bg-blue-600 p-3 text-center text-sm font-medium text-white opacity-90 hover:opacity-100",
-          pending ? "cursor-wait" : "cursor-pointer",
-        )}
-        disabled={pending}
-        type="submit"
-      >
-        {pending ? <LoadingDots className="bg-white" /> : "Proceed to Checkout"}
-      </button>
-    </>
-  );
-}

+ 0 - 23
src/components/cart/OpenCart.tsx

@@ -1,23 +0,0 @@
-import { ShoppingCartIcon } from "@heroicons/react/24/outline";
-import clsx from "clsx";
-
-export default function OpenCart({
-  className,
-  quantity,
-}: {
-  className?: string;
-  quantity?: number | string;
-}) {
-  return (
-    <div className="relative flex  items-center justify-center rounded-md w-6 h-6 border-0 lg:border border-solid border-neutral-200 dark:border-neutral-700 lg:h-11 lg:w-11">
-      <ShoppingCartIcon className={clsx("h-5 w-5", className)} />
-{/* -mr-2  */}
-      {quantity ? (
-        <div className="absolute right-0 bottom-0 margin-t lg:-mt-2 w-3 h-3  rounded-full bg-[#FF4500FF]  text-[11px] text-center leading-3 font-medium text-white">
-          {quantity}
-        </div>
-      ) : null}
-    </div>
-  );
-}
-

+ 0 - 21
src/components/cart/index.tsx

@@ -1,21 +0,0 @@
-import CartModal from "./CartModal";
-
-export default function Cart({
-  children,
-  className,
-  onOpen,
-  onClose,
-  isOpen,
-}: {
-  children?: React.ReactNode;
-  className?: string;
-  onOpen?: () => void;
-  onClose?: () => void;
-  isOpen?: boolean;
-}) {
-  return (
-    <CartModal className={className} onOpen={onOpen} onClose={onClose} isOpen={isOpen}>
-      {children}
-    </CartModal>
-  );
-}

+ 2 - 2
src/components/catalog/product/NewProductGridItems.tsx

@@ -1,4 +1,4 @@
-import { baseUrl, getImageUrl, NOT_IMAGE } from "@/utils/constants";
+import { getImageUrl } from "@/utils/constants";
 import Grid from "@/components/theme/ui/grid/Grid";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
 import Image from "next/image";
@@ -10,7 +10,7 @@ export default function NewProductGridItems({
 }) {
   return products.map((product: any) => {
 
-    const imageUrl = getImageUrl(product?.baseImageUrl, baseUrl, NOT_IMAGE);
+    const imageUrl = getImageUrl(product?.baseImageUrl);
     const price =
       product?.type === "configurable"
         ? product?.minimumPrice ?? "0"

+ 2 - 2
src/components/catalog/product/ProductGridItems.tsx

@@ -1,4 +1,4 @@
-import { baseUrl, getImageUrl, NOT_IMAGE } from "@/utils/constants";
+import { getImageUrl } from "@/utils/constants";
 import { ProductCard } from "./ProductCard";
 
 export default function ProductGridItems({
@@ -8,7 +8,7 @@ export default function ProductGridItems({
 }) {
   return products.map((product: any, index: number) => {
 
-    const imageUrl = getImageUrl(product?.baseImageUrl, baseUrl, NOT_IMAGE);
+    const imageUrl = getImageUrl(product?.baseImageUrl);
     const price =
       product?.type === "configurable"
         ? product?.minimumPrice ?? "0"

+ 3 - 3
src/components/catalog/product/ProductsSection.tsx

@@ -1,7 +1,7 @@
 
-import { NOT_IMAGE } from "@/utils/constants";
+
 import Grid from "../../theme/ui/grid/Grid";
-import { baseUrl, getImageUrl } from "@/utils/constants";
+import { getImageUrl } from "@/utils/constants";
 import { ProductCard } from "./ProductCard";
 import type { ProductsSectionProps } from "../type";
 
@@ -18,7 +18,7 @@ export function ProductsSection({ title, description, products }: ProductsSectio
 
       <Grid className="grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
         {products.map((item, index) => {
-          const imageUrl = getImageUrl(item?.baseImageUrl, baseUrl, NOT_IMAGE);
+          const imageUrl = getImageUrl(item?.baseImageUrl);
           const ProductPrice =
             item?.type === "configurable"
               ? item?.minimumPrice ?? "0"

+ 37 - 30
src/components/common/LoginModal/LoginModal.tsx

@@ -190,45 +190,52 @@ export default function LoginModal({
         <CommonModal 
             isOpen={isShow}
             onClose={closeModal}
-            header={
-                <div className="relative flex justify-center items-center pt-6 px-4 pb-4">
-                    <button 
-                        className={clsx("flex-none",{
-                            "text-ly-18 font-bold": activeTab === "register",
-                            "text-ly-16 font-media text-ly-deepgray": activeTab !== "register"
-                        })}
-                        onClick={() => toggleLoginRegister("register")}
-                    >
-                        Register
+            contentClassName={"top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-145 w-99.5 bg-white rounded-xl"}
+            header={<div className="box-border w-full pt-6 px-6 pb-6">
+                <div className="flex justify-end">
+                    <button className="w-6 h-6 flex-none" onClick={() => closeModal(false)}>
+                        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon" className="h-6 transition-all ease-in-out hover:scale-110"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12"></path></svg>
                     </button>
-                    <span className="flex-none w-0.5 h-4.5 bg-ly-deepgray mx-5"></span>
+                </div>
+                
+                <div className="relative flex justify-center items-center mt-1.5">
                     <button 
                          className={clsx("flex-none",{
                             "text-ly-18 font-bold": activeTab === "login",
-                            "text-ly-16 font-media text-ly-deepgray": activeTab !== "login"
+                            "text-ly-18 text-ly-deepgray": activeTab !== "login"
                         })}
                         onClick={() => toggleLoginRegister("login")}
                     >
-                        Sign in
+                        Log in
                     </button>
-
-                    <button className="absolute top-4 right-4 w-6 h-6" onClick={() => closeModal(false)}>
-                        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth="1.5" stroke="currentColor" aria-hidden="true" data-slot="icon" className="h-6 transition-all ease-in-out hover:scale-110"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18 18 6M6 6l12 12"></path></svg>
+                    
+                    <span className="flex-none w-0.5 h-4.5 bg-ly-deepgray mx-5"></span>
+                    <button 
+                        className={clsx("flex-none",{
+                            "text-ly-18 font-bold": activeTab === "register",
+                            "text-ly-18 text-ly-deepgray": activeTab !== "register"
+                        })}
+                        onClick={() => toggleLoginRegister("register")}
+                    >
+                        Sign Up
                     </button>
+                    
                 </div>
-            }
+                    
+            </div>}
+            footer={<div className="h-6">{/**布局占位 */}</div>}
             body={
                 <>
                     <form 
-                        className={clsx("relative px-4 pt-2 pb-6",{
+                        className={clsx("relative box-border px-6",{
                             "hidden": activeTab !== 'register'
                         })} 
                         onSubmit={registerForm.handleSubmit(registerSubmit)}
                     >
-                        
+                  
                         <div className="w-full flex justify-between">
                             <div className="w-41.25 flex-none">
-                                <label className="text-ly-12 block mb-3 font-semibold">
+                                <label className="text-ly-16 block mb-1">
                                     First Name *
                                 </label>
                                 <InputText
@@ -245,7 +252,7 @@ export default function LoginModal({
                                 />
                             </div>
                             <div className="w-41.25 flex-none">
-                                <label className="text-ly-12 block mb-3 font-semibold">
+                                <label className="text-ly-16 block mb-1">
                                     Last Name *
                                 </label>
                                 <InputText
@@ -264,7 +271,7 @@ export default function LoginModal({
 
                         </div>
                         <div className="w-full mt-4">
-                            <label className="text-ly-12 block mb-3 font-semibold">
+                            <label className="text-ly-16 block mb-1">
                                 Email *
                             </label>
                             <InputText
@@ -282,7 +289,7 @@ export default function LoginModal({
                         </div>
 
                         <div className="w-full mt-4">
-                            <label className="text-ly-12 block mb-3 font-semibold">
+                            <label className="text-ly-16 block mb-1">
                                 Password *
                             </label>
                             <PasswordInput
@@ -307,7 +314,7 @@ export default function LoginModal({
                         </div>
 
                         <div className="w-full mt-4">
-                            <label className="text-ly-12 block mb-3 font-semibold">
+                            <label className="text-ly-16 block mb-1">
                                Confirm Password *
                             </label>
                             <PasswordInput
@@ -334,7 +341,7 @@ export default function LoginModal({
                             />
                         </div>
                         <div className="w-full mt-4"> 
-                            <label className="text-ly-12 block mb-3 font-semibold">Phone number *</label>
+                            <label className="text-ly-16 block mb-1">Phone number *</label>
                             <Controller
                                 name="phone"
                                 control={registerForm.control}
@@ -413,19 +420,19 @@ export default function LoginModal({
                                                 
                             </label>
                         </div>
-
+                  
                     </form>
 
 
                     <form 
-                        className={clsx("relative px-4 pt-2",{
+                        className={clsx("relative px-6",{
                             "hidden": activeTab !== "login"
                         })} 
                         onSubmit={loginForm.handleSubmit(loginSubmit)}
                     >
                     
                         <div className="w-full">
-                            <label className="text-ly-12 block mb-3 font-semibold">
+                            <label className="text-ly-16 block mb-1">
                                 Email *
                             </label>
                             <InputText
@@ -442,7 +449,7 @@ export default function LoginModal({
                             />
                         </div>
                         <div className="w-full mt-4">
-                            <label className="text-ly-12 block mb-3 font-semibold">
+                            <label className="text-ly-16 block mb-1">
                                 Password *
                             </label>
                             <PasswordInput
@@ -466,7 +473,7 @@ export default function LoginModal({
                             />
                         </div>
                         
-                        <button className="flex items-center justify-center mt-6 w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16 font-bold">Log In</button>
+                        <button className="flex items-center justify-center mt-6 w-full h-12 bg-ly-green text-white rounded-3xl text-ly-16">Log In</button>
                         
                     </form>
                 </>

+ 0 - 31
src/components/common/skeleton/AuthPlaceHolder.tsx

@@ -1,31 +0,0 @@
-export const AuthPlaceHolder = () => (
-  <div className="mx-auto animate-pulse pt-4 md:pt-8 lg:pt-12">
-    <div className="grid grid-cols-1 gap-12 md:grid-cols-2">
-      <div className="flex px-4">
-        <div className="w-full animate-pulse">
-          <div className="h-12 w-1/3 rounded bg-gray-200"></div>
-          <div className="mt-2 h-4 w-3/4 rounded bg-gray-200"></div>
-
-          <div className="mt-12 space-y-4">
-            <div className="grid grid-cols-2 gap-4">
-              <div className="h-12 rounded-lg bg-gray-200"></div>
-              <div className="h-12 rounded-lg bg-gray-200"></div>
-            </div>
-
-            <div className="h-12 w-full rounded-lg bg-gray-200"></div>
-
-            <div className="h-12 w-full rounded-lg bg-gray-200"></div>
-
-            <div className="h-12 w-full rounded-lg bg-gray-200"></div>
-
-            <div className="mt-12 h-12 w-full rounded-lg bg-gray-300"></div>
-          </div>
-
-          <div className="mt-4 h-6 w-1/2 rounded bg-gray-200"></div>
-        </div>
-      </div>
-
-      <div className="hidden h-80 w-full rounded-xl bg-gray-200 dark:bg-gray-700 sm:h-[400px] md:hidden md:h-[600px] lg:inline" />
-    </div>
-  </div>
-);

+ 1 - 1
src/components/customer/accountSwiper.tsx

@@ -18,7 +18,7 @@ interface ProductItem {
   rating: number;
   review_count: number;
 }
-// import 'swiper/css';
+
 // 模拟商品数据(和图片内容匹配)
 const productList:ProductItem[] = [
                     {

+ 2 - 1
src/components/error/ErrorBoundary.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import React, { Component, type ErrorInfo, type ReactNode } from "react";
+import { env } from "@/env";
 
 interface Props {
   children: ReactNode;
@@ -59,7 +60,7 @@ export class ErrorBoundary extends Component<Props, State> {
               Try Again
             </button>
           </div>
-          {process.env.NEXT_PUBLIC_APP_ENV !== "production" && this.state.error && (
+          {env.NEXT_PUBLIC_APP_ENV !== "production" && this.state.error && (
             <details className="mt-6 w-full max-w-2xl">
               <summary className="cursor-pointer text-sm text-gray-500">
                 Error Details (Development Only)

+ 0 - 1
src/components/home/HomeImageBanner.tsx

@@ -2,7 +2,6 @@
 
 import { useRouter } from 'next/navigation';
 import { Swiper, SwiperSlide } from 'swiper/react';
-import 'swiper/css';
 import {CurrencySwitch} from "@/components/common/CurrencySwitch/CurrencySwitch";
 import makeClient from "@/lib/ApolloClientBrowser";
 import { GET_CART_ITEM,GET_FILTER_ATTRIBUTES,CREATE_APPLY_COUPON, GET_COUNTRY_STATES} from "@/graphql";

+ 2 - 1
src/components/home/ImageCarousel.tsx

@@ -3,6 +3,7 @@
 import { type FC, useState, useEffect, useCallback, useRef } from "react";
 import Link from "next/link";
 import Image from "next/image";
+import { env } from "@/env";
 import { Shimmer } from "@/components/common/Shimmer";
 
 interface ImageCarouselProps {
@@ -28,7 +29,7 @@ const ImageCarousel: FC<ImageCarouselProps> = ({ options }) => {
             return imagePath;
         }
 
-        const backendUrl = process.env.NEXT_PUBLIC_BAGISTO_ENDPOINT;
+        const backendUrl = env.NEXT_PUBLIC_BAGISTO_ENDPOINT;
         if (!backendUrl) return "";
 
         const cleanPath = imagePath.startsWith('/') ? imagePath.slice(1) : imagePath;

+ 2 - 4
src/components/home/ProductCarouselTheme.tsx

@@ -1,6 +1,6 @@
 import { ProductCard } from "@components/catalog/product/ProductCard";
 import type { ProductsSectionProps } from "@components/catalog/type";
-import { baseUrl, getImageUrl, NOT_IMAGE } from "@utils/constants";
+import { getImageUrl } from "@utils/constants";
 
 const Theme = ({ title, description, products }: ProductsSectionProps) => {
   return (
@@ -18,9 +18,7 @@ const Theme = ({ title, description, products }: ProductsSectionProps) => {
         <ul className="m-0 grid grid-cols-2 justify-center gap-5 lg:gap-11.5 p-0 xss:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
           {products.map((item, index) => {
             const imageUrl = getImageUrl(
-              item?.baseImageUrl,
-              baseUrl,
-              NOT_IMAGE,
+              item?.baseImageUrl
             );
             const ProductPrice =
               item?.type === "configurable"

+ 35 - 4
src/components/layout/footer/index.tsx

@@ -1,21 +1,52 @@
 import Link from "next/link";
 import { Suspense } from "react";
-import { getThemeCustomization } from "@/utils/bagisto";
+import { serverGraphqlFetch } from "@/utils/bagisto";
+import type {
+  GetFooterResponse,
+  ThemeCustomizationResult
+} from "@/types/theme/theme-customization";
+import {
+  GET_FOOTER
+} from "@/graphql";
+import { env } from "@/env";
 import LogoIcon from "@components/common/icons/LogoIcon";
 import FaceBookIcon from "@components/common/icons/social-icon/FaceBookIcon";
 import InstaGramIcon from "@components/common/icons/social-icon/InstaGramIcon";
 import TwitterIcon from "@components/common/icons/social-icon/TwitterIcon";
 import Subscribe from "./Subscribe";
 import FooterMenu from "./FooterMenu";
-const { COMPANY_NAME, SITE_NAME } = process.env;
+
+async function getFooterData(): Promise<ThemeCustomizationResult> {
+ 
+    const [{data: footerRes}, {data:servicesRes}] = await Promise.all([
+      serverGraphqlFetch<GetFooterResponse,{type: string;}>({
+        query: GET_FOOTER,
+        variables:{
+          type: "footer_links",
+        },
+        takeAuthorization: false
+      }),
+      serverGraphqlFetch<GetFooterResponse,{type: string;}>({
+        query: GET_FOOTER,
+        variables:{
+          type: "services_content",
+        },
+        takeAuthorization: false
+      }),
+    ]);
+    return {
+      footer_links: footerRes,
+      services_content: servicesRes,
+    };
+}
 
 export default async function Footer() {
   const currentYear = new Date().getFullYear();
   const copyrightDate = 2010 + (currentYear > 2010 ? `-${currentYear}` : "");
   const skeleton =
     "w-full h-6 animate-pulse rounded bg-neutral-200 dark:bg-neutral-700";
-  const menu = await getThemeCustomization();
-  const copyrightName = COMPANY_NAME || SITE_NAME || "";
+  const menu = await getFooterData();
+  const copyrightName = env.STORE_NAME;
 
 
   return (

+ 3 - 45
src/components/layout/navbar/MobileSearch.tsx

@@ -1,49 +1,9 @@
 "use client";
-import clsx from "clsx";
-import { Suspense, useState } from "react";
+
+import { Suspense } from "react";
 import Search from "./Search";
-import OpenCart from "@/components/cart/OpenCart";
 import { SearchSkeleton } from "@/components/common/skeleton/SearchSkeleton";
 
-const MobileSearch = () => {
-  const [search, setSearch] = useState<boolean>(false);
-  return (
-    <>
-      <Suspense fallback={<OpenCart />}>
-        <button
-          onClick={() => setSearch(!search)}
-          aria-label="Open cart"
-          className="size-9 lg:size-11 flex cursor-pointer items-center justify-center rounded-sm border border-solid border-neutral-200 dark:border-neutral-700 md:hidden"
-        >
-          <svg
-            xmlns="http://www.w3.org/2000/svg"
-            fill="none"
-            viewBox="0 0 24 24"
-            strokeWidth="1.5"
-            stroke="currentColor"
-            className="size-5"
-          >
-            <path
-              strokeLinecap="round"
-              strokeLinejoin="round"
-              d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
-            />
-          </svg>
-        </button>
-      </Suspense>
-      <div
-        className={clsx(
-          "absolute z-50 left-0 right-0 top-0 flex w-full justify-center bg-neutral-50 dark:bg-neutral-900 p-[15px] transition-all duration-300 md:hidden ",
-          search ? "translate-y-0 opacity-100" : " -translate-y-full opacity-0"
-        )}
-      >
-        <Suspense fallback={<SearchSkeleton />}>
-          <Search search={search} setSearch={setSearch} />
-        </Suspense>
-      </div>
-    </>
-  );
-};
 
 
 
@@ -54,7 +14,7 @@ export const MobileSearchBar = ({
 }) => {
   return (
     <>
-      <div className="block w-full justify-center md:hidden px-4 mt-3 z-10">
+      <div className="block w-full justify-center px-4 mt-3 z-10">
         <Suspense fallback={<SearchSkeleton />}>
           <Search search={false} onClose={onClose} />
         </Suspense>
@@ -62,5 +22,3 @@ export const MobileSearchBar = ({
     </>
   );
 };
-
-export default MobileSearch;

Diferenças do arquivo suprimidas por serem muito extensas
+ 1 - 1
src/components/layout/navbar/NavbarActions.tsx


+ 1 - 1
src/components/layout/navbar/Search.tsx

@@ -92,7 +92,7 @@ export default function Search({
         <button
           onClick={() => setSearch(!search)}
           type="button"
-          className="absolute bottom-0 left-1 top-0 flex w-9 cursor-pointer items-center justify-center border-r border-neutral-200 dark:border-neutral-700 md:hidden"
+          className="absolute bottom-0 left-1 top-0 flex w-9 cursor-pointer items-center justify-center border-r border-neutral-200 dark:border-neutral-700"
         >
           <ArrowLeftIcon className="size-5 stroke-neutral-500" />
         </button>

+ 1 - 1
src/components/layout/navbar/SearchPop.tsx

@@ -112,7 +112,7 @@ export default function SearchPop({ open, onClose }: SearchPopProps) {
         </button>
       </div>
       {/* 搜索输入框容器 */}
-      <div className="block w-full justify-center md:hidden px-4 mt-3 z-10">
+      <div className="block w-full justify-center px-4 mt-3 z-10">
         <div
           className={`${isDesktop ? "max-w-[550px]" : ""} relative w-full mx-auto xl:min-w-[516px] outline-none hover:outline-none`}
         >

+ 2 - 4
src/components/theme/ui/CommonModal.tsx

@@ -174,15 +174,13 @@ export default function CommonModal({
                         {header && header}
                     </div>
                     
-                    <div className="w-full box-border h-full overflow-y-auto relative">
+                    <div className="w-full box-border h-full overflow-y-auto relative scrollbar-thin scrollbar-thumb-gray-200">
                         
                         {body}
                         
                     </div>
 
-                    <div ref={footerRef} className={clsx("w-full box-border absolute bottom-0 left-0",{
-                        "border-t border-gray-200": footer !==null && footer !== undefined
-                    })}>
+                    <div ref={footerRef} className={clsx("z-1 w-full box-border absolute bottom-0 left-0")}>
                         {footer && footer}
                     </div>
                 </div>

+ 70 - 0
src/env.ts

@@ -0,0 +1,70 @@
+import { createEnv } from '@t3-oss/env-nextjs'
+import * as z from 'zod'
+/**
+ *  z.string()           // 必须是字符串
+    z.string().min(1)    // 字符串且不能是空字符串(最常用的"必填"写法)
+    z.string().url()     // 必须是合法 URL 格式(http://xxx 或 https://xxx)
+    z.string().length(3) // 字符串长度必须恰好是 3(比如货币代码 USD)
+    z.enum(['a', 'b'])   // 值必须是给定的几个字符串之一,多了写不了就用这个
+    z.string().startsWith('pk_') // 必须以某个前缀开头
+    .default('xxx')      // 如果没配置,给个默认值,不会校验失败
+    .optional()           // 允许是 undefined(不常用在这里,因为你都要求必填)
+ */
+export const env = createEnv({
+  /**
+   * 服务端变量:只能在服务端代码里访问(API Route / Server Component / Server Action)
+   * 客户端组件里引用会直接报错,防止密钥泄漏
+   */
+  server: {
+    // 业务环境标识,限定只能是这几个值之一
+    APP_ENV: z.enum(['development', 'test', 'production']),
+
+    // NextAuth 配置
+    NEXTAUTH_URL: z.url(),
+    // NextAuth 官方建议至少 32 位随机字符串,这里加个长度校验,防止手滑写太短
+    NEXTAUTH_SECRET: z.string().min(32, 'NEXTAUTH_SECRET 长度至少 32 位,建议用 openssl rand -base64 32 生成'),
+
+    // PayPal
+    PAYPAL_SANDBOX_CLIENT_ID: z.string().min(1),
+    PAYPAL_PRODUCTION_CLIENT_ID: z.string().min(1),
+
+    // 业务配置
+    COMPANY_NAME: z.string().min(1),
+    STORE_NAME: z.string().min(1),
+    DEFAULT_CURRENCY: z.string().length(3), // 货币代码固定 3 位,比如 USD、CNY
+    DEFAULT_LOCAL: z.string().min(2),        // 语言代码,比如 en、zh-CN
+    DEFAULT_CHANNEL: z.string().min(1),
+  },
+
+  /**
+   * 客户端变量:必须以 NEXT_PUBLIC_ 开头,会被打包进浏览器 bundle,不要放密钥
+   */
+  client: {
+    NEXT_PUBLIC_APP_ENV: z.enum(['development', 'test', 'production']),
+    NEXT_PUBLIC_SITE_URL: z.url(),
+    NEXT_PUBLIC_BAGISTO_ENDPOINT: z.url(),
+    // storefront key 有固定前缀,顺手校验一下,配错了能第一时间发现
+    NEXT_PUBLIC_BAGISTO_STOREFRONT_KEY: z.string().startsWith('pk_storefront_'),
+  },
+
+  /**
+   * 只需要列出 client 变量即可,Next.js >= 13.4.4 都用这个写法
+   */
+  experimental__runtimeEnv: {
+    NEXT_PUBLIC_APP_ENV: process.env.NEXT_PUBLIC_APP_ENV,
+    NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
+    NEXT_PUBLIC_BAGISTO_ENDPOINT: process.env.NEXT_PUBLIC_BAGISTO_ENDPOINT,
+    NEXT_PUBLIC_BAGISTO_STOREFRONT_KEY: process.env.NEXT_PUBLIC_BAGISTO_STOREFRONT_KEY,
+  },
+
+  /**
+   * docker build 或 CI 阶段如果没有真实环境变量,可以临时跳过校验
+   * SKIP_ENV_VALIDATION 变量需要在docker的配置文件中注入
+   */
+  skipValidation: !!process.env.SKIP_ENV_VALIDATION,
+
+  /**
+   * .env 里如果某个变量写成空字符串(比如 XXX=),当作没配置处理,而不是校验通过一个空串
+   */
+  emptyStringAsUndefined: true,
+})

+ 1 - 0
src/graphql/cart/mutations/AddProductToCart.ts

@@ -38,6 +38,7 @@ export const CREATE_ADD_PRODUCT_IN_CART: TypedDocumentNode<AddToCartData,AddToCa
               type
               productUrlKey
               canChangeQty
+              options
             }
           }
         }

+ 15 - 14
src/graphql/cart/mutations/CreateApplyCoupon.ts

@@ -15,22 +15,23 @@ export const CREATE_APPLY_COUPON: TypedDocumentNode<CreateApplyCouponData,{coupo
                     isGuest
                     itemsCount
                     items {
-                    edges {
-                        node {
-                        id
-                        cartId
-                        productId
-                        name
-                        price
-                        baseImage
-                        sku
-                        quantity
-                        type
-                        productUrlKey
-                        canChangeQty
+                        edges {
+                            node {
+                                id
+                                cartId
+                                productId
+                                name
+                                price
+                                baseImage
+                                sku
+                                quantity
+                                type
+                                productUrlKey
+                                canChangeQty
+                                options
+                            }
                         }
                     }
-                    }
                     earnableRewardPoints
                     rewardPoints
                     rewardPointsUsed

+ 1 - 0
src/graphql/cart/mutations/CreateMergeCart.ts

@@ -30,6 +30,7 @@ export const CREATE_MERGE_CART: TypedDocumentNode<CreateMergeCartData> = gql`
               type
               productUrlKey
               canChangeQty
+              options
             }
           }
         }

+ 1 - 0
src/graphql/cart/mutations/CreateRemoveCoupon.ts

@@ -24,6 +24,7 @@ export const CREATE_REMOVE_COUPON: TypedDocumentNode<CreateRemoveCouponData> = g
                         type
                         productUrlKey
                         canChangeQty
+                        options
                         }
                     }
                     }

+ 1 - 0
src/graphql/cart/mutations/GetCartItem.ts

@@ -24,6 +24,7 @@ export const GET_CART_ITEM: TypedDocumentNode<GetCartItemData> = gql`
               type
               productUrlKey
               canChangeQty
+              options
             }
           }
         }

+ 1 - 0
src/graphql/cart/mutations/RemoveCartItem.ts

@@ -24,6 +24,7 @@ export const REMOVE_CART_ITEM: TypedDocumentNode<RemoveCartItemData,{cartItemId:
               type
               productUrlKey
               canChangeQty
+              options
             }
           }
         }

+ 1 - 0
src/graphql/cart/mutations/UpdateCartItems.ts

@@ -33,6 +33,7 @@ export const UPDATE_CART_ITEM: TypedDocumentNode<UpdateCartItemData,{cartItemId:
               type
               productUrlKey
               canChangeQty
+              options
             }
           }
         }

+ 1 - 0
src/graphql/checkout/mutations/CreateSaveCheckoutCart.ts

@@ -42,6 +42,7 @@ export const CREATE_SAVE_CHECKOUT_CART: TypedDocumentNode<SaveCheckoutCartData,S
                             type
                             productUrlKey
                             canChangeQty
+                            options
                         }
                     }
                 }

+ 2 - 1
src/lib/Airwallex/airwallexInit.ts

@@ -1,4 +1,5 @@
 import { init, type InitResult,createElement, type Payment } from '@airwallex/components-sdk';
+import { env } from "@/env";
 
 // ============================================================
 // Types
@@ -65,7 +66,7 @@ export function airwallexInit() {
 
     initPromise = init({
       env:
-        process.env.NEXT_PUBLIC_APP_ENV !== 'production'
+        env.NEXT_PUBLIC_APP_ENV !== 'production'
           ? 'demo'
           : 'prod',
 

+ 2 - 4
src/lib/ApolloClientServer.ts

@@ -6,6 +6,7 @@ import {
   ApolloClient,
   InMemoryCache,
 } from "@apollo/client-integration-nextjs";
+import { env } from "@/env";
 
 // apollo client 底层还是调的nextjs的fetch函数。
 // apollo client 的缓存策略是建立在nextjs缓存策略之上的,相当于两个沙盒
@@ -24,10 +25,7 @@ export const { getClient, query, PreloadQuery } = registerApolloClient(() => {
       */
     });
     const authLink = new SetContextLink((prevContext) => {
-        const storefrontKey =
-            process.env.BAGISTO_STOREFRONT_KEY ||
-            process.env.NEXT_PUBLIC_BAGISTO_STOREFRONT_KEY ||
-            "";
+        const storefrontKey = env.NEXT_PUBLIC_BAGISTO_STOREFRONT_KEY;
 
         return {
             headers: {

+ 0 - 130
src/lib/graphql-fetch.ts

@@ -1,130 +0,0 @@
-
-import { type DocumentNode } from "graphql";
-import {
-  type OperationVariables,
-  ApolloClient
-} from "@apollo/client";
-import type {GraphqlRequestResult} from "@/types/graphqlFetch/type";
-import {getClient} from "@/lib/ApolloClientServer";
-
-
-
-/* 定义自己的context类型
-import "@apollo/client";
-import { HttpLink } from "@apollo/client";
-declare module "@apollo/client" {
-  interface DefaultContext extends HttpLink.ContextOptions {}
-}
-*/
-// Comprehensive error handling example. https://www.apollographql.com/docs/react/data/error-handling
-
-export type CacheLifePreset =
-  | "seconds"
-  | "minutes"
-  | "hours"
-  | "days"
-  | "weeks"
-  | "max";
-
-export type CacheLifeOption = number | CacheLifePreset;
-
-export function getRevalidateTime(
-  life?: CacheLifeOption
-): number | false {
-  if (!life) return false;
-  if (typeof life === "number") return life;
-
-  switch (life) {
-    case "seconds":
-      return 10;
-    case "minutes":
-      return 60;
-    case "hours":
-      return 3600;
-    case "days":
-      return 86400;
-    case "weeks":
-      return 604800;
-    case "max":
-      return false;
-    default:
-      return false;
-  }
-}
-
-export interface GraphQLRequestOptions {
-  tags?: string[];
-  life?: CacheLifeOption;
-  noCache?: boolean;
-  context?: Record<string, unknown>;
-  fetchPolicy?:
-  | "cache-first"
-  | "network-only"
-  | "no-cache"
-  | "cache-only";
-}
-
-export async function graphqlRequest<
-  TData = unknown,
-  TVariables extends OperationVariables = OperationVariables
->(
-  query: DocumentNode,
-  variables?: TVariables,
-  options?: GraphQLRequestOptions
-): Promise<GraphqlRequestResult<TData>> {
-  const client = getClient();
-  let resData;
-  const revalidate = getRevalidateTime(options?.life);
-  let queryOption: ApolloClient.QueryOptions<TData> = {
-    query,
-    variables,
-    fetchPolicy: "network-only",
-    context: {
-      fetchOptions: {
-        next: {
-          revalidate,
-          tags: options?.tags,
-        },
-      },
-    }
-  };
-
-  if (options?.noCache) {
-    /***
-     * client.query的参数是一个对象:
-     * {query, variables, context, fetchPolicy, errorPolicy}
-     * context.fetchOptions 可以设置nextjs fetch的缓存策略 https://www.apollographql.com/docs/react/integrations/nextjs
-     * context: {
-        fetchOptions: {
-          next: {
-            revalidate: 60,      // 对应 life: 'minutes'
-            tags: ['posts'],     // 对应 tags 选项
-          },
-        },
-      },
-     */
-    queryOption = {
-      query,
-      variables,
-      context: options?.context,
-      fetchPolicy: "no-cache", // 跳过 apollo client 的缓存,直接调fetch
-    };
-  }
-
-  if (options?.context) {
-    throw new Error(
-      "graphqlRequest: Caching with `context` is unsafe. Use noCache instead."
-    );
-  }
-  try {
-      // Promise-based APIs (e.g. client.query, client.mutate) - Errors either reject the promise or are returned in the result as the error field.
-      // 如果错误被reject 则会进入catch
-      const result: ApolloClient.QueryResult<TData> = await client.query(queryOption);
-
-      resData = result.data || null;
-      return {data: resData, error: result.error? result.error.message : '' };
-  } catch (error) {
-    throw error;
-  }
-}
-

+ 2 - 1
src/providers/PaymentSDKProvider.tsx

@@ -6,6 +6,7 @@ import {
   useContext,
   useState
 } from "react";
+import { env } from "@/env";
 
 type SDKLoadState = "none" | "loading" | "error" | "success";
 type SDKForPaymentMethod = "klarna" | "afterpay" | "clearpay" | "applepay";
@@ -23,7 +24,7 @@ interface PaymentContextProps {
 }
 
 const klarnaSDKURL = "https://x.klarnacdn.net/kp/lib/v1/api.js";
-const afterpaySDKURL = process.env.NEXT_PUBLIC_APP_ENV === "production" ? "https://portal.afterpay.com/afterpay.js" : "https://portal.sandbox.afterpay.com/afterpay.js";
+const afterpaySDKURL = env.NEXT_PUBLIC_APP_ENV === "production" ? "https://portal.afterpay.com/afterpay.js" : "https://portal.sandbox.afterpay.com/afterpay.js";
 const applepaySDKURL = "https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js";
 
 const PaymentSDKContext = createContext<PaymentContextProps|null>(null);

+ 2 - 1
src/providers/PaypalWrapper.tsx

@@ -1,5 +1,6 @@
 "use client";
 import { PayPalProvider } from "@paypal/react-paypal-js/sdk-v6";
+import { env } from "@/env";
 
 export function PayPalWrapper({ 
     children,
@@ -21,7 +22,7 @@ export function PayPalWrapper({
             "paypal-messages",
             "applepay-payments"
         ]}
-        environment={process.env.NEXT_PUBLIC_APP_ENV === "production" ? "production" : "sandbox"} // 上线改为 "production"
+        environment={env.NEXT_PUBLIC_APP_ENV === "production" ? "production" : "sandbox"} // 上线改为 "production"
         pageType="checkout"
     >
         {children}

+ 2 - 1
src/proxy/auth.ts

@@ -1,6 +1,7 @@
 import { NextResponse } from "next/server";
 import type { NextRequest } from "next/server";
 import { getToken } from "next-auth/jwt";
+import { env } from "@/env";
 export async function handleAuth(
     request: NextRequest,
     response: NextResponse
@@ -31,7 +32,7 @@ export async function handleAuth(
 
     const token = await getToken({
         req: request,
-        secret: process.env.NEXTAUTH_SECRET
+        secret: env.NEXTAUTH_SECRET
     });
 
     if (!token) {

+ 4 - 3
src/proxy/currency.ts

@@ -2,6 +2,7 @@ import type { NextRequest } from "next/server";
 import type { NextResponse } from "next/server";
 import {CURRENT_CURRENCY,CURRENT_LOCAL,CURRENT_CHANNEL} from "@/utils/constants";
 import {currencyCookieOptions} from "@/utils/constants/server";
+import { env } from "@/env";
 
 export function handleCurrency(
     request: NextRequest,
@@ -11,9 +12,9 @@ export function handleCurrency(
     const currency = request.cookies.get(CURRENT_CURRENCY);
     const local = request.cookies.get(CURRENT_LOCAL);
     const channel = request.cookies.get(CURRENT_CHANNEL);
-    const defaultCurrency = process.env.DEFAULT_CURRENCY;
-    const defaultLocal = process.env.DEFAULT_LOCAL;
-    const defaultChannel = process.env.DEFAULT_CHANNEL;
+    const defaultCurrency = env.DEFAULT_CURRENCY;
+    const defaultLocal = env.DEFAULT_LOCAL;
+    const defaultChannel = env.DEFAULT_CHANNEL;
 
     if (!currency && defaultCurrency) {
         

+ 12 - 0
src/types/cart/type.ts

@@ -1,4 +1,13 @@
 // Cart Item (node)
+export interface CartItemOptions {
+  value_id: number;
+  option_id: number;
+  value_code: string;
+  option_code: string;
+  value_label: string;
+  option_label: string;
+  attribute_name: string;                 
+}
 export interface CartItem {
   id: string;
   cartId: string;
@@ -11,6 +20,9 @@ export interface CartItem {
   type: string;
   productUrlKey: string;
   canChangeQty: boolean;
+  options: {
+    edges: Array<{node:CartItemOptions}>
+  }
 }
 
 // GraphQL Edge wrapper

+ 0 - 4
src/types/graphqlFetch/type.ts

@@ -1,7 +1,3 @@
-export interface GraphqlRequestResult<TData = unknown> {
-  data: TData | null;
-  error: string | undefined;
-}
 
 export interface FetchGraphqlError {
     message: string;

+ 2 - 1
src/utils/auth.ts

@@ -3,6 +3,7 @@ import CredentialsProvider from "next-auth/providers/credentials";
 import { bagistoFetch, /*restApiFetch*/} from "@/utils/bagisto";
 import { CUSTOMER_LOGIN } from "@/graphql/customer/mutations";
 import type {CustomerLoginResponse} from "@/types/customer/type";
+import { env } from "@/env";
 
 export const authOptions: NextAuthOptions = {
   session: {
@@ -97,7 +98,7 @@ export const authOptions: NextAuthOptions = {
     error: "/login",
   },
 
-  secret: process.env.NEXTAUTH_SECRET,
+  secret: env.NEXTAUTH_SECRET,
 };
 
 export const handler = NextAuth(authOptions);

+ 10 - 63
src/utils/bagisto/index.ts

@@ -12,17 +12,14 @@ import {
   CURRENT_CHANNEL,
 } from "@/utils/constants";
 import {
-  GET_FOOTER,
   PAGE_BY_URL_KEY,
 } from "@/graphql";
 import { SUBSCRIBE_TO_NEWSLETTER } from "@/graphql/theme/mutations";
-import { cachedGraphQLRequest } from "@/utils/hooks/useCache";
 import type {
-  GetFooterResponse,
-  ThemeCustomizationResult,
   PageData,
 } from "@/types/theme/theme-customization";
 import type {FetchGraphqlResult} from "@/types/graphqlFetch/type";
+import { env } from "@/env";
 
 
 type ExtractVariables<T> = T extends { variables: object }
@@ -34,7 +31,7 @@ type ExtractGraphqlData<T> = T extends { data: infer D } ? { data: D } : any;
 interface PageByUrlKeyResponse {
   pageByUrlKeypages?: PageData[];
 }
-const STOREFRONT_KEY = process.env.NEXT_PUBLIC_BAGISTO_STOREFRONT_KEY;
+const STOREFRONT_KEY = env.NEXT_PUBLIC_BAGISTO_STOREFRONT_KEY;
 async function getBaseHeader() {
     /** 
      * Headers:
@@ -69,23 +66,7 @@ async function getBaseHeader() {
 export async function getAuthorizationToken(): Promise<{token: string | null; isGuest: boolean;}> {
   
   // 登录用户从nextAuth里获取
-  /*
-  const tokenCookie =
-      cookieStore.get("next-auth.session-token")
-      ?? cookieStore.get("__Secure-next-auth.session-token");
 
-  if (tokenCookie?.value) {
-      const token = await decode({
-          token: tokenCookie.value,
-          secret: process.env.NEXTAUTH_SECRET!,
-      });
-          console.log('getAuthorizationToken decode ===============',token);
-      return {
-        token: token?.accessToken ?? '',
-        isGuest: false,
-      };
-  }
-  */
   const authSession = await auth();
   const accessToken = authSession?.user?.accessToken;
   if (accessToken) {
@@ -128,12 +109,6 @@ export async function restApiFetch<T>({
   revalidate?: number;
   takeAuthorization?: boolean;
 }): Promise<{ status: number; body: ExtractRestFulData<T> } | never> {
-  try {
-    const apiUrl = api.startsWith("http") ? api : `${REST_API_URL}${api}`;
-    const url = new URL(apiUrl);
-
-    
-
     const headerRes = await getBaseHeader();
     const baseHeaders: Record<string, string> = {...headerRes};
 
@@ -143,7 +118,9 @@ export async function restApiFetch<T>({
         baseHeaders.Authorization = `Bearer ${tokenRes.token}`;
       }
     }
-
+  try {
+    const apiUrl = api.startsWith("http") ? api : `${REST_API_URL}${api}`;
+    const url = new URL(apiUrl);
 
     if (headers) {
       if (headers instanceof Headers) {
@@ -213,10 +190,6 @@ export async function serverGraphqlFetch<
   revalidate?: number;
   takeAuthorization?: boolean;
 }): Promise<FetchGraphqlResult<TData>> {
-  try {
-    const queryString = typeof query === "string" ? query : print(query);
-
-    
     const headerRes = await getBaseHeader();
     const baseHeaders: Record<string, string> = {...headerRes};
     
@@ -226,6 +199,8 @@ export async function serverGraphqlFetch<
         baseHeaders['Authorization'] = `Bearer ${tokenRes.token}`;
       }
     }
+  try {
+    const queryString = typeof query === "string" ? query : print(query);
 
     if (headers) {
       if (headers instanceof Headers) {
@@ -291,11 +266,6 @@ export async function bagistoFetch<T>({
   takeAuthorization?: boolean;
   revalidate?: number;
 }): Promise<{ status: number; body: ExtractGraphqlData<T> } | never> {
-  try {
-    const queryString =
-      typeof query === "string" ? query : print(query);
-
-    
     const headerRes = await getBaseHeader();
     const baseHeaders: Record<string, string> = {...headerRes};
 
@@ -306,10 +276,10 @@ export async function bagistoFetch<T>({
       }
 
     }
+  try {
+    const queryString =
+      typeof query === "string" ? query : print(query);
     
-
-    
-
     if (headers) {
       if (headers instanceof Headers) {
         headers.forEach((value, key) => (baseHeaders[key] = value));
@@ -366,30 +336,7 @@ export async function subscribeUser(
   }
 }
 
-export async function getThemeCustomization(): Promise<ThemeCustomizationResult> {
-  try {
-    const [{data: footerRes}, {data:servicesRes}] = await Promise.all([
-      cachedGraphQLRequest<GetFooterResponse>("static", GET_FOOTER, {
-        type: "footer_links",
-      }),
-      cachedGraphQLRequest<GetFooterResponse>("static", GET_FOOTER, {
-        type: "services_content",
-      }),
-    ]);
 
-    return {
-      footer_links: footerRes,
-      services_content: servicesRes,
-    };
-  } catch (err) {
-    console.error("ThemeCustomization Error:", err);
-  }
-
-  return {
-    footer_links: null,
-    services_content: null,
-  };
-}
 
 
 export async function getPage(input: { urlKey: string }): Promise<PageData[]> {

+ 15 - 1
src/utils/cartDetailTools.ts

@@ -1,4 +1,4 @@
-import type { CartDetail, CartAddress } from "@/types/cart/type";
+import type { CartDetail, CartAddress, CartItemOptions } from "@/types/cart/type";
 import type {ShipAddressFormData,BillAddressFormData} from "@/types/checkout/type";
 export function formatCartDetail(cart:CartDetail): CartDetail { 
     return {
@@ -98,4 +98,18 @@ export function billingAddressToCartAddress(address: BillAddressFormData):CartAd
         phone: address.billingPhoneNumber
     }
     return res;
+}
+
+export function formatCartProductOptions(options:CartItemOptions[]) {
+    let res = '';
+    if(options.length > 0) {
+        for(let i = 0; i < options.length; i++) {
+            if(res) {
+                res += '/' + options[i].option_label + ':' + options[i].value_label;
+            } else {
+                res = options[i].option_label + ':' + options[i].value_label;
+            }
+        }
+    }
+    return res;
 }

+ 4 - 4
src/utils/constants.ts

@@ -1,3 +1,4 @@
+import { env } from '@/env';
 
 export const HIDDEN_PRODUCT_TAG = "nextjs-frontend-hidden";
 export const DEFAULT_OPTION = "Default Title";
@@ -15,8 +16,7 @@ export const PRODUCT_OFFER_TYPE = "AggregateOffer";
  * cookies constant
  */
 export const TOKEN = "token";
-export const BASE_URL = process.env.NEXT_PUBLIC_SITE_URL;
-export const baseUrl = process.env.NEXT_PUBLIC_BAGISTO_ENDPOINT;
+
 
 
 // -----Pagination--------//
@@ -39,7 +39,7 @@ export const variants = {
 
 
 
-export function getImageUrl(url?: string, baseUrl?: string, fallback: string = NOT_IMAGE) {
+export function getImageUrl(url?: string, baseUrl: string = env.NEXT_PUBLIC_BAGISTO_ENDPOINT, fallback: string = NOT_IMAGE) {
   if (!url) return fallback;
 
   if (url.startsWith("http://") || url.startsWith("https://")) {
@@ -155,7 +155,7 @@ export const GUEST_COOKIE_OPTION = {
     encode: true,          // 默认编码,更加安全
     path: '/',
     // domain:,
-    secure: process.env.NEXT_PUBLIC_APP_ENV !== 'development',
+    secure: env.NEXT_PUBLIC_APP_ENV !== 'development',
     sameSite: sameSite,
 };
 

+ 6 - 102
src/utils/constants/server.ts

@@ -1,110 +1,14 @@
+import { env } from "@/env";
 import type { ResponseCookie } from "next/dist/compiled/@edge-runtime/cookies";
+
 export const BAGISTO_GRAPHQL_API_ENDPOINT = "/api/graphql";
 export const BAGISTO_REST_API_ENDPOINT = "/api";
-export const GRAPHQL_URL = `${(process.env.NEXT_PUBLIC_BAGISTO_ENDPOINT || '').replace(/\/$/, '')}${BAGISTO_GRAPHQL_API_ENDPOINT}`;
-export const REST_API_URL = `${(process.env.NEXT_PUBLIC_BAGISTO_ENDPOINT || '').replace(/\/$/, '')}${BAGISTO_REST_API_ENDPOINT}`;
+export const GRAPHQL_URL = `${(env.NEXT_PUBLIC_BAGISTO_ENDPOINT).replace(/\/$/, '')}${BAGISTO_GRAPHQL_API_ENDPOINT}`;
+export const REST_API_URL = `${(env.NEXT_PUBLIC_BAGISTO_ENDPOINT).replace(/\/$/, '')}${BAGISTO_REST_API_ENDPOINT}`;
 
 export const currencyCookieOptions = {
     path: '/', // 该cookie对那些路径有效,/ 表示所有路径
     maxAge: 60 * 60 * 24 * 365, // 过期时间 1年,到期后自动删除
     sameSite: 'lax', // 第三方网站请求你的站点时,浏览器是否携带 Cookie。
-    secure: process.env.APP_ENV !== 'development' // true 表示这个 Cookie 是否只能通过 HTTPS 发送
-} satisfies Partial<ResponseCookie>;
-
-
-export const configHeader = [
-  // Security headers for all routes
-  {
-    source: "/:path*",
-    headers: [
-      { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
-      //{ key: "X-Frame-Options", value: "DENY" },
-      { key: "X-Content-Type-Options", value: "nosniff" },
-      // { key: "X-XSS-Protection", value: "1; mode=block" },
-      // {
-      //   key: "Permissions-Policy",
-      //   value:
-      //     "camera=(), microphone=(), geolocation=(), browsing-topics=()",
-      // },
-    ],
-  },
-  // Cache dynamic pages - shorter cache for frequently updated content
-  {
-    source: "/category/:path*",
-    headers: [
-      {
-        key: "Cache-Control",
-        value: "public, s-maxage=3600, stale-while-revalidate=86400",
-      },
-    ],
-  },
-  {
-    source: "/product/:path*",
-    headers: [
-      {
-        key: "Cache-Control",
-        value: "public, s-maxage=3600, stale-while-revalidate=86400",
-      },
-      {
-        key: "Vary",
-        value: "Accept-Encoding",
-      },
-    ],
-  },
-  {
-    source: "/checkout/:path*",
-    headers: [
-      {
-        key: "Cache-Control",
-        value: "private, no-cache, no-store, must-revalidate",
-      },
-    ],
-  },
-  {
-    source: "/customer/:path*",
-    headers: [
-      {
-        key: "Cache-Control",
-        value: "private, no-cache, no-store, must-revalidate",
-      },
-    ],
-  },
-  // Next.js static assets - long cache with immutable
-  {
-    source: "/_next/static/:path*",
-    headers: [
-      {
-        key: "Cache-Control",
-        value: "public, max-age=31536000, immutable",
-      },
-    ],
-  },
-  {
-    source: "/_next/image/:path*",
-    headers: [
-      {
-        key: "Cache-Control",
-        value: "public, max-age=31536000, immutable",
-      },
-    ],
-  },
-  // Public folder assets - long cache
-  {
-    source: "/image/:path*",
-    headers: [
-      {
-        key: "Cache-Control",
-        value: "public, max-age=31536000, immutable",
-      },
-    ],
-  },
-  {
-    source: "/fonts/:path*",
-    headers: [
-      {
-        key: "Cache-Control",
-        value: "public, max-age=31536000, immutable",
-      },
-    ],
-  },
-];
+    secure: env.APP_ENV !== 'development' // true 表示这个 Cookie 是否只能通过 HTTPS 发送
+} satisfies Partial<ResponseCookie>;

+ 4 - 10
src/utils/helper.ts

@@ -2,7 +2,7 @@ import { ReadonlyURLSearchParams } from "next/navigation";
 import { type Metadata } from "next";
 import type { FilterDataTypes } from "@/types/types";
 import { isArray } from "./type-guards";
-import { BASE_URL, baseUrl } from "./constants";
+import { env } from '@/env';
 
 
 // Build revision identifier — emitted in <meta> for SSR cache validation.
@@ -20,13 +20,7 @@ export const createUrl = (
   return `${pathname}${queryString}`;
 };
 
-/**
- * Get base url
- * @returns string
- */
-export const getBaseUrl = (baseUrl: string) => {
-  return baseUrl ? `https://${baseUrl}` : "http://localhost:3000";
-};
+
 
 export const isCleanFilter = (
   filters: FilterDataTypes[],
@@ -139,7 +133,7 @@ export async function generateMetadataForPage(
     seo.description || fallback?.description || "Default page description.";
   const ogImage = seo.image || fallback?.image || "/default-og.png";
   const canonicalUrl =
-    seo.canonical || fallback?.canonical || `${BASE_URL}/${slug}`;
+    seo.canonical || fallback?.canonical || `${env.NEXT_PUBLIC_SITE_URL}/${slug}`;
 
   const otherMeta = {
     ...DEFAULT_OTHER,
@@ -148,7 +142,7 @@ export async function generateMetadataForPage(
   };
 
   return {
-    metadataBase: new URL(baseUrl || BASE_URL || "http://localhost:3000"),
+    metadataBase: new URL(env.NEXT_PUBLIC_SITE_URL),
 
     title,
     description,

+ 0 - 134
src/utils/hooks/useCache.ts

@@ -1,134 +0,0 @@
-import { type DocumentNode, type OperationVariables } from "@apollo/client";
-import { graphqlRequest, type CacheLifeOption } from "@/lib/graphql-fetch";
-import type {GraphqlRequestResult} from "@/types/graphqlFetch/type";
-import { GET_FILTER_ATTRIBUTES } from "@/graphql";
-
-export interface PageCacheConfig {
-  tags: string[];
-  life: CacheLifeOption;
-}
-
-/**
- * Cache configuration for different pages and queries
- * Centralized management of cache tags and revalidation times
- */
-export const PAGE_CACHE_CONFIG: Record<string, PageCacheConfig> = {
-  // Home page
-  home: {
-    tags: ["home-page"],
-    life: "hours",
-  },
-
-  // Product pages
-  product: {
-    tags: ["all-products"],
-    life: "hours",
-  },
-
-  // Category/Collection pages
-  category: {
-    tags: ["categories"],
-    life: "hours",
-  },
-
-  // Static content
-  static: {
-    tags: ["static-content"],
-    life: "days",
-  },
-
-  // Search results
-  search: {
-    tags: ["search-results"],
-    life: "hours",
-  },
-};
-
-/**
- * Helper to get cache config for a specific page
- */
-export function getPageCacheConfig(
-  page: keyof typeof PAGE_CACHE_CONFIG,
-): PageCacheConfig {
-  return PAGE_CACHE_CONFIG[page];
-}
-
-/**
- * Helper to create dynamic product cache config with specific product identifier
- */
-// export function getProductCacheConfig(productId: string): PageCacheConfig {
-//   return {
-//     tags: ["products", `product-${productId}`],
-//     life: "hours",
-//   };
-// }
-export function getProductCacheConfig(): {noCache: boolean} {
-  return {noCache: true};
-}
-/**
- * Helper to create dynamic category cache config with specific category identifier
- */
-export function getCategoryCacheConfig(categoryId: string): PageCacheConfig {
-  return {
-    tags: ["categories", `category-${categoryId}`],
-    life: "hours",
-  };
-}
-
-/**
- * Wrapper hook for graphqlRequest with automatic cache management
- * Usage: const data = await cachedGraphQLRequest('home', query, variables);
- */
-export async function cachedGraphQLRequest<
-  TData = unknown,
-  TVariables extends OperationVariables = OperationVariables,
->(
-  page: keyof typeof PAGE_CACHE_CONFIG,
-  query: DocumentNode,
-  variables?: TVariables,
-): Promise<GraphqlRequestResult<TData>> {
-  const config = getPageCacheConfig(page);
-  return graphqlRequest<TData, TVariables>(query, variables, config);
-}
-
-
-
-/**
- * Wrapper for category-specific queries with dynamic cache tags
- */
-export async function cachedCategoryRequest<
-  TData = unknown,
-  TVariables extends OperationVariables = OperationVariables,
->(
-  categoryId: string,
-  query: DocumentNode,
-  variables?: TVariables,
-): Promise<GraphqlRequestResult<TData>> {
-  const config = getCategoryCacheConfig(categoryId);
-  return graphqlRequest<TData, TVariables>(query, variables, config);
-}
-
-/**
- * Fetches filter attributes (color, size, brand) for product filtering
- *
- * @returns Promise with formatted filter attributes
- */
-export async function getFilterAttributes() {
-  const {data: filterData } = await cachedGraphQLRequest<{
-    color: any;
-    size: any;
-    brand: any;
-  }>("static", GET_FILTER_ATTRIBUTES, { locale: "en" });
-
-  const attributes = [filterData?.color, filterData?.size, filterData?.brand];
-
-  return attributes.filter(Boolean).map((attr) => ({
-    id: attr.id,
-    code: attr.code,
-    adminName: attr.code.toUpperCase(),
-    options: attr.options.edges.map((o: any) => ({
-      id: o.node.id,
-      adminName: o.node.adminName,
-    })),
-  }));
-}