spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1import Link from 'next/link';2import { t } from '@/i18n';3import { formatDate } from '@/lib/format';4import { routes } from '@/lib/site';5import { Logo } from '@/components/brand/Logo';67/**8 * Site footer: explore/reference link columns, licence note, credits ("Made by … · contact", "Hosted on MacLustr").9 * `builtAt`/`runId` (optional) come from the API `meta` of the page that renders it. Reuse `<FooterCredits />`10 * on /sources and /api pages for the contact line.11 */12export function SiteFooter({ builtAt, runId }: { builtAt?: string | null; runId?: string | null }) {13 const explore = [14 { href: routes.explore(), label: t('nav.explore') },15 { href: routes.countries(), label: t('nav.countries') },16 { href: routes.compare(), label: t('nav.compare') },17 { href: routes.rankings(), label: t('nav.rankings') },18 { href: routes.indicators(), label: t('nav.indicators') },19 { href: routes.regions(), label: t('nav.regions') },20 { href: routes.changes(), label: t('site.footer.changes') },21 { href: routes.trajectories(), label: t('nav.trajectories') },22 { href: routes.scatter(), label: t('nav.scatter') },23 { href: routes.finder(), label: t('nav.finder') },24 { href: routes.extremes(), label: t('nav.extremes') },25 { href: routes.stories(), label: t('nav.stories') },26 ];27 const reference = [28 { href: routes.sources(), label: t('site.footer.sources') },29 { href: routes.methodology(), label: t('site.footer.methodology') },30 { href: routes.api(), label: t('site.footer.api') },31 { href: routes.download(), label: t('site.footer.data') },32 { href: routes.updates(), label: t('nav.updates') },33 { href: routes.peers(), label: t('nav.peers') },34 ];35 return (36 <footer className="mt-16 border-t border-rule bg-surface-2/40 text-sm">37 <div className="container-x mx-auto max-w-[1400px] py-10">38 <div className="grid gap-8 md:grid-cols-[1.4fr_1.2fr_1fr]">39 <div className="max-w-md">40 <Logo variant="full" />41 <p className="mt-3 text-ink-2">{t('site.tagline')}</p>42 <p className="mt-3 text-xs leading-relaxed text-ink-3">{t('site.licence')}</p>43 {builtAt ? (44 <p className="tnum mt-3 text-xs text-ink-3">45 {t('site.footer.refreshed', { date: formatDate(builtAt) })}46 {runId ? ` · ${t('site.footer.build', { run: runId })}` : ''}47 </p>48 ) : null}49 </div>50 <FooterColumn title={t('site.footer.explore')} items={explore} />51 <FooterColumn title={t('site.footer.reference')} items={reference} />52 </div>53 <div className="mt-8 flex flex-col gap-2 border-t border-rule pt-5 text-xs text-ink-2 md:flex-row md:items-center md:justify-between">54 <FooterCredits />55 <p className="text-ink-3">{t('site.footer.made')}</p>56 </div>57 </div>58 </footer>59 );60}6162function FooterColumn({ title, items }: { title: string; items: Array<{ href: string; label: string }> }) {63 return (64 <div>65 <div className="eyebrow mb-2">{title}</div>66 <ul className="grid grid-cols-2 gap-x-4 gap-y-1 sm:grid-cols-1">67 {items.map((it) => (68 <li key={it.href}>69 <Link href={it.href} className="inline-flex min-h-[44px] items-center text-ink-2 hover:text-accent md:min-h-[32px]">70 {it.label}71 </Link>72 </li>73 ))}74 </ul>75 </div>76 );77}7879/** "Made by Simon-Pierre Boucher · contact@spboucher.ai" + "Hosted on MacLustr". */80export function FooterCredits({ className }: { className?: string }) {81 return (82 <p className={className}>83 {t('site.footer.madeBy')} <span className="text-ink">{t('site.footer.author')}</span> ·{' '}84 <a href={`mailto:${t('site.footer.contact')}`} className="text-accent hover:underline">85 {t('site.footer.contact')}86 </a>87 <span className="mx-2 text-ink-3">·</span>88 {t('site.footer.hostedOn')}{' '}89 <a href="https://www.maclustr.io" target="_blank" rel="noopener noreferrer" className="text-accent hover:underline">90 {t('site.footer.host')}91 </a>92 </p>93 );94}95