spb/earth-now Public License
earth-now.co — real-time planetary dashboard: live world metrics modeled, not streamed.
TypeScript 93%
Shell 2.3%
SQL 1.4%
JavaScript 1.3%
Dockerfile 1.2%
CSS 0.8%
1/**2 * earth-now.co3 * Author: Simon-Pierre Boucher4 * Contact: contact@spboucher.ai5 * File: apps/web/components/CountryRanking.tsx6 * Purpose: Top-10 countries by population — live re-ranked list, ticking values, single-hue magnitude bars (length encodes, not color)7 */89"use client";1011import { useEffect, useState } from "react";12import { counterValue, formatRate, formatValue, rateAt, type CounterModel } from "@earth-now/counter";13import type { MetricSummary } from "@/lib/api";14import { useI18n } from "@/lib/i18n";1516const FLAGS: Record<string, string> = {17 country_population_india: "🇮🇳",18 country_population_china: "🇨🇳",19 country_population_usa: "🇺🇸",20 country_population_indonesia: "🇮🇩",21 country_population_pakistan: "🇵🇰",22 country_population_nigeria: "🇳🇬",23 country_population_brazil: "🇧🇷",24 country_population_bangladesh: "🇧🇩",25 country_population_russia: "🇷🇺",26 country_population_mexico: "🇲🇽",27 continent_population_asia: "🌏",28 continent_population_africa: "🌍",29 continent_population_europe: "🌍",30 continent_population_latam: "🌎",31 continent_population_north_america: "🌎",32 continent_population_oceania: "🌏",33};3435interface Row {36 metric: MetricSummary;37 value: number;38 perSecond: number;39}4041export interface CountryRankingProps {42 metrics: MetricSummary[];43 models: Record<string, CounterModel>;44}4546export default function CountryRanking({ metrics, models }: CountryRankingProps) {47 const { locale } = useI18n();48 const [rows, setRows] = useState<Row[]>([]);4950 useEffect(() => {51 const compute = () => {52 const now = Date.now();53 const next: Row[] = [];54 for (const metric of metrics) {55 const model = models[metric.id];56 if (model === undefined) continue;57 const value = counterValue(model, now);58 if (!Number.isFinite(value)) continue;59 next.push({ metric, value, perSecond: rateAt(model, now) });60 }61 next.sort((a, b) => b.value - a.value);62 setRows(next);63 };64 compute();65 const interval = window.setInterval(compute, 1_000);66 return () => window.clearInterval(interval);67 }, [metrics, models]);6869 if (rows.length === 0) return null;70 const max = rows[0]!.value;7172 return (73 <ol className="divide-y divide-line rounded-2xl border bg-surface">74 {rows.map((row, index) => {75 const hints = {76 decimals: row.metric.display.decimals,77 unit: row.metric.display.unit[locale],78 ...(row.metric.display.scale !== undefined ? { scale: row.metric.display.scale } : {}),79 };80 return (81 <li key={row.metric.id} className="flex min-w-0 items-center gap-3 px-4 py-2.5 transition-colors hover:bg-page/60">82 <span className="w-5 shrink-0 text-right text-xs text-muted tabular-nums">83 {index + 1}84 </span>85 <span aria-hidden className="shrink-0 text-base leading-none">86 {FLAGS[row.metric.id] ?? "🌍"}87 </span>88 <span className="w-28 shrink-0 truncate text-sm text-ink sm:w-36">89 {row.metric.name[locale]}90 </span>91 <span className="hidden flex-1 sm:block" aria-hidden>92 {/* Magnitude bar: single hue, length encodes the value (4px rounded ends). */}93 <span className="block h-1.5 w-full rounded-full bg-line">94 <span95 className="block h-1.5 rounded-full bg-accent transition-[width] duration-1000 ease-linear"96 style={{ width: `${Math.max(2, (row.value / max) * 100)}%` }}97 />98 </span>99 </span>100 <span className="ml-auto min-w-0 shrink text-right text-sm font-medium text-ink tabular-nums">101 {formatValue(row.value, hints, { locale, applySigFigs: false })}102 </span>103 <span className="hidden w-20 shrink-0 text-right text-xs text-ink2 tabular-nums sm:block">104 {row.perSecond >= 0 ? "+" : ""}105 {formatRate(row.perSecond, hints, { locale })}106 </span>107 </li>108 );109 })}110 </ol>111 );112}113