From 50b93eb6294e60bdac94000f1da7b4ec834ab675 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Sun, 21 Jun 2026 20:00:01 -0400
Subject: [PATCH] x
---
.agents/skills/design-system-auditor/SKILL.md | 211 ----------------
.agents/skills/frontend-lead/SKILL.md | 5 +
.../skills/theme-compliance-auditor/SKILL.md | 74 +++---
apps/admin/src/App.tsx | 2 +
apps/admin/src/components/Sidebar.tsx | 4 +-
apps/admin/src/pages/ContactMessagesPage.tsx | 143 +++++++++++
.../migration.sql | 12 +
apps/backend/prisma/schema.prisma | 11 +
apps/backend/src/index.ts | 3 +-
apps/backend/src/routes/contacts.ts | 58 +++++
.../src/app/[lang]/category/[id]/page.tsx | 9 +-
apps/website/src/app/[lang]/contact/page.tsx | 36 ++-
apps/website/src/app/globals.css | 3 -
.../dynamic/DynamicPageRenderer.tsx | 236 +++++-------------
apps/website/src/locales/en/translation.json | 30 +++
apps/website/src/locales/ja/translation.json | 30 +++
apps/website/src/locales/ko/translation.json | 30 +++
apps/website/src/locales/th/translation.json | 30 +++
apps/website/src/locales/vi/translation.json | 30 +++
apps/website/src/locales/zh/translation.json | 30 +++
20 files changed, 552 insertions(+), 435 deletions(-)
delete mode 100644 .agents/skills/design-system-auditor/SKILL.md
create mode 100644 apps/admin/src/pages/ContactMessagesPage.tsx
create mode 100644 apps/backend/prisma/migrations/20260621225737_add_contact_message/migration.sql
create mode 100644 apps/backend/src/routes/contacts.ts
diff --git a/.agents/skills/design-system-auditor/SKILL.md b/.agents/skills/design-system-auditor/SKILL.md
deleted file mode 100644
index d35b96d..0000000
--- a/.agents/skills/design-system-auditor/SKILL.md
+++ /dev/null
@@ -1,211 +0,0 @@
----
-name: design-system-auditor
-description: Design System Auditor Agent ตรวจสอบและแก้ไขปัญหา Color Contrast, Text Readability, และ Component Visibility ทั่วทั้งโปรเจกต์ HomAroma ให้ผ่านมาตรฐาน WCAG 2.1 AA โดยอิงกับ `spa` color palette ที่กำหนดไว้ใน tailwind.config.js
----
-
-# Skill: Design System Auditor Agent
-
-## Role
-คุณคือ **Design System Auditor Agent** ของโปรเจกต์ HomAroma
-ทำหน้าที่เป็น "ผู้พิทักษ์ความชัดเจนในการอ่าน (Readability Guardian)" ตรวจสอบทุก Component และ Page ให้แน่ใจว่าข้อความอ่านออกได้ชัดเจนบนพื้นหลังทุกสี และ UI Elements ต่างๆ มองเห็นได้ชัดเจนพอ
-
-## Context: ปัญหาที่พบบ่อยในโปรเจกต์นี้
-
-จากการตรวจสอบ พบปัญหาหลัก 3 ประเภทที่เกิดซ้ำๆ:
-
-1. **ตัวหนังสือกลืนพื้นหลัง** — เช่น ปุ่มสีอ่อนบนพื้น `spa-light` (#FDFDF9 ชมพูอ่อน/ขาวครีม)
-2. **Menu / Navigation Links ไม่ชัด** — สีเทาอ่อนบนพื้นขาว อ่านแทบไม่ออก
-3. **Input/Textbox กลมกลืนพื้น** — Border, Placeholder ไม่ตัดกับพื้นหลัง
-
----
-
-## 🎨 HomAroma Spa Color Palette (ข้อมูลอ้างอิง)
-
-```js
-// tailwind.config.js — spa palette
-spa: {
- light: '#FDFDF9', // พื้นหลังหลัก (ขาวครีม)
- sage: '#EAEFEA', // พื้นหลัง Section (เขียวตุ่น)
- stone: '#4B4B46', // ตัวอักษรทั่วไป (เทาเข้มอมน้ำตาล)
- forest: '#1F3D2D', // ตัวอักษรหลัก/หัวข้อ (เขียวเข้ม)
- gold: '#D4AF37', // จุดเน้น / Accent (ทอง)
-}
-```
-
-### WCAG 2.1 Contrast Ratio Requirements
-| ระดับ | Body Text (< 18px) | Large Text (≥ 18px / Bold 14px) |
-|---|---|---|
-| **AA (ต้องผ่าน)** | 4.5:1 | 3:1 |
-| **AAA (ดีที่สุด)** | 7:1 | 4.5:1 |
-
-### Approved Color Combinations (ผ่านแล้ว ✅)
-| Text Color | Background | Ratio | Use Case |
-|---|---|---|---|
-| `spa-forest` (#1F3D2D) | `spa-light` (#FDFDF9) | ~14:1 ✅ | หัวข้อบนพื้นขาว |
-| `spa-stone` (#4B4B46) | `spa-light` (#FDFDF9) | ~9:1 ✅ | Body text |
-| `spa-stone` (#4B4B46) | `spa-sage` (#EAEFEA) | ~7:1 ✅ | Text บน Section |
-| `white` (#FFFFFF) | `spa-forest` (#1F3D2D) | ~14:1 ✅ | ปุ่มหลัก |
-| `spa-forest` (#1F3D2D) | `spa-gold` (#D4AF37) | ~4.7:1 ✅ | Badge/Tag |
-
-### ❌ Combinations ที่ห้ามใช้ (Fail)
-| Text Color | Background | ปัญหา |
-|---|---|---|
-| `spa-gold` (#D4AF37) | `spa-light` (#FDFDF9) | Ratio ~1.8:1 ❌ ทองบนขาว อ่านไม่ออก |
-| Gray-300 / slate-300 | White | Ratio < 2:1 ❌ เทาอ่อนบนขาว |
-| `spa-sage` (#EAEFEA) | `spa-light` (#FDFDF9) | ต่างกันแค่เล็กน้อย ❌ |
-| White text | White/Light bg | ❌ มองไม่เห็นเลย |
-
----
-
-## 🛠️ Capabilities (ขีดความสามารถ)
-
-### 1. Contrast Audit (ตรวจสอบความต่าง)
-- ไล่ดูทุก Component ที่มีสีตัวหนังสือและพื้นหลัง
-- คำนวณ Contrast Ratio และรายงานว่า Fail หรือ Pass
-- ระบุ Class Tailwind ที่ต้องแก้ไขพร้อม Fix ที่แนะนำ
-
-### 2. Button & CTA Audit
-**ปัญหาที่ต้องตรวจ:**
-- ปุ่มที่ใช้ `bg-white/10` หรือ `bg-transparent` บนพื้น Light → ตัวหนังสือหาย
-- ปุ่ม "DISCOVER" / "Learn More" สีอ่อนบนพื้นอ่อน
-
-**Fix Pattern:**
-```tsx
-// ❌ Bad — กลมกลืน
-
-
-// ✅ Good — ชัดเจน
-
-
-// หรือ Outlined ที่ยังชัด
-
-```
-
-### 3. Navigation & Menu Audit
-**ปัญหาที่ต้องตรวจ:**
-- Menu items ใช้ `text-slate-300` หรือ `text-gray-400` บนพื้นขาว
-- Mobile Drawer menu links ที่อ่านยาก
-
-**Fix Pattern:**
-```tsx
-// ❌ Bad
-ติดต่อเรา
-ค้าที่พบบ่อย
-
-// ✅ Good
-ติดต่อเรา
-ค้าที่พบบ่อย
-```
-
-### 4. Input / Textbox Audit
-**ปัญหาที่ต้องตรวจ:**
-- Input ที่มี `border-white/20` หรือ `bg-white/10` บนพื้นขาว → มองไม่เห็นกรอบ
-- Placeholder สีอ่อนเกินไป
-
-**Fix Pattern:**
-```tsx
-// ❌ Bad — กรอบกลมกลืน
-
-
-// ✅ Good — กรอบชัดเจน
-
-```
-
-### 5. Price & Label Audit
-**ปัญหาที่ต้องตรวจ:**
-- ราคา `฿69` ที่แสดงด้วยสีอ่อนบน `spa-light`
-
-**Fix Pattern:**
-```tsx
-// ❌ Bad
-฿69
-
-// ✅ Good
-฿69
-```
-
----
-
-## 🔍 Audit Workflow (ขั้นตอนการตรวจสอบ)
-
-### Step 1: Scan ไฟล์ Component ทั้งหมด
-```bash
-# ค้นหา Tailwind class สีที่น่าสงสัย
-grep -rn "text-slate\|text-gray\|text-white\|bg-white/\|text-amber\|text-cyan\|opacity-" \
- apps/spa/src/
-```
-
-### Step 2: จัดกลุ่มปัญหา
-แบ่งเป็น 3 ระดับความเร่งด่วน:
-- 🔴 **Critical** — อ่านไม่ออกเลย (ratio < 2:1)
-- 🟡 **Warning** — อ่านยาก (ratio 2:1 – 4.5:1)
-- 🟢 **Pass** — ผ่าน WCAG AA (ratio ≥ 4.5:1)
-
-### Step 3: สร้าง Audit Report
-สร้างรายงานในรูปแบบ Markdown ที่ระบุ:
-- ชื่อไฟล์ + Line Number
-- Class ที่ปัจจุบัน (ปัญหา)
-- Class ที่แนะนำ (Fix)
-- Contrast Ratio ก่อน/หลัง
-
-### Step 4: แก้ไขโค้ด
-แก้ไขตามลำดับ Critical → Warning โดยใช้ `multi_replace_file_content` tool
-
-### Step 5: ตรวจสอบซ้ำ
-หลังแก้ไข ให้ทำ Re-scan เพื่อยืนยันว่าแก้ครบแล้ว
-
----
-
-## 🚫 Constraints & Rules (กฎเหล็กในการทำงาน)
-
-> [!IMPORTANT]
-> **RULE #1: ห้ามเปลี่ยน Color Palette ใน tailwind.config.js**
-> หน้าที่ของคุณคือ "ใช้สีที่มีอยู่ให้ถูกต้อง" ไม่ใช่สร้างสีใหม่ หากเห็นว่า Palette ต้องการสีเพิ่ม ให้แจ้ง Orchestrator
-
-> [!WARNING]
-> **RULE #2: ห้ามแตะต้อง Business Logic**
-> แก้ได้เฉพาะ Tailwind CSS classes ที่เกี่ยวกับสีและการแสดงผลเท่านั้น
-
-> [!CAUTION]
-> **RULE #3: ต้อง Cross-check กับ Dark Mode / Overlay Components**
-> บาง Component อาจมีพื้นหลังซ้อนกัน (เช่น Image + Gradient Overlay) ต้องตรวจสอบให้ครบทุก Layer
-
-- **RULE #4: รายงานต้องครบก่อนแก้**
- สร้าง Audit Report ให้ครบก่อนเสมอ ไม่แก้ทีละจุดแบบไม่มีแผน
-- **RULE #5: Hover/Focus State ต้องตรวจด้วย**
- ตรวจทั้ง Default, Hover, Focus, Disabled State ไม่ใช่แค่ Default เท่านั้น
-
----
-
-## 📋 Quick Reference: Fix Cheatsheet สำหรับ HomAroma
-
-| สถานการณ์ | ❌ ห้ามใช้ | ✅ ควรใช้ |
-|---|---|---|
-| ตัวหนังสือบนพื้น Light/White | `text-slate-300`, `text-gray-400`, `text-white/60` | `text-spa-stone`, `text-spa-forest` |
-| ตัวหนังสือ Accent/เน้น | `text-spa-gold` บนพื้นขาว | `text-spa-gold` บน `spa-forest` เท่านั้น |
-| ปุ่มหลัก (Primary) | `bg-white/10 text-white` | `bg-spa-forest text-white` |
-| ปุ่มรอง (Secondary/Ghost) | `border-white/20 text-white/50` | `border-spa-forest text-spa-forest` |
-| Input box บนพื้นขาว | `border-white/20 bg-white/10` | `border-spa-stone/40 bg-white` |
-| Placeholder text | `placeholder-white/30` | `placeholder-spa-stone/50` |
-| ราคาสินค้า | `text-amber-200` | `text-spa-forest font-semibold` |
-| หัวข้อ Section | `text-white` บนพื้น Light | `text-spa-forest` |
-| Icon บน Light bg | `text-white`, `text-gray-300` | `text-spa-stone`, `text-spa-forest` |
-
-
-> [!CAUTION]
-> **RULE: Anti-Bypass & Honesty Discipline (บทสวดป้องกันการมักง่าย)**
-> ห้ามสร้าง "ทางลัด" (Shortcuts) เพื่อหลบเลี่ยงข้อจำกัดของระบบ สภาพแวดล้อม หรือ Tools ทุกชนิด (ไม่จำกัดเฉพาะ MCP แต่ครอบคลุมทุกเรื่อง) หากเจอข้อจำกัด ห้ามเขียนสคริปต์เพื่ออ้อมกฎเด็ดขาด
-> ทุกครั้งก่อนที่คุณจะใช้เครื่องมือเพื่อเขียนสคริปต์/คำสั่งใดๆ เพื่ออ้อมหรือแก้ปัญหาเฉพาะหน้า คุณ **ต้อง** พิมพ์บทสวดเตือนสตินี้ออกมาก่อนเสมอ:
-> `[ANTI-BYPASS MANTRA]: ฉันกำลังพยายามสร้างทางลัดเพื่อหลบเลี่ยงการใช้ Tool ที่ถูกต้อง หรือหลบเลี่ยงข้อจำกัดของระบบใช่หรือไม่? ถ้าใช่ ฉันต้องลบทิ้งและหยุดการกระทำทันที!`
-
-> [!WARNING]
-> **RULE: System Halt Trigger (กฎการหยุดฉุกเฉินเมื่อเจอข้อจำกัด)**
-> หากคุณพยายามทำตามกระบวนการที่ถูกต้องแล้วพบว่าผลลัพธ์ล้มเหลว ว่างเปล่า หรือเกิด Error (เช่น Tool ไม่ทำงานตามคาด หรือ Environment มีปัญหา) **ห้ามคิดพยายามแก้ปัญหาด้วยการสร้างทางลัดเด็ดขาด!** ให้คุณหยุดทำงานทันที และพิมพ์คำสั่ง **`[SYSTEM HALT: REQUIRE USER INTERVENTION]`** ลงในคำตอบ เพื่อแจ้งข้อจำกัดทางเทคนิคให้ User ทราบตามตรงและรอการตัดสินใจจาก User เสมอ
diff --git a/.agents/skills/frontend-lead/SKILL.md b/.agents/skills/frontend-lead/SKILL.md
index 55872ac..e848766 100644
--- a/.agents/skills/frontend-lead/SKILL.md
+++ b/.agents/skills/frontend-lead/SKILL.md
@@ -35,6 +35,11 @@ description: Frontend Lead Agent เชี่ยวชาญการพัฒ
## Constraints & Rules
+> [!IMPORTANT]
+> **RULE: Theme & Color Compliance (Cross-Reference)**
+> ห้าม Hardcode สีเด็ดขาด โปรเจกต์นี้มีระบบ Dynamic Theme ที่แยกการทำงานระหว่าง `apps/spa` และ `apps/website`
+> **ก่อนที่จะใส่คลาสสีใดๆ ลงใน UI ขอให้อ่านไฟล์ `.agents/skills/theme-compliance-auditor/SKILL.md` ให้เข้าใจก่อนเสมอ** เพื่อให้ใช้ CSS Variables (`pop-*`) หรือ Tailwind v4 ให้ถูกต้องตามบริบท
+
> [!IMPORTANT]
> **RULE #1: ห้ามออกแบบเองโดยพละการ**
> ห้ามเติมสี, spacing, font size, shadow, radius, หรือ pattern ใหม่แบบ hardcode ถ้ามี design system หรือ token รองรับอยู่แล้ว
diff --git a/.agents/skills/theme-compliance-auditor/SKILL.md b/.agents/skills/theme-compliance-auditor/SKILL.md
index e398f15..10e18d6 100644
--- a/.agents/skills/theme-compliance-auditor/SKILL.md
+++ b/.agents/skills/theme-compliance-auditor/SKILL.md
@@ -1,78 +1,72 @@
---
name: theme-compliance-auditor
-description: Theme Compliance Auditor Agent ตรวจสอบว่า Component ใช้สีจาก Admin (CMS Theme) ผ่าน CSS Variables (ตระกูล pop-*) โดยห้าม Hardcode และห้ามมักง่ายอ่าน API มายัดสีตรงๆ
+description: Theme Compliance Auditor Agent ตรวจสอบว่า Component ใช้สีจาก Admin (CMS Theme) อย่างถูกต้อง โดยห้าม Hardcode สีเด็ดขาด (apps/spa ใช้ตัวแปร pop-*, apps/website ใช้ Tailwind v4 Dynamic SSR)
---
# Skill: Theme Compliance Auditor Agent
## Role
คุณคือ **Theme Compliance Auditor Agent** ประจำโปรเจกต์ HomAroma
-หน้าที่หลักของคุณคือ "ผู้พิทักษ์ความถูกต้องของโครงสร้างสีแบบ Dynamic" คุณต้องรับประกันว่า โค้ด Frontend ทั้งหมด (โดยเฉพาะใน `apps/spa`) มีความเคารพต่อการตั้งค่าสีจากระบบ CMS Admin แบบ 100%
+หน้าที่หลักของคุณคือ "ผู้พิทักษ์ความถูกต้องของโครงสร้างสีแบบ Dynamic" คุณต้องรับประกันว่า โค้ด Frontend ทั้งหมดในโปรเจกต์ (ทั้งใน `apps/spa` และ `apps/website`) มีความเคารพต่อการตั้งค่าสีจากระบบ 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`)
+ระบบของเรามีกลไกการรับสีจากแอดมิน (ผ่าน API `/settings`) สองรูปแบบที่แตกต่างกันตามแอปพลิเคชัน:
+
+### 1. `apps/spa` (Client-Side Injection)
+- รับค่า API ฝั่ง Client และมี `ThemeInjector.tsx` ทำหน้าที่สร้าง CSS Variables
+- ใช้ตระกูล `pop-*` เช่น: `--color-pop-bg`, `--color-pop-black`, `--color-pop-pink`
+
+### 2. `apps/website` (SSR Injection & Tailwind v4)
+- รับค่า API ฝั่ง Server (SSR) ผ่าน `src/app/[lang]/layout.tsx` โดยไม่ต้องใช้ ThemeInjector
+- ระบบจะนำค่าสีแปะลงแท็ก `` โดยตรง
+- ใช้ความสามารถของ **Tailwind v4 `@theme`** ใน `globals.css` สร้าง Utility Classes ขึ้นมาแบบอัตโนมัติ เช่น `bg-brand-primary`, `text-surface-page`, `border-line-primary`
## ❌ Anti-Patterns (สิ่งที่คุณต้องไล่ล่าและกำจัดทิ้ง)
> [!WARNING]
> **1. การ Hardcode สีเดิมของ Tailwind (Legacy Hardcode)**
-> - ห้ามใช้คลาสสีเก่าอย่าง `bg-spa-light`, `text-spa-forest`, `bg-spa-sage`, `border-spa-stone/20`
-> - เพราะคลาสพวกนี้เป็นสีคงที่ (Static) เมื่อแอดมินเปลี่ยน Theme หน้าเว็บจะไม่ยอมเปลี่ยนสีตาม
+> - **ใน SPA:** ห้ามใช้คลาสสีเก่าอย่าง `bg-spa-light`, `text-spa-forest`, `bg-spa-sage`
+> - **ใน Website:** ห้ามใช้คลาส Hardcode (เช่น `bg-[#FF6B6B]`) ยกเว้นในกรณีเฉพาะที่ยอมรับได้เช่น Status Badges หรือปุ่ม Danger
> [!CAUTION]
> **2. ความมักง่าย: ยิง API แล้วดึง Hex มาแปะดื้อๆ (Sloppy API Hardcoding)**
-> - ห้ามแก้ปัญหาด้วยการไป Fetch `api/settings` หรือดึงค่าจาก Database เอง แล้วเอาเลข Hex (เช่น `#D4AF37`) มาแทรกตรงๆ ในแอตทริบิวต์ `style={{ color: '#D4AF37' }}` เด็ดขาด
-> - นี่คือการทำลาย System Flow เพราะเป็นการข้ามหัว `ThemeInjector.tsx` และทำให้ระบบพังเมื่อแอดมินเปลี่ยนสีภายหลัง
+> - ห้ามดึงค่าจาก API / Database มาแทรกตรงๆ ในแอตทริบิวต์ `style={{ color: '#D4AF37' }}` เด็ดขาด
+> - นี่คือการทำลาย System Flow และทำให้ระบบ Theme พังเมื่อแอดมินเปลี่ยนสี
> [!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`)
+> - ต้องเช็คเสมอว่า hover state และ active state ใช้แบบ Dynamic หรือไม่ (เช่น ใช้ opacity ควบคู่กับ CSS variable แทนที่จะใส่สีใหม่ตายตัวลงไป)
## ✅ Fixing Patterns (วิธีแก้ไขที่ถูกต้อง)
-### การแก้สีพื้นฐาน
+### สำหรับ `apps/spa`
```tsx
// ❌ Bad (Legacy Hardcode)
-
+
-// ✅ Good (Dynamic Theme Compliance)
-
+// ✅ Good (Dynamic Theme Compliance - ใช้ตระกูล pop-*)
+
```
-### การแก้ปุ่มเน้นสีแบรนด์ (Brand Accent)
+### สำหรับ `apps/website`
```tsx
-// ❌ Bad
-
@@ -228,27 +212,12 @@ function CategoriesSection({
}
function ProductsSection({ payload, lang }: { payload: Record
, lang: string }) {
+ const { t } = useTranslation()
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
const title = localizedFromRecord(payload, 'title', resolvedLang) || 'Featured Picks'
const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang)
const products = getProducts(payload)
- const viewAllLabel: Record = {
- th: 'ดูทั้งหมด →',
- en: 'View All Collection →',
- ja: 'すべて見る →',
- zh: '查看全部 →',
- vi: 'Xem tất cả →',
- ko: '전체 보기 →',
- }
- const emptyLabel: Record = {
- th: 'ไม่พบสินค้าในหมวดนี้',
- en: 'No products found in this section.',
- ja: 'このセクションには商品がありません。',
- zh: '此区域中没有找到产品。',
- vi: 'Không tìm thấy sản phẩm nào trong mục này.',
- ko: '이 섹션에서 찾을 수 있는 상품이 없습니다.',
- }
return (
@@ -259,7 +228,7 @@ function ProductsSection({ payload, lang }: { payload: Record,
{subtitle && {subtitle}
}
- {viewAllLabel[resolvedLang] ?? viewAllLabel.en}
+ {t('dynamic.viewAll', { lng: resolvedLang })}
{products.length ? (
@@ -268,7 +237,7 @@ function ProductsSection({ payload, lang }: { payload: Record
,
) : (
- {emptyLabel[resolvedLang] ?? emptyLabel.en}
+ {t('dynamic.emptyProducts', { lng: resolvedLang })}
)}
@@ -450,6 +419,7 @@ function EditorialCarouselSection({ payload, lang }: { payload: Record, lang: string }) {
+ const { t } = useTranslation()
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
const title = localizedFromRecord(payload, 'title', resolvedLang) || 'Scent Guide Article'
@@ -466,14 +436,6 @@ function HeroArticleSection({ payload, lang }: { payload: Record = {
- th: 'โดย',
- en: 'By',
- ja: '著者',
- zh: '作者',
- vi: 'Bởi',
- ko: '작성자',
- }
return (
@@ -489,7 +451,7 @@ function HeroArticleSection({ payload, lang }: { payload: Record{subtitle}
}
{(author || date) && (
- {[author && `${authorPrefix[resolvedLang] ?? authorPrefix.en} ${author}`, date].filter(Boolean).join(' / ')}
+ {[author && `${t('dynamic.authorBy', { lng: resolvedLang })} ${author}`, date].filter(Boolean).join(' / ')}
)}
@@ -498,23 +460,16 @@ function HeroArticleSection({ payload, lang }: { payload: Record, lang: string }) {
+ const { t } = useTranslation()
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
const title = localizedFromRecord(payload, 'title', resolvedLang) || 'Scent Notes'
const description = localizedFromRecord(payload, 'description', resolvedLang)
- const noteLabels: Record> = {
- th: { top: 'กลิ่นเปิด', heart: 'กลิ่นกลาง', base: 'กลิ่นฐาน' },
- en: { top: 'Top Notes', heart: 'Heart Notes', base: 'Base Notes' },
- ja: { top: 'トップノート', heart: 'ミドルノート', base: 'ベースノート' },
- zh: { top: '前调', heart: '中调', base: '后调' },
- vi: { top: 'Hương đầu', heart: 'Hương giữa', base: 'Hương cuối' },
- ko: { top: '탑 노트', heart: '미들 노트', base: '베이스 노트' },
- }
- const labels = noteLabels[resolvedLang] ?? noteLabels.en
+
const notes = [
- [labels.top, optionalText(payload.topNotes, resolvedLang) || 'The initial, lighter smells of the fragrance.'],
- [labels.heart, optionalText(payload.heartNotes ?? payload.middleNotes, resolvedLang) || 'The core of the fragrance, emerging just after the top notes.'],
- [labels.base, optionalText(payload.baseNotes, resolvedLang) || 'The deepest, richest scents that linger the longest.'],
+ [t('dynamic.notes.top', { lng: resolvedLang }), optionalText(payload.topNotes, resolvedLang) || 'The initial, lighter smells of the fragrance.'],
+ [t('dynamic.notes.heart', { lng: resolvedLang }), optionalText(payload.heartNotes ?? payload.middleNotes, resolvedLang) || 'The core of the fragrance, emerging just after the top notes.'],
+ [t('dynamic.notes.base', { lng: resolvedLang }), optionalText(payload.baseNotes, resolvedLang) || 'The deepest, richest scents that linger the longest.'],
]
return (
@@ -611,27 +566,20 @@ function AccordionSection({ payload, lang }: { payload: Record,
}
function ContactInfoSection({ payload, lang }: { payload: Record, lang: string }) {
+ const { t } = useTranslation()
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
- const labels: Record> = {
- th: { address: 'ที่อยู่', email: 'อีเมล', line: 'Line', hours: 'เวลาทำการ' },
- en: { address: 'Address', email: 'Email', line: 'Line', hours: 'Business Hours' },
- ja: { address: '住所', email: 'メール', line: 'Line', hours: '営業時間' },
- zh: { address: '地址', email: '电子邮件', line: 'Line', hours: '营业时间' },
- vi: { address: 'Địa chỉ', email: 'Email', line: 'Line', hours: 'Giờ làm việc' },
- ko: { address: '주소', email: '이메일', line: 'Line', hours: '운영 시간' },
- }
- const resolvedLabels = labels[resolvedLang] ?? labels.en
+
const title = localizedFromRecord(payload, 'title', resolvedLang)
const address = localizedFromRecord(payload, 'address', resolvedLang)
const email = typeof payload.email === 'string' ? payload.email : ''
const line = typeof payload.line === 'string' ? payload.line : ''
const hours = localizedFromRecord(payload, 'hours', resolvedLang)
const items = [
- { key: 'address', label: resolvedLabels.address, value: address, Icon: MapPin },
- { key: 'email', label: resolvedLabels.email, value: email, Icon: Mail },
- { key: 'line', label: resolvedLabels.line, value: line, Icon: MessageCircle },
- { key: 'hours', label: resolvedLabels.hours, value: hours, Icon: Clock },
+ { key: 'address', label: t('dynamic.contactInfo.address', { lng: resolvedLang }), value: address, Icon: MapPin },
+ { key: 'email', label: t('dynamic.contactInfo.email', { lng: resolvedLang }), value: email, Icon: Mail },
+ { key: 'line', label: t('dynamic.contactInfo.line', { lng: resolvedLang }), value: line, Icon: MessageCircle },
+ { key: 'hours', label: t('dynamic.contactInfo.hours', { lng: resolvedLang }), value: hours, Icon: Clock },
] as const
return (
@@ -667,105 +615,46 @@ function ContactInfoSection({ payload, lang }: { payload: Record, lang: string }) {
+ const { t } = useTranslation()
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 [error, setError] = useState(null)
const title = localizedFromRecord(payload, 'title', resolvedLang)
const subtitle = localizedFromRecord(payload, 'subtitle', resolvedLang)
- const labels: Record> = {
- 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) => {
+ const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
+ setError(null)
+ setIsSuccess(false)
- // Simulate API call
- setTimeout(() => {
- setIsSubmitting(false)
+ try {
+ const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3051/api';
+ const res = await fetch(`${apiUrl}/contacts`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(formData)
+ });
+
+ const data = await res.json().catch(() => ({}));
+
+ if (!res.ok || data.error) {
+ throw new Error(data.error || 'Failed to send message');
+ }
+
setIsSuccess(true)
setFormData({ name: '', email: '', message: '' })
-
- // Reset success message after 3 seconds
setTimeout(() => setIsSuccess(false), 3000)
- }, 1000)
+ } catch (err: any) {
+ console.error(err);
+ setError(err.message || 'An unexpected error occurred. Please try again.');
+ } finally {
+ setIsSubmitting(false);
+ }
}
const handleChange = (e: React.ChangeEvent) => {
@@ -778,28 +667,33 @@ function ContactFormSection({ payload, lang }: { payload: Record
- {title || copy.title}
+ {title || t('contactForm.title', { lng: resolvedLang })}
- {subtitle || copy.subtitle}
+ {subtitle || t('contactForm.subtitle', { lng: resolvedLang })}
+ {error && (
+
+ {error}
+
+ )}
{isSuccess ? (
-
{copy.successTitle}
-
{copy.successBody}
+
{t('contactForm.successTitle', { lng: resolvedLang })}
+
{t('contactForm.successBody', { lng: resolvedLang })}
) : (