fogwind 1 неделя назад
Родитель
Сommit
9984f85ea8
36 измененных файлов с 444 добавлено и 674 удалено
  1. BIN
      public/image/footer-payment.webp
  2. BIN
      public/image/logo-white.webp
  3. BIN
      public/image/logo.webp
  4. 2 1
      src/actions/index.ts
  5. 41 0
      src/actions/subscribeNewsletterAction.tsx
  6. 1 1
      src/app/(checkout)/checkout/_components/CheckoutWrapper.tsx
  7. 1 1
      src/app/(checkout)/checkout/_components/ContinueToPay/PaymentMethodContinueTpPay.tsx
  8. 4 6
      src/app/(checkout)/checkout/loading.tsx
  9. 15 1
      src/app/(public)/(cms)/[slug]/page.tsx
  10. 2 2
      src/app/(public)/customer/login/_components/LoginPageWrapper.tsx
  11. 1 2
      src/app/(public)/loading.tsx
  12. 1 1
      src/app/(public)/product/_components/ProductInformation.tsx
  13. 1 1
      src/app/(public)/product/_components/ProductText.tsx
  14. 0 14
      src/app/globals.css
  15. 20 5
      src/assets/styles/cms-content.css
  16. 13 11
      src/components/common/LoginModal/LoginModal.tsx
  17. 0 37
      src/components/common/icons/social-icon/FaceBookIcon.tsx
  18. 0 19
      src/components/common/icons/social-icon/InstaGramIcon.tsx
  19. 0 19
      src/components/common/icons/social-icon/TwitterIcon.tsx
  20. 0 14
      src/components/customer/types.ts
  21. 46 0
      src/components/layout/footer/CompanyInfo.tsx
  22. 79 72
      src/components/layout/footer/FooterMenu.tsx
  23. 0 116
      src/components/layout/footer/Subscribe.tsx
  24. 103 0
      src/components/layout/footer/SubscribeNewsletter.tsx
  25. 48 121
      src/components/layout/footer/index.tsx
  26. 4 3
      src/graphql/theme/mutations/subscribeToNewsletter.ts
  27. 1 1
      src/graphql/theme/queries/ThemeCustomization.ts
  28. 0 26
      src/graphql/theme/queries/Footer.ts
  29. 26 0
      src/graphql/theme/queries/GetThemeCustomization.ts
  30. 4 4
      src/graphql/theme/queries/index.ts
  31. 9 0
      src/types/customer/type.ts
  32. 18 69
      src/types/theme/theme-customization.ts
  33. 0 77
      src/utils/actions.ts
  34. 0 46
      src/utils/bagisto/index.ts
  35. 1 1
      src/utils/constants.ts
  36. 3 3
      src/utils/metadata.ts

BIN
public/image/footer-payment.webp


BIN
public/image/logo-white.webp


BIN
public/image/logo.webp


+ 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">

+ 4 - 6
src/app/(checkout)/checkout/loading.tsx

@@ -46,17 +46,15 @@ export default function Loading() {
             </div>
 
             <div className="mt-6 box-border px-4">
-                <h3 className="text-ly-24 font-medium bg-skeletonbg">Shipping Method</h3>
+                <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">
-                            <span>USPS/FedEx/DHL/UPS</span>
-                        </p>
-                        <p className="text-ly-12 bg-skeletonbg mt-1">(2-5 business days)</p>
+                        <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">$0.00</span>
+                            <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>

+ 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,

+ 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>
                         

+ 1 - 2
src/app/(public)/loading.tsx

@@ -29,8 +29,7 @@ export default function Loading() {
             <div className="box-border px-4 mt-6">
                 <div className="h-6 w-40 bg-skeletonbg"></div>
                 <div className="flex justify-between flex-wrap">
-                    {arr.map((i) => {
-                        if(i > 4) return <></>;
+                    {[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} />

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

+ 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 (

+ 0 - 14
src/app/globals.css

@@ -136,16 +136,9 @@
 
   --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);
 }
 
@@ -173,16 +166,9 @@
     --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;
   }
 

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

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

Разница между файлами не показана из-за своего большого размера
+ 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 - 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;

Разница между файлами не показана из-за своего большого размера
+ 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 - 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.",
-        },
-      },
-    };
-  }
-}
-
-

+ 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 },

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