fogwind vor 2 Wochen
Ursprung
Commit
7bc3c31f97
76 geänderte Dateien mit 1098 neuen und 1409 gelöschten Zeilen
  1. 1 1
      .env.example
  2. BIN
      public/image/footer-payment.webp
  3. BIN
      public/image/logo-white.webp
  4. BIN
      public/image/logo.webp
  5. BIN
      public/image/placeholder.webp
  6. 23 6
      src/actions/addProductToCartAction.ts
  7. 2 1
      src/actions/index.ts
  8. 41 0
      src/actions/subscribeNewsletterAction.tsx
  9. 1 1
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  10. 1 1
      src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx
  11. 3 3
      src/app/(checkout)/checkout/_components/LoadingPaymentMethod.tsx
  12. 48 0
      src/app/(checkout)/checkout/cart/loading.tsx
  13. 99 0
      src/app/(checkout)/checkout/continuetopay/loading.tsx
  14. 96 0
      src/app/(checkout)/checkout/loading.tsx
  15. 15 1
      src/app/(public)/(cms)/[slug]/page.tsx
  16. 11 0
      src/app/(public)/(cms)/loading.tsx
  17. 0 0
      src/app/(public)/(cms)/track_order/_components/TrackFaqs.tsx
  18. 0 0
      src/app/(public)/(cms)/track_order/_components/TrackOrder.tsx
  19. 0 0
      src/app/(public)/(cms)/track_order/page.tsx
  20. 45 0
      src/app/(public)/category/[slug]/[id]/loading.tsx
  21. 2 4
      src/app/(public)/category/[slug]/[id]/page.tsx
  22. 11 0
      src/app/(public)/customer/loading.tsx
  23. 2 2
      src/app/(public)/customer/login/_components/LoginPageWrapper.tsx
  24. 55 0
      src/app/(public)/loading.tsx
  25. 6 14
      src/app/(public)/paymentresult/_components/SkeletonCancelOrFailure.tsx
  26. 90 106
      src/app/(public)/paymentresult/_components/SuccessPageSkeleton.tsx
  27. 6 0
      src/app/(public)/paymentresult/result/loading.tsx
  28. 53 0
      src/app/(public)/product/[...urlProduct]/loading.tsx
  29. 1 6
      src/app/(public)/product/[...urlProduct]/page.tsx
  30. 1 1
      src/app/(public)/product/_components/ProductInformation.tsx
  31. 3 3
      src/app/(public)/product/_components/ProductMedia.tsx
  32. 1 1
      src/app/(public)/product/_components/ProductText.tsx
  33. 48 0
      src/app/(public)/search/loading.tsx
  34. 2 3
      src/app/api/home/route.ts
  35. 2 4
      src/app/api/shop/theme-customizations/route.ts
  36. 8 12
      src/app/globals.css
  37. 20 5
      src/assets/styles/cms-content.css
  38. 6 2
      src/components/common/LoadingSpinner.tsx
  39. 13 11
      src/components/common/LoginModal/LoginModal.tsx
  40. 0 37
      src/components/common/icons/social-icon/FaceBookIcon.tsx
  41. 0 19
      src/components/common/icons/social-icon/InstaGramIcon.tsx
  42. 0 19
      src/components/common/icons/social-icon/TwitterIcon.tsx
  43. 0 26
      src/components/common/skeleton/AddressSkeleton.tsx
  44. 0 31
      src/components/common/skeleton/AuthPlaceHolder.tsx
  45. 0 61
      src/components/common/skeleton/CheckoutSkeleton.tsx
  46. 0 50
      src/components/common/skeleton/FilterSkeleton.tsx
  47. 0 14
      src/components/common/skeleton/IconSkeleton.tsx
  48. 0 22
      src/components/common/skeleton/NavbarSkeleton.tsx
  49. 0 79
      src/components/common/skeleton/ProductCartSkeleton.tsx
  50. 22 87
      src/components/common/skeleton/ProductSkeleton.tsx
  51. 0 15
      src/components/common/skeleton/SearchSkeleton.tsx
  52. 0 135
      src/components/common/slider/HeroCarousel.tsx
  53. 0 42
      src/components/common/slider/HeroCarouselShimmer.tsx
  54. 0 2
      src/components/common/slider/index.ts
  55. 0 14
      src/components/customer/types.ts
  56. 46 0
      src/components/layout/footer/CompanyInfo.tsx
  57. 79 72
      src/components/layout/footer/FooterMenu.tsx
  58. 0 116
      src/components/layout/footer/Subscribe.tsx
  59. 103 0
      src/components/layout/footer/SubscribeNewsletter.tsx
  60. 48 121
      src/components/layout/footer/index.tsx
  61. 4 4
      src/components/layout/navbar/MobileSearch.tsx
  62. 0 17
      src/components/layout/navbar/NavigationSkeleton.tsx
  63. 4 7
      src/components/layout/navbar/index.tsx
  64. 4 3
      src/graphql/theme/mutations/subscribeToNewsletter.ts
  65. 1 1
      src/graphql/theme/queries/ThemeCustomization.ts
  66. 0 26
      src/graphql/theme/queries/Footer.ts
  67. 26 0
      src/graphql/theme/queries/GetThemeCustomization.ts
  68. 4 4
      src/graphql/theme/queries/index.ts
  69. 9 0
      src/types/customer/type.ts
  70. 18 69
      src/types/theme/theme-customization.ts
  71. 0 77
      src/utils/actions.ts
  72. 3 0
      src/utils/auth.ts
  73. 0 46
      src/utils/bagisto/index.ts
  74. 1 1
      src/utils/constants.ts
  75. 7 1
      src/utils/hooks/useAddToCart.ts
  76. 3 3
      src/utils/metadata.ts

+ 1 - 1
.env.example

@@ -16,7 +16,7 @@ NEXT_PUBLIC_BAGISTO_STOREFRONT_KEY=your_storefront_key_here
 
 # NextAuth Configuration 测试环境和生产环境要写实际部署的域名
 NEXTAUTH_URL=http://localhost:3001
-# NextAuth 使用 secret(下面的值)对 session中的token 对象进行 签名 + 加密,生成一个 JWT 字符串,解密这个JWT字符串就可以还原token对象。
+# NextAuth 使用 secret(下面的值)对 session中的token 对象进行 签名 + 加密,生成一个 JWT 字符串,解密这个JWT字符串就可以还原token对象。openssl rand -base64 32
 NEXTAUTH_SECRET=9jstKgQ1Q22PlDDhPtXTZ6G4MEzHGDJIkebcd/5UyZ4=
 
 # paypal client id 测试站 AXSFRWCfsqKWU2OuXAirHm5FG7U3J4KlYOpP4NvvJ9hQzK10SWMrHqotwnvQhjznf9A-stXrtVmrHBQR

BIN
public/image/footer-payment.webp


BIN
public/image/logo-white.webp


BIN
public/image/logo.webp


BIN
public/image/placeholder.webp


+ 23 - 6
src/actions/addProductToCartAction.ts

@@ -17,14 +17,15 @@ export async function addProductToCartAction(param:AddToCartVariables) {
     const cookieStore = await cookies();
     const guestCartToken = cookieStore.get(GUEST_CART_TOKEN)?.value;
 
-
+    // 针对游客
     if(!accessToken && !guestCartToken) {
         const createRes = await createGuestCartToken();
         if(!createRes.success) {
             return {
                 success: false,
                 msg: createRes.msg,
-                cartData: null
+                cartData: null,
+                error: new Error(createRes.msg)
             }
         }
     }
@@ -42,6 +43,7 @@ export async function addProductToCartAction(param:AddToCartVariables) {
             if(responseData) {
                 const responseCartId = responseData.id;
                 const responseCartToken = responseData.cartToken;
+                // 游客状态下
                 if(!accessToken && responseCartId !== responseCartToken) {
                     await setGuestCookie({
                         guestCartId: responseCartId,
@@ -49,27 +51,42 @@ export async function addProductToCartAction(param:AddToCartVariables) {
                         isGuest: true
                     });
                 }
+                // 登录状态,但是后端发现用户的accessToken不可用返回一个游客token时,需要走退出登录流程,提示用户过期了需要重新登录
+                if(accessToken && responseCartId !== responseCartToken) {
+                    const e = new Error("Authorization Bearer token has expired!");
+                    e.name = "UnauthorizedError";
+                    return {
+                        success: false,
+                        msg: e.message,
+                        cartData: response.data,
+                        error: e
+                    }
+                }
             }
             /** 兜底代码 end*/
 
             return {
                 success: true,
                 msg: '',
-                cartData: response.data
+                cartData: response.data,
+                error: null
             }
 
         } else {
             return {
                 success: false,
                 msg: response.error.message,
-                cartData: null
+                cartData: null,
+                error: new Error(response.error.message)
             }
         }
     } catch(error) {
+        const msg = error instanceof Error ? error.message : 'Add product to cart failed!';
         return {
             success: false,
-            msg: error instanceof Error ? error.message : 'Add product to cart failed!',
-            cartData: null
+            msg: msg,
+            cartData: null,
+            error: new Error(msg)
         }
     }
 };

+ 2 - 1
src/actions/index.ts

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

+ 41 - 0
src/actions/subscribeNewsletterAction.tsx

@@ -0,0 +1,41 @@
+"use server";
+
+import { serverGraphqlFetch } from "@utils/bagisto";
+import { SUBSCRIBE_TO_NEWSLETTER } from "@/graphql/theme/mutations";
+import type { CreateNewsletterResponse } from "@/types/customer/type";
+
+
+export async function subscribeNewsletterAction(param: {email: string;}){
+
+    try{
+        const response = await serverGraphqlFetch<CreateNewsletterResponse,{input:{customerEmail: string}}>({
+            query: SUBSCRIBE_TO_NEWSLETTER,
+            variables: {
+                input: {
+                    customerEmail: param.email
+                }
+            },
+            cache: "no-store",
+            takeAuthorization: false
+        });
+        const newsletterRes = response.data?.createNewsletter?.newsletter;
+        let success = true;
+        let msg = "";
+        if(response.error) {
+            success = false;
+            msg = response.error.message;
+        } else {
+            success = newsletterRes?.success ?? false;
+            msg = newsletterRes?.message ?? 'Fail to subscribe!';
+        }
+        return {
+            success: success,
+            msg: msg
+        };
+    } catch(e: any) {
+        return {
+            success:false,
+            msg: e.message || 'Fail to subscribe!'
+        };
+    }
+}

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

@@ -425,7 +425,7 @@ export default function CheckoutWrapper({
                 
             </div>
             <div className="mt-3 box-border px-4 pb-8">
-                <p className="text-ly-12 text-[#666666] leading-ly-20">By providing your information, you agree to Wiggins'sPrivacy Policyand Terms of Use.</p>
+                <p className="text-ly-12 text-[#666666] leading-ly-20">By providing your information, you agree to Asteria'sPrivacy Policyand Terms of Use.</p>
                 <div className="mt-6 w-full">
                     { paymentMethodLoading && <LoadingCheckoutPlaceOrder />}
                     

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

@@ -489,7 +489,7 @@ export function PaymentMethodContinueTpPay({
             
             <div className="w-full box-border px-4 mt-3">
                 <p className="text-ly-12 text-[#666666] leading-4.75">
-                    By providing your information, you agree to Wiggins'sPrivacy Policyand Terms of Use.
+                    By providing your information, you agree to Asteria'sPrivacy Policyand Terms of Use.
                 </p>
 
                 <div className="mt-6 w-full">

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

@@ -2,11 +2,11 @@ export default function LoadingPaymentMethod() {
     return (
         <div className="mt-3 w-full animate-pulse px-0">
             <div className="group box-border flex items-center relative mt-3 p-3 rounded-sm">
-                <p className="pr-2 flex-none h-8 w-12 bg-gray-300"></p>
-                <p className="text-ly-12 text-gray-300 bg-gray-300 ml-1">Paypay Smart Button</p>
+                <p className="pr-2 flex-none h-8 w-12 bg-skeletonbg"></p>
+                <p className="text-ly-12 w-45 h-3.5 bg-skeletonbg ml-1"></p>
   
 
-                <div className="absolute top-1/2 -translate-y-1/2 right-3 bg-gray-300 w-4 h-4"></div>
+                <div className="absolute top-1/2 -translate-y-1/2 right-3 bg-skeletonbg w-4 h-4"></div>
             </div>
         </div>
     );

+ 48 - 0
src/app/(checkout)/checkout/cart/loading.tsx

@@ -0,0 +1,48 @@
+import Image from "next/image";
+import {NOT_IMAGE} from "@/utils/constants";
+export default function Loading() {
+    const arr = [1,2,3,4,5];
+    return (
+        <div className="w-full h-full animate-pulse">
+            <header className="sticky top-0 z-10 w-full">
+                <div className="relative w-full h-15 flex items-center justify-center bg-neutral-50 text-ly-16">
+                    <div className="absolute left-4 w-6 h-6 bg-skeletonbg"></div>
+                    <p className="flex-none h-6 w-25 bg-skeletonbg"></p>
+                </div>
+            </header>
+            <div className="mt-6 box-border px-4 pb-8">
+                {arr.map((i) => {
+                    return (
+                        <div key={i} 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 bg-skeletonbg">
+                                <Image className="object-cover" fill src={NOT_IMAGE} alt="placeholder image"/>
+                            </div>
+                            <div className="w-full flex flex-col justify-between">
+                                <div className="w-full">
+                                    <div className="flex items-center gap-2 w-full">
+                                        <div className="w-full line-clamp-2 text-ly-12 leading-5">
+                                            <p className="bg-skeletonbg h-5">&nbsp;</p>
+                                            <p className="bg-skeletonbg w-1/2 h-5 mt-0.5">&nbsp;</p>
+                                        </div>
+                                        <button className="flex-none w-4 h-4 bg-skeletonbg" type="button"></button>
+                                    </div>
+                                    <div className="w-full mt-2 text-ly-12 h-5 rounded-sm px-2.5 py-1.5 leading-5 bg-skeletonbg"></div>
+                                </div>
+                                <div className="flex items-center justify-between">
+                                    <div className="flex items-center gap-1.5">
+                                        <span className="text-ly-14 font-bold bg-skeletonbg h-4 w-15"></span>
+                                        {/* <span className="text-ly-12 text-[#d3d3d3] line-through">$200.12</span> */}
+                                    </div>
+                                    <div className="w-25 h-8 flex-none flex rounded-lg p-2 gap-4 bg-skeletonbg"></div>
+                                </div>
+                            </div>
+                        </div>
+                    )
+                })}        
+                
+
+            
+            </div>
+        </div>
+    );
+}

+ 99 - 0
src/app/(checkout)/checkout/continuetopay/loading.tsx

@@ -0,0 +1,99 @@
+import Image from "next/image";
+import {NOT_IMAGE} from "@/utils/constants";
+export default function Loading() {
+    const arr = [1,2];
+    return (
+        <div className="w-full h-full animate-pulse">
+            <header className="sticky top-0 z-10 w-full">
+                <div className="relative w-full h-15 flex items-center justify-center bg-neutral-50 text-ly-16">
+                    <div className="absolute left-4 w-6 h-6 bg-skeletonbg"></div>
+                    <p className="flex-none h-6 w-25 bg-skeletonbg"></p>
+                </div>
+            </header>
+            <div>
+                <div className="sticky top-15 z-10 box-border w-full h-14 px-4 flex items-center justify-between bg-skeletonbg"></div>
+                        
+                <div className="mt-6 box-border px-4">
+                    <h3 className="text-ly-24 font-medium bg-skeletonbg w-30 h-6"></h3>
+                    <div className="w-full mt-3 box-border rounded-md px-3.75">
+                        <div className="flex h-11.25 items-center justify-between">
+                            <p className="h-3.5 font-medium w-30 bg-skeletonbg"></p>
+                        </div>
+                        <div className="flex items-center h-10">
+                            <span className="text-ly-12 ml-2.5 bg-skeletonbg w-90 h-3.5"></span>
+                        </div>
+                    </div>  
+    
+                </div>
+            
+                <div className="mt-6 box-border px-4">
+                    <h3 className="text-ly-24 font-medium bg-skeletonbg w-30 h-6"></h3>
+                    <div className="w-full mt-3 box-border rounded-md px-3.75">
+                        <div className="flex h-11.25 items-center justify-between">
+                            <p className="h-3.5 font-medium w-30 bg-skeletonbg"></p>
+                        </div>
+                        <div className="flex items-center h-10">
+                            <span className="text-ly-12 ml-2.5 bg-skeletonbg w-90 h-3.5"></span>
+                        </div>
+                    </div>  
+                </div>
+                         
+            
+                <div className="w-full box-border px-4 mt-6">
+                    <div className="flex items-center">
+                        <span className="text-ly-12 flex-none mr-1 bg-skeletonbg">
+                            Order Summary(2)
+                        </span>
+                        <button className="flex-none w-4 h-4 bg-skeletonbg" type="button"></button>             
+                    </div>
+                    <div className="relative w-full box-border pr-9 mt-3.5">
+                        <div className="w-full box-border flex overflow-x-auto">
+                            {arr.map((i) => { 
+                               
+                                return(
+                                    <div key={i} className="relative flex-none w-16 h-21.5 mr-3 last:mr-0 bg-skeletonbg">
+                                        <Image className="object-cover" fill src={NOT_IMAGE} alt="placeholder image"/>
+                                    </div>
+                                )
+                            })}
+    
+                        </div>
+                        <button className="absolute top-1/2 right-1 w-4 h-4 -translate-y-1/2 bg-skeletonbg"></button>
+                    </div>
+                </div>
+            
+                <div className="box-border px-4 w-full mt-6">
+                    <h3 className="text-ly-24 font-medium bg-skeletonbg w-30 h-6"></h3>
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20 bg-skeletonbg w-15 h-5"></p>
+                        <p className="text-ly-12 leading-ly-20 bg-skeletonbg w-15 h-5"></p>
+                    </div>
+     
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20 bg-skeletonbg w-15 h-5"></p>
+                        <p className="text-ly-12 leading-ly-20 bg-skeletonbg w-15 h-5"></p>
+                    </div>
+                    
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20 bg-skeletonbg w-15 h-5"></p>
+                        <p className="text-ly-12 leading-ly-20 bg-skeletonbg w-15 h-5"></p>
+                    </div>
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20 bg-skeletonbg w-15 h-5"></p>
+                        <p className="text-ly-12 leading-ly-20 bg-skeletonbg w-15 h-5"></p>
+                    </div>
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-12 leading-ly-20 bg-skeletonbg w-15 h-5"></p>
+                        <p className="text-ly-12 leading-ly-20 bg-skeletonbg w-15 h-5"></p>
+                    </div>
+                    
+                    <div className="flex justify-between mt-3 first:mt-0">
+                        <p className="text-ly-16 leading-ly-24 bg-skeletonbg w-15 h-6"></p>
+                        <p className="text-ly-16 leading-ly-24 font-bold bg-skeletonbg w-15 h-6"></p>
+                    </div>
+                </div>         
+            </div>
+            
+        </div>
+    );
+}

+ 96 - 0
src/app/(checkout)/checkout/loading.tsx

@@ -0,0 +1,96 @@
+import Image from "next/image";
+import {NOT_IMAGE} from "@/utils/constants";
+export default function Loading() {
+    const arr = [1,2];
+    return (
+        <div className="w-full h-full animate-pulse">
+            <header className="sticky top-0 z-10 w-full">
+                <div className="relative w-full h-15 flex items-center justify-center bg-neutral-50 text-ly-16">
+                    <div className="absolute left-4 w-6 h-6 bg-skeletonbg"></div>
+                    <p className="flex-none h-6 w-25 bg-skeletonbg"></p>
+                </div>
+            </header>
+            <div className="w-full px-0">
+                <div className="box-border w-full h-15 flex items-center px-3.75 relative">
+                    <div className="w-4 h-4 mr-1 bg-skeletonbg"></div>
+                
+                    <span className="text-ly-12 bg-skeletonbg w-70 h-3.5"></span>
+                    <div className="absolute top-1/2 right-3.75 -translate-y-1/2 w-4 h-4 bg-skeletonbg"></div>
+                </div>
+            
+            </div>
+
+            <div className="relative box-border w-full mt-6 px-4">
+                <div className="flex justify-between items-center">
+                    <button className="flex items-center text-ly-12">
+                        <span className="bg-skeletonbg w-30 h-3.5"> </span>
+                        <div className="w-4 h-4 ml-2 bg-skeletonbg"></div>
+                    </button>
+                    <span className="text-ly-14 font-bold bg-skeletonbg w-20 h-4"></span>
+                </div>
+    
+                <div className="relative mt-3 w-full">
+                    <div className="flex w-full gap-2 overflow-auto pr-6">
+                        {arr.map((i) => {
+                            
+                            return (
+                                <div className="relative w-22.5 h-30 flex-none bg-skeletonbg" key={i}>
+                                    <Image fill className="object-cover" src={NOT_IMAGE} alt="placeholder image" />
+                                </div>
+                            )
+                        })}
+                    </div>
+
+                </div>
+                
+            </div>
+
+            <div className="mt-6 box-border px-4">
+                <h3 className="text-ly-24 font-medium bg-skeletonbg w-30 h-6"></h3>
+            
+                <div className="w-full">
+                    <label className="group block relative box-border mt-3 p-3 border-1 border-skeletonbg rounded-sm ">
+                        <p className="text-ly-13 bg-skeletonbg w-40 h-4"></p>
+                        <p className="text-ly-12 bg-skeletonbg mt-1 w-30 h-3.5"></p>
+
+                        <div className="absolute top-1/2 -translate-y-1/2 right-3 flex items-center">
+                            <span className="text-ly-14 font-medium mr-4 bg-skeletonbg h-3.5 w-20"></span>
+                            <div className="w-4 h-4 bg-skeletonbg"></div>
+                        </div>
+                    </label>
+                </div>
+            </div>
+
+            <div className="mt-3 box-border px-4">
+                <div className="relative box-border w-full bg-skeletonbg p-3">
+                    <div className="flex justify-between items-center">
+                        <span className="text-ly-13 w-30 h-3.5"></span>
+                        <span className="text-ly-12 w-10 h-3.5"></span>
+                    </div>
+                    <div className="mt-2 w-57 text-ly-12 leading-3.5 h-3.5"></div>
+                </div>
+            </div>
+
+
+            <div className="mt-6 box-border px-4">
+                <div className="relative box-border w-full flex justify-between items-center">
+                    <span className="text-ly-24 font-medium bg-skeletonbg w-15 h-6"></span>
+                    <div className="flex items-center">
+                    
+                        <span className="flex items-center text-ly-14 px-2 flex-none h-6 w-10 bg-skeletonbg mr-2"></span>
+                        <button className="w-6 h-6 flex-none bg-skeletonbg" type="button"></button>
+                    </div>
+                </div>
+                
+                <div className="w-full mt-3">
+                    <div className="box-border w-full h-40 bg-skeletonbg p-3 rounded-md"></div>
+                    
+                    <div className="flex justify-between items-center mt-3 bg-skeletonbg border-skeletonbg box-border w-full border-1 h-12 rounded-sm pl-3 pr-2"></div>
+                </div>
+            </div>
+
+
+            
+        </div>
+    );
+}

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

@@ -1,8 +1,22 @@
 import { notFound } from "next/navigation";
-import { getPage } from "@utils/bagisto";
+import { serverGraphqlFetch } from "@/utils/bagisto";
+import { PAGE_BY_URL_KEY } from "@/graphql";
 import type { PageData } from "@/types/theme/theme-customization";
 import CmsContent from "./_components/CMSContent";
 
+interface PageByUrlKeyResponse {
+  pageByUrlKeypages?: PageData[];
+}
+export async function getPage(input: { urlKey: string }): Promise<PageData[]> {
+  const res = await serverGraphqlFetch<PageByUrlKeyResponse,{ pageByUrlKey: string }>({
+    query: PAGE_BY_URL_KEY,
+    cache: "no-store",
+    variables: { pageByUrlKey: input.urlKey },
+    takeAuthorization: false
+  });
+
+  return res.data?.pageByUrlKeypages || [];
+}
 
 export default async function Page({
     params,

+ 11 - 0
src/app/(public)/(cms)/loading.tsx

@@ -0,0 +1,11 @@
+import {LoadingSpinner} from "@/components/common/LoadingSpinner";
+export default function Loading() {
+
+    return (
+        <div className="fixed top-0 left-0 w-full h-full box-border">
+            <div className="w-full h-full flex justify-center items-center bg-skeletonbg/60">
+                <LoadingSpinner size={40} />
+            </div>
+        </div>
+    );
+}

src/app/(public)/track_order/_components/TrackFaqs.tsx → src/app/(public)/(cms)/track_order/_components/TrackFaqs.tsx


src/app/(public)/track_order/_components/TrackOrder.tsx → src/app/(public)/(cms)/track_order/_components/TrackOrder.tsx


src/app/(public)/track_order/page.tsx → src/app/(public)/(cms)/track_order/page.tsx


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

@@ -0,0 +1,45 @@
+import {NOT_IMAGE} from "@/utils/constants";
+export default function Loading() {
+    const arr = [1,2,3,4,5,6];
+    return (
+        <div className="w-full h-full box-border px-4 animate-pulse">
+     
+            <div className="w-full mt-3 bg-skeletonbg">
+                <img className="block w-full h-42.75 object-cover" src={NOT_IMAGE} />       
+            </div>
+            <div>
+                <div className="bg-skeletonbg w-20 h-6 mt-8"></div>
+                <p className="bg-skeletonbg h-3.5 mt-4"></p>
+                <p className="bg-skeletonbg w-1/2 h-3.5 mt-1"></p>
+                <div className="flex justify-end mt-2">
+                    <span className="w-25 h-4.5 bg-skeletonbg"></span>
+                </div>
+            </div>
+
+
+            <div className="flex justify-between items-center mt-6">
+                <div className="w-15 h-4 bg-skeletonbg"></div>
+                <div className="w-18 h-7 bg-skeletonbg"></div>
+            </div>
+
+            <div className="flex flex-wrap justify-between items-center">
+                {arr.map((i) => {
+                    return (
+                        <div className="flex-none w-42.5 mt-4" key={i}>
+                            <img className="block w-42.5 h-57 object-contain bg-skeletonbg" src={NOT_IMAGE} />
+                            <p className="mt-2 h-5 bg-skeletonbg"></p>
+                            <p className="w-26 h-5 bg-skeletonbg mt-2"></p>
+                            <p className="mt-2 flex items-center justify-between">
+                                <span className="w-20 h-5 bg-skeletonbg"></span>
+                                <span className="h-8 w-8 bg-skeletonbg"></span>
+                            </p>
+                        </div>
+                    );
+
+                })}
+                
+            </div>
+
+        </div>
+    );
+}

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

@@ -22,8 +22,6 @@ import { newSortByFields } from "@utils/constants";
 // import { CategoryDetail } from "@components/theme/search/CategoryDetail";
 import CategoryDesc from "./_components/CategoryDesc";
 import FaqList, { type FaqItem } from "./_components/FaqList";
-import { Suspense } from "react";
-import FilterListSkeleton from "@components/common/skeleton/FilterSkeleton";
 // import { TreeCategoriesResponse } from "@/types/theme/category-tree";
 // import { MobileSearchBar } from "@components/layout/navbar/MobileSearch";
 import {
@@ -297,7 +295,7 @@ export default async function CategoryPage({
     <>
       {/* <MobileSearchBar /> */}
       <section>
-        <Suspense fallback={<FilterListSkeleton />}>
+    
           {/* <CategoryDetail
             categoryItem={{
               description: translation?.description ?? "",
@@ -318,7 +316,7 @@ export default async function CategoryPage({
               categoryProductDatas? categoryProductDatas.categoryProducts.description: ''
             } //translation?.description ?? ""}
           />
-        </Suspense>
+   
         {/* <div className="my-10 hidden gap-4 md:flex md:items-baseline md:justify-between w-full max-w-screen-2xl mx-auto px-4">
           <FilterList filterAttributes={filterAttributes} />
           <SortOrder sortOrders={SortByFields} title="Sort by" />

+ 11 - 0
src/app/(public)/customer/loading.tsx

@@ -0,0 +1,11 @@
+import {LoadingSpinner} from "@/components/common/LoadingSpinner";
+export default function Loading() {
+
+    return (
+        <div className="fixed top-0 left-0 w-full h-full box-border">
+            <div className="w-full h-full flex justify-center items-center bg-skeletonbg/60">
+                <LoadingSpinner size={40} />
+            </div>
+        </div>
+    );
+}

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

@@ -388,9 +388,9 @@ export function LoginPageWrapper({
                         </span>
                         <span className="text-ly-12 text-[#666666]">
                             I Agree To The 
-                            Terms & Conditions
+                            <Link className="underline" href="/terms-conditions">Terms & Conditions</Link>.
                             And 
-                            Privacy Policy
+                            <Link className="underline" href="/privacy-policy">Privacy Policy</Link> 
                             Of  asteriahair.com
                         </span>
                         

+ 55 - 0
src/app/(public)/loading.tsx

@@ -0,0 +1,55 @@
+import {NOT_IMAGE} from "@/utils/constants";
+export default function Loading() {
+    const arr = [1,2,3,4,5,6];
+    return (
+        <div className="w-full h-full box-border animate-pulse">
+     
+            <div className="w-full bg-skeletonbg">
+                <img className="block w-full h-130 object-cover" src={NOT_IMAGE} />       
+            </div>
+
+            <div className="flex justify-between box-border px-4 py-1.5">
+                <span className="w-20 h-5 bg-skeletonbg"></span>
+                <span className="w-20 h-5 bg-skeletonbg"></span>
+                <span className="w-20 h-5 bg-skeletonbg"></span>
+            </div>
+
+            <div className="box-border px-4 mt-1 flex flex-wrap justify-between">
+                {arr.map((i) => {
+                    return (
+                        <div className="w-27 h-36 mt-3 bg-skeletonbg" key={'one'+String(i)}>
+                            <img className="block w-full h-full object-cover" src={NOT_IMAGE} />
+                        </div>
+                    )
+                })}
+                
+            </div>
+
+            
+            <div className="box-border px-4 mt-6">
+                <div className="h-6 w-40 bg-skeletonbg"></div>
+                <div className="flex justify-between flex-wrap">
+                    {[1,2,3,4].map((i) => {
+                        return (
+                            <div className="flex-none w-42.75 mt-4" key={'two'+String(i)}>
+                                <img className="block w-42.75 h-57 bg-skeletonbg object-cover" src={NOT_IMAGE} />
+                                <p className="h-5 mt-2 bg-skeletonbg"></p>
+                                <p className="w-3/5 h-4 mt-2 bg-skeletonbg"></p>
+                                <div className="flex justify-between items-center mt-2">
+                                    <span className="w-25 h-5 bg-skeletonbg"></span>
+                                    <span className="w-8 h-8 bg-skeletonbg"></span>
+                                </div>
+                            </div>
+                        )
+
+                    })}
+                    
+                </div>
+            </div>
+
+        
+            
+
+        </div>
+    );
+}

+ 6 - 14
src/app/(public)/paymentresult/_components/SkeletonCancelOrFailure.tsx

@@ -5,26 +5,18 @@ export default function SkeletonCancelOrFailure() {
 
             <div className="w-full">
                 <div className="box-border px-4 w-full">
-                    <div className="w-12 h-12 mx-auto mt-9 bg-gray-300"></div>
+                    <div className="w-12 h-12 mx-auto mt-9 bg-skeletonbg"></div>
                     <p className="text-ly-16 text-center mt-6">
-                        <span className="text-gray-300 bg-gray-300">Your Order Has Been Received</span>
+                        <span className="inline-block bg-skeletonbg h-4 w-70"></span>
                     </p>
                     <p className="text-ly-16 mt-4 flex justify-center">
-                        <span className="px-3.75 leading-ly-28 text-gray-300 bg-gray-300">Order #600000000</span>
+                        <span className="px-3.75 h-7 w-30 bg-skeletonbg"></span>
                     </p>
               
-                    <div className="mt-6 flex justify-center items-center h-12 border-1 rounded-3xl text-ly-14">
-                        <span className="text-gray-300 bg-gray-300">Continue Shopping</span>
-                    </div>
+                    <div className="mt-6 flex justify-center items-center h-12 rounded-3xl bg-skeletonbg"></div>
                 </div>
-                <div className="box-border px-4 w-full mt-6">
-                    <div className="relative w-full bg-ly-lightgray box-border p-3">
-                        <button className="absolute top-3 right-3 w-6 h-6 bg-gray-300"></button>
-                        <p className="text-ly-13"><span className="text-gray-300 bg-gray-300">Faqs</span></p>
-                        <p className="text-ly-12 text-[#a6a6a6] mt-2">
-                            <span className="text-gray-300 bg-gray-300">Pls contact customer service if need any help</span>
-                        </p>
-                    </div>
+                <div className="box-border px-4 w-full mt-6 ">
+                    <div className="w-full h-17.5 bg-skeletonbg"></div>
                 </div>
 
 

+ 90 - 106
src/app/(public)/paymentresult/_components/SuccessPageSkeleton.tsx

@@ -1,124 +1,108 @@
-
-export default function SuccessPageSkeleton() { 
+import Image from "next/image";
+import {NOT_IMAGE} from "@/utils/constants";
+export default function SuccessPageSkeleton() {
     return (
-        <div className="w-full animate-pulse px-0">
+        <div className="w-full h-full animate-pulse">
+
+
+            
+            <div className="box-border px-4 w-full">
+                <div className="w-12 h-12 mx-auto mt-9 bg-skeletonbg"></div>
+                <p className="text-ly-16 text-center mt-6">
+                    <span className="bg-skeletonbg w-60 h-4 inline-block"></span>
+                </p>
+                <p className="text-ly-16 mt-4 flex justify-center">
+                    <span className="px-3.75 leading-ly-28 bg-skeletonbg w-40 h-7"></span>
+                </p>
+                <p className="text-ly-12 text-center mt-4 leading-ly-22">
+                    <span className="bg-skeletonbg w-25 h-3.5 inline-block"></span>
+                </p>
+                <p className="text-ly-12 text-center leading-ly-22">
+                    <span className="bg-skeletonbg w-40 h-3.5 inline-block"></span>
+                </p>
+                <p className="text-ly-12 text-center leading-ly-22">
+                    <span className="bg-skeletonbg w-50 h-3.5 inline-block"></span>
+                </p>
+                <p className="text-ly-12 text-center leading-ly-22">
+                    <span className="bg-skeletonbg w-60 h-3.5 inline-block"></span>
+                </p>
+                <div className="mt-6 flex justify-center items-center h-12 rounded-3xl text-ly-14 bg-skeletonbg"></div>
+            </div>
+            <div className="box-border px-4 w-full mt-6 ">
+                <div className="w-full h-17.5 bg-skeletonbg"></div>
+            </div>
+
 
-            <div className="w-full">
-                <div className="box-border px-4 w-full">
-                    <div className="w-12 h-12 mx-auto mt-9 bg-gray-300"></div>
-                    <p className="text-ly-16 text-center mt-6">
-                        <span className="text-gray-300 bg-gray-300">Your Order Has Been Received</span>
-                    </p>
-                    <p className="text-ly-16 mt-4 flex justify-center">
-                        <span className="px-3.75 leading-ly-28 text-gray-300 bg-gray-300">Order #600000000</span>
-                    </p>
-                    <p className="text-ly-12 text-center mt-4 leading-ly-22">
-                        <span className="text-gray-300 bg-gray-300">Hello, lala</span>
-                    </p>
-                    <p className="text-ly-12 text-center leading-ly-22">
-                        <span className="text-gray-300 bg-gray-300">Thank you so much for your order!</span>
-                    </p>
-                    <p className="text-ly-12 text-center leading-ly-22">
-                        <span className="text-gray-300 bg-gray-300">We’ll get started on your order very soon!</span>
-                    </p>
-                    <p className="text-ly-12 text-center leading-ly-22">
-                        <span className="text-gray-300 bg-gray-300">You’ll receive an order confirmation email with details of your order in an hour, please be sure to check your email and confirm your order, that is very important for us to process your order.</span>
-                    </p>
-                    <div className="mt-6 flex justify-center items-center h-12 border-1 rounded-3xl text-ly-14">
-                        <span className="text-gray-300 bg-gray-300">Continue Shopping</span>
+            <div className="box-border px-4 w-full mt-6">
+                <h4 className="text-ly-24 font-medium bg-skeletonbg w-70 h-6"></h4>
+                <div className="mt-6 flex justify-between">
+                    <div className="text-ly-12 flex-none w-20 h-3.5 bg-skeletonbg"></div>
+                    <div className="w-40.5 text-ly-12 leading-ly-20 flex-none">
+                        <p className="w-full h-3.5 bg-skeletonbg"></p>
+                        <p className="w-full h-3.5 bg-skeletonbg mt-0.5"></p>
+                        <p className="w-20 h-3.5 bg-skeletonbg mt-0.5"></p>
                     </div>
                 </div>
-                <div className="box-border px-4 w-full mt-6">
-                    <div className="relative w-full bg-ly-lightgray box-border p-3">
-                        <button className="absolute top-3 right-3 w-6 h-6 bg-gray-300"></button>
-                        <p className="text-ly-13"><span className="text-gray-300 bg-gray-300">Faqs</span></p>
-                        <p className="text-ly-12 text-[#a6a6a6] mt-2">
-                            <span className="text-gray-300 bg-gray-300">Pls contact customer service if need any help</span>
-                        </p>
+                <div className="mt-6 flex justify-between">
+                    <div className="text-ly-12 flex-none w-20 h-3.5 bg-skeletonbg"></div>
+                    <div className="w-40.5 text-ly-12 leading-ly-20 flex-none">
+                        <p className="w-20 h-3.5 bg-skeletonbg"></p>
                     </div>
                 </div>
-                <div className="box-border px-4 w-full mt-6">
-                    <h4 className="text-ly-24 font-medium">
-                        <span className="text-gray-300 bg-gray-300">Order Information</span>
-                    </h4>
-                    <div className="mt-6 flex justify-between">
-                        <div className="text-ly-12 flex-none text-gray-300 bg-gray-300">Ship to:</div>
-                        <div className="w-40.5 text-ly-12 leading-ly-20 flex-none text-gray-300 bg-gray-300">
-                            Alessandro  6263378809
-                        </div>
+                <div className="mt-6 flex justify-between">
+                    <div className="text-ly-12 flex-none w-20 h-3.5 bg-skeletonbg"></div>
+                    <div className="w-40.5 text-ly-12 leading-ly-20 flex-none">
+                        <p className="w-20 h-3.5 bg-skeletonbg"></p>
                     </div>
-                    <div className="mt-6 flex justify-between">
-                        <div className="text-ly-12 flex-none text-gray-300 bg-gray-300">Order number:</div>
-                        <div className="w-40.5 text-ly-12 leading-ly-20 flex-none text-gray-300 bg-gray-300">
-                            600465290
-                        </div>
-                    </div>
-                    <div className="mt-6 flex justify-between">
-                        <div className="text-ly-12 flex-none text-gray-300 bg-gray-300">Status:</div>
-                        <div className="w-40.5 text-ly-12 leading-ly-20 flex-none text-gray-300 bg-gray-300">
-                            Processing
-                        </div>
-                    </div>
-                    <div className="mt-6 flex justify-between">
-                        <div className="text-ly-12 flex-none text-gray-300 bg-gray-300">Order date:</div>
-                        <div className="w-40.5 text-ly-12 leading-ly-20 flex-none text-gray-300 bg-gray-300">
-                            September 24,2020
-                        </div>
+                </div>
+                <div className="mt-6 flex justify-between">
+                    <div className="text-ly-12 flex-none w-20 h-3.5 bg-skeletonbg"></div>
+                    <div className="w-40.5 text-ly-12 leading-ly-20 flex-none">
+                        <p className="w-20 h-3.5 bg-skeletonbg"></p>
                     </div>
-                    <div className="mt-6 flex justify-between">
-                        <div className="text-ly-12 flex-none text-gray-300 bg-gray-300">Shipping method:</div>
-                        <div className="w-40.5 text-ly-12 leading-ly-20 flex-none text-gray-300 bg-gray-300">
-                            Same day delivery
-                        </div>
+                </div>
+                <div className="mt-6 flex justify-between">
+                    <div className="text-ly-12 flex-none w-20 h-3.5 bg-skeletonbg"></div>
+                    <div className="w-40.5 text-ly-12 leading-ly-20 flex-none">
+                        <p className="w-20 h-3.5 bg-skeletonbg"></p>
                     </div>
-                    <div className="mt-6 flex justify-between">
-                        <div className="text-ly-12 flex-none text-gray-300 bg-gray-300">Payment method:</div>
-                        <div className="w-40.5 text-ly-12 leading-ly-20 flex-none text-gray-300 bg-gray-300">
-                            Paypal-express
-                        </div>
+                </div>
+                <div className="mt-6 flex justify-between">
+                    <div className="text-ly-12 flex-none w-20 h-3.5 bg-skeletonbg"></div>
+                    <div className="w-40.5 text-ly-12 leading-ly-20 flex-none">
+                        <p className="w-20 h-3.5 bg-skeletonbg"></p>
                     </div>
-
                 </div>
 
-                {/* <div className="box-border px-4 w-full mt-6">
-                    <div 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" />
-                        </div>
-                        <div className="flex-none w-50.75 flex flex-col justify-between">
-                            <div>
-                                <p className="text-ly-12 leading-ly-20 line-clamp-2">
-                                    Deep Wave Wig 100% Human Hair Deep Wave Wig 100% Human Hair  100% Human Hair
-                                </p>
-                                <p className="mt-2 text-ly-12 leading-ly-20 text-ly-placeholder">14/4*4 HD Lace/150%</p>
-                            </div>
-                            <div className="flex justify-between">
-                                <p className="text-ly-14 leading-ly-20 font-bold">$10.00</p>
-                                <p className="text-ly-12 leading-ly-20">x 1</p>
-                            </div>
-                        </div>
-                    </div>
-                </div> */}
+            </div>
 
-                {/* <div className="box-border px-4 w-full mt-6 pt-6 border-t-1 border-black">
-                    <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">0.00</p>
+            <div className="box-border px-4 w-full mt-6">
+                
+                <div className="w-full flex gap-2 pb-4 last:border-0 last:pb-0">
+                    <div className="relative w-33 h-44 flex-none bg-skeletonbg">
+                        <Image className="object-cover" fill src={NOT_IMAGE} alt="placeholder imag" />
                     </div>
-                    <div className="flex justify-between mt-3 first:mt-0">
-                        <p className="text-ly-12 leading-ly-20">Shipping Method</p>
-                        <p className="text-ly-12 leading-ly-20">0.00</p>
-                    </div>
-                    <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">600 Reward Points</p>
-                    </div>
-                    <div className="flex justify-between mt-3 first:mt-0">
-                        <p className="text-ly-16 leading-ly-24">Total</p>
-                        <p className="text-ly-16 leading-ly-24 font-bold">0.00</p>
+                    <div className="flex-none w-50.75 flex flex-col justify-between">
+                        <div>
+                            <p className="text-ly-12 leading-ly-20 line-clamp-2">
+                                <span className="bg-skeletonbg w-full h-3.5 inline-block"></span>
+                                <span className="bg-skeletonbg w-1/2 h-3.5 inline-block"></span>
+                            </p>
+                            <p className="mt-2 text-ly-12 leading-ly-20">
+                                <span className="bg-skeletonbg w-3/4 h-3.5 inline-block"></span>
+                            </p>
+                        </div>
+
+                        <div className="flex justify-between">
+                            <p className="text-ly-14 leading-ly-20 font-bold bg-skeletonbg w-20 h-5"></p>
+                            <p className="text-ly-12 leading-ly-20 bg-skeletonbg w-10 h-5"></p>
+                        </div>
                     </div>
-                </div> */}
+                </div>
+                
+                
             </div>
+
         </div>
     );
-}
+}

+ 6 - 0
src/app/(public)/paymentresult/result/loading.tsx

@@ -0,0 +1,6 @@
+import SuccessPageSkeleton from '../_components/SuccessPageSkeleton';
+export default function Loading() {
+    return (<>
+        <SuccessPageSkeleton />
+    </>);
+}

+ 53 - 0
src/app/(public)/product/[...urlProduct]/loading.tsx

@@ -0,0 +1,53 @@
+import {NOT_IMAGE} from "@/utils/constants";
+export default function Loading() {
+    const arr = [1,2,3];
+    return (
+        <div className="w-full h-full box-border animate-pulse">
+     
+            <div className="w-full bg-skeletonbg">
+                <img className="block w-full h-130 object-cover" src={NOT_IMAGE} />       
+            </div>
+            <div className="flex flex-wrap pl-4 mt-1">
+                {arr.map((i) => {
+                    return (
+                        <div className="flex-none w-15 h-20 mr-2" key={i}>
+                            <img className="block w-15 h-20 object-contain bg-skeletonbg" src={NOT_IMAGE} />
+                        </div>
+                    );
+
+                })}
+                
+            </div>
+
+            <div className="box-border px-4 mt-4">
+  
+                <p className="bg-skeletonbg h-5"></p>
+                <p className="bg-skeletonbg w-3/4 h-5 mt-1"></p>
+
+            </div>
+            
+            <div className="box-border px-4 mt-2">
+                <div className="h-10 bg-skeletonbg mt-2"></div>
+            </div>
+
+            <div className="box-border px-4 flex justify-between items-center mt-2">
+                <div className="w-40 h-7 bg-skeletonbg"></div>
+                <div className="w-60 h-5 bg-skeletonbg"></div>
+            </div>
+
+            <div className="box-border w-full px-4 mt-3">
+                <div className="h-5 bg-skeletonbg w-3/4"></div>
+            </div>
+
+            <div className="box-border w-full px-4 mt-3">
+                <div className="h-10 bg-skeletonbg"></div>
+            </div>
+
+            <div className="box-border w-full px-4 mt-3">
+                <div className="h-35 bg-skeletonbg"></div>
+            </div>
+            
+
+        </div>
+    );
+}

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

@@ -1,7 +1,6 @@
 import { notFound } from "next/navigation";
 import { Suspense } from "react";
 import {
-  ProductDetailSkeleton,
   RelatedProductSkeleton,
 } from "@/components/common/skeleton/ProductSkeleton";
 import { BASE_SCHEMA_URL, PRODUCT_TYPE } from "@/utils/constants";
@@ -13,7 +12,6 @@ import type {
   ProductMediaType,
 } from "@components/catalog/type";
 import { RelatedProductsSection } from "@components/catalog/product/RelatedProductsSection";
-import { HeroCarouselShimmer } from "@components/common/slider";
 import { ProductMedia } from "@/app/(public)/product/_components/ProductMedia";
 import { ProductInformation } from "@/app/(public)/product/_components/ProductInformation";
 import { ProductDetail } from "@/app/(public)/product/_components/ProductDetail";
@@ -88,13 +86,11 @@ export default async function ProductPage({
         type="application/ld+json"
       />
       <div className="w-full">
-        <Suspense fallback={<HeroCarouselShimmer />}>
           <ProductMedia mediaData={mediaImgs} />
-        </Suspense>
       </div>
 
       <div className="">
-        <Suspense fallback={<ProductDetailSkeleton />}>
+
           <ProductInformation
             key={product._id}
             productId={product._id}
@@ -115,7 +111,6 @@ export default async function ProductPage({
           />
           <ProductDetail detail={product.description || ""} />
           
-        </Suspense>
         <Recommend/>
       </div>
         

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

@@ -214,7 +214,7 @@ export function ProductInformation({
             if(res.data) {
                 const responseData = res.data;
                 if(responseData.success) {
-                    router.push('/checkout?step=address');
+                    router.push('/checkout');
                 }
             }
         }

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

@@ -64,7 +64,7 @@ export function ProductMedia({
         ))}
         {/* 自定义分页数字 */}
         <div slot="container-end">
-          <div className="product-media-swiper-pagination z-1 absolute top-3 right-4 pt-1.5 pb-1.5 pl-3 pr-3 rounded-3xl text-ly-14 text-white bg-ly-deepgreen/50"></div>
+          <div className="product-media-swiper-pagination z-1 absolute top-3 right-4 flex items-center h-7 px-2.5 rounded-3xl text-ly-14 text-white bg-ly-deepgreen/50"></div>
         </div>
       </Swiper>
       <div className=" pl-4">
@@ -91,8 +91,8 @@ export function ProductMedia({
               >
                 <Image
                   src={getImageUrl(img.publicPath)}
-                  width={100}
-                  height={130}
+                  width={60}
+                  height={80}
                   loading="eager"
                   alt={`Thumbnail ${idx + 1}`}
                   className="w-full h-full object-cover"

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

@@ -9,7 +9,7 @@ export default function ProductText({ ProductText }: { ProductText: string }) {
   // 模拟商品长标题
   const titleText =
      (ProductText ? stripHtml(ProductText) : null) ||
-    "Wiggins Hair HD Lace Frontal Wigs straight A Grade Human Hair 13x4 Transparent Lace Front Wig For Women Natural Black 18-30 Inch";
+    "Asteria Hair HD Lace Frontal Wigs straight A Grade Human Hair 13x4 Transparent Lace Front Wig For Women Natural Black 18-30 Inch";
   const [isExpand, setIsExpand] = useState(false);
 
   return (

+ 48 - 0
src/app/(public)/search/loading.tsx

@@ -0,0 +1,48 @@
+import {NOT_IMAGE} from "@/utils/constants";
+export default function Loading() {
+    const arr = [1,2,3,4,5,6];
+    return (
+        <div className="w-full h-full box-border px-4 animate-pulse">
+     
+            <div className="w-full h-10 mt-3 bg-skeletonbg"></div>
+
+                <div className="w-full mx-auto px-4 py-6">
+                <div className="text-base font-medium mb-4 w-25 h-4"></div>
+                <div className="flex flex-wrap items-center justify-start gap-3">
+                
+                    <div className="bg-skeletonbg rounded-sm w-30 h-8"></div>
+                    <div className="bg-skeletonbg rounded-sm w-20 h-8"></div>
+                    <div className="bg-skeletonbg rounded-sm w-25 h-8"></div>
+                    <div className="bg-skeletonbg rounded-sm w-18 h-8"></div>
+                    <div className="bg-skeletonbg rounded-sm w-32 h-8"></div>
+                    <div className="bg-skeletonbg rounded-sm w-20 h-8"></div>
+                    <div className="bg-skeletonbg rounded-sm w-20 h-8"></div>
+                </div>
+            </div>
+
+            <div className="flex justify-between items-center mt-6">
+                <div className="w-15 h-4 bg-skeletonbg"></div>
+                <div className="w-18 h-7 bg-skeletonbg"></div>
+            </div>
+
+            <div className="flex flex-wrap justify-between items-center">
+                {arr.map((i) => {
+                    return (
+                        <div className="flex-none w-42.5 mt-4" key={i}>
+                            <img className="block w-42.5 h-57 object-contain bg-skeletonbg" src={NOT_IMAGE} />
+                            <p className="mt-2 h-5 bg-skeletonbg"></p>
+                            <p className="w-26 h-5 bg-skeletonbg mt-2"></p>
+                            <p className="mt-2 flex items-center justify-between">
+                                <span className="w-20 h-5 bg-skeletonbg"></span>
+                                <span className="h-8 w-8 bg-skeletonbg"></span>
+                            </p>
+                        </div>
+                    );
+
+                })}
+                
+            </div>
+
+        </div>
+    );
+}

+ 2 - 3
src/app/api/home/route.ts

@@ -18,9 +18,8 @@ export async function GET() {
 
     });
 
-    return NextResponse.json({
-      status: response.status,
-      data: response.body,
+    return NextResponse.json(response.body,{
+        status: response.status,
     });
 
   } catch (error) {

+ 2 - 4
src/app/api/shop/theme-customizations/route.ts

@@ -19,10 +19,8 @@ export async function GET(req: NextRequest) {
         name:searchParams.get("name") ?? "Top_Bar",
       }
     });
-
-    return NextResponse.json({
-      status: response.status,
-      data: response.body,
+    return NextResponse.json(response.body,{
+        status: response.status,
     });
 
   } catch (error) {

+ 8 - 12
src/app/globals.css

@@ -136,15 +136,10 @@
 
   --color-selected-black: var(--selected-black);
 
-  --color-dark-grey: var(--dark-grey);
-
   --color-selected-color: var(--selected-color);
   --color-selected-color-dark: var(--selected-color-dark);
 
-  --color-selected-bottom-dark: var(--selected-bottom-dark);
-  --color-selected-bg-bottom-dark: var(--selected-bg-bottom-dark);
-
-  --color-input-color: var(--input-color); /*placeholder:text-input-color 设置placeholder颜色*/
+  --color-skeletonbg: var(--skeleton-loading-background-color);
 }
 
 
@@ -171,15 +166,10 @@
     --hero-background: #ffffff;
     --hero-foreground: #000000;
 
-    --dark-grey: #404040;
-
     --selected-color: #e3ecff;
     --selected-color-dark: #555b69;
 
-    --selected-bottom-dark: #95b6ff;
-    --selected-bg-bottom-dark: #314779;
-
-    --input-color: #b3b3b3;
+    --skeleton-loading-background-color: #f7f7f7;
   }
 
 
@@ -294,6 +284,12 @@
       top: unset;
   }
 
+  .product-media-swiper .product-media-swiper-pagination{
+      width: auto;
+      left: auto;
+      bottom:auto;
+  }
+
   .fade-in{
     animation: fade-in 0.3s ease-in-out forwards;
   }

+ 20 - 5
src/assets/styles/cms-content.css

@@ -1,8 +1,28 @@
 .cms-content h2{
+    font-size: 1.25rem;
+    font-weight: 500;
+    margin: 1rem 0;
+}
+.cms-content h3{
     font-size: 1.125rem;
     font-weight: 500;
     margin: 1rem 0;
 }
+.cms-content h4{
+    font-size: 1rem;
+    font-weight: 500;
+    margin: 1rem 0;
+}
+.cms-content h5{
+    font-size: 0.875rem;
+    font-weight: 500;
+    margin: 1rem 0;
+}
+.cms-content h6{
+    font-size: 0.75rem;
+    font-weight: 500;
+    margin: 1rem 0;
+}
 .cms-content p{
     font-size: 0.875rem;
     line-height: 1.375rem;
@@ -14,9 +34,4 @@
 .cms-content ul li {
     font-size: 0.875rem;
     line-height: 1.375rem;
-}
-.cms-content img{
-    display: block;
-    width: 100%;
-    height: auto;
 }

+ 6 - 2
src/components/common/LoadingSpinner.tsx

@@ -2,7 +2,7 @@ import { type FC } from "react";
 
 interface LoadingSpinnerProps {
   colorClassName?: string;
-  size?: 16 | 24 | 32 | 48;
+  size?: 16 | 20 | 24 | 28 | 32 | 36 | 40 | 48;
   className?: string;
 }
 
@@ -13,8 +13,12 @@ export const LoadingSpinner: FC<LoadingSpinnerProps> = ({
 }) => {
   const sizeClasses = {
     16: "h-4 w-4",
+    20: "h-5 w-5",
     24: "h-6 w-6",
-    32: "h-8 w-8", 
+    28: "h-7 w-7",
+    32: "h-8 w-8",
+    36: "w-9 h-9", 
+    40: "w-10 h-10",
     48: "h-12 w-12"
   };
 

+ 13 - 11
src/components/common/LoginModal/LoginModal.tsx

@@ -2,9 +2,10 @@
 
 import {useState, useLayoutEffect, useEffect} from "react";
 import { useRouter } from 'next/navigation';
+import Link from "next/link";
 import { signIn } from "next-auth/react";
 import clsx from "clsx";
-import { useForm, Controller } from "react-hook-form";
+import { useForm, Controller, type FieldErrors } from "react-hook-form";
 import { EMAIL_REGEX, IS_VALID_INPUT, IS_VALID_FULL_PHONE } from "@utils/constants";
 import {mergeCartAction} from "@/actions";
 import { useAppDispatch } from "@/store/hooks";
@@ -186,6 +187,13 @@ export default function LoginModal({
         }
 
     };
+    const onRegisterInvalid = (errors: FieldErrors<RegisterFormData>) => {
+        const message = errors.isAgree?.message;
+
+        if (message) {
+            showToast(message, "warning");
+        }
+    };
     return (<>
         <CommonModal 
             isOpen={isShow}
@@ -223,7 +231,7 @@ export default function LoginModal({
                         className={clsx("relative px-4 pt-2 pb-6",{
                             "hidden": activeTab !== 'register'
                         })} 
-                        onSubmit={registerForm.handleSubmit(registerSubmit)}
+                        onSubmit={registerForm.handleSubmit(registerSubmit,onRegisterInvalid)}
                     >
                         
                         <div className="w-full flex justify-between">
@@ -374,13 +382,7 @@ export default function LoginModal({
                                     id="is_agree" 
                                     className="absolute opacity-0" 
                                     {...registerForm.register("isAgree", {
-                                        validate: (value: boolean) => {
-                                            if(!value) {
-                                                showToast('Please Agree To The Terms & Conditions And Privacy Policy.', "warning");
-                                                return 'Please Agree To The Terms & Conditions And Privacy Policy.';
-                                            } 
-                                            return true;
-                                        },
+                                        required: 'Please Agree To The Terms & Conditions And Privacy Policy.'
                                     })}
                                 />
                                 <span className="flex-none mr-2">
@@ -389,9 +391,9 @@ export default function LoginModal({
                                 </span>
                                 <span className="text-ly-12 text-[#666666]">
                                     I Agree To The 
-                                    Terms & Conditions
+                                    <Link className="underline" href="/terms-conditions">Terms & Conditions</Link>.
                                     And 
-                                    Privacy Policy
+                                    <Link className="underline" href="/privacy-policy">Privacy Policy</Link> 
                                     Of  asteriahair.com
                                 </span>
                                 

+ 0 - 37
src/components/common/icons/social-icon/FaceBookIcon.tsx

@@ -1,37 +0,0 @@
-const FaceBookIcon = () => {
-  return (
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      version="1.1"
-      id="svg9"
-      width="24"
-      height="24"
-      viewBox="0 0 666.66668 666.66717"
-    >
-      <defs id="defs13">
-        <clipPath clipPathUnits="userSpaceOnUse" id="clipPath25">
-          <path d="M 0,700 H 700 V 0 H 0 Z" id="path23" />
-        </clipPath>
-      </defs>
-      <g
-        id="g17"
-        transform="matrix(1.3333333,0,0,-1.3333333,-133.33333,799.99999)"
-      >
-        <g id="g19">
-          <g id="g21" clipPath="url(#clipPath25)">
-            <g id="g27" transform="translate(600,350)">
-              <path
-                d="m 0,0 c 0,138.071 -111.929,250 -250,250 -138.071,0 -250,-111.929 -250,-250 0,-117.245 80.715,-215.622 189.606,-242.638 v 166.242 h -51.552 V 0 h 51.552 v 32.919 c 0,85.092 38.508,124.532 122.048,124.532 15.838,0 43.167,-3.105 54.347,-6.211 V 81.986 c -5.901,0.621 -16.149,0.932 -28.882,0.932 -40.993,0 -56.832,-15.528 -56.832,-55.9 V 0 h 81.659 l -14.028,-76.396 h -67.631 V -248.169 C -95.927,-233.218 0,-127.818 0,0"
-                className="fill-black dark:fill-white"
-                id="path29"
-                fillOpacity="0.62"
-              />
-            </g>
-          </g>
-        </g>
-      </g>
-    </svg>
-  );
-};
-
-export default FaceBookIcon;

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 19
src/components/common/icons/social-icon/InstaGramIcon.tsx


+ 0 - 19
src/components/common/icons/social-icon/TwitterIcon.tsx

@@ -1,19 +0,0 @@
-const TwitterIcon = () => {
-  return (
-    <svg
-      width="24"
-      height="24"
-      viewBox="0 0 24 24"
-      fill="none"
-      xmlns="http://www.w3.org/2000/svg"
-      className="fill-black/[62] dark:fill-white"
-    >
-      <path
-        d="M13.6753 10.6587L20.2284 3.19922H18.6761L12.9836 9.67485L8.4405 3.19922H3.19922L10.0708 12.9924L3.19922 20.8139H4.75154L10.759 13.9739L15.5579 20.8139H20.7992M5.31181 4.34557H7.69661L18.6749 19.7239H16.2895"
-        fillOpacity="0.62"
-      />
-    </svg>
-  );
-};
-
-export default TwitterIcon;

+ 0 - 26
src/components/common/skeleton/AddressSkeleton.tsx

@@ -1,26 +0,0 @@
-export default function AddressSkeleton() {
-  return (
-    <div className="mt-5 grid grid-cols-1 gap-4 md:grid-cols-2">
-      {[...Array(4)].map((_, i) => (
-        <div
-          key={i}
-          className="animate-pulse space-y-3 rounded-2xl border border-neutral-300 bg-neutral-200 p-4 dark:border-neutral-800 dark:bg-neutral-900"
-        >
-          <div className="flex items-center justify-between">
-            <div className="h-6 w-6 rounded-full bg-neutral-300 dark:bg-neutral-800" />
-            <div className="flex items-center space-x-2">
-              <div className="h-4 w-4 rounded-full bg-neutral-300 dark:bg-neutral-800" />
-              <div className="h-4 w-4 rounded bg-neutral-300 dark:bg-neutral-800" />
-            </div>
-          </div>
-
-          <div className="h-4 w-2/3 rounded bg-neutral-300 dark:bg-neutral-800" />
-
-          <div className="h-3 w-full rounded bg-neutral-300 dark:bg-neutral-800" />
-          <div className="h-3 w-5/6 rounded bg-neutral-300 dark:bg-neutral-800" />
-          <div className="h-3 w-1/2 rounded bg-neutral-300 dark:bg-neutral-800" />
-        </div>
-      ))}
-    </div>
-  );
-}

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

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

+ 0 - 61
src/components/common/skeleton/CheckoutSkeleton.tsx

@@ -1,61 +0,0 @@
-export default function CheckoutSkeleton() {
-  return (
-    <div className="mx-auto w-full flex-1 animate-pulse space-y-6 rounded-md px-0 py-10 text-white">
-      {/* Logo */}
-      <div className="flex items-center gap-x-2">
-        <div className="h-10 w-12 rounded-sm bg-gray-300 text-center text-sm text-black dark:bg-gray-600 dark:text-white" />
-        <div className="h-6 w-24 rounded-sm bg-gray-300 text-center text-sm text-black dark:bg-gray-600 dark:text-white" />
-      </div>
-
-      <div className="flex items-center gap-3">
-        <div className="h-6 w-6 rounded-full bg-gray-300 text-center text-sm text-black dark:bg-gray-600 dark:text-white">
-          1
-        </div>
-        <div className="h-4 w-20 rounded bg-gray-200 dark:bg-gray-700" />
-      </div>
-
-      <div className="h-12 w-full rounded bg-gray-200 dark:bg-gray-800" />
-
-      <div className="h-4 w-40 rounded bg-gray-200 dark:bg-gray-700" />
-
-      <div className="flex justify-end">
-        <div className="h-12 w-24 rounded-full bg-gray-300 dark:bg-gray-700" />
-      </div>
-
-      {[2, 3, 4, 5].map((step) => (
-        <div key={step} className="flex items-center gap-3">
-          <div className="h-6 w-6 rounded-full bg-gray-300 text-center text-sm text-black dark:bg-gray-700 dark:text-white">
-            {step}
-          </div>
-          <div className="h-4 w-24 rounded bg-gray-200 dark:bg-gray-700" />
-        </div>
-      ))}
-    </div>
-  );
-}
-
-export const CartCheckoutPageSkeleton = () => {
-  return (
-    <section className="w-full animate-pulse py-6 text-black dark:text-white">
-      <div className="flex w-full flex-col">
-        {[1, 2].map((_, index) => (
-          <div
-            key={index}
-            className="mb-4 flex w-full gap-4 rounded-lg bg-white px-4 py-4 dark:bg-neutral-900"
-          >
-            <div className="h-6 w-6 flex-shrink-0 rounded-full bg-gray-200 dark:bg-gray-700" />
-
-            <div className="flex flex-1 flex-col justify-between space-y-2">
-              <div className="flex items-start justify-between">
-                <div className="h-6 w-3/5 rounded bg-gray-200 dark:bg-gray-700" />
-                <div className="h-6 w-16 rounded bg-gray-200 dark:bg-gray-700" />
-              </div>
-
-              <div className="h-3 w-1/2 rounded bg-gray-200 dark:bg-gray-700" />
-            </div>
-          </div>
-        ))}
-      </div>
-    </section>
-  );
-};

+ 0 - 50
src/components/common/skeleton/FilterSkeleton.tsx

@@ -1,50 +0,0 @@
-import { type FC } from "react";
-
-export default function FilterListSkeleton() {
-  return (
-    <div className="flex w-full flex-auto items-center gap-x-4 sm:max-w-md">
-      {Array.from({ length: 3 }).map((_, idx) => (
-        <div key={idx} className="w-full md:min-w-48">
-          <div className="animate-pulse space-y-2">
-            <div className="h-4 w-1/2 rounded bg-gray-300 dark:bg-gray-700" />
-            <div className="h-10 w-full rounded bg-gray-200 dark:bg-gray-800" />
-            <div className="mt-2 flex flex-wrap gap-2">
-              {Array.from({ length: 2 }).map((__, chipIdx) => (
-                <div
-                  key={chipIdx}
-                  className="h-6 w-16 rounded-full bg-gray-300 dark:bg-gray-700"
-                />
-              ))}
-            </div>
-          </div>
-        </div>
-      ))}
-    </div>
-  );
-}
-
-export const SortOrderSkeleton: FC = () => {
-  return (
-    <section className="flex w-full flex-1 items-center gap-x-2.5 sm:max-w-[15.625rem]">
-      <div className="hidden h-4 w-24 animate-pulse rounded bg-gray-300 md:block dark:bg-gray-700" />
-      <div className="h-10 w-full animate-pulse rounded bg-gray-200 dark:bg-gray-800" />
-    </section>
-  );
-};
-export const ProductCardSkeleton: FC = () => {
-  return (
-    <div className="flex animate-pulse flex-col gap-y-2.5">
-      <div className="group relative overflow-hidden rounded-lg">
-        <div className="aspect-square w-full rounded-lg bg-gray-300 dark:bg-gray-700" />
-        <div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 items-center gap-x-4 rounded-full bg-white/50 px-4 py-1.5 text-xs font-semibold text-black opacity-0 backdrop-blur-md duration-300 group-hover:opacity-100 dark:text-white">
-          <div className="h-6 w-6 rounded-full bg-gray-400 dark:bg-gray-600" />
-        </div>
-      </div>
-
-      <div>
-        <div className="mb-2.5 h-5 w-3/4 rounded bg-gray-300 dark:bg-gray-700" />
-        <div className="h-4 w-1/3 rounded bg-gray-300 dark:bg-gray-700" />
-      </div>
-    </div>
-  );
-};

+ 0 - 14
src/components/common/skeleton/IconSkeleton.tsx

@@ -1,14 +0,0 @@
-
-type IconSkeletonProps = {
-  className?: string;
-};
-
-export function IconSkeleton({ className }: IconSkeletonProps) {
-  return (
-    <div
-      className={`relative flex h-9 w-9 items-center justify-center rounded-md border border-solid border-neutral-200 bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-800 md:h-9 md:w-9 lg:h-11 lg:w-11 ${className ?? ""}`}
-    >
-      <div className="h-4 w-4 rounded-full bg-neutral-200 dark:bg-neutral-700 animate-pulse" />
-    </div>
-  );
-}

+ 0 - 22
src/components/common/skeleton/NavbarSkeleton.tsx

@@ -1,22 +0,0 @@
-export function NavbarSkeletonLoader() {
-  return (
-    <div className="space-y-6">
-      <div className="h-12 w-full animate-pulse rounded-md bg-gray-300" />
-      <div className="h-8 w-3/4 animate-pulse rounded-md bg-gray-300" />
-
-      <div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
-        {Array.from({ length: 6 }).map((_, index) => (
-          <div
-            key={index}
-            className="space-y-4 rounded-md bg-white p-4 shadow-lg"
-          >
-            <div className="h-48 animate-pulse rounded-md bg-gray-300" />
-
-            <div className="h-6 animate-pulse rounded-md bg-gray-300" />
-            <div className="h-4 w-3/4 animate-pulse rounded-md bg-gray-300" />
-          </div>
-        ))}
-      </div>
-    </div>
-  );
-}

+ 0 - 79
src/components/common/skeleton/ProductCartSkeleton.tsx

@@ -1,79 +0,0 @@
-import clsx from "clsx";
-import CheckoutSkeleton from "./CheckoutSkeleton";
-
-export default function ProductCartSkeleton() {
-  return (
-    <div className="flex">
-      <CheckoutSkeleton />
-      <CartSkeleton className="w-1/2" />
-    </div>
-  );
-}
-
-export const CartSkeleton = ({ className }: { className: string }) => {
-  return (
-    <div
-      className={clsx(
-        "hidden h-full w-1/2 flex-col justify-between px-8 py-8 pt-4 text-black dark:text-white lg:flex lg:max-w-full",
-        className,
-      )}
-    >
-      <div>
-        <div className="my-6 animate-pulse" role="status">
-          <div className="h-12 w-1/2 rounded-md bg-gray-200 dark:bg-gray-700" />
-          <span className="sr-only">Loading...</span>
-        </div>
-
-        {Array(2)
-          .fill(0)
-          .map((_, index) => (
-            <div
-              key={index}
-              className="mb-3 flex w-full max-w-full animate-pulse gap-4 rounded-lg bg-white px-4 py-4 dark:bg-neutral-900"
-            >
-              <div className="h-[194px] w-[194px] flex-shrink-0 rounded-md bg-gray-200 dark:bg-gray-700" />
-
-              <div className="flex flex-1 flex-col justify-between space-y-2">
-                <div className="flex items-start justify-between">
-                  <div className="h-8 w-3/5 rounded bg-gray-200 dark:bg-gray-700" />
-                  <div className="h-8 w-16 rounded bg-gray-200 dark:bg-gray-700" />
-                </div>
-
-                <div className="h-3 w-1/2 rounded bg-gray-200 dark:bg-gray-700" />
-
-                <div className="h-8 w-24 rounded bg-gray-200 dark:bg-gray-700" />
-
-                <div className="space-y-1 pt-2">
-                  <div className="h-3 w-full rounded bg-gray-200 dark:bg-gray-700" />
-                  <div className="h-3 w-11/12 rounded bg-gray-200 dark:bg-gray-700" />
-                  <div className="h-3 w-3/4 rounded bg-gray-200 dark:bg-gray-700" />
-                </div>
-              </div>
-            </div>
-          ))}
-      </div>
-      <CartTotalSkeleton />
-    </div>
-  );
-};
-
-export function CartTotalSkeleton() {
-  return (
-    <div className="w-full max-w-full animate-pulse space-y-4 rounded-md bg-white py-4 text-black dark:bg-neutral-900 dark:text-white">
-      <div className="flex items-center justify-between px-4">
-        <div className="h-4 w-20 rounded bg-gray-200 dark:bg-gray-700" />
-        <div className="h-4 w-20 rounded bg-gray-200 dark:bg-gray-700" />
-      </div>
-
-      <div className="flex items-center justify-between px-4">
-        <div className="h-4 w-24 rounded bg-gray-200 dark:bg-gray-700" />
-        <div className="h-4 w-24 rounded bg-gray-200 dark:bg-gray-700" />
-      </div>
-
-      <div className="flex items-center justify-between px-4 pt-2">
-        <div className="h-6 w-32 rounded bg-gray-200 dark:bg-gray-700" />
-        <div className="h-6 w-32 rounded bg-gray-200 dark:bg-gray-700" />
-      </div>
-    </div>
-  );
-}

+ 22 - 87
src/components/common/skeleton/ProductSkeleton.tsx

@@ -1,89 +1,24 @@
-"use client";
-
-import { useScrollToTop } from "@/utils/hooks/useScrollTo";
-
-export const ProductPageSkeleton = () => {
-  useScrollToTop();
-
-  return (
-    <div className="min-h-screen py-8">
-      <div className="container mx-auto">
-        <ProductDetailSkeleton />
-        <RelatedProductSkeleton />
-      </div>
-    </div>
-  );
-};
-
-export const ProductDetailSkeleton = () => {
-  return (
-    <div className="flex flex-row gap-x-8">
-      <div className="flex min-w-[878px] items-center justify-center">
-        <div className="h-[736px] w-full animate-pulse rounded-2xl bg-neutral-100 dark:bg-neutral-900" />
-      </div>
-
-      <div className="flex w-full flex-col space-y-6">
-        <div className="h-12 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-        <div className="h-12 w-1/2 animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-        <div className="flex justify-between">
-          <div className="h-6 w-1/3 animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-
-          <div className="h-6 w-1/3 animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-        </div>
-        <hr className="text-neutral-100 dark:text-neutral-900" />
-
-        <div className="space-y-4">
-          <div className="h-6 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-4 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-6 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-4 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-4 w-2/3 animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-        </div>
-        <div className="flex gap-x-6">
-          <div className="h-10 w-24 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-10 w-24 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-10 w-24 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-10 w-24 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />
-        </div>
-        <div className="flex gap-x-6">
-          <div className="h-12 w-32 animate-pulse rounded-full bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-12 w-48 animate-pulse rounded-xl bg-neutral-100 dark:bg-neutral-900" />
-        </div>
-
-        <hr className="text-neutral-100 dark:text-neutral-900" />
-
-        <div className="space-y-4">
-          <div className="h-10 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-10 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-          <div className="h-10 w-full animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-900" />
-        </div>
-      </div>
-    </div>
-  );
-};
-
+import {NOT_IMAGE} from "@/utils/constants";
 export const RelatedProductSkeleton = () => {
-  return (
-    <div className="mt-20">
-      <div className="flex flex-col">
-        <div className="h-12 w-1/3 animate-pulse rounded-md bg-neutral-100 text-2xl font-semibold text-white dark:bg-neutral-900" />
-        <div className="mt-4 h-6 w-1/2 animate-pulse rounded-md bg-neutral-100 text-2xl font-semibold text-white dark:bg-neutral-900" />
-        <div className="mt-1 h-6 w-1/6 animate-pulse rounded-md bg-neutral-100 text-2xl font-semibold text-white dark:bg-neutral-900" />
-      </div>
-
-      <div className="mt-10 grid grid-flow-row grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
-        {Array(4)
-          .fill(0)
-          .map((_, index) => {
-            return (
-              <div key={index} className="animate-pulse">
-                <div className="max-w-92.5 aspect-square w-full rounded-lg bg-neutral-100 dark:bg-neutral-900" />
-                <div className="mt-4 h-6 w-3/4 rounded-md bg-neutral-100 dark:bg-neutral-900" />
-                <div className="mt-2 h-4 w-1/2 rounded-md bg-neutral-100 dark:bg-neutral-900" />
-              </div>
-            );
-          })}
-      </div>
-    </div>
-  );
+    const arr = [1,2,3,4];
+    return (
+        <div className="w-full px-4 py-8 box-border">
+            <div className="bg-skeletonbg h-6 w-60"></div>
+            <div className="w-full flex justify-between flex-wrap mt-2">
+                {arr.map((i) => {
+                    return (
+                        <div className="flex-none w-42.5 mt-4" key={i}>
+                            <img className="block w-42.5 h-57 object-contain bg-skeletonbg" src={NOT_IMAGE} />
+                            <p className="mt-2 h-5 bg-skeletonbg"></p>
+                            <p className="w-26 h-5 bg-skeletonbg mt-2"></p>
+                            <p className="mt-2 flex items-center justify-between">
+                                <span className="w-20 h-5 bg-skeletonbg"></span>
+                                <span className="h-8 w-8 bg-skeletonbg"></span>
+                            </p>
+                        </div>
+                    );
+                })}
+            </div>
+        </div>
+    );
 };

+ 0 - 15
src/components/common/skeleton/SearchSkeleton.tsx

@@ -1,15 +0,0 @@
-import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
-
-export const SearchSkeleton = () => {
-  return (
-    <div className="max-w-[550px] relative w-full md:min-w-[386px] xl:min-w-[516px]">
-      <input
-        className="input w-full rounded-lg border border-neutral-200 bg-white py-2 pl-3 pr-10 text-sm text-black outline-none placeholder:text-neutral-500 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400 md:pl-4"
-        placeholder="Search for products..."
-      />
-      <div className="absolute bottom-0 right-1 top-0 flex w-9 items-center justify-center border-l border-neutral-200 dark:border-neutral-700 md:border-0">
-        <MagnifyingGlassIcon className="size-5 stroke-neutral-500" />
-      </div>
-    </div>
-  );
-}

+ 0 - 135
src/components/common/slider/HeroCarousel.tsx

@@ -1,135 +0,0 @@
-"use client";
-
-import * as React from "react";
-import Image from "next/image";
-import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline";
-import { GridTileImage } from "@/components/theme/ui/grid/Tile";
-import { Shimmer } from "@/components/common/Shimmer";
-import {
-  HeroCarouselShimmer,
-  HeroCarouselThumbnailShimmer,
-} from "./HeroCarouselShimmer";
-
-export default function HeroCarousel({
-  images,
-}: {
-  images: { src: string; altText: string }[];
-}) {
-  const [current, setCurrent] = React.useState(0);
-  const [isLoading, setIsLoading] = React.useState(true);
-
-  const prevSlide = () => {
-    setIsLoading(true);
-    setCurrent((prev) => (prev === 0 ? images.length - 1 : prev - 1));
-  };
-
-  const nextSlide = () => {
-    setIsLoading(true);
-    setCurrent((prev) => (prev === images.length - 1 ? 0 : prev + 1));
-  };
-
-  if (!images || images.length === 0) {
-    return (
-      <>
-        <HeroCarouselShimmer />
-        <HeroCarouselThumbnailShimmer count={3} />
-      </>
-    );
-  }
-  const buttonClassName =
-    "flex h-full cursor-pointer items-center justify-center px-6 transition-all ease-in-out hover:scale-110 hover:text-black dark:hover:text-white";
-
-  return (
-    <>
-      <div className="group relative overflow-hidden">
-        <div
-          key={current}
-          className="group relative h-full max-h-[738px] w-full overflow-hidden rounded-2xl transition-opacity duration-600 ease-in-out"
-          style={{
-            aspectRatio: "380/316",
-          }}
-        >
-          <div className="relative h-full w-full">
-            {isLoading && (
-              <Shimmer
-                className="absolute inset-0 z-10 h-full w-full"
-                rounded="lg"
-              />
-            )}
-            <Image
-              fill
-              alt={images[current]?.altText as string}
-              className={`h-full w-full object-cover transition duration-300 ease-in-out group-hover:scale-105 ${
-                isLoading ? "opacity-0" : "opacity-100"
-              }`}
-              priority={true}
-              sizes="(max-width: 1024px) 100vw, (max-width: 1536px) 66vw, 1000px"
-              src={images[current]?.src as string}
-              onLoad={() => setIsLoading(false)}
-              onError={() => setIsLoading(false)}
-            />
-          </div>
-        </div>
-
-        {images?.length > 1 ? (
-          <div className="absolute bottom-[5%] flex w-full justify-center">
-            <div className="mx-auto flex h-11 items-center rounded-full border border-gray-500/70 bg-gray-500/70 text-white/80 backdrop-blur transition-all duration-300 dark:border-black dark:bg-neutral-900/80">
-              <button
-                aria-label="Previous image"
-                className={buttonClassName}
-                onClick={prevSlide}
-              >
-                <ArrowLeftIcon className="h-5" />
-              </button>
-              <div className="mx-1 h-6 w-px bg-white/80" />
-              <button
-                aria-label="Next image"
-                className={buttonClassName}
-                onClick={nextSlide}
-              >
-                <ArrowRightIcon className="h-5" />
-              </button>
-            </div>
-          </div>
-        ) : null}
-      </div>
-
-      {images?.length > 1 ? (
-        <ul className="fade-in my-3 flex flex-nowrap gap-2 overflow-x-auto overflow-y-hidden py-1 sm:my-7 lg:mb-0">
-          {images.map((image, index) => {
-            const isActive = index === current;
-
-            return (
-              <li
-                key={image.src}
-                className="relative aspect-square w-16 md:w-32 flex-shrink-0"
-              >
-                <button
-                  className="h-full w-full"
-                  onClick={() => {
-                    setCurrent(index);
-                  }}
-                >
-                  <GridTileImage
-                    active={isActive}
-                    alt={image.altText}
-                    fill
-                    objectFit="cover"
-                    src={image.src}
-                    sizes="(max-width: 768px) 16vw, 10vw"
-                  />
-                </button>
-              </li>
-            );
-          })}
-        </ul>
-      ) : isLoading ? (
-        <div className="my-3 sm:my-7 lg:mb-0">
-          <div className="relative aspect-square w-16 md:w-32">
-            <Shimmer className="h-full w-full" rounded="lg" />
-          </div>
-        </div>
-      ) : null}
-    </>
-  );
-}

+ 0 - 42
src/components/common/slider/HeroCarouselShimmer.tsx

@@ -1,42 +0,0 @@
-"use client";
-
-import { Shimmer } from "@/components/common/Shimmer";
-// 占位组件
-export function HeroCarouselShimmer({ratio = "3/4"} : {ratio?: string}) {
-  return (
-    <div className="group relative overflow-hidden">
-      <div
-        className="group relative h-full max-h-[738px] w-full overflow-hidden rounded-2xl"
-        style={{
-          aspectRatio: ratio,
-        }}
-      >
-        <div className="relative h-full w-full">
-          <Shimmer
-            className="absolute inset-0 z-10 h-full w-full"
-            rounded="lg"
-          />
-        </div>
-      </div>
-    </div>
-  );
-}
-
-export function HeroCarouselThumbnailShimmer({
-  count = 3,
-}: {
-  count?: number;
-}) {
-  return (
-    <ul className="fade-in my-3 flex flex-nowrap gap-2 overflow-x-auto overflow-y-hidden py-1 sm:my-7 lg:mb-0">
-      {Array.from({ length: count }).map((_, index) => (
-        <li
-          key={index}
-          className="relative aspect-square w-16 md:w-32 flex-shrink-0"
-        >
-          <Shimmer className="h-full w-full" rounded="lg" />
-        </li>
-      ))}
-    </ul>
-  );
-}

+ 0 - 2
src/components/common/slider/index.ts

@@ -1,2 +0,0 @@
-export { default as HeroCarousel } from './HeroCarousel';
-export { HeroCarouselShimmer, HeroCarouselThumbnailShimmer } from './HeroCarouselShimmer';

+ 0 - 14
src/components/customer/types.ts

@@ -1,14 +0,0 @@
-export type RecoverPasswordFormState = {
-  success?: {
-    status: boolean;
-    msg: string;
-  };
-  errors?: {
-    email?: string[];
-    apiRes?: {
-      status: boolean;
-      msg: string;
-    };
-  };
-};
-

+ 46 - 0
src/components/layout/footer/CompanyInfo.tsx

@@ -0,0 +1,46 @@
+"use client";
+
+import Link from "next/link";
+import { useState } from "react";
+import clsx from "clsx";
+import { motion } from "framer-motion";
+
+
+export function CompanyInfo() {
+    const [isSpread, setIsSpread] = useState(false);
+    return (
+        <div className="w-full border-b-1 border-[#404D49]">
+            <div className="flex py-6 justify-between items-center">
+                <span className="text-ly-16 text-[#FFDBA3] font-bold">Company Info</span>
+                <button type="button" aria-expanded={isSpread} className={clsx("w-6 h-6 transition-transform", isSpread && '-rotate-180')} onClick={() => setIsSpread(!isSpread)}>
+                    <svg className="block w-full h-full" 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(255, 219, 163, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M18 9L12 15L6 9"></path></svg>
+                </button>
+            </div>
+            <motion.div
+                layout
+                initial={{ height: 0, }}
+                animate={{ height: isSpread ? 'auto' : 0 }}
+                exit={{ height: 0 }}
+                className="overflow-hidden"
+            >
+                <div className="w-full">
+                    <Link href="mailto:Service@asteriahair.com" className="block text-ly-14 text-white underline mt-6 first:mt-0 last:mb-8">
+                        service@asteriahair.com
+                    </Link>
+                    <Link href="tel:+8616696557019" className="block text-ly-14 text-white underline mt-6 first:mt-0 last:mb-8">
+                        Phone/WhatsApp: +86 166 9655 7019
+                    </Link>
+                    <p className="block text-ly-14 text-white underline mt-6 first:mt-0 last:mb-8">
+                        Wholesale WhatsApp: <Link href="tel:+8615238065852">+86 152 3806 5852</Link> / <Link href="tel:+8615136424039">+86 151 3642 4039</Link>
+                    </p>
+                    <p className="block text-ly-14 text-white underline mt-6 first:mt-0 last:mb-8">
+                        Company: LONGYIHK IMP EXP CO.,LIMITED FLAT 2502B FLOOR 25 148 ELECTRIC ROAD, NORTH POINT, HK
+                    </p>
+                    <p className="block text-ly-14 text-white underline mt-6 first:mt-0 last:mb-8">
+                        Customer Service Available: Sunday to Thursday, PDT 17:00pm-3:00am
+                    </p>
+                </div>
+            </motion.div>
+        </div>
+    )
+}

+ 79 - 72
src/components/layout/footer/FooterMenu.tsx

@@ -1,84 +1,91 @@
-import Link from "next/link";
-import type { ThemeOptions } from "@/types/types";
-import { isArray } from "@/utils/type-guards";
-import { safeParse } from "@/utils/helper";
-
-const getUrlparams = (url: string) => {
-  const splitUrl = url.split("/");
+"use client";
 
-  if (isArray(splitUrl)) {
-    const urlLength = splitUrl.length;
-
-    if (urlLength >= 1) {
-      return `/${splitUrl.at(urlLength - 1)}`;
-    }
-  }
+import { use, useState } from "react";
+import clsx from "clsx";
+import Link from "next/link";
+import { motion } from "framer-motion";
+import type { FetchGraphqlResult } from "@/types/graphqlFetch/type";
+import type { GetThemeCustomization } from "@/types/theme/theme-customization";
 
-  return "/";
-};
+interface FooterMenuLink {
+    url: string;
+    title: string;
+    sort_order: string;
+}
 
-const FooterMenuItem = ({ item }: { item: ThemeOptions }) => {
-  return (
-    <li className="text-selected-black">
-      <Link
-        aria-label={`${item?.title}`}
-        title={`${item?.title}`}
-        className="block px-0 py-1 text-nowrap text-sm underline-offset-4 text-selected-black"
-        href={getUrlparams(item.url)}
-      >
-        {item.title}
-      </Link>
-    </li>
-  );
-};
 
 export default function FooterMenu({
-  menu,
+    footerLinksPromise,
 }: {
-  menu: any;
+    footerLinksPromise: Promise<FetchGraphqlResult<GetThemeCustomization>>;
 }) {
-  if (!menu || menu.length === 0) return null;
+    const [columnStatus,setColumnStatus] = useState(new Set());
+    const res = use(footerLinksPromise);
 
-  const firstMenu = menu[0]?.node;
-  const firstTranslation = firstMenu?.translations?.edges?.[0]?.node;
-  const channels = typeof firstTranslation?.options === 'string'
-    ? safeParse(firstTranslation.options)
-    : firstTranslation?.options;
+    const linkList = [];
+    if(!res.error) {
+        const edgeItem = res.data?.themeCustomizations.edges[0];
+        if(edgeItem) {
+            const options = JSON.parse(edgeItem.node.translation.options);
+            linkList[0] = {
+                title: "Asteria Hair",
+                id: "column_1",
+                list: options['column_1'] || []
+            };
+            linkList[1] = {
+                title: "Guide",
+                id: "column_2",
+                list: options['column_2'] || []
+            };
+            linkList[2] = {
+                title: "How Can We Help",
+                id: "column_3",
+                list: options['column_3'] || []
+            };
+        }
+    }
 
-  return (
-    <div className="">
-      {/* Render columns 1 */}
-      {isArray(channels?.column_1) ? (
-        <nav className="w-full">
-          <ul>
-            {channels.column_1.map((item: ThemeOptions, index: number) => {
-              return <FooterMenuItem key={index} item={item} />;
-            })}
-          </ul>
-        </nav>
-      ) : null}
+    const columnClick = (id: string) => {
+        setColumnStatus((prev) => {
+            const next = new Set(prev);
+            if(next.has(id)) {
+                next.delete(id);  
+            } else {
+                next.add(id);
+            }
+            return next;
+        });
+    };
 
-      {/* Render columns 2 */}
-      {isArray(channels?.column_2) ? (
-        <nav className="w-full">
-          <ul>
-            {channels.column_2.map((item: ThemeOptions, index: number) => {
-              return <FooterMenuItem key={index} item={item} />;
-            })}
-          </ul>
-        </nav>
-      ) : null}
+    return (<>
+        {res.error && <p className="text-center text-ly-errorcolor text-ly-12 pt-4">{res.error.message}</p>}
+        {linkList.map((listItem) => {
+            return  <div className="w-full border-b-1 border-[#404D49]" key={listItem.id}>
+                        <div className="flex py-6 justify-between items-center">
+                            <span className="text-ly-16 text-[#FFDBA3] font-bold">{listItem.title}</span>
+                            <button type="button" aria-expanded={columnStatus.has(listItem.id)}
+                                className={clsx("w-6 h-6 transition-transform", columnStatus.has(listItem.id) && '-rotate-180')} 
+                                onClick={() => columnClick(listItem.id)}
+                            >
+                                <svg className="block w-full h-full" 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(255, 219, 163, 1)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="square"  d="M18 9L12 15L6 9"></path></svg>
+                            </button>
+                        </div>
+                        <motion.div
+                            layout
+                            initial={{ height: 0, }}
+                            animate={{ height: columnStatus.has(listItem.id) ? 'auto' : 0 }}
+                            exit={{ height: 0 }}
+                            className="overflow-hidden"
+                        >
+                            <div className="w-full">
+                                {listItem.list.map((item: FooterMenuLink) => {
+                                    return <Link key={item.title} className="block text-ly-14 text-white underline mt-6 first:mt-0 last:mb-8" href={item.url}>{item.title}</Link>
+                                })}
+                            </div>
+                        </motion.div>
+                        
 
-      {/* Render columns 3 */}
-      {isArray(channels?.column_3) ? (
-        <nav className="w-full">
-          <ul>
-            {channels.column_3.map((item: ThemeOptions, index: number) => {
-              return <FooterMenuItem key={index} item={item} />;
-            })}
-          </ul>
-        </nav>
-      ) : null}
-    </div>
-  );
+                    </div>
+        })}
+    </>);
 }

+ 0 - 116
src/components/layout/footer/Subscribe.tsx

@@ -1,116 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import clsx from "clsx";
-import { useForm } from "react-hook-form";
-import type { RecoverPasswordFormState } from "@components/customer/types";
-import { Button } from "@components/common/button/LoadingButton";
-import { userSubscribe } from "@utils/actions";
-import { useCustomToast } from "@utils/hooks/useToast";
-import { EMAIL_REGEX } from "@utils/constants";
-
-type FormValues = {
-  email: string;
-};
-
-const Subscribe = () => {
-  const [loading, setLoading] = useState<boolean>(false);
-  const {
-    register,
-    handleSubmit,
-    reset,
-    formState: { errors, isSubmitting },
-  } = useForm<FormValues>({ mode: "onSubmit" });
-  const { showToast } = useCustomToast();
-
-  const [status, setStatus] = useState<
-    RecoverPasswordFormState["errors"] | null
-  >(null);
-
-  const onSubmit = async (data: FormValues) => {
-    setStatus(null);
-    const formData = new FormData();
-
-    formData.append("email", data.email);
-    setLoading(true);
-    const result = await userSubscribe(undefined as any, formData);
-
-    setStatus(result?.errors || null);
-    if (result?.errors?.apiRes?.status) {
-      reset();
-    }
-    setLoading(false);
-  };
-
-  useEffect(() => {
-    if (status) {
-      setTimeout(() => {
-        setStatus(null);
-      }, 3500);
-    }
-    if (status?.email) {
-      showToast(status?.email[0], "warning");
-    }
-    if (status?.apiRes?.status === false) {
-      showToast(status?.apiRes?.msg, "warning");
-    }
-    if (status?.apiRes?.status === true) {
-      showToast("Successfully Subscribed", "success");
-    }
-  }, [status]);
-
-  return (
-    <form
-      noValidate
-      className="mt-4 md:mt-0 md:px-0 relative"
-      onSubmit={handleSubmit(onSubmit)}
-    >
-      <p className="mb-1 text-base font-semibold">Newsletter</p>
-      <p className="font-sm font-normal">
-        Subscribe to our newsletter for exclusive offers!
-      </p>
-
-      <div className="mt-4 flex gap-x-3">
-        <input
-          type="email"
-          aria-label="Email Address"
-          {...register("email", {
-            required: "Email is required",
-            pattern: {
-              value: EMAIL_REGEX,
-              message: "Enter a valid email",
-            },
-          })}
-          className={clsx(
-            "block rounded-2xl w-full xl:min-w-[283px] p-2.5 text-sm focus:border-blue-200 border border-neutral-200 bg-white  text-black outline-none placeholder:text-neutral-500 focus:ring-1 focus:ring-neutral-300 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400 ",
-            errors.email || status?.email
-              ? "border-red-500 dark:border-red-500"
-              : "border-gray-300 dark:border-neutral-200"
-          )}
-          placeholder="Email Address"
-        />
-        <Button
-          className={clsx(
-            "relative flex font-medium w-full min-w-32 max-w-32 items-center justify-center !rounded-2xl border-1 border-solid border-neutral-600 bg-transparent px-5 py-2.5 font-outfit text-[15px]  tracking-wide dark:!text-white !text-neutral-600",
-            {
-              "hover:opacity-90": !isSubmitting,
-              "cursor-not-allowed opacity-50": isSubmitting,
-            }
-          )}
-          disabled={loading || isSubmitting}
-          loading={loading || isSubmitting}
-          title="Subscribe"
-          type="submit"
-        />
-      </div>
-
-      {errors.email && (
-        <p className="mt-1 absolute left-0 -bottom-2.5 text-sm text-red-600 dark:text-red-400">
-          {errors.email.message}
-        </p>
-      )}
-    </form>
-  );
-};
-
-export default Subscribe;

Datei-Diff unterdrückt, da er zu groß ist
+ 103 - 0
src/components/layout/footer/SubscribeNewsletter.tsx


+ 48 - 121
src/components/layout/footer/index.tsx

@@ -1,135 +1,62 @@
-import Link from "next/link";
+import Image from "next/image";
 import { Suspense } from "react";
 import { serverGraphqlFetch } from "@/utils/bagisto";
-import type {
-  GetFooterResponse,
-  ThemeCustomizationResult
-} from "@/types/theme/theme-customization";
-import {
-  GET_FOOTER
-} from "@/graphql";
+import type { GetThemeCustomization } from "@/types/theme/theme-customization";
+import { GET_THEME_CUSTOMIZATION } from "@/graphql";
 import { env } from "@/env";
-import LogoIcon from "@components/common/icons/LogoIcon";
-import FaceBookIcon from "@components/common/icons/social-icon/FaceBookIcon";
-import InstaGramIcon from "@components/common/icons/social-icon/InstaGramIcon";
-import TwitterIcon from "@components/common/icons/social-icon/TwitterIcon";
-import Subscribe from "./Subscribe";
+import { CompanyInfo } from "./CompanyInfo";
+import SubscribeNewsletter from "./SubscribeNewsletter";
 import FooterMenu from "./FooterMenu";
 
 
-async function getFooterData(): Promise<ThemeCustomizationResult> {
- 
-    const [{data: footerRes}, {data:servicesRes}] = await Promise.all([
-      serverGraphqlFetch<GetFooterResponse,{type: string;}>({
-        query: GET_FOOTER,
-        variables:{
-          type: "footer_links",
-        },
-        takeAuthorization: false
-      }),
-      serverGraphqlFetch<GetFooterResponse,{type: string;}>({
-        query: GET_FOOTER,
+export default async function Footer() {
+    const currentYear = new Date().getFullYear();
+    const copyrightDate = 2016 + (currentYear > 2010 ? `-${currentYear}` : "");
+    const copyrightName = env.STORE_NAME;
+
+    const footerLinksPromise = serverGraphqlFetch<GetThemeCustomization,{name: string;}>({
+        query: GET_THEME_CUSTOMIZATION,
         variables:{
-          type: "services_content",
+            name: "bottom_footer_links",
         },
         takeAuthorization: false
-      }),
-    ]);
-    return {
-      footer_links: footerRes,
-      services_content: servicesRes,
-    };
-}
+    });
 
-export default async function Footer() {
-  const currentYear = new Date().getFullYear();
-  const copyrightDate = 2010 + (currentYear > 2010 ? `-${currentYear}` : "");
-  const skeleton =
-    "w-full h-6 animate-pulse rounded bg-neutral-200 dark:bg-neutral-700";
-  const menu = await getFooterData();
-  const copyrightName = env.STORE_NAME;
 
+    return (
+        
+        <footer className="box-border w-full bg-[#1B2623] px-4 pb-9">
+            
+            <Suspense fallback={<div className="text-center text-white text-ly-12 py-6">Loading menu list...</div>}>
+                <FooterMenu footerLinksPromise={footerLinksPromise} />
+            </Suspense>
+
+            <CompanyInfo />
 
-  return (
-    <>
-      <footer className="border-t border-neutral-200 text-sm text-neutral-500">
-        <div className="mx-auto flex w-full flex-col justify-between gap-6 gap-y-6 px-6 py-12 text-sm">
-          <div className="flex flex-col gap-[14px]">
-            <Link
-              className="flex items-center gap-2 cursor-pointer"
-              href="/"
-              aria-label="Go to homepage"
-              title="Go to homepage"
-            >
-              <LogoIcon />
-              <span className="sr-only">Go to homepage</span>
-            </Link>
-            <div className="flex gap-[14px]">
-              <Link
-                href={"#"}
-                aria-label="Visit Bagisto Store on Facebook"
-                title="Facebook"
-                target="_blank"
-                className="cursor-pointer"
-              >
-                <FaceBookIcon />
-                <span className="sr-only">Facebook</span>
-              </Link>
-              <Link
-                href={"#"}
-                aria-label="Visit Bagisto Store on Instagram"
-                title="Instagram"
-                target="_blank"
-                className="cursor-pointer"
-              >
-                <InstaGramIcon />
-                <span className="sr-only">Instagram</span>
-              </Link>
-              <Link
-                href={"#"}
-                aria-label="Visit Bagisto Store on Twitter"
-                title="Twitter"
-                target="_blank"
-                className="cursor-pointer"
-              >
-                <TwitterIcon />
-                <span className="sr-only">Twitter</span>
-              </Link>
+            <div className="w-full mt-6 pb-6 border-b-1 border-[#404D49]">
+                <SubscribeNewsletter />
             </div>
-          </div>
-          <div className="flex flex-col gap-x-8">
-            <Suspense
-              fallback={
-                <div className="flex h-[188px] w-[200px] flex-col gap-2">
-                  {Array(6)
-                    .fill(0)
-                    .map((_, index) => (
-                      <div key={index} className={skeleton} />
-                    ))}
-                </div>
-              }
-            >
-              <FooterMenu
-                menu={menu?.footer_links?.themeCustomizations?.edges}
-              />
-            </Suspense>
-            <Subscribe />
-          </div>
-        </div>
-        <div className="border-t border-neutral-200 py-6 text-sm dark:border-neutral-700">
-          <div className="mx-auto flex w-full max-w-screen-2xl flex-col justify-center gap-1 px-4 md:flex-row">
-            <p className="text-center">
-              &copy; {copyrightDate} {copyrightName}
-              {copyrightName.length && !copyrightName.endsWith(".")
-                ? "."
-                : ""}{" "}
-              All rights reserved.
-            </p>
-            <hr className="mx-4 hidden h-4 w-[1px] border-l border-neutral-400 md:inline-block" />
-            <p className="text-center">Designed in Bagisto</p>
-          </div>
-        </div>
-      </footer>
-    </>
-  );
+
+
+            <div className="w-full mt-6">
+                <Image src="/image/footer-payment.webp"
+                    className="block w-87.5 h-19 mx-auto"
+                    alt="Support payment methods"
+                    width={350}
+                    height={76}
+                />
+            </div>
+
+            <div className="w-full mt-9">
+                <Image src="/image/logo-white.webp"
+                    className="block w-51.5 h-12.75 mx-auto"
+                    alt="Asteriahair logo"
+                    width={206}
+                    height={51}
+                />
+                <p className="text-center mt-3 text-ly-14 text-white">{copyrightDate} {copyrightName} Hair . All Rights Reserved</p>
+            </div>
+
+        </footer>
+    );
 }

+ 4 - 4
src/components/layout/navbar/MobileSearch.tsx

@@ -1,7 +1,7 @@
 "use client";
-import { Suspense } from "react";
+
 import Search from "./Search";
-import { SearchSkeleton } from "@/components/common/skeleton/SearchSkeleton";
+
 
 
 
@@ -13,9 +13,9 @@ export const MobileSearchBar = ({
   return (
     <>
       <div className="block w-full justify-center md:hidden px-4 mt-3 z-10">
-        <Suspense fallback={<SearchSkeleton />}>
+ 
           <Search search={false} onClose={onClose} />
-        </Suspense>
+   
       </div>
     </>
   );

+ 0 - 17
src/components/layout/navbar/NavigationSkeleton.tsx

@@ -1,17 +0,0 @@
-export function NavigationSkeleton() {
-  return (
-    <>
-      {/* Mobile menu skeleton */}
-      <div className="h-6 w-6 animate-pulse rounded bg-neutral-200 dark:bg-neutral-700 lg:hidden" />
-      
-      {/* Desktop navigation skeleton */}
-      <ul className="hidden gap-4 text-sm md:items-center lg:flex xl:gap-6">
-        {Array.from({ length: 4 }).map((_, i) => (
-          <li key={i}>
-            <div className="h-4 w-16 animate-pulse rounded bg-neutral-200 dark:bg-neutral-700" />
-          </li>
-        ))}
-      </ul>
-    </>
-  );
-}

+ 4 - 7
src/components/layout/navbar/index.tsx

@@ -1,9 +1,7 @@
 import Link from "next/link";
-import { Suspense } from "react";
 import LogoIcon from "@components/common/icons/LogoIcon";
 import MobileMenuTrigger from "./MobileMenuTrigger";
 import { NavbarActions } from "./NavbarActions";
-import { NavigationSkeleton } from "./NavigationSkeleton";
 import { NavbarErrorBoundary } from "@/components/error/ErrorBoundary";
 
 import { 
@@ -11,8 +9,7 @@ import {
 import { serverGraphqlFetch } from "@utils/bagisto/index";
 import NavbarSearchIcon from "./NavbarSearchIcon"
 import type { GetTreefrontMenusResult } from "@/types/theme/menus";
-import { authOptions } from "@utils/auth";
-import { getServerSession } from "next-auth";
+import { auth } from "@/utils/auth/auth-helper";
 import TopSwiperBanner from "./TopSwiperBanner"
 import { restApiFetch } from "@utils/bagisto/index";
 import type { AnnouncementItem } from "./TopSwiperBanner";
@@ -40,7 +37,7 @@ export default async function Navbar() {
   }
   console.log('topbar-res----------------------------:',res);
   
-    const session = await getServerSession(authOptions);// 游客是null
+    const session = await auth();// 游客是null
     const isGuest = session === null;
 
     const { data: menudata } = await serverGraphqlFetch<
@@ -60,9 +57,9 @@ export default async function Navbar() {
             {/* 1. THE STATIC SHELL (Visible Instantly) */}
             <div className="flex items-center max-w-fit gap-5 xl:gap-6">
               {/* 2. STATIC HOLE: Categories (Suspended) */}
-              <Suspense fallback={<NavigationSkeleton />}>
+        
                 <MobileMenuTrigger isGuest={isGuest} menuData={menudatas} />
-              </Suspense>
+        
               <div className="flex-1 justify-center flex">
                 {/* <Suspense fallback={<SearchSkeleton />}>
                   <Search search={false} />

+ 4 - 3
src/graphql/theme/mutations/subscribeToNewsletter.ts

@@ -1,12 +1,13 @@
 import { gql } from "@apollo/client";
 
 export const SUBSCRIBE_TO_NEWSLETTER = gql`
-  mutation SubscribeToNewsletter($input: createNewsletterSubscriptionInput!) {
-    createNewsletterSubscription(input: $input) {
-      newsletterSubscription {
+  mutation createNewsletter($input: createNewsletterInput!) {
+    createNewsletter(input: $input) {
+      newsletter {
         success
         message
       }
     }
   }
 `;
+

+ 1 - 1
src/graphql/theme/queries/ThemeCustomization.ts

@@ -16,7 +16,7 @@ import { gql } from "@apollo/client";
  * 如果定义变量时后边有!,表示这个变量是必须要有值的,没有!表示这个变量可以不传值(也就是接口参数是否是必传的)
  * edges 和 node 是graphql中的概念,edges相当于数组,node相当于数组中的项
  */
-export const GET_THEME_CUSTOMIZATION = gql`
+export const ALL_THEME_CUSTOMIZATIONS = gql`
   query themeCustomization($first: Int) {
     themeCustomizations(first: $first) {
       edges {

+ 0 - 26
src/graphql/theme/queries/Footer.ts

@@ -1,26 +0,0 @@
-import { gql } from "@apollo/client";
-
-export const GET_FOOTER = gql`
-  query footerQuery($type: String) {
-    themeCustomizations(type: $type) {
-      edges {
-        node {
-          id
-          type
-          name
-          status
-          translations {
-            edges {
-              node {
-                id
-                themeCustomizationId
-                locale
-                options
-              }
-            }
-          }
-        }
-      }
-    }
-  }
-`;

+ 26 - 0
src/graphql/theme/queries/GetThemeCustomization.ts

@@ -0,0 +1,26 @@
+import { gql } from "@apollo/client";
+
+// type 查询的是一组type相同的数据
+// name查询的是一个name是给定值的数据
+export const GET_THEME_CUSTOMIZATION = gql`
+  query ThemeCustomizationQuery($type: String,$name:String) {
+    themeCustomizations(type: $type, name:$name) {
+      edges {
+        node {
+          id
+          type
+          name
+          status
+          translation {
+            id
+            _id
+            themeCustomizationId
+            locale
+            options
+            
+          }
+        }
+      }
+    }
+  }
+`;

+ 4 - 4
src/graphql/theme/queries/index.ts

@@ -1,10 +1,10 @@
 export {
-    GET_FOOTER
-} from "./Footer"
+    GET_THEME_CUSTOMIZATION
+} from "./GetThemeCustomization"
 
 export {
-GET_THEME_CUSTOMIZATION
-} from "./ThemeCustomization"
+ALL_THEME_CUSTOMIZATIONS
+} from "./AllThemeCustomization"
 
 export {
     PAGE_BY_URL_KEY

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

@@ -110,4 +110,13 @@ export interface GetCustomerAddressResponse {
         endCursor: string;
     };
     totalCount: number;
+}
+
+export interface CreateNewsletterResponse{
+    createNewsletter: {
+        newsletter: {
+            success: boolean;
+            message: string;
+        }
+    }
 }

+ 18 - 69
src/types/theme/theme-customization.ts

@@ -1,74 +1,23 @@
-export interface ThemeTranslationNode {
-  id: string;
-  themeCustomizationId: string;
-  locale: string;
-  options: string;
-}
-
-export interface ThemeTranslationEdge {
-  node: ThemeTranslationNode;
-}
-
-
-
-// footer 
-export interface ThemeOptions {
-  title: string;
-  url: string;
-}
-export interface FooterColumns {
-  column_1?: ThemeOptions[];
-  column_2?: ThemeOptions[];
-  column_3?: ThemeOptions[];
-}
-export interface GetFooterResponse {
-  themeCustomizations: ThemeCustomizationConnection;
-}
-
-export interface GetFooterVariables {
-  type?: string;
-}
-export interface ThemeCustomizationConnection {
-  edges: ThemeCustomizationEdge[];
-}
-
-export interface ThemeCustomizationEdge {
-  node: ThemeCustomizationFooterNode;
+export interface ThemeCustomizationNodeTranslation {
+    id: string;
+    _id: number;
+    themeCustomizationId: string;
+    locale: string;
+    options: string;
+}
+export interface ThemeCustomizationNode {
+    id: string;
+    type: string;
+    name: string
+    status: string;
+    translation: ThemeCustomizationNodeTranslation;
+}
+export interface GetThemeCustomization {
+  themeCustomizations: {
+    edges: Array<{node:ThemeCustomizationNode}>
+  }
 }
 
-export interface ThemeCustomizationFooterNode {
-  id: string;
-  type: string;
-  name: string;
-  status: boolean;
-  themeCode?: string;
-  translations: ThemeCustomizationTranslationConnection;
-}
-export interface ThemeCustomizationTranslationConnection {
-  edges: ThemeCustomizationTranslationEdge[];
-}
-
-export interface ThemeCustomizationTranslationNode {
-  id: string;
-  themeCustomizationId: string;
-  locale: string;
-  options: string | FooterColumns;
-}
-
-
-export interface ThemeCustomizationTranslationEdge {
-  node: ThemeCustomizationTranslationNode;
-}
-export interface FooterMenuProps {
-  menu: ThemeCustomizationEdge[];
-}
-
-export interface ThemeCustomizationResult {
-  footer_links: GetFooterResponse | null;
-  services_content: GetFooterResponse | null;
-}
-
-
 
 // Theme Customization Options Types
 

+ 0 - 77
src/utils/actions.ts

@@ -1,77 +0,0 @@
-"use server";
-// 文件顶部的 "use server" 是所有导出的异步函数自动成为 Server Action 的标志。
-import { redirect } from "next/navigation";
-import {
-  subscribeUser,
-} from '@/utils/bagisto';
-
-import type { RecoverPasswordFormState } from "@components/customer/types";
-
-export type RegisterFormState = {
-  errors?: {
-    firstName?: string[];
-    lastName?: string[];
-    email?: string[];
-    password?: string[];
-    passwordConfirmation?: string[];
-    apiError?: string;
-    agreement?: string[];
-  };
-};
-
-export async function redirectToCheckout(formData: FormData) {
-  const url = formData.get("url") as string;
-  redirect(url);
-}
-
-
-
-export async function userSubscribe(
-  _prevState: RecoverPasswordFormState,
-  formData: FormData
-): Promise<RecoverPasswordFormState> {
-  const email = formData.get("email");
-
-  const data = {
-    email: typeof email === "string" ? email.trim() : "",
-  };
-
-  try {
-    const result = await subscribeUser(data) as Record<string, unknown>;
-
-    if (result?.error) {
-      const error = result.error as Record<string, unknown>;
-      return {
-        errors: {
-          apiRes: {
-            status: false,
-            msg: (error.message as string) || "Something went wrong",
-          },
-        },
-      };
-    }
-
-    const body = result?.body as Record<string, unknown>;
-    const bodyData = body?.data as Record<string, unknown>;
-
-    return {
-      errors: {
-        apiRes: {
-          status: true,
-          msg: (bodyData?.message as string) || "Subscription successful!",
-        },
-      },
-    };
-  } catch {
-    return {
-      errors: {
-        apiRes: {
-          status: false,
-          msg: "Unexpected error occurred. Please try again.",
-        },
-      },
-    };
-  }
-}
-
-

+ 3 - 0
src/utils/auth.ts

@@ -69,8 +69,11 @@ export const authOptions: NextAuthOptions = {
   ],
 
   //https://next-auth.js.org/configuration/callbacks
+  // nextAuth.js 的jwt token 和session 存在自动续签问题
+  // 但是accessToken没有刷新机制
   callbacks: {
     async jwt({ token, user }) {
+      // 登录时 user是有值的,登录过后,获取用户信息user是null
       if (user) {
         token.id = user.id;
         token.apiToken = user.apiToken;

+ 0 - 46
src/utils/bagisto/index.ts

@@ -11,13 +11,6 @@ import {
   CURRENT_LOCAL,
   CURRENT_CHANNEL,
 } from "@/utils/constants";
-import {
-  PAGE_BY_URL_KEY,
-} from "@/graphql";
-import { SUBSCRIBE_TO_NEWSLETTER } from "@/graphql/theme/mutations";
-import type {
-  PageData,
-} from "@/types/theme/theme-customization";
 import type {FetchGraphqlResult} from "@/types/graphqlFetch/type";
 import { env } from "@/env";
 
@@ -28,9 +21,6 @@ type ExtractVariables<T> = T extends { variables: object }
 type ExtractRestFulData<T> = T extends { data: infer D } ? D : any;
 type ExtractGraphqlData<T> = T extends { data: infer D } ? { data: D } : any;
 
-interface PageByUrlKeyResponse {
-  pageByUrlKeypages?: PageData[];
-}
 const STOREFRONT_KEY = env.NEXT_PUBLIC_BAGISTO_STOREFRONT_KEY;
 async function getBaseHeader() {
     /** 
@@ -313,39 +303,3 @@ export async function bagistoFetch<T>({
     throw e;
   }
 }
-
-
-
-export async function subscribeUser(
-  input: Record<string, unknown>,
-): Promise<unknown> {
-  try {
-    return await bagistoFetch<{
-      data: unknown;
-      variables: Record<string, unknown>;
-    }>({
-      query: SUBSCRIBE_TO_NEWSLETTER,
-      variables: {
-        ...input,
-      },
-      cache: "no-store",
-    });
-  } catch (error) {
-    return error;
-  }
-}
-
-
-export async function getPage(input: { urlKey: string }): Promise<PageData[]> {
-  const res = await bagistoFetch<{
-    data: PageByUrlKeyResponse;
-    variables: { pageByUrlKey: string };
-  }>({
-    query: PAGE_BY_URL_KEY,
-    cache: "no-store",
-    // isCookies: false,
-    variables: { pageByUrlKey: input.urlKey },
-  });
-
-  return res.body.data?.pageByUrlKeypages || [];
-}

+ 1 - 1
src/utils/constants.ts

@@ -29,7 +29,7 @@ export const SORT = "sort";
  * Placeholder Images
  */
 export const NOT_IMAGE = "/image/placeholder.webp";
-export const LOGO_IMG = "/image/logo.png";
+export const LOGO_IMG = "/image/logo.webp";
 
 export const variants = {
   hidden: { opacity: 0, y: 50 },

+ 7 - 1
src/utils/hooks/useAddToCart.ts

@@ -9,6 +9,7 @@ import { useMutation, useApolloClient } from "@apollo/client/react";
 import {
   UPDATE_CART_ITEM,
 } from "@/graphql";
+import { emitAuthExpired } from "@/utils/auth/auth-events";
 import { formatCartDetail } from "@/utils/cartDetailTools";
 import {handleApolloBusinessError} from "@/lib/ApolloErrorHandler";
 import {addProductToCartAction, deleteCartProductAction} from '@/actions';
@@ -43,7 +44,12 @@ export const useAddProduct = () => {
           
 
       } else {
-          showToast(res.msg, "danger");
+          if(res.error && res.error.name === 'UnauthorizedError') {
+            emitAuthExpired();
+          } else {
+            showToast(res.msg, "danger");
+          }
+          
       }
       return result;
   };

+ 3 - 3
src/utils/metadata.ts

@@ -3,19 +3,19 @@ export const staticSeo = {
   default: {
     title: "Aateria Hair",
     description: "Aateria Hair",
-    image: "/logo.png",
+    image: "/image/logo.webp",
     canonical: "/",
   },
   login: {
     title: "Login or Register",
     description: "Login or Register to Aateria",
-    image: "/logo.png",
+    image: "/image/logo.webp",
     canonical: "/customer/login",
   },
   forget:{
     title: "Forget Password",
   description: "Recover your account by resetting your password.",
-  image: "/logo.png",
+  image: "/image/logo.webp",
   canonical: "/customer/forget-password",
   }
 };