From 4dd7a5cb9bc71726549165f2031eec55ad72950d Mon Sep 17 00:00:00 2001 From: Nate Emmert Date: Tue, 17 Mar 2026 16:01:16 -0400 Subject: [PATCH] Persist theme to file-backed content and add forest/sunset themes --- README.md | 2 +- src/App.tsx | 34 +++++++++++++++++++++++++++++++--- src/index.css | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c72747e..d50d9da 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/src/App.tsx b/src/App.tsx index bbb9212..3236a6f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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({ + +
@@ -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} diff --git a/src/index.css b/src/index.css index a30773c..afe6367 100644 --- a/src/index.css +++ b/src/index.css @@ -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; }