SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
6.9 KB · 135 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { Breadcrumbs } from "@/components/entity/breadcrumbs";4import { SectionNav } from "@/components/entity/section-nav";5import { SourcesFooter } from "@/components/entity/sources-footer";6import { DatasetForm } from "@/components/intelligence/download/dataset-form";7import { ApiNote, HonestEmpty } from "@/components/intelligence/shared";8import { Badge } from "@/components/ui/badge";9import { Page, PageHeader, Section } from "@/components/ui/card";10import { EmptyState, OfflineNotice } from "@/components/ui/states";11import { api } from "@/lib/api";12import { fmtInt } from "@/lib/format";13import { routes } from "@/lib/routes";14import { pageMetadata } from "@/lib/seo";1516export const revalidate = 120;1718export const metadata: Metadata = pageMetadata({19  title: "Downloads",20  description: "Download the index as CSV, JSON or GeoJSON: facilities, projects, operators, events, cloud regions, internet exchanges, countries and markets — licence-gated, with attribution and excluded sources listed per dataset.",21  path: routes.download(),22});2324export default async function DownloadPage() {25  const r = await api.downloadDatasets();26  const datasets = r.ok ? r.data : [];27  return (28    <Page>29      <Breadcrumbs items={[{ name: "Downloads", href: routes.download() }]} className="pt-3" />30      <PageHeader eyebrow="Downloads" title="Datasets" description="Every dataset streams straight from the API with the filters below. Rows whose only sources forbid redistribution are excluded and listed; each row keeps the names of the sources it was built from." className="pt-3" />31      {!r.ok && <OfflineNotice detail={r.error} className="mb-3" />}32      <ApiNote label="Limits" note="Large datasets are capped per request (50 000 rows) — narrow with filters to fetch a complete slice. JSON responses carry meta.excludedSources; CSV/GeoJSON carry the same list in the x-dci-excluded-sources header. Figures are published values only, with the coverage caveats of the API." className="mb-4">33        {" "}34        <Link href={routes.apiDocsPage()} className="underline decoration-hair-2 hover:text-ink">35          API documentation36        </Link>37      </ApiNote>3839      {datasets.length === 0 ? (40        <EmptyState title="No dataset available" description="The API did not return the dataset registry." />41      ) : (42        <>43          <SectionNav items={datasets.map((d) => ({ id: d.key, label: d.label, count: d.rows }))} />44          <div className="mt-6 flex flex-col gap-10">45            {datasets.map((d) => (46              <Section47                key={d.key}48                id={d.key}49                title={d.label}50                description={d.description}51                className="scroll-mt-28"52                aside={53                  <div className="flex flex-col items-end gap-1">54                    <span className="figure text-[13px] text-ink">55                      {fmtInt(d.rows)} <span className="text-ink-3">rows</span>56                    </span>57                    <span className="inline-flex gap-1">58                      {d.formats.map((f) => (59                        <Badge key={f} tone="outline" size="xs" mono>60                          {f}61                        </Badge>62                      ))}63                    </span>64                  </div>65                }66              >67                <div className="grid gap-5 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">68                  <div className="min-w-0">69                    <DatasetForm dataset={d} />70                  </div>71                  <div className="min-w-0 flex flex-col gap-3 text-[12.5px]">72                    <div>73                      <div className="label-caps mb-1">Licence</div>74                      <p className="leading-relaxed text-ink-2">{d.license}</p>75                    </div>76                    <div>77                      <div className="label-caps mb-1">Attribution ({fmtInt(d.attribution.length)})</div>78                      {d.attribution.length === 0 ? (79                        <p className="text-ink-3">No attribution line recorded for this dataset&rsquo;s sources — the source registry applies.</p>80                      ) : d.attribution.length <= 6 ? (81                        <ul className="flex flex-wrap gap-1">82                          {d.attribution.map((a) => (83                            <li key={a}>84                              <Badge tone="outline" size="xs" className="max-w-[260px]">85                                <span className="truncate">{a}</span>86                              </Badge>87                            </li>88                          ))}89                        </ul>90                      ) : (91                        <details>92                          <summary className="inline-flex min-h-10 cursor-pointer items-center text-ink-2 hover:text-ink sm:min-h-0">Show all {fmtInt(d.attribution.length)} attribution lines</summary>93                          <ul className="mt-1.5 flex flex-wrap gap-1">94                            {d.attribution.map((a) => (95                              <li key={a}>96                                <Badge tone="outline" size="xs" className="max-w-[260px]">97                                  <span className="truncate">{a}</span>98                                </Badge>99                              </li>100                            ))}101                          </ul>102                        </details>103                      )}104                    </div>105                    <div>106                      <div className="label-caps mb-1">Excluded sources</div>107                      {d.excludedSources.length === 0 ? (108                        <p className="text-ink-3">None — every source behind this dataset allows redistribution with attribution.</p>109                      ) : (110                        <ul className="divide-y divide-hair">111                          {d.excludedSources.map((s) => (112                            <li key={s.id} className="py-1.5">113                              <Link href={routes.source(s.id)} className="font-medium text-ink hover:text-accent hover:underline">114                                {s.name}115                              </Link>116                              <span className="text-ink-3"> — {s.reason}</span>117                            </li>118                          ))}119                        </ul>120                      )}121                    </div>122                    {d.filters.length === 0 && <HonestEmpty title="Whole-table dataset" detail="no filter is defined for this key" />}123                  </div>124                </div>125              </Section>126            ))}127          </div>128        </>129      )}130131      <SourcesFooter sources={r.ok ? r.sources : undefined} note="Re-users must keep the per-row source attribution and respect each source's licence (see /sources). Nothing is imputed; rows without a published figure carry an empty cell, not zero." />132    </Page>133  );134}135