From f44058bd6eed180eac80704d25f39d9180fd0e1e Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Sun, 27 Sep 2026 21:51:44 +0330 Subject: [PATCH] Route partnership CTA to the store account and polish search modal. Co-authored-by: Cursor --- src/app/categories/[...path]/page.tsx | 14 +- src/app/partners/page.tsx | 7 + src/app/products/page.tsx | 16 +- src/components/home/HeaderSearch.tsx | 288 +++++++++--------- src/components/home/HoverFillButton.tsx | 33 +- src/components/home/PartnerCta.tsx | 3 +- src/components/products/ProductEmptyState.tsx | 55 ++-- 7 files changed, 200 insertions(+), 216 deletions(-) create mode 100644 src/app/partners/page.tsx diff --git a/src/app/categories/[...path]/page.tsx b/src/app/categories/[...path]/page.tsx index 685d02f..35f658a 100644 --- a/src/app/categories/[...path]/page.tsx +++ b/src/app/categories/[...path]/page.tsx @@ -172,7 +172,6 @@ export default async function CategoryPage({ params, searchParams }: PageProps) products, query.page, ); - const hasExtraFilters = Boolean(query.q || query.min || query.max); return (
@@ -202,18 +201,7 @@ export default async function CategoryPage({ params, searchParams }: PageProps) />
{total === 0 ? ( - + ) : ( <> diff --git a/src/app/partners/page.tsx b/src/app/partners/page.tsx new file mode 100644 index 0000000..94ff04a --- /dev/null +++ b/src/app/partners/page.tsx @@ -0,0 +1,7 @@ +import { redirect } from "next/navigation"; +import { STORE_URL } from "@/lib/public-api"; + +/** Partnership form lives in the customer dashboard (auth already shared). */ +export default function PartnersPage() { + redirect(`${STORE_URL}/account/partners`); +} diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx index 69d3395..272fc9e 100644 --- a/src/app/products/page.tsx +++ b/src/app/products/page.tsx @@ -92,9 +92,6 @@ export default async function ProductsPage({ searchParams }: PageProps) { const categoryLabel = query.cat ? findCategoryName(apiCategories, query.cat) : null; - const hasFilters = Boolean( - query.q || query.cat || query.min || query.max || query.sale, - ); const heroTitle = query.sale ? "فروش ویژه" @@ -152,18 +149,7 @@ export default async function ProductsPage({ searchParams }: PageProps) { />
{total === 0 ? ( - + ) : ( <> diff --git a/src/components/home/HeaderSearch.tsx b/src/components/home/HeaderSearch.tsx index 7bbe5a7..d063df9 100644 --- a/src/components/home/HeaderSearch.tsx +++ b/src/components/home/HeaderSearch.tsx @@ -2,8 +2,8 @@ import Image from "next/image"; import Link from "next/link"; -import { FormEvent, useEffect, useRef, useState } from "react"; -import { categoryPath } from "@/lib/category-path"; +import { useEffect, useId, useRef, useState } from "react"; +import { createPortal } from "react-dom"; import { productPath } from "@/lib/product-path"; import { API_URL, STORE_SLUG } from "@/lib/public-api"; @@ -15,29 +15,35 @@ type SearchHit = { price?: number; }; -type CategoryHit = { - id: number; - nameFa: string; - nameEn: string; -}; - export default function HeaderSearch() { + const titleId = useId(); + const [mounted, setMounted] = useState(false); const [open, setOpen] = useState(false); const [q, setQ] = useState(""); const [loading, setLoading] = useState(false); const [products, setProducts] = useState([]); - const [categories, setCategories] = useState([]); const inputRef = useRef(null); - const panelRef = useRef(null); + + useEffect(() => { + setMounted(true); + }, []); useEffect(() => { if (!open) return; - inputRef.current?.focus(); + const frame = window.requestAnimationFrame(() => { + inputRef.current?.focus(); + }); const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setOpen(false); }; + const prevOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; window.addEventListener("keydown", onKey); - return () => window.removeEventListener("keydown", onKey); + return () => { + window.cancelAnimationFrame(frame); + document.body.style.overflow = prevOverflow; + window.removeEventListener("keydown", onKey); + }; }, [open]); useEffect(() => { @@ -45,7 +51,7 @@ export default function HeaderSearch() { const query = q.trim(); if (query.length < 2) { setProducts([]); - setCategories([]); + setLoading(false); return; } const t = window.setTimeout(async () => { @@ -54,19 +60,16 @@ export default function HeaderSearch() { const params = new URLSearchParams({ storeSlug: STORE_SLUG, q: query, - limit: "8", + limit: "12", }); 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); } @@ -74,13 +77,125 @@ export default function HeaderSearch() { 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)}`; - } + const modal = + open && mounted + ? createPortal( +
+ +
+ +
+ +
+ +
+ {q.trim().length < 2 ? ( +

+ برای نمایش نتایج، حداقل ۲ حرف وارد کنید. +

+ ) : loading ? ( +

در حال جستجو...

+ ) : !products.length ? ( +

نتیجه‌ای یافت نشد.

+ ) : ( +
    + {products.map((p) => ( +
  • + setOpen(false)} + className="flex items-center gap-3 border border-transparent px-2 py-2.5 hover:border-line hover:bg-[#f7f7f5]" + > + + {/* eslint-disable-next-line @next/next/no-img-element */} + + + + + {p.nameFa} + + + {p.nameEn} + + + +
  • + ))} +
+ )} +
+
+
, + document.body, + ) + : null; return ( <> @@ -88,6 +203,7 @@ export default function HeaderSearch() { type="button" aria-label="جستجو" aria-expanded={open} + aria-haspopup="dialog" className="transition-opacity hover:opacity-70" onClick={() => setOpen(true)} > @@ -99,129 +215,7 @@ export default function HeaderSearch() { unoptimized /> - - {open ? ( -
- - - -
- {loading ? ( -

در حال جستجو...

- ) : q.trim().length < 2 ? ( -

- حداقل ۲ حرف وارد کنید. -

- ) : !products.length && !categories.length ? ( -

نتیجه‌ای یافت نشد.

- ) : ( -
- {categories.length ? ( -
-

دسته‌بندی‌ها

-
    - {categories.map((c) => ( -
  • - setOpen(false)} - className="block border border-transparent px-2 py-2 text-sm hover:border-line hover:bg-[#f7f7f5]" - > - {c.nameFa} - {c.nameEn ? ( - - {c.nameEn} - - ) : null} - -
  • - ))} -
-
- ) : null} - {products.length ? ( -
-

محصولات

-
    - {products.map((p) => ( -
  • - setOpen(false)} - className="flex items-center gap-3 border border-transparent px-2 py-2 hover:border-line hover:bg-[#f7f7f5]" - > - - {/* eslint-disable-next-line @next/next/no-img-element */} - - - - - {p.nameFa} - - - {p.nameEn} - - - -
  • - ))} -
-
- ) : null} -
- )} -
-
- - ) : null} + {modal} ); } diff --git a/src/components/home/HoverFillButton.tsx b/src/components/home/HoverFillButton.tsx index e5d6dc6..4abdeae 100644 --- a/src/components/home/HoverFillButton.tsx +++ b/src/components/home/HoverFillButton.tsx @@ -17,17 +17,16 @@ export default function HoverFillButton({ variant = "light", }: HoverFillButtonProps) { const isDark = variant === "dark"; + const isExternal = /^https?:\/\//i.test(href); - return ( - + const classNames = `group relative inline-flex h-[52px] items-center justify-between gap-3 overflow-hidden rounded-[5px] border px-4 backdrop-blur-sm sm:h-[56px] ${ + isDark + ? "border-white bg-transparent text-white" + : "border-ink bg-white/30 text-ink" + } ${className}`; + + const content = ( + <> + + ); + + if (isExternal) { + return ( + + {content} + + ); + } + + return ( + + {content} ); } diff --git a/src/components/home/PartnerCta.tsx b/src/components/home/PartnerCta.tsx index eebf2ef..2c4df21 100644 --- a/src/components/home/PartnerCta.tsx +++ b/src/components/home/PartnerCta.tsx @@ -1,4 +1,5 @@ import HoverFillButton from "./HoverFillButton"; +import { STORE_URL } from "@/lib/public-api"; export default function PartnerCta() { return ( @@ -14,7 +15,7 @@ export default function PartnerCta() { diff --git a/src/components/products/ProductEmptyState.tsx b/src/components/products/ProductEmptyState.tsx index 236676e..cf865bb 100644 --- a/src/components/products/ProductEmptyState.tsx +++ b/src/components/products/ProductEmptyState.tsx @@ -1,44 +1,39 @@ type ProductEmptyStateProps = { - title?: string; - description?: string; + titleFa?: string; + titleEn?: string; }; export default function ProductEmptyState({ - title = "محصولی پیدا نشد", - description = "با فیلترهای انتخاب‌شده نتیجه‌ای وجود ندارد. فیلترها را تغییر دهید یا همه را پاک کنید.", + titleFa = "متاسفیم، بر اساس جستجوی شما، محصولی وجود ندارد", + titleEn = "We're sorry. There is no result based on your criteria", }: ProductEmptyStateProps) { return (
-
- +

- - - + {titleFa} +

+

+ {titleEn} +

-

{title}

-

- {description} -

); }