x
This commit is contained in:
1 parent
2d0f49f750
commit
c2269ea090
2 files changed
+162
-5
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 { 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,9 +179,36 @@ export default function ThemeSettingsPage() {
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl space-y-8 animate-fade-in">
|
||||
<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 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">
|
||||
|
||||
@@ -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