This commit is contained in:
amirhosein.ashourloo
2026-08-19 17:54:11 +03:30
parent b065fac757
commit 149b77f734
80 changed files with 3015 additions and 92 deletions
+120
View File
@@ -0,0 +1,120 @@
"use client";
import { useEffect, useRef, useState } from "react";
import Link from "next/link";
import { getProducts } from "@/lib/api";
import type { ApiProduct } from "@/lib/types";
import { formatToman } from "@/lib/format";
import { SearchIcon } from "./icons";
const MIN_QUERY_LENGTH = 3;
const DEBOUNCE_MS = 350;
export default function SearchBox({
placeholder = "جستجوی محصولات...",
inputHeightClassName = "h-11",
}: {
placeholder?: string;
inputHeightClassName?: string;
}) {
const [query, setQuery] = useState("");
const [results, setResults] = useState<ApiProduct[]>([]);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
function handleQueryChange(value: string) {
setQuery(value);
if (value.trim().length < MIN_QUERY_LENGTH) {
setResults([]);
setOpen(false);
setLoading(false);
} else {
setLoading(true);
}
}
useEffect(() => {
const term = query.trim();
if (term.length < MIN_QUERY_LENGTH) return;
const handle = setTimeout(() => {
getProducts({ name: term, pageSize: 6 })
.then((res) => {
setResults(res.items);
setOpen(true);
})
.catch(() => setResults([]))
.finally(() => setLoading(false));
}, DEBOUNCE_MS);
return () => clearTimeout(handle);
}, [query]);
useEffect(() => {
function onClickOutside(event: MouseEvent) {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setOpen(false);
}
}
document.addEventListener("mousedown", onClickOutside);
return () => document.removeEventListener("mousedown", onClickOutside);
}, []);
const term = query.trim();
const showDropdown = open && term.length >= MIN_QUERY_LENGTH;
return (
<div ref={containerRef} className="relative w-full">
<div
className={`flex w-full items-center gap-2 rounded-full border border-line bg-surface px-4 ${inputHeightClassName}`}
>
<SearchIcon className="size-4.5 text-body shrink-0" />
<input
type="text"
value={query}
onChange={(e) => handleQueryChange(e.target.value)}
onFocus={() => term.length >= MIN_QUERY_LENGTH && setOpen(true)}
placeholder={placeholder}
className="flex-1 bg-transparent outline-none text-sm placeholder:text-body/70"
/>
</div>
{showDropdown && (
<div className="absolute top-full mt-2 start-0 w-full bg-white border border-line rounded-xl shadow-lg z-50 max-h-96 overflow-y-auto">
{loading && <p className="p-4 text-sm text-body text-center">در حال جستجو...</p>}
{!loading && results.length === 0 && (
<p className="p-4 text-sm text-body text-center">نتیجه‌ای یافت نشد</p>
)}
{!loading &&
results.map((product) => (
<Link
key={product.id}
href={`/products/${product.slug}`}
onClick={() => setOpen(false)}
className="flex items-center gap-3 p-3 hover:bg-surface transition-colors border-b border-line last:border-0"
>
<img
src={product.thumbnailUrl || product.image || "/images/products/product-1.svg"}
alt=""
className="size-12 rounded-md object-cover shrink-0 bg-surface"
/>
<div className="min-w-0 flex-1">
<p className="text-xs md:text-sm text-ink line-clamp-1">
{product.nameFa || product.title}
</p>
<p className="mt-0.5 text-xs font-bold text-brand">
{product.store?.minPrice != null
? formatToman(product.store.minPrice)
: "تماس بگیرید"}
</p>
</div>
</Link>
))}
</div>
)}
</div>
);
}