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:
co-authored by
Claude Sonnet 5
parent
1f81236d0a
commit
2b5f4e437b
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user