'use client'; import { useCallback, useEffect, useState } from 'react'; import { Badge } from '@/components/ui'; import { Markdown } from '@/components/content/Markdown'; import { t } from '@/lib/i18n'; import { formatDate, slugify } from '@/lib/utils'; type PostTag = { tag: { id: string; name: string; slug: string } }; type Post = { id: string; slug: string; title: string; excerpt: string; body: string; status: string; featured: boolean; readingMinutes: number; publishedAt: string | null; updatedAt: string; author: { id: string; name: string }; tags: PostTag[]; }; type AuthorOption = { id: string; name: string }; type FormState = { id: string | null; title: string; slug: string; slugEdited: boolean; excerpt: string; body: string; status: 'draft' | 'published'; featured: boolean; authorId: string; tags: string; }; const EMPTY = (authorId: string): FormState => ({ id: null, title: '', slug: '', slugEdited: false, excerpt: '', body: '', status: 'draft', featured: false, authorId, tags: '', }); export function PostsManager({ authors, tagOptions }: { authors: AuthorOption[]; tagOptions: string[] }) { const [items, setItems] = useState([]); const [status, setStatus] = useState(''); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [form, setForm] = useState(null); const [showPreview, setShowPreview] = useState(false); const [formError, setFormError] = useState(null); const [busy, setBusy] = useState(false); const load = useCallback(async () => { setLoading(true); setError(null); try { const res = await fetch(`/api/admin/posts${status ? `?status=${status}` : ''}`); if (!res.ok) throw new Error('request failed'); const data = (await res.json()) as { posts: Post[] }; setItems(data.posts); } catch { setError(t('common.error')); } finally { setLoading(false); } }, [status]); useEffect(() => { void load(); }, [load]); function openCreate() { setFormError(null); setShowPreview(false); setForm(EMPTY(authors[0]?.id ?? '')); } function openEdit(post: Post) { setFormError(null); setShowPreview(false); setForm({ id: post.id, title: post.title, slug: post.slug, slugEdited: true, excerpt: post.excerpt, body: post.body, status: post.status === 'published' ? 'published' : 'draft', featured: post.featured, authorId: post.author.id, tags: post.tags.map(({ tag }) => tag.name).join(', '), }); } function setField(key: K, value: FormState[K]) { setForm((prev) => (prev ? { ...prev, [key]: value } : prev)); } async function save() { if (!form) return; setBusy(true); setFormError(null); const payload = { title: form.title, slug: form.slug, excerpt: form.excerpt, body: form.body, status: form.status, featured: form.featured, authorId: form.authorId, tags: form.tags .split(',') .map((s) => s.trim()) .filter(Boolean), }; const res = await fetch(form.id ? `/api/admin/posts/${form.id}` : '/api/admin/posts', { method: form.id ? 'PATCH' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); setBusy(false); if (!res.ok) { setFormError(((await res.json().catch(() => ({}))) as { error?: string }).error || t('common.error')); return; } setForm(null); await load(); } async function toggleStatus(post: Post) { setBusy(true); const next = post.status === 'published' ? 'draft' : 'published'; await fetch(`/api/admin/posts/${post.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: post.title, slug: post.slug, excerpt: post.excerpt, body: post.body, status: next, featured: post.featured, authorId: post.author.id, tags: post.tags.map(({ tag }) => tag.name), }), }); setBusy(false); await load(); } async function remove(post: Post) { if (!window.confirm(`${t('admin.delete')} “${post.title}”? This cannot be undone.`)) return; setBusy(true); await fetch(`/api/admin/posts/${post.id}`, { method: 'DELETE' }); setBusy(false); await load(); } const previewBody = form?.body ?? ''; const suggestedSlug = form ? (form.slugEdited ? form.slug : slugify(form.title)) : ''; return (

{t('admin.posts')}

{items.length} posts

{form && (

{form.id ? t('admin.editPost') : t('admin.newPost')}

{showPreview ? (

{form.title || t('admin.untitled')}

{t('admin.previewHint')}

) : (