import { useCallback, useEffect, useMemo, useState } from 'react' import { BookText, Plus, Save, Trash2 } from 'lucide-react' import { createAdminBlogPost, deleteAdminBlogPost, fetchAdminBlogAuthors, fetchAdminBlogCategories, fetchAdminBlogPosts, fetchAdminBlogTags, type BlogPost, type BlogStatus, type BlogTaxonomy, updateAdminBlogPost, } from '../blog' type EditorState = { id?: string title: string slug: string excerpt: string content: string contentPagePath: string status: BlogStatus publishedAt: string categories: string tags: string authorId: string } const EMPTY_EDITOR: EditorState = { title: '', slug: '', excerpt: '', content: '', contentPagePath: '', status: 'draft', publishedAt: '', categories: '', tags: '', authorId: '', } function toDateTimeInput(value?: number): string { if (!value || !Number.isFinite(value)) { return '' } const date = new Date(value) const offsetMs = date.getTimezoneOffset() * 60_000 return new Date(date.getTime() - offsetMs).toISOString().slice(0, 16) } function fromDateTimeInput(value: string): number | undefined { const trimmed = value.trim() if (!trimmed) { return undefined } const timestamp = Date.parse(trimmed) return Number.isFinite(timestamp) ? timestamp : undefined } function parseTokenList(value: string): string[] { return value .split(',') .map(token => token.trim()) .filter(Boolean) } function toEditor(post: BlogPost): EditorState { return { id: post.id, title: post.title, slug: post.slug, excerpt: post.excerpt ?? '', content: post.content ?? '', contentPagePath: post.contentPagePath ?? '', status: post.status ?? 'draft', publishedAt: toDateTimeInput(post.publishedAt), categories: (post.categories ?? []).join(', '), tags: (post.tags ?? []).join(', '), authorId: post.authorId ?? '', } } function formatDate(value?: number): string { if (!value) { return '-' } return new Date(value).toLocaleString() } export function AdminBlogPage() { const [posts, setPosts] = useState([]) const [categories, setCategories] = useState([]) const [tags, setTags] = useState([]) const [authors, setAuthors] = useState([]) const [selectedID, setSelectedID] = useState(null) const [editor, setEditor] = useState(EMPTY_EDITOR) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [notice, setNotice] = useState(null) const sortedPosts = useMemo( () => [...posts].sort((a, b) => (b.updatedAt ?? b.createdAt ?? 0) - (a.updatedAt ?? a.createdAt ?? 0)), [posts], ) const loadAll = useCallback(async () => { setLoading(true) setError(null) try { const [postsResult, categoriesResult, tagsResult, authorsResult] = await Promise.all([ fetchAdminBlogPosts(), fetchAdminBlogCategories(), fetchAdminBlogTags(), fetchAdminBlogAuthors(), ]) setPosts(postsResult) setCategories(categoriesResult) setTags(tagsResult) setAuthors(authorsResult) if (selectedID) { const current = postsResult.find(post => post.id === selectedID) if (current) { setEditor(toEditor(current)) } } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load blog administration data') } finally { setLoading(false) } }, [selectedID]) useEffect(() => { void loadAll() }, [loadAll]) const selectPost = (post: BlogPost) => { setSelectedID(post.id) setEditor(toEditor(post)) setNotice(null) setError(null) } const newPost = () => { setSelectedID(null) setEditor(EMPTY_EDITOR) setNotice(null) setError(null) } const savePost = async () => { const title = editor.title.trim() if (!title) { setError('Title is required') return } setSaving(true) setError(null) setNotice(null) const payload = { title, slug: editor.slug.trim() || undefined, excerpt: editor.excerpt.trim() || undefined, content: editor.content.trim() || undefined, contentPagePath: editor.contentPagePath.trim() || undefined, status: editor.status, publishedAt: fromDateTimeInput(editor.publishedAt), categories: parseTokenList(editor.categories), tags: parseTokenList(editor.tags), authorId: editor.authorId.trim() || undefined, } try { if (editor.id) { const updated = await updateAdminBlogPost(editor.id, payload) setNotice('Post updated') setSelectedID(updated.id) } else { const created = await createAdminBlogPost(payload) setNotice('Post created') setSelectedID(created.id) } await loadAll() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save blog post') } finally { setSaving(false) } } const removePost = async () => { if (!selectedID) { return } setSaving(true) setError(null) setNotice(null) try { await deleteAdminBlogPost(selectedID) setNotice('Post deleted') setSelectedID(null) setEditor(EMPTY_EDITOR) await loadAll() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to delete blog post') } finally { setSaving(false) } } return (

Blog Administration

{selectedID ? ( ) : null}
{error ?

{error}

: null} {notice ?

{notice}

: null}