🎨 Moved integrations in widgets directory
This commit is contained in:
73
src/widgets/weather/WeatherIcon.tsx
Normal file
73
src/widgets/weather/WeatherIcon.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Box, Tooltip } from '@mantine/core';
|
||||
import {
|
||||
IconCloud,
|
||||
IconCloudFog,
|
||||
IconCloudRain,
|
||||
IconCloudSnow,
|
||||
IconCloudStorm,
|
||||
IconQuestionMark,
|
||||
IconSnowflake,
|
||||
IconSun,
|
||||
TablerIcon,
|
||||
} from '@tabler/icons';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
interface WeatherIconProps {
|
||||
code: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon which should be displayed when specific code is defined
|
||||
* @param code weather code from api
|
||||
* @returns weather tile component
|
||||
*/
|
||||
export const WeatherIcon = ({ code }: WeatherIconProps) => {
|
||||
const { t } = useTranslation('modules/weather');
|
||||
|
||||
const { icon: Icon, name } =
|
||||
weatherDefinitions.find((wd) => wd.codes.includes(code)) ?? unknownWeather;
|
||||
|
||||
return (
|
||||
<Tooltip withinPortal withArrow label={t(`card.weatherDescriptions.${name}`)}>
|
||||
<Box>
|
||||
<Icon size={50} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
type WeatherDefinitionType = { icon: TablerIcon; name: string; codes: number[] };
|
||||
|
||||
// 0 Clear sky
|
||||
// 1, 2, 3 Mainly clear, partly cloudy, and overcast
|
||||
// 45, 48 Fog and depositing rime fog
|
||||
// 51, 53, 55 Drizzle: Light, moderate, and dense intensity
|
||||
// 56, 57 Freezing Drizzle: Light and dense intensity
|
||||
// 61, 63, 65 Rain: Slight, moderate and heavy intensity
|
||||
// 66, 67 Freezing Rain: Light and heavy intensity
|
||||
// 71, 73, 75 Snow fall: Slight, moderate, and heavy intensity
|
||||
// 77 Snow grains
|
||||
// 80, 81, 82 Rain showers: Slight, moderate, and violent
|
||||
// 85, 86Snow showers slight and heavy
|
||||
// 95 *Thunderstorm: Slight or moderate
|
||||
// 96, 99 *Thunderstorm with slight and heavy hail
|
||||
const weatherDefinitions: WeatherDefinitionType[] = [
|
||||
{ icon: IconSun, name: 'clear', codes: [0] },
|
||||
{ icon: IconCloud, name: 'mainlyClear', codes: [1, 2, 3] },
|
||||
{ icon: IconCloudFog, name: 'fog', codes: [45, 48] },
|
||||
{ icon: IconCloud, name: 'drizzle', codes: [51, 53, 55] },
|
||||
{ icon: IconSnowflake, name: 'freezingDrizzle', codes: [56, 57] },
|
||||
{ icon: IconCloudRain, name: 'rain', codes: [61, 63, 65] },
|
||||
{ icon: IconCloudRain, name: 'freezingRain', codes: [66, 67] },
|
||||
{ icon: IconCloudSnow, name: 'snowFall', codes: [71, 73, 75] },
|
||||
{ icon: IconCloudSnow, name: 'snowGrains', codes: [77] },
|
||||
{ icon: IconCloudRain, name: 'rainShowers', codes: [80, 81, 82] },
|
||||
{ icon: IconCloudSnow, name: 'snowShowers', codes: [85, 86] },
|
||||
{ icon: IconCloudStorm, name: 'thunderstorm', codes: [95] },
|
||||
{ icon: IconCloudStorm, name: 'thunderstormWithHail', codes: [96, 99] },
|
||||
];
|
||||
|
||||
const unknownWeather: Omit<WeatherDefinitionType, 'codes'> = {
|
||||
icon: IconQuestionMark,
|
||||
name: 'unknown',
|
||||
};
|
||||
96
src/widgets/weather/WeatherTile.tsx
Normal file
96
src/widgets/weather/WeatherTile.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Center, Group, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||
import { IconArrowDownRight, IconArrowUpRight } from '@tabler/icons';
|
||||
import { HomarrCardWrapper } from '../../components/Dashboard/Tiles/HomarrCardWrapper';
|
||||
import { WidgetsMenu } from '../../components/Dashboard/Tiles/Widgets/WidgetsMenu';
|
||||
import { BaseTileProps } from '../../components/Dashboard/Tiles/type';
|
||||
import { WeatherIntegrationType } from '../../types/integration';
|
||||
import { useWeatherForCity } from './useWeatherForCity';
|
||||
import { WeatherIcon } from './WeatherIcon';
|
||||
|
||||
interface WeatherTileProps extends BaseTileProps {
|
||||
module: WeatherIntegrationType; // TODO: change to new type defined through widgetDefinition
|
||||
}
|
||||
|
||||
export const WeatherTile = ({ className, module }: WeatherTileProps) => {
|
||||
const {
|
||||
data: weather,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useWeatherForCity(module?.properties.location ?? 'Paris');
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<HomarrCardWrapper className={className}>
|
||||
<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>
|
||||
</HomarrCardWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<HomarrCardWrapper className={className}>
|
||||
<Center>
|
||||
<Text weight={500}>An error occured</Text>
|
||||
</Center>
|
||||
</HomarrCardWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: add widgetWrapper that is generic and uses the definition
|
||||
return (
|
||||
<HomarrCardWrapper className={className}>
|
||||
<WidgetsMenu
|
||||
integration="weather"
|
||||
module={module}
|
||||
options={module?.properties}
|
||||
labels={{
|
||||
isFahrenheit: 'descriptor.settings.displayInFahrenheit.label',
|
||||
location: 'descriptor.settings.location.label',
|
||||
}}
|
||||
/>
|
||||
<Center style={{ height: '100%' }}>
|
||||
<Group spacing="md" noWrap align="center">
|
||||
<WeatherIcon code={weather!.current_weather.weathercode} />
|
||||
<Stack p={0} spacing={4}>
|
||||
<Title order={2}>
|
||||
{getPerferedUnit(
|
||||
weather!.current_weather.temperature,
|
||||
module?.properties.isFahrenheit
|
||||
)}
|
||||
</Title>
|
||||
<Group spacing="xs" noWrap>
|
||||
<div>
|
||||
<span>
|
||||
{getPerferedUnit(
|
||||
weather!.daily.temperature_2m_max[0],
|
||||
module?.properties.isFahrenheit
|
||||
)}
|
||||
</span>
|
||||
<IconArrowUpRight size={16} style={{ right: 15 }} />
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
{getPerferedUnit(
|
||||
weather!.daily.temperature_2m_min[0],
|
||||
module?.properties.isFahrenheit
|
||||
)}
|
||||
</span>
|
||||
<IconArrowDownRight size={16} />
|
||||
</div>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Center>
|
||||
</HomarrCardWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const getPerferedUnit = (value: number, isFahrenheit = false): string =>
|
||||
isFahrenheit ? `${(value * (9 / 5) + 32).toFixed(1)}°F` : `${value.toFixed(1)}°C`;
|
||||
41
src/widgets/weather/types.ts
Normal file
41
src/widgets/weather/types.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// To parse this data:
|
||||
//
|
||||
// import { Convert, WeatherResponse } from "./file";
|
||||
//
|
||||
// const weatherResponse = Convert.toWeatherResponse(json);
|
||||
//
|
||||
// These functions will throw an error if the JSON doesn't
|
||||
// match the expected interface, even if the JSON is valid.
|
||||
|
||||
export interface WeatherResponse {
|
||||
current_weather: CurrentWeather;
|
||||
utc_offset_seconds: number;
|
||||
latitude: number;
|
||||
elevation: number;
|
||||
longitude: number;
|
||||
generationtime_ms: number;
|
||||
daily_units: DailyUnits;
|
||||
daily: Daily;
|
||||
}
|
||||
|
||||
export interface CurrentWeather {
|
||||
winddirection: number;
|
||||
windspeed: number;
|
||||
time: string;
|
||||
weathercode: number;
|
||||
temperature: number;
|
||||
}
|
||||
|
||||
export interface Daily {
|
||||
temperature_2m_max: number[];
|
||||
time: Date[];
|
||||
temperature_2m_min: number[];
|
||||
weathercode: number[];
|
||||
}
|
||||
|
||||
export interface DailyUnits {
|
||||
temperature_2m_max: string;
|
||||
temperature_2m_min: string;
|
||||
time: string;
|
||||
weathercode: string;
|
||||
}
|
||||
53
src/widgets/weather/useWeatherForCity.ts
Normal file
53
src/widgets/weather/useWeatherForCity.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { WeatherResponse } from './types';
|
||||
|
||||
/**
|
||||
* Requests the weather of the specified city
|
||||
* @param cityName name of the city where the weather should be requested
|
||||
* @returns weather of specified city
|
||||
*/
|
||||
export const useWeatherForCity = (cityName: string) => {
|
||||
const {
|
||||
data: city,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({ queryKey: ['weatherCity', { cityName }], queryFn: () => fetchCity(cityName) });
|
||||
const weatherQuery = useQuery({
|
||||
queryKey: ['weather', { cityName }],
|
||||
queryFn: () => fetchWeather(city?.results[0]),
|
||||
enabled: !!city,
|
||||
refetchInterval: 1000 * 60 * 5, // requests the weather every 5 minutes
|
||||
});
|
||||
|
||||
return {
|
||||
...weatherQuery,
|
||||
isLoading: weatherQuery.isLoading || isLoading,
|
||||
isError: weatherQuery.isError || isError,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Requests the coordinates of a city
|
||||
* @param cityName name of city
|
||||
* @returns list with all coordinates for citites with specified name
|
||||
*/
|
||||
const fetchCity = async (cityName: string) => {
|
||||
const res = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${cityName}`);
|
||||
return (await res.json()) as { results: Coordinates[] };
|
||||
};
|
||||
|
||||
/**
|
||||
* Requests the weather of specific coordinates
|
||||
* @param coordinates of the location the weather should be fetched
|
||||
* @returns weather of specified coordinates
|
||||
*/
|
||||
const fetchWeather = async (coordinates?: Coordinates) => {
|
||||
if (!coordinates) return;
|
||||
const { longitude, latitude } = coordinates;
|
||||
const res = await fetch(
|
||||
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min¤t_weather=true&timezone=Europe%2FLondon`
|
||||
);
|
||||
return (await res.json()) as WeatherResponse;
|
||||
};
|
||||
|
||||
type Coordinates = { latitude: number; longitude: number };
|
||||
Reference in New Issue
Block a user