spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1'use client';2import Link from 'next/link';3import { useEffect, useState } from 'react';4import { t } from '@/i18n';5import { clientExplore } from '@/lib/client-api-explore';6import { cn } from '@/lib/cn';7import { fixed } from '@/lib/format';8import { routes } from '@/lib/site';9import type { SimilarResponse, SimilarityMode } from '@/lib/types';10import { EntityPicker, type PickedEntity } from '@/components/explore/entity-picker';1112const MODES: SimilarityMode[] = ['overall', 'economic', 'demographic', 'energy', 'social'];1314/** Pick a country → peers per mode from `/countries/{id}/similar?mode=`. Mode tabs, score bars (0–100). */15export function SimilarityPlayground({ initial }: { initial?: PickedEntity | null }) {16 const [country, setCountry] = useState<PickedEntity | null>(initial ?? null);17 const [mode, setMode] = useState<SimilarityMode>('overall');18 const [data, setData] = useState<Record<string, SimilarResponse | null>>({});19 const [loading, setLoading] = useState(false);2021 useEffect(() => {22 if (!country) return;23 const key = `${country.id}:${mode}`;24 if (data[key] !== undefined) return;25 const ctrl = new AbortController();26 setLoading(true);27 clientExplore28 .similar(country.id, mode, 10, ctrl.signal)29 .then((r) => setData((d) => ({ ...d, [key]: r })))30 .catch(() => setData((d) => ({ ...d, [key]: null })))31 .finally(() => {32 if (!ctrl.signal.aborted) setLoading(false);33 });34 return () => ctrl.abort();35 }, [country, mode, data]);3637 const current = country ? data[`${country.id}:${mode}`] : undefined;38 const peers = current?.peers ?? [];3940 return (41 <div className="min-w-0">42 <div className="flex flex-wrap items-center gap-2">43 <EntityPicker type="country" placeholder={t('explore.similar.pick')} onPick={setCountry} className="w-full max-w-sm" />44 {country ? (45 <Link href={routes.country(country.slug)} className="inline-flex min-h-[44px] items-center gap-1.5 text-sm text-ink md:min-h-[36px]">46 <span aria-hidden className="text-lg leading-none">{country.flag}</span>47 <span className="font-medium">{country.name}</span>48 <span className="text-ink-3">→</span>49 </Link>50 ) : null}51 </div>52 {country ? (53 <>54 <div role="tablist" aria-label={t('explore.similar.peers', { name: country.name })} className="scrollbar-none -mx-4 mt-3 flex gap-1 overflow-x-auto px-4 sm:mx-0 sm:px-0">55 {MODES.map((m) => (56 <button key={m} role="tab" aria-selected={mode === m} type="button" onClick={() => setMode(m)} className={cn('inline-flex h-11 shrink-0 items-center rounded-sm px-3 text-sm md:h-9', mode === m ? 'bg-ink text-paper' : 'text-ink-2 hover:bg-surface-2 hover:text-ink')}>57 {t(`country.similar.mode.${m}` as const)}58 </button>59 ))}60 </div>61 <div className={cn('mt-3 min-h-[240px] transition-opacity', loading && 'opacity-50')} aria-busy={loading}>62 {current === null || (current && peers.length === 0) ? (63 <p className="py-4 text-sm text-ink-3">{t('explore.similar.none', { name: country.name })}</p>64 ) : (65 <ol className="divide-y divide-rule">66 {peers.map((p) => (67 <li key={p.country.id} className="grid grid-cols-[1.5rem_minmax(0,1fr)_3rem] items-center gap-x-2 py-1 sm:grid-cols-[1.5rem_minmax(0,1fr)_minmax(6rem,14rem)_3rem] sm:py-1.5">68 <span className="tnum text-xs text-ink-3">{p.rank}</span>69 <Link href={routes.country(p.country.slug ?? p.country.id)} className="link-quiet flex min-h-[44px] min-w-0 items-center gap-1.5 text-sm sm:min-h-0">70 <span aria-hidden className="text-base leading-none">{p.country.flag}</span>71 <span className="truncate">{p.country.name}</span>72 </Link>73 <div className="col-span-full mb-1 h-2 rounded-xs bg-surface-2 sm:col-span-1 sm:mb-0" aria-hidden>74 <div className="h-full rounded-xs bg-accent" style={{ width: `${Math.max(2, Math.min(100, p.score ?? 0))}%` }} />75 </div>76 <span className="tnum text-right text-sm font-medium text-ink" aria-label={`${t('explore.similar.score')} ${fixed(p.score ?? 0, 0)}`}>77 {p.score != null ? fixed(p.score, 0) : t('common.na')}78 </span>79 </li>80 ))}81 </ol>82 )}83 </div>84 </>85 ) : (86 <p className="mt-4 text-sm text-ink-3">{t('explore.similar.empty')}</p>87 )}88 </div>89 );90}91