11import { LuxeEditor , getEditorJSON } from 'luxe-edit'
22import 'luxe-edit/index.css'
3+ import { Plus , Trash2 , AlertCircle , CheckCircle2 , Loader2 , RefreshCw } from 'lucide-react'
34import { useAdminAuthContext } from '../context/AdminAuthContext'
45import { useBlogsStore } from '../hooks/useBlogsStore'
56import 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
711function 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
2321function makeText ( text : string , format = 0 ) {
2422 return { detail : 0 , format, mode : 'normal' , style : '' , text, type : 'text' , version : 1 }
2523}
2624
2725function 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
6260export 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