From 49d57024b944cce2cc52d9703071573d0e091685 Mon Sep 17 00:00:00 2001 From: "Thomas \"ajnart\" Camlong" Date: Sun, 15 May 2022 18:52:29 +0200 Subject: [PATCH] Advancement on the weather widget --- .../modules/weather/WeatherInterface.ts | 236 ++++++++++++++++++ .../modules/weather/WeatherModule.story.tsx | 22 ++ .../modules/weather/WeatherModule.tsx | 141 +++++++++-- src/components/modules/weather/mockdata.json | 58 +++++ 4 files changed, 442 insertions(+), 15 deletions(-) create mode 100644 src/components/modules/weather/WeatherInterface.ts create mode 100644 src/components/modules/weather/WeatherModule.story.tsx create mode 100644 src/components/modules/weather/mockdata.json diff --git a/src/components/modules/weather/WeatherInterface.ts b/src/components/modules/weather/WeatherInterface.ts new file mode 100644 index 000000000..d6b728a75 --- /dev/null +++ b/src/components/modules/weather/WeatherInterface.ts @@ -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(val: any, typ: any): T { + return transform(val, typ, jsonToJSProps); +} + +function uncast(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 + ), +}; diff --git a/src/components/modules/weather/WeatherModule.story.tsx b/src/components/modules/weather/WeatherModule.story.tsx new file mode 100644 index 000000000..2681443ec --- /dev/null +++ b/src/components/modules/weather/WeatherModule.story.tsx @@ -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) => ; +Default.parameters = { + mockData: [ + { + url: 'https://api.open-meteo.com/v1/forecast', + method: 'GET', + status: 200, + response: { + data: mockdata, + }, + }, + ], +}; diff --git a/src/components/modules/weather/WeatherModule.tsx b/src/components/modules/weather/WeatherModule.tsx index bb157fda9..536f33387 100644 --- a/src/components/modules/weather/WeatherModule.tsx +++ b/src/components/modules/weather/WeatherModule.tsx @@ -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 { 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 { WeatherResponse, Convert } from './WeatherInterface'; export const WeatherModule: IModule = { title: 'Weather', @@ -11,29 +22,129 @@ export const WeatherModule: IModule = { 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: , name: 'Clear' }; + break; + } + case 1: + case 2: + case 3: { + data = { icon: , name: 'Mainly clear' }; + break; + } + case 45: + case 48: { + data = { icon: , name: 'Fog' }; + break; + } + case 51: + case 53: + case 55: { + data = { icon: , name: 'Drizzle' }; + break; + } + case 56: + case 57: { + data = { icon: , name: 'Freezing drizzle' }; + break; + } + case 61: + case 63: + case 65: { + data = { icon: , name: 'Rain' }; + break; + } + case 66: + case 67: { + data = { icon: , name: 'Freezing rain' }; + break; + } + case 71: + case 73: + case 75: { + data = { icon: , name: 'Snow fall' }; + break; + } + case 77: { + data = { icon: , name: 'Snow grains' }; + break; + } + case 80: + case 81: + case 82: { + data = { icon: , name: 'Rain showers' }; + + break; + } + case 85: + case 86: { + data = { icon: , name: 'Snow showers' }; + break; + } + case 95: { + data = { icon: , name: 'Thunderstorm' }; + break; + } + case 96: + case 99: { + data = { icon: , name: 'Thunderstorm with hail' }; + break; + } + default: { + data = { icon: , name: 'Unknown' }; + } + } + return {data.icon}; +} + export default function WeatherComponent(props: any) { - const [date, setDate] = useState(new Date()); - const hours = date.getHours(); - const minutes = date.getMinutes(); + // 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 }); + }); + } - // Change date on minute change - // Note: Using 10 000ms instead of 1000ms to chill a little :) useEffect(() => { - setInterval(() => { - setDate(new Date()); - }, 10000); + 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; + } + console.log(weather.current_weather); return ( - - {hours < 10 ? `0${hours}` : hours}:{minutes < 10 ? `0${minutes}` : minutes} - + {weather.current_weather.temperature}°C + { // Use dayjs to format the date // https://day.js.org/en/getting-started/installation/ - dayjs(date).format('dddd, MMMM D') } diff --git a/src/components/modules/weather/mockdata.json b/src/components/modules/weather/mockdata.json new file mode 100644 index 000000000..b9e3795b1 --- /dev/null +++ b/src/components/modules/weather/mockdata.json @@ -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 + ] + } +} \ No newline at end of file