spb/autoscholar
Public
1import { listSeries, searchPapers } from "@autoscholar/core";2import { PublicSearchQuery } from "@autoscholar/schemas";3import type { Metadata } from "next";4import { Disclosure } from "@/components/Disclosure";5import { EmptyState } from "@/components/EmptyState";6import { Pagination } from "@/components/Pagination";7import { PaperCard } from "@/components/PaperCard";8import { SearchFilters } from "@/components/SearchFilters";9import { t } from "@/lib/i18n";10import { db } from "@/lib/server/db";1112export const dynamic = "force-dynamic";13export const metadata: Metadata = {14 title: "Research library",15 description: "Searchable library of AutoScholar working papers.",16 alternates: { canonical: "/research" },17};1819export default async function ResearchPage({20 searchParams,21}: {22 searchParams: Promise<Record<string, string | string[] | undefined>>;23}) {24 const sp = await searchParams;25 const raw: Record<string, string | undefined> = {};26 for (const [k, v] of Object.entries(sp)) raw[k] = Array.isArray(v) ? v[0] : v;27 for (const k of Object.keys(raw)) if (raw[k] === "") delete raw[k];28 const parsed = PublicSearchQuery.safeParse(raw);29 const q = parsed.success ? parsed.data : PublicSearchQuery.parse({});30 const d = db();31 const res = await searchPapers(d, q);32 const series = await listSeries(d);33 const hrefFor = (p: number) => {34 const u = new URLSearchParams();35 for (const [k, v] of Object.entries(raw)) if (v && k !== "page") u.set(k, v);36 u.set("page", String(p));37 return `/research?${u.toString()}`;38 };39 const anyFilter = Object.keys(raw).some((k) => !["page", "sort", "pageSize"].includes(k));40 return (41 <div className="space-y-6">42 <header>43 <p className="eyebrow">Library</p>44 <h1 className="serif mt-1 text-3xl font-semibold">Working papers</h1>45 <p className="muted mt-2 max-w-2xl text-sm">46 Search titles, abstracts, keywords and verified metadata. Filter by discipline, methodology, series,47 availability of code and data, and publication status.48 </p>49 </header>50 <SearchFilters q={raw} series={series} />51 {!parsed.success && (52 <p role="alert" className="text-sm" style={{ color: "var(--color-danger-500)" }}>53 Some filters were invalid and have been ignored.54 </p>55 )}56 <div className="flex items-center justify-between text-sm">57 <p className="muted" aria-live="polite">58 {res.total === 0 ? "No results" : `${res.total} ${res.total === 1 ? "paper" : "papers"}`}59 {raw.q ? ` for “${raw.q}”` : ""}60 </p>61 <Pagination page={res.page} pageSize={res.pageSize} total={res.total} hrefFor={hrefFor} />62 </div>63 {res.items.length === 0 ? (64 <EmptyState title={anyFilter ? t("search.empty") : "No working papers yet."}>65 {anyFilter ? <a href="/research">Clear all filters</a> : t("library.empty")}66 </EmptyState>67 ) : (68 <ul className="grid gap-4 md:grid-cols-2">69 {res.items.map((p) => (70 <li key={p.publicId}>71 <PaperCard p={p} />72 </li>73 ))}74 </ul>75 )}76 <div className="flex justify-end">77 <Pagination page={res.page} pageSize={res.pageSize} total={res.total} hrefFor={hrefFor} />78 </div>79 <Disclosure compact />80 </div>81 );82}83