/* global React, ReactDOM */
const { useState: useS, useEffect: useE, useRef: useR } = React;
function useLang() {
const [lang, setLang] = useS(() => localStorage.getItem("penumbra_lang") || "fr");
useE(() => { localStorage.setItem("penumbra_lang", lang); document.documentElement.lang = lang; }, [lang]);
return [lang, setLang];
}
function useHashRoute() {
return "predict";
}
function todayStr() {
return new Date().toISOString().slice(0, 10);
}
function App() {
const [lang, setLang] = useLang();
const route = useHashRoute();
const t = (k) => (window.I18N[lang] && window.I18N[lang][k]) || k;
return (
<>
>
);
}
function PredictPage({ t, lang }) {
const [depIata, setDepIata] = useS("CDG");
const [arrIata, setArrIata] = useS("JFK");
const [date, setDate] = useS(todayStr());
const [departureTime, setDepartureTime] = useS("10:00");
const [error, setError] = useS("");
const [loading, setLoading] = useS(false);
const [trajectory, setTrajectory] = useS(null);
const [meta, setMeta] = useS(null);
const [solarEvent, setSolarEvent] = useS(null);
const onCalculate = async () => {
setError("");
setTrajectory(null);
setSolarEvent(null);
const dep = depIata.trim().toUpperCase();
const arr = arrIata.trim().toUpperCase();
if (!dep || !arr || dep.length < 3 || arr.length < 3) {
setError(t("err_iata_required"));
return;
}
setLoading(true);
try {
const { trajectory: traj, meta: m, solarEvent: se } = await window.buildTrajectory({
depIata: dep,
arrIata: arr,
date,
departureTime,
});
setTrajectory(traj);
setMeta(m);
setSolarEvent(se);
} catch (e) {
setError(e.message || t("err_unknown_flight"));
} finally {
setLoading(false);
}
};
const onSubmit = (e) => { e.preventDefault(); onCalculate(); };
return (
<>
{t("hero_title")}
{t("hero_lead")}
{t("hero_badge")}
{!trajectory && (
)}
{trajectory && (
<>
{solarEvent && (
)}
{ setTrajectory(null); setMeta(null); setSolarEvent(null); }}
/>
>
)}
}
title={t("exp1_title")} text={t("exp1_text")}
/>
}
title={t("exp2_title")} text={t("exp2_text")}
/>
}
title={t("exp3_title")} text={t("exp3_text")}
/>
>
);
}
function AirportInput({ label, value, onChange, placeholder }) {
const [query, setQuery] = useS(value || "");
const [suggestions, setSuggestions] = useS([]);
const [open, setOpen] = useS(false);
const timer = useR(null);
const fetch_ = (q) => {
clearTimeout(timer.current);
if (q.length < 2) { setSuggestions([]); return; }
timer.current = setTimeout(async () => {
try {
const r = await fetch(`/airports/search?q=${encodeURIComponent(q)}`);
if (r.ok) setSuggestions(await r.json());
} catch (_) {}
}, 180);
};
const onInput = (e) => {
const v = e.target.value;
setQuery(v);
setOpen(true);
fetch_(v);
};
const onSelect = (ap) => {
const display = `${ap.iata} — ${ap.city}`;
setQuery(display);
onChange(ap.iata);
setSuggestions([]);
setOpen(false);
};
const onBlur = () => setTimeout(() => setOpen(false), 160);
const onFocus = () => { if (query.length >= 2) setOpen(true); };
return (
{open && suggestions.length > 0 && (
{suggestions.map((ap) => (
- onSelect(ap)}>
{ap.iata}
{ap.city}
{ap.name}
))}
)}
);
}
function SolarEventAlert({ event, t }) {
const stormColor = { g1: "#e6b800", g2: "#e68600", g3: "#e65200", g4: "#c42b00", g5: "#8b0000" };
const scale = event.scale || "";
const badgeColor = stormColor[scale.toLowerCase()] || "#c42b00";
return (
{t("solar_alert_title")}
{scale && (
{t("solar_alert_storm")}
{scale}
— Kp max {event.kp_max}
)}
{event.flare_class && (
{t("solar_alert_flare")} {event.flare_class}
)}
{t("solar_alert_note")}
);
}
function ExpCard({ icon, title, text }) {
return (
);
}
function ResultPanel({ trajectory, meta, t, lang, onReset }) {
const dose = trajectory.totalDose;
const low = trajectory.lowDose;
const high = trajectory.highDose;
const riskKey = { low: "risk_low", mod: "risk_mod", high: "risk_high" }[trajectory.totalRisk];
const chestXrays = dose / 100;
const annualPct = (dose / 1000) * 100;
const groundHours = dose / 0.27;
const groundDays = groundHours / 24;
const fmtDose = window.fmtNumber(dose, 0);
const fmtLow = window.fmtNumber(low, 0);
const fmtHigh = window.fmtNumber(high, 0);
const dateLocale = lang === "fr" ? "fr-FR" : "en-US";
const dateFmt = new Date(meta.date).toLocaleDateString(dateLocale, { day: "numeric", month: "long", year: "numeric" });
const durationStr = (() => {
const h = Math.floor(trajectory.durationHours);
const m = Math.round((trajectory.durationHours - h) * 60);
return `${h} h ${String(m).padStart(2, "0")}`;
})();
const xrayValTxt = window.fmtNumber(chestXrays, chestXrays < 1 ? 2 : 1) + " " + (chestXrays >= 2 ? t("cmp_xray_v_many") : t("cmp_xray_v_one"));
const groundValTxt = groundDays >= 1
? window.fmtNumber(groundDays, groundDays < 2 ? 1 : 0) + " " + (groundDays >= 2 ? t("ground_unit_d_many") : t("ground_unit_d_one"))
: window.fmtNumber(groundHours, 0) + " " + t("ground_unit_h");
return (
{fmtDose}
µSv
{t("res_ci")} · {fmtLow} – {fmtHigh} µSv
{t(riskKey)}
{t("cmp_xray_k")}
{xrayValTxt}
{t("cmp_annual_k")}
{window.fmtNumber(annualPct, annualPct < 1 ? 2 : 1)} %
{t("cmp_ground_k")}
{groundValTxt}
{t("meta_route")}
{meta.fromAp.city} ({meta.fromAp.iata}) → {meta.toAp.city} ({meta.toAp.iata})
{t("meta_date")}
{dateFmt}
{t("meta_duration")}
{durationStr}
{t("meta_distance")}
{window.fmtNumber(trajectory.distanceKm, 0)} km
{t("meta_alt")}
FL{Math.round(meta.cruiseFt / 100)}
{meta.kpUsed !== undefined && (
Kp ({meta.dataSource})
{typeof meta.kpUsed === "number" ? meta.kpUsed.toFixed(1) : meta.kpUsed}
)}
);
}
ReactDOM.createRoot(document.getElementById("root")).render();