This commit is contained in:
Your Name committed 2026-06-21 07:04:03 -04:00
1 parent f7ac94ca0d
commit 335f3b4f59
41 files changed
+809 -413

No files matched your search

+32 -32
View File
@@ -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" />
ดำเนินการชำระเงิน
+5 -5
View File
@@ -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]}
+5 -5
View File
@@ -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">&rarr;</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>
+2 -2
View File
@@ -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>
)}
+10 -10
View File
@@ -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"
+5 -5
View File
@@ -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')}
&rarr;
@@ -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"
>
+15 -13
View File
@@ -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>
+3 -3
View File
@@ -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>
+5 -5
View File
@@ -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>
+2 -2
View File
@@ -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>
+23 -23
View File
@@ -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>
+2 -2
View File
@@ -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>
)
+25 -25
View File
@@ -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>
+35 -35
View File
@@ -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" />}
+10 -10
View File
@@ -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
+26 -26
View File
@@ -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>
)}
+10 -10
View File
@@ -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
View File
@@ -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;
}
+2 -2
View File
@@ -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 />