From c19a4009f37ec2aba8d4edd2d19819605ecb9575 Mon Sep 17 00:00:00 2001 From: Yossi Hillali Date: Wed, 29 Jan 2025 23:02:13 +0200 Subject: [PATCH] feat: weather widget - add wind speed, option to disable decimals for temperature & redesign dropdown forecast (#2099) --- packages/api/src/router/widgets/weather.ts | 6 ++- packages/old-import/src/widgets/options.ts | 2 + packages/translation/src/lang/en.json | 14 +++++ packages/validation/src/widgets/weather.ts | 5 ++ packages/widgets/src/weather/component.tsx | 63 ++++++++++++++++++---- packages/widgets/src/weather/icon.tsx | 28 ++++++++-- packages/widgets/src/weather/index.ts | 2 + 7 files changed, 106 insertions(+), 14 deletions(-) diff --git a/packages/api/src/router/widgets/weather.ts b/packages/api/src/router/widgets/weather.ts index 3a53157f8..ded3bc5b0 100644 --- a/packages/api/src/router/widgets/weather.ts +++ b/packages/api/src/router/widgets/weather.ts @@ -6,7 +6,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc"; export const weatherRouter = createTRPCRouter({ atLocation: publicProcedure.input(validation.widget.weather.atLocationInput).query(async ({ input }) => { const res = await fetchWithTimeout( - `https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min¤t_weather=true&timezone=auto`, + `https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max¤t_weather=true&timezone=auto`, ); const json: unknown = await res.json(); const weather = await validation.widget.weather.atLocationOutput.parseAsync(json); @@ -18,6 +18,10 @@ export const weatherRouter = createTRPCRouter({ weatherCode: weather.daily.weathercode[index] ?? 404, maxTemp: weather.daily.temperature_2m_max[index], minTemp: weather.daily.temperature_2m_min[index], + sunrise: weather.daily.sunrise[index], + sunset: weather.daily.sunset[index], + maxWindSpeed: weather.daily.wind_speed_10m_max[index], + maxWindGusts: weather.daily.wind_gusts_10m_max[index], }; }), }; diff --git a/packages/old-import/src/widgets/options.ts b/packages/old-import/src/widgets/options.ts index e9082565f..3e2d27b4e 100644 --- a/packages/old-import/src/widgets/options.ts +++ b/packages/old-import/src/widgets/options.ts @@ -77,6 +77,8 @@ const optionMapping: OptionMapping = { forecastDayCount: (oldOptions) => oldOptions.forecastDays, hasForecast: (oldOptions) => oldOptions.displayWeekly, isFormatFahrenheit: (oldOptions) => oldOptions.displayInFahrenheit, + disableTemperatureDecimals: () => undefined, + showCurrentWindSpeed: () => undefined, location: (oldOptions) => oldOptions.location, showCity: (oldOptions) => oldOptions.displayCityName, dateFormat: (oldOptions) => (oldOptions.dateFormat === "hide" ? undefined : oldOptions.dateFormat), diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 440a3a3b3..31274b054 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -1373,6 +1373,13 @@ "isFormatFahrenheit": { "label": "Temperature in Fahrenheit" }, + "disableTemperatureDecimals": { + "label": "Disable temperature decimals" + }, + "showCurrentWindSpeed": { + "label": "Show current wind speed", + "description": "Only on current weather" + }, "location": { "label": "Weather location" }, @@ -1391,6 +1398,13 @@ "description": "How the date should look like" } }, + "currentWindSpeed": "{currentWindSpeed} km/h", + "dailyForecast": { + "sunrise": "Sunrise", + "sunset": "Sunset", + "maxWindSpeed": "Max wind speed: {maxWindSpeed} km/h", + "maxWindGusts": "Max wind gusts: {maxWindGusts} km/h" + }, "kind": { "clear": "Clear", "mainlyClear": "Mainly clear", diff --git a/packages/validation/src/widgets/weather.ts b/packages/validation/src/widgets/weather.ts index 29b93e578..6b9bf74b9 100644 --- a/packages/validation/src/widgets/weather.ts +++ b/packages/validation/src/widgets/weather.ts @@ -9,12 +9,17 @@ export const atLocationOutput = z.object({ current_weather: z.object({ weathercode: z.number(), temperature: z.number(), + windspeed: z.number(), }), daily: z.object({ time: z.array(z.string()), weathercode: z.array(z.number()), temperature_2m_max: z.array(z.number()), temperature_2m_min: z.array(z.number()), + sunrise: z.array(z.string()), + sunset: z.array(z.string()), + wind_speed_10m_max: z.array(z.number()), + wind_gusts_10m_max: z.array(z.number()), }), }); diff --git a/packages/widgets/src/weather/component.tsx b/packages/widgets/src/weather/component.tsx index dfff28da6..f80aee520 100644 --- a/packages/widgets/src/weather/component.tsx +++ b/packages/widgets/src/weather/component.tsx @@ -1,12 +1,13 @@ "use client"; import { Box, Group, HoverCard, Space, Stack, Text } from "@mantine/core"; -import { IconArrowDownRight, IconArrowUpRight, IconMapPin } from "@tabler/icons-react"; +import { IconArrowDownRight, IconArrowUpRight, IconMapPin, 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 { useScopedI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; import { WeatherDescription, WeatherIcon } from "./icon"; @@ -47,6 +48,7 @@ interface WeatherProps extends Pick, "options"> } const DailyWeather = ({ options, weather }: WeatherProps) => { + const t = useScopedI18n("widget.weather"); return ( <> @@ -60,15 +62,32 @@ const DailyWeather = ({ options, weather }: WeatherProps) => { - {getPreferredUnit(weather.current.temperature, options.isFormatFahrenheit)} + + {getPreferredUnit( + weather.current.temperature, + options.isFormatFahrenheit, + options.disableTemperatureDecimals, + )} + + {options.showCurrentWindSpeed && ( + + + {t("currentWindSpeed", { currentWindSpeed: weather.current.windspeed })} + + )} + - {getPreferredUnit(weather.daily[0]?.maxTemp, options.isFormatFahrenheit)} + + {getPreferredUnit(weather.daily[0]?.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)} + - {getPreferredUnit(weather.daily[0]?.minTemp, options.isFormatFahrenheit)} + + {getPreferredUnit(weather.daily[0]?.minTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)} + {options.showCity && ( <> @@ -108,7 +127,13 @@ const WeeklyForecast = ({ options, weather }: WeatherProps) => { - {getPreferredUnit(weather.current.temperature, options.isFormatFahrenheit)} + + {getPreferredUnit( + weather.current.temperature, + options.isFormatFahrenheit, + options.disableTemperatureDecimals, + )} + @@ -134,7 +159,9 @@ function Forecast({ weather, options }: WeatherProps) { > {dayjs(dayWeather.time).format("dd")} - {getPreferredUnit(dayWeather.maxTemp, options.isFormatFahrenheit)} + + {getPreferredUnit(dayWeather.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)} + @@ -142,8 +169,20 @@ function Forecast({ weather, options }: WeatherProps) { dateFormat={dateFormat} time={dayWeather.time} weatherCode={dayWeather.weatherCode} - maxTemp={getPreferredUnit(dayWeather.maxTemp, options.isFormatFahrenheit)} - minTemp={getPreferredUnit(dayWeather.minTemp, options.isFormatFahrenheit)} + maxTemp={getPreferredUnit( + dayWeather.maxTemp, + options.isFormatFahrenheit, + options.disableTemperatureDecimals, + )} + minTemp={getPreferredUnit( + dayWeather.minTemp, + options.isFormatFahrenheit, + options.disableTemperatureDecimals, + )} + sunrise={dayjs(dayWeather.sunrise).format("HH:mm")} + sunset={dayjs(dayWeather.sunset).format("HH:mm")} + maxWindSpeed={dayWeather.maxWindSpeed} + maxWindGusts={dayWeather.maxWindGusts} /> @@ -152,5 +191,9 @@ function Forecast({ weather, options }: WeatherProps) { ); } -const getPreferredUnit = (value?: number, isFahrenheit = false): string => - value ? (isFahrenheit ? `${(value * (9 / 5) + 32).toFixed(1)}°F` : `${value.toFixed(1)}°C`) : "?"; +const getPreferredUnit = (value?: number, isFahrenheit = false, disableTemperatureDecimals = false): string => + value + ? isFahrenheit + ? `${(value * (9 / 5) + 32).toFixed(disableTemperatureDecimals ? 0 : 1)}°F` + : `${value.toFixed(disableTemperatureDecimals ? 0 : 1)}°C` + : "?"; diff --git a/packages/widgets/src/weather/icon.tsx b/packages/widgets/src/weather/icon.tsx index d6aa54d30..f93eba241 100644 --- a/packages/widgets/src/weather/icon.tsx +++ b/packages/widgets/src/weather/icon.tsx @@ -1,13 +1,17 @@ -import { Stack, Text } from "@mantine/core"; +import { List, Stack, Text } from "@mantine/core"; import { IconCloud, IconCloudFog, IconCloudRain, IconCloudSnow, IconCloudStorm, + IconMoon, IconQuestionMark, IconSnowflake, IconSun, + IconTemperatureMinus, + IconTemperaturePlus, + IconWind, } from "@tabler/icons-react"; import dayjs from "dayjs"; @@ -41,6 +45,10 @@ interface WeatherDescriptionProps { weatherCode: number; maxTemp?: string; minTemp?: string; + sunrise?: string; + sunset?: string; + maxWindSpeed?: number; + maxWindGusts?: number; } /** @@ -50,6 +58,10 @@ interface WeatherDescriptionProps { * @param weatherCode weather code from api * @param maxTemp preformatted string for max temperature * @param minTemp preformatted string for min temperature + * @param sunrise preformatted string for sunrise time + * @param sunset preformatted string for sunset time + * @param maxWindSpeed maximum wind speed + * @param maxWindGusts maximum wind gusts * @returns Content for a HoverCard dropdown presenting weather information */ export const WeatherDescription = ({ @@ -59,6 +71,10 @@ export const WeatherDescription = ({ weatherCode, maxTemp, minTemp, + sunrise, + sunset, + maxWindSpeed, + maxWindGusts, }: WeatherDescriptionProps) => { const t = useScopedI18n("widget.weather"); const tCommon = useScopedI18n("common"); @@ -73,8 +89,14 @@ export const WeatherDescription = ({ {dayjs(time).format(dateFormat)} {t(`kind.${name}`)} - {`${tCommon("information.max")}: ${maxTemp}`} - {`${tCommon("information.min")}: ${minTemp}`} + + }>{`${tCommon("information.max")}: ${maxTemp}`} + }>{`${tCommon("information.min")}: ${minTemp}`} + }>{`${t("dailyForecast.sunrise")}: ${sunrise}`} + }>{`${t("dailyForecast.sunset")}: ${sunset}`} + }>{t("dailyForecast.maxWindSpeed", { maxWindSpeed })} + }>{t("dailyForecast.maxWindGusts", { maxWindGusts })} + ); }; diff --git a/packages/widgets/src/weather/index.ts b/packages/widgets/src/weather/index.ts index c20ef1e39..96bb83031 100644 --- a/packages/widgets/src/weather/index.ts +++ b/packages/widgets/src/weather/index.ts @@ -10,6 +10,8 @@ export const { definition, componentLoader } = createWidgetDefinition("weather", options: optionsBuilder.from( (factory) => ({ isFormatFahrenheit: factory.switch(), + disableTemperatureDecimals: factory.switch(), + showCurrentWindSpeed: factory.switch({ withDescription: true }), location: factory.location({ defaultValue: { name: "Paris",