"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);
};