SearchPop.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. "use client";
  2. import { useEffect, useRef, useState } from "react";
  3. // import dynamicImport from "next/dynamic";
  4. import Link from "next/link";
  5. import { useSearchProduct } from "@/utils/hooks/useSearchProduct";
  6. import Grid from "@/components/theme/ui/grid/Grid";
  7. import HotSearch from "@/app/(public)/search/_components/HotSearch";
  8. import NewProductGridItems from "@components/catalog/product/NewProductGridItems";
  9. import Recommend from "@/app/(public)/product/_components/youmaylike/Recommend";
  10. // const ProductGridItems = dynamicImport(
  11. // () => import("@/components/catalog/product/ProductGridItems"),
  12. // );
  13. interface SearchPopProps {
  14. open: boolean;
  15. onClose: () => void;
  16. }
  17. const placeholderList = [
  18. "Search for products...",
  19. "Find best sellers",
  20. "Search new arrivals",
  21. "Search discount items",
  22. "Search trending products",
  23. ];
  24. export default function SearchPop({
  25. open,
  26. onClose,
  27. }: SearchPopProps) {
  28. const { data, error, searchProducts,loading } = useSearchProduct();
  29. const [keyword, setKeyword] = useState("");
  30. const [placeholderIndex, setPlaceholderIndex] = useState(0);
  31. const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
  32. const inputRef = useRef<HTMLInputElement>(null);
  33. const products = data?.edges?.map((e) => e.node) ?? [];
  34. const hasKeyword = keyword.trim().length > 0;
  35. const hasSearchResults = hasKeyword && products.length > 0;
  36. /**
  37. * 两种情况展示 You May Like:
  38. *
  39. * 1. 刚打开,还没有输入
  40. * 2. 输入了,但是没有搜到产品
  41. */
  42. const showYouMayLike = !hasKeyword || (hasKeyword && products.length === 0);
  43. // =========================
  44. // 搜索接口
  45. // =========================
  46. const triggerSearch = async (searchWord: string) => {
  47. const value = searchWord.trim();
  48. if (!value) return;
  49. const params = {
  50. suggest: true,
  51. query: value,
  52. first: 8,
  53. };
  54. console.log("【接口入参 variables】", params);
  55. const res = await searchProducts(params);
  56. console.log("【接口完整返回结果】", res);
  57. console.log("【商品列表数据】", res.data?.search);
  58. };
  59. // =========================
  60. // 输入 + 防抖
  61. // =========================
  62. const handleKeywordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  63. const value = e.target.value;
  64. setKeyword(value);
  65. if (debounceTimerRef.current) {
  66. clearTimeout(debounceTimerRef.current);
  67. }
  68. if (!value.trim()) {
  69. return;
  70. }
  71. debounceTimerRef.current = setTimeout(() => {
  72. triggerSearch(value);
  73. }, 300);
  74. };
  75. // =========================
  76. // 点击 Search / Enter
  77. // =========================
  78. const handleSearch = () => {
  79. if (debounceTimerRef.current) {
  80. clearTimeout(debounceTimerRef.current);
  81. }
  82. triggerSearch(keyword);
  83. };
  84. // =========================
  85. // 关闭
  86. // =========================
  87. const handleClose = () => {
  88. if (debounceTimerRef.current) {
  89. clearTimeout(debounceTimerRef.current);
  90. }
  91. setKeyword("");
  92. onClose();
  93. };
  94. // =========================
  95. // placeholder 轮播
  96. // =========================
  97. useEffect(() => {
  98. const interval = window.setInterval(() => {
  99. setPlaceholderIndex((prev) => (prev + 1) % placeholderList.length);
  100. }, 3000);
  101. return () => window.clearInterval(interval);
  102. }, []);
  103. // =========================
  104. // 打开自动 focus
  105. // =========================
  106. useEffect(() => {
  107. if (!open) return;
  108. const timer = window.setTimeout(() => {
  109. inputRef.current?.focus();
  110. }, 100);
  111. return () => window.clearTimeout(timer);
  112. }, [open]);
  113. // =========================
  114. // 打开时锁定页面滚动
  115. // =========================
  116. useEffect(() => {
  117. if (!open) return;
  118. const oldOverflow = document.body.style.overflow;
  119. document.body.style.overflow = "hidden";
  120. return () => {
  121. document.body.style.overflow = oldOverflow;
  122. };
  123. }, [open]);
  124. // =========================
  125. // Esc 关闭
  126. // =========================
  127. useEffect(() => {
  128. if (!open) return;
  129. const handleKeyDown = (e: KeyboardEvent) => {
  130. if (e.key === "Escape") {
  131. handleClose();
  132. }
  133. };
  134. window.addEventListener("keydown", handleKeyDown);
  135. return () => {
  136. window.removeEventListener("keydown", handleKeyDown);
  137. };
  138. }, [open]);
  139. useEffect(() => {
  140. return () => {
  141. if (debounceTimerRef.current) {
  142. clearTimeout(debounceTimerRef.current);
  143. }
  144. };
  145. }, []);
  146. if (!open) return null;
  147. const currentPlaceholder = placeholderList[placeholderIndex];
  148. return (
  149. <div
  150. className="
  151. fixed
  152. inset-x-0
  153. bottom-0
  154. top-[151px]
  155. z-[9998]
  156. bg-black/70
  157. "
  158. onMouseDown={handleClose}
  159. >
  160. <div
  161. className="
  162. mx-auto
  163. w-[75%]
  164. max-w-[1060px]
  165. overflow-y-auto
  166. bg-white
  167. px-[90px]
  168. pb-8
  169. pt-6
  170. "
  171. onMouseDown={(e) => e.stopPropagation()}
  172. >
  173. <div
  174. className="
  175. flex
  176. h-[42px]
  177. w-full
  178. items-stretch
  179. border
  180. border-[#333333]
  181. "
  182. >
  183. <div
  184. className="
  185. flex
  186. min-w-0
  187. flex-1
  188. items-center
  189. gap-3
  190. px-4
  191. "
  192. >
  193. <svg
  194. xmlns="http://www.w3.org/2000/svg"
  195. width="18"
  196. height="18"
  197. viewBox="0 0 24 24"
  198. fill="none"
  199. >
  200. <circle
  201. cx="11"
  202. cy="11"
  203. r="7"
  204. stroke="#222222"
  205. strokeWidth="1.5"
  206. />
  207. <path
  208. d="M16.5 16.5L21 21"
  209. stroke="#222222"
  210. strokeWidth="1.5"
  211. strokeLinecap="round"
  212. />
  213. </svg>
  214. <input
  215. ref={inputRef}
  216. value={keyword}
  217. type="text"
  218. name="search"
  219. autoComplete="off"
  220. placeholder={currentPlaceholder}
  221. onChange={handleKeywordChange}
  222. onKeyDown={(e) => {
  223. if (e.key === "Enter") {
  224. handleSearch();
  225. }
  226. }}
  227. className="
  228. h-full
  229. min-w-0
  230. flex-1
  231. bg-transparent
  232. text-[14px]
  233. text-black
  234. outline-none
  235. placeholder:text-[#777777]
  236. "
  237. />
  238. </div>
  239. <button
  240. type="button"
  241. onClick={handleSearch}
  242. className="
  243. m-[5px]
  244. flex
  245. w-[84px]
  246. shrink-0
  247. items-center
  248. justify-center
  249. rounded-[4px]
  250. bg-[#036141]
  251. text-[13px]
  252. font-medium
  253. text-white
  254. transition-opacity
  255. hover:opacity-90
  256. "
  257. >
  258. Search
  259. </button>
  260. </div>
  261. <div className="mt-6">
  262. <HotSearch />
  263. </div>
  264. {error && (
  265. <p
  266. className="
  267. my-4
  268. text-center
  269. text-sm
  270. text-red-500
  271. "
  272. >
  273. {error}
  274. </p>
  275. )}
  276. {hasSearchResults && (
  277. <div className="">
  278. <Grid className="grid grid-cols-4 gap-8 ">
  279. <NewProductGridItems products={products} />
  280. </Grid>
  281. {/* See All Results */}
  282. <div
  283. className="
  284. mb-2
  285. mt-8
  286. flex
  287. justify-center
  288. "
  289. >
  290. <Link
  291. href={`/search?q=${encodeURIComponent(keyword)}`}
  292. onClick={handleClose}
  293. className="
  294. inline-flex
  295. h-[30px]
  296. min-w-[140px]
  297. items-center
  298. justify-center
  299. rounded-full
  300. border
  301. border-[#333333]
  302. px-7
  303. text-[12px]
  304. text-[#222222]
  305. transition-colors
  306. hover:bg-[#F5F5F5]
  307. "
  308. >
  309. See All Results
  310. </Link>
  311. </div>
  312. </div>
  313. )}
  314. {showYouMayLike && (
  315. <div className="">
  316. {/* <h2
  317. className="
  318. mb-5
  319. text-[18px]
  320. font-semibold
  321. text-[#111111]
  322. "
  323. >
  324. You May Like
  325. </h2>
  326. {youMayLikeProducts.length > 0 ? (
  327. <Grid
  328. className="
  329. grid
  330. w-full
  331. grid-cols-4
  332. gap-x-4
  333. gap-y-7
  334. "
  335. >
  336. <ProductGridItems products={youMayLikeProducts} />
  337. </Grid>
  338. ) :
  339. null} */}
  340. {(hasKeyword && !loading && products.length === 0)&&(
  341. <p className="text-center font-medium text-base leading-[24px] mb-9">Sorry, we couldn't find any results</p>
  342. )}
  343. <Recommend
  344. paddingClassName="py-0"
  345. titleClassName="text-base font-semibold"
  346. />
  347. </div>
  348. )}
  349. </div>
  350. </div>
  351. );
  352. }