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.
|
- 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.
|
- Click **Save project** to apply updates instantly.
|
||||||
- Saved edits are written to `data/admin-content.json` through the API server.
|
- 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 **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`.
|
- 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 THEME_STORAGE_KEY = 'portfolio-theme-v1'
|
||||||
const ADMIN_CONTENT_API = '/api/admin-content'
|
const ADMIN_CONTENT_API = '/api/admin-content'
|
||||||
|
|
||||||
type ThemeName = 'sand' | 'ocean' | 'midnight'
|
type ThemeName = 'sand' | 'ocean' | 'midnight' | 'forest' | 'sunset'
|
||||||
|
|
||||||
type SiteContent = {
|
type SiteContent = {
|
||||||
|
theme: ThemeName
|
||||||
homeEyebrow: string
|
homeEyebrow: string
|
||||||
homeTitle: string
|
homeTitle: string
|
||||||
homeIntro: string
|
homeIntro: string
|
||||||
@@ -19,6 +20,7 @@ type SiteContent = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultSiteContent: SiteContent = {
|
const defaultSiteContent: SiteContent = {
|
||||||
|
theme: 'sand',
|
||||||
homeEyebrow: 'Siteforge',
|
homeEyebrow: 'Siteforge',
|
||||||
homeTitle: 'Sites and Apps I Build',
|
homeTitle: 'Sites and Apps I Build',
|
||||||
homeIntro:
|
homeIntro:
|
||||||
@@ -107,7 +109,13 @@ function createEmptyProject(existing: Project[]): Project {
|
|||||||
|
|
||||||
function readStoredTheme(): ThemeName {
|
function readStoredTheme(): ThemeName {
|
||||||
const saved = localStorage.getItem(THEME_STORAGE_KEY)
|
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
|
return saved
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +147,14 @@ async function fetchAdminContent(): Promise<
|
|||||||
return {
|
return {
|
||||||
projects: payload.projects,
|
projects: payload.projects,
|
||||||
siteContent: {
|
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,
|
homeEyebrow: payload.siteContent.homeEyebrow ?? defaultSiteContent.homeEyebrow,
|
||||||
homeTitle: payload.siteContent.homeTitle ?? defaultSiteContent.homeTitle,
|
homeTitle: payload.siteContent.homeTitle ?? defaultSiteContent.homeTitle,
|
||||||
homeIntro: payload.siteContent.homeIntro ?? defaultSiteContent.homeIntro,
|
homeIntro: payload.siteContent.homeIntro ?? defaultSiteContent.homeIntro,
|
||||||
@@ -774,6 +790,8 @@ function AdminPage({
|
|||||||
<option value="sand">Sandstone</option>
|
<option value="sand">Sandstone</option>
|
||||||
<option value="ocean">Ocean</option>
|
<option value="ocean">Ocean</option>
|
||||||
<option value="midnight">Midnight</option>
|
<option value="midnight">Midnight</option>
|
||||||
|
<option value="forest">Forest</option>
|
||||||
|
<option value="sunset">Sunset</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div className="admin-grid">
|
<div className="admin-grid">
|
||||||
@@ -1026,6 +1044,7 @@ function App() {
|
|||||||
|
|
||||||
setManagedProjects(loaded.projects)
|
setManagedProjects(loaded.projects)
|
||||||
setSiteContent(loaded.siteContent)
|
setSiteContent(loaded.siteContent)
|
||||||
|
setTheme(loaded.siteContent.theme)
|
||||||
})()
|
})()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -1093,10 +1112,19 @@ function App() {
|
|||||||
const handleResetSiteContent = () => {
|
const handleResetSiteContent = () => {
|
||||||
const updatedSiteContent = structuredClone(defaultSiteContent)
|
const updatedSiteContent = structuredClone(defaultSiteContent)
|
||||||
setSiteContent(updatedSiteContent)
|
setSiteContent(updatedSiteContent)
|
||||||
|
setTheme(updatedSiteContent.theme)
|
||||||
void persistAdminContent(managedProjects, updatedSiteContent)
|
void persistAdminContent(managedProjects, updatedSiteContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveSiteContent = (updatedSiteContent: SiteContent) => {
|
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)
|
setSiteContent(updatedSiteContent)
|
||||||
void persistAdminContent(managedProjects, updatedSiteContent)
|
void persistAdminContent(managedProjects, updatedSiteContent)
|
||||||
}
|
}
|
||||||
@@ -1122,7 +1150,7 @@ function App() {
|
|||||||
onDeleteProject={handleDeleteProject}
|
onDeleteProject={handleDeleteProject}
|
||||||
onCreateProject={handleCreateProject}
|
onCreateProject={handleCreateProject}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onThemeChange={setTheme}
|
onThemeChange={handleThemeChange}
|
||||||
siteContent={siteContent}
|
siteContent={siteContent}
|
||||||
onSaveSiteContent={handleSaveSiteContent}
|
onSaveSiteContent={handleSaveSiteContent}
|
||||||
onResetSiteContent={handleResetSiteContent}
|
onResetSiteContent={handleResetSiteContent}
|
||||||
|
|||||||
@@ -59,6 +59,40 @@
|
|||||||
--input-bg: #121d2b;
|
--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;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user