Advancement on the weather widget

This commit is contained in:
Thomas "ajnart" Camlong
2022-05-15 18:52:29 +02:00
parent 31deb5010f
commit 49d57024b9
4 changed files with 442 additions and 15 deletions

View File

@@ -0,0 +1,236 @@
// 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;
}
// Converts JSON strings to/from your types
// and asserts the results of JSON.parse at runtime
export class Convert {
public static toWeatherResponse(json: string): WeatherResponse {
return cast(JSON.parse(json), r('WeatherResponse'));
}
public static weatherResponseToJson(value: WeatherResponse): string {
return JSON.stringify(uncast(value, r('WeatherResponse')), null, 2);
}
}
function invalidValue(typ: any, val: any, key: any = ''): never {
if (key) {
throw Error(
`Invalid value for key "${key}". Expected type ${JSON.stringify(
typ
)} but got ${JSON.stringify(val)}`
);
}
throw Error(`Invalid value ${JSON.stringify(val)} for type ${JSON.stringify(typ)}`);
}
function jsonToJSProps(typ: any): any {
if (typ.jsonToJS === undefined) {
const map: any = {};
typ.props.forEach((p: any) => (map[p.json] = { key: p.js, typ: p.typ }));
typ.jsonToJS = map;
}
return typ.jsonToJS;
}
function jsToJSONProps(typ: any): any {
if (typ.jsToJSON === undefined) {
const map: any = {};
typ.props.forEach((p: any) => (map[p.js] = { key: p.json, typ: p.typ }));
typ.jsToJSON = map;
}
return typ.jsToJSON;
}
function transform(val: any, typ: any, getProps: any, key: any = ''): any {
function transformPrimitive(typ: string, val: any): any {
if (typeof typ === typeof val) return val;
return invalidValue(typ, val, key);
}
function transformUnion(typs: any[], val: any): any {
// val must validate against one typ in typs
const l = typs.length;
for (let i = 0; i < l; i++) {
const typ = typs[i];
try {
return transform(val, typ, getProps);
} catch (_) {}
}
return invalidValue(typs, val);
}
function transformEnum(cases: string[], val: any): any {
if (cases.indexOf(val) !== -1) return val;
return invalidValue(cases, val);
}
function transformArray(typ: any, val: any): any {
// val must be an array with no invalid elements
if (!Array.isArray(val)) return invalidValue('array', val);
return val.map((el) => transform(el, typ, getProps));
}
function transformDate(val: any): any {
if (val === null) {
return null;
}
const d = new Date(val);
if (isNaN(d.valueOf())) {
return invalidValue('Date', val);
}
return d;
}
function transformObject(props: { [k: string]: any }, additional: any, val: any): any {
if (val === null || typeof val !== 'object' || Array.isArray(val)) {
return invalidValue('object', val);
}
const result: any = {};
Object.getOwnPropertyNames(props).forEach((key) => {
const prop = props[key];
const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined;
result[prop.key] = transform(v, prop.typ, getProps, prop.key);
});
Object.getOwnPropertyNames(val).forEach((key) => {
if (!Object.prototype.hasOwnProperty.call(props, key)) {
result[key] = transform(val[key], additional, getProps, key);
}
});
return result;
}
if (typ === 'any') return val;
if (typ === null) {
if (val === null) return val;
return invalidValue(typ, val);
}
if (typ === false) return invalidValue(typ, val);
while (typeof typ === 'object' && typ.ref !== undefined) {
typ = typeMap[typ.ref];
}
if (Array.isArray(typ)) return transformEnum(typ, val);
if (typeof typ === 'object') {
return typ.hasOwnProperty('unionMembers')
? transformUnion(typ.unionMembers, val)
: typ.hasOwnProperty('arrayItems')
? transformArray(typ.arrayItems, val)
: typ.hasOwnProperty('props')
? transformObject(getProps(typ), typ.additional, val)
: invalidValue(typ, val);
}
// Numbers can be parsed by Date but shouldn't be.
if (typ === Date && typeof val !== 'number') return transformDate(val);
return transformPrimitive(typ, val);
}
function cast<T>(val: any, typ: any): T {
return transform(val, typ, jsonToJSProps);
}
function uncast<T>(val: T, typ: any): any {
return transform(val, typ, jsToJSONProps);
}
function a(typ: any) {
return { arrayItems: typ };
}
function u(...typs: any[]) {
return { unionMembers: typs };
}
function o(props: any[], additional: any) {
return { props, additional };
}
function m(additional: any) {
return { props: [], additional };
}
function r(name: string) {
return { ref: name };
}
const typeMap: any = {
WeatherResponse: o(
[
{ json: 'current_weather', js: 'current_weather', typ: r('CurrentWeather') },
{ json: 'utc_offset_seconds', js: 'utc_offset_seconds', typ: 0 },
{ json: 'latitude', js: 'latitude', typ: 3.14 },
{ json: 'elevation', js: 'elevation', typ: 3.14 },
{ json: 'longitude', js: 'longitude', typ: 3.14 },
{ json: 'generationtime_ms', js: 'generationtime_ms', typ: 3.14 },
{ json: 'daily_units', js: 'daily_units', typ: r('DailyUnits') },
{ json: 'daily', js: 'daily', typ: r('Daily') },
],
false
),
CurrentWeather: o(
[
{ json: 'winddirection', js: 'winddirection', typ: 0 },
{ json: 'windspeed', js: 'windspeed', typ: 3.14 },
{ json: 'time', js: 'time', typ: '' },
{ json: 'weathercode', js: 'weathercode', typ: 0 },
{ json: 'temperature', js: 'temperature', typ: 3.14 },
],
false
),
Daily: o(
[
{ json: 'temperature_2m_max', js: 'temperature_2m_max', typ: a(3.14) },
{ json: 'time', js: 'time', typ: a(Date) },
{ json: 'temperature_2m_min', js: 'temperature_2m_min', typ: a(3.14) },
{ json: 'weathercode', js: 'weathercode', typ: a(0) },
],
false
),
DailyUnits: o(
[
{ json: 'temperature_2m_max', js: 'temperature_2m_max', typ: '' },
{ json: 'temperature_2m_min', js: 'temperature_2m_min', typ: '' },
{ json: 'time', js: 'time', typ: '' },
{ json: 'weathercode', js: 'weathercode', typ: '' },
],
false
),
};

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

@@ -1,8 +1,19 @@
import { Group, Text, Title } from '@mantine/core'; import { Group, Text, Title, Tooltip } from '@mantine/core';
import axios from 'axios';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Clock, Cloud } from 'tabler-icons-react'; import {
Cloud,
CloudFog,
CloudRain,
CloudSnow,
CloudStorm,
QuestionMark,
Snowflake,
Sun,
} from 'tabler-icons-react';
import { IModule } from '../modules'; import { IModule } from '../modules';
import { WeatherResponse, Convert } from './WeatherInterface';
export const WeatherModule: IModule = { export const WeatherModule: IModule = {
title: 'Weather', title: 'Weather',
@@ -11,29 +22,129 @@ export const WeatherModule: IModule = {
component: WeatherComponent, 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, 86 Snow 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}</Tooltip>;
}
export default function WeatherComponent(props: any) { export default function WeatherComponent(props: any) {
const [date, setDate] = useState(new Date()); // Get location from browser
const hours = date.getHours(); const [location, setLocation] = useState({ lat: 0, lng: 0 });
const minutes = date.getMinutes(); 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 });
});
}
// Change date on minute change
// Note: Using 10 000ms instead of 1000ms to chill a little :)
useEffect(() => { useEffect(() => {
setInterval(() => { axios
setDate(new Date()); .get(
}, 10000); `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;
}
console.log(weather.current_weather);
return ( return (
<Group p="sm" direction="column"> <Group p="sm" direction="column">
<Title> <Title>{weather.current_weather.temperature}°C</Title>
{hours < 10 ? `0${hours}` : hours}:{minutes < 10 ? `0${minutes}` : minutes} <WeatherIcon code={weather.current_weather.weathercode} />
</Title>
<Text size="xl"> <Text size="xl">
{ {
// Use dayjs to format the date // Use dayjs to format the date
// https://day.js.org/en/getting-started/installation/ // https://day.js.org/en/getting-started/installation/
dayjs(date).format('dddd, MMMM D')
} }
</Text> </Text>
</Group> </Group>

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
]
}
}