Connect product/blog pages, mega menu, and instant search to Meshkee backend

Add /products (list + detail) and /blog (list + detail) routes backed by
the real Meshkee API, a real category-hierarchy mega menu (level 2/3 only),
debounced instant search proxied through a same-origin API route, and
tel:-link fallbacks on product cards when no price is set. Also fixes
Product price normalization to read the real API shape instead of a
guessed one, and hardens product linking against catalog sources that
don't expose a slug.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
amirhosein.ashourloo
2026-08-18 13:42:08 +03:30
co-authored by Claude Sonnet 5
parent 1f81236d0a
commit 2b5f4e437b
19 changed files with 1018 additions and 175 deletions
+83 -79
View File
@@ -2,8 +2,9 @@
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import type { Category, CategoryGroup } from "@/lib/meshkee";
import type { Category, CategoryNode, CategoryTreeGroup } from "@/lib/meshkee";
import { EmptyState } from "@/components/ui/EmptyState";
import { SearchBar } from "./SearchBar";
const navLinks = [
{ label: "فروش ویژه", href: "#" },
@@ -12,6 +13,8 @@ const navLinks = [
{ label: "تماس با ما", href: "#" },
];
const LOGIN_URL = "https://customer.sanihome.ir";
function Logo({ large = false }: { large?: boolean }) {
return (
<a href="/" className="flex shrink-0 items-center">
@@ -43,35 +46,22 @@ function ChevronDownIcon({ className = "" }: { className?: string }) {
);
}
function SearchBar() {
return (
<div className="flex overflow-hidden rounded-xl border border-border bg-background">
<input
type="search"
placeholder="جستجوی محصول، برند یا دسته‌بندی..."
className="flex-1 bg-transparent px-4 py-2.5 text-sm outline-none placeholder:text-muted"
/>
<button
type="button"
className="bg-red px-5 text-sm font-medium text-white transition-colors hover:bg-red-dark"
>
جستجو
</button>
</div>
);
function flattenLevel2(categoryTree: CategoryTreeGroup[]): CategoryNode[] {
return categoryTree.flatMap((group) => group.items);
}
function MobileDrawer({
open,
onClose,
categoryGroups,
categoryTree,
}: {
open: boolean;
onClose: () => void;
categoryGroups: CategoryGroup[];
categoryTree: CategoryTreeGroup[];
}) {
const [categoriesOpen, setCategoriesOpen] = useState(false);
const [mounted, setMounted] = useState(false);
const level2 = flattenLevel2(categoryTree);
useEffect(() => {
setMounted(true);
@@ -144,32 +134,37 @@ function MobileDrawer({
}`}
>
<div className="overflow-hidden">
{categoryGroups.length === 0 ? (
{level2.length === 0 ? (
<div className="bg-background px-5 pb-4">
<EmptyState message="هنوز دسته‌بندی‌ای ثبت نشده است." />
</div>
) : (
<div className="space-y-4 bg-background px-5 pb-4">
{categoryGroups.map((group) => (
<div key={group.id}>
<p className="mb-1.5 pt-3 text-xs font-bold text-red-dark">
{group.title}
</p>
<ul className="space-y-1">
{group.items.map((item) => (
<li key={item.name}>
<a
href="#"
className="block rounded-lg px-3 py-1.5 text-muted transition-colors hover:bg-red-light hover:text-red"
>
{item.name}
</a>
</li>
))}
</ul>
</div>
<ul className="space-y-1 bg-background px-5 pb-4">
{level2.map((node) => (
<li key={node.id}>
<a
href={`/products?categoryId=${node.id}`}
className="block rounded-lg px-3 py-1.5 font-medium text-foreground transition-colors hover:bg-red-light hover:text-red"
>
{node.nameFa}
</a>
{node.children.length > 0 && (
<ul className="mr-3 space-y-1 border-r border-border pr-3">
{node.children.map((child) => (
<li key={child.id}>
<a
href={`/products?categoryId=${child.id}`}
className="block rounded-lg px-3 py-1 text-muted transition-colors hover:bg-red-light hover:text-red"
>
{child.nameFa}
</a>
</li>
))}
</ul>
)}
</li>
))}
</div>
</ul>
)}
</div>
</div>
@@ -185,13 +180,13 @@ function MobileDrawer({
</a>
))}
<button
type="button"
<a
href={LOGIN_URL}
className="flex items-center gap-2 px-5 py-4 text-right font-medium transition-colors hover:bg-red-light/40 hover:text-red"
>
<span aria-hidden>👤</span>
ورود
</button>
</a>
</nav>
</div>
</div>,
@@ -200,15 +195,16 @@ function MobileDrawer({
}
export function Header({
categoryGroups,
categoryTree,
categories,
}: {
categoryGroups: CategoryGroup[];
categoryTree: CategoryTreeGroup[];
categories: Category[];
}) {
const [menuOpen, setMenuOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const level2 = flattenLevel2(categoryTree);
useEffect(() => {
const onClickOutside = (event: MouseEvent) => {
@@ -248,13 +244,13 @@ export function Header({
</div>
<div className="flex items-center justify-end gap-3 text-sm">
<button
type="button"
<a
href={LOGIN_URL}
className="hidden items-center gap-1.5 text-muted transition-colors hover:text-red sm:flex"
>
<span>👤</span>
<span>ورود</span>
</button>
</a>
<button
type="button"
className="relative flex items-center gap-1.5 rounded-lg border border-border px-3 py-2 transition-colors hover:border-red hover:text-red"
@@ -291,7 +287,7 @@ export function Header({
</button>
{menuOpen && (
<div className="absolute top-full right-0 z-50 mt-0 w-[min(92vw,760px)] animate-fade-up rounded-b-2xl border border-t-0 border-border bg-surface p-5 shadow-xl">
<div className="absolute top-full right-0 z-50 mt-0 max-h-[70vh] w-[min(92vw,900px)] animate-fade-up overflow-y-auto rounded-b-2xl border border-t-0 border-border bg-surface p-5 shadow-xl">
<div className="mb-4 flex items-end justify-between gap-3 border-b border-border pb-3">
<div>
<p className="font-bold text-red-dark">همه دسته‌بندی‌ها</p>
@@ -299,49 +295,57 @@ export function Header({
Shop by Category
</p>
</div>
<a href="#" className="text-xs font-medium text-red hover:text-red-dark">
<a
href="/products"
className="text-xs font-medium text-red hover:text-red-dark"
>
مشاهده همه ←
</a>
</div>
{categoryGroups.length === 0 ? (
{level2.length === 0 ? (
<EmptyState message="هنوز دسته‌بندی‌ای ثبت نشده است." />
) : (
<>
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
{categoryGroups.map((column) => (
<div key={column.id}>
<p className="mb-3 text-sm font-bold text-red-dark">{column.title}</p>
<ul className="space-y-2">
{column.items.map((item) => (
<li key={item.name}>
<div className="columns-1 gap-6 sm:columns-2 lg:columns-4">
{level2.map((node) => (
<div key={node.id} className="mb-4 break-inside-avoid">
<a
href={`/products?categoryId=${node.id}`}
className="text-sm font-bold text-red-dark transition-colors hover:text-red"
>
{node.nameFa}
</a>
{node.children.length > 0 && (
<ul className="mt-1.5 space-y-1.5">
{node.children.map((child) => (
<li key={child.id}>
<a
href="#"
href={`/products?categoryId=${child.id}`}
className="text-sm text-muted transition-colors hover:text-red"
>
{item.name}
{child.nameFa}
</a>
</li>
))}
</ul>
</div>
))}
</div>
{categories.length > 0 && (
<div className="mt-5 grid grid-cols-2 gap-2 border-t border-border pt-4 sm:grid-cols-4">
{categories.slice(0, 4).map((cat) => (
<a
key={cat.id}
href="#"
className="rounded-lg bg-red-light/50 px-3 py-2 text-center text-xs font-medium text-red-dark transition-colors hover:bg-red-light"
>
{cat.name}
</a>
))}
)}
</div>
)}
</>
))}
</div>
)}
{categories.length > 0 && (
<div className="mt-5 grid grid-cols-2 gap-2 border-t border-border pt-4 sm:grid-cols-4">
{categories.slice(0, 4).map((cat) => (
<a
key={cat.id}
href={`/products?categoryId=${cat.id}`}
className="rounded-lg bg-red-light/50 px-3 py-2 text-center text-xs font-medium text-red-dark transition-colors hover:bg-red-light"
>
{cat.name}
</a>
))}
</div>
)}
</div>
)}
@@ -362,7 +366,7 @@ export function Header({
<MobileDrawer
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
categoryGroups={categoryGroups}
categoryTree={categoryTree}
/>
</header>
);