import type { Metadata } from "next"; import Link from "next/link"; import { searchTools, facets, type Filters } from "@/lib/public/queries"; import { parseFilters, toQuery, toggle, type SP } from "@/lib/public/params"; import { ToolCard } from "@/components/tool-card"; import { Pagination } from "@/components/pagination"; import { Breadcrumbs } from "@/components/breadcrumbs"; import { SearchBox } from "@/components/search-box"; import { label } from "@/lib/utils"; import { X, SlidersHorizontal } from "lucide-react"; export const dynamic = "force-dynamic"; export async function generateMetadata({ searchParams }: { searchParams: Promise }): Promise { const sp = await searchParams; const f = parseFilters(sp); const title = f.q ? `Search: ${f.q}` : "Directory"; const hasFilters = Object.values(sp).some((v) => v !== undefined); return { title, description: "Browse and filter the full directory of tools by category, platform, license, pricing model and more.", robots: hasFilters ? { index: false, follow: true } : undefined, alternates: { canonical: "/tools" } }; } const GROUPS: { key: keyof Filters; title: string; facet: string }[] = [ { key: "category", title: "Category", facet: "category" }, { key: "exec", title: "Runs", facet: "exec" }, { key: "source", title: "Source model", facet: "source" }, { key: "pricing", title: "Pricing model", facet: "pricing" }, { key: "iface", title: "Interface", facet: "iface" }, { key: "os", title: "Operating system", facet: "os" }, { key: "status", title: "Product status", facet: "status" }, { key: "depth", title: "Editorial depth", facet: "depth" }, ]; export default async function Directory({ searchParams }: { searchParams: Promise }) { const f = parseFilters(await searchParams); const [res, fc] = await Promise.all([searchTools(f), facets(f)]); const active: { key: keyof Filters; value: string; text: string }[] = []; for (const g of GROUPS) for (const v of (f[g.key] as string[]) ?? []) active.push({ key: g.key, value: v, text: fc[g.facet]?.find((x) => x.value === v)?.label ?? label(v) }); for (const v of f.tag ?? []) active.push({ key: "tag", value: v, text: `#${v}` }); for (const v of f.use ?? []) active.push({ key: "use", value: v, text: label(v) }); const base = toQuery(f, { page: 1 }); return (

{f.q ? <>Results for “{f.q}” : "Directory"}

{res.total} {res.total === 1 ? "tool" : "tools"}{res.mode !== "browse" && <> · {res.mode} search · {res.tookMs} ms}

{active.length > 0 && (
{active.map((a) => {a.text} remove filter)} {f.selfhost && Self-hostable } Clear all
)}

Results

Sort {(["relevance", "name", "updated", "quality"] as const).filter((s) => s !== "relevance" || f.q).map((s) => ( {label(s)} ))}
{res.items.length === 0 ? (

No tools match{f.q ? ` “${f.q}”` : ""}.

Try fewer filters, a broader term, or browse by category. Missing something? Suggest a tool.

) : (
{res.items.map((t) => )}
)} `/tools${toQuery(f, { page: p })}`} />

Page {res.page} of {res.pages}

{base ? <>Share this view: /tools{base} : null}

); } function FilterGroups({ f, fc }: { f: Filters; fc: Record }) { return (
Self-hostable only
{GROUPS.map((g) => { const items = fc[g.facet] ?? []; if (!items.length) return null; const sel = (f[g.key] as string[]) ?? []; return (
{g.title}
    {items.slice(0, g.key === "category" ? 40 : 12).map((it) => { const on = sel.includes(it.value); return (
  • {on && } {it.label === it.value ? label(it.value) : it.label}{on && (selected)}{it.count}
  • ); })}
); })}
); }