Files
homaroma/apps/website/src/components/ProductStickyPurchaseBar.tsx
T
2026-07-25 21:16:14 -04:00

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>
)
}