Add Meshkee product catalog navigation and details

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-26 19:42:35 +03:30
co-authored by Cursor
parent 00126e5777
commit 34ce4f6925
13 changed files with 1499 additions and 97 deletions
+82 -54
View File
@@ -5,14 +5,13 @@ import {
ArrowUpLeft,
BriefcaseBusiness,
Camera,
ChevronDown,
ChevronLeft,
ChevronRight,
CircleUserRound,
FlaskConical,
Gauge,
Globe2,
Link,
Link as LinkIcon,
Mail,
MapPin,
Menu,
@@ -25,13 +24,18 @@ import {
Sparkles,
X,
} from "lucide-react";
import NextLink from "next/link";
import type { FormEvent } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import {
cssAspectRatio,
fetchHomeStaticImageSlots,
fetchLatestProducts,
productPath,
type Product,
type StaticImageSlot,
} from "./lib/meshkee";
import HeaderNavigation from "./components/HeaderNavigation";
const assetBase = "/assets";
@@ -86,41 +90,78 @@ const categories = [
},
];
const products = [
type DisplayProduct = {
id: string;
title: string;
en: string;
category: string;
image: string;
code: string;
href: string;
};
const fallbackProducts: DisplayProduct[] = [
{
id: 1,
id: "fallback-1",
title: "دستگاه کوادینگ ۶۰۰ میلی‌متر",
en: "600mm Quadding Machine | QUA-21 | LESMO",
category: "دستگاه تابنده",
image: `${assetBase}/product-quadding.jpg`,
code: "LESMO / QUA-21",
href: "#products",
},
{
id: 2,
id: "fallback-2",
title: "بریدر ۱۶ کریر",
en: "16 Carriers Braider | 16N4 | SPIRKA",
category: "دستگاه بافنده",
image: `${assetBase}/product-braider.jpg`,
code: "SPIRKA / 16N4",
href: "#products",
},
{
id: 3,
id: "fallback-3",
title: "خط کشش ۰.۵–۰.۱۵ میلی‌متر",
en: "0.15–0.5 SQmm Drawing Line | TRB3 | SAMP",
category: "دستگاه کشش",
image: `${assetBase}/product-drawing-line.jpg`,
code: "SAMP / TRB3",
href: "#products",
},
{
id: 4,
id: "fallback-4",
title: "پی‌آف ۶۰۰ میلی‌متر",
en: "600mm Pay Off | TAP D-0800 | KDM",
category: "دستگاه پی‌آف",
image: `${assetBase}/product-payoff.jpg`,
code: "KDM / TAP D-0800",
href: "#products",
},
];
function toDisplayProduct(product: Product, index: number): DisplayProduct {
const fallback = fallbackProducts[index % fallbackProducts.length];
const brand =
typeof product.brand === "string"
? product.brand
: product.brand?.nameFa || product.brand?.name;
return {
id: product.id,
title: product.nameFa || product.title,
en: product.title,
category:
product.categoryNameFa || product.categoryName || "ماشین‌آلات صنعتی",
image:
product.thumbnailUrl ||
product.thumbnail ||
product.image ||
fallback.image,
code: brand || product.categoryName || "SAMAN ANDISH",
href: productPath(product),
};
}
const news = [
{
title: "شرکت مهندسی، مشاوره و بازرگانی HService",
@@ -179,6 +220,8 @@ export default function Home() {
const [query, setQuery] = useState("");
const [searchOpen, setSearchOpen] = useState(false);
const [staticImageSlots, setStaticImageSlots] = useState<StaticImageSlot[]>([]);
const [displayProducts, setDisplayProducts] =
useState<DisplayProduct[]>(fallbackProducts);
const searchInput = useRef<HTMLInputElement>(null);
const slotsByKey = useMemo(
@@ -242,6 +285,14 @@ export default function Home() {
// Keep the local image fallbacks when the API is unavailable.
});
fetchLatestProducts(4, controller.signal)
.then((items) => {
if (items.length) setDisplayProducts(items.map(toDisplayProduct));
})
.catch(() => {
// Keep the local product fallbacks when the API is unavailable.
});
return () => controller.abort();
}, []);
@@ -258,14 +309,14 @@ export default function Home() {
const results = useMemo(() => {
const normalized = query.trim().toLowerCase();
if (!normalized) return products.slice(0, 3);
return products.filter((product) =>
if (!normalized) return displayProducts.slice(0, 3);
return displayProducts.filter((product) =>
[product.title, product.en, product.category, product.code]
.join(" ")
.toLowerCase()
.includes(normalized),
);
}, [query]);
}, [displayProducts, query]);
const focusSearch = () => {
document.getElementById("smart-search")?.scrollIntoView({
@@ -310,19 +361,11 @@ export default function Home() {
<a className="logo" href="#" aria-label="سامان اندیش">
<img src={`${assetBase}/KN83Drb6oI.png`} alt="سامان اندیش" />
</a>
<nav className="desktop-nav" aria-label="ناوبری اصلی">
<a className="active" href="#">
صفحه اصلی
</a>
<a href="#products">
محصولات
<ChevronDown size={14} />
</a>
<a href="#services">خدمات</a>
<a href="#news">اخبار</a>
<a href="/about-us">درباره ما</a>
<a href="/contact-us">تماس با ما</a>
</nav>
<HeaderNavigation
active="home"
mobileOpen={mobileOpen}
onNavigate={() => setMobileOpen(false)}
/>
<div className="header__actions">
<button className="icon-button" onClick={focusSearch} aria-label="جستجو">
<Search size={22} />
@@ -343,23 +386,6 @@ export default function Home() {
</button>
</div>
</div>
{mobileOpen && (
<nav className="mobile-nav">
{[
["صفحه اصلی", "#"],
["محصولات", "#products"],
["خدمات", "#services"],
["اخبار", "#news"],
["درباره ما", "/about-us"],
["تماس با ما", "/contact-us"],
].map(([label, href]) => (
<a key={label} href={href} onClick={() => setMobileOpen(false)}>
{label}
<ChevronLeft size={17} />
</a>
))}
</nav>
)}
</header>
<section className="hero">
@@ -390,10 +416,10 @@ export default function Home() {
<h1>{activeHero.title}</h1>
<p className="hero__description">{activeHero.text}</p>
<div className="hero__buttons">
<a className="button button--red" href="#products">
<NextLink className="button button--red" href="/products">
مشاهده محصولات
<ArrowLeft size={19} />
</a>
</NextLink>
<a className="button button--glass" href="/about-us">
درباره سامان اندیش
</a>
@@ -497,7 +523,7 @@ export default function Home() {
</div>
{results.length ? (
results.map((product) => (
<a href="#products" key={product.id}>
<NextLink href={product.href} key={product.id}>
<img src={product.image} alt={product.title} />
<div>
<strong>{product.title}</strong>
@@ -505,7 +531,7 @@ export default function Home() {
</div>
<span>{product.category}</span>
<ChevronLeft size={18} />
</a>
</NextLink>
))
) : (
<div className="empty-result">
@@ -556,25 +582,27 @@ export default function Home() {
<section className="products section-gray" id="products">
<div className="container">
<div className="section-heading-row">
<SectionTitle eyebrow="SPECIAL MACHINERY" title="ماشین‌آلات منتخب" />
<a className="text-link" href="#">
<SectionTitle eyebrow="LATEST MACHINERY" title="جدیدترین ماشین‌آلات" />
<NextLink className="text-link" href="/products">
مشاهده همه محصولات
<ArrowLeft size={18} />
</a>
</NextLink>
</div>
<div className="product-grid">
{products.map((product) => (
{displayProducts.map((product) => (
<article className="product-card" key={product.id}>
<a className="product-card__image" href="#">
<NextLink className="product-card__image" href={product.href}>
<img src={product.image} alt={product.title} />
<span>فروش ویژه</span>
<span>جدیدترین</span>
<i>
<ArrowUpLeft />
</i>
</a>
</NextLink>
<div className="product-card__body">
<small>{product.category}</small>
<h3>{product.title}</h3>
<h3>
<NextLink href={product.href}>{product.title}</NextLink>
</h3>
<p dir="ltr">{product.en}</p>
<div>
<code>{product.code}</code>
@@ -791,7 +819,7 @@ export default function Home() {
<Camera />
</a>
<a href="#" aria-label="لینکدین">
<Link />
<LinkIcon />
</a>
<a href="#" aria-label="تلگرام">
<MessageCircle />
@@ -801,7 +829,7 @@ export default function Home() {
<div className="footer__links">
<h3>دسترسی سریع</h3>
<a href="#">صفحه اصلی</a>
<a href="#products">ماشین‌آلات</a>
<NextLink href="/products">ماشین‌آلات</NextLink>
<a href="#news">اخبار و مقالات</a>
<a href="/about-us">درباره ما</a>
<a href="/contact-us">تماس با ما</a>