initial commit
This commit is contained in:
127
components/PostTable.tsx
Normal file
127
components/PostTable.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { api } from "@/lib/client";
|
||||
import { categoryLabel } from "@/lib/validate";
|
||||
import type { BlogPost } from "@/lib/types";
|
||||
|
||||
type Props = { posts: BlogPost[]; revision: string; categories: string[] };
|
||||
|
||||
export default function PostTable({ posts, revision, categories }: Props) {
|
||||
const router = useRouter();
|
||||
const [query, setQuery] = useState("");
|
||||
const [category, setCategory] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState<string | null>(null);
|
||||
|
||||
const visible = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
return posts.filter(
|
||||
(p) =>
|
||||
(!category || p.category === category) &&
|
||||
(!q || [p.title, p.slug, p.description, ...p.tags].some((s) => s.toLowerCase().includes(q)))
|
||||
);
|
||||
}, [posts, query, category]);
|
||||
|
||||
async function remove(post: BlogPost) {
|
||||
const typed = window.prompt(`Delete "${post.title}"?\n\nType the slug (${post.slug}) to confirm.`);
|
||||
if (typed === null) return;
|
||||
if (typed.trim() !== post.slug) {
|
||||
setError("Slug didn't match; nothing was deleted.");
|
||||
return;
|
||||
}
|
||||
const withImages = window.confirm(
|
||||
`Also delete the image folder public/img/blog/${post.slug}/?\n\nOK = delete images too, Cancel = keep them.`
|
||||
);
|
||||
setBusy(post.slug);
|
||||
setError(null);
|
||||
try {
|
||||
const params = new URLSearchParams({ revision, ...(withImages ? { images: "1" } : {}) });
|
||||
await api(`/api/posts/${post.slug}?${params}`, { method: "DELETE" });
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
} finally {
|
||||
setBusy(null);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="toolbar-row">
|
||||
<h1 style={{ margin: 0, flex: 1 }}>Posts</h1>
|
||||
<input placeholder="search title, slug, tags…" value={query} onChange={(e) => setQuery(e.target.value)} />
|
||||
<select value={category} onChange={(e) => setCategory(e.target.value)}>
|
||||
<option value="">all categories</option>
|
||||
{categories.map((c) => (
|
||||
<option key={c} value={c}>
|
||||
{categoryLabel(c)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Link href="/posts/new" className="button primary">
|
||||
+ New post
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{error && <div className="notice error">{error}</div>}
|
||||
|
||||
<table className="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>title</th>
|
||||
<th>date</th>
|
||||
<th>category</th>
|
||||
<th>tags</th>
|
||||
<th>images</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visible.map((post) => (
|
||||
<tr key={post.slug}>
|
||||
<td>
|
||||
<Link href={`/posts/${post.slug}`} className="title-cell">
|
||||
{post.title}
|
||||
</Link>
|
||||
<div className="mono small muted">/blog/{post.slug}</div>
|
||||
</td>
|
||||
<td className="mono small">{post.date}</td>
|
||||
<td>
|
||||
<span className="chip">{post.category}</span>
|
||||
</td>
|
||||
<td>
|
||||
{post.tags.map((t) => (
|
||||
<span key={t} className="chip">
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
<td className="mono small">{post.images?.length ?? 0}</td>
|
||||
<td className="actions">
|
||||
<Link href={`/posts/${post.slug}`} className="button">
|
||||
Edit
|
||||
</Link>
|
||||
<button className="danger" disabled={busy === post.slug} onClick={() => remove(post)}>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{visible.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className="muted">
|
||||
{posts.length ? "No posts match." : "No posts yet."}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<p className="muted small">
|
||||
{visible.length} of {posts.length} posts
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user