zgl 6 napja
szülő
commit
36c0c65835
100 módosított fájl, 2366 hozzáadás és 2342 törlés
  1. 13 8
      README.md
  2. 2 0
      package.json
  3. 16 0
      pnpm-lock.yaml
  4. BIN
      public/image/Logo.webp
  5. BIN
      public/image/empty-cart.webp
  6. BIN
      public/image/forget-password.webp
  7. BIN
      public/image/login.webp
  8. BIN
      public/image/payment/applepay.png
  9. BIN
      public/image/payment/cash-icon.png
  10. BIN
      public/image/payment/credit-card.png
  11. 1 0
      public/image/payment/paypal-logo.svg
  12. 1 0
      public/image/plus-crown.svg
  13. 11 9
      src/actions/addProductToCartAction.ts
  14. 1 1
      src/actions/deleteCartProductAction.ts
  15. 2 1
      src/actions/index.ts
  16. 1 1
      src/actions/mergeCartAction.ts
  17. 47 0
      src/actions/recoverPasswordAction.ts
  18. 30 12
      src/app/(checkout)/checkout/_components/CheckoutAddress/BillingAddressCheckout.tsx
  19. 65 8
      src/app/(checkout)/checkout/_components/CheckoutAddress/CheckoutAssress.tsx
  20. 10 1
      src/app/(checkout)/checkout/_components/CheckoutAddress/ShippingAddressCheckout.tsx
  21. 10 2
      src/app/(checkout)/checkout/_components/CheckoutProducts/CheckoutProducts.tsx
  22. 2 0
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  23. 5 1
      src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx
  24. 14 6
      src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx
  25. 5 1
      src/app/(checkout)/checkout/_components/ContinueToPay/ProductsModalPendingOrder.tsx
  26. 24 3
      src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx
  27. 1 1
      src/app/(checkout)/checkout/_components/RewardPointsRedeemMoney/RewardPointsRedeemMoney.tsx
  28. 33 0
      src/app/(checkout)/checkout/cart/_components/BonusBuy.tsx
  29. 427 0
      src/app/(checkout)/checkout/cart/_components/CartWrapper.tsx
  30. 47 0
      src/app/(checkout)/checkout/cart/_components/EmptyCart.tsx
  31. 23 0
      src/app/(checkout)/checkout/cart/_components/PromiseInCart.tsx
  32. 288 0
      src/app/(checkout)/checkout/cart/_components/UnpaidOrderSection.tsx
  33. 84 0
      src/app/(checkout)/checkout/cart/page.tsx
  34. 27 26
      src/app/(checkout)/checkout/continuetopay/page.tsx
  35. 6 2
      src/app/(checkout)/checkout/page.tsx
  36. 2 4
      src/app/(checkout)/layout.tsx
  37. 2 2
      src/app/(public)/category/[slug]/[id]/page.tsx
  38. 138 0
      src/app/(public)/customer/forget-password/_components/ForgetPasswordForm.tsx
  39. 5 9
      src/app/(public)/customer/forget-password/page.tsx
  40. 422 0
      src/app/(public)/customer/login/_components/LoginPageWrapper.tsx
  41. 12 9
      src/app/(public)/customer/login/page.tsx
  42. 0 13
      src/app/(public)/customer/register/page.tsx
  43. 155 0
      src/app/(public)/customer/reset-password/_components/ResetPasswordWrapper.tsx
  44. 28 0
      src/app/(public)/customer/reset-password/page.tsx
  45. 1 1
      src/app/(public)/error.tsx
  46. 1 1
      src/app/(public)/layout.tsx
  47. 3 14
      src/app/(public)/page.tsx
  48. 2 2
      src/app/(public)/paymentresult/_components/CancelOrFailure.tsx
  49. 11 3
      src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx
  50. 4 4
      src/app/(public)/paymentresult/result/page.tsx
  51. 23 58
      src/app/(public)/product/[...urlProduct]/page.tsx
  52. 0 3
      src/app/(public)/product/_components/ProductReviewSection.tsx
  53. 3 13
      src/app/(public)/product/_components/SwitchButton.tsx
  54. 3 8
      src/app/(public)/product/_components/review/ReviewDetail.tsx
  55. 5 4
      src/app/(public)/product/_components/review/ReviewModal.tsx
  56. 16 101
      src/app/(public)/search/page.tsx
  57. 36 0
      src/app/api/customer/check-email/route.ts
  58. 42 1
      src/app/api/revalidate/route.ts
  59. 3 4
      src/app/layout.tsx
  60. 0 123
      src/components/catalog/product/ProductMoreDetail.tsx
  61. 5 5
      src/components/catalog/product/RelatedProductsSection.tsx
  62. 0 71
      src/components/catalog/product/VariantSelector.tsx
  63. 0 204
      src/components/catalog/review/ReviewDetail.tsx
  64. 1 1
      src/components/catalog/type.ts
  65. 9 15
      src/components/common/LoginModal/LoginModal.tsx
  66. 0 116
      src/components/common/form/Input.tsx
  67. 0 32
      src/components/common/skeleton/CategoryCarouselSkeleton.tsx
  68. 0 137
      src/components/customer/ForgetPassword.tsx
  69. 0 183
      src/components/customer/LoginForm.tsx
  70. 0 214
      src/components/customer/RegistrationForm.tsx
  71. 0 161
      src/components/home/CategoryCarousel.tsx
  72. 0 84
      src/components/home/ProductCarousel.tsx
  73. 0 68
      src/components/home/RenderThemeCustomization.tsx
  74. 0 146
      src/components/home/ThreeItemGrid.tsx
  75. 0 32
      src/components/theme/product-carousel/index.tsx
  76. 0 52
      src/components/theme/product-carousel/theme.tsx
  77. 16 2
      src/components/theme/ui/CommonModal.tsx
  78. 4 1
      src/components/theme/ui/Select.tsx
  79. 0 58
      src/components/theme/ui/grid/ThreeItemGrid.tsx
  80. 0 23
      src/components/theme/ui/grid/ThreeItemsSkeleton.tsx
  81. 6 4
      src/graphql/checkout/mutations/CreateCancelOrder.ts
  82. 23 0
      src/graphql/customer/mutations/ResetPassword.ts
  83. 2 1
      src/graphql/customer/mutations/index.ts
  84. 111 0
      src/graphql/customer/query/GetCustomerOrders.ts
  85. 1 0
      src/graphql/customer/query/GetOrderDetails.ts
  86. 2 1
      src/graphql/customer/query/index.ts
  87. 1 1
      src/graphql/index.ts
  88. 0 17
      src/graphql/types/index.ts
  89. 0 170
      src/graphql/types/product.types.ts
  90. 0 47
      src/lib/graphql-fetch.ts
  91. 1 1
      src/lib/restApiClient.ts
  92. 2 0
      src/providers/LoginModalProvider.tsx
  93. 1 1
      src/server-service/guestCartTokenService.ts
  94. 1 1
      src/types/cart/type.ts
  95. 0 31
      src/types/category/type.ts
  96. 1 0
      src/types/checkout/type.ts
  97. 25 1
      src/types/customer/order.ts
  98. 40 0
      src/types/customer/type.ts
  99. 1 1
      src/types/graphqlFetch/type.ts
  100. 0 0
      src/types/theme/theme-customization.ts

+ 13 - 8
README.md

@@ -278,15 +278,15 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 
 ## TODO
 需要做的工作:
-1. 注册成功后需要走next-auth的登录流程
-2. 登录成功后需要合并购物车(在服务端完成) -- 待定
+1. 注册成功后需要走next-auth的登录流程 -- 完成
+2. 登录成功后需要合并购物车(在服务端完成) -- 前端合并,已完成
 
 3. createCart 接口报错 -- 已解决
 4. 由于apollo client缓存策略以及graphql代理接口问题导致获取不到数据 -- 已解决
      - 3和4的问题是相互关联的
 
 5. 尝试升级apollo client版本到v4 -- 完成
-6. jwt-cookie 放弃base64编码,尝试改为加密解密
+6. jwt-cookie 放弃base64编码,尝试改为加密解密 -- 不用了,已删除jwt-cookie 文件
 
 7. 登录状态点击退出登录后GetCartItem接口会报错 -- 待处理
   - 错误信息:
@@ -312,10 +312,10 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 13. 详情页缓存策略(不要缓存吧)
 14. getSession  过期提示重新登录;token校验(见第47条)
 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  -- 已完成
 18. 需要一个遮挡整个视口的loading组件  -- 已完成
-19. 结账页地址模块还差两个功能:登录用户从地址列表选择地址;未登录用户输入邮箱后判断是否注册过,如果注册过让用户登录;
+19. 结账页地址模块,登录用户从地址列表选择地址;--- 已完成
      - 电话区号切换还有问题
 20. 登录用户结账页支付失败后要跳转到支付失败页面提醒用户可以再次支付 -- 待定
 21. 游客结账页支付失败或者取消支付,先调paymentCallback 再调cancelOrder。但是不删除cookie里之前的cartToken --- 完成
@@ -332,7 +332,8 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
 30. 货币列表,国家列表等关于商城配置的数据采用全局ConfigProvider管理; -- 已完成
 31. 当前货币等采用redux管理;-- 废弃,已改为全局ConfigProvider管理
 32. 当前货币切换功能      --- 已完成
-33. 登录用户购物车页获取未支付订单
+33. 登录用户购物车页获取未支付订单 --- 已完成
+
 34. 结账页下单成功后清空redux购物车数据导致结账页重新渲染问题  -- 已解决
     - 通过修改结账页数据初始化逻辑解决
 35. 结账页默认设置运输方式问题(导致结算联动风暴)
@@ -355,7 +356,7 @@ useEffect 只会在客户端执行,具体是在浏览器绘制后执行,服
     对于需要token的接口:登录用户校验token; 游客只需要检查有没有token.
     ---- 47 条 请求方法已经扩展是否携带token的参数,但是不再添加对于token的有效性校验,以后端接口返回的错误为依据判断有效性
 48. 游客token的创建优化(想在服务端创建写入token)--- 已完成
-
+49. 结账页地址模块未登录用户输入邮箱后判断是否注册过,如果注册过让用户登录;--- 已完成
 
 
 > 39,40 参考 https://chatgpt.com/share/6a4f0637-ad28-83ea-ad44-423740795054
@@ -485,4 +486,8 @@ Hook 是 React 在 render 过程中注册状态依赖的函数调用点,而不
 ## 关于set-state-in-effect
 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
-3. https://github.com/react/react/issues/34743
+3. https://github.com/react/react/issues/34743
+
+
+## 关于数学加减乘除等计算
+本项目中使用[big.js](https://mikemcl.github.io/big.js)进行数字的数学计算

+ 2 - 0
package.json

@@ -36,6 +36,7 @@
     "@react-aria/visually-hidden": "^3.8.31",
     "@react-types/shared": "^3.33.1",
     "@reduxjs/toolkit": "^2.10.1",
+    "big.js": "^7.0.1",
     "clsx": "^2.1.1",
     "framer-motion": "^12.23.24",
     "graphql": "^16.12.0",
@@ -52,6 +53,7 @@
   },
   "devDependencies": {
     "@tailwindcss/postcss": "^4",
+    "@types/big.js": "^7.0.0",
     "@types/node": "^20",
     "@types/react": "19.2.14",
     "@types/react-dom": "19.2.3",

+ 16 - 0
pnpm-lock.yaml

@@ -87,6 +87,9 @@ importers:
       '@reduxjs/toolkit':
         specifier: ^2.10.1
         version: 2.12.0(react-redux@9.3.0(@types/react@19.2.14)(react@19.2.5)(redux@5.0.1))(react@19.2.5)
+      big.js:
+        specifier: ^7.0.1
+        version: 7.0.1
       clsx:
         specifier: ^2.1.1
         version: 2.1.1
@@ -130,6 +133,9 @@ importers:
       '@tailwindcss/postcss':
         specifier: ^4
         version: 4.3.0
+      '@types/big.js':
+        specifier: ^7.0.0
+        version: 7.0.0
       '@types/node':
         specifier: ^20
         version: 20.19.41
@@ -1680,6 +1686,9 @@ packages:
   '@tybys/wasm-util@0.10.2':
     resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
 
+  '@types/big.js@7.0.0':
+    resolution: {integrity: sha512-WfAGp7IbJvyB8EmWK4tJD24rJRAL6uVbw3LV/hJntFNam+os9KWKj0PzXo8rRRpjupYK8U0M8FoBB8dBhWF2dg==}
+
   '@types/estree@1.0.9':
     resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
 
@@ -1996,6 +2005,9 @@ packages:
     engines: {node: '>=6.0.0'}
     hasBin: true
 
+  big.js@7.0.1:
+    resolution: {integrity: sha512-iFgV784tD8kq4ccF1xtNMZnXeZzVuXWWM+ERFzKQjv+A5G9HC8CY3DuV45vgzFFcW+u2tIvmF95+AzWgs6BjCg==}
+
   brace-expansion@1.1.14:
     resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==}
 
@@ -5475,6 +5487,8 @@ snapshots:
       tslib: 2.8.1
     optional: true
 
+  '@types/big.js@7.0.0': {}
+
   '@types/estree@1.0.9': {}
 
   '@types/json-schema@7.0.15': {}
@@ -5788,6 +5802,8 @@ snapshots:
 
   baseline-browser-mapping@2.10.31: {}
 
+  big.js@7.0.1: {}
+
   brace-expansion@1.1.14:
     dependencies:
       balanced-match: 1.0.2

BIN
public/image/Logo.webp


BIN
public/image/empty-cart.webp


BIN
public/image/forget-password.webp


BIN
public/image/login.webp


BIN
public/image/payment/applepay.png


BIN
public/image/payment/cash-icon.png


BIN
public/image/payment/credit-card.png


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
public/image/payment/paypal-logo.svg


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1 - 0
public/image/plus-crown.svg


+ 11 - 9
src/actions/addProductToCartAction.ts

@@ -35,18 +35,20 @@ export async function addProductToCartAction(param:AddToCartVariables) {
             cache: "no-store",
         });
         if(!response.error) {
-            const responseData = response.data.createAddProductInCart?.addProductInCart;
+            const responseData = response.data?.createAddProductInCart?.addProductInCart;
             /** 兜底代码 start*/
             // 游客加购,然后清空购物车,然后刷新页面,然后再加购,然后再刷新页面,购物车会失效,所以添加兜底代码
             // 如果之前的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*/
 

+ 1 - 1
src/actions/deleteCartProductAction.ts

@@ -18,7 +18,7 @@ export async function deleteCartProductAction(cartItemId:number) {
             cache: "no-store",
         });
         if(!response.error) {
-            const resCatData = response.data.createRemoveCartItem?.removeCartItem ?? null;
+            const resCatData = response.data?.createRemoveCartItem?.removeCartItem ?? null;
             
             if(!resCatData || !resCatData?.itemsQty) {
                 // 购物车空了,删除游客cookie

+ 2 - 1
src/actions/index.ts

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

+ 1 - 1
src/actions/mergeCartAction.ts

@@ -33,7 +33,7 @@ export async function mergeCartAction(){
             },
             cache: "no-store",
         });
-        const cartData = response.data.createMergeCart?.mergeCart ?? null;
+        const cartData = response.data?.createMergeCart?.mergeCart ?? null;
         let success = true;
         let msg = "";
         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!',
+        }
+    }
+}

+ 30 - 12
src/app/(checkout)/checkout/_components/CheckoutAddress/BillingAddressCheckout.tsx

@@ -45,6 +45,7 @@ export default function BillingAddressCheckout ({
         getValues,
         setValue,
         register,
+        clearErrors,
         formState: { errors },
         control 
     } = 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({
         control,
         name: [
@@ -119,12 +137,12 @@ export default function BillingAddressCheckout ({
         setValue("billingCompanyName", shippingCompanyName);
         setValue("billingAddress", shippingAddress);
         setValue("billingCountry", shippingCountry);
-        setValue("billingState", shippingState);
         setValue("billingCity", shippingCity);
         setValue("billingPostcode", shippingPostcode);
         setValue("billingPhoneNumber", shippingPhoneNumber);
+        setValue("billingState", shippingState,{shouldValidate: true}); // 必须触发校验,因为使用浏览器自动填充的时候,可能导致billingState校验不通过,导致无法下单
     }, [billingSameAsShipping, shippingFields, setValue]);
-    
+    */
     
 
     const selectedCountry = useMemo(() => {
@@ -304,7 +322,7 @@ export default function BillingAddressCheckout ({
                         type="email"
                         placeholder="Enter your email address"
                         {...register("billingEmail", {
-                            required: "Email is required",
+                            required: !billingSameAsShipping ? "Email is required" : false,
                             pattern: {
                                 value: EMAIL_REGEX,
                                 message: "Please enter a valid email address",
@@ -323,7 +341,7 @@ export default function BillingAddressCheckout ({
                                 type="text"
                                 placeholder="First name"
                                 {...register("billingFirstName", {
-                                    required: "First name is required",
+                                    required: !billingSameAsShipping ? "First name is required" : false,
                                     pattern: {
                                         value: IS_VALID_INPUT,
                                         message: "Invalid First Name",
@@ -337,7 +355,7 @@ export default function BillingAddressCheckout ({
                                 type="text"
                                 placeholder="Last name"
                                 {...register("billingLastName", {
-                                    required: "Last name is required",
+                                    required: !billingSameAsShipping ? "Last name is required" : false,
                                     pattern: {
                                         value: IS_VALID_INPUT,
                                         message: "Invalid Last Name",
@@ -355,7 +373,7 @@ export default function BillingAddressCheckout ({
                         type="text"
                         placeholder="Please Input Your Detailed Address With Apt. No"
                         {...register("billingAddress", {
-                            required: "Address is required",
+                            required: !billingSameAsShipping ? "Address is required" : false,
                         })}
                         error={errors.billingAddress && errors.billingAddress.message as string}
                     />
@@ -369,7 +387,7 @@ export default function BillingAddressCheckout ({
                             <div className="w-41.25 flex-none">
                                 <Select placeholder="Country/Region"
                                     {...register("billingCountry", {
-                                        required: "Country/Region field is required",
+                                        required: !billingSameAsShipping ? "Country/Region field is required" : false,
                                         onChange: () => {
                                             setValue('billingState','',{
                                                 shouldDirty: true,
@@ -391,7 +409,7 @@ export default function BillingAddressCheckout ({
                                 (
                                     <Select placeholder="State/Province"
                                         {...register("billingState", {
-                                            required: "State/Province field is required"
+                                            required: !billingSameAsShipping ? "State/Province field is required" : false
                                         })}
                                         options={statesOptions}
                                         error={errors.billingState && errors.billingState.message as string}
@@ -402,7 +420,7 @@ export default function BillingAddressCheckout ({
                                         type="text"
                                         placeholder="State/Province"
                                         {...register("billingState", {
-                                            required: "State/Province field is required"
+                                            required: !billingSameAsShipping ? "State/Province field is required" : false
                                         })}
                                         error={errors.billingState && errors.billingState.message as string}
                                     />
@@ -417,7 +435,7 @@ export default function BillingAddressCheckout ({
                                     type="text"
                                     placeholder="Zip/Postal Code"
                                     {...register("billingPostcode", {
-                                        required: "Postcode field is required",
+                                        required: !billingSameAsShipping ? "Postcode field is required" : false,
                                         pattern: {
                                             value: IS_VALID_INPUT,
                                             message: "Invalid Postcode",
@@ -431,7 +449,7 @@ export default function BillingAddressCheckout ({
                                     type="text"
                                     placeholder="City"
                                     {...register("billingCity", {
-                                        required: "City field is required",
+                                        required: !billingSameAsShipping ? "City field is required" : false,
                                         pattern: {
                                             value: IS_VALID_INPUT,
                                             message: "Invalid City",
@@ -451,7 +469,7 @@ export default function BillingAddressCheckout ({
                         name="billingPhoneNumber"
                         control={control}
                         rules={{
-                            required: "Phone Number is required",
+                            required: !billingSameAsShipping ? "Phone Number is required" : false,
                             pattern: {
                                 value: IS_VALID_FULL_PHONE,
                                 message: "Invalid Phone Number",

+ 65 - 8
src/app/(checkout)/checkout/_components/CheckoutAddress/CheckoutAssress.tsx

@@ -1,24 +1,25 @@
 "use client";
 
 import {Ref, useState, useEffect, useCallback, useImperativeHandle, useMemo } from "react";
-
+import { useRouter } from 'next/navigation';
 import { useForm, FormProvider } from "react-hook-form";
 import { useCustomToast } from "@/utils/hooks/useToast";
 import {useCheckoutAddress} from "@/utils/hooks/useCheckoutAddress"
-
+import {useLoginModal} from "@/providers/LoginModalProvider";
 import { useGetCustomerAddress } from "@utils/hooks/useGetCustomerAddress";
+import { EMAIL_REGEX } from "@utils/constants";
 import { 
     ShipAddressFormData,
     FullAddressFormData,
     CreateCheckoutAddressVariables,
 } from "@/types/checkout/type";
-import { CustomerAddressItem } from "@/types/customer/type";
+import { CustomerAddressItem,CheckEmailRegisteredData } from "@/types/customer/type";
 import { 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 {normalizePhoneForForm} from "@/utils/phoneNumberTools";
-
 import AddressResultDisplay from "./AddressResultDisplay";
 import ShippingAddressCheckout from "./ShippingAddressCheckout";
 import BillingAddressCheckout from "./BillingAddressCheckout";
@@ -210,13 +211,18 @@ export function CheckoutAddress({
     loginEmail,
     cartData,
     onSaveAddress,
+    onCartChange
 }: {
     ref: Ref<RefCheckoutAddressHandle>;
     loginEmail: string;
     cartData: CartDetail;
     onSaveAddress: (saveRes: {billingAddress: CartAddress; shippingAddress: CartAddress;}) => void;
+    onCartChange: (data: CartDetail,dispatch: boolean) => void;
 }) {
 
+    const router = useRouter();
+    const {openLoginModal} = useLoginModal();
+
     const addressFormDefaultValues = useMemo(() => {
         return getAddressFormDataFromCart(cartData,loginEmail)
     },[cartData,loginEmail]);
@@ -374,12 +380,62 @@ export function CheckoutAddress({
             }  
         }
         loadAddress();
+    },[showToast,cartData.isGuest]);
+    useEffect(() => {
+        if(cartData.isGuest) return;
         const timer = window.requestAnimationFrame(() => {
             addressForm.reset(addressFormDefaultValues);
         });
         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() {
     //     const arr = [
     //         ['US','3803800217'], 
@@ -425,6 +481,7 @@ export function CheckoutAddress({
                                     customerAddressList={myAddressList}
                                     showFormField={showShipFormField}
                                     onShowFormFieldChange={showShipFormFieldChange}
+                                    onEmailChange={verifyEmailIsRegistered}
                                 />
                                 <BillingAddressCheckout 
                                     noAddress={noBillAddress}

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

@@ -30,12 +30,14 @@ export default function ShippingAddressCheckout({
     showFormField,
     onShowFormFieldChange,
     customerAddressList,
+    onEmailChange
 }: {
     noAddress: boolean;
     isGuest: boolean;
     showFormField: boolean;
     onShowFormFieldChange: (e: boolean) => void;
     customerAddressList: CustomerAddressItem[];
+    onEmailChange: (value: string) => void;
 }) {
 
     const {countries} = useConfig();
@@ -46,7 +48,7 @@ export default function ShippingAddressCheckout({
         formState: { errors },
         control,
     } = useFormContext() // retrieve all hook methods
-    console.log('errors ----- ', errors);
+    
     
     const countriesOptions = useMemo(() => {
         return countries.map((country) => ({
@@ -263,6 +265,13 @@ export default function ShippingAddressCheckout({
                         pattern: {
                             value: EMAIL_REGEX,
                             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}

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

@@ -9,6 +9,7 @@ import { useCustomToast } from "@/utils/hooks/useToast";
 import {useConfig} from "@/utils/hooks/useConfig";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 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";
 
 
@@ -50,6 +51,11 @@ export default function CheckoutProducts({
         // 删除商品;购物车是否还有商品,没有的话:
         // 登录用户 跳转到空购物车页面或首页
         // 游客  清除cookie 跳转到首页或空购物车页面
+        const confirmres = await confirmDialog({
+            title: "Warning",
+            content: "Are you sure you want to delete this product.",
+        });
+        if(!confirmres) return; // 取消删除
         overlayLoading.start();
         const res = await deleteProductFromCart(Number(cartItemId));
         if(!res.error) {
@@ -79,6 +85,7 @@ export default function CheckoutProducts({
             if(qty < 1) {
                 showToast("Quantity must be at least 1", "warning");
             } else {
+                overlayLoading.start();
                 const res = await editProductQtyFromCart(Number(item.id),qty);
                 if(!res.error) {
                     if(res.data) {
@@ -91,6 +98,7 @@ export default function CheckoutProducts({
                 } else {
                     showToast(res.msg, "danger");
                 }
+                overlayLoading.stop();
             }
         } else {
             showToast("This product can't change quantity.","warning");
@@ -114,7 +122,7 @@ export default function CheckoutProducts({
                         const baseImage = product.baseImage ? JSON.parse(product.baseImage) : {};
                         return (
                             <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>
                         )
                     })}
@@ -150,7 +158,7 @@ export default function CheckoutProducts({
                         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 src={baseImage.medium_image_url || ''} alt={product.name} />
+                                    <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">

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

@@ -331,6 +331,7 @@ export default function CheckoutWrapper({
                     loginEmail={loginEmail}
                     cartData={cartData}
                     onSaveAddress={saveAddressCallback}
+                    onCartChange={onCartDataChange}
                 />
  
                   
@@ -414,6 +415,7 @@ export default function CheckoutWrapper({
                 {(!paymentMethodLoading && !paymentMethodError) &&
                 <PaymentMethodCheckout 
                     ref={methodPaymentRef}
+                    grandTotal={cartData.grandTotal}
                     paymentMethods={paymentMethodDatas} 
                     selectedPaymentMethod={selectedPaymentMethod}
                     onPaymentMethodChange={handlePaymentMethodChange}

+ 5 - 1
src/app/(checkout)/checkout/_components/ContinueToPay/ContinueToPayOrderInfo.tsx

@@ -140,9 +140,13 @@ export default function ContinueToPayOrderInfo({
                 <div className="relative w-full box-border pr-9 mt-3.5">
                     <div className="w-full box-border flex overflow-x-auto">
                         {productItems.map((product) => { 
+                            const baseImage = JSON.parse(product.baseImage);
                             return(
                                 <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>
                             )
                         })}

+ 14 - 6
src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx

@@ -11,6 +11,7 @@ import {
     airwallexManager,
     useAirwallexCardState
 } from "@/lib/Airwallex/useAirwallexCard";
+import {getPaymentMethodLogo,getPaymentDescription} from "@/utils/paymentTools";
 import {RepayOrderPaymentMethodsData} from "@/types/checkout/type";
 import { OrderDetails } from "@/types/customer/type";
 import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
@@ -39,16 +40,16 @@ export function PaymentMethodContinueTpPay({
     const router = useRouter();
     const { showToast } = useCustomToast();
     const paymentMethodsData = use(paymentMethodsDataPromise);
-    const paymentMethods =  paymentMethodsData.data.collectionRepayOrderPaymentMethods || [];
+    const paymentMethods =  paymentMethodsData.data?.collectionRepayOrderPaymentMethods || [];
 
     const { paymentRepay, createPaymentCallback } = usePlaceOrder();
 
     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 orderCurrencyCode = orderCurrency?.code || currentCurrencyCode;
+    const orderCurrencyCode = orderCurrency?.code || currentCurrency.code;
 
 
     const airwallexCardState = useAirwallexCardState();
@@ -422,6 +423,13 @@ export function PaymentMethodContinueTpPay({
             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;
@@ -440,12 +448,12 @@ export function PaymentMethodContinueTpPay({
                                 >
                                     <p className="pr-2 flex-none relative">
                                         <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}
                                             height={32}
                                         />
                                     </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">
                                         <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>

+ 5 - 1
src/app/(checkout)/checkout/_components/ContinueToPay/ProductsModalPendingOrder.tsx

@@ -87,10 +87,14 @@ export default function ProductsModalPendingOrder({
       body={
         <div className="w-full px-4 pb-6">
             {products.map((product) => {
+                const baseImage = JSON.parse(product.baseImage);
                 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 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 className="flex-auto flex flex-col justify-between ">
                             <div>

+ 24 - 3
src/app/(checkout)/checkout/_components/PaymentMethodCheckout.tsx

@@ -3,6 +3,8 @@
 import Image from "next/image";
 import { Ref, useEffect, useImperativeHandle } from "react";
 import { useForm, get } from "react-hook-form";
+import {useConfig} from "@/utils/hooks/useConfig";
+import {getPaymentMethodLogo,getPaymentDescription} from "@/utils/paymentTools";
 import { CheckoutPaymentMethod } from "@/types/checkout/type";
 import AirwallexCardInput from "./PaymentMethodAdditional/AirwallexCardInput";
 import {useAirwallexCardState} from "@/lib/Airwallex/useAirwallexCard";
@@ -20,14 +22,19 @@ export function PaymentMethodCheckout({
     ref,
     paymentMethods,
     selectedPaymentMethod,
+    grandTotal,
     onPaymentMethodChange,
 }: {
     ref: Ref<RefPaymentMethodsHandle>;
     paymentMethods: CheckoutPaymentMethod[];
     selectedPaymentMethod: string;
+    grandTotal: number;
     onPaymentMethodChange: (value: string) => Promise<boolean>;
 }) {
 
+    const {getCurrentCurrencyItem} = useConfig();
+    const currentCurrency = getCurrentCurrencyItem();
+    const currencySymbol = currentCurrency.symbol;
 
     const airwallexCardState = useAirwallexCardState();
   
@@ -91,7 +98,13 @@ export function PaymentMethodCheckout({
 
     const airwallexOtherInfoShow = selectedPaymentMethod === 'airwallex' && airwallexCardState.cardNumberReady && airwallexCardState.expiryReady && airwallexCardState.cvcReady;
 
-    
+    const genetePaymentTips = (payment: string) => {
+        return getPaymentDescription({
+            payment: payment,
+            grandTotal: grandTotal,
+            symble: currencySymbol
+        })
+    };
     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"
                                 >
                                     <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 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">
                                         <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/RewardPointsRedeemMoney/RewardPointsRedeemMoney.tsx

@@ -65,7 +65,7 @@ export default function RewardPointsRedeemMoney({
                 if(res.mergeCartSuccess) { // 购物车合并成功
                     if(res.cartAfterMerge) {
                         const newCartDetail = formatCartDetail(res.cartAfterMerge);
-                        onPointsChange(newCartDetail, false);
+                        onPointsChange(newCartDetail,false);
                     }
                     
                 } else { // 购物车合并失败

+ 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>
+    );
+}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 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 { 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 { GetCartItemData } from "@/types/cart/type";
+import { 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}
+                />
+            }
+        </>
+    );
+  
+}

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

@@ -10,6 +10,7 @@ import { OrderDetailsData } from "@/types/customer/type";
 import { RepayOrderPaymentMethodsData } from "@/types/checkout/type";
 import {PaymentMethodContinueTpPay} from "../_components/ContinueToPay/PaymentMethodContinueTpPay";
 import LoadingPaymentMethod from "../_components/LoadingPaymentMethod";
+import TitleHeader from "@components/layout/navbar/TitleHeader";
 
 // 只能存在一个待支付订单
 export const dynamic = "force-dynamic";
@@ -32,7 +33,7 @@ export const dynamic = "force-dynamic";
 // status: "pending"
 // success: true
 export default async function ContinueToPay({searchParams}: {
-  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+  searchParams: Promise<{ orderid: string }>;
 }) {
     
     const session = await auth();// 游客是null
@@ -42,14 +43,14 @@ export default async function ContinueToPay({searchParams}: {
     }
 
     const query = await searchParams;
-    const orderid = query?.orderid;
+    const orderid = query.orderid;
     const {data: orderDetailResponse, error: orderDetailError} = await serverGraphqlFetch<OrderDetailsData,{id:string}>({
         query: GET_ORDER_DETAILS,
         variables: {
-            id: orderid as string
+            id: orderid
         }
     }); 
-    const orderDetailData = orderDetailResponse.customerOrder;
+    const orderDetailData = orderDetailResponse?.customerOrder;
     if(!orderDetailData  || orderDetailData?.status !== "pending")  {
         redirect('/', RedirectType.replace);
     }
@@ -62,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">
             
                 
@@ -79,27 +81,26 @@ export default async function ContinueToPay({searchParams}: {
                     Continue Shopping
                 </Link>
             </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>
-            <Suspense fallback={<LoadingPaymentMethod/>}> 
-                <PaymentMethodContinueTpPay 
-                    orderId={orderDetailData._id}
-                    orderDetail={orderDetailData}
-                    paymentMethodsDataPromise={paymentMethodsResponse}
-                    defaultPaymentMethod={orderDetailData.payment}
-                />
-            </Suspense>
-            
-        </div>
-    );
+        }
+    </>);
   
 }

+ 6 - 2
src/app/(checkout)/checkout/page.tsx

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

+ 2 - 4
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({
+export default async function CheckoutLayout({
   children,
 }: {
   children: ReactNode;
@@ -8,8 +8,6 @@ export default async function RootLayout({
   return (
     <>
 
-      <TitleHeader title="Checkout" />
-
       <div className="mx-auto w-full">
         {children}
       </div>

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

@@ -59,7 +59,7 @@ export async function generateMetadata({
 
   //   const categories = treeData?.treeCategories || [];
   //   const categoryItem = findCategoryBySlug(categories, categorySlug);
-  const categoryItem = categoryData.category;
+  const categoryItem = categoryData?.category;
   if (!categoryItem) return notFound();
 
   const translation = categoryItem.translation;
@@ -110,7 +110,7 @@ export default async function CategoryPage({
       id: Number(categoryId),
     },
   });
-  const categoryItem = categoryData.category;
+  const categoryItem = categoryData?.category;
   if (!categoryItem) return notFound();
 
   console.log("categoryItem +++++++++++", categoryItem);

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

@@ -0,0 +1,138 @@
+"use client";
+import { useState } from "react";
+import { useForm, 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 { 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 async function generateMetadata() {
@@ -11,9 +8,8 @@ export async function generateMetadata() {
 }
 
 export default function ForgetPasswordPage() {
-  return (
-    <Suspense fallback={<ForgetSkeleton />}>
-      <ForgetPasswordForm />
-    </Suspense>
-  );
+    return (<div className="w-full mt-6 box-border">
+        <ForgetPasswordForm />
+
+    </div>);
 }

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 422 - 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 { staticSeo } from "@utils/metadata";
-
-
-export const revalidate = 60;
+import {LoginPageWrapper} from "./_components/LoginPageWrapper";
 
 export async function generateMetadata() {
   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 || '/'} />
 
-  );
+  </>);
 }

+ 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, 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}
+            />
+        }
+        
+    </>);
+}

+ 1 - 1
src/app/(public)/error.tsx

@@ -14,7 +14,7 @@ export default function Error({
 }) {
   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.";
+  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.";
   }

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

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

+ 3 - 14
src/app/(public)/page.tsx

@@ -1,9 +1,3 @@
-// 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 { restApiFetch } from "@utils/bagisto/index";
 import {HomeApiData} from "@/types/home/type"
@@ -11,12 +5,7 @@ export const revalidate = 3600;
 
 
 export default async function Home() {
-  // const
-  // const {data} = await cachedGraphQLRequest<ThemeCustomizationResponse>(
-  //   "home",
-  //   GET_THEME_CUSTOMIZATION,
-  //   { first: 20 }
-  // );
+
 // 2. 服务端 fetch 请求我们自己的 Next.js API
 const res = await restApiFetch({
   api: `/home`,
@@ -31,8 +20,8 @@ const homeData: HomeApiData = res.body.data;
   return (
     <>
       {/* <HomeImageBanner /> */}
-      <DatePicker label={"Birth date"} labelPlacement={"outside"} />
-      {/* <RenderThemeCustomization themeCustomizations={data?.themeCustomizations ?? {edges: []}} /> */}
+
+
       <HomeContent homeData={homeData} />
     </>
   );

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

@@ -19,7 +19,7 @@ export default function CancelOrFailure({
 }) { 
     const dispatch = useAppDispatch();
     const promiseRes = use(orderDetailPromise);
-    const orderDetailRes = promiseRes.data.customerOrder ?? null;
+    const orderDetailRes = promiseRes.data?.customerOrder ?? null;
     const {cancelOrder} = usePlaceOrder();
     const orderId = orderDetailRes?._id;
     const orderStatus = orderDetailRes?.status;
@@ -68,7 +68,7 @@ export default function CancelOrFailure({
                         {isGuest ?
                             "payment cancelled! Your order has been cancelled."
                         :
-                            "Payment cancelled! You can try again."
+                            "Payment cancelled! You can continue to pay."
                         }
                         
                     </p>

+ 11 - 3
src/app/(public)/paymentresult/_components/OrderDetailWrapper.tsx

@@ -1,6 +1,7 @@
 "use client";
 
 import {use, useEffect} from "react";
+import { redirect, RedirectType } from 'next/navigation';
 import Link from "next/link";
 import Image from "next/image";
 import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
@@ -43,8 +44,11 @@ export default function OrderDetailWrapper({
 
     const {currencies} = useConfig();
     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';
     if(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">
                 {productItems.map((product) => {
+                    const baseImage = JSON.parse(product.baseImage);
                     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 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 className="flex-none w-50.75 flex flex-col justify-between">
                                 <div>

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

@@ -13,7 +13,7 @@ import { GUEST_CART_TOKEN } from "@/utils/constants";
 
 // 如果是游客:请求完订单详情后删除cookie里原来的carttoken cartid
 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 auth();// 游客是null
     const isGuest = session === null;
@@ -25,8 +25,8 @@ export default async function SuccessPage({searchParams}: {
     
     
     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) {
@@ -38,7 +38,7 @@ export default async function SuccessPage({searchParams}: {
     const orderDetailPromise = serverGraphqlFetch<OrderDetailsData,{id:string}>({
         query: GET_ORDER_DETAILS,
         variables: {
-            id:  orderid as string
+            id:  orderid
         }
     });
 

+ 23 - 58
src/app/(public)/product/[...urlProduct]/page.tsx

@@ -1,16 +1,12 @@
 import { notFound } from "next/navigation";
 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 {
   ProductDetailSkeleton,
   RelatedProductSkeleton,
 } from "@/components/common/skeleton/ProductSkeleton";
 import { BASE_SCHEMA_URL, PRODUCT_TYPE } from "@/utils/constants";
 import { GET_PRODUCT_BY_URL_KEY } from "@/graphql";
-import { cachedProductRequest } from "@/utils/hooks/useCache";
+import {serverGraphqlFetch} from "@utils/bagisto/index";
 import {
   ProductOption,
   SingleProductResponse,
@@ -20,7 +16,6 @@ import { RelatedProductsSection } from "@components/catalog/product/RelatedProdu
 import { HeroCarouselShimmer } from "@components/common/slider";
 import { ProductMedia } from "@/app/(public)/product/_components/ProductMedia";
 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 ProductDescription from "../_components/ProductDescription";
 import SwitchButton from "../_components/SwitchButton";
@@ -31,31 +26,7 @@ import { formatFlexibleVariants } from "@/utils/variantTools";
 // export const dynamic = 'auto'
 // // 'auto' | 'force-dynamic' | 'error' | 'force-static'
 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) {
-    console.log('getSingleProduct error ======= ', 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
 export default async function ProductPage({
   params,
@@ -65,21 +36,26 @@ export default async function ProductPage({
 }) {
   const { urlProduct } = await params;
   const fullPath = urlProduct.join("/");
-  const product = await getSingleProduct(fullPath);
-  console.log('ProductPage product +++++++++ ', product);
-  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 imageUrl = getImageUrl(product?.baseImageUrl, baseUrl, NOT_IMAGE);
+  
 
   // JSON-LD数据格式,便于搜索引擎识别页面信息,利于seo
   const productJsonLd = {
@@ -101,15 +77,7 @@ console.log("productCustomerFeatures----------------------",product);
 
   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 (
     <>
@@ -137,19 +105,16 @@ console.log("productCustomerFeatures----------------------",product);
             shortDescription={product.shortDescription || ""}
           />
           <ShopServicePanel />
-          {/* <ProductShortDescription
-            shortDescription={product.shortDescription || ""}
-          /> */}
+
          <ProductDescription   htmlString={product?.feature || ""} />
           <SwitchButton
             productId={"3792"} //|| String(product._id)
-            // reviewList={allReviews}
-            // reviewTotal={allReviews.length}
+
             questionTotal={questionTotal}
             questionList={["aaa"]}
           />
           <ProductDetail detail={product.description || ""} />
-          {/* <ProductReviewSection reviews={allReviews} productId={String(product._id)} /> */}
+          
         </Suspense>
         <Recommend/>
       </div>

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

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

+ 3 - 13
src/app/(public)/product/_components/SwitchButton.tsx

@@ -6,6 +6,7 @@ import ReviewAdd from "@/app/(public)/product/_components/review/ReviewAdd";
 import FaqList, { FaqItem } from "./question/FaqList";
 import HairPhotoGallery from "./review/HairPhotoGallery";
 import { clientFetch } from "@/lib/restApiClient";
+import { getUuId } from "@/utils/helper";
 interface ReviewQuestionTabProps {
   productId: string;
   questionList: any[];
@@ -53,18 +54,7 @@ export default function SwitchButton({
   // 自己通过接口拿到,不再由父组件传入
   const [reviewList, setReviewList] = useState<any[]>([]);
   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(() => {
   if (!productId) return;
   // 1. 请求中断控制器,组件卸载时取消网络请求,防止卸载后执行setState
@@ -76,6 +66,7 @@ const [clientUuid] = useState(() => getUuId());
   
   const fetchReviews = async () => {
     try {
+      const clientUuid = getUuId();
       const res = await clientFetch(
         `/api/shop/products/${productId}/reviews?page=1&per_page=10&has_images=0&sort=all&client_id=${clientUuid}`,
         { signal } // 绑定中断信号
@@ -200,7 +191,6 @@ const [clientUuid] = useState(() => getUuId());
               onOpenBaseModal={openAllReviewModal}
               activeTab={activeModalTab}
               onChangeTab={onTabChange}
-              uuid={clientUuid}
             />
           )
         ) : isEmptyQuestion ? (

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

@@ -9,7 +9,6 @@ import {
 } from "@/utils/helper";
 import { isArray } from "@/utils/type-guards";
 import { Avatar } from "@heroui/avatar";
-// import { Tooltip } from "@heroui/tooltip";
 import clsx from "clsx";
 import React, { FC, useState } from "react";
 import { ProductReviewNode } from "@/components/catalog/type";
@@ -17,6 +16,7 @@ import ReviewModal from "./ReviewModal";
 // import { ReviewTabType } from "@/types/products/review";
 import { useCustomToast } from "@utils/hooks/useToast";
 import { clientFetch } from "@/lib/restApiClient";
+import { getUuId } from "@/utils/helper";
 interface ProductReviewEdge {
   __typename: "ProductReviewEdge";
   node: ProductReviewNode;
@@ -31,7 +31,6 @@ interface ReviewDetailProps {
   activeTab: string;
   onChangeTab: (tabKey: string) => void;
   onOpenBaseModal: () => void;
-  uuid:string;
 }
 
 const ReviewDetail: FC<ReviewDetailProps> = ({
@@ -43,7 +42,6 @@ const ReviewDetail: FC<ReviewDetailProps> = ({
   activeTab,
   onChangeTab,
   onOpenBaseModal,
-  uuid,
 }) => {
  
   const [visibleCount, setVisibleCount] = useState(5);
@@ -65,10 +63,7 @@ const ReviewDetail: FC<ReviewDetailProps> = ({
 
   const visibleReviews = reviewDetails.slice(0, visibleCount);
   console.log("visibleReviews-------------------:", visibleReviews);
-  // useEffect(() => {
-  //   const uuid = getGuestUUID();
-  //   if (uuid) setGuestId(uuid);
-  // }, []);
+
   const handleLikeClick = async (item: any) => {
     // id直接从item拿
     const id = item.id;
@@ -90,6 +85,7 @@ const ReviewDetail: FC<ReviewDetailProps> = ({
       },
     }));
     try {
+      const uuid = getUuId();
       const res = await clientFetch(`/api/shop/reviews/${id}/like`, {
         method: "POST",
         headers: {
@@ -368,7 +364,6 @@ const ReviewDetail: FC<ReviewDetailProps> = ({
         productId={productId}
         onChangeTab={onChangeTab}
         activeTab={activeTab}
-        guestId={uuid}
       />
     </>
   );

+ 5 - 4
src/app/(public)/product/_components/review/ReviewModal.tsx

@@ -9,6 +9,7 @@ import ReviewImageGalleryModal from "./ReviewImageGalleryModal";
 import Image from "next/image";
 import { clientFetch } from "@/lib/restApiClient";
 import { useCustomToast } from "@utils/hooks/useToast";
+import { getUuId } from "@/utils/helper";
 // 分页常量
 const PAGE_SIZE = 10;
 
@@ -19,7 +20,6 @@ interface ReviewModalProps {
   productId: string;
   // 父组件传入默认Tab all / newest / photos
   activeTab: ReviewTabType;
-  guestId: string | null;
 }
 // 模拟后端请求函数,替换为你的真实接口
 async function fetchReviewApi(
@@ -127,7 +127,6 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
     productId,
     onChangeTab,
     activeTab,
-    guestId,
   }) {
     // 全部原始评论数据(接口返回完整数据缓存)
     const [fullReviewList, setFullReviewList] = useState<ProductReviewList>([]);
@@ -161,7 +160,8 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
       async (tab: ReviewTabType, pageNum: number, reset = false) => {
         setLoading(true);
         try {
-          const res = await fetchReviewApi(productId, tab, pageNum, guestId);
+          const uuid = getUuId();
+          const res = await fetchReviewApi(productId, tab, pageNum, uuid);
           if (reset) {
             // 切换Tab/首次打开,重置列表
             setFullReviewList(res.list);
@@ -287,13 +287,14 @@ const ReviewModal: React.FC<ReviewModalProps> = React.memo(
         },
       }));
       try {
+        const uuid = getUuId();
         const res = await clientFetch(`/api/shop/reviews/${id}/like`, {
           method: "POST",
           headers: {
             "Content-Type": "application/json",
           },
           body: JSON.stringify({
-            client_id: guestId,
+            client_id: uuid,
           }),
         });
 

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

@@ -4,22 +4,15 @@ import Grid from "@/components/theme/ui/grid/Grid";
 import { isArray } from "@/utils/type-guards";
 import { serverGraphqlFetch } from "@utils/bagisto/index";
 import {
-  GET_PRODUCTS,
-  // GET_PRODUCTS_PAGINATION,
-  // GET_FILTER_PRODUCTS,
   SEARCH_PRODUCTS,
 } from "@/graphql";
-import {
-  cachedGraphQLRequest,
-  // getFilterAttributes,
-} from "@/utils/hooks/useCache";
+
 import { generateMetadataForPage, buildProductFilters } from "@/utils/helper";
 import SortOrder from "@/components/theme/filters/SortOrder";
 import { SortByFields } from "@/utils/constants";
 // import MobileFilter from "@/components/theme/filters/MobileFilter";
 // import FilterList from "@/components/theme/filters/FilterList";
 import {
-  ProductsResponse,
   SearchProductsResponse,
   SearchProductsVariables,
 } from "@/components/catalog/type";
@@ -33,66 +26,12 @@ const ProductGridItems = dynamicImport(
 );
 
 export const dynamicParams = true;
-// 获取全站商品
-export async function generateStaticParams() {
-  try {
-    const itemsPerPage = 8;
-    const commonSearches = [""];
-    const params = [];
-    for (const query of commonSearches) {
-      // 获取默认产品回头换成 you may like 的产品
-      const { data } = await cachedGraphQLRequest<ProductsResponse>(
-        "search",
-        GET_PRODUCTS,
-        {
-          query: query,
-          first: 1,
-          sortKey: "CREATED_AT",
-          reverse: true,
-        },
-      );
-
-      const totalCount = data?.products?.totalCount || 0;
-      const totalPages = Math.ceil(totalCount / itemsPerPage);
-      let cursor: string | undefined;
-
-      for (let i = 0; i < totalPages; i++) {
-        const pageParams: { page: string; cursor?: string } = {
-          page: String(i + 1),
-        };
-        if (i > 0 && cursor) {
-          pageParams.cursor = cursor;
-        }
-        params.push(pageParams);
-        if (i < totalPages - 1) {
-          const { data: pageData } =
-            await cachedGraphQLRequest<ProductsResponse>(
-              "search",
-              GET_PRODUCTS,
-              {
-                query: query,
-                first: itemsPerPage,
-                sortKey: "CREATED_AT",
-                reverse: true,
-                ...(cursor && { after: cursor }),
-              },
-            );
-          cursor = pageData?.products?.pageInfo?.endCursor;
-        }
-      }
-    }
 
-    return params;
-  } catch (error) {
-    console.error("Error generating static params:", error);
-    return [];
-  }
-}
 // SEO+分享
 export async function generateMetadata({
   searchParams,
 }: {
-  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+  searchParams?: Promise<{ q: string }>;
 }) {
   const params = await searchParams;
   const searchQuery = params?.q as string | undefined;
@@ -109,7 +48,13 @@ export async function generateMetadata({
 export default async function SearchPage({
   searchParams,
 }: {
-  searchParams?: Promise<{ [key: string]: string | string[] | undefined }>;
+  searchParams?: Promise<{ 
+    q: string;
+    page: string;
+    sort: string;
+    cursor: string;
+    before: string;
+   }>;
 }) {
   const params = await searchParams;
   const {
@@ -117,9 +62,7 @@ export default async function SearchPage({
     page,
     cursor,
     before,
-  } = (params || {}) as {
-    [key: string]: string;
-  };
+  } = (params || {});
   const itemsPerPage = 8;
   const currentPage = page ? parseInt(page) - 1 : 0;
   const sortValue = params?.sort || "name-asc";
@@ -139,7 +82,7 @@ export default async function SearchPage({
       query: SEARCH_PRODUCTS,
       variables: {
         suggest: true,
-        query: searchValue,
+        query: searchValue || '',
         filter: filterInput,
         first: 8,
         ...(beforeCursor
@@ -149,19 +92,7 @@ export default async function SearchPage({
         reverse: selectedSort.reverse,
       },
     });
-    //  dataPromise = cachedGraphQLRequest<ProductsResponse>(
-    //     "search",
-    //     GET_FILTER_PRODUCTS,
-    //     {
-    //       query: searchValue,
-    //       filter: filterInput,
-    //       ...(beforeCursor
-    //         ? { last: itemsPerPage, before: beforeCursor }
-    //         : { first: itemsPerPage, after: afterCursor }),
-    //       sortKey: selectedSort.sortKey,
-    //       reverse: selectedSort.reverse,
-    //     },
-    //   );
+
   } else {
     dataPromise = (async () => {
       let currentAfterCursor: string | undefined = afterCursor;
@@ -176,22 +107,13 @@ export default async function SearchPage({
           query: SEARCH_PRODUCTS,
           variables: {
             suggest: true,
-            query: searchValue,
+            query: searchValue || '',
             first: currentPage * itemsPerPage,
             sortKey: selectedSort.sortKey,
             reverse: selectedSort.reverse,
           },
         });
-        // await cachedGraphQLRequest<ProductsResponse>(
-        //   "search",
-        //   GET_PRODUCTS_PAGINATION,
-        //   {
-        //     query: searchValue,
-        //     first: currentPage * itemsPerPage,
-        //     sortKey: selectedSort.sortKey,
-        //     reverse: selectedSort.reverse,
-        //   },
-        // );
+
         currentAfterCursor = cursorData?.search?.pageInfo?.endCursor;
         // console.log("currentAfterCursoraaaaa:",currentAfterCursor);
       }
@@ -202,7 +124,7 @@ export default async function SearchPage({
         query: SEARCH_PRODUCTS,
         variables: {
           suggest: true,
-          query: searchValue,
+          query: searchValue || '',
           ...(beforeCursor
             ? { last: itemsPerPage, before: beforeCursor }
             : { first: itemsPerPage, after: currentAfterCursor }),
@@ -210,14 +132,7 @@ export default async function SearchPage({
           reverse: selectedSort.reverse,
         },
       });
-      //    cachedGraphQLRequest<ProductsResponse>("search", GET_PRODUCTS, {
-      //   query: searchValue,
-      //   ...(beforeCursor
-      //     ? { last: itemsPerPage, before: beforeCursor }
-      //     : { first: itemsPerPage, after: currentAfterCursor }),
-      //   sortKey: selectedSort.sortKey,
-      //   reverse: selectedSort.reverse,
-      // });
+
     })();
   }
 

+ 36 - 0
src/app/api/customer/check-email/route.ts

@@ -0,0 +1,36 @@
+import { NextResponse, NextRequest } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+
+
+// http://nshop.test/api/customer/check-email?email=bb@tt.com
+export async function GET(request: NextRequest) {
+  try {
+    const searchParams = request.nextUrl.searchParams;
+    const email = searchParams.get('email');
+    const response = await restApiFetch<any>({
+      api: "/customer/check-email",
+      method: "GET",
+      cache: "no-store",
+      variables: {
+        email: email
+      }
+
+    });
+
+    return NextResponse.json(
+      response.body,
+      {status: response.status}
+    );
+
+  } catch (error) {
+
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 }
+    );
+  }
+}

+ 42 - 1
src/app/api/revalidate/route.ts

@@ -1,5 +1,46 @@
-import { revalidate } from "@/utils/bagisto";
 import { NextRequest, NextResponse } from "next/server";
+import { headers } from "next/headers";
+import { revalidatePath } from "next/cache";
+
+
+async function revalidate(req: NextRequest): Promise<NextResponse> {
+  const collectionWebhooks = [
+    "collections/create",
+    "collections/delete",
+    "collections/update",
+  ];
+  const productWebhooks = [
+    "products/create",
+    "products/delete",
+    "products/update",
+  ];
+  const topic = (await headers()).get("x-bagisto-topic") || "unknown";
+  const secret = req.nextUrl.searchParams.get("secret");
+  const isCollectionUpdate = collectionWebhooks.includes(topic);
+  const isProductUpdate = productWebhooks.includes(topic);
+
+  if (!secret || secret !== process.env.BAGISTO_REVALIDATION_SECRET) {
+    return NextResponse.json({ status: 200 });
+  }
+
+  if (!isCollectionUpdate && !isProductUpdate) {
+    return NextResponse.json({ status: 200, message: "No action needed" });
+  }
+
+  if (isProductUpdate) {
+    revalidatePath("/", "layout");
+  } else if (isCollectionUpdate) {
+    revalidatePath("/", "layout");
+  }
+
+  return NextResponse.json({
+    status: 200,
+    revalidated: true,
+    topic,
+    now: Date.now(),
+  });
+}
+
 
 export async function POST(req: NextRequest): Promise<NextResponse> {
   return revalidate(req);

+ 3 - 4
src/app/layout.tsx

@@ -101,8 +101,8 @@ export default async function RootLayout({
     tags: ["config-currency-list"]
   });
  
-  const countries = countryResData.countries;
-  const currencyList = currencyResData.currencies?.edges.map((item) => item.node) ?? [];
+  const countries = countryResData?.countries ?? [];
+  const currencyList = currencyResData?.currencies?.edges.map((item) => item.node) ?? [];
   const storeConfig = {
     // 系统默认值
     defaultChannel: defaultChannel,
@@ -119,7 +119,7 @@ export default async function RootLayout({
     query: GET_CART_ITEM
   });
   
-  const cartDetails = cartDetailsRes.createReadCart?.readCart ?? null;
+  const cartDetails = cartDetailsRes?.createReadCart?.readCart ?? null;
   return (
     <html lang="en" suppressHydrationWarning>
       <head>
@@ -163,7 +163,6 @@ export default async function RootLayout({
         </main>
         <div id="modal-root"></div>
         <KernelProvider />
-        {process.env.NODE_ENV} / {process.env.APP_ENV}
         <span aria-hidden="true" data-nx-locale style={__srOnly}>{__lr}</span>
       </body>
     </html>

+ 0 - 123
src/components/catalog/product/ProductMoreDetail.tsx

@@ -1,123 +0,0 @@
-"use client";
-import { Accordion, AccordionItem } from "@heroui/accordion";
-import React, { FC } from "react";
-import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
-import Prose from "@/components/theme/search/Prose";
-import ReviewSection from "../review/ReviewSection";
-import ReviewDetail from "../review/ReviewDetail";
-import { additionalDataTypes } from "../type";
-
-export const ProductMoreDetails: FC<{
-  description: string;
-  additionalData: additionalDataTypes[];
-  productId: string;
-  reviews: any[];
-  totalReview: number;
-  expandedKeys: Set<string>;
-  setExpandedKeys: (keys: Set<string>) => void;
-}> = ({ description, additionalData, reviews, productId, totalReview, expandedKeys, setExpandedKeys }) => {
-
-  const filterAdditionalData = additionalData.filter((item) => item?.attribute?.isVisibleOnFront == "1");
-
-
-  return (
-    <div className="mt-7 sm:my-7">
-      <Accordion
-        itemClasses={{
-          base: "shadow-none  bg-neutral-100 dark:bg-neutral-800",
-        }}
-        className="px-0"
-        selectionMode="multiple"
-        showDivider={false}
-        variant="splitted"
-        selectedKeys={expandedKeys}
-         onSelectionChange={(keys) => setExpandedKeys(keys as Set<string>)}
-      >
-        <AccordionItem
-          key="1"
-          classNames={{
-            title: "text-start",
-            trigger: "cursor-pointer",
-          }}
-          indicator={({ isOpen }) =>
-            isOpen ? (
-              <ChevronLeftIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
-            ) : (
-              <ChevronRightIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
-            )
-          }
-          aria-label="Description"
-          title="Description"
-        >
-          <Prose className="pb-2 text-selected-black dark:text-white font-light" html={description} />
-        </AccordionItem>
-
-        {filterAdditionalData.length > 0
-          ?
-          <AccordionItem
-            key="2"
-            classNames={{
-              title: "text-start",
-              trigger: "cursor-pointer",
-            }}
-            indicator={({ isOpen }) =>
-              isOpen ? (
-                <ChevronLeftIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
-              ) : (
-                <ChevronRightIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
-              )
-            }
-            aria-label="Additional Information"
-            title="Additional Information"
-          >
-            <div className="grid max-w-max grid-cols-[auto_1fr] gap-x-8 gap-y-4 px-1 pb-2">
-              {filterAdditionalData?.map((item) => (
-                <React.Fragment key={item.label}>
-                  <div className="grid">
-                    <p className="text-base font-normal text-selected-black dark:text-white">
-                      {item?.attribute?.adminName}
-                    </p>
-                  </div>
-                  <div className="grid">
-                    <p className="text-base font-normal text-selected-black dark:text-white">
-                      {item?.value || "--"}
-                    </p>
-                  </div>
-                </React.Fragment>
-              ))}
-            </div>
-          </AccordionItem>
-
-          : null}
-        <AccordionItem
-          key={filterAdditionalData.length > 0 ? "3" : "2"}
-          classNames={{
-            title: "text-start",
-            trigger: "cursor-pointer",
-          }}
-          indicator={({ isOpen }) =>
-            isOpen ? (
-              <ChevronLeftIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
-            ) : (
-              <ChevronRightIcon className="h-5 w-5 stroke-neutral-800 dark:stroke-white" />
-            )
-          }
-          aria-label="Ratings"
-          title="Ratings"
-        >
-          {totalReview > 0 ? (
-            <>
-              <ReviewDetail
-                reviewDetails={reviews}
-                totalReview={totalReview}
-                productId={productId}
-              />
-            </>
-          ) : (
-            <ReviewSection productId={productId}  totalReview={totalReview} />
-          )}
-        </AccordionItem>
-      </Accordion>
-    </div>
-  );
-};

+ 5 - 5
src/components/catalog/product/RelatedProductsSection.tsx

@@ -1,7 +1,7 @@
 import { GET_RELATED_PRODUCTS } from "@/graphql";
 import { ProductsSection } from "./ProductsSection";
 import { SingleProductResponse } from "@components/catalog/type";
-import { cachedProductRequest } from "@/utils/hooks/useCache";
+import {serverGraphqlFetch} from "@utils/bagisto/index";
 
 export async function RelatedProductsSection({
   fullPath,
@@ -10,14 +10,14 @@ export async function RelatedProductsSection({
 }) {
     async function getRelatedProduct(urlKey: string) {
       try {
-        const {data:dataById} = await cachedProductRequest<SingleProductResponse>(
+        const {data:dataById} = await serverGraphqlFetch<SingleProductResponse,{urlKey:string;first:number;}>({
           // urlKey,
-          GET_RELATED_PRODUCTS,
-          {
+          query:GET_RELATED_PRODUCTS,
+          variables:{
             urlKey: urlKey,
             first: 4,
           }
-        );
+        });
     
         return dataById?.product || null;
       } catch (error) {

+ 0 - 71
src/components/catalog/product/VariantSelector.tsx

@@ -1,71 +0,0 @@
-"use client";
-
-import { AttributeData, AttributeOptionNode } from "@/types/types";
-import { createUrl, getValidTitle } from "@/utils/helper";
-import clsx from "clsx";
-import { usePathname, useRouter, useSearchParams } from "next/navigation";
-
-export function VariantSelector({
-  variants,
-  setUserInteracted,
-}: {
-  variants: AttributeData[];
-  setUserInteracted: React.Dispatch<React.SetStateAction<boolean>>;
-  possibleOptions: Record<string, number[]>;
-}) {
-  const router = useRouter();
-  const pathname = usePathname();
-  const searchParams = useSearchParams();
-  if (!variants?.length) return null;
-
-  return (
-    <>
-      {variants.map((option , index : number) => {
-        const attributeCode = option.code;
-        const _isAlreadySelected = searchParams.has(attributeCode);
-        return (
-          <dl key={`${option.id} + ${index}` } className="mb-8">
-            <dt className="mb-4 text-sm capitalize tracking-wide">
-              {getValidTitle(attributeCode)}
-            </dt>
-
-            <dd className="flex flex-wrap gap-3">
-              {(option.options as AttributeOptionNode[]).map((node) => {
-                const isActive = searchParams.get(attributeCode) === String(node.id);
-                const isAvailable = node?.isValid;
-                const nextParams = new URLSearchParams(searchParams.toString());
-                nextParams.set(attributeCode, String(node.id));
-
-                const optionUrl = createUrl(pathname, nextParams);
-
-                return (
-                  <button
-                    key={node.id}
-                    disabled={!isAvailable}
-                    onClick={() => {
-                      if (!isAvailable) return;
-                      router.replace(optionUrl, { scroll: false });
-                      setUserInteracted(true);
-                    }}
-                    className={clsx(
-                      "flex min-w-[48px] cursor-pointer items-center justify-center rounded-lg bg-neutral-100 px-3.5 py-2.5 text-sm dark:border-neutral-800 dark:bg-neutral-800",
-                      {
-                        "cursor-default ring-2 ring-blue-600 text-blue-600": isActive,
-                        "ring-[0] transition duration-300 ease-in-out hover:scale-110 hover:border-blue-600":
-                          !isActive && isAvailable,
-                        "relative z-10 cursor-not-allowed overflow-hidden bg-neutral-100 text-neutral-500 ring-1 ring-neutral-300 before:absolute before:inset-x-0 before:-z-10 before:h-px before:-rotate-45 before:bg-neutral-300 before:transition-transform dark:bg-neutral-900 dark:text-neutral-400 dark:ring-neutral-700 before:dark:bg-neutral-700":
-                          !isAvailable,
-                      }
-                    )}
-                  >
-                    {node.label || node.adminName}
-                  </button>
-                );
-              })}
-            </dd>
-          </dl>
-        );
-      })}
-    </>
-  );
-}

+ 0 - 204
src/components/catalog/review/ReviewDetail.tsx

@@ -1,204 +0,0 @@
-"use client";
-
-import { Rating } from "@components/common/Rating";
-import { GridTileImage } from "@components/theme/ui/grid/Tile";
-import { formatDate, getInitials, getReviews } from "@/utils/helper";
-import { isArray } from "@/utils/type-guards";
-import { Avatar } from "@heroui/avatar";
-import { Tooltip } from "@heroui/tooltip";
-import clsx from "clsx";
-import { FC, useState } from "react";
-import ReviewSection from "./ReviewSection";
-import { ProductReviewNode } from "@/components/catalog/type";
-
-interface ProductReviewEdge {
-  __typename: "ProductReviewEdge";
-  node: ProductReviewNode;
-}
-
-interface ReviewDetailProps {
-  reviewDetails: ProductReviewEdge[];
-  totalReview: number;
-  productId: string;
-}
-
-const ReviewDetail: FC<ReviewDetailProps> = ({
-  reviewDetails,
-  totalReview,
-  productId,
-}) => {
-  const [visibleCount, setVisibleCount] = useState(5);
-
-  const reviews: ProductReviewNode[] =
-    reviewDetails?.map((edge) => edge.node) || [];
-
-  const { reviewAvg, ratingCounts } = getReviews(reviews);
-
-  const visibleReviews = reviews.slice(0, visibleCount);
-
-  return (
-    <>
-      <div className="flex flex-col flex-wrap gap-x-5 sm:gap-x-10">
-        <div className="my-2 flex w-full flex-col flex-wrap justify-between gap-4 sm:flex-row sm:items-center min-[1350px]:flex-nowrap">
-          <div className="flex items-center gap-x-2">
-            <Rating
-              length={5}
-              size="size-5"
-              star={reviewAvg}
-              reviewCount={totalReview}
-            />
-          </div>
-
-          <div className="flex w-full max-w-[280px] overflow-hidden rounded-sm">
-            {Object.entries(ratingCounts)
-              .reverse()
-              .filter(([_, count]) => (count as number) > 0)
-              .map(([star, count]) => (
-                <div
-                  key={star}
-                  style={{ flex: count as number }}
-                  className="min-h-4"
-                >
-                  <Tooltip
-                    content={
-                      <p className="text-center">
-                        {star} Star <br /> {count as number}{" "}
-                        {(count as number) >= 2 ? "Reviews" : "Review"}
-                      </p>
-                    }
-                    placement="top"
-                    className="cursor-pointer"
-                  >
-                    <div
-                      className={clsx(
-                        "h-full w-full !cursor-pointer",
-                        star === "5"
-                          ? "bg-green-700"
-                          : star === "4"
-                            ? "bg-cyan-400"
-                            : star === "3"
-                              ? "bg-violet-600"
-                              : star === "2"
-                                ? "bg-yellow-400"
-                                : "bg-red-600",
-                      )}
-                    />
-                  </Tooltip>
-                </div>
-              ))}
-          </div>
-        </div>
-
-        <div className="flex w-full flex-1 flex-col gap-5 py-2 sm:pt-6">
-          {/* Scrollable Container */}
-          <div
-            className="max-h-[380px] overflow-y-auto pr-2 
-        scrollbar-thin scrollbar-track-transparent 
-        scrollbar-thumb-neutral-400 dark:scrollbar-thumb-neutral-600"
-          >
-            {visibleReviews &&
-              visibleReviews.map(
-                (
-                  {
-                    name,
-                    title,
-                    comment,
-                    createdAt,
-                    rating,
-                    images,
-                    customer,
-                  }: ProductReviewNode,
-                  index: number,
-                ) => (
-                  <div
-                    key={index}
-                    className={clsx("flex flex-col gap-y-2 py-3", {
-                      "border-b border-neutral-200 dark:border-neutral-700":
-                        visibleReviews.length > 1 &&
-                        index < visibleReviews.length - 1,
-                    })}
-                  >
-                    <h1 className="font-outfit text-xl font-medium text-black/[80%] dark:text-white">
-                      {title}
-                    </h1>
-                    <Rating
-                      className="my-1"
-                      length={5}
-                      size="size-5"
-                      star={rating}
-                    />
-
-                    <h2 className="font-outfit text-base font-normal text-black/[80%] dark:text-white">
-                      {comment}
-                    </h2>
-
-                    <div className="flex gap-4">
-                      <div className="flex w-full items-center gap-2">
-                        <Avatar
-                          className="h-[56px] min-w-[56px] border border-solid border-black/10 bg-white text-large dark:bg-neutral-900"
-                          name={getInitials(name)}
-                          src={customer?.imageUrl}
-                        />
-                        <div>
-                          <h1 className="font-outfit text-base font-medium text-black/80 dark:text-white">
-                            {name}
-                          </h1>
-                          <p className="text-base text-black/80 dark:text-white">
-                            {formatDate(createdAt)}
-                          </p>
-                        </div>
-                      </div>
-                    </div>
-
-                    {isArray(images) && images && images.length > 0 && (
-                      <div className="mt-2 flex h-full min-h-[50px] w-full max-w-[60px] flex-wrap gap-2">
-                        {images.map((img) => (
-                          <GridTileImage
-                            key={img.reviewId}
-                            fill
-                            alt={`${img.reviewId}-review`}
-                            className="rounded-lg"
-                            src={img.url}
-                          />
-                        ))}
-                      </div>
-                    )}
-                  </div>
-                ),
-              )}
-
-            {reviews.length > visibleCount && (
-              <div className="py-4">
-                <button
-                  onClick={() => setVisibleCount((prev) => prev + 5)}
-                  className="flex items-start gap-2 text-primary-600 cursor-pointer hover:text-primary-700 dark:text-primary-400 dark:hover:text-primary-300 font-medium transition-colors duration-200 group"
-                >
-                  <span>Load More</span>
-                  <svg
-                    className="w-4 h-4 transition-transform duration-200 group-hover:translate-x-1 mt-1"
-                    fill="none"
-                    stroke="currentColor"
-                    viewBox="0 0 24 24"
-                    xmlns="http://www.w3.org/2000/svg"
-                  >
-                    <path
-                      strokeLinecap="round"
-                      strokeLinejoin="round"
-                      strokeWidth={2}
-                      d="M9 5l7 7-7 7"
-                    />
-                  </svg>
-                </button>
-              </div>
-            )}
-          </div>
-        </div>
-        <div className="mx-auto ">
-          <ReviewSection productId={productId} totalReview={totalReview} />
-        </div>
-      </div>
-    </>
-  );
-};
-
-export default ReviewDetail;

+ 1 - 1
src/components/catalog/type.ts

@@ -1,5 +1,5 @@
 export interface SingleProductResponse {
-  product: ProductNode;
+  product: ProductNode | null;
 }
 export interface ProductOptionValue {
   code: string;

+ 9 - 15
src/components/common/LoginModal/LoginModal.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import {useState, useLayoutEffect} from "react";
+import {useState, useLayoutEffect, useEffect} from "react";
 import { useRouter } from 'next/navigation';
 import { signIn } from "next-auth/react";
 import clsx from "clsx";
@@ -14,6 +14,7 @@ import { useUserRegister } from "@/utils/hooks/useUserRegister";
 import { overlayLoading } from "@/components/theme/ui/kernel/loading/api";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import {CartDetail} from "@/types/cart/type";
+import {LoginFormData, RegisterFormData} from "@/types/customer/type";
 import CommonModal from "@/components/theme/ui/CommonModal";
 import InputText from "@/components/theme/ui/InputText";
 import PasswordInput from "@/components/theme/ui/PasswordInput";
@@ -21,20 +22,6 @@ import PhoneNumberInput from "@/components/theme/ui/PhoneNumberInput/PhoneNumber
 
 type TabFlag = "login"|"register";
 
-interface LoginFormData {
-    username:string;
-    password:string;
-}
-interface RegisterFormData {
-    firstName: string;
-    lastName: string;
-    email: string;
-    password: string;
-    confirmPassword: string;
-    phone: string;
-    subscribedToNewsLetter: boolean;
-    isAgree: boolean;
-}
 export interface LoginModalReult {
     loginSuccess: boolean;
     registerSuccess: boolean;
@@ -46,10 +33,12 @@ export interface LoginModalReult {
 export default function LoginModal({
     isShow,
     callbackUrl,
+    defaultEmail,
     closeModal,
     onFinalResult = async () => {},
 }: {
     isShow: boolean;
+    defaultEmail?: string;
     callbackUrl?: string
     closeModal: (bool: boolean) => void;
     onFinalResult?: (param: LoginModalReult) =>void | Promise<void>;
@@ -82,6 +71,11 @@ export default function LoginModal({
     });
     const [activeTab, setActiveTab] = useState<TabFlag>('login');
 
+    useEffect(() => {
+        
+        loginForm.setValue('username',defaultEmail || '');
+        
+    },[defaultEmail]);
 
     useLayoutEffect(() => {
         if(isShow) {

+ 0 - 116
src/components/common/form/Input.tsx

@@ -1,116 +0,0 @@
-import { ExclamationCircleIcon } from "@heroicons/react/24/outline";
-import clsx from "clsx";
-import { forwardRef } from "react";
-import { isArray } from '@/utils/type-guards';
-interface InputTextProps
-  extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
-  className?: string;
-  label: string;
-  name: string;
-  errorMsg?: string | string[] | Record<string, unknown>;
-  defaultValue?: string;
-  typeName?: string;
-  placeholder?: string;
-  size?: "sm" | "md" | "lg";
-  labelPlacement?: "inside" | "outside" | "outside-left";
-  rounded?: "sm" | "md" | "lg";
-  showAsterisk?: boolean; // Show asterisk if true
-}
-
-const sizeClasses = {
-  sm: "text-sm px-2 py-1",
-  md: "text-base px-3 py-2",
-  lg: "text-lg px-4 py-3",
-};
-
-const roundedClasses = {
-  sm: "rounded-sm",
-  md: "rounded-md",
-  lg: "rounded-lg",
-};
-
-const InputText = forwardRef<HTMLInputElement, InputTextProps>(
-  (
-    {
-      className,
-      label,
-      name,
-      errorMsg,
-      defaultValue,
-      typeName = "text",
-      placeholder,
-      size = "sm",
-      labelPlacement = "inside",
-      rounded = "sm",
-      required,
-      showAsterisk = true,
-      ...rest
-    },
-    ref
-  ) => {
-    const hasError = Boolean(errorMsg);
-
-    const borderColorClass = hasError
-      ? "border-red-500"
-      : "border-gray-300 dark:border-gray-500";
-
-    return (
-      <div className={clsx("max-w-full mb-2.5", className)}>
-        {labelPlacement !== "inside" && (
-          <label
-            className={clsx(
-              "px-1 mb-1 block font-medium text-black dark:text-white"
-            )}
-            htmlFor={name}
-          >
-            {label} {showAsterisk && <span className="text-red-500">*</span>}
-          </label>
-        )}
-        <div className="relative">
-          <input
-            ref={ref}
-            className={clsx(
-              "w-full !rounded-[0.62rem] border bg-transparent text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:text-white",
-              borderColorClass,
-              sizeClasses[size],
-              roundedClasses[rounded],
-              labelPlacement === "inside"
-                ? "placeholder-input-color dark:placeholder-selected-color-dark"
-                : ""
-            )}
-            defaultValue={defaultValue}
-            id={name}
-            name={name}
-            placeholder={labelPlacement === "inside" ? label : placeholder}
-            type={typeName}
-            required={required}
-            {...rest}
-          />
-          {hasError && (
-            <ul className="absolute -bottom-8 py-2 text-sm text-red-500">
-              {isArray(errorMsg) ? (
-                (errorMsg as string[]).map((msg, index) => (
-                  <li key={index} className="flex items-center gap-1">
-                    <ExclamationCircleIcon className="h-5 w-5" />
-                    {msg}
-                  </li>
-                ))
-              ) : (
-                <li className="flex items-center gap-1 text-xs sm:text-sm">
-                  <ExclamationCircleIcon className="size-[18px]" />
-                  {typeof errorMsg === "string"
-                    ? errorMsg
-                    : JSON.stringify(errorMsg)}
-                </li>
-              )}
-            </ul>
-          )}
-        </div>
-      </div>
-    );
-  }
-);
-
-InputText.displayName = "InputText";
-
-export default InputText;

+ 0 - 32
src/components/common/skeleton/CategoryCarouselSkeleton.tsx

@@ -1,32 +0,0 @@
-export const CategoryCarouselSkeleton = () => {
-    return (
-        <section className="pt-8 sm:pt-12 lg:pt-20">
-            <div className="md:max-w-4.5xl mx-auto mb-10 w-auto text-center md:px-36">
-                {/* Heading skeleton */}
-                <div className="mb-2 mx-auto h-8 w-64 animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900 md:h-10 md:w-80" />
-
-                {/* Description skeleton */}
-                <div className="mx-auto mt-3 h-4 w-full max-w-2xl animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900 md:h-5" />
-                <div className="mx-auto mt-2 h-4 w-3/4 max-w-xl animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900 md:h-5" />
-            </div>
-
-            <div className="w-full overflow-x-auto overflow-y-hidden">
-                <ul className="m-0 grid grid-cols-1 gap-7 p-0 xss:grid-cols-2 sm:grid-cols-3">
-                    {[1, 2, 3].map((index) => (
-                        <li
-                            key={index}
-                            className="relative aspect-498/665 h-full w-full max-w-[498px] flex-none overflow-hidden rounded-[18px]"
-                        >
-                            <div className="relative h-full w-full animate-pulse rounded-[18px] bg-neutral-100 dark:bg-neutral-900">
-                                {/* Category label skeleton */}
-                                <div className="absolute bottom-0 left-0 right-0 p-6">
-                                    <div className="h-6 w-32 animate-pulse rounded-md bg-neutral-200 dark:bg-neutral-800" />
-                                </div>
-                            </div>
-                        </li>
-                    ))}
-                </ul>
-            </div>
-        </section>
-    );
-};

+ 0 - 137
src/components/customer/ForgetPassword.tsx

@@ -1,137 +0,0 @@
-"use client";
-import { useState } from "react";
-import { useForm, SubmitHandler } from "react-hook-form";
-import clsx from "clsx";
-import Image from "next/image";
-import Link from "next/link";
-
-import { recoverPassword } from "@utils/actions";
-
-import { Button } from "@components/common/button/Button";
-
-import { EMAIL_REGEX, FORGET_PASSWORD_IMG } from '@/utils/constants';
-import InputText from '@components/common/form/Input';
-import { useCustomToast } from '@/utils/hooks/useToast';
-
-type ForgetPasswordInputs = {
-  email: string;
-};
-
-export default function ForgetPasswordForm() {
-  const { showToast } = useCustomToast();
-  const [loading, setLoading] = useState(false);
-
-  const {
-    register,
-    handleSubmit,
-    formState: { errors, isSubmitting },
-  } = useForm<ForgetPasswordInputs>({
-    mode: "onSubmit",
-    reValidateMode: "onChange",
-  });
-
-  const onSubmit: SubmitHandler<ForgetPasswordInputs> = async (data: {
-    email: string;
-  }) => {
-    setLoading(true);
-
-    const formData = new FormData();
-
-    formData.append("email", data.email);
-
-    try {
-      const result = await recoverPassword({
-        email: data?.email,
-      });
-
-      // Show success/error API response
-      if (result.success) {
-        showToast(result.success.msg, "success");
-      } else if (result.errors?.apiRes) {
-        showToast(result.errors.apiRes?.msg, "danger");
-      }
-
-      // Field-specific errors
-      if (result.errors?.email) {
-        showToast(
-          Array.isArray(result.errors.email)
-            ? result.errors.email[0]
-            : result.errors.email,
-          "danger"
-        );
-      }
-    } catch {
-      showToast("Something went wrong. Please try again later.", "danger");
-    } finally {
-      setLoading(false);
-    }
-  };
-
-  return (
-    <div className="my-8 flex w-full items-center w-full max-w-screen-2xl mx-auto px-4 xss:px-7.5 justify-between gap-4 lg:my-16 xl:my-32">
-      <div className="flex w-full flex-col gap-y-4 lg:max-w-[583px] lg:gap-y-12">
-        <div className="font-outfit">
-          <h2 className="py-1 text-2xl font-semibold sm:text-4xl">
-            Recover Password
-          </h2>
-          <p className="mt-2 text-base md:text-lg font-normal text-black/60 dark:text-neutral-400">
-            If you forgot your password, recover it by entering your email
-            address.
-          </p>
-        </div>
-
-        <form
-          noValidate
-          className="flex flex-col gap-y-5 md:gap-y-10"
-          onSubmit={handleSubmit(onSubmit)}
-        >
-          <InputText
-            {...register("email", {
-              required: "Email is required",
-              pattern: {
-                value: EMAIL_REGEX,
-                message: "Please enter a valid email address.",
-              },
-            })}
-            errorMsg={errors?.email?.message ? [errors.email.message] : undefined}
-            label="Enter Your Email Address"
-            labelPlacement="outside"
-            name="email"
-            placeholder="Enter email address"
-            size="lg"
-            typeName="email"
-          />
-
-          <div className="flex flex-col gap-y-3 md:gap-y-2">
-            <Button
-              disabled={loading || isSubmitting}
-              loading={loading || isSubmitting}
-              title="Reset Password"
-              type="submit"
-            />
-            <span className="px-1 mx-auto md:mx-0 font-outfit">
-              Back to sign in?{" "}
-              <Link className="text-blue-600 underline" href="/customer/login" aria-label="Go to sign in page">
-                Sign In
-              </Link>
-            </span>
-          </div>
-        </form>
-      </div>
-
-      <div className="relative hidden aspect-[1] max-h-[692px] w-full max-w-[790px] sm:block md:aspect-[1.14]">
-        <Image
-          fill
-          priority
-          alt="Forget Password Illustration"
-          className={clsx(
-            "relative h-full w-full object-fill",
-            "transition duration-300 ease-in-out group-hover:scale-105"
-          )}
-          sizes={"(min-width: 768px) 66vw, 100vw"}
-          src={FORGET_PASSWORD_IMG}
-        />
-      </div>
-    </div>
-  );
-}

+ 0 - 183
src/components/customer/LoginForm.tsx

@@ -1,183 +0,0 @@
-"use client";
-
-import clsx from "clsx";
-import { signIn } from "next-auth/react";
-import Image from "next/image";
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-import { SubmitHandler, useForm } from "react-hook-form";
-import { Button } from "@components/common/button/Button";
-import { EMAIL_REGEX, SIGNIN_IMG } from "@/utils/constants";
-import InputText from "@components/common/form/Input";
-import { useCustomToast } from "@/utils/hooks/useToast";
-import { mergeCartAction, deleteGuestCookieAction } from "@/actions";
-import { useAppSelector } from "@/store/hooks";
-import { useCartDetail } from "@utils/hooks/useCartDetail";
-
-type LoginFormInputs = {
-  username: string;
-  password: string;
-};
-
-export default function LoginForm() {
-  const router = useRouter();
-  const { showToast } = useCustomToast();
-  const { getCartDetail } = useCartDetail()
-
-  const cart = useAppSelector((state) => state.cartDetail.cart);
-  const {
-    register,
-    handleSubmit,
-    formState: { errors, isSubmitting },
-  } = useForm<LoginFormInputs>({
-    mode: "onSubmit",
-    reValidateMode: "onChange",
-  });
-
-  const onSubmit: SubmitHandler<LoginFormInputs> = async (data) => {
-    try {
-
-      const result = await signIn("credentials", {
-        redirect: false,
-        ...data,
-        callbackUrl: "/", // The callbackUrl specifies to which URL the user will be redirected after signing in. Defaults to the page URL the sign-in is initiated from.
-      });
-      console.log('result --- ',result);
-
-      if (!result?.ok) {
-        showToast(result?.error || "Invalid login credentials.", "warning");
-        return;
-      }
-      showToast("Welcome! Successfully logged in.", "success");
-
-
-        if(cart) {
-          await mergeCartAction();
-        } else {
-          await deleteGuestCookieAction(false);
-          await getCartDetail();
-        }
-
-        setTimeout(() => {
-          router.push("/");
-          router.refresh();
-        }, 100);
-
-
-    } catch (error) {
-      console.error(error);
-      showToast("Something went wrong. Please try again.", "danger");
-    }
-  };
-
-  return (
-    <div className="flex w-full items-center max-w-screen-2xl mx-auto px-4  xss:px-7.5 justify-between gap-4 lg:my-16 xl:my-28">
-      <div className="flex w-full max-w-[583px] flex-col gap-y-4 lg:gap-y-12">
-        <div className="font-outfit">
-          <h2 className="py-1 text-2xl font-semibold sm:text-4xl">
-            Sign in to your account
-          </h2>
-          <p className="mt-2  text-base md:text-lg font-normal text-black/60 dark:text-neutral-400">
-            If you have an account, sign in with your email address.
-          </p>
-        </div>
-
-        <form
-          noValidate
-          className="flex flex-col gap-y-4 lg:gap-y-12"
-          onSubmit={handleSubmit(onSubmit)}
-        >
-          <div className="flex flex-col gap-y-2.5 lg:gap-4">
-            <InputText
-              {...register("username", {
-                required: "Email is required",
-                pattern: {
-                  value: EMAIL_REGEX,
-                  message: "Please enter a valid email.",
-                },
-              })}
-              errorMsg={
-                errors.username?.message ? [errors.username.message] : undefined
-              }
-              label="Enter Your Email Address"
-              labelPlacement="outside"
-              name="username"
-              placeholder="Enter your email address"
-              rounded="md"
-              size="lg"
-              typeName="email"
-            />
-
-            <InputText
-              {...register("password", {
-                required: "Password is required",
-                minLength: {
-                  value: 2,
-                  message: "Be at least 2 characters long",
-                },
-                validate: (value) => {
-                  if (!/[0-2]/.test(value))
-                    return "Contain at least one number.";
-
-                  return true;
-                },
-              })}
-              errorMsg={
-                errors.password?.message ? [errors.password.message] : undefined
-              }
-              label="Enter Password"
-              labelPlacement="outside"
-              name="password"
-              placeholder="Enter your password"
-              rounded="md"
-              size="lg"
-              typeName="password"
-            />
-
-            <Link
-              className="text-end text-sm font-medium text-blue-600 underline hover:text-blue-500 underline"
-              href="/customer/forget-password"
-              aria-label="Go to forgot password page"
-            >
-              Forgot your password ?
-            </Link>
-          </div>
-
-          <div className="flex flex-col gap-2 lg:gap-y-3">
-            <Button
-              className="cursor-pointer"
-              disabled={isSubmitting}
-              loading={isSubmitting}
-              title="Sign In"
-              type="submit"
-            />
-            <span className="mx-auto font-outfit sm:mx-0">
-              New customer?{" "}
-              <Link
-                className="font-medium text-blue-600 hover:text-blue-500 underline"
-                href="/customer/register"
-                aria-label="Go to create account page"
-              >
-                Create your account
-              </Link>
-            </span>
-          </div>
-        </form>
-      </div>
-
-      <div className="relative hidden aspect-[0.9] max-h-[692px] w-full max-w-[790px] sm:block md:aspect-[1.14]">
-        <Image
-          fill
-          priority
-          alt="Sign In Image"
-          className={clsx(
-            "relative h-full w-full object-fill",
-            "transition duration-300 ease-in-out group-hover:scale-105"
-          )}
-          sizes={"(min-width: 768px) 66vw, 100vw"}
-          src={SIGNIN_IMG}
-        />
-      </div>
-    </div>
-  );
-}

+ 0 - 214
src/components/customer/RegistrationForm.tsx

@@ -1,214 +0,0 @@
-"use client";
-
-import Image from "next/image";
-import Link from "next/link";
-import { SubmitHandler, useForm } from "react-hook-form";
-import InputText from "@components/common/form/Input";
-import { useCustomToast } from "@/utils/hooks/useToast";
-import { useRouter } from "next/navigation";
-import { EMAIL_REGEX, SIGNUP_IMG, IS_VALID_INPUT } from "@utils/constants";
-import { createUser } from "@utils/actions";
-import { Button } from "@components/common/button/Button";
-
-export type RegisterInputs = {
-  firstName: string;
-  lastName: string;
-  email: string;
-  password: string;
-  passwordConfirmation: string;
-};
-
-export default function RegistrationForm() {
-  const router = useRouter();
-
-  const {
-    register,
-    handleSubmit,
-    formState: { errors, isSubmitting },
-  } = useForm<RegisterInputs>({
-    mode: "onSubmit",
-    reValidateMode: "onChange",
-  });
-
-  const { showToast } = useCustomToast();
-
-  /**
-   * 
-   * 浏览器端 RegistrationForm.onSubmit
-      → 调用 createUser(data)  ← 这是一个 Server Action
-        → 发送 POST 请求到当前页面路由(或者由框架生成的端点)
-          → 服务端执行 createUser 函数(访问 cookies、session、数据库等)
-            → 返回结果给浏览器 
-   */
-  const onSubmit: SubmitHandler<RegisterInputs> = async (data) => {
-    console.log('register run where?--------');
-    if (data.password !== data.passwordConfirmation) {
-      showToast("The Passwords do not match.", "warning");
-      return;
-    }
-
-    await createUser(data)
-      .then((res) => {
-        if (res?.success) {
-          showToast("User created successfully", "success");
-          router.replace("/customer/login");
-        } else {
-          showToast(res?.error?.message || "Failed to create user", "warning");
-        }
-      })
-      .catch((error) => {
-        showToast(error.message || "An error occurred", "warning");
-      });
-  };
-
-  return (
-    <div className="mt-5 md:my-8 md:mt-0 flex w-full items-center w-full max-w-screen-2xl mx-auto px-4 xss:px-7.5 justify-between gap-0 md:gap-4 lg:my-16 xl:my-28">
-      <div className="relative flex w-full max-w-[583px] flex-col gap-y-4 lg:gap-y-12">
-        <div className="font-outfit">
-          <h2 className="py-1 text-2xl font-semibold sm:text-4xl">
-            Become User
-          </h2>
-          <p className="mt-2 text-base md:text-lg font-normal text-black/[60%] dark:text-neutral-400 sm:mt-2">
-            You are new to our store, we are glad to have you as a member.
-          </p>
-        </div>
-
-        <form
-          noValidate
-          className="flex flex-col gap-y-5 lg:gap-y-12"
-          onSubmit={handleSubmit(onSubmit)}
-        >
-          <div className="flex flex-col gap-y-2.5 lg:gap-[18px]">
-            <div className="flex w-full gap-2.5 lg:gap-[18px]">
-              <InputText
-                {...register("firstName", {
-                  required: "First name is required",
-                  pattern: {
-                    value: IS_VALID_INPUT,
-                    message: "Invalid First Name",
-                  },
-                })}
-                className="w-full"
-                errorMsg={
-                  errors.firstName?.message
-                    ? [errors.firstName.message]
-                    : undefined
-                }
-                label="First Name"
-                labelPlacement="outside"
-                name="firstName"
-                placeholder="Enter first name"
-                size="lg"
-              />
-              <InputText
-                {...register("lastName",
-                  {
-                    required: "Last name is required",
-                    pattern: {
-                      value: IS_VALID_INPUT,
-                      message: "Invalid Last Name",
-                    },
-                  })}
-                className="w-full"
-                errorMsg={
-                  errors.lastName?.message
-                    ? [errors.lastName.message]
-                    : undefined
-                }
-                label="Last Name"
-                labelPlacement="outside"
-                name="lastName"
-                placeholder="Enter last name"
-                size="lg"
-              />
-            </div>
-
-            <InputText
-              {...register("email", {
-                required: "Email is required",
-                pattern: {
-                  value: EMAIL_REGEX,
-                  message: "Please enter a valid email.",
-                },
-              })}
-              errorMsg={errors.email?.message}
-              label="Email"
-              labelPlacement="outside"
-              name="email"
-              placeholder="Enter email address"
-              size="lg"
-            />
-
-            <InputText
-              {...register("password", {
-                required: "Password is required",
-                minLength: {
-                  value: 8,
-                  message: "Must be at least 8 characters",
-                },
-                validate: (val) => {
-                  if (!/[A-Z]/.test(val))
-                    return "Must contain at least one uppercase letter";
-                  if (!/[a-z]/.test(val))
-                    return "Must contain at least one lowercase letter";
-                  if (!/[0-9]/.test(val))
-                    return "Must contain at least one number";
-                  if (/\s/.test(val)) return "Cannot contain spaces";
-
-                  return true;
-                },
-              })}
-              label="Password"
-              labelPlacement="outside"
-              name="password"
-              placeholder="Enter password"
-              typeName="password"
-              size="lg"
-              errorMsg={
-                errors.password?.message ? [errors.password.message] : undefined
-              }
-            />
-
-            <InputText
-              {...register("passwordConfirmation", {
-                required: "Please confirm your password",
-              })}
-              label="Confirm Password"
-              labelPlacement="outside"
-              name="passwordConfirmation"
-              placeholder="Enter confirm password"
-              size="lg"
-              typeName="password"
-            />
-          </div>
-
-          <div className="flex flex-col gap-y-3 mb-8 lg:mb-0">
-            <Button
-              disabled={isSubmitting}
-              loading={isSubmitting}
-              title="Sign Up"
-              type="submit"
-            />
-            <span className="mx-auto md:mx-0 font-outfit">
-              Already have an account?{" "}
-              <Link className="text-blue-600 underline" href="/customer/login" aria-label="Go to sign in page">
-                Sign In
-              </Link>
-            </span>
-          </div>
-        </form>
-      </div>
-
-      <div className="relative hidden aspect-[0.9] max-h-[692px] w-full max-w-[790px] sm:block md:aspect-[1.14]">
-        <Image
-          fill
-          priority
-          alt="Sign Up Image"
-          className="h-full w-full object-cover transition duration-300 ease-in-out group-hover:scale-105"
-          sizes="(min-width: 768px) 66vw, 100vw"
-          src={SIGNUP_IMG}
-        />
-      </div>
-    </div>
-  );
-}

+ 0 - 161
src/components/home/CategoryCarousel.tsx

@@ -1,161 +0,0 @@
-import { FC } from "react";
-import Link from "next/link";
-import clsx from "clsx";
-import { cachedGraphQLRequest } from "@/utils/hooks/useCache";
-import { GridTileImage } from "../theme/ui/grid/Tile";
-import { NOT_IMAGE } from "@/utils/constants";
-import { GET_HOME_CATEGORIES } from "@/graphql";
-import { CategoriesResponse } from "@/types/category/type";
-
-interface CategoryCarouselProps {
-  options: {
-    filters: Record<string, string | number | boolean | undefined>;
-  };
-}
-
-interface MobileCategoryItemProps {
-  category: any;
-  size: "full" | "half";
-  priority?: boolean;
-}
-
-const MobileCategoryItem: FC<MobileCategoryItemProps> = ({
-  category,
-  size,
-  priority,
-}) => {
-  return (
-    <div
-      className={
-        size === 'full' ? 'col-span-1 xxs:col-span-2 order-2' : 'col-span-1'
-      }
-    >
-      <Link
-        aria-label={`Shop ${category.translation.name} category`}
-        className={clsx(
-          "relative block h-full w-full aspect-[380/280]",
-          size === "half" && "xxs:aspect-[182/280]"
-        )}
-        href={`/category/${category.translation.slug}`}
-      >
-        <GridTileImage
-          fill
-          alt={`${category.translation.name} category image`}
-          className="relative h-full w-full object-cover transition duration-300 ease-in-out group-hover:scale-105"
-          label={{
-            position: "center",
-            title: category.translation.name || "",
-            page: "category",
-            amount: "0",
-            currencyCode: "USD",
-          }}
-          priority={priority}
-          sizes={
-            size === "full"
-              ? "100vw"
-              : "(min-width: 480px) 50vw, 100vw"
-          }
-          src={category.logoUrl || NOT_IMAGE}
-        />
-      </Link>
-    </div>
-  );
-};
-
-interface CategoryCarouselProps {
-  options: {
-    filters: Record<string, string | number | boolean | undefined>;
-  };
-}
-
-const CategoryCarousel: FC<CategoryCarouselProps> = async ({
-  options: _options,
-}) => {
-  
-    const {data} = await cachedGraphQLRequest<CategoriesResponse>(
-      "home",
-      GET_HOME_CATEGORIES,
-      {}
-    );
-
-    const categories =
-      data?.categories?.edges?.map((edge) => edge.node) || [];
-
-    const topCategories = categories
-      .filter((category) => category.id !== "1")
-      .sort((a, b) => (a.position || 0) - (b.position || 0))
-      .slice(1, 4);
-
-    if (!topCategories.length) return null;
-
-    return (
-      <section className="pt-8 sm:pt-12 lg:pt-20">
-        <div className="md:max-w-4.5xl mx-auto mb-10 w-auto text-center md:px-36">
-          <h2 className="mb-2 text-xl md:text-4xl  font-semibold">
-            Shop by Category
-          </h2>
-          <p className="text-sm md:text-base font-normal text-black/60 dark:text-neutral-300">
-            Discover the latest trends! Fresh products just added—shop new
-            styles, tech, and essentials before they&apos;re gone.
-          </p>
-        </div>
-        <div className="w-full overflow-x-auto overflow-y-hidden">
-          <div className="grid gap-4 grid-cols-1 xxs:grid-cols-2 lg:max-h-[calc(100vh-200px)] sm:hidden">
-            {topCategories.length > 0 && (
-              <MobileCategoryItem
-                category={topCategories[0]}
-                size="half"
-                priority={true}
-              />
-            )}
-            {topCategories.length > 1 && (
-              <MobileCategoryItem
-                category={topCategories[1]}
-                size="full"
-                priority={true}
-              />
-            )}
-            {topCategories.length > 2 && (
-              <MobileCategoryItem
-                category={topCategories[2]}
-                size="half"
-              />
-            )}
-          </div>
-
-          <ul className="m-0 hidden grid-cols-1 gap-7 p-0 xxs:grid-cols-2 sm:grid sm:grid-cols-3">
-            {topCategories.map((category) => (
-              <li
-                key={category.id}
-                className="relative aspect-498/665 h-full w-full max-w-[498px] flex-none overflow-hidden rounded-[18px]"
-              >
-                <Link
-                  className="relative h-full w-full"
-                  href={`/category/${category.translation.slug}`}
-                  aria-label={`Shop ${category.translation.name} category`}
-                >
-                  <GridTileImage
-                    fill
-                    alt={`${category.translation.name} category image`}
-                    className={
-                      "relative rounded-[18px] overflow-hidden object-cover transition duration-300 ease-in-out group-hover:scale-105"
-                    }
-                    label={{
-                      title: category.translation.name || "",
-                      page: "category",
-                      amount: "0",
-                      currencyCode: "USD",
-                    }}
-                    src={category.logoUrl || NOT_IMAGE}
-                  />
-                </Link>
-              </li>
-            ))}
-          </ul>
-        </div>
-      </section>
-    );
-
-};
-
-export default CategoryCarousel;

+ 0 - 84
src/components/home/ProductCarousel.tsx

@@ -1,84 +0,0 @@
-import { FC } from "react";
-import { cachedGraphQLRequest } from "@/utils/hooks/useCache";
-import { ThreeItemGrid } from "./ThreeItemGrid";
-import Theme from "./ProductCarouselTheme";
-import { GET_PRODUCTS } from "@/graphql";
-
-interface ProductCarouselProps {
-  options: {
-    title?: string;
-    filters: Record<string, any>;
-  };
-  itemCount?: number;
-  sortOrder?: number;
-}
-
-const ProductCarousel: FC<ProductCarouselProps> = async ({
-  options,
-  itemCount = 4,
-  sortOrder,
-}) => {
-  const { filters, title } = options;
-
-    const { sort, limit, ...rest } = filters || {};
-    const filterObject: Record<string, string> = {};
-    Object.entries(rest).forEach(([key, value]) => {
-      if (value !== undefined && value !== null) {
-        filterObject[key] = String(value);
-      }
-    });
-    const filterInput =
-      Object.keys(filterObject).length > 0
-        ? JSON.stringify(filterObject)
-        : undefined;
-
-    let sortKey = "CREATED_AT";
-    let reverse = true;
-
-    if (sort === "created_at-desc") {
-      sortKey = "CREATED_AT";
-      reverse = true;
-    } else if (sort === "price-desc") {
-      sortKey = "PRICE";
-      reverse = true;
-    }
-
-    const {data} = await cachedGraphQLRequest<any>(
-      "home",
-      GET_PRODUCTS,
-      {
-        sortKey,
-        filter: filterInput,
-        first: limit ? parseInt(limit, 10) : itemCount,
-        reverse,
-      }
-    );
-
-    const products =
-      data?.products?.edges?.slice(0, 8).map((edge: any) => edge.node) || [];
-
-    if (!products.length) {
-      return null;
-    }
-
-    if (sortOrder === 2) {
-      return (
-        <ThreeItemGrid
-          title={title || "Products"}
-          description="Discover the latest trends! Fresh products just added—shop new styles, tech, and essentials before they're gone."
-          products={products.slice(0, 3)}
-        />
-      );
-    }
-
-    return (
-      <Theme
-        title={title || "Products"}
-        description="Discover the latest trends! Fresh products just added—shop new styles, tech, and essentials before they're gone."
-        products={products}
-      />
-    );
-
-};
-
-export default ProductCarousel;

+ 0 - 68
src/components/home/RenderThemeCustomization.tsx

@@ -1,68 +0,0 @@
-import { FC, Suspense } from "react";
-import { safeParse } from "@utils/helper";
-import { CategoryCarouselOptions, ProductCarouselOptions, ThemeCustomizationResponse } from "@/types/theme/theme-customization";
-import ImageCarousel from "./ImageCarousel";
-import ProductCarousel from "./ProductCarousel";
-import CategoryCarousel from "./CategoryCarousel";
-import { MobileSearchBar } from "@components/layout/navbar/MobileSearch";
-import { CategoryCarouselSkeleton } from "@components/common/skeleton/CategoryCarouselSkeleton";
-import { ThemeSkeleton } from "@components/common/skeleton/ThemeSkeleton";
-import { ThreeItemGridSkeleton } from "@components/theme/ui/grid/ThreeItemsSkeleton";
-
-interface RenderThemeCustomizationProps {
-    themeCustomizations: ThemeCustomizationResponse['themeCustomizations'];
-}
-
-const RenderThemeCustomization: FC<RenderThemeCustomizationProps> = ({ themeCustomizations }) => {
-    if (!themeCustomizations?.edges?.length) return null;
-
-    let productCarouselIndex = 0;
-
-    const sortedEdges = [...themeCustomizations.edges].sort((a, b) =>
-        (a.node.sortOrder || 0) - (b.node.sortOrder || 0)
-    );
-
-    return (
-        <>
-            <MobileSearchBar />
-            <section className="w-full max-w-screen-2xl mx-auto pb-4 px-4 xss:px-7.5">
-                {sortedEdges.map(({ node }) => {
-                    const translation = node.translations.edges.find(e => e.node.locale === 'en') || node.translations.edges[0];
-                    if (!translation) return null;
-
-                    const options = safeParse(translation.node.options) || {};
-                    if (Object.keys(options).length === 0) {
-                        console.error("Error parsing options for", node.type);
-                    }
-
-                    switch (node.type) {
-                        case "image_carousel":
-                            return <ImageCarousel key={node.id} options={options as any} />;
-                        case "product_carousel": {
-                            productCarouselIndex++;
-                            const opts = options as ProductCarouselOptions;
-                            const limit = opts?.filters?.limit ? parseInt(String(opts.filters.limit), 10) : null;
-                            const itemCount = limit || (productCarouselIndex === 1 ? 3 : 4);
-                            const fallback = node.sortOrder === 2 ? <ThreeItemGridSkeleton /> : <ThemeSkeleton />;
-                            return (
-                                <Suspense key={node.id} fallback={fallback}>
-                                    <ProductCarousel key={node.id} options={{ ...options, title: node.name } as ProductCarouselOptions} itemCount={itemCount} sortOrder={node?.sortOrder} />
-                                </Suspense>
-                            );
-                        }
-                        case "category_carousel":
-                            return (
-                                <Suspense key={node.id} fallback={<CategoryCarouselSkeleton />}>
-                                    <CategoryCarousel options={options as CategoryCarouselOptions} />
-                                </Suspense>
-                            );
-                        default:
-                            return null;
-                    }
-                })}
-            </section>
-        </>
-    );
-};
-
-export default RenderThemeCustomization;

+ 0 - 146
src/components/home/ThreeItemGrid.tsx

@@ -1,146 +0,0 @@
-"use client"
-
-import { FC } from "react";
-import Link from "next/link";
-import clsx from "clsx";
-import { GridTileImage } from "@/components/theme/ui/grid/Tile";
-
-interface ThreeItemGridProps {
-    title: string;
-    description: string;
-    products: Array<{
-        id: string;
-        name: string;
-        urlKey: string;
-        baseImageUrl: string;
-        price: string | number;
-        minimumPrice?: string | number;
-        type: string;
-    }>;
-}
-
-interface ProductItem {
-    id: string;
-    name: string;
-    urlKey: string;
-    baseImageUrl: string;
-    price: string | number;
-    minimumPrice?: string | number;
-    type: string;
-}
-
-function ThreeItemGridItem({ product, size, priority }: {
-    product: ProductItem;
-    size: 'full' | 'half';
-    priority?: boolean;
-}) {
-    return (
-        <div
-            className={
-                size === 'full'
-                    ? 'md:col-span-4 md:row-span-2'
-                    : 'md:col-span-2 md:row-span-1'
-            }
-        >
-            <Link
-                className="relative block h-full w-full"
-                href={`/product/${product.urlKey}`}
-                aria-label={`${product?.name}`}
-                style={{
-                    aspectRatio: size === 'full' ? '1018 / 800' : '502 / 393'
-                }}
-            >
-                <GridTileImage
-                    src={product.baseImageUrl}
-                    className="object-cover "
-                    fill
-                    sizes={
-                        size === 'full'
-                            ? '(min-width: 768px) 66vw, 100vw'
-                            : '(min-width: 768px) 33vw, 100vw'
-                    }
-                    priority={priority}
-                    alt={product.name}
-                    label={{
-                        position: size === 'full' ? 'center' : 'bottom',
-                        title: product.name,
-                        amount: String(product.type === 'configurable' ? (product.minimumPrice || '0') : (product.price || '0')),
-                        currencyCode: 'USD',
-                    }}
-                />
-            </Link>
-        </div>
-    );
-}
-
-
-function MobileThreeItemGridItem({ product, size, priority }: {
-    product: ProductItem;
-    size: 'full' | 'half';
-    priority?: boolean;
-}) {
-
-    return (
-        <div
-            className={
-                size === 'full' ? 'col-span-1 xxs:col-span-2 order-2' : 'col-span-1'
-            }
-        >
-            <Link
-                className={clsx(
-                    "relative block h-full w-full aspect-[380/280]",
-                    size === "half" && "xxs:aspect-[182/280]"
-                )}
-                href={`/product/${product.urlKey}`}
-                aria-label={`${product?.name}`}
-            >
-                <GridTileImage
-                    src={product.baseImageUrl}
-                    className="object-cover "
-                    fill
-                    priority={priority}
-                    alt={product.name}
-                    label={{
-                        position: size === 'full' ? 'center' : 'bottom',
-                        title: product.name,
-                        amount: String(product.type === 'configurable' ? (product.minimumPrice || '0') : (product.price || '0')),
-                        currencyCode: 'USD',
-                    }}
-                />
-            </Link>
-        </div>
-    );
-}
-
-export const ThreeItemGrid: FC<ThreeItemGridProps> = ({ title, description, products }) => {
-    if (!products || products.length < 3) return null;
-
-    const [firstProduct, secondProduct, thirdProduct] = products;
-
-    return (
-        <section className="pt-8 sm:pt-12 lg:pt-20">
-            <div className="md:max-w-4.5xl mx-auto mb-10 w-auto px-0 text-center md:px-36">
-                <h1 className="mb-4 font-outfit text-xl md:text-4xl font-semibold text-black dark:text-white">
-                    {title}
-                </h1>
-                <p className="text-sm md:text-base font-normal text-black/60 dark:text-neutral-300">
-                    {description}
-                </p>
-            </div>
-
-            <div className="hidden md:grid gap-4 md:grid-cols-6 md:grid-rows-2 lg:max-h-[calc(100vh-200px)]">
-                <ThreeItemGridItem product={firstProduct} size="full" priority={true} />
-                <ThreeItemGridItem product={secondProduct} size="half" priority={true} />
-                <ThreeItemGridItem product={thirdProduct} size="half" />
-            </div>
-
-            <div className="grid md:hidden gap-4 grid-cols-1 xxs:grid-cols-2 lg:max-h-[calc(100vh-200px)]">
-                <MobileThreeItemGridItem product={firstProduct} size="full" priority={true} />
-                <MobileThreeItemGridItem product={secondProduct} size="half" priority={true} />
-                <MobileThreeItemGridItem product={thirdProduct} size="half" />
-            </div>
-        </section>
-    );
-};
-
-export default ThreeItemGrid;

+ 0 - 32
src/components/theme/product-carousel/index.tsx

@@ -1,32 +0,0 @@
-import { FC } from "react";
-import { isObject } from "@/utils/type-guards";
-import { FilterDataTypes } from "@/types/types";
-
-
-interface ProductCarouselProps {
-  name: string;
-  data: {
-    options: {
-      filters: FilterDataTypes[];
-    };
-  }[];
-  sortOrder: string;
-}
-
-const ProductCarousel: FC<ProductCarouselProps> = async ({
-  data,
-}) => {
-  const options = isObject(data?.[0]) ? data[0].options : null;
-
-  if (!options) {
-    return null;
-  }
-
-
-  return (
-     <div className="flex flex-col items-center gap-y-6">
-     </div>
-  );
-};
-
-export default ProductCarousel;

+ 0 - 52
src/components/theme/product-carousel/theme.tsx

@@ -1,52 +0,0 @@
-import { FC } from "react";
-import { NOT_IMAGE } from "@/utils/constants";
-import { ProductCard } from "@/components/catalog/product/ProductCard";
-import { BagistoProductInfo } from "@/types/types";
-
-const Theme: FC<{
-  products: BagistoProductInfo[];
-  name: string;
-}> = ({ products, name }) => {
-  return (
-    <section>
-      <div className="md:max-w-4.5xl mx-auto mb-6 w-full px-0 text-center xss:mb-10 md:px-36">
-        <h2 className="mb-2 text-[28px] font-semibold text-black dark:text-white xss:mb-4 xss:text-4xl">
-          {name}
-        </h2>
-        <p className="font-normal text-black/60 dark:text-neutral-300 text-lg">
-          Discover the latest trends! Fresh products just added—shop new styles,
-          tech, and essentials before they&apos;re gone.
-        </p>
-      </div>
-
-      <div className="w-full pb-6 pt-1">
-        <ul className="m-0 grid grid-cols-2 justify-center gap-6 p-0 xss:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 lg:gap-6 xl:gap-[46px]">
-          {products.map((product, index) => {
-            const imageUrl =
-              product?.cacheGalleryImages?.[0]?.originalImageUrl ??
-              product?.images?.[0]?.url ??
-              NOT_IMAGE;
-            const price =
-              product?.priceHtml?.finalPrice ||
-              product?.priceHtml?.regularPrice ||
-              "0";
-            const currency = product?.priceHtml?.currencyCode;
-
-            return (
-              <ProductCard
-                key={index}
-                currency={currency}
-                imageUrl={imageUrl}
-                price={price}
-                product={product}
-                priority={index < 4}
-              />
-            );
-          })}
-        </ul>
-      </div>
-    </section>
-  );
-};
-
-export default Theme;

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

@@ -12,20 +12,26 @@ export default function CommonModal({
     isOpen,
     destroyOnClose = false,
     contentClassName,
+    contentStyle = {},
     clickOutsideClose,
     onClose,
+    animationEndCallback = () => {},
     header,
     body,
-    footer
+    footer,
+    zIndex,
 }: {
     isOpen: boolean;
     destroyOnClose?: boolean;
     contentClassName?: string;
+    contentStyle?: React.CSSProperties;
     clickOutsideClose?: boolean;
     onClose: (e: boolean) => void;
+    animationEndCallback?: (flag: string) => void; 
     header?: React.ReactNode;
     body: React.ReactNode;
     footer?: React.ReactNode;
+    zIndex?: string;
 }) {
     // show - 显示
     // outing - 隐藏动画中
@@ -118,12 +124,17 @@ export default function CommonModal({
     }
     // 如果是关闭,动画结束之后才隐藏
     const handlerAnimationEnd = (e: React.AnimationEvent<HTMLDivElement>) => {
+        // 如果子元素上也有自己的动画效果,也会触发父元素的动画事件
+        if (e.target !== e.currentTarget) return;
         if(e.animationName === "fade-out") {
             setRootVisible('hidden');
             if(destroyOnClose){
                 setMounted(false);
                 cssInit.current=false;
             }
+            animationEndCallback('close');
+        } else {
+            animationEndCallback('show');
         }
     }
     // 初始化时是隐藏状态
@@ -136,7 +147,9 @@ export default function CommonModal({
         <Portal>
             <div 
                 ref={rootRef} 
-                className={clsx("w-full h-full backdrop-blur-md backdrop-saturate-150 bg-black/30 fixed inset-0 z-50 transition-all ease-in-out",{
+                className={clsx(
+                    "w-full h-full backdrop-blur-md backdrop-saturate-150 bg-black/30 fixed inset-0 transition-all ease-in-out",
+                    (zIndex ? zIndex : "z-50"),{
                     "block fade-in": rootVisible === "show",
                     "hidden": rootVisible === "hidden",
                     "fade-out": rootVisible === "outing"
@@ -152,6 +165,7 @@ export default function CommonModal({
                         }
                     )}
                     style={{
+                        ...contentStyle,
                         paddingTop: rootPddingTop,
                         paddingBottom: rootPddingBottom
                     }}

+ 4 - 1
src/components/theme/ui/Select.tsx

@@ -12,7 +12,8 @@ export default function Select({
     onBlur,
     name,
     error,
-    ref
+    ref,
+    autoComplete = "off"
 }: {
     placeholder?: string;
     options: Option[];
@@ -21,6 +22,7 @@ export default function Select({
     name: string;
     error?: string;
     ref?: React.Ref<HTMLSelectElement>;
+    autoComplete?: string;
 }) {
 
     return (
@@ -30,6 +32,7 @@ export default function Select({
                 onBlur={onBlur}
                 name={name}
                 ref={ref}
+                autoComplete={autoComplete}
             >
                 {placeholder && <option value="">{placeholder}</option>}
                 {options.map((option) => (

+ 0 - 58
src/components/theme/ui/grid/ThreeItemGrid.tsx

@@ -1,58 +0,0 @@
-import clsx from "clsx";
-import Link from "next/link";
-
-import { NOT_IMAGE } from "@/utils/constants";
-import { ProductDetailsInfo } from "@/types/types";
-import { GridTileImage } from "./Tile";
-export default function ThreeItemGridItem({
-  item,
-  size,
-  priority,
-}: {
-  item: ProductDetailsInfo;
-  size: "full" | "half";
-  priority?: boolean;
-}) {
-  return (
-    <div
-      className={
-        size === "full"
-          ? "md:col-span-4 md:row-span-2"
-          : "md:col-span-2 md:row-span-1"
-      }
-    >
-      <Link
-        aria-label={`${item?.name}`}
-        className="relative block aspect-square h-full w-full"
-        href={`/product/${item.urlKey}?type=${item.type}`}
-      >
-        <GridTileImage
-          fill
-          alt={item?.name || "Images"}
-          className={clsx(
-            "relative h-full w-full object-cover",
-            "transition duration-300 ease-in-out group-hover:scale-105"
-          )}
-          label={{
-            position: size === "full" ? "left" : "center",
-            title: item?.name as string,
-            amount:
-              item.priceHtml?.finalPrice || item.priceHtml?.regularPrice || "0",
-            currencyCode: item.priceHtml?.currencyCode,
-          }}
-          priority={priority}
-          sizes={
-            size === "full"
-              ? "(min-width: 768px) 66vw, 100vw"
-              : "(min-width: 768px) 33vw, 100vw"
-          }
-          src={
-            item?.cacheGalleryImages?.[0]?.originalImageUrl ??
-            item?.images?.[0]?.url ??
-            NOT_IMAGE
-          }
-        />
-      </Link>
-    </div>
-  );
-}

+ 0 - 23
src/components/theme/ui/grid/ThreeItemsSkeleton.tsx

@@ -1,23 +0,0 @@
-
-export const ThreeItemGridSkeleton = () => {
-  return (
-    <section className="pt-8 sm:pt-12 lg:pt-20">
-      <div className="md:max-w-4.5xl mx-auto mb-10 w-auto px-0 text-center md:px-36">
-        <div className="mb-4 h-10 w-2/3 mx-auto animate-pulse rounded bg-gray-200 dark:bg-neutral-800 md:h-12" />
-        <div className="h-5 w-full max-w-xl mx-auto animate-pulse rounded bg-gray-200 dark:bg-neutral-800" />
-      </div>
-
-      <div className="hidden md:grid gap-4 md:grid-cols-6 md:grid-rows-2 lg:max-h-[calc(100vh-200px)]">
-        <div className="md:col-span-4 md:row-span-2 animate-pulse rounded-lg bg-gray-200 dark:bg-neutral-800" style={{ aspectRatio: '1018 / 800' }} />
-        <div className="md:col-span-2 md:row-span-1 animate-pulse rounded-lg bg-gray-200 dark:bg-neutral-800" style={{ aspectRatio: '502 / 393' }} />
-        <div className="md:col-span-2 md:row-span-1 animate-pulse rounded-lg bg-gray-200 dark:bg-neutral-800" style={{ aspectRatio: '502 / 393' }} />
-      </div>
-
-      <div className="grid md:hidden gap-4 grid-cols-1 xxs:grid-cols-2 lg:max-h-[calc(100vh-200px)]">
-        <div className="col-span-1 animate-pulse rounded-lg bg-gray-200 dark:bg-neutral-800 aspect-[380/280] xxs:aspect-[182/280]" />
-        <div className="col-span-1 animate-pulse rounded-lg bg-gray-200 dark:bg-neutral-800 aspect-[380/280] xxs:aspect-[182/280]" />
-        <div className="col-span-1 xxs:col-span-2 animate-pulse rounded-lg bg-gray-200 dark:bg-neutral-800 aspect-[380/280]" />
-      </div>
-    </section>
-  );
-};

+ 6 - 4
src/graphql/checkout/mutations/CreateCancelOrder.ts

@@ -1,13 +1,15 @@
 import { gql,TypedDocumentNode } from "@apollo/client";
-import { CreateCancelOrderData } from "@/types/checkout/type";
+import { CreateCancelOrderData,CreateCancelOrderVariables } from "@/types/checkout/type";
 
-export const CREATE_CANCEL_ORDER: TypedDocumentNode<CreateCancelOrderData> = gql`
+export const CREATE_CANCEL_ORDER: TypedDocumentNode<CreateCancelOrderData,CreateCancelOrderVariables> = gql`
   mutation CreateCancelOrder(
-      $orderId: Int
+      $orderId: Int!
+      $userManual: Boolean
   ) {
       createCancelOrder(
         input: {
-          orderId: $orderId
+          orderId: $orderId,
+          userManual: $userManual
         }
       ) {
           cancelOrder {

+ 23 - 0
src/graphql/customer/mutations/ResetPassword.ts

@@ -0,0 +1,23 @@
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { ResetPasswordData, ResetPasswordVariables } from "@/types/customer/type";
+
+
+export const RESET_PASSWORD: TypedDocumentNode<ResetPasswordData,ResetPasswordVariables> = gql`
+mutation createResetPassword(
+    $token: String!,
+    $email: String!,
+    $password: String!,
+    $passwordConfirmation: String!
+) { 
+    createResetPassword(input: { 
+        token: $token,
+        email: $email,
+        password: $password,
+        passwordConfirmation: $passwordConfirmation
+    }) { 
+        resetPassword { 
+            success 
+            message 
+        } 
+    } 
+}`;

+ 2 - 1
src/graphql/customer/mutations/index.ts

@@ -2,4 +2,5 @@ export { CUSTOMER_REGISTRATION} from "./CustomerRegistration";
 export { CUSTOMER_LOGIN} from "./CustomerLogin";
 export { CUSTOMER_LOGOUT} from "./CustomerLogout";
 export { VERIFY_CUSTOMER} from "./VerifyCustomer";
-export {FORGET_PASSWORD} from "./ForgetPassword"
+export {FORGET_PASSWORD} from "./ForgetPassword";
+export {RESET_PASSWORD} from "./ResetPassword";

+ 111 - 0
src/graphql/customer/query/GetCustomerOrders.ts

@@ -0,0 +1,111 @@
+
+import { gql,TypedDocumentNode } from "@apollo/client";
+import { OrderListData, OrderListDataVariables } from "@/types/customer/type";
+
+export const GET_CUSTOMER_ORDERS: TypedDocumentNode<OrderListData,OrderListDataVariables> = gql`
+query GetCustomerOrders(
+    $status:String,
+    $first:Int, 
+    $after: String, 
+    $last:Int, 
+    $before: String
+) {
+    customerOrders(
+        status: $status,
+        first: $first, 
+        after: $after,
+        last: $last,
+        before: $before
+    ) {
+        pageInfo {
+          endCursor
+          startCursor
+          hasNextPage
+          hasPreviousPage
+        }
+        totalCount
+        edges {
+            cursor
+            node {
+                incrementId
+                id
+                _id
+                status
+                channelName
+                customerEmail
+                customerFirstName
+                customerLastName
+                payment {
+                    method
+                    methodTitle
+                }
+                shippingMethod
+                shippingTitle
+                couponCode
+                totalItemCount
+                totalQtyOrdered
+                grandTotal
+                baseGrandTotal
+                grandTotalInvoiced
+                grandTotalRefunded
+                subTotal
+                baseSubTotal
+                taxAmount
+                baseTaxAmount
+                discountAmount
+                baseDiscountAmount
+                shippingAmount
+                baseShippingAmount
+                vipPlusAmount
+                vipDiscountAmount
+                baseVipPlusAmount
+                baseVipDiscountAmount
+                rewardPointsUsed    
+                rewardPointsAmount
+                baseRewardPointsAmount
+                rewardPointsInvoiced
+                baseRewardPointsInvoiced
+                rewardPointsEarned
+                shippingInsuranceAmount
+                baseShippingInsuranceAmount
+                shippingInsuranceInvoiced
+                baseShippingInsuranceInvoiced
+                giftcardNumber
+                giftcardAmount
+                baseGiftcardAmount
+                baseCurrencyCode
+                channelCurrencyCode
+                orderCurrencyCode
+                items {
+                    edges {
+                        node {
+                            id
+                            sku
+                            type
+                            name
+                            baseImage
+                            additional
+                            qtyOrdered
+                            qtyShipped
+                            qtyInvoiced
+                            qtyCanceled
+                            qtyRefunded
+                            price
+                            basePrice
+                            total
+                            baseTotal
+                            discountPercent
+                            discountAmount
+                            baseDiscountAmount
+                        }
+                    }
+                }
+
+                createdAt
+                updatedAt
+        
+            }
+        }
+    }
+}
+`

+ 1 - 0
src/graphql/customer/query/GetOrderDetails.ts

@@ -61,6 +61,7 @@ export const GET_ORDER_DETAILS: TypedDocumentNode<OrderDetailsData> = gql`
           sku
           type
           name
+          baseImage
           additional
           qtyOrdered
           qtyShipped

+ 2 - 1
src/graphql/customer/query/index.ts

@@ -1,4 +1,5 @@
 export {
     GET_ORDER_DETAILS
 } from "./GetOrderDetails";
-export {GET_CUSTOMER_ADDRESS} from "./GetCustomerAddress";
+export {GET_CUSTOMER_ADDRESS} from "./GetCustomerAddress";
+export {GET_CUSTOMER_ORDERS} from "./GetCustomerOrders";

+ 1 - 1
src/graphql/index.ts

@@ -8,5 +8,5 @@ export * from "./checkout/mutations";
 export * from "./customer/query/index";
 export * from "./customer/mutations/index";
 export * from "./currency/query/index";
-export * from "./types";
+
 

+ 0 - 17
src/graphql/types/index.ts

@@ -1,17 +0,0 @@
-export type {
-  PageInfo,
-  AttributeValue,
-  SuperAttribute,
-  ProductVariant,
-  ProductCore,
-  ProductDetailed,
-  ProductEdge,
-  ProductConnection,
-  GetProductsVariables,
-  GetProductByIdVariables,
-  GetRelatedProductsVariables,
-  GetProductsResponse,
-  GetProductByIdResponse,
-  GetRelatedProductsResponse,
-  GetProductsPaginationResponse,
-} from "./product.types";

+ 0 - 170
src/graphql/types/product.types.ts

@@ -1,170 +0,0 @@
-/**
- * GraphQL Type Definitions for Products
- * These types match the GraphQL schema and provide type safety
- */
-
-export interface PageInfo {
-  startCursor: string | null;
-  endCursor: string | null;
-  hasNextPage: boolean;
-  hasPreviousPage: boolean;
-}
-
-export interface AttributeValue {
-  id: string;
-  locale?: string;
-  channel?: string;
-  value?: string;
-  textValue?: string;
-  attribute: {
-    id: string;
-    code: string;
-  };
-}
-
-export interface SuperAttribute {
-  id: string;
-  code: string;
-}
-
-export interface ProductVariant {
-  id: string;
-  sku: string;
-  type: string;
-  name?: string;
-  price?: number;
-  attributeValues: {
-    edges: Array<{
-      node: AttributeValue;
-    }>;
-  };
-}
-
-export interface ProductCore {
-  id: string;
-  _id: string;
-  sku: string;
-  type: string;
-  name: string;
-  urlKey: string;
-  status: string;
-  price: number;
-  specialPrice?: number;
-  baseImageUrl: string;
-  new: boolean;
-  featured: boolean;
-  visibleIndividually: boolean;
-}
-
-export interface ProductDetailed extends ProductCore {
-  createdAt: string;
-  updatedAt: string;
-  description?: string;
-  shortDescription?: string;
-  descriptionHtml?: string;
-  weight?: number;
-  productNumber?: string;
-  guestCheckout?: boolean;
-  manageStock?: boolean;
-  metaTitle?: string;
-  metaKeywords?: string;
-  taxCategoryId?: string;
-  specialPriceFrom?: string;
-  specialPriceTo?: string;
-  superAttributes: {
-    edges: Array<{
-      node: SuperAttribute;
-    }>;
-  };
-  attributeValues: {
-    edges: Array<{
-      node: AttributeValue;
-    }>;
-  };
-  variants?: {
-    edges: Array<{
-      node: ProductVariant;
-    }>;
-  };
-  upSells?: {
-    edges: Array<{
-      node: ProductCore;
-    }>;
-  };
-  crossSells?: {
-    edges: Array<{
-      node: ProductCore;
-    }>;
-  };
-  relatedProducts?: {
-    edges: Array<{
-      node: ProductCore;
-    }>;
-  };
-}
-
-export interface ProductEdge {
-  node: ProductDetailed;
-}
-
-export interface ProductConnection {
-  totalCount: number;
-  pageInfo: PageInfo;
-  edges: ProductEdge[];
-}
-
-// Query Variables Types
-export interface GetProductsVariables {
-  query?: string;
-  sortKey?: string;
-  reverse?: boolean;
-  first?: number;
-  after?: string;
-  before?: string;
-  channel?: string;
-  locale?: string;
-}
-
-export interface GetProductByIdVariables {
-  id: string;
-}
-
-export interface GetRelatedProductsVariables {
-  id: string;
-  first?: number;
-}
-
-// Query Response Types
-export interface GetProductsResponse {
-  products: ProductConnection;
-}
-
-export interface GetProductByIdResponse {
-  product: ProductDetailed;
-}
-
-export interface GetRelatedProductsResponse {
-  product: {
-    id: string;
-    _id: string;
-    sku: string;
-    name: string;
-    relatedProducts: {
-      edges: Array<{
-        node: ProductCore;
-      }>;
-    };
-  };
-}
-
-export interface GetProductsPaginationResponse {
-  products: {
-    totalCount: number;
-    pageInfo: PageInfo;
-    edges: Array<{
-      node: {
-        id: string;
-      };
-    }>;
-  };
-}

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

@@ -52,31 +52,6 @@ export function getRevalidateTime(
   }
 }
 
-
-
-
-
-export function stableStringify(value: unknown): string {
-  if (value === null || typeof value !== "object") {
-    return JSON.stringify(value);
-  }
-
-  if (Array.isArray(value)) {
-    return `[${value.map(stableStringify).join(",")}]`;
-  }
-
-  const obj = value as Record<string, unknown>;
-  return `{${Object.keys(obj)
-    .sort()
-    .map(
-      (key) => `"${key}":${stableStringify(obj[key])}`
-    )
-    .join(",")}}`;
-}
-
-
-
-
 export interface GraphQLRequestOptions {
   tags?: string[];
   life?: CacheLifeOption;
@@ -158,25 +133,3 @@ export async function graphqlRequest<
   }
 }
 
-
-
-export async function graphqlRequestNoCache<
-  TData = unknown,
-  TVariables extends OperationVariables = OperationVariables
->(
-  query: DocumentNode,
-  variables?: TVariables,
-  options?: Omit<
-    GraphQLRequestOptions,
-    "noCache" | "tags" | "life"
-  >
-): Promise<GraphqlRequestResult<TData>> {
-  return graphqlRequest<TData, TVariables>(
-    query,
-    variables,
-    {
-      ...options,
-      noCache: true,
-    }
-  );
-}

+ 1 - 1
src/lib/restApiClient.ts

@@ -1,7 +1,7 @@
 'use client';
 /**前端客户端组件调rest api 接口 */
 import { emitAuthExpired } from "@/utils/auth/auth-events";
-export async function clientFetch<T = any>(apiUrl: string, options: RequestInit = {}): Promise<T> {
+export async function clientFetch<T = any>(apiUrl: string | URL, options: RequestInit = {}): Promise<T> {
     // 请求的是nextjs的代理接口
 
     const headers = {

+ 2 - 0
src/providers/LoginModalProvider.tsx

@@ -11,6 +11,7 @@ import {
 import LoginModal, {LoginModalReult} from "@/components/common/LoginModal/LoginModal";
 
 type LoginModalOptions = {
+    defaultEmail?: string;
     callbackUrl?: string;
     onFinalResult?:(param:LoginModalReult)=>void | Promise<void>;
 };
@@ -75,6 +76,7 @@ export function LoginModalProvider({
                 closeModal={closeLoginModal}
                 callbackUrl={modalOptions?.callbackUrl}
                 onFinalResult={ modalOptions?.onFinalResult }
+                defaultEmail={modalOptions?.defaultEmail}
             />
 
 

+ 1 - 1
src/server-service/guestCartTokenService.ts

@@ -65,7 +65,7 @@ export async function createGuestCartToken() {
         let guestToken = null;
         let guestCartId = null;
         const msg = response.error ? response.error.message : '';
-        if(success && response.data.createCartToken) {
+        if(success && response.data?.createCartToken) {
             guestToken = response.data.createCartToken.cartToken.sessionToken;
             guestCartId = response.data.createCartToken.cartToken.id;
 

+ 1 - 1
src/types/cart/type.ts

@@ -106,7 +106,7 @@ export interface CreateReadCart {
 
 // Mutation response data
 export interface GetCartItemData {
-  createReadCart: CreateReadCart;
+  createReadCart: CreateReadCart | null;
 }
 
 export interface ReadCartOperation {

+ 0 - 31
src/types/category/type.ts

@@ -41,40 +41,9 @@ export interface ProductReviewEdge {
   node: ProductReview;
 }
 
-export interface ProductVariant {
-  id: string;
-  sku: string;
-  baseImageUrl: string;
-}
 
-export interface ProductVariantEdge {
-  node: ProductVariant;
-}
 
-export interface ProductNode {
-  id: string;
-  sku: string;
-  type: string;
-  name: string;
-  urlKey: string;
-  description?: string | null;
-  shortDescription?: string | null;
-  price: number | string;
-  baseImageUrl?: string | null;
-  minimumPrice?: number | string | null;
-
-  variants?: {
-    edges: ProductVariantEdge[];
-  };
 
-  reviews?: {
-    edges: ProductReviewEdge[];
-  };
-}
-
-export interface SingleProductResponse {
-  product: ProductNode;
-}
 
 // Product Swatch Review Types
 export interface SuperAttributeOption {

+ 1 - 0
src/types/checkout/type.ts

@@ -345,6 +345,7 @@ export interface CreateCancelOrderData {
 }
 export interface CreateCancelOrderVariables {
   orderId: number;
+  userManual?: boolean; //userManual 区分是否是用户直接主动取消订单
 }
 
 

+ 25 - 1
src/types/customer/order.ts

@@ -3,6 +3,7 @@ export interface ProductsInOrderDetails {
     sku: string;
     type: string;
     name: string;
+    baseImage: string;
     additional: string;
     qtyOrdered: number;
     qtyShipped: number;
@@ -134,5 +135,28 @@ export interface OrderDetails {
 }
 
 export interface OrderDetailsData {
-  customerOrder: OrderDetails;
+  customerOrder: OrderDetails | null;
+}
+
+export interface OrderListDataVariables {
+    status?: string;
+    first?: number;
+    after?: string;
+    last?: number;
+    before?: string;
+}
+export interface OrderListData {
+    customerOrders: {
+        pageInfo: {
+          endCursor: string;
+          startCursor: string;
+          hasNextPage: boolean;
+          hasPreviousPage: boolean;
+        };
+        totalCount: number;
+        edges: {
+            cursor: string;
+            node: Omit<OrderDetails, 'addresses'> ;
+        }[];
+    }
 }

+ 40 - 0
src/types/customer/type.ts

@@ -4,12 +4,52 @@ export type {
     ProductsInOrderDetails,
     OrderDetails, 
     OrderDetailsData, 
+    OrderListData,
+    OrderListDataVariables,
     ProductItemAdditional,
     AddressListInOrderDetails,
     AddressInOrderDetails,
     OrderAddressType
 } from "./order";
 
+export interface CheckEmailRegisteredData {
+    success: boolean;
+    message: string;
+    data:{
+        email: string;
+        exists: boolean;
+    }
+}
+export interface ResetPasswordData{
+    createResetPassword: {
+      resetPassword: {
+        success: boolean;
+        message: string;
+      }
+    }
+}
+export interface ResetPasswordVariables {
+    token: string;
+    email: string;
+    password: string;
+    passwordConfirmation: string;
+}
+
+export interface LoginFormData {
+    username:string;
+    password:string;
+}
+export interface RegisterFormData {
+    firstName: string;
+    lastName: string;
+    email: string;
+    password: string;
+    confirmPassword: string;
+    phone: string;
+    subscribedToNewsLetter: boolean;
+    isAgree: boolean;
+}
+
 export interface CustomerLoginData{
     id: string;
     apiToken: string;

+ 1 - 1
src/types/graphqlFetch/type.ts

@@ -14,6 +14,6 @@ export interface FetchGraphqlError {
 }
 export interface FetchGraphqlResult<TData = unknown> {
   status: number;
-  data: TData;
+  data: TData | null;
   error: FetchGraphqlError | null;
 }

+ 0 - 0
src/types/theme/theme-customization.ts


Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott