Persist theme to file-backed content and add forest/sunset themes
This commit is contained in:
@@ -78,7 +78,7 @@ Persistent admin saves:
|
||||
- Enter a URL and click **Scan URL metadata** to auto-fill title, summary, domain, category, and icon when available.
|
||||
- Click **Save project** to apply updates instantly.
|
||||
- Saved edits are written to `data/admin-content.json` through the API server.
|
||||
- Use the **Theme** dropdown to switch between Sandstone, Ocean, and Midnight.
|
||||
- Use the **Theme** dropdown to switch between Sandstone, Ocean, Midnight, Forest, and Sunset.
|
||||
- Use **Remove project** to delete the selected project from your local Admin data.
|
||||
- Use **Reset project** or **Reset all** to restore defaults from `src/data/projects.ts`.
|
||||
|
||||
|
||||
+31
-3
@@ -7,9 +7,10 @@ import type { Project } from './data/projects'
|
||||
const THEME_STORAGE_KEY = 'portfolio-theme-v1'
|
||||
const ADMIN_CONTENT_API = '/api/admin-content'
|
||||
|
||||
type ThemeName = 'sand' | 'ocean' | 'midnight'
|
||||
type ThemeName = 'sand' | 'ocean' | 'midnight' | 'forest' | 'sunset'
|
||||
|
||||
type SiteContent = {
|
||||
theme: ThemeName
|
||||
homeEyebrow: string
|
||||
homeTitle: string
|
||||
homeIntro: string
|
||||
@@ -19,6 +20,7 @@ type SiteContent = {
|
||||
}
|
||||
|
||||
const defaultSiteContent: SiteContent = {
|
||||
theme: 'sand',
|
||||
homeEyebrow: 'Siteforge',
|
||||
homeTitle: 'Sites and Apps I Build',
|
||||
homeIntro:
|
||||
@@ -107,7 +109,13 @@ function createEmptyProject(existing: Project[]): Project {
|
||||
|
||||
function readStoredTheme(): ThemeName {
|
||||
const saved = localStorage.getItem(THEME_STORAGE_KEY)
|
||||
if (saved === 'ocean' || saved === 'midnight' || saved === 'sand') {
|
||||
if (
|
||||
saved === 'ocean' ||
|
||||
saved === 'midnight' ||
|
||||
saved === 'sand' ||
|
||||
saved === 'forest' ||
|
||||
saved === 'sunset'
|
||||
) {
|
||||
return saved
|
||||
}
|
||||
|
||||
@@ -139,6 +147,14 @@ async function fetchAdminContent(): Promise<
|
||||
return {
|
||||
projects: payload.projects,
|
||||
siteContent: {
|
||||
theme:
|
||||
payload.siteContent.theme === 'ocean' ||
|
||||
payload.siteContent.theme === 'midnight' ||
|
||||
payload.siteContent.theme === 'sand' ||
|
||||
payload.siteContent.theme === 'forest' ||
|
||||
payload.siteContent.theme === 'sunset'
|
||||
? payload.siteContent.theme
|
||||
: defaultSiteContent.theme,
|
||||
homeEyebrow: payload.siteContent.homeEyebrow ?? defaultSiteContent.homeEyebrow,
|
||||
homeTitle: payload.siteContent.homeTitle ?? defaultSiteContent.homeTitle,
|
||||
homeIntro: payload.siteContent.homeIntro ?? defaultSiteContent.homeIntro,
|
||||
@@ -774,6 +790,8 @@ function AdminPage({
|
||||
<option value="sand">Sandstone</option>
|
||||
<option value="ocean">Ocean</option>
|
||||
<option value="midnight">Midnight</option>
|
||||
<option value="forest">Forest</option>
|
||||
<option value="sunset">Sunset</option>
|
||||
</select>
|
||||
|
||||
<div className="admin-grid">
|
||||
@@ -1026,6 +1044,7 @@ function App() {
|
||||
|
||||
setManagedProjects(loaded.projects)
|
||||
setSiteContent(loaded.siteContent)
|
||||
setTheme(loaded.siteContent.theme)
|
||||
})()
|
||||
}, [])
|
||||
|
||||
@@ -1093,10 +1112,19 @@ function App() {
|
||||
const handleResetSiteContent = () => {
|
||||
const updatedSiteContent = structuredClone(defaultSiteContent)
|
||||
setSiteContent(updatedSiteContent)
|
||||
setTheme(updatedSiteContent.theme)
|
||||
void persistAdminContent(managedProjects, updatedSiteContent)
|
||||
}
|
||||
|
||||
const handleSaveSiteContent = (updatedSiteContent: SiteContent) => {
|
||||
setSiteContent(updatedSiteContent)
|
||||
setTheme(updatedSiteContent.theme)
|
||||
void persistAdminContent(managedProjects, updatedSiteContent)
|
||||
}
|
||||
|
||||
const handleThemeChange = (nextTheme: ThemeName) => {
|
||||
setTheme(nextTheme)
|
||||
const updatedSiteContent = { ...siteContent, theme: nextTheme }
|
||||
setSiteContent(updatedSiteContent)
|
||||
void persistAdminContent(managedProjects, updatedSiteContent)
|
||||
}
|
||||
@@ -1122,7 +1150,7 @@ function App() {
|
||||
onDeleteProject={handleDeleteProject}
|
||||
onCreateProject={handleCreateProject}
|
||||
theme={theme}
|
||||
onThemeChange={setTheme}
|
||||
onThemeChange={handleThemeChange}
|
||||
siteContent={siteContent}
|
||||
onSaveSiteContent={handleSaveSiteContent}
|
||||
onResetSiteContent={handleResetSiteContent}
|
||||
|
||||
@@ -59,6 +59,40 @@
|
||||
--input-bg: #121d2b;
|
||||
}
|
||||
|
||||
:root[data-theme='forest'] {
|
||||
--bg: #edf4ee;
|
||||
--ink: #1a2a20;
|
||||
--ink-muted: #446051;
|
||||
--line: #bfd6c4;
|
||||
--accent: #2f7a4e;
|
||||
--hero-glow: #9ad3ac;
|
||||
--bg-radial: #dceedd;
|
||||
--bg-bottom: #d9eadc;
|
||||
--surface: #ffffff;
|
||||
--surface-soft: #f6fbf7;
|
||||
--chip-bg: #edf6ef;
|
||||
--pill-bg: #dff0e3;
|
||||
--pill-ink: #24593a;
|
||||
--input-bg: #f8fcf8;
|
||||
}
|
||||
|
||||
:root[data-theme='sunset'] {
|
||||
--bg: #fff1e8;
|
||||
--ink: #3d1f1a;
|
||||
--ink-muted: #7d4a3d;
|
||||
--line: #f0c8b5;
|
||||
--accent: #d85b34;
|
||||
--hero-glow: #ffc08f;
|
||||
--bg-radial: #ffe1cc;
|
||||
--bg-bottom: #ffd8bf;
|
||||
--surface: #fffaf7;
|
||||
--surface-soft: #fff5ef;
|
||||
--chip-bg: #ffece0;
|
||||
--pill-bg: #ffe2d1;
|
||||
--pill-ink: #8a3f2a;
|
||||
--input-bg: #fff9f4;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user