import Link from "next/link"; import BrowseHeader from "@/components/BrowseHeader"; import ArticleTable from "@/components/articles/ArticleTable"; import SiteChips from "@/components/articles/SiteChips"; import { badgeVariants } from "@/components/ui/badge"; import { listSiteRows, type ArticleRow } from "@/lib/articles/sites"; export const dynamic = "force-dynamic"; // Every site's articles, private sites first. Zero client JS: the filters are // links that change searchParams (/browse/decisions' idiom), so a filtered // view is one pasteable URL. type Search = { site?: string; status?: string; notes?: string }; export default async function SitesPage({ searchParams }: { searchParams: Promise }) { const sp = await searchParams; const all = await listSiteRows(); const keep = (a: ArticleRow) => (!sp.status || a.status === sp.status) && (sp.notes !== "open" || a.openNotes > 0); const sites = all .filter((s) => !sp.site || s.siteId === sp.site) .map((s) => ({ ...s, shown: s.articles.filter(keep) })) .filter((s) => s.shown.length > 0 || (!sp.status && !sp.notes)); const articles = all.flatMap((s) => s.articles); const open = articles.reduce((n, a) => n + a.openNotes, 0); const qs = (next: Partial) => { const p = new URLSearchParams(); for (const [k, v] of Object.entries({ ...sp, ...next })) if (v) p.set(k, String(v)); const s = p.toString(); return `/sites${s ? `?${s}` : ""}`; }; return (
site {all.map((s) => ( ))} status a.status === "published").length}`} /> a.status === "draft").length}`} /> notes a.openNotes > 0).length}`} />
{sites.length === 0 ? (

{all.length === 0 ? "no sites" : "nothing matches that filter"}

) : (
{sites.map((s) => (

{s.title} {s.siteId} {s.published} published · {s.drafts} draft{s.drafts === 1 ? "" : "s"}

))}
)}
); } function Chip({ href, on, label }: { href: string; on: boolean; label: string }) { return ( {label} ); }