feat: add weekly forecast to weather widget (#1932)
This commit is contained in:
@@ -10,6 +10,12 @@
|
|||||||
"displayCityName":{
|
"displayCityName":{
|
||||||
"label":"Display City Name"
|
"label":"Display City Name"
|
||||||
},
|
},
|
||||||
|
"displayWeekly":{
|
||||||
|
"label": "Display Weekly Forecast"
|
||||||
|
},
|
||||||
|
"forecastDays":{
|
||||||
|
"label": "Days To Display"
|
||||||
|
},
|
||||||
"location": {
|
"location": {
|
||||||
"label": "Weather location"
|
"label": "Weather location"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ const weatherSchema = z.object({
|
|||||||
temperature: z.number(),
|
temperature: z.number(),
|
||||||
}),
|
}),
|
||||||
daily: z.object({
|
daily: z.object({
|
||||||
|
time: z.array(z.string()),
|
||||||
|
weathercode: z.array(z.number()),
|
||||||
temperature_2m_max: z.array(z.number()),
|
temperature_2m_max: z.array(z.number()),
|
||||||
temperature_2m_min: z.array(z.number()),
|
temperature_2m_min: z.array(z.number()),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Center, Flex, Group, Skeleton, Stack, Text, Title } from '@mantine/core';
|
import { Card, Center, Flex, Group, Stack, Text, Title } from '@mantine/core';
|
||||||
import { useElementSize } from '@mantine/hooks';
|
import { useElementSize } from '@mantine/hooks';
|
||||||
import {
|
import {
|
||||||
IconArrowDownRight,
|
IconArrowDownRight,
|
||||||
@@ -7,9 +7,11 @@ import {
|
|||||||
IconMapPin,
|
IconMapPin,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Weather } from '~/server/api/routers/weather';
|
||||||
import { api } from '~/utils/api';
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
import { defineWidget } from '../helper';
|
import { defineWidget } from '../helper';
|
||||||
|
import { WidgetLoading } from '../loading';
|
||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
import { WeatherIcon } from './WeatherIcon';
|
import { WeatherIcon } from './WeatherIcon';
|
||||||
|
|
||||||
@@ -25,6 +27,17 @@ const definition = defineWidget({
|
|||||||
type: 'switch',
|
type: 'switch',
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
|
displayWeekly: {
|
||||||
|
type: 'switch',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
forecastDays: {
|
||||||
|
type: 'slider',
|
||||||
|
defaultValue: 5,
|
||||||
|
min: 1,
|
||||||
|
max: 7,
|
||||||
|
step: 1,
|
||||||
|
},
|
||||||
location: {
|
location: {
|
||||||
type: 'location',
|
type: 'location',
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
@@ -55,24 +68,7 @@ function WeatherTile({ widget }: WeatherTileProps) {
|
|||||||
const { t } = useTranslation('modules/weather');
|
const { t } = useTranslation('modules/weather');
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return <WidgetLoading />;
|
||||||
<Stack
|
|
||||||
ref={ref}
|
|
||||||
spacing="xs"
|
|
||||||
justify="space-around"
|
|
||||||
align="center"
|
|
||||||
style={{ height: '100%', width: '100%' }}
|
|
||||||
>
|
|
||||||
<Skeleton height={40} width={100} mb="xl" />
|
|
||||||
<Group noWrap>
|
|
||||||
<Skeleton height={50} circle />
|
|
||||||
<Group>
|
|
||||||
<Skeleton height={25} width={70} mr="lg" />
|
|
||||||
<Skeleton height={25} width={70} />
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
@@ -83,56 +79,114 @@ function WeatherTile({ widget }: WeatherTileProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add widgetWrapper that is generic and uses the definition
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack w="100%" h="100%" justify="space-around" ref={ref} spacing={0} align="center">
|
||||||
style={{ height: '100%', width: '100%' }}
|
{(widget?.properties.displayWeekly && (
|
||||||
justify="space-around"
|
<>
|
||||||
ref={ref}
|
<Flex
|
||||||
spacing={0}
|
align="center"
|
||||||
align="center"
|
gap={width < 120 ? '0.25rem' : 'xs'}
|
||||||
>
|
justify={'center'}
|
||||||
<Flex
|
direction={'row'}
|
||||||
align="center"
|
>
|
||||||
gap={width < 120 ? '0.25rem' : 'xs'}
|
{widget.properties.displayCityName && (
|
||||||
justify={'center'}
|
<Group noWrap spacing={5} align="center">
|
||||||
direction={width < 200 ? 'column' : 'row'}
|
<IconMapPin color="blue" size={30} />
|
||||||
>
|
<Text size={25} style={{ whiteSpace: 'nowrap' }}>
|
||||||
<WeatherIcon size={width < 300 ? 30 : 50} code={weather.current_weather.weathercode} />
|
{widget.properties.location.name}
|
||||||
<Title size={'h2'}>
|
</Text>
|
||||||
{getPerferedUnit(
|
</Group>
|
||||||
weather.current_weather.temperature,
|
)}
|
||||||
widget.properties.displayInFahrenheit
|
<WeatherIcon size={width < 300 ? 30 : 50} code={weather.current_weather.weathercode} />
|
||||||
)}
|
<Title size={'h2'} color={weather.current_weather.temperature > 20 ? 'red' : 'blue'}>
|
||||||
</Title>
|
{getPreferredUnit(
|
||||||
</Flex>
|
weather.current_weather.temperature,
|
||||||
|
widget.properties.displayInFahrenheit
|
||||||
|
)}
|
||||||
|
</Title>
|
||||||
|
</Flex>
|
||||||
|
<Forecast weather={weather} widget={widget} />
|
||||||
|
</>
|
||||||
|
)) || (
|
||||||
|
<>
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
gap={width < 120 ? '0.25rem' : 'xs'}
|
||||||
|
justify={'center'}
|
||||||
|
direction={width < 200 ? 'column' : 'row'}
|
||||||
|
>
|
||||||
|
<WeatherIcon size={width < 300 ? 30 : 50} code={weather.current_weather.weathercode} />
|
||||||
|
<Title size={'h2'}>
|
||||||
|
{getPreferredUnit(
|
||||||
|
weather.current_weather.temperature,
|
||||||
|
widget.properties.displayInFahrenheit
|
||||||
|
)}
|
||||||
|
</Title>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
{width > 200 && (
|
{width > 200 && (
|
||||||
<Group noWrap spacing="xs">
|
<Group noWrap spacing="xs">
|
||||||
<IconArrowUpRight />
|
<IconArrowUpRight />
|
||||||
{getPerferedUnit(
|
{getPreferredUnit(
|
||||||
weather.daily.temperature_2m_max[0],
|
weather.daily.temperature_2m_max[0],
|
||||||
widget.properties.displayInFahrenheit
|
widget.properties.displayInFahrenheit
|
||||||
|
)}
|
||||||
|
<IconArrowDownRight />
|
||||||
|
{getPreferredUnit(
|
||||||
|
weather.daily.temperature_2m_min[0],
|
||||||
|
widget.properties.displayInFahrenheit
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
)}
|
)}
|
||||||
<IconArrowDownRight />
|
|
||||||
{getPerferedUnit(
|
|
||||||
weather.daily.temperature_2m_min[0],
|
|
||||||
widget.properties.displayInFahrenheit
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{widget.properties.displayCityName && (
|
{widget.properties.displayCityName && (
|
||||||
<Group noWrap spacing={5} align="center">
|
<Group noWrap spacing={5} align="center">
|
||||||
<IconMapPin height={15} width={15} />
|
<IconMapPin height={15} width={15} />
|
||||||
<Text style={{ whiteSpace: 'nowrap' }}>{widget.properties.location.name}</Text>
|
<Text style={{ whiteSpace: 'nowrap' }}>{widget.properties.location.name}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPerferedUnit = (value: number, isFahrenheit = false): string =>
|
const getPreferredUnit = (value: number, isFahrenheit = false): string =>
|
||||||
isFahrenheit ? `${(value * (9 / 5) + 32).toFixed(1)}°F` : `${value.toFixed(1)}°C`;
|
isFahrenheit ? `${(value * (9 / 5) + 32).toFixed(1)}°F` : `${value.toFixed(1)}°C`;
|
||||||
|
|
||||||
|
interface ForecastProps {
|
||||||
|
weather: Weather;
|
||||||
|
widget: IWeatherWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Forecast({ weather: { daily }, widget }: ForecastProps) {
|
||||||
|
const { width } = useElementSize();
|
||||||
|
return (
|
||||||
|
<Flex align="center" direction="row" justify="space-between" w="100%" px="sm">
|
||||||
|
{daily.time.slice(0, widget.properties.forecastDays).map((time: any, index: number) => (
|
||||||
|
<Card key={index} padding="0.25rem">
|
||||||
|
<Flex direction="column" align="center">
|
||||||
|
<Text fw={700} lh="1.25rem">
|
||||||
|
{time.split('-')[2]}
|
||||||
|
</Text>
|
||||||
|
<WeatherIcon size={width < 300 ? 30 : 50} code={daily.weathercode[index]} />
|
||||||
|
<Text fz="sm" lh="1rem">
|
||||||
|
{getPreferredUnit(
|
||||||
|
daily.temperature_2m_max[index],
|
||||||
|
widget.properties.displayInFahrenheit
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<Text fz="sm" lh="1rem" color="grey">
|
||||||
|
{getPreferredUnit(
|
||||||
|
daily.temperature_2m_min[index],
|
||||||
|
widget.properties.displayInFahrenheit
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default definition;
|
export default definition;
|
||||||
|
|||||||
Reference in New Issue
Block a user