// aero-city.jsx — City forecast detail (editorial article-like) function PageCity({ lang, cityId = "smr", setLang }) { const t = window.WX.T[lang]; const city = window.WX.CITIES.find(c => c.id === cityId) || window.WX.CITIES[0]; const [data, setData] = useState(window.WX.DATA); useEffect(() => { let alive = true; window.WX.fetchWeather(city.lat, city.lon, 7).then(j => { if (alive) setData({ ...j, _city: city }); }); return () => { alive = false; }; }, [cityId]); const cur = data.current_complete || window.WX.DATA.current_complete; const hour = window.WX.hourOf(cur.time); const palette = window.WX.palette(cur.weathercode, hour); const today = data.forecast[0]; return (
{/* ───── EDITORIAL HEADER ─────────────────────────────────── */}
{/* breadcrumb */}
{cur.time.slice(0, 10)} · {cur.time.slice(11)} {data.timezone_abbr || "GMT-3"} {data._fallback && ` · ${lang === "pt" ? "amostra local" : "local sample"}`}

{city.name}

{cur.weathercode_desc[lang]}, {Math.round(cur.temperature_2m)}°C.
{Math.round(cur.temperature_2m)}°
{t.feels_like.toUpperCase()} {cur.apparent_temperature.toFixed(1)}° · {" "}{Math.round(today.temperature_2m_min)}° / {Math.round(today.temperature_2m_max)}°
{/* ───── KEY METRIC TABLE ─────────────────────────────────── */} {lang === "pt" ? "Leituras de superfície" : "Surface readings"}
{[ [t.humidity, cur.relative_humidity_2m + "%", null], [t.dewpoint, cur.dew_point_2m.toFixed(1) + "°", null], [t.pressure, Math.round(cur.surface_pressure) + " hPa", lang === "pt" ? "Caindo" : "Falling"], [t.wind, cur.wind_speed_10m.toFixed(1) + " m/s", `${window.WX.compassLabel(cur.wind_direction_10m, lang)} · ${cur.wind_direction_10m}°`], [t.gusts, cur.wind_gusts_10m.toFixed(0) + " m/s", null], [t.cloud, cur.cloud_cover + "%", null], ].map(([label, value, sub], i) => (
{label}
{value}
{sub &&
{sub}
}
))}
{/* ───── EDITORIAL BLURB + SUN/MOON ───────────────────────── */}
{lang === "pt" ? "Análise do momento" : "Current analysis"}

{(() => { const code = cur.weathercode; const pt = lang === "pt"; if (code <= 3) return pt ? "A massa de ar seco domina a região, garantindo estabilidade e amplas aberturas de sol. A pressão atmosférica em superfície se mantém controlada, inibindo a formação de nebulosidade." : "Dry air mass dominates the region, ensuring stability and wide sunny spells. Surface atmospheric pressure remains controlled, inhibiting cloudiness."; if (code >= 45 && code <= 48) return pt ? "Condições de inversão térmica favorecem o acúmulo de umidade e a formação de nevoeiro restrito à camada limite, reduzindo a visibilidade." : "Thermal inversion conditions favor moisture accumulation and boundary-layer fog, reducing visibility."; if (code >= 51 && code <= 67) return pt ? "A camada de superfície permanece saturada devido à constante advecção de umidade. Precipitação estratiforme de intensidade leve a moderada domina as observações." : "The surface layer remains saturated due to constant moisture advection. Stratiform precipitation of light to moderate intensity dominates observations."; if (code >= 71 && code <= 77) return pt ? "Advecção de ar polar nos baixos e médios níveis sustenta condições termodinâmicas para precipitação invernal ao longo do planalto." : "Polar air advection in low and mid levels sustains thermodynamic conditions for winter precipitation along the plateau."; if (code >= 80 && code <= 82) return pt ? "O escoamento instável gera núcleos convectivos rasos, resultando em pancadas de chuva intermitentes e localizadas na região." : "Unstable flow generates shallow convective cores, resulting in intermittent and localized rain showers."; if (code >= 95) return pt ? "Forte divergência em altos níveis aliada ao intenso cisalhamento sustenta atividade convectiva profunda. Condição de alerta para tempo severo." : "Strong high-level divergence coupled with intense shear sustains deep convective activity. Severe weather alert condition."; return pt ? "As condições meteorológicas seguem a climatologia típica da estação, com variações normais de temperatura na escala diária." : "Meteorological conditions follow typical seasonal climatology, with normal temperature variations on the daily scale."; })()}

{(() => { const pt = lang === "pt"; const wind = pt ? `Ventos de superfície em torno de ${cur.wind_speed_10m.toFixed(1)} m/s` : `Surface winds around ${cur.wind_speed_10m.toFixed(1)} m/s`; const press = pt ? `pressão em ${Math.round(cur.surface_pressure)} hPa` : `pressure at ${Math.round(cur.surface_pressure)} hPa`; const temp = pt ? `amplitude de ${Math.round(today.temperature_2m_min)}°C a ${Math.round(today.temperature_2m_max)}°C` : `amplitude from ${Math.round(today.temperature_2m_min)}°C to ${Math.round(today.temperature_2m_max)}°C`; return pt ? `${wind} comandam a advecção atual, combinados com a ${press}. O perfil térmico do dia deve apresentar uma ${temp}.` : `${wind} drive the current advection, combined with a ${press}. The thermal profile of the day indicates an ${temp}.`; })()}

{lang === "pt" ? "Assinado" : "Signed"} · Sistema Agregador · {" "}{lang === "pt" ? `Boletim Automático GMT-3` : `Auto Bulletin GMT-3`}
{/* Sun/moon block */}
{t.daylight}
{(today.daylight_duration / 3600).toFixed(1)}h {lang === "pt" ? "de luz" : "of daylight"} {" · "}{(today.sunshine_duration / 3600).toFixed(1)}h {lang === "pt" ? "de sol" : "of sun"}
{t.moon}
{lang === "pt" ? "Crescente gibosa" : "Waxing gibbous"}
{(today.moon_phase * 100).toFixed(0)}% {lang === "pt" ? "iluminada" : "illuminated"}
{/* ───── HOURLY TIMELINE ──────────────────────────────────── */} {lang === "pt" ? "Evolução horária · 24h" : "Hourly evolution · 24h"} {/* ───── 7-DAY ROW ───────────────────────────────────────── */} {lang === "pt" ? "Próximos 7 dias" : "Next 7 days"} {/* ───── BOTTOM ATMOSPHERE GRID ──────────────────────────── */} {lang === "pt" ? "Aerograma · perfil termodinâmico" : "Aerogram · thermodynamic profile"}
); } // ───────────────────────────────────────────────────────────────────── function CityPickerInline({ lang, currentId }) { const opts = window.WX.CITIES.slice(0, 8); return (
{lang === "pt" ? "Ir para" : "Jump to"} {opts.map(c => ( {c.name} ))}
); } // ───────────────────────────────────────────────────────────────────── // Hourly chart — bigger version of the mobile screen one // ───────────────────────────────────────────────────────────────────── function CityHourlyChart({ data, palette, lang, t }) { const h = data.hourly; const [idx, setIdx] = useState(window.WX.hourOf(data.current_complete.time)); const W = 1200, H = 320; const cw = W - 80; function pickIdx(e) { const r = e.currentTarget.getBoundingClientRect(); const x = (e.touches ? e.touches[0].clientX : e.clientX) - r.left - 60; const i = Math.max(0, Math.min(23, Math.round((x / cw) * 23))); setIdx(i); } const tempMin = Math.min(...h.temperature_2m, ...h.dew_point_2m) - 1; const tempMax = Math.max(...h.temperature_2m) + 1; const tempY = (v) => H - 90 - ((v - tempMin) / (tempMax - tempMin)) * (H - 130); return (
{/* readout strip */}
{h.time[idx].slice(11)}
{Math.round(h.temperature_2m[idx])}°
{[ [t.dewpoint, h.dew_point_2m[idx].toFixed(1) + "°"], [t.humidity, h.relative_humidity_2m[idx] + "%"], [t.precip, h.precipitation[idx] + " mm"], [t.wind, h.wind_speed_10m[idx].toFixed(1) + " m/s"], [t.pressure, Math.round(h.surface_pressure[idx]) + " hPa"], [t.cloud, h.cloud_cover[idx] + "%"], ].map(([k, v], i) => (
{k}
{v}
))}
{/* chart */}
e.buttons === 1 && pickIdx(e)} onTouchStart={pickIdx} onTouchMove={pickIdx} style={{ userSelect: 'none', cursor: 'col-resize' }}> {/* y-axis temp grid */} {(() => { const tics = []; const step = (tempMax - tempMin) > 12 ? 5 : 2; for (let v = Math.ceil(tempMin / step) * step; v < tempMax; v += step) { const y = tempY(v); tics.push( {v}° ); } return tics; })()} {/* x ticks */} {[0, 3, 6, 9, 12, 15, 18, 21, 23].map(i => { const x = 60 + (i / 23) * cw; return {String(i).padStart(2, "0")}h ; })} {/* precip bars */} {h.precipitation.map((v, i) => { const x = (i / 23) * cw; const bw = cw / 24 * 0.7; const ph = Math.min(50, v * 12); return ; })} PRECIP (mm) {/* dewpoint */} {/* temp */} {/* cursor */}
{/* legend */}
{lang === "pt" ? "Temperatura" : "Temperature"} 2m {t.dewpoint} {t.precip} {lang === "pt" ? "Arraste para escolher hora" : "Drag to scrub"}
); } // ───────────────────────────────────────────────────────────────────── // 7-DAY STRIP // ───────────────────────────────────────────────────────────────────── function SevenDayStrip({ data, lang, t }) { const days = data.forecast.slice(0, 7); const allMin = Math.min(...days.map(d => d.temperature_2m_min)); const allMax = Math.max(...days.map(d => d.temperature_2m_max)); return (
{days.map((d, i) => { const lp = (d.temperature_2m_min - allMin) / (allMax - allMin); const rp = (d.temperature_2m_max - allMin) / (allMax - allMin); return (
{i === 0 ? t.today : window.WX.fmtDate(d.date, lang)} · {d.date.slice(8, 10)}
{Math.round(d.temperature_2m_max)}°
↓ {Math.round(d.temperature_2m_min)}°
{d.weathercode_desc[lang]}
☂ {d.precipitation_probability_max}% · {d.precipitation_sum} mm
); })}
); } // ───────────────────────────────────────────────────────────────────── // Stylized Skew-T diagram — pure decorative SVG. Hand-drawn meteorological // diagram with adiabats + temp/dewpoint traces. // ───────────────────────────────────────────────────────────────────── function SkewT({ palette }) { const W = 520, H = 420; const skewAngle = 30 * Math.PI / 180; const skew = Math.tan(skewAngle); // pressure levels (hPa) on y axis: 1000 bottom, 100 top const pLevels = [1000, 850, 700, 500, 300, 200, 100]; const yOfP = (p) => 40 + (1 - Math.log(p / 100) / Math.log(1000 / 100)) * (H - 80); // isotherms (skewed) every 10°C from -90 to +40 const isotherms = []; for (let T = -90; T <= 40; T += 10) { // x-axis 0..1 maps to -40..+40 at p=1000 const x0 = ((T + 40) / 80) * (W - 120) + 60; const x1 = x0 + (yOfP(100) - yOfP(1000)) * skew; isotherms.push( ); } // dry adiabats — decorative curves const dryAd = []; for (let i = 0; i < 8; i++) { const sx = 100 + i * 60; dryAd.push( ); } // Temperature trace (right curve) const tempPts = [ [1000, 14], [950, 13.2], [900, 11], [850, 8], [700, -2], [500, -16], [300, -42], [200, -54], [100, -68], ]; const dewPts = [ [1000, 14], [950, 12.5], [900, 9], [850, 4], [700, -8], [500, -28], [300, -56], [200, -68], [100, -78], ]; const toXY = ([p, T]) => { const x0 = ((T + 40) / 80) * (W - 120) + 60; const y = yOfP(p); return [x0 + (y - yOfP(1000)) * skew, y]; }; const tempPath = "M " + tempPts.map(p => toXY(p).join(",")).join(" L "); const dewPath = "M " + dewPts.map(p => toXY(p).join(",")).join(" L "); return (
Skew-T log-P
{isotherms} {dryAd} {/* pressure axis labels */} {pLevels.map(p => ( {p} ))} {/* temperature isotherms label */} +40°C -40°C {/* axis title */} PRESSURE (hPa) {/* Dewpoint then temp */} {/* wind barbs along right edge */} {[1000, 850, 700, 500, 300].map((p, i) => { const y = yOfP(p); return ( {i > 1 && } ); })}
T (env) Td 06z init · ECMWF
); } function DiagBigCard({ label, value, unit, status }) { const sCol = status === "LOW" || status === "STABLE" || status === "WEAK" ? AERO.accent : status === "CAP" || status === "HIGH" ? AERO.warm : AERO.danger; return (
{label}
{value} {unit}
{status}
); } Object.assign(window, { PageCity });