From 730fdb49e6c60862c0cfd8d088f86e0832fe02c1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 29 Sep 2026 10:13:48 +0700 Subject: [PATCH] x --- apps/admin/src/App.tsx | 4 +- .../admin/src/components/ThemeLivePreview.tsx | 869 ++++++++++++++++++ apps/admin/src/pages/InstallSeedPage.tsx | 203 ++++ apps/admin/src/pages/LoginPage.tsx | 15 +- apps/admin/src/pages/ThemeSettingsPage.tsx | 309 +++++-- apps/backend/src/lib/installerSeed.ts | 224 +++++ apps/backend/src/routes/auth.ts | 45 + apps/backend/src/routes/pages.ts | 14 +- 8 files changed, 1611 insertions(+), 72 deletions(-) create mode 100644 apps/admin/src/components/ThemeLivePreview.tsx create mode 100644 apps/admin/src/pages/InstallSeedPage.tsx create mode 100644 apps/backend/src/lib/installerSeed.ts diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index 0a73177..70d9b90 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -13,6 +13,7 @@ import PageReorderPage from './pages/PageReorderPage' import CmsMockupPage from './pages/CmsMockupPage' import RawMaterialsPage from './pages/RawMaterialsPage' import LoginPage from './pages/LoginPage' +import InstallSeedPage from './pages/InstallSeedPage' import ProtectedRoute from './components/ProtectedRoute' import OrdersPage from './pages/OrdersPage' import OrderDetailPage from './pages/OrderDetailPage' @@ -78,8 +79,9 @@ function App() { - {/* Public Route */} + {/* Public Routes */} } /> + } /> {/* Guarded Admin Routes */} }> diff --git a/apps/admin/src/components/ThemeLivePreview.tsx b/apps/admin/src/components/ThemeLivePreview.tsx new file mode 100644 index 0000000..33a8c77 --- /dev/null +++ b/apps/admin/src/components/ThemeLivePreview.tsx @@ -0,0 +1,869 @@ +import React, { useState } from 'react' +import { + Monitor, + Smartphone, + Sparkles, + Star, + MessageCircle, + ShoppingCart, + Search, + CheckCircle2, + AlertTriangle, + ChevronDown, + ChevronUp, + ChevronLeft, + ChevronRight, + Info +} from 'lucide-react' + +interface ThemeLivePreviewProps { + tokens: Record +} + +// ── WCAG Relative Luminance & Contrast Calculation ────────────────────────── +function parseHex(hex: string): [number, number, number] { + const clean = (hex || '').replace('#', '').trim() + if (clean.length === 3) { + const r = parseInt(clean[0] + clean[0], 16) || 0 + const g = parseInt(clean[1] + clean[1], 16) || 0 + const b = parseInt(clean[2] + clean[2], 16) || 0 + return [r, g, b] + } + if (clean.length >= 6) { + const r = parseInt(clean.substring(0, 2), 16) || 0 + const g = parseInt(clean.substring(2, 4), 16) || 0 + const b = parseInt(clean.substring(4, 6), 16) || 0 + return [r, g, b] + } + return [0, 0, 0] +} + +function getLuminance(r: number, g: number, b: number): number { + const a = [r, g, b].map(v => { + v /= 255 + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4) + }) + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722 +} + +function getContrastRatio(hex1: string, hex2: string): number { + const [r1, g1, b1] = parseHex(hex1) + const [r2, g2, b2] = parseHex(hex2) + const l1 = getLuminance(r1, g1, b1) + const l2 = getLuminance(r2, g2, b2) + const brightest = Math.max(l1, l2) + const darkest = Math.min(l1, l2) + return (brightest + 0.05) / (darkest + 0.05) +} + +export default function ThemeLivePreview({ tokens }: ThemeLivePreviewProps) { + const [viewMode, setViewMode] = useState<'desktop' | 'mobile'>('desktop') + const [zoomScale, setZoomScale] = useState(80) + const [isExpanded, setIsExpanded] = useState(true) + const [activeTab, setActiveTab] = useState<'storefront' | 'components'>('storefront') + const [currentSlide, setCurrentSlide] = useState(0) + + // Calculate Accessibility Contrast Ratios + const textOnPageContrast = getContrastRatio(tokens.textMain || '#333333', tokens.surfacePage || '#FFFFFF') + const textOnCardContrast = getContrastRatio(tokens.textMain || '#333333', tokens.surfaceCard || '#FFFFFF') + const buttonContrast = getContrastRatio('#FFFFFF', tokens.brandPrimary || '#FF6B6B') + + // Sample Hero Slide Content + const heroSlides = [ + { + id: 1, + tag: 'NEW COLLECTION 2026', + tagBg: 'var(--p-brand-cool)', + tagColor: 'var(--p-brand-primary)', + title: 'Pure Origins, Artisan Craft', + subtitle: 'สัมผัสกลิ่นอายความหอมที่รังสรรค์อย่างประณีต เพื่อสุนทรียภาพแห่งการผ่อนคลายในทุกมุมห้องของคุณ', + primaryBtnText: 'Shop Collection', + primaryBtnType: 'brand', + secondaryBtnText: 'Explore Scent', + secondaryBtnType: 'outline', + icon: '🕯️', + gradient: 'from-amber-500/10 via-rose-500/5 to-transparent' + }, + { + id: 2, + tag: '100% NATURAL BOTANICAL', + tagBg: 'var(--p-brand-soft)', + tagColor: '#222222', + title: 'Essential Wellness & Energy', + subtitle: 'น้ำมันหอมระเหยบริสุทธิ์จากพืชพรรณธรรมชาติ ปลุกพลังความสดชื่น บำบัดอารมณ์ให้แจ่มใสในทุกวัน', + primaryBtnText: 'Order via LINE', + primaryBtnType: 'line', + secondaryBtnText: 'View Scent Guide', + secondaryBtnType: 'outline', + icon: '🌿', + gradient: 'from-emerald-500/10 via-teal-500/5 to-transparent' + }, + { + id: 3, + tag: 'SPECIAL GIFT SET -30%', + tagBg: 'var(--p-brand-accent)', + tagColor: '#FFFFFF', + title: 'Luxury Sensory Experience', + subtitle: 'ชุดของขวัญอโรม่าสุดพรีเมียม ส่งมอบความรู้สึกพิเศษและบรรยากาศอันอบอุ่นแด่คนสำคัญ', + primaryBtnText: 'Claim Special Offer', + primaryBtnType: 'brand', + secondaryBtnText: 'Browse Catalog', + secondaryBtnType: 'outline', + icon: '✨', + gradient: 'from-purple-500/10 via-pink-500/5 to-transparent' + } + ] + + const activeSlide = heroSlides[currentSlide] || heroSlides[0] + + // Inline CSS variables scope for realistic live rendering + const previewCssVars = { + '--p-brand-primary': tokens.brandPrimary || '#FF6B6B', + '--p-brand-primary-hover': tokens.brandPrimaryHover || '#FF4500', + '--p-brand-accent': tokens.brandAccent || '#FFF200', + '--p-brand-warm': tokens.brandWarm || '#FFA07A', + '--p-brand-cool': tokens.brandCool || '#E0FFFF', + '--p-brand-success': tokens.brandSuccess || '#98FB98', + '--p-brand-soft': tokens.brandSoft || '#D8BFD8', + '--p-surface-page': tokens.surfacePage || '#FFF0F5', + '--p-surface-nav-rest': tokens.surfaceNavRest || '#F9F8F5', + '--p-surface-card': tokens.surfaceCard || '#FFFFFF', + '--p-surface-muted': tokens.surfaceMuted || '#EDE6DD', + '--p-text-main': tokens.textMain || '#333333', + '--p-muted-text': tokens.mutedText || '#666666', + '--p-caption-text': tokens.captionText || '#B0A396', + '--p-border-color': tokens.borderColor || '#E8E0D8', + '--p-line-primary': tokens.linePrimary || '#00B900', + '--p-line-primary-hover': tokens.linePrimaryHover || '#009900', + '--p-radius-base': tokens.radiusBase || '8px', + '--p-radius-card': tokens.radiusCard || '16px', + '--p-shadow-card': tokens.shadowCard || '0 4px 12px rgba(0,0,0,0.08)', + '--p-shadow-card-hover': tokens.shadowCardHover || '0 12px 28px rgba(0,0,0,0.15)', + } as React.CSSProperties + + return ( +
+ + {/* ── Compact Toolbar Header ── */} +
+
+
+ +
+
+
+

Live Preview

+ + Real-Time + +
+
+
+ + {/* Action Controls */} +
+ {/* Zoom / Scale Selector */} +
+ {[ + { label: '70%', val: 70 }, + { label: '80%', val: 80 }, + { label: '100%', val: 100 }, + ].map(z => ( + + ))} +
+ + {/* View Mode Toggle */} +
+ + +
+ + {/* Toggle Expand/Collapse */} + +
+
+ + {isExpanded && ( + <> + {/* ── Sub-header: Contrast Bar & Tabs ── */} +
+
+ WCAG: + + = 4.5 ? 'text-emerald-400 bg-emerald-500/10' : 'text-amber-400 bg-amber-500/10' + }`}> + Page {textOnPageContrast.toFixed(1)}:1 + + + = 4.5 ? 'text-emerald-400 bg-emerald-500/10' : 'text-amber-400 bg-amber-500/10' + }`}> + Card {textOnCardContrast.toFixed(1)}:1 + + + = 4.5 ? 'text-emerald-400 bg-emerald-500/10' : 'text-amber-400 bg-amber-500/10' + }`}> + Btn {buttonContrast.toFixed(1)}:1 + +
+ + {/* View Tabs */} +
+ + +
+
+ + {/* ── Main Scrollable Preview Stage ── */} +
+ + {/* Scoped CSS Style Sheet for Native Hover States */} + + +
+ + {/* Mobile Device Notch/Bar */} + {viewMode === 'mobile' && ( +
+ 9:41 +
+ 5G 100% +
+ )} + + {/* ── 1. Navbar Simulation (surfaceNavRest) ── */} +
+
+ + HOMAROMA + + + {viewMode === 'desktop' && ( + + )} +
+ +
+ + +
+ + + 2 + +
+ + +
+
+ + {/* ── 2. Content Body Simulation (surfacePage) ── */} +
+ + {activeTab === 'storefront' ? ( + <> + {/* ── Hero Slide / Carousel Banner ── */} +
+ {/* Interactive Prev/Next Navigation Buttons */} + + + + + {/* Slide Body Content */} +
+ + {/* Slide Eyebrow Badge */} +
+ + {activeSlide.icon} + {activeSlide.tag} + +
+ + {/* Slide Title */} +

+ {activeSlide.title} +

+ + {/* Slide Subtitle */} +

+ {activeSlide.subtitle} +

+ + {/* Interactive Hero Buttons */} +
+ {activeSlide.primaryBtnType === 'line' ? ( + + ) : ( + + )} + + +
+ + {/* Slide Indicator Dots */} +
+ {heroSlides.map((s, idx) => ( +
+ +
+
+ + {/* Product Cards Grid Showcase (surfaceCard) */} +
+ + {/* Product Card 1 */} +
+
+ 🕯️ + + {/* Sale Badge (brandAccent) */} + + SALE 25% + + + {/* In Stock Badge (brandSuccess) */} + + ● Ready + +
+ +
+ {/* Rating Stars (brandWarm) */} +
+ {[1, 2, 3, 4, 5].map(s => ( + + ))} + + 5.0 (42) + +
+ + {/* Title (textMain) */} +

+ Midnight Lavender Candle +

+ + {/* Description (mutedText) */} +

+ กลิ่นลาเวนเดอร์บริสุทธิ์ ช่วยให้ผ่อนคลาย หลับสบายตลอดค่ำคืน +

+ + {/* Price & Action */} +
+
+
+ ฿890 +
+
+ ฿1,190 +
+
+ + +
+
+
+ + {/* Product Card 2 (Muted / Secondary Style) */} +
+
+ 🌿 + + {/* Soft Highlight Badge (brandSoft) */} + + ORGANIC + + + + 320 ชิ้น + +
+ +
+
+ {[1, 2, 3, 4, 5].map(s => ( + + ))} + + 4.8 (89) + +
+ +

+ Eucalyptus Essential Oil +

+ +

+ น้ำมันหอมสกัดบริสุทธิ์ 100% หายใจโล่งสดชื่น +

+ +
+
+ ฿650 +
+ + +
+
+
+ +
+ + {/* Muted Section / Banner Showcase (surfaceMuted) */} +
+
+ + Muted Section + +

+ รับสิทธิ์ VIP Member ลดทันที 15% +

+
+ + +
+ + ) : ( + /* ── Component Matrix View ── */ +
+
+

+ Buttons & Hover Demonstration +

+ +
+
+ + + brandPrimary → Hover + +
+ +
+ + + borderColor & textMain + +
+ +
+ + + linePrimary → Hover + +
+
+
+ + {/* Badges & Accents Palette */} +
+

+ Badges & Accents +

+
+ + brandAccent (Sale) + + + brandSuccess (Ready) + + + brandWarm (Rating ⭐) + + + brandCool (Tag) + + + brandSoft (Pastel) + +
+
+ + {/* Typography Scale */} +
+

+ Typography Hierarchy +

+
+ textMain — ข้อความหัวข้อหลัก H1-H3 +
+
+ mutedText — คำอธิบายสินค้า และเนื้อหารอง +
+
+ captionText — หมายเหตุ วันที่ รหัสสินค้า และสถิติ +
+
+
+ )} + +
+ +
+ +
+ + )} + +
+ ) +} diff --git a/apps/admin/src/pages/InstallSeedPage.tsx b/apps/admin/src/pages/InstallSeedPage.tsx new file mode 100644 index 0000000..9baedc7 --- /dev/null +++ b/apps/admin/src/pages/InstallSeedPage.tsx @@ -0,0 +1,203 @@ +import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { KeyRound, ShieldAlert, CheckCircle2, Loader2, ArrowRight, Database, UserCheck, Layers } from 'lucide-react' +import { API_BASE } from '@homaroma/types' + +export default function InstallSeedPage() { + const [password, setPassword] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [result, setResult] = useState(null) + const navigate = useNavigate() + + const handleRunSeed = async (e: React.FormEvent) => { + e.preventDefault() + setError('') + setResult(null) + setLoading(true) + + try { + const response = await fetch(`${API_BASE}/auth/install-seed`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ password }), + }) + + const data = await response.json() + + if (!response.ok || !data.success) { + throw new Error(data.error || 'Failed to run installer seed') + } + + setResult(data) + } catch (err: any) { + setError(err.message || 'Connection failed') + } finally { + setLoading(false) + } + } + + return ( +
+ {/* Background Glows */} +
+
+ +
+ {/* Header */} +
+
+ Emergency System Installer +
+

+ HomAroma Seed & Recovery +

+

+ ระบบกู้คืนและติดตั้งข้อมูลตั้งต้น (Admin User, เมนูหลัก, หมวดหมู่, แบรนด์กลิ่น, และ CMS Pages) +

+
+ + {/* Card Form */} +
+ {error && ( +
+ +
+

เกิดข้อผิดพลาดในการติดตั้ง

+

{error}

+
+
+ )} + + {!result ? ( +
+
+ +
+ + setPassword(e.target.value)} + className="w-full pl-11 pr-4 py-3 bg-night-950/80 border border-white/10 focus:border-emerald-500/50 rounded-xl outline-none text-white transition-all font-mono tracking-wider" + placeholder="รหัสผ่านยืนยัน (เช่น 66195221)" + autoFocus + /> +
+

+ * ป้องกันการรันซ้ำโดยไม่ได้ตั้งใจ กรุณากรอกรหัสผ่านเพื่อดำเนินการ +

+
+ +
+
+ รายการที่จะถูกสร้าง/กู้คืนอัตโนมัติ: +
+
    +
  • สร้างบัญชี admin (รหัสผ่านเริ่มต้น: homaroma123)
  • +
  • สร้างโครงสร้างเมนูหลัก (Home, Shop, Scent Guide, About, FAQ, Track Order)
  • +
  • สร้าง 45 กลิ่น/แบรนด์น้ำมันหอมระเหย และ 5 หมวดหมู่สินค้า
  • +
  • สร้างหน้า CMS Dynamic Pages (Home, Shop Layout, Wishlist, Checkout ฯลฯ)
  • +
+
+ + +
+ ) : ( +
+
+ +
+

ติดตั้งและกู้คืนข้อมูลสำเร็จ!

+

{result.message}

+
+
+ + {/* Admin credentials card */} +
+
+ ข้อมูลเข้าสู่ระบบ Admin: +
+
+
+ Username + admin +
+
+ Password + homaroma123 +
+
+
+ + {/* Stats summary */} + {result.summary && ( +
+
+ Brands / กลิ่น + {result.summary.brandsCount || 45} +
+
+ Categories + {result.summary.categoriesCount || 5} +
+
+ CMS Pages + {result.summary.pagesCount || 14} +
+
+ )} + +
+ + +
+
+ )} +
+ + {/* Back to Login link */} +
+ +
+
+
+ ) +} diff --git a/apps/admin/src/pages/LoginPage.tsx b/apps/admin/src/pages/LoginPage.tsx index 05337a6..46cfb54 100644 --- a/apps/admin/src/pages/LoginPage.tsx +++ b/apps/admin/src/pages/LoginPage.tsx @@ -129,9 +129,18 @@ export default function LoginPage() { -

- © {new Date().getFullYear()} {brandName} -

+
+ +

+ © {new Date().getFullYear()} {brandName} +

+
) diff --git a/apps/admin/src/pages/ThemeSettingsPage.tsx b/apps/admin/src/pages/ThemeSettingsPage.tsx index b001e3f..53f6a3f 100644 --- a/apps/admin/src/pages/ThemeSettingsPage.tsx +++ b/apps/admin/src/pages/ThemeSettingsPage.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef } from 'react' import { API_BASE, type StoreSettings } from '@homaroma/types' +import ThemeLivePreview from '../components/ThemeLivePreview' import { Save, Loader2, @@ -468,7 +469,7 @@ export default function ThemeSettingsPage() { if (loading) return
return ( -
+
{/* Top Header & Action Buttons */}
@@ -705,97 +706,275 @@ export default function ThemeSettingsPage() { {/* Main Settings Form */}
- - {/* ── 1. Color Palette Tokens ── */} -
+
+ + {/* ── Left Column: Color Palette & Surfaces & Settings ── */} +
+ + {/* ── 1. Color Palette Tokens ── */} +
🎨

Color Palette & Surfaces

-

Core brand colors, background surfaces, text hierarchy, and borders

+

Core brand colors, background surfaces, text hierarchy, and borders with micro-annotations

{/* Group 1: Brand & Accent Colors */}
-

Brand & Accent Colors

-
+
+

Brand & Accent Colors

+ ปุ่มหลัก, ป้ายโปรโมชั่น และสีไฮไลต์ประจำแบรนด์ +
+
{[ - { key: 'brandPrimary', label: 'Brand Primary' }, - { key: 'brandPrimaryHover', label: 'Primary Hover' }, - { key: 'brandAccent', label: 'Brand Accent' }, - { key: 'brandWarm', label: 'Brand Warm' }, - { key: 'brandCool', label: 'Brand Cool' }, - { key: 'brandSuccess', label: 'Brand Success' }, - { key: 'brandSoft', label: 'Brand Soft' }, - ].map(({ key, label }) => ( -
-
- {label} - {themeTokens[key] || '#000'} + { + key: 'brandPrimary', + label: 'Brand Primary', + role: 'ปุ่มหลัก (Primary CTA) & Active Nav', + desc: 'สีปุ่มดำเนินการหลัก เช่น เพิ่มลงตะกร้า, ลิงก์เมนูที่ถูกเลือก', + badgeColor: 'text-cyan-glow bg-cyan-glow/10 border-cyan-glow/20' + }, + { + key: 'brandPrimaryHover', + label: 'Primary Hover', + role: 'สีปุ่มหลักเมื่อชี้เมาส์ (Hover State)', + desc: 'เฉดสีปุ่มเมื่อผู้ใช้ Hover เมาส์เหนือปุ่ม Primary CTA', + badgeColor: 'text-amber-400 bg-amber-400/10 border-amber-400/20' + }, + { + key: 'brandAccent', + label: 'Brand Accent', + role: 'ป้ายโปรโมชั่น (Sale Badge) & ไฮไลต์', + desc: 'ป้ายลดราคา, ตัวเลขแจ้งเตือนในตะกร้า, จุดเด่นสะดุดตา', + badgeColor: 'text-rose-400 bg-rose-400/10 border-rose-400/20' + }, + { + key: 'brandWarm', + label: 'Brand Warm', + role: 'ดาวรีวิว (Rating ⭐) & แสงอบอุ่น', + desc: 'ดาวแสดงคะแนนรีวิว, ไฮไลต์ประกายโทนอุ่น', + badgeColor: 'text-amber-300 bg-amber-300/10 border-amber-300/20' + }, + { + key: 'brandCool', + label: 'Brand Cool', + role: 'ป้ายแท็กโทนเย็น & พื้นหลังสดชื่น', + desc: 'พื้นหลังแท็กสินค้าใหม่ (New Arrival), หมวดพิเศษ', + badgeColor: 'text-sky-400 bg-sky-400/10 border-sky-400/20' + }, + { + key: 'brandSuccess', + label: 'Brand Success', + role: 'สถานะพร้อมส่ง (In Stock) & ชำระสำเร็จ', + desc: 'ป้ายสินค้าพร้อมส่ง, สัญลักษณ์การทำงานสำเร็จ', + badgeColor: 'text-emerald-400 bg-emerald-400/10 border-emerald-400/20' + }, + { + key: 'brandSoft', + label: 'Brand Soft', + role: 'ไฮไลต์พาสเทล / ป้าย Organic', + desc: 'พื้นหลัง Badge สินค้าออร์แกนิก, กรอบไฮไลต์ละมุน', + badgeColor: 'text-purple-300 bg-purple-300/10 border-purple-300/20' + }, + ].map(({ key, label, role, desc, badgeColor }) => ( +
+
+
+ {label} +
+
+
+ {role} +
+

+ {desc} +

+
+ +
+ setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))} + className="h-8 w-10 rounded-lg border border-white/10 bg-night-950 p-0.5 cursor-pointer shrink-0" + /> + setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))} + className="h-8 w-full rounded-lg border border-white/10 bg-night-950 px-2 font-mono text-[11px] text-slate-200 uppercase outline-none focus:border-cyan-glow transition" + placeholder="#000000" + />
- setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))} - className="h-10 w-full rounded-lg border border-white/10 bg-night-950 p-1 cursor-pointer" - />
))}
{/* Group 2: Surfaces & Backgrounds */} -
-

Surfaces & Backgrounds

+
+
+

Surfaces & Backgrounds

+ มิติและเลเยอร์ของพื้นหลังหน้าเว็บ และกล่องเนื้อหา +
{[ - { key: 'surfacePage', label: 'Page Background' }, - { key: 'surfaceNavRest', label: 'Navbar Background' }, - { key: 'surfaceCard', label: 'Card Surface' }, - { key: 'surfaceMuted', label: 'Muted Section Surface' }, - ].map(({ key, label }) => ( -
-
- {label} - {themeTokens[key] || '#000'} + { + key: 'surfacePage', + label: 'Page Background', + role: 'พื้นหลังหลักของเว็บไซต์ (Body)', + desc: 'สีผืนใหญ่ด้านหลังสุดที่คลุมทั้งหน้าเว็บ', + badgeColor: 'text-purple-300 bg-purple-300/10 border-purple-300/20' + }, + { + key: 'surfaceNavRest', + label: 'Navbar Background', + role: 'พื้นหลังแถบเมนูด้านบน (Header)', + desc: 'สีแถบเมนูด้านบนสุดก่อนเลื่อนหน้าจอ', + badgeColor: 'text-indigo-300 bg-indigo-300/10 border-indigo-300/20' + }, + { + key: 'surfaceCard', + label: 'Card Surface', + role: 'พื้นผิวกล่องการ์ดสินค้า & Dialog', + desc: 'สีพื้นหลังของกล่องสินค้าแต่ละชิ้น และกล่องข้อความ', + badgeColor: 'text-pink-300 bg-pink-300/10 border-pink-300/20' + }, + { + key: 'surfaceMuted', + label: 'Muted Section Surface', + role: 'พื้นผิว Section สลับลาย & Footer', + desc: 'สีพื้นหลังของแถบข้อความโปรโมชั่น, แถบรีวิว, และส่วนท้าย', + badgeColor: 'text-fuchsia-300 bg-fuchsia-300/10 border-fuchsia-300/20' + }, + ].map(({ key, label, role, desc, badgeColor }) => ( +
+
+
+ {label} +
+
+
+ {role} +
+

+ {desc} +

+
+ +
+ setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))} + className="h-8 w-10 rounded-lg border border-white/10 bg-night-950 p-0.5 cursor-pointer shrink-0" + /> + setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))} + className="h-8 w-full rounded-lg border border-white/10 bg-night-950 px-2 font-mono text-[11px] text-slate-200 uppercase outline-none focus:border-cyan-glow transition" + placeholder="#000000" + />
- setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))} - className="h-10 w-full rounded-lg border border-white/10 bg-night-950 p-1 cursor-pointer" - />
))}
{/* Group 3: Text & Lines */} -
-

Typography & Borders

+
+
+

Typography & Borders

+ ลำดับการอ่านข้อความ, เส้นกรอบ และปุ่มติดต่อ LINE +
{[ - { key: 'textMain', label: 'Main Text' }, - { key: 'mutedText', label: 'Muted Text' }, - { key: 'captionText', label: 'Caption Text' }, - { key: 'borderColor', label: 'Border Color' }, - { key: 'linePrimary', label: 'LINE Primary' }, - { key: 'linePrimaryHover', label: 'LINE Hover' }, - ].map(({ key, label }) => ( -
-
- {label} - {themeTokens[key] || '#000'} + { + key: 'textMain', + label: 'Main Text', + role: 'ข้อความหัวข้อและเนื้อหาหลัก', + desc: 'ชื่อสินค้า, หัวข้อ H1-H3, เนื้อหาหลักที่อ่านชัดเจน', + badgeColor: 'text-emerald-400 bg-emerald-400/10 border-emerald-400/20' + }, + { + key: 'mutedText', + label: 'Muted Text', + role: 'ข้อความรอง & คำอธิบายย่อย', + desc: 'คำบรรยายสินค้า, เมนูย่อย, รายละเอียดที่ไม่เน้น', + badgeColor: 'text-slate-300 bg-slate-300/10 border-slate-300/20' + }, + { + key: 'captionText', + label: 'Caption Text', + role: 'หมายเหตุ, วันที่, ข้อมูลสถิติ', + desc: 'ตัวหนังสือขนาดเล็ก, วันที่รีวิว, ยอดขายสินค้า', + badgeColor: 'text-zinc-400 bg-zinc-400/10 border-zinc-400/20' + }, + { + key: 'borderColor', + label: 'Border Color', + role: 'เส้นขอบการ์ด & เส้นแบ่ง Divider', + desc: 'เส้นคั่นแบ่งแถว, เส้นกรอบรอบกล่องสินค้าและฟอร์ม', + badgeColor: 'text-amber-200 bg-amber-200/10 border-amber-200/20' + }, + { + key: 'linePrimary', + label: 'LINE Primary', + role: 'ปุ่มแชท LINE Official', + desc: 'สีปุ่มติดต่อสั่งซื้อทาง LINE (สีเขียวของแบรนด์ LINE)', + badgeColor: 'text-emerald-300 bg-emerald-300/10 border-emerald-300/20' + }, + { + key: 'linePrimaryHover', + label: 'LINE Hover', + role: 'สีปุ่ม LINE เมื่อชี้เมาส์ (Hover)', + desc: 'เฉดสีปุ่ม LINE เมื่อผู้ใช้ Hover เมาส์', + badgeColor: 'text-emerald-500 bg-emerald-500/10 border-emerald-500/20' + }, + ].map(({ key, label, role, desc, badgeColor }) => ( +
+
+
+ {label} +
+
+
+ {role} +
+

+ {desc} +

+
+ +
+ setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))} + className="h-8 w-10 rounded-lg border border-white/10 bg-night-950 p-0.5 cursor-pointer shrink-0" + /> + setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))} + className="h-8 w-full rounded-lg border border-white/10 bg-night-950 px-2 font-mono text-[11px] text-slate-200 uppercase outline-none focus:border-cyan-glow transition" + placeholder="#000000" + />
- setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))} - className="h-10 w-full rounded-lg border border-white/10 bg-night-950 p-1 cursor-pointer" - />
))}
@@ -1218,6 +1397,14 @@ export default function ThemeSettingsPage() { {saving ? : } Save Theme Settings +
+
+ + {/* ── Right Column: Live Interactive UI Preview (Sticky) ── */} +
+ +
+
diff --git a/apps/backend/src/lib/installerSeed.ts b/apps/backend/src/lib/installerSeed.ts new file mode 100644 index 0000000..8452d36 --- /dev/null +++ b/apps/backend/src/lib/installerSeed.ts @@ -0,0 +1,224 @@ +import bcrypt from "bcryptjs"; +import prisma from "./prisma"; + +export const brandNames = [ + "ตะไคร้", "ลาเวนเดอร์", "วานิลลา", "ซากุระ", "มะลิ", "ยูคาลิปตัส", "เฟรชลาเวนเดอร์", + "เปเปอร์มิ้น", "โอเชี่ยน", "เซเว่นซี", "ดอกแก้ว", "ดอกโมก", "ลีลาวดี", "ส้ม", "มะนาว", + "แคนตาลูป", "แอปเปิ้ลแดง", "แอปเปิ้ลเขียว", "ดอกลิลลี่", "โรสเมรี่", "อัญชัน", + "กุหลาบแดง", "หมากฝรั่ง", "ผลไม้รวม", "มะกรูด", "แซนด้าวูส", "ดอกปีบ", "ดอกบัว", + "โอโซน", "ไทยสปา", "กล้วย", "กาแฟ", "ดอกชมนาด", "กลิ่นน้ำนม", "ดอกการะเวก", + "ดอกจำปา", "ดอกพิกุล", "ดอกพุด", "ดอกราตรี", "ดอกลำดวน", "ทับทิม", "มะละกอสุก", + "ดอกราชาวดี", "กลิ่นลูกพีช", "กลิ่นองุ่น" +]; + +export const categoryConfigs = [ + { + name: "Oil Ozone", + sizes: ["30ml", "60ml", "85ml", "120ml"], + priceMap: { "30ml": 69, "60ml": 100, "85ml": 120, "120ml": 150 } + }, + { + name: "Aroma Oil", + sizes: ["30ml", "60ml", "85ml", "120ml"], + priceMap: { "30ml": 90, "60ml": 150, "85ml": 165, "120ml": 225 } + }, + { + name: "Reed Diffuser", + sizes: ["60ml", "85ml", "120ml"], + priceMap: { "60ml": 120, "85ml": 160, "120ml": 190 } + }, + { + name: "Room Spray", + sizes: ["85ml"], + priceMap: { "85ml": 150 } + }, + { + name: "Car Diffuser", + sizes: ["25ml"], + priceMap: { "25ml": 60 } + } +]; + +export async function runEmergencySeed() { + const summary: Record = {}; + + // 1. Seed Admin User + const adminPassword = await bcrypt.hash("homaroma123", 10); + const adminUser = await prisma.user.upsert({ + where: { username: "admin" }, + update: { password: adminPassword }, + create: { + username: "admin", + password: adminPassword, + role: "ADMIN" + } + }); + summary.admin = { username: adminUser.username, defaultPassword: "homaroma123" }; + + // 2. Seed Core Menus + const coreMenus = [ + { name: "main", items: [{ id: 1, label: "Home", url: "/", order: 1 }, { id: 2, label: "Shop", url: "/shop", order: 2 }, { id: 3, label: "Scent Guide", url: "/scent-guide", order: 3 }, { id: 4, label: "About Us", url: "/about", order: 4 }] }, + { name: "footer-explore", items: [{ id: 5, label: "Home", url: "/", order: 1 }, { id: 6, label: "Shop", url: "/shop", order: 2 }, { id: 7, label: "Scent Guide", url: "/scent-guide", order: 3 }, { id: 8, label: "About Us", url: "/about", order: 4 }] }, + { name: "footer-support", items: [{ id: 9, label: "Track Order", url: "/track-order", order: 1 }, { id: 10, label: "FAQ", url: "/faq", order: 2 }, { id: 11, label: "Contact Us", url: "/contact", order: 3 }, { id: 12, label: "Delivery & Returns", url: "/delivery", order: 4 }] } + ]; + + for (const menu of coreMenus) { + const m = await prisma.menu.upsert({ + where: { name: menu.name }, + update: {}, + create: { name: menu.name } + }); + for (const item of menu.items) { + await prisma.menuItem.upsert({ + where: { id: item.id }, + update: { label: item.label, url: item.url, order: item.order, menuId: m.id }, + create: { id: item.id, label: item.label, url: item.url, order: item.order, menuId: m.id } + }); + } + } + summary.menusCount = coreMenus.length; + + // 3. Seed Brands & Categories + const brands: any[] = []; + for (const name of brandNames) { + const s = await prisma.productBrand.upsert({ + where: { name }, + update: {}, + create: { name } + }); + brands.push(s); + } + summary.brandsCount = brands.length; + + const categories: any[] = []; + for (const config of categoryConfigs) { + const c = await prisma.category.upsert({ + where: { name: config.name }, + update: {}, + create: { name: config.name } + }); + categories.push(c); + } + summary.categoriesCount = categories.length; + + // 4. Seed StoreSettings + const existingSettings = await prisma.storeSettings.findFirst(); + if (!existingSettings) { + await prisma.storeSettings.create({ + data: { + storeName: "HomAroma", + siteTitle: "HomAroma — Scents for Serenity", + siteDescription: "Premium sensory experiences crafted for mindful living.", + contactEmail: "contact@homaroma.shop", + contactPhone: "+66 81 234 5678", + headerStyle: "single_line", + scrollRevealStyle: "fade-up", + scrollRevealDuration: 700 + } + }); + } + + // 5. Seed Pages & Sections + const pagesConfig = [ + { slug: "home", title: "Home Page" }, + { slug: "shop-layout", title: "Shop Page Layout" }, + { slug: "scent-guide-layout", title: "Scent Guide Layout" }, + { slug: "category-layout", title: "Category Page Layout" }, + { slug: "product-detail-layout", title: "Product Detail Layout" }, + { slug: "faq-layout", title: "FAQ Page Layout" }, + { slug: "wishlist-layout", title: "Wishlist Page Layout" }, + { slug: "search-layout", title: "Search Page Layout" }, + { slug: "coupons-layout", title: "Coupons Page Layout" }, + { slug: "contact-layout", title: "Contact Page Layout" }, + { slug: "order-success-layout", title: "Order Success Page Layout" }, + { slug: "track-order-layout", title: "Track Order Page Layout" }, + { slug: "checkout-layout", title: "Checkout Page Layout" }, + { slug: "profile-layout", title: "Profile Page Layout" } + ]; + + let pagesCount = 0; + for (const pg of pagesConfig) { + const p = await prisma.page.upsert({ + where: { slug: pg.slug }, + update: { title: pg.title, isPublished: true }, + create: { slug: pg.slug, title: pg.title, isPublished: true } + }); + pagesCount++; + + // Seed default hero section if no sections exist + const sectionCount = await prisma.pageSection.count({ where: { pageId: p.id } }); + if (sectionCount === 0) { + if (pg.slug === "home") { + await prisma.pageSection.create({ + data: { + pageId: p.id, + type: "HERO", + order: 1, + isActive: true, + payload: { + tagline: "VIBRANT ELEGANCE COLLECTION", + title: "Inhale [Serenity]", + description: "Crafting premium sensory experiences that transform your space into a sanctuary of peace.", + imageUrl: "/images/hero.png", + buttonText: "Explore Collection", + buttonLink: "/shop" + } + } + }); + await prisma.pageSection.create({ + data: { + pageId: p.id, + type: "CATEGORIES", + order: 2, + isActive: true, + payload: { + title: "Our Collections", + subtitle: "Discover the perfect scent for every moment" + } + } + }); + await prisma.pageSection.create({ + data: { + pageId: p.id, + type: "PRODUCTS", + order: 3, + isActive: true, + payload: { + title: "Featured Aromas", + subtitle: "Handcrafted blends designed for your well-being", + category: "All", + limit: 8 + } + } + }); + } else { + await prisma.pageSection.create({ + data: { + pageId: p.id, + type: "BANNER", + order: 1, + isActive: true, + payload: { + title: pg.title, + subtitle: "Explore our handcrafted scents and collections", + buttonText: "Shop Now", + buttonLink: "/shop" + } + } + }); + } + } + } + summary.pagesCount = pagesCount; + + // 6. Reset PostgreSQL Sequence + try { + await prisma.$executeRawUnsafe( + `SELECT setval('"MenuItem_id_seq"', COALESCE((SELECT MAX(id) FROM "MenuItem") + 1, 1), false);` + ); + } catch (err) { + // SQLite or other non-postgres ignored + } + + return summary; +} diff --git a/apps/backend/src/routes/auth.ts b/apps/backend/src/routes/auth.ts index ea28ebf..39b4e1c 100644 --- a/apps/backend/src/routes/auth.ts +++ b/apps/backend/src/routes/auth.ts @@ -110,4 +110,49 @@ export const authRoute = new Elysia({ prefix: "/auth" }) tags: ["auth"], }, } + ) + .post( + "/install-seed", + async ({ body, set }) => { + const { password, pin } = body as { password?: string; pin?: string }; + const secret = password || pin; + + if (secret !== "66195221") { + set.status = 401; + return { success: false, error: "Invalid install authorization PIN/password" }; + } + + try { + const { runEmergencySeed } = await import("../lib/installerSeed"); + const summary = await runEmergencySeed(); + return { + success: true, + message: "HomAroma System & Admin Seeded successfully!", + adminUser: { + username: "admin", + password: "homaroma123", + }, + summary, + }; + } catch (err) { + console.error("[InstallSeed] Error:", err); + set.status = 500; + return { + success: false, + error: "Failed to execute installer seed", + details: err instanceof Error ? err.message : String(err), + }; + } + }, + { + body: t.Object({ + password: t.Optional(t.String()), + pin: t.Optional(t.String()), + }), + detail: { + summary: "Emergency System & Admin Installer Seed", + tags: ["auth"], + }, + } ); + diff --git a/apps/backend/src/routes/pages.ts b/apps/backend/src/routes/pages.ts index 37a5fd9..e8f03f3 100644 --- a/apps/backend/src/routes/pages.ts +++ b/apps/backend/src/routes/pages.ts @@ -345,7 +345,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" }) orderBy: { updatedAt: "desc" }, }); }) - .get("/:slug", async ({ params: { slug }, query, set }) => { + .get("/:id", async ({ params: { id: slug }, query, set }) => { const includeInactive = String((query as any)?.includeInactive || "").toLowerCase() === "true"; const lang = (query as any)?.lang || "th"; @@ -391,7 +391,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" }) translations: page.translations, }; }) - .get("/:slug/sections", async ({ params: { slug }, set }) => { + .get("/:id/sections", async ({ params: { id: slug }, set }) => { try { return await prisma.pageSection.findMany({ where: { pageId: Number(slug) }, @@ -1031,7 +1031,7 @@ export const pagesRoute = new Elysia({ prefix: "/pages" }) }), }, ) - .delete("/:slug", async ({ params: { slug }, set }) => { + .delete("/:id", async ({ params: { id: slug }, set }) => { try { await prisma.page.delete({ where: { id: Number(slug) }, @@ -1075,8 +1075,8 @@ export const pagesRoute = new Elysia({ prefix: "/pages" }) return { success: true, translations: results, page: updated }; }, { body: t.Any() }) .post( - "/:slug/sections", - async ({ params: { slug }, body, set }) => { + "/:id/sections", + async ({ params: { id: slug }, body, set }) => { try { const pageId = Number(slug); const { type, order, payload, isActive } = body as any; @@ -1240,8 +1240,8 @@ export const pagesRoute = new Elysia({ prefix: "/pages" }) }, ) .put( - "/:slug/sections/reorder", - async ({ params: { slug }, body, set }) => { + "/:id/sections/reorder", + async ({ params: { id: slug }, body, set }) => { try { const pageId = Number(slug); const { sectionOrders } = body as {