spb/autoscholar
Public
1import type { PublicPaperSummary } from "@autoscholar/schemas";2import Link from "next/link";3import { disciplineLabel, fmtDate, modeLabel } from "@/lib/format";4import { IconArrow, IconCode, IconData, IconDoc } from "./Icons";5import { StatusBadge } from "./StatusBadge";67function Avail({ p }: { p: PublicPaperSummary }) {8 const items: Array<[boolean, string, React.ReactNode]> = [9 [true, "PDF", <IconDoc key="pdf" size={14} />],10 [11 p.hasLatex,12 "LaTeX",13 <span key="tex" className="font-mono text-[10px]">14 TeX15 </span>,16 ],17 [p.hasCode, "Code", <IconCode key="code" size={14} />],18 [p.hasData, "Data", <IconData key="data" size={14} />],19 ];20 return (21 <ul className="flex items-center gap-2" aria-label="Available artifacts">22 {items23 .filter(([ok]) => ok)24 .map(([, label, icon]) => (25 <li26 key={label}27 className="subtle inline-flex items-center gap-1 text-[11px]"28 title={`${label} available`}29 >30 {icon}31 <span className="sr-only sm:not-sr-only">{label}</span>32 </li>33 ))}34 </ul>35 );36}3738/** ResearchPaperCard — premium publication card. `variant="row"` is denser for lists; `featured` is the editorial lead. */39export function PaperCard({40 p,41 featured = false,42 variant = "card",43 index,44}: {45 p: PublicPaperSummary;46 featured?: boolean;47 variant?: "card" | "row";48 index?: number;49}) {50 const href = `/research/${p.slug}`;51 const delay = index !== undefined ? `reveal reveal-${Math.min(4, (index % 4) + 1)}` : "";52 if (featured) {53 return (54 <article55 className={`surface card-link relative grid gap-6 p-6 sm:p-8 lg:grid-cols-12 lg:p-10 ${delay}`}56 >57 <div className="lg:col-span-8">58 <div className="flex flex-wrap items-center gap-x-3 gap-y-1.5">59 <span className="mono-id">{p.publicId}</span>60 <span className="badge badge-accent">Featured</span>61 <span className="muted text-xs">62 {disciplineLabel(p.discipline)} · {modeLabel(p.researchMode)}63 </span>64 {p.status !== "published" && <StatusBadge status={p.status} />}65 {p.synthetic && <span className="badge">staging sample</span>}66 </div>67 <h3 className="title-1 mt-4">68 <Link href={href} className="no-underline after:absolute after:inset-0 hover:underline">69 {p.title}70 </Link>71 </h3>72 <p className="prose-paper muted mt-4 line-clamp-4 text-[1.02rem]">{p.abstract}</p>73 </div>74 <div className="flex flex-col justify-between gap-4 border-t pt-4 hairline lg:col-span-4 lg:border-l lg:border-t-0 lg:pl-8 lg:pt-0">75 <dl className="grid grid-cols-2 gap-x-4 gap-y-3 text-sm lg:grid-cols-1">76 <div>77 <dt className="label">Released</dt>78 <dd className="mt-0.5">{fmtDate(p.firstPublishedAt)}</dd>79 </div>80 <div>81 <dt className="label">Version</dt>82 <dd className="mt-0.5">v{p.versionNo}</dd>83 </div>84 <div className="col-span-2 lg:col-span-1">85 <dt className="label">Keywords</dt>86 <dd className="mt-1 flex flex-wrap gap-1.5">87 {p.keywords.slice(0, 5).map((k) => (88 <span key={k} className="badge normal-case tracking-normal">89 {k}90 </span>91 ))}92 </dd>93 </div>94 </dl>95 <div className="flex items-center justify-between gap-3">96 <Avail p={p} />97 <span98 className="inline-flex items-center gap-1 text-sm font-medium"99 style={{ color: "var(--accent-strong)" }}100 >101 Read <IconArrow size={16} />102 </span>103 </div>104 </div>105 </article>106 );107 }108 if (variant === "row") {109 return (110 <article111 className={`group relative grid gap-3 border-b py-5 hairline sm:grid-cols-12 sm:gap-6 ${delay}`}112 >113 <div className="sm:col-span-3">114 <p className="mono-id">{p.publicId}</p>115 <p className="muted mt-1 text-xs">116 {fmtDate(p.firstPublishedAt)} · v{p.versionNo}117 </p>118 <p className="muted mt-1 text-xs">119 {disciplineLabel(p.discipline)} · {modeLabel(p.researchMode)}120 </p>121 </div>122 <div className="min-w-0 sm:col-span-9">123 <div className="flex flex-wrap items-center gap-2">124 {p.status !== "published" && <StatusBadge status={p.status} />}125 {p.synthetic && <span className="badge">staging sample</span>}126 </div>127 <h3 className="title-3 mt-1">128 <Link href={href} className="no-underline after:absolute after:inset-0 group-hover:underline">129 {p.title}130 </Link>131 </h3>132 <p className="muted mt-2 line-clamp-2 text-sm leading-relaxed">{p.abstract}</p>133 <div className="mt-3 flex flex-wrap items-center justify-between gap-2">134 <ul className="flex flex-wrap gap-1.5">135 {p.keywords.slice(0, 4).map((k) => (136 <li key={k} className="badge normal-case tracking-normal">137 {k}138 </li>139 ))}140 </ul>141 <Avail p={p} />142 </div>143 </div>144 </article>145 );146 }147 return (148 <article className={`surface card-link relative flex h-full flex-col gap-3 p-5 ${delay}`}>149 <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">150 <span className="mono-id">{p.publicId}</span>151 <span className="muted">152 {disciplineLabel(p.discipline)} · {modeLabel(p.researchMode)}153 </span>154 {p.status !== "published" && <StatusBadge status={p.status} />}155 {p.synthetic && <span className="badge">staging sample</span>}156 </div>157 <h3 className="title-3">158 <Link href={href} className="no-underline after:absolute after:inset-0 hover:underline">159 {p.title}160 </Link>161 </h3>162 <p className="muted line-clamp-3 text-sm leading-relaxed">{p.abstract}</p>163 <div className="mt-auto flex items-center justify-between gap-2 pt-2 text-xs">164 <span className="subtle">165 {fmtDate(p.firstPublishedAt)} · v{p.versionNo}166 </span>167 <Avail p={p} />168 </div>169 </article>170 );171}172export { PaperCard as ResearchPaperCard };173export function FeaturedPaper({ p }: { p: PublicPaperSummary }) {174 return <PaperCard p={p} featured />;175}176