x
This commit is contained in:
1 parent
f7ac94ca0d
commit
335f3b4f59
41 files changed
+809
-413
No files matched your search
@@ -32,41 +32,41 @@ export default function CartDrawer() {
|
||||
/>
|
||||
|
||||
{/* Drawer */}
|
||||
<div className="relative w-full max-w-md h-full bg-spa-light shadow-2xl flex flex-col animate-slide-left">
|
||||
<div className="relative w-full max-w-md h-full bg-pop-bg shadow-2xl flex flex-col animate-slide-left">
|
||||
{/* Header */}
|
||||
<div className="p-6 border-b border-spa-stone/10 flex items-center justify-between bg-spa-light">
|
||||
<div className="p-6 border-b border-pop-black/10 flex items-center justify-between bg-pop-bg">
|
||||
<h2 className="text-xl font-bold flex items-center gap-2 text-pop-black">
|
||||
<ShoppingBag className="w-5 h-5 text-spa-forest" />
|
||||
<ShoppingBag className="w-5 h-5 text-pop-pink" />
|
||||
{t('cart.title')}
|
||||
<span className="text-sm font-normal text-spa-stone bg-spa-sage px-2 py-0.5 rounded-full ml-2">
|
||||
<span className="text-sm font-normal text-pop-black bg-pop-black/5 px-2 py-0.5 rounded-full ml-2">
|
||||
{t('cart.items', { count: items.length })}
|
||||
</span>
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => toggleCart(false)}
|
||||
className="p-2 hover:bg-spa-sage rounded-full transition-colors"
|
||||
className="p-2 hover:bg-pop-black/5 rounded-full transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-spa-stone hover:text-pop-black" />
|
||||
<X className="w-5 h-5 text-pop-black/50 hover:text-pop-black" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Cart Items */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
{items.length === 0 ? (
|
||||
<div className="h-full flex flex-col items-center justify-center text-spa-stone space-y-4">
|
||||
<div className="h-full flex flex-col items-center justify-center text-pop-black/50 space-y-4">
|
||||
<ShoppingBag className="w-16 h-16 opacity-20" />
|
||||
<p>{t('cart.empty')}</p>
|
||||
<button
|
||||
onClick={() => toggleCart(false)}
|
||||
className="text-spa-forest hover:underline tracking-widest uppercase text-xs font-bold"
|
||||
className="text-pop-pink hover:underline tracking-widest uppercase text-xs font-bold"
|
||||
>
|
||||
{t('cart.continueShopping')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
items.map((item) => (
|
||||
<div key={item.cartItemId} className="flex gap-4 p-4 rounded-2xl bg-spa-light border border-spa-stone/10 shadow-sm group relative">
|
||||
<div className="w-18 h-22 rounded-lg bg-spa-sage/30 overflow-hidden flex-shrink-0" style={{ width: '72px', height: '88px' }}>
|
||||
<div key={item.cartItemId} className="flex gap-4 p-4 rounded-2xl bg-pop-bg border border-pop-black/10 shadow-sm group relative">
|
||||
<div className="w-18 h-22 rounded-lg bg-pop-black/5 overflow-hidden flex-shrink-0" style={{ width: '72px', height: '88px' }}>
|
||||
<img
|
||||
src={item.productImage || '/images/fallback-product.svg'}
|
||||
alt={item.productName}
|
||||
@@ -75,33 +75,33 @@ export default function CartDrawer() {
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col pt-1 space-y-1 relative min-w-0">
|
||||
<h3 className="font-bold text-sm text-spa-forest leading-tight pr-6 truncate">{item.productName}</h3>
|
||||
<h3 className="font-bold text-sm text-pop-black leading-tight pr-6 truncate">{item.productName}</h3>
|
||||
{item.variantSize && (
|
||||
<p className="text-xs text-spa-stone">{item.variantSize}</p>
|
||||
<p className="text-xs text-pop-black/60">{item.variantSize}</p>
|
||||
)}
|
||||
|
||||
{/* Price row */}
|
||||
<div className="flex items-baseline gap-1.5 mt-0.5">
|
||||
<span className="text-xs text-spa-stone/80">{formatPrice(item.price)}</span>
|
||||
<span className="text-xs text-spa-stone/60">×</span>
|
||||
<span className="text-xs text-spa-stone/80">{item.quantity}</span>
|
||||
<span className="text-xs text-spa-stone/60">=</span>
|
||||
<span className="text-xs text-pop-black/80">{formatPrice(item.price)}</span>
|
||||
<span className="text-xs text-pop-black/60">×</span>
|
||||
<span className="text-xs text-pop-black/80">{item.quantity}</span>
|
||||
<span className="text-xs text-pop-black/60">=</span>
|
||||
<span className="text-sm font-bold text-rose-500">{formatPrice(item.price * item.quantity)}</span>
|
||||
</div>
|
||||
|
||||
{/* Quantity controls */}
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="flex items-center gap-2 bg-spa-sage/50 rounded-lg border border-spa-stone/5">
|
||||
<div className="flex items-center gap-2 bg-pop-black/5 rounded-lg border border-pop-black/5">
|
||||
<button
|
||||
onClick={() => updateQuantity(item.cartItemId, item.quantity - 1)}
|
||||
className="p-1.5 text-spa-stone hover:text-spa-forest transition-colors"
|
||||
className="p-1.5 text-pop-black/50 hover:text-pop-black transition-colors"
|
||||
>
|
||||
<Minus className="w-3 h-3" />
|
||||
</button>
|
||||
<span className="w-5 text-center text-xs font-bold text-pop-black">{item.quantity}</span>
|
||||
<button
|
||||
onClick={() => updateQuantity(item.cartItemId, item.quantity + 1)}
|
||||
className="p-1.5 text-spa-stone hover:text-spa-forest transition-colors"
|
||||
className="p-1.5 text-pop-black/50 hover:text-pop-black transition-colors"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
</button>
|
||||
@@ -110,7 +110,7 @@ export default function CartDrawer() {
|
||||
|
||||
<button
|
||||
onClick={() => removeFromCart(item.cartItemId)}
|
||||
className="absolute top-0 right-0 p-1.5 text-spa-stone/50 hover:text-rose-500 transition-colors"
|
||||
className="absolute top-0 right-0 p-1.5 text-pop-black/50 hover:text-rose-500 transition-colors"
|
||||
title="Remove item"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
@@ -214,7 +214,7 @@ function CartDrawerFooter({ subtotal, onClose, t }: { subtotal: number; onClose:
|
||||
const total = Math.max(0, subtotalAfterL2 - discountAmount)
|
||||
|
||||
return (
|
||||
<div className="p-5 border-t border-spa-stone/10 bg-spa-light space-y-4">
|
||||
<div className="p-5 border-t border-pop-black/10 bg-pop-bg space-y-4">
|
||||
|
||||
{/* Coupon Input */}
|
||||
{!savedCoupon && (
|
||||
@@ -225,12 +225,12 @@ function CartDrawerFooter({ subtotal, onClose, t }: { subtotal: number; onClose:
|
||||
value={couponInput}
|
||||
onChange={e => setCouponInput(e.target.value.toUpperCase())}
|
||||
placeholder={t('cart.couponPlaceholder', 'Discount Code')}
|
||||
className="flex-1 bg-spa-light border border-spa-stone/20 rounded-lg px-3 py-2 text-sm text-pop-black focus:border-spa-forest outline-none uppercase tracking-widest placeholder:text-spa-stone/50"
|
||||
className="flex-1 bg-pop-bg border border-pop-black/20 rounded-lg px-3 py-2 text-sm text-pop-black focus:border-pop-pink outline-none uppercase tracking-widest placeholder:text-pop-black/50"
|
||||
/>
|
||||
<button
|
||||
onClick={handleApplyCoupon}
|
||||
disabled={couponLoading || !couponInput.trim()}
|
||||
className="px-4 bg-spa-stone/10 hover:bg-spa-stone/20 border border-spa-stone/10 rounded-lg text-xs font-bold uppercase tracking-widest disabled:opacity-50 transition-all text-pop-black"
|
||||
className="px-4 bg-pop-black/5 hover:bg-pop-black/10 border border-pop-black/10 rounded-lg text-xs font-bold uppercase tracking-widest disabled:opacity-50 transition-all text-pop-black"
|
||||
>
|
||||
{couponLoading ? '...' : t('cart.applyCoupon', 'Apply')}
|
||||
</button>
|
||||
@@ -241,30 +241,30 @@ function CartDrawerFooter({ subtotal, onClose, t }: { subtotal: number; onClose:
|
||||
|
||||
{/* Totals */}
|
||||
<div className="space-y-2 text-sm text-pop-black">
|
||||
<div className="flex justify-between text-spa-stone">
|
||||
<div className="flex justify-between text-pop-black/60">
|
||||
<span>ราคาสินค้า</span>
|
||||
<span>{formatPrice(subtotal)}</span>
|
||||
</div>
|
||||
{promoLevel1Disc > 0 && (
|
||||
<div className="flex justify-between text-spa-forest font-medium">
|
||||
<div className="flex justify-between text-pop-pink font-medium">
|
||||
<span>{promoLevel1?.name}</span>
|
||||
<span>- {formatPrice(promoLevel1Disc)}</span>
|
||||
</div>
|
||||
)}
|
||||
{promoLevel2Disc > 0 && (
|
||||
<div className="flex justify-between text-spa-forest font-medium">
|
||||
<div className="flex justify-between text-pop-pink font-medium">
|
||||
<span>{promoLevel2?.name}</span>
|
||||
<span>- {formatPrice(promoLevel2Disc)}</span>
|
||||
</div>
|
||||
)}
|
||||
{(discountAmount > 0 || isFreeShipping) && savedCoupon && (
|
||||
<div className="flex justify-between text-spa-forest font-medium">
|
||||
<div className="flex justify-between text-pop-pink font-medium">
|
||||
<span className="flex items-center gap-1.5">
|
||||
ส่วนลดคูปอง
|
||||
<span className="text-[10px] font-bold bg-spa-sage px-1.5 py-0.5 rounded tracking-widest text-spa-forest">
|
||||
<span className="text-[10px] font-bold bg-pop-pink/10 px-1.5 py-0.5 rounded tracking-widest text-pop-pink">
|
||||
{savedCoupon}
|
||||
</span>
|
||||
<button onClick={removeCoupon} className="text-spa-stone/50 hover:text-rose-500 transition-colors" title="ยกเลิก">
|
||||
<button onClick={removeCoupon} className="text-pop-black/50 hover:text-rose-500 transition-colors" title="ยกเลิก">
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
@@ -276,13 +276,13 @@ function CartDrawerFooter({ subtotal, onClose, t }: { subtotal: number; onClose:
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Tag className="w-3 h-3" />
|
||||
คูปอง <span className="font-bold tracking-widest">{savedCoupon}</span> (ยังไม่ถึงยอดขั้นต่ำ)
|
||||
<button onClick={removeCoupon} className="text-spa-stone/50 hover:text-rose-500 transition-colors">
|
||||
<button onClick={removeCoupon} className="text-pop-black/50 hover:text-rose-500 transition-colors">
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between font-bold text-base pt-2 border-t border-spa-stone/10 text-pop-black">
|
||||
<div className="flex justify-between font-bold text-base pt-2 border-t border-pop-black/10 text-pop-black">
|
||||
<span>รวมสุทธิ</span>
|
||||
<span className="text-rose-500">{formatPrice(total)}</span>
|
||||
</div>
|
||||
@@ -292,7 +292,7 @@ function CartDrawerFooter({ subtotal, onClose, t }: { subtotal: number; onClose:
|
||||
<Link
|
||||
to="/checkout"
|
||||
onClick={onClose}
|
||||
className="flex items-center justify-center gap-2 w-full py-4 bg-spa-forest text-spa-light font-bold rounded-xl uppercase tracking-widest text-sm hover:bg-spa-stone hover:shadow-lg transition-all duration-300 group"
|
||||
className="flex items-center justify-center gap-2 w-full py-4 bg-pop-pink text-pop-bg font-bold rounded-xl uppercase tracking-widest text-sm hover:bg-pop-black hover:text-pop-bg hover:shadow-lg transition-all duration-300 group"
|
||||
>
|
||||
<ShoppingBag className="w-4 h-4" />
|
||||
ดำเนินการชำระเงิน
|
||||
|
||||
@@ -49,8 +49,8 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
|
||||
to={item.url || '#'}
|
||||
className={`flex items-center gap-1 transition-colors py-2 border-b ${
|
||||
isOpen
|
||||
? 'text-spa-forest border-spa-forest'
|
||||
: 'text-spa-stone border-transparent hover:text-spa-forest hover:border-spa-forest'
|
||||
? 'text-pop-black border-pop-black'
|
||||
: 'text-pop-black/70 border-transparent hover:text-pop-black hover:border-pop-black'
|
||||
}`}
|
||||
>
|
||||
{getLocalizedText(item.label, lang)}
|
||||
@@ -78,11 +78,11 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
|
||||
onMouseEnter={() => handleOpen(uniqueId)}
|
||||
onMouseLeave={handleClose}
|
||||
>
|
||||
<div className="w-full bg-spa-sage border-b border-black/5 shadow-sm cursor-default">
|
||||
<div className="w-full bg-pop-bg border-b border-pop-black/10 shadow-sm cursor-default">
|
||||
<div className="flex justify-center gap-16 py-10 max-w-7xl mx-auto px-6">
|
||||
{item.columns!.map((col, idx) => (
|
||||
<div key={idx} className="flex flex-col min-w-[140px] text-left">
|
||||
<h4 className="text-[10px] uppercase tracking-widest text-spa-stone font-bold mb-4">
|
||||
<h4 className="text-[10px] uppercase tracking-widest text-pop-black/70 font-bold mb-4">
|
||||
{getLocalizedText(col.title, lang)}
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
@@ -90,7 +90,7 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
|
||||
<li key={lIdx}>
|
||||
<Link
|
||||
to={link.url || '#'}
|
||||
className="text-sm font-normal normal-case tracking-normal text-spa-stone hover:text-spa-forest hover:opacity-80 block transition-colors"
|
||||
className="text-sm font-normal normal-case tracking-normal text-pop-black/80 hover:text-pop-black hover:opacity-80 block transition-colors"
|
||||
>
|
||||
{getLocalizedText(link.label, lang)}
|
||||
</Link>
|
||||
|
||||
@@ -14,15 +14,14 @@ interface DynamicAnnouncementBarProps {
|
||||
export default function DynamicAnnouncementBar({ payload }: DynamicAnnouncementBarProps) {
|
||||
const { i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
const { backgroundColor = '#1F3D2D', textColor = '#FDFDF9', speed = 3000 } = payload;
|
||||
const { speed = 3000 } = payload;
|
||||
const slides = (payload.slides || []).map((slide) => getLocalizedField(slide, lang));
|
||||
|
||||
if (slides.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="announcement-bar relative z-30 w-full overflow-hidden px-4 py-2.5 text-center text-[10px] font-semibold uppercase tracking-[0.2em] sm:text-xs"
|
||||
style={{ backgroundColor, color: textColor }}
|
||||
className="announcement-bar relative z-30 w-full overflow-hidden px-4 py-2.5 text-center text-[10px] font-semibold uppercase tracking-[0.2em] sm:text-xs bg-pop-black text-pop-bg"
|
||||
>
|
||||
<Swiper
|
||||
modules={[Autoplay]}
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) {
|
||||
const buttonText = getLocalizedField(payload.buttonText, lang);
|
||||
|
||||
return (
|
||||
<section className="ui-section-band bg-spa-light relative overflow-hidden">
|
||||
<section className="ui-section-band bg-pop-bg relative overflow-hidden">
|
||||
{linkUrl ? (
|
||||
<Link to={linkUrl} className="ui-banner-shell group block cursor-pointer">
|
||||
<div className="absolute inset-0 z-0">
|
||||
@@ -30,12 +30,12 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) {
|
||||
|
||||
<div className="ui-banner-content">
|
||||
{title && (
|
||||
<h3 className="font-serif text-3xl font-light leading-tight text-spa-gold md:text-5xl">
|
||||
<h3 className="font-serif text-3xl font-light leading-tight text-pop-pink md:text-5xl">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
{description && (
|
||||
<p className="font-light text-base text-spa-light/95 md:text-lg">
|
||||
<p className="font-light text-base text-pop-bg/95 md:text-lg">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
@@ -61,12 +61,12 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) {
|
||||
|
||||
<div className="ui-banner-content">
|
||||
{title && (
|
||||
<h3 className="font-serif text-3xl font-light leading-tight text-spa-gold md:text-5xl">
|
||||
<h3 className="font-serif text-3xl font-light leading-tight text-pop-pink md:text-5xl">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
{description && (
|
||||
<p className="font-light text-base text-spa-light/95 md:text-lg">
|
||||
<p className="font-light text-base text-pop-bg/95 md:text-lg">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function DynamicCategories({ payload, categories, loading }: Dyna
|
||||
}, [categories, categoryIds]);
|
||||
|
||||
return (
|
||||
<section id="categories" className="ui-section-band relative overflow-hidden bg-spa-light">
|
||||
<section id="categories" className="ui-section-band relative overflow-hidden bg-pop-bg">
|
||||
<div className="ui-section-inner space-y-16 relative z-10">
|
||||
<div className="space-y-4 text-center">
|
||||
<h2 className="text-4xl font-bold tracking-tight text-pop-black md:text-5xl">
|
||||
@@ -40,7 +40,7 @@ export default function DynamicCategories({ payload, categories, loading }: Dyna
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-2 gap-6 md:grid-cols-5">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div key={i} className="ui-shell-card h-80 animate-pulse bg-spa-sage" />
|
||||
<div key={i} className="ui-shell-card h-80 animate-pulse bg-pop-black/10" />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
@@ -51,19 +51,19 @@ export default function DynamicCategories({ payload, categories, loading }: Dyna
|
||||
to={`/category/${cat.id}`}
|
||||
className="ui-shell-card ui-shell-card-hover group relative h-80 overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent z-10 opacity-80 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-pop-bg/90 via-pop-bg/40 to-transparent z-10 opacity-80 group-hover:opacity-100 transition-opacity" />
|
||||
<img
|
||||
src={cat.imageUrl || getCategoryImage(getLocalizedField(cat.name, lang))}
|
||||
className="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
||||
alt={getLocalizedField(cat.name, lang)}
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
||||
/>
|
||||
<div className="absolute bottom-6 left-6 right-6 z-20 text-spa-light">
|
||||
<div className="absolute bottom-6 left-6 right-6 z-20 text-pop-black">
|
||||
<span className="mb-1 block text-xl font-semibold">
|
||||
{getLocalizedField(cat.name, localStorage.getItem('app_lang') || 'en')}
|
||||
</span>
|
||||
{cat._count && (
|
||||
<span className="text-sm font-medium text-spa-light">
|
||||
<span className="text-sm font-medium text-pop-black/90">
|
||||
{cat._count.products} Products
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
|
||||
const [accent, rest] = part.split(']');
|
||||
return (
|
||||
<span key={i}>
|
||||
<span className="text-spa-gold font-medium italic">
|
||||
<span className="text-pop-pink font-medium italic">
|
||||
{accent}
|
||||
</span>
|
||||
{rest}
|
||||
@@ -49,10 +49,10 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
|
||||
<section className="ui-dark-shell overflow-hidden">
|
||||
<div className="ui-section-inner space-y-12">
|
||||
<div className="space-y-3 text-center">
|
||||
<h2 className="font-serif text-3xl font-light tracking-wide text-spa-gold md:text-5xl">
|
||||
<h2 className="font-serif text-3xl font-light tracking-wide text-pop-pink md:text-5xl">
|
||||
{renderTitle(title) || title}
|
||||
</h2>
|
||||
<div className="w-16 h-[1px] bg-spa-gold mx-auto"></div>
|
||||
<div className="w-16 h-[1px] bg-pop-pink mx-auto"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative select-none pb-12">
|
||||
@@ -64,13 +64,13 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
|
||||
640: { slidesPerView: 2 },
|
||||
1024: { slidesPerView: 3 }
|
||||
}}
|
||||
pagination={{ clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-spa-gold' }}
|
||||
pagination={{ clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-pop-pink' }}
|
||||
autoplay={{ delay: 5000, disableOnInteraction: false }}
|
||||
className="w-full"
|
||||
>
|
||||
{slides.map((slide, idx) => (
|
||||
<SwiperSlide key={idx} className="h-auto flex flex-col">
|
||||
<div className="ui-dark-card group flex h-full flex-col transition-all duration-300 hover:border-spa-gold/30">
|
||||
<div className="ui-dark-card group flex h-full flex-col transition-all duration-300 hover:border-pop-pink/30">
|
||||
<div className="aspect-[4/3] overflow-hidden relative">
|
||||
<img
|
||||
src={slide.imageUrl}
|
||||
@@ -80,18 +80,18 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-spa-forest/60 via-transparent to-transparent"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-night-900/80 via-transparent to-transparent"></div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 md:p-8 flex flex-col flex-1 justify-between space-y-6">
|
||||
<div className="space-y-3">
|
||||
<span className="block text-[10px] font-bold uppercase tracking-[0.2em] text-spa-gold">
|
||||
<span className="block text-[10px] font-bold uppercase tracking-[0.2em] text-pop-pink">
|
||||
{slide.category}
|
||||
</span>
|
||||
<h3 className="font-serif text-xl font-light tracking-wide text-spa-light">
|
||||
<h3 className="font-serif text-xl font-light tracking-wide text-pop-black">
|
||||
{renderTitle(slide.title) || slide.title}
|
||||
</h3>
|
||||
<p className="line-clamp-3 text-xs font-light leading-relaxed text-spa-light/70 md:text-sm">
|
||||
<p className="line-clamp-3 text-xs font-light leading-relaxed text-pop-black/70 md:text-sm">
|
||||
{slide.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -100,7 +100,7 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
|
||||
<div className="pt-2">
|
||||
<Link
|
||||
to={slide.buttonLink}
|
||||
className="inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-spa-gold hover:text-spa-light transition-colors group/link"
|
||||
className="inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-pop-pink hover:text-pop-black transition-colors group/link"
|
||||
>
|
||||
{slide.buttonText}
|
||||
<span className="group-hover/link:translate-x-1 transition-transform">→</span>
|
||||
|
||||
@@ -13,9 +13,7 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
|
||||
const {
|
||||
imageUrl,
|
||||
imagePosition = 'left',
|
||||
buttonLink,
|
||||
backgroundColor = '#FDFDF9',
|
||||
textColor = '#1F3D2D'
|
||||
buttonLink
|
||||
} = payload;
|
||||
const title = getLocalizedField(payload.title, lang);
|
||||
const description = getLocalizedField(payload.description, lang);
|
||||
@@ -28,7 +26,7 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
|
||||
const [accent, rest] = part.split(']');
|
||||
return (
|
||||
<span key={i}>
|
||||
<span className="text-spa-gold font-medium italic">
|
||||
<span className="text-pop-pink font-medium italic">
|
||||
{accent}
|
||||
</span>
|
||||
{rest}
|
||||
@@ -41,8 +39,7 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
|
||||
|
||||
return (
|
||||
<section
|
||||
className="ui-section-band w-full overflow-hidden"
|
||||
style={{ backgroundColor, color: textColor }}
|
||||
className="ui-section-band w-full overflow-hidden bg-pop-bg text-pop-black"
|
||||
>
|
||||
<div className="ui-section-inner grid grid-cols-1 items-center gap-12 md:grid-cols-2 md:gap-20">
|
||||
<div className={`group relative aspect-square w-full overflow-hidden rounded-3xl shadow-xl ${
|
||||
@@ -65,7 +62,7 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
|
||||
<h2 className="font-serif text-3xl font-light leading-tight tracking-wide md:text-5xl">
|
||||
{renderTitle(title) || title}
|
||||
</h2>
|
||||
<div className="w-12 h-[1px] bg-spa-gold"></div>
|
||||
<div className="w-12 h-[1px] bg-pop-pink"></div>
|
||||
<p className="whitespace-pre-line text-sm font-light leading-relaxed opacity-90 md:text-base">
|
||||
{description}
|
||||
</p>
|
||||
@@ -73,7 +70,7 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
|
||||
<div className="pt-4">
|
||||
<Link
|
||||
to={buttonLink}
|
||||
className="ui-chip border-current px-8 py-3.5 hover:bg-spa-gold hover:border-spa-gold hover:text-spa-light"
|
||||
className="ui-chip border-current px-8 py-3.5 hover:bg-pop-pink hover:border-pop-pink hover:text-pop-bg"
|
||||
>
|
||||
{buttonText}
|
||||
</Link>
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
|
||||
const [accent, rest] = part.split(']');
|
||||
return (
|
||||
<span key={i}>
|
||||
<span className="text-spa-gold font-medium italic">
|
||||
<span className="text-pop-pink font-medium italic">
|
||||
{accent}
|
||||
</span>
|
||||
{rest}
|
||||
@@ -50,7 +50,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
|
||||
</div>
|
||||
|
||||
<div className="ui-hero-content animate-fade-in">
|
||||
<p className="text-spa-sage font-semibold tracking-[0.3em] text-xs md:text-sm uppercase drop-shadow-md">
|
||||
<p className="text-pop-black/70 font-semibold tracking-[0.3em] text-xs md:text-sm uppercase drop-shadow-md">
|
||||
{tagline || t('hero.defaultTagline')}
|
||||
</p>
|
||||
<h1 className="ui-hero-title">
|
||||
|
||||
@@ -28,9 +28,9 @@ export default function DynamicHeroArticle({ payload }: DynamicHeroArticleProps)
|
||||
<div className="ui-hero-overlay" />
|
||||
</div>
|
||||
)}
|
||||
<div className="ui-hero-content text-spa-light">
|
||||
<div className="ui-hero-content text-pop-bg">
|
||||
{category && (
|
||||
<span className="mb-4 block text-sm font-bold uppercase tracking-widest text-spa-gold">
|
||||
<span className="mb-4 block text-sm font-bold uppercase tracking-widest text-pop-pink">
|
||||
{category}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -55,24 +55,24 @@ export default function DynamicNewsletter({ payload }: DynamicNewsletterProps) {
|
||||
return (
|
||||
<section className="ui-dark-shell relative overflow-hidden">
|
||||
<div className="absolute inset-0 opacity-10 pointer-events-none">
|
||||
<div className="absolute top-10 left-10 w-96 h-96 rounded-full bg-spa-gold filter blur-3xl"></div>
|
||||
<div className="absolute bottom-10 right-10 w-96 h-96 rounded-full bg-spa-sage filter blur-3xl"></div>
|
||||
<div className="absolute top-10 left-10 w-96 h-96 rounded-full bg-pop-pink filter blur-3xl"></div>
|
||||
<div className="absolute bottom-10 right-10 w-96 h-96 rounded-full bg-pop-pink opacity-50 filter blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<div className="ui-section-inner relative z-10 space-y-8 text-center">
|
||||
<div className="space-y-3">
|
||||
<h2 className="font-serif text-3xl font-light tracking-wide text-spa-gold md:text-5xl">
|
||||
<h2 className="font-serif text-3xl font-light tracking-wide text-pop-pink md:text-5xl">
|
||||
{title || t('newsletter.defaultTitle')}
|
||||
</h2>
|
||||
<p className="mx-auto max-w-xl text-sm font-light text-spa-light/80 md:text-base">
|
||||
<p className="mx-auto max-w-xl text-sm font-light text-pop-bg/80 md:text-base">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{status === 'success' ? (
|
||||
<div className="p-6 bg-spa-light border border-spa-gold/30 rounded-2xl animate-pop-in space-y-2">
|
||||
<h4 className="text-spa-gold font-semibold text-lg">{t('newsletter.thankYou')}</h4>
|
||||
<p className="text-xs text-spa-light/75">{t('newsletter.successMessage')}</p>
|
||||
<div className="p-6 bg-pop-bg border border-pop-pink/30 rounded-2xl animate-pop-in space-y-2">
|
||||
<h4 className="text-pop-pink font-semibold text-lg">{t('newsletter.thankYou')}</h4>
|
||||
<p className="text-xs text-pop-black/75">{t('newsletter.successMessage')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="mx-auto max-w-lg space-y-4">
|
||||
@@ -96,16 +96,16 @@ export default function DynamicNewsletter({ payload }: DynamicNewsletterProps) {
|
||||
</div>
|
||||
|
||||
{status === 'error' && (
|
||||
<p className="text-rose-400 text-xs text-left px-4">{errorMessage}</p>
|
||||
<p className="text-pop-red text-xs text-left px-4">{errorMessage}</p>
|
||||
)}
|
||||
|
||||
{showPrivacyCheckbox && (
|
||||
<label className="flex items-start gap-3 text-[11px] text-spa-light/60 text-left px-4 cursor-pointer select-none">
|
||||
<label className="flex items-start gap-3 text-[11px] text-pop-bg/60 text-left px-4 cursor-pointer select-none">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={agreed}
|
||||
onChange={(e) => setAgreed(e.target.checked)}
|
||||
className="mt-0.5 accent-spa-gold"
|
||||
className="mt-0.5 accent-pop-pink"
|
||||
/>
|
||||
<span>{privacyText}</span>
|
||||
</label>
|
||||
|
||||
@@ -12,6 +12,7 @@ import DynamicHeroArticle from './DynamicHeroArticle';
|
||||
import DynamicScentNotesBreakdown from './DynamicScentNotesBreakdown';
|
||||
import DynamicQuoteCallout from './DynamicQuoteCallout';
|
||||
import DynamicProductCarousel from './DynamicProductCarousel';
|
||||
import DynamicRichText from './DynamicRichText';
|
||||
import AccordionSection from './sections/AccordionSection';
|
||||
import ContactInfoSection from './sections/ContactInfoSection';
|
||||
import ContactFormSection from './sections/ContactFormSection';
|
||||
@@ -60,6 +61,8 @@ export default function DynamicPageRenderer({
|
||||
return <DynamicBanner key={section.id} payload={section.payload} />;
|
||||
case 'TEXT':
|
||||
return <DynamicText key={section.id} payload={section.payload} />;
|
||||
case 'RICH_TEXT':
|
||||
return <DynamicRichText key={section.id} section={section} />;
|
||||
case 'EDITORIAL_TILE':
|
||||
return <DynamicEditorialTile key={section.id} payload={section.payload} />;
|
||||
case 'EDITORIAL_CAROUSEL':
|
||||
|
||||
@@ -20,21 +20,21 @@ export default function DynamicProductCarousel({ payload }: DynamicProductCarous
|
||||
const description = getLocalizedField(p.description, lang);
|
||||
|
||||
return (
|
||||
<section className="py-20 bg-spa-sage/30 px-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<section className="ui-section-band bg-pop-bg">
|
||||
<div className="ui-section-inner">
|
||||
{(title || description) && (
|
||||
<div className="text-center mb-12">
|
||||
{title && <h2 className="text-3xl md:text-5xl font-serif text-spa-forest mb-4">{title}</h2>}
|
||||
{description && <p className="text-lg text-spa-stone/70 max-w-2xl mx-auto">{description}</p>}
|
||||
{title && <h2 className="text-3xl md:text-5xl font-serif text-pop-black mb-4">{title}</h2>}
|
||||
{description && <p className="text-lg text-pop-black/70 max-w-2xl mx-auto">{description}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{products.length === 0 ? (
|
||||
<div className="text-center py-12 text-spa-stone/50 italic">
|
||||
<div className="text-center py-12 text-pop-black/50 italic">
|
||||
No products to display.
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative pb-12">
|
||||
<div className="relative pb-12" style={{ '--swiper-navigation-color': 'var(--color-pop-black)' } as React.CSSProperties}>
|
||||
<Swiper
|
||||
modules={[Autoplay, Pagination, Navigation]}
|
||||
spaceBetween={32}
|
||||
@@ -44,7 +44,7 @@ export default function DynamicProductCarousel({ payload }: DynamicProductCarous
|
||||
1024: { slidesPerView: 3 },
|
||||
1280: { slidesPerView: 4 }
|
||||
}}
|
||||
pagination={{ clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-spa-forest' }}
|
||||
pagination={{ clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-pop-black' }}
|
||||
navigation
|
||||
autoplay={{ delay: 5000, disableOnInteraction: false }}
|
||||
className="w-full !px-4"
|
||||
|
||||
@@ -26,18 +26,18 @@ export default function DynamicProducts({ payload, categories = [] }: DynamicPro
|
||||
return (
|
||||
<section className="ui-section-band relative bg-pop-bg">
|
||||
<div className="ui-section-inner relative z-10 space-y-12">
|
||||
<div className="flex items-end justify-between gap-6 border-b border-spa-stone/20 pb-6 md:flex-row">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-end justify-between gap-6 border-b border-pop-black/10 pb-6">
|
||||
<div>
|
||||
<h2 className="text-4xl font-bold text-pop-black md:text-5xl">
|
||||
{title || "Featured Picks"}
|
||||
</h2>
|
||||
{subtitle && (
|
||||
<p className="text-spa-stone mt-2">{subtitle}</p>
|
||||
<p className="text-pop-black/60 mt-2">{subtitle}</p>
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
to="/shop"
|
||||
className="text-pop-pink font-medium hover:text-pop-red transition-colors flex items-center gap-2"
|
||||
className="text-pop-pink font-medium hover:opacity-80 transition-opacity flex items-center gap-2"
|
||||
>
|
||||
{t('common.viewAllCollection')}
|
||||
→
|
||||
@@ -45,7 +45,7 @@ export default function DynamicProducts({ payload, categories = [] }: DynamicPro
|
||||
</div>
|
||||
|
||||
{products.length === 0 ? (
|
||||
<div className="py-12 text-center text-spa-stone">
|
||||
<div className="py-12 text-center text-pop-black/60">
|
||||
No products found in this section
|
||||
</div>
|
||||
) : layout === 'carousel' ? (
|
||||
@@ -58,7 +58,7 @@ export default function DynamicProducts({ payload, categories = [] }: DynamicPro
|
||||
640: { slidesPerView: 2 },
|
||||
1024: { slidesPerView: 3 }
|
||||
}}
|
||||
pagination={{ clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-spa-forest' }}
|
||||
pagination={{ clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-pop-black' }}
|
||||
autoplay={{ delay: 6000, disableOnInteraction: false }}
|
||||
className="w-full"
|
||||
>
|
||||
|
||||
@@ -14,20 +14,22 @@ export default function DynamicQuoteCallout({ payload }: DynamicQuoteCalloutProp
|
||||
const role = getLocalizedField(payload.role, lang);
|
||||
|
||||
return (
|
||||
<section className="ui-section-band bg-spa-light">
|
||||
<section className="ui-section-band bg-pop-bg">
|
||||
<div className="ui-section-inner text-center">
|
||||
<svg className="w-12 h-12 mx-auto text-spa-gold/40 mb-8" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z" />
|
||||
</svg>
|
||||
<blockquote className="mb-8 font-serif text-2xl italic leading-relaxed text-spa-forest md:text-4xl">
|
||||
"{quote || 'Fragrance is the invisible, unforgettable, ultimate accessory of fashion.'}"
|
||||
</blockquote>
|
||||
{(author || role) && (
|
||||
<div className="flex flex-col items-center">
|
||||
{author && <span className="font-bold uppercase tracking-widest text-spa-stone">{author}</span>}
|
||||
{role && <span className="text-sm text-spa-stone/60 mt-1 uppercase tracking-wide">{role}</span>}
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-pop-black/5 rounded-3xl p-8 max-w-4xl mx-auto">
|
||||
<svg className="w-12 h-12 mx-auto text-pop-black/40 mb-8" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z" />
|
||||
</svg>
|
||||
<blockquote className="mb-8 font-serif text-2xl italic leading-relaxed text-pop-black md:text-4xl">
|
||||
"{quote || 'Fragrance is the invisible, unforgettable, ultimate accessory of fashion.'}"
|
||||
</blockquote>
|
||||
{(author || role) && (
|
||||
<div className="flex flex-col items-center">
|
||||
{author && <span className="font-bold uppercase tracking-widest text-pop-black/70">{author}</span>}
|
||||
{role && <span className="text-sm text-pop-black/60 mt-1 uppercase tracking-wide">{role}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { PageSection } from '@homaroma/types';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getLocalizedField } from '../utils/localization';
|
||||
|
||||
interface DynamicRichTextProps {
|
||||
section: PageSection;
|
||||
}
|
||||
|
||||
export default function DynamicRichText({ section }: DynamicRichTextProps) {
|
||||
const { i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
const payload = section.payload || {};
|
||||
|
||||
const title = getLocalizedField(payload.title, lang);
|
||||
const content = getLocalizedField(payload.content, lang);
|
||||
|
||||
if (!content) return null;
|
||||
|
||||
return (
|
||||
<section className="ui-section-band bg-pop-bg relative">
|
||||
<div className="ui-section-inner space-y-6">
|
||||
{title && (
|
||||
<h2 className="font-serif text-3xl font-semibold text-pop-black md:text-4xl text-center">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
<div
|
||||
className="ui-prose text-base font-light leading-relaxed text-pop-black/80 md:text-lg max-w-3xl mx-auto"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -28,35 +28,35 @@ export default function DynamicScentNotesBreakdown({ payload }: DynamicScentNote
|
||||
const baseNotes = notes(payload.baseNotes);
|
||||
|
||||
return (
|
||||
<section className="ui-section-band bg-spa-light">
|
||||
<section className="ui-section-band bg-pop-bg">
|
||||
<div className="ui-section-inner text-center">
|
||||
<h2 className="mb-6 font-serif text-3xl text-spa-forest md:text-5xl">
|
||||
<h2 className="mb-6 font-serif text-3xl text-pop-black md:text-5xl">
|
||||
{title || 'Scent Notes'}
|
||||
</h2>
|
||||
{description && (
|
||||
<p className="ui-section-subtitle mb-16 text-lg text-spa-stone/80">
|
||||
<p className="ui-section-subtitle mb-16 text-lg text-pop-black/80">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||
<div className="ui-shell-card p-8">
|
||||
<h3 className="mb-4 text-xl font-bold uppercase tracking-widest text-spa-forest">{labelSet.top}</h3>
|
||||
<p className="text-spa-stone font-light leading-relaxed">
|
||||
<div className="ui-shell-card p-8 hover:-translate-y-1 hover:shadow-lg transition-all duration-300">
|
||||
<h3 className="mb-4 text-xl font-bold uppercase tracking-widest text-pop-black">{labelSet.top}</h3>
|
||||
<p className="text-pop-black/80 font-light leading-relaxed">
|
||||
{topNotes || 'The initial, lighter smells of the fragrance.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="ui-shell-card p-8">
|
||||
<h3 className="mb-4 text-xl font-bold uppercase tracking-widest text-spa-forest">{labelSet.heart}</h3>
|
||||
<p className="text-spa-stone font-light leading-relaxed">
|
||||
<div className="ui-shell-card p-8 hover:-translate-y-1 hover:shadow-lg transition-all duration-300">
|
||||
<h3 className="mb-4 text-xl font-bold uppercase tracking-widest text-pop-black">{labelSet.heart}</h3>
|
||||
<p className="text-pop-black/80 font-light leading-relaxed">
|
||||
{heartNotes || 'The core of the fragrance, emerging just after the top notes.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="ui-shell-card p-8">
|
||||
<h3 className="mb-4 text-xl font-bold uppercase tracking-widest text-spa-forest">{labelSet.base}</h3>
|
||||
<p className="text-spa-stone font-light leading-relaxed">
|
||||
<div className="ui-shell-card p-8 hover:-translate-y-1 hover:shadow-lg transition-all duration-300">
|
||||
<h3 className="mb-4 text-xl font-bold uppercase tracking-widest text-pop-black">{labelSet.base}</h3>
|
||||
<p className="text-pop-black/80 font-light leading-relaxed">
|
||||
{baseNotes || 'The deepest, richest scents that linger the longest.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -20,15 +20,15 @@ export default function DynamicText({ payload }: DynamicTextProps) {
|
||||
}[textAlign] || 'text-left';
|
||||
|
||||
return (
|
||||
<section className="ui-section-band bg-spa-light relative">
|
||||
<section className="ui-section-band bg-pop-bg relative">
|
||||
<div className={`ui-section-inner space-y-6 ${alignClass}`}>
|
||||
{title && (
|
||||
<h2 className="font-serif text-3xl font-semibold text-spa-forest md:text-4xl">
|
||||
<h2 className="font-serif text-3xl font-semibold text-pop-black md:text-4xl">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
<div
|
||||
className="whitespace-pre-wrap text-base font-light leading-relaxed text-spa-stone md:text-lg"
|
||||
className="ui-prose text-base font-light leading-relaxed text-pop-black/80 md:text-lg"
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -17,18 +17,18 @@ export default function EmptyState({
|
||||
actionClick
|
||||
}: EmptyStateProps) {
|
||||
const ButtonContent = () => (
|
||||
<span className="inline-block mt-6 px-8 py-3 bg-spa-forest text-spa-light rounded-xl text-sm font-bold uppercase tracking-widest hover:bg-spa-stone hover:shadow-lg transition-all cursor-pointer">
|
||||
<span className="inline-block mt-6 px-8 py-3 bg-pop-black text-pop-bg rounded-xl text-sm font-bold uppercase tracking-widest hover:bg-pop-pink hover:text-pop-bg hover:shadow-lg transition-all cursor-pointer">
|
||||
{actionText}
|
||||
</span>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center p-12 bg-spa-light border border-spa-stone/10 rounded-2xl shadow-sm max-w-lg mx-auto my-8">
|
||||
<div className="w-16 h-16 bg-spa-sage/30 rounded-full flex items-center justify-center mb-2">
|
||||
<PackageOpen className="w-8 h-8 text-spa-forest" />
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center p-12 bg-pop-bg border border-pop-black/10 rounded-2xl shadow-sm max-w-lg mx-auto my-8">
|
||||
<div className="w-16 h-16 bg-pop-black/5 rounded-full flex items-center justify-center mb-2">
|
||||
<PackageOpen className="w-8 h-8 text-pop-black" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-pop-black">{title}</h3>
|
||||
<p className="text-spa-stone">{message}</p>
|
||||
<p className="text-pop-black/70">{message}</p>
|
||||
|
||||
{actionClick ? (
|
||||
<div onClick={actionClick}><ButtonContent /></div>
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function ErrorState({
|
||||
showHomeButton = true
|
||||
}: ErrorStateProps) {
|
||||
const content = (
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center p-8 bg-spa-light border border-rose-500/20 rounded-2xl shadow-sm max-w-md mx-auto">
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center p-8 bg-pop-bg border border-rose-500/20 rounded-2xl shadow-sm max-w-md mx-auto">
|
||||
<div className="w-12 h-12 bg-rose-500/10 rounded-full flex items-center justify-center">
|
||||
<AlertCircle className="w-6 h-6 text-rose-500" />
|
||||
</div>
|
||||
@@ -21,7 +21,7 @@ export default function ErrorState({
|
||||
{showHomeButton && (
|
||||
<Link
|
||||
to="/"
|
||||
className="mt-4 px-6 py-2 bg-spa-forest text-spa-light rounded-lg text-sm font-bold uppercase tracking-widest hover:bg-spa-stone transition-colors"
|
||||
className="mt-4 px-6 py-2 bg-pop-black text-pop-bg rounded-lg text-sm font-bold uppercase tracking-widest hover:bg-pop-black/80 transition-colors"
|
||||
>
|
||||
Return Home
|
||||
</Link>
|
||||
|
||||
@@ -71,31 +71,31 @@ export default function Footer({ categories = [] }: FooterProps) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<footer className="bg-night-900 px-6 py-20 border-t border-spa-light">
|
||||
<footer className="bg-night-900 text-pop-black px-6 py-20 border-t border-pop-black/20">
|
||||
<div className="ui-section-inner grid grid-cols-1 gap-12 md:grid-cols-4">
|
||||
<div className="space-y-6">
|
||||
<div className="text-2xl font-bold tracking-widest">
|
||||
<div className="text-2xl font-bold tracking-widest text-pop-black">
|
||||
{settings?.logoUrl ? <img src={settings.logoUrl} alt={brandName} className="h-8 md:h-10 object-contain" /> : brandName}
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed text-spa-stone">
|
||||
<p className="text-sm leading-relaxed text-pop-black/60">
|
||||
{settings?.footerDescription || t('footer.catchphrase')}
|
||||
</p>
|
||||
<div className="flex gap-4">
|
||||
{settings && (settings.facebookUrl || settings.instagramUrl || settings.tiktokUrl) ? (
|
||||
<>
|
||||
{settings.facebookUrl && (
|
||||
<a href={settings.facebookUrl} target="_blank" rel="noreferrer" className="ui-chip h-9 w-9 border-spa-light bg-spa-light text-[10px] text-spa-light hover:bg-spa-gold hover:text-night-900">FB</a>
|
||||
<a href={settings.facebookUrl} target="_blank" rel="noreferrer" className="ui-chip h-9 w-9 border-pop-black/20 bg-transparent text-[10px] text-pop-black hover:bg-pop-pink hover:border-pop-pink hover:text-pop-bg">FB</a>
|
||||
)}
|
||||
{settings.instagramUrl && (
|
||||
<a href={settings.instagramUrl} target="_blank" rel="noreferrer" className="ui-chip h-9 w-9 border-spa-light bg-spa-light text-[10px] text-spa-light hover:bg-spa-gold hover:text-night-900">IG</a>
|
||||
<a href={settings.instagramUrl} target="_blank" rel="noreferrer" className="ui-chip h-9 w-9 border-pop-black/20 bg-transparent text-[10px] text-pop-black hover:bg-pop-pink hover:border-pop-pink hover:text-pop-bg">IG</a>
|
||||
)}
|
||||
{settings.tiktokUrl && (
|
||||
<a href={settings.tiktokUrl} target="_blank" rel="noreferrer" className="ui-chip h-9 w-9 border-spa-light bg-spa-light text-[10px] text-spa-light hover:bg-spa-gold hover:text-night-900">TT</a>
|
||||
<a href={settings.tiktokUrl} target="_blank" rel="noreferrer" className="ui-chip h-9 w-9 border-pop-black/20 bg-transparent text-[10px] text-pop-black hover:bg-pop-pink hover:border-pop-pink hover:text-pop-bg">TT</a>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
['IG', 'FB', 'TW', 'YT'].map((s) => (
|
||||
<a key={s} href="#" className="ui-chip h-9 w-9 border-spa-light bg-spa-light text-[10px] text-spa-light hover:bg-spa-gold hover:text-night-900">
|
||||
<a key={s} href="#" className="ui-chip h-9 w-9 border-pop-black/20 bg-transparent text-[10px] text-pop-black hover:bg-pop-pink hover:border-pop-pink hover:text-pop-bg">
|
||||
{s}
|
||||
</a>
|
||||
))
|
||||
@@ -104,30 +104,30 @@ export default function Footer({ categories = [] }: FooterProps) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-bold uppercase tracking-widest text-spa-light">{t('footer.explore')}</h4>
|
||||
<div className="flex flex-col space-y-2 text-sm text-spa-stone">
|
||||
<h4 className="text-xs font-bold uppercase tracking-widest text-pop-black">{t('footer.explore')}</h4>
|
||||
<div className="flex flex-col space-y-2 text-sm text-pop-black/60">
|
||||
{exploreMenu.length > 0 ? (
|
||||
exploreMenu.map(item => (
|
||||
<Link key={item.id} to={item.url} className="hover:text-spa-light transition-colors">{translateMenuLabel(item)}</Link>
|
||||
<Link key={item.id} to={item.url} className="hover:text-pop-pink transition-colors">{translateMenuLabel(item)}</Link>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
<Link to="/" className="hover:text-spa-light transition-colors">{t('nav.home')}</Link>
|
||||
<Link to="/shop" className="hover:text-spa-light transition-colors">{t('nav.shop')}</Link>
|
||||
<Link to="/scent-guide" className="hover:text-spa-light transition-colors">{t('nav.scentGuide')}</Link>
|
||||
<Link to="/about" className="hover:text-spa-light transition-colors">{t('nav.about')}</Link>
|
||||
<Link to="/contact" className="hover:text-spa-light transition-colors">{t('nav.contact')}</Link>
|
||||
<Link to="/faq" className="hover:text-spa-light transition-colors">{t('nav.faq')}</Link>
|
||||
<Link to="/" className="hover:text-pop-pink transition-colors">{t('nav.home')}</Link>
|
||||
<Link to="/shop" className="hover:text-pop-pink transition-colors">{t('nav.shop')}</Link>
|
||||
<Link to="/scent-guide" className="hover:text-pop-pink transition-colors">{t('nav.scentGuide')}</Link>
|
||||
<Link to="/about" className="hover:text-pop-pink transition-colors">{t('nav.about')}</Link>
|
||||
<Link to="/contact" className="hover:text-pop-pink transition-colors">{t('nav.contact')}</Link>
|
||||
<Link to="/faq" className="hover:text-pop-pink transition-colors">{t('nav.faq')}</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-bold uppercase tracking-widest text-spa-light">{t('nav.collections')}</h4>
|
||||
<div className="flex flex-col space-y-2 text-sm text-spa-stone">
|
||||
<h4 className="text-xs font-bold uppercase tracking-widest text-pop-black">{t('nav.collections')}</h4>
|
||||
<div className="flex flex-col space-y-2 text-sm text-pop-black/60">
|
||||
{categories.map((c) => (
|
||||
<Link key={c.id} to={`/category/${c.id}`} className="hover:text-spa-light transition-colors">
|
||||
<Link key={c.id} to={`/category/${c.id}`} className="hover:text-pop-pink transition-colors">
|
||||
{getLocalizedField(c.name, localStorage.getItem('app_lang') || 'en')}
|
||||
</Link>
|
||||
))}
|
||||
@@ -135,17 +135,17 @@ export default function Footer({ categories = [] }: FooterProps) {
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-xs font-bold uppercase tracking-widest text-spa-light">
|
||||
<h4 className="text-xs font-bold uppercase tracking-widest text-pop-black">
|
||||
{settings?.footerSupportHeading || t('footer.support', 'Support & Legal')}
|
||||
</h4>
|
||||
<div className="flex flex-col space-y-2 text-sm text-spa-stone">
|
||||
<div className="flex flex-col space-y-2 text-sm text-pop-black/60">
|
||||
{supportMenu.map(item => (
|
||||
<Link key={item.id} to={item.url} className="hover:text-spa-light transition-colors">{translateMenuLabel(item)}</Link>
|
||||
<Link key={item.id} to={item.url} className="hover:text-pop-pink transition-colors">{translateMenuLabel(item)}</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<p className="text-[10px] text-spa-stone uppercase tracking-widest">
|
||||
<p className="text-[10px] text-pop-black/40 uppercase tracking-widest">
|
||||
{settings?.footerCopyrightText || `© ${new Date().getFullYear()} ${brandName}. ${t('footer.rights')}`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,8 @@ interface LoadingStateProps {
|
||||
|
||||
export default function LoadingState({ message = 'Loading...', fullScreen = false }: LoadingStateProps) {
|
||||
const content = (
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-spa-stone p-8">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-spa-forest" />
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-pop-black/70 p-8">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-pop-pink" />
|
||||
<p className="text-sm font-semibold tracking-widest uppercase">{message}</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -62,19 +62,19 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
|
||||
return (
|
||||
<div className="px-6 py-2 space-y-1">
|
||||
{view.data.map((item, index) => (
|
||||
<div key={item.id || `menu-root-${index}`} className="border-b border-spa-stone/10 last:border-0">
|
||||
<div key={item.id || `menu-root-${index}`} className="border-b border-pop-black/10 last:border-0">
|
||||
{item.columns && item.columns.length > 0 ? (
|
||||
<button
|
||||
onClick={() => pushView({ type: 'mega', data: item.columns!, title: item.label })}
|
||||
className="w-full flex items-center justify-between py-4 text-sm font-medium tracking-wide text-spa-stone hover:text-spa-forest uppercase group"
|
||||
className="w-full flex items-center justify-between py-4 text-sm font-medium tracking-wide text-pop-black/70 hover:text-pop-black uppercase group"
|
||||
>
|
||||
<span>{getLocalizedText(item.label, lang)}</span>
|
||||
<ChevronRight className="w-4 h-4 text-spa-stone/40 group-hover:text-spa-forest transition-colors" />
|
||||
<ChevronRight className="w-4 h-4 text-pop-black/40 group-hover:text-pop-black transition-colors" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleLinkClick(item.url || '#')}
|
||||
className="w-full text-left py-4 text-sm font-medium tracking-wide text-spa-stone hover:text-spa-forest uppercase"
|
||||
className="w-full text-left py-4 text-sm font-medium tracking-wide text-pop-black/70 hover:text-pop-black uppercase"
|
||||
>
|
||||
{getLocalizedText(item.label, lang)}
|
||||
</button>
|
||||
@@ -82,10 +82,10 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
|
||||
</div>
|
||||
))}
|
||||
{customer && (
|
||||
<div className="border-b border-spa-stone/10">
|
||||
<div className="border-b border-pop-black/10">
|
||||
<button
|
||||
onClick={() => handleLinkClick('/profile')}
|
||||
className="w-full text-left py-4 text-sm font-medium tracking-wide text-spa-stone hover:text-spa-forest uppercase"
|
||||
className="w-full text-left py-4 text-sm font-medium tracking-wide text-pop-black/70 hover:text-pop-black uppercase"
|
||||
>
|
||||
👤 My Profile
|
||||
</button>
|
||||
@@ -100,7 +100,7 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
|
||||
<div className="px-6 py-4">
|
||||
{view.data.map((col, idx) => (
|
||||
<div key={idx} className="mb-8 last:mb-0">
|
||||
<h4 className="text-[10px] uppercase tracking-widest text-spa-stone/60 font-bold mb-4">
|
||||
<h4 className="text-[10px] uppercase tracking-widest text-pop-black/60 font-bold mb-4">
|
||||
{getLocalizedText(col.title, lang)}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
@@ -109,15 +109,15 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
|
||||
{link.links && link.links.length > 0 ? (
|
||||
<button
|
||||
onClick={() => pushView({ type: 'links', data: link.links!, title: link.label })}
|
||||
className="w-full flex items-center justify-between py-2 text-xs uppercase tracking-widest text-spa-stone hover:text-spa-forest group"
|
||||
className="w-full flex items-center justify-between py-2 text-xs uppercase tracking-widest text-pop-black/70 hover:text-pop-black group"
|
||||
>
|
||||
<span>{getLocalizedText(link.label, lang)}</span>
|
||||
<ChevronRight className="w-4 h-4 text-spa-stone/40 group-hover:text-spa-forest transition-colors" />
|
||||
<ChevronRight className="w-4 h-4 text-pop-black/40 group-hover:text-pop-black transition-colors" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleLinkClick(link.url || '#')}
|
||||
className="w-full text-left py-2 text-xs uppercase tracking-widest text-spa-stone hover:text-spa-forest"
|
||||
className="w-full text-left py-2 text-xs uppercase tracking-widest text-pop-black/70 hover:text-pop-black"
|
||||
>
|
||||
{getLocalizedText(link.label, lang)}
|
||||
</button>
|
||||
@@ -139,15 +139,15 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
|
||||
{link.links && link.links.length > 0 ? (
|
||||
<button
|
||||
onClick={() => pushView({ type: 'links', data: link.links!, title: link.label })}
|
||||
className="w-full flex items-center justify-between py-2 text-xs uppercase tracking-widest text-spa-stone hover:text-spa-forest group"
|
||||
className="w-full flex items-center justify-between py-2 text-xs uppercase tracking-widest text-pop-black/70 hover:text-pop-black group"
|
||||
>
|
||||
<span>{getLocalizedText(link.label, lang)}</span>
|
||||
<ChevronRight className="w-4 h-4 text-spa-stone/40 group-hover:text-spa-forest transition-colors" />
|
||||
<ChevronRight className="w-4 h-4 text-pop-black/40 group-hover:text-pop-black transition-colors" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleLinkClick(link.url || '#')}
|
||||
className="w-full text-left py-2 text-xs uppercase tracking-widest text-spa-stone hover:text-spa-forest"
|
||||
className="w-full text-left py-2 text-xs uppercase tracking-widest text-pop-black/70 hover:text-pop-black"
|
||||
>
|
||||
{getLocalizedText(link.label, lang)}
|
||||
</button>
|
||||
@@ -164,28 +164,28 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
|
||||
return (
|
||||
<div className={`fixed inset-0 z-[60] md:hidden transition-all duration-300 ${isOpen ? 'visible' : 'invisible'}`}>
|
||||
<div
|
||||
className={`absolute inset-0 bg-spa-stone/40 backdrop-blur-sm transition-opacity duration-300 ${isOpen ? 'opacity-100' : 'opacity-0'}`}
|
||||
className={`absolute inset-0 bg-pop-black/40 backdrop-blur-sm transition-opacity duration-300 ${isOpen ? 'opacity-100' : 'opacity-0'}`}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div
|
||||
className={`absolute top-0 right-0 h-full w-80 max-w-[85vw] bg-spa-light shadow-2xl flex flex-col transition-transform duration-300 ${isOpen ? 'translate-x-0' : 'translate-x-full'}`}
|
||||
className={`absolute top-0 right-0 h-full w-80 max-w-[85vw] bg-pop-bg shadow-2xl flex flex-col transition-transform duration-300 ${isOpen ? 'translate-x-0' : 'translate-x-full'}`}
|
||||
>
|
||||
{/* Static Top Header */}
|
||||
<div className="flex items-center justify-between px-6 py-5 border-b border-spa-stone/10 bg-spa-light z-10 shrink-0">
|
||||
<span className="text-sm font-bold uppercase tracking-widest text-spa-forest">Menu</span>
|
||||
<div className="flex items-center justify-between px-6 py-5 border-b border-pop-black/10 bg-pop-bg z-10 shrink-0">
|
||||
<span className="text-sm font-bold uppercase tracking-widest text-pop-black">Menu</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={onOpenLang} className="px-2.5 py-1 rounded-md text-[10px] font-bold text-spa-forest border border-spa-forest/30 uppercase bg-spa-sage flex items-center gap-1 shrink-0">
|
||||
<button onClick={onOpenLang} className="px-2.5 py-1 rounded-md text-[10px] font-bold text-pop-black border border-pop-black/20 uppercase bg-pop-black/5 flex items-center gap-1 shrink-0">
|
||||
<Globe className="w-3 h-3" />
|
||||
{i18n.language.substring(0, 2).toUpperCase()}
|
||||
</button>
|
||||
<button onClick={onClose} className="p-1.5 rounded-full hover:bg-spa-sage transition-all text-spa-stone">
|
||||
<button onClick={onClose} className="p-1.5 rounded-full hover:bg-pop-black/5 transition-all text-pop-black/70">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sliding Views Container */}
|
||||
<div className="flex-1 relative overflow-hidden bg-spa-light">
|
||||
<div className="flex-1 relative overflow-hidden bg-pop-bg">
|
||||
{stack.map((view, index) => {
|
||||
const isCurrent = index === stack.length - 1;
|
||||
const isPast = index < stack.length - 1;
|
||||
@@ -193,7 +193,7 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`absolute inset-0 flex flex-col bg-spa-light transition-transform duration-300 ease-in-out ${
|
||||
className={`absolute inset-0 flex flex-col bg-pop-bg transition-transform duration-300 ease-in-out ${
|
||||
isCurrent ? 'translate-x-0' : isPast ? '-translate-x-full' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
@@ -201,7 +201,7 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
|
||||
{index > 0 && (
|
||||
<button
|
||||
onClick={popView}
|
||||
className="flex items-center px-6 py-4 text-xs font-bold uppercase tracking-widest text-spa-forest border-b border-spa-stone/5 hover:bg-spa-sage transition-colors shrink-0"
|
||||
className="flex items-center px-6 py-4 text-xs font-bold uppercase tracking-widest text-pop-black border-b border-pop-black/5 hover:bg-pop-black/5 transition-colors shrink-0"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4 mr-2" />
|
||||
Back to {getLocalizedText(stack[index - 1].title, lang)}
|
||||
@@ -211,7 +211,7 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
|
||||
{/* Optional View Title Header for nested views to emphasize context */}
|
||||
{index > 0 && (
|
||||
<div className="px-6 py-5 shrink-0">
|
||||
<h3 className="text-lg font-bold uppercase tracking-widest text-spa-forest">{getLocalizedText(view.title, lang)}</h3>
|
||||
<h3 className="text-lg font-bold uppercase tracking-widest text-pop-black">{getLocalizedText(view.title, lang)}</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -225,10 +225,10 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
|
||||
</div>
|
||||
|
||||
{/* Static Bottom Footer */}
|
||||
<div className="px-6 pb-8 pt-6 border-t border-spa-stone/10 bg-spa-light z-10 shrink-0 space-y-3">
|
||||
<div className="px-6 pb-8 pt-6 border-t border-pop-black/10 bg-pop-bg z-10 shrink-0 space-y-3">
|
||||
<button
|
||||
onClick={() => handleLinkClick('/shop')}
|
||||
className="block w-full py-3.5 bg-spa-forest text-spa-light font-bold text-center rounded-xl text-xs uppercase tracking-widest hover:bg-spa-stone transition-colors duration-300 shadow-sm"
|
||||
className="block w-full py-3.5 bg-pop-black text-pop-bg font-bold text-center rounded-xl text-xs uppercase tracking-widest hover:bg-pop-black/80 transition-colors duration-300 shadow-sm"
|
||||
>
|
||||
Shop Now
|
||||
</button>
|
||||
|
||||
@@ -151,16 +151,16 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
<>
|
||||
<nav
|
||||
ref={navRef}
|
||||
className={`fixed ${finalTopClassName} z-50 w-full border-b border-black/5 transition-all duration-300 ${
|
||||
scrolled ? 'shadow-lg shadow-black/5' : 'shadow-none'
|
||||
} bg-spa-light px-4 py-3.5 backdrop-blur-xl md:bg-spa-sage md:px-6 md:py-4 md:backdrop-blur-none`}
|
||||
className={`fixed ${finalTopClassName} z-50 w-full border-b border-pop-black/10 transition-all duration-300 ${
|
||||
scrolled ? 'shadow-lg shadow-pop-black/10 backdrop-blur-xl bg-pop-bg/90' : 'shadow-none bg-pop-bg'
|
||||
} px-4 py-3.5 md:px-6 md:py-4`}
|
||||
>
|
||||
{/* Single-row layout: Logo (left) | Menu (center, desktop) | Actions (right) */}
|
||||
<div className="w-full flex items-center justify-between gap-4">
|
||||
|
||||
{/* Left: Logo */}
|
||||
<div className="flex items-center shrink-0">
|
||||
<Link to="/" onClick={() => setMobileOpen(false)} className="text-xl font-bold italic tracking-widest text-spa-forest uppercase shrink-0 md:text-2xl">
|
||||
<Link to="/" onClick={() => setMobileOpen(false)} className="text-xl font-bold italic tracking-widest text-pop-black uppercase shrink-0 md:text-2xl">
|
||||
{logoUrl ? <img src={logoUrl} alt={brandName} className="h-8 md:h-10 object-contain" /> : brandName}
|
||||
</Link>
|
||||
</div>
|
||||
@@ -177,10 +177,10 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
{/* Search — desktop & mobile */}
|
||||
<button
|
||||
onClick={() => { setSearchOpen(true); setMobileOpen(false); }}
|
||||
className="group rounded-full p-1.5 text-spa-stone transition-all hover:bg-spa-sage md:p-2"
|
||||
className="group rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5 md:p-2"
|
||||
aria-label="Search"
|
||||
>
|
||||
<svg className="w-4 h-4 group-hover:text-spa-forest transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg className="w-4 h-4 group-hover:text-pop-black transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -188,12 +188,12 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
<Link
|
||||
to="/wishlist"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="group relative rounded-full p-1.5 text-spa-stone transition-all hover:bg-spa-sage md:p-2"
|
||||
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5 md:p-2"
|
||||
aria-label="Wishlist"
|
||||
>
|
||||
<Heart className="w-4 h-4 group-hover:text-rose-500 transition-colors" />
|
||||
{wishlistCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-rose-500 text-spa-light text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
|
||||
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
|
||||
{wishlistCount}
|
||||
</span>
|
||||
)}
|
||||
@@ -201,12 +201,12 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
|
||||
<button
|
||||
onClick={() => { toggleCart(true); setMobileOpen(false); }}
|
||||
className="group relative rounded-full p-1.5 text-spa-stone transition-all hover:bg-spa-sage md:p-2"
|
||||
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5 md:p-2"
|
||||
aria-label="Cart"
|
||||
>
|
||||
<ShoppingBag className="w-4 h-4 group-hover:text-spa-forest transition-colors" />
|
||||
<ShoppingBag className="w-4 h-4 group-hover:text-pop-black transition-colors" />
|
||||
{cartCount > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-rose-500 text-spa-light text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
|
||||
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
|
||||
{cartCount}
|
||||
</span>
|
||||
)}
|
||||
@@ -215,19 +215,19 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
{isDesktop && (
|
||||
<>
|
||||
<div className="relative group flex items-center">
|
||||
<button className="px-3 py-2 rounded-full text-[10px] font-bold transition-all text-spa-forest border border-spa-forest/30 hover:bg-spa-sage bg-spa-light flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<button className="px-3 py-2 rounded-full text-[10px] font-bold transition-all text-pop-black border border-pop-black/20 hover:bg-pop-black/5 bg-pop-bg flex items-center gap-1.5 uppercase tracking-wider">
|
||||
<Globe className="w-3.5 h-3.5" strokeWidth={1.5} />
|
||||
{LANGUAGES.find(l => i18n.language.startsWith(l.code))?.short || 'TH'}
|
||||
</button>
|
||||
<div className="absolute top-full right-0 pt-2 w-36 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300">
|
||||
<div className="bg-spa-light backdrop-blur-xl border border-spa-stone/10 rounded-xl overflow-hidden shadow-2xl p-1.5 space-y-0.5">
|
||||
<div className="bg-pop-bg/95 backdrop-blur-xl border border-pop-black/10 rounded-xl overflow-hidden shadow-2xl p-1.5 space-y-0.5">
|
||||
{LANGUAGES.map(lang => {
|
||||
const isActive = i18n.language.startsWith(lang.code)
|
||||
return (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => changeLanguage(lang.code)}
|
||||
className={`w-full flex items-center justify-between px-3 py-2.5 text-xs rounded-lg transition-colors font-medium ${isActive ? 'text-spa-forest bg-spa-sage/60 font-bold' : 'text-spa-stone hover:bg-spa-light hover:text-spa-forest'}`}
|
||||
className={`w-full flex items-center justify-between px-3 py-2.5 text-xs rounded-lg transition-colors font-medium ${isActive ? 'text-pop-black bg-pop-black/10 font-bold' : 'text-pop-black/70 hover:bg-pop-black/5 hover:text-pop-black'}`}
|
||||
>
|
||||
<span>{lang.label}</span>
|
||||
{isActive && <Check className="w-3.5 h-3.5" />}
|
||||
@@ -244,18 +244,18 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
<img
|
||||
src={customer.pictureUrl || `https://ui-avatars.com/api/?name=${customer.displayName}`}
|
||||
alt="Profile"
|
||||
className="w-9 h-9 rounded-full border-2 border-spa-forest/30 cursor-pointer object-cover animate-fade-in"
|
||||
className="w-9 h-9 rounded-full border-2 border-pop-black/20 cursor-pointer object-cover animate-fade-in"
|
||||
/>
|
||||
</Link>
|
||||
<div className="absolute top-full right-0 pt-3 w-36 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300">
|
||||
<div className="bg-spa-light backdrop-blur-xl border border-spa-stone/10 rounded-xl overflow-hidden shadow-xl p-1">
|
||||
<div className="px-3 py-2 border-b border-spa-stone/10 mb-1">
|
||||
<p className="text-[10px] text-spa-stone/60 uppercase tracking-widest leading-tight truncate">{t('nav.signedInAs', 'Signed in as')}</p>
|
||||
<p className="text-xs text-spa-forest font-bold truncate">{customer.displayName}</p>
|
||||
<div className="bg-pop-bg/95 backdrop-blur-xl border border-pop-black/10 rounded-xl overflow-hidden shadow-xl p-1">
|
||||
<div className="px-3 py-2 border-b border-pop-black/10 mb-1">
|
||||
<p className="text-[10px] text-pop-black/60 uppercase tracking-widest leading-tight truncate">{t('nav.signedInAs', 'Signed in as')}</p>
|
||||
<p className="text-xs text-pop-black font-bold truncate">{customer.displayName}</p>
|
||||
</div>
|
||||
<Link
|
||||
to="/profile"
|
||||
className="block px-3 py-2 text-xs uppercase tracking-widest text-spa-stone hover:text-spa-forest hover:bg-spa-light rounded-lg transition-colors font-bold"
|
||||
className="block px-3 py-2 text-xs uppercase tracking-widest text-pop-black/70 hover:text-pop-black hover:bg-pop-black/5 rounded-lg transition-colors font-bold"
|
||||
>
|
||||
My Profile
|
||||
</Link>
|
||||
@@ -271,7 +271,7 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
) : (
|
||||
<button
|
||||
onClick={loginLiff}
|
||||
className="ml-2 px-4 py-2 rounded-full text-xs font-bold uppercase tracking-widest bg-[#00B900] text-spa-light hover:bg-[#009900] transition-all duration-300 shadow-sm"
|
||||
className="ml-2 px-4 py-2 rounded-full text-xs font-bold uppercase tracking-widest bg-[#00B900] text-white hover:bg-[#009900] transition-all duration-300 shadow-sm"
|
||||
>
|
||||
{t('nav.login', 'LINE Login')}
|
||||
</button>
|
||||
@@ -282,7 +282,7 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
{!isDesktop && (
|
||||
<button
|
||||
onClick={() => setMobileOpen(prev => !prev)}
|
||||
className="p-1.5 rounded-full text-spa-stone hover:bg-spa-sage transition-all"
|
||||
className="p-1.5 rounded-full text-pop-black/80 hover:bg-pop-black/5 transition-all"
|
||||
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
|
||||
>
|
||||
{mobileOpen ? <X className="w-5 h-5" /> : <MenuIcon className="w-5 h-5" />}
|
||||
@@ -303,13 +303,13 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
|
||||
{searchOpen && (
|
||||
<div
|
||||
className="fixed inset-0 z-[100] bg-spa-light/97 backdrop-blur-xl flex flex-col items-center pt-24 md:pt-32 px-4 md:px-6"
|
||||
className="fixed inset-0 z-[100] bg-pop-bg/95 backdrop-blur-xl flex flex-col items-center pt-24 md:pt-32 px-4 md:px-6"
|
||||
onClick={(e) => { if (e.target === e.currentTarget) setSearchOpen(false) }}
|
||||
>
|
||||
<div className="w-full max-w-2xl space-y-6 animate-fade-in">
|
||||
<div className="flex justify-between items-center">
|
||||
<p className="text-spa-stone text-xs uppercase tracking-widest font-semibold">{t('nav.search.title')}</p>
|
||||
<button onClick={() => setSearchOpen(false)} className="p-2 rounded-full hover:bg-spa-sage transition-all text-spa-stone">
|
||||
<p className="text-pop-black/70 text-xs uppercase tracking-widest font-semibold">{t('nav.search.title')}</p>
|
||||
<button onClick={() => setSearchOpen(false)} className="p-2 rounded-full hover:bg-pop-black/5 transition-all text-pop-black/80">
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
@@ -317,8 +317,8 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSearch}>
|
||||
<div className="flex bg-spa-light border-2 border-spa-stone/20 focus-within:border-spa-forest rounded-2xl overflow-hidden transition-colors shadow-sm">
|
||||
<svg className="w-5 h-5 text-spa-stone/50 m-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div className="flex bg-pop-bg border-2 border-pop-black/20 focus-within:border-pop-black rounded-2xl overflow-hidden transition-colors shadow-sm">
|
||||
<svg className="w-5 h-5 text-pop-black/50 m-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<input
|
||||
@@ -326,10 +326,10 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="ค้นหากลิ่น, หมวดหมู่..."
|
||||
className="bg-transparent border-none outline-none text-lg md:text-xl p-4 pl-0 w-full text-spa-forest placeholder-spa-stone/40"
|
||||
className="bg-transparent border-none outline-none text-lg md:text-xl p-4 pl-0 w-full text-pop-black placeholder-pop-black/40"
|
||||
autoFocus
|
||||
/>
|
||||
<button type="submit" className="bg-spa-forest text-spa-light px-4 md:px-6 font-bold text-xs uppercase tracking-widest shrink-0 hover:bg-spa-stone transition-colors">
|
||||
<button type="submit" className="bg-pop-black text-pop-bg px-4 md:px-6 font-bold text-xs uppercase tracking-widest shrink-0 hover:bg-pop-black/80 transition-colors">
|
||||
Go
|
||||
</button>
|
||||
</div>
|
||||
@@ -342,22 +342,22 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
className={`fixed inset-0 z-[110] md:hidden transition-all duration-300 ${langSheetOpen ? 'visible' : 'invisible'}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute inset-0 bg-spa-stone/40 backdrop-blur-sm transition-opacity duration-300 ${langSheetOpen ? 'opacity-100' : 'opacity-0'}`}
|
||||
className={`absolute inset-0 bg-pop-black/40 backdrop-blur-sm transition-opacity duration-300 ${langSheetOpen ? 'opacity-100' : 'opacity-0'}`}
|
||||
onClick={() => setLangSheetOpen(false)}
|
||||
/>
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 bg-spa-light rounded-t-3xl transition-transform duration-300 shadow-2xl pb-safe ${langSheetOpen ? 'translate-y-0' : 'translate-y-full'}`}
|
||||
className={`absolute bottom-0 left-0 right-0 bg-pop-bg rounded-t-3xl transition-transform duration-300 shadow-2xl pb-safe ${langSheetOpen ? 'translate-y-0' : 'translate-y-full'}`}
|
||||
>
|
||||
<div className="flex justify-center pt-3 pb-1">
|
||||
<div className="w-12 h-1.5 bg-spa-stone/20 rounded-full" />
|
||||
<div className="w-12 h-1.5 bg-pop-black/20 rounded-full" />
|
||||
</div>
|
||||
<div className="px-6 pb-6 pt-2">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-sm font-bold uppercase tracking-widest text-spa-forest flex items-center gap-2">
|
||||
<h3 className="text-sm font-bold uppercase tracking-widest text-pop-black flex items-center gap-2">
|
||||
<Globe className="w-4 h-4" />
|
||||
Select Language
|
||||
</h3>
|
||||
<button onClick={() => setLangSheetOpen(false)} className="p-2 rounded-full hover:bg-spa-sage text-spa-stone">
|
||||
<button onClick={() => setLangSheetOpen(false)} className="p-2 rounded-full hover:bg-pop-black/5 text-pop-black/80">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -368,7 +368,7 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => changeLanguage(lang.code)}
|
||||
className={`w-full flex items-center justify-between px-4 py-4 rounded-xl transition-colors font-medium border border-transparent ${isActive ? 'bg-spa-sage border-spa-forest/20 text-spa-forest font-bold' : 'bg-spa-light/50 text-spa-stone hover:bg-spa-light'}`}
|
||||
className={`w-full flex items-center justify-between px-4 py-4 rounded-xl transition-colors font-medium border border-transparent ${isActive ? 'bg-pop-black/10 border-pop-black/20 text-pop-black font-bold' : 'bg-pop-bg/50 text-pop-black/80 hover:bg-pop-black/5'}`}
|
||||
>
|
||||
<span className="text-base">{lang.label}</span>
|
||||
{isActive && <Check className="w-5 h-5" />}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function ProductCard({ product, enableQuickShop = false, category
|
||||
onClick={handleCardClick}
|
||||
className="ui-shell-card ui-shell-card-hover group flex cursor-pointer flex-col overflow-hidden p-2"
|
||||
>
|
||||
<div className="relative h-48 overflow-hidden rounded-xl bg-spa-sage/20 md:h-64">
|
||||
<div className="relative h-48 overflow-hidden rounded-xl bg-pop-black/5 md:h-64">
|
||||
<img
|
||||
src={product.images?.[0]?.url || getCategoryImage(displayCategoryName)}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
||||
@@ -57,26 +57,26 @@ export default function ProductCard({ product, enableQuickShop = false, category
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
<div className="ui-chip absolute left-3 top-3 border-spa-light bg-spa-light px-2.5 py-1 text-[10px] text-spa-forest shadow-sm backdrop-blur-sm">
|
||||
<div className="ui-chip absolute left-3 top-3 border-pop-bg bg-pop-bg/90 px-2.5 py-1 text-[10px] text-pop-black shadow-sm backdrop-blur-sm">
|
||||
{displayCategoryName}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleWishlistClick}
|
||||
className="absolute right-3 top-3 z-10 rounded-full bg-spa-light p-2 shadow-sm backdrop-blur-sm transition-transform hover:scale-110 cursor-pointer"
|
||||
className="absolute right-3 top-3 z-10 rounded-full bg-pop-bg/90 p-2 shadow-sm backdrop-blur-sm transition-transform hover:scale-110 cursor-pointer"
|
||||
>
|
||||
<Heart
|
||||
className={`w-4 h-4 transition-colors ${isWished ? 'fill-rose-500 text-rose-500' : 'text-spa-stone/60 hover:text-rose-500'}`}
|
||||
className={`w-4 h-4 transition-colors ${isWished ? 'fill-pop-pink text-pop-pink' : 'text-pop-black/40 hover:text-pop-pink'}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 flex-col p-4">
|
||||
<div className="mb-2">
|
||||
<h3 className="line-clamp-1 text-base font-bold leading-tight text-pop-black transition-colors group-hover:text-spa-forest md:text-lg">
|
||||
<h3 className="line-clamp-1 text-base font-bold leading-tight text-pop-black transition-colors group-hover:text-pop-pink md:text-lg">
|
||||
{displayName}
|
||||
</h3>
|
||||
<p className="mt-1 line-clamp-1 text-[10px] font-semibold uppercase tracking-widest text-spa-stone/60 md:text-xs">
|
||||
<p className="mt-1 line-clamp-1 text-[10px] font-semibold uppercase tracking-widest text-pop-black/50 md:text-xs">
|
||||
{displayCategoryName}
|
||||
</p>
|
||||
</div>
|
||||
@@ -96,8 +96,8 @@ export default function ProductCard({ product, enableQuickShop = false, category
|
||||
}}
|
||||
className={`rounded-full border px-3 py-1 text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer ${
|
||||
isSelected
|
||||
? 'border-spa-forest bg-spa-forest text-spa-light shadow-sm'
|
||||
: 'border-spa-stone/10 bg-spa-light text-spa-stone hover:border-spa-forest/30'
|
||||
? 'border-pop-black bg-pop-black text-pop-bg shadow-sm'
|
||||
: 'border-pop-black/10 bg-transparent text-pop-black/70 hover:border-pop-black/30 hover:bg-pop-black/5'
|
||||
}`}
|
||||
>
|
||||
{variant.size}
|
||||
@@ -109,11 +109,11 @@ export default function ProductCard({ product, enableQuickShop = false, category
|
||||
|
||||
<div className="mt-auto flex flex-col gap-2 pt-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-black text-rose-500 md:text-lg">
|
||||
<span className="text-sm font-black text-pop-pink md:text-lg">
|
||||
{formatPrice(selectedVariant?.price ?? product.variants[0]?.price)}
|
||||
</span>
|
||||
{!enableQuickShop && (
|
||||
<span className="flex items-center gap-1 text-[10px] font-bold uppercase tracking-widest text-spa-forest transition-transform group-hover:translate-x-1 md:text-xs">
|
||||
<span className="flex items-center gap-1 text-[10px] font-bold uppercase tracking-widest text-pop-black transition-transform group-hover:translate-x-1 group-hover:text-pop-pink md:text-xs">
|
||||
{t('product.viewDetails')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function ProductImageGallery({ images, fallback, alt = 'Product I
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Main Image */}
|
||||
<div className="relative rounded-2xl overflow-hidden h-[480px] group bg-spa-light shadow-sm border border-spa-stone/10 flex items-center justify-center">
|
||||
<div className="relative rounded-2xl overflow-hidden h-[480px] group bg-pop-bg shadow-sm border border-pop-black/10 flex items-center justify-center">
|
||||
<img
|
||||
src={currentImageUrl}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
||||
@@ -38,7 +38,7 @@ export default function ProductImageGallery({ images, fallback, alt = 'Product I
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent pointer-events-none" />
|
||||
|
||||
{badgeText && (
|
||||
<div className="absolute top-6 left-6 bg-spa-light backdrop-blur-sm px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-widest text-spa-forest shadow-sm pointer-events-none">
|
||||
<div className="absolute top-6 left-6 bg-pop-bg backdrop-blur-sm px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-widest text-pop-pink shadow-sm pointer-events-none">
|
||||
{badgeText}
|
||||
</div>
|
||||
)}
|
||||
@@ -48,7 +48,7 @@ export default function ProductImageGallery({ images, fallback, alt = 'Product I
|
||||
<>
|
||||
<button
|
||||
onClick={handlePrev}
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 w-10 h-10 bg-spa-light backdrop-blur-md rounded-full flex items-center justify-center text-spa-forest opacity-0 group-hover:opacity-100 transition-opacity hover:bg-spa-light hover:scale-110 shadow-sm"
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 w-10 h-10 bg-pop-bg backdrop-blur-md rounded-full flex items-center justify-center text-pop-black opacity-0 group-hover:opacity-100 transition-opacity hover:bg-pop-bg hover:text-pop-pink hover:scale-110 shadow-sm"
|
||||
aria-label="Previous image"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -57,7 +57,7 @@ export default function ProductImageGallery({ images, fallback, alt = 'Product I
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 w-10 h-10 bg-spa-light backdrop-blur-md rounded-full flex items-center justify-center text-spa-forest opacity-0 group-hover:opacity-100 transition-opacity hover:bg-spa-light hover:scale-110 shadow-sm"
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 w-10 h-10 bg-pop-bg backdrop-blur-md rounded-full flex items-center justify-center text-pop-black opacity-0 group-hover:opacity-100 transition-opacity hover:bg-pop-bg hover:text-pop-pink hover:scale-110 shadow-sm"
|
||||
aria-label="Next image"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
@@ -76,7 +76,7 @@ export default function ProductImageGallery({ images, fallback, alt = 'Product I
|
||||
key={image.id || idx}
|
||||
onClick={() => setCurrentIndex(idx)}
|
||||
className={`relative flex-shrink-0 w-16 h-16 rounded-xl overflow-hidden border-2 transition-all ${
|
||||
currentIndex === idx ? 'border-spa-forest shadow-md' : 'border-transparent hover:border-spa-stone/30'
|
||||
currentIndex === idx ? 'border-pop-black shadow-md' : 'border-transparent hover:border-pop-black/30'
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -79,29 +79,29 @@ export default function ProductReviews({ productId }: { productId: number }) {
|
||||
const visibleReviews = filteredReviews.slice(0, visibleCount)
|
||||
|
||||
return (
|
||||
<div className="pt-16 border-t border-spa-stone/10 space-y-12">
|
||||
<div className="pt-16 border-t border-pop-black/10 space-y-12">
|
||||
<div className="flex flex-col md:flex-row gap-12 items-start">
|
||||
{/* Summary */}
|
||||
<div className="w-full md:w-1/3 bg-spa-light p-8 rounded-3xl text-center space-y-4">
|
||||
<div className="w-full md:w-1/3 bg-pop-bg p-8 rounded-3xl text-center space-y-4">
|
||||
<h2 className="text-3xl font-bold text-pop-black">{t('product.reviews')}</h2>
|
||||
<div className="text-6xl font-black text-spa-forest">{avgRating}</div>
|
||||
<div className="text-6xl font-black text-pop-pink">{avgRating}</div>
|
||||
<div className="flex justify-center gap-1">
|
||||
{[1, 2, 3, 4, 5].map(star => (
|
||||
<Star key={star} className={`w-5 h-5 ${star <= Number(avgRating) ? 'fill-orange-400 text-orange-400' : 'text-spa-stone/30 fill-spa-stone/10'}`} />
|
||||
<Star key={star} className={`w-5 h-5 ${star <= Number(avgRating) ? 'fill-orange-400 text-orange-400' : 'text-pop-black/30 fill-pop-black/10'}`} />
|
||||
))}
|
||||
</div>
|
||||
<p className="text-spa-stone text-sm">{reviews.length} reviews</p>
|
||||
<p className="text-pop-black/60 text-sm">{reviews.length} reviews</p>
|
||||
</div>
|
||||
|
||||
{/* List & Form */}
|
||||
<div className="w-full md:w-2/3 space-y-8">
|
||||
{customer ? (
|
||||
<form onSubmit={handleSubmit} className="bg-spa-light p-6 rounded-3xl border border-spa-stone/10 shadow-sm space-y-4">
|
||||
<form onSubmit={handleSubmit} className="bg-pop-bg p-6 rounded-3xl border border-pop-black/10 shadow-sm space-y-4">
|
||||
<h3 className="font-bold text-pop-black">Write a Review</h3>
|
||||
<div className="flex gap-2">
|
||||
{[1, 2, 3, 4, 5].map(star => (
|
||||
<button type="button" key={star} onClick={() => setRating(star)} className="focus:outline-none">
|
||||
<Star className={`w-8 h-8 ${star <= rating ? 'fill-orange-400 text-orange-400' : 'text-spa-stone/30 fill-spa-stone/10 hover:text-orange-300'}`} />
|
||||
<Star className={`w-8 h-8 ${star <= rating ? 'fill-orange-400 text-orange-400' : 'text-pop-black/30 fill-pop-black/10 hover:text-orange-300'}`} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -109,21 +109,21 @@ export default function ProductReviews({ productId }: { productId: number }) {
|
||||
value={comment}
|
||||
onChange={e => setComment(e.target.value)}
|
||||
placeholder="Share your experience with this scent..."
|
||||
className="w-full bg-spa-light/50 border border-spa-stone/20 rounded-xl p-4 text-sm focus:outline-none focus:border-spa-forest/50 focus:ring-2 focus:ring-spa-forest/20 transition-all"
|
||||
className="w-full bg-pop-bg/50 border border-pop-black/20 rounded-xl p-4 text-sm focus:outline-none focus:border-pop-pink/50 focus:ring-2 focus:ring-pop-pink/20 transition-all text-pop-black placeholder:text-pop-black/50"
|
||||
rows={3}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="px-6 py-2.5 bg-spa-forest text-spa-light text-sm font-bold tracking-widest uppercase rounded-full hover:bg-spa-stone transition-all disabled:opacity-50"
|
||||
className="px-6 py-2.5 bg-pop-pink text-pop-bg text-sm font-bold tracking-widest uppercase rounded-full hover:bg-pop-black transition-all disabled:opacity-50"
|
||||
>
|
||||
{submitting ? 'Submitting...' : 'Submit Review'}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="bg-spa-light p-6 rounded-3xl text-center border border-dashed border-spa-stone/30">
|
||||
<p className="text-spa-stone text-sm mb-2">Login to write a review</p>
|
||||
<button onClick={() => window.location.href='/track'} className="text-spa-forest font-bold text-sm tracking-widest uppercase hover:underline">Go to Login</button>
|
||||
<div className="bg-pop-bg p-6 rounded-3xl text-center border border-dashed border-pop-black/30">
|
||||
<p className="text-pop-black/60 text-sm mb-2">Login to write a review</p>
|
||||
<button onClick={() => window.location.href='/track'} className="text-pop-pink font-bold text-sm tracking-widest uppercase hover:underline">Go to Login</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -135,8 +135,8 @@ export default function ProductReviews({ productId }: { productId: number }) {
|
||||
onClick={() => { setFilterRating(null); setVisibleCount(3); }}
|
||||
className={`flex-shrink-0 px-4 py-1.5 rounded-full text-xs font-bold transition-all border ${
|
||||
filterRating === null
|
||||
? 'bg-spa-stone text-spa-light border-spa-stone'
|
||||
: 'bg-spa-light text-spa-stone border-spa-stone/20 hover:border-spa-stone/50'
|
||||
? 'bg-pop-black text-pop-bg border-pop-black'
|
||||
: 'bg-pop-bg text-pop-black border-pop-black/20 hover:border-pop-black/50'
|
||||
}`}
|
||||
>
|
||||
All ({reviews.length})
|
||||
@@ -150,11 +150,11 @@ export default function ProductReviews({ productId }: { productId: number }) {
|
||||
onClick={() => { setFilterRating(star); setVisibleCount(3); }}
|
||||
className={`flex-shrink-0 flex items-center gap-1 px-4 py-1.5 rounded-full text-xs font-bold transition-all border ${
|
||||
filterRating === star
|
||||
? 'bg-spa-stone text-spa-light border-spa-stone'
|
||||
: 'bg-spa-light text-spa-stone border-spa-stone/20 hover:border-spa-stone/50'
|
||||
? 'bg-pop-black text-pop-bg border-pop-black'
|
||||
: 'bg-pop-bg text-pop-black border-pop-black/20 hover:border-pop-black/50'
|
||||
}`}
|
||||
>
|
||||
{star} <Star className={`w-3 h-3 ${filterRating === star ? 'fill-white text-spa-light' : 'fill-orange-400 text-orange-400'}`} /> ({count})
|
||||
{star} <Star className={`w-3 h-3 ${filterRating === star ? 'fill-white text-pop-bg' : 'fill-orange-400 text-orange-400'}`} /> ({count})
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
@@ -162,19 +162,19 @@ export default function ProductReviews({ productId }: { productId: number }) {
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<p className="text-spa-stone animate-pulse text-sm">Loading reviews...</p>
|
||||
<p className="text-pop-black/60 animate-pulse text-sm">Loading reviews...</p>
|
||||
) : reviews.length === 0 ? (
|
||||
<p className="text-spa-stone/60 text-sm italic">No reviews yet. Be the first to share your thoughts!</p>
|
||||
<p className="text-pop-black/60 text-sm italic">No reviews yet. Be the first to share your thoughts!</p>
|
||||
) : visibleReviews.length === 0 ? (
|
||||
<p className="text-spa-stone/60 text-sm italic">No reviews found for this rating.</p>
|
||||
<p className="text-pop-black/60 text-sm italic">No reviews found for this rating.</p>
|
||||
) : (
|
||||
visibleReviews.map(review => (
|
||||
<div key={review.id} className="border-b border-spa-stone/10 pb-6 last:border-0">
|
||||
<div key={review.id} className="border-b border-pop-black/10 pb-6 last:border-0">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
{review.customer?.pictureUrl ? (
|
||||
<img src={review.customer.pictureUrl} className="w-10 h-10 rounded-full object-cover" />
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-full bg-spa-stone/20 flex items-center justify-center text-spa-light font-bold">
|
||||
<div className="w-10 h-10 rounded-full bg-pop-black/20 flex items-center justify-center text-pop-bg font-bold">
|
||||
{(review.customerName || 'A')[0].toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
@@ -182,13 +182,13 @@ export default function ProductReviews({ productId }: { productId: number }) {
|
||||
<p className="font-bold text-pop-black text-sm">{review.customerName}</p>
|
||||
<div className="flex gap-0.5">
|
||||
{[1, 2, 3, 4, 5].map(star => (
|
||||
<Star key={star} className={`w-3 h-3 ${star <= review.rating ? 'fill-orange-400 text-orange-400' : 'text-spa-stone/30'}`} />
|
||||
<Star key={star} className={`w-3 h-3 ${star <= review.rating ? 'fill-orange-400 text-orange-400' : 'text-pop-black/30'}`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<span className="ml-auto text-xs text-spa-stone/50">{new Date(review.createdAt).toLocaleDateString()}</span>
|
||||
<span className="ml-auto text-xs text-pop-black/50">{new Date(review.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
{review.comment && <p className="text-spa-stone text-sm ml-13 leading-relaxed">{review.comment}</p>}
|
||||
{review.comment && <p className="text-pop-black/60 text-sm ml-13 leading-relaxed">{review.comment}</p>}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -197,7 +197,7 @@ export default function ProductReviews({ productId }: { productId: number }) {
|
||||
<div className="text-center pt-4">
|
||||
<button
|
||||
onClick={() => setVisibleCount(prev => prev + 5)}
|
||||
className="px-6 py-2.5 border border-spa-stone/20 text-spa-stone text-xs font-bold tracking-widest uppercase rounded-full hover:border-spa-stone hover:text-pop-black transition-all bg-spa-light"
|
||||
className="px-6 py-2.5 border border-pop-black/20 text-pop-black/60 text-xs font-bold tracking-widest uppercase rounded-full hover:border-pop-black hover:text-pop-black transition-all bg-pop-bg"
|
||||
>
|
||||
Show More Reviews
|
||||
</button>
|
||||
|
||||
@@ -16,12 +16,6 @@ export default function ThemeInjector() {
|
||||
// Map tokens from the backend (which use brandPrimary etc.)
|
||||
// to the CSS variables expected by SPA's tailwind.config.js
|
||||
const mapping: Record<string, string> = {
|
||||
'--color-spa-light': tokens.surfacePage,
|
||||
'--color-spa-sage': tokens.brandSoft,
|
||||
'--color-spa-stone': tokens.mutedText,
|
||||
'--color-spa-forest': tokens.brandPrimary,
|
||||
'--color-spa-gold': tokens.brandAccent,
|
||||
|
||||
'--color-pop-pink': tokens.brandPrimary,
|
||||
'--color-pop-yellow': tokens.brandAccent,
|
||||
'--color-pop-orange': tokens.brandWarm,
|
||||
|
||||
@@ -19,17 +19,17 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="ui-section-band w-full bg-spa-sand/20 px-4 md:px-8">
|
||||
<section className="ui-section-band w-full bg-pop-bg px-4 md:px-8">
|
||||
<div className="ui-section-inner">
|
||||
{title && (
|
||||
<h2 className="mb-10 text-center font-serif text-3xl text-spa-charcoal md:text-4xl">
|
||||
<h2 className="mb-10 text-center font-serif text-3xl text-pop-black md:text-4xl">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{payload.categories.map((category, catIndex) => (
|
||||
<div key={catIndex} className="mb-12 last:mb-0">
|
||||
<h3 className="mb-6 flex items-center gap-3 font-serif text-2xl text-spa-charcoal">
|
||||
{category.icon && <span className="text-spa-sage">{category.icon}</span>}
|
||||
<h3 className="mb-6 flex items-center gap-3 font-serif text-2xl text-pop-black">
|
||||
{category.icon && <span className="text-pop-pink">{category.icon}</span>}
|
||||
{getLocalizedField(category.name, lang)}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -43,13 +43,15 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between p-5 text-left focus:outline-none focus:ring-2 focus:ring-spa-sage/50"
|
||||
className={`flex w-full items-center justify-between p-5 text-left focus:outline-none focus:ring-2 focus:ring-pop-pink/50 hover:bg-pop-black/5 transition-colors ${
|
||||
isOpen ? 'bg-pop-black/5' : ''
|
||||
}`}
|
||||
onClick={() => toggleAccordion(indexKey)}
|
||||
>
|
||||
<span className="pr-4 text-lg font-medium text-spa-charcoal">
|
||||
<span className="pr-4 text-lg font-medium text-pop-black">
|
||||
{getLocalizedField(item.q, lang)}
|
||||
</span>
|
||||
<span className="flex-shrink-0 text-spa-sage">
|
||||
<span className="flex-shrink-0 text-pop-pink">
|
||||
{isOpen ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
|
||||
</span>
|
||||
</button>
|
||||
@@ -58,7 +60,7 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
|
||||
isOpen ? 'max-h-96 pb-5 opacity-100' : 'max-h-0 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<p className="text-spa-charcoal/80 leading-relaxed whitespace-pre-wrap">
|
||||
<p className="text-pop-black/80 leading-relaxed whitespace-pre-wrap">
|
||||
{getLocalizedField(item.a, lang)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -118,31 +118,31 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="w-full py-20 px-4 md:px-8 bg-spa-sand/30">
|
||||
<section className="w-full py-20 px-4 md:px-8 bg-night-900">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-serif text-spa-charcoal mb-4">
|
||||
<h2 className="text-3xl md:text-4xl font-serif text-pop-black mb-4">
|
||||
{title || copy.title}
|
||||
</h2>
|
||||
<p className="text-lg text-spa-charcoal/70 max-w-xl mx-auto">
|
||||
<p className="text-lg text-pop-black/70 max-w-xl mx-auto">
|
||||
{subtitle || copy.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-spa-light rounded-3xl shadow-xl shadow-spa-sand/50 p-8 md:p-12 border border-spa-sage/10">
|
||||
<div className="bg-pop-bg rounded-3xl shadow-xl shadow-pop-black/5 p-8 md:p-12 border border-pop-black/10">
|
||||
{isSuccess ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 bg-green-100 text-green-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<div className="w-16 h-16 bg-pop-black/5 text-pop-black rounded-full flex items-center justify-center mx-auto mb-6">
|
||||
<Send size={32} />
|
||||
</div>
|
||||
<h3 className="text-2xl font-serif text-spa-charcoal mb-2">{copy.successTitle}</h3>
|
||||
<p className="text-spa-charcoal/70">{copy.successBody}</p>
|
||||
<h3 className="text-2xl font-serif text-pop-black mb-2">{copy.successTitle}</h3>
|
||||
<p className="text-pop-black/70">{copy.successBody}</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-spa-charcoal/80">
|
||||
<label htmlFor="name" className="block text-sm font-medium text-pop-black/80">
|
||||
{copy.name}
|
||||
</label>
|
||||
<input
|
||||
@@ -152,12 +152,12 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 rounded-xl border border-spa-sage/30 focus:outline-none focus:ring-2 focus:ring-spa-sage/50 bg-spa-sand/5 transition-all"
|
||||
className="w-full px-4 py-3 rounded-xl border border-pop-black/20 focus:outline-none focus:ring-2 focus:ring-pop-pink/50 focus:border-pop-pink/50 bg-pop-black/5 transition-all"
|
||||
placeholder={copy.namePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-spa-charcoal/80">
|
||||
<label htmlFor="email" className="block text-sm font-medium text-pop-black/80">
|
||||
{copy.email}
|
||||
</label>
|
||||
<input
|
||||
@@ -167,14 +167,14 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="w-full px-4 py-3 rounded-xl border border-spa-sage/30 focus:outline-none focus:ring-2 focus:ring-spa-sage/50 bg-spa-sand/5 transition-all"
|
||||
className="w-full px-4 py-3 rounded-xl border border-pop-black/20 focus:outline-none focus:ring-2 focus:ring-pop-pink/50 focus:border-pop-pink/50 bg-pop-black/5 transition-all"
|
||||
placeholder="example@email.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="message" className="block text-sm font-medium text-spa-charcoal/80">
|
||||
<label htmlFor="message" className="block text-sm font-medium text-pop-black/80">
|
||||
{copy.message}
|
||||
</label>
|
||||
<textarea
|
||||
@@ -184,7 +184,7 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
rows={5}
|
||||
className="w-full px-4 py-3 rounded-xl border border-spa-sage/30 focus:outline-none focus:ring-2 focus:ring-spa-sage/50 bg-spa-sand/5 transition-all resize-none"
|
||||
className="w-full px-4 py-3 rounded-xl border border-pop-black/20 focus:outline-none focus:ring-2 focus:ring-pop-pink/50 focus:border-pop-pink/50 bg-pop-black/5 transition-all resize-none"
|
||||
placeholder={copy.messagePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
@@ -192,10 +192,10 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full py-4 px-6 bg-spa-sage hover:bg-spa-sage-dark text-spa-light rounded-xl font-medium flex items-center justify-center gap-2 transition-colors disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
className="w-full py-4 px-6 bg-pop-black hover:bg-pop-pink text-pop-bg rounded-xl font-medium flex items-center justify-center gap-2 transition-colors disabled:opacity-70 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span className="w-6 h-6 border-2 border-spa-light border-t-white rounded-full animate-spin" />
|
||||
<span className="w-6 h-6 border-2 border-pop-bg/30 border-t-pop-bg rounded-full animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Send size={18} />
|
||||
|
||||
@@ -30,11 +30,11 @@ export default function ContactInfoSection({ payload }: ContactInfoSectionProps)
|
||||
] as const;
|
||||
|
||||
return (
|
||||
<section className="ui-section-band w-full bg-spa-light px-4 md:px-8">
|
||||
<section className="ui-section-band w-full bg-pop-bg px-4 md:px-8">
|
||||
<div className="ui-section-inner">
|
||||
{title && (
|
||||
<div className="mb-10 text-center">
|
||||
<h2 className="font-serif text-3xl text-spa-charcoal md:text-4xl">
|
||||
<h2 className="font-serif text-3xl text-pop-black md:text-4xl">
|
||||
{title}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -44,17 +44,17 @@ export default function ContactInfoSection({ payload }: ContactInfoSectionProps)
|
||||
if (!value) return null;
|
||||
|
||||
return (
|
||||
<div key={label} className="ui-shell-card flex flex-col items-center p-6 text-center">
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-spa-sage/10 text-spa-sage">
|
||||
<div key={label} className="ui-shell-card flex flex-col items-center p-6 text-center hover:-translate-y-1 hover:shadow-lg transition-all duration-300">
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-pop-black/5 text-pop-black">
|
||||
<Icon size={24} />
|
||||
</div>
|
||||
<h4 className="mb-2 font-serif text-lg text-spa-charcoal">{label}</h4>
|
||||
<h4 className="mb-2 font-serif text-lg text-pop-black">{label}</h4>
|
||||
{label === labelSet.email ? (
|
||||
<a href={`mailto:${value}`} className="text-spa-charcoal/70 hover:text-spa-sage transition-colors">
|
||||
<a href={`mailto:${value}`} className="text-pop-black/70 hover:text-pop-pink transition-colors">
|
||||
{value}
|
||||
</a>
|
||||
) : (
|
||||
<p className="text-spa-charcoal/70 leading-relaxed whitespace-pre-wrap">
|
||||
<p className="text-pop-black/70 leading-relaxed whitespace-pre-wrap">
|
||||
{value}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -78,11 +78,11 @@ export function ThaiAddressInput({ value, onChange }: ThaiAddressInputProps) {
|
||||
if (focusedField !== field || results.length === 0) return null;
|
||||
|
||||
return (
|
||||
<ul className="absolute z-50 w-full bg-spa-light border border-spa-stone/20 rounded-lg mt-1 max-h-60 overflow-y-auto shadow-xl">
|
||||
<ul className="absolute z-50 w-full bg-pop-bg border border-pop-black/20 rounded-lg mt-1 max-h-60 overflow-y-auto shadow-xl">
|
||||
{results.map((item, idx) => (
|
||||
<li
|
||||
key={`${item.district}-${item.amphoe}-${item.province}-${item.zipcode}-${idx}`}
|
||||
className="p-3 hover:bg-spa-sage/10 cursor-pointer text-xs text-pop-black border-b border-spa-stone/10 last:border-0"
|
||||
className="p-3 hover:bg-pop-black/5 cursor-pointer text-xs text-pop-black border-b border-pop-black/10 last:border-0"
|
||||
onMouseDown={(e) => {
|
||||
// use onMouseDown to prevent onBlur from firing before onClick
|
||||
e.preventDefault();
|
||||
@@ -99,7 +99,7 @@ export function ThaiAddressInput({ value, onChange }: ThaiAddressInputProps) {
|
||||
return (
|
||||
<div ref={containerRef} className="grid grid-cols-2 md:grid-cols-4 gap-4 w-full">
|
||||
<div className="space-y-1.5 relative">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">แขวง / ตำบล</label>
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-pop-black/80">แขวง / ตำบล</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
@@ -107,13 +107,13 @@ export function ThaiAddressInput({ value, onChange }: ThaiAddressInputProps) {
|
||||
onChange={e => handleSearch(e.target.value, 'subdistrict')}
|
||||
onFocus={() => { setFocusedField('subdistrict'); handleSearch(value.subdistrict, 'subdistrict'); }}
|
||||
onBlur={() => setTimeout(() => { if (focusedField === 'subdistrict') setFocusedField(null); }, 200)}
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all"
|
||||
className="w-full bg-pop-bg border border-pop-black/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-pop-black outline-none transition-all"
|
||||
/>
|
||||
{renderDropdown('subdistrict')}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 relative">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">เขต / อำเภอ</label>
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-pop-black/80">เขต / อำเภอ</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
@@ -121,13 +121,13 @@ export function ThaiAddressInput({ value, onChange }: ThaiAddressInputProps) {
|
||||
onChange={e => handleSearch(e.target.value, 'district')}
|
||||
onFocus={() => { setFocusedField('district'); handleSearch(value.district, 'district'); }}
|
||||
onBlur={() => setTimeout(() => { if (focusedField === 'district') setFocusedField(null); }, 200)}
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all"
|
||||
className="w-full bg-pop-bg border border-pop-black/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-pop-black outline-none transition-all"
|
||||
/>
|
||||
{renderDropdown('district')}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 relative">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">จังหวัด</label>
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-pop-black/80">จังหวัด</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
@@ -135,13 +135,13 @@ export function ThaiAddressInput({ value, onChange }: ThaiAddressInputProps) {
|
||||
onChange={e => handleSearch(e.target.value, 'province')}
|
||||
onFocus={() => { setFocusedField('province'); handleSearch(value.province, 'province'); }}
|
||||
onBlur={() => setTimeout(() => { if (focusedField === 'province') setFocusedField(null); }, 200)}
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all"
|
||||
className="w-full bg-pop-bg border border-pop-black/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-pop-black outline-none transition-all"
|
||||
/>
|
||||
{renderDropdown('province')}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 relative">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">รหัสไปรษณีย์</label>
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-pop-black/80">รหัสไปรษณีย์</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
@@ -149,7 +149,7 @@ export function ThaiAddressInput({ value, onChange }: ThaiAddressInputProps) {
|
||||
onChange={e => handleSearch(e.target.value, 'zipcode')}
|
||||
onFocus={() => { setFocusedField('zipcode'); handleSearch(value.zipcode, 'zipcode'); }}
|
||||
onBlur={() => setTimeout(() => { if (focusedField === 'zipcode') setFocusedField(null); }, 200)}
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all"
|
||||
className="w-full bg-pop-bg border border-pop-black/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-pop-black outline-none transition-all"
|
||||
/>
|
||||
{renderDropdown('zipcode')}
|
||||
</div>
|
||||
|
||||
+60
-24
@@ -99,11 +99,11 @@
|
||||
}
|
||||
|
||||
.ui-section-subtitle {
|
||||
@apply mx-auto max-w-2xl text-spa-stone;
|
||||
@apply mx-auto max-w-2xl text-pop-black/60;
|
||||
}
|
||||
|
||||
.ui-shell-card {
|
||||
@apply rounded-3xl border border-spa-stone/10 bg-spa-light shadow-brutal transition-all duration-300;
|
||||
@apply rounded-3xl border border-pop-black/10 bg-night-900 shadow-brutal transition-all duration-300;
|
||||
}
|
||||
|
||||
.ui-shell-card-hover {
|
||||
@@ -115,31 +115,40 @@
|
||||
}
|
||||
|
||||
.ui-button-primary {
|
||||
@apply inline-flex items-center justify-center rounded-full bg-spa-forest px-6 py-3 text-xs font-bold uppercase tracking-widest text-spa-light transition-colors hover:bg-spa-forest/90;
|
||||
@apply inline-flex items-center justify-center rounded-full bg-pop-pink px-6 py-3 text-xs font-bold uppercase tracking-widest text-pop-bg transition-colors hover:opacity-90;
|
||||
}
|
||||
|
||||
.ui-button-secondary {
|
||||
@apply inline-flex items-center justify-center rounded-full bg-spa-light px-6 py-3 text-xs font-bold uppercase tracking-widest text-spa-forest transition-colors hover:bg-spa-sage;
|
||||
@apply inline-flex items-center justify-center rounded-full bg-pop-bg px-6 py-3 text-xs font-bold uppercase tracking-widest text-pop-black transition-colors hover:bg-pop-black/5;
|
||||
}
|
||||
|
||||
.ui-input-shell {
|
||||
@apply w-full rounded-full border border-spa-light bg-spa-light px-6 py-4 text-sm text-spa-light outline-none transition placeholder:text-spa-light focus:border-spa-gold/80;
|
||||
@apply w-full rounded-full border border-pop-black/20 bg-transparent px-6 py-4 text-sm text-pop-black outline-none transition placeholder:text-pop-black/50 focus:border-pop-pink;
|
||||
}
|
||||
|
||||
.ui-muted-text {
|
||||
@apply text-spa-stone/70;
|
||||
@apply text-pop-black/70;
|
||||
}
|
||||
|
||||
.ui-hero-shell {
|
||||
@apply relative flex min-h-[90vh] items-center justify-center overflow-hidden bg-night-900 text-spa-light md:aspect-[21/9] md:min-h-0;
|
||||
@apply relative flex min-h-[90vh] items-center justify-center overflow-hidden bg-night-900 text-pop-bg md:aspect-[21/9] md:min-h-0;
|
||||
}
|
||||
|
||||
.ui-hero-overlay {
|
||||
@apply absolute inset-0 bg-neutral-900/40;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: color-mix(in srgb, var(--color-pop-bg) 40%, transparent);
|
||||
}
|
||||
|
||||
.ui-hero-overlay-strong {
|
||||
@apply absolute inset-0 bg-gradient-to-t from-neutral-900/90 via-neutral-900/10 to-neutral-900/30;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
color-mix(in srgb, var(--color-pop-bg) 90%, transparent),
|
||||
color-mix(in srgb, var(--color-pop-bg) 10%, transparent),
|
||||
color-mix(in srgb, var(--color-pop-bg) 30%, transparent)
|
||||
);
|
||||
}
|
||||
|
||||
.ui-hero-content {
|
||||
@@ -147,11 +156,13 @@
|
||||
}
|
||||
|
||||
.ui-hero-title {
|
||||
@apply font-serif text-5xl font-light leading-tight tracking-wide text-spa-light drop-shadow-md md:text-7xl lg:text-8xl;
|
||||
@apply font-serif text-5xl font-light leading-tight tracking-wide drop-shadow-md md:text-7xl lg:text-8xl;
|
||||
color: var(--color-pop-black);
|
||||
}
|
||||
|
||||
.ui-hero-copy {
|
||||
@apply mx-auto max-w-2xl pb-8 pt-4 text-lg font-light text-spa-light drop-shadow-md md:text-xl;
|
||||
@apply mx-auto max-w-2xl pb-8 pt-4 text-lg font-light drop-shadow-md md:text-xl;
|
||||
color: var(--color-pop-black);
|
||||
}
|
||||
|
||||
.ui-banner-shell {
|
||||
@@ -159,39 +170,39 @@
|
||||
}
|
||||
|
||||
.ui-banner-overlay {
|
||||
@apply absolute inset-0 bg-gradient-to-r from-spa-forest/80 via-spa-forest/40 to-transparent;
|
||||
@apply absolute inset-0 bg-gradient-to-r from-pop-black/80 via-pop-black/40 to-transparent;
|
||||
}
|
||||
|
||||
.ui-banner-content {
|
||||
@apply relative z-10 max-w-xl space-y-4 p-8 text-spa-light md:p-16;
|
||||
@apply relative z-10 max-w-xl space-y-4 p-8 text-pop-bg md:p-16;
|
||||
}
|
||||
|
||||
.ui-card-soft {
|
||||
@apply rounded-2xl border border-spa-stone/10 bg-spa-light shadow-sm;
|
||||
@apply rounded-2xl border border-pop-black/10 bg-night-900 shadow-sm;
|
||||
}
|
||||
|
||||
.ui-dark-shell {
|
||||
@apply bg-spa-forest px-6 py-20 text-spa-light;
|
||||
@apply bg-pop-black px-6 py-20 text-pop-bg;
|
||||
}
|
||||
|
||||
.ui-dark-card {
|
||||
@apply overflow-hidden rounded-3xl border border-spa-light bg-spa-light;
|
||||
@apply overflow-hidden rounded-3xl border border-pop-black/10 bg-night-900;
|
||||
}
|
||||
|
||||
.ui-dark-card-title {
|
||||
@apply text-xl font-serif font-light tracking-wide text-spa-light md:text-2xl;
|
||||
@apply text-xl font-serif font-light tracking-wide text-pop-black md:text-2xl;
|
||||
}
|
||||
|
||||
.ui-dark-card-copy {
|
||||
@apply text-sm font-light leading-relaxed text-spa-light/70;
|
||||
@apply text-sm font-light leading-relaxed text-pop-black/70;
|
||||
}
|
||||
|
||||
.ui-dark-link {
|
||||
@apply inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-spa-gold transition-colors hover:text-spa-light;
|
||||
@apply inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-pop-pink transition-colors hover:text-pop-black;
|
||||
}
|
||||
|
||||
.brutal-box {
|
||||
@apply bg-spa-light border border-spa-stone/20 rounded-2xl shadow-brutal transition-all duration-300;
|
||||
@apply bg-night-900 border border-pop-black/20 rounded-2xl shadow-brutal transition-all duration-300;
|
||||
}
|
||||
|
||||
.brutal-box-hover {
|
||||
@@ -199,11 +210,11 @@
|
||||
}
|
||||
|
||||
.brutal-button {
|
||||
@apply px-8 py-4 bg-pop-pink text-spa-light font-medium rounded-full shadow-brutal transition-all duration-300 hover:bg-pop-red hover:-translate-y-1 hover:shadow-brutal-hover;
|
||||
@apply px-8 py-4 bg-pop-pink text-pop-bg font-medium rounded-full shadow-brutal transition-all duration-300 hover:opacity-90 hover:-translate-y-1 hover:shadow-brutal-hover;
|
||||
}
|
||||
|
||||
.brutal-button-alt {
|
||||
@apply px-8 py-4 bg-spa-light text-pop-pink font-medium rounded-full border border-pop-pink shadow-brutal transition-all duration-300 hover:bg-pop-bg hover:-translate-y-1 hover:shadow-brutal-hover;
|
||||
@apply px-8 py-4 bg-transparent text-pop-pink font-medium rounded-full border border-pop-pink shadow-brutal transition-all duration-300 hover:bg-pop-pink hover:text-pop-bg hover:-translate-y-1 hover:shadow-brutal-hover;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,11 +228,11 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-spa-sage rounded-full;
|
||||
@apply bg-pop-black/30 rounded-full;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-spa-sage;
|
||||
@apply bg-pop-black/50;
|
||||
}
|
||||
|
||||
/* Smooth Scrolling */
|
||||
@@ -243,3 +254,28 @@ html {
|
||||
:lang(th) .font-serif {
|
||||
font-family: 'Prompt', sans-serif !important;
|
||||
}
|
||||
|
||||
.ui-prose p {
|
||||
@apply mb-4 leading-relaxed;
|
||||
}
|
||||
.ui-prose p:last-child {
|
||||
@apply mb-0;
|
||||
}
|
||||
.ui-prose ul {
|
||||
@apply list-inside list-disc mb-4 space-y-1;
|
||||
}
|
||||
.ui-prose ol {
|
||||
@apply list-inside list-decimal mb-4 space-y-1;
|
||||
}
|
||||
.ui-prose h1, .ui-prose h2, .ui-prose h3, .ui-prose h4 {
|
||||
@apply font-serif font-semibold mt-8 mb-4 text-pop-black;
|
||||
}
|
||||
.ui-prose a {
|
||||
@apply text-pop-pink underline hover:opacity-80 transition-opacity;
|
||||
}
|
||||
.ui-prose strong {
|
||||
@apply font-bold;
|
||||
}
|
||||
.ui-prose blockquote {
|
||||
@apply border-l-4 border-pop-pink pl-4 italic opacity-80 my-4;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export default function CustomPage() {
|
||||
}, [slug, i18n.language]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-spa-light text-spa-forest">
|
||||
<div className="min-h-screen bg-pop-bg text-pop-black">
|
||||
{announcementSections.map((section) => (
|
||||
<DynamicPageRenderer
|
||||
key={section.id}
|
||||
@@ -78,7 +78,7 @@ export default function CustomPage() {
|
||||
|
||||
{loading ? (
|
||||
<div className="min-h-[60vh] flex items-center justify-center">
|
||||
<div className="w-12 h-12 border-4 border-spa-forest border-t-transparent rounded-full animate-spin"></div>
|
||||
<div className="w-12 h-12 border-4 border-pop-black border-t-transparent rounded-full animate-spin"></div>
|
||||
</div>
|
||||
) : error ? (
|
||||
<NotFoundPage />
|
||||
|
||||
Reference in new issue
Block a user