Persist theme to file-backed content and add forest/sunset themes

This commit is contained in:
Nate Emmert
2026-03-17 16:01:16 -04:00
parent d6c6aaf234
commit 4dd7a5cb9b
3 changed files with 66 additions and 4 deletions
+1 -1
View File
@@ -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
View File
@@ -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}
+34
View File
@@ -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;
}