✨ Add Weather module (beta)
Shows the current weather !
This commit is contained in:
@@ -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',
|
||||||
|
|||||||
@@ -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)**
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 ?? [];
|
||||||
|
|||||||
41
src/components/modules/weather/WeatherInterface.ts
Normal file
41
src/components/modules/weather/WeatherInterface.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;
|
||||||
|
}
|
||||||
22
src/components/modules/weather/WeatherModule.story.tsx
Normal file
22
src/components/modules/weather/WeatherModule.story.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
158
src/components/modules/weather/WeatherModule.tsx
Normal file
158
src/components/modules/weather/WeatherModule.tsx
Normal 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¤t_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>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
src/components/modules/weather/index.ts
Normal file
1
src/components/modules/weather/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { WeatherModule } from './WeatherModule';
|
||||||
58
src/components/modules/weather/mockdata.json
Normal file
58
src/components/modules/weather/mockdata.json
Normal 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
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user