SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
4.5 KB · 92 lines tsx
Raw Blame History
1import { ExternalLink } from 'lucide-react';2import type { Metadata } from 'next';3import Link from 'next/link';4import { t } from '@/i18n';5import { isNotBuilt } from '@/lib/api';6import { apiExplore } from '@/lib/api-explore';7import { compact, formatDate, grouped } from '@/lib/format';8import { routes } from '@/lib/site';9import type { SourcesResponse } from '@/lib/types-explore';10import { NotBuiltState } from '@/components/data/empty-state';11import { FreshnessBadge } from '@/components/data/freshness-badge';12import { PageHeader } from '@/components/explore/page-header';13import { FooterCredits } from '@/components/layout/site-footer';1415export const revalidate = 900;1617export const metadata: Metadata = {18  title: t('sources.title'),19  description: t('sources.sub', { n: 9 }),20  alternates: { canonical: routes.sources() },21};2223export default async function SourcesPage() {24  let data: SourcesResponse | null = null;25  try {26    data = await apiExplore.sources();27  } catch (e) {28    if (!isNotBuilt(e)) throw e;29  }30  const items = data?.items ?? [];31  return (32    <>33      <PageHeader title={t('sources.title')} lede={t('sources.sub', { n: grouped(items.length || 9) })} />34      {!data ? (35        <NotBuiltState />36      ) : (37        <>38          <p className="max-w-prose border-y border-rule py-4 text-sm leading-relaxed text-ink-2">{t('sources.rule')}</p>39          <ol className="divide-y divide-rule">40            {items.map((s) => (41              <li key={s.id} className="grid gap-x-8 gap-y-2 py-4 md:grid-cols-[minmax(0,1fr)_16rem]">42                <div className="min-w-0">43                  <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">44                    <Link href={routes.source(s.id)} className="link-quiet text-lg font-semibold text-ink">45                      {s.name ?? s.id}46                    </Link>47                    <span className="text-sm text-ink-3">{s.organization}</span>48                    {s.licence ? <span className="rounded-xs border border-rule px-1 py-px text-2xs text-ink-2">{s.licence}</span> : null}49                  </div>50                  {s.attribution ? <p className="mt-1 text-sm text-ink-2">{s.attribution}</p> : null}51                  <div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">52                    <code className="font-mono text-ink-3">{s.id}</code>53                    {s.url ? (54                      <a href={s.url} target="_blank" rel="noopener noreferrer" className="inline-flex min-h-[32px] items-center gap-1 text-accent hover:underline">55                        <ExternalLink size={12} aria-hidden /> {t('sources.website')}56                      </a>57                    ) : null}58                    <Link href={routes.source(s.id)} className="inline-flex min-h-[32px] items-center text-accent hover:underline">59                      {t('common.more')} →60                    </Link>61                  </div>62                </div>63                <dl className="tnum grid grid-cols-2 gap-x-4 gap-y-1 text-sm md:grid-cols-1">64                  <div className="flex justify-between gap-2 md:justify-start">65                    <dt className="text-ink-3">{t('sources.observations', { n: '' }).trim()}</dt>66                    <dd className="text-ink">{s.n_observations != null ? compact(s.n_observations) : t('common.na')}</dd>67                  </div>68                  <div className="flex justify-between gap-2 md:justify-start">69                    <dt className="text-ink-3">{t('sources.indicators', { n: '' }).trim()}</dt>70                    <dd className="text-ink">{s.n_indicators != null ? grouped(s.n_indicators) : t('common.na')}</dd>71                  </div>72                  <div className="col-span-2 flex flex-wrap items-center gap-2 md:col-span-1">73                    <dt className="sr-only">{t('sources.lastImport', { date: '' })}</dt>74                    <dd className="text-xs text-ink-3">{t('sources.lastImport', { date: formatDate(s.last_success_at ?? s.last_retrieved_at) })}</dd>75                    <dd>76                      <FreshnessBadge retrievedAt={s.last_success_at ?? s.last_retrieved_at} />77                    </dd>78                  </div>79                </dl>80              </li>81            ))}82          </ol>83          <div className="mt-8 border-t border-rule pt-4 text-sm text-ink-2">84            <span className="mr-2 font-medium text-ink">{t('sources.contact')}</span>85            <FooterCredits className="mt-1 inline text-xs" />86          </div>87        </>88      )}89    </>90  );91}92