Adds legacy meteo modal

This commit is contained in:
litoral05
2026-05-29 17:08:16 +01:00
parent 3905e2adfe
commit 540e4ed560
10 changed files with 648 additions and 85 deletions
+371 -13
View File
@@ -4,13 +4,18 @@ import {
Cloud,
CloudRain,
Droplets,
Gauge,
Compass,
Eye,
Sun,
Zap,
Thermometer,
Wind,
X,
type LucideIcon,
} from "lucide-react";
import weatherBoardBackground from "../../../assets/meteo-pane.png";
import sunnyBackground from "../../../assets/weather/Sunny.png";
import partlyCloudyBackground from "../../../assets/weather/Partly Cloudy.png";
import cloudyBackground from "../../../assets/weather/Cloudy.png";
@@ -38,7 +43,8 @@ import WorkspaceChart, {
} from "../../../components/charts/WorkspaceChart";
import type { HistorianPoint } from "../components/MeteoHistoryModal";
import { useAccumulatedHistory } from "../hooks/useAccumulatedHistory";
import type { AccumulatedBucket } from "../hooks/useAccumulatedHistory";
type MeteoPageProps = {
theme: "dark" | "light";
};
@@ -75,16 +81,33 @@ type HistoryKey = (typeof HISTORY_KEYS)[keyof typeof HISTORY_KEYS];
export function MeteoPage({ theme }: MeteoPageProps) {
const { sensors } = useMeteoModuleStream();
const weatherForecast = useWeatherForecast();
const [weatherBoardOpen, setWeatherBoardOpen] = useState(false);
const selected = selectMeteoSensors(sensors);
const temperature = selected.temperature;
const humidity = selected.humidity;
const windDirection = selected.windDirection;
const windSpeed = selected.windSpeed;
const co2 = selected.co2;
const radiation = selected.radiation;
const forecast = weatherForecast.forecast;
const { buckets: rainBuckets } = useAccumulatedHistory(selected.rain ?? null, "7d");
const { buckets: radiationBuckets } = useAccumulatedHistory(radiation ?? null, "7d");
const rainTodayBucket = todayAccumulatedBucket(rainBuckets);
const radiationTodayBucket = todayAccumulatedBucket(radiationBuckets);
const accumulatedRainToday = formatAccumulatedValue(
rainTodayBucket?.total ?? null,
rainTodayBucket?.unit ?? "mm",
);
const accumulatedRadiationToday = formatAccumulatedValue(
radiationTodayBucket?.total ?? null,
radiationTodayBucket?.unit,
);
const { pointsByKey, loading: historyLoading } = useMeteoMultiHistory(
[
temperature ?? null,
@@ -118,6 +141,7 @@ export function MeteoPage({ theme }: MeteoPageProps) {
const currentWindSpeed = numericSensorValue(windSpeed);
const currentRadiation = numericSensorValue(radiation);
const currentWindDirection = numericSensorValue(windDirection);
const currentCo2 = numericSensorValue(co2)
const currentDewPoint = calculateDewPoint(
currentTemperature,
currentHumidity,
@@ -185,6 +209,7 @@ export function MeteoPage({ theme }: MeteoPageProps) {
selectedDayIndex={selectedForecastDayIndex}
loading={weatherForecast.loading}
error={weatherForecast.error}
onOpenWeatherBoard={() => setWeatherBoardOpen(true)}
/>
<ForecastPanel
@@ -204,10 +229,10 @@ export function MeteoPage({ theme }: MeteoPageProps) {
windSpeed={currentWindSpeed}
dewPoint={currentDewPoint}
radiation={currentRadiation}
temperatureUnit={temperature?.unit ?? "°C"}
humidityUnit={humidity?.unit ?? "%"}
windUnit={windSpeed?.unit ?? "km/h"}
radiationUnit={radiation?.unit ?? "W/m²"}
temperatureUnit={displayUnit(temperature?.unit ?? "°C")}
humidityUnit={displayUnit(humidity?.unit ?? "%")}
windUnit={displayUnit(windSpeed?.unit ?? "km/h")}
radiationUnit={displayUnit(radiation?.unit ?? "W/m²")}
/>
<WindDirectionPanel
@@ -226,6 +251,310 @@ export function MeteoPage({ theme }: MeteoPageProps) {
hours={HISTORY_HOURS}
/>
</div>
{weatherBoardOpen && (
<WeatherBoardModal
theme={theme}
temperature={currentTemperature}
humidity={currentHumidity}
windSpeed={currentWindSpeed}
co2={currentCo2}
windDirection={currentWindDirection}
radiation={currentRadiation}
dewPoint={currentDewPoint}
accumulatedRain={accumulatedRainToday}
accumulatedRadiation={accumulatedRadiationToday}
onClose={() => setWeatherBoardOpen(false)}
/>
)}
</div>
);
}
function WeatherBoardModal({
temperature,
humidity,
windSpeed,
windDirection,
co2,
radiation,
dewPoint,
accumulatedRain, // ← add
accumulatedRadiation, // ← add
onClose,
}: {
theme: "dark" | "light";
temperature: number | null;
humidity: number | null;
windSpeed: number | null;
co2: number | null;
windDirection: number | null;
radiation: number | null;
dewPoint: number | null;
accumulatedRain: { value: string; unit: string };
accumulatedRadiation: { value: string; unit: string };
onClose: () => void;
}) {
const angle = windDirection ?? 0;
const cardinal = directionName(windDirection);
return (
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 p-6 backdrop-blur-sm">
<section className="relative aspect-[1449/1085] w-[min(96vw,calc(94vh*1449/1085))] overflow-hidden rounded-[5px] border border-sky-400/20 bg-[#020817] shadow-[0_0_80px_rgba(14,165,233,0.18)]">
<img
src={weatherBoardBackground}
alt=""
className="absolute inset-0 h-full w-full"
/>
<button
type="button"
onClick={onClose}
className="absolute right-[2%] top-[2%] z-30 inline-flex h-10 w-10 items-center justify-center rounded-[5px] border border-white/10 bg-black/30 text-slate-200 transition hover:bg-white/[0.12] hover:text-white"
>
<X className="h-5 w-5" />
</button>
<BoardText className="left-[1.6%] top-[1.2%]" value={new Date().toLocaleDateString("pt-PT")} />
<BoardText className="right-[13%] top-[1.2%]" value={new Date().toLocaleTimeString("pt-PT")} />
<BoardMetric
className="left-[10.5%] top-[13%]"
icon={Droplets}
label="Humidade"
value={formatNumber(humidity, 0)}
unit="%"
color="blue"
/>
<BoardMetric
className="left-[10.5%] top-[28%]"
icon={Gauge}
label="DPV"
value={formatNumber(dewPoint, 1)}
unit="mbar"
color="blue"
/>
<BoardMetric
className="left-[10.5%] top-[43%]"
icon={Activity}
label="Humidade absoluta"
value="13,5"
unit="g/m³"
color="blue"
/>
<BoardMetric
className="left-[10.5%] top-[66%]"
icon={CloudRain}
label="Precipitação hoje"
value={accumulatedRain.value}
unit={accumulatedRain.unit || "mm"}
color="orange"
/>
<BoardMetric
className="left-[10.5%] top-[79%]"
icon={CloudRain}
label="Precipitação instant."
value="0.0"
unit="mm/min"
color="orange"
/>
<BoardMetric
className="left-1/2 top-[4%] w-[240px] -translate-x-1/2 text-center"
icon={Thermometer}
label="Temperatura"
value={formatNumber(temperature, 1)}
unit="°C"
color="yellow"
/>
<BoardMetric
className="left-[74%] top-[13%]"
icon={Wind}
label="Velocidade vento"
value={formatNumber(windSpeed, 0)}
unit="km/h"
color="cyan"
/>
<BoardMetric
className="left-[74%] top-[43%]"
icon={Compass}
label="Direção vento"
value={cardinal}
unit=""
color="white"
/>
<BoardMetric
className="left-[74%] top-[61%]"
icon={Sun}
label="Radiação"
value={formatNumber(radiation, 0)}
unit="W/m²"
color="yellow"
/>
<BoardMetric
className="left-[74%] top-[79%]"
icon={Zap}
label="Acumulada hoje"
value={accumulatedRadiation.value}
unit={accumulatedRadiation.unit}
color="yellow"
/>
<BoardMetric
className="left-1/2 bottom-[12%] w-[220px] -translate-x-1/2 text-center"
icon={Activity}
label="CO₂"
value={formatNumber(co2, 0)}
unit="ppm"
color="muted"
/>
<div
className="absolute z-20 h-[15%] w-[15%] -translate-x-1/2 -translate-y-1/2"
style={{
left: "50.3%",
top: "53.2%",
}}
>
<svg
className="absolute inset-0 h-full w-full overflow-visible"
viewBox="0 0 100 100"
>
<g
style={{
transformOrigin: "50px 50px",
transform: `rotate(${angle}deg)`,
}}
>
{/* Needle pointing up (north = 0°) */}
<polygon
points="50,2 44,50 50,56 56,50"
fill="white"
filter="drop-shadow(0 0 8px rgba(255,255,255,0.85))"
/>
{/* Tail */}
<polygon
points="50,98 44,50 50,56 56,50"
fill="rgba(255, 255, 255, 0)"
/>
</g>
</svg>
{/* Center dot */}
<div className="absolute left-1/2 top-1/2 z-[12] h-7 w-7 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white shadow-[0_0_18px_rgba(255,255,255,0.95)]" />
{/* Degree label */}
<div className="absolute left-1/2 top-[6%] z-20 -translate-x-1/2 rounded bg-black/85 px-1.5 py-0.5 text-[10px] font-bold leading-none text-white shadow-[0_0_10px_rgba(0,0,0,0.55)]">
{Math.round(windDirection ?? 0)}°
</div>
</div>
</section >
</div >
);
}
function todayAccumulatedBucket(buckets: AccumulatedBucket[]) {
const now = new Date();
const todayLabel = `${String(now.getDate()).padStart(2, "0")}/${String(
now.getMonth() + 1,
).padStart(2, "0")}`;
return buckets.find((bucket) => bucket.label === todayLabel) ?? null;
}
function formatAccumulatedValue(value: number | null, unit?: string) {
if (value === null) return { value: "--", unit: "" };
if (unit === "Wh/m²") {
return {
value: (value / 1000).toFixed(2),
unit: "kWh/m²",
};
}
return {
value: value.toFixed(1),
unit: unit ?? "",
};
}
function BoardText({
className,
value,
}: {
className: string;
value: string;
}) {
return (
<div className={`absolute z-20 text-[1.55vw] font-medium text-white ${className}`}>
{value}
</div>
);
}
function BoardMetric({
className,
icon: Icon,
label,
value,
unit,
color,
}: {
className: string;
icon: LucideIcon;
label: string;
value: string;
unit: string;
color: "blue" | "cyan" | "yellow" | "orange" | "white" | "muted";
}) {
const valueColor =
color === "blue"
? "text-blue-400"
: color === "cyan"
? "text-cyan-300"
: color === "yellow"
? "text-yellow-300"
: color === "orange"
? "text-orange-500"
: color === "muted"
? "text-slate-300"
: "text-white";
return (
<div className={`absolute z-20 w-[220px] ${className}`}>
<div
className={[
"flex items-center gap-2 text-white",
className.includes("text-center") ? "justify-center" : "",
].join(" ")}
>
<Icon className="h-[clamp(17px,1.45vw,24px)] w-[clamp(17px,1.45vw,24px)] opacity-90" />
<p className="text-[clamp(12px,0.95vw,15px)] font-black">
{label}
</p>
</div>
<p
className={[
"mt-3 text-[clamp(22px,1.85vw,30px)] font-black leading-none",
valueColor,
className.includes("text-center") ? "text-center" : "",
].join(" ")}
>
{value}
{unit && (
<span className="ml-1 text-[clamp(10px,0.78vw,13px)] font-bold">
({unit})
</span>
)}
</p>
</div>
);
}
@@ -237,6 +566,7 @@ function WeatherHeroPanel({
selectedDayIndex,
loading,
error,
onOpenWeatherBoard,
}: {
theme: "dark" | "light";
forecast: WeatherForecastResponse | null;
@@ -244,6 +574,7 @@ function WeatherHeroPanel({
selectedDayIndex: number;
loading: boolean;
error: string | null;
onOpenWeatherBoard: () => void;
}) {
const isDark = theme === "dark";
const day = selectedDay ?? forecast?.daily?.[0];
@@ -298,10 +629,11 @@ function WeatherHeroPanel({
<button
type="button"
onClick={onOpenWeatherBoard}
className="inline-flex items-center gap-2 rounded-[5px] border border-sky-400/25 bg-sky-400/10 px-3 py-2 text-sm font-black text-sky-200 transition hover:bg-sky-400/15"
>
<Activity className="h-4 w-4" />
Ver quadro legado
Abrir quadro meteorológico
</button>
</div>
@@ -733,18 +1065,34 @@ function WindDirectionPanel({
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative h-24 w-24">
<div
className="absolute left-1/2 top-1/2 h-[112px] w-[46px] origin-bottom rounded-t-full bg-gradient-to-b from-sky-400 to-blue-700/20"
className="absolute left-1/2 top-1/2 h-[105px] w-[34px] origin-bottom bg-gradient-to-b from-sky-400 to-blue-700/20"
style={{
clipPath: "polygon(50% 0%, 100% 100%, 50% 82%, 0% 100%)",
clipPath:
"polygon(50% 0%, 85% 70%, 60% 70%, 60% 100%, 40% 100%, 40% 70%, 15% 70%)",
transform: `translate(-50%, -100%) rotate(${angle}deg)`,
}}
/>
<div className={isDark ? "absolute left-1/2 top-1/2 h-24 w-24 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/10 bg-[#0b1220]" : "absolute left-1/2 top-1/2 h-24 w-24 -translate-x-1/2 -translate-y-1/2 rounded-full border border-slate-200 bg-white"} />
<div
className={
isDark
? "absolute left-1/2 top-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/10 bg-[#0b1220]"
: "absolute left-1/2 top-1/2 h-20 w-20 -translate-x-1/2 -translate-y-1/2 rounded-full border border-slate-200 bg-white"
}
/>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className={isDark ? "text-4xl font-black text-slate-100" : "text-4xl font-black text-slate-950"}>
<span
className={
isDark
? "text-3xl font-black text-slate-100"
: "text-3xl font-black text-slate-950"
}
>
{cardinal}
</span>
<span className="text-xs font-black text-slate-500">
<span className="text-[11px] font-black text-slate-500">
{degrees ?? "--"}°
</span>
</div>
@@ -1031,7 +1379,6 @@ function SmallStat({
);
}
function CompassLabel({
label,
className,
@@ -1291,4 +1638,15 @@ function accentColors(accent: Accent) {
bg: "bg-violet-400",
};
}
}
function displayUnit(unit: string | null | undefined) {
if (!unit) return "";
const normalized = unit.trim();
if (normalized === "C" || normalized === "ºC") return "°C";
if (normalized === "w/m2" || normalized === "W/m2") return "W/m²";
return normalized;
}