x
This commit is contained in:
1 parent
2d0f49f750
commit
c2269ea090
2 files changed
+159
-2
No files matched your search
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { API_BASE, type StoreSettings } from '@homaroma/types'
|
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 = {
|
const defaultThemeTokens = {
|
||||||
brandPrimary: '#FF6B6B', brandPrimaryHover: '#FF4500', brandAccent: '#FFF200',
|
brandPrimary: '#FF6B6B', brandPrimaryHover: '#FF4500', brandAccent: '#FFF200',
|
||||||
@@ -38,6 +38,7 @@ function resolveFontPreset(tokens: Record<string, string>) {
|
|||||||
export default function ThemeSettingsPage() {
|
export default function ThemeSettingsPage() {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const [themeTokens, setThemeTokens] = useState<Record<string, string>>(defaultThemeTokens)
|
const [themeTokens, setThemeTokens] = useState<Record<string, string>>(defaultThemeTokens)
|
||||||
const [scrollRevealEnabled, setScrollRevealEnabled] = useState(true)
|
const [scrollRevealEnabled, setScrollRevealEnabled] = useState(true)
|
||||||
@@ -66,6 +67,95 @@ export default function ThemeSettingsPage() {
|
|||||||
.finally(() => setLoading(false))
|
.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) => {
|
const handleSave = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
@@ -89,10 +179,37 @@ export default function ThemeSettingsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-3xl space-y-8 animate-fade-in">
|
<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">
|
<div className="space-y-1">
|
||||||
<h1 className="text-3xl font-bold">Theme & Design</h1>
|
<h1 className="text-3xl font-bold">Theme & Design</h1>
|
||||||
<p className="text-slate-400 text-sm">Color tokens, typography, and scroll animation settings.</p>
|
<p className="text-slate-400 text-sm">Color tokens, typography, and scroll animation settings.</p>
|
||||||
</div>
|
</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">
|
<form onSubmit={handleSave} className="space-y-8">
|
||||||
{/* Theme Tokens */}
|
{/* Theme Tokens */}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
Reference in new issue
Block a user