158 lines
6.1 KiB
TypeScript
158 lines
6.1 KiB
TypeScript
"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"
|
||
disabled={!variant?.id}
|
||
onClick={() => {
|
||
if (!variant?.id) return;
|
||
addToCart({
|
||
id: variant.id,
|
||
slug: product.slug,
|
||
name: product.nameFa || product.title,
|
||
image: images[0] || PLACEHOLDER.product,
|
||
price,
|
||
quantity: 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] disabled:opacity-60"
|
||
>
|
||
<IconCart className="w-5 h-5 text-white" />
|
||
{added ? "به سبد اضافه شد" : "افزودن به سبد خرید"}
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|