Append Blog Posts management card
cgen-793a605130df4e51961ee306e6b97805
This commit is contained in:
parent
ccc666ba7f
commit
6a6b93317b
1 changed files with 108 additions and 0 deletions
|
|
@ -113,6 +113,48 @@ export default function Admin() {
|
|||
);
|
||||
}
|
||||
|
||||
const [blogPosts, setBlogPosts] = useState<any[]>([]);
|
||||
const [loadingPosts, setLoadingPosts] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
setLoadingPosts(true);
|
||||
const res = await fetch("/api/blog?limit=100");
|
||||
const data = res.ok ? await res.json() : [];
|
||||
if (Array.isArray(data)) setBlogPosts(data);
|
||||
} catch (e) {
|
||||
console.warn("Failed to load blog posts:", e);
|
||||
} finally {
|
||||
setLoadingPosts(false);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const savePost = async (idx: number) => {
|
||||
const p = blogPosts[idx];
|
||||
const payload = { ...p, slug: (p.slug || p.title || "").toLowerCase().trim().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-") };
|
||||
const res = await fetch("/api/blog", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) return aethexToast.error({ title: "Save failed", description: await res.text().catch(() => "") });
|
||||
const saved = await res.json();
|
||||
const next = blogPosts.slice();
|
||||
next[idx] = saved;
|
||||
setBlogPosts(next);
|
||||
aethexToast.success({ title: "Saved", description: saved.title });
|
||||
};
|
||||
|
||||
const deletePost = async (idx: number) => {
|
||||
const p = blogPosts[idx];
|
||||
const res = await fetch(`/api/blog/${encodeURIComponent(p.slug)}`, { method: "DELETE" });
|
||||
if (!res.ok) return aethexToast.error({ title: "Delete failed", description: await res.text().catch(() => "") });
|
||||
setBlogPosts(blogPosts.filter((_, i) => i !== idx));
|
||||
aethexToast.info({ title: "Deleted", description: p.title });
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="min-h-screen bg-aethex-gradient py-12">
|
||||
|
|
@ -191,6 +233,72 @@ export default function Admin() {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Blog Posts Management */}
|
||||
<Card className="bg-card/50 border-border/50 md:col-span-2 lg:col-span-3">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<PenTool className="h-5 w-5 text-aethex-400" />
|
||||
<CardTitle className="text-lg">Blog Posts</CardTitle>
|
||||
</div>
|
||||
<CardDescription>Manage blog content stored in Supabase</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={loadingPosts}
|
||||
onClick={async () => {
|
||||
try {
|
||||
setLoadingPosts(true);
|
||||
const res = await fetch("/api/blog?limit=100");
|
||||
const data = res.ok ? await res.json() : [];
|
||||
if (Array.isArray(data)) setBlogPosts(data);
|
||||
} finally {
|
||||
setLoadingPosts(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => setBlogPosts([{ title: "New Post", slug: "new-post", category: "General" }, ...blogPosts])}
|
||||
>
|
||||
Add Post
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{blogPosts.map((p, i) => (
|
||||
<div key={p.id || p.slug || i} className="p-3 rounded border border-border/40 space-y-2">
|
||||
<div className="grid md:grid-cols-2 gap-2">
|
||||
<input className="bg-background/50 border border-border/40 rounded px-2 py-1 text-sm" placeholder="Title" value={p.title || ""} onChange={(e) => {
|
||||
const next = blogPosts.slice(); next[i] = { ...next[i], title: e.target.value }; setBlogPosts(next);
|
||||
}} />
|
||||
<input className="bg-background/50 border border-border/40 rounded px-2 py-1 text-sm" placeholder="Slug" value={p.slug || ""} onChange={(e) => {
|
||||
const next = blogPosts.slice(); next[i] = { ...next[i], slug: e.target.value }; setBlogPosts(next);
|
||||
}} />
|
||||
</div>
|
||||
<div className="grid md:grid-cols-2 gap-2">
|
||||
<input className="bg-background/50 border border-border/40 rounded px-2 py-1 text-sm" placeholder="Author" value={p.author || ""} onChange={(e) => { const n = blogPosts.slice(); n[i] = { ...n[i], author: e.target.value }; setBlogPosts(n); }} />
|
||||
<input className="bg-background/50 border border-border/40 rounded px-2 py-1 text-sm" placeholder="Date" value={p.date || ""} onChange={(e) => { const n = blogPosts.slice(); n[i] = { ...n[i], date: e.target.value }; setBlogPosts(n); }} />
|
||||
</div>
|
||||
<div className="grid md:grid-cols-3 gap-2">
|
||||
<input className="bg-background/50 border border-border/40 rounded px-2 py-1 text-sm" placeholder="Read time (e.g., 8 min read)" value={p.read_time || ""} onChange={(e) => { const n = blogPosts.slice(); n[i] = { ...n[i], read_time: e.target.value }; setBlogPosts(n); }} />
|
||||
<input className="bg-background/50 border border-border/40 rounded px-2 py-1 text-sm" placeholder="Category" value={p.category || ""} onChange={(e) => { const n = blogPosts.slice(); n[i] = { ...n[i], category: e.target.value }; setBlogPosts(n); }} />
|
||||
<input className="bg-background/50 border border-border/40 rounded px-2 py-1 text-sm" placeholder="Image URL" value={p.image || ""} onChange={(e) => { const n = blogPosts.slice(); n[i] = { ...n[i], image: e.target.value }; setBlogPosts(n); }} />
|
||||
</div>
|
||||
<textarea className="w-full bg-background/50 border border-border/40 rounded px-2 py-1 text-sm" rows={2} placeholder="Excerpt" value={p.excerpt || ""} onChange={(e) => { const n = blogPosts.slice(); n[i] = { ...n[i], excerpt: e.target.value }; setBlogPosts(n); }} />
|
||||
<textarea className="w-full bg-background/50 border border-border/40 rounded px-2 py-1 text-sm" rows={6} placeholder="Body HTML" value={p.body_html || ""} onChange={(e) => { const n = blogPosts.slice(); n[i] = { ...n[i], body_html: e.target.value }; setBlogPosts(n); }} />
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => deletePost(i)}>Delete</Button>
|
||||
<Button size="sm" onClick={() => savePost(i)}>Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-card/50 border-border/50">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
Loading…
Reference in a new issue