SPB Git

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%
4.1 KB · 113 lines tsx
Raw Blame History
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