"use client"; import { useId } from "react"; import { Box, Group, Popover, Stack, Table, Text, UnstyledButton } from "@mantine/core"; import { IconArrowDownRight, IconArrowUpRight, IconDroplets, IconMapPin, IconSunrise, IconSunset, IconWind, } from "@tabler/icons-react"; import combineClasses from "clsx"; import dayjs from "dayjs"; import type { RouterOutputs } from "@homarr/api"; import { clientApi } from "@homarr/api/client"; import { metricToImperial } from "@homarr/common"; import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client"; import { WidgetEmptyState } from "../common/empty-state"; import { formatLocalizedDate, formatLocalizedTime } from "../common/locale"; import { getUsableWidgetQueryData, isInitialWidgetQueryPending } from "../common/query-state"; import { WidgetQueryErrorIndicator, WidgetQueryLoadingState } from "../common/query-state-indicator"; import type { WidgetComponentProps } from "../definition"; import { AnimatedWeatherIcon } from "./animated-icon"; import { formatWeatherDate, WeatherDescription } from "./icon"; import classes from "./component.module.css"; export default function WeatherWidget({ isEditMode, options, width, height, displayMode, }: WidgetComponentProps<"weather">) { const t = useScopedI18n("widget.weather"); const input = { latitude: options.location.latitude, longitude: options.location.longitude, }; const weatherQuery = clientApi.widget.weather.atLocation.useQuery(input); const weather = getUsableWidgetQueryData(weatherQuery); if (isInitialWidgetQueryPending(weatherQuery)) return ; if (!weather) return ; const content = displayMode === "advanced" ? ( ) : ( {options.hasForecast ? ( ) : ( )} ); return ( {content} {weatherQuery.error && ( )} ); } interface WeatherProps extends Pick, "options"> { weather: NonNullable; } interface CompactWeatherProps extends WeatherProps { isEditMode: boolean; } const AdvancedWeather = ({ options, weather }: WeatherProps) => { const forecastHeadingId = useId(); const locale = useCurrentIntlLocale(); const t = useScopedI18n("widget.weather"); const tCommon = useScopedI18n("common"); return ( {getPreferredUnit( weather.current.temperature, options.isFormatFahrenheit, options.disableTemperatureDecimals, )} {options.location.name} {t("currentWindSpeed", { currentWindSpeed: getPreferredWindSpeed(weather.current.windspeed, options.useImperialSpeed), unit: options.useImperialSpeed ? tCommon("unit.speed.milesPerHour") : tCommon("unit.speed.kilometersPerHour"), })} {t("advanced.forecast")} {t("advanced.day")} {t("advanced.conditions")} {t("advanced.temperature")} {t("advanced.humidity")} {t("advanced.wind", { unit: options.useImperialSpeed ? tCommon("unit.speed.milesPerHour") : tCommon("unit.speed.kilometersPerHour"), })} {t("advanced.sun")} {weather.daily.length === 0 ? ( {t("advanced.noForecast")} ) : ( weather.daily.map((dayWeather) => ( {formatLocalizedDate(dayWeather.time, locale, { weekday: "short" })} {formatLocalizedDate(dayWeather.time, locale, { month: "short", day: "numeric" })} {getPreferredUnit( dayWeather.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals, )} / {getPreferredUnit( dayWeather.minTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals, )} {dayWeather.humidity === undefined ? "?" : `${dayWeather.humidity}%`} {getPreferredWindSpeed(dayWeather.maxWindSpeed, options.useImperialSpeed)} /{" "} {getPreferredWindSpeed(dayWeather.maxWindGusts, options.useImperialSpeed)} {getPreferredTime(dayWeather.sunrise, locale)} / {getPreferredTime(dayWeather.sunset, locale)} )) )}
); }; const DailyWeather = ({ options, weather, isEditMode }: CompactWeatherProps) => { const t = useScopedI18n("widget.weather"); const tCommon = useScopedI18n("common"); return ( <> {getPreferredUnit( weather.current.temperature, options.isFormatFahrenheit, options.disableTemperatureDecimals, )} {options.showCurrentWindSpeed && ( {t("currentWindSpeed", { currentWindSpeed: (options.useImperialSpeed ? metricToImperial(weather.current.windspeed) : weather.current.windspeed ).toFixed(1), unit: options.useImperialSpeed ? tCommon("unit.speed.milesPerHour") : tCommon("unit.speed.kilometersPerHour"), })} )} {weather.daily[0]?.humidity !== undefined && ( {t("dailyForecast.humidity", { humidity: weather.daily[0].humidity })} )} {getPreferredUnit( weather.daily[0]?.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals, )} {getPreferredUnit( weather.daily[0]?.minTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals, )} {options.showCity && ( <> {options.location.name} )} ); }; const WeeklyForecast = ({ options, weather, isEditMode, maxDays }: CompactWeatherProps & { maxDays: number }) => { const t = useScopedI18n("widget.weather"); return ( <> {options.showCity && ( {options.location.name} )} {getPreferredUnit( weather.current.temperature, options.isFormatFahrenheit, options.disableTemperatureDecimals, )} ); }; function Forecast({ weather, options, isEditMode, maxDays }: CompactWeatherProps & { maxDays: number }) { const dateFormat = options.dateFormat; const t = useScopedI18n("widget.weather"); const locale = useCurrentIntlLocale(); return ( {weather.daily.slice(0, maxDays).map((dayWeather, index) => ( {formatLocalizedDate(dayWeather.time, locale, { weekday: "short" })} {getPreferredUnit(dayWeather.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)} ))} ); } export const getPreferredUnit = (value?: number, isFahrenheit = false, disableTemperatureDecimals = false): string => value !== undefined ? isFahrenheit ? `${(value * (9 / 5) + 32).toFixed(disableTemperatureDecimals ? 0 : 1)}°F` : `${value.toFixed(disableTemperatureDecimals ? 0 : 1)}°C` : "?"; export const getPreferredWindSpeed = (value: number | undefined, useImperialSpeed = false): string => value === undefined ? "?" : (useImperialSpeed ? metricToImperial(value) : value).toFixed(1); export const getPreferredTime = (value: string | undefined, locale = "en-US"): string => value ? formatLocalizedTime(value, locale) : "?"; export const getVisibleForecastDays = (configuredDays: number, width: number, height: number): number => { const widthBudget = Math.max(1, Math.floor(width / 58)); const heightBudget = height < 120 ? 3 : configuredDays; return Math.min(configuredDays, widthBudget, heightBudget); };