85 lines
3.2 KiB
TypeScript
85 lines
3.2 KiB
TypeScript
import type { Metadata } from "next";
|
|
import Link from "next/link";
|
|
import { StatusBadge } from "@/components/admin/StatusBadge";
|
|
import { LinkButton } from "@/components/ui";
|
|
import { requireAdmin } from "@/lib/auth/dal";
|
|
import { formatDate } from "@/lib/format";
|
|
import { listAllPages } from "@/lib/services/pages";
|
|
import { countPostsByStatus, listAllPosts } from "@/lib/services/posts";
|
|
import { listAllTags } from "@/lib/services/tags";
|
|
|
|
export const metadata: Metadata = { title: "Dashboard" };
|
|
|
|
export default async function AdminDashboard() {
|
|
await requireAdmin();
|
|
const [postCounts, allPosts, allPages, allTags] = await Promise.all([
|
|
countPostsByStatus(),
|
|
listAllPosts(),
|
|
listAllPages(),
|
|
listAllTags(),
|
|
]);
|
|
const recentPosts = allPosts.slice(0, 5);
|
|
|
|
const stats = [
|
|
{ label: "Published posts", value: postCounts.published },
|
|
{ label: "Draft posts", value: postCounts.draft },
|
|
{ label: "Pages", value: allPages.length },
|
|
{ label: "Tags", value: allTags.length },
|
|
];
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
<h1 className="text-2xl font-bold tracking-tight text-ink-bright">Dashboard</h1>
|
|
<div className="flex gap-2">
|
|
<LinkButton href="/admin/posts/new">New post</LinkButton>
|
|
<LinkButton href="/admin/pages/new" variant="secondary">New page</LinkButton>
|
|
</div>
|
|
</div>
|
|
|
|
<dl className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-4">
|
|
{stats.map((stat) => (
|
|
<div key={stat.label} className="rounded-lg border border-edge bg-surface p-5">
|
|
<dt className="text-sm text-ink-muted">{stat.label}</dt>
|
|
<dd className="mt-1 text-3xl font-semibold text-ink-bright">{stat.value}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
|
|
<section aria-labelledby="recent-heading" className="mt-10">
|
|
<h2 id="recent-heading" className="text-lg font-semibold text-ink-strong">
|
|
Recently updated posts
|
|
</h2>
|
|
{recentPosts.length === 0 ? (
|
|
<p className="mt-4 rounded-lg border border-dashed border-edge-strong px-4 py-8 text-center text-sm text-ink-muted">
|
|
No posts yet —{" "}
|
|
<Link href="/admin/posts/new" className="text-link underline underline-offset-4">
|
|
write the first one
|
|
</Link>
|
|
.
|
|
</p>
|
|
) : (
|
|
<ul className="mt-4 divide-y divide-edge rounded-lg border border-edge bg-surface">
|
|
{recentPosts.map((post) => (
|
|
<li key={post.id} className="flex items-center justify-between gap-4 px-4 py-3">
|
|
<div className="min-w-0">
|
|
<Link
|
|
href={`/admin/posts/${post.id}/edit`}
|
|
className="font-medium text-ink-strong transition-colors hover:text-link"
|
|
>
|
|
{post.title}
|
|
</Link>
|
|
<p className="mt-0.5 text-xs text-ink-muted">
|
|
Updated {formatDate(post.updatedAt)}
|
|
</p>
|
|
</div>
|
|
<StatusBadge status={post.status} />
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|