SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
7.0 KB · 136 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { Download } from 'lucide-react';4import { PageHeader, Section, Note } from '@/components/ui/section';5import { EmptyState } from '@/components/ui/empty-state';6import { Badge } from '@/components/ui/badge';7import { listMetrics, snapshotsForMetric } from '@/lib/queries/rankings';8import { listSources } from '@/lib/queries/sources';9import { jsonLd, datasetLd } from '@/lib/seo';10import { SITE_URL } from '@/lib/site';11import { fmtDate, fmtInt, scopeLabel } from '@/lib/format';1213export const metadata: Metadata = { title: 'Data downloads', description: 'Legally redistributable CancerIndex datasets: derived ranking snapshots as CSV with attribution.' };14export const revalidate = 600;1516export default async function DataPage() {17  const [metrics, sources] = await Promise.all([listMetrics(), listSources()]);18  const withSnaps = metrics.filter((m) => m.snapshot_count > 0);19  const snapshotLists = await Promise.all(withSnaps.map((m) => snapshotsForMetric(m.slug)));20  const datasets = withSnaps.flatMap((m, i) => (snapshotLists[i] ?? []).map((s) => ({ metric: m, snap: s })));21  const redistributable = sources.filter((s) => s.redistribution === 'allowed' || s.redistribution === 'attribution');22  const restricted = sources.filter((s) => !(s.redistribution === 'allowed' || s.redistribution === 'attribution'));2324  return (25    <div>26      {datasets.slice(0, 20).map(({ metric, snap }) => (27        <script28          key={snap.id}29          type="application/ld+json"30          dangerouslySetInnerHTML={{31            __html: jsonLd(32              datasetLd({33                name: `CancerIndex ranking — ${metric.name} (${scopeLabel(snap.scope_key)})`,34                description: `${metric.description} Formula ${snap.formula_version}; ${snap.eligible_entities} eligible entities.`,35                url: `${SITE_URL}/rankings/${metric.slug}?scope=${encodeURIComponent(snap.scope_key)}`,36                distributionUrl: `${SITE_URL}/api/export/rankings.csv?metric=${metric.slug}&scope=${encodeURIComponent(snap.scope_key)}`,37                license: 'https://creativecommons.org/licenses/by/4.0/',38                dateModified: snap.generated_at,39              }),40            ),41          }}42        />43      ))}44      <PageHeader kicker="Data" title="Downloads" lede="Only datasets CancerIndex may legally redistribute are offered here. Derived ranking snapshots are CancerIndex's own work (CC BY 4.0) and carry attribution rows for the underlying providers. Source datasets under restrictive terms are linked, not mirrored." />4546      <Section id="rankings" kicker="CancerIndex-derived" title="Ranking snapshots (CSV)" description="One file per metric and scope; header rows carry attribution, formula version, scope, generation time and inputs hash.">47        {datasets.length ? (48          <div className="ci-table-wrap">49            <table className="ci-table">50              <thead>51                <tr>52                  <th>Metric</th>53                  <th>Scope</th>54                  <th>Formula</th>55                  <th className="num">Rows</th>56                  <th>Generated</th>57                  <th>Download</th>58                </tr>59              </thead>60              <tbody>61                {datasets.map(({ metric, snap }) => (62                  <tr key={snap.id}>63                    <td>64                      <Link className="ci-link" href={`/rankings/${metric.slug}?scope=${encodeURIComponent(snap.scope_key)}`}>65                        {metric.name}66                      </Link>67                    </td>68                    <td className="text-[12.5px]">{scopeLabel(snap.scope_key)}</td>69                    <td className="ci-mono text-[11.5px]">{snap.formula_version}</td>70                    <td className="num">{fmtInt(snap.eligible_entities)}</td>71                    <td className="whitespace-nowrap text-[12.5px]">{fmtDate(snap.generated_at)}</td>72                    <td>73                      <a className="inline-flex items-center gap-1 text-[13px] text-accent" href={`/api/export/rankings.csv?metric=${metric.slug}&scope=${encodeURIComponent(snap.scope_key)}`}>74                        <Download className="h-3.5 w-3.5" aria-hidden /> CSV75                      </a>76                    </td>77                  </tr>78                ))}79              </tbody>80            </table>81          </div>82        ) : (83          <EmptyState title="No ranking snapshot to download yet" knows={[{ label: 'Metric catalog', href: '/methodology#metrics' }, { label: 'Public API', href: '/developers' }]}>84            Downloads appear as soon as the ranking engine produces its first snapshot. The CSV endpoint is <code className="ci-mono">/api/export/rankings.csv?metric=&lt;slug&gt;&amp;scope=&lt;scope_key&gt;</code>.85          </EmptyState>86        )}87      </Section>8889      <Section id="sources" kicker="Upstream" title="Source datasets" description="Redistribution terms as reviewed. Restricted sources are linked to their official download pages instead.">90        <div className="grid gap-6 md:grid-cols-2">91          <div>92            <p className="ci-kicker mb-1">Redistributable with attribution</p>93            {redistributable.length ? (94              <ul className="divide-y divide-rule text-[13.5px]">95                {redistributable.map((s) => (96                  <li key={s.slug} className="flex items-center justify-between gap-2 py-1.5">97                    <Link className="ci-link" href={`/source/${s.slug}`}>98                      {s.name}99                    </Link>100                    <Badge tone="ok">{s.redistribution}</Badge>101                  </li>102                ))}103              </ul>104            ) : (105              <p className="text-[13px] text-ink-3">None reviewed as redistributable yet.</p>106            )}107          </div>108          <div>109            <p className="ci-kicker mb-1">Restricted or under review</p>110            {restricted.length ? (111              <ul className="divide-y divide-rule text-[13.5px]">112                {restricted.map((s) => (113                  <li key={s.slug} className="flex items-center justify-between gap-2 py-1.5">114                    <span>115                      <Link className="ci-link" href={`/source/${s.slug}`}>116                        {s.name}117                      </Link>118                      {s.homepage ? (119                        <a className="ml-2 text-[12px] text-ink-3 hover:text-accent" href={s.homepage} target="_blank" rel="noopener noreferrer">120                          official site121                        </a>122                      ) : null}123                    </span>124                    <Badge tone="warn">{s.redistribution}</Badge>125                  </li>126                ))}127              </ul>128            ) : null}129          </div>130        </div>131      </Section>132      <Note>Attribution for derived files: "CancerIndex (cancerindex.io), CC BY 4.0; underlying data © respective providers." The public API at /api/v1 returns the same data with a sources array in every envelope.</Note>133    </div>134  );135}136