import type { ReactNode } from "react"; import { useId } from "react"; import { CompositeChart } from "@mantine/charts"; import { Box, Group, Paper, Popover, ScrollArea, SimpleGrid, Stack, Text, UnstyledButton, VisuallyHidden, } from "@mantine/core"; import { IconDroplets, IconMapPin, IconSunHigh, IconSunrise, IconSunset, IconWind } from "@tabler/icons-react"; import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client"; import type { WidgetProps } from "../definition"; import { AnimatedWeatherIcon } from "./animated-icon"; import classes from "./component.module.css"; import { DailyWeatherDetails } from "./details"; import type { HourlyChartRow, WeatherData } from "./format"; import { getHourlyChartData, getPreferredDate, getPreferredTime, getPreferredUnit, getPreferredWindSpeed, } from "./format"; import { getWeatherKind, WeatherDescription } from "./icon"; import { getAdvancedWeatherLayout } from "./layout"; interface AdvancedWeatherProps { height: number; options: WidgetProps<"weather">["options"]; weather: WeatherData; width: number; } interface MetricCardProps { icon: ReactNode; label: string; value: ReactNode; } interface WeatherChartDatum { detailLabel: string; observedAt: string; precipitationProbability: number; precipitationSummary: string; temperature: number; temperatureSummary: string; tooltipLabel: string; } interface HourlyChartTooltipProps { active?: boolean; payload?: Array<{ payload?: WeatherChartDatum }>; } const HourlyChartTooltip = ({ active, payload }: HourlyChartTooltipProps) => { const datum = payload?.[0]?.payload; if (!active || !datum) return null; return ( {datum.tooltipLabel} {datum.temperatureSummary} · {datum.precipitationSummary} {datum.detailLabel} ); }; const MetricCard = ({ icon, label, value }: MetricCardProps) => ( {icon} {label} {value} ); export const AdvancedWeather = ({ height, options, weather, width }: AdvancedWeatherProps) => { const headingId = useId(); const chartId = useId(); const locale = useCurrentIntlLocale(); const t = useScopedI18n("widget.weather"); const tCommon = useScopedI18n("common"); const layout = getAdvancedWeatherLayout(width, height); const today = weather.daily[0]; const nextHour = weather.hourly[0]; const hourly = getHourlyChartData(weather, options.isFormatFahrenheit); const speedUnit = options.useImperialSpeed ? tCommon("unit.speed.milesPerHour") : tCommon("unit.speed.kilometersPerHour"); const temperatureUnit = options.isFormatFahrenheit ? "°F" : "°C"; const chartData = hourly.map((hour) => { const condition = hour.weatherCode === null ? t("kind.unknown") : t(`kind.${getWeatherKind(hour.weatherCode)}`); const temperature = hour.temperature === null ? "?" : hour.temperature.toFixed(1); const precipitationProbability = hour.precipitationProbability ?? "?"; let detailLabel = t("advanced.hourlyTooltipWithoutHumidity", { amount: hour.precipitation?.toFixed(1) ?? "?", feelsLike: hour.apparentTemperature?.toFixed(1) ?? "?", speed: getPreferredWindSpeed(hour.windSpeed, options.useImperialSpeed), speedUnit, temperatureUnit, }); if (options.showHumidity) { detailLabel = t("advanced.hourlyTooltip", { amount: hour.precipitation?.toFixed(1) ?? "?", feelsLike: hour.apparentTemperature?.toFixed(1) ?? "?", humidity: hour.relativeHumidity ?? "?", speed: getPreferredWindSpeed(hour.windSpeed, options.useImperialSpeed), speedUnit, temperatureUnit, }); } return { detailLabel, observedAt: hour.observedAt, precipitationProbability: hour.precipitationProbability ?? Number.NaN, precipitationSummary: `${t("advanced.precipitationChance")}: ${precipitationProbability}%`, temperature: hour.temperature ?? Number.NaN, temperatureSummary: `${t("advanced.temperature")}: ${temperature}${temperatureUnit}`, tooltipLabel: `${getPreferredTime(hour.observedAt, locale, weather.timezone)} · ${condition}`, }; }); const ticks = hourly.filter((_, index) => index % layout.hourlyTickStep === 0).map((hour) => hour.observedAt); return ( {getPreferredUnit( weather.current.temperature, options.isFormatFahrenheit, options.disableTemperatureDecimals, )} {t("advanced.feelsLike", { temperature: getPreferredUnit( weather.current.apparentTemperature, options.isFormatFahrenheit, options.disableTemperatureDecimals, ), })} {options.location.name} {t("advanced.observedAt", { time: getPreferredTime(weather.current.observedAt, locale, weather.timezone), })} {today && ( {t("advanced.highLow", { maximum: getPreferredUnit( today.maxTemperature, options.isFormatFahrenheit, options.disableTemperatureDecimals, ), minimum: getPreferredUnit( today.minTemperature, options.isFormatFahrenheit, options.disableTemperatureDecimals, ), })} )} } label={t("advanced.nextHourPrecipitation")} value={t("advanced.precipitationValue", { amount: nextHour?.precipitation?.toFixed(1) ?? "?", probability: nextHour?.precipitationProbability ?? "?", })} /> {options.showHumidity && ( } label={t("advanced.currentHumidity")} value={`${weather.current.relativeHumidity}%`} /> )} } label={t("advanced.currentWind")} value={t("advanced.windValue", { gusts: getPreferredWindSpeed(weather.current.windGusts, options.useImperialSpeed), speed: getPreferredWindSpeed(weather.current.windSpeed, options.useImperialSpeed), unit: speedUnit, })} /> } label={t("advanced.todayUv")} value={today?.uvIndex?.toFixed(1) ?? "?"} /> } label={t("dailyForecast.sunrise")} value={getPreferredTime(today?.sunriseAt, locale, weather.timezone)} /> } label={t("dailyForecast.sunset")} value={getPreferredTime(today?.sunsetAt, locale, weather.timezone)} /> {chartData.length > 0 && ( {t("advanced.hourlyForecast")} `${value}°` }} rightYAxisProps={{ width: 38, domain: [0, 100], tickFormatter: (value: number) => `${value}%` }} xAxisProps={{ ticks, interval: 0, tickFormatter: (value: string) => getPreferredTime(value, locale, weather.timezone), }} tooltipAnimationDuration={0} tooltipProps={{ content: }} /> )} {t("advanced.forecast")} {weather.daily.map((day) => ( {getPreferredDate(day.date, locale, { weekday: "short" })} {getPreferredDate(day.date, locale, { month: "short", day: "numeric" })} {t(`kind.${getWeatherKind(day.weatherCode)}`)} {getPreferredUnit( day.maxTemperature, options.isFormatFahrenheit, options.disableTemperatureDecimals, )} {getPreferredUnit( day.minTemperature, options.isFormatFahrenheit, options.disableTemperatureDecimals, )} {day.precipitationProbability ?? "?"}% {getPreferredWindSpeed(day.maxWindSpeed, options.useImperialSpeed)} {speedUnit} ))} ); }; interface HourlyDetailProps { locale: string; showHumidity: boolean; speedUnit: string; temperatureUnit: string; timeZone: string; useImperialSpeed: boolean; } const HourlyWeatherTable = ({ hourly, locale, showHumidity, speedUnit, temperatureUnit, timeZone, useImperialSpeed, }: HourlyDetailProps & { hourly: HourlyChartRow[] }) => { const t = useScopedI18n("widget.weather"); return ( {showHumidity && } {hourly.map((hour) => ( {showHumidity && } ))}
{t("advanced.hourlyForecast")}
{t("advanced.time")} {t("advanced.conditions")} {t("advanced.temperature")} {t("advanced.feelsLikeLabel")} {t("advanced.precipitationChance")}{t("advanced.humidity")}{t("advanced.wind", { unit: speedUnit })}
{getPreferredTime(hour.observedAt, locale, timeZone)} {hour.weatherCode === null ? t("kind.unknown") : t(`kind.${getWeatherKind(hour.weatherCode)}`)} {hour.temperature === null ? "?" : `${hour.temperature.toFixed(1)}${temperatureUnit}`} {hour.apparentTemperature === null ? "?" : `${hour.apparentTemperature.toFixed(1)}${temperatureUnit}`} {hour.precipitationProbability === null ? "?" : `${hour.precipitationProbability}%`}{hour.relativeHumidity === null ? "?" : `${hour.relativeHumidity}%`} {getPreferredWindSpeed(hour.windSpeed, useImperialSpeed)} /{" "} {getPreferredWindSpeed(hour.windGusts, useImperialSpeed)} {speedUnit}
); };