瀏覽代碼

Merge branch 'master' into zzf-account

zhangzf 2 天之前
父節點
當前提交
1363e16ed1
共有 100 個文件被更改,包括 2600 次插入695 次删除
  1. 0 5
      @types/graphql.d.ts
  2. 0 36
      @types/next-auth.d.ts
  3. 29 10
      README.md
  4. 6 3
      package.json
  5. 361 274
      pnpm-lock.yaml
  6. 1 5
      public/favicon.svg
  7. 二進制
      public/image/Logo.webp
  8. 二進制
      public/image/empty-cart.webp
  9. 二進制
      public/image/forget-password.webp
  10. 二進制
      public/image/login.webp
  11. 二進制
      public/image/payment/applepay.png
  12. 二進制
      public/image/payment/cash-icon.png
  13. 二進制
      public/image/payment/credit-card.png
  14. 1 0
      public/image/payment/paypal-logo.svg
  15. 1 0
      public/image/plus-crown.svg
  16. 14 14
      src/actions/addProductToCartAction.ts
  17. 2 2
      src/actions/deleteCartProductAction.ts
  18. 4 5
      src/actions/deleteGuestCookieAction.ts
  19. 2 1
      src/actions/index.ts
  20. 1 1
      src/actions/logoutAction.ts
  21. 2 2
      src/actions/mergeCartAction.ts
  22. 47 0
      src/actions/recoverPasswordAction.ts
  23. 1 1
      src/app/(checkout)/checkout/_components/CheckoutAddress/AddressResultDisplay.tsx
  24. 32 14
      src/app/(checkout)/checkout/_components/CheckoutAddress/BillingAddressCheckout.tsx
  25. 68 11
      src/app/(checkout)/checkout/_components/CheckoutAddress/CheckoutAssress.tsx
  26. 12 3
      src/app/(checkout)/checkout/_components/CheckoutAddress/ShippingAddressCheckout.tsx
  27. 1 1
      src/app/(checkout)/checkout/_components/CheckoutCoupon/CheckoutCoupon.tsx
  28. 2 2
      src/app/(checkout)/checkout/_components/CheckoutPlaceOrder.tsx
  29. 11 3
      src/app/(checkout)/checkout/_components/CheckoutProducts/CheckoutProducts.tsx
  30. 7 5
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  31. 6 2
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx
  32. 17 9
      src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx
  33. 6 2
      src/app/(checkout)/checkout/_components/ContinueToPay/ProductsModalPendingOrder.tsx
  34. 2 2
      src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx
  35. 1 1
      src/app/(checkout)/checkout/_components/LostInsurance/LostInsurance.tsx
  36. 2 2
      src/app/(checkout)/checkout/_components/PaymentButton/PaypalApplepayButton.tsx
  37. 26 5
      src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx
  38. 1 1
      src/app/(checkout)/checkout/_components/PromotionsDetails.tsx
  39. 2 2
      src/app/(checkout)/checkout/_components/RewardPointsRedeemMoney/RewardPointsRedeemMoney.tsx
  40. 2 2
      src/app/(checkout)/checkout/_components/ShippingMethodCheckout.tsx
  41. 1 1
      src/app/(checkout)/checkout/_components/VipPlus/CheckoutVipPlus.tsx
  42. 33 0
      src/app/(checkout)/checkout/cart/_components/BonusBuy.tsx
  43. 427 0
      src/app/(checkout)/checkout/cart/_components/CartWrapper.tsx
  44. 47 0
      src/app/(checkout)/checkout/cart/_components/EmptyCart.tsx
  45. 23 0
      src/app/(checkout)/checkout/cart/_components/PromiseInCart.tsx
  46. 288 0
      src/app/(checkout)/checkout/cart/_components/UnpaidOrderSection.tsx
  47. 84 0
      src/app/(checkout)/checkout/cart/page.tsx
  48. 31 31
      src/app/(checkout)/checkout/continuetopay/page.tsx
  49. 9 7
      src/app/(checkout)/checkout/page.tsx
  50. 47 5
      src/app/(checkout)/error.tsx
  51. 3 5
      src/app/(checkout)/layout.tsx
  52. 0 10
      src/app/(public)/[page]/opengraph-image.tsx
  53. 1 1
      src/app/(public)/[page]/page.tsx
  54. 1 1
      src/app/(public)/_components/CategoryTabSwiper .tsx
  55. 2 2
      src/app/(public)/_components/HomeBestSellers.tsx
  56. 5 3
      src/app/(public)/_components/HomeContent.tsx
  57. 1 1
      src/app/(public)/_components/HomeMiddleBanner.tsx
  58. 2 2
      src/app/(public)/_components/HomeNewArrival.tsx
  59. 1 1
      src/app/(public)/_components/JinGangSwiper.tsx
  60. 2 2
      src/app/(public)/_components/MiddleImageSwiper.tsx
  61. 2 2
      src/app/(public)/_components/RecommendedProducts.tsx
  62. 1 1
      src/app/(public)/_components/TopImageSwiper.tsx
  63. 2 2
      src/app/(public)/_components/Trending.tsx
  64. 1 1
      src/app/(public)/category/[slug]/[id]/_components/CategoryDesc.tsx
  65. 5 5
      src/app/(public)/category/[slug]/[id]/page.tsx
  66. 3 3
      src/app/(public)/customer/account/_components/AccountVipPopup.tsx
  67. 13 9
      src/app/(public)/customer/account/edit/page.tsx
  68. 5 5
      src/app/(public)/customer/account/mypoints/_components/GetPoints.tsx
  69. 1 1
      src/app/(public)/customer/account/mypoints/_components/PointsRule.tsx
  70. 2 2
      src/app/(public)/customer/account/pointlist/page.tsx
  71. 1 1
      src/app/(public)/customer/account/pointsreceivelist/page.tsx
  72. 2 2
      src/app/(public)/customer/address/eidt/[id]/page.tsx
  73. 1 1
      src/app/(public)/customer/address/new/_components/NewAddressDefault.tsx
  74. 1 1
      src/app/(public)/customer/address/page.tsx
  75. 138 0
      src/app/(public)/customer/forget-password/_components/ForgetPasswordForm.tsx
  76. 5 9
      src/app/(public)/customer/forget-password/page.tsx
  77. 420 0
      src/app/(public)/customer/login/_components/LoginPageWrapper.tsx
  78. 12 9
      src/app/(public)/customer/login/page.tsx
  79. 1 1
      src/app/(public)/customer/order/track/order_id/[id]/_components/TrackDetail.tsx
  80. 1 0
      src/app/(public)/customer/order/track/order_id/[id]/page.tsx
  81. 1 0
      src/app/(public)/customer/order/view/order_id/[id]/page.tsx
  82. 0 13
      src/app/(public)/customer/register/page.tsx
  83. 155 0
      src/app/(public)/customer/reset-password/_components/ResetPasswordWrapper.tsx
  84. 28 0
      src/app/(public)/customer/reset-password/page.tsx
  85. 47 5
      src/app/(public)/error.tsx
  86. 2 2
      src/app/(public)/layout.tsx
  87. 8 20
      src/app/(public)/page.tsx
  88. 4 4
      src/app/(public)/paymentresult/_components/CancelOrFailure.tsx
  89. 13 5
      src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx
  90. 7 8
      src/app/(public)/paymentresult/result/page.tsx
  91. 24 57
      src/app/(public)/product/[...urlProduct]/page.tsx
  92. 4 3
      src/app/(public)/product/_components/ProductInformation.tsx
  93. 1 1
      src/app/(public)/product/_components/ProductMedia.tsx
  94. 0 3
      src/app/(public)/product/_components/ProductReviewSection.tsx
  95. 6 16
      src/app/(public)/product/_components/SwitchButton.tsx
  96. 1 1
      src/app/(public)/product/_components/question/ComponentsQuestionModal.tsx
  97. 1 1
      src/app/(public)/product/_components/review/AddProductReview.tsx
  98. 1 1
      src/app/(public)/product/_components/review/AddRatingStar.tsx
  99. 2 2
      src/app/(public)/product/_components/review/QuestionInputModal.tsx
  100. 0 0
      src/app/(public)/product/_components/review/ReviewDetail.tsx

+ 0 - 5
@types/graphql.d.ts

@@ -1,5 +0,0 @@
-declare module '*.graphql' {
-  import { DocumentNode } from 'graphql';
-  const Schema: DocumentNode;
-  export = Schema;
-}

+ 0 - 36
@types/next-auth.d.ts

@@ -1,36 +0,0 @@
-import { DefaultSession } from 'next-auth';
-
-declare module 'next-auth' {
-  /**
-   * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
-   */
-  interface Session {
-    user: {
-      id: string;
-      firstname?: string;
-      lastname?: string;
-      token?: string;
-      accessToken?: string;
-      apiToken?: string;
-      role?: string;
-    } & DefaultSession['user'];
-  }
-
-  interface User {
-    id: string;
-    accessToken?: string;
-    apiToken?: string;
-    role?: string;
-    tokenType?: string;
-    expiresIn?: number;
-  }
-}
-
-declare module 'next-auth/jwt' {
-  interface JWT {
-    accessToken?: string;
-    apiToken?: string;
-    role?: string;
-    id?: string;
-  }
-}

+ 29 - 10
README.md

@@ -278,15 +278,15 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 
 
 ## TODO
 ## TODO
 需要做的工作:
 需要做的工作:
-1. 注册成功后需要走next-auth的登录流程
-2. 登录成功后需要合并购物车(在服务端完成) -- 待定
+1. 注册成功后需要走next-auth的登录流程 -- 完成
+2. 登录成功后需要合并购物车(在服务端完成) -- 前端合并,已完成
 
 
 3. createCart 接口报错 -- 已解决
 3. createCart 接口报错 -- 已解决
 4. 由于apollo client缓存策略以及graphql代理接口问题导致获取不到数据 -- 已解决
 4. 由于apollo client缓存策略以及graphql代理接口问题导致获取不到数据 -- 已解决
      - 3和4的问题是相互关联的
      - 3和4的问题是相互关联的
 
 
 5. 尝试升级apollo client版本到v4 -- 完成
 5. 尝试升级apollo client版本到v4 -- 完成
-6. jwt-cookie 放弃base64编码,尝试改为加密解密
+6. jwt-cookie 放弃base64编码,尝试改为加密解密 -- 不用了,已删除jwt-cookie 文件
 
 
 7. 登录状态点击退出登录后GetCartItem接口会报错 -- 待处理
 7. 登录状态点击退出登录后GetCartItem接口会报错 -- 待处理
   - 错误信息:
   - 错误信息:
@@ -310,12 +310,12 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 11. 代码eslint检查修改 -- 已完成
 11. 代码eslint检查修改 -- 已完成
 12. 下单成功后需要重新创建购物车token -- 无需处理
 12. 下单成功后需要重新创建购物车token -- 无需处理
 13. 详情页缓存策略(不要缓存吧)
 13. 详情页缓存策略(不要缓存吧)
-14. getSession  过期提示重新登录;token校验
+14. getSession  过期提示重新登录;token校验(见第47条)
 15. checkout 页面 src\components\Portal.tsx (15:33) 会报 document is not defined -- 已解决
 15. checkout 页面 src\components\Portal.tsx (15:33) 会报 document is not defined -- 已解决
-16. redux cartDetail 里的billingAddress 和 shippingAddress 字段改到cart里的billingAddress 和 shippingAddress以及保存完地址后同步到redux
+16. redux cartDetail 里的billingAddress 和 shippingAddress 字段改到cart里的billingAddress 和 shippingAddress以及保存完地址后同步到redux -- 已完成
 17. redux 中 cartDetail 异步获取数据 https://chat.deepseek.com/share/76kj30h2cws9jcl4jt  -- 已完成
 17. redux 中 cartDetail 异步获取数据 https://chat.deepseek.com/share/76kj30h2cws9jcl4jt  -- 已完成
 18. 需要一个遮挡整个视口的loading组件  -- 已完成
 18. 需要一个遮挡整个视口的loading组件  -- 已完成
-19. 结账页地址模块还差两个功能:登录用户从地址列表选择地址;未登录用户输入邮箱后判断是否注册过,如果注册过让用户登录;
+19. 结账页地址模块,登录用户从地址列表选择地址;--- 已完成
      - 电话区号切换还有问题
      - 电话区号切换还有问题
 20. 登录用户结账页支付失败后要跳转到支付失败页面提醒用户可以再次支付 -- 待定
 20. 登录用户结账页支付失败后要跳转到支付失败页面提醒用户可以再次支付 -- 待定
 21. 游客结账页支付失败或者取消支付,先调paymentCallback 再调cancelOrder。但是不删除cookie里之前的cartToken --- 完成
 21. 游客结账页支付失败或者取消支付,先调paymentCallback 再调cancelOrder。但是不删除cookie里之前的cartToken --- 完成
@@ -332,7 +332,8 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 30. 货币列表,国家列表等关于商城配置的数据采用全局ConfigProvider管理; -- 已完成
 30. 货币列表,国家列表等关于商城配置的数据采用全局ConfigProvider管理; -- 已完成
 31. 当前货币等采用redux管理;-- 废弃,已改为全局ConfigProvider管理
 31. 当前货币等采用redux管理;-- 废弃,已改为全局ConfigProvider管理
 32. 当前货币切换功能      --- 已完成
 32. 当前货币切换功能      --- 已完成
-33. 登录用户购物车页获取未支付订单
+33. 登录用户购物车页获取未支付订单 --- 已完成
+
 34. 结账页下单成功后清空redux购物车数据导致结账页重新渲染问题  -- 已解决
 34. 结账页下单成功后清空redux购物车数据导致结账页重新渲染问题  -- 已解决
     - 通过修改结账页数据初始化逻辑解决
     - 通过修改结账页数据初始化逻辑解决
 35. 结账页默认设置运输方式问题(导致结算联动风暴)
 35. 结账页默认设置运输方式问题(导致结算联动风暴)
@@ -350,14 +351,28 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 45. nextjs缓存与bagisto后台管理打通(后台修改配置通知nextjs清除缓存)
 45. nextjs缓存与bagisto后台管理打通(后台修改配置通知nextjs清除缓存)
     -- 参考https://chat.deepseek.com/share/6h1huduahkqln69gu1
     -- 参考https://chat.deepseek.com/share/6h1huduahkqln69gu1
 46. 购物车详情里的subtotal和grandtotal金额不对,(一个sku产品加购多个,钱只算了一个)--- 后端已处理
 46. 购物车详情里的subtotal和grandtotal金额不对,(一个sku产品加购多个,钱只算了一个)--- 后端已处理
-47. 请求接口时需要区分哪些接口需要token,哪些不需要(不需要的不在请求头中添加token,所以要扩展请求方法,加一个是否携带token的参数)。因为登录用户的token,需要校验有效性(设想是在nextAuth中校验)。
+47. 请求接口时需要区分哪些接口需要token,哪些不需要(不需要的不在请求头中添加token,所以要扩展请求方法,加一个是否携带token的参数)。
+    因为登录用户的token,需要校验有效性(设想是在nextAuth中校验)。
     对于需要token的接口:登录用户校验token; 游客只需要检查有没有token.
     对于需要token的接口:登录用户校验token; 游客只需要检查有没有token.
+    ---- 47 条 请求方法已经扩展是否携带token的参数,但是不再添加对于token的有效性校验,以后端接口返回的错误为依据判断有效性
 48. 游客token的创建优化(想在服务端创建写入token)--- 已完成
 48. 游客token的创建优化(想在服务端创建写入token)--- 已完成
-
+49. 结账页地址模块未登录用户输入邮箱后判断是否注册过,如果注册过让用户登录;--- 已完成
+50. 注册接口,提交的phone是 +1 345345345 格式,但是保存到数据库后成了+1345345345
 
 
 
 
 > 39,40 参考 https://chatgpt.com/share/6a4f0637-ad28-83ea-ad44-423740795054
 > 39,40 参考 https://chatgpt.com/share/6a4f0637-ad28-83ea-ad44-423740795054
 
 
+## 关于token过期
+
+第一种,服务端或者客户端请求接口时,接口返回token过期或者需要需要登录的状态码(401),根据状态码提示用户登录。
+> 客户端通过弹窗提示;服务端通过抛出错误,让error.tsx提示
+
+第二种,通过后端提供的校验token接口结合NextAuth.js主动校验token是否可用
+
+
+需要注意的是,不管哪一种,都要在token失效后,执行nextAuth.js的signOut函数。
+
+
 ## 关于请求接口和错误处理
 ## 关于请求接口和错误处理
 接口请求有四种情况:服务端组件里的graphql请求和restful请求;客户端组件里的graphql请求和restful请求。
 接口请求有四种情况:服务端组件里的graphql请求和restful请求;客户端组件里的graphql请求和restful请求。
 ### 服务端组件里的graphql请求
 ### 服务端组件里的graphql请求
@@ -472,4 +487,8 @@ Hook 是 React 在 render 过程中注册状态依赖的函数调用点,而不
 ## 关于set-state-in-effect
 ## 关于set-state-in-effect
 1. https://yceffort.kr/en/2025/12/react-set-state-in-effect-lint-rule
 1. https://yceffort.kr/en/2025/12/react-set-state-in-effect-lint-rule
 2. 使用useReducer规避待验证:https://stackoverflow.com/questions/79856121/calling-setstate-synchronously-within-an-effect-can-trigger-cascading-renders
 2. 使用useReducer规避待验证:https://stackoverflow.com/questions/79856121/calling-setstate-synchronously-within-an-effect-can-trigger-cascading-renders
-3. https://github.com/react/react/issues/34743
+3. https://github.com/react/react/issues/34743
+
+
+## 关于数学加减乘除等计算
+本项目中使用[big.js](https://mikemcl.github.io/big.js)进行数字的数学计算

+ 6 - 3
package.json

@@ -7,7 +7,7 @@
     "build": "eslint . && next build",
     "build": "eslint . && next build",
     "start": "next start -p 3001",
     "start": "next start -p 3001",
     "lint": "eslint .",
     "lint": "eslint .",
-    "lint:fix": "next lint --fix",
+    "lint:fix": "eslint . --fix",
     "package-version": "npx npm-check-updates"
     "package-version": "npx npm-check-updates"
   },
   },
   "dependencies": {
   "dependencies": {
@@ -33,16 +33,18 @@
     "@heroui/tooltip": "^2.2.29",
     "@heroui/tooltip": "^2.2.29",
     "@heroui/use-disclosure": "^2.2.19",
     "@heroui/use-disclosure": "^2.2.19",
     "@internationalized/date": "3.12.0",
     "@internationalized/date": "3.12.0",
+    "@livechat/widget-react": "^1.4.1",
     "@paypal/react-paypal-js": "^10.1.2",
     "@paypal/react-paypal-js": "^10.1.2",
     "@react-aria/visually-hidden": "^3.8.31",
     "@react-aria/visually-hidden": "^3.8.31",
     "@react-types/shared": "^3.33.1",
     "@react-types/shared": "^3.33.1",
     "@reduxjs/toolkit": "^2.10.1",
     "@reduxjs/toolkit": "^2.10.1",
+    "big.js": "^7.0.1",
     "clsx": "^2.1.1",
     "clsx": "^2.1.1",
     "framer-motion": "^12.23.24",
     "framer-motion": "^12.23.24",
     "graphql": "^16.12.0",
     "graphql": "^16.12.0",
     "libphonenumber-js": "^1.13.4",
     "libphonenumber-js": "^1.13.4",
     "lucide-react": "^0.563.0",
     "lucide-react": "^0.563.0",
-    "next": "16.2.6",
+    "next": "16.3.4",
     "next-auth": "^4.24.13",
     "next-auth": "^4.24.13",
     "react": "19.2.5",
     "react": "19.2.5",
     "react-dom": "19.2.5",
     "react-dom": "19.2.5",
@@ -53,12 +55,13 @@
   },
   },
   "devDependencies": {
   "devDependencies": {
     "@tailwindcss/postcss": "^4",
     "@tailwindcss/postcss": "^4",
+    "@types/big.js": "^7.0.0",
     "@types/node": "^20",
     "@types/node": "^20",
     "@types/react": "19.2.14",
     "@types/react": "19.2.14",
     "@types/react-dom": "19.2.3",
     "@types/react-dom": "19.2.3",
     "cross-env": "^10.1.0",
     "cross-env": "^10.1.0",
     "eslint": "^9",
     "eslint": "^9",
-    "eslint-config-next": "16.2.3",
+    "eslint-config-next": "16.3.4",
     "tailwindcss": "^4",
     "tailwindcss": "^4",
     "ts-node": "^10.9.2",
     "ts-node": "^10.9.2",
     "typescript": "^5"
     "typescript": "^5"

文件差異過大導致無法顯示
+ 361 - 274
pnpm-lock.yaml


文件差異過大導致無法顯示
+ 1 - 5
public/favicon.svg


二進制
public/image/Logo.webp


二進制
public/image/empty-cart.webp


二進制
public/image/forget-password.webp


二進制
public/image/login.webp


二進制
public/image/payment/applepay.png


二進制
public/image/payment/cash-icon.png


二進制
public/image/payment/credit-card.png


文件差異過大導致無法顯示
+ 1 - 0
public/image/payment/paypal-logo.svg


文件差異過大導致無法顯示
+ 1 - 0
public/image/plus-crown.svg


+ 14 - 14
src/actions/addProductToCartAction.ts

@@ -3,17 +3,15 @@
 import {cookies} from "next/headers";
 import {cookies} from "next/headers";
 import { GUEST_CART_TOKEN } from "@/utils/constants";
 import { GUEST_CART_TOKEN } from "@/utils/constants";
 import { CREATE_ADD_PRODUCT_IN_CART } from "@/graphql";
 import { CREATE_ADD_PRODUCT_IN_CART } from "@/graphql";
-import { getServerSession } from "next-auth";
-import { authOptions } from "@utils/auth";
+import { auth } from "@/utils/auth/auth-helper";
 import { serverGraphqlFetch } from "@utils/bagisto";
 import { serverGraphqlFetch } from "@utils/bagisto";
-import { BagistoSession } from "@/types/types";
 import {createGuestCartToken,setGuestCookie} from "@/server-service/guestCartTokenService";
 import {createGuestCartToken,setGuestCookie} from "@/server-service/guestCartTokenService";
-import { AddToCartData,AddToCartVariables } from "@/types/cart/type";
+import type { AddToCartData,AddToCartVariables } from "@/types/cart/type";
 
 
 
 
 export async function addProductToCartAction(param:AddToCartVariables) {
 export async function addProductToCartAction(param:AddToCartVariables) {
 
 
-    const authSession = (await getServerSession(authOptions)) as BagistoSession | null;
+    const authSession = await auth();
     const accessToken = authSession?.user?.accessToken;
     const accessToken = authSession?.user?.accessToken;
 
 
     const cookieStore = await cookies();
     const cookieStore = await cookies();
@@ -37,18 +35,20 @@ export async function addProductToCartAction(param:AddToCartVariables) {
             cache: "no-store",
             cache: "no-store",
         });
         });
         if(!response.error) {
         if(!response.error) {
-            const responseData = response.data.createAddProductInCart?.addProductInCart;
+            const responseData = response.data?.createAddProductInCart?.addProductInCart;
             /** 兜底代码 start*/
             /** 兜底代码 start*/
             // 游客加购,然后清空购物车,然后刷新页面,然后再加购,然后再刷新页面,购物车会失效,所以添加兜底代码
             // 游客加购,然后清空购物车,然后刷新页面,然后再加购,然后再刷新页面,购物车会失效,所以添加兜底代码
             // 如果之前的cartToken有效,responseCartId和responseCartToken的值是一样的都是cart id; 否则两者不相等
             // 如果之前的cartToken有效,responseCartId和responseCartToken的值是一样的都是cart id; 否则两者不相等
-            const responseCartId = responseData.id;
-            const responseCartToken = responseData.cartToken;
-            if(!accessToken && responseCartId !== responseCartToken) {
-                await setGuestCookie({
-                    guestCartId: responseCartId,
-                    guestToken: responseCartToken,
-                    isGuest: true
-                });
+            if(responseData) {
+                const responseCartId = responseData.id;
+                const responseCartToken = responseData.cartToken;
+                if(!accessToken && responseCartId !== responseCartToken) {
+                    await setGuestCookie({
+                        guestCartId: responseCartId,
+                        guestToken: responseCartToken,
+                        isGuest: true
+                    });
+                }
             }
             }
             /** 兜底代码 end*/
             /** 兜底代码 end*/
 
 

+ 2 - 2
src/actions/deleteCartProductAction.ts

@@ -4,7 +4,7 @@
 import { REMOVE_CART_ITEM } from "@/graphql";
 import { REMOVE_CART_ITEM } from "@/graphql";
 import { serverGraphqlFetch } from "@utils/bagisto";
 import { serverGraphqlFetch } from "@utils/bagisto";
 import {deleteGuestCookie} from "@/server-service/guestCartTokenService";
 import {deleteGuestCookie} from "@/server-service/guestCartTokenService";
-import { RemoveCartItemData } from "@/types/cart/type";
+import type { RemoveCartItemData } from "@/types/cart/type";
 
 
 
 
 export async function deleteCartProductAction(cartItemId:number) {
 export async function deleteCartProductAction(cartItemId:number) {
@@ -18,7 +18,7 @@ export async function deleteCartProductAction(cartItemId:number) {
             cache: "no-store",
             cache: "no-store",
         });
         });
         if(!response.error) {
         if(!response.error) {
-            const resCatData = response.data.createRemoveCartItem?.removeCartItem ?? null;
+            const resCatData = response.data?.createRemoveCartItem?.removeCartItem ?? null;
             
             
             if(!resCatData || !resCatData?.itemsQty) {
             if(!resCatData || !resCatData?.itemsQty) {
                 // 购物车空了,删除游客cookie
                 // 购物车空了,删除游客cookie

+ 4 - 5
src/actions/deleteGuestCookieAction.ts

@@ -2,12 +2,11 @@
 
 
 import {deleteGuestCookie} from "@/server-service/guestCartTokenService";
 import {deleteGuestCookie} from "@/server-service/guestCartTokenService";
 
 
-export async function deleteGuestCookieAction() {
-        
-    await deleteGuestCookie();
-
+export async function deleteGuestCookieAction(isGuest: boolean = true) {
+    await deleteGuestCookie(isGuest);
+  
     return {
     return {
         success: true,
         success: true,
-        msg: ""
+        msg: "",
     };
     };
 }
 }

+ 2 - 1
src/actions/index.ts

@@ -4,4 +4,5 @@ export {createGuestCartAction} from "./createGuestCartAction";
 export {switchCurrencyAction} from "./switchCurrencyAction";
 export {switchCurrencyAction} from "./switchCurrencyAction";
 export {addProductToCartAction} from "./addProductToCartAction";
 export {addProductToCartAction} from "./addProductToCartAction";
 export {deleteCartProductAction} from "./deleteCartProductAction";
 export {deleteCartProductAction} from "./deleteCartProductAction";
-export {logoutAction} from "@/actions/logoutAction";
+export {logoutAction} from "@/actions/logoutAction";
+export {recoverPasswordAction} from "@/actions/recoverPasswordAction";

+ 1 - 1
src/actions/logoutAction.ts

@@ -4,7 +4,7 @@ import {cookies} from "next/headers";
 import { IS_GUEST } from "@/utils/constants";
 import { IS_GUEST } from "@/utils/constants";
 import { serverGraphqlFetch } from "@utils/bagisto";
 import { serverGraphqlFetch } from "@utils/bagisto";
 import {CUSTOMER_LOGOUT} from "@/graphql/customer/mutations";
 import {CUSTOMER_LOGOUT} from "@/graphql/customer/mutations";
-import { LogoutData } from "@/types/types";
+import type { LogoutData } from "@/types/types";
 
 
 export async function logoutAction() {
 export async function logoutAction() {
 
 

+ 2 - 2
src/actions/mergeCartAction.ts

@@ -5,7 +5,7 @@ import {cookies} from "next/headers";
 import { GUEST_CART_ID, GUEST_CART_TOKEN, IS_GUEST } from "@/utils/constants";
 import { GUEST_CART_ID, GUEST_CART_TOKEN, IS_GUEST } from "@/utils/constants";
 import { serverGraphqlFetch } from "@utils/bagisto";
 import { serverGraphqlFetch } from "@utils/bagisto";
 import { CREATE_MERGE_CART } from "@/graphql";
 import { CREATE_MERGE_CART } from "@/graphql";
-import { CreateMergeCartData } from "@/types/cart/type";
+import type { CreateMergeCartData } from "@/types/cart/type";
 
 
 export async function mergeCartAction(){
 export async function mergeCartAction(){
 
 
@@ -33,7 +33,7 @@ export async function mergeCartAction(){
             },
             },
             cache: "no-store",
             cache: "no-store",
         });
         });
-        const cartData = response.data.createMergeCart?.mergeCart ?? null;
+        const cartData = response.data?.createMergeCart?.mergeCart ?? null;
         let success = true;
         let success = true;
         let msg = "";
         let msg = "";
         if(response.error) {
         if(response.error) {

+ 47 - 0
src/actions/recoverPasswordAction.ts

@@ -0,0 +1,47 @@
+"use server";
+
+import {FORGET_PASSWORD} from "@/graphql/customer/mutations";
+import { serverGraphqlFetch } from "@utils/bagisto";
+
+interface ForgetPasswordData{
+     createForgotPassword: {
+      forgotPassword: {
+        success: boolean;
+        message: string;
+      }
+    }
+}
+
+
+export async function recoverPasswordAction(formData: {
+  email: string;
+}): Promise<{ success: boolean; msg: string;}> {
+    try {
+        const response = await serverGraphqlFetch<ForgetPasswordData,{email: string;}>({
+            query: FORGET_PASSWORD,
+            variables: {
+                email: formData.email
+            }
+        });
+        if(!response.error) {
+            const resData = response?.data?.createForgotPassword?.forgotPassword;
+
+            return {
+                success: resData?.success ?? false,
+                msg: resData?.message ?? '',
+            };
+
+        } else {
+            return {
+                success: false,
+                msg: response.error.message,
+            }
+        }
+
+    } catch(error) {
+        return {
+            success: false,
+            msg: error instanceof Error ? error.message : 'Add product to cart failed!',
+        }
+    }
+}

+ 1 - 1
src/app/(checkout)/checkout/_components/CheckoutAddress/AddressResultDisplay.tsx

@@ -1,6 +1,6 @@
 "use client";
 "use client";
 
 
-import {CartAddress} from "@/types/cart/type";
+import type {CartAddress} from "@/types/cart/type";
 
 
 function isAddressAvailable(address: CartAddress | null): boolean {
 function isAddressAvailable(address: CartAddress | null): boolean {
     if(address && 
     if(address && 

+ 32 - 14
src/app/(checkout)/checkout/_components/CheckoutAddress/BillingAddressCheckout.tsx

@@ -20,7 +20,7 @@ import {
     IS_VALID_FULL_PHONE 
     IS_VALID_FULL_PHONE 
 } from "@utils/constants";
 } from "@utils/constants";
 import { useConfig } from "@utils/hooks/useConfig";
 import { useConfig } from "@utils/hooks/useConfig";
-import { CustomerAddressItem } from "@/types/customer/type";
+import type { CustomerAddressItem } from "@/types/customer/type";
 import InputText from "@/components/theme/ui/InputText";
 import InputText from "@/components/theme/ui/InputText";
 import Select from "@/components/theme/ui/Select";
 import Select from "@/components/theme/ui/Select";
 import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumberInput";
 import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumberInput";
@@ -45,6 +45,7 @@ export default function BillingAddressCheckout ({
         getValues,
         getValues,
         setValue,
         setValue,
         register,
         register,
+        clearErrors,
         formState: { errors },
         formState: { errors },
         control 
         control 
     } = useFormContext() // retrieve all hook methods
     } = useFormContext() // retrieve all hook methods
@@ -82,6 +83,23 @@ export default function BillingAddressCheckout ({
         ]
         ]
     });
     });
 
 
+    useEffect(() => {
+        if (billingSameAsShipping) {
+            clearErrors([
+                "billingEmail",
+                "billingFirstName",
+                "billingLastName",
+                "billingCompanyName",
+                "billingAddress",
+                "billingCountry",
+                "billingState",
+                "billingCity",
+                "billingPostcode",
+                "billingPhoneNumber",
+            ]);
+        }
+    }, [billingSameAsShipping, clearErrors]);
+    /*
     const shippingFields = useWatch({
     const shippingFields = useWatch({
         control,
         control,
         name: [
         name: [
@@ -119,12 +137,12 @@ export default function BillingAddressCheckout ({
         setValue("billingCompanyName", shippingCompanyName);
         setValue("billingCompanyName", shippingCompanyName);
         setValue("billingAddress", shippingAddress);
         setValue("billingAddress", shippingAddress);
         setValue("billingCountry", shippingCountry);
         setValue("billingCountry", shippingCountry);
-        setValue("billingState", shippingState);
         setValue("billingCity", shippingCity);
         setValue("billingCity", shippingCity);
         setValue("billingPostcode", shippingPostcode);
         setValue("billingPostcode", shippingPostcode);
         setValue("billingPhoneNumber", shippingPhoneNumber);
         setValue("billingPhoneNumber", shippingPhoneNumber);
+        setValue("billingState", shippingState,{shouldValidate: true}); // 必须触发校验,因为使用浏览器自动填充的时候,可能导致billingState校验不通过,导致无法下单
     }, [billingSameAsShipping, shippingFields, setValue]);
     }, [billingSameAsShipping, shippingFields, setValue]);
-    
+    */
     
     
 
 
     const selectedCountry = useMemo(() => {
     const selectedCountry = useMemo(() => {
@@ -226,7 +244,7 @@ export default function BillingAddressCheckout ({
                     >
                     >
                         <span className="text-ly-12">+New Shipping Address</span>
                         <span className="text-ly-12">+New Shipping Address</span>
                         {!isGuest && 
                         {!isGuest && 
-                            <button className="flex-none" title="change address">
+                            <button className="flex-none" title="change address" type="button">
                                 <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="16" y="0" width="16" height="16" transform="rotate(90 16 0)"   fill="#FFFFFF" fillOpacity="0"></rect><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
                                 <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="16" y="0" width="16" height="16" transform="rotate(90 16 0)"   fill="#FFFFFF" fillOpacity="0"></rect><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
                             </button>
                             </button>
                         }
                         }
@@ -304,7 +322,7 @@ export default function BillingAddressCheckout ({
                         type="email"
                         type="email"
                         placeholder="Enter your email address"
                         placeholder="Enter your email address"
                         {...register("billingEmail", {
                         {...register("billingEmail", {
-                            required: "Email is required",
+                            required: !billingSameAsShipping ? "Email is required" : false,
                             pattern: {
                             pattern: {
                                 value: EMAIL_REGEX,
                                 value: EMAIL_REGEX,
                                 message: "Please enter a valid email address",
                                 message: "Please enter a valid email address",
@@ -323,7 +341,7 @@ export default function BillingAddressCheckout ({
                                 type="text"
                                 type="text"
                                 placeholder="First name"
                                 placeholder="First name"
                                 {...register("billingFirstName", {
                                 {...register("billingFirstName", {
-                                    required: "First name is required",
+                                    required: !billingSameAsShipping ? "First name is required" : false,
                                     pattern: {
                                     pattern: {
                                         value: IS_VALID_INPUT,
                                         value: IS_VALID_INPUT,
                                         message: "Invalid First Name",
                                         message: "Invalid First Name",
@@ -337,7 +355,7 @@ export default function BillingAddressCheckout ({
                                 type="text"
                                 type="text"
                                 placeholder="Last name"
                                 placeholder="Last name"
                                 {...register("billingLastName", {
                                 {...register("billingLastName", {
-                                    required: "Last name is required",
+                                    required: !billingSameAsShipping ? "Last name is required" : false,
                                     pattern: {
                                     pattern: {
                                         value: IS_VALID_INPUT,
                                         value: IS_VALID_INPUT,
                                         message: "Invalid Last Name",
                                         message: "Invalid Last Name",
@@ -355,7 +373,7 @@ export default function BillingAddressCheckout ({
                         type="text"
                         type="text"
                         placeholder="Please Input Your Detailed Address With Apt. No"
                         placeholder="Please Input Your Detailed Address With Apt. No"
                         {...register("billingAddress", {
                         {...register("billingAddress", {
-                            required: "Address is required",
+                            required: !billingSameAsShipping ? "Address is required" : false,
                         })}
                         })}
                         error={errors.billingAddress && errors.billingAddress.message as string}
                         error={errors.billingAddress && errors.billingAddress.message as string}
                     />
                     />
@@ -369,7 +387,7 @@ export default function BillingAddressCheckout ({
                             <div className="w-41.25 flex-none">
                             <div className="w-41.25 flex-none">
                                 <Select placeholder="Country/Region"
                                 <Select placeholder="Country/Region"
                                     {...register("billingCountry", {
                                     {...register("billingCountry", {
-                                        required: "Country/Region field is required",
+                                        required: !billingSameAsShipping ? "Country/Region field is required" : false,
                                         onChange: () => {
                                         onChange: () => {
                                             setValue('billingState','',{
                                             setValue('billingState','',{
                                                 shouldDirty: true,
                                                 shouldDirty: true,
@@ -391,7 +409,7 @@ export default function BillingAddressCheckout ({
                                 (
                                 (
                                     <Select placeholder="State/Province"
                                     <Select placeholder="State/Province"
                                         {...register("billingState", {
                                         {...register("billingState", {
-                                            required: "State/Province field is required"
+                                            required: !billingSameAsShipping ? "State/Province field is required" : false
                                         })}
                                         })}
                                         options={statesOptions}
                                         options={statesOptions}
                                         error={errors.billingState && errors.billingState.message as string}
                                         error={errors.billingState && errors.billingState.message as string}
@@ -402,7 +420,7 @@ export default function BillingAddressCheckout ({
                                         type="text"
                                         type="text"
                                         placeholder="State/Province"
                                         placeholder="State/Province"
                                         {...register("billingState", {
                                         {...register("billingState", {
-                                            required: "State/Province field is required"
+                                            required: !billingSameAsShipping ? "State/Province field is required" : false
                                         })}
                                         })}
                                         error={errors.billingState && errors.billingState.message as string}
                                         error={errors.billingState && errors.billingState.message as string}
                                     />
                                     />
@@ -417,7 +435,7 @@ export default function BillingAddressCheckout ({
                                     type="text"
                                     type="text"
                                     placeholder="Zip/Postal Code"
                                     placeholder="Zip/Postal Code"
                                     {...register("billingPostcode", {
                                     {...register("billingPostcode", {
-                                        required: "Postcode field is required",
+                                        required: !billingSameAsShipping ? "Postcode field is required" : false,
                                         pattern: {
                                         pattern: {
                                             value: IS_VALID_INPUT,
                                             value: IS_VALID_INPUT,
                                             message: "Invalid Postcode",
                                             message: "Invalid Postcode",
@@ -431,7 +449,7 @@ export default function BillingAddressCheckout ({
                                     type="text"
                                     type="text"
                                     placeholder="City"
                                     placeholder="City"
                                     {...register("billingCity", {
                                     {...register("billingCity", {
-                                        required: "City field is required",
+                                        required: !billingSameAsShipping ? "City field is required" : false,
                                         pattern: {
                                         pattern: {
                                             value: IS_VALID_INPUT,
                                             value: IS_VALID_INPUT,
                                             message: "Invalid City",
                                             message: "Invalid City",
@@ -451,7 +469,7 @@ export default function BillingAddressCheckout ({
                         name="billingPhoneNumber"
                         name="billingPhoneNumber"
                         control={control}
                         control={control}
                         rules={{
                         rules={{
-                            required: "Phone Number is required",
+                            required: !billingSameAsShipping ? "Phone Number is required" : false,
                             pattern: {
                             pattern: {
                                 value: IS_VALID_FULL_PHONE,
                                 value: IS_VALID_FULL_PHONE,
                                 message: "Invalid Phone Number",
                                 message: "Invalid Phone Number",

+ 68 - 11
src/app/(checkout)/checkout/_components/CheckoutAddress/CheckoutAssress.tsx

@@ -1,24 +1,25 @@
 "use client";
 "use client";
 
 
-import {Ref, useState, useEffect, useCallback, useImperativeHandle, useMemo } from "react";
-
+import {type Ref, useState, useEffect, useCallback, useImperativeHandle, useMemo } from "react";
+import { useRouter } from 'next/navigation';
 import { useForm, FormProvider } from "react-hook-form";
 import { useForm, FormProvider } from "react-hook-form";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import {useCheckoutAddress} from "@/utils/hooks/useCheckoutAddress"
 import {useCheckoutAddress} from "@/utils/hooks/useCheckoutAddress"
-
+import {useLoginModal} from "@/providers/LoginModalProvider";
 import { useGetCustomerAddress } from "@utils/hooks/useGetCustomerAddress";
 import { useGetCustomerAddress } from "@utils/hooks/useGetCustomerAddress";
-import { 
+import { EMAIL_REGEX } from "@utils/constants";
+import type { 
     ShipAddressFormData,
     ShipAddressFormData,
     FullAddressFormData,
     FullAddressFormData,
     CreateCheckoutAddressVariables,
     CreateCheckoutAddressVariables,
 } from "@/types/checkout/type";
 } from "@/types/checkout/type";
-import { CustomerAddressItem } from "@/types/customer/type";
-import { CartDetail,CartAddress } from "@/types/cart/type";
-
+import type { CustomerAddressItem,CheckEmailRegisteredData } from "@/types/customer/type";
+import type { CartDetail,CartAddress } from "@/types/cart/type";
+import { formatCartDetail } from "@/utils/cartDetailTools";
+import {clientFetch} from "@/lib/restApiClient";
+import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
-
 import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
 import {normalizePhoneForForm} from "@/utils/phoneNumberTools";
-
 import AddressResultDisplay from "./AddressResultDisplay";
 import AddressResultDisplay from "./AddressResultDisplay";
 import ShippingAddressCheckout from "./ShippingAddressCheckout";
 import ShippingAddressCheckout from "./ShippingAddressCheckout";
 import BillingAddressCheckout from "./BillingAddressCheckout";
 import BillingAddressCheckout from "./BillingAddressCheckout";
@@ -210,13 +211,18 @@ export function CheckoutAddress({
     loginEmail,
     loginEmail,
     cartData,
     cartData,
     onSaveAddress,
     onSaveAddress,
+    onCartChange
 }: {
 }: {
     ref: Ref<RefCheckoutAddressHandle>;
     ref: Ref<RefCheckoutAddressHandle>;
     loginEmail: string;
     loginEmail: string;
     cartData: CartDetail;
     cartData: CartDetail;
     onSaveAddress: (saveRes: {billingAddress: CartAddress; shippingAddress: CartAddress;}) => void;
     onSaveAddress: (saveRes: {billingAddress: CartAddress; shippingAddress: CartAddress;}) => void;
+    onCartChange: (data: CartDetail,dispatch: boolean) => void;
 }) {
 }) {
 
 
+    const router = useRouter();
+    const {openLoginModal} = useLoginModal();
+
     const addressFormDefaultValues = useMemo(() => {
     const addressFormDefaultValues = useMemo(() => {
         return getAddressFormDataFromCart(cartData,loginEmail)
         return getAddressFormDataFromCart(cartData,loginEmail)
     },[cartData,loginEmail]);
     },[cartData,loginEmail]);
@@ -374,12 +380,62 @@ export function CheckoutAddress({
             }  
             }  
         }
         }
         loadAddress();
         loadAddress();
+    },[showToast,cartData.isGuest]);
+    useEffect(() => {
+        if(cartData.isGuest) return;
         const timer = window.requestAnimationFrame(() => {
         const timer = window.requestAnimationFrame(() => {
             addressForm.reset(addressFormDefaultValues);
             addressForm.reset(addressFormDefaultValues);
         });
         });
         return () => window.cancelAnimationFrame(timer)
         return () => window.cancelAnimationFrame(timer)
-    },[showToast,cartData.isGuest,addressFormDefaultValues]);
-    
+    },[cartData.isGuest,addressFormDefaultValues]);
+
+    const verifyEmailIsRegistered = async (value: string) => {
+        if(!cartData.isGuest) return;
+        if(!EMAIL_REGEX.test(value)) return;
+        const param = new URLSearchParams({ email: value });
+
+        const res = await clientFetch<CheckEmailRegisteredData>(`/api/customer/check-email?${param.toString()}`,{
+            method: 'GET',
+        });
+
+        if(res.success && res.data.exists) { // 注册过
+            openLoginModal({
+                defaultEmail: value,
+                onFinalResult: async (res) => {
+                    if(res.mergeCartSuccess) { // 购物车合并成功
+                        if(res.cartAfterMerge){
+                            const newCartDetail = formatCartDetail(res.cartAfterMerge);
+                            onCartChange(newCartDetail,false);
+                        }
+                        
+                    } else { // 购物车合并失败
+                        // 三种 情况: 登录成功; 注册成功;
+                        if(res.loginSuccess) {
+                            // 登录成功 需要刷新页面
+                            showToast('You have logged in successfully. But something wrong','danger');
+                            await confirmDialog({
+                                title: "Warning",
+                                content: "You have logged in successfully. But something wrong. You must refresh the page.",
+                                noCancel: true,
+                            });
+                            window.location.reload();
+                        }
+                        if(res.registerSuccess && !res.loginSuccess) {
+                            // 注册成功,去登录
+                            await confirmDialog({
+                                title: "Warning",
+                                content: "You have registered successfully. But something wrong. You have to login.",
+                                noCancel: true,
+                            });
+                            router.replace('/customer/login');
+                        }
+                    }
+                }
+            });
+        }
+        
+    };
+
     // function tt() {
     // function tt() {
     //     const arr = [
     //     const arr = [
     //         ['US','3803800217'], 
     //         ['US','3803800217'], 
@@ -425,6 +481,7 @@ export function CheckoutAddress({
                                     customerAddressList={myAddressList}
                                     customerAddressList={myAddressList}
                                     showFormField={showShipFormField}
                                     showFormField={showShipFormField}
                                     onShowFormFieldChange={showShipFormFieldChange}
                                     onShowFormFieldChange={showShipFormFieldChange}
+                                    onEmailChange={verifyEmailIsRegistered}
                                 />
                                 />
                                 <BillingAddressCheckout 
                                 <BillingAddressCheckout 
                                     noAddress={noBillAddress}
                                     noAddress={noBillAddress}

+ 12 - 3
src/app/(checkout)/checkout/_components/CheckoutAddress/ShippingAddressCheckout.tsx

@@ -14,7 +14,7 @@ import {
 } from "@/components/theme/ui/PhoneNumberInput/phoneCodeMetaData";
 } from "@/components/theme/ui/PhoneNumberInput/phoneCodeMetaData";
 import { GET_COUNTRY_STATES } from "@/graphql";
 import { GET_COUNTRY_STATES } from "@/graphql";
 import { EMAIL_REGEX, IS_VALID_INPUT, IS_VALID_FULL_PHONE, IS_VALID_PHONECODE } from "@utils/constants";
 import { EMAIL_REGEX, IS_VALID_INPUT, IS_VALID_FULL_PHONE, IS_VALID_PHONECODE } from "@utils/constants";
-import { CustomerAddressItem } from "@/types/customer/type";
+import type { CustomerAddressItem } from "@/types/customer/type";
 import { useConfig } from "@utils/hooks/useConfig";
 import { useConfig } from "@utils/hooks/useConfig";
 import InputText from "@/components/theme/ui/InputText";
 import InputText from "@/components/theme/ui/InputText";
 import Select from "@/components/theme/ui/Select";
 import Select from "@/components/theme/ui/Select";
@@ -30,12 +30,14 @@ export default function ShippingAddressCheckout({
     showFormField,
     showFormField,
     onShowFormFieldChange,
     onShowFormFieldChange,
     customerAddressList,
     customerAddressList,
+    onEmailChange
 }: {
 }: {
     noAddress: boolean;
     noAddress: boolean;
     isGuest: boolean;
     isGuest: boolean;
     showFormField: boolean;
     showFormField: boolean;
     onShowFormFieldChange: (e: boolean) => void;
     onShowFormFieldChange: (e: boolean) => void;
     customerAddressList: CustomerAddressItem[];
     customerAddressList: CustomerAddressItem[];
+    onEmailChange: (value: string) => void;
 }) {
 }) {
 
 
     const {countries} = useConfig();
     const {countries} = useConfig();
@@ -46,7 +48,7 @@ export default function ShippingAddressCheckout({
         formState: { errors },
         formState: { errors },
         control,
         control,
     } = useFormContext() // retrieve all hook methods
     } = useFormContext() // retrieve all hook methods
-    console.log('errors ----- ', errors);
+    
     
     
     const countriesOptions = useMemo(() => {
     const countriesOptions = useMemo(() => {
         return countries.map((country) => ({
         return countries.map((country) => ({
@@ -175,7 +177,7 @@ export default function ShippingAddressCheckout({
                 >
                 >
                     <span className="text-ly-12">+New Shipping Address</span>
                     <span className="text-ly-12">+New Shipping Address</span>
                     {!isGuest && 
                     {!isGuest && 
-                        <button className="flex-none" title="change address">
+                        <button className="flex-none" title="change address" type="button">
                             <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="16" y="0" width="16" height="16" transform="rotate(90 16 0)"   fill="#FFFFFF" fillOpacity="0"></rect><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
                             <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="16" y="0" width="16" height="16" transform="rotate(90 16 0)"   fill="#FFFFFF" fillOpacity="0"></rect><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M11.7295 6.32028L7.62964 10.4201L3.52978 6.32028"></path></svg>
                         </button>
                         </button>
                     }
                     }
@@ -263,6 +265,13 @@ export default function ShippingAddressCheckout({
                         pattern: {
                         pattern: {
                             value: EMAIL_REGEX,
                             value: EMAIL_REGEX,
                             message: "Please enter a valid email address",
                             message: "Please enter a valid email address",
+                        },
+                        onChange: (e) => {
+                            onEmailChange(e.target.value);
+                            // console.log(e.target.value);
+                            // if(isGuest) {
+                            //     verifyEmailIsRegistered(e.target.value);
+                            // }
                         }
                         }
                     })}
                     })}
                     error={errors.shippingEmail && errors.shippingEmail.message as string}
                     error={errors.shippingEmail && errors.shippingEmail.message as string}

+ 1 - 1
src/app/(checkout)/checkout/_components/CheckoutCoupon/CheckoutCoupon.tsx

@@ -5,7 +5,7 @@ import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { formatCartDetail } from "@/utils/cartDetailTools";
-import {CartDetail} from "@/types/cart/type";
+import type {CartDetail} from "@/types/cart/type";
 
 
 
 
 
 

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

@@ -2,10 +2,10 @@
 
 
 import clsx from 'clsx';
 import clsx from 'clsx';
 import { useRouter } from 'next/navigation';
 import { useRouter } from 'next/navigation';
-import {AirwallexCartNumberElementType} from "@/lib/Airwallex/airwallexInit";
+import type {AirwallexCartNumberElementType} from "@/lib/Airwallex/airwallexInit";
 import PaypalButton from "./PaymentButton/PaypalButton";
 import PaypalButton from "./PaymentButton/PaypalButton";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
-import { PlaceOrderFunction,KlarnaOtherData } from "@/types/checkout/type";
+import type { PlaceOrderFunction,KlarnaOtherData } from "@/types/checkout/type";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";

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

@@ -3,12 +3,13 @@
 import {useState} from "react";
 import {useState} from "react";
 import { useRouter } from 'next/navigation';
 import { useRouter } from 'next/navigation';
 import Image from "next/image";
 import Image from "next/image";
-import {CartDetail, CartItem} from "@/types/cart/type";
+import type {CartDetail, CartItem} from "@/types/cart/type";
 import { useAddProduct } from "@utils/hooks/useAddToCart";
 import { useAddProduct } from "@utils/hooks/useAddToCart";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import {useConfig} from "@/utils/hooks/useConfig";
 import {useConfig} from "@/utils/hooks/useConfig";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
+import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import CommonModal from "@/components/theme/ui/CommonModal";
 import CommonModal from "@/components/theme/ui/CommonModal";
 
 
 
 
@@ -50,6 +51,11 @@ export default function CheckoutProducts({
         // 删除商品;购物车是否还有商品,没有的话:
         // 删除商品;购物车是否还有商品,没有的话:
         // 登录用户 跳转到空购物车页面或首页
         // 登录用户 跳转到空购物车页面或首页
         // 游客  清除cookie 跳转到首页或空购物车页面
         // 游客  清除cookie 跳转到首页或空购物车页面
+        const confirmres = await confirmDialog({
+            title: "Warning",
+            content: "Are you sure you want to delete this product.",
+        });
+        if(!confirmres) return; // 取消删除
         overlayLoading.start();
         overlayLoading.start();
         const res = await deleteProductFromCart(Number(cartItemId));
         const res = await deleteProductFromCart(Number(cartItemId));
         if(!res.error) {
         if(!res.error) {
@@ -79,6 +85,7 @@ export default function CheckoutProducts({
             if(qty < 1) {
             if(qty < 1) {
                 showToast("Quantity must be at least 1", "warning");
                 showToast("Quantity must be at least 1", "warning");
             } else {
             } else {
+                overlayLoading.start();
                 const res = await editProductQtyFromCart(Number(item.id),qty);
                 const res = await editProductQtyFromCart(Number(item.id),qty);
                 if(!res.error) {
                 if(!res.error) {
                     if(res.data) {
                     if(res.data) {
@@ -91,6 +98,7 @@ export default function CheckoutProducts({
                 } else {
                 } else {
                     showToast(res.msg, "danger");
                     showToast(res.msg, "danger");
                 }
                 }
+                overlayLoading.stop();
             }
             }
         } else {
         } else {
             showToast("This product can't change quantity.","warning");
             showToast("This product can't change quantity.","warning");
@@ -114,7 +122,7 @@ export default function CheckoutProducts({
                         const baseImage = product.baseImage ? JSON.parse(product.baseImage) : {};
                         const baseImage = product.baseImage ? JSON.parse(product.baseImage) : {};
                         return (
                         return (
                             <div className="relative w-22.5 h-30 flex-none" key={product.id}>
                             <div className="relative w-22.5 h-30 flex-none" key={product.id}>
-                                <Image fill src={baseImage.medium_image_url || ''} alt={product.name} />
+                                <Image fill className="object-cover" src={baseImage.medium_image_url || ''} alt={product.name} />
                             </div>
                             </div>
                         )
                         )
                     })}
                     })}
@@ -150,7 +158,7 @@ export default function CheckoutProducts({
                         return (
                         return (
                             <div key={product.id} className="w-full flex gap-2 mt-4 first:mt-0 pb-4 border-b-1 border-b-[#f0f0f0] last:border-none">
                             <div key={product.id} className="w-full flex gap-2 mt-4 first:mt-0 pb-4 border-b-1 border-b-[#f0f0f0] last:border-none">
                                 <div className="relative w-33 h-44 flex-none">
                                 <div className="relative w-33 h-44 flex-none">
-                                    <Image fill src={baseImage.medium_image_url || ''} alt={product.name} />
+                                    <Image fill className="object-cover" src={baseImage.medium_image_url || ''} alt={product.name} />
                                 </div>
                                 </div>
                                 <div className="w-full flex flex-col justify-between">
                                 <div className="w-full flex flex-col justify-between">
                                     <div className="w-full">
                                     <div className="w-full">

+ 7 - 5
src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx

@@ -11,19 +11,19 @@ import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import {usePaymentSDKContext} from "@/providers/PaymentSDKProvider";
 import {usePaymentSDKContext} from "@/providers/PaymentSDKProvider";
 import {airwallexManager} from "@/lib/Airwallex/useAirwallexCard";
 import {airwallexManager} from "@/lib/Airwallex/useAirwallexCard";
-import { 
+import type { 
     PlaceOrderResult,
     PlaceOrderResult,
     PlaceOrderFunction,
     PlaceOrderFunction,
     CreatePaymentInitiateVariables
     CreatePaymentInitiateVariables
 } from "@/types/checkout/type";
 } from "@/types/checkout/type";
-import { CartDetail,CartAddress } from "@/types/cart/type";
+import type { CartDetail,CartAddress } from "@/types/cart/type";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
-import {ShippingMethodCheckout,RefShippingMethodsHandle} from "./ShippingMethodCheckout";
-import {PaymentMethodCheckout,RefPaymentMethodsHandle} from "./PaymentMethodCheckout";
+import {ShippingMethodCheckout, type RefShippingMethodsHandle} from "./ShippingMethodCheckout";
+import {PaymentMethodCheckout,type RefPaymentMethodsHandle} from "./PaymentMethodCheckout";
 import LoadingPaymentMethod from "./LoadingPaymentMethod";
 import LoadingPaymentMethod from "./LoadingPaymentMethod";
-import {CheckoutAddress, RefCheckoutAddressHandle} from "./CheckoutAddress/CheckoutAssress";
+import {CheckoutAddress, type RefCheckoutAddressHandle} from "./CheckoutAddress/CheckoutAssress";
 import LoadingCheckoutPlaceOrder from "./LoadingCheckoutPlaceOrder";
 import LoadingCheckoutPlaceOrder from "./LoadingCheckoutPlaceOrder";
 import CheckoutPlaceOrder from "./CheckoutPlaceOrder";
 import CheckoutPlaceOrder from "./CheckoutPlaceOrder";
 import PaypalApplepayButton from "./PaymentButton/PaypalApplepayButton";
 import PaypalApplepayButton from "./PaymentButton/PaypalApplepayButton";
@@ -331,6 +331,7 @@ export default function CheckoutWrapper({
                     loginEmail={loginEmail}
                     loginEmail={loginEmail}
                     cartData={cartData}
                     cartData={cartData}
                     onSaveAddress={saveAddressCallback}
                     onSaveAddress={saveAddressCallback}
+                    onCartChange={onCartDataChange}
                 />
                 />
  
  
                   
                   
@@ -414,6 +415,7 @@ export default function CheckoutWrapper({
                 {(!paymentMethodLoading && !paymentMethodError) &&
                 {(!paymentMethodLoading && !paymentMethodError) &&
                 <PaymentMethodCheckout 
                 <PaymentMethodCheckout 
                     ref={methodPaymentRef}
                     ref={methodPaymentRef}
+                    grandTotal={cartData.grandTotal}
                     paymentMethods={paymentMethodDatas} 
                     paymentMethods={paymentMethodDatas} 
                     selectedPaymentMethod={selectedPaymentMethod}
                     selectedPaymentMethod={selectedPaymentMethod}
                     onPaymentMethodChange={handlePaymentMethodChange}
                     onPaymentMethodChange={handlePaymentMethodChange}

+ 6 - 2
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx

@@ -4,7 +4,7 @@ import { useState } from "react";
 import Image from "next/image";
 import Image from "next/image";
 import { useRouter } from 'next/navigation';
 import { useRouter } from 'next/navigation';
 import {useConfig} from "@/utils/hooks/useConfig";
 import {useConfig} from "@/utils/hooks/useConfig";
-import { OrderDetails } from "@/types/customer/type";
+import type { OrderDetails } from "@/types/customer/type";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import ProductsModalPendingOrder from "./ProductsModalPendingOrder";
 import ProductsModalPendingOrder from "./ProductsModalPendingOrder";
 import dynamic from "next/dynamic";
 import dynamic from "next/dynamic";
@@ -140,9 +140,13 @@ export default function ContinueToPayOrderInfo({
                 <div className="relative w-full box-border pr-9 mt-3.5">
                 <div className="relative w-full box-border pr-9 mt-3.5">
                     <div className="w-full box-border flex overflow-x-auto">
                     <div className="w-full box-border flex overflow-x-auto">
                         {productItems.map((product) => { 
                         {productItems.map((product) => { 
+                            const baseImage = JSON.parse(product.baseImage);
                             return(
                             return(
                                 <div key={product.id} className="relative flex-none w-16 h-21.5 mr-3 last:mr-0">
                                 <div key={product.id} className="relative flex-none w-16 h-21.5 mr-3 last:mr-0">
-                                    <Image src="https://cdn.asteriahair.com/media/catalog/product/6/_/6_3_2.png" alt="product" fill={true} />
+                                    <Image className="object-cover" fill
+                                        src={baseImage.medium_image_url}
+                                        alt={product.name} 
+                                    />
                                 </div>
                                 </div>
                             )
                             )
                         })}
                         })}

+ 17 - 9
src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx

@@ -11,9 +11,10 @@ import {
     airwallexManager,
     airwallexManager,
     useAirwallexCardState
     useAirwallexCardState
 } from "@/lib/Airwallex/useAirwallexCard";
 } from "@/lib/Airwallex/useAirwallexCard";
-import {RepayOrderPaymentMethodsData} from "@/types/checkout/type";
-import { OrderDetails } from "@/types/customer/type";
-import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
+import {getPaymentMethodLogo,getPaymentDescription} from "@/utils/paymentTools";
+import type {RepayOrderPaymentMethodsData} from "@/types/checkout/type";
+import type { OrderDetails } from "@/types/customer/type";
+import type {FetchGraphqlResult} from "@/types/graphqlFetch/type";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import PaypalButton from "../PaymentButton/PaypalButton";
 import PaypalButton from "../PaymentButton/PaypalButton";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
@@ -39,16 +40,16 @@ export function PaymentMethodContinueTpPay({
     const router = useRouter();
     const router = useRouter();
     const { showToast } = useCustomToast();
     const { showToast } = useCustomToast();
     const paymentMethodsData = use(paymentMethodsDataPromise);
     const paymentMethodsData = use(paymentMethodsDataPromise);
-    const paymentMethods =  paymentMethodsData.data.collectionRepayOrderPaymentMethods || [];
+    const paymentMethods =  paymentMethodsData.data?.collectionRepayOrderPaymentMethods || [];
 
 
     const { paymentRepay, createPaymentCallback } = usePlaceOrder();
     const { paymentRepay, createPaymentCallback } = usePlaceOrder();
 
 
     const {loadSdk} = usePaymentSDKContext();
     const {loadSdk} = usePaymentSDKContext();
-    const {store,currencies} = useConfig();
-    const currentCurrencyCode  = store.currentCurrency;
+    const {currencies, getCurrentCurrencyItem} = useConfig();
+    const currentCurrency = getCurrentCurrencyItem();
 
 
     const orderCurrency = currencies.find(item => item.code === orderDetail.orderCurrencyCode) || null;
     const orderCurrency = currencies.find(item => item.code === orderDetail.orderCurrencyCode) || null;
-    const orderCurrencyCode = orderCurrency?.code || currentCurrencyCode;
+    const orderCurrencyCode = orderCurrency?.code || currentCurrency.code;
 
 
 
 
     const airwallexCardState = useAirwallexCardState();
     const airwallexCardState = useAirwallexCardState();
@@ -422,6 +423,13 @@ export function PaymentMethodContinueTpPay({
             loadSdk(method);
             loadSdk(method);
         }
         }
     };
     };
+    const genetePaymentTips = (payment: string) => {
+        return getPaymentDescription({
+            payment: payment,
+            grandTotal: orderDetail.grandTotal,
+            symble: orderCurrency?.symbol ?? currentCurrency.symbol
+        })
+    };
 
 
 
 
     const airwallexCardElementShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
     const airwallexCardElementShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
@@ -440,12 +448,12 @@ export function PaymentMethodContinueTpPay({
                                 >
                                 >
                                     <p className="pr-2 flex-none relative">
                                     <p className="pr-2 flex-none relative">
                                         <Image className="block h-8 w-auto"
                                         <Image className="block h-8 w-auto"
-                                            src={item.icon || ''} alt={"Payment Method " + item.title}
+                                            src={getPaymentMethodLogo(item.method)} alt={"Payment Method " + item.title}
                                             width={32}
                                             width={32}
                                             height={32}
                                             height={32}
                                         />
                                         />
                                     </p>
                                     </p>
-                                    <p className="text-ly-12 text-[#666666] ml-1">{item.description}</p>
+                                    <p className="text-ly-12 text-[#666666] ml-1">{genetePaymentTips(item.method)}</p>
 
 
                                     <div className="absolute top-1/2 -translate-y-1/2 right-3 flex items-center">
                                     <div className="absolute top-1/2 -translate-y-1/2 right-3 flex items-center">
                                         <svg className="check w-4 h-4 hidden group-has-[:checked]:block" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect  x="1" y="1" width="14" height="14" rx="1"    fill="#000000" ></rect><path    stroke="rgba(255, 255, 255, 1)" strokeWidth="1.5"  strokeLinecap="square"  d="M5.3335 8L7.3335 10L11.3335 6"></path></svg>
                                         <svg className="check w-4 h-4 hidden group-has-[:checked]:block" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect  x="1" y="1" width="14" height="14" rx="1"    fill="#000000" ></rect><path    stroke="rgba(255, 255, 255, 1)" strokeWidth="1.5"  strokeLinecap="square"  d="M5.3335 8L7.3335 10L11.3335 6"></path></svg>

+ 6 - 2
src/app/(checkout)/checkout/_components/ContinueToPay/ProductsModalPendingOrder.tsx

@@ -2,7 +2,7 @@
 
 
 import Image from "next/image";
 import Image from "next/image";
 import CommonModal from "@/components/theme/ui/CommonModal";
 import CommonModal from "@/components/theme/ui/CommonModal";
-import {ProductsInOrderDetails, ProductItemAdditional} from "@/types/customer/type";
+import type {ProductsInOrderDetails, ProductItemAdditional} from "@/types/customer/type";
 
 
 
 
 // 根据products 计算总价和总个数
 // 根据products 计算总价和总个数
@@ -87,10 +87,14 @@ export default function ProductsModalPendingOrder({
       body={
       body={
         <div className="w-full px-4 pb-6">
         <div className="w-full px-4 pb-6">
             {products.map((product) => {
             {products.map((product) => {
+                const baseImage = JSON.parse(product.baseImage);
                 return (
                 return (
                     <div key={product.id} className="flex justify-between w-full box-border pb-4 border-b-1 border-[#f0f0f0] mt-4 last:border-0 last:pb-0 last:mt-0">
                     <div key={product.id} className="flex justify-between w-full box-border pb-4 border-b-1 border-[#f0f0f0] mt-4 last:border-0 last:pb-0 last:mt-0">
                         <div className="relative flex-none w-33 h-44 mr-2">
                         <div className="relative flex-none w-33 h-44 mr-2">
-                            <Image fill={true} src="https://cdn.asteriahair.com/media/catalog/product/6/_/6_3_2.png" alt={product.name} />
+                            <Image className="object-cover" fill
+                                src={baseImage.medium_image_url}
+                                alt={product.name} 
+                            />
                         </div>
                         </div>
                         <div className="flex-auto flex flex-col justify-between ">
                         <div className="flex-auto flex flex-col justify-between ">
                             <div>
                             <div>

+ 2 - 2
src/app/(checkout)/checkout/_components/GiftCard/CheckoutGiftCardGate.tsx

@@ -2,13 +2,13 @@
 
 
 import {useState, useRef, useEffect, useCallback} from "react";
 import {useState, useRef, useEffect, useCallback} from "react";
 import clsx from "clsx";
 import clsx from "clsx";
-import {CartDetail} from "@/types/cart/type";
+import type {CartDetail} from "@/types/cart/type";
 import {clientFetch} from "@/lib/restApiClient";
 import {clientFetch} from "@/lib/restApiClient";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
-import {GiftCardItem, GiftCardRespBody} from "@/types/api/gift/my-gift-cards";
+import type {GiftCardItem, GiftCardRespBody} from "@/types/api/gift/my-gift-cards";
 import CommonModal from "@/components/theme/ui/CommonModal";
 import CommonModal from "@/components/theme/ui/CommonModal";
 import { LoadingSpinner } from "@components/common/LoadingSpinner";
 import { LoadingSpinner } from "@components/common/LoadingSpinner";
 
 

+ 1 - 1
src/app/(checkout)/checkout/_components/LostInsurance/LostInsurance.tsx

@@ -6,7 +6,7 @@ import { useCustomToast } from "@/utils/hooks/useToast";
 import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
-import {CartDetail} from "@/types/cart/type";
+import type {CartDetail} from "@/types/cart/type";
 
 
 /**
 /**
  * 先通过 shippingInsuranceEnabled 判断网站是否开通丢件险功能
  * 先通过 shippingInsuranceEnabled 判断网站是否开通丢件险功能

+ 2 - 2
src/app/(checkout)/checkout/_components/PaymentButton/PaypalApplepayButton.tsx

@@ -14,9 +14,9 @@ import {
 } from "@paypal/react-paypal-js/sdk-v6";
 } from "@paypal/react-paypal-js/sdk-v6";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
-import { CartAddress } from "@/types/cart/type";
+import type { CartAddress } from "@/types/cart/type";
 import { useConfig } from "@/utils/hooks/useConfig";
 import { useConfig } from "@/utils/hooks/useConfig";
-import {ApplePayAddressContact} from "@/lib/Applepay/type";
+import type {ApplePayAddressContact} from "@/lib/Applepay/type";
 // import {getPaypalApplePayConfig, type PaypalApplePaySession} from "@/lib/Applepay/applepay";
 // import {getPaypalApplePayConfig, type PaypalApplePaySession} from "@/lib/Applepay/applepay";
 
 
 export default function PaypalApplepayButton({
 export default function PaypalApplepayButton({

+ 26 - 5
src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx

@@ -1,9 +1,11 @@
 "use client";
 "use client";
 
 
 import Image from "next/image";
 import Image from "next/image";
-import { Ref, useEffect, useImperativeHandle } from "react";
+import { type Ref, useEffect, useImperativeHandle } from "react";
 import { useForm, get } from "react-hook-form";
 import { useForm, get } from "react-hook-form";
-import { CheckoutPaymentMethod } from "@/types/checkout/type";
+import {useConfig} from "@/utils/hooks/useConfig";
+import {getPaymentMethodLogo,getPaymentDescription} from "@/utils/paymentTools";
+import type { CheckoutPaymentMethod } from "@/types/checkout/type";
 import AirwallexCardInput from "./PaymentMethodAdditional/AirwallexCardInput";
 import AirwallexCardInput from "./PaymentMethodAdditional/AirwallexCardInput";
 import {useAirwallexCardState} from "@/lib/Airwallex/useAirwallexCard";
 import {useAirwallexCardState} from "@/lib/Airwallex/useAirwallexCard";
 
 
@@ -20,14 +22,19 @@ export function PaymentMethodCheckout({
     ref,
     ref,
     paymentMethods,
     paymentMethods,
     selectedPaymentMethod,
     selectedPaymentMethod,
+    grandTotal,
     onPaymentMethodChange,
     onPaymentMethodChange,
 }: {
 }: {
     ref: Ref<RefPaymentMethodsHandle>;
     ref: Ref<RefPaymentMethodsHandle>;
     paymentMethods: CheckoutPaymentMethod[];
     paymentMethods: CheckoutPaymentMethod[];
     selectedPaymentMethod: string;
     selectedPaymentMethod: string;
+    grandTotal: number;
     onPaymentMethodChange: (value: string) => Promise<boolean>;
     onPaymentMethodChange: (value: string) => Promise<boolean>;
 }) {
 }) {
 
 
+    const {getCurrentCurrencyItem} = useConfig();
+    const currentCurrency = getCurrentCurrencyItem();
+    const currencySymbol = currentCurrency.symbol;
 
 
     const airwallexCardState = useAirwallexCardState();
     const airwallexCardState = useAirwallexCardState();
   
   
@@ -91,7 +98,13 @@ export function PaymentMethodCheckout({
 
 
     const airwallexOtherInfoShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
     const airwallexOtherInfoShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
 
 
-    
+    const genetePaymentTips = (payment: string) => {
+        return getPaymentDescription({
+            payment: payment,
+            grandTotal: grandTotal,
+            symble: currencySymbol
+        })
+    };
     return (<>
     return (<>
 
 
    
    
@@ -107,9 +120,17 @@ export function PaymentMethodCheckout({
                                     className="group box-border flex items-center relative bg-ly-lightgray p-3 rounded-sm has-checked:bg-white has-checked:border-1 has-checked:border-ly-green"
                                     className="group box-border flex items-center relative bg-ly-lightgray p-3 rounded-sm has-checked:bg-white has-checked:border-1 has-checked:border-ly-green"
                                 >
                                 >
                                     <p className="pr-2 flex-none relative">
                                     <p className="pr-2 flex-none relative">
-                                        <Image className="block h-8 w-auto" width={32} height={32} src={item.icon || ''} alt={"Payment Method " + item.title} />
+                                        {/**这里的width=32 height=32仅仅是让next.js有一个值计算图片的比例,并不能决定图片的渲染尺寸 */}
+                                        <Image className="block h-8 w-auto" 
+                                            width={32} 
+                                            height={32} 
+                                            src={getPaymentMethodLogo(item.method)} 
+                                            alt={"Payment Method " + item.title} 
+                                        />
+                                    </p>
+                                    <p className="text-ly-12 text-[#666666] ml-1">
+                                        {genetePaymentTips(item.method)}
                                     </p>
                                     </p>
-                                    <p className="text-ly-12 text-[#666666] ml-1">{item.description}</p>
 
 
                                     <div className="absolute top-1/2 -translate-y-1/2 right-3 flex items-center">
                                     <div className="absolute top-1/2 -translate-y-1/2 right-3 flex items-center">
                                         <svg className="check w-4 h-4 hidden group-has-[:checked]:block" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect  x="1" y="1" width="14" height="14" rx="1"    fill="#000000" ></rect><path    stroke="rgba(255, 255, 255, 1)" strokeWidth="1.5"  strokeLinecap="square"  d="M5.3335 8L7.3335 10L11.3335 6"></path></svg>
                                         <svg className="check w-4 h-4 hidden group-has-[:checked]:block" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect  x="1" y="1" width="14" height="14" rx="1"    fill="#000000" ></rect><path    stroke="rgba(255, 255, 255, 1)" strokeWidth="1.5"  strokeLinecap="square"  d="M5.3335 8L7.3335 10L11.3335 6"></path></svg>

+ 1 - 1
src/app/(checkout)/checkout/_components/PromotionsDetails.tsx

@@ -1,6 +1,6 @@
 "use client";
 "use client";
 
 
-import { CartDetail } from "@/types/cart/type";
+import type { CartDetail } from "@/types/cart/type";
 
 
 
 
 // 金额明细
 // 金额明细

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

@@ -8,7 +8,7 @@ import {useLoginModal} from "@/providers/LoginModalProvider";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
 import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
-import {CartDetail} from "@/types/cart/type";
+import type {CartDetail} from "@/types/cart/type";
 
 
 // earnableRewardPoints 判断是否开启下单送积分
 // earnableRewardPoints 判断是否开启下单送积分
 
 
@@ -65,7 +65,7 @@ export default function RewardPointsRedeemMoney({
                 if(res.mergeCartSuccess) { // 购物车合并成功
                 if(res.mergeCartSuccess) { // 购物车合并成功
                     if(res.cartAfterMerge) {
                     if(res.cartAfterMerge) {
                         const newCartDetail = formatCartDetail(res.cartAfterMerge);
                         const newCartDetail = formatCartDetail(res.cartAfterMerge);
-                        onPointsChange(newCartDetail, false);
+                        onPointsChange(newCartDetail,false);
                     }
                     }
                     
                     
                 } else { // 购物车合并失败
                 } else { // 购物车合并失败

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

@@ -1,10 +1,10 @@
 "use client";
 "use client";
 
 
 import clsx from "clsx";
 import clsx from "clsx";
-import { useEffect, useImperativeHandle, Ref  } from "react";
+import { useEffect, useImperativeHandle, type Ref  } from "react";
 import { useForm, get } from "react-hook-form";
 import { useForm, get } from "react-hook-form";
 
 
-import {CheckoutShippingRate} from "@/types/checkout/type";
+import type {CheckoutShippingRate} from "@/types/checkout/type";
 import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
 import CheckoutShippingMethodLoading from "./CheckoutShippingMethodLoading";
 
 
 
 

+ 1 - 1
src/app/(checkout)/checkout/_components/VipPlus/CheckoutVipPlus.tsx

@@ -2,7 +2,7 @@
 
 
 import {useState} from "react";
 import {useState} from "react";
 import { useRouter } from 'next/navigation';
 import { useRouter } from 'next/navigation';
-import {CartDetail} from "@/types/cart/type";
+import type {CartDetail} from "@/types/cart/type";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import {useSaveCheckoutCart} from "@/utils/hooks/useSaveCheckoutCart";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import { formatCartDetail } from "@/utils/cartDetailTools";

+ 33 - 0
src/app/(checkout)/checkout/cart/_components/BonusBuy.tsx

@@ -0,0 +1,33 @@
+"use client";
+
+import Image from "next/image";
+
+export function BonusBuy() { 
+
+
+    return (
+        <div className="box-border w-full">
+            <div className="text-ly-24 font-medium">Bonus Buy</div>
+            <div className="box-border w-full flex justify-between">
+                <div className="flex-none w-41.25 mt-4">
+                    <div className="relative flex-none w-41.25 h-42.5">
+                        <Image className="object-cover" fill
+                            src="https://cdn.asteriahair.com/media/catalog/product/1/_/1_1_5.png"
+                            alt="test" 
+                        />
+                    </div>
+                    <p className="mt-2 w-full line-clamp-2 text-ly-12 leading-5">
+                        USA Orders Only-DoublelayerSatin Silk Sleep Cap 
+                    </p>
+                    <button className="flex justify-between items-center box-border w-full mt-3 px-3 border-1 h-10 rounded-3xl">
+                        <span className="text-ly-14 font-bold">$9.99</span>
+                        <span className="flex items-center text-ly-12">
+                            <svg className="w-4 h-4" xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16" fill="none"><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.3333333333333333" strokeLinejoin="round" strokeLinecap="square"  d="M8.02001 3.33301L8.00781 12.6663"></path><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.3333333333333333" strokeLinejoin="round" strokeLinecap="square"  d="M3.3335 8L12.6668 8"></path></svg>
+                            ADD
+                        </span>
+                    </button>
+                </div>
+            </div>
+        </div>
+    );
+}

文件差異過大導致無法顯示
+ 427 - 0
src/app/(checkout)/checkout/cart/_components/CartWrapper.tsx


+ 47 - 0
src/app/(checkout)/checkout/cart/_components/EmptyCart.tsx

@@ -0,0 +1,47 @@
+"use client";
+
+import Image from "next/image";
+import { useRouter } from 'next/navigation';
+
+
+export function EmptyCart({isGuest}:{isGuest: boolean;}) { 
+
+    const router = useRouter();
+    return (
+        <div className="w-full box-border">
+            <div className="w-37.5 h-37.5 mt-6 mx-auto">
+                <Image src="/image/empty-cart.webp"
+                    width={150}
+                    height={150}
+                    alt="empty cart"
+                />
+            </div>
+            <p className="text-center text-ly-16">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">
+                    <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"
+                        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">
+                        <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">
+                            <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>
+                        
+                    </div>
+                </div>
+            </>}
+            
+        </div>
+    );
+}

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

@@ -0,0 +1,23 @@
+"use client";
+
+
+
+export function PromiseInCart() { 
+
+
+    return (
+        <div className="flex items-center justify-between w-full box-border bg-[#EAE9E1] h-10 px-5">
+            <div className="flex-none w-17.5 text-center text-ly-12 leading-4">
+                30 Days <br/>Free Return
+            </div>
+            <div className="relative text-center text-ly-12 leading-4 px-8">
+                <span className=" absolute left-0 top-1/2 -translate-y-1/2 h-5 border-[#cccccc] border-l-1"></span>
+                24-48 Hrs <br/>Free Fast Shipping
+                <span className=" absolute right-0 top-1/2 -translate-y-1/2 h-5 border-[#cccccc] border-l-1"></span>
+            </div>
+            <div className="flex-none w-17.5 text-center text-ly-12 leading-4">
+                Buy Now <br/>& Pay Later
+            </div>
+        </div>
+    );
+}

+ 288 - 0
src/app/(checkout)/checkout/cart/_components/UnpaidOrderSection.tsx

@@ -0,0 +1,288 @@
+"use client";
+
+import { useState } from "react";
+import dynamic from "next/dynamic";
+import Image from "next/image";
+import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
+import { useRouter } from 'next/navigation';
+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 type { OrderDetails, ProductsInOrderDetails } from "@/types/customer/type";
+import CommonModal from "@/components/theme/ui/CommonModal";
+const CountDown = dynamic(() => import("@/components/common/CountDown"), {
+  ssr: false,
+});
+export function UnpaidOrderSection({
+    orderDetail
+}: {
+    orderDetail: Omit<OrderDetails, 'addresses'>
+}) {
+    const { cancelOrder } = usePlaceOrder();
+    const router = useRouter();
+    const { showToast } = useCustomToast();
+    const {currencies,getCurrentCurrencyItem} = useConfig();
+    const currentCurrency = getCurrentCurrencyItem();
+    const currencySymbol = currentCurrency.symbol;
+    const orderCurrency = currencies.find(item => item.code === orderDetail.orderCurrencyCode) || null;
+    const orderSymbol = orderCurrency?.symbol || currencySymbol;
+
+    const [isShowUpaidOrder, setIsShowUpaidOrder] = useState(true);
+    const [showProductsModal, setShowProductsModal] = useState(false);
+    const [showDetails, setShowDetails] = useState(false);
+
+    const countDownEnd = (new Date(orderDetail.createdAt)).getTime() + 24*3600*1000;
+    const productsData: ProductsInOrderDetails[] = orderDetail.items.edges.map((edge) => {
+        return edge.node;
+    });
+    const totalItemTxt = orderDetail.totalItemCount > 1 ? `${orderDetail.totalItemCount} items` :  `${orderDetail.totalItemCount} item`;
+
+
+    const cancelOrderHandler = async () => {
+        const confirmRes = await confirmDialog({
+            title: "Warning",
+            content: "Would you want to cancel the unpaid order?",
+        });
+        if(!confirmRes) return;
+        overlayLoading.start();
+        const res = await cancelOrder({
+            orderId: Number(orderDetail._id),
+            userManual: true
+        });
+        overlayLoading.stop();
+        if(!res.error) {
+            showToast('Unpaid order has been canceled!', 'success');
+            setIsShowUpaidOrder(false);
+        } else {
+            showToast(res.msg, 'danger');
+        }
+    };
+
+    // 倒计时结束不再显示未支付订单(取消未支付订单)
+    const handleCountDownEnd = () => { 
+        setIsShowUpaidOrder(false);
+    };
+    const openProductsModal = () => {
+        setShowProductsModal(true);
+    };
+    const closeProductsModal = () => {
+        setShowProductsModal(false);
+    };
+    const toContinuePay = () => {
+        if(isShowUpaidOrder) {
+            router.push("/checkout/continuetopay?orderid=" + encodeURIComponent(orderDetail._id));
+        }
+    };
+
+    const openDetails = () => {
+        setShowDetails(true);
+    };
+    const closeDetails = () => {
+        setShowDetails(false);
+    };
+    if(!isShowUpaidOrder) return null; 
+    return (<div className="w-full box-border p-4">
+        <div className="relative w-full box-border p-3 bg-ly-lightgray rounded-lg">
+            <div className="flex items-center w-full">
+                Unpaid Order ({totalItemTxt})
+                <div className="ml-2">
+                    <CountDown
+                        endTime={countDownEnd}
+                        onCountDownEnd={handleCountDownEnd}
+                        boxClassName={"flex items-center gap-2 flex-none text-ly-14"}
+                        timerClassName={"flex items-center justify-center w-6 h-6 bg-ly-middlegreen rounded-xs text-ly-12 font-medium text-white"}
+                    />
+                </div>
+            </div>
+            <div className="w-full mt-3 overflow-x-scroll" onClick={openProductsModal}>
+                <div className="w-full flex gap-3">
+                    {productsData.map((product) => {
+                        const baseImage = JSON.parse(product.baseImage);
+                        return(
+                        <div className="relative w-16.5 h-22 flex-none" key={product.id}>
+                            <Image className="object-cover" fill
+                                src={baseImage.medium_image_url}
+                                alt={product.name}
+                            />
+                        </div>
+                        );
+                    })}
+
+                </div>
+            </div>
+
+            <div className=" flex items-center justify-between w-full mt-3">
+                <span className="text-ly-14 font-medium">Total: {orderSymbol}{orderDetail.grandTotal}</span>
+                <button className="flex-none flex justify-center items-center px-2 h-8 text-ly-14 text-white bg-ly-middlegreen rounded-md"
+                    onClick={toContinuePay}
+                >
+                    Pay Now
+                </button>
+            </div>
+
+            <button className="absolute top-3 right-3 w-6 h-6" onClick={cancelOrderHandler}>
+                <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.2" strokeLinejoin="round" strokeLinecap="round"  d="M7 7L17 17"></path><path    stroke="rgba(0, 0, 0, 1)" strokeWidth="1.2" strokeLinejoin="round" strokeLinecap="round"  d="M7 17L17 7"></path></svg>
+            </button>
+        </div>
+
+        <CommonModal
+            isOpen={showProductsModal}
+            onClose={closeProductsModal}
+            header={
+                <div className="w-full box-border p-4">
+                    <div className="w-full box-border flex justify-between">
+                        <span className="text-ly-16 font-medium">Order Summary (2 items)</span>
+                        <button className="w-6 h-6" onClick={closeProductsModal}>
+                            <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>
+            }
+            footer={
+                <div className="w-full box-border p-4 inset-shadow-gray-50">
+                    <div className="flex w-full justify-between items-center">
+                        <span className="text-ly-14 font-medium">Total:</span>
+                        <button onClick={openDetails} className="flex items-center text-ly-14 font-medium">
+                            {orderSymbol}{orderDetail.grandTotal}
+                            <svg className="flex-none w-6 h-6" 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="M18 9L12 15L6 9"></path></svg>
+                        </button>
+                    </div>
+                    <button className="w-full h-12 rounded-3xl mt-3 flex justify-center items-center bg-ly-middlegreen text-ly-16 font-medium text-white"
+                        onClick={toContinuePay}
+                    >
+                        Pay Now
+                    </button>
+                </div>
+            }
+            body={
+                <div className="relative box-border w-full h-auto px-4 pt-2 pb-8">
+                    {productsData.map((product) => {
+                        const baseImage = JSON.parse(product.baseImage);
+                        return(
+
+                            <div key={product.id} className="w-full flex gap-2 mt-4 first:mt-0 pb-4 border-b-1 border-b-[#f0f0f0] last:border-none">
+                                <div className="relative w-33 h-44 flex-none">
+                                    <Image fill className="object-cover" 
+                                        src={baseImage.medium_image_url}
+                                        alt={product.name}
+                                    />
+                                </div>
+                                <div className="w-full flex flex-col justify-between">
+                                    <div className="w-full">
+                                        <div className="w-full">
+                                            <div className="w-full line-clamp-2 text-ly-12 text-ly-placeholder leading-5">
+                                                {product.name}
+                                            </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}
+                                        </div>
+                                    </div>
+                                    <div className="flex items-center justify-between">
+                                        <div className="flex items-center gap-1.5">
+                                            <span className="text-ly-14 font-bold">{orderSymbol}{product.price}</span>
+                                            {/* <span className="text-ly-12 text-[#d3d3d3] line-through">$200.12</span> */}
+                                        </div>
+
+                                        <span className="flex-none text-ly-14">x{product.qtyOrdered}</span>
+
+                                    </div>
+                                </div>
+                            </div>
+                        );
+                    })}
+
+                            
+                </div>
+            }
+        />
+
+        {/**金额明细 */}
+        <CommonModal
+            zIndex="z-51"
+            isOpen={showDetails}
+            onClose={closeDetails}
+            contentClassName={"top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 w-82 bg-white rounded-xl"}
+            body={
+                <div className="relative box-border w-full h-auto p-4">
+                    <div className="flex justify-between items-center text-ly-14 font-bold">
+                        Promotion Details
+                        <button className="flex-none w-6 h-6" onClick={closeDetails}>
+                            <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 className="w-full mt-4">
+
+                        <div className="flex justify-between mt-3 first:mt-0">
+                            <p className="text-ly-12 leading-ly-20">Subtotal</p>
+                            <p className="text-ly-12 leading-ly-20">{orderSymbol}{orderDetail.subTotal}</p>
+                        </div>
+                        {orderDetail.rewardPointsEarned > 0 &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">You Earn</p>
+                                <p className="text-ly-12 leading-ly-20">{orderDetail.rewardPointsEarned} Reward Points</p>
+                            </div>
+                        }
+                        {orderDetail.shippingAmount !== 0 &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Shipping</p>
+                                <p className="text-ly-12 leading-ly-20">{orderSymbol}{orderDetail.shippingAmount}</p>
+                            </div>
+                        }
+                        {Number(orderDetail?.shippingInsuranceAmount) > 0 &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Shipping</p>
+                                <p className="text-ly-12 leading-ly-20">{orderSymbol}{Number(orderDetail.shippingInsuranceAmount)}</p>
+                            </div>
+                        }
+                        {orderDetail.taxAmount !== 0 &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Tax</p>
+                                <p className="text-ly-12 leading-ly-20">{orderSymbol}{orderDetail.taxAmount}</p>
+                            </div>
+                        }
+                        {(orderDetail.vipPlusAmount !== '0.00' && orderDetail.vipPlusAmount !== null)  &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Plus Fee</p>
+                                <p className="text-ly-12 leading-ly-20">{orderSymbol}{orderDetail.vipPlusAmount}</p>
+                            </div>
+                        }
+                        {orderDetail.discountAmount !== 0 &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Discount</p>
+                                <p className="text-ly-12 leading-ly-20">-{orderSymbol}{orderDetail.discountAmount}</p>
+                            </div>
+                        }
+                        {(orderDetail.giftcardAmount !== '0.00' && orderDetail.giftcardAmount !== null) &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Gift Card</p>
+                                <p className="text-ly-12 leading-ly-20">-{orderSymbol}{orderDetail.giftcardAmount}</p>
+                            </div>
+                        }
+                        {(orderDetail.vipDiscountAmount !== '0.00' && orderDetail.vipDiscountAmount !== null) &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">Plus Save</p>
+                                <p className="text-ly-12 leading-ly-20">-{orderSymbol}{orderDetail.vipDiscountAmount}</p>
+                            </div>
+                        }
+                        {orderDetail.rewardPointsAmount > 0 &&
+                            <div className="flex justify-between mt-3 first:mt-0">
+                                <p className="text-ly-12 leading-ly-20">You Redeem ({orderDetail.rewardPointsUsed} Reward Points)</p>
+                                <p className="text-ly-12 leading-ly-20">-{orderSymbol}{orderDetail.rewardPointsAmount}</p>
+                            </div>
+                        }
+
+                        <div className="flex justify-between mt-3 first:mt-0">
+                            <p className="text-ly-16 leading-ly-24">Grand Total</p>
+                            <p className="text-ly-16 leading-ly-24 font-bold">{orderSymbol}{orderDetail.grandTotal}</p>
+                        </div>
+                    </div>
+
+                            
+                </div>
+            }
+        />
+
+    </div>);
+}

+ 84 - 0
src/app/(checkout)/checkout/cart/page.tsx

@@ -0,0 +1,84 @@
+import { createHash } from "crypto";
+import {serverGraphqlFetch} from "@utils/bagisto/index";
+import { GET_CART_ITEM, GET_CUSTOMER_ORDERS } from "@/graphql";
+import { auth } from "@/utils/auth/auth-helper";
+import type { GetCartItemData } from "@/types/cart/type";
+import type { OrderListData, OrderListDataVariables, OrderDetails } from "@/types/customer/type";
+import TitleHeader from "@components/layout/navbar/TitleHeader";
+import { EmptyCart } from "./_components/EmptyCart";
+import { PromiseInCart } from "./_components/PromiseInCart";
+import { CartWrapper } from "./_components/CartWrapper";
+import { UnpaidOrderSection } from "./_components/UnpaidOrderSection";
+/**
+ * 购物车页 
+ * 登录用户的话,获取用户地址列表 从auth session 中获取email
+ * 获取billing地址和shipping地址
+ * 获取购物车数据
+ * 获取运输方式
+ * 获取支付方式
+ *  
+ * 
+ */
+export const dynamic = "force-dynamic";
+
+export default async function CheckoutPage() {
+
+    const session = await auth(); // 游客是null
+    const isGuest = session === null ? true : false;
+    let unpaidOrder: Omit<OrderDetails, 'addresses'> | null = null;
+    let unpaidOrderExpiried = false;
+    if(!isGuest) {
+        try {
+            const {data: ordersResponse } = await serverGraphqlFetch<OrderListData,OrderListDataVariables>({
+                query: GET_CUSTOMER_ORDERS,
+                variables: {
+                    first: 1,
+                    status: 'pending'
+                }
+            }); 
+            unpaidOrder = ordersResponse?.customerOrders?.edges[0]?.node ?? null;
+            if(unpaidOrder) {
+                const countDownEnd = (new Date(unpaidOrder.createdAt)).getTime() + 24*3600*1000;
+                unpaidOrderExpiried = countDownEnd <= (new Date()).getTime() ? true : false;
+            }
+            
+        } catch(err) {
+            console.error('get unpaid order:', err)
+        }
+    }
+    const {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({
+        query: GET_CART_ITEM
+    });
+
+    let cartDatahash = "";
+    const cartDetails = cartDetailsRes?.createReadCart?.readCart ?? null;
+    if(cartDetails) {
+        cartDatahash = createHash('sha256').update(JSON.stringify(cartDetails)).digest('hex');
+    }
+    const title = "My Cart" + (cartDetails?.itemsQty ? ' ('+cartDetails?.itemsQty+')' : '');
+    return (
+        <>
+            <TitleHeader title={title} />
+            <PromiseInCart />
+            {(unpaidOrder && !unpaidOrderExpiried) &&
+
+                <UnpaidOrderSection 
+                    orderDetail={unpaidOrder}
+                />
+
+            }
+            
+            {!cartDetails && 
+                <EmptyCart 
+                    isGuest={isGuest}
+                />
+            }
+            {cartDetails &&
+                <CartWrapper key={cartDatahash}
+                    cartDetailData={cartDetails}
+                />
+            }
+        </>
+    );
+  
+}

+ 31 - 31
src/app/(checkout)/checkout/continuetopay/page.tsx

@@ -1,16 +1,16 @@
 import {Suspense} from "react";
 import {Suspense} from "react";
-import { getServerSession } from "next-auth";
 import Link from "next/link";
 import Link from "next/link";
 import { redirect, RedirectType } from 'next/navigation';
 import { redirect, RedirectType } from 'next/navigation';
-import { authOptions } from "@utils/auth";
+import { auth } from "@/utils/auth/auth-helper";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
 import ContinueToPayOrderInfo from "../_components/ContinueToPay/ContinueToPayOrderInfo";
 import ContinueToPayOrderInfo from "../_components/ContinueToPay/ContinueToPayOrderInfo";
 import { GET_ORDER_DETAILS, GET_REPAYORDER_PAYMENT_METHODS } from "@/graphql";
 import { GET_ORDER_DETAILS, GET_REPAYORDER_PAYMENT_METHODS } from "@/graphql";
 // import { Country } from "@/types/checkout/type";
 // import { Country } from "@/types/checkout/type";
-import { OrderDetailsData } from "@/types/customer/type";
-import { RepayOrderPaymentMethodsData } from "@/types/checkout/type";
+import type { OrderDetailsData } from "@/types/customer/type";
+import type { RepayOrderPaymentMethodsData } from "@/types/checkout/type";
 import {PaymentMethodContinueTpPay} from "../_components/ContinueToPay/PaymentMethodContinueTpPay";
 import {PaymentMethodContinueTpPay} from "../_components/ContinueToPay/PaymentMethodContinueTpPay";
 import LoadingPaymentMethod from "../_components/LoadingPaymentMethod";
 import LoadingPaymentMethod from "../_components/LoadingPaymentMethod";
+import TitleHeader from "@components/layout/navbar/TitleHeader";
 
 
 // 只能存在一个待支付订单
 // 只能存在一个待支付订单
 export const dynamic = "force-dynamic";
 export const dynamic = "force-dynamic";
@@ -33,24 +33,24 @@ export const dynamic = "force-dynamic";
 // status: "pending"
 // status: "pending"
 // success: true
 // success: true
 export default async function ContinueToPay({searchParams}: {
 export default async function ContinueToPay({searchParams}: {
-  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+  searchParams: Promise<{ orderid: string }>;
 }) {
 }) {
     
     
-    const session = await getServerSession(authOptions);// 游客是null
+    const session = await auth();// 游客是null
     // 游客跳转首页
     // 游客跳转首页
     if(session === null) {
     if(session === null) {
         redirect('/', RedirectType.replace);
         redirect('/', RedirectType.replace);
     }
     }
 
 
     const query = await searchParams;
     const query = await searchParams;
-    const orderid = query?.orderid;
+    const orderid = query.orderid;
     const {data: orderDetailResponse, error: orderDetailError} = await serverGraphqlFetch<OrderDetailsData,{id:string}>({
     const {data: orderDetailResponse, error: orderDetailError} = await serverGraphqlFetch<OrderDetailsData,{id:string}>({
         query: GET_ORDER_DETAILS,
         query: GET_ORDER_DETAILS,
         variables: {
         variables: {
-            id: orderid as string
+            id: orderid
         }
         }
     }); 
     }); 
-    const orderDetailData = orderDetailResponse.customerOrder;
+    const orderDetailData = orderDetailResponse?.customerOrder;
     if(!orderDetailData  || orderDetailData?.status !== "pending")  {
     if(!orderDetailData  || orderDetailData?.status !== "pending")  {
         redirect('/', RedirectType.replace);
         redirect('/', RedirectType.replace);
     }
     }
@@ -63,8 +63,9 @@ export default async function ContinueToPay({searchParams}: {
         }
         }
     });
     });
       
       
-    if(orderDetailError) {
-        return (
+    return (<>
+        <TitleHeader title="Continue To Pay" />
+        {orderDetailError ?
             <div className="box-border px-4 w-full">
             <div className="box-border px-4 w-full">
             
             
                 
                 
@@ -80,27 +81,26 @@ export default async function ContinueToPay({searchParams}: {
                     Continue Shopping
                     Continue Shopping
                 </Link>
                 </Link>
             </div>
             </div>
-        );
-    }
-    return (
-        <div className="w-full pb-8">
-        
-            <ContinueToPayOrderInfo orderDetail={orderDetailData} />
-           
-            <div className="mt-6 box-border px-4">
-                <h3 className="text-ly-24 font-medium">Payment Method</h3>
+        :
+            <div className="w-full pb-8">
+            
+                <ContinueToPayOrderInfo orderDetail={orderDetailData} />
+            
+                <div className="mt-6 box-border px-4">
+                    <h3 className="text-ly-24 font-medium">Payment Method</h3>
 
 
+                </div>
+                <Suspense fallback={<LoadingPaymentMethod/>}> 
+                    <PaymentMethodContinueTpPay 
+                        orderId={orderDetailData._id}
+                        orderDetail={orderDetailData}
+                        paymentMethodsDataPromise={paymentMethodsResponse}
+                        defaultPaymentMethod={orderDetailData.payment}
+                    />
+                </Suspense>
+                
             </div>
             </div>
-            <Suspense fallback={<LoadingPaymentMethod/>}> 
-                <PaymentMethodContinueTpPay 
-                    orderId={orderDetailData._id}
-                    orderDetail={orderDetailData}
-                    paymentMethodsDataPromise={paymentMethodsResponse}
-                    defaultPaymentMethod={orderDetailData.payment}
-                />
-            </Suspense>
-            
-        </div>
-    );
+        }
+    </>);
   
   
 }
 }

+ 9 - 7
src/app/(checkout)/checkout/page.tsx

@@ -1,11 +1,11 @@
-import { getServerSession } from "next-auth";
-import { authOptions } from "@utils/auth";
+import { createHash } from "crypto";
 import { redirect, RedirectType } from 'next/navigation';
 import { redirect, RedirectType } from 'next/navigation';
 import {serverGraphqlFetch} from "@utils/bagisto/index";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
 import CheckoutWrapper from "./_components/CheckoutWrapper";
 import CheckoutWrapper from "./_components/CheckoutWrapper";
 import { GET_CART_ITEM } from "@/graphql";
 import { GET_CART_ITEM } from "@/graphql";
-
-import { GetCartItemData } from "@/types/cart/type";
+import { auth } from "@/utils/auth/auth-helper";
+import type { GetCartItemData } from "@/types/cart/type";
+import TitleHeader from "@components/layout/navbar/TitleHeader";
 
 
 /**
 /**
  * 进入结账页 
  * 进入结账页 
@@ -21,19 +21,21 @@ export const dynamic = "force-dynamic";
 
 
 export default async function CheckoutPage() {
 export default async function CheckoutPage() {
 
 
-    const session = await getServerSession(authOptions); // 游客是null
+    const session = await auth(); // 游客是null
 
 
 
 
     const {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({
     const {data: cartDetailsRes} = await serverGraphqlFetch<GetCartItemData>({
         query: GET_CART_ITEM
         query: GET_CART_ITEM
     });
     });
-    if(cartDetailsRes.createReadCart === null) {
+    if(!cartDetailsRes || cartDetailsRes.createReadCart === null) {
         redirect('/', RedirectType.replace);
         redirect('/', RedirectType.replace);
     }
     }
     const cartDetails = cartDetailsRes.createReadCart.readCart;
     const cartDetails = cartDetailsRes.createReadCart.readCart;
+    const cartDatahash = createHash('sha256').update(JSON.stringify(cartDetails)).digest('hex');
     return (
     return (
         <>
         <>
-            <CheckoutWrapper key={cartDetails.grandTotal}
+            <TitleHeader title="Checkout" />
+            <CheckoutWrapper key={cartDatahash}
                 cartDetailData={cartDetails}
                 cartDetailData={cartDetails}
                 loginEmail={session?.user?.email || ""}
                 loginEmail={session?.user?.email || ""}
             />
             />

+ 47 - 5
src/app/(checkout)/error.tsx

@@ -1,18 +1,60 @@
 "use client";
 "use client";
+import { useEffect } from 'react'
+import { useRouter } from 'next/navigation';
+import { signOut } from "next-auth/react";
+import { useAppDispatch } from "@/store/hooks";
+import { clearCart } from "@/store/slices/cart-slice";
+//https://nextjs.org/docs/app/api-reference/file-conventions/error
+export default function Error({
+  error, 
+  reset
+}: {
+  reset: () => void;
+  error: Error & { digest?: string };
+}) {
+  const router = useRouter();
+  const dispatch = useAppDispatch();
+  let msg = "There was an issue with our storefront. This could be a temporary issue, please try your action again.";
+  if(error.name === 'UnauthorizedError') {
+    msg = "Authorization Bearer token is required. Will redirect to Login page.";
+  }
+  const clickHandler = () => {
+      if(error.name === 'UnauthorizedError') {
+          router.push('/customer/login')
+      } else {
+        reset();
+      }
+  };
+  useEffect(() => {
+      if (error.name === "UnauthorizedError") {
+            signOut({
+              callbackUrl:"/customer/login",
+              redirect: false,
+            }).then(() => {
+                dispatch(clearCart());
+     
+                setTimeout(() => {
+                  router.push("/customer/login");
+                  router.refresh();
+                }, 100);
+            });
+
+      }
+    // Log the error to an error reporting service
+    console.error(error)
+  }, [error])
 
 
-export default function Error({ reset }: { reset: () => void }) {
   return (
   return (
     <div className="mx-auto my-4 flex max-w-xl flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black">
     <div className="mx-auto my-4 flex max-w-xl flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black">
       <h2 className="text-xl font-bold">Oh no!</h2>
       <h2 className="text-xl font-bold">Oh no!</h2>
       <p className="my-2">
       <p className="my-2">
-        There was an issue with our storefront. This could be a temporary issue,
-        please try your action again.
+        {msg}
       </p>
       </p>
       <button
       <button
         className="mx-auto mt-4 flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white hover:opacity-90"
         className="mx-auto mt-4 flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white hover:opacity-90"
-        onClick={() => reset()}
+        onClick={clickHandler}
       >
       >
-        Try Again
+        {error.name === 'UnauthorizedError' ? "To Login" : "Try Again"}
       </button>
       </button>
     </div>
     </div>
   );
   );

+ 3 - 5
src/app/(checkout)/layout.tsx

@@ -1,6 +1,6 @@
-import TitleHeader from "@components/layout/navbar/TitleHeader";
-import { ReactNode } from "react";
-export default async function RootLayout({
+
+import { type ReactNode } from "react";
+export default async function CheckoutLayout({
   children,
   children,
 }: {
 }: {
   children: ReactNode;
   children: ReactNode;
@@ -8,8 +8,6 @@ export default async function RootLayout({
   return (
   return (
     <>
     <>
 
 
-      <TitleHeader title="Checkout" />
-
       <div className="mx-auto w-full">
       <div className="mx-auto w-full">
         {children}
         {children}
       </div>
       </div>

+ 0 - 10
src/app/(public)/[page]/opengraph-image.tsx

@@ -1,10 +0,0 @@
-import OpenGraphImage from "@components/common/OpenGraphImage";
-import { getPage } from "@utils/bagisto";
-
-export default async function Image({ params }: { params: { page: string } }) {
-  const page = await getPage({ urlKey: params.page }) as { translation?: { metaTitle?: string; pageTitle?: string } }[];
-  const pageData = page && page.length > 0 ? page[0].translation : undefined;
-  const title = pageData?.metaTitle || pageData?.pageTitle;
-
-  return await OpenGraphImage({ title });
-}

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

@@ -1,7 +1,7 @@
 import { notFound } from "next/navigation";
 import { notFound } from "next/navigation";
 import Prose from "@components/theme/search/Prose";
 import Prose from "@components/theme/search/Prose";
 import { getPage } from "@utils/bagisto";
 import { getPage } from "@utils/bagisto";
-import { PageData } from "@/types/theme/theme-customization";
+import type { PageData } from "@/types/theme/theme-customization";
 
 
 
 
 export default async function Page({
 export default async function Page({

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

@@ -1,5 +1,5 @@
 "use client";
 "use client";
-import React from "react";
+
 import Link from "next/link";
 import Link from "next/link";
 import { Swiper, SwiperSlide } from "swiper/react";
 import { Swiper, SwiperSlide } from "swiper/react";
 import { FreeMode } from "swiper/modules";
 import { FreeMode } from "swiper/modules";

+ 2 - 2
src/app/(public)/_components/HomeBestSellers.tsx

@@ -1,11 +1,11 @@
-import React from "react";
+
 import { Swiper, SwiperSlide } from "swiper/react";
 import { Swiper, SwiperSlide } from "swiper/react";
 import { Navigation, Pagination } from "swiper/modules";
 import { Navigation, Pagination } from "swiper/modules";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
 // 导入Swiper基础样式
 // 导入Swiper基础样式
 import "swiper/css";
 import "swiper/css";
 import "swiper/css/navigation";
 import "swiper/css/navigation";
-import { HomeProduct } from "@/types/home/type";
+import type { HomeProduct } from "@/types/home/type";
 import Link from "next/link";
 import Link from "next/link";
 interface HomeBestSellersProps {
 interface HomeBestSellersProps {
   bestSellers: HomeProduct[];
   bestSellers: HomeProduct[];

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

@@ -12,7 +12,8 @@ import RecommendedProducts from "./RecommendedProducts";
 import CategoryTabSwiper  from "./CategoryTabSwiper ";
 import CategoryTabSwiper  from "./CategoryTabSwiper ";
 import "swiper/css";
 import "swiper/css";
 import "swiper/css/navigation";
 import "swiper/css/navigation";
-import {HomeApiData,HomeProduct } from "@/types/home/type"
+import type {HomeApiData,HomeProduct } from "@/types/home/type"
+import {CustomerService} from "@/components/common/CustomerService/CustomerService";
 // 定义组件入参类型
 // 定义组件入参类型
 interface HomeContentProps {
 interface HomeContentProps {
   homeData: HomeApiData;
   homeData: HomeApiData;
@@ -125,7 +126,7 @@ export default function HomeContent({ homeData }: HomeContentProps) {
    // 4. Recommended
    // 4. Recommended
   const recommendedSection = sections.find(sec => sec.name === "Recommended_For_You");
   const recommendedSection = sections.find(sec => sec.name === "Recommended_For_You");
   const recommended: HomeProduct[] = recommendedSection?.products ?? [];
   const recommended: HomeProduct[] = recommendedSection?.products ?? [];
-  return (
+  return (<>
     <div className="min-h-screen bg-white font-sans">
     <div className="min-h-screen bg-white font-sans">
       {/* 顶部Banner */}
       {/* 顶部Banner */}
       <div className="w-full relative">
       <div className="w-full relative">
@@ -152,5 +153,6 @@ export default function HomeContent({ homeData }: HomeContentProps) {
       <RecommendedProducts recommended={recommended} />
       <RecommendedProducts recommended={recommended} />
       <CategoryTabSwiper />
       <CategoryTabSwiper />
     </div>
     </div>
-  );
+    <CustomerService />
+  </>);
 }
 }

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

@@ -5,7 +5,7 @@ import { Navigation } from "swiper/modules";
 import "swiper/css";
 import "swiper/css";
 import "swiper/css/navigation";
 import "swiper/css/navigation";
 import Image from "next/image";
 import Image from "next/image";
-import { HomeSectionImage } from "@/types/home/type";
+import type { HomeSectionImage } from "@/types/home/type";
 interface middleBannerProps {
 interface middleBannerProps {
   middleBanner: HomeSectionImage[];
   middleBanner: HomeSectionImage[];
 }
 }

+ 2 - 2
src/app/(public)/_components/HomeNewArrival.tsx

@@ -1,11 +1,11 @@
-import React from "react";
+
 import { Swiper, SwiperSlide } from "swiper/react";
 import { Swiper, SwiperSlide } from "swiper/react";
 import { Navigation, Pagination } from "swiper/modules";
 import { Navigation, Pagination } from "swiper/modules";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
 // 导入Swiper基础样式
 // 导入Swiper基础样式
 import "swiper/css";
 import "swiper/css";
 import "swiper/css/navigation";
 import "swiper/css/navigation";
-import {HomeProduct} from "@/types/home/type"
+import type {HomeProduct} from "@/types/home/type"
 interface NewArrivalProps {
 interface NewArrivalProps {
   newArrival: HomeProduct[];
   newArrival: HomeProduct[];
 }
 }

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

@@ -6,7 +6,7 @@ import { Navigation } from "swiper/modules";
 import "swiper/css";
 import "swiper/css";
 import "swiper/css/navigation";
 import "swiper/css/navigation";
 import Image from "next/image";
 import Image from "next/image";
-import { HomeSectionImage } from "@/types/home/type";
+import type { HomeSectionImage } from "@/types/home/type";
 interface jinGangProps {
 interface jinGangProps {
   jinGang: HomeSectionImage[];
   jinGang: HomeSectionImage[];
 }
 }

+ 2 - 2
src/app/(public)/_components/MiddleImageSwiper.tsx

@@ -1,4 +1,4 @@
-import React from "react";
+
 import { Swiper, SwiperSlide } from "swiper/react";
 import { Swiper, SwiperSlide } from "swiper/react";
 import {  Pagination  } from "swiper/modules";
 import {  Pagination  } from "swiper/modules";
 import "swiper/css";
 import "swiper/css";
@@ -6,7 +6,7 @@ import "swiper/css/pagination";
 import "swiper/css/navigation";
 import "swiper/css/navigation";
 import "./css/swiper-custom.css";
 import "./css/swiper-custom.css";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
-import {HomeProduct,HomeSectionImage} from "@/types/home/type"
+import type {HomeProduct,HomeSectionImage} from "@/types/home/type"
 interface zeroWearProps {
 interface zeroWearProps {
   zeroWear: HomeProduct[];
   zeroWear: HomeProduct[];
   zeroWearImage:HomeSectionImage[]
   zeroWearImage:HomeSectionImage[]

+ 2 - 2
src/app/(public)/_components/RecommendedProducts.tsx

@@ -1,8 +1,8 @@
 "use client";
 "use client";
-import React, { useState, useEffect, useCallback } from "react";
+import { useState, useEffect, useCallback } from "react";
 import Image from "next/image";
 import Image from "next/image";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
-import {HomeProduct} from "@/types/home/type"
+import type {HomeProduct} from "@/types/home/type"
 interface recommendedProps {
 interface recommendedProps {
   recommended: HomeProduct[];
   recommended: HomeProduct[];
 }
 }

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

@@ -1,4 +1,4 @@
-import React from 'react';
+
 import { Swiper, SwiperSlide } from 'swiper/react';
 import { Swiper, SwiperSlide } from 'swiper/react';
 import 'swiper/css';
 import 'swiper/css';
 // import { Navigation, Pagination, Mousewheel, Keyboard } from 'swiper/modules';
 // import { Navigation, Pagination, Mousewheel, Keyboard } from 'swiper/modules';

+ 2 - 2
src/app/(public)/_components/Trending.tsx

@@ -1,4 +1,4 @@
-import React, { useState, useRef, useEffect } from "react";
+import { useState, useRef, useEffect } from "react";
 import { Swiper, SwiperSlide } from "swiper/react";
 import { Swiper, SwiperSlide } from "swiper/react";
 import { Scrollbar, Navigation } from "swiper/modules";
 import { Scrollbar, Navigation } from "swiper/modules";
 import "swiper/css";
 import "swiper/css";
@@ -6,7 +6,7 @@ import "swiper/css/scrollbar";
 import "swiper/css/navigation";
 import "swiper/css/navigation";
 import Image from "next/image";
 import Image from "next/image";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
 import { OpenAddToCartModalButton } from "@/components/common/AddToCartModal/OpenAddToCartModalButton";
-import {HomeProduct} from "@/types/home/type"
+import type {HomeProduct} from "@/types/home/type"
 interface trendingProps {
 interface trendingProps {
   trending: HomeProduct[];
   trending: HomeProduct[];
 }
 }

+ 1 - 1
src/app/(public)/category/[slug]/[id]/_components/CategoryDesc.tsx

@@ -1,6 +1,6 @@
 "use client";
 "use client";
 
 
-import { FC, useState, useEffect, useRef } from "react";
+import { type FC, useState, useEffect, useRef } from "react";
 
 
 interface CategoryDescProps {
 interface CategoryDescProps {
   title: string;
   title: string;

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

@@ -1,4 +1,4 @@
-import { Metadata } from "next";
+import { type Metadata } from "next";
 import { notFound } from "next/navigation";
 import { notFound } from "next/navigation";
 // import { isArray } from "@/utils/type-guards";
 // import { isArray } from "@/utils/type-guards";
 // import FilterList from "@components/theme/filters/FilterList";
 // import FilterList from "@components/theme/filters/FilterList";
@@ -24,7 +24,7 @@ import {
 import { newSortByFields } from "@utils/constants";
 import { newSortByFields } from "@utils/constants";
 // import { CategoryDetail } from "@components/theme/search/CategoryDetail";
 // import { CategoryDetail } from "@components/theme/search/CategoryDetail";
 import CategoryDesc from "./_components/CategoryDesc";
 import CategoryDesc from "./_components/CategoryDesc";
-import FaqList, { FaqItem } from "./_components/FaqList";
+import FaqList, { type FaqItem } from "./_components/FaqList";
 import { Suspense } from "react";
 import { Suspense } from "react";
 import FilterListSkeleton from "@components/common/skeleton/FilterSkeleton";
 import FilterListSkeleton from "@components/common/skeleton/FilterSkeleton";
 // import { TreeCategoriesResponse } from "@/types/theme/category-tree";
 // import { TreeCategoriesResponse } from "@/types/theme/category-tree";
@@ -35,7 +35,7 @@ import {
   newBuildProductFilters,
   newBuildProductFilters,
 } from "@utils/helper";
 } from "@utils/helper";
 import { serverGraphqlFetch } from "@/utils/bagisto/index";
 import { serverGraphqlFetch } from "@/utils/bagisto/index";
-import type{
+import type {
   // CategoryNode,
   // CategoryNode,
   CategorySingleResponse,
   CategorySingleResponse,
 } from "@/types/theme/category-tree";
 } from "@/types/theme/category-tree";
@@ -65,7 +65,7 @@ export async function generateMetadata({
 
 
   //   const categories = treeData?.treeCategories || [];
   //   const categories = treeData?.treeCategories || [];
   //   const categoryItem = findCategoryBySlug(categories, categorySlug);
   //   const categoryItem = findCategoryBySlug(categories, categorySlug);
-  const categoryItem = categoryData.category;
+  const categoryItem = categoryData?.category;
   if (!categoryItem) return notFound();
   if (!categoryItem) return notFound();
 
 
   const translation = categoryItem.translation;
   const translation = categoryItem.translation;
@@ -136,7 +136,7 @@ export default async function CategoryPage({
       id: Number(categoryId),
       id: Number(categoryId),
     },
     },
   });
   });
-  const categoryItem = categoryData.category;
+  const categoryItem = categoryData?.category;
   if (!categoryItem) return notFound();
   if (!categoryItem) return notFound();
 
 
   console.log("categoryItem +++++++++++", categoryItem);
   console.log("categoryItem +++++++++++", categoryItem);

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

@@ -133,9 +133,9 @@ export default function AccountVipPopup({
       try {
       try {
         setLoading(true);
         setLoading(true);
         const res = await clientFetch("/api/customer/growth-value");
         const res = await clientFetch("/api/customer/growth-value");
-        if (res.data.success) {
-          setVipGrothData(res.data.data);
-          console.log("vip成长值数据:", res.data.data);
+        if (res.success) {
+          setVipGrothData(res.data);
+          console.log("vip成长值数据:", res.data);
         }
         }
       } catch (error) {
       } catch (error) {
         console.error("获取vip成长值失败:", error);
         console.error("获取vip成长值失败:", error);

+ 13 - 9
src/app/(public)/customer/account/edit/page.tsx

@@ -2,15 +2,19 @@
 import React from "react";
 import React from "react";
 import { useState } from "react";
 import { useState } from "react";
 import { useEffect } from "react";
 import { useEffect } from "react";
-import { parseDate, CalendarDate, DateValue } from "@internationalized/date";
+import {
+  parseDate,
+  CalendarDate,
+  type DateValue
+} from "@internationalized/date";
 import Link from "next/link";
 import Link from "next/link";
 import { DatePicker } from "@heroui/date-picker";
 import { DatePicker } from "@heroui/date-picker";
 import CustomerAddressPhone from "../../address/new/_components/CustomerAddressPhone";
 import CustomerAddressPhone from "../../address/new/_components/CustomerAddressPhone";
 import { useForm, FormProvider,Controller } from "react-hook-form";
 import { useForm, FormProvider,Controller } from "react-hook-form";
 import { useMutation, useApolloClient } from "@apollo/client/react";
 import { useMutation, useApolloClient } from "@apollo/client/react";
-import { CustomerProfileResult } from "@components/customer/information/information";
+import type{ CustomerProfileResult } from "@/types/customer/information/information";
 import {GET_CUSTOMER_PROFILE,UPDATE_CUSTOMER_PROFILE} from "@/graphql";
 import {GET_CUSTOMER_PROFILE,UPDATE_CUSTOMER_PROFILE} from "@/graphql";
-import { UpdateCustomerProfileVars } from "@components/customer/information/information";
+import type{ UpdateCustomerProfileVars } from "@/types/customer/information/information";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import { useCustomToast } from "@/utils/hooks/useToast";
 const AccountEditPage = () => {
 const AccountEditPage = () => {
   interface UserForm {
   interface UserForm {
@@ -81,16 +85,16 @@ const AccountEditPage = () => {
     if (!mounted) return;
     if (!mounted) return;
 
 
     reset({
     reset({
-      firstname: profile.firstName,
-      lastname: profile.lastName,
-      email: profile.email,
-      phone: profile.phone,
-      date: profile.dateOfBirth || null,
+      firstname: profile?.firstName,
+      lastname: profile?.lastName,
+      email: profile?.email,
+      phone: profile?.phone,
+      date: profile?.dateOfBirth || null,
       current_password: "",
       current_password: "",
       password: "",
       password: "",
     });
     });
 
 
-    setHasBirthDate(!!profile.dateOfBirth);
+    setHasBirthDate(!!profile?.dateOfBirth);
   };
   };
 
 
   loadProducts();
   loadProducts();

+ 5 - 5
src/app/(public)/customer/account/mypoints/_components/GetPoints.tsx

@@ -1,6 +1,6 @@
 "use client";
 "use client";
 // import Link from "next/link";
 // import Link from "next/link";
-import React, { useState, useEffect } from "react";
+import { useState, useEffect } from "react";
 import { clientFetch } from "@/lib/restApiClient";
 import { clientFetch } from "@/lib/restApiClient";
 
 
 export default function GetPoints() {
 export default function GetPoints() {
@@ -12,8 +12,8 @@ export default function GetPoints() {
         setLoading(true);
         setLoading(true);
         const res = await clientFetch("/api/customer/reward-points/follow-status");
         const res = await clientFetch("/api/customer/reward-points/follow-status");
       
       
-        setTaskData(res.data.data);
-        console.log("积分任务数据:", res.data.data);
+        setTaskData(res.data);
+        console.log("积分任务数据:", res.data);
       } catch (error) {
       } catch (error) {
         console.error("获取任务状态失败:", error);
         console.error("获取任务状态失败:", error);
       } finally {
       } finally {
@@ -62,12 +62,12 @@ export default function GetPoints() {
         method: "POST",
         method: "POST",
         body: JSON.stringify({ categorys: item.type }),
         body: JSON.stringify({ categorys: item.type }),
       });
       });
-      if(getpointsres.data.success){
+      if(getpointsres.success){
         alert("Points claimed successfully!");
         alert("Points claimed successfully!");
       }
       }
       
       
       const res = await clientFetch("/api/customer/reward-points/follow-status");
       const res = await clientFetch("/api/customer/reward-points/follow-status");
-      setTaskData(res.data.data);
+      setTaskData(res.data);
     } catch (err) {
     } catch (err) {
       console.error("领取失败", err);
       console.error("领取失败", err);
       alert("Failed to claim points");
       alert("Failed to claim points");

+ 1 - 1
src/app/(public)/customer/account/mypoints/_components/PointsRule.tsx

@@ -1,5 +1,5 @@
 "use client";
 "use client";
-import React, { useState } from "react";
+import { useState } from "react";
 import PointsRuleModal from "@/components/customer/PointsRule";
 import PointsRuleModal from "@/components/customer/PointsRule";
 export default function PointsRule() {
 export default function PointsRule() {
   // 控制弹窗显隐的状态
   // 控制弹窗显隐的状态

+ 2 - 2
src/app/(public)/customer/account/pointlist/page.tsx

@@ -1,5 +1,5 @@
 "use client";
 "use client";
-import React, { useState, useEffect, useRef, useCallback } from "react";
+import { useState, useEffect, useRef, useCallback } from "react";
 import Link from "next/link";
 import Link from "next/link";
 import { clientFetch } from "@/lib/restApiClient";
 import { clientFetch } from "@/lib/restApiClient";
 
 
@@ -42,7 +42,7 @@ const PointsDetails = () => {
       console.log("接口返回:", resp);
       console.log("接口返回:", resp);
 
 
       if (resp.status === 200) {
       if (resp.status === 200) {
-        const formData = resp.data?.data || resp.data || { history: [] };
+        const formData = resp?.data || resp.data || { history: [] };
         const newList = formData.history || [];
         const newList = formData.history || [];
         // 测试数据
         // 测试数据
         //  const testList = [...newList, ...newList, ...newList, ...newList];
         //  const testList = [...newList, ...newList, ...newList, ...newList];

+ 1 - 1
src/app/(public)/customer/account/pointsreceivelist/page.tsx

@@ -1,4 +1,4 @@
-import React from "react";
+
 import Link from "next/link";
 import Link from "next/link";
 import RedeemGiftCard from "./_components/RedeemGiftCard"
 import RedeemGiftCard from "./_components/RedeemGiftCard"
 // 定义积分兑换项的类型
 // 定义积分兑换项的类型

+ 2 - 2
src/app/(public)/customer/address/eidt/[id]/page.tsx

@@ -51,7 +51,7 @@ const CustomerAddressEditPage = () => {
     });
     });
     const json = await res;
     const json = await res;
     console.log("提交成功,返回结果:", json);
     console.log("提交成功,返回结果:", json);
-    if (json?.data?.success) {
+    if (json?.success) {
       window.location.reload();
       window.location.reload();
     } else {
     } else {
       alert("error");
       alert("error");
@@ -112,7 +112,7 @@ const CustomerAddressEditPage = () => {
       console.log("【完整接口响应】", resp);
       console.log("【完整接口响应】", resp);
 
 
       // 自动兼容后端 99% 的返回格式
       // 自动兼容后端 99% 的返回格式
-      const formData = resp.data?.data || resp.data || {};
+      const formData = resp?.data || resp.data || {};
       console.log("✅ 回填数据:", formData);
       console.log("✅ 回填数据:", formData);
 
 
       reset(formData);
       reset(formData);

+ 1 - 1
src/app/(public)/customer/address/new/_components/NewAddressDefault.tsx

@@ -40,7 +40,7 @@ export default function NewAddressDefault({ countries }: { countries: any[] }) {
     });
     });
     const json = await res;
     const json = await res;
     console.log("提交成功,返回结果:", json);
     console.log("提交成功,返回结果:", json);
-    if(json?.status== 201){
+    if(json?.success){
         router.push("/customer/address/");
         router.push("/customer/address/");
     }else{
     }else{
       alert("error");
       alert("error");

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

@@ -1,4 +1,4 @@
-import React from "react";
+
 import Link from "next/link";
 import Link from "next/link";
 import DefaultAddress from "./_components/DefaultAddress";
 import DefaultAddress from "./_components/DefaultAddress";
 const CustomerAddressPage = async () => {
 const CustomerAddressPage = async () => {

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

@@ -0,0 +1,138 @@
+"use client";
+import { useState } from "react";
+import { useForm, type SubmitHandler } from "react-hook-form";
+import Link from "next/link";
+import {recoverPasswordAction} from "@/actions";
+import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
+import { EMAIL_REGEX } from '@/utils/constants';
+import InputText from "@/components/theme/ui/InputText";
+import { useCustomToast } from '@/utils/hooks/useToast';
+
+type ForgetPasswordInputs = {
+    email: string;
+};
+
+export default function ForgetPasswordForm() {
+    const { showToast } = useCustomToast();
+    const [emailValue, setEmailValue] = useState('');
+
+    const {
+        register,
+        handleSubmit,
+        formState: { errors },
+    } = useForm<ForgetPasswordInputs>({
+        mode: "onSubmit",
+        reValidateMode: "onChange",
+    });
+
+    const onSubmit: SubmitHandler<ForgetPasswordInputs> = async (data: {email: string;}) => {
+
+        overlayLoading.start();
+        const formData = new FormData();
+
+        formData.append("email", data.email);
+
+        try {
+            const result = await recoverPasswordAction({
+                email: data?.email,
+            });
+
+            // Show success/error API response
+            if (result.success) {
+                showToast(result.msg, "success");
+                setEmailValue(data.email);
+            } else {
+                showToast(result.msg, "danger");
+            }
+        } catch {
+            showToast("Something went wrong. Please try again later.", "danger");
+        } finally {
+            overlayLoading.stop();
+        }
+    };
+
+    return (<>{!emailValue ?
+
+        <div className="w-full box-border px-3">
+            <h2 className="text-center text-ly-18 font-bold">
+                Retrieve Your Password Here
+            </h2>
+            <p className="mt-6 text-center text-ly-16 leading-6">
+                Please enter your email address below. We will email you a verification code to reset your password.
+            </p>
+
+            <form
+                noValidate
+                className="w-full  mt-9"
+                onSubmit={handleSubmit(onSubmit)}
+            >
+                <label className="text-ly-12 block mb-1 font-semibold">
+                    *Email Address
+                </label>
+                <InputText
+                    type="email"
+                    placeholder="Enter your email address"
+                    {...register("email", {
+                        required: "Email is required",
+                        pattern: {
+                            value: EMAIL_REGEX,
+                            message: "Please enter a valid email address.",
+                        },
+                    })}
+                    error={errors?.email?.message ? errors.email.message : undefined}
+                />
+
+
+                <div className="w-full mt-9">
+                    <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"
+                    >
+                        Submit
+                    </button>
+                
+                    <Link className="mt-3 flex items-center justify-center w-full h-12 border-1  text-ly-16 rounded-3xl"
+                        href="/customer/login" 
+                        aria-label="Go to sign in page"
+                    >
+                        Back To Login
+                    </Link>
+                    
+                </div>
+            </form>
+        </div>
+    :
+        <div className="w-full box-border px-3 mt-6">
+
+            <h2 className="text-center text-ly-18 font-bold">
+                Password Reset
+            </h2>
+            <p className="mt-6 text-center text-ly-16 leading-6">
+                We sent an email to <br/>
+                <span className="underline text-[#00BAAD]">{emailValue}</span><br/>
+                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">
+                <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>
+            <p className="relative pl-4  mt-3 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>
+                If you have not received an email, please check your junk and spam folders.
+            </p>
+            <p className="relative pl-4  mt-3 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>
+                If you still don't receive the email after requesting a password reset, please contact our customer
+            </p>
+
+            <Link className="mt-6 flex items-center justify-center w-full h-12 bg-ly-middlegreen text-ly-16 text-white rounded-3xl"
+                href="/customer/login" 
+                aria-label="Go to sign in page"
+            >
+                Back To Login
+            </Link>
+
+        </div>
+    }</>);
+}

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

@@ -1,9 +1,6 @@
-import dynamic from "next/dynamic";
 import { generateMetadataForPage } from "@utils/helper";
 import { generateMetadataForPage } from "@utils/helper";
 import { staticSeo } from "@utils/metadata";
 import { staticSeo } from "@utils/metadata";
-import { Suspense } from "react";
-import ForgetSkeleton from "@components/common/skeleton/ForgetSkeleton";
-const ForgetPasswordForm = dynamic(() => import("@components/customer/ForgetPassword"));
+import ForgetPasswordForm from "./_components/ForgetPasswordForm";
 
 
 export const revalidate = 3600;
 export const revalidate = 3600;
 export async function generateMetadata() {
 export async function generateMetadata() {
@@ -11,9 +8,8 @@ export async function generateMetadata() {
 }
 }
 
 
 export default function ForgetPasswordPage() {
 export default function ForgetPasswordPage() {
-  return (
-    <Suspense fallback={<ForgetSkeleton />}>
-      <ForgetPasswordForm />
-    </Suspense>
-  );
+    return (<div className="w-full mt-6 box-border">
+        <ForgetPasswordForm />
+
+    </div>);
 }
 }

文件差異過大導致無法顯示
+ 420 - 0
src/app/(public)/customer/login/_components/LoginPageWrapper.tsx


+ 12 - 9
src/app/(public)/customer/login/page.tsx

@@ -1,18 +1,21 @@
-import LoginForm from "@components/customer/LoginForm";
 import { generateMetadataForPage } from "@utils/helper";
 import { generateMetadataForPage } from "@utils/helper";
 import { staticSeo } from "@utils/metadata";
 import { staticSeo } from "@utils/metadata";
-
-
-export const revalidate = 60;
+import {LoginPageWrapper} from "./_components/LoginPageWrapper";
 
 
 export async function generateMetadata() {
 export async function generateMetadata() {
   return generateMetadataForPage("", staticSeo.login);
   return generateMetadataForPage("", staticSeo.login);
 }
 }
 
 
-export default async function LoginPage() {
-  return (
-
-      <LoginForm />
+export default async function LoginPage({
+  searchParams
+}:{
+  searchParams: Promise<{
+      callbackUrl?: string;
+  }>;
+}) {
+  const { callbackUrl } = await searchParams;
+  return (<>
+      <LoginPageWrapper callbackUrl={callbackUrl || '/'} />
 
 
-  );
+  </>);
 }
 }

+ 1 - 1
src/app/(public)/customer/order/track/order_id/[id]/_components/TrackDetail.tsx

@@ -67,7 +67,7 @@ export default function TrackAddress({ address, id }: TrackAddressProps) {
         const res = await clientFetch(
         const res = await clientFetch(
           `/api/customer/orders/tracking?track_number=${id}`,
           `/api/customer/orders/tracking?track_number=${id}`,
         );
         );
-        let resData = res?.data;
+        let resData = res;
 
 
         // ========== 开发mock,上线务必删除这段赋值 ==========
         // ========== 开发mock,上线务必删除这段赋值 ==========
         resData = {
         resData = {

+ 1 - 0
src/app/(public)/customer/order/track/order_id/[id]/page.tsx

@@ -56,6 +56,7 @@ export default async function TrackOrderDetail({ params }: { params: Params }) {
   const res = await restApiFetch<OrderApiResponse>({
   const res = await restApiFetch<OrderApiResponse>({
     api: `/customer/orders/${orderId}`,
     api: `/customer/orders/${orderId}`,
     method: "GET",
     method: "GET",
+    isRoute: false
   });
   });
   const orderData: OrderData = res.body.data;
   const orderData: OrderData = res.body.data;
   const shippingAddr: AddressData = formatOrderAddress(orderData.shipping_address);
   const shippingAddr: AddressData = formatOrderAddress(orderData.shipping_address);

+ 1 - 0
src/app/(public)/customer/order/view/order_id/[id]/page.tsx

@@ -18,6 +18,7 @@ export default async function OrderDetail({ params }: { params: Params }) {
   const res = await restApiFetch<OrderApiResponse>({
   const res = await restApiFetch<OrderApiResponse>({
     api:`/customer/orders/${orderId}`,
     api:`/customer/orders/${orderId}`,
     method:"GET",
     method:"GET",
+    isRoute: false
   }
   }
   );
   );
 
 

+ 0 - 13
src/app/(public)/customer/register/page.tsx

@@ -1,13 +0,0 @@
-import RegistrationForm from "@components/customer/RegistrationForm";
-import { generateMetadataForPage } from "@utils/helper";
-import { staticSeo } from "@utils/metadata";
-
- export const revalidate = 60;
-
-export async function generateMetadata() {
-  return generateMetadataForPage("", staticSeo.register);
-}
-
-export default async function Register() {
-  return <RegistrationForm />;
-}

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

@@ -0,0 +1,155 @@
+"use client";
+
+import { useForm, type SubmitHandler } from "react-hook-form";
+import {RESET_PASSWORD} from "@/graphql";
+import { useRouter } from "next/navigation";
+import { useApolloClient } from "@apollo/client/react";
+import { useCustomToast } from '@/utils/hooks/useToast';
+import { confirmDialog } from "@/components/theme/ui/kernel/confirm/api";
+import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
+import PasswordInput from "@/components/theme/ui/PasswordInput";
+
+type ResetPasswordFormData= {
+    password: string;
+    passwordConfirmation: string;
+};
+
+export default function ResetPasswordWrapper({
+    token,
+    email
+}: {
+    token: string;
+    email: string;
+}) {
+    const router = useRouter();
+    const { showToast } = useCustomToast();
+    const apolloClient = useApolloClient();
+    const {
+        register,
+        handleSubmit,
+        formState: { errors },
+    } = useForm<ResetPasswordFormData>({
+        mode: "onChange",
+        reValidateMode: "onChange",
+        defaultValues: {
+            "password": '',
+            "passwordConfirmation": ''
+        }
+    });
+
+    const onSubmit: SubmitHandler<ResetPasswordFormData> = async (data: ResetPasswordFormData) => {
+
+        overlayLoading.start();
+        console.log(data);
+
+        apolloClient.mutate({
+            mutation: RESET_PASSWORD,
+            variables: {
+                token: token,
+                email: email,
+                password: data.password,
+                passwordConfirmation: data.passwordConfirmation
+            }
+        }).then((res) => {
+            overlayLoading.stop();
+            const resData = res.data?.createResetPassword?.resetPassword ?? null;
+            if(resData?.success) {
+
+                confirmDialog({
+                    title: "Notice",
+                    content: "Reset password successfully. Please to login.",
+                    noCancel: true,
+                }).then(() => {
+                    router.replace('/customer/login');
+                });
+            } else {
+                // const msg = (resData?.message ?? 'Reset password failed.') + ' You can try again.'
+                // confirmDialog({
+                //     title: "Notice",
+                //     content: msg,
+                // }).then(() => {
+                //     router.replace('/customer/forget-password');
+                // }).catch(() => {/**关闭 */});
+                showToast(resData?.message ?? 'Reset password failed.', "danger");
+            }
+        }).catch((err) => {
+            overlayLoading.stop();
+            showToast(err.message, "danger");
+        });
+    };
+
+    return (
+
+        <div className="w-full box-border px-3 mt-6">
+            <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">
+                        *New Password
+                    </label>
+                    <PasswordInput
+                        placeholder="New password"
+                        {...register("password", {
+                            required: "Password is required",
+                            minLength: {
+                                value: 6,
+                                message: "Be at least 6 characters long",
+                            },
+                            validate: (value: string) => {
+                                const pass=value.trim(); /*strip leading and trailing spaces*/
+                                const res = pass.length>0 && pass.length < 6;
+                                if(res) {
+                                    return 'Please enter 6 or more characters. Leading or trailing spaces will be ignored.';
+                                } 
+                                return true;
+                            },
+                        })}
+                        error={errors.password && errors.password.message}
+                    />
+                </div>
+                <div className="w-full mt-4">
+                    <label className="text-ly-12 block mb-1 font-semibold">
+                        *Confirm Password
+                    </label>
+
+                    <PasswordInput
+                        placeholder="Confirm password"
+                        {...register("passwordConfirmation", {
+                            required: "Confirm Password is required",
+                            minLength: {
+                                value: 6,
+                                message: "Be at least 6 characters long",
+                            },
+                            validate: (value: string, {password}) => {
+                                const pass = value.trim(); /*strip leading and trailing spaces*/
+                                const res = pass.length>0 && pass.length < 6;
+                                if(res) {
+                                    return 'Please enter 6 or more characters. Leading or trailing spaces will be ignored.';
+                                } 
+                                if(pass !== password) {
+                                    return 'The Confirm Passwords do not match the Password';
+                                }
+                                return true;
+                            },
+                        })}
+                        error={errors.passwordConfirmation && errors.passwordConfirmation.message}
+                    />
+                </div>
+
+
+                <div className="w-full mt-9">
+                    <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"
+                    >
+                        Submit
+                    </button>
+                    
+                </div>
+            </form>
+        </div>
+    );
+}

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

@@ -0,0 +1,28 @@
+import ResetPasswordWrapper from "./_components/ResetPasswordWrapper";
+
+export default async function ResetPasswordPage({
+    searchParams
+}: {
+    searchParams: Promise<{
+        email: string;
+        token: string;
+    }>;
+}) {
+    const resolvedParams = await searchParams; // resolvedParams可能是空对象
+    let errorMsg: string | null = null;
+    if(!resolvedParams.token || !resolvedParams.email) {
+        errorMsg = "token or email is undefined!"
+    }
+
+    return (<>
+        {errorMsg ?
+            <p className="mt-6 text-center text-ly-14">{errorMsg}</p>
+        :
+            <ResetPasswordWrapper 
+                token={resolvedParams.token}
+                email={resolvedParams.email}
+            />
+        }
+        
+    </>);
+}

+ 47 - 5
src/app/(public)/error.tsx

@@ -1,18 +1,60 @@
 "use client";
 "use client";
+import { useEffect } from 'react'
+import { useRouter } from 'next/navigation';
+import { signOut } from "next-auth/react";
+import { useAppDispatch } from "@/store/hooks";
+import { clearCart } from "@/store/slices/cart-slice";
+//https://nextjs.org/docs/app/api-reference/file-conventions/error
+export default function Error({
+  error, 
+  reset
+}: {
+  reset: () => void;
+  error: Error & { digest?: string };
+}) {
+  const router = useRouter();
+  const dispatch = useAppDispatch();
+  let msg = error.message || "There was an issue with our storefront. This could be a temporary issue, please try your action again.";
+  if(error.name === 'UnauthorizedError') {
+    msg = "Authorization Bearer token is required. Will redirect to Login page.";
+  }
+  const clickHandler = () => {
+      if(error.name === 'UnauthorizedError') {
+          router.push('/customer/login')
+      } else {
+        reset();
+      }
+  };
+  useEffect(() => {
+      if (error.name === "UnauthorizedError") {
+            signOut({
+              callbackUrl:"/customer/login",
+              redirect: false,
+            }).then(() => {
+                dispatch(clearCart());
+     
+                setTimeout(() => {
+                  router.push("/customer/login");
+                  router.refresh();
+                }, 100);
+            });
+
+      }
+    // Log the error to an error reporting service
+    console.error(error)
+  }, [error])
 
 
-export default function Error({ reset }: { reset: () => void }) {
   return (
   return (
     <div className="mx-auto my-4 flex max-w-xl flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black">
     <div className="mx-auto my-4 flex max-w-xl flex-col rounded-lg border border-neutral-200 bg-white p-8 md:p-12 dark:border-neutral-800 dark:bg-black">
       <h2 className="text-xl font-bold">Oh no!</h2>
       <h2 className="text-xl font-bold">Oh no!</h2>
       <p className="my-2">
       <p className="my-2">
-        There was an issue with our storefront. This could be a temporary issue,
-        please try your action again.
+        {msg}
       </p>
       </p>
       <button
       <button
         className="mx-auto mt-4 flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white hover:opacity-90"
         className="mx-auto mt-4 flex w-full items-center justify-center rounded-full bg-blue-600 p-4 tracking-wide text-white hover:opacity-90"
-        onClick={() => reset()}
+        onClick={clickHandler}
       >
       >
-        Try Again
+        {error.name === 'UnauthorizedError' ? "To Login" : "Try Again"}
       </button>
       </button>
     </div>
     </div>
   );
   );

+ 2 - 2
src/app/(public)/layout.tsx

@@ -1,8 +1,8 @@
-import { ReactNode } from "react";
+import { type ReactNode } from "react";
 import Footer from "@/components/layout/footer";
 import Footer from "@/components/layout/footer";
 import Navbar from "@/components/layout/navbar";
 import Navbar from "@/components/layout/navbar";
 
 
-export default async function RootLayout({
+export default async function PublicLayout({
   children,
   children,
 }: {
 }: {
   children: ReactNode;
   children: ReactNode;

+ 8 - 20
src/app/(public)/page.tsx

@@ -1,39 +1,27 @@
-// import { GET_THEME_CUSTOMIZATION } from "@/graphql";
-// import RenderThemeCustomization from "@components/home/RenderThemeCustomization";
-// import HomeImageBanner from "@components/home/HomeImageBanner";
-// import { ThemeCustomizationResponse } from "@/types/theme/theme-customization";
-// import { cachedGraphQLRequest } from "@/utils/hooks/useCache";
-// import { DatePicker } from "@heroui/date-picker";
 import HomeContent from "./_components/HomeContent";
 import HomeContent from "./_components/HomeContent";
 import { restApiFetch } from "@utils/bagisto/index";
 import { restApiFetch } from "@utils/bagisto/index";
-import {HomeApiData} from "@/types/home/type"
+import type {HomeApiData} from "@/types/home/type"
 export const revalidate = 3600;
 export const revalidate = 3600;
 
 
 
 
 export default async function Home() {
 export default async function Home() {
-  // const
-  // const {data} = await cachedGraphQLRequest<ThemeCustomizationResponse>(
-  //   "home",
-  //   GET_THEME_CUSTOMIZATION,
-  //   { first: 20 }
-  // );
+
 // 2. 服务端 fetch 请求我们自己的 Next.js API
 // 2. 服务端 fetch 请求我们自己的 Next.js API
 const res = await restApiFetch({
 const res = await restApiFetch({
   api: `/home`,
   api: `/home`,
   method: "GET",
   method: "GET",
-  isCookies: true, // 关键开关
-  headers: {
-    "X-Channel": "wap",
-  },
+  isRoute: false,
+  takeAuthorization: false,
+
 });
 });
-console.log("res----------------------",res);
+
 const homeData: HomeApiData = res.body.data;
 const homeData: HomeApiData = res.body.data;
 
 
   return (
   return (
     <>
     <>
       {/* <HomeImageBanner /> */}
       {/* <HomeImageBanner /> */}
-      {/* <DatePicker label={"Birth date"} labelPlacement={"outside"} /> */}
-      {/* <RenderThemeCustomization themeCustomizations={data?.themeCustomizations ?? {edges: []}} /> */}
+
+
       <HomeContent homeData={homeData} />
       <HomeContent homeData={homeData} />
     </>
     </>
   );
   );

+ 4 - 4
src/app/(public)/paymentresult/_components/CancelOrFailure.tsx

@@ -2,8 +2,8 @@
 
 
 import {use, useEffect, useRef} from "react";
 import {use, useEffect, useRef} from "react";
 import Link from "next/link";
 import Link from "next/link";
-import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
-import { OrderDetailsData } from "@/types/customer/type";
+import type {FetchGraphqlResult} from "@/types/graphqlFetch/type";
+import type { OrderDetailsData } from "@/types/customer/type";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import { usePlaceOrder } from "@/utils/hooks/usePlaceOrder";
 import Faqs from "./Faqs";
 import Faqs from "./Faqs";
 import { useAppDispatch } from "@/store/hooks";
 import { useAppDispatch } from "@/store/hooks";
@@ -19,7 +19,7 @@ export default function CancelOrFailure({
 }) { 
 }) { 
     const dispatch = useAppDispatch();
     const dispatch = useAppDispatch();
     const promiseRes = use(orderDetailPromise);
     const promiseRes = use(orderDetailPromise);
-    const orderDetailRes = promiseRes.data.customerOrder ?? null;
+    const orderDetailRes = promiseRes.data?.customerOrder ?? null;
     const {cancelOrder} = usePlaceOrder();
     const {cancelOrder} = usePlaceOrder();
     const orderId = orderDetailRes?._id;
     const orderId = orderDetailRes?._id;
     const orderStatus = orderDetailRes?.status;
     const orderStatus = orderDetailRes?.status;
@@ -68,7 +68,7 @@ export default function CancelOrFailure({
                         {isGuest ?
                         {isGuest ?
                             "payment cancelled! Your order has been cancelled."
                             "payment cancelled! Your order has been cancelled."
                         :
                         :
-                            "Payment cancelled! You can try again."
+                            "Payment cancelled! You can continue to pay."
                         }
                         }
                         
                         
                     </p>
                     </p>

+ 13 - 5
src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx

@@ -1,10 +1,11 @@
 "use client";
 "use client";
 
 
 import {use, useEffect} from "react";
 import {use, useEffect} from "react";
+import { redirect, RedirectType } from 'next/navigation';
 import Link from "next/link";
 import Link from "next/link";
 import Image from "next/image";
 import Image from "next/image";
-import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
-import { OrderDetailsData,ProductItemAdditional } from "@/types/customer/type";
+import type {FetchGraphqlResult} from "@/types/graphqlFetch/type";
+import type { OrderDetailsData,ProductItemAdditional } from "@/types/customer/type";
 import {deleteGuestCookieAction} from "@/actions";
 import {deleteGuestCookieAction} from "@/actions";
 import { useConfig } from "@utils/hooks/useConfig";
 import { useConfig } from "@utils/hooks/useConfig";
 import {getAddressFromOrderDetailAddressList} from "@/utils/orderDetailTools";
 import {getAddressFromOrderDetailAddressList} from "@/utils/orderDetailTools";
@@ -43,8 +44,11 @@ export default function OrderDetailWrapper({
 
 
     const {currencies} = useConfig();
     const {currencies} = useConfig();
     const promiseRes = use(orderDetailPromise);
     const promiseRes = use(orderDetailPromise);
-    const orderDetailRes = promiseRes.data.customerOrder;
-    const shippingAddress = getAddressFromOrderDetailAddressList(orderDetailRes.addresses,"order_shipping");
+    const orderDetailRes = promiseRes.data?.customerOrder ?? null;
+    if(!orderDetailRes) {
+        redirect('/', RedirectType.replace);
+    }
+    const shippingAddress = getAddressFromOrderDetailAddressList(orderDetailRes?.addresses,"order_shipping");
     let orderCreatedAt = 'Can\'t get order date';
     let orderCreatedAt = 'Can\'t get order date';
     if(orderDetailRes?.createdAt) {
     if(orderDetailRes?.createdAt) {
         const localDate = new Date(orderDetailRes.createdAt);
         const localDate = new Date(orderDetailRes.createdAt);
@@ -143,10 +147,14 @@ export default function OrderDetailWrapper({
 
 
             <div className="box-border px-4 w-full mt-6">
             <div className="box-border px-4 w-full mt-6">
                 {productItems.map((product) => {
                 {productItems.map((product) => {
+                    const baseImage = JSON.parse(product.baseImage);
                     return (
                     return (
                         <div key={product.id} className="w-full flex gap-2 pb-4 border-b-1 border-[#f0f0f0] last:border-0 last:pb-0">
                         <div key={product.id} className="w-full flex gap-2 pb-4 border-b-1 border-[#f0f0f0] last:border-0 last:pb-0">
                             <div className="relative w-33 h-44 flex-none">
                             <div className="relative w-33 h-44 flex-none">
-                                <Image fill src="https://cdn.alipearlhair.com/media/catalog/product/2/_/2_104_20.jpg" alt="logo" />
+                                <Image className="object-cover" fill
+                                    src={baseImage.medium_image_url}
+                                    alt={product.name} 
+                                />
                             </div>
                             </div>
                             <div className="flex-none w-50.75 flex flex-col justify-between">
                             <div className="flex-none w-50.75 flex flex-col justify-between">
                                 <div>
                                 <div>

+ 7 - 8
src/app/(public)/paymentresult/result/page.tsx

@@ -1,11 +1,10 @@
 import {Suspense} from "react";
 import {Suspense} from "react";
-import { getServerSession } from "next-auth";
 import { cookies } from 'next/headers';
 import { cookies } from 'next/headers';
 import { redirect, RedirectType } from 'next/navigation'
 import { redirect, RedirectType } from 'next/navigation'
-import { authOptions } from "@utils/auth";
+import { auth } from "@/utils/auth/auth-helper";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
 import {serverGraphqlFetch} from "@utils/bagisto/index";
 import {GET_ORDER_DETAILS} from "@/graphql";
 import {GET_ORDER_DETAILS} from "@/graphql";
-import { OrderDetailsData } from "@/types/customer/type";
+import type { OrderDetailsData } from "@/types/customer/type";
 import SuccessPageSkeleton from "../_components/SuccessPageSkeleton";
 import SuccessPageSkeleton from "../_components/SuccessPageSkeleton";
 import SkeletonCancelOrFailure from "../_components/SkeletonCancelOrFailure";
 import SkeletonCancelOrFailure from "../_components/SkeletonCancelOrFailure";
 import CancelOrFailure from "../_components/CancelOrFailure";
 import CancelOrFailure from "../_components/CancelOrFailure";
@@ -14,9 +13,9 @@ import { GUEST_CART_TOKEN } from "@/utils/constants";
 
 
 // 如果是游客:请求完订单详情后删除cookie里原来的carttoken cartid
 // 如果是游客:请求完订单详情后删除cookie里原来的carttoken cartid
 export default async function SuccessPage({searchParams}: {
 export default async function SuccessPage({searchParams}: {
-  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+  searchParams: Promise<{ orderId: string;awx_return_result:string; return_result: string;}>;
 })  {
 })  {
-    const session = await getServerSession(authOptions);// 游客是null
+    const session = await auth();// 游客是null
     const isGuest = session === null;
     const isGuest = session === null;
     const cookieStore = await cookies();
     const cookieStore = await cookies();
     // 登录用户cookie里没有GUEST_CART_ID
     // 登录用户cookie里没有GUEST_CART_ID
@@ -26,8 +25,8 @@ export default async function SuccessPage({searchParams}: {
     
     
     
     
     const query = await searchParams; // awx_return_result
     const query = await searchParams; // awx_return_result
-    const orderid = query?.orderId;
-    const paymentResult = query?.awx_return_result ?? query?.return_result; // 支付状态
+    const orderid = query.orderId;
+    const paymentResult = query.awx_return_result ?? query.return_result; // 支付状态
 
 
 
 
     if (!orderid) {
     if (!orderid) {
@@ -39,7 +38,7 @@ export default async function SuccessPage({searchParams}: {
     const orderDetailPromise = serverGraphqlFetch<OrderDetailsData,{id:string}>({
     const orderDetailPromise = serverGraphqlFetch<OrderDetailsData,{id:string}>({
         query: GET_ORDER_DETAILS,
         query: GET_ORDER_DETAILS,
         variables: {
         variables: {
-            id:  orderid as string
+            id:  orderid
         }
         }
     });
     });
 
 

+ 24 - 57
src/app/(public)/product/[...urlProduct]/page.tsx

@@ -1,17 +1,13 @@
 import { notFound } from "next/navigation";
 import { notFound } from "next/navigation";
 import { Suspense } from "react";
 import { Suspense } from "react";
-// import clsx from "clsx";
-// import type { ProductReviewList } from "@/types/products/productDetail";
-// import { getProductReviews } from "@utils/hooks/getProductReviews";
-// import { restApiFetch } from "@utils/bagisto/index";
 import {
 import {
   ProductDetailSkeleton,
   ProductDetailSkeleton,
   RelatedProductSkeleton,
   RelatedProductSkeleton,
 } from "@/components/common/skeleton/ProductSkeleton";
 } from "@/components/common/skeleton/ProductSkeleton";
 import { BASE_SCHEMA_URL, PRODUCT_TYPE } from "@/utils/constants";
 import { BASE_SCHEMA_URL, PRODUCT_TYPE } from "@/utils/constants";
 import { GET_PRODUCT_BY_URL_KEY } from "@/graphql";
 import { GET_PRODUCT_BY_URL_KEY } from "@/graphql";
-import { cachedProductRequest } from "@/utils/hooks/useCache";
-import {
+import {serverGraphqlFetch} from "@utils/bagisto/index";
+import type {
   ProductOption,
   ProductOption,
   SingleProductResponse,
   SingleProductResponse,
   ProductMediaType,
   ProductMediaType,
@@ -20,7 +16,6 @@ import { RelatedProductsSection } from "@components/catalog/product/RelatedProdu
 import { HeroCarouselShimmer } from "@components/common/slider";
 import { HeroCarouselShimmer } from "@components/common/slider";
 import { ProductMedia } from "@/app/(public)/product/_components/ProductMedia";
 import { ProductMedia } from "@/app/(public)/product/_components/ProductMedia";
 import { ProductInformation } from "@/app/(public)/product/_components/ProductInformation";
 import { ProductInformation } from "@/app/(public)/product/_components/ProductInformation";
-// import { ProductShortDescription } from "@/app/(public)/product/_components/ProductShortDescription";
 import { ProductDetail } from "@/app/(public)/product/_components/ProductDetail";
 import { ProductDetail } from "@/app/(public)/product/_components/ProductDetail";
 import ProductDescription from "../_components/ProductDescription";
 import ProductDescription from "../_components/ProductDescription";
 import SwitchButton from "../_components/SwitchButton";
 import SwitchButton from "../_components/SwitchButton";
@@ -31,30 +26,7 @@ import { formatFlexibleVariants } from "@/utils/variantTools";
 // export const dynamic = 'auto'
 // export const dynamic = 'auto'
 // // 'auto' | 'force-dynamic' | 'error' | 'force-static'
 // // 'auto' | 'force-dynamic' | 'error' | 'force-static'
 export const dynamic = "force-dynamic";
 export const dynamic = "force-dynamic";
-async function getSingleProduct(urlKey: string) {
-  try {
-    const { data: dataById } =
-      await cachedProductRequest<SingleProductResponse>(
-        // urlKey, // 产品名称
-        GET_PRODUCT_BY_URL_KEY, // gql查询语句
-        { urlKey: urlKey },
-      );
-
-    const product = dataById?.product || null;
 
 
-    return product;
-  } catch (error) {
-    if (error instanceof Error) {
-      console.error("Error fetching product:", {
-        message: error.message,
-        urlKey,
-        graphQLErrors: (error as unknown as Record<string, unknown>)
-          .graphQLErrors,
-      });
-    }
-    return null;
-  }
-}
 //动态路由中的参数 -- params
 //动态路由中的参数 -- params
 export default async function ProductPage({
 export default async function ProductPage({
   params,
   params,
@@ -64,20 +36,26 @@ export default async function ProductPage({
 }) {
 }) {
   const { urlProduct } = await params;
   const { urlProduct } = await params;
   const fullPath = urlProduct.join("/");
   const fullPath = urlProduct.join("/");
-  const product = await getSingleProduct(fullPath);
-  if (!product) return notFound();
-  // const allReviews: ProductReviewList = await getProductReviews(
-  //   String(product._id),
-  // );
-  // 3792
-  // const res = await restApiFetch({
-  // api: `/shop/products/${3792}/reviews?page=1&per_page=10&has_images=0&sort=all`,
-  // method: "GET",
-// });
-// const allReviews = res?.body?.data;
-console.log("productCustomerFeatures----------------------",product);
+  
+  const resProductData = await serverGraphqlFetch<SingleProductResponse,{urlKey: string;}>({
+      // urlKey, // 产品名称
+      query: GET_PRODUCT_BY_URL_KEY, // gql查询语句
+      variables:{ 
+        urlKey: fullPath 
+      },
+      takeAuthorization: false,
+      cache: 'no-cache'
+      
+  });
+  console.log("productCustomerFeatures----------------------",resProductData);
+  const product = resProductData.data?.product;
+  const fetchProductError = resProductData.error;
+  if (fetchProductError && fetchProductError.extensions.status === 404) return notFound();
+  if(!product) throw new Error(fetchProductError?.message ?? 'Get product data failed');
+ 
+
   const questionTotal = 73;
   const questionTotal = 73;
-  // const imageUrl = getImageUrl(product?.baseImageUrl, baseUrl, NOT_IMAGE);
+  
 
 
   // JSON-LD数据格式,便于搜索引擎识别页面信息,利于seo
   // JSON-LD数据格式,便于搜索引擎识别页面信息,利于seo
   const productJsonLd = {
   const productJsonLd = {
@@ -99,15 +77,7 @@ console.log("productCustomerFeatures----------------------",product);
 
 
   const flexibleVariants = formatFlexibleVariants(product?.flexibleVariants);
   const flexibleVariants = formatFlexibleVariants(product?.flexibleVariants);
 
 
-  // const reviews = Array.isArray(product?.reviews?.edges)
-  // ? product?.reviews.edges.map((e) => e.node)
-  // : [];
 
 
-  // const VariantImages = isArray(product?.variants?.edges)
-  //   ? product?.variants.edges.map(
-  //       (edge: { node: ProductVariantNode }) => edge.node,
-  //     )
-  //   : [];
 
 
   return (
   return (
     <>
     <>
@@ -135,19 +105,16 @@ console.log("productCustomerFeatures----------------------",product);
             shortDescription={product.shortDescription || ""}
             shortDescription={product.shortDescription || ""}
           />
           />
           <ShopServicePanel />
           <ShopServicePanel />
-          {/* <ProductShortDescription
-            shortDescription={product.shortDescription || ""}
-          /> */}
+
          <ProductDescription   htmlString={product?.feature || ""} />
          <ProductDescription   htmlString={product?.feature || ""} />
           <SwitchButton
           <SwitchButton
             productId={"3792"} //|| String(product._id)
             productId={"3792"} //|| String(product._id)
-            // reviewList={allReviews}
-            // reviewTotal={allReviews.length}
+
             questionTotal={questionTotal}
             questionTotal={questionTotal}
             questionList={["aaa"]}
             questionList={["aaa"]}
           />
           />
           <ProductDetail detail={product.description || ""} />
           <ProductDetail detail={product.description || ""} />
-          {/* <ProductReviewSection reviews={allReviews} productId={String(product._id)} /> */}
+          
         </Suspense>
         </Suspense>
         <Recommend/>
         <Recommend/>
       </div>
       </div>

+ 4 - 3
src/app/(public)/product/_components/ProductInformation.tsx

@@ -2,11 +2,11 @@
 
 
 import { useState, useMemo } from "react";
 import { useState, useMemo } from "react";
 import clsx from "clsx";
 import clsx from "clsx";
-import { ProductOption, ResolvedVariant } from "@/components/catalog/type";
+import type { ProductOption, ResolvedVariant } from "@/components/catalog/type";
 import { ProductAddToCart } from "@/app/(public)/product/_components/ProductAddToCart";
 import { ProductAddToCart } from "@/app/(public)/product/_components/ProductAddToCart";
 import { useCustomToast } from "@utils/hooks/useToast";
 import { useCustomToast } from "@utils/hooks/useToast";
 import { useAddProduct } from "@utils/hooks/useAddToCart";
 import { useAddProduct } from "@utils/hooks/useAddToCart";
-import { redirect, RedirectType } from "next/navigation";
+import { useRouter } from "next/navigation";
 import InstallmentPopup from "./popup/InstallmentPopup";
 import InstallmentPopup from "./popup/InstallmentPopup";
 import Guide from "./popup/Guide";
 import Guide from "./popup/Guide";
 import ProductText from "./ProductText";
 import ProductText from "./ProductText";
@@ -42,6 +42,7 @@ export function ProductInformation({
 }) {
 }) {
   const { appendProductToCart } = useAddProduct();
   const { appendProductToCart } = useAddProduct();
   const { showToast } = useCustomToast();
   const { showToast } = useCustomToast();
+  const router = useRouter();
 
 
   // 记录当前哪个选项组刚刚被点击(用于实现“点击组内全部可点”)
   // 记录当前哪个选项组刚刚被点击(用于实现“点击组内全部可点”)
   const [lastClickedOptionId, setLastClickedOptionId] = useState<number>(
   const [lastClickedOptionId, setLastClickedOptionId] = useState<number>(
@@ -213,7 +214,7 @@ export function ProductInformation({
             if(res.data) {
             if(res.data) {
                 const responseData = res.data;
                 const responseData = res.data;
                 if(responseData.success) {
                 if(responseData.success) {
-                    redirect('/checkout?step=address', RedirectType.push);
+                    router.push('/checkout?step=address');
                 }
                 }
             }
             }
         }
         }

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

@@ -7,7 +7,7 @@ import { Swiper, SwiperSlide } from "swiper/react";
 import { Pagination } from "swiper/modules";
 import { Pagination } from "swiper/modules";
 import type { Swiper as SwiperType } from "swiper";
 import type { Swiper as SwiperType } from "swiper";
 import type { PaginationOptions } from "swiper/types";
 import type { PaginationOptions } from "swiper/types";
-import { ProductMediaType } from "@/components/catalog/type";
+import type { ProductMediaType } from "@/components/catalog/type";
 import { baseUrl, getImageUrl } from "@/utils/constants";
 import { baseUrl, getImageUrl } from "@/utils/constants";
 import { useState, useRef } from "react";
 import { useState, useRef } from "react";
 
 

+ 0 - 3
src/app/(public)/product/_components/ProductReviewSection.tsx

@@ -12,7 +12,6 @@ interface ProductReviewSectionProps {
   onOpenBaseModal: () => void;
   onOpenBaseModal: () => void;
   activeTab: string;
   activeTab: string;
   onChangeTab: (tabKey: string) => void;
   onChangeTab: (tabKey: string) => void;
-  uuid:string;
 }
 }
 
 
 function ProductReviewSection({
 function ProductReviewSection({
@@ -23,7 +22,6 @@ function ProductReviewSection({
   activeTab,
   activeTab,
   onOpenBaseModal,
   onOpenBaseModal,
   onChangeTab,
   onChangeTab,
-  uuid,
 }: ProductReviewSectionProps) {
 }: ProductReviewSectionProps) {
   // const getAllreviews = await getProductReviews(productId)
   // const getAllreviews = await getProductReviews(productId)
   console.log("productId-------------:", productId, reviews);
   console.log("productId-------------:", productId, reviews);
@@ -41,7 +39,6 @@ function ProductReviewSection({
               onClose={closeModal}
               onClose={closeModal}
               activeTab={activeTab}
               activeTab={activeTab}
               onChangeTab={onChangeTab} // 关键:把切换tab的函数传给弹窗
               onChangeTab={onChangeTab} // 关键:把切换tab的函数传给弹窗
-              uuid={uuid}
             />
             />
           </>
           </>
         ) : (
         ) : (

+ 6 - 16
src/app/(public)/product/_components/SwitchButton.tsx

@@ -3,9 +3,10 @@ import { useState, useMemo, useEffect, useCallback } from "react";
 import ProductReviewSection  from "@/app/(public)/product/_components/ProductReviewSection";
 import ProductReviewSection  from "@/app/(public)/product/_components/ProductReviewSection";
 import Image from "next/image";
 import Image from "next/image";
 import ReviewAdd from "@/app/(public)/product/_components/review/ReviewAdd";
 import ReviewAdd from "@/app/(public)/product/_components/review/ReviewAdd";
-import FaqList, { FaqItem } from "./question/FaqList";
+import FaqList, { type FaqItem } from "./question/FaqList";
 import HairPhotoGallery from "./review/HairPhotoGallery";
 import HairPhotoGallery from "./review/HairPhotoGallery";
 import { clientFetch } from "@/lib/restApiClient";
 import { clientFetch } from "@/lib/restApiClient";
+import { getUuId } from "@/utils/helper";
 interface ReviewQuestionTabProps {
 interface ReviewQuestionTabProps {
   productId: string;
   productId: string;
   questionList: any[];
   questionList: any[];
@@ -53,18 +54,7 @@ export default function SwitchButton({
   // 自己通过接口拿到,不再由父组件传入
   // 自己通过接口拿到,不再由父组件传入
   const [reviewList, setReviewList] = useState<any[]>([]);
   const [reviewList, setReviewList] = useState<any[]>([]);
   const [reviewTotal, setReviewTotal] = useState<number>(0);
   const [reviewTotal, setReviewTotal] = useState<number>(0);
-    // 获取/生成访客UUID
-  const getUuId = () => {
-    const key = "GUEST_UUID";
-    let id = localStorage.getItem(key);
-    if (!id) {
-      id = crypto.randomUUID();
-      localStorage.setItem(key, id);
-    }
-    return id;
-  };
-  // 组件顶层,只生成一次uuid
-const [clientUuid] = useState(() => getUuId());
+
   useEffect(() => {
   useEffect(() => {
   if (!productId) return;
   if (!productId) return;
   // 1. 请求中断控制器,组件卸载时取消网络请求,防止卸载后执行setState
   // 1. 请求中断控制器,组件卸载时取消网络请求,防止卸载后执行setState
@@ -76,14 +66,15 @@ const [clientUuid] = useState(() => getUuId());
   
   
   const fetchReviews = async () => {
   const fetchReviews = async () => {
     try {
     try {
+      const clientUuid = getUuId();
       const res = await clientFetch(
       const res = await clientFetch(
         `/api/shop/products/${productId}/reviews?page=1&per_page=10&has_images=0&sort=all&client_id=${clientUuid}`,
         `/api/shop/products/${productId}/reviews?page=1&per_page=10&has_images=0&sort=all&client_id=${clientUuid}`,
         { signal } // 绑定中断信号
         { signal } // 绑定中断信号
       );
       );
       console.log('res------------------------------ccc:',res);
       console.log('res------------------------------ccc:',res);
 
 
-      if (res.data.success) {
-        setReviewList(res?.data?.data ?? []);
+      if (res.success) {
+        setReviewList(res?.data ?? []);
         setReviewTotal(res?.pagination?.total ?? 0);
         setReviewTotal(res?.pagination?.total ?? 0);
       }
       }
     } catch (err: any) {
     } catch (err: any) {
@@ -200,7 +191,6 @@ const [clientUuid] = useState(() => getUuId());
               onOpenBaseModal={openAllReviewModal}
               onOpenBaseModal={openAllReviewModal}
               activeTab={activeModalTab}
               activeTab={activeModalTab}
               onChangeTab={onTabChange}
               onChangeTab={onTabChange}
-              uuid={clientUuid}
             />
             />
           )
           )
         ) : isEmptyQuestion ? (
         ) : isEmptyQuestion ? (

+ 1 - 1
src/app/(public)/product/_components/question/ComponentsQuestionModal.tsx

@@ -1,6 +1,6 @@
 import { useState, useEffect } from "react";
 import { useState, useEffect } from "react";
 import { X } from "lucide-react";
 import { X } from "lucide-react";
-import { QaItem } from "./QAModal";
+import { type QaItem } from "./QAModal";
 // interface WigAnswer {
 // interface WigAnswer {
   // id: string;
   // id: string;
   // answer: string;
   // answer: string;

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

@@ -8,7 +8,7 @@ import { useCustomToast } from "@utils/hooks/useToast";
 import { useProductReview } from "@utils/hooks/useProductReview";
 import { useProductReview } from "@utils/hooks/useProductReview";
 import Image from "next/image";
 import Image from "next/image";
 
 
-import { CreateProductReviewInput } from "@/types/review";
+import type { CreateProductReviewInput } from "@/types/review";
 import { AddUploadImage } from "@components/common/icons/AddUploadImage";
 import { AddUploadImage } from "@components/common/icons/AddUploadImage";
 
 
 interface UploadImgItem {
 interface UploadImgItem {

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

@@ -3,7 +3,7 @@
 import { useState, useEffect } from "react";
 import { useState, useEffect } from "react";
 import clsx from "clsx";
 import clsx from "clsx";
 import { StarIcon } from "@heroicons/react/24/solid";
 import { StarIcon } from "@heroicons/react/24/solid";
-import { RatingTypes } from "@/components/catalog/type";
+import type { RatingTypes } from "@/components/catalog/type";
 
 
 
 
 export const AddRatingStar = ({
 export const AddRatingStar = ({

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

@@ -1,5 +1,5 @@
 "use client";
 "use client";
-import { useState } from "react";
+import { useState, type SubmitEvent } from "react";
 import { Textarea } from "@heroui/input";
 import { Textarea } from "@heroui/input";
 import { Button } from "@components/common/button/Button";
 import { Button } from "@components/common/button/Button";
 
 
@@ -18,7 +18,7 @@ export default function QuestionInputModal({
   const [errorMsg, setErrorMsg] = useState("");
   const [errorMsg, setErrorMsg] = useState("");
 
 
   // 提交处理
   // 提交处理
-  const handleSubmit = async (e: React.FormEvent) => {
+  const handleSubmit = async (e: SubmitEvent) => {
     e.preventDefault();
     e.preventDefault();
     setErrorMsg("");
     setErrorMsg("");
     const trimText = content.trim();
     const trimText = content.trim();

+ 0 - 0
src/app/(public)/product/_components/review/ReviewDetail.tsx


部分文件因文件數量過多而無法顯示