// game-parts.jsx — shared UI bits for the prediction game.
// ── PM2.5 colour scale (Colombia / OMS-ish ranges, μg/m³) ─────────
function pmColor(v){
if(v <= 12) return '#00E676'; // Buena
if(v <= 25) return '#FFEB3B'; // Aceptable
if(v <= 37) return '#FFB300'; // Dañina grupos sensibles
if(v <= 55) return '#FF8A3D'; // Dañina
if(v <= 150) return '#E8735A'; // Muy dañina
return '#B0306E'; // Peligrosa
}
function pmLabel(v){
if(v <= 12) return 'Buena';
if(v <= 25) return 'Aceptable';
if(v <= 37) return 'Dañina para sensibles';
if(v <= 55) return 'Dañina';
if(v <= 150) return 'Muy dañina';
return 'Peligrosa';
}
function pmEmoji(v){
if(v <= 12) return '😊';
if(v <= 25) return '🙂';
if(v <= 37) return '😐';
if(v <= 55) return '😷';
if(v <= 150) return '🤢';
return '☠️';
}
// ── Top chrome ────────────────────────────────────────────────────
function TopBar({step, total, onRestart, onRetryLevel, label}){
return (
{label || 'Juego · ¿Puedes predecir el aire?'}
{Array.from({length:total}).map((_,i)=>(
))}
{String(step+1).padStart(2,'0')}
/
{String(total).padStart(2,'0')}
{onRetryLevel && (
)}
{onRestart && (
)}
);
}
function BottomBar(){
return (
siata.gov.co · Valle de Aburrá · 2025
Pronóstico operacional · 1 corrida/día
);
}
// ── Big primary button ────────────────────────────────────────────
function PrimaryBtn({children, onClick, disabled, variant}){
const v = variant || 'cyan';
const styles = {
cyan: {bg:'linear-gradient(135deg, #0DCCC0 0%, #00897B 100%)', sh:'rgba(13,204,192,0.4)'},
green: {bg:'linear-gradient(135deg, #84BD45 0%, #5e8a2e 100%)', sh:'rgba(132,189,69,0.4)'},
ghost: {bg:'transparent', sh:'transparent'},
}[v];
return (
);
}
// ── PM2.5 colour scale legend ────────────────────────────────────
function PMScale({value, showValue=true}){
const stops = [
{v:0, c:'#00E676', l:'Buena'},
{v:12, c:'#FFEB3B', l:'Aceptable'},
{v:25, c:'#FFB300', l:'Sensibles'},
{v:37, c:'#FF8A3D', l:'Dañina'},
{v:55, c:'#E8735A', l:'Muy dañina'},
];
const max = 80;
const pct = Math.min(100, (value/max)*100);
return (
{stops.map((s,i)=>(
{s.l}
{s.v}{i===stops.length-1?'+':''} μg
))}
);
}
// ── Card shell ───────────────────────────────────────────────────
function Card({children, style, accent}){
return (
{accent && (
)}
{children}
);
}
// ── Kicker + Title ───────────────────────────────────────────────
function ScreenHead({kicker, title, sub}){
return (
{kicker}
{title}
{sub && (
{sub}
)}
);
}
// ── Animated number ──────────────────────────────────────────────
function AnimNum({value, decimals=0, duration=600}){
const [v, setV] = React.useState(value);
const prevRef = React.useRef(value);
React.useEffect(()=>{
const from = prevRef.current;
const to = value;
const t0 = performance.now();
let raf;
const tick = (t)=>{
const k = Math.min(1, (t-t0)/duration);
const eased = 1 - Math.pow(1-k, 3);
setV(from + (to-from)*eased);
if(k<1) raf = requestAnimationFrame(tick);
else prevRef.current = to;
};
raf = requestAnimationFrame(tick);
return ()=> cancelAnimationFrame(raf);
},[value]);
return {v.toFixed(decimals)};
}
Object.assign(window, {
pmColor, pmLabel, pmEmoji,
TopBar, BottomBar, PrimaryBtn, PMScale, Card, ScreenHead, AnimNum,
});