Files
dashboards/components/WebsiteBlogListPage.tsx

164 lines
4.9 KiB
TypeScript

"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import CatalogEmptyState from "@/components/catalog/CatalogEmptyState";
import { excerpt, formatDate } from "@/components/catalog/utils";
import ConfirmDialog from "@/components/ConfirmDialog";
import {
deleteWebsiteBlogPost,
listWebsiteBlogPosts,
type WebsiteBlogPost,
} from "@/services/website-blog";
import styles from "./WebsiteBlogListPage.module.css";
export default function WebsiteBlogListPage() {
const [items, setItems] = useState<WebsiteBlogPost[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [removing, setRemoving] = useState<WebsiteBlogPost | null>(null);
const [removeBusy, setRemoveBusy] = useState(false);
useEffect(() => {
let cancelled = false;
void (async () => {
setLoading(true);
setError("");
try {
const data = await listWebsiteBlogPosts();
if (!cancelled) setItems(data.items);
} catch (err) {
if (!cancelled) {
setError(
err instanceof Error ? err.message : "Could not load blog posts.",
);
}
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, []);
async function confirmRemove() {
if (!removing) return;
setRemoveBusy(true);
try {
await deleteWebsiteBlogPost(removing.id);
setItems((prev) => prev.filter((item) => item.id !== removing.id));
setRemoving(null);
} catch (err) {
setError(
err instanceof Error ? err.message : "Could not remove blog post.",
);
setRemoving(null);
} finally {
setRemoveBusy(false);
}
}
return (
<div className={styles.page}>
<header className={styles.head}>
<div>
<nav className={styles.breadcrumb} aria-label="Breadcrumb">
<Link href="/">Home</Link>
<span className={styles.breadcrumbSep} aria-hidden>
/
</span>
<span className={styles.breadcrumbCurrent}>Blog</span>
</nav>
<h1 className={styles.title}>Blog</h1>
<p className={styles.lead}>
Stories published on the IranBay website.
</p>
</div>
<div className={styles.headActions}>
<p className={styles.count}>
{loading ? "Loading…" : `${items.length} posts`}
</p>
<Link href="/website/blog/new" className={`outline-btn ${styles.addBtn}`}>
Add
</Link>
</div>
</header>
{error ? (
<p className={styles.error} role="alert">
{error}
</p>
) : null}
{loading && items.length === 0 ? (
<p className={styles.hint}>Loading blog…</p>
) : null}
{!loading && items.length === 0 ? (
<CatalogEmptyState
icon="/images/icons/gallery.svg"
title="No blog posts yet"
lead="Add the first story for the website."
actionHref="/website/blog/new"
actionLabel="Add"
/>
) : null}
{items.length ? (
<div className={styles.grid}>
{items.map((post) => {
const text = post.abstract ? excerpt(post.abstract, 160) : "";
return (
<article key={post.id} className={styles.card}>
<div className={styles.media}>
{post.imageUrl ? (
<img src={post.imageUrl} alt="" />
) : (
<div className={styles.fallback} aria-hidden />
)}
</div>
<div className={`tile-hover ${styles.foot}`}>
<div className={styles.body}>
<p className={styles.category}>{post.categoryName}</p>
<h2 dir="auto">{post.title}</h2>
{text ? <p dir="auto">{text}</p> : null}
{post.createdAt ? (
<span className={styles.meta}>
{formatDate(post.createdAt)}
</span>
) : null}
</div>
<div className={styles.controls}>
<button
type="button"
className={styles.remove}
onClick={() => setRemoving(post)}
>
Remove
</button>
</div>
</div>
</article>
);
})}
</div>
) : null}
<ConfirmDialog
open={Boolean(removing)}
danger
busy={removeBusy}
kicker="Blog"
title="Remove post"
message="This deletes the blog post. This cannot be undone."
confirmLabel="Remove"
onConfirm={() => void confirmRemove()}
onClose={() => {
if (!removeBusy) setRemoving(null);
}}
/>
</div>
);
}