Files
website/src/components/product-detail/ProductBuyBox.tsx
T

241 lines
8.0 KiB
TypeScript

"use client";
import Image from "next/image";
import { useMemo, useState } from "react";
import type { ProductDetail, ProductVariantOption } from "@/data/product-detail";
import BilingualTitle from "@/components/ui/BilingualTitle";
type ProductBuyBoxProps = {
product: ProductDetail;
};
function OptionRow({
labelFa,
labelEn,
options,
value,
onChange,
kind,
}: {
labelFa: string;
labelEn: string;
options: ProductVariantOption[];
value: string;
onChange: (id: string) => void;
kind: "color" | "size" | "package";
}) {
if (!options.length) return null;
return (
<div className="space-y-2.5">
<div className="flex items-baseline justify-between gap-3">
<p className="text-sm text-ink">{labelFa}</p>
<p dir="ltr" className="font-montserrat text-[10px] tracking-[0.14em] text-muted uppercase">
{labelEn}
</p>
</div>
<div className="flex flex-wrap gap-2">
{options.map((opt) => {
const selected = value === opt.id;
const disabled = opt.available === false;
const tip = opt.labelEn || opt.labelFa;
if (kind === "color") {
return (
<button
key={opt.id}
type="button"
disabled={disabled}
title={tip}
aria-label={tip}
onClick={() => onChange(opt.id)}
className={`group/swatch relative h-9 w-9 rounded-full border transition-transform ${
selected ? "border-ink scale-105" : "border-line"
} ${disabled ? "opacity-35 cursor-not-allowed" : "hover:border-ink"}`}
aria-pressed={selected}
>
<span
className="absolute inset-1 rounded-full border border-black/5"
style={{ background: opt.swatch || "#ccc" }}
/>
<span
role="tooltip"
className="pointer-events-none absolute bottom-[calc(100%+6px)] left-1/2 z-20 -translate-x-1/2 whitespace-nowrap rounded-[3px] bg-ink px-2 py-1 font-montserrat text-[10px] uppercase tracking-[0.08em] text-white opacity-0 transition-opacity group-hover/swatch:opacity-100"
>
{tip}
</span>
</button>
);
}
return (
<button
key={opt.id}
type="button"
disabled={disabled}
title={tip}
aria-label={tip}
onClick={() => onChange(opt.id)}
className={`min-w-[2.75rem] border px-3 py-2 text-sm uppercase tracking-[0.04em] transition-colors ${
selected
? "border-ink bg-ink text-white"
: "border-line bg-white text-ink hover:border-ink"
} ${disabled ? "opacity-35 cursor-not-allowed line-through" : ""}`}
aria-pressed={selected}
>
{kind === "size" ? opt.labelEn.toUpperCase() : opt.labelFa}
</button>
);
})}
</div>
</div>
);
}
export default function ProductBuyBox({ product }: ProductBuyBoxProps) {
const firstColor =
product.colors.find((c) => c.available !== false)?.id ??
product.colors[0]?.id ??
"";
const firstSize =
product.sizes.find((s) => s.available !== false)?.id ??
product.sizes[0]?.id ??
"";
const firstPackage =
product.packages.find((p) => p.available !== false)?.id ??
product.packages[0]?.id ??
"";
const [color, setColor] = useState(firstColor);
const [size, setSize] = useState(firstSize);
const [pkg, setPkg] = useState(firstPackage);
const selectedColor = useMemo(
() => product.colors.find((c) => c.id === color),
[color, product.colors],
);
return (
<div className="flex flex-col gap-6 lg:gap-8">
<div className="text-right">
<BilingualTitle
titleFa={product.nameFa}
titleEn={product.nameEn}
size="lg"
align="end"
/>
<p className="mt-4 text-sm leading-7 text-ink/75">{product.summary}</p>
</div>
<div className="space-y-5">
<OptionRow
labelFa={`رنگ${selectedColor ? ` · ${selectedColor.labelFa}` : ""}`}
labelEn="Color"
options={product.colors}
value={color}
onChange={setColor}
kind="color"
/>
<OptionRow
labelFa="سایز"
labelEn="Size"
options={product.sizes}
value={size}
onChange={setSize}
kind="size"
/>
<OptionRow
labelFa="بسته‌بندی"
labelEn="Package"
options={product.packages}
value={pkg}
onChange={setPkg}
kind="package"
/>
</div>
<div dir="ltr" className="flex items-baseline gap-2 border-y border-line py-4">
<p className="font-montserrat text-2xl text-ink sm:text-3xl">
{product.price}
</p>
<p className="text-xs text-muted">IRT</p>
</div>
{/* Sizing guide frame */}
<div className="relative overflow-hidden rounded-[5px] border border-dashed border-ink/25 bg-[#f7f7f5]">
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-60"
style={{
backgroundImage:
"radial-gradient(circle, rgba(10,10,10,0.12) 0.7px, transparent 1px)",
backgroundSize: "12px 12px",
}}
/>
<div className="relative grid gap-4 p-4 sm:grid-cols-[minmax(0,1fr)_140px] sm:items-center">
<div>
<p className="font-doran text-lg text-ink">راهنمای سایز</p>
<p
dir="ltr"
className="mt-1 font-montserrat text-[10px] uppercase tracking-[0.16em] text-muted"
>
Sizing guide
</p>
<p className="mt-2 text-xs leading-6 text-ink/70">
{product.sizeGuideNote}
</p>
<a
href="#product-tabs"
className="mt-3 inline-flex text-sm text-ink underline underline-offset-4 hover:opacity-70"
>
مشاهده جدول اندازه‌ها
</a>
</div>
<div className="relative mx-auto aspect-[3/4] w-full max-w-[140px] overflow-hidden rounded-[5px] border border-line bg-white">
{product.sizeGuideImage ? (
<Image
src={product.sizeGuideImage}
alt=""
fill
sizes="140px"
className="object-cover object-top opacity-90"
/>
) : (
<span className="absolute inset-0 flex items-center justify-center text-[11px] text-muted">
Size chart
</span>
)}
</div>
</div>
</div>
<div className="flex flex-col gap-2 sm:flex-row">
<button
type="button"
className="group relative inline-flex h-12 flex-1 items-center justify-center overflow-hidden rounded-[5px] border border-ink bg-ink px-4 text-white"
>
<span
aria-hidden
className="pointer-events-none absolute inset-0 origin-bottom scale-y-0 bg-white transition-transform duration-[400ms] ease-out group-hover:scale-y-100"
/>
<span className="relative z-10 text-sm transition-colors duration-[400ms] group-hover:text-ink sm:text-base">
افزودن به سبد
</span>
</button>
<button
type="button"
className="inline-flex h-12 items-center justify-center gap-2 rounded-[5px] border border-line px-4 text-sm text-ink transition-colors hover:border-ink"
aria-label="افزودن به علاقه‌مندی"
>
<Image
src="/images/home/icons/bookmark.svg"
alt=""
width={20}
height={20}
unoptimized
/>
علاقه‌مندی
</button>
</div>
</div>
);
}