This commit is contained in:
Your Name committed 2026-06-23 20:02:29 -04:00
1 parent ed69a4b840
commit 992335a52b
22 files changed
+1064 -346

No files matched your search

+193
View File
@@ -0,0 +1,193 @@
"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>
</>
)
}