194 lines
7.7 KiB
TypeScript
194 lines
7.7 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useMemo } from 'react'
|
|
import Link from './LocalizedLink'
|
|
import { getCategoryImage, formatPrice, type Product } from '@homaroma/types'
|
|
import { useCartStore } from '../store/cartStore'
|
|
import { resolveCategoryName, resolveLocalized } from '../lib/localization'
|
|
import { useLang } from '../hooks/useLang'
|
|
|
|
interface Props {
|
|
product: Product
|
|
}
|
|
|
|
export default function AddToCartSection({ product }: Props) {
|
|
const [selectedOptions, setSelectedOptions] = useState<string[]>(() => {
|
|
if (product.variants && product.variants.length > 0) {
|
|
const first = product.variants[0];
|
|
return [first.option1 || '', first.option2 || '', first.option3 || ''];
|
|
}
|
|
return ['', '', ''];
|
|
});
|
|
|
|
const { addToCart, toggleCart } = useCartStore()
|
|
const lang = useLang()
|
|
|
|
const currentVariant = useMemo(() => {
|
|
if (!product.variants) return undefined;
|
|
return product.variants.find(v => {
|
|
const v1 = v.option1 || '';
|
|
const v2 = v.option2 || '';
|
|
const v3 = v.option3 || '';
|
|
const s1 = selectedOptions[0] || '';
|
|
const s2 = selectedOptions[1] || '';
|
|
const s3 = selectedOptions[2] || '';
|
|
return v1 === s1 && v2 === s2 && v3 === s3;
|
|
});
|
|
}, [product.variants, selectedOptions]);
|
|
|
|
const categoryName = resolveCategoryName(product.category, lang)
|
|
const productName = resolveLocalized(product.name, lang) || product.brand.name
|
|
const description = resolveLocalized(product.description, lang)
|
|
|
|
const isOptionAvailable = (optionIndex: number, value: string) => {
|
|
if (!product.variants) return false;
|
|
return product.variants.some(v => {
|
|
const vOpts = [v.option1 || '', v.option2 || '', v.option3 || ''];
|
|
if (vOpts[optionIndex] !== value) return false;
|
|
|
|
// Check if it matches currently selected options for OTHER indices
|
|
for (let i = 0; i < (product.options?.length || 0); i++) {
|
|
if (i === optionIndex) continue;
|
|
if (vOpts[i] !== (selectedOptions[i] || '')) return false;
|
|
}
|
|
return true;
|
|
});
|
|
};
|
|
|
|
const handleOptionChange = (optIndex: number, val: string) => {
|
|
setSelectedOptions(prev => {
|
|
const next = [...prev];
|
|
next[optIndex] = val;
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const handleAddToCart = () => {
|
|
if (!currentVariant) return
|
|
|
|
const variantName = [currentVariant.option1, currentVariant.option2, currentVariant.option3].filter(Boolean).join(' / ');
|
|
|
|
addToCart({
|
|
productId: product.id,
|
|
variantId: currentVariant.id,
|
|
quantity: 1,
|
|
price: currentVariant.price || product.price || 0,
|
|
productName,
|
|
variantSize: variantName || undefined,
|
|
productImage: product.images?.[0]?.url || product.category.imageUrl || getCategoryImage(categoryName),
|
|
})
|
|
toggleCart(true)
|
|
}
|
|
|
|
const currentVariantName = currentVariant ? [currentVariant.option1, currentVariant.option2, currentVariant.option3].filter(Boolean).join(' / ') : null;
|
|
|
|
return (
|
|
<>
|
|
<div className="space-y-8">
|
|
<div className="space-y-2">
|
|
{categoryName && (
|
|
<p className="ui-eyebrow">{categoryName}</p>
|
|
)}
|
|
<h1 className="text-4xl font-semibold leading-tight text-text-main md:text-6xl">
|
|
{productName}
|
|
</h1>
|
|
<p className="text-sm font-semibold uppercase tracking-widest text-muted-text">
|
|
Premium Collection
|
|
</p>
|
|
</div>
|
|
|
|
{description && <p className="leading-relaxed text-muted-text">{description}</p>}
|
|
|
|
<div className="flex items-baseline gap-3">
|
|
<span className="text-4xl font-bold text-brand-primary">
|
|
{formatPrice(currentVariant?.price ?? product.price)}
|
|
</span>
|
|
{currentVariantName && <span className="text-sm text-muted-text">/ {currentVariantName}</span>}
|
|
</div>
|
|
|
|
{product.options && product.options.length > 0 && (
|
|
<div className="space-y-6">
|
|
{product.options.map((option, optIndex) => (
|
|
<div key={option.name} className="space-y-3">
|
|
<p className="text-xs font-bold uppercase tracking-widest text-muted-text">Select {option.name}</p>
|
|
<div className="flex flex-wrap gap-3">
|
|
{option.values.map(val => {
|
|
const isSelected = selectedOptions[optIndex] === val;
|
|
const isAvailable = isOptionAvailable(optIndex, val);
|
|
|
|
return (
|
|
<button
|
|
key={val}
|
|
disabled={!isAvailable}
|
|
onClick={() => handleOptionChange(optIndex, val)}
|
|
className={`rounded-xl border px-5 py-3 text-sm font-bold uppercase tracking-widest transition ${
|
|
isSelected
|
|
? 'border-brand-primary bg-brand-primary text-surface-card shadow-card'
|
|
: isAvailable
|
|
? 'border-line-primary/20 bg-surface-card text-muted-text hover:border-brand-primary hover:text-brand-primary'
|
|
: 'border-line-primary/10 bg-surface-card/50 text-muted-text/30 cursor-not-allowed'
|
|
}`}
|
|
>
|
|
{val}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="hidden gap-4 md:flex">
|
|
<button
|
|
onClick={handleAddToCart}
|
|
disabled={!currentVariant}
|
|
className={`ui-form-inline-button flex-1 ${!currentVariant ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
>
|
|
{currentVariant ? 'Add to Cart' : 'Unavailable'}
|
|
</button>
|
|
{categoryName && (
|
|
<Link
|
|
href={`/category/${product.category.id}`}
|
|
className="rounded-full border border-line-primary/20 bg-surface-card px-8 py-4 font-bold uppercase tracking-widest text-muted-text transition hover:border-brand-primary hover:text-brand-primary"
|
|
>
|
|
{categoryName}
|
|
</Link>
|
|
)}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4 border-t border-line-primary/20 pt-6 text-sm text-muted-text">
|
|
<div>
|
|
<span className="mb-1 block text-xs font-bold uppercase tracking-widest text-muted-text/70">Brand</span>
|
|
<span className="font-semibold text-text-main">{product.brand.name}</span>
|
|
</div>
|
|
<div>
|
|
<span className="mb-1 block text-xs font-bold uppercase tracking-widest text-muted-text/70">Category</span>
|
|
<Link href={`/category/${product.category.id}`} className="font-semibold text-text-main transition hover:text-brand-primary">
|
|
{categoryName}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="fixed bottom-0 left-0 right-0 z-40 border-t border-line-primary/20 bg-surface-card/95 p-4 shadow-[0_-10px_30px_rgba(0,0,0,0.08)] backdrop-blur md:hidden">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="flex flex-col">
|
|
<span className="text-[10px] font-bold uppercase tracking-widest text-muted-text/70">
|
|
{currentVariantName || 'Total Price'}
|
|
</span>
|
|
<span className="text-xl font-bold text-brand-primary">{formatPrice(currentVariant?.price ?? product.price)}</span>
|
|
</div>
|
|
<button
|
|
onClick={handleAddToCart}
|
|
disabled={!currentVariant}
|
|
className={`ui-form-inline-button flex-1 px-5 py-3.5 ${!currentVariant ? 'opacity-50 cursor-not-allowed' : ''}`}
|
|
>
|
|
{currentVariant ? 'Add to Cart' : 'Unavailable'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|