// aero-home.jsx — Landing page function PageHome({ lang, setLang }) { const t = window.WX.T[lang]; const [data, setData] = useState(window.WX.DATA); const [locationName, setLocationName] = useState("Santa Maria, RS"); const [coords, setCoords] = useState({ lat: -29.69, lon: -53.84, alt: 77 }); useEffect(() => { let alive = true; const fetchLocationAndWeather = async () => { try { if (localStorage.getItem('aerograma_consent') !== 'granted') { window.WX.fetchWeather(-29.69, -53.84, 7).then(j => alive && setData(j)); return; } navigator.geolocation.getCurrentPosition(async (pos) => { const { latitude: lat, longitude: lon, altitude: alt } = pos.coords; const roundedLat = Math.round(lat * 100) / 100; const roundedLon = Math.round(lon * 100) / 100; if (alive) setCoords({ lat: roundedLat, lon: roundedLon, alt: alt ? Math.round(alt) : 0 }); try { const res = await fetch(`https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=${lang}`); const geo = await res.json(); if (alive && geo.city) { setLocationName(`${geo.city}, ${geo.principalSubdivision || geo.countryCode}`); } else if (alive) { setLocationName(lang === "pt" ? "Sua Localização" : "Your Location"); } } catch (e) { if (alive) setLocationName(lang === "pt" ? "Sua Localização" : "Your Location"); } const wData = await window.WX.fetchWeather(lat, lon, 7); if (alive) setData(wData); }, () => { // Fallback if denied window.WX.fetchWeather(-29.69, -53.84, 7).then(j => alive && setData(j)); }, { timeout: 10000 }); } catch (e) { window.WX.fetchWeather(-29.69, -53.84, 7).then(j => alive && setData(j)); } }; fetchLocationAndWeather(); const onConsent = () => fetchLocationAndWeather(); window.addEventListener('aerograma_consent_granted', onConsent); return () => { alive = false; window.removeEventListener('aerograma_consent_granted', onConsent); }; }, [lang]); const [inmetAlerts, setInmetAlerts] = useState(null); // null = loading const [alertFilter, setAlertFilter] = useState('Todos'); // 'Todos', 'Regional', 'Nacional', 'Internacional' const cur = data.current_complete || window.WX.DATA.current_complete; const today = (data.forecast && data.forecast[0]) ? data.forecast[0] : window.WX.DATA.forecast[0]; const hour = window.WX.hourOf(cur.time); const palette = window.WX.palette(cur.weathercode, hour); useEffect(() => { let alive = true; window.WX.fetchInmetAlerts().then(res => { if (alive) { // scope vem real do INMET via api.aerograma.pro/v1/alerts setInmetAlerts(res.alerts || []); } }); return () => { alive = false; }; }, []); const updatedTime = new Date().toLocaleTimeString(lang === "pt" ? "pt-BR" : "en-US", { hour: '2-digit', minute: '2-digit' }); // Map INMET level to HomeAlert level prop function inmetLevel(al) { const lev = (al.level || '').toLowerCase(); if (lev === 'extreme') return 'warning'; if (lev === 'warning') return 'warning'; if (lev === 'watch') return 'watch'; return 'advisory'; } // Format expiry from ISO or "DD/MM/YYYY HH:mm" strings function fmtExpiry(str) { if (!str) return ''; const m = str.match(/(\d{2})[\/\-](\d{2})[\/\-](\d{4})\s+(\d{2}:\d{2})/); if (m) return `${m[2]}/${m[1]} ${m[4]}`; const m2 = str.match(/(\d{4})-(\d{2})-(\d{2})T(\d{2}:\d{2})/); if (m2) return `${m2[3]}/${m2[2]} ${m2[4]}`; return str.slice(0, 16); } const filteredAlerts = React.useMemo(() => { if (!inmetAlerts) return null; if (alertFilter === 'Todos') return inmetAlerts; return inmetAlerts.filter(al => (al.scope || 'Regional') === alertFilter); }, [inmetAlerts, alertFilter]); return (
{/* ─── HERO ──────────────────────────────────────────────────── */} {/* ─── ACTIVE ALERTS BANNER ─────────────────────────────────── */} {lang === "pt" ? "Alertas ativos" : "Active alerts"} {inmetAlerts === null ? ( /* Loading skeleton */
{[0, 1].map(i => (
))}
) : inmetAlerts.length > 0 ? ( <>
{['Todos', 'Regional', 'Nacional', 'Internacional'].map(f => ( ))}
{filteredAlerts.length > 0 ? (
{filteredAlerts.slice(0, 6).map((al, idx) => ( ))}
) : (
{lang === "pt" ? `Nenhum alerta ${alertFilter.toLowerCase()} em vigor.` : `No ${alertFilter.toLowerCase()} alerts in effect.`}
)} ) : (
{lang === "pt" ? "Nenhum alerta meteorológico em vigor · fonte: INMET" : "No weather alerts in effect · source: INMET"}
)} {/* ─── FEATURED CITIES ──────────────────────────────────────── */} {lang === "pt" ? "Observações em destaque" : "Featured observations"} {/* ─── MINI RADAR ───────────────────────────────────────────── */} {lang === "pt" ? "Radar composto · Brasil" : "Composite radar · Brazil"} {/* ─── EDITORIAL / ANALYSIS ─────────────────────────────────── */} {lang === "pt" ? "Boletim do dia" : "Today's bulletin"}
{/* ─── STATS STRIP ──────────────────────────────────────────── */}
); } // ───────────────────────────────────────────────────────────────────── // HERO — full-bleed atmospheric backdrop, gigantic editorial type // ───────────────────────────────────────────────────────────────────── function HeroBlock({ palette, cur, data, lang, t, locationName = "Santa Maria, RS", coords = { lat: -29.69, lon: -53.84, alt: 77 } }) { const today = data.forecast[0]; return (
{/* paper wash so the cream chrome bleeds in */}
{lang === "pt" ? "Em destaque agora" : "Featured now"} · {locationName} {cur.time.slice(11)} {data.timezone_abbr || Intl.DateTimeFormat().resolvedOptions().timeZone} · {Math.abs(coords.lat)}°{coords.lat >= 0 ? 'N' : 'S'}, {Math.abs(coords.lon)}°{coords.lon >= 0 ? 'E' : 'W'} · {coords.alt}m

{(() => { const code = cur.weathercode; const desc = cur.weathercode_desc[lang]; if (lang === "pt") { if (code <= 3) return `${desc} e tempo estável no Centro do Estado.`; if (code >= 45 && code <= 48) return `Baixa visibilidade com ${desc.toLowerCase()} na região.`; if (code >= 51 && code <= 67) return `${desc} contínua cobre o Centro do Estado.`; if (code >= 71 && code <= 77) return `Condições de ${desc.toLowerCase()} na serra e planalto.`; if (code >= 80 && code <= 82) return `Instabilidade traz pancadas de chuva isoladas.`; if (code >= 95) return `Alerta de tempestade severa se aproximando.`; return `${desc} no momento.`; } else { if (code <= 3) return `${desc} and stable weather in the central region.`; if (code >= 45 && code <= 48) return `Low visibility with ${desc.toLowerCase()} in the area.`; if (code >= 51 && code <= 67) return `Continuous ${desc.toLowerCase()} covers the region.`; if (code >= 71 && code <= 77) return `${desc} conditions over the highlands.`; if (code >= 80 && code <= 82) return `Instability brings isolated showers.`; if (code >= 95) return `Severe thunderstorm warning in effect.`; return `${desc} at the moment.`; } })()}

{/* Live numbers row */}
{t.now_label} · {cur.weathercode_desc[lang]}
{Math.round(cur.temperature_2m)}°
{t.feels_like.toUpperCase()} {cur.apparent_temperature.toFixed(1)}° · {" "}{t.min.toUpperCase()} {Math.round(today.temperature_2m_min)}° / {" "}{t.max.toUpperCase()} {Math.round(today.temperature_2m_max)}°
{/* CTA row */}
); } function HeroStat({ label, value, unit, sub, trend, palette }) { return (
{label}
{value} {unit && {unit}}
{(sub || trend) &&
{sub || trend}
}
); } // ───────────────────────────────────────────────────────────────────── // ALERT CARDS (home) // ───────────────────────────────────────────────────────────────────── function HomeAlert({ level, lang, title, area, value, until, description, scope = "Regional" }) { const [isOpen, setIsOpen] = useState(false); const accent = level === "warning" ? AERO.danger : level === "watch" ? AERO.warm : AERO.accent; // advisory const levelLabel = level === "warning" ? (lang === "pt" ? "Alerta" : "Warning") : level === "watch" ? (lang === "pt" ? "Atenção" : "Watch") : (lang === "pt" ? "Aviso" : "Advisory"); const scopeColor = scope === 'Internacional' ? '#8e44ad' : scope === 'Nacional' ? '#2980b9' : '#27ae60'; return (
setIsOpen(!isOpen)} style={{ display: 'block', position: 'relative', padding: '22px 26px 22px 30px', background: '#fff', border: `0.5px solid ${AERO.hair}`, borderRadius: 4, textDecoration: 'none', transition: 'box-shadow 0.18s', cursor: 'pointer', }} onMouseEnter={e => e.currentTarget.style.boxShadow = `0 4px 20px rgba(0,0,0,0.07)`} onMouseLeave={e => e.currentTarget.style.boxShadow = 'none'} > {/* colour bar */}
{/* left column */}
{levelLabel}
{scope}
{title}
{area && (
{area}
)} {description && isOpen && (
{description}
)}
{/* right column */}
{value && (
{value}
)} {until && (
{lang === "pt" ? "expira" : "expires"}
{until}
)}
); } // ───────────────────────────────────────────────────────────────────── // CITY GRID // ───────────────────────────────────────────────────────────────────── // CITY_DEFS — metadados estáticos; dados meteorológicos são buscados live. const CITY_DEFS = [ { id: "smr", name: "Santa Maria", region: "RS", lat: -29.69, lon: -53.84 }, { id: "poa", name: "Porto Alegre", region: "RS", lat: -30.03, lon: -51.23 }, { id: "sao", name: "São Paulo", region: "SP", lat: -23.55, lon: -46.63 }, { id: "rio", name: "Rio de Janeiro", region: "RJ", lat: -22.91, lon: -43.17 }, { id: "bsb", name: "Brasília", region: "DF", lat: -15.78, lon: -47.93 }, { id: "rec", name: "Recife", region: "PE", lat: -8.05, lon: -34.88 }, { id: "man", name: "Manaus", region: "AM", lat: -3.10, lon: -60.02 }, { id: "for", name: "Fortaleza", region: "CE", lat: -3.72, lon: -38.54 }, { id: "lis", name: "Lisboa", region: "PT", lat: 38.72, lon: -9.14 }, { id: "lon", name: "London", region: "UK", lat: 51.51, lon: -0.13 }, { id: "nyc", name: "New York", region: "NY", lat: 40.71, lon: -74.01 }, { id: "tok", name: "Tokyo", region: "JP", lat: 35.68, lon: 139.69 }, ]; function CityGrid({ lang }) { // Estado inicial: esqueleto com dados null (mostra placeholder de carregamento). const [cities, setCities] = useState( CITY_DEFS.map(c => ({ ...c, temp: null, code: null, lo: null, hi: null, hour: null, wind: null })) ); useEffect(() => { // 1. Tenta via api.aerograma.pro/v1/cities (cache server-side, 1h) window.WX.fetchCities().then(serverCities => { if (serverCities && serverCities.length > 0) { setCities(serverCities.map(c => ({ ...c, // Garante compatibilidade de campos code: c.code ?? c.dcode ?? 3, hour: c.hour ?? 12, }))); return; } // 2. Fallback: batch direto const lats = CITY_DEFS.map(c => c.lat).join(","); const lons = CITY_DEFS.map(c => c.lon).join(","); const url = [ "https://api.open-meteo.com/v1/forecast", `?latitude=${lats}&longitude=${lons}`, "¤t=temperature_2m,apparent_temperature,weather_code,wind_speed_10m,wind_direction_10m,is_day", "&daily=temperature_2m_max,temperature_2m_min", "&wind_speed_unit=ms&timezone=auto&forecast_days=1&models=best_match", ].join(""); fetch(url) .then(r => r.ok ? r.json() : Promise.reject("HTTP " + r.status)) .then(results => { const arr = Array.isArray(results) ? results : [results]; setCities(CITY_DEFS.map((def, i) => { const d = arr[i]; if (!d) return { ...def, temp: null, code: null, lo: null, hi: null, hour: null, wind: null }; const cur = d.current || {}; const daily = d.daily || {}; let hour = new Date().getUTCHours(); if (d.utc_offset_seconds != null) { hour = Math.floor(((Date.now() / 1000 + d.utc_offset_seconds) % 86400) / 3600); } return { ...def, temp: cur.temperature_2m ?? null, feels: cur.apparent_temperature ?? null, code: cur.weather_code ?? null, lo: (daily.temperature_2m_min || [])[0] ?? null, hi: (daily.temperature_2m_max || [])[0] ?? null, hour, wind: cur.wind_speed_10m ?? null, is_day: cur.is_day ?? 1, }; })); }) .catch(() => { // Mantém esqueleto (já está como null, UI mostra "carregando…") }); }); }, []); return (
{cities.map((c) => )}
); } function CityCard({ city, lang }) { const loading = city.temp === null; const palette = window.WX.palette(city.code ?? 3, city.hour ?? 12); const desc = describeCode(city.code ?? 3, lang); return (
{city.region}
{city.name}
{loading ? "—" : Math.round(city.temp)} °
{loading ? (lang === "pt" ? "carregando…" : "loading…") : `${desc} · ${city.lo != null ? Math.round(city.lo) : "—"}° / ${city.hi != null ? Math.round(city.hi) : "—"}°` }
); } function describeCode(code, lang) { if (code === 0) return lang === "pt" ? "Limpo" : "Clear"; if (code <= 3) return lang === "pt" ? "Parc. nublado" : "Partly cloudy"; if (code >= 45 && code <= 48) return lang === "pt" ? "Neblina" : "Fog"; if (code >= 51 && code <= 57) return lang === "pt" ? "Garoa" : "Drizzle"; if (code >= 61 && code <= 67) return lang === "pt" ? "Chuva" : "Rain"; if (code >= 71 && code <= 77) return lang === "pt" ? "Neve" : "Snow"; if (code >= 80) return lang === "pt" ? "Pancadas" : "Showers"; if (code >= 95) return lang === "pt" ? "Tempestade" : "Thunderstorm"; return ""; } // ───────────────────────────────────────────────────────────────────── // MINI RADAR — wide map preview that links to the full radar page // ───────────────────────────────────────────────────────────────────── function MiniRadarBlock({ lang, palette }) { return (
{lang === "pt" ? "Eco ativo" : "Active echo"}
{lang === "pt" ? "Núcleo convectivo ao oeste do RS" : "Convective core W of RS"}
{lang === "pt" ? "Refletividade 48 dBZ · deslocando NE a 32 km/h" : "48 dBZ · moving NE at 32 km/h"}
{lang === "pt" ? "Abrir mapa inteiro →" : "Open full map →"}
); } // Stylized Brazil outline + radar blobs — wide aspect. function BrazilMiniMap() { // Simplified outline path. Approximate, decorative only. return ( {/* lat/lon grid */} {[60, 120, 180, 240, 300].map((y, i) => ( ))} {[150, 300, 450, 600, 750, 900, 1050].map((x, i) => ( ))} {/* stylized Brazil-ish landmass path (decorative) */} {/* state divisions (decorative scribble) */} {/* radar reflectivity blobs over RS area */} {/* secondary cells elsewhere */} {/* city markers */} {[ [510, 270, "Santa Maria"], [560, 290, "Porto Alegre"], [800, 240, "São Paulo"], [840, 220, "Rio de Janeiro"], [720, 140, "Brasília"], [600, 80, "Manaus"], [880, 90, "Recife"], ].map(([x, y, n], i) => ( {n} ))} {/* reflectivity legend overlay */} REFLECTIVITY (dBZ) {["#3a6f4b", "#5a9c5e", "#c5a23a", "#c97c3a", "#a8463a", "#7a2a6a"].map((c, i) => ( ))} {[5, 20, 30, 40, 50, 60].map((v, i) => ( {v} ))} ); } // ───────────────────────────────────────────────────────────────────── // EDITORIAL CARDS // ───────────────────────────────────────────────────────────────────── function FeatureCard({ lang, kicker, title, excerpt, time, author, lead }) { return (
{kicker} · {time}

{title}

{excerpt}

{author &&
{author}
}
); } // ───────────────────────────────────────────────────────────────────── // BIG STATS // ───────────────────────────────────────────────────────────────────── function BigStat({ value, unit }) { return (
{value}
{unit}
); } Object.assign(window, { PageHome });