Files
salar-store/components/ProductView.tsx
T

158 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}