Initial commit: Hoomat storefront with Meshkee API and static-image slots.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-04 15:00:41 +03:30
co-authored by Cursor
commit cfabf3e327
95 changed files with 12651 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useRef } from "react";
type Product = {
title: string;
enName: string;
price: string;
image: string;
href?: string;
};
type SaleBanner = {
image?: string | null;
linkUrl?: string | null;
ariaLabel?: string;
};
function ProductPrice({ price }: { price: string }) {
const trimmed = price.trim();
const match = trimmed.match(/^([\d٬,.\s]+)\s*(تومان)$/);
if (match) {
return (
<p className="price price-with-amount" dir="ltr">
<span className="price-amount ltr-number" lang="en">
{match[1].trim()}
</span>
<span className="price-unit">تومان</span>
</p>
);
}
return <p className="price price-note">{trimmed}</p>;
}
function ProductCard({ product }: { product: Product }) {
return (
<Link
href={product.href || "/store"}
className="product-card"
aria-label={product.title}
>
<Image src={product.image} alt={product.title} width={560} height={560} />
<div className="product-body">
<p className="product-en-name">{product.enName}</p>
<h3>{product.title}</h3>
<ProductPrice price={product.price} />
</div>
</Link>
);
}
function SaleBannerCard({ banner }: { banner?: SaleBanner | null }) {
const style = banner?.image
? {
backgroundImage: `linear-gradient(90deg, rgba(136, 63, 63, 0.55), rgba(0, 35, 75, 0.5)), url(${banner.image})`,
backgroundSize: "cover",
backgroundPosition: "center",
}
: undefined;
const className = "sale sale-promo-card";
const label = banner?.ariaLabel || "فروش ویژه";
if (banner?.linkUrl) {
return (
<a href={banner.linkUrl} className={className} style={style} aria-label={label} />
);
}
return (
<Link href="/store" className={className} style={style} aria-label={label} />
);
}
export function ProductCarousel({
products,
ariaLabel,
withSaleBanner = false,
saleBanner,
}: {
products: Product[];
ariaLabel: string;
withSaleBanner?: boolean;
saleBanner?: SaleBanner | null;
}) {
const carouselRef = useRef<HTMLDivElement>(null);
const scrollCarousel = (direction: "next" | "previous") => {
const amount = 300;
carouselRef.current?.scrollBy({
left: direction === "next" ? -amount : amount,
behavior: "smooth",
});
};
return (
<div className="product-carousel-shell">
<button
className="carousel-chevron carousel-chevron-left"
type="button"
aria-label="محصولات بعدی"
onClick={() => scrollCarousel("next")}
>
‹
</button>
<div
ref={carouselRef}
className={`carousel ${withSaleBanner ? "sale-carousel" : ""}`}
aria-label={ariaLabel}
>
{withSaleBanner ? <SaleBannerCard banner={saleBanner} /> : null}
{products.map((product) => (
<ProductCard key={`${ariaLabel}-${product.title}`} product={product} />
))}
</div>
<button
className="carousel-chevron carousel-chevron-right"
type="button"
aria-label="محصولات قبلی"
onClick={() => scrollCarousel("previous")}
>
›
</button>
</div>
);
}