spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
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