Selaa lähdekoodia

Merge branch 'master' into dev

zhangzf 1 päivä sitten
vanhempi
commit
5902fc3384

+ 3 - 3
.env.example

@@ -9,9 +9,9 @@ NEXT_PUBLIC_APP_ENV=development
 NEXT_PUBLIC_SITE_URL=http://localhost:3001
 
 # Bagisto API Configuration
-# 后端接口域名
+# 后端接口域名 测试站 https://sisbridal.com/
 NEXT_PUBLIC_BAGISTO_ENDPOINT=https://your-bagisto-instance.com
-# 后端接口需要的key
+# 后端接口需要的key 测试站  pk_storefront_5JHJ7e8Vb10aCmlTexi5OaaFRqPYsrOy
 NEXT_PUBLIC_BAGISTO_STOREFRONT_KEY=your_storefront_key_here
 
 # NextAuth Configuration 测试环境和生产环境要写实际部署的域名
@@ -19,7 +19,7 @@ NEXTAUTH_URL=http://localhost:3001
 # NextAuth 使用 secret(下面的值)对 session中的token 对象进行 签名 + 加密,生成一个 JWT 字符串,解密这个JWT字符串就可以还原token对象。
 NEXTAUTH_SECRET=9jstKgQ1Q22PlDDhPtXTZ6G4MEzHGDJIkebcd/5UyZ4=
 
-# paypal client id
+# paypal client id 测试站 AXSFRWCfsqKWU2OuXAirHm5FG7U3J4KlYOpP4NvvJ9hQzK10SWMrHqotwnvQhjznf9A-stXrtVmrHBQR
 PAYPAL_SANDBOX_CLIENT_ID=
 PAYPAL_PRODUCTION_CLIENT_ID=
 

+ 19 - 0
src/app/(public)/(cms)/[slug]/_components/CMSContent.tsx

@@ -0,0 +1,19 @@
+"use client";
+
+
+export default function CmsContent({
+    html,
+}: {
+    html: string;
+}) {
+    return (
+        <div
+            className="cms-content"
+            dangerouslySetInnerHTML={{
+                __html: html,
+            }}
+        />
+    );
+}
+
+

+ 28 - 0
src/app/(public)/(cms)/[slug]/page.tsx

@@ -0,0 +1,28 @@
+import { notFound } from "next/navigation";
+import { getPage } from "@utils/bagisto";
+import type { PageData } from "@/types/theme/theme-customization";
+import CmsContent from "./_components/CMSContent";
+
+
+export default async function Page({
+    params,
+}: {
+    params: Promise<{ slug: string }>;
+}) {
+    const { slug: pageParams } = await params;
+    const pageDataArray: PageData[] = await getPage({ urlKey: pageParams });
+    if (!pageDataArray?.length) return notFound();
+    const pageData = pageDataArray?.[0]?.translation;
+
+    return (
+        <div className="my-6 px-4 mx-auto">
+
+            <h1 className="relative pb-4 text-2xl text-center font-bold leading-8 mb-6 after:absolute after:left-1/2 after:-bottom-1 after:-translate-x-1/2 after:w-20 after:h-1 after:bg-black">
+                {pageData?.pageTitle}
+            </h1>
+            <CmsContent html={pageData?.htmlContent || ""} />
+
+        
+        </div>
+    );
+}

src/app/(public)/hair-care-tips/page.tsx → src/app/(public)/(cms)/hair-care-tips/page.tsx


src/app/(public)/help-faqs/_components/Faqs.tsx → src/app/(public)/(cms)/help-faqs/_components/Faqs.tsx


src/app/(public)/help-faqs/page.tsx → src/app/(public)/(cms)/help-faqs/page.tsx


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

@@ -1,35 +0,0 @@
-import { notFound } from "next/navigation";
-import Prose from "@components/theme/search/Prose";
-import { getPage } from "@utils/bagisto";
-import type { PageData } from "@/types/theme/theme-customization";
-
-
-export default async function Page({
-  params,
-}: {
-  params: Promise<{ page: string }>;
-}) {
-  const { page: pageParams } = await params;
-  const pageDataArray: PageData[] = await getPage({ urlKey: pageParams });
-  if (!pageDataArray?.length) return notFound();
-  const pageData = pageDataArray?.[0]?.translation;
-
-  return (
-    <div className="my-4 flex flex-col justify-between p-4">
-      <div className="flex flex-col gap-4 mx-auto">
-        <h1 className="text-2xl md:text-3xl font-bold">{pageData?.pageTitle}</h1>
-        <Prose className="mb-8" html={pageData?.htmlContent || ""} />
-      <p className="text-sm italic">
-        {`This document was last updated on ${new Intl.DateTimeFormat(
-          undefined,
-          {
-            year: "numeric",
-            month: "long",
-            day: "numeric",
-          },
-        )?.format(new Date(pageDataArray?.[0]?.updatedAt || "---"))}.`}
-      </p>
-      </div>
-    </div>
-  );
-}

+ 26 - 9
src/app/(public)/_components/HomeContent.tsx

@@ -10,6 +10,8 @@ import MiddleImageSwiper from "./MiddleImageSwiper";
 import Trending from "./Trending";
 import RecommendedProducts from "./RecommendedProducts";
 import CategoryTabSwiper  from "./CategoryTabSwiper ";
+import Image from "next/image";
+import Link from "next/link";
 import type {HomeApiData,HomeProduct } from "@/types/home/type"
 import {CustomerService} from "@/components/common/CustomerService/CustomerService";
 // 定义组件入参类型
@@ -98,31 +100,34 @@ export default function HomeContent({ homeData }: HomeContentProps) {
         // categories 
       } = homeData;
       // 过滤出 banner 板块
-  const bannerSection = sections.find(sec => sec.name === "banner");
+  const bannerSection = sections.find(sec => sec.showstyle === '2');
   const bannerImages = bannerSection?.images ?? [];
        // 过滤出 banner 板块
-  const jinGangSection = sections.find(sec => sec.id === 13);
+  const jinGangSection = sections.find(sec => sec.showstyle === '3');
   const jinGang = jinGangSection?.images ?? [];
     // 2. 筛选热销板块,假设后端name为 best_sellers,自行修改匹配后端标识
-  const bestSellerSection = sections.find(sec => sec.name === "BestSellers");
+  const bestSellerSection = sections.find(sec => sec.showstyle === '4');
   const bestSellers: HomeProduct[] = bestSellerSection?.products ?? [];
         // 过滤出 banner 板块
-  const middleBannerSection = sections.find(sec => sec.id === 15);
+  const middleBannerSection = sections.find(sec => sec.showstyle === '5');
   const middleBanner = middleBannerSection?.images ?? [];
    // 3. 筛选new Arrival板块
-  const newArrivalSection = sections.find(sec => sec.name === "New_Arrival");
+  const newArrivalSection = sections.find(sec => sec.showstyle === '6');
   const newArrival: HomeProduct[] = newArrivalSection?.products ?? [];
    // 4. 筛选7板块
-  const zeroWearImageSection = sections.find(sec => sec.id === 18);
+  const zeroWearImageSection = sections.find(sec => sec.showstyle === '7');
   const zeroWearImage= zeroWearImageSection?.images ?? [];
    // 4. 筛选7板块
-  const zeroWearSection = sections.find(sec => sec.name === "7");
+  const zeroWearSection = sections.find(sec => sec.showstyle === '8');
   const zeroWear: HomeProduct[] = zeroWearSection?.products ?? [];
    // 4. 筛选trendingSection板块
-  const trendingSection = sections.find(sec => sec.name === "Trending_Now");
+  const trendingSection = sections.find(sec => sec.showstyle === '9');
   const trending: HomeProduct[] = trendingSection?.products ?? [];
+    // 4. 筛选7板块
+  const bottomBannerSection = sections.find(sec => sec.showstyle === '10');
+  const bottomBanner= bottomBannerSection?.images ?? [];
    // 4. Recommended
-  const recommendedSection = sections.find(sec => sec.name === "Recommended_For_You");
+  const recommendedSection = sections.find(sec => sec.showstyle === '11');
   const recommended: HomeProduct[] = recommendedSection?.products ?? [];
   return (<>
     <div className="min-h-screen bg-white font-sans">
@@ -148,6 +153,18 @@ export default function HomeContent({ homeData }: HomeContentProps) {
       <HomeNewArrival newArrival={newArrival} />
       <MiddleImageSwiper zeroWear={zeroWear}  zeroWearImage={zeroWearImage} />
       <Trending trending={trending}/>
+      {
+        (bottomBanner && bottomBanner.length !== 0) ? (
+          <Link href={bottomBanner[0].link} className="relative w-full aspect-[3/4] mt-8">
+            <Image
+              src={bottomBanner[0].image}
+              alt={bottomBanner[0].title}
+              fill
+              className="object-cover rounded-md"
+            />
+          </Link>
+        ) : null
+      }
       <RecommendedProducts recommended={recommended} />
       <CategoryTabSwiper />
     </div>

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

@@ -139,12 +139,13 @@ const Trending = ({ trending }: trendingProps) => {
                 {/* 单个商品卡片 */}
                 <div className=" overflow-hidden  flex flex-col">
                   {/* 图片区域 + 中间播放按钮 */}
-                  <div className="relative rounded-xl w-full aspect-[4/5] overflow-hidden rounded-xl">
-                    <img
-                      src={item.image_url}
-                      alt={item.name}
-                      className="w-full h-full object-cover"
-                    />
+                  <div className="relative  w-full aspect-[4/5] overflow-hidden rounded-xl">
+                   <Image
+                       src={item.image_url}
+                       alt={item.name}
+                       fill
+                       className="object-cover"
+                     />
                     {/* 居中圆形播放按钮 */}
                     <button
                       onClick={() => handlePlayClick(item.videoUrl||"/video/hair-demo-1.mp4" )}
@@ -305,16 +306,7 @@ const Trending = ({ trending }: trendingProps) => {
           </div>
         )}
       </div>
-      <div className="relative w-full aspect-[3/4] mt-8">
-        <Image
-          src={
-            "https://cdn.asteriahair.com/uploads/202607/01/20260701165110_3fff435adf276b89.jpg"
-          }
-          alt={"cscscs"}
-          fill
-          className="object-cover rounded-md"
-        />
-      </div>
+    
     </div>
   );
 };

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

@@ -8,12 +8,12 @@ export default async function PublicLayout({
   children: ReactNode;
 }) {
   return (
-    <main>
+    <>
       <Navbar />
       <div className="mx-auto min-h-[calc(100vh-580px)] w-full">
         {children}
       </div>
       <Footer />
-    </main>
+    </>
   );
 }

+ 39 - 0
src/app/api/shop/theme-customizations/route.ts

@@ -0,0 +1,39 @@
+import {  type NextRequest,NextResponse } from "next/server";
+import { restApiFetch } from "@/utils/bagisto";
+
+
+export async function GET(req: NextRequest) {
+  try {
+    //  const { id } = await params; // 解包拿到订单ID
+  
+     const searchParams = req.nextUrl.searchParams; 
+   const apiUrl = `/shop/theme-customizations`;
+
+    console.log("最终请求API地址:", apiUrl);
+
+    const response = await restApiFetch<any>({
+      api: apiUrl,
+      method: "GET",
+      cache: "no-store",
+      variables:{
+        name:searchParams.get("name") ?? "Top_Bar",
+      }
+    });
+
+    return NextResponse.json({
+      status: response.status,
+      data: response.body,
+    });
+
+  } catch (error) {
+
+
+    return NextResponse.json(
+      {
+        message: "Network error",
+        error: error instanceof Error ? error.message : error,
+      },
+      { status: 500 }
+    );
+  }
+}

+ 2 - 0
src/app/globals.css

@@ -43,6 +43,8 @@
 @custom-variant dark (&:is(.dark *));
 
 
+@import "../assets/styles/cms-content.css" layer(components);
+
 /* =========================================================
  * Tailwind theme tokens
  *

+ 22 - 0
src/assets/styles/cms-content.css

@@ -0,0 +1,22 @@
+.cms-content h2{
+    font-size: 1.125rem;
+    font-weight: 500;
+    margin: 1rem 0;
+}
+.cms-content p{
+    font-size: 0.875rem;
+    line-height: 1.375rem;
+    margin-top: 0.875rem;
+}
+.cms-content ul{
+    margin-top: 0.75rem;
+}
+.cms-content ul li {
+    font-size: 0.875rem;
+    line-height: 1.375rem;
+}
+.cms-content img{
+    display: block;
+    width: 100%;
+    height: auto;
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 171
src/components/common/icons/NotFoundIcon.tsx


+ 112 - 0
src/components/layout/navbar/TopSwiperBanner.tsx

@@ -0,0 +1,112 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import Link from "next/link";
+
+const ITEM_HEIGHT = 40;
+const INTERVAL = 3000;
+const TRANSITION_DURATION = 500;
+
+export interface AnnouncementItem {
+  link: string;
+  image?: string;
+  title: string;
+}
+
+interface AnnouncementBarProps {
+  items?: AnnouncementItem[];
+}
+
+export default function TopSwiperBanner({ items = [] }: AnnouncementBarProps) {
+  const [index, setIndex] = useState(0);
+
+  const [enableTransition, setEnableTransition] = useState(true);
+
+  const loopItems = [...items, ...(items.length ? [items[0]] : [])];
+
+  // 自动滚动
+  useEffect(() => {
+    if (items.length <= 1) {
+      return;
+    }
+
+    const timer = window.setInterval(() => {
+      setIndex((prev) => prev + 1);
+    }, INTERVAL);
+
+    return () => {
+      window.clearInterval(timer);
+    };
+  }, [items.length]);
+
+  // 无缝循环重置
+  useEffect(() => {
+    if (index !== items.length) {
+      return;
+    }
+
+    const timer = window.setTimeout(() => {
+      setEnableTransition(false);
+
+      setIndex(0);
+
+      requestAnimationFrame(() => {
+        requestAnimationFrame(() => {
+          setEnableTransition(true);
+        });
+      });
+    }, TRANSITION_DURATION);
+
+    return () => {
+      window.clearTimeout(timer);
+    };
+  }, [index, items.length]);
+  if (!items.length) {
+    return null;
+  }
+
+  return (
+    <div
+      className="
+        h-10
+        w-full
+        overflow-hidden
+        bg-[#036141]
+      "
+    >
+      <div
+        className={
+          enableTransition
+            ? "transition-transform duration-500 ease-in-out"
+            : ""
+        }
+        style={{
+          transform: `translateY(-${index * ITEM_HEIGHT}px)`,
+        }}
+      >
+        {loopItems.map((item, itemIndex) => (
+          <div
+            key={itemIndex}
+            className="
+                  flex
+                  h-10
+                  items-center
+                  justify-center
+                  px-4
+                  text-center
+                  text-[14px]
+                  font-medium
+                  text-white
+                "
+          >
+            {item.link ? (
+              <Link href={item.link}>{item.title}</Link>
+            ) : (
+              <span>{item.title}</span>
+            )}
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+}

+ 27 - 1
src/components/layout/navbar/index.tsx

@@ -13,8 +13,33 @@ import NavbarSearchIcon from "./NavbarSearchIcon"
 import type { GetTreefrontMenusResult } from "@/types/theme/menus";
 import { authOptions } from "@utils/auth";
 import { getServerSession } from "next-auth";
+import TopSwiperBanner from "./TopSwiperBanner"
+import { restApiFetch } from "@utils/bagisto/index";
+import type { AnnouncementItem } from "./TopSwiperBanner";
 export default async function Navbar() {
-
+  // 2. 服务端 fetch 请求我们自己的 Next.js API
+  const res = await restApiFetch({
+    api: `/shop/theme-customizations?name=Top_Bar`,
+    method: "GET",
+    isRoute: false,
+    takeAuthorization: false,
+  
+  });
+  let topBarData: AnnouncementItem[] = [];
+  try {
+    // 安全取值链
+    const themeItem = res?.body?.[0];
+    const optionsStr = themeItem?.translation?.options;
+    if (optionsStr) {
+      const parsed = JSON.parse(optionsStr);
+      topBarData = parsed?.images ?? [];
+    }
+  } catch (err) {
+    console.error("解析 translation.options 失败", err);
+    topBarData = [];
+  }
+  console.log('topbar-res----------------------------:',res);
+  
     const session = await getServerSession(authOptions);// 游客是null
     const isGuest = session === null;
 
@@ -29,6 +54,7 @@ export default async function Navbar() {
   return (
     <NavbarErrorBoundary>
       <header className="sticky top-0 z-10">
+         <TopSwiperBanner  items={topBarData} /> 
         <nav className="relative flex flex-col items-center justify-between gap-4 bg-neutral-50 p-4 pt-3 pb-3 dark:bg-neutral-900 md:flex-row lg:px-6 lg:py-4">
           <div className="flex w-full items-center justify-between gap-0 sm:gap-4">
             {/* 1. THE STATIC SHELL (Visible Instantly) */}

+ 0 - 57
src/components/theme/search/CategoryDetail.tsx

@@ -1,57 +0,0 @@
-import { type FC } from "react";
-import type { FunctionComponent } from "react";
-import clsx from "clsx";
-import Link from "next/link";
-
-interface TextProps {
-  html: string;
-  className?: string;
-}
-
-export const CategoryDetail: FC<{
-  categoryItem: {
-    description: string;
-    name: string;
-  };
-}> = ({ categoryItem }) => {
-  if (!categoryItem) return null;
-
-  return (
-    <div className="px-4 w-full max-w-screen-2xl mt-8 md:mt-14 mx-auto mb-0 sm:mb-10">
-      <div className="flex flex-col lg:flex-row justify-between gap-4 md:gap-8 lg:gap-20 items-start">
-        <div className=" flex-col gap-3 shrink-0">
-          <Link href="/" className="hidden lg:flex w-fit text-sm font-medium text-nowrap relative text-neutral-500 before:absolute before:bottom-0 before:left-0 before:h-px before:w-0 before:bg-current before:transition-all before:duration-300 before:content-[''] hover:text-black hover:before:w-full dark:text-neutral-400 dark:hover:text-neutral-300">
-            Home /
-          </Link>
-          <h1 className="text-xl md:text-3xl font-bold tracking-tight text-black dark:text-white capitalize">
-            {categoryItem.name}
-          </h1>
-        </div>
-
-        <div className="w-full md:max-w-2xl lg:max-w-3xl">
-          {categoryItem.description && (
-            <Prose
-              className="w-full text-base text-gray-600 dark:text-gray-300"
-              html={categoryItem.description}
-            />
-          )}
-        </div>
-      </div>
-    </div>
-  );
-};
-
-
-const Prose: FunctionComponent<TextProps> = ({ html, className }) => {
-  return (
-    <div
-      dangerouslySetInnerHTML={{ __html: html as string }}
-      className={clsx(
-        "prose prose-h1:text-[24px]",
-        className
-      )}
-
-    />
-  );
-};
-export default Prose;

+ 0 - 22
src/components/theme/search/Prose.tsx

@@ -1,22 +0,0 @@
-import type { FunctionComponent } from "react";
-
-import clsx from "clsx";
-
-interface TextProps {
-  html: string;
-  className?: string;
-}
-
-const Prose: FunctionComponent<TextProps> = ({ html, className }) => {
-  return (
-    <div
-      dangerouslySetInnerHTML={{ __html: html as string }}
-      className={clsx(
-        "prose mx-auto max-w-6xl lg:px-0 text-base leading-7 prose-headings:mt-8 prose-headings:font-semibold prose-headings:tracking-wide prose-headings:text-black prose-h1:text-5xl prose-h2:text-4xl prose-h3:text-3xl prose-h4:text-2xl prose-h5:text-xl prose-h6:text-lg prose-a:text-black prose-a:break-all prose-a:underline hover:prose-a:text-neutral-300 prose-strong:text-black prose-ol:mt-8 prose-ol:list-decimal prose-ol:pl-6 prose-ul:mt-8 prose-ul:list-disc prose-ul:pl-6 ",
-        className
-      )}
-    />
-  );
-};
-
-export default Prose;

+ 0 - 19
src/components/theme/search/not-found.tsx

@@ -1,19 +0,0 @@
-import { EventButton } from '@components/common/button/EventButton';
-import NotFoundIcon from '@components/common/icons/NotFoundIcon';
-const NotFound = ({ msg }: { msg: string }) => {
-  return (
-    <div className="my-12 px-4 flex flex-col flex-wrap items-center justify-center gap-y-4">
-      <div>
-        <NotFoundIcon />
-      </div>
-
-      <h1 className="mt-4 font-outfit text-2xl sm:text-3xl lg:text-4xl font-semibold">
-        Oops!, No products available in this category
-      </h1>
-      <p className="text-black/60 dark:text-white">{msg}</p>
-      <EventButton buttonName="Back to Home" redirect="/" />
-    </div>
-  );
-};
-
-export default NotFound;

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

@@ -26,6 +26,7 @@ type HomeSection = {
   type: "image_carousel" | "product_carousel";
   name: string;
   sort_order: number;
+  showstyle?: string;
   title?: string;
   images?: HomeSectionImage[];
   products?: HomeProduct[];