Add Weather module (beta)

Shows the current weather !
This commit is contained in:
Thomas Camlong
2022-05-18 22:17:58 +02:00
committed by GitHub
16 changed files with 1597 additions and 1342 deletions

View File

@@ -3,6 +3,7 @@ module.exports = {
addons: [ addons: [
'storybook-dark-mode', 'storybook-dark-mode',
'@storybook/addon-links', '@storybook/addon-links',
'storybook-addon-mock/register',
'@storybook/addon-essentials', '@storybook/addon-essentials',
{ {
name: 'storybook-addon-turbo-build', name: 'storybook-addon-turbo-build',

View File

@@ -169,10 +169,13 @@ Icons are requested in the following way: <br>
Modules are blocks shown on the sides of the Homarr dashboard that display information. They can be enabled in settings. Modules are blocks shown on the sides of the Homarr dashboard that display information. They can be enabled in settings.
**Clock Module** **Clock Module**
The clock module will display your current time and date. The Clock Module will display your current time and date.
**Calendar Module** **Calendar Module**
The Calendar module uses [integrations](#--integrations-1) to display new content. The Calendar Module uses [integrations](#--integrations-1) to display new content.
**Weather Module**
The Weather Module uses your devices location to display the current, highest, and lowest temperature.
**[⤴️ Back to Top](#-table-of-contents)** **[⤴️ Back to Top](#-table-of-contents)**

View File

@@ -79,9 +79,13 @@
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^2.0.0",
"jest": "^27.5.1", "jest": "^27.5.1",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"storybook-addon-mock": "^2.3.2",
"storybook-addon-turbo-build": "^1.1.0", "storybook-addon-turbo-build": "^1.1.0",
"storybook-dark-mode": "^1.0.9", "storybook-dark-mode": "^1.0.9",
"ts-jest": "^27.1.4", "ts-jest": "^27.1.4",
"typescript": "4.6.3" "typescript": "4.6.3"
},
"resolutions": {
"@types/react": "17.0.30"
} }
} }

View File

@@ -154,7 +154,14 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
return ( return (
<> <>
<Center> <Center>
<Image height={120} width={120} fit="contain" src={form.values.icon} alt="Placeholder" withPlaceholder /> <Image
height={120}
width={120}
fit="contain"
src={form.values.icon}
alt="Placeholder"
withPlaceholder
/>
</Center> </Center>
<form <form
onSubmit={form.onSubmit(() => { onSubmit={form.onSubmit(() => {

View File

@@ -1,7 +1,6 @@
import { Aside as MantineAside, Group } from '@mantine/core'; import { Aside as MantineAside, Group } from '@mantine/core';
import { DateModule } from '../modules'; import { WeatherModule, DateModule, CalendarModule } from '../modules';
import { CalendarModule } from '../modules/calendar/CalendarModule'; import { ModuleWrapper } from '../modules/moduleWrapper';
import ModuleWrapper from '../modules/moduleWrapper';
export default function Aside(props: any) { export default function Aside(props: any) {
return ( return (
@@ -18,6 +17,7 @@ export default function Aside(props: any) {
<Group mt="sm" grow direction="column"> <Group mt="sm" grow direction="column">
<ModuleWrapper module={CalendarModule} /> <ModuleWrapper module={CalendarModule} />
<ModuleWrapper module={DateModule} /> <ModuleWrapper module={DateModule} />
<ModuleWrapper module={WeatherModule} />
</Group> </Group>
</MantineAside> </MantineAside>
); );

View File

@@ -10,11 +10,7 @@ const useStyles = createStyles((theme) => ({
export default function Layout({ children, style }: any) { export default function Layout({ children, style }: any) {
const { classes, cx } = useStyles(); const { classes, cx } = useStyles();
return ( return (
<AppShell <AppShell aside={<Aside />} header={<Header />} footer={<Footer links={[]} />}>
aside={<Aside />}
header={<Header />}
footer={<Footer links={[]} />}
>
<main <main
className={cx(classes.main)} className={cx(classes.main)}
style={{ style={{

View File

@@ -1,6 +1,6 @@
import { Group, Navbar as MantineNavbar } from '@mantine/core'; import { Group, Navbar as MantineNavbar } from '@mantine/core';
import { DateModule } from '../modules/date/DateModule'; import { WeatherModule, DateModule } from '../modules';
import ModuleWrapper from '../modules/moduleWrapper'; import { ModuleWrapper } from '../modules/moduleWrapper';
export default function Navbar() { export default function Navbar() {
return ( return (
@@ -16,6 +16,8 @@ export default function Navbar() {
> >
<Group mt="sm" direction="column" align="center"> <Group mt="sm" direction="column" align="center">
<ModuleWrapper module={DateModule} /> <ModuleWrapper module={DateModule} />
<ModuleWrapper module={WeatherModule} />
<ModuleWrapper module={WeatherModule} />
</Group> </Group>
</MantineNavbar> </MantineNavbar>
); );

View File

@@ -2,3 +2,4 @@ export * from './date';
export * from './calendar'; export * from './calendar';
export * from './search'; export * from './search';
export * from './ping'; export * from './ping';
export * from './weather';

View File

@@ -2,7 +2,7 @@ import { Card, Menu, Switch, useMantineTheme } from '@mantine/core';
import { useConfig } from '../../tools/state'; import { useConfig } from '../../tools/state';
import { IModule } from './modules'; import { IModule } from './modules';
export default function ModuleWrapper(props: any) { export function ModuleWrapper(props: any) {
const { module }: { module: IModule } = props; const { module }: { module: IModule } = props;
const { config, setConfig } = useConfig(); const { config, setConfig } = useConfig();
const enabledModules = config.settings.enabledModules ?? []; const enabledModules = config.settings.enabledModules ?? [];

View 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;
}

View File

@@ -0,0 +1,22 @@
import withMock from 'storybook-addon-mock';
import WeatherComponent from './WeatherModule';
import mockdata from './mockdata.json';
export default {
title: 'Weather module',
decorators: [withMock],
};
export const Default = (args: any) => <WeatherComponent {...args} />;
Default.parameters = {
mockData: [
{
url: 'https://api.open-meteo.com/v1/forecast',
method: 'GET',
status: 200,
response: {
data: mockdata,
},
},
],
};

View File

@@ -0,0 +1,158 @@
import { Group, Space, Title, Tooltip } from '@mantine/core';
import axios from 'axios';
import { useEffect, useState } from 'react';
import {
ArrowDownRight,
ArrowUpRight,
Cloud,
CloudFog,
CloudRain,
CloudSnow,
CloudStorm,
QuestionMark,
Snowflake,
Sun,
} from 'tabler-icons-react';
import { IModule } from '../modules';
import { WeatherResponse } from './WeatherInterface';
export const WeatherModule: IModule = {
title: 'Weather (beta)',
description: 'Look up the current weather in your location',
icon: Sun,
component: WeatherComponent,
};
// 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
export function WeatherIcon(props: any) {
const { code } = props;
let data: { icon: any; name: string };
switch (code) {
case 0: {
data = { icon: Sun, name: 'Clear' };
break;
}
case 1:
case 2:
case 3: {
data = { icon: Cloud, name: 'Mainly clear' };
break;
}
case 45:
case 48: {
data = { icon: CloudFog, name: 'Fog' };
break;
}
case 51:
case 53:
case 55: {
data = { icon: Cloud, name: 'Drizzle' };
break;
}
case 56:
case 57: {
data = { icon: Snowflake, name: 'Freezing drizzle' };
break;
}
case 61:
case 63:
case 65: {
data = { icon: CloudRain, name: 'Rain' };
break;
}
case 66:
case 67: {
data = { icon: CloudRain, name: 'Freezing rain' };
break;
}
case 71:
case 73:
case 75: {
data = { icon: CloudSnow, name: 'Snow fall' };
break;
}
case 77: {
data = { icon: CloudSnow, name: 'Snow grains' };
break;
}
case 80:
case 81:
case 82: {
data = { icon: CloudRain, name: 'Rain showers' };
break;
}
case 85:
case 86: {
data = { icon: CloudSnow, name: 'Snow showers' };
break;
}
case 95: {
data = { icon: CloudStorm, name: 'Thunderstorm' };
break;
}
case 96:
case 99: {
data = { icon: CloudStorm, name: 'Thunderstorm with hail' };
break;
}
default: {
data = { icon: QuestionMark, name: 'Unknown' };
}
}
return (
<Tooltip label={data.name}>
<data.icon size={50} />
</Tooltip>
);
}
export default function WeatherComponent(props: any) {
// Get location from browser
const [location, setLocation] = useState({ lat: 0, lng: 0 });
const [weather, setWeather] = useState({} as WeatherResponse);
if ('geolocation' in navigator && location.lat === 0 && location.lng === 0) {
navigator.geolocation.getCurrentPosition((position) => {
setLocation({ lat: position.coords.latitude, lng: position.coords.longitude });
});
}
useEffect(() => {
axios
.get(
`https://api.open-meteo.com/v1/forecast?latitude=${location.lat}&longitude=${location.lng}&daily=weathercode,temperature_2m_max,temperature_2m_min&current_weather=true&timezone=Europe%2FLondon`
)
.then((res) => {
setWeather(res.data);
});
}, []);
if (!weather.current_weather) {
return null;
}
return (
<Group position="left" direction="column">
<Title>{weather.current_weather.temperature}°C</Title>
<Group spacing={0}>
<WeatherIcon code={weather.current_weather.weathercode} />
<Space mx="sm" />
<span>{weather.daily.temperature_2m_max[0]}°C</span>
<ArrowUpRight size={16} style={{ right: 15 }} />
<Space mx="sm" />
<span>{weather.daily.temperature_2m_min[0]}°C</span>
<ArrowDownRight size={16} />
</Group>
</Group>
);
}

View File

@@ -0,0 +1 @@
export { WeatherModule } from './WeatherModule';

View File

@@ -0,0 +1,58 @@
{
"current_weather": {
"winddirection": 121,
"windspeed": 12.7,
"time": "2022-05-15T14:00",
"weathercode": 3,
"temperature": 28.7
},
"utc_offset_seconds": 3600,
"latitude": 48.86,
"elevation": 46.1875,
"longitude": 2.3599997,
"generationtime_ms": 0.36406517028808594,
"daily_units": {
"temperature_2m_max": "°C",
"temperature_2m_min": "°C",
"time": "iso8601",
"weathercode": "wmo code"
},
"daily": {
"temperature_2m_max": [
29.1,
25.4,
28.2,
29.7,
24.6,
27.1,
22.9
],
"time": [
"2022-05-15",
"2022-05-16",
"2022-05-17",
"2022-05-18",
"2022-05-19",
"2022-05-20",
"2022-05-21"
],
"temperature_2m_min": [
14.3,
16.9,
17.2,
17.7,
19.2,
19.1,
14
],
"weathercode": [
95,
3,
3,
3,
3,
80,
3
]
}
}

View File

@@ -24,7 +24,7 @@ export default function App(props: AppProps & { colorScheme: ColorScheme }) {
return ( return (
<> <>
<Head> <Head>
<title>Homarr - A homepage for your server!</title> <title>Homarr 🦞</title>
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" /> <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
<link rel="shortcut icon" href="/favicon.svg" /> <link rel="shortcut icon" href="/favicon.svg" />
</Head> </Head>

2615
yarn.lock

File diff suppressed because it is too large Load Diff