228 lines
7.8 KiB
TypeScript
228 lines
7.8 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { FormEvent, useEffect, useRef, useState } from "react";
|
|
import { categoryPath } from "@/lib/category-path";
|
|
import { productPath } from "@/lib/product-path";
|
|
import { API_URL, STORE_SLUG } from "@/lib/public-api";
|
|
|
|
type SearchHit = {
|
|
id: number | string;
|
|
nameFa: string;
|
|
nameEn: string;
|
|
imageUrl?: string | null;
|
|
price?: number;
|
|
};
|
|
|
|
type CategoryHit = {
|
|
id: number;
|
|
nameFa: string;
|
|
nameEn: string;
|
|
};
|
|
|
|
export default function HeaderSearch() {
|
|
const [open, setOpen] = useState(false);
|
|
const [q, setQ] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [products, setProducts] = useState<SearchHit[]>([]);
|
|
const [categories, setCategories] = useState<CategoryHit[]>([]);
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
inputRef.current?.focus();
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") setOpen(false);
|
|
};
|
|
window.addEventListener("keydown", onKey);
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const query = q.trim();
|
|
if (query.length < 2) {
|
|
setProducts([]);
|
|
setCategories([]);
|
|
return;
|
|
}
|
|
const t = window.setTimeout(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const params = new URLSearchParams({
|
|
storeSlug: STORE_SLUG,
|
|
q: query,
|
|
limit: "8",
|
|
});
|
|
const res = await fetch(`${API_URL}/public/search?${params}`);
|
|
if (!res.ok) throw new Error("search failed");
|
|
const data = (await res.json()) as {
|
|
products?: SearchHit[];
|
|
categories?: CategoryHit[];
|
|
};
|
|
setProducts(data.products ?? []);
|
|
setCategories(data.categories ?? []);
|
|
} catch {
|
|
setProducts([]);
|
|
setCategories([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, 280);
|
|
return () => window.clearTimeout(t);
|
|
}, [q, open]);
|
|
|
|
function handleSubmit(e: FormEvent) {
|
|
e.preventDefault();
|
|
const query = q.trim();
|
|
if (!query) return;
|
|
setOpen(false);
|
|
window.location.href = `/products?q=${encodeURIComponent(query)}`;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
aria-label="جستجو"
|
|
aria-expanded={open}
|
|
className="transition-opacity hover:opacity-70"
|
|
onClick={() => setOpen(true)}
|
|
>
|
|
<Image
|
|
src="/images/home/icons/search.svg"
|
|
alt=""
|
|
width={20}
|
|
height={20}
|
|
unoptimized
|
|
/>
|
|
</button>
|
|
|
|
{open ? (
|
|
<div className="fixed inset-0 z-[70]">
|
|
<button
|
|
type="button"
|
|
className="absolute inset-0 bg-ink/40"
|
|
aria-label="بستن جستجو"
|
|
onClick={() => setOpen(false)}
|
|
/>
|
|
<div
|
|
ref={panelRef}
|
|
className="absolute inset-x-0 top-0 bg-white shadow-[0_12px_40px_rgba(0,0,0,0.12)]"
|
|
>
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
className="mx-auto flex max-w-[900px] items-center gap-3 px-4 py-4 sm:px-6"
|
|
dir="rtl"
|
|
>
|
|
<Image
|
|
src="/images/home/icons/search.svg"
|
|
alt=""
|
|
width={18}
|
|
height={18}
|
|
unoptimized
|
|
/>
|
|
<input
|
|
ref={inputRef}
|
|
value={q}
|
|
onChange={(e) => setQ(e.target.value)}
|
|
placeholder="جستجوی محصول یا دستهبندی..."
|
|
className="min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen(false)}
|
|
className="text-sm text-muted hover:text-ink"
|
|
>
|
|
بستن
|
|
</button>
|
|
</form>
|
|
|
|
<div className="mx-auto max-w-[900px] border-t border-line px-4 py-4 sm:px-6">
|
|
{loading ? (
|
|
<p className="text-sm text-muted">در حال جستجو...</p>
|
|
) : q.trim().length < 2 ? (
|
|
<p className="text-sm text-muted">
|
|
حداقل ۲ حرف وارد کنید.
|
|
</p>
|
|
) : !products.length && !categories.length ? (
|
|
<p className="text-sm text-muted">نتیجهای یافت نشد.</p>
|
|
) : (
|
|
<div className="grid gap-6 sm:grid-cols-2">
|
|
{categories.length ? (
|
|
<div>
|
|
<p className="mb-2 text-xs text-muted">دستهبندیها</p>
|
|
<ul className="space-y-1">
|
|
{categories.map((c) => (
|
|
<li key={c.id}>
|
|
<Link
|
|
href={categoryPath(c)}
|
|
onClick={() => setOpen(false)}
|
|
className="block border border-transparent px-2 py-2 text-sm hover:border-line hover:bg-[#f7f7f5]"
|
|
>
|
|
{c.nameFa}
|
|
{c.nameEn ? (
|
|
<span
|
|
dir="ltr"
|
|
className="ms-2 font-montserrat text-[10px] text-muted"
|
|
>
|
|
{c.nameEn}
|
|
</span>
|
|
) : null}
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
) : null}
|
|
{products.length ? (
|
|
<div>
|
|
<p className="mb-2 text-xs text-muted">محصولات</p>
|
|
<ul className="space-y-1">
|
|
{products.map((p) => (
|
|
<li key={p.id}>
|
|
<Link
|
|
href={productPath(p)}
|
|
onClick={() => setOpen(false)}
|
|
className="flex items-center gap-3 border border-transparent px-2 py-2 hover:border-line hover:bg-[#f7f7f5]"
|
|
>
|
|
<span className="relative h-12 w-10 shrink-0 overflow-hidden bg-[#f3f3f1]">
|
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
<img
|
|
src={
|
|
p.imageUrl ||
|
|
"/images/home/products/coat-blue.jpg"
|
|
}
|
|
alt=""
|
|
className="h-full w-full object-cover"
|
|
/>
|
|
</span>
|
|
<span className="min-w-0">
|
|
<span className="block truncate text-sm text-ink">
|
|
{p.nameFa}
|
|
</span>
|
|
<span
|
|
dir="ltr"
|
|
className="block truncate font-montserrat text-[10px] text-muted"
|
|
>
|
|
{p.nameEn}
|
|
</span>
|
|
</span>
|
|
</Link>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|