Build the Salar Store storefront and connect products, blogs, and home specials to the Meshkee API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-16 01:36:29 +03:30
co-authored by Cursor
parent 2783305f07
commit 7d26eaa838
113 changed files with 3269 additions and 96 deletions
+156
View File
@@ -0,0 +1,156 @@
"use client";
import { useMemo, useState } from "react";
import type { ApiProduct, ApiVariant } from "@/lib/api";
import { addToCart } from "@/lib/cart";
import { PLACEHOLDER } from "@/lib/data";
import { discountPercent, formatPrice } from "@/lib/format";
import { IconCart } from "./Icons";
import { Media } from "./Media";
type Props = {
product: ApiProduct;
variants: ApiVariant[];
};
export function ProductView({ product, variants }: Props) {
const images = useMemo(() => {
const urls = [
product.thumbnailUrl,
product.image,
product.thumbnail,
...(product.images ?? []).map((item) => item.url),
].filter((url): url is string => Boolean(url));
return [...new Set(urls)];
}, [product]);
const [image, setImage] = useState(images[0] || PLACEHOLDER.product);
const [variantId, setVariantId] = useState(variants[0]?.id);
const [qty, setQty] = useState(1);
const [added, setAdded] = useState(false);
const variant = variants.find((item) => item.id === variantId) ?? variants[0];
const price = variant ? (variant.discountedPrice ?? variant.price) : (product.store?.minPrice ?? 0);
const oldPrice = variant?.discountedPrice ? variant.price : undefined;
const stock = variant ? (variant.stockQuantity ?? 0) : product.store?.inStock ? 1 : 0;
const unavailable = !price || stock === 0;
const off = discountPercent(price, oldPrice);
return (
<div className="grid gap-6 lg:grid-cols-12 items-start">
<div className="lg:col-span-6">
<div className="relative overflow-hidden rounded-2xl border border-black/5">
{off > 0 && (
<span className="absolute top-3 left-3 z-[2] rounded-full bg-brand px-2.5 py-1.5 font-oswald text-[13px] font-bold leading-none text-white shadow-md">
{off}%
</span>
)}
<Media src={image} alt={product.nameFa || product.title} className="aspect-square" sizes="560px" priority />
</div>
{images.length > 1 && (
<div className="mt-3 grid grid-cols-6 gap-2">
{images.map((src) => (
<button
key={src}
type="button"
onClick={() => setImage(src)}
className={`relative aspect-square overflow-hidden rounded-lg border ${
src === image ? "border-brand" : "border-black/10"
}`}
>
<Media src={src} alt="" className="size-full" sizes="80px" />
</button>
))}
</div>
)}
</div>
<div className="rounded-2xl border border-black/5 p-5 md:p-6 lg:col-span-6">
{product.categoryNameFa && <p className="mb-2 text-xs text-zinc-400">{product.categoryNameFa}</p>}
<h1 className="text-xl md:text-2xl font-extrabold leading-9">{product.nameFa || product.title}</h1>
{product.title && product.nameFa && (
<p dir="ltr" className="mt-1 text-sm text-zinc-400">
{product.title}
</p>
)}
{variants.length > 1 && (
<div className="mt-5">
<p className="mb-2 text-sm text-zinc-500">انتخاب تنوع</p>
<div className="flex flex-wrap gap-2">
{variants.map((item) => (
<button
key={item.id}
type="button"
onClick={() => setVariantId(item.id)}
className={`rounded-lg border px-3 py-2 text-sm ${
item.id === variant?.id ? "border-brand text-brand" : "border-black/10 text-zinc-600"
}`}
>
{item.label || item.selections?.map((selection) => selection.value).join(" / ") || "تنوع"}
</button>
))}
</div>
</div>
)}
<div className="mt-6">
{unavailable ? (
<span className="text-lg text-zinc-400">ناموجود</span>
) : (
<div className="flex flex-col items-start gap-1">
{oldPrice ? <span className="text-sm font-medium text-brand line-through">{formatPrice(oldPrice)}</span> : null}
<span className="text-2xl font-extrabold">
{formatPrice(price)}
<span className="mr-1 text-sm font-normal">تومان</span>
</span>
</div>
)}
</div>
{!unavailable && (
<div className="mt-6 flex flex-wrap items-center gap-3">
<div className="flex items-center overflow-hidden rounded-xl border border-black/10">
<button
type="button"
aria-label="کاهش"
onClick={() => setQty((value) => Math.max(1, value - 1))}
className="grid size-10 place-items-center text-lg hover:bg-zinc-50"
>
−
</button>
<span className="min-w-10 text-center text-sm font-bold">{qty.toLocaleString("fa-IR")}</span>
<button
type="button"
aria-label="افزایش"
onClick={() => setQty((value) => Math.min(stock || 99, value + 1))}
className="grid size-10 place-items-center text-lg hover:bg-zinc-50"
>
+
</button>
</div>
<button
type="button"
onClick={() => {
addToCart({
productId: product.id,
variantId: variant?.id,
slug: product.slug,
name: product.nameFa || product.title,
image: images[0] || PLACEHOLDER.product,
price,
qty,
});
setAdded(true);
}}
className="inline-flex flex-1 items-center justify-center gap-2 rounded-xl bg-brand px-5 py-2.5 text-sm text-white hover:bg-[#e51f1f]"
>
<IconCart className="w-5 h-5 text-white" />
{added ? "به سبد اضافه شد" : "افزودن به سبد خرید"}
</button>
</div>
)}
</div>
</div>
);
}