HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import Link from 'next/link';2import { SourcesTable } from '@/components/entity/blocks';3import { ViewBeacon } from '@/components/layout/view-beacon';4import { BreadcrumbLd, Breadcrumbs } from '@/components/meta/breadcrumb-ld';5import { EntityBadge } from '@/components/ui/badges';6import { EntityLink, QualityMark } from '@/components/ui/entity';7import { Container, Note, Section } from '@/components/ui/section';8import { api, safe } from '@/lib/api';9import { fmtAgo, fmtDate, fmtInt } from '@/lib/format';10import { routes, SITE_NAME, SITE_URL } from '@/lib/site';11import type { EntityDetail, EntitySummary } from '@/lib/types';1213/*14 Researcher page (server, async): affiliations (stated relations / organization), papers (`authored` out), coauthors15 (researchers named on the same papers — from the papers' `authored` relations). Public professional data only; the16 page says plainly when the record is a name-only row created from an author list.17*/1819function relItems(d: EntityDetail, types: string[], predicates?: string[]): EntitySummary[] {20 const out: EntitySummary[] = [];21 const seen = new Set<string>();22 for (const g of d.relations ?? []) {23 if (predicates && !predicates.includes(g.predicate)) continue;24 for (const it of g.items) if (types.includes(it.entity_type) && !seen.has(it.id)) {25 seen.add(it.id);26 out.push(it);27 }28 }29 return out;30}3132export async function ResearcherPage({ d, canonical }: { d: EntityDetail; canonical: string }) {33 const papers = relItems(d, ['paper'], ['authored']);34 const affiliations = relItems(d, ['company', 'organization', 'lab', 'university']);35 const details = await Promise.all(papers.slice(0, 8).map((p) => safe(api.entity(p.slug))));36 const co = new Map<string, EntitySummary & { shared: number }>();37 for (const pd of details) {38 if (!pd) continue;39 for (const r of relItems(pd, ['researcher'], ['authored'])) {40 if (r.id === d.id) continue;41 const cur = co.get(r.id);42 if (cur) cur.shared += 1;43 else co.set(r.id, { ...r, shared: 1 });44 }45 }46 const coauthors = [...co.values()].sort((a, b) => b.shared - a.shared || a.name.localeCompare(b.name));47 const nameOnly = !d.identifiers?.length && !d.organization && affiliations.length === 0;48 const crumbs = [{ name: SITE_NAME, href: '/' }, { name: 'Researchers', href: '/explore/researcher' }, { name: d.name, href: canonical }];49 const ld = { '@context': 'https://schema.org', '@type': 'Person', name: d.name, url: `${SITE_URL}${canonical}`, affiliation: (d.organization ? [d.organization] : affiliations).map((o) => ({ '@type': 'Organization', name: o.name })), identifier: d.identifiers?.map((i) => ({ '@type': 'PropertyValue', propertyID: i.scheme, value: i.value })) };5051 return (52 <Container wide>53 <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(ld) }} />54 <BreadcrumbLd items={crumbs} />55 <ViewBeacon path={canonical} />56 <Breadcrumbs items={crumbs} />57 <header className="pb-6 pt-4 md:pb-8 md:pt-5">58 <div className="flex flex-wrap items-center gap-2">59 <EntityBadge type="researcher" />60 {nameOnly && <span className="rounded-[3px] bg-surface-2 px-1.5 text-[10px] uppercase tracking-wide text-ink-3">name-only record</span>}61 </div>62 <div className="mt-3 flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">63 <div className="min-w-0">64 <h1 className="display text-[30px] md:text-[44px]">{d.name}</h1>65 <p className="mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-[15px] text-ink-2">66 {d.organization && (67 <Link href={routes.entity({ entity_type: 'company', slug: d.organization.slug })} className="font-medium text-ink hover:text-accent">68 {d.organization.name}69 </Link>70 )}71 <span className="tnum text-ink-3">72 {fmtInt(papers.length)} paper{papers.length === 1 ? '' : 's'} in the atlas73 </span>74 </p>75 {nameOnly && <Note className="mt-3 max-w-2xl">This record was created from a paper's author list. It carries no identifier (ORCID, OpenAlex…) and no affiliation, so homonyms may be merged and spelling variants split. Only public professional data — papers and stated affiliations — is shown.</Note>}76 </div>77 <div className="shrink-0 text-xs text-ink-3 lg:text-right">78 <QualityMark q={d.quality?.score} label />79 <p className="mt-1" title={d.updated_at}>80 Updated {fmtAgo(d.updated_at)} · first seen {fmtDate(d.first_seen_at)}81 </p>82 <p className="mono mt-0.5 text-[11px]">{d.id}</p>83 </div>84 </div>85 </header>8687 <div className="grid gap-10 pb-16 lg:grid-cols-[minmax(0,1fr)_22rem]">88 <div className="min-w-0">89 <Section id="affiliations" eyebrow="Affiliations" title={<>Affiliations <span className="tnum text-base font-normal text-ink-3">{fmtInt(affiliations.length + (d.organization ? 1 : 0))}</span></>} hairline={false} className="pt-0">90 {d.organization || affiliations.length ? (91 <ul className="divide-y divide-rule border-y border-rule text-sm">92 {d.organization && (93 <li className="flex items-center gap-2 py-2">94 <EntityBadge type="company" small />95 <Link href={routes.entity({ entity_type: 'company', slug: d.organization.slug })} className="text-ink hover:text-accent hover:underline">96 {d.organization.name}97 </Link>98 </li>99 )}100 {affiliations.map((o) => (101 <li key={o.id} className="flex items-center gap-2 py-2">102 <EntityBadge type={o.entity_type} small />103 <EntityLink e={o} />104 </li>105 ))}106 </ul>107 ) : (108 <p className="text-sm text-ink-3">No affiliation stated by a source.</p>109 )}110 </Section>111 <Section id="papers" eyebrow="Research" title={<>Papers <span className="tnum text-base font-normal text-ink-3">{fmtInt(papers.length)}</span></>}>112 {papers.length === 0 ? (113 <p className="text-sm text-ink-3">No paper linked.</p>114 ) : (115 <ul className="divide-y divide-rule border-y border-rule">116 {papers.map((p) => (117 <li key={p.id} className="py-2 text-sm">118 <EntityLink e={p} />119 <span className="tnum ml-2 text-xs text-ink-3">{typeof p.attributes?.published_at === 'string' ? fmtDate(p.attributes.published_at as string) : ''}</span>120 </li>121 ))}122 </ul>123 )}124 </Section>125 <Section id="coauthors" eyebrow="Network" title={<>Co-authors <span className="tnum text-base font-normal text-ink-3">{fmtInt(coauthors.length)}</span></>} lede={papers.length > 8 ? 'From the first 8 papers.' : 'Researchers named on the same papers.'}>126 {coauthors.length === 0 ? (127 <p className="text-sm text-ink-3">No co-author record found on the linked papers.</p>128 ) : (129 <ul className="grid gap-x-8 sm:grid-cols-2 lg:grid-cols-3">130 {coauthors.map((c) => (131 <li key={c.id} className="flex items-center justify-between gap-2 border-b border-rule py-1.5 text-sm">132 <EntityLink e={c} className="truncate" />133 <span className="tnum shrink-0 text-xs text-ink-3">134 {c.shared} shared135 </span>136 </li>137 ))}138 </ul>139 )}140 </Section>141 <Section id="sources" eyebrow="Sources" title={<>Sources <span className="tnum text-base font-normal text-ink-3">{fmtInt(d.sources?.length ?? 0)}</span></>}>142 <SourcesTable sources={d.sources ?? []} />143 </Section>144 </div>145 <aside className="min-w-0 space-y-8">146 <section>147 <p className="eyebrow mb-2">Identifiers</p>148 {d.identifiers?.length ? (149 <ul className="space-y-0.5 text-xs">150 {d.identifiers.map((i) => (151 <li key={`${i.scheme}:${i.value}`} className="flex gap-2">152 <span className="mono text-ink-3">{i.scheme}</span>153 <span className="mono truncate text-ink-2">{i.value}</span>154 </li>155 ))}156 </ul>157 ) : (158 <p className="text-xs text-ink-3">None recorded.</p>159 )}160 </section>161 {d.aliases?.length > 1 && (162 <section>163 <p className="eyebrow mb-2">Also written as</p>164 <p className="text-sm text-ink-2">{d.aliases.filter((x) => x !== d.name).join(' · ')}</p>165 </section>166 )}167 <section>168 <p className="eyebrow mb-2">Graph</p>169 <Link href={`/graph/${encodeURIComponent(d.slug)}?mode=research`} className="link text-sm">170 Research network →171 </Link>172 </section>173 </aside>174 </div>175 </Container>176 );177}178