feat(charts): add persistent workspace chart management

This commit is contained in:
litoral05
2026-05-27 14:38:25 +01:00
parent d7ef36fc53
commit ffe3c64cfa
23 changed files with 4407 additions and 202 deletions
+47 -57
View File
@@ -22,6 +22,10 @@ import {
} from "../hooks/useAccumulatedHistory";
import { useWeatherForecast } from "../hooks/useWeatherForecast";
import { WeatherForecastCard } from "../components/WeatherForecastCard";
import {
numericSensorValue,
selectMeteoSensors,
} from "../domain/meteoSensorSelectors";
type MeteoPageProps = {
theme: "dark" | "light";
@@ -33,9 +37,26 @@ type Accent = "amber" | "blue" | "cyan" | "emerald";
const MAX_HISTORY_POINTS = 34;
const RADIUS = "rounded-[5px]";
const HISTORY_KEYS = {
temperature: "temperature",
humidity: "humidity",
windSpeed: "windSpeed",
radiation: "radiation",
rain: "rain",
} as const;
export function MeteoPage({ theme }: MeteoPageProps) {
const { sensors } = useMeteoModuleStream();
const selected = selectMeteoSensors(sensors);
const temperature = selected.temperature;
const humidity = selected.humidity;
const windDirection = selected.windDirection;
const windSpeed = selected.windSpeed;
const radiation = selected.radiation;
const rainSensor = selected.rain;
const [selectedSensor, setSelectedSensor] =
useState<ModuleSensorResponse | null>(null);
@@ -50,27 +71,7 @@ export function MeteoPage({ theme }: MeteoPageProps) {
const [accumulatedRange, setAccumulatedRange] =
useState<AccumulatedRange>("7d");
const temperature = findSensor(sensors, "temperatura.exterior");
const humidity = findSensor(sensors, "humidade.exterior");
const windDirection = findSensor(sensors, "direcao.vento");
const windSpeed = maxSensor(
sensors.filter((sensor) => sensor.key.startsWith("velocidade.vento.")),
);
const radiation = maxSensor(
sensors.filter((sensor) => sensor.key.startsWith("radiacao.")),
);
const rainSensors = sensors.filter((sensor) => sensor.key.startsWith("chuva."));
const rainSensor =
findSensor(sensors, "chuva.atual") ??
findSensor(sensors, "chuva.instantanea") ??
findSensor(sensors, "chuva.intensidade") ??
maxSensor(rainSensors);
const rainValue = numericValue(rainSensor);
const rainValue = numericSensorValue(rainSensor);
const isRaining = rainValue !== null && rainValue > 0;
const meteoHistory = useMeteoHistory(selectedSensor);
@@ -84,11 +85,11 @@ export function MeteoPage({ theme }: MeteoPageProps) {
useEffect(() => {
const samples: Array<[string, number | null]> = [
["temperatura.exterior", numericValue(temperature)],
["humidade.exterior", numericValue(humidity)],
["vento.velocidade", numericValue(windSpeed)],
["radiacao.solar", numericValue(radiation)],
["chuva.total", numericValue(rainSensor)],
[HISTORY_KEYS.temperature, numericSensorValue(temperature)],
[HISTORY_KEYS.humidity, numericSensorValue(humidity)],
[HISTORY_KEYS.windSpeed, numericSensorValue(windSpeed)],
[HISTORY_KEYS.radiation, numericSensorValue(radiation)],
[HISTORY_KEYS.rain, numericSensorValue(rainSensor)],
];
setHistory((current) => {
@@ -129,12 +130,12 @@ export function MeteoPage({ theme }: MeteoPageProps) {
<MetricTile
theme={theme}
title="Temperatura"
subtitle="Temperatura exterior"
subtitle={temperature?.name ?? "Temperatura exterior"}
sensor={temperature}
icon={<Thermometer className="h-5 w-5" />}
accent="amber"
status={temperatureBadge(temperature)}
values={history["temperatura.exterior"]}
values={history[HISTORY_KEYS.temperature]}
menuOpen={openMenu === "temperature"}
onMenuToggle={() =>
setOpenMenu(openMenu === "temperature" ? null : "temperature")
@@ -154,12 +155,12 @@ export function MeteoPage({ theme }: MeteoPageProps) {
<MetricTile
theme={theme}
title="Humidade"
subtitle="Humidade relativa"
subtitle={humidity?.name ?? "Humidade relativa"}
sensor={humidity}
icon={<Droplets className="h-5 w-5" />}
accent="blue"
status={humidityBadge(humidity)}
values={history["humidade.exterior"]}
values={history[HISTORY_KEYS.humidity]}
menuOpen={openMenu === "humidity"}
onMenuToggle={() =>
setOpenMenu(openMenu === "humidity" ? null : "humidity")
@@ -179,12 +180,12 @@ export function MeteoPage({ theme }: MeteoPageProps) {
<MetricTile
theme={theme}
title="Precipitação"
subtitle="Precipitação atual"
subtitle={rainSensor?.name ?? "Precipitação atual"}
sensor={rainSensor}
icon={<CloudRain className="h-5 w-5" />}
accent="emerald"
status={isRaining ? "A chover" : "Sem chuva"}
values={history["chuva.total"]}
values={history[HISTORY_KEYS.rain]}
menuOpen={openMenu === "rain"}
onMenuToggle={() =>
setOpenMenu(openMenu === "rain" ? null : "rain")
@@ -213,18 +214,21 @@ export function MeteoPage({ theme }: MeteoPageProps) {
/>
</div>
<CompassPanel theme={theme} direction={numericValue(windDirection)} />
<CompassPanel
theme={theme}
direction={numericSensorValue(windDirection)}
/>
<div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-1">
<MetricTile
theme={theme}
title="Vento"
subtitle="Velocidade do vento"
subtitle={windSpeed?.name ?? "Velocidade do vento"}
sensor={windSpeed}
icon={<Wind className="h-5 w-5" />}
accent="cyan"
status={windBadge(windSpeed)}
values={history["vento.velocidade"]}
values={history[HISTORY_KEYS.windSpeed]}
menuOpen={openMenu === "wind"}
onMenuToggle={() =>
setOpenMenu(openMenu === "wind" ? null : "wind")
@@ -244,12 +248,12 @@ export function MeteoPage({ theme }: MeteoPageProps) {
<MetricTile
theme={theme}
title="Radiação solar"
subtitle="Radiação instantânea"
subtitle={radiation?.name ?? "Radiação instantânea"}
sensor={radiation}
icon={<Sun className="h-5 w-5" />}
accent="amber"
status={radiationBadge(radiation)}
values={history["radiacao.solar"]}
values={history[HISTORY_KEYS.radiation]}
menuOpen={openMenu === "radiation"}
onMenuToggle={() =>
setOpenMenu(openMenu === "radiation" ? null : "radiation")
@@ -703,8 +707,8 @@ function CompassLabel({
return (
<span
className={`absolute flex h-6 w-6 items-center justify-center rounded-full text-[13px] font-black leading-none tracking-[0.02em] ${isDark
? "bg-white/[0.03] text-slate-300"
: "bg-white text-slate-500 shadow-sm"
? "bg-white/[0.03] text-slate-300"
: "bg-white text-slate-500 shadow-sm"
} ${className}`}
>
{label}
@@ -760,20 +764,6 @@ function Sparkline({
);
}
function findSensor(sensors: ModuleSensorResponse[], key: string) {
return sensors.find((sensor) => sensor.key === key);
}
function numericValue(sensor?: ModuleSensorResponse) {
return typeof sensor?.value === "number" ? sensor.value : null;
}
function maxSensor(sensors: ModuleSensorResponse[]) {
return sensors
.filter((sensor) => typeof sensor.value === "number")
.sort((a, b) => Number(b.value) - Number(a.value))[0];
}
function formatValue(sensor?: ModuleSensorResponse) {
if (!sensor) return "--";
@@ -824,7 +814,7 @@ function getTrend(values?: number[]) {
}
function temperatureBadge(sensor?: ModuleSensorResponse) {
const value = numericValue(sensor);
const value = numericSensorValue(sensor);
if (value === null) return "Sem dados";
if (value >= 30) return "Quente";
if (value <= 10) return "Frio";
@@ -832,7 +822,7 @@ function temperatureBadge(sensor?: ModuleSensorResponse) {
}
function humidityBadge(sensor?: ModuleSensorResponse) {
const value = numericValue(sensor);
const value = numericSensorValue(sensor);
if (value === null) return "Sem dados";
if (value >= 80) return "Alta";
if (value <= 35) return "Baixa";
@@ -840,7 +830,7 @@ function humidityBadge(sensor?: ModuleSensorResponse) {
}
function windBadge(sensor?: ModuleSensorResponse) {
const value = numericValue(sensor);
const value = numericSensorValue(sensor);
if (value === null) return "Sem dados";
if (value >= 30) return "Forte";
if (value >= 10) return "Moderado";
@@ -848,7 +838,7 @@ function windBadge(sensor?: ModuleSensorResponse) {
}
function radiationBadge(sensor?: ModuleSensorResponse) {
const value = numericValue(sensor);
const value = numericSensorValue(sensor);
if (value === null) return "Sem dados";
if (value >= 800) return "Alta";
if (value >= 400) return "Média";