From 62d35808e0f1712bdbdaee9086f74a2bda897c1d Mon Sep 17 00:00:00 2001 From: anhhchu Date: Mon, 8 Jun 2026 15:28:36 -0700 Subject: [PATCH] Tags and Stats --- web/src/app/page.tsx | 45 +--------- web/src/components/blog-index.tsx | 135 ++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 41 deletions(-) create mode 100644 web/src/components/blog-index.tsx diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index e6b7408..c09e83d 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,12 +1,12 @@ -import Link from "next/link"; -import { getAllPosts, formatDate } from "@/lib/posts"; +import { getAllPosts } from "@/lib/posts"; +import BlogIndex from "@/components/blog-index"; export default function Home() { const posts = getAllPosts(); return (
-
+

Writing

@@ -15,44 +15,7 @@ export default function Home() {

-
    - {posts.map((post) => ( -
  • - -
    -

    - {post.title} -

    - -
    -

    - {post.description} -

    - {post.tags.length > 0 && ( -
    - {post.tags.map((tag) => ( - - {tag} - - ))} -
    - )} - -
  • - ))} -
- - {posts.length === 0 && ( -

No posts yet.

- )} +
); } diff --git a/web/src/components/blog-index.tsx b/web/src/components/blog-index.tsx new file mode 100644 index 0000000..30db25a --- /dev/null +++ b/web/src/components/blog-index.tsx @@ -0,0 +1,135 @@ +"use client"; + +import { useMemo, useState } from "react"; +import Link from "next/link"; +import { clsx } from "clsx"; +import type { PostMeta } from "@/lib/posts"; + +function formatDate(iso: string): string { + if (!iso) return ""; + return new Date(iso).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); +} + +export default function BlogIndex({ posts }: { posts: PostMeta[] }) { + const [active, setActive] = useState(null); + + // Tags with their post counts, most frequent first. + const tags = useMemo(() => { + const counts = new Map(); + for (const post of posts) { + for (const tag of post.tags) { + counts.set(tag, (counts.get(tag) ?? 0) + 1); + } + } + return [...counts.entries()] + .map(([tag, count]) => ({ tag, count })) + .sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag)); + }, [posts]); + + const filtered = active + ? posts.filter((post) => post.tags.includes(active)) + : posts; + + return ( + <> + {tags.length > 0 && ( +
+ setActive(null)} + /> + {tags.map(({ tag, count }) => ( + setActive(active === tag ? null : tag)} + /> + ))} +
+ )} + +
    + {filtered.map((post) => ( +
  • + +
    +

    + {post.title} +

    + +
    +

    + {post.description} +

    + {post.tags.length > 0 && ( +
    + {post.tags.map((tag) => ( + + {tag} + + ))} +
    + )} + +
  • + ))} +
+ + {filtered.length === 0 && ( +

No posts yet.

+ )} + + ); +} + +function TagChip({ + label, + count, + selected, + onClick, +}: { + label: string; + count: number; + selected: boolean; + onClick: () => void; +}) { + return ( + + ); +}