This commit is contained in:
Your Name committed 2026-09-23 06:08:36 +07:00
1 parent 2d0f49f750
commit c2269ea090
2 files changed
+159 -2

No files matched your search

+119 -2
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import { useState, useEffect, useRef } from 'react'
import { API_BASE, type StoreSettings } from '@homaroma/types'
import { Save, Loader2, Sparkles } from 'lucide-react'
import { Save, Loader2, Sparkles, Download, Upload } from 'lucide-react'
const defaultThemeTokens = {
brandPrimary: '#FF6B6B', brandPrimaryHover: '#FF4500', brandAccent: '#FFF200',
@@ -38,6 +38,7 @@ function resolveFontPreset(tokens: Record<string, string>) {
export default function ThemeSettingsPage() {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
const [themeTokens, setThemeTokens] = useState<Record<string, string>>(defaultThemeTokens)
const [scrollRevealEnabled, setScrollRevealEnabled] = useState(true)
@@ -66,6 +67,95 @@ export default function ThemeSettingsPage() {
.finally(() => setLoading(false))
}, [])
const handleExport = () => {
const data = {
version: '1.0',
exportedAt: new Date().toISOString(),
themeTokens,
scrollRevealEnabled,
scrollRevealStyle,
scrollRevealDuration: parseInt(scrollRevealDuration) || 700,
headerStyle,
}
const dataStr = JSON.stringify(data, null, 2)
const blob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `theme-settings-${new Date().toISOString().split('T')[0]}.json`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = (event) => {
try {
const content = event.target?.result as string
const parsed = JSON.parse(content)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('Invalid JSON format: expected an object')
}
// Extract tokens (support "tokens", "themeTokens", or root dictionary)
const extractedTokens =
(parsed.tokens && typeof parsed.tokens === 'object' && !Array.isArray(parsed.tokens)) ? parsed.tokens :
(parsed.themeTokens && typeof parsed.themeTokens === 'object' && !Array.isArray(parsed.themeTokens)) ? parsed.themeTokens :
(parsed.brandPrimary || parsed.surfacePage || parsed.textMain) ? parsed : null
if (extractedTokens) {
setThemeTokens(cur => ({ ...cur, ...extractedTokens }))
}
// Extract Scroll Reveal (support nested scrollReveal object or flat properties)
if (parsed.scrollReveal && typeof parsed.scrollReveal === 'object') {
if (typeof parsed.scrollReveal.enabled === 'boolean') {
setScrollRevealEnabled(parsed.scrollReveal.enabled)
}
if (typeof parsed.scrollReveal.style === 'string') {
setScrollRevealStyle(parsed.scrollReveal.style)
}
if (parsed.scrollReveal.duration !== undefined) {
setScrollRevealDuration(parsed.scrollReveal.duration.toString())
}
} else {
if (typeof parsed.scrollRevealEnabled === 'boolean') {
setScrollRevealEnabled(parsed.scrollRevealEnabled)
}
if (typeof parsed.scrollRevealStyle === 'string') {
setScrollRevealStyle(parsed.scrollRevealStyle)
}
if (parsed.scrollRevealDuration !== undefined) {
setScrollRevealDuration(parsed.scrollRevealDuration.toString())
}
}
// Extract Header Style (support nested header object or flat headerStyle)
if (parsed.header && typeof parsed.header === 'object' && typeof parsed.header.style === 'string') {
setHeaderStyle(parsed.header.style)
} else if (typeof parsed.headerStyle === 'string') {
setHeaderStyle(parsed.headerStyle)
}
alert('Theme settings loaded from JSON! Click "Save Theme Settings" to apply changes.')
} catch (err) {
console.error(err)
alert('Failed to parse JSON file. Please make sure it is a valid theme settings JSON.')
} finally {
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
}
reader.readAsText(file)
}
const handleSave = async (e: React.FormEvent) => {
e.preventDefault()
setSaving(true)
@@ -89,10 +179,37 @@ export default function ThemeSettingsPage() {
return (
<div className="max-w-3xl space-y-8 animate-fade-in">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div className="space-y-1">
<h1 className="text-3xl font-bold">Theme & Design</h1>
<p className="text-slate-400 text-sm">Color tokens, typography, and scroll animation settings.</p>
</div>
<div className="flex items-center gap-2">
<input
type="file"
ref={fileInputRef}
onChange={handleImport}
accept=".json,application/json"
className="hidden"
/>
<button
type="button"
onClick={() => fileInputRef.current?.click()}
className="btn-secondary py-2 px-3.5 text-xs font-semibold flex items-center gap-1.5"
>
<Upload className="w-4 h-4 text-cyan-glow" />
Import JSON
</button>
<button
type="button"
onClick={handleExport}
className="btn-secondary py-2 px-3.5 text-xs font-semibold flex items-center gap-1.5"
>
<Download className="w-4 h-4 text-slate-300" />
Export JSON
</button>
</div>
</div>
<form onSubmit={handleSave} className="space-y-8">
{/* Theme Tokens */}
+40
View File
@@ -0,0 +1,40 @@
{
"version": "1.0",
"themeTokens": {
"brandPrimary": "#B8956A",
"brandPrimaryHover": "#A07D3A",
"brandAccent": "#D4B97A",
"brandWarm": "#D4785A",
"brandCool": "#EDE6DD",
"brandSuccess": "#5B9A4D",
"brandSoft": "#DABE7E",
"surfacePage": "#F5F0EB",
"surfaceNavRest": "#14171C",
"surfaceCard": "#FFFFFF",
"textMain": "#EDE7E1",
"mutedText": "#BDB7B0",
"linePrimary": "#B8956A",
"linePrimaryHover": "#D4B97A",
"captionText": "#A9A39D",
"borderColor": "#2A2E34",
"surfaceMuted": "#1B1F24",
"fontBody": "",
"fontDisplay": "",
"shadowCard": "0 1px 4px rgba(0,0,0,0.12)",
"shadowCardHover": "0 4px 12px rgba(0,0,0,0.18)",
"radiusBase": "8px",
"radiusCard": "16px"
},
"scrollRevealEnabled": true,
"scrollRevealStyle": "fade-up",
"scrollRevealDuration": 700,
"headerStyle": "single_line"
}