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

@@ -1,6 +1,6 @@
"use client"
import { useState } from 'react'
import { useState, useMemo } from 'react'
import Link from './LocalizedLink'
import { getCategoryImage, formatPrice, type Product } from '@homaroma/types'
import { useCartStore } from '../store/cartStore'
@@ -12,30 +12,76 @@ interface Props {
}
export default function AddToCartSection({ product }: Props) {
const [selectedVariant, setSelectedVariant] = useState(0)
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 = product.variants[selectedVariant]
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.scent.name
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: currentVariant.size,
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">
@@ -57,39 +103,49 @@ export default function AddToCartSection({ product }: Props) {
<span className="text-4xl font-bold text-brand-primary">
{formatPrice(currentVariant?.price ?? product.price)}
</span>
{currentVariant && <span className="text-sm text-muted-text">/ {currentVariant.size}</span>}
{currentVariantName && <span className="text-sm text-muted-text">/ {currentVariantName}</span>}
</div>
{product.variants.length > 0 && (
<div className="space-y-3">
<p className="text-xs font-bold uppercase tracking-widest text-muted-text">Select Size</p>
<div className="flex flex-wrap gap-3">
{product.variants.map((variant, index) => (
<button
key={variant.id}
onClick={() => setSelectedVariant(index)}
className={`rounded-xl border px-5 py-3 text-sm font-bold uppercase tracking-widest transition ${
selectedVariant === index
? 'border-brand-primary bg-brand-primary text-surface-card shadow-card'
: 'border-line-primary/20 bg-surface-card text-muted-text hover:border-brand-primary hover:text-brand-primary'
}`}
>
{variant.size}
<span className={`mt-1 block text-[10px] ${selectedVariant === index ? 'text-surface-card' : 'text-muted-text/70'}`}>
{formatPrice(variant.price)}
</span>
</button>
))}
</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}
className="ui-form-inline-button flex-1"
disabled={!currentVariant}
className={`ui-form-inline-button flex-1 ${!currentVariant ? 'opacity-50 cursor-not-allowed' : ''}`}
>
Add to Cart
{currentVariant ? 'Add to Cart' : 'Unavailable'}
</button>
{categoryName && (
<Link
@@ -103,8 +159,8 @@ export default function AddToCartSection({ product }: Props) {
<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">Scent</span>
<span className="font-semibold text-text-main">{product.scent.name}</span>
<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>
@@ -116,35 +172,19 @@ export default function AddToCartSection({ product }: Props) {
</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">
{product.variants.length > 1 && (
<div className="mb-3 flex gap-2 overflow-x-auto pb-1" style={{ scrollbarWidth: 'none' }}>
{product.variants.map((variant, index) => (
<button
key={`mobile-v-${variant.id}`}
onClick={() => setSelectedVariant(index)}
className={`flex-shrink-0 rounded-full border px-4 py-2 text-[10px] font-bold uppercase tracking-widest transition ${
selectedVariant === index
? 'border-brand-primary bg-brand-primary text-surface-card'
: 'border-line-primary/20 bg-surface-card text-muted-text'
}`}
>
{variant.size}
</button>
))}
</div>
)}
<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">
{product.variants.length > 1 ? currentVariant?.size : 'Total Price'}
{currentVariantName || 'Total Price'}
</span>
<span className="text-xl font-bold text-brand-primary">{formatPrice(currentVariant?.price ?? product.price)}</span>
</div>
<button
onClick={handleAddToCart}
className="ui-form-inline-button flex-1 px-5 py-3.5"
disabled={!currentVariant}
className={`ui-form-inline-button flex-1 px-5 py-3.5 ${!currentVariant ? 'opacity-50 cursor-not-allowed' : ''}`}
>
Add to Cart
{currentVariant ? 'Add to Cart' : 'Unavailable'}
</button>
</div>
</div>
+3 -3
View File
@@ -21,7 +21,7 @@ export default function ProductCard({ product }: ProductCardProps) {
const [selectedVariant, setSelectedVariant] = useState(product.variants?.[0] || null)
const categoryName = resolveCategoryName(product.category, lang)
const productName = resolveLocalized(product.name, lang) || product.scent.name
const productName = resolveLocalized(product.name, lang) || product.brand.name
const imageUrl = product.images?.[0]?.url || product.category.imageUrl || getCategoryImage(categoryName)
const displayPrice = selectedVariant?.price ?? product.variants[0]?.price ?? product.price
@@ -88,7 +88,7 @@ export default function ProductCard({ product }: ProductCardProps) {
: 'border-line-primary/20 bg-surface-card text-text-main hover:border-brand-primary/50'
}`}
>
{variant.size}
{[variant.option1, variant.option2, variant.option3].filter(Boolean).join(' / ')}
</button>
)
})}
@@ -114,7 +114,7 @@ export default function ProductCard({ product }: ProductCardProps) {
quantity: 1,
price: selectedVariant.price || 0,
productName: productName,
variantSize: selectedVariant.size,
variantSize: [selectedVariant.option1, selectedVariant.option2, selectedVariant.option3].filter(Boolean).join(' / '),
productImage: imageUrl
})
}}