x
This commit is contained in:
1 parent
c42dd4272f
commit
730fdb49e6
8 files changed
+1611
-72
No files matched your search
@@ -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() {
|
||||
<StoreSettingsProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
{/* Public Route */}
|
||||
{/* Public Routes */}
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/install-seed" element={<InstallSeedPage />} />
|
||||
|
||||
{/* Guarded Admin Routes */}
|
||||
<Route element={<ProtectedRoute />}>
|
||||
|
||||
@@ -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<string, string>
|
||||
}
|
||||
|
||||
// ── 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<number>(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 (
|
||||
<div className={`admin-card border-cyan-glow/30 bg-night-950/90 shadow-2xl overflow-hidden flex flex-col transition-all duration-300 ${
|
||||
isExpanded ? 'h-[calc(100vh-6.5rem)]' : 'h-auto'
|
||||
}`}>
|
||||
|
||||
{/* ── Compact Toolbar Header ── */}
|
||||
<div className="shrink-0 p-3 px-4 bg-white/[0.03] border-b border-white/5 flex flex-wrap items-center justify-between gap-2.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-cyan-glow/10 text-cyan-glow">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<h2 className="text-xs font-bold text-white tracking-wide">Live Preview</h2>
|
||||
<span className="px-1.5 py-0.2 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-[9px] font-bold uppercase">
|
||||
Real-Time
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Controls */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Zoom / Scale Selector */}
|
||||
<div className="flex items-center bg-white/5 border border-white/10 rounded-lg p-0.5 text-[10px] font-bold">
|
||||
{[
|
||||
{ label: '70%', val: 70 },
|
||||
{ label: '80%', val: 80 },
|
||||
{ label: '100%', val: 100 },
|
||||
].map(z => (
|
||||
<button
|
||||
key={z.val}
|
||||
type="button"
|
||||
onClick={() => setZoomScale(z.val)}
|
||||
className={`px-2 py-0.5 rounded transition cursor-pointer ${
|
||||
zoomScale === z.val
|
||||
? 'bg-cyan-glow text-night-950 shadow-sm'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
title={`Scale preview to ${z.label}`}
|
||||
>
|
||||
{z.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex items-center p-0.5 bg-white/5 border border-white/10 rounded-lg text-xs font-semibold">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode('desktop')}
|
||||
className={`flex items-center gap-1 px-2.5 py-1 rounded transition-all cursor-pointer ${
|
||||
viewMode === 'desktop'
|
||||
? 'bg-cyan-glow text-night-950 font-bold shadow-sm'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
title="Desktop View"
|
||||
>
|
||||
<Monitor className="w-3 h-3" />
|
||||
<span className="text-[10px]">PC</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setViewMode('mobile')}
|
||||
className={`flex items-center gap-1 px-2.5 py-1 rounded transition-all cursor-pointer ${
|
||||
viewMode === 'mobile'
|
||||
? 'bg-cyan-glow text-night-950 font-bold shadow-sm'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
title="Mobile View (375px)"
|
||||
>
|
||||
<Smartphone className="w-3 h-3" />
|
||||
<span className="text-[10px]">Mobile</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Toggle Expand/Collapse */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="p-1 rounded-lg bg-white/5 hover:bg-white/10 text-slate-300 hover:text-white border border-white/10 transition cursor-pointer"
|
||||
title={isExpanded ? 'Collapse Preview' : 'Expand Preview'}
|
||||
>
|
||||
{isExpanded ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<>
|
||||
{/* ── Sub-header: Contrast Bar & Tabs ── */}
|
||||
<div className="shrink-0 px-4 py-1.5 bg-night-900/70 border-b border-white/5 flex flex-wrap items-center justify-between text-[11px] gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] text-slate-400 font-semibold uppercase">WCAG:</span>
|
||||
|
||||
<span className={`px-1.5 py-0.2 rounded font-mono text-[10px] font-bold ${
|
||||
textOnPageContrast >= 4.5 ? 'text-emerald-400 bg-emerald-500/10' : 'text-amber-400 bg-amber-500/10'
|
||||
}`}>
|
||||
Page {textOnPageContrast.toFixed(1)}:1
|
||||
</span>
|
||||
|
||||
<span className={`px-1.5 py-0.2 rounded font-mono text-[10px] font-bold ${
|
||||
textOnCardContrast >= 4.5 ? 'text-emerald-400 bg-emerald-500/10' : 'text-amber-400 bg-amber-500/10'
|
||||
}`}>
|
||||
Card {textOnCardContrast.toFixed(1)}:1
|
||||
</span>
|
||||
|
||||
<span className={`px-1.5 py-0.2 rounded font-mono text-[10px] font-bold ${
|
||||
buttonContrast >= 4.5 ? 'text-emerald-400 bg-emerald-500/10' : 'text-amber-400 bg-amber-500/10'
|
||||
}`}>
|
||||
Btn {buttonContrast.toFixed(1)}:1
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* View Tabs */}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('storefront')}
|
||||
className={`px-2 py-0.5 rounded text-[10px] font-bold transition cursor-pointer ${
|
||||
activeTab === 'storefront' ? 'bg-cyan-glow/20 text-cyan-glow' : 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Storefront
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveTab('components')}
|
||||
className={`px-2 py-0.5 rounded text-[10px] font-bold transition cursor-pointer ${
|
||||
activeTab === 'components' ? 'bg-cyan-glow/20 text-cyan-glow' : 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Buttons
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Main Scrollable Preview Stage ── */}
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-night-950 flex justify-center items-start custom-scrollbar">
|
||||
|
||||
{/* Scoped CSS Style Sheet for Native Hover States */}
|
||||
<style>{`
|
||||
.preview-theme-scope {
|
||||
background-color: var(--p-surface-page);
|
||||
color: var(--p-text-main);
|
||||
}
|
||||
.preview-btn-primary {
|
||||
background-color: var(--p-brand-primary);
|
||||
color: #FFFFFF;
|
||||
border-radius: var(--p-radius-base);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.preview-btn-primary:hover {
|
||||
background-color: var(--p-brand-primary-hover);
|
||||
transform: translateY(-1.5px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.18);
|
||||
}
|
||||
.preview-btn-secondary {
|
||||
background-color: transparent;
|
||||
color: var(--p-text-main);
|
||||
border: 1px solid var(--p-border-color);
|
||||
border-radius: var(--p-radius-base);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.preview-btn-secondary:hover {
|
||||
background-color: var(--p-surface-card);
|
||||
border-color: var(--p-brand-primary);
|
||||
color: var(--p-brand-primary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.preview-btn-line {
|
||||
background-color: var(--p-line-primary);
|
||||
color: #FFFFFF;
|
||||
border-radius: var(--p-radius-base);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.preview-btn-line:hover {
|
||||
background-color: var(--p-line-primary-hover);
|
||||
transform: translateY(-1.5px);
|
||||
box-shadow: 0 4px 12px rgba(0,185,0,0.25);
|
||||
}
|
||||
.preview-card-item {
|
||||
background-color: var(--p-surface-card);
|
||||
border: 1px solid var(--p-border-color);
|
||||
border-radius: var(--p-radius-card);
|
||||
box-shadow: var(--p-shadow-card);
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.preview-card-item:hover {
|
||||
box-shadow: var(--p-shadow-card-hover);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
.preview-nav-arrow {
|
||||
background-color: color-mix(in srgb, var(--p-surface-card) 85%, transparent);
|
||||
color: var(--p-text-main);
|
||||
border: 1px solid var(--p-border-color);
|
||||
backdrop-filter: blur(8px);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.preview-nav-arrow:hover {
|
||||
background-color: var(--p-brand-primary);
|
||||
color: #FFFFFF;
|
||||
border-color: var(--p-brand-primary);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<div
|
||||
style={{
|
||||
...previewCssVars,
|
||||
zoom: zoomScale / 100,
|
||||
}}
|
||||
className={`preview-theme-scope transition-all duration-300 overflow-hidden border border-slate-800 shrink-0 ${
|
||||
viewMode === 'mobile'
|
||||
? 'w-[375px] rounded-[36px] shadow-2xl ring-6 ring-slate-800/80 my-2'
|
||||
: 'w-full max-w-xl rounded-2xl shadow-xl my-2'
|
||||
}`}
|
||||
>
|
||||
|
||||
{/* Mobile Device Notch/Bar */}
|
||||
{viewMode === 'mobile' && (
|
||||
<div className="h-6 bg-black/40 flex items-center justify-between px-6 text-[10px] text-slate-400 font-mono select-none">
|
||||
<span>9:41</span>
|
||||
<div className="w-20 h-3 bg-black/60 rounded-full" />
|
||||
<span>5G 100%</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── 1. Navbar Simulation (surfaceNavRest) ── */}
|
||||
<header
|
||||
style={{
|
||||
backgroundColor: 'var(--p-surface-nav-rest)',
|
||||
borderColor: 'var(--p-border-color)',
|
||||
}}
|
||||
className="px-4 py-2.5 border-b flex items-center justify-between transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
style={{ color: 'var(--p-text-main)' }}
|
||||
className="font-bold text-xs tracking-widest uppercase flex items-center gap-1.5"
|
||||
>
|
||||
<span>HOMAROMA</span>
|
||||
</span>
|
||||
|
||||
{viewMode === 'desktop' && (
|
||||
<nav className="hidden sm:flex items-center gap-3 text-[11px] font-semibold ml-2">
|
||||
<span
|
||||
style={{ color: 'var(--p-brand-primary)' }}
|
||||
className="cursor-pointer border-b-2 pb-0.5"
|
||||
>
|
||||
Home
|
||||
</span>
|
||||
<span
|
||||
style={{ color: 'var(--p-muted-text)' }}
|
||||
className="cursor-pointer hover:opacity-80 transition"
|
||||
>
|
||||
Shop
|
||||
</span>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
style={{ color: 'var(--p-text-main)' }}
|
||||
className="p-1 rounded-full hover:bg-black/5 transition"
|
||||
title="Search"
|
||||
>
|
||||
<Search className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
style={{ color: 'var(--p-text-main)' }}
|
||||
className="p-1 rounded-full hover:bg-black/5 transition"
|
||||
title="Cart"
|
||||
>
|
||||
<ShoppingCart className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<span
|
||||
style={{ backgroundColor: 'var(--p-brand-accent)', color: '#FFFFFF' }}
|
||||
className="absolute -top-1 -right-1 h-3.5 w-3.5 rounded-full text-[8px] font-bold flex items-center justify-center shadow-sm"
|
||||
>
|
||||
2
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="preview-btn-line px-2.5 py-1 text-[10px] font-bold flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
<MessageCircle className="w-3 h-3" />
|
||||
<span>LINE</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* ── 2. Content Body Simulation (surfacePage) ── */}
|
||||
<div className="p-4 sm:p-5 space-y-4">
|
||||
|
||||
{activeTab === 'storefront' ? (
|
||||
<>
|
||||
{/* ── Hero Slide / Carousel Banner ── */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--p-surface-card)',
|
||||
borderColor: 'var(--p-border-color)',
|
||||
borderRadius: 'var(--p-radius-card)',
|
||||
boxShadow: 'var(--p-shadow-card)'
|
||||
}}
|
||||
className={`relative border overflow-hidden transition-all duration-300 bg-gradient-to-br ${activeSlide.gradient}`}
|
||||
>
|
||||
{/* Interactive Prev/Next Navigation Buttons */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentSlide(prev => (prev === 0 ? heroSlides.length - 1 : prev - 1))}
|
||||
className="preview-nav-arrow absolute left-2 top-1/2 -translate-y-1/2 z-10 w-6 h-6 rounded-full flex items-center justify-center cursor-pointer shadow-md"
|
||||
title="Previous Slide"
|
||||
>
|
||||
<ChevronLeft className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentSlide(prev => (prev === heroSlides.length - 1 ? 0 : prev + 1))}
|
||||
className="preview-nav-arrow absolute right-2 top-1/2 -translate-y-1/2 z-10 w-6 h-6 rounded-full flex items-center justify-center cursor-pointer shadow-md"
|
||||
title="Next Slide"
|
||||
>
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
{/* Slide Body Content */}
|
||||
<div className="px-8 py-5 text-center space-y-2.5">
|
||||
|
||||
{/* Slide Eyebrow Badge */}
|
||||
<div className="flex justify-center">
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: activeSlide.tagBg,
|
||||
color: activeSlide.tagColor
|
||||
}}
|
||||
className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider shadow-sm transition-all"
|
||||
>
|
||||
<span>{activeSlide.icon}</span>
|
||||
<span>{activeSlide.tag}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Slide Title */}
|
||||
<h1
|
||||
style={{ color: 'var(--p-text-main)' }}
|
||||
className="text-lg md:text-xl font-bold tracking-tight transition-colors line-clamp-1"
|
||||
>
|
||||
{activeSlide.title}
|
||||
</h1>
|
||||
|
||||
{/* Slide Subtitle */}
|
||||
<p
|
||||
style={{ color: 'var(--p-muted-text)' }}
|
||||
className="text-xs leading-relaxed max-w-md mx-auto line-clamp-2"
|
||||
>
|
||||
{activeSlide.subtitle}
|
||||
</p>
|
||||
|
||||
{/* Interactive Hero Buttons */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-2 pt-1">
|
||||
{activeSlide.primaryBtnType === 'line' ? (
|
||||
<button
|
||||
type="button"
|
||||
className="preview-btn-line px-3.5 py-1.5 text-[11px] font-bold tracking-wider uppercase flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
<MessageCircle className="w-3 h-3" />
|
||||
<span>{activeSlide.primaryBtnText}</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="preview-btn-primary px-3.5 py-1.5 text-[11px] font-bold tracking-wider uppercase cursor-pointer"
|
||||
>
|
||||
{activeSlide.primaryBtnText}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="preview-btn-secondary px-3.5 py-1.5 text-[11px] font-bold tracking-wider uppercase cursor-pointer"
|
||||
>
|
||||
{activeSlide.secondaryBtnText}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Slide Indicator Dots */}
|
||||
<div className="flex items-center justify-center gap-1.5 pt-2">
|
||||
{heroSlides.map((s, idx) => (
|
||||
<button
|
||||
key={s.id}
|
||||
type="button"
|
||||
onClick={() => setCurrentSlide(idx)}
|
||||
style={{
|
||||
backgroundColor: idx === currentSlide ? 'var(--p-brand-primary)' : 'var(--p-border-color)'
|
||||
}}
|
||||
className={`h-1.5 rounded-full transition-all cursor-pointer ${
|
||||
idx === currentSlide ? 'w-5 shadow-sm' : 'w-1.5 hover:opacity-80'
|
||||
}`}
|
||||
title={`Slide ${idx + 1}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Cards Grid Showcase (surfaceCard) */}
|
||||
<div className="grid gap-3.5 grid-cols-1 sm:grid-cols-2">
|
||||
|
||||
{/* Product Card 1 */}
|
||||
<div className="preview-card-item p-3.5 space-y-2.5 cursor-pointer">
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--p-surface-muted)',
|
||||
borderRadius: 'calc(var(--p-radius-card) - 4px)',
|
||||
borderColor: 'var(--p-border-color)'
|
||||
}}
|
||||
className="relative h-32 w-full flex items-center justify-center overflow-hidden border"
|
||||
>
|
||||
<span className="text-3xl select-none">🕯️</span>
|
||||
|
||||
{/* Sale Badge (brandAccent) */}
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: 'var(--p-brand-accent)',
|
||||
color: '#FFFFFF'
|
||||
}}
|
||||
className="absolute top-2 left-2 px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider shadow-md"
|
||||
>
|
||||
SALE 25%
|
||||
</span>
|
||||
|
||||
{/* In Stock Badge (brandSuccess) */}
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: 'var(--p-brand-success)',
|
||||
color: '#111111'
|
||||
}}
|
||||
className="absolute bottom-2 right-2 px-1.5 py-0.5 rounded text-[8px] font-bold uppercase tracking-wider shadow-sm flex items-center gap-0.5"
|
||||
>
|
||||
<span>●</span> Ready
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
{/* Rating Stars (brandWarm) */}
|
||||
<div className="flex items-center gap-0.5">
|
||||
{[1, 2, 3, 4, 5].map(s => (
|
||||
<Star
|
||||
key={s}
|
||||
style={{
|
||||
color: 'var(--p-brand-warm)',
|
||||
fill: 'var(--p-brand-warm)'
|
||||
}}
|
||||
className="w-2.5 h-2.5"
|
||||
/>
|
||||
))}
|
||||
<span
|
||||
style={{ color: 'var(--p-caption-text)' }}
|
||||
className="text-[9px] ml-1 font-mono"
|
||||
>
|
||||
5.0 (42)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title (textMain) */}
|
||||
<h3
|
||||
style={{ color: 'var(--p-text-main)' }}
|
||||
className="text-xs font-bold truncate"
|
||||
>
|
||||
Midnight Lavender Candle
|
||||
</h3>
|
||||
|
||||
{/* Description (mutedText) */}
|
||||
<p
|
||||
style={{ color: 'var(--p-muted-text)' }}
|
||||
className="text-[11px] line-clamp-2 leading-relaxed"
|
||||
>
|
||||
กลิ่นลาเวนเดอร์บริสุทธิ์ ช่วยให้ผ่อนคลาย หลับสบายตลอดค่ำคืน
|
||||
</p>
|
||||
|
||||
{/* Price & Action */}
|
||||
<div className="flex items-center justify-between pt-1.5 border-t" style={{ borderColor: 'var(--p-border-color)' }}>
|
||||
<div>
|
||||
<div
|
||||
style={{ color: 'var(--p-brand-primary)' }}
|
||||
className="text-sm font-bold font-mono"
|
||||
>
|
||||
฿890
|
||||
</div>
|
||||
<div
|
||||
style={{ color: 'var(--p-caption-text)' }}
|
||||
className="text-[9px] line-through font-mono"
|
||||
>
|
||||
฿1,190
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="preview-btn-primary px-2.5 py-1 text-[10px] font-bold cursor-pointer"
|
||||
>
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Card 2 (Muted / Secondary Style) */}
|
||||
<div className="preview-card-item p-3.5 space-y-2.5 cursor-pointer">
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--p-surface-muted)',
|
||||
borderRadius: 'calc(var(--p-radius-card) - 4px)',
|
||||
borderColor: 'var(--p-border-color)'
|
||||
}}
|
||||
className="relative h-32 w-full flex items-center justify-center overflow-hidden border"
|
||||
>
|
||||
<span className="text-3xl select-none">🌿</span>
|
||||
|
||||
{/* Soft Highlight Badge (brandSoft) */}
|
||||
<span
|
||||
style={{
|
||||
backgroundColor: 'var(--p-brand-soft)',
|
||||
color: '#222222'
|
||||
}}
|
||||
className="absolute top-2 left-2 px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider shadow-sm"
|
||||
>
|
||||
ORGANIC
|
||||
</span>
|
||||
|
||||
<span
|
||||
style={{ color: 'var(--p-caption-text)' }}
|
||||
className="absolute bottom-2 right-2 text-[8px] font-mono"
|
||||
>
|
||||
320 ชิ้น
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-0.5">
|
||||
{[1, 2, 3, 4, 5].map(s => (
|
||||
<Star
|
||||
key={s}
|
||||
style={{
|
||||
color: 'var(--p-brand-warm)',
|
||||
fill: s <= 4 ? 'var(--p-brand-warm)' : 'none'
|
||||
}}
|
||||
className="w-2.5 h-2.5"
|
||||
/>
|
||||
))}
|
||||
<span
|
||||
style={{ color: 'var(--p-caption-text)' }}
|
||||
className="text-[9px] ml-1 font-mono"
|
||||
>
|
||||
4.8 (89)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h3
|
||||
style={{ color: 'var(--p-text-main)' }}
|
||||
className="text-xs font-bold truncate"
|
||||
>
|
||||
Eucalyptus Essential Oil
|
||||
</h3>
|
||||
|
||||
<p
|
||||
style={{ color: 'var(--p-muted-text)' }}
|
||||
className="text-[11px] line-clamp-2 leading-relaxed"
|
||||
>
|
||||
น้ำมันหอมสกัดบริสุทธิ์ 100% หายใจโล่งสดชื่น
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between pt-1.5 border-t" style={{ borderColor: 'var(--p-border-color)' }}>
|
||||
<div
|
||||
style={{ color: 'var(--p-brand-primary)' }}
|
||||
className="text-sm font-bold font-mono"
|
||||
>
|
||||
฿650
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="preview-btn-line px-2.5 py-1 text-[10px] font-bold flex items-center gap-0.5 cursor-pointer"
|
||||
>
|
||||
<MessageCircle className="w-3 h-3" />
|
||||
<span>LINE</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Muted Section / Banner Showcase (surfaceMuted) */}
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--p-surface-muted)',
|
||||
borderColor: 'var(--p-border-color)',
|
||||
borderRadius: 'var(--p-radius-card)'
|
||||
}}
|
||||
className="p-3.5 border flex items-center justify-between gap-3 text-left"
|
||||
>
|
||||
<div className="space-y-0.5">
|
||||
<span
|
||||
style={{ color: 'var(--p-caption-text)' }}
|
||||
className="text-[9px] font-bold uppercase tracking-widest block"
|
||||
>
|
||||
Muted Section
|
||||
</span>
|
||||
<h4
|
||||
style={{ color: 'var(--p-text-main)' }}
|
||||
className="text-xs font-bold"
|
||||
>
|
||||
รับสิทธิ์ VIP Member ลดทันที 15%
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="preview-btn-primary px-3 py-1.5 text-[10px] font-bold uppercase whitespace-nowrap cursor-pointer shrink-0"
|
||||
>
|
||||
Join
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* ── Component Matrix View ── */
|
||||
<div className="space-y-4">
|
||||
<div className="p-3.5 rounded-xl border" style={{ backgroundColor: 'var(--p-surface-card)', borderColor: 'var(--p-border-color)' }}>
|
||||
<h4 className="text-[11px] font-bold uppercase tracking-wider mb-2" style={{ color: 'var(--p-text-main)' }}>
|
||||
Buttons & Hover Demonstration
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
||||
<div className="space-y-1 text-center">
|
||||
<button type="button" className="preview-btn-primary w-full py-2 text-[11px] font-bold uppercase cursor-pointer">
|
||||
Primary CTA
|
||||
</button>
|
||||
<span className="text-[9px] block font-mono" style={{ color: 'var(--p-caption-text)' }}>
|
||||
brandPrimary → Hover
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-center">
|
||||
<button type="button" className="preview-btn-secondary w-full py-2 text-[11px] font-bold uppercase cursor-pointer">
|
||||
Outline Action
|
||||
</button>
|
||||
<span className="text-[9px] block font-mono" style={{ color: 'var(--p-caption-text)' }}>
|
||||
borderColor & textMain
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-center">
|
||||
<button type="button" className="preview-btn-line w-full py-2 text-[11px] font-bold uppercase flex items-center justify-center gap-1 cursor-pointer">
|
||||
<MessageCircle className="w-3 h-3" />
|
||||
<span>LINE</span>
|
||||
</button>
|
||||
<span className="text-[9px] block font-mono" style={{ color: 'var(--p-caption-text)' }}>
|
||||
linePrimary → Hover
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badges & Accents Palette */}
|
||||
<div className="p-3.5 rounded-xl border" style={{ backgroundColor: 'var(--p-surface-card)', borderColor: 'var(--p-border-color)' }}>
|
||||
<h4 className="text-[11px] font-bold uppercase tracking-wider mb-2" style={{ color: 'var(--p-text-main)' }}>
|
||||
Badges & Accents
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold shadow-sm" style={{ backgroundColor: 'var(--p-brand-accent)', color: '#FFFFFF' }}>
|
||||
brandAccent (Sale)
|
||||
</span>
|
||||
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold shadow-sm" style={{ backgroundColor: 'var(--p-brand-success)', color: '#111111' }}>
|
||||
brandSuccess (Ready)
|
||||
</span>
|
||||
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold shadow-sm" style={{ backgroundColor: 'var(--p-brand-warm)', color: '#111111' }}>
|
||||
brandWarm (Rating ⭐)
|
||||
</span>
|
||||
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold shadow-sm" style={{ backgroundColor: 'var(--p-brand-cool)', color: '#222222' }}>
|
||||
brandCool (Tag)
|
||||
</span>
|
||||
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold shadow-sm" style={{ backgroundColor: 'var(--p-brand-soft)', color: '#222222' }}>
|
||||
brandSoft (Pastel)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Typography Scale */}
|
||||
<div className="p-3.5 rounded-xl border space-y-1.5" style={{ backgroundColor: 'var(--p-surface-card)', borderColor: 'var(--p-border-color)' }}>
|
||||
<h4 className="text-[11px] font-bold uppercase tracking-wider mb-1" style={{ color: 'var(--p-text-main)' }}>
|
||||
Typography Hierarchy
|
||||
</h4>
|
||||
<div className="text-sm font-bold" style={{ color: 'var(--p-text-main)' }}>
|
||||
textMain — ข้อความหัวข้อหลัก H1-H3
|
||||
</div>
|
||||
<div className="text-xs" style={{ color: 'var(--p-muted-text)' }}>
|
||||
mutedText — คำอธิบายสินค้า และเนื้อหารอง
|
||||
</div>
|
||||
<div className="text-[10px] font-mono" style={{ color: 'var(--p-caption-text)' }}>
|
||||
captionText — หมายเหตุ วันที่ รหัสสินค้า และสถิติ
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<any>(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 (
|
||||
<div className="min-h-screen flex items-center justify-center p-6 bg-night-950 relative overflow-hidden">
|
||||
{/* Background Glows */}
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-emerald-500/10 blur-[140px] rounded-full -translate-y-1/2 translate-x-1/2 pointer-events-none" />
|
||||
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-cyan-glow/5 blur-[140px] rounded-full translate-y-1/2 -translate-x-1/2 pointer-events-none" />
|
||||
|
||||
<div className="w-full max-w-lg space-y-8 relative z-10 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-2">
|
||||
<div className="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full bg-amber-500/10 border border-amber-500/20 text-[11px] font-bold uppercase tracking-[0.2em] text-amber-400 mb-2">
|
||||
<ShieldAlert className="w-3.5 h-3.5" /> Emergency System Installer
|
||||
</div>
|
||||
<h1 className="text-3xl font-extrabold tracking-tight text-white">
|
||||
HomAroma <span className="text-emerald-400">Seed & Recovery</span>
|
||||
</h1>
|
||||
<p className="text-slate-400 text-sm">
|
||||
ระบบกู้คืนและติดตั้งข้อมูลตั้งต้น (Admin User, เมนูหลัก, หมวดหมู่, แบรนด์กลิ่น, และ CMS Pages)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Card Form */}
|
||||
<div className="admin-card border-white/10 shadow-2xl p-6 sm:p-8 space-y-6 bg-night-900/90 backdrop-blur-xl rounded-2xl">
|
||||
{error && (
|
||||
<div className="flex items-start gap-3 p-4 bg-rose-500/10 border border-rose-500/20 rounded-xl text-rose-400 text-sm animate-shake">
|
||||
<ShieldAlert className="w-5 h-5 shrink-0 mt-0.5 text-rose-500" />
|
||||
<div>
|
||||
<p className="font-semibold">เกิดข้อผิดพลาดในการติดตั้ง</p>
|
||||
<p className="text-xs text-rose-300/80 mt-0.5">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!result ? (
|
||||
<form onSubmit={handleRunSeed} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[11px] uppercase font-bold tracking-widest text-slate-400 ml-1">
|
||||
กรอกรหัสผ่าน / Authorization PIN
|
||||
</label>
|
||||
<div className="relative group">
|
||||
<KeyRound className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 group-focus-within:text-emerald-400 transition-colors" />
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-500 ml-1">
|
||||
* ป้องกันการรันซ้ำโดยไม่ได้ตั้งใจ กรุณากรอกรหัสผ่านเพื่อดำเนินการ
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-night-950/60 border border-white/5 rounded-xl p-4 space-y-2.5 text-xs text-slate-300">
|
||||
<div className="font-semibold text-slate-200 flex items-center gap-1.5">
|
||||
<Database className="w-4 h-4 text-emerald-400" /> รายการที่จะถูกสร้าง/กู้คืนอัตโนมัติ:
|
||||
</div>
|
||||
<ul className="space-y-1 text-slate-400 list-disc list-inside ml-1 text-[11px]">
|
||||
<li>สร้างบัญชี <b className="text-slate-200">admin</b> (รหัสผ่านเริ่มต้น: <code className="text-emerald-400">homaroma123</code>)</li>
|
||||
<li>สร้างโครงสร้างเมนูหลัก (Home, Shop, Scent Guide, About, FAQ, Track Order)</li>
|
||||
<li>สร้าง 45 กลิ่น/แบรนด์น้ำมันหอมระเหย และ 5 หมวดหมู่สินค้า</li>
|
||||
<li>สร้างหน้า CMS Dynamic Pages (Home, Shop Layout, Wishlist, Checkout ฯลฯ)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || !password}
|
||||
className="w-full py-3.5 px-4 bg-emerald-600 hover:bg-emerald-500 active:scale-[0.99] disabled:opacity-50 text-white font-semibold rounded-xl flex items-center justify-center gap-2 transition-all shadow-lg shadow-emerald-950/40 text-sm cursor-pointer"
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
กำลังดำเนินการ Seed ข้อมูลลง Database...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Database className="w-4 h-4" />
|
||||
🚀 รัน Emergency System Seed เดี๋ยวนี้
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
<div className="flex items-center gap-3 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-xl text-emerald-400 text-sm">
|
||||
<CheckCircle2 className="w-6 h-6 shrink-0 text-emerald-400" />
|
||||
<div>
|
||||
<h4 className="font-bold text-white text-base">ติดตั้งและกู้คืนข้อมูลสำเร็จ!</h4>
|
||||
<p className="text-xs text-emerald-300/80 mt-0.5">{result.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Admin credentials card */}
|
||||
<div className="bg-night-950/80 border border-emerald-500/20 rounded-xl p-4 space-y-3">
|
||||
<div className="flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-emerald-400">
|
||||
<UserCheck className="w-4 h-4" /> ข้อมูลเข้าสู่ระบบ Admin:
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 text-xs bg-black/40 p-3 rounded-lg border border-white/5">
|
||||
<div>
|
||||
<span className="text-slate-500 block text-[10px] uppercase font-bold">Username</span>
|
||||
<span className="text-white font-mono font-bold text-sm">admin</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500 block text-[10px] uppercase font-bold">Password</span>
|
||||
<span className="text-emerald-400 font-mono font-bold text-sm">homaroma123</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats summary */}
|
||||
{result.summary && (
|
||||
<div className="grid grid-cols-3 gap-2 text-center text-xs">
|
||||
<div className="bg-night-950/40 border border-white/5 p-2.5 rounded-lg">
|
||||
<span className="text-slate-500 block text-[10px]">Brands / กลิ่น</span>
|
||||
<span className="font-bold text-white text-sm">{result.summary.brandsCount || 45}</span>
|
||||
</div>
|
||||
<div className="bg-night-950/40 border border-white/5 p-2.5 rounded-lg">
|
||||
<span className="text-slate-500 block text-[10px]">Categories</span>
|
||||
<span className="font-bold text-white text-sm">{result.summary.categoriesCount || 5}</span>
|
||||
</div>
|
||||
<div className="bg-night-950/40 border border-white/5 p-2.5 rounded-lg">
|
||||
<span className="text-slate-500 block text-[10px]">CMS Pages</span>
|
||||
<span className="font-bold text-white text-sm">{result.summary.pagesCount || 14}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/login')}
|
||||
className="flex-1 py-3.5 px-4 bg-emerald-600 hover:bg-emerald-500 text-white font-semibold rounded-xl flex items-center justify-center gap-2 transition-all shadow-lg text-sm cursor-pointer"
|
||||
>
|
||||
ไปที่หน้าเข้าสู่ระบบ Admin <ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setResult(null); setPassword(''); }}
|
||||
className="py-3.5 px-4 bg-white/5 hover:bg-white/10 text-slate-300 font-medium rounded-xl transition-all text-sm cursor-pointer"
|
||||
>
|
||||
รันใหม่อีกครั้ง
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Back to Login link */}
|
||||
<div className="text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/login')}
|
||||
className="text-xs text-slate-500 hover:text-slate-300 transition-colors inline-flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
← กลับไปที่หน้าเข้าสู่ระบบปกติ (Login)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -129,9 +129,18 @@ export default function LoginPage() {
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="text-center text-[10px] text-slate-600 uppercase tracking-widest">
|
||||
© {new Date().getFullYear()} {brandName}
|
||||
</p>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate('/install-seed')}
|
||||
className="text-[11px] text-slate-500 hover:text-emerald-400 transition-colors cursor-pointer flex items-center gap-1"
|
||||
>
|
||||
⚙️ ติดตั้งระบบ / กู้คืนข้อมูลเริ่มต้น (Emergency Seed)
|
||||
</button>
|
||||
<p className="text-center text-[10px] text-slate-600 uppercase tracking-widest">
|
||||
© {new Date().getFullYear()} {brandName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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 <div className="flex items-center justify-center p-20"><Loader2 className="w-8 h-8 text-cyan-glow animate-spin" /></div>
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl space-y-8 animate-fade-in pb-20">
|
||||
<div className="w-full space-y-8 animate-fade-in pb-20">
|
||||
|
||||
{/* Top Header & Action Buttons */}
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 border-b border-white/5 pb-6">
|
||||
@@ -705,97 +706,275 @@ export default function ThemeSettingsPage() {
|
||||
|
||||
{/* Main Settings Form */}
|
||||
<form onSubmit={handleSave} className="space-y-8">
|
||||
|
||||
{/* ── 1. Color Palette Tokens ── */}
|
||||
<div className="admin-card p-6 md:p-8 space-y-6">
|
||||
<div className="grid grid-cols-1 xl:grid-cols-12 gap-8 items-start">
|
||||
|
||||
{/* ── Left Column: Color Palette & Surfaces & Settings ── */}
|
||||
<div className="xl:col-span-7 space-y-8">
|
||||
|
||||
{/* ── 1. Color Palette Tokens ── */}
|
||||
<div className="admin-card p-6 md:p-8 space-y-6">
|
||||
<div className="flex items-center gap-3 border-b border-white/10 pb-4">
|
||||
<div className="p-2 bg-cyan-glow/10 rounded-xl text-cyan-glow">
|
||||
<span className="text-xl">🎨</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white">Color Palette & Surfaces</h2>
|
||||
<p className="text-xs text-slate-400">Core brand colors, background surfaces, text hierarchy, and borders</p>
|
||||
<p className="text-xs text-slate-400">Core brand colors, background surfaces, text hierarchy, and borders with micro-annotations</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Group 1: Brand & Accent Colors */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-cyan-glow">Brand & Accent Colors</h3>
|
||||
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-cyan-glow">Brand & Accent Colors</h3>
|
||||
<span className="text-[11px] text-slate-500">ปุ่มหลัก, ป้ายโปรโมชั่น และสีไฮไลต์ประจำแบรนด์</span>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{[
|
||||
{ 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 }) => (
|
||||
<div key={key} className="space-y-1.5 p-3 rounded-xl bg-white/[0.02] border border-white/5">
|
||||
<div className="flex justify-between items-center text-[11px] font-semibold text-slate-300">
|
||||
<span>{label}</span>
|
||||
<span className="font-mono text-[10px] text-slate-500 uppercase">{themeTokens[key] || '#000'}</span>
|
||||
{
|
||||
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 }) => (
|
||||
<div key={key} className="space-y-2 p-3.5 rounded-2xl bg-white/[0.02] border border-white/5 hover:border-white/15 transition-all flex flex-col justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-start justify-between gap-1.5">
|
||||
<span className="text-xs font-bold text-white tracking-wide">{label}</span>
|
||||
<div
|
||||
className="w-4 h-4 rounded-full border border-white/20 shadow-sm shrink-0 mt-0.5"
|
||||
style={{ backgroundColor: themeTokens[key] || '#000000' }}
|
||||
/>
|
||||
</div>
|
||||
<div className={`inline-block px-1.5 py-0.5 rounded-md text-[10px] font-semibold border ${badgeColor}`}>
|
||||
{role}
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-400 leading-tight pt-0.5">
|
||||
{desc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-white/5">
|
||||
<input
|
||||
type="color"
|
||||
value={themeTokens[key] || '#000000'}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={themeTokens[key] || ''}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="color"
|
||||
value={themeTokens[key] || '#000000'}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Group 2: Surfaces & Backgrounds */}
|
||||
<div className="space-y-3 pt-4 border-t border-white/5">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-purple-400">Surfaces & Backgrounds</h3>
|
||||
<div className="space-y-3 pt-6 border-t border-white/5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-purple-400">Surfaces & Backgrounds</h3>
|
||||
<span className="text-[11px] text-slate-500">มิติและเลเยอร์ของพื้นหลังหน้าเว็บ และกล่องเนื้อหา</span>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-4">
|
||||
{[
|
||||
{ key: 'surfacePage', label: 'Page Background' },
|
||||
{ key: 'surfaceNavRest', label: 'Navbar Background' },
|
||||
{ key: 'surfaceCard', label: 'Card Surface' },
|
||||
{ key: 'surfaceMuted', label: 'Muted Section Surface' },
|
||||
].map(({ key, label }) => (
|
||||
<div key={key} className="space-y-1.5 p-3 rounded-xl bg-white/[0.02] border border-white/5">
|
||||
<div className="flex justify-between items-center text-[11px] font-semibold text-slate-300">
|
||||
<span>{label}</span>
|
||||
<span className="font-mono text-[10px] text-slate-500 uppercase">{themeTokens[key] || '#000'}</span>
|
||||
{
|
||||
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 }) => (
|
||||
<div key={key} className="space-y-2 p-3.5 rounded-2xl bg-white/[0.02] border border-white/5 hover:border-white/15 transition-all flex flex-col justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-start justify-between gap-1.5">
|
||||
<span className="text-xs font-bold text-white tracking-wide">{label}</span>
|
||||
<div
|
||||
className="w-4 h-4 rounded-full border border-white/20 shadow-sm shrink-0 mt-0.5"
|
||||
style={{ backgroundColor: themeTokens[key] || '#000000' }}
|
||||
/>
|
||||
</div>
|
||||
<div className={`inline-block px-1.5 py-0.5 rounded-md text-[10px] font-semibold border ${badgeColor}`}>
|
||||
{role}
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-400 leading-tight pt-0.5">
|
||||
{desc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-white/5">
|
||||
<input
|
||||
type="color"
|
||||
value={themeTokens[key] || '#000000'}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={themeTokens[key] || ''}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="color"
|
||||
value={themeTokens[key] || '#000000'}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Group 3: Text & Lines */}
|
||||
<div className="space-y-3 pt-4 border-t border-white/5">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-emerald-400">Typography & Borders</h3>
|
||||
<div className="space-y-3 pt-6 border-t border-white/5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-emerald-400">Typography & Borders</h3>
|
||||
<span className="text-[11px] text-slate-500">ลำดับการอ่านข้อความ, เส้นกรอบ และปุ่มติดต่อ LINE</span>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
{[
|
||||
{ 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 }) => (
|
||||
<div key={key} className="space-y-1.5 p-3 rounded-xl bg-white/[0.02] border border-white/5">
|
||||
<div className="flex justify-between items-center text-[11px] font-semibold text-slate-300">
|
||||
<span>{label}</span>
|
||||
<span className="font-mono text-[10px] text-slate-500 uppercase">{themeTokens[key] || '#000'}</span>
|
||||
{
|
||||
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 }) => (
|
||||
<div key={key} className="space-y-2 p-3.5 rounded-2xl bg-white/[0.02] border border-white/5 hover:border-white/15 transition-all flex flex-col justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-start justify-between gap-1.5">
|
||||
<span className="text-xs font-bold text-white tracking-wide">{label}</span>
|
||||
<div
|
||||
className="w-4 h-4 rounded-full border border-white/20 shadow-sm shrink-0 mt-0.5"
|
||||
style={{ backgroundColor: themeTokens[key] || '#000000' }}
|
||||
/>
|
||||
</div>
|
||||
<div className={`inline-block px-1.5 py-0.5 rounded-md text-[10px] font-semibold border ${badgeColor}`}>
|
||||
{role}
|
||||
</div>
|
||||
<p className="text-[11px] text-slate-400 leading-tight pt-0.5">
|
||||
{desc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 pt-2 border-t border-white/5">
|
||||
<input
|
||||
type="color"
|
||||
value={themeTokens[key] || '#000000'}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={themeTokens[key] || ''}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="color"
|
||||
value={themeTokens[key] || '#000000'}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1218,6 +1397,14 @@ export default function ThemeSettingsPage() {
|
||||
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
<span>Save Theme Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Right Column: Live Interactive UI Preview (Sticky) ── */}
|
||||
<div className="xl:col-span-5 xl:sticky xl:top-20 space-y-6">
|
||||
<ThemeLivePreview tokens={themeTokens} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -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<string, any> = {};
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -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"],
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user