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:
co-authored by
Cursor
parent
2783305f07
commit
7d26eaa838
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user