"use client"; import { Suspense } from "react"; import { useSearchParams } from "next/navigation"; import Link from "next/link"; import { POSTS_PER_PAGE, blogCategories, categoryLabel, paginate, postsForCategory, type BlogPost, } from "@/data/content"; import { useStaggerReveal } from "@/hooks/useAnimations"; import BlogPostCard from "@/components/BlogPostCard"; import "../styles/blog.css"; /** Sort posts newest-first by date (ISO display dates compare lexicographically). */ function sortPostsNewestFirst(posts: BlogPost[]): BlogPost[] { return [...posts].sort((a, b) => b.date.localeCompare(a.date)); } type BlogListProps = { /** * Category slug (e.g. from /blog/category/[category]) that takes precedence * over the ?category= search param. The param still controls ?page=. */ category?: string; }; /** * The blog tab: a paginated, category-filterable stack of posts, newest first. * * useSearchParams() suspends during static rendering, so Next.js requires it * to sit inside a boundary -- /blog is statically rendered, which * is exactly why the default export wraps the inner component. */ export default function BlogList({ category: categoryProp }: BlogListProps) { return ( } > ); } function BlogListInner({ categoryProp }: { categoryProp?: string }) { const searchParams = useSearchParams(); const rawPage = searchParams.get("page") ?? ""; const requestedPage = rawPage === "" ? 1 : Number(rawPage); const paramCategory = searchParams.get("category") ?? undefined; const category = categoryProp ?? paramCategory; const categories = blogCategories(); const posts = sortPostsNewestFirst(postsForCategory(category)); const { items, page, totalPages, total, startIndex, endIndex } = paginate( posts, requestedPage, POSTS_PER_PAGE ); const visible = useStaggerReveal(items.length, 100); const activeCategory = category ?? "all"; const activeCategoryLabel = category ? categoryLabel(category) : "All"; return (
{"# blog"}

{"Writing"}

{category ? `In ${activeCategoryLabel}: ` : ""} {total > 0 ? `showing ${startIndex}–${endIndex} of ${total}` : "no posts"}
{items.length > 0 ? ( // Keyed per category+page: useStaggerReveal only re-runs when its // count changes, so remounting the container re-triggers the reveal // even when consecutive pages hold the same number of posts.
{items.map((post, i) => ( ))}
) : (

No posts in this category yet.

)}
); } /** * Single source of truth for /blog URLs: /blog, /blog?page=2, * /blog?category=meta, /blog?category=meta&page=2. Omitted values are * dropped (page 1 is the default page, the missing category is "all"), * so chip links never carry a stale ?page=. */ function blogHref({ page, category }: { page?: number; category?: string } = {}): string { const params = new URLSearchParams(); if (category) params.set("category", category); if (page !== undefined && page > 1) params.set("page", String(page)); const qs = params.toString(); return qs ? `/blog?${qs}` : "/blog"; }