|
|
@@ -1,177 +1,396 @@
|
|
|
"use client";
|
|
|
-import { useState, useEffect, useRef } from "react";
|
|
|
+import { useEffect, useRef, useState } from "react";
|
|
|
+// import dynamicImport from "next/dynamic";
|
|
|
+import Link from "next/link";
|
|
|
import { useSearchProduct } from "@/utils/hooks/useSearchProduct";
|
|
|
import Grid from "@/components/theme/ui/grid/Grid";
|
|
|
-import dynamicImport from "next/dynamic";
|
|
|
-import HotSearch from "@/app/(public)/search/_components/HotSearch"
|
|
|
-import { useMediaQuery } from "@utils/hooks/useMediaQueryHook";
|
|
|
-import Link from "next/link";
|
|
|
-const ProductGridItems = dynamicImport(
|
|
|
- () => import("@/components/catalog/product/ProductGridItems"),
|
|
|
-);
|
|
|
+import HotSearch from "@/app/(public)/search/_components/HotSearch";
|
|
|
+import NewProductGridItems from "@components/catalog/product/NewProductGridItems";
|
|
|
+import Recommend from "@/app/(public)/product/_components/youmaylike/Recommend";
|
|
|
+// const ProductGridItems = dynamicImport(
|
|
|
+// () => import("@/components/catalog/product/ProductGridItems"),
|
|
|
+// );
|
|
|
|
|
|
-// 定义弹窗入参类型
|
|
|
interface SearchPopProps {
|
|
|
open: boolean;
|
|
|
onClose: () => void;
|
|
|
}
|
|
|
|
|
|
-export default function SearchPop({ open, onClose }: SearchPopProps) {
|
|
|
+const placeholderList = [
|
|
|
+ "Search for products...",
|
|
|
+ "Find best sellers",
|
|
|
+ "Search new arrivals",
|
|
|
+ "Search discount items",
|
|
|
+ "Search trending products",
|
|
|
+];
|
|
|
+
|
|
|
+export default function SearchPop({
|
|
|
+ open,
|
|
|
+ onClose,
|
|
|
+}: SearchPopProps) {
|
|
|
const { data, error, searchProducts } = useSearchProduct();
|
|
|
+
|
|
|
const [keyword, setKeyword] = useState("");
|
|
|
+ const [placeholderIndex, setPlaceholderIndex] = useState(0);
|
|
|
+
|
|
|
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
|
|
- const products = data?.edges?.map((e) => e.node) || [];
|
|
|
- const isDesktop = useMediaQuery("(min-width: 1024px)");
|
|
|
+ const inputRef = useRef<HTMLInputElement>(null);
|
|
|
|
|
|
- // 执行搜索接口
|
|
|
+ const products = data?.edges?.map((e) => e.node) ?? [];
|
|
|
+
|
|
|
+ const hasKeyword = keyword.trim().length > 0;
|
|
|
+
|
|
|
+ const hasSearchResults = hasKeyword && products.length > 0;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 两种情况展示 You May Like:
|
|
|
+ *
|
|
|
+ * 1. 刚打开,还没有输入
|
|
|
+ * 2. 输入了,但是没有搜到产品
|
|
|
+ */
|
|
|
+ const showYouMayLike = !hasKeyword || (hasKeyword && products.length === 0);
|
|
|
+
|
|
|
+ // =========================
|
|
|
+ // 搜索接口
|
|
|
+ // =========================
|
|
|
const triggerSearch = async (searchWord: string) => {
|
|
|
- if (!searchWord.trim()) return;
|
|
|
+ const value = searchWord.trim();
|
|
|
+
|
|
|
+ if (!value) return;
|
|
|
+
|
|
|
const params = {
|
|
|
suggest: true,
|
|
|
- query: searchWord,
|
|
|
+ query: value,
|
|
|
first: 8,
|
|
|
};
|
|
|
+
|
|
|
console.log("【接口入参 variables】", params);
|
|
|
+
|
|
|
const res = await searchProducts(params);
|
|
|
+
|
|
|
console.log("【接口完整返回结果】", res);
|
|
|
+
|
|
|
console.log("【商品列表数据】", res.data?.search);
|
|
|
};
|
|
|
|
|
|
- // 防抖输入
|
|
|
+ // =========================
|
|
|
+ // 输入 + 防抖
|
|
|
+ // =========================
|
|
|
const handleKeywordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
const value = e.target.value;
|
|
|
+
|
|
|
setKeyword(value);
|
|
|
- if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
|
- debounceTimerRef.current = setTimeout(() => triggerSearch(value), 300);
|
|
|
+
|
|
|
+ if (debounceTimerRef.current) {
|
|
|
+ clearTimeout(debounceTimerRef.current);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!value.trim()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ debounceTimerRef.current = setTimeout(() => {
|
|
|
+ triggerSearch(value);
|
|
|
+ }, 300);
|
|
|
};
|
|
|
|
|
|
- // 点击搜索/回车搜索
|
|
|
+ // =========================
|
|
|
+ // 点击 Search / Enter
|
|
|
+ // =========================
|
|
|
const handleSearch = () => {
|
|
|
- if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
|
+ if (debounceTimerRef.current) {
|
|
|
+ clearTimeout(debounceTimerRef.current);
|
|
|
+ }
|
|
|
+
|
|
|
triggerSearch(keyword);
|
|
|
};
|
|
|
|
|
|
- // 销毁清除定时器
|
|
|
+ // =========================
|
|
|
+ // 关闭
|
|
|
+ // =========================
|
|
|
+ const handleClose = () => {
|
|
|
+ if (debounceTimerRef.current) {
|
|
|
+ clearTimeout(debounceTimerRef.current);
|
|
|
+ }
|
|
|
+
|
|
|
+ setKeyword("");
|
|
|
+ onClose();
|
|
|
+ };
|
|
|
+
|
|
|
+ // =========================
|
|
|
+ // placeholder 轮播
|
|
|
+ // =========================
|
|
|
useEffect(() => {
|
|
|
+ const interval = window.setInterval(() => {
|
|
|
+ setPlaceholderIndex((prev) => (prev + 1) % placeholderList.length);
|
|
|
+ }, 3000);
|
|
|
+
|
|
|
+ return () => window.clearInterval(interval);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ // =========================
|
|
|
+ // 打开自动 focus
|
|
|
+ // =========================
|
|
|
+ useEffect(() => {
|
|
|
+ if (!open) return;
|
|
|
+
|
|
|
+ const timer = window.setTimeout(() => {
|
|
|
+ inputRef.current?.focus();
|
|
|
+ }, 100);
|
|
|
+
|
|
|
+ return () => window.clearTimeout(timer);
|
|
|
+ }, [open]);
|
|
|
+
|
|
|
+ // =========================
|
|
|
+ // 打开时锁定页面滚动
|
|
|
+ // =========================
|
|
|
+ useEffect(() => {
|
|
|
+ if (!open) return;
|
|
|
+
|
|
|
+ const oldOverflow = document.body.style.overflow;
|
|
|
+
|
|
|
+ document.body.style.overflow = "hidden";
|
|
|
+
|
|
|
return () => {
|
|
|
- if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
|
|
|
+ document.body.style.overflow = oldOverflow;
|
|
|
};
|
|
|
- }, []);
|
|
|
+ }, [open]);
|
|
|
|
|
|
- // 轮播占位文案
|
|
|
- const placeholderList = [
|
|
|
- "Search for products...",
|
|
|
- "Find best sellers",
|
|
|
- "Search new arrivals",
|
|
|
- "Search discount items",
|
|
|
- "Search trending products",
|
|
|
- ];
|
|
|
- const [placeholderIndex, setPlaceholderIndex] = useState(0);
|
|
|
+ // =========================
|
|
|
+ // Esc 关闭
|
|
|
+ // =========================
|
|
|
useEffect(() => {
|
|
|
- const interval = setInterval(() => {
|
|
|
- setPlaceholderIndex((prev) => (prev + 1) % placeholderList.length);
|
|
|
- }, 3000);
|
|
|
- return () => clearInterval(interval);
|
|
|
- }, [placeholderList.length]);
|
|
|
- const currentPlaceholder = placeholderList[placeholderIndex];
|
|
|
+ if (!open) return;
|
|
|
|
|
|
- // 关闭弹窗并清空关键词
|
|
|
- const handleClose = () => {
|
|
|
- setKeyword("");
|
|
|
- onClose();
|
|
|
- };
|
|
|
+ const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
+ if (e.key === "Escape") {
|
|
|
+ handleClose();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ window.addEventListener("keydown", handleKeyDown);
|
|
|
+
|
|
|
+ return () => {
|
|
|
+ window.removeEventListener("keydown", handleKeyDown);
|
|
|
+ };
|
|
|
+ }, [open]);
|
|
|
+ useEffect(() => {
|
|
|
+ return () => {
|
|
|
+ if (debounceTimerRef.current) {
|
|
|
+ clearTimeout(debounceTimerRef.current);
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }, []);
|
|
|
|
|
|
- // 弹窗未打开直接返回null
|
|
|
if (!open) return null;
|
|
|
|
|
|
+ const currentPlaceholder = placeholderList[placeholderIndex];
|
|
|
+
|
|
|
return (
|
|
|
- // 全屏弹窗根容器,纯div无多余大括号
|
|
|
- <div className="fixed inset-0 z-[9999] bg-white overflow-y-auto">
|
|
|
- {/* 顶部搜索栏区域 */}
|
|
|
- <div className="relative flex items-center justify-center px-4 py-4 border-b border-neutral-200 pt-10">
|
|
|
- {/* 右上角关闭按钮 严格24px w-6 h-6 */}
|
|
|
- <button
|
|
|
- onClick={handleClose}
|
|
|
- className="absolute right-4 top-4 w-6 h-6 flex items-center justify-center cursor-pointer"
|
|
|
- >
|
|
|
- <svg
|
|
|
- xmlns="http://www.w3.org/2000/svg"
|
|
|
- fill="none"
|
|
|
- viewBox="0 0 24 24"
|
|
|
- strokeWidth={1.8}
|
|
|
- stroke="currentColor"
|
|
|
- className="w-full h-full text-neutral-700"
|
|
|
- >
|
|
|
- <path
|
|
|
- strokeLinecap="round"
|
|
|
- strokeLinejoin="round"
|
|
|
- d="M6 18L18 6M6 6l12 12"
|
|
|
- />
|
|
|
- </svg>
|
|
|
- </button>
|
|
|
- </div>
|
|
|
- {/* 搜索输入框容器 */}
|
|
|
- <div className="block w-full justify-center px-4 mt-3 z-10">
|
|
|
+ <div
|
|
|
+ className="
|
|
|
+ fixed
|
|
|
+ inset-x-0
|
|
|
+ bottom-0
|
|
|
+ top-[151px]
|
|
|
+ z-[9998]
|
|
|
+ bg-black/70
|
|
|
+ "
|
|
|
+ onMouseDown={handleClose}
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ className="
|
|
|
+ mx-auto
|
|
|
+ w-[75%]
|
|
|
+ max-w-[1060px]
|
|
|
+ overflow-y-auto
|
|
|
+ bg-white
|
|
|
+ px-[90px]
|
|
|
+ pb-8
|
|
|
+ pt-6
|
|
|
+ "
|
|
|
+ onMouseDown={(e) => e.stopPropagation()}
|
|
|
+ >
|
|
|
<div
|
|
|
- className={`${isDesktop ? "max-w-[550px]" : ""} relative w-full mx-auto xl:min-w-[516px] outline-none hover:outline-none`}
|
|
|
+ className="
|
|
|
+ flex
|
|
|
+ h-[42px]
|
|
|
+ w-full
|
|
|
+ items-stretch
|
|
|
+ border
|
|
|
+ border-[#333333]
|
|
|
+ "
|
|
|
>
|
|
|
- <input
|
|
|
- onChange={handleKeywordChange}
|
|
|
- onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
|
|
- autoComplete="off"
|
|
|
- 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 focus:ring-2 focus:ring-neutral-300 dark:border-neutral-800 dark:bg-transparent dark:text-white dark:placeholder:text-neutral-400 md:pl-4"
|
|
|
- name="search"
|
|
|
- placeholder={currentPlaceholder}
|
|
|
- type="text"
|
|
|
- value={keyword}
|
|
|
- />
|
|
|
- {/* 搜索图标按钮 */}
|
|
|
<div
|
|
|
- onClick={handleSearch}
|
|
|
- className="absolute bottom-0 right-1 top-0 flex w-9 cursor-pointer items-center justify-center border-l border-neutral-200 dark:border-neutral-700 md:border-0"
|
|
|
+ className="
|
|
|
+ flex
|
|
|
+ min-w-0
|
|
|
+ flex-1
|
|
|
+ items-center
|
|
|
+ gap-3
|
|
|
+ px-4
|
|
|
+ "
|
|
|
>
|
|
|
<svg
|
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
|
- fill="none"
|
|
|
+ width="18"
|
|
|
+ height="18"
|
|
|
viewBox="0 0 24 24"
|
|
|
- strokeWidth="1.5"
|
|
|
- className="size-5 stroke-neutral-500 dark:stroke-white md:stroke-black"
|
|
|
+ fill="none"
|
|
|
>
|
|
|
+ <circle
|
|
|
+ cx="11"
|
|
|
+ cy="11"
|
|
|
+ r="7"
|
|
|
+ stroke="#222222"
|
|
|
+ strokeWidth="1.5"
|
|
|
+ />
|
|
|
+
|
|
|
<path
|
|
|
+ d="M16.5 16.5L21 21"
|
|
|
+ stroke="#222222"
|
|
|
+ strokeWidth="1.5"
|
|
|
strokeLinecap="round"
|
|
|
- strokeLinejoin="round"
|
|
|
- d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
|
|
|
/>
|
|
|
</svg>
|
|
|
+
|
|
|
+ <input
|
|
|
+ ref={inputRef}
|
|
|
+ value={keyword}
|
|
|
+ type="text"
|
|
|
+ name="search"
|
|
|
+ autoComplete="off"
|
|
|
+ placeholder={currentPlaceholder}
|
|
|
+ onChange={handleKeywordChange}
|
|
|
+ onKeyDown={(e) => {
|
|
|
+ if (e.key === "Enter") {
|
|
|
+ handleSearch();
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ className="
|
|
|
+ h-full
|
|
|
+ min-w-0
|
|
|
+ flex-1
|
|
|
+ bg-transparent
|
|
|
+ text-[14px]
|
|
|
+ text-black
|
|
|
+ outline-none
|
|
|
+ placeholder:text-[#777777]
|
|
|
+ "
|
|
|
+ />
|
|
|
</div>
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={handleSearch}
|
|
|
+ className="
|
|
|
+ m-[5px]
|
|
|
+ flex
|
|
|
+ w-[84px]
|
|
|
+ shrink-0
|
|
|
+ items-center
|
|
|
+ justify-center
|
|
|
+ rounded-[4px]
|
|
|
+ bg-[#036141]
|
|
|
+ text-[13px]
|
|
|
+ font-medium
|
|
|
+ text-white
|
|
|
+ transition-opacity
|
|
|
+ hover:opacity-90
|
|
|
+ "
|
|
|
+ >
|
|
|
+ Search
|
|
|
+ </button>
|
|
|
</div>
|
|
|
- </div>
|
|
|
- {/* hot search */}
|
|
|
- <HotSearch />
|
|
|
- {/* 商品列表内容区 */}
|
|
|
- <div >
|
|
|
- {error && <p className="text-red-500 my-2 text-center">{error}</p>}
|
|
|
- {data?.edges?.length === 0 && keyword && (
|
|
|
- <p className="text-center text-gray-500 my-6">No matching products</p>
|
|
|
+ <div className="mt-6">
|
|
|
+ <HotSearch />
|
|
|
+ </div>
|
|
|
+ {error && (
|
|
|
+ <p
|
|
|
+ className="
|
|
|
+ my-4
|
|
|
+ text-center
|
|
|
+ text-sm
|
|
|
+ text-red-500
|
|
|
+ "
|
|
|
+ >
|
|
|
+ {error}
|
|
|
+ </p>
|
|
|
)}
|
|
|
- {keyword && data?.edges?.length !== 0 && (
|
|
|
- <div>
|
|
|
- <Grid className="grid grid-flow-row grid-cols-2 gap-5 lg:gap-11.5 w-full max-w-screen-2xl mx-auto md:grid-cols-3 lg:grid-cols-4 px-4 xss:px-7.5">
|
|
|
- <ProductGridItems products={products} />
|
|
|
+ {hasSearchResults && (
|
|
|
+ <div className="">
|
|
|
+ <Grid className="grid grid-cols-4 gap-8 ">
|
|
|
+ <NewProductGridItems products={products} />
|
|
|
</Grid>
|
|
|
|
|
|
- <div className="flex justify-center mt-10 mb-16">
|
|
|
+ {/* See All Results */}
|
|
|
+ <div
|
|
|
+ className="
|
|
|
+ mb-2
|
|
|
+ mt-8
|
|
|
+ flex
|
|
|
+ justify-center
|
|
|
+ "
|
|
|
+ >
|
|
|
<Link
|
|
|
href={`/search?q=${encodeURIComponent(keyword)}`}
|
|
|
onClick={handleClose}
|
|
|
- className="inline-block border border-black rounded-full px-12 py-4 text-xl font-normal text-black hover:bg-gray-100 transition-colors"
|
|
|
+ className="
|
|
|
+ inline-flex
|
|
|
+ h-[30px]
|
|
|
+ min-w-[140px]
|
|
|
+ items-center
|
|
|
+ justify-center
|
|
|
+ rounded-full
|
|
|
+ border
|
|
|
+ border-[#333333]
|
|
|
+ px-7
|
|
|
+ text-[12px]
|
|
|
+ text-[#222222]
|
|
|
+ transition-colors
|
|
|
+ hover:bg-[#F5F5F5]
|
|
|
+ "
|
|
|
>
|
|
|
- View More
|
|
|
+ See All Results
|
|
|
</Link>
|
|
|
</div>
|
|
|
</div>
|
|
|
)}
|
|
|
+
|
|
|
+ {showYouMayLike && (
|
|
|
+ <div className="">
|
|
|
+ {/* <h2
|
|
|
+ className="
|
|
|
+ mb-5
|
|
|
+ text-[18px]
|
|
|
+ font-semibold
|
|
|
+ text-[#111111]
|
|
|
+ "
|
|
|
+ >
|
|
|
+ You May Like
|
|
|
+ </h2>
|
|
|
+
|
|
|
+ {youMayLikeProducts.length > 0 ? (
|
|
|
+ <Grid
|
|
|
+ className="
|
|
|
+ grid
|
|
|
+ w-full
|
|
|
+ grid-cols-4
|
|
|
+ gap-x-4
|
|
|
+ gap-y-7
|
|
|
+ "
|
|
|
+ >
|
|
|
+ <ProductGridItems products={youMayLikeProducts} />
|
|
|
+ </Grid>
|
|
|
+ ) :
|
|
|
+ null} */}
|
|
|
+ <p className="text-center font-medium text-base leading-[24px] mb-9">Sorry, we couldn't find any results</p>
|
|
|
+ <Recommend
|
|
|
+ paddingClassName="py-0"
|
|
|
+ titleClassName="text-base font-semibold"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
</div>
|
|
|
</div>
|
|
|
);
|