spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { t } from '@/i18n';4import { isNotBuilt } from '@/lib/api';5import { apiExplore } from '@/lib/api-explore';6import { formatDate } from '@/lib/format';7import { routes } from '@/lib/site';8import type { ChangesFeedResponse } from '@/lib/types-explore';9import { ChangesFeed } from '@/components/changes/changes-feed';10import { NotBuiltState } from '@/components/data/empty-state';11import { Section } from '@/components/data/section';12import { PageHeader } from '@/components/explore/page-header';13import { Movers } from '@/components/home/movers';14import { apiAnalytics } from '@/lib/api-analytics';15import { safe } from '@/lib/api';1617export const revalidate = 900;1819export const metadata: Metadata = {20 title: t('changes.title'),21 description: t('changes.sub'),22 alternates: { canonical: routes.changes() },23};2425type SP = Record<string, string | string[] | undefined>;2627export default async function ChangesPage({ searchParams }: { searchParams: Promise<SP> }) {28 const sp = await searchParams;29 const countryRaw = Array.isArray(sp.country) ? sp.country[0] : sp.country;30 const country = countryRaw && /^[a-z0-9-]+$/i.test(countryRaw) ? countryRaw : undefined;31 let data: ChangesFeedResponse | null = null;32 try {33 data = await apiExplore.changes({ limit: 100, country });34 } catch (e) {35 if (!isNotBuilt(e)) throw e;36 }37 const winRaw = Array.isArray(sp.window) ? sp.window[0] : sp.window;38 const win: 5 | 10 = winRaw === '10' ? 10 : 5;39 const movers = await safe(apiAnalytics.movers({ window: win, limit: 12 }));40 return (41 <>42 <PageHeader title={t('changes.title')} lede={t('changes.sub')} meta={data?.meta.built_at ? t('site.footer.refreshed', { date: formatDate(data.meta.built_at) }) : undefined} />43 {country ? (44 <p className="mb-3 text-sm text-ink-2">45 {t('changes.filteredCountry', { country })}{' '}46 <Link href={routes.changes()} className="text-accent hover:underline">47 {t('changes.clearCountry')}48 </Link>49 </p>50 ) : null}51 {!data ? (52 <NotBuiltState />53 ) : (54 <>55 <nav aria-label={t('changes.windows.nav')} className="mb-3 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-ink-2">56 <span>{t('changes.windows.nav')}</span>57 <span className="inline-flex min-h-[36px] items-center rounded-sm bg-ink px-2 text-xs text-paper">{t('home.movers.window.1')}</span>58 <Link href={`${routes.changes()}?window=5#windows`} className="inline-flex min-h-[36px] items-center text-accent hover:underline">59 {t('home.movers.window.5')} ↓60 </Link>61 <Link href={`${routes.changes()}?window=10#windows`} className="inline-flex min-h-[36px] items-center text-accent hover:underline">62 {t('home.movers.window.10')} ↓63 </Link>64 </nav>65 <details className="mb-2 border-y border-rule py-3 text-sm">66 <summary className="flex min-h-[44px] cursor-pointer list-none items-center gap-2 font-medium text-ink-2 hover:text-ink">67 <span aria-hidden>›</span> {t('changes.methodology')}68 </summary>69 <p className="mt-2 max-w-prose leading-relaxed text-ink-2">{t('changes.how')}</p>70 <p className="mt-2 text-xs">71 <Link href={`${routes.methodology()}#changes`} className="text-accent hover:underline">72 {t('site.footer.methodology')} →73 </Link>74 </p>75 </details>76 <ChangesFeed initial={data.items} kinds={data.kinds ?? []} limit={100} />77 <Section id="windows" title={t('changes.windows.title')} subtitle={t('changes.windows.sub')}>78 <Movers key={win} initial={movers} initialWindow={win} limit={12} compact />79 </Section>80 </>81 )}82 </>83 );84}85