Skip to content

Commit c330aeb

Browse files
amide-initclaude
andcommitted
fix: give template picker its own dedicated page and sidebar link
Root cause: template picker was buried inside Portfolio Settings form — users looked under Settings in the sidebar and couldn't find it. Fix: - New AdminTemplatePage at /admin/template with just the 4 template cards - Added "Template" link to the sidebar Settings group (grid icon) - Removed template picker from AdminConfigPage to avoid duplication Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0b3355a commit c330aeb

4 files changed

Lines changed: 157 additions & 122 deletions

File tree

src/admin/pages/AdminConfigPage.tsx

Lines changed: 0 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -7,91 +7,6 @@ import {
77
import { useAdminAuthContext } from '../context/AdminAuthContext'
88
import { useConfigForm } from '../hooks/useConfigForm'
99

10-
const TEMPLATES = [
11-
{
12-
id: 'minimal' as const,
13-
label: 'Minimal',
14-
description: 'Dark, text-first, developer-focused',
15-
preview: (
16-
<svg viewBox="0 0 120 80" className="h-full w-full" aria-hidden="true">
17-
<rect width="120" height="80" fill="#050509" />
18-
<rect x="12" y="12" width="50" height="6" rx="2" fill="#6366f1" opacity="0.5" />
19-
<rect x="12" y="22" width="70" height="10" rx="2" fill="#f8fafc" opacity="0.9" />
20-
<rect x="12" y="36" width="55" height="4" rx="1" fill="#94a3b8" opacity="0.6" />
21-
<rect x="12" y="44" width="40" height="4" rx="1" fill="#94a3b8" opacity="0.4" />
22-
<rect x="12" y="58" width="28" height="14" rx="3" fill="#1e1e2e" stroke="#ffffff" strokeOpacity="0.1" />
23-
<rect x="46" y="58" width="28" height="14" rx="3" fill="#1e1e2e" stroke="#ffffff" strokeOpacity="0.1" />
24-
<rect x="80" y="58" width="28" height="14" rx="3" fill="#1e1e2e" stroke="#ffffff" strokeOpacity="0.1" />
25-
</svg>
26-
),
27-
},
28-
{
29-
id: 'classic' as const,
30-
label: 'Classic',
31-
description: 'Light, card-based, resume-style',
32-
preview: (
33-
<svg viewBox="0 0 120 80" className="h-full w-full" aria-hidden="true">
34-
<rect width="120" height="80" fill="#f8fafc" />
35-
<rect x="12" y="10" width="96" height="22" rx="3" fill="#ffffff" stroke="#e2e8f0" />
36-
<rect x="18" y="15" width="30" height="5" rx="1.5" fill="#1e293b" opacity="0.8" />
37-
<rect x="18" y="23" width="50" height="3" rx="1" fill="#64748b" opacity="0.5" />
38-
<rect x="12" y="38" width="29" height="18" rx="3" fill="#ffffff" stroke="#e2e8f0" />
39-
<rect x="47" y="38" width="29" height="18" rx="3" fill="#ffffff" stroke="#e2e8f0" />
40-
<rect x="82" y="38" width="26" height="18" rx="3" fill="#ffffff" stroke="#e2e8f0" />
41-
<rect x="16" y="42" width="18" height="3" rx="1" fill="#1e293b" opacity="0.7" />
42-
<rect x="16" y="48" width="22" height="2" rx="1" fill="#94a3b8" opacity="0.5" />
43-
<rect x="51" y="42" width="18" height="3" rx="1" fill="#1e293b" opacity="0.7" />
44-
<rect x="51" y="48" width="22" height="2" rx="1" fill="#94a3b8" opacity="0.5" />
45-
</svg>
46-
),
47-
},
48-
{
49-
id: 'bento' as const,
50-
label: 'Bento',
51-
description: 'Mosaic grid, creative layout',
52-
preview: (
53-
<svg viewBox="0 0 120 80" className="h-full w-full" aria-hidden="true">
54-
<rect width="120" height="80" fill="#0f0f1a" />
55-
<rect x="8" y="8" width="55" height="40" rx="4" fill="#1a1a2e" stroke="#ffffff" strokeOpacity="0.08" />
56-
<rect x="69" y="8" width="43" height="18" rx="4" fill="#1a1a2e" stroke="#ffffff" strokeOpacity="0.08" />
57-
<rect x="69" y="30" width="43" height="18" rx="4" fill="#1a1a2e" stroke="#ffffff" strokeOpacity="0.08" />
58-
<rect x="8" y="53" width="27" height="19" rx="4" fill="#1a1a2e" stroke="#ffffff" strokeOpacity="0.08" />
59-
<rect x="39" y="53" width="27" height="19" rx="4" fill="#1a1a2e" stroke="#ffffff" strokeOpacity="0.08" />
60-
<rect x="70" y="53" width="42" height="19" rx="4" fill="#6366f1" opacity="0.3" />
61-
<rect x="14" y="18" width="30" height="5" rx="1.5" fill="#f8fafc" opacity="0.8" />
62-
<rect x="14" y="27" width="42" height="3" rx="1" fill="#94a3b8" opacity="0.4" />
63-
</svg>
64-
),
65-
},
66-
{
67-
id: 'hacker' as const,
68-
label: 'Hacker',
69-
description: 'Terminal, green-on-black, ASCII',
70-
preview: (
71-
<svg viewBox="0 0 120 80" className="h-full w-full" aria-hidden="true">
72-
<rect width="120" height="80" fill="#000000" />
73-
<rect x="0" y="0" width="120" height="10" fill="#020d02" />
74-
<circle cx="8" cy="5" r="2" fill="#ff5f56" opacity="0.7" />
75-
<circle cx="15" cy="5" r="2" fill="#ffbd2e" opacity="0.7" />
76-
<circle cx="22" cy="5" r="2" fill="#27c93f" opacity="0.7" />
77-
<rect x="8" y="16" width="6" height="2" rx="0.5" fill="#004400" />
78-
<rect x="16" y="16" width="40" height="2" rx="0.5" fill="#00aa44" opacity="0.8" />
79-
<rect x="8" y="22" width="60" height="2" rx="0.5" fill="#006600" opacity="0.5" />
80-
<rect x="8" y="30" width="6" height="2" rx="0.5" fill="#004400" />
81-
<rect x="16" y="30" width="30" height="2" rx="0.5" fill="#00aa44" opacity="0.8" />
82-
<rect x="8" y="38" width="4" height="2" rx="0" fill="#002200" />
83-
<rect x="14" y="38" width="42" height="2" rx="0" fill="#00ff41" opacity="0.5" />
84-
<rect x="8" y="43" width="4" height="2" rx="0" fill="#002200" />
85-
<rect x="14" y="43" width="26" height="2" rx="0" fill="#00cc33" opacity="0.5" />
86-
<rect x="8" y="48" width="4" height="2" rx="0" fill="#002200" />
87-
<rect x="14" y="48" width="56" height="2" rx="0" fill="#00ff41" opacity="0.4" />
88-
<rect x="8" y="60" width="6" height="2" rx="0.5" fill="#004400" />
89-
<rect x="16" y="58" width="4" height="8" rx="0" fill="#00ff41" opacity="0.9" />
90-
</svg>
91-
),
92-
},
93-
]
94-
9510
export function AdminConfigPage() {
9611
const {
9712
config,
@@ -132,43 +47,6 @@ export function AdminConfigPage() {
13247
</p>
13348
</div>
13449

135-
<section className="space-y-3 rounded-xl border border-slate-800 bg-slate-900/40 p-4">
136-
<h3 className="text-sm font-semibold text-slate-200">Template</h3>
137-
<p className="text-xs text-slate-400">
138-
Choose the visual layout for your portfolio. Affects all public pages.
139-
</p>
140-
<div className="mt-2 grid grid-cols-2 gap-3 sm:grid-cols-4">
141-
{TEMPLATES.map(({ id, label, description, preview }) => {
142-
const isActive = (config.template ?? 'minimal') === id
143-
return (
144-
<button
145-
key={id}
146-
type="button"
147-
onClick={() => updateConfigField('template', id)}
148-
className={`relative flex flex-col overflow-hidden rounded-xl border text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 ${
149-
isActive
150-
? 'border-indigo-500 ring-2 ring-indigo-500/40'
151-
: 'border-slate-700 hover:border-slate-500'
152-
}`}
153-
>
154-
<div className="aspect-[3/2] w-full bg-slate-950">{preview}</div>
155-
<div className="p-2.5">
156-
<p className="flex items-center gap-1.5 text-xs font-semibold text-slate-200">
157-
{isActive && (
158-
<span className="inline-flex h-3.5 w-3.5 items-center justify-center rounded-full bg-indigo-500 text-[8px] text-white">
159-
160-
</span>
161-
)}
162-
{label}
163-
</p>
164-
<p className="mt-0.5 text-[10px] text-slate-500">{description}</p>
165-
</div>
166-
</button>
167-
)
168-
})}
169-
</div>
170-
</section>
171-
17250
<AdminHeroForm
17351
hero={config.hero}
17452
onEyebrowChange={(v) => updateHero('eyebrow', v)}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { useAdminAuthContext } from '../context/AdminAuthContext'
2+
import { useConfigForm } from '../hooks/useConfigForm'
3+
import { AdminFormFooter } from '../../components/admin'
4+
5+
const TEMPLATES = [
6+
{
7+
id: 'minimal' as const,
8+
label: 'Minimal',
9+
description: 'Dark, text-first, developer-focused',
10+
preview: (
11+
<svg viewBox="0 0 120 80" className="h-full w-full" aria-hidden="true">
12+
<rect width="120" height="80" fill="#050509" />
13+
<rect x="12" y="12" width="50" height="6" rx="2" fill="#6366f1" opacity="0.5" />
14+
<rect x="12" y="22" width="70" height="10" rx="2" fill="#f8fafc" opacity="0.9" />
15+
<rect x="12" y="36" width="55" height="4" rx="1" fill="#94a3b8" opacity="0.6" />
16+
<rect x="12" y="44" width="40" height="4" rx="1" fill="#94a3b8" opacity="0.4" />
17+
<rect x="12" y="58" width="28" height="14" rx="3" fill="#1e1e2e" stroke="#ffffff" strokeOpacity="0.1" />
18+
<rect x="46" y="58" width="28" height="14" rx="3" fill="#1e1e2e" stroke="#ffffff" strokeOpacity="0.1" />
19+
<rect x="80" y="58" width="28" height="14" rx="3" fill="#1e1e2e" stroke="#ffffff" strokeOpacity="0.1" />
20+
</svg>
21+
),
22+
},
23+
{
24+
id: 'classic' as const,
25+
label: 'Classic',
26+
description: 'Light, card-based, resume-style',
27+
preview: (
28+
<svg viewBox="0 0 120 80" className="h-full w-full" aria-hidden="true">
29+
<rect width="120" height="80" fill="#f8fafc" />
30+
<rect x="12" y="10" width="96" height="22" rx="3" fill="#ffffff" stroke="#e2e8f0" />
31+
<rect x="18" y="15" width="30" height="5" rx="1.5" fill="#1e293b" opacity="0.8" />
32+
<rect x="18" y="23" width="50" height="3" rx="1" fill="#64748b" opacity="0.5" />
33+
<rect x="12" y="38" width="29" height="18" rx="3" fill="#ffffff" stroke="#e2e8f0" />
34+
<rect x="47" y="38" width="29" height="18" rx="3" fill="#ffffff" stroke="#e2e8f0" />
35+
<rect x="82" y="38" width="26" height="18" rx="3" fill="#ffffff" stroke="#e2e8f0" />
36+
<rect x="16" y="42" width="18" height="3" rx="1" fill="#1e293b" opacity="0.7" />
37+
<rect x="16" y="48" width="22" height="2" rx="1" fill="#94a3b8" opacity="0.5" />
38+
<rect x="51" y="42" width="18" height="3" rx="1" fill="#1e293b" opacity="0.7" />
39+
<rect x="51" y="48" width="22" height="2" rx="1" fill="#94a3b8" opacity="0.5" />
40+
</svg>
41+
),
42+
},
43+
{
44+
id: 'bento' as const,
45+
label: 'Bento',
46+
description: 'Mosaic grid, creative layout',
47+
preview: (
48+
<svg viewBox="0 0 120 80" className="h-full w-full" aria-hidden="true">
49+
<rect width="120" height="80" fill="#0f0f1a" />
50+
<rect x="8" y="8" width="55" height="40" rx="4" fill="#1a1a2e" stroke="#ffffff" strokeOpacity="0.08" />
51+
<rect x="69" y="8" width="43" height="18" rx="4" fill="#1a1a2e" stroke="#ffffff" strokeOpacity="0.08" />
52+
<rect x="69" y="30" width="43" height="18" rx="4" fill="#1a1a2e" stroke="#ffffff" strokeOpacity="0.08" />
53+
<rect x="8" y="53" width="27" height="19" rx="4" fill="#1a1a2e" stroke="#ffffff" strokeOpacity="0.08" />
54+
<rect x="39" y="53" width="27" height="19" rx="4" fill="#1a1a2e" stroke="#ffffff" strokeOpacity="0.08" />
55+
<rect x="70" y="53" width="42" height="19" rx="4" fill="#6366f1" opacity="0.3" />
56+
<rect x="14" y="18" width="30" height="5" rx="1.5" fill="#f8fafc" opacity="0.8" />
57+
<rect x="14" y="27" width="42" height="3" rx="1" fill="#94a3b8" opacity="0.4" />
58+
</svg>
59+
),
60+
},
61+
{
62+
id: 'hacker' as const,
63+
label: 'Hacker',
64+
description: 'Terminal, green-on-black, ASCII',
65+
preview: (
66+
<svg viewBox="0 0 120 80" className="h-full w-full" aria-hidden="true">
67+
<rect width="120" height="80" fill="#000000" />
68+
<rect x="0" y="0" width="120" height="10" fill="#020d02" />
69+
<circle cx="8" cy="5" r="2" fill="#ff5f56" opacity="0.7" />
70+
<circle cx="15" cy="5" r="2" fill="#ffbd2e" opacity="0.7" />
71+
<circle cx="22" cy="5" r="2" fill="#27c93f" opacity="0.7" />
72+
<rect x="8" y="16" width="6" height="2" rx="0.5" fill="#004400" />
73+
<rect x="16" y="16" width="40" height="2" rx="0.5" fill="#00aa44" opacity="0.8" />
74+
<rect x="8" y="22" width="60" height="2" rx="0.5" fill="#006600" opacity="0.5" />
75+
<rect x="8" y="30" width="6" height="2" rx="0.5" fill="#004400" />
76+
<rect x="16" y="30" width="30" height="2" rx="0.5" fill="#00aa44" opacity="0.8" />
77+
<rect x="8" y="38" width="4" height="2" rx="0" fill="#002200" />
78+
<rect x="14" y="38" width="42" height="2" rx="0" fill="#00ff41" opacity="0.5" />
79+
<rect x="8" y="43" width="4" height="2" rx="0" fill="#002200" />
80+
<rect x="14" y="43" width="26" height="2" rx="0" fill="#00cc33" opacity="0.5" />
81+
<rect x="8" y="48" width="4" height="2" rx="0" fill="#002200" />
82+
<rect x="14" y="48" width="56" height="2" rx="0" fill="#00ff41" opacity="0.4" />
83+
<rect x="8" y="60" width="6" height="2" rx="0.5" fill="#004400" />
84+
<rect x="16" y="58" width="4" height="8" rx="0" fill="#00ff41" opacity="0.9" />
85+
</svg>
86+
),
87+
},
88+
]
89+
90+
export function AdminTemplatePage() {
91+
const { config, setConfig, error, saveSuccess, handleSave, isBusy, viewState } =
92+
useAdminAuthContext()
93+
const { updateConfigField } = useConfigForm(config, setConfig)
94+
95+
if (!config || (viewState !== 'ready' && viewState !== 'saving')) return null
96+
97+
return (
98+
<form onSubmit={handleSave} className="space-y-6">
99+
<div>
100+
<h2 className="text-lg font-semibold text-slate-100">Template</h2>
101+
<p className="mt-1 text-sm text-slate-400">
102+
Choose the visual layout for your portfolio. Changes are committed to the repo and trigger a rebuild.
103+
</p>
104+
</div>
105+
106+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
107+
{TEMPLATES.map(({ id, label, description, preview }) => {
108+
const isActive = (config.template ?? 'minimal') === id
109+
return (
110+
<button
111+
key={id}
112+
type="button"
113+
onClick={() => updateConfigField('template', id)}
114+
className={`relative flex flex-col overflow-hidden rounded-xl border text-left transition focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 ${
115+
isActive
116+
? 'border-indigo-500 ring-2 ring-indigo-500/40'
117+
: 'border-slate-700 hover:border-slate-500'
118+
}`}
119+
>
120+
<div className="aspect-[3/2] w-full bg-slate-950">{preview}</div>
121+
<div className="p-3">
122+
<p className="flex items-center gap-1.5 text-sm font-semibold text-slate-200">
123+
{isActive && (
124+
<span className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-indigo-500 text-[9px] text-white">
125+
126+
</span>
127+
)}
128+
{label}
129+
</p>
130+
<p className="mt-0.5 text-xs text-slate-500">{description}</p>
131+
</div>
132+
</button>
133+
)
134+
})}
135+
</div>
136+
137+
<AdminFormFooter
138+
errorMessage={error?.message ?? null}
139+
saveSuccessMessage={saveSuccess}
140+
isSaving={viewState === 'saving'}
141+
isBusy={isBusy}
142+
/>
143+
</form>
144+
)
145+
}

src/components/admin/AdminSidebar.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ const SIDEBAR_ITEMS = [
4646
] as const
4747

4848
const SETTINGS_ITEMS = [
49+
{
50+
to: '/admin/template',
51+
end: false,
52+
label: 'Template',
53+
icon: (
54+
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
55+
<path strokeLinecap="round" strokeLinejoin="round" d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
56+
</svg>
57+
),
58+
},
4959
{
5060
to: '/admin/settings',
5161
end: false,

src/main.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { AdminConfigPage } from './admin/pages/AdminConfigPage'
1414
import { AdminProjectsPage } from './admin/pages/AdminProjectsPage'
1515
import { AdminVideosPage } from './admin/pages/AdminVideosPage'
1616
import { AdminSettingsPage } from './admin/pages/AdminSettingsPage'
17+
import { AdminTemplatePage } from './admin/pages/AdminTemplatePage'
1718

1819
createRoot(document.getElementById('root')!).render(
1920
<StrictMode>
@@ -32,6 +33,7 @@ createRoot(document.getElementById('root')!).render(
3233
<Route path="projects" element={<AdminProjectsPage />} />
3334
<Route path="blogs" element={<AdminBlogsPage />} />
3435
<Route path="videos" element={<AdminVideosPage />} />
36+
<Route path="template" element={<AdminTemplatePage />} />
3537
<Route path="settings" element={<AdminSettingsPage />} />
3638
</Route>
3739
</Routes>

0 commit comments

Comments
 (0)