Skip to content

Commit 4d45174

Browse files
amide-initclaude
andcommitted
feat: redesign admin content pages with shadcn/ui
Rewrite Settings, Projects, Videos, and Blogs pages using shadcn/ui Card, Input, Textarea, Button, Switch, and Dialog components. Replace raw checkboxes with Switch toggles, replace custom confirm modal in Settings with Dialog, replace emerald/slate colors with zinc/blue throughout. Preserve LuxeEditor in BlogCard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ccc3de4 commit 4d45174

4 files changed

Lines changed: 445 additions & 587 deletions

File tree

src/admin/pages/AdminBlogsPage.tsx

Lines changed: 70 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,37 @@
11
import { LuxeEditor, getEditorJSON } from 'luxe-edit'
22
import 'luxe-edit/index.css'
3+
import { Plus, Trash2, AlertCircle, CheckCircle2, Loader2, RefreshCw } from 'lucide-react'
34
import { useAdminAuthContext } from '../context/AdminAuthContext'
45
import { useBlogsStore } from '../hooks/useBlogsStore'
56
import type { Blog } from '../../types/contentTypes'
7+
import { Button } from '../../components/ui/button'
8+
import { Input } from '../../components/ui/input'
9+
import { Card, CardContent, CardHeader } from '../../components/ui/card'
610

711
function formatDate(iso: string) {
812
try {
9-
return new Date(iso).toLocaleDateString(undefined, {
10-
year: 'numeric',
11-
month: 'short',
12-
day: 'numeric',
13-
})
13+
return new Date(iso).toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' })
1414
} catch {
1515
return iso
1616
}
1717
}
1818

1919
// ── Minimal markdown → Lexical JSON ──────────────────────────────────────────
20-
// Handles headings, bold, italic, strikethrough, and paragraphs.
21-
// Returns a JSON string suitable for initialConfig.editorState.
2220

2321
function makeText(text: string, format = 0) {
2422
return { detail: 0, format, mode: 'normal', style: '', text, type: 'text', version: 1 }
2523
}
2624

2725
function parseInline(line: string) {
2826
const nodes: object[] = []
29-
// Tokenise bold (**), italic (*/_), strikethrough (~~)
3027
const re = /(\*\*(.+?)\*\*|\*(.+?)\*|_(.+?)_|~~(.+?)~~|([^*_~]+))/g
3128
let m: RegExpExecArray | null
3229
while ((m = re.exec(line)) !== null) {
33-
if (m[2] != null) nodes.push(makeText(m[2], 1)) // bold
34-
else if (m[3] != null) nodes.push(makeText(m[3], 2)) // *italic*
35-
else if (m[4] != null) nodes.push(makeText(m[4], 2)) // _italic_
36-
else if (m[5] != null) nodes.push(makeText(m[5], 4)) // strikethrough
37-
else if (m[6] != null) nodes.push(makeText(m[6], 0)) // plain
30+
if (m[2] != null) nodes.push(makeText(m[2], 1))
31+
else if (m[3] != null) nodes.push(makeText(m[3], 2))
32+
else if (m[4] != null) nodes.push(makeText(m[4], 2))
33+
else if (m[5] != null) nodes.push(makeText(m[5], 4))
34+
else if (m[6] != null) nodes.push(makeText(m[6], 0))
3835
}
3936
return nodes.length ? nodes : [makeText(line, 0)]
4037
}
@@ -57,6 +54,7 @@ function markdownToLexicalJSON(md: string): string {
5754
if (!blocks.length) blocks.push(makeBlock('paragraph', []))
5855
return JSON.stringify({ root: { children: blocks, direction: 'ltr', format: '', indent: 0, type: 'root', version: 1 } })
5956
}
57+
6058
// ─────────────────────────────────────────────────────────────────────────────
6159

6260
export function AdminBlogsPage() {
@@ -65,56 +63,46 @@ export function AdminBlogsPage() {
6563

6664
if (store.loading) {
6765
return (
68-
<div className="rounded-xl border border-slate-800 bg-slate-900/40 p-8">
69-
<p className="text-sm text-slate-400">Loading blogs…</p>
70-
</div>
66+
<Card className="border-zinc-800 bg-zinc-900">
67+
<CardContent className="flex items-center gap-3 py-8">
68+
<Loader2 className="h-4 w-4 animate-spin text-zinc-500" />
69+
<p className="text-sm text-zinc-400">Loading blogs…</p>
70+
</CardContent>
71+
</Card>
7172
)
7273
}
7374

7475
return (
7576
<div className="space-y-6">
7677
<div className="flex items-center justify-between">
7778
<div>
78-
<h2 className="text-lg font-semibold text-slate-100">Blogs</h2>
79-
<p className="mt-1 text-sm text-slate-400">
80-
Store in <code className="rounded bg-slate-800 px-1">data/blogs.json</code>
79+
<h1 className="text-lg font-semibold text-zinc-100">Blogs</h1>
80+
<p className="mt-1 text-sm text-zinc-400">
81+
Stored in <code className="rounded bg-zinc-800 px-1 text-zinc-300">data/blogs.json</code>
8182
</p>
8283
</div>
83-
<div className="flex items-center gap-3">
84-
<button
85-
type="button"
86-
onClick={store.add}
87-
className="rounded-md border border-slate-600 px-4 py-2 text-sm font-medium text-slate-200 transition hover:bg-slate-800"
88-
>
89-
+ Add blog
90-
</button>
91-
<button
92-
type="button"
93-
onClick={store.persist}
94-
disabled={store.saving}
95-
className="rounded-md bg-emerald-500 px-4 py-2 text-sm font-semibold text-slate-950 transition hover:bg-emerald-400 disabled:opacity-60"
96-
>
97-
{store.saving ? 'Saving…' : 'Save'}
98-
</button>
84+
<div className="flex items-center gap-2">
85+
<Button type="button" variant="outline" size="sm" onClick={store.add} className="gap-1.5 border-zinc-700 bg-zinc-900 text-zinc-300 hover:bg-zinc-800 hover:text-zinc-100">
86+
<Plus className="h-3.5 w-3.5" /> Add blog
87+
</Button>
88+
<Button type="button" size="sm" onClick={store.persist} disabled={store.saving} className="min-w-16 bg-blue-600 text-white hover:bg-blue-500">
89+
{store.saving ? <><Loader2 className="h-3.5 w-3.5 animate-spin" /> Saving…</> : 'Save'}
90+
</Button>
9991
</div>
10092
</div>
10193

10294
{store.error && (
103-
<div className="flex items-center justify-between gap-3 rounded-lg border border-rose-800 bg-rose-950/40 p-3 text-sm text-rose-400">
104-
<span>{store.error}</span>
105-
<button
106-
type="button"
107-
onClick={store.reload}
108-
className="shrink-0 rounded px-2 py-1 text-xs font-medium text-rose-200 hover:bg-rose-900/60"
109-
>
110-
Reload
111-
</button>
95+
<div className="flex items-center justify-between gap-3 rounded-lg border border-red-800/50 bg-red-950/30 px-3 py-2.5 text-sm text-red-400">
96+
<span className="flex items-start gap-2"><AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />{store.error}</span>
97+
<Button type="button" variant="ghost" size="sm" onClick={store.reload} className="shrink-0 gap-1.5 text-red-300 hover:bg-red-950/50 hover:text-red-200">
98+
<RefreshCw className="h-3 w-3" /> Reload
99+
</Button>
112100
</div>
113101
)}
114102
{store.success && (
115-
<p className="rounded-lg border border-emerald-800 bg-emerald-950/40 p-3 text-sm text-emerald-400">
116-
{store.success}
117-
</p>
103+
<div className="flex items-start gap-2 rounded-lg border border-green-800/50 bg-green-950/30 px-3 py-2.5 text-sm text-green-400">
104+
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" />{store.success}
105+
</div>
118106
)}
119107

120108
<div className="space-y-4">
@@ -127,7 +115,7 @@ export function AdminBlogsPage() {
127115
/>
128116
))}
129117
{store.items.length === 0 && (
130-
<p className="rounded-xl border border-dashed border-slate-700 p-8 text-center text-sm text-slate-500">
118+
<p className="rounded-xl border border-dashed border-zinc-800 py-10 text-center text-sm text-zinc-600">
131119
No blogs yet. Click "Add blog" to create one.
132120
</p>
133121
)}
@@ -145,49 +133,43 @@ function BlogCard({
145133
onUpdate: (u: Partial<Pick<Blog, 'title' | 'content' | 'contentJSON'>>) => void
146134
onRemove: () => void
147135
}) {
148-
// For legacy blogs (markdown only), seed the editor from converted markdown.
149136
const legacyEditorState =
150-
blog.contentJSON == null && blog.content
151-
? markdownToLexicalJSON(blog.content)
152-
: undefined
137+
blog.contentJSON == null && blog.content ? markdownToLexicalJSON(blog.content) : undefined
153138

154139
return (
155-
<div className="rounded-xl border border-slate-800 bg-slate-900/40 p-5">
156-
<div className="flex items-start justify-between gap-4">
157-
<div className="flex-1 space-y-3">
158-
<input
159-
type="text"
160-
placeholder="Title"
161-
className="w-full rounded-md border border-slate-700 bg-slate-950 px-3 py-2 text-sm font-medium text-slate-100 focus:border-emerald-500 focus:outline-none"
162-
value={blog.title}
163-
onChange={(e) => onUpdate({ title: e.target.value })}
164-
/>
165-
<LuxeEditor
166-
colorScheme="dark"
167-
initialConfig={{
168-
namespace: `blog-${blog.id}`,
169-
...(legacyEditorState ? { editorState: legacyEditorState } : {}),
170-
}}
171-
initialJSON={blog.contentJSON}
172-
onChange={(editorState) => {
173-
onUpdate({ contentJSON: getEditorJSON(editorState) })
174-
}}
175-
ignoreInitialChange
176-
/>
177-
<p className="text-[11px] text-slate-500">
178-
Created {formatDate(blog.createdAt)} · Updated{' '}
179-
{formatDate(blog.updatedAt)}
180-
</p>
140+
<Card className="border-zinc-800 bg-zinc-900">
141+
<CardHeader className="pb-3 pt-4 px-4">
142+
<div className="flex items-center justify-between">
143+
<span className="text-xs font-medium text-zinc-500">Blog post</span>
144+
<Button type="button" variant="ghost" size="icon" onClick={onRemove} className="h-7 w-7 text-zinc-600 hover:text-red-400 hover:bg-red-950/30">
145+
<Trash2 className="h-3.5 w-3.5" />
146+
</Button>
181147
</div>
182-
<button
183-
type="button"
184-
onClick={onRemove}
185-
className="rounded px-2 py-1 text-xs text-slate-500 hover:bg-slate-800 hover:text-rose-400"
186-
aria-label="Remove blog"
187-
>
188-
Delete
189-
</button>
190-
</div>
191-
</div>
148+
</CardHeader>
149+
<CardContent className="space-y-3 px-4 pb-4">
150+
<Input
151+
type="text"
152+
placeholder="Title"
153+
value={blog.title}
154+
onChange={(e) => onUpdate({ title: e.target.value })}
155+
className="border-zinc-700 bg-zinc-950 text-zinc-100 placeholder:text-zinc-600 focus-visible:ring-blue-500"
156+
/>
157+
<LuxeEditor
158+
colorScheme="dark"
159+
initialConfig={{
160+
namespace: `blog-${blog.id}`,
161+
...(legacyEditorState ? { editorState: legacyEditorState } : {}),
162+
}}
163+
initialJSON={blog.contentJSON}
164+
onChange={(editorState) => {
165+
onUpdate({ contentJSON: getEditorJSON(editorState) })
166+
}}
167+
ignoreInitialChange
168+
/>
169+
<p className="text-[11px] text-zinc-600">
170+
Created {formatDate(blog.createdAt)} · Updated {formatDate(blog.updatedAt)}
171+
</p>
172+
</CardContent>
173+
</Card>
192174
)
193175
}

0 commit comments

Comments
 (0)