172 lines
6.9 KiB
TypeScript
172 lines
6.9 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
import Image from 'next/image'
|
|
import Link from './LocalizedLink'
|
|
import { Heart, Search, ShoppingBag, Star, User } from 'lucide-react'
|
|
import { formatPrice, getCategoryImage, type Product, type StoreSettings } from '@homaroma/types'
|
|
import { useCartStore } from '../store/cartStore'
|
|
import { resolveCategoryName, resolveLocalized } from '../lib/localization'
|
|
import { useLang } from '../hooks/useLang'
|
|
|
|
function resolveOptionValue(val: string, options: Product['options'], lang: string): string {
|
|
if (!val || !options || !Array.isArray(options)) return val
|
|
|
|
for (const option of options) {
|
|
if (!Array.isArray(option.values)) continue
|
|
for (const item of option.values) {
|
|
const base = typeof item === 'string' ? item : item.th || item.en || Object.values(item)[0] || ''
|
|
if (base === val) {
|
|
if (typeof item === 'string') return item
|
|
return item[lang] ?? item[lang.slice(0, 2)] ?? item.th ?? item.en ?? Object.values(item)[0] ?? base
|
|
}
|
|
}
|
|
}
|
|
|
|
return val
|
|
}
|
|
|
|
export default function ProductStickyPurchaseBar({
|
|
product,
|
|
triggerId,
|
|
settings = null,
|
|
}: {
|
|
product: Product
|
|
triggerId: string
|
|
settings?: StoreSettings | null
|
|
}) {
|
|
const lang = useLang()
|
|
const [isVisible, setIsVisible] = useState(false)
|
|
const { addToCart, toggleCart } = useCartStore()
|
|
const productName = resolveLocalized(product.name, lang) || product.brand.name
|
|
const categoryName = resolveCategoryName(product.category, lang)
|
|
const brandName = settings?.brandName || 'HomAroma'
|
|
const variant = product.variants?.[0]
|
|
const imageUrl = product.images?.[0]?.url || product.category.imageUrl || getCategoryImage(categoryName)
|
|
|
|
const variantName = useMemo(() => {
|
|
if (!variant) return ''
|
|
return [variant.option1, variant.option2, variant.option3]
|
|
.filter(Boolean)
|
|
.map((option) => resolveOptionValue(option, product.options, lang))
|
|
.join(' / ')
|
|
}, [lang, product.options, variant])
|
|
|
|
const handleAddToCart = () => {
|
|
if (!variant) return
|
|
|
|
addToCart({
|
|
productId: product.id,
|
|
variantId: variant.id,
|
|
quantity: 1,
|
|
price: variant.price || 0,
|
|
productName,
|
|
variantName: variantName || undefined,
|
|
productImage: imageUrl,
|
|
})
|
|
toggleCart(true)
|
|
}
|
|
|
|
useEffect(() => {
|
|
const updateVisibility = () => {
|
|
const trigger = document.getElementById(triggerId)
|
|
if (!trigger) {
|
|
setIsVisible(false)
|
|
if (document.documentElement.dataset.productStickyVisible !== 'false') {
|
|
document.documentElement.dataset.productStickyVisible = 'false'
|
|
window.dispatchEvent(new CustomEvent('product-sticky-change', { detail: false }))
|
|
}
|
|
return
|
|
}
|
|
|
|
const nextVisible = trigger.getBoundingClientRect().top <= 0
|
|
setIsVisible(nextVisible)
|
|
const nextStr = nextVisible ? 'true' : 'false'
|
|
if (document.documentElement.dataset.productStickyVisible !== nextStr) {
|
|
document.documentElement.dataset.productStickyVisible = nextStr
|
|
window.dispatchEvent(new CustomEvent('product-sticky-change', { detail: nextVisible }))
|
|
}
|
|
}
|
|
|
|
updateVisibility()
|
|
window.addEventListener('scroll', updateVisibility, { passive: true })
|
|
window.addEventListener('resize', updateVisibility)
|
|
|
|
return () => {
|
|
document.documentElement.dataset.productStickyVisible = 'false'
|
|
window.dispatchEvent(new CustomEvent('product-sticky-change', { detail: false }))
|
|
window.removeEventListener('scroll', updateVisibility)
|
|
window.removeEventListener('resize', updateVisibility)
|
|
}
|
|
}, [triggerId])
|
|
|
|
return (
|
|
<div className={`fixed left-0 right-0 top-0 z-50 hidden border-b border-line-primary/15 bg-surface-page/98 px-6 py-3 backdrop-blur transition duration-300 md:block ${
|
|
isVisible ? 'translate-y-0 opacity-100' : 'pointer-events-none -translate-y-full opacity-0'
|
|
}`}>
|
|
<div className="relative mx-auto flex max-w-7xl items-start justify-between gap-6">
|
|
<div className="absolute left-1/2 top-0 hidden -translate-x-1/2 md:block">
|
|
<Link href="/" className="flex h-10 items-center justify-center">
|
|
{settings?.logoUrl ? (
|
|
<Image
|
|
src={settings.logoUrl}
|
|
alt={brandName}
|
|
width={180}
|
|
height={44}
|
|
unoptimized
|
|
className="max-h-10 w-auto object-contain"
|
|
/>
|
|
) : (
|
|
<span className="text-3xl font-bold uppercase tracking-[0.22em] text-text-main">{brandName}</span>
|
|
)}
|
|
</Link>
|
|
</div>
|
|
|
|
<div className="flex min-w-0 items-center gap-5 pt-9 md:max-w-[48%]">
|
|
<div className="relative h-12 w-12 shrink-0 overflow-hidden bg-surface-card">
|
|
<Image src={imageUrl} alt={productName} fill className="object-cover" sizes="56px" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="truncate text-base font-semibold uppercase tracking-[0.18em] text-text-main">{productName}</p>
|
|
<p className="mt-1 truncate text-sm text-muted-text">
|
|
{variantName || categoryName}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex shrink-0 flex-col items-end gap-6">
|
|
<div className="hidden items-center gap-5 text-text-main md:flex">
|
|
<button type="button" aria-label="Search" className="transition hover:text-brand-primary">
|
|
<Search className="h-5 w-5" strokeWidth={1.6} />
|
|
</button>
|
|
<Link href="/profile" aria-label="Account" className="transition hover:text-brand-primary">
|
|
<User className="h-5 w-5" strokeWidth={1.6} />
|
|
</Link>
|
|
<Link href="/wishlist" aria-label="Wishlist" className="transition hover:text-brand-primary">
|
|
<Star className="h-5 w-5" strokeWidth={1.6} />
|
|
</Link>
|
|
<Link href="/wishlist" aria-label="Favorites" className="transition hover:text-brand-primary">
|
|
<Heart className="h-5 w-5" strokeWidth={1.6} />
|
|
</Link>
|
|
<button type="button" aria-label="Cart" onClick={() => toggleCart(true)} className="transition hover:text-brand-primary">
|
|
<ShoppingBag className="h-5 w-5" strokeWidth={1.6} />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-8">
|
|
<p className="hidden text-base font-semibold uppercase tracking-[0.16em] text-text-main sm:block">{formatPrice(variant?.price ?? 0)}</p>
|
|
<button
|
|
type="button"
|
|
onClick={handleAddToCart}
|
|
disabled={!variant}
|
|
className="bg-text-main px-8 py-4 text-xs font-bold uppercase tracking-widest text-surface-page transition hover:bg-brand-primary disabled:cursor-not-allowed disabled:opacity-50 md:min-w-32"
|
|
>
|
|
{variant ? 'Add to Cart' : 'Unavailable'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|