spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1'use client';2import Link from 'next/link';3import { useEffect, useMemo, useState } from 'react';4import { t } from '@/i18n';5import { clientApi } from '@/lib/client-api';6import { routes } from '@/lib/site';7import type { DNAResponse, DnaDimension } from '@/lib/types';8import { DNA_DIMS, DnaRadial } from '@/components/charts/dna-radial';9import { Segmented } from '@/components/controls/indicator-select';10import { EntityPicker, type PickedEntity } from '@/components/explore/entity-picker';1112type RefKind = 'world' | 'region' | 'income' | 'country';1314/**15 * Country DNA 2.0: the fingerprint with a reference profile — World (50 on every axis), the region's median,16 * income peers' median or another country — fetched from `/countries/{id}/dna?reference=`. The table below lists17 * the nine percentile dimensions with the reference values. Descriptive, never a score.18 */19export function DnaPanel({ countryId, name, initial }: { countryId: string; name: string; initial: DNAResponse | null }) {20 const [kind, setKind] = useState<RefKind>('world');21 const [peer, setPeer] = useState<PickedEntity | null>(null);22 const [cache, setCache] = useState<Record<string, DNAResponse | null>>({});23 const [loading, setLoading] = useState(false);24 const refKey = kind === 'country' ? (peer ? `country:${peer.id}` : null) : kind === 'world' ? null : kind;2526 useEffect(() => {27 if (!refKey || cache[refKey] !== undefined) return;28 const ctrl = new AbortController();29 setLoading(true);30 const reference = refKey.startsWith('country:') ? refKey.slice(8) : refKey;31 clientApi32 .countryDna(countryId, reference, ctrl.signal)33 .then((r) => setCache((c) => ({ ...c, [refKey]: r })))34 .catch(() => setCache((c) => ({ ...c, [refKey]: null })))35 .finally(() => {36 if (!ctrl.signal.aborted) setLoading(false);37 });38 return () => ctrl.abort();39 }, [refKey, countryId, cache]);4041 const base = initial;42 const withRef = refKey ? cache[refKey] : null;43 const reference: Record<string, number | null> | null = useMemo(() => {44 if (kind === 'world') return Object.fromEntries(DNA_DIMS.map((d) => [d, 50]));45 return withRef?.reference?.dims ?? null;46 }, [kind, withRef]);47 const refLabel = kind === 'world' ? t('country.dna.ref.worldLabel') : withRef?.reference?.label ?? (kind === 'country' ? peer?.name ?? null : null);4849 if (!base) return <p className="text-sm text-ink-3">{t('country.dna.none')}</p>;50 return (51 <div className="min-w-0">52 <div className="flex flex-wrap items-center gap-2">53 <Segmented<RefKind> value={kind} onChange={setKind} label={t('country.dna.ref')} size="sm" options={[{ value: 'world', label: t('country.dna.ref.world') }, { value: 'region', label: t('country.dna.ref.region') }, { value: 'income', label: t('country.dna.ref.income') }, { value: 'country', label: t('country.dna.ref.country') }]} />54 {kind === 'country' ? <EntityPicker type="country" placeholder={t('country.dna.ref.pick')} onPick={setPeer} exclude={[countryId]} keepValue className="w-56" size="sm" /> : null}55 </div>56 <div className={loading ? 'mt-3 opacity-60 transition-opacity' : 'mt-3'} aria-busy={loading}>57 <DnaRadial dna={base} name={name} size={360} reference={kind === 'country' && !peer ? null : reference} referenceLabel={refLabel} />58 </div>59 <p className="mt-2 text-center text-xs text-ink-3">60 {t('country.dna.notScore')}61 {base.year_ref ? <span className="tnum"> · {base.year_ref}</span> : null}62 </p>63 <table className="mt-3 w-full border-collapse text-xs">64 <caption className="sr-only">{t('country.dna.title')}</caption>65 <thead>66 <tr className="border-b border-rule text-left text-2xs uppercase tracking-wide text-ink-3">67 <th scope="col" className="py-1 pr-2 font-medium">68 {t('country.dna.dimension')}69 </th>70 <th scope="col" className="py-1 pr-2 text-right font-medium">71 {name}72 </th>73 {reference && (kind !== 'country' || peer) ? (74 <th scope="col" className="py-1 text-right font-medium">75 {refLabel}76 </th>77 ) : null}78 </tr>79 </thead>80 <tbody className="divide-y divide-rule">81 {DNA_DIMS.map((d: DnaDimension) => {82 const v = base.dims[d];83 const r = reference?.[d];84 const row = base.dimensions.find((x) => x.id === d);85 return (86 <tr key={d}>87 <td className="py-1 pr-2 text-ink-2">88 {row?.indicator ? (89 <Link href={routes.indicator(row.indicator)} className="link-quiet -my-2 inline-flex min-h-[44px] items-center py-2 md:my-0 md:min-h-0 md:py-0">90 {t(`country.dna.${d}` as const)}91 </Link>92 ) : (93 t(`country.dna.${d}` as const)94 )}95 </td>96 <td className="tnum py-1 pr-2 text-right font-medium text-ink">{v != null ? Math.round(v) : '—'}</td>97 {reference && (kind !== 'country' || peer) ? <td className="tnum py-1 text-right text-ink-2">{r != null ? Math.round(r) : '—'}</td> : null}98 </tr>99 );100 })}101 </tbody>102 </table>103 </div>104 );105}106