x
This commit is contained in:
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>
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}}
|
||||
|
||||
Reference in new issue
Block a user