/** * earth-now.co * Author: Simon-Pierre Boucher * Contact: contact@spboucher.ai * File: apps/web/components/MetricDetail.tsx * Purpose: Per-metric page body — big live counter and the metric's own methodology section (model family, sources, uncertainty) */ "use client"; import Link from "next/link"; import { useEffect, useMemo, useState } from "react"; import { formatUncertainty, formatValue, type CounterModel } from "@earth-now/counter"; import type { Locale, MetricSummary } from "@/lib/api"; import { hintsFor, resolvePrimaryWindow } from "@/lib/derived"; import { I18nProvider, useI18n } from "@/lib/i18n"; import { formatDateUtc, isLocale, type MessageKey } from "@/lib/messages"; import { tierFor } from "@/lib/tiers"; import LiveCounter from "./LiveCounter"; const LOCALE_STORAGE_KEY = "earth-now.locale"; const PERCENT_HINTS = { decimals: 0, unit: "%" } as const; export interface MetricDetailProps { metric: MetricSummary; model: CounterModel | null; } export default function MetricDetail({ metric, model }: MetricDetailProps) { const [locale, setLocaleState] = useState("fr"); useEffect(() => { const saved = window.localStorage.getItem(LOCALE_STORAGE_KEY); if (isLocale(saved)) setLocaleState(saved); }, []); useEffect(() => { document.documentElement.lang = locale; }, [locale]); const setLocale = (next: Locale) => { setLocaleState(next); window.localStorage.setItem(LOCALE_STORAGE_KEY, next); }; return ( ); } function MetricDetailBody({ metric, model }: MetricDetailProps) { const { locale, t } = useI18n(); const sessionStart = useMemo(() => Date.now(), []); const live = model !== null && tierFor(metric, model, sessionStart) === "live"; const hints = hintsFor(metric, locale); return (

{t("metric.methodTitle")}

{familyExplanation(metric.model, t)}

{t(`kind.${metric.kind}` as MessageKey)} · {t(`domain.${metric.domain}` as MessageKey)} {metric.model} {model !== null && model.modelVersion !== metric.model ? ` (${model.modelVersion})` : ""}{" "} — {t("metric.level")} {String(metric.level)} {model !== null && ( {formatDateUtc(model.observedAt, locale)} )} {model?.uncertainty !== undefined && ( {formatUncertainty(model.uncertainty.low, model.uncertainty.high, hints, { locale, })}{" "} {metric.display.unit[locale]} )} {metric.uncertaintyFraction !== undefined && ( ± {formatValue(metric.uncertaintyFraction * 100, PERCENT_HINTS, { locale })} % ( {t("info.estimateNote")}) )} {metric.windows.map((w) => t(`window.${w}` as MessageKey)).join(" · ")}

{t("methodology.source")}

    {metric.sources.map((source) => (
  • {source.name}
    {t("methodology.license")} : {source.license} · {t("methodology.cadence")} :{" "} {source.cadence}
  • ))}
{metric.editorialNote !== undefined && (

{metric.editorialNote[locale]}

)}

{t("metric.fullMethodology")}

); } function MethodRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } /** One-sentence how-it-works per model family (the per-metric methodology promise). */ function familyExplanation(family: string, t: (key: MessageKey) => string): string { const known = new Set([ "seasonal-spline-v2", "keeling-fusion-v1", "seasonal-ytd-v1", "linear-ytd-v1", "linear-stock-v1", "static-rt-v1", "derived", ]); return known.has(family) ? t(`explain.${family}` as MessageKey) : t("explain.default"); }