Skip to content

Commit

Permalink
feat: weather widget - add wind speed, option to disable decimals for…
Browse files Browse the repository at this point in the history
… temperature & redesign dropdown forecast (#2099)
  • Loading branch information
hillaliy authored Jan 29, 2025
1 parent 5657384 commit c19a400
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 14 deletions.
6 changes: 5 additions & 1 deletion packages/api/src/router/widgets/weather.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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&current_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&current_weather=true&timezone=auto`,
);
const json: unknown = await res.json();
const weather = await validation.widget.weather.atLocationOutput.parseAsync(json);
Expand All @@ -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],
};
}),
};
Expand Down
2 changes: 2 additions & 0 deletions packages/old-import/src/widgets/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
14 changes: 14 additions & 0 deletions packages/translation/src/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions packages/validation/src/widgets/weather.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
}),
});

Expand Down
63 changes: 53 additions & 10 deletions packages/widgets/src/weather/component.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -47,6 +48,7 @@ interface WeatherProps extends Pick<WidgetComponentProps<"weather">, "options">
}

const DailyWeather = ({ options, weather }: WeatherProps) => {
const t = useScopedI18n("widget.weather");
return (
<>
<Group className="weather-day-group" gap="1cqmin">
Expand All @@ -60,15 +62,32 @@ const DailyWeather = ({ options, weather }: WeatherProps) => {
<WeatherDescription weatherOnly weatherCode={weather.current.weathercode} />
</HoverCard.Dropdown>
</HoverCard>
<Text fz="17.5cqmin">{getPreferredUnit(weather.current.temperature, options.isFormatFahrenheit)}</Text>
<Text fz="17.5cqmin">
{getPreferredUnit(
weather.current.temperature,
options.isFormatFahrenheit,
options.disableTemperatureDecimals,
)}
</Text>
</Group>
<Space h="1cqmin" />
{options.showCurrentWindSpeed && (
<Group className="weather-current-wind-speed-group" wrap="nowrap" gap="1cqmin">
<IconWind size="12.5cqmin" />
<Text fz="10cqmin">{t("currentWindSpeed", { currentWindSpeed: weather.current.windspeed })}</Text>
</Group>
)}
<Space h="1cqmin" />
<Group className="weather-max-min-temp-group" wrap="nowrap" gap="1cqmin">
<IconArrowUpRight size="12.5cqmin" />
<Text fz="10cqmin">{getPreferredUnit(weather.daily[0]?.maxTemp, options.isFormatFahrenheit)}</Text>
<Text fz="10cqmin">
{getPreferredUnit(weather.daily[0]?.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)}
</Text>
<Space w="2.5cqmin" />
<IconArrowDownRight size="12.5cqmin" />
<Text fz="10cqmin">{getPreferredUnit(weather.daily[0]?.minTemp, options.isFormatFahrenheit)}</Text>
<Text fz="10cqmin">
{getPreferredUnit(weather.daily[0]?.minTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)}
</Text>
</Group>
{options.showCity && (
<>
Expand Down Expand Up @@ -108,7 +127,13 @@ const WeeklyForecast = ({ options, weather }: WeatherProps) => {
<WeatherDescription weatherOnly weatherCode={weather.current.weathercode} />
</HoverCard.Dropdown>
</HoverCard>
<Text fz="20cqmin">{getPreferredUnit(weather.current.temperature, options.isFormatFahrenheit)}</Text>
<Text fz="20cqmin">
{getPreferredUnit(
weather.current.temperature,
options.isFormatFahrenheit,
options.disableTemperatureDecimals,
)}
</Text>
</Group>
<Space h="2.5cqmin" />
<Forecast weather={weather} options={options} />
Expand All @@ -134,16 +159,30 @@ function Forecast({ weather, options }: WeatherProps) {
>
<Text fz="10cqmin">{dayjs(dayWeather.time).format("dd")}</Text>
<WeatherIcon size="15cqmin" code={dayWeather.weatherCode} />
<Text fz="10cqmin">{getPreferredUnit(dayWeather.maxTemp, options.isFormatFahrenheit)}</Text>
<Text fz="10cqmin">
{getPreferredUnit(dayWeather.maxTemp, options.isFormatFahrenheit, options.disableTemperatureDecimals)}
</Text>
</Stack>
</HoverCard.Target>
<HoverCard.Dropdown>
<WeatherDescription
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}
/>
</HoverCard.Dropdown>
</HoverCard>
Expand All @@ -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`
: "?";
28 changes: 25 additions & 3 deletions packages/widgets/src/weather/icon.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -41,6 +45,10 @@ interface WeatherDescriptionProps {
weatherCode: number;
maxTemp?: string;
minTemp?: string;
sunrise?: string;
sunset?: string;
maxWindSpeed?: number;
maxWindGusts?: number;
}

/**
Expand All @@ -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 = ({
Expand All @@ -59,6 +71,10 @@ export const WeatherDescription = ({
weatherCode,
maxTemp,
minTemp,
sunrise,
sunset,
maxWindSpeed,
maxWindGusts,
}: WeatherDescriptionProps) => {
const t = useScopedI18n("widget.weather");
const tCommon = useScopedI18n("common");
Expand All @@ -73,8 +89,14 @@ export const WeatherDescription = ({
<Stack align="center" gap="0">
<Text fz="24px">{dayjs(time).format(dateFormat)}</Text>
<Text fz="16px">{t(`kind.${name}`)}</Text>
<Text fz="16px">{`${tCommon("information.max")}: ${maxTemp}`}</Text>
<Text fz="16px">{`${tCommon("information.min")}: ${minTemp}`}</Text>
<List>
<List.Item icon={<IconTemperaturePlus size={15} />}>{`${tCommon("information.max")}: ${maxTemp}`}</List.Item>
<List.Item icon={<IconTemperatureMinus size={15} />}>{`${tCommon("information.min")}: ${minTemp}`}</List.Item>
<List.Item icon={<IconSun size={15} />}>{`${t("dailyForecast.sunrise")}: ${sunrise}`}</List.Item>
<List.Item icon={<IconMoon size={15} />}>{`${t("dailyForecast.sunset")}: ${sunset}`}</List.Item>
<List.Item icon={<IconWind size={15} />}>{t("dailyForecast.maxWindSpeed", { maxWindSpeed })}</List.Item>
<List.Item icon={<IconWind size={15} />}>{t("dailyForecast.maxWindGusts", { maxWindGusts })}</List.Item>
</List>
</Stack>
);
};
Expand Down
2 changes: 2 additions & 0 deletions packages/widgets/src/weather/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit c19a400

Please sign in to comment.