Files
website/src/components/home/HeaderSearch.tsx
T

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}
</>
);
}