spb/wp-spboucher-ai
Public
1import Link from "next/link";2import { FileText } from "lucide-react";3import { fmtDate, fmtInt, href, t, type Locale } from "@/lib/i18n";4import type { PaperSummary } from "@/lib/papers/types";56export function AuthorsLine({ authors, locale, link = true }: { authors: PaperSummary["authors"]; locale: Locale; link?: boolean }) {7 return <span>{authors.map((a, i) => <span key={a.slug}>{i > 0 && (i === authors.length - 1 ? (locale === "fr" ? " et " : " and ") : ", ")}{link ? <Link href={href(locale, `/authors/${a.slug}`)} className="text-fg-2 no-underline hover:underline">{a.display_name}</Link> : a.display_name}</span>)}</span>;8}9export function StatusChips({ p, locale }: { p: PaperSummary; locale: Locale }) {10 const d = t(locale);11 return (12 <span className="inline-flex flex-wrap gap-1.5">13 {p.status !== "published" && <span className="chip chip-burgundy">{d.misc.status[p.status as keyof typeof d.misc.status]}</span>}14 {p.publication_status !== "working_paper" && <span className="chip chip-navy">{d.misc.pubStatus[p.publication_status as keyof typeof d.misc.pubStatus]}</span>}15 {p.notice && <span className="chip">{p.notice}</span>}16 {p.has_pdf ? <span className="chip"><FileText size={12} aria-hidden /> {d.papers.pdf}{p.pages ? ` · ${p.pages} ${d.papers.pages}` : ""}</span> : <span className="chip">{d.paper.notAvailable}</span>}17 </span>18 );19}20function excerpt(p: PaperSummary, n = 320) { const s = (p.abstract?.[0] ?? "").replace(/\s+/g, " "); return s.length > n ? s.slice(0, n).replace(/\s\S*$/, "") + "…" : s; }21const seriesName = (p: PaperSummary, locale: Locale) => locale === "fr" ? p.series_name_fr : p.series_name_en;2223export function PaperCard({ p, locale, badge }: { p: PaperSummary; locale: Locale; badge?: string }) {24 const d = t(locale);25 return (26 <article className="card p-5 flex flex-col gap-3 h-full">27 <div className="flex items-center justify-between gap-2 text-xs">28 <span className="font-mono text-navy">{p.identifier}</span>29 <span className="flex items-center gap-2">{badge && <span className="chip chip-burgundy">{badge}</span>}<span className="text-fg-3">{p.year ?? ""}</span></span>30 </div>31 <h3 className="text-lg leading-snug"><Link href={href(locale, `/papers/${p.slug}`)} className="no-underline hover:underline text-fg">{locale === "fr" && p.title_fr ? p.title_fr : p.title}</Link></h3>32 <p className="meta"><AuthorsLine authors={p.authors} locale={locale} /></p>33 <p className="text-sm text-fg-2 leading-relaxed">{excerpt(p, 260)}</p>34 <div className="mt-auto flex flex-wrap items-center gap-1.5 pt-1">35 {p.domain && <Link href={href(locale, `/topics/${p.domain.slug}`)} className="chip chip-navy no-underline">{locale === "fr" ? p.domain.name_fr : p.domain.name_en}</Link>}36 <StatusChips p={p} locale={locale} />37 <Link href={href(locale, `/papers/${p.slug}`)} className="ml-auto text-sm text-navy no-underline hover:underline">{d.papers.view} →</Link>38 </div>39 </article>40 );41}42export function PaperRow({ p, locale, compact = false }: { p: PaperSummary; locale: Locale; compact?: boolean }) {43 const d = t(locale);44 return (45 <article className={`py-4 ${compact ? "py-2.5" : ""} border-b border-line last:border-0`}>46 <div className="flex gap-4">47 <div className="w-16 shrink-0 pt-0.5 text-xs font-mono text-navy">{p.identifier}</div>48 <div className="min-w-0 flex-1">49 <h3 className={`${compact ? "text-base" : "text-lg"} leading-snug`}><Link href={href(locale, `/papers/${p.slug}`)} className="no-underline hover:underline text-fg">{locale === "fr" && p.title_fr ? p.title_fr : p.title}</Link></h3>50 {!compact && p.subtitle && <p className="text-sm text-fg-2 mt-0.5">{p.subtitle}</p>}51 <p className="meta mt-1"><AuthorsLine authors={p.authors} locale={locale} /> · {seriesName(p, locale)} · {fmtDate(locale, p.first_released_on, p.first_released_precision)}{p.last_revised_on && p.last_revised_on !== p.first_released_on ? ` · ${d.paper.revised.toLowerCase()} ${fmtDate(locale, p.last_revised_on, p.last_revised_precision)}` : ""}{p.version_label ? ` · ${p.version_label}` : ""}</p>52 {!compact && <p className="text-sm text-fg-2 leading-relaxed mt-2">{excerpt(p)}</p>}53 <div className="mt-2 flex flex-wrap items-center gap-1.5">54 {p.domain && <Link href={href(locale, `/topics/${p.domain.slug}`)} className="chip chip-navy no-underline">{locale === "fr" ? p.domain.name_fr : p.domain.name_en}</Link>}55 {!compact && p.keywords.slice(0, 4).map((k) => <Link key={k} href={href(locale, `/papers?keyword=${encodeURIComponent(k)}`)} className="chip no-underline">{k}</Link>)}56 <StatusChips p={p} locale={locale} />57 </div>58 </div>59 </div>60 </article>61 );62}63export function PaperTable({ items, locale }: { items: PaperSummary[]; locale: Locale }) {64 const d = t(locale);65 return (66 <div className="overflow-x-auto card">67 <table className="data">68 <thead><tr><th>{d.papers.number}</th><th>{d.paper.identifier}</th><th>{d.paper.authors}</th><th>{d.paper.year}</th><th>{d.paper.version}</th><th>{d.paper.domain}</th><th>{d.paper.pagesLabel}</th><th>{d.paper.viewsLabel}</th><th>{d.paper.downloadsLabel}</th></tr></thead>69 <tbody>{items.map((p) => <tr key={p.id}><td className="font-mono text-navy whitespace-nowrap">{p.identifier}</td><td><Link href={href(locale, `/papers/${p.slug}`)} className="text-fg no-underline hover:underline">{p.title}</Link></td><td className="text-fg-2 whitespace-nowrap"><AuthorsLine authors={p.authors} locale={locale} link={false} /></td><td>{p.year ?? ""}</td><td className="whitespace-nowrap">{p.version_label ?? ""}</td><td className="text-fg-2">{p.domain ? (locale === "fr" ? p.domain.name_fr : p.domain.name_en) : ""}</td><td>{p.pages ?? ""}</td><td>{fmtInt(locale, p.view_count)}</td><td>{fmtInt(locale, p.download_count)}</td></tr>)}</tbody>70 </table>71 </div>72 );73}74