"use client"; import { format } from "date-fns"; import FuzzySearch from "fuzzy-search"; import Link from "next/link"; import { useMemo, useState } from "react"; import { Container } from "@/components/container"; import type { BlogEntry } from "./page"; function truncate(text: string, length: number) { return text.length > length ? `${text.slice(0, length)}…` : text; } function SearchIcon({ className }: { className?: string }) { return ( ); } export function BlogWithSearchMagazine({ blogs }: { blogs: BlogEntry[] }) { if (blogs.length === 0) { return (

No blog posts yet.

); } // `blogs` arrives pre-sorted from the server: explicitly featured posts // first (ordered by `featured_order` asc, then date desc), then the rest // by date desc. If nothing is explicitly featured, fall back to treating // the newest post as the cover so the layout never feels empty up top. // `MagazineSearchGrid` re-filters using `heroSlugs` so the hero/featured // posts never duplicate into the archive grid. const explicitlyFeatured = blogs.filter((b) => b.featured); const heroBlogs = explicitlyFeatured.length > 0 ? explicitlyFeatured : blogs.slice(0, 1); const heroSlugs = new Set(heroBlogs.map((b) => b.slug)); const [coverStory, ...secondaryFeatured] = heroBlogs; return (

Blog

{secondaryFeatured.length > 0 ? ( ) : null}
); } function MoreFeatured({ blogs }: { blogs: BlogEntry[] }) { return (
); } function MagazineFeatured({ blog }: { blog: BlogEntry }) { return (
{blog.image ? ( {blog.title} ) : null}
Cover story

{blog.title}

{truncate(blog.description, 160)}

{blog.author} {blog.author} ·
); } function MagazineSearchGrid({ blogs: allBlogs, excludedSlugs, }: { blogs: BlogEntry[]; /** Slugs already shown above the archive (cover story + "More featured"). */ excludedSlugs: Set; }) { const [search, setSearch] = useState(""); const searcher = useMemo( () => new FuzzySearch(allBlogs, ["title", "description"], { caseSensitive: false, }), [allBlogs] ); const gridItems = useMemo(() => { // When the reader is searching, surface every match (including // featured posts they may be looking for); otherwise hide the posts // that are already rendered as featured above the archive. const results = search.trim() ? searcher.search(search) : allBlogs; if (search.trim()) { return results; } return results.filter((b) => !excludedSlugs.has(b.slug)); }, [search, searcher, allBlogs, excludedSlugs]); return (

From the archive

{gridItems.length === 0 ? (

No articles match that search.

) : (
    {gridItems.map((blog) => (
  • ))}
)}
); } function MagazineCard({ blog }: { blog: BlogEntry }) { return (
{blog.image ? ( {blog.title} ) : (
No image
)}

{blog.title}

{truncate(blog.description, 110)}

{blog.author} {blog.author}
); }