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

@@ -0,0 +1,78 @@
---
name: theme-compliance-auditor
description: Theme Compliance Auditor Agent ตรวจสอบว่า Component ใช้สีจาก Admin (CMS Theme) ผ่าน CSS Variables (ตระกูล pop-*) โดยห้าม Hardcode และห้ามมักง่ายอ่าน API มายัดสีตรงๆ
---
# Skill: Theme Compliance Auditor Agent
## Role
คุณคือ **Theme Compliance Auditor Agent** ประจำโปรเจกต์ HomAroma
หน้าที่หลักของคุณคือ "ผู้พิทักษ์ความถูกต้องของโครงสร้างสีแบบ Dynamic" คุณต้องรับประกันว่า โค้ด Frontend ทั้งหมด (โดยเฉพาะใน `apps/spa`) มีความเคารพต่อการตั้งค่าสีจากระบบ CMS Admin แบบ 100%
## Context: กลไกการทำงานของระบบสี (System Flow)
1. แอดมินตั้งค่าสีในระบบ CMS (เช่น BRAND PRIMARY, SURFACE PAGE, TEXT MAIN)
2. ค่าเหล่านี้ถูกส่งมาผ่าน API (`/api/settings`)
3. ในฝั่ง `apps/spa` เรามี `ThemeInjector.tsx` ทำหน้าที่รับค่า API เหล่านั้น แล้วสร้างเป็น **CSS Variables** ไว้ให้ใช้งาน เช่น:
- `--color-pop-bg` (ดึงค่าจาก `SURFACE PAGE`)
- `--color-pop-black` (ดึงค่าจาก `TEXT MAIN`)
- `--color-pop-pink` (ดึงค่าจาก `BRAND PRIMARY` / จุดเด่น)
- `--color-night-900` (ดึงค่าจาก `SURFACE CARD`)
## ❌ Anti-Patterns (สิ่งที่คุณต้องไล่ล่าและกำจัดทิ้ง)
> [!WARNING]
> **1. การ Hardcode สีเดิมของ Tailwind (Legacy Hardcode)**
> - ห้ามใช้คลาสสีเก่าอย่าง `bg-spa-light`, `text-spa-forest`, `bg-spa-sage`, `border-spa-stone/20`
> - เพราะคลาสพวกนี้เป็นสีคงที่ (Static) เมื่อแอดมินเปลี่ยน Theme หน้าเว็บจะไม่ยอมเปลี่ยนสีตาม
> [!CAUTION]
> **2. ความมักง่าย: ยิง API แล้วดึง Hex มาแปะดื้อๆ (Sloppy API Hardcoding)**
> - ห้ามแก้ปัญหาด้วยการไป Fetch `api/settings` หรือดึงค่าจาก Database เอง แล้วเอาเลข Hex (เช่น `#D4AF37`) มาแทรกตรงๆ ในแอตทริบิวต์ `style={{ color: '#D4AF37' }}` เด็ดขาด
> - นี่คือการทำลาย System Flow เพราะเป็นการข้ามหัว `ThemeInjector.tsx` และทำให้ระบบพังเมื่อแอดมินเปลี่ยนสีภายหลัง
> [!WARNING]
> **3. ละเลยสีสถานะ (Hover / Active / Opacity)**
> - การเปลี่ยนแค่สีหลักไม่พอ หากมี Hover ที่เป็นสี Hardcode (`hover:bg-spa-light`) ต้องแก้เป็น Dynamic (`hover:bg-pop-black/5`)
> - หากมีปุ่มสถานะแบบ Active (เช่นปุ่มเลือกขนาด) ต้องจัดการสีแบบ Invert อย่างสากล (เช่น สลับไปใช้ `bg-pop-black text-pop-bg`)
## ✅ Fixing Patterns (วิธีแก้ไขที่ถูกต้อง)
### การแก้สีพื้นฐาน
```tsx
// ❌ Bad (Legacy Hardcode)
<div className="bg-spa-light text-spa-forest border-spa-stone/10">
// ✅ Good (Dynamic Theme Compliance)
<div className="bg-pop-bg text-pop-black border-pop-black/10">
```
### การแก้ปุ่มเน้นสีแบรนด์ (Brand Accent)
```tsx
// ❌ Bad
<button className="bg-spa-forest text-white">
// ✅ Good (ใช้ pop-pink ซึ่งผูกกับ Brand Primary)
<button className="bg-pop-pink text-pop-bg">
```
### การแก้ Hover State ด้วยความโปร่งแสง
```tsx
// ❌ Bad
<a className="hover:bg-spa-sage text-spa-stone hover:text-spa-forest">
// ✅ Good (ใช้ opacity หรือดึงสีอักษรมาทำสีพื้นหลังจางๆ)
<a className="text-pop-black/70 hover:bg-pop-black/5 hover:text-pop-black">
```
## 🔍 Audit Workflow (กระบวนการทำงานของคุณ)
1. **รับคำสั่ง:** ตรวจสอบชื่อไฟล์ / Component Block ที่รับมอบหมาย
2. **Scan Code:** มองหาคลาส Tailwind กลุ่ม `spa-*` หรือรหัส Hex Color ที่ถูกฝังลงไปดื้อๆ
3. **Analyze Logic:** พิจารณาว่าจุดนั้นคือพื้นหลัง (Background), ข้อความ (Text), จุดเด่น (Accent), หรือสถานะ (Hover/Active)
4. **Refactor:** ใช้เครื่องมือ `replace_file_content` หรือ `multi_replace_file_content` แทนที่ด้วยคลาส `pop-*` ให้ครบถ้วน
5. **Verify Context:** ตรวจทานความถูกต้องของ Contrast เช่น ถ้าพื้นหลังเป็น `bg-pop-black` ตัวหนังสือต้องเป็น `text-pop-bg` (ห้ามใช้สีมืดชนกันเอง)
## ⚖️ Disciplines
- **"Do not break the pipe"**: คุณต้องตระหนักว่าคลาส `pop-pink` ไม่ใช่ "สีชมพู" แต่มันคือ "ท่อลำเลียงสี Brand Primary" ที่มาจาก Admin
- **"System Flow is King"**: หน้าที่ของคุณคือผูกโค้ดเข้ากับท่อส่งตัวแปร CSS เหล่านั้น ห้ามดึงน้ำออกจากท่อมาสาดใส่โค้ดเองเด็ดขาด
+1
View File
@@ -21,6 +21,7 @@ When working in this repository, read this file first.
- QA, user flow, regression, empty/error states, or release readiness: `.agents/skills/qa-product-auditor/SKILL.md` - QA, user flow, regression, empty/error states, or release readiness: `.agents/skills/qa-product-auditor/SKILL.md`
- Brand, SEO, positioning, or marketing copy: `.agents/skills/brand-strategist/SKILL.md` - Brand, SEO, positioning, or marketing copy: `.agents/skills/brand-strategist/SKILL.md`
- Design system, color contrast, readability, or token compliance: `.agents/skills/design-system-auditor/SKILL.md` - Design system, color contrast, readability, or token compliance: `.agents/skills/design-system-auditor/SKILL.md`
- Dynamic Theme compliance, cleaning up hardcoded UI colors: `.agents/skills/theme-compliance-auditor/SKILL.md`
## Operating Rules ## Operating Rules
- Do not guess field names, API contracts, routes, schema, design tokens, or component names. - Do not guess field names, API contracts, routes, schema, design tokens, or component names.
+20 -20
View File
@@ -948,15 +948,15 @@ export default function PageBuilderPage() {
section.payload?.title section.payload?.title
|| section.payload?.tagline || section.payload?.tagline
|| section.payload?.quote || section.payload?.quote
|| (section.type === 'ANNOUNCEMENT_BAR' ? section.payload?.slides?.[0] : undefined) || (section.type?.toUpperCase() === 'ANNOUNCEMENT_BAR' ? section.payload?.slides?.[0] : undefined)
) || `Block #${section.id}`} ) || `Block #${section.id}`}
</h3> </h3>
{section.type === 'PRODUCTS' && ( {section.type?.toUpperCase() === 'PRODUCTS' && (
<p className="text-xs text-slate-400 mt-0.5"> <p className="text-xs text-slate-400 mt-0.5">
Type: {section.payload?.type} • Limit: {section.payload?.limit || 3} products Type: {section.payload?.type} • Limit: {section.payload?.limit || 3} products
</p> </p>
)} )}
{section.type === 'ANNOUNCEMENT_BAR' && ( {section.type?.toUpperCase() === 'ANNOUNCEMENT_BAR' && (
<p className="text-xs text-slate-400 mt-0.5"> <p className="text-xs text-slate-400 mt-0.5">
Slides: {section.payload?.slides?.length || 0} messages Slides: {section.payload?.slides?.length || 0} messages
</p> </p>
@@ -1067,7 +1067,7 @@ export default function PageBuilderPage() {
)} )}
{/* Dynamic Forms based on Type */} {/* Dynamic Forms based on Type */}
{editingSection.type === 'HERO' && ( {editingSection.type?.toUpperCase() === 'HERO' && (
<HeroForm <HeroForm
editingSection={editingSection} editingSection={editingSection}
editingLang={editingLang} editingLang={editingLang}
@@ -1079,7 +1079,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'ANNOUNCEMENT_BAR' && ( {editingSection.type?.toUpperCase() === 'ANNOUNCEMENT_BAR' && (
<AnnouncementBarForm <AnnouncementBarForm
editingSection={editingSection} editingSection={editingSection}
setEditingSection={setEditingSection} setEditingSection={setEditingSection}
@@ -1089,7 +1089,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'NEWSLETTER' && ( {editingSection.type?.toUpperCase() === 'NEWSLETTER' && (
<NewsletterForm <NewsletterForm
editingSection={editingSection} editingSection={editingSection}
setEditingSection={setEditingSection} setEditingSection={setEditingSection}
@@ -1099,7 +1099,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'EDITORIAL_TILE' && ( {editingSection.type?.toUpperCase() === 'EDITORIAL_TILE' && (
<EditorialTileForm <EditorialTileForm
editingSection={editingSection} editingSection={editingSection}
setEditingSection={setEditingSection} setEditingSection={setEditingSection}
@@ -1112,7 +1112,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'EDITORIAL_CAROUSEL' && ( {editingSection.type?.toUpperCase() === 'EDITORIAL_CAROUSEL' && (
<EditorialCarouselForm <EditorialCarouselForm
editingSection={editingSection} editingSection={editingSection}
setEditingSection={setEditingSection} setEditingSection={setEditingSection}
@@ -1127,7 +1127,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'CATEGORIES' && ( {editingSection.type?.toUpperCase() === 'CATEGORIES' && (
<CategoriesForm <CategoriesForm
editingSection={editingSection} editingSection={editingSection}
setEditingSection={setEditingSection} setEditingSection={setEditingSection}
@@ -1138,7 +1138,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'PRODUCTS' && ( {editingSection.type?.toUpperCase() === 'PRODUCTS' && (
<ProductsForm <ProductsForm
editingSection={editingSection} editingSection={editingSection}
setEditingSection={setEditingSection} setEditingSection={setEditingSection}
@@ -1149,7 +1149,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'BANNER' && ( {editingSection.type?.toUpperCase() === 'BANNER' && (
<BannerForm <BannerForm
editingSection={editingSection} editingSection={editingSection}
editingLang={editingLang} editingLang={editingLang}
@@ -1161,7 +1161,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'TEXT' && ( {editingSection.type?.toUpperCase() === 'TEXT' && (
<TextForm <TextForm
editingSection={editingSection} editingSection={editingSection}
setEditingSection={setEditingSection} setEditingSection={setEditingSection}
@@ -1171,7 +1171,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'HERO_ARTICLE' && ( {editingSection.type?.toUpperCase() === 'HERO_ARTICLE' && (
<HeroArticleForm <HeroArticleForm
editingSection={editingSection} editingSection={editingSection}
setEditingSection={setEditingSection} setEditingSection={setEditingSection}
@@ -1184,7 +1184,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'RICH_TEXT' && ( {editingSection.type?.toUpperCase() === 'RICH_TEXT' && (
<RichTextForm <RichTextForm
editingSection={editingSection} editingSection={editingSection}
editingLang={editingLang} editingLang={editingLang}
@@ -1193,7 +1193,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'SCENT_NOTES_BREAKDOWN' && ( {editingSection.type?.toUpperCase() === 'SCENT_NOTES_BREAKDOWN' && (
<ScentNotesBreakdownForm <ScentNotesBreakdownForm
editingSection={editingSection} editingSection={editingSection}
setEditingSection={setEditingSection} setEditingSection={setEditingSection}
@@ -1203,7 +1203,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'QUOTE_CALLOUT' && ( {editingSection.type?.toUpperCase() === 'QUOTE_CALLOUT' && (
<QuoteCalloutForm <QuoteCalloutForm
editingSection={editingSection} editingSection={editingSection}
editingLang={editingLang} editingLang={editingLang}
@@ -1212,7 +1212,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'PRODUCT_CAROUSEL' && ( {editingSection.type?.toUpperCase() === 'PRODUCT_CAROUSEL' && (
<ProductCarouselForm <ProductCarouselForm
editingSection={editingSection} editingSection={editingSection}
setEditingSection={setEditingSection} setEditingSection={setEditingSection}
@@ -1222,7 +1222,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'ACCORDION' && ( {editingSection.type?.toUpperCase() === 'ACCORDION' && (
<AccordionForm <AccordionForm
editingSection={editingSection} editingSection={editingSection}
setEditingSection={setEditingSection} setEditingSection={setEditingSection}
@@ -1235,7 +1235,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'CONTACT_INFO' && ( {editingSection.type?.toUpperCase() === 'CONTACT_INFO' && (
<ContactInfoForm <ContactInfoForm
editingSection={editingSection} editingSection={editingSection}
setEditingSection={setEditingSection} setEditingSection={setEditingSection}
@@ -1245,7 +1245,7 @@ export default function PageBuilderPage() {
/> />
)} )}
{editingSection.type === 'CONTACT_FORM' && ( {editingSection.type?.toUpperCase() === 'CONTACT_FORM' && (
<ContactFormForm <ContactFormForm
editingSection={editingSection} editingSection={editingSection}
editingLang={editingLang} editingLang={editingLang}
Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

+32 -32
View File
@@ -32,41 +32,41 @@ export default function CartDrawer() {
/> />
{/* Drawer */} {/* 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 */} {/* 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"> <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')} {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 })} {t('cart.items', { count: items.length })}
</span> </span>
</h2> </h2>
<button <button
onClick={() => toggleCart(false)} 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> </button>
</div> </div>
{/* Cart Items */} {/* Cart Items */}
<div className="flex-1 overflow-y-auto p-6 space-y-4"> <div className="flex-1 overflow-y-auto p-6 space-y-4">
{items.length === 0 ? ( {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" /> <ShoppingBag className="w-16 h-16 opacity-20" />
<p>{t('cart.empty')}</p> <p>{t('cart.empty')}</p>
<button <button
onClick={() => toggleCart(false)} 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')} {t('cart.continueShopping')}
</button> </button>
</div> </div>
) : ( ) : (
items.map((item) => ( 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 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-spa-sage/30 overflow-hidden flex-shrink-0" style={{ width: '72px', height: '88px' }}> <div className="w-18 h-22 rounded-lg bg-pop-black/5 overflow-hidden flex-shrink-0" style={{ width: '72px', height: '88px' }}>
<img <img
src={item.productImage || '/images/fallback-product.svg'} src={item.productImage || '/images/fallback-product.svg'}
alt={item.productName} alt={item.productName}
@@ -75,33 +75,33 @@ export default function CartDrawer() {
</div> </div>
<div className="flex-1 flex flex-col pt-1 space-y-1 relative min-w-0"> <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 && ( {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 */} {/* Price row */}
<div className="flex items-baseline gap-1.5 mt-0.5"> <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-pop-black/80">{formatPrice(item.price)}</span>
<span className="text-xs text-spa-stone/60">×</span> <span className="text-xs text-pop-black/60">×</span>
<span className="text-xs text-spa-stone/80">{item.quantity}</span> <span className="text-xs text-pop-black/80">{item.quantity}</span>
<span className="text-xs text-spa-stone/60">=</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> <span className="text-sm font-bold text-rose-500">{formatPrice(item.price * item.quantity)}</span>
</div> </div>
{/* Quantity controls */} {/* Quantity controls */}
<div className="flex items-center gap-2 mt-1"> <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 <button
onClick={() => updateQuantity(item.cartItemId, item.quantity - 1)} 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" /> <Minus className="w-3 h-3" />
</button> </button>
<span className="w-5 text-center text-xs font-bold text-pop-black">{item.quantity}</span> <span className="w-5 text-center text-xs font-bold text-pop-black">{item.quantity}</span>
<button <button
onClick={() => updateQuantity(item.cartItemId, item.quantity + 1)} 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" /> <Plus className="w-3 h-3" />
</button> </button>
@@ -110,7 +110,7 @@ export default function CartDrawer() {
<button <button
onClick={() => removeFromCart(item.cartItemId)} 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" title="Remove item"
> >
<Trash2 className="w-3.5 h-3.5" /> <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) const total = Math.max(0, subtotalAfterL2 - discountAmount)
return ( 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 */} {/* Coupon Input */}
{!savedCoupon && ( {!savedCoupon && (
@@ -225,12 +225,12 @@ function CartDrawerFooter({ subtotal, onClose, t }: { subtotal: number; onClose:
value={couponInput} value={couponInput}
onChange={e => setCouponInput(e.target.value.toUpperCase())} onChange={e => setCouponInput(e.target.value.toUpperCase())}
placeholder={t('cart.couponPlaceholder', 'Discount Code')} 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 <button
onClick={handleApplyCoupon} onClick={handleApplyCoupon}
disabled={couponLoading || !couponInput.trim()} 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')} {couponLoading ? '...' : t('cart.applyCoupon', 'Apply')}
</button> </button>
@@ -241,30 +241,30 @@ function CartDrawerFooter({ subtotal, onClose, t }: { subtotal: number; onClose:
{/* Totals */} {/* Totals */}
<div className="space-y-2 text-sm text-pop-black"> <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>ราคาสินค้า</span>
<span>{formatPrice(subtotal)}</span> <span>{formatPrice(subtotal)}</span>
</div> </div>
{promoLevel1Disc > 0 && ( {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>{promoLevel1?.name}</span>
<span>- {formatPrice(promoLevel1Disc)}</span> <span>- {formatPrice(promoLevel1Disc)}</span>
</div> </div>
)} )}
{promoLevel2Disc > 0 && ( {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>{promoLevel2?.name}</span>
<span>- {formatPrice(promoLevel2Disc)}</span> <span>- {formatPrice(promoLevel2Disc)}</span>
</div> </div>
)} )}
{(discountAmount > 0 || isFreeShipping) && savedCoupon && ( {(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="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} {savedCoupon}
</span> </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" /> <X className="w-3 h-3" />
</button> </button>
</span> </span>
@@ -276,13 +276,13 @@ function CartDrawerFooter({ subtotal, onClose, t }: { subtotal: number; onClose:
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<Tag className="w-3 h-3" /> <Tag className="w-3 h-3" />
คูปอง <span className="font-bold tracking-widest">{savedCoupon}</span> (ยังไม่ถึงยอดขั้นต่ำ) คูปอง <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" /> <X className="w-3 h-3" />
</button> </button>
</span> </span>
</div> </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>รวมสุทธิ</span>
<span className="text-rose-500">{formatPrice(total)}</span> <span className="text-rose-500">{formatPrice(total)}</span>
</div> </div>
@@ -292,7 +292,7 @@ function CartDrawerFooter({ subtotal, onClose, t }: { subtotal: number; onClose:
<Link <Link
to="/checkout" to="/checkout"
onClick={onClose} 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" /> <ShoppingBag className="w-4 h-4" />
ดำเนินการชำระเงิน ดำเนินการชำระเงิน
+5 -5
View File
@@ -49,8 +49,8 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
to={item.url || '#'} to={item.url || '#'}
className={`flex items-center gap-1 transition-colors py-2 border-b ${ className={`flex items-center gap-1 transition-colors py-2 border-b ${
isOpen isOpen
? 'text-spa-forest border-spa-forest' ? 'text-pop-black border-pop-black'
: 'text-spa-stone border-transparent hover:text-spa-forest hover:border-spa-forest' : 'text-pop-black/70 border-transparent hover:text-pop-black hover:border-pop-black'
}`} }`}
> >
{getLocalizedText(item.label, lang)} {getLocalizedText(item.label, lang)}
@@ -78,11 +78,11 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
onMouseEnter={() => handleOpen(uniqueId)} onMouseEnter={() => handleOpen(uniqueId)}
onMouseLeave={handleClose} 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"> <div className="flex justify-center gap-16 py-10 max-w-7xl mx-auto px-6">
{item.columns!.map((col, idx) => ( {item.columns!.map((col, idx) => (
<div key={idx} className="flex flex-col min-w-[140px] text-left"> <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)} {getLocalizedText(col.title, lang)}
</h4> </h4>
<ul className="space-y-3"> <ul className="space-y-3">
@@ -90,7 +90,7 @@ export function DesktopMegaMenu({ items }: DesktopMegaMenuProps) {
<li key={lIdx}> <li key={lIdx}>
<Link <Link
to={link.url || '#'} 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)} {getLocalizedText(link.label, lang)}
</Link> </Link>
@@ -14,15 +14,14 @@ interface DynamicAnnouncementBarProps {
export default function DynamicAnnouncementBar({ payload }: DynamicAnnouncementBarProps) { export default function DynamicAnnouncementBar({ payload }: DynamicAnnouncementBarProps) {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const lang = i18n.language; 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)); const slides = (payload.slides || []).map((slide) => getLocalizedField(slide, lang));
if (slides.length === 0) return null; if (slides.length === 0) return null;
return ( return (
<div <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" 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"
style={{ backgroundColor, color: textColor }}
> >
<Swiper <Swiper
modules={[Autoplay]} modules={[Autoplay]}
+5 -5
View File
@@ -16,7 +16,7 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) {
const buttonText = getLocalizedField(payload.buttonText, lang); const buttonText = getLocalizedField(payload.buttonText, lang);
return ( return (
<section className="ui-section-band bg-spa-light relative overflow-hidden"> <section className="ui-section-band bg-pop-bg relative overflow-hidden">
{linkUrl ? ( {linkUrl ? (
<Link to={linkUrl} className="ui-banner-shell group block cursor-pointer"> <Link to={linkUrl} className="ui-banner-shell group block cursor-pointer">
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
@@ -30,12 +30,12 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) {
<div className="ui-banner-content"> <div className="ui-banner-content">
{title && ( {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} {title}
</h3> </h3>
)} )}
{description && ( {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} {description}
</p> </p>
)} )}
@@ -61,12 +61,12 @@ export default function DynamicBanner({ payload }: DynamicBannerProps) {
<div className="ui-banner-content"> <div className="ui-banner-content">
{title && ( {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} {title}
</h3> </h3>
)} )}
{description && ( {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} {description}
</p> </p>
)} )}
@@ -26,7 +26,7 @@ export default function DynamicCategories({ payload, categories, loading }: Dyna
}, [categories, categoryIds]); }, [categories, categoryIds]);
return ( 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="ui-section-inner space-y-16 relative z-10">
<div className="space-y-4 text-center"> <div className="space-y-4 text-center">
<h2 className="text-4xl font-bold tracking-tight text-pop-black md:text-5xl"> <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 ? ( {loading ? (
<div className="grid grid-cols-2 gap-6 md:grid-cols-5"> <div className="grid grid-cols-2 gap-6 md:grid-cols-5">
{Array.from({ length: 5 }).map((_, i) => ( {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> </div>
) : ( ) : (
@@ -51,19 +51,19 @@ export default function DynamicCategories({ payload, categories, loading }: Dyna
to={`/category/${cat.id}`} to={`/category/${cat.id}`}
className="ui-shell-card ui-shell-card-hover group relative h-80 overflow-hidden" 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 <img
src={cat.imageUrl || getCategoryImage(getLocalizedField(cat.name, lang))} 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" className="absolute inset-0 w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
alt={getLocalizedField(cat.name, lang)} alt={getLocalizedField(cat.name, lang)}
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} 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"> <span className="mb-1 block text-xl font-semibold">
{getLocalizedField(cat.name, localStorage.getItem('app_lang') || 'en')} {getLocalizedField(cat.name, localStorage.getItem('app_lang') || 'en')}
</span> </span>
{cat._count && ( {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 {cat._count.products} Products
</span> </span>
)} )}
@@ -34,7 +34,7 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
const [accent, rest] = part.split(']'); const [accent, rest] = part.split(']');
return ( return (
<span key={i}> <span key={i}>
<span className="text-spa-gold font-medium italic"> <span className="text-pop-pink font-medium italic">
{accent} {accent}
</span> </span>
{rest} {rest}
@@ -49,10 +49,10 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
<section className="ui-dark-shell overflow-hidden"> <section className="ui-dark-shell overflow-hidden">
<div className="ui-section-inner space-y-12"> <div className="ui-section-inner space-y-12">
<div className="space-y-3 text-center"> <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} {renderTitle(title) || title}
</h2> </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>
<div className="relative select-none pb-12"> <div className="relative select-none pb-12">
@@ -64,13 +64,13 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
640: { slidesPerView: 2 }, 640: { slidesPerView: 2 },
1024: { slidesPerView: 3 } 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 }} autoplay={{ delay: 5000, disableOnInteraction: false }}
className="w-full" className="w-full"
> >
{slides.map((slide, idx) => ( {slides.map((slide, idx) => (
<SwiperSlide key={idx} className="h-auto flex flex-col"> <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"> <div className="aspect-[4/3] overflow-hidden relative">
<img <img
src={slide.imageUrl} src={slide.imageUrl}
@@ -80,18 +80,18 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
(e.target as HTMLImageElement).style.display = 'none'; (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>
<div className="p-6 md:p-8 flex flex-col flex-1 justify-between space-y-6"> <div className="p-6 md:p-8 flex flex-col flex-1 justify-between space-y-6">
<div className="space-y-3"> <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} {slide.category}
</span> </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} {renderTitle(slide.title) || slide.title}
</h3> </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} {slide.description}
</p> </p>
</div> </div>
@@ -100,7 +100,7 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
<div className="pt-2"> <div className="pt-2">
<Link <Link
to={slide.buttonLink} 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} {slide.buttonText}
<span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span> <span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span>
@@ -13,9 +13,7 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
const { const {
imageUrl, imageUrl,
imagePosition = 'left', imagePosition = 'left',
buttonLink, buttonLink
backgroundColor = '#FDFDF9',
textColor = '#1F3D2D'
} = payload; } = payload;
const title = getLocalizedField(payload.title, lang); const title = getLocalizedField(payload.title, lang);
const description = getLocalizedField(payload.description, lang); const description = getLocalizedField(payload.description, lang);
@@ -28,7 +26,7 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
const [accent, rest] = part.split(']'); const [accent, rest] = part.split(']');
return ( return (
<span key={i}> <span key={i}>
<span className="text-spa-gold font-medium italic"> <span className="text-pop-pink font-medium italic">
{accent} {accent}
</span> </span>
{rest} {rest}
@@ -41,8 +39,7 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
return ( return (
<section <section
className="ui-section-band w-full overflow-hidden" className="ui-section-band w-full overflow-hidden bg-pop-bg text-pop-black"
style={{ backgroundColor, color: textColor }}
> >
<div className="ui-section-inner grid grid-cols-1 items-center gap-12 md:grid-cols-2 md:gap-20"> <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 ${ <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"> <h2 className="font-serif text-3xl font-light leading-tight tracking-wide md:text-5xl">
{renderTitle(title) || title} {renderTitle(title) || title}
</h2> </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"> <p className="whitespace-pre-line text-sm font-light leading-relaxed opacity-90 md:text-base">
{description} {description}
</p> </p>
@@ -73,7 +70,7 @@ export default function DynamicEditorialTile({ payload }: DynamicEditorialTilePr
<div className="pt-4"> <div className="pt-4">
<Link <Link
to={buttonLink} 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} {buttonText}
</Link> </Link>
+2 -2
View File
@@ -23,7 +23,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
const [accent, rest] = part.split(']'); const [accent, rest] = part.split(']');
return ( return (
<span key={i}> <span key={i}>
<span className="text-spa-gold font-medium italic"> <span className="text-pop-pink font-medium italic">
{accent} {accent}
</span> </span>
{rest} {rest}
@@ -50,7 +50,7 @@ export default function DynamicHero({ payload }: DynamicHeroProps) {
</div> </div>
<div className="ui-hero-content animate-fade-in"> <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')} {tagline || t('hero.defaultTagline')}
</p> </p>
<h1 className="ui-hero-title"> <h1 className="ui-hero-title">
@@ -28,9 +28,9 @@ export default function DynamicHeroArticle({ payload }: DynamicHeroArticleProps)
<div className="ui-hero-overlay" /> <div className="ui-hero-overlay" />
</div> </div>
)} )}
<div className="ui-hero-content text-spa-light"> <div className="ui-hero-content text-pop-bg">
{category && ( {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} {category}
</span> </span>
)} )}
+10 -10
View File
@@ -55,24 +55,24 @@ export default function DynamicNewsletter({ payload }: DynamicNewsletterProps) {
return ( return (
<section className="ui-dark-shell relative overflow-hidden"> <section className="ui-dark-shell relative overflow-hidden">
<div className="absolute inset-0 opacity-10 pointer-events-none"> <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 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-spa-sage 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>
<div className="ui-section-inner relative z-10 space-y-8 text-center"> <div className="ui-section-inner relative z-10 space-y-8 text-center">
<div className="space-y-3"> <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')} {title || t('newsletter.defaultTitle')}
</h2> </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} {description}
</p> </p>
</div> </div>
{status === 'success' ? ( {status === 'success' ? (
<div className="p-6 bg-spa-light border border-spa-gold/30 rounded-2xl animate-pop-in space-y-2"> <div className="p-6 bg-pop-bg border border-pop-pink/30 rounded-2xl animate-pop-in space-y-2">
<h4 className="text-spa-gold font-semibold text-lg">{t('newsletter.thankYou')}</h4> <h4 className="text-pop-pink font-semibold text-lg">{t('newsletter.thankYou')}</h4>
<p className="text-xs text-spa-light/75">{t('newsletter.successMessage')}</p> <p className="text-xs text-pop-black/75">{t('newsletter.successMessage')}</p>
</div> </div>
) : ( ) : (
<form onSubmit={handleSubmit} className="mx-auto max-w-lg space-y-4"> <form onSubmit={handleSubmit} className="mx-auto max-w-lg space-y-4">
@@ -96,16 +96,16 @@ export default function DynamicNewsletter({ payload }: DynamicNewsletterProps) {
</div> </div>
{status === 'error' && ( {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 && ( {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 <input
type="checkbox" type="checkbox"
checked={agreed} checked={agreed}
onChange={(e) => setAgreed(e.target.checked)} onChange={(e) => setAgreed(e.target.checked)}
className="mt-0.5 accent-spa-gold" className="mt-0.5 accent-pop-pink"
/> />
<span>{privacyText}</span> <span>{privacyText}</span>
</label> </label>
@@ -12,6 +12,7 @@ import DynamicHeroArticle from './DynamicHeroArticle';
import DynamicScentNotesBreakdown from './DynamicScentNotesBreakdown'; import DynamicScentNotesBreakdown from './DynamicScentNotesBreakdown';
import DynamicQuoteCallout from './DynamicQuoteCallout'; import DynamicQuoteCallout from './DynamicQuoteCallout';
import DynamicProductCarousel from './DynamicProductCarousel'; import DynamicProductCarousel from './DynamicProductCarousel';
import DynamicRichText from './DynamicRichText';
import AccordionSection from './sections/AccordionSection'; import AccordionSection from './sections/AccordionSection';
import ContactInfoSection from './sections/ContactInfoSection'; import ContactInfoSection from './sections/ContactInfoSection';
import ContactFormSection from './sections/ContactFormSection'; import ContactFormSection from './sections/ContactFormSection';
@@ -60,6 +61,8 @@ export default function DynamicPageRenderer({
return <DynamicBanner key={section.id} payload={section.payload} />; return <DynamicBanner key={section.id} payload={section.payload} />;
case 'TEXT': case 'TEXT':
return <DynamicText key={section.id} payload={section.payload} />; return <DynamicText key={section.id} payload={section.payload} />;
case 'RICH_TEXT':
return <DynamicRichText key={section.id} section={section} />;
case 'EDITORIAL_TILE': case 'EDITORIAL_TILE':
return <DynamicEditorialTile key={section.id} payload={section.payload} />; return <DynamicEditorialTile key={section.id} payload={section.payload} />;
case 'EDITORIAL_CAROUSEL': case 'EDITORIAL_CAROUSEL':
@@ -20,21 +20,21 @@ export default function DynamicProductCarousel({ payload }: DynamicProductCarous
const description = getLocalizedField(p.description, lang); const description = getLocalizedField(p.description, lang);
return ( return (
<section className="py-20 bg-spa-sage/30 px-6"> <section className="ui-section-band bg-pop-bg">
<div className="max-w-7xl mx-auto"> <div className="ui-section-inner">
{(title || description) && ( {(title || description) && (
<div className="text-center mb-12"> <div className="text-center mb-12">
{title && <h2 className="text-3xl md:text-5xl font-serif text-spa-forest mb-4">{title}</h2>} {title && <h2 className="text-3xl md:text-5xl font-serif text-pop-black mb-4">{title}</h2>}
{description && <p className="text-lg text-spa-stone/70 max-w-2xl mx-auto">{description}</p>} {description && <p className="text-lg text-pop-black/70 max-w-2xl mx-auto">{description}</p>}
</div> </div>
)} )}
{products.length === 0 ? ( {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. No products to display.
</div> </div>
) : ( ) : (
<div className="relative pb-12"> <div className="relative pb-12" style={{ '--swiper-navigation-color': 'var(--color-pop-black)' } as React.CSSProperties}>
<Swiper <Swiper
modules={[Autoplay, Pagination, Navigation]} modules={[Autoplay, Pagination, Navigation]}
spaceBetween={32} spaceBetween={32}
@@ -44,7 +44,7 @@ export default function DynamicProductCarousel({ payload }: DynamicProductCarous
1024: { slidesPerView: 3 }, 1024: { slidesPerView: 3 },
1280: { slidesPerView: 4 } 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 navigation
autoplay={{ delay: 5000, disableOnInteraction: false }} autoplay={{ delay: 5000, disableOnInteraction: false }}
className="w-full !px-4" className="w-full !px-4"
+5 -5
View File
@@ -26,18 +26,18 @@ export default function DynamicProducts({ payload, categories = [] }: DynamicPro
return ( return (
<section className="ui-section-band relative bg-pop-bg"> <section className="ui-section-band relative bg-pop-bg">
<div className="ui-section-inner relative z-10 space-y-12"> <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> <div>
<h2 className="text-4xl font-bold text-pop-black md:text-5xl"> <h2 className="text-4xl font-bold text-pop-black md:text-5xl">
{title || "Featured Picks"} {title || "Featured Picks"}
</h2> </h2>
{subtitle && ( {subtitle && (
<p className="text-spa-stone mt-2">{subtitle}</p> <p className="text-pop-black/60 mt-2">{subtitle}</p>
)} )}
</div> </div>
<Link <Link
to="/shop" 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')} {t('common.viewAllCollection')}
&rarr; &rarr;
@@ -45,7 +45,7 @@ export default function DynamicProducts({ payload, categories = [] }: DynamicPro
</div> </div>
{products.length === 0 ? ( {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 No products found in this section
</div> </div>
) : layout === 'carousel' ? ( ) : layout === 'carousel' ? (
@@ -58,7 +58,7 @@ export default function DynamicProducts({ payload, categories = [] }: DynamicPro
640: { slidesPerView: 2 }, 640: { slidesPerView: 2 },
1024: { slidesPerView: 3 } 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 }} autoplay={{ delay: 6000, disableOnInteraction: false }}
className="w-full" className="w-full"
> >
+15 -13
View File
@@ -14,20 +14,22 @@ export default function DynamicQuoteCallout({ payload }: DynamicQuoteCalloutProp
const role = getLocalizedField(payload.role, lang); const role = getLocalizedField(payload.role, lang);
return ( return (
<section className="ui-section-band bg-spa-light"> <section className="ui-section-band bg-pop-bg">
<div className="ui-section-inner text-center"> <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"> <div className="bg-pop-black/5 rounded-3xl p-8 max-w-4xl mx-auto">
<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 className="w-12 h-12 mx-auto text-pop-black/40 mb-8" fill="currentColor" viewBox="0 0 24 24">
</svg> <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" />
<blockquote className="mb-8 font-serif text-2xl italic leading-relaxed text-spa-forest md:text-4xl"> </svg>
"{quote || 'Fragrance is the invisible, unforgettable, ultimate accessory of fashion.'}" <blockquote className="mb-8 font-serif text-2xl italic leading-relaxed text-pop-black md:text-4xl">
</blockquote> "{quote || 'Fragrance is the invisible, unforgettable, ultimate accessory of fashion.'}"
{(author || role) && ( </blockquote>
<div className="flex flex-col items-center"> {(author || role) && (
{author && <span className="font-bold uppercase tracking-widest text-spa-stone">{author}</span>} <div className="flex flex-col items-center">
{role && <span className="text-sm text-spa-stone/60 mt-1 uppercase tracking-wide">{role}</span>} {author && <span className="font-bold uppercase tracking-widest text-pop-black/70">{author}</span>}
</div> {role && <span className="text-sm text-pop-black/60 mt-1 uppercase tracking-wide">{role}</span>}
)} </div>
)}
</div>
</div> </div>
</section> </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); const baseNotes = notes(payload.baseNotes);
return ( return (
<section className="ui-section-band bg-spa-light"> <section className="ui-section-band bg-pop-bg">
<div className="ui-section-inner text-center"> <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'} {title || 'Scent Notes'}
</h2> </h2>
{description && ( {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} {description}
</p> </p>
)} )}
<div className="grid grid-cols-1 gap-8 md:grid-cols-3"> <div className="grid grid-cols-1 gap-8 md:grid-cols-3">
<div className="ui-shell-card p-8"> <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-spa-forest">{labelSet.top}</h3> <h3 className="mb-4 text-xl font-bold uppercase tracking-widest text-pop-black">{labelSet.top}</h3>
<p className="text-spa-stone font-light leading-relaxed"> <p className="text-pop-black/80 font-light leading-relaxed">
{topNotes || 'The initial, lighter smells of the fragrance.'} {topNotes || 'The initial, lighter smells of the fragrance.'}
</p> </p>
</div> </div>
<div className="ui-shell-card p-8"> <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-spa-forest">{labelSet.heart}</h3> <h3 className="mb-4 text-xl font-bold uppercase tracking-widest text-pop-black">{labelSet.heart}</h3>
<p className="text-spa-stone font-light leading-relaxed"> <p className="text-pop-black/80 font-light leading-relaxed">
{heartNotes || 'The core of the fragrance, emerging just after the top notes.'} {heartNotes || 'The core of the fragrance, emerging just after the top notes.'}
</p> </p>
</div> </div>
<div className="ui-shell-card p-8"> <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-spa-forest">{labelSet.base}</h3> <h3 className="mb-4 text-xl font-bold uppercase tracking-widest text-pop-black">{labelSet.base}</h3>
<p className="text-spa-stone font-light leading-relaxed"> <p className="text-pop-black/80 font-light leading-relaxed">
{baseNotes || 'The deepest, richest scents that linger the longest.'} {baseNotes || 'The deepest, richest scents that linger the longest.'}
</p> </p>
</div> </div>
+3 -3
View File
@@ -20,15 +20,15 @@ export default function DynamicText({ payload }: DynamicTextProps) {
}[textAlign] || 'text-left'; }[textAlign] || 'text-left';
return ( 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}`}> <div className={`ui-section-inner space-y-6 ${alignClass}`}>
{title && ( {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} {title}
</h2> </h2>
)} )}
<div <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 }} dangerouslySetInnerHTML={{ __html: content }}
/> />
</div> </div>
+5 -5
View File
@@ -17,18 +17,18 @@ export default function EmptyState({
actionClick actionClick
}: EmptyStateProps) { }: EmptyStateProps) {
const ButtonContent = () => ( 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} {actionText}
</span> </span>
) )
return ( 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="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-spa-sage/30 rounded-full flex items-center justify-center mb-2"> <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-spa-forest" /> <PackageOpen className="w-8 h-8 text-pop-black" />
</div> </div>
<h3 className="text-xl font-bold text-pop-black">{title}</h3> <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 ? ( {actionClick ? (
<div onClick={actionClick}><ButtonContent /></div> <div onClick={actionClick}><ButtonContent /></div>
+2 -2
View File
@@ -13,7 +13,7 @@ export default function ErrorState({
showHomeButton = true showHomeButton = true
}: ErrorStateProps) { }: ErrorStateProps) {
const content = ( 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"> <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" /> <AlertCircle className="w-6 h-6 text-rose-500" />
</div> </div>
@@ -21,7 +21,7 @@ export default function ErrorState({
{showHomeButton && ( {showHomeButton && (
<Link <Link
to="/" 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 Return Home
</Link> </Link>
+23 -23
View File
@@ -71,31 +71,31 @@ export default function Footer({ categories = [] }: FooterProps) {
}, []) }, [])
return ( 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="ui-section-inner grid grid-cols-1 gap-12 md:grid-cols-4">
<div className="space-y-6"> <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} {settings?.logoUrl ? <img src={settings.logoUrl} alt={brandName} className="h-8 md:h-10 object-contain" /> : brandName}
</div> </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')} {settings?.footerDescription || t('footer.catchphrase')}
</p> </p>
<div className="flex gap-4"> <div className="flex gap-4">
{settings && (settings.facebookUrl || settings.instagramUrl || settings.tiktokUrl) ? ( {settings && (settings.facebookUrl || settings.instagramUrl || settings.tiktokUrl) ? (
<> <>
{settings.facebookUrl && ( {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 && ( {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 && ( {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) => ( ['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} {s}
</a> </a>
)) ))
@@ -104,30 +104,30 @@ export default function Footer({ categories = [] }: FooterProps) {
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<h4 className="text-xs font-bold uppercase tracking-widest text-spa-light">{t('footer.explore')}</h4> <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-spa-stone"> <div className="flex flex-col space-y-2 text-sm text-pop-black/60">
{exploreMenu.length > 0 ? ( {exploreMenu.length > 0 ? (
exploreMenu.map(item => ( 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="/" className="hover:text-pop-pink transition-colors">{t('nav.home')}</Link>
<Link to="/shop" className="hover:text-spa-light transition-colors">{t('nav.shop')}</Link> <Link to="/shop" className="hover:text-pop-pink transition-colors">{t('nav.shop')}</Link>
<Link to="/scent-guide" className="hover:text-spa-light transition-colors">{t('nav.scentGuide')}</Link> <Link to="/scent-guide" className="hover:text-pop-pink transition-colors">{t('nav.scentGuide')}</Link>
<Link to="/about" className="hover:text-spa-light transition-colors">{t('nav.about')}</Link> <Link to="/about" className="hover:text-pop-pink transition-colors">{t('nav.about')}</Link>
<Link to="/contact" className="hover:text-spa-light transition-colors">{t('nav.contact')}</Link> <Link to="/contact" className="hover:text-pop-pink transition-colors">{t('nav.contact')}</Link>
<Link to="/faq" className="hover:text-spa-light transition-colors">{t('nav.faq')}</Link> <Link to="/faq" className="hover:text-pop-pink transition-colors">{t('nav.faq')}</Link>
</> </>
)} )}
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<h4 className="text-xs font-bold uppercase tracking-widest text-spa-light">{t('nav.collections')}</h4> <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-spa-stone"> <div className="flex flex-col space-y-2 text-sm text-pop-black/60">
{categories.map((c) => ( {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')} {getLocalizedField(c.name, localStorage.getItem('app_lang') || 'en')}
</Link> </Link>
))} ))}
@@ -135,17 +135,17 @@ export default function Footer({ categories = [] }: FooterProps) {
</div> </div>
<div className="space-y-4"> <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')} {settings?.footerSupportHeading || t('footer.support', 'Support & Legal')}
</h4> </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 => ( {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>
<div className="pt-4"> <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')}`} {settings?.footerCopyrightText || `© ${new Date().getFullYear()} ${brandName}. ${t('footer.rights')}`}
</p> </p>
</div> </div>
+2 -2
View File
@@ -7,8 +7,8 @@ interface LoadingStateProps {
export default function LoadingState({ message = 'Loading...', fullScreen = false }: LoadingStateProps) { export default function LoadingState({ message = 'Loading...', fullScreen = false }: LoadingStateProps) {
const content = ( const content = (
<div className="flex flex-col items-center justify-center space-y-4 text-spa-stone p-8"> <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-spa-forest" /> <Loader2 className="w-8 h-8 animate-spin text-pop-pink" />
<p className="text-sm font-semibold tracking-widest uppercase">{message}</p> <p className="text-sm font-semibold tracking-widest uppercase">{message}</p>
</div> </div>
) )
+25 -25
View File
@@ -62,19 +62,19 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
return ( return (
<div className="px-6 py-2 space-y-1"> <div className="px-6 py-2 space-y-1">
{view.data.map((item, index) => ( {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 ? ( {item.columns && item.columns.length > 0 ? (
<button <button
onClick={() => pushView({ type: 'mega', data: item.columns!, title: item.label })} 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> <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>
) : ( ) : (
<button <button
onClick={() => handleLinkClick(item.url || '#')} 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)} {getLocalizedText(item.label, lang)}
</button> </button>
@@ -82,10 +82,10 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
</div> </div>
))} ))}
{customer && ( {customer && (
<div className="border-b border-spa-stone/10"> <div className="border-b border-pop-black/10">
<button <button
onClick={() => handleLinkClick('/profile')} 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 👤 My Profile
</button> </button>
@@ -100,7 +100,7 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
<div className="px-6 py-4"> <div className="px-6 py-4">
{view.data.map((col, idx) => ( {view.data.map((col, idx) => (
<div key={idx} className="mb-8 last:mb-0"> <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)} {getLocalizedText(col.title, lang)}
</h4> </h4>
<div className="space-y-2"> <div className="space-y-2">
@@ -109,15 +109,15 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
{link.links && link.links.length > 0 ? ( {link.links && link.links.length > 0 ? (
<button <button
onClick={() => pushView({ type: 'links', data: link.links!, title: link.label })} 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> <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>
) : ( ) : (
<button <button
onClick={() => handleLinkClick(link.url || '#')} 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)} {getLocalizedText(link.label, lang)}
</button> </button>
@@ -139,15 +139,15 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
{link.links && link.links.length > 0 ? ( {link.links && link.links.length > 0 ? (
<button <button
onClick={() => pushView({ type: 'links', data: link.links!, title: link.label })} 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> <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>
) : ( ) : (
<button <button
onClick={() => handleLinkClick(link.url || '#')} 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)} {getLocalizedText(link.label, lang)}
</button> </button>
@@ -164,28 +164,28 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
return ( return (
<div className={`fixed inset-0 z-[60] md:hidden transition-all duration-300 ${isOpen ? 'visible' : 'invisible'}`}> <div className={`fixed inset-0 z-[60] md:hidden transition-all duration-300 ${isOpen ? 'visible' : 'invisible'}`}>
<div <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} onClick={onClose}
/> />
<div <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 */} {/* 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"> <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-spa-forest">Menu</span> <span className="text-sm font-bold uppercase tracking-widest text-pop-black">Menu</span>
<div className="flex items-center gap-3"> <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" /> <Globe className="w-3 h-3" />
{i18n.language.substring(0, 2).toUpperCase()} {i18n.language.substring(0, 2).toUpperCase()}
</button> </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" /> <X className="w-5 h-5" />
</button> </button>
</div> </div>
</div> </div>
{/* Sliding Views Container */} {/* 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) => { {stack.map((view, index) => {
const isCurrent = index === stack.length - 1; const isCurrent = index === stack.length - 1;
const isPast = index < stack.length - 1; const isPast = index < stack.length - 1;
@@ -193,7 +193,7 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
return ( return (
<div <div
key={index} 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' 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 && ( {index > 0 && (
<button <button
onClick={popView} 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" /> <ChevronLeft className="w-4 h-4 mr-2" />
Back to {getLocalizedText(stack[index - 1].title, lang)} 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 */} {/* Optional View Title Header for nested views to emphasize context */}
{index > 0 && ( {index > 0 && (
<div className="px-6 py-5 shrink-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> </div>
)} )}
@@ -225,10 +225,10 @@ export function MobileDrawer({ isOpen, onClose, items, onOpenLang }: MobileDrawe
</div> </div>
{/* Static Bottom Footer */} {/* 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 <button
onClick={() => handleLinkClick('/shop')} 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 Shop Now
</button> </button>
+35 -35
View File
@@ -151,16 +151,16 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
<> <>
<nav <nav
ref={navRef} ref={navRef}
className={`fixed ${finalTopClassName} z-50 w-full border-b border-black/5 transition-all duration-300 ${ className={`fixed ${finalTopClassName} z-50 w-full border-b border-pop-black/10 transition-all duration-300 ${
scrolled ? 'shadow-lg shadow-black/5' : 'shadow-none' scrolled ? 'shadow-lg shadow-pop-black/10 backdrop-blur-xl bg-pop-bg/90' : 'shadow-none bg-pop-bg'
} 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`} } px-4 py-3.5 md:px-6 md:py-4`}
> >
{/* Single-row layout: Logo (left) | Menu (center, desktop) | Actions (right) */} {/* Single-row layout: Logo (left) | Menu (center, desktop) | Actions (right) */}
<div className="w-full flex items-center justify-between gap-4"> <div className="w-full flex items-center justify-between gap-4">
{/* Left: Logo */} {/* Left: Logo */}
<div className="flex items-center shrink-0"> <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} {logoUrl ? <img src={logoUrl} alt={brandName} className="h-8 md:h-10 object-contain" /> : brandName}
</Link> </Link>
</div> </div>
@@ -177,10 +177,10 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
{/* Search — desktop & mobile */} {/* Search — desktop & mobile */}
<button <button
onClick={() => { setSearchOpen(true); setMobileOpen(false); }} 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" 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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
</button> </button>
@@ -188,12 +188,12 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
<Link <Link
to="/wishlist" to="/wishlist"
onClick={() => setMobileOpen(false)} 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" aria-label="Wishlist"
> >
<Heart className="w-4 h-4 group-hover:text-rose-500 transition-colors" /> <Heart className="w-4 h-4 group-hover:text-rose-500 transition-colors" />
{wishlistCount > 0 && ( {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} {wishlistCount}
</span> </span>
)} )}
@@ -201,12 +201,12 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
<button <button
onClick={() => { toggleCart(true); setMobileOpen(false); }} 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" 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 && ( {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} {cartCount}
</span> </span>
)} )}
@@ -215,19 +215,19 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
{isDesktop && ( {isDesktop && (
<> <>
<div className="relative group flex items-center"> <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} /> <Globe className="w-3.5 h-3.5" strokeWidth={1.5} />
{LANGUAGES.find(l => i18n.language.startsWith(l.code))?.short || 'TH'} {LANGUAGES.find(l => i18n.language.startsWith(l.code))?.short || 'TH'}
</button> </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="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 => { {LANGUAGES.map(lang => {
const isActive = i18n.language.startsWith(lang.code) const isActive = i18n.language.startsWith(lang.code)
return ( return (
<button <button
key={lang.code} key={lang.code}
onClick={() => changeLanguage(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> <span>{lang.label}</span>
{isActive && <Check className="w-3.5 h-3.5" />} {isActive && <Check className="w-3.5 h-3.5" />}
@@ -244,18 +244,18 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
<img <img
src={customer.pictureUrl || `https://ui-avatars.com/api/?name=${customer.displayName}`} src={customer.pictureUrl || `https://ui-avatars.com/api/?name=${customer.displayName}`}
alt="Profile" 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> </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="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="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-spa-stone/10 mb-1"> <div className="px-3 py-2 border-b border-pop-black/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-[10px] text-pop-black/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> <p className="text-xs text-pop-black font-bold truncate">{customer.displayName}</p>
</div> </div>
<Link <Link
to="/profile" 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 My Profile
</Link> </Link>
@@ -271,7 +271,7 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
) : ( ) : (
<button <button
onClick={loginLiff} 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')} {t('nav.login', 'LINE Login')}
</button> </button>
@@ -282,7 +282,7 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
{!isDesktop && ( {!isDesktop && (
<button <button
onClick={() => setMobileOpen(prev => !prev)} 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'} aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
> >
{mobileOpen ? <X className="w-5 h-5" /> : <MenuIcon className="w-5 h-5" />} {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 && ( {searchOpen && (
<div <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) }} onClick={(e) => { if (e.target === e.currentTarget) setSearchOpen(false) }}
> >
<div className="w-full max-w-2xl space-y-6 animate-fade-in"> <div className="w-full max-w-2xl space-y-6 animate-fade-in">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<p className="text-spa-stone text-xs uppercase tracking-widest font-semibold">{t('nav.search.title')}</p> <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-spa-sage transition-all text-spa-stone"> <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"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
@@ -317,8 +317,8 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
</div> </div>
<form onSubmit={handleSearch}> <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"> <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-spa-stone/50 m-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg> </svg>
<input <input
@@ -326,10 +326,10 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="ค้นหากลิ่น, หมวดหมู่..." 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 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 Go
</button> </button>
</div> </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'}`} className={`fixed inset-0 z-[110] md:hidden transition-all duration-300 ${langSheetOpen ? 'visible' : 'invisible'}`}
> >
<div <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)} onClick={() => setLangSheetOpen(false)}
/> />
<div <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="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>
<div className="px-6 pb-6 pt-2"> <div className="px-6 pb-6 pt-2">
<div className="flex justify-between items-center mb-4"> <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" /> <Globe className="w-4 h-4" />
Select Language Select Language
</h3> </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" /> <X className="w-4 h-4" />
</button> </button>
</div> </div>
@@ -368,7 +368,7 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) {
<button <button
key={lang.code} key={lang.code}
onClick={() => changeLanguage(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> <span className="text-base">{lang.label}</span>
{isActive && <Check className="w-5 h-5" />} {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} onClick={handleCardClick}
className="ui-shell-card ui-shell-card-hover group flex cursor-pointer flex-col overflow-hidden p-2" 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 <img
src={product.images?.[0]?.url || getCategoryImage(displayCategoryName)} src={product.images?.[0]?.url || getCategoryImage(displayCategoryName)}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700" 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'; (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} {displayCategoryName}
</div> </div>
<button <button
type="button" type="button"
onClick={handleWishlistClick} 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 <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> </button>
</div> </div>
<div className="flex flex-1 flex-col p-4"> <div className="flex flex-1 flex-col p-4">
<div className="mb-2"> <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} {displayName}
</h3> </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} {displayCategoryName}
</p> </p>
</div> </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 ${ className={`rounded-full border px-3 py-1 text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer ${
isSelected isSelected
? 'border-spa-forest bg-spa-forest text-spa-light shadow-sm' ? 'border-pop-black bg-pop-black text-pop-bg shadow-sm'
: 'border-spa-stone/10 bg-spa-light text-spa-stone hover:border-spa-forest/30' : 'border-pop-black/10 bg-transparent text-pop-black/70 hover:border-pop-black/30 hover:bg-pop-black/5'
}`} }`}
> >
{variant.size} {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="mt-auto flex flex-col gap-2 pt-3">
<div className="flex items-center justify-between"> <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)} {formatPrice(selectedVariant?.price ?? product.variants[0]?.price)}
</span> </span>
{!enableQuickShop && ( {!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')} {t('product.viewDetails')}
</span> </span>
)} )}
@@ -28,7 +28,7 @@ export default function ProductImageGallery({ images, fallback, alt = 'Product I
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{/* Main Image */} {/* 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 <img
src={currentImageUrl} src={currentImageUrl}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700" 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" /> <div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent pointer-events-none" />
{badgeText && ( {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} {badgeText}
</div> </div>
)} )}
@@ -48,7 +48,7 @@ export default function ProductImageGallery({ images, fallback, alt = 'Product I
<> <>
<button <button
onClick={handlePrev} 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" aria-label="Previous image"
> >
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <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>
<button <button
onClick={handleNext} 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" aria-label="Next image"
> >
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <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} key={image.id || idx}
onClick={() => setCurrentIndex(idx)} onClick={() => setCurrentIndex(idx)}
className={`relative flex-shrink-0 w-16 h-16 rounded-xl overflow-hidden border-2 transition-all ${ 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 <img
+26 -26
View File
@@ -79,29 +79,29 @@ export default function ProductReviews({ productId }: { productId: number }) {
const visibleReviews = filteredReviews.slice(0, visibleCount) const visibleReviews = filteredReviews.slice(0, visibleCount)
return ( 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"> <div className="flex flex-col md:flex-row gap-12 items-start">
{/* Summary */} {/* 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> <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"> <div className="flex justify-center gap-1">
{[1, 2, 3, 4, 5].map(star => ( {[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> </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> </div>
{/* List & Form */} {/* List & Form */}
<div className="w-full md:w-2/3 space-y-8"> <div className="w-full md:w-2/3 space-y-8">
{customer ? ( {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> <h3 className="font-bold text-pop-black">Write a Review</h3>
<div className="flex gap-2"> <div className="flex gap-2">
{[1, 2, 3, 4, 5].map(star => ( {[1, 2, 3, 4, 5].map(star => (
<button type="button" key={star} onClick={() => setRating(star)} className="focus:outline-none"> <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> </button>
))} ))}
</div> </div>
@@ -109,21 +109,21 @@ export default function ProductReviews({ productId }: { productId: number }) {
value={comment} value={comment}
onChange={e => setComment(e.target.value)} onChange={e => setComment(e.target.value)}
placeholder="Share your experience with this scent..." 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} rows={3}
/> />
<button <button
type="submit" type="submit"
disabled={submitting} 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'} {submitting ? 'Submitting...' : 'Submit Review'}
</button> </button>
</form> </form>
) : ( ) : (
<div className="bg-spa-light p-6 rounded-3xl text-center border border-dashed border-spa-stone/30"> <div className="bg-pop-bg p-6 rounded-3xl text-center border border-dashed border-pop-black/30">
<p className="text-spa-stone text-sm mb-2">Login to write a review</p> <p className="text-pop-black/60 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> <button onClick={() => window.location.href='/track'} className="text-pop-pink font-bold text-sm tracking-widest uppercase hover:underline">Go to Login</button>
</div> </div>
)} )}
@@ -135,8 +135,8 @@ export default function ProductReviews({ productId }: { productId: number }) {
onClick={() => { setFilterRating(null); setVisibleCount(3); }} onClick={() => { setFilterRating(null); setVisibleCount(3); }}
className={`flex-shrink-0 px-4 py-1.5 rounded-full text-xs font-bold transition-all border ${ className={`flex-shrink-0 px-4 py-1.5 rounded-full text-xs font-bold transition-all border ${
filterRating === null filterRating === null
? 'bg-spa-stone text-spa-light border-spa-stone' ? 'bg-pop-black text-pop-bg border-pop-black'
: 'bg-spa-light text-spa-stone border-spa-stone/20 hover:border-spa-stone/50' : 'bg-pop-bg text-pop-black border-pop-black/20 hover:border-pop-black/50'
}`} }`}
> >
All ({reviews.length}) All ({reviews.length})
@@ -150,11 +150,11 @@ export default function ProductReviews({ productId }: { productId: number }) {
onClick={() => { setFilterRating(star); setVisibleCount(3); }} 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 ${ 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 filterRating === star
? 'bg-spa-stone text-spa-light border-spa-stone' ? 'bg-pop-black text-pop-bg border-pop-black'
: 'bg-spa-light text-spa-stone border-spa-stone/20 hover:border-spa-stone/50' : '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> </button>
); );
})} })}
@@ -162,19 +162,19 @@ export default function ProductReviews({ productId }: { productId: number }) {
)} )}
{loading ? ( {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 ? ( ) : 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 ? ( ) : 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 => ( 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"> <div className="flex items-center gap-3 mb-2">
{review.customer?.pictureUrl ? ( {review.customer?.pictureUrl ? (
<img src={review.customer.pictureUrl} className="w-10 h-10 rounded-full object-cover" /> <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()} {(review.customerName || 'A')[0].toUpperCase()}
</div> </div>
)} )}
@@ -182,13 +182,13 @@ export default function ProductReviews({ productId }: { productId: number }) {
<p className="font-bold text-pop-black text-sm">{review.customerName}</p> <p className="font-bold text-pop-black text-sm">{review.customerName}</p>
<div className="flex gap-0.5"> <div className="flex gap-0.5">
{[1, 2, 3, 4, 5].map(star => ( {[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>
</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> </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> </div>
)) ))
)} )}
@@ -197,7 +197,7 @@ export default function ProductReviews({ productId }: { productId: number }) {
<div className="text-center pt-4"> <div className="text-center pt-4">
<button <button
onClick={() => setVisibleCount(prev => prev + 5)} 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 Show More Reviews
</button> </button>
@@ -16,12 +16,6 @@ export default function ThemeInjector() {
// Map tokens from the backend (which use brandPrimary etc.) // Map tokens from the backend (which use brandPrimary etc.)
// to the CSS variables expected by SPA's tailwind.config.js // to the CSS variables expected by SPA's tailwind.config.js
const mapping: Record<string, string> = { 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-pink': tokens.brandPrimary,
'--color-pop-yellow': tokens.brandAccent, '--color-pop-yellow': tokens.brandAccent,
'--color-pop-orange': tokens.brandWarm, '--color-pop-orange': tokens.brandWarm,
@@ -19,17 +19,17 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
}; };
return ( 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"> <div className="ui-section-inner">
{title && ( {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} {title}
</h2> </h2>
)} )}
{payload.categories.map((category, catIndex) => ( {payload.categories.map((category, catIndex) => (
<div key={catIndex} className="mb-12 last:mb-0"> <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"> <h3 className="mb-6 flex items-center gap-3 font-serif text-2xl text-pop-black">
{category.icon && <span className="text-spa-sage">{category.icon}</span>} {category.icon && <span className="text-pop-pink">{category.icon}</span>}
{getLocalizedField(category.name, lang)} {getLocalizedField(category.name, lang)}
</h3> </h3>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -43,13 +43,15 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
> >
<button <button
type="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)} 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)} {getLocalizedField(item.q, lang)}
</span> </span>
<span className="flex-shrink-0 text-spa-sage"> <span className="flex-shrink-0 text-pop-pink">
{isOpen ? <ChevronUp size={20} /> : <ChevronDown size={20} />} {isOpen ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
</span> </span>
</button> </button>
@@ -58,7 +60,7 @@ export default function AccordionSection({ payload }: AccordionSectionProps) {
isOpen ? 'max-h-96 pb-5 opacity-100' : 'max-h-0 opacity-0' 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)} {getLocalizedField(item.a, lang)}
</p> </p>
</div> </div>
@@ -118,31 +118,31 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
}; };
return ( 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="max-w-3xl mx-auto">
<div className="text-center mb-12"> <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} {title || copy.title}
</h2> </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} {subtitle || copy.subtitle}
</p> </p>
</div> </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 ? ( {isSuccess ? (
<div className="text-center py-12"> <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} /> <Send size={32} />
</div> </div>
<h3 className="text-2xl font-serif text-spa-charcoal mb-2">{copy.successTitle}</h3> <h3 className="text-2xl font-serif text-pop-black mb-2">{copy.successTitle}</h3>
<p className="text-spa-charcoal/70">{copy.successBody}</p> <p className="text-pop-black/70">{copy.successBody}</p>
</div> </div>
) : ( ) : (
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2"> <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} {copy.name}
</label> </label>
<input <input
@@ -152,12 +152,12 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
required required
value={formData.name} value={formData.name}
onChange={handleChange} 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} placeholder={copy.namePlaceholder}
/> />
</div> </div>
<div className="space-y-2"> <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} {copy.email}
</label> </label>
<input <input
@@ -167,14 +167,14 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
required required
value={formData.email} value={formData.email}
onChange={handleChange} 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" placeholder="example@email.com"
/> />
</div> </div>
</div> </div>
<div className="space-y-2"> <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} {copy.message}
</label> </label>
<textarea <textarea
@@ -184,7 +184,7 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
value={formData.message} value={formData.message}
onChange={handleChange} onChange={handleChange}
rows={5} 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} placeholder={copy.messagePlaceholder}
/> />
</div> </div>
@@ -192,10 +192,10 @@ export default function ContactFormSection({ payload }: ContactFormSectionProps)
<button <button
type="submit" type="submit"
disabled={isSubmitting} 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 ? ( {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} /> <Send size={18} />
@@ -30,11 +30,11 @@ export default function ContactInfoSection({ payload }: ContactInfoSectionProps)
] as const; ] as const;
return ( 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"> <div className="ui-section-inner">
{title && ( {title && (
<div className="mb-10 text-center"> <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} {title}
</h2> </h2>
</div> </div>
@@ -44,17 +44,17 @@ export default function ContactInfoSection({ payload }: ContactInfoSectionProps)
if (!value) return null; if (!value) return null;
return ( return (
<div key={label} className="ui-shell-card flex flex-col items-center p-6 text-center"> <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-spa-sage/10 text-spa-sage"> <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} /> <Icon size={24} />
</div> </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 ? ( {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} {value}
</a> </a>
) : ( ) : (
<p className="text-spa-charcoal/70 leading-relaxed whitespace-pre-wrap"> <p className="text-pop-black/70 leading-relaxed whitespace-pre-wrap">
{value} {value}
</p> </p>
)} )}
+10 -10
View File
@@ -78,11 +78,11 @@ export function ThaiAddressInput({ value, onChange }: ThaiAddressInputProps) {
if (focusedField !== field || results.length === 0) return null; if (focusedField !== field || results.length === 0) return null;
return ( 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) => ( {results.map((item, idx) => (
<li <li
key={`${item.district}-${item.amphoe}-${item.province}-${item.zipcode}-${idx}`} 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) => { onMouseDown={(e) => {
// use onMouseDown to prevent onBlur from firing before onClick // use onMouseDown to prevent onBlur from firing before onClick
e.preventDefault(); e.preventDefault();
@@ -99,7 +99,7 @@ export function ThaiAddressInput({ value, onChange }: ThaiAddressInputProps) {
return ( return (
<div ref={containerRef} className="grid grid-cols-2 md:grid-cols-4 gap-4 w-full"> <div ref={containerRef} className="grid grid-cols-2 md:grid-cols-4 gap-4 w-full">
<div className="space-y-1.5 relative"> <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 <input
type="text" type="text"
required required
@@ -107,13 +107,13 @@ export function ThaiAddressInput({ value, onChange }: ThaiAddressInputProps) {
onChange={e => handleSearch(e.target.value, 'subdistrict')} onChange={e => handleSearch(e.target.value, 'subdistrict')}
onFocus={() => { setFocusedField('subdistrict'); handleSearch(value.subdistrict, 'subdistrict'); }} onFocus={() => { setFocusedField('subdistrict'); handleSearch(value.subdistrict, 'subdistrict'); }}
onBlur={() => setTimeout(() => { if (focusedField === 'subdistrict') setFocusedField(null); }, 200)} 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')} {renderDropdown('subdistrict')}
</div> </div>
<div className="space-y-1.5 relative"> <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 <input
type="text" type="text"
required required
@@ -121,13 +121,13 @@ export function ThaiAddressInput({ value, onChange }: ThaiAddressInputProps) {
onChange={e => handleSearch(e.target.value, 'district')} onChange={e => handleSearch(e.target.value, 'district')}
onFocus={() => { setFocusedField('district'); handleSearch(value.district, 'district'); }} onFocus={() => { setFocusedField('district'); handleSearch(value.district, 'district'); }}
onBlur={() => setTimeout(() => { if (focusedField === 'district') setFocusedField(null); }, 200)} 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')} {renderDropdown('district')}
</div> </div>
<div className="space-y-1.5 relative"> <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 <input
type="text" type="text"
required required
@@ -135,13 +135,13 @@ export function ThaiAddressInput({ value, onChange }: ThaiAddressInputProps) {
onChange={e => handleSearch(e.target.value, 'province')} onChange={e => handleSearch(e.target.value, 'province')}
onFocus={() => { setFocusedField('province'); handleSearch(value.province, 'province'); }} onFocus={() => { setFocusedField('province'); handleSearch(value.province, 'province'); }}
onBlur={() => setTimeout(() => { if (focusedField === 'province') setFocusedField(null); }, 200)} 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')} {renderDropdown('province')}
</div> </div>
<div className="space-y-1.5 relative"> <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 <input
type="text" type="text"
required required
@@ -149,7 +149,7 @@ export function ThaiAddressInput({ value, onChange }: ThaiAddressInputProps) {
onChange={e => handleSearch(e.target.value, 'zipcode')} onChange={e => handleSearch(e.target.value, 'zipcode')}
onFocus={() => { setFocusedField('zipcode'); handleSearch(value.zipcode, 'zipcode'); }} onFocus={() => { setFocusedField('zipcode'); handleSearch(value.zipcode, 'zipcode'); }}
onBlur={() => setTimeout(() => { if (focusedField === 'zipcode') setFocusedField(null); }, 200)} 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')} {renderDropdown('zipcode')}
</div> </div>
+60 -24
View File
@@ -99,11 +99,11 @@
} }
.ui-section-subtitle { .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 { .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 { .ui-shell-card-hover {
@@ -115,31 +115,40 @@
} }
.ui-button-primary { .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 { .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 { .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 { .ui-muted-text {
@apply text-spa-stone/70; @apply text-pop-black/70;
} }
.ui-hero-shell { .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 { .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 { .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 { .ui-hero-content {
@@ -147,11 +156,13 @@
} }
.ui-hero-title { .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 { .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 { .ui-banner-shell {
@@ -159,39 +170,39 @@
} }
.ui-banner-overlay { .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 { .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 { .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 { .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 { .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 { .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 { .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 { .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 { .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 { .brutal-box-hover {
@@ -199,11 +210,11 @@
} }
.brutal-button { .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 { .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 { ::-webkit-scrollbar-thumb {
@apply bg-spa-sage rounded-full; @apply bg-pop-black/30 rounded-full;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
@apply bg-spa-sage; @apply bg-pop-black/50;
} }
/* Smooth Scrolling */ /* Smooth Scrolling */
@@ -243,3 +254,28 @@ html {
:lang(th) .font-serif { :lang(th) .font-serif {
font-family: 'Prompt', sans-serif !important; 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]); }, [slug, i18n.language]);
return ( 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) => ( {announcementSections.map((section) => (
<DynamicPageRenderer <DynamicPageRenderer
key={section.id} key={section.id}
@@ -78,7 +78,7 @@ export default function CustomPage() {
{loading ? ( {loading ? (
<div className="min-h-[60vh] flex items-center justify-center"> <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> </div>
) : error ? ( ) : error ? (
<NotFoundPage /> <NotFoundPage />
+24 -28
View File
@@ -123,7 +123,13 @@
} }
.ui-button-line { .ui-button-line {
@apply inline-flex items-center justify-center rounded-full bg-line-primary px-4 py-2 text-xs font-bold uppercase tracking-widest text-surface-card shadow-sm transition-colors hover:bg-line-primary-hover; @apply inline-flex items-center justify-center rounded-full px-4 py-2 text-xs font-bold uppercase tracking-widest shadow-sm transition-colors;
background-color: #00B900;
color: #ffffff;
}
.ui-button-line:hover {
background-color: #009900;
} }
.ui-icon-button { .ui-icon-button {
@@ -214,14 +220,14 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden; overflow: hidden;
background: var(--color-text-main); background: var(--color-surface-page);
color: var(--color-surface-card); color: var(--color-text-main);
} }
.ui-hero-overlay { .ui-hero-overlay {
position: absolute; position: absolute;
inset: 0; inset: 0;
background: color-mix(in srgb, var(--color-text-main) 40%, transparent); background: color-mix(in srgb, var(--color-surface-page) 40%, transparent);
} }
.ui-hero-overlay-strong { .ui-hero-overlay-strong {
@@ -229,14 +235,14 @@
inset: 0; inset: 0;
background: linear-gradient( background: linear-gradient(
to top, to top,
color-mix(in srgb, var(--color-text-main) 90%, transparent), color-mix(in srgb, var(--color-surface-page) 90%, transparent),
color-mix(in srgb, var(--color-text-main) 10%, transparent), color-mix(in srgb, var(--color-surface-page) 10%, transparent),
color-mix(in srgb, var(--color-text-main) 30%, transparent) color-mix(in srgb, var(--color-surface-page) 30%, transparent)
); );
} }
.ui-hero-title { .ui-hero-title {
color: var(--color-surface-card); color: var(--color-text-main);
font-family: var(--font-display-locale); font-family: var(--font-display-locale);
font-size: 3rem; font-size: 3rem;
font-weight: 300; font-weight: 300;
@@ -250,7 +256,7 @@
max-width: 42rem; max-width: 42rem;
padding-top: 1rem; padding-top: 1rem;
padding-bottom: 2rem; padding-bottom: 2rem;
color: rgb(255 255 255 / 0.9); color: color-mix(in srgb, var(--color-text-main) 90%, transparent);
font-size: 1.125rem; font-size: 1.125rem;
font-weight: 300; font-weight: 300;
line-height: 1.75rem; line-height: 1.75rem;
@@ -258,28 +264,18 @@
} }
.ui-hero-button { .ui-hero-button {
border-radius: 9999px; @apply inline-flex items-center justify-center rounded-full bg-[#FDFDF9] px-6 py-3 text-xs font-bold uppercase tracking-widest text-[#1F3D2D] transition-colors hover:bg-[#EAEFEA];
background: var(--color-surface-card); box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
padding: 1rem 2rem;
color: var(--color-text-main);
font-size: 0.875rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
transition: background-color 300ms ease, box-shadow 300ms ease;
} }
.ui-hero-link { .ui-hero-link {
display: flex; @apply inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest transition-colors;
align-items: center; color: #D4AF37;
gap: 0.5rem; padding: 0.75rem 1.5rem;
padding: 1rem 2rem; }
color: var(--color-surface-card);
font-size: 0.875rem; .ui-hero-link:hover {
font-weight: 500; color: #FDFDF9;
letter-spacing: 0.1em;
text-transform: uppercase;
transition: color 150ms ease;
} }
.ui-banner-shell { .ui-banner-shell {
+79 -26
View File
@@ -1,11 +1,14 @@
"use client" "use client"
import { useState } from 'react'
import Image from 'next/image' import Image from 'next/image'
import Link from './LocalizedLink' import Link from './LocalizedLink'
import { ShoppingBag } from 'lucide-react'
import type { Product } from '@homaroma/types' import type { Product } from '@homaroma/types'
import { getCategoryImage, formatPrice } from '@homaroma/types' import { getCategoryImage, formatPrice } from '@homaroma/types'
import { resolveCategoryName, resolveLocalized } from '../lib/localization' import { resolveCategoryName, resolveLocalized } from '../lib/localization'
import { useLang } from '../hooks/useLang' import { useLang } from '../hooks/useLang'
import { useCartStore } from '../store/cartStore'
import WishlistButton from './WishlistButton' import WishlistButton from './WishlistButton'
interface ProductCardProps { interface ProductCardProps {
@@ -14,14 +17,27 @@ interface ProductCardProps {
export default function ProductCard({ product }: ProductCardProps) { export default function ProductCard({ product }: ProductCardProps) {
const lang = useLang() const lang = useLang()
const { addToCart } = useCartStore()
const [selectedVariant, setSelectedVariant] = useState(product.variants?.[0] || null)
const categoryName = resolveCategoryName(product.category, lang) const categoryName = resolveCategoryName(product.category, lang)
const productName = resolveLocalized(product.name, lang) || product.scent.name const productName = resolveLocalized(product.name, lang) || product.scent.name
const imageUrl = product.images?.[0]?.url || product.category.imageUrl || getCategoryImage(categoryName) const imageUrl = product.images?.[0]?.url || product.category.imageUrl || getCategoryImage(categoryName)
const firstPrice = product.variants[0]?.price ?? product.price const displayPrice = selectedVariant?.price ?? product.variants[0]?.price ?? product.price
const addToCartLabels: Record<string, string> = {
th: 'เพิ่มลงตะกร้า',
en: 'Add to Cart',
ja: 'カートに追加',
zh: '加入购物车',
vi: 'Thêm vào giỏ',
ko: '장바구니에 담기'
}
const addToCartText = addToCartLabels[lang] || addToCartLabels.en
return ( return (
<article className="ui-card ui-card-hover group flex h-full flex-col overflow-hidden"> <div className="ui-card ui-card-hover group flex h-full cursor-pointer flex-col overflow-hidden p-2">
<div className="relative aspect-[4/3] overflow-hidden bg-surface-page"> <div className="relative aspect-[4/3] overflow-hidden rounded-xl bg-surface-page md:h-64 md:aspect-auto">
<WishlistButton product={product} /> <WishlistButton product={product} />
<Link href={`/products/${product.id}`} className="block h-full w-full"> <Link href={`/products/${product.id}`} className="block h-full w-full">
<Image <Image
@@ -32,47 +48,84 @@ export default function ProductCard({ product }: ProductCardProps) {
className="h-full w-full object-cover transition duration-700 group-hover:scale-105" className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
/> />
{categoryName && ( {categoryName && (
<span className="ui-status-badge absolute left-4 top-4 border-line-primary/20 bg-surface-card/90 px-3 py-1 text-[10px] uppercase tracking-widest text-text-main shadow-sm"> <span className="ui-chip absolute left-3 top-3 border-line-primary/20 bg-surface-card/90 px-2.5 py-1 text-[10px] text-text-main shadow-sm backdrop-blur-sm">
{categoryName} {categoryName}
</span> </span>
)} )}
</Link> </Link>
</div> </div>
<div className="flex flex-1 flex-col gap-4 p-5"> <div className="flex flex-1 flex-col p-4">
<div className="space-y-1"> <div className="mb-2">
<h3 className="line-clamp-2 text-lg font-semibold leading-snug text-text-main"> <h3 className="line-clamp-1 text-base font-bold leading-tight text-text-main transition-colors group-hover:text-brand-primary md:text-lg">
<Link href={`/products/${product.id}`} className="transition hover:text-brand-primary"> <Link href={`/products/${product.id}`}>
{productName} {productName}
</Link> </Link>
</h3> </h3>
{product.scent.name && ( {categoryName && (
<p className="text-xs font-semibold uppercase tracking-widest text-muted-text/70"> <p className="mt-1 line-clamp-1 text-[10px] font-semibold uppercase tracking-widest text-muted-text md:text-xs">
{product.scent.name} {categoryName}
</p> </p>
)} )}
</div> </div>
{product.variants.length > 0 && ( {product.variants.length > 0 && (
<div className="flex flex-wrap gap-2"> <div className="mb-1 mt-2 flex flex-wrap gap-1.5">
{product.variants.slice(0, 3).map((variant) => ( {product.variants.map((variant) => {
<span key={variant.id} className="rounded-full bg-surface-page px-3 py-1 text-xs font-semibold text-muted-text"> const isSelected = selectedVariant?.id === variant.id
{variant.size} return (
</span> <button
))} key={variant.id}
type="button"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
setSelectedVariant(variant)
}}
className={`rounded-full border px-3 py-1 text-[10px] font-bold uppercase tracking-wider transition-all cursor-pointer ${
isSelected
? 'border-brand-primary bg-brand-primary text-surface-card shadow-sm'
: 'border-line-primary/20 bg-surface-card text-text-main hover:border-brand-primary/50'
}`}
>
{variant.size}
</button>
)
})}
</div> </div>
)} )}
<div className="mt-auto flex items-center justify-between gap-4"> <div className="mt-auto flex flex-col gap-2 pt-3">
<span className="text-lg font-bold text-brand-primary">{formatPrice(firstPrice)}</span> <div className="flex items-center justify-between">
<Link <span className="text-sm font-black text-brand-primary md:text-lg">
href={`/products/${product.id}`} {formatPrice(displayPrice)}
className="ui-button-primary px-4 py-2 text-[10px]" </span>
> </div>
View
</Link> {selectedVariant && (
<button
type="button"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
addToCart({
productId: product.id,
variantId: selectedVariant.id,
quantity: 1,
price: selectedVariant.price || 0,
productName: productName,
variantSize: selectedVariant.size,
productImage: imageUrl
})
}}
className="ui-button-primary w-full gap-1.5 rounded-xl py-2.5 shadow-sm"
>
<ShoppingBag className="h-3.5 w-3.5" />
{addToCartText}
</button>
)}
</div> </div>
</div> </div>
</article> </div>
) )
} }
@@ -2,7 +2,8 @@
import Image from 'next/image' import Image from 'next/image'
import Link from '../LocalizedLink' import Link from '../LocalizedLink'
import { Clock, Mail, MapPin, MessageCircle } from 'lucide-react' import { Clock, Mail, MapPin, MessageCircle, Send } from 'lucide-react'
import { useState } from 'react'
import type { Category, PageSection, Product } from '@homaroma/types' import type { Category, PageSection, Product } from '@homaroma/types'
import { getCategoryImage } from '@homaroma/types' import { getCategoryImage } from '@homaroma/types'
import ProductCard from '../ProductCard' import ProductCard from '../ProductCard'
@@ -125,12 +126,12 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
<div className="relative z-10 mx-auto mt-16 w-full max-w-4xl space-y-6 px-8 text-center md:mt-0"> <div className="relative z-10 mx-auto mt-16 w-full max-w-4xl space-y-6 px-8 text-center md:mt-0">
<div className="space-y-6"> <div className="space-y-6">
{tagline && ( {tagline && (
<p className="text-xs font-semibold uppercase tracking-[0.3em] text-surface-card drop-shadow-md md:text-sm"> <p className="text-xs font-semibold uppercase tracking-[0.3em] text-[#EAEFEA] drop-shadow-md md:text-sm">
{tagline} {tagline}
</p> </p>
)} )}
<h1 className="ui-hero-title"> <h1 className="ui-hero-title">
{renderAccentTitle(title || '')} {renderAccentTitle(title || '', 'text-[#D4AF37] font-medium italic')}
</h1> </h1>
{description && ( {description && (
<p className="ui-hero-copy"> <p className="ui-hero-copy">
@@ -146,7 +147,7 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
</Link> </Link>
<a <a
href="#categories" href="#categories"
className="ui-hero-link" className="ui-hero-link group"
> >
{heroLearnMoreText(resolvedLang)} {heroLearnMoreText(resolvedLang)}
<span className="transition-transform group-hover:translate-x-1">→</span> <span className="transition-transform group-hover:translate-x-1">→</span>
@@ -208,11 +209,11 @@ function CategoriesSection({
unoptimized unoptimized
className="absolute inset-0 h-full w-full object-cover transition duration-700 group-hover:scale-105" className="absolute inset-0 h-full w-full object-cover transition duration-700 group-hover:scale-105"
/> />
<div className="absolute inset-0 bg-gradient-to-t from-text-main/65 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent z-10 opacity-80 group-hover:opacity-100 transition-opacity" />
<div className="absolute bottom-5 left-5 right-5 text-surface-card"> <div className="absolute bottom-5 left-5 right-5 z-20 text-white">
<span className="block text-lg font-semibold">{name}</span> <span className="block text-lg font-semibold">{name}</span>
{category._count && ( {category._count && (
<span className="text-sm text-surface-card"> <span className="text-sm text-white/90">
{category._count.products} {productsLabel[resolvedLang] ?? productsLabel.en} {category._count.products} {productsLabel[resolvedLang] ?? productsLabel.en}
</span> </span>
)} )}
@@ -665,6 +666,207 @@ function ContactInfoSection({ payload, lang }: { payload: Record<string, unknown
) )
} }
function ContactFormSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
const [formData, setFormData] = useState({ name: '', email: '', message: '' })
const [isSubmitting, setIsSubmitting] = useState(false)
const [isSuccess, setIsSuccess] = useState(false)
const title = localizedFromRecord(payload, 'title', resolvedLang)
const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang)
const labels: Record<string, Record<string, string>> = {
th: {
title: 'ติดต่อเรา',
subtitle: 'ส่งข้อความถึงทีมงานของเรา เรายินดีให้บริการและพร้อมตอบทุกข้อสงสัย',
successTitle: 'ส่งข้อความสำเร็จ',
successBody: 'ขอบคุณที่ติดต่อเรา ทีมงานจะรีบตอบกลับโดยเร็วที่สุด',
name: 'ชื่อ-นามสกุล',
namePlaceholder: 'กรอกชื่อของคุณ',
email: 'อีเมล',
message: 'ข้อความ',
messagePlaceholder: 'พิมพ์ข้อความของคุณที่นี่...',
submit: 'ส่งข้อความ',
},
en: {
title: 'Contact Us',
subtitle: 'Send a message to our team and we will get back to you soon.',
successTitle: 'Message sent successfully',
successBody: 'Thanks for reaching out. Our team will reply as soon as possible.',
name: 'Full Name',
namePlaceholder: 'Enter your name',
email: 'Email',
message: 'Message',
messagePlaceholder: 'Type your message here...',
submit: 'Send Message',
},
ja: {
title: 'お問い合わせ',
subtitle: 'チームへメッセージをお送りください。できるだけ早くご返信します。',
successTitle: '送信に成功しました',
successBody: 'お問い合わせありがとうございます。担当者ができるだけ早く返信します。',
name: '氏名',
namePlaceholder: 'お名前を入力してください',
email: 'メール',
message: 'メッセージ',
messagePlaceholder: 'ここにメッセージを入力してください...',
submit: '送信',
},
zh: {
title: '联系我们',
subtitle: '给我们的团队留言,我们会尽快回复您。',
successTitle: '发送成功',
successBody: '感谢您的联系,我们会尽快回复。',
name: '姓名',
namePlaceholder: '请输入您的姓名',
email: '邮箱',
message: '留言',
messagePlaceholder: '在这里输入您的留言...',
submit: '发送消息',
},
vi: {
title: 'Liên hệ',
subtitle: 'Gửi tin nhắn cho đội ngũ của chúng tôi, chúng tôi sẽ phản hồi sớm nhất.',
successTitle: 'Gửi tin nhắn thành công',
successBody: 'Cảm ơn bạn đã liên hệ, đội ngũ của chúng tôi sẽ phản hồi sớm nhất có thể.',
name: 'Họ và tên',
namePlaceholder: 'Nhập tên của bạn',
email: 'Email',
message: 'Tin nhắn',
messagePlaceholder: 'Nhập tin nhắn của bạn tại đây...',
submit: 'Gửi tin nhắn',
},
ko: {
title: '문의하기',
subtitle: '팀에 메시지를 보내주시면 최대한 빨리 답변드리겠습니다.',
successTitle: '메시지 전송 완료',
successBody: '문의해 주셔서 감사합니다. 가능한 빨리 답변드리겠습니다.',
name: '이름',
namePlaceholder: '이름을 입력하세요',
email: '이메일',
message: '메시지',
messagePlaceholder: '여기에 메시지를 입력하세요...',
submit: '메시지 보내기',
}
}
const copy = labels[resolvedLang] ?? labels.en
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
// Simulate API call
setTimeout(() => {
setIsSubmitting(false)
setIsSuccess(true)
setFormData({ name: '', email: '', message: '' })
// Reset success message after 3 seconds
setTimeout(() => setIsSuccess(false), 3000)
}, 1000)
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
return (
<section className="ui-section-band bg-surface-page px-4 py-20 md:px-8">
<div className="mx-auto max-w-3xl">
<div className="mb-12 text-center">
<h2 className="mb-4 text-3xl font-semibold text-text-main md:text-4xl">
{title || copy.title}
</h2>
<p className="mx-auto max-w-xl text-lg text-muted-text">
{subtitle || copy.subtitle}
</p>
</div>
<div className="rounded-3xl border border-line-primary/20 bg-surface-card p-8 shadow-card md:p-12">
{isSuccess ? (
<div className="py-12 text-center">
<div className="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-brand-success/20 text-brand-success">
<Send size={32} />
</div>
<h3 className="mb-2 text-2xl font-semibold text-text-main">{copy.successTitle}</h3>
<p className="text-muted-text">{copy.successBody}</p>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<div className="space-y-2">
<label htmlFor="name" className="block text-sm font-medium text-text-main">
{copy.name}
</label>
<input
type="text"
id="name"
name="name"
required
value={formData.name}
onChange={handleChange}
className="w-full rounded-xl border border-line-primary/30 bg-surface-page px-4 py-3 text-text-main outline-none transition-all focus:border-brand-primary focus:ring-2 focus:ring-brand-primary/20"
placeholder={copy.namePlaceholder}
/>
</div>
<div className="space-y-2">
<label htmlFor="email" className="block text-sm font-medium text-text-main">
{copy.email}
</label>
<input
type="email"
id="email"
name="email"
required
value={formData.email}
onChange={handleChange}
className="w-full rounded-xl border border-line-primary/30 bg-surface-page px-4 py-3 text-text-main outline-none transition-all focus:border-brand-primary focus:ring-2 focus:ring-brand-primary/20"
placeholder="example@email.com"
/>
</div>
</div>
<div className="space-y-2">
<label htmlFor="message" className="block text-sm font-medium text-text-main">
{copy.message}
</label>
<textarea
id="message"
name="message"
required
value={formData.message}
onChange={handleChange}
rows={5}
className="w-full resize-none rounded-xl border border-line-primary/30 bg-surface-page px-4 py-3 text-text-main outline-none transition-all focus:border-brand-primary focus:ring-2 focus:ring-brand-primary/20"
placeholder={copy.messagePlaceholder}
/>
</div>
<button
type="submit"
disabled={isSubmitting}
className="flex w-full items-center justify-center gap-2 rounded-xl bg-brand-primary px-6 py-4 font-semibold text-white transition-colors hover:bg-brand-primary-hover disabled:cursor-not-allowed disabled:opacity-70"
>
{isSubmitting ? (
<span className="h-6 w-6 animate-spin rounded-full border-2 border-white/30 border-t-white" />
) : (
<>
<Send size={18} />
{copy.submit}
</>
)}
</button>
</form>
)}
</div>
</div>
</section>
)
}
export default function DynamicPageRenderer({ sections, categories, lang: langProp }: DynamicPageRendererProps) { export default function DynamicPageRenderer({ sections, categories, lang: langProp }: DynamicPageRendererProps) {
const i18nLang = useLang() const i18nLang = useLang()
// Use i18n.language reactively — switches immediately when user changes language // Use i18n.language reactively — switches immediately when user changes language
@@ -709,6 +911,8 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
return <AccordionSection key={section.id} payload={payload} lang={lang} /> return <AccordionSection key={section.id} payload={payload} lang={lang} />
case 'CONTACT_INFO': case 'CONTACT_INFO':
return <ContactInfoSection key={section.id} payload={payload} lang={lang} /> return <ContactInfoSection key={section.id} payload={payload} lang={lang} />
case 'CONTACT_FORM':
return <ContactFormSection key={section.id} payload={payload} lang={lang} />
default: default:
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
console.warn(`[DynamicPageRenderer] Unsupported section type: ${section.type}`) console.warn(`[DynamicPageRenderer] Unsupported section type: ${section.type}`)
+2 -5
View File
@@ -12,10 +12,7 @@ export function splitCmsLayoutSections(sections: PageSection[]) {
return { return {
announcementSections: activeSections.filter((section) => announcementTypes.has(sectionType(section))), announcementSections: activeSections.filter((section) => announcementTypes.has(sectionType(section))),
heroSections: activeSections.filter((section) => heroTypes.has(sectionType(section))), heroSections: [], // Keep for backwards compatibility with page components
bottomSections: activeSections.filter((section) => { bottomSections: activeSections.filter((section) => !announcementTypes.has(sectionType(section))),
const type = sectionType(section)
return !announcementTypes.has(type) && !heroTypes.has(type)
}),
} }
} }