feat(charts): add persistent workspace chart management
This commit is contained in:
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user