Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
settings.json
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2020 Eric Lewin
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"presets": ["@babel/preset-env", "@babel/preset-react"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"parser": "babel-eslint",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 9,
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaFeatures": {
|
||||||
|
"jsx": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"globals": {
|
||||||
|
"Atomics": "readonly",
|
||||||
|
"SharedArrayBuffer": "readonly"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"react/jsx-uses-react": 1,
|
||||||
|
"babel/semi": 1,
|
||||||
|
"babel/camelcase": 1,
|
||||||
|
"no-useless-return": 2,
|
||||||
|
"no-self-compare": 2,
|
||||||
|
"no-implicit-globals" : 2,
|
||||||
|
"no-eval": 2,
|
||||||
|
"no-alert": 2,
|
||||||
|
"no-multiple-empty-lines" : 1,
|
||||||
|
"no-empty-function": 2,
|
||||||
|
"guard-for-in": 2,
|
||||||
|
"babel/no-unused-expressions": 2,
|
||||||
|
"jsx-quotes" : ["warn", "prefer-double"],
|
||||||
|
"comma-spacing" : 1,
|
||||||
|
"arrow-spacing": 1,
|
||||||
|
"prefer-destructuring" : 1,
|
||||||
|
"multiline-comment-style" : 0,
|
||||||
|
"react-hooks/rules-of-hooks": "error",
|
||||||
|
"react-hooks/exhaustive-deps": "warn",
|
||||||
|
"react/display-name": 0,
|
||||||
|
"eslint-comments/no-unused-disable": 1,
|
||||||
|
"eslint-comments/disable-enable-pair": 0,
|
||||||
|
"react/no-unused-prop-types": 2,
|
||||||
|
"jsdoc/require-jsdoc": [
|
||||||
|
"error", {
|
||||||
|
"publicOnly": true,
|
||||||
|
"require": {
|
||||||
|
"ClassDeclaration": true,
|
||||||
|
"ArrowFunctionExpression": true,
|
||||||
|
"MethodDefinition": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"jsdoc/require-param-description": 0,
|
||||||
|
"jsdoc/no-undefined-types": 0,
|
||||||
|
"jsdoc/check-types" : ["warn", {"noDefaults": true}]
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"es6": true,
|
||||||
|
"browser": true,
|
||||||
|
"jest": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:react/recommended",
|
||||||
|
"plugin:eslint-comments/recommended",
|
||||||
|
"plugin:jsdoc/recommended"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"babel",
|
||||||
|
"react-hooks",
|
||||||
|
"react",
|
||||||
|
"jsdoc"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"react":{
|
||||||
|
"version": "detect"
|
||||||
|
},
|
||||||
|
"jsdoc": {
|
||||||
|
"tagNamePreference": {
|
||||||
|
"property": "prop",
|
||||||
|
"augments": "extends"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
(window.webpackJsonp=window.webpackJsonp||[]).push([[1],{373:function(n,i,o){var e=o(7),r=o(376);"string"==typeof(r=r.__esModule?r.default:r)&&(r=[[n.i,r,""]]);var s={insert:"head",singleton:!1};e(r,s);n.exports=r.locals||{}},376:function(n,i,o){"use strict";o.r(i);var e=o(3),r=o.n(e)()(!1);r.push([n.i,"body {\r\n font-family: Rubik; \r\n margin: 0px; \r\n }",""]),i.default=r}}]);
|
||||||
Vendored
+318
File diff suppressed because one or more lines are too long
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
<!doctype html><html lang="en"><head><link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ==" crossorigin=""/><script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js" integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew==" crossorigin=""></script><link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300&display=swap" rel="stylesheet"><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>Pi Weather Station</title></head><body><div id="root"></div><script src="/bundle.min.js"></script></body></html>
|
||||||
Generated
+7073
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "pi-weather-station-client",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Client for pi-weather-station",
|
||||||
|
"main": "webpack.config.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"dev": "webpack --progress --watch --mode development",
|
||||||
|
"prod": "webpack -p --env.BUILD_PRODUCTION=1"
|
||||||
|
},
|
||||||
|
"author": "Eric Lewin",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "^7.11.1",
|
||||||
|
"@babel/preset-env": "^7.11.0",
|
||||||
|
"@babel/preset-react": "^7.10.4",
|
||||||
|
"@iconify/icons-bx": "^1.0.3",
|
||||||
|
"@iconify/icons-carbon": "^1.0.4",
|
||||||
|
"@iconify/icons-dashicons": "^1.0.11",
|
||||||
|
"@iconify/icons-gridicons": "^1.0.6",
|
||||||
|
"@iconify/icons-ic": "^1.0.15",
|
||||||
|
"@iconify/icons-ion": "^1.0.13",
|
||||||
|
"@iconify/icons-map": "^1.0.5",
|
||||||
|
"@iconify/icons-wi": "^1.0.3",
|
||||||
|
"@iconify/react": "^1.1.3",
|
||||||
|
"autoprefixer": "^9.8.6",
|
||||||
|
"axios": "^0.19.2",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"babel-loader": "^8.1.0",
|
||||||
|
"chart.js": "^2.9.3",
|
||||||
|
"css-loader": "^4.2.1",
|
||||||
|
"date-fns": "^2.15.0",
|
||||||
|
"debounce": "^1.2.0",
|
||||||
|
"eslint": "^7.6.0",
|
||||||
|
"eslint-loader": "^4.0.2",
|
||||||
|
"eslint-plugin-babel": "^5.3.1",
|
||||||
|
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||||
|
"eslint-plugin-jsdoc": "^30.2.1",
|
||||||
|
"eslint-plugin-react": "^7.20.6",
|
||||||
|
"eslint-plugin-react-hooks": "^4.0.8",
|
||||||
|
"file-loader": "^6.0.0",
|
||||||
|
"html-loader": "^1.1.0",
|
||||||
|
"html-webpack-plugin": "^4.3.0",
|
||||||
|
"leaflet": "^1.6.0",
|
||||||
|
"postcss": "^7.0.32",
|
||||||
|
"postcss-loader": "^3.0.0",
|
||||||
|
"postcss-preset-env": "^6.7.0",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react": "^16.13.1",
|
||||||
|
"react-chartjs-2": "^2.10.0",
|
||||||
|
"react-dom": "^16.13.1",
|
||||||
|
"react-leaflet": "^2.7.0",
|
||||||
|
"react-transition-group": "^4.4.1",
|
||||||
|
"style-loader": "^1.2.1",
|
||||||
|
"url-loader": "^4.1.0",
|
||||||
|
"webpack": "^4.44.1",
|
||||||
|
"webpack-cli": "^3.3.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const postcssPresetEnv = require("postcss-preset-env");
|
||||||
|
const autoprefixer = require("autoprefixer");
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
plugins: [
|
||||||
|
autoprefixer,
|
||||||
|
postcssPresetEnv()
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
@@ -0,0 +1,587 @@
|
|||||||
|
import React, { createContext, useState } from "react";
|
||||||
|
import { getSettings } from "~/settings";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { getCoordsFromApi } from "~/services/geolocation";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
export const AppContext = createContext();
|
||||||
|
|
||||||
|
const TEMP_UNIT_STORAGE_KEY = "tempUnit";
|
||||||
|
const SPEED_UNIT_STORAGE_KEY = "speedUnit";
|
||||||
|
const LENGTH_UNIT_STORAGE_KEY = "lengthUnit";
|
||||||
|
const CLOCK_UNIT_STORAGE_KEY = "clockTime";
|
||||||
|
const MOUSE_HIDE_STORAGE_KEY = "mouseHide";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App context provider
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Node} props.children
|
||||||
|
* @returns {JSX.Element} Context provider
|
||||||
|
*/
|
||||||
|
export function AppContextProvider({ children }) {
|
||||||
|
const [weatherApiKey, setWeatherApiKey] = useState(null);
|
||||||
|
const [mapApiKey, setMapApiKey] = useState(null);
|
||||||
|
const [reverseGeoApiKey, setReverseGeoApiKey] = useState(null);
|
||||||
|
const [browserGeo, setBrowserGeo] = useState(null);
|
||||||
|
const [mapGeo, setMapGeo] = useState(null);
|
||||||
|
const [darkMode, setDarkMode] = useState(true);
|
||||||
|
const [currentWeatherData, setCurrentWeatherData] = useState(null);
|
||||||
|
const [currentWeatherDataErr, setCurrentWeatherDataErr] = useState(null);
|
||||||
|
const [currentWeatherDataErrMsg, setCurrentWeatherDataErrMsg] = useState(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [hourlyWeatherData, setHourlyWeatherData] = useState(null);
|
||||||
|
const [hourlyWeatherDataErr, setHourlyWeatherDataErr] = useState(null);
|
||||||
|
const [hourlyWeatherDataErrMsg, setHourlyWeatherDataErrMsg] = useState(null);
|
||||||
|
const [dailyWeatherData, setDailyWeatherData] = useState(null);
|
||||||
|
const [dailyWeatherDataErr, setDailyWeatherDataErr] = useState(null);
|
||||||
|
const [dailyWeatherDataErrMsg, setDailyWeatherDataErrMsg] = useState(null);
|
||||||
|
const [panToCoords, setPanToCoords] = useState(null);
|
||||||
|
const [markerIsVisible, setMarkerIsVisible] = useState(true);
|
||||||
|
const [tempUnit, setTempUnit] = useState("f"); // fahrenheit or celsius
|
||||||
|
const [speedUnit, setSpeedUnit] = useState("mph"); // mph or ms for m/s
|
||||||
|
const [lengthUnit, setLengthUnit] = useState("in"); // in or mm
|
||||||
|
const [clockTime, setClockTime] = useState("12"); // 12h or 24h time for clock
|
||||||
|
const [animateWeatherMap, setAnimateWeatherMap] = useState(false);
|
||||||
|
const [settingsMenuOpen, setSettingsMenuOpen] = useState(false);
|
||||||
|
const [customLat, setCustomLat] = useState(null);
|
||||||
|
const [customLon, setCustomLon] = useState(null);
|
||||||
|
const [mouseHide, setMouseHide] = useState(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save mouse hide state
|
||||||
|
*
|
||||||
|
* @param {Boolean} newVal
|
||||||
|
*/
|
||||||
|
function saveMouseHide(newVal) {
|
||||||
|
let newState;
|
||||||
|
try {
|
||||||
|
newState = JSON.parse(newVal);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("saveMouseHide", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMouseHide(newState);
|
||||||
|
window.localStorage.setItem(MOUSE_HIDE_STORAGE_KEY, newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save clock time
|
||||||
|
*
|
||||||
|
* @param {String} newVal `12` or `24`
|
||||||
|
*/
|
||||||
|
function saveClockTime(newVal) {
|
||||||
|
setClockTime(newVal);
|
||||||
|
window.localStorage.setItem(CLOCK_UNIT_STORAGE_KEY, newVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save temp unit
|
||||||
|
*
|
||||||
|
* @param {String} newVal `f` or `c`
|
||||||
|
*/
|
||||||
|
function saveTempUnit(newVal) {
|
||||||
|
setTempUnit(newVal);
|
||||||
|
window.localStorage.setItem(TEMP_UNIT_STORAGE_KEY, newVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save speed unit
|
||||||
|
*
|
||||||
|
* @param {String} newVal `mph` or `ms`
|
||||||
|
*/
|
||||||
|
function saveSpeedUnit(newVal) {
|
||||||
|
setSpeedUnit(newVal);
|
||||||
|
window.localStorage.setItem(SPEED_UNIT_STORAGE_KEY, newVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save length unit
|
||||||
|
*
|
||||||
|
* @param {String} newVal `in` or `mm`
|
||||||
|
*/
|
||||||
|
function saveLengthUnit(newVal) {
|
||||||
|
setLengthUnit(newVal);
|
||||||
|
window.localStorage.setItem(LENGTH_UNIT_STORAGE_KEY, newVal);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadStoredData() {
|
||||||
|
const temp = window.localStorage.getItem(TEMP_UNIT_STORAGE_KEY);
|
||||||
|
const speed = window.localStorage.getItem(SPEED_UNIT_STORAGE_KEY);
|
||||||
|
const length = window.localStorage.getItem(LENGTH_UNIT_STORAGE_KEY);
|
||||||
|
const clock = window.localStorage.getItem(CLOCK_UNIT_STORAGE_KEY);
|
||||||
|
|
||||||
|
let mouseHide;
|
||||||
|
try {
|
||||||
|
mouseHide = JSON.parse(
|
||||||
|
window.localStorage.getItem(MOUSE_HIDE_STORAGE_KEY)
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("mouseHide", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMouseHide(!!mouseHide);
|
||||||
|
if (temp) {
|
||||||
|
setTempUnit(temp);
|
||||||
|
}
|
||||||
|
if (speed) {
|
||||||
|
setSpeedUnit(speed);
|
||||||
|
}
|
||||||
|
if (length) {
|
||||||
|
setLengthUnit(length);
|
||||||
|
}
|
||||||
|
if (clock) {
|
||||||
|
setClockTime(clock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set custom starting lat/lon
|
||||||
|
*
|
||||||
|
* @returns {Promise} lat/lon
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function getCustomLatLon() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getSettings()
|
||||||
|
.then((res) => {
|
||||||
|
if (res) {
|
||||||
|
const { startingLat, startingLon } = res;
|
||||||
|
if (startingLat) {
|
||||||
|
setCustomLat(startingLat);
|
||||||
|
}
|
||||||
|
if (startingLon) {
|
||||||
|
setCustomLon(startingLon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(res);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("could not read settings.json", err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the map to a given position
|
||||||
|
*
|
||||||
|
* @param {Object} coords coordinates
|
||||||
|
* @param {String} coords.latitude
|
||||||
|
* @param {String} coords.longitude
|
||||||
|
*/
|
||||||
|
function setMapPosition(coords) {
|
||||||
|
updateCurrentWeatherData(coords);
|
||||||
|
updateHourlyWeatherData(coords);
|
||||||
|
updateDailyWeatherData(coords);
|
||||||
|
setMapGeo(coords);
|
||||||
|
setPanToCoords(coords);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the map position to browser geolocation coordinates
|
||||||
|
*/
|
||||||
|
function resetMapPosition() {
|
||||||
|
setMapPosition(browserGeo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets geolocation and sets it, unless custom starting coordinates are provided.
|
||||||
|
*
|
||||||
|
* @returns {Object} coords
|
||||||
|
*/
|
||||||
|
function getBrowserGeo() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getCustomLatLon()
|
||||||
|
.then((res) => {
|
||||||
|
const { startingLat, startingLon } = res;
|
||||||
|
if (startingLat && startingLon) {
|
||||||
|
const latLon = {
|
||||||
|
latitude: parseFloat(startingLat),
|
||||||
|
longitude: parseFloat(startingLon),
|
||||||
|
};
|
||||||
|
setBrowserGeo(latLon);
|
||||||
|
setMapGeo(latLon); //Set initial map coords to custom lat/lon
|
||||||
|
resolve(latLon);
|
||||||
|
} else {
|
||||||
|
getCoordsFromApi()
|
||||||
|
.then((res) => {
|
||||||
|
if (!res) {
|
||||||
|
return reject("Could not get browser geolocation data");
|
||||||
|
}
|
||||||
|
const { latitude, longitude } = res;
|
||||||
|
setBrowserGeo({ latitude, longitude });
|
||||||
|
setMapGeo({ latitude, longitude }); //Set initial map coords to browser geolocation
|
||||||
|
resolve(res);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("err!", err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves weather API key and sets it
|
||||||
|
*
|
||||||
|
* @returns {Promise} Weather API Key
|
||||||
|
*/
|
||||||
|
function getWeatherApiKey() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getSettings()
|
||||||
|
.then((res) => {
|
||||||
|
if (!res || (res && !res.weatherApiKey)) {
|
||||||
|
setSettingsMenuOpen(true);
|
||||||
|
return reject("Weather API key missing");
|
||||||
|
}
|
||||||
|
setWeatherApiKey(res && res.weatherApiKey ? res.weatherApiKey : null);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves map API key and sets it
|
||||||
|
*
|
||||||
|
* @returns {Promise} Weather API Key
|
||||||
|
*/
|
||||||
|
function getMapApiKey() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getSettings()
|
||||||
|
.then((res) => {
|
||||||
|
if (!res || (res && !res.mapApiKey)) {
|
||||||
|
setSettingsMenuOpen(true);
|
||||||
|
return reject("Map API key missing!");
|
||||||
|
}
|
||||||
|
setMapApiKey(res && res.mapApiKey ? res.mapApiKey : null);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves reverse geolocation API key and sets it
|
||||||
|
*
|
||||||
|
* @returns {Promise} Weather API Key
|
||||||
|
*/
|
||||||
|
function getReverseGeoApiKey() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
getSettings()
|
||||||
|
.then((res) => {
|
||||||
|
if (!res || (res && !res.reverseGeoApiKey)) {
|
||||||
|
return reject("Reverse geolocation API key missing!");
|
||||||
|
}
|
||||||
|
setReverseGeoApiKey(
|
||||||
|
res && res.reverseGeoApiKey ? res.reverseGeoApiKey : null
|
||||||
|
);
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates hourly weather data
|
||||||
|
*
|
||||||
|
* @param {Object} coords
|
||||||
|
* @param {Number} coords.latitude latitude
|
||||||
|
* @param {Number} coords.longitude longitude
|
||||||
|
*
|
||||||
|
* @returns {Promise} hourly weather data
|
||||||
|
*/
|
||||||
|
function updateHourlyWeatherData(coords) {
|
||||||
|
setHourlyWeatherDataErr(null);
|
||||||
|
setHourlyWeatherDataErrMsg(null);
|
||||||
|
const { latitude, longitude } = coords;
|
||||||
|
const fields = [
|
||||||
|
"temp",
|
||||||
|
"precipitation_probability",
|
||||||
|
"precipitation",
|
||||||
|
"wind_speed",
|
||||||
|
].join("%2c");
|
||||||
|
|
||||||
|
const endTime = new Date(
|
||||||
|
new Date().getTime() + 60 * 60 * 23 * 1000
|
||||||
|
).toISOString();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!coords) {
|
||||||
|
setHourlyWeatherDataErr(true);
|
||||||
|
return reject("No coords");
|
||||||
|
}
|
||||||
|
if (!weatherApiKey) {
|
||||||
|
setHourlyWeatherDataErr(true);
|
||||||
|
setSettingsMenuOpen(true);
|
||||||
|
return reject("Missing weather API key");
|
||||||
|
}
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(
|
||||||
|
`https://api.climacell.co/v3/weather/forecast/hourly?unit_system=si&fields=${fields}&apikey=${weatherApiKey}&lat=${latitude}&lon=${longitude}&end_time=${endTime}`
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
if (!res) {
|
||||||
|
return reject({ message: "No response" });
|
||||||
|
}
|
||||||
|
const { data } = res;
|
||||||
|
setHourlyWeatherData(data);
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setHourlyWeatherDataErr(true);
|
||||||
|
if (err && err.message) {
|
||||||
|
setHourlyWeatherDataErrMsg(err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates daily weather data
|
||||||
|
*
|
||||||
|
* @param {Object} coords
|
||||||
|
* @param {Number} coords.latitude latitude
|
||||||
|
* @param {Number} coords.longitude longitude
|
||||||
|
*
|
||||||
|
* @returns {Promise} daily weather data
|
||||||
|
*/
|
||||||
|
function updateDailyWeatherData(coords) {
|
||||||
|
setDailyWeatherDataErr(null);
|
||||||
|
setDailyWeatherDataErrMsg(null);
|
||||||
|
const { latitude, longitude } = coords;
|
||||||
|
const fields = [
|
||||||
|
"temp",
|
||||||
|
"precipitation_probability",
|
||||||
|
"precipitation",
|
||||||
|
"wind_speed",
|
||||||
|
].join("%2c");
|
||||||
|
|
||||||
|
const endTime = new Date(
|
||||||
|
new Date().getTime() + 6 * 60 * 60 * 24 * 1000
|
||||||
|
).toISOString();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!coords) {
|
||||||
|
setDailyWeatherDataErr(true);
|
||||||
|
return reject("No coords");
|
||||||
|
}
|
||||||
|
if (!weatherApiKey) {
|
||||||
|
setDailyWeatherDataErr(true);
|
||||||
|
setSettingsMenuOpen(true);
|
||||||
|
return reject("Missing weather API key");
|
||||||
|
}
|
||||||
|
axios
|
||||||
|
.get(
|
||||||
|
`https://api.climacell.co/v3/weather/forecast/daily?unit_system=si&fields=${fields}&apikey=${weatherApiKey}&lat=${latitude}&lon=${longitude}&end_time=${endTime}`
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
if (!res) {
|
||||||
|
return reject({ message: "No response" });
|
||||||
|
}
|
||||||
|
const { data } = res;
|
||||||
|
setDailyWeatherData(data);
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setDailyWeatherDataErr(true);
|
||||||
|
if (err && err.message) {
|
||||||
|
setDailyWeatherDataErrMsg(err.message);
|
||||||
|
}
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates current weather data
|
||||||
|
*
|
||||||
|
* @param {Object} coords
|
||||||
|
* @param {Number} coords.latitude latitude
|
||||||
|
* @param {Number} coords.longitude longitude
|
||||||
|
*
|
||||||
|
* @returns {Promise} current weather data
|
||||||
|
*/
|
||||||
|
function updateCurrentWeatherData(coords) {
|
||||||
|
setCurrentWeatherDataErr(null);
|
||||||
|
setCurrentWeatherDataErrMsg(null);
|
||||||
|
const { latitude, longitude } = coords;
|
||||||
|
|
||||||
|
// https://developer.climacell.co/v3/reference#data-layers-weather
|
||||||
|
const fields = [
|
||||||
|
"temp",
|
||||||
|
"humidity",
|
||||||
|
"wind_speed",
|
||||||
|
"precipitation",
|
||||||
|
"precipitation_type",
|
||||||
|
"sunrise",
|
||||||
|
"sunset",
|
||||||
|
"cloud_cover",
|
||||||
|
"weather_code",
|
||||||
|
].join("%2c");
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!coords) {
|
||||||
|
setCurrentWeatherDataErr(true);
|
||||||
|
return reject("No coords");
|
||||||
|
}
|
||||||
|
if (!weatherApiKey) {
|
||||||
|
setCurrentWeatherDataErr(true);
|
||||||
|
setSettingsMenuOpen(true);
|
||||||
|
return reject("Missing weather API key");
|
||||||
|
}
|
||||||
|
axios
|
||||||
|
.get(
|
||||||
|
`https://api.climacell.co/v3/weather/realtime?unit_system=si&fields=${fields}&apikey=${weatherApiKey}&lat=${latitude}&lon=${longitude};`
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
if (!res) {
|
||||||
|
return reject({ message: "No response" });
|
||||||
|
}
|
||||||
|
const { data } = res;
|
||||||
|
setCurrentWeatherData(data);
|
||||||
|
resolve(data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setCurrentWeatherDataErr(true);
|
||||||
|
if (err && err.message) {
|
||||||
|
setCurrentWeatherDataErrMsg(err.message);
|
||||||
|
}
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the marker on and off
|
||||||
|
*/
|
||||||
|
function toggleMarker() {
|
||||||
|
setMarkerIsVisible(!markerIsVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles weather map animation on/off
|
||||||
|
*/
|
||||||
|
function toggleAnimateWeatherMap() {
|
||||||
|
setAnimateWeatherMap(!animateWeatherMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles settings menu open/closed
|
||||||
|
*/
|
||||||
|
function toggleSettingsMenuOpen() {
|
||||||
|
setSettingsMenuOpen(!settingsMenuOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves settings to `settings.json`
|
||||||
|
*
|
||||||
|
* @param {Object} settings
|
||||||
|
* @param {String} [settings.mapsKey]
|
||||||
|
* @param {String} [settings.weatherKey]
|
||||||
|
* @param {String} [settings.geoKey]
|
||||||
|
* @param {String} [settings.lat]
|
||||||
|
* @param {String} [settings.lon]
|
||||||
|
* @returns {Promise} Resolves when complete
|
||||||
|
*/
|
||||||
|
function saveSettingsToJson({ mapsKey, weatherKey, geoKey, lat, lon }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios
|
||||||
|
.put("/settings", {
|
||||||
|
weatherApiKey: weatherKey,
|
||||||
|
mapApiKey: mapsKey,
|
||||||
|
reverseGeoApiKey: geoKey,
|
||||||
|
startingLat: lat,
|
||||||
|
startingLon: lon,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
resolve(res);
|
||||||
|
setMapApiKey(mapsKey);
|
||||||
|
setWeatherApiKey(weatherKey);
|
||||||
|
setReverseGeoApiKey(geoKey);
|
||||||
|
setCustomLat(lat);
|
||||||
|
setCustomLon(lon);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultContext = {
|
||||||
|
weatherApiKey,
|
||||||
|
getWeatherApiKey,
|
||||||
|
reverseGeoApiKey,
|
||||||
|
getReverseGeoApiKey,
|
||||||
|
mapApiKey,
|
||||||
|
getMapApiKey,
|
||||||
|
browserGeo,
|
||||||
|
getBrowserGeo,
|
||||||
|
darkMode,
|
||||||
|
setDarkMode,
|
||||||
|
mapGeo,
|
||||||
|
setMapGeo,
|
||||||
|
setMapPosition,
|
||||||
|
resetMapPosition,
|
||||||
|
panToCoords,
|
||||||
|
setPanToCoords,
|
||||||
|
markerIsVisible,
|
||||||
|
toggleMarker,
|
||||||
|
tempUnit,
|
||||||
|
saveTempUnit,
|
||||||
|
speedUnit,
|
||||||
|
saveSpeedUnit,
|
||||||
|
lengthUnit,
|
||||||
|
saveLengthUnit,
|
||||||
|
animateWeatherMap,
|
||||||
|
toggleAnimateWeatherMap,
|
||||||
|
settingsMenuOpen,
|
||||||
|
setSettingsMenuOpen,
|
||||||
|
toggleSettingsMenuOpen,
|
||||||
|
getCustomLatLon,
|
||||||
|
customLat,
|
||||||
|
customLon,
|
||||||
|
loadStoredData,
|
||||||
|
clockTime,
|
||||||
|
saveClockTime,
|
||||||
|
saveSettingsToJson,
|
||||||
|
updateCurrentWeatherData,
|
||||||
|
updateDailyWeatherData,
|
||||||
|
updateHourlyWeatherData,
|
||||||
|
currentWeatherData,
|
||||||
|
currentWeatherDataErr,
|
||||||
|
currentWeatherDataErrMsg,
|
||||||
|
hourlyWeatherData,
|
||||||
|
hourlyWeatherDataErr,
|
||||||
|
hourlyWeatherDataErrMsg,
|
||||||
|
dailyWeatherData,
|
||||||
|
dailyWeatherDataErr,
|
||||||
|
dailyWeatherDataErrMsg,
|
||||||
|
mouseHide,
|
||||||
|
saveMouseHide,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppContext.Provider value={defaultContext}>{children}</AppContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppContextProvider.propTypes = {
|
||||||
|
children: PropTypes.oneOfType([
|
||||||
|
PropTypes.arrayOf(PropTypes.node),
|
||||||
|
PropTypes.node,
|
||||||
|
]).isRequired,
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import React, { useEffect, useContext } from "react";
|
||||||
|
import styles from "./styles.css";
|
||||||
|
import { AppContext } from "~/AppContext";
|
||||||
|
|
||||||
|
import WeatherMap from "~/components/WeatherMap";
|
||||||
|
import InfoPanel from "~/components/InfoPanel";
|
||||||
|
import Settings from "~/components/Settings";
|
||||||
|
|
||||||
|
import "!style-loader!css-loader!./overrides.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main component
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Main component
|
||||||
|
*/
|
||||||
|
const App = () => {
|
||||||
|
const {
|
||||||
|
getBrowserGeo,
|
||||||
|
getCustomLatLon,
|
||||||
|
loadStoredData,
|
||||||
|
darkMode,
|
||||||
|
mouseHide,
|
||||||
|
} = useContext(AppContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getCustomLatLon();
|
||||||
|
getBrowserGeo();
|
||||||
|
loadStoredData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${darkMode ? styles.dark : styles.light} ${
|
||||||
|
mouseHide ? styles.hideMouse : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.settingsContainer}>
|
||||||
|
<Settings />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={`${styles.weatherMap} map-container ${
|
||||||
|
mouseHide ? "map-mouse-hide" : ""
|
||||||
|
} ${darkMode ? "map-dark-mode" : ""}`}
|
||||||
|
>
|
||||||
|
<WeatherMap zoom={9} dark={darkMode} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.infoContainer}>
|
||||||
|
<InfoPanel />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.map-container.map-mouse-hide .leaflet-grab {
|
||||||
|
cursor: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-container.map-mouse-hide .leaflet-interactive {
|
||||||
|
cursor: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-container.map-dark-mode .leaflet-bar a {
|
||||||
|
background-color: #3a3938;
|
||||||
|
color: #f6f6f4;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
.container {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
min-width: 800px;
|
||||||
|
min-height: 480px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 300px;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-template-areas: "map info";
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .container {
|
||||||
|
background-color: #3a3938;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light .container {
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-map {
|
||||||
|
grid-area: map;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-container {
|
||||||
|
grid-area: info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .info-container {
|
||||||
|
border-left: 1px solid #484848;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light .info-container {
|
||||||
|
border-left: 1px solid #dadada;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 5000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide-mouse{
|
||||||
|
cursor: none !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React, { useEffect, useState, useContext } from "react";
|
||||||
|
import { AppContext } from "~/AppContext";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import styles from "./styles.css";
|
||||||
|
import SunRiseSet from "~/components/SunRiseSet";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays time and date
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Clock component
|
||||||
|
*/
|
||||||
|
const Clock = () => {
|
||||||
|
const { clockTime } = useContext(AppContext);
|
||||||
|
const [date, setDate] = useState(new Date().getTime());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const clockInterval = setInterval(() => {
|
||||||
|
setDate(new Date().getTime());
|
||||||
|
}, 1000);
|
||||||
|
return () => {
|
||||||
|
clearInterval(clockInterval);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.date}>
|
||||||
|
{format(date, "cccc").toUpperCase()}{" "}
|
||||||
|
{format(date, "LLLL").toUpperCase()} {format(date, "d")}
|
||||||
|
</div>
|
||||||
|
<div className={styles.time}>{format(date, clockTime === "12" ? "p" : "HH:mm")}</div>
|
||||||
|
<div className={styles.sunRiseSetContainer}>
|
||||||
|
<SunRiseSet/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Clock;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
.date{
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time{
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
font-size: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sun-rise-set-container{
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import React, { useContext } from "react";
|
||||||
|
import { AppContext } from "~/AppContext";
|
||||||
|
import styles from "./styles.css";
|
||||||
|
import { InlineIcon } from "@iconify/react";
|
||||||
|
import locationArrow from "@iconify/icons-map/location-arrow";
|
||||||
|
import contrastIcon from "@iconify/icons-carbon/contrast";
|
||||||
|
import sharpSettings from "@iconify/icons-ic/sharp-settings";
|
||||||
|
import roundLocationOn from "@iconify/icons-ic/round-location-on";
|
||||||
|
import roundLocationOff from "@iconify/icons-ic/round-location-off";
|
||||||
|
import playFilledAlt from "@iconify/icons-carbon/play-filled-alt";
|
||||||
|
import stopFilledAlt from "@iconify/icons-carbon/stop-filled-alt";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buttons group component
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Control buttons
|
||||||
|
*/
|
||||||
|
const ControlButtons = () => {
|
||||||
|
const {
|
||||||
|
darkMode,
|
||||||
|
setDarkMode,
|
||||||
|
resetMapPosition,
|
||||||
|
markerIsVisible,
|
||||||
|
toggleMarker,
|
||||||
|
toggleAnimateWeatherMap,
|
||||||
|
animateWeatherMap,
|
||||||
|
toggleSettingsMenuOpen,
|
||||||
|
settingsMenuOpen,
|
||||||
|
mouseHide,
|
||||||
|
} = useContext(AppContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.container} ${
|
||||||
|
darkMode ? styles.dark : styles.light
|
||||||
|
} ${!mouseHide ? styles.showMouse : ""}`}
|
||||||
|
>
|
||||||
|
<div onClick={resetMapPosition}>
|
||||||
|
<InlineIcon icon={locationArrow} />
|
||||||
|
</div>
|
||||||
|
<div onClick={toggleMarker}>
|
||||||
|
<InlineIcon
|
||||||
|
icon={markerIsVisible ? roundLocationOff : roundLocationOn}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={toggleAnimateWeatherMap}
|
||||||
|
className={`${animateWeatherMap ? styles.buttonDown : ""}`}
|
||||||
|
>
|
||||||
|
<InlineIcon icon={animateWeatherMap ? stopFilledAlt : playFilledAlt} />
|
||||||
|
</div>
|
||||||
|
<div onClick={() => setDarkMode(!darkMode)}>
|
||||||
|
<InlineIcon icon={contrastIcon} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={toggleSettingsMenuOpen}
|
||||||
|
className={`${settingsMenuOpen ? styles.buttonDown : ""}`}
|
||||||
|
>
|
||||||
|
<InlineIcon icon={sharpSettings} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ControlButtons;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > div {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-mouse.container > div{
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.dark > div {
|
||||||
|
background: linear-gradient(#5d5c5c, #4e4e4e);
|
||||||
|
border-right: 1px solid #4c4c4c;
|
||||||
|
border-left: 1px solid #757575;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.dark > div:active {
|
||||||
|
background: linear-gradient(#4e4e4e, #5d5c5c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.light > div {
|
||||||
|
background: linear-gradient(#d6d6d6, #c7c7c7);
|
||||||
|
border-right: 1px solid #bdbdbd;
|
||||||
|
border-left: 1px solid #dadada;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.light > div:active {
|
||||||
|
background: linear-gradient(#d7d7d7, #e6e6e6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.light > div:first-child {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light .button-down {
|
||||||
|
background: linear-gradient(#E4E4E4, #F3F3F3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .button-down {
|
||||||
|
background: linear-gradient(#686868, #777676) !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import React, { useContext } from "react";
|
||||||
|
import { AppContext } from "~/AppContext";
|
||||||
|
import styles from "./styles.css";
|
||||||
|
import {
|
||||||
|
convertTemp,
|
||||||
|
convertSpeed,
|
||||||
|
convertLength,
|
||||||
|
} from "~/services/conversions";
|
||||||
|
|
||||||
|
import { InlineIcon } from "@iconify/react";
|
||||||
|
import degreesIcon from "@iconify/icons-wi/degrees";
|
||||||
|
import nightClear from "@iconify/icons-wi/night-clear";
|
||||||
|
import daySunny from "@iconify/icons-wi/day-sunny";
|
||||||
|
import dayCloudy from "@iconify/icons-wi/day-cloudy";
|
||||||
|
import nightAltCloudy from "@iconify/icons-wi/night-alt-cloudy";
|
||||||
|
import dayRain from "@iconify/icons-wi/day-rain";
|
||||||
|
import nightRain from "@iconify/icons-wi/night-rain";
|
||||||
|
import humidityAlt from "@iconify/icons-carbon/humidity-alt";
|
||||||
|
import cloudIcon from "@iconify/icons-wi/cloud";
|
||||||
|
import strongWind from "@iconify/icons-wi/strong-wind";
|
||||||
|
import snowIcon from "@iconify/icons-ion/snow";
|
||||||
|
import rainIcon from "@iconify/icons-wi/rain";
|
||||||
|
import rainMix from "@iconify/icons-wi/rain-mix";
|
||||||
|
import thunderstormIcon from "@iconify/icons-wi/thunderstorm";
|
||||||
|
import fogIcon from "@iconify/icons-wi/fog";
|
||||||
|
import cloudyIcon from "@iconify/icons-wi/cloudy";
|
||||||
|
import daySunnyOvercast from "@iconify/icons-wi/day-sunny-overcast";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current weather conditions
|
||||||
|
* https://developer.climacell.co/v3/reference#data-layers-weather
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Current weather conditions component
|
||||||
|
*/
|
||||||
|
const CurrentWeather = () => {
|
||||||
|
const { currentWeatherData, tempUnit, speedUnit, lengthUnit } = useContext(
|
||||||
|
AppContext
|
||||||
|
);
|
||||||
|
if (currentWeatherData) {
|
||||||
|
const {
|
||||||
|
cloud_cover: { value: cloudCover },
|
||||||
|
humidity: { value: humidity },
|
||||||
|
precipitation: { value: precipitation },
|
||||||
|
precipitation_type: { value: precipitationType },
|
||||||
|
temp: { value: temp },
|
||||||
|
weather_code: { value: weatherCode },
|
||||||
|
wind_speed: { value: windSpeed },
|
||||||
|
sunrise: { value: sunrise },
|
||||||
|
sunset: { value: sunset },
|
||||||
|
} = currentWeatherData;
|
||||||
|
|
||||||
|
const { icon: weatherIcon, desc: weatherDesc } =
|
||||||
|
parseWeatherCode(weatherCode, isDaylight(sunrise, sunset)) || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.currentTemp}>
|
||||||
|
{convertTemp(temp, tempUnit)}
|
||||||
|
<InlineIcon icon={degreesIcon} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.iconContainer}>
|
||||||
|
<div className={styles.weatherIcon}>
|
||||||
|
{weatherIcon ? <InlineIcon icon={weatherIcon} /> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.stats}>
|
||||||
|
<div>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div>
|
||||||
|
<InlineIcon icon={cloudIcon} />
|
||||||
|
</div>
|
||||||
|
<div>{parseInt(cloudCover)}%</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div>
|
||||||
|
<InlineIcon icon={strongWind} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.textUnit}>
|
||||||
|
<div>{convertSpeed(windSpeed, speedUnit)}</div>
|
||||||
|
<div className={styles.statUnit}>
|
||||||
|
{speedUnit === "mph" ? " mph" : " m/s"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div>
|
||||||
|
<InlineIcon icon={humidityAlt} />
|
||||||
|
</div>
|
||||||
|
<div>{parseInt(humidity)}%</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div>
|
||||||
|
<InlineIcon
|
||||||
|
icon={precipitationType === "snow" ? snowIcon : rainIcon}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.textUnit}>
|
||||||
|
<div>{convertLength(precipitation, lengthUnit)}</div>
|
||||||
|
<div>{lengthUnit}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.description}>{weatherDesc || ""}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse weather code
|
||||||
|
*
|
||||||
|
* @param {String} code
|
||||||
|
* @param {Boolean} [isDay] if it is currently day
|
||||||
|
* @returns {Object} weather description and icon
|
||||||
|
*/
|
||||||
|
const parseWeatherCode = (code, isDay) => {
|
||||||
|
switch (code) {
|
||||||
|
case "freezing_rain_heavy":
|
||||||
|
return { desc: "Heavy freezing rain", icon: isDay ? dayRain : nightRain };
|
||||||
|
case "freezing_rain":
|
||||||
|
return { desc: "Freezing rain", icon: isDay ? dayRain : nightRain };
|
||||||
|
case "freezing_rain_light":
|
||||||
|
return { desc: "Light freezing rain", icon: isDay ? dayRain : nightRain };
|
||||||
|
case "freezing_drizzle":
|
||||||
|
return { desc: "Freezing drizzle", icon: rainMix };
|
||||||
|
case "ice_pellets_heavy":
|
||||||
|
return { desc: "Heavy ice pellets", icon: rainMix };
|
||||||
|
case "ice_pellets":
|
||||||
|
return { desc: "Ice pellets", icon: rainMix };
|
||||||
|
case "ice_pellets_light":
|
||||||
|
return { desc: "Light ice pellets", icon: rainMix };
|
||||||
|
case "snow_heavy":
|
||||||
|
return { desc: "Heavy snow", icon: snowIcon };
|
||||||
|
case "snow":
|
||||||
|
return { desc: "Show", icon: snowIcon };
|
||||||
|
case "snow_light":
|
||||||
|
return { desc: "Light snow", icon: snowIcon };
|
||||||
|
case "flurries":
|
||||||
|
return { desc: "Flurries", icon: snowIcon };
|
||||||
|
case "tstorm":
|
||||||
|
return { desc: "Thunder storm", icon: thunderstormIcon };
|
||||||
|
case "rain_heavy":
|
||||||
|
return { desc: "Heavy rain", icon: isDay ? dayRain : nightRain };
|
||||||
|
case "rain":
|
||||||
|
return { desc: "Rain", icon: isDay ? dayRain : nightRain };
|
||||||
|
case "rain_light":
|
||||||
|
return { desc: "Light rain", icon: isDay ? dayRain : nightRain };
|
||||||
|
case "drizzle":
|
||||||
|
return { desc: "Drizzle", icon: rainMix };
|
||||||
|
case "fog_light":
|
||||||
|
return { desc: "Light fog", icon: fogIcon };
|
||||||
|
case "fog":
|
||||||
|
return { desc: "Fog", icon: fogIcon };
|
||||||
|
case "cloudy":
|
||||||
|
return { desc: "Cloudy", icon: cloudyIcon };
|
||||||
|
case "mostly_cloudy":
|
||||||
|
return { desc: "Mostly cloudy", icon: cloudyIcon };
|
||||||
|
case "partly_cloudy":
|
||||||
|
return {
|
||||||
|
desc: "Partly cloudy",
|
||||||
|
icon: isDay ? daySunnyOvercast : nightAltCloudy,
|
||||||
|
};
|
||||||
|
case "mostly_clear":
|
||||||
|
return { desc: "Mostly clear", icon: isDay ? dayCloudy : nightAltCloudy };
|
||||||
|
case "clear":
|
||||||
|
return { desc: "Clear", icon: isDay ? daySunny : nightClear };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if it is currently daylight
|
||||||
|
*
|
||||||
|
* @param {Date} sunrise
|
||||||
|
* @param {Date} sunset
|
||||||
|
* @returns {Boolean} if current time is during daylight
|
||||||
|
*/
|
||||||
|
function isDaylight(sunrise, sunset) {
|
||||||
|
const sunriseTime = new Date(sunrise).getTime();
|
||||||
|
const sunsetTime = new Date(sunset).getTime();
|
||||||
|
const now = new Date().getTime();
|
||||||
|
return !!(now > sunriseTime && now < sunsetTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CurrentWeather;
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import { AppContext } from "~/AppContext";
|
||||||
|
import styles from "./styles.css";
|
||||||
|
import {
|
||||||
|
convertTemp,
|
||||||
|
convertSpeed,
|
||||||
|
convertLength,
|
||||||
|
} from "~/services/conversions";
|
||||||
|
import { capitalizeFirstLetter } from "~/services/formatting";
|
||||||
|
import { InlineIcon } from "@iconify/react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
import degreesIcon from "@iconify/icons-wi/degrees";
|
||||||
|
import nightClear from "@iconify/icons-wi/night-clear";
|
||||||
|
import daySunny from "@iconify/icons-wi/day-sunny";
|
||||||
|
import nightPartlyCloudy from "@iconify/icons-wi/night-partly-cloudy";
|
||||||
|
import nightAltPartlyCloudy from "@iconify/icons-wi/night-alt-partly-cloudy";
|
||||||
|
import dayCloudy from "@iconify/icons-wi/day-cloudy";
|
||||||
|
import nightAltCloudy from "@iconify/icons-wi/night-alt-cloudy";
|
||||||
|
import dayRain from "@iconify/icons-wi/day-rain";
|
||||||
|
import nightRain from "@iconify/icons-wi/night-rain";
|
||||||
|
import dayThunderstorm from "@iconify/icons-wi/day-thunderstorm";
|
||||||
|
import nightAltThunderstorm from "@iconify/icons-wi/night-alt-thunderstorm";
|
||||||
|
import daySnow from "@iconify/icons-wi/day-snow";
|
||||||
|
import nightAltSnow from "@iconify/icons-wi/night-alt-snow";
|
||||||
|
import dayFog from "@iconify/icons-wi/day-fog";
|
||||||
|
import nightFog from "@iconify/icons-wi/night-fog";
|
||||||
|
import humidityIcon from "@iconify/icons-carbon/humidity";
|
||||||
|
import cloudIcon from "@iconify/icons-wi/cloud";
|
||||||
|
import strongWind from "@iconify/icons-wi/strong-wind";
|
||||||
|
import snowIcon from "@iconify/icons-ion/snow";
|
||||||
|
import rainIcon from "@iconify/icons-wi/rain";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current weather conditions
|
||||||
|
* see https://openweathermap.org/api/one-call-api
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Current weather conditions component
|
||||||
|
*/
|
||||||
|
const CurrentWeather = () => {
|
||||||
|
const { weatherData, tempUnit, speedUnit, lengthUnit } = useContext(
|
||||||
|
AppContext
|
||||||
|
);
|
||||||
|
if (weatherData) {
|
||||||
|
const {
|
||||||
|
current: {
|
||||||
|
temp,
|
||||||
|
humidity,
|
||||||
|
clouds,
|
||||||
|
wind_speed, // eslint-disable-line babel/camelcase
|
||||||
|
rain,
|
||||||
|
snow,
|
||||||
|
weather: currentWeather,
|
||||||
|
},
|
||||||
|
} = weatherData;
|
||||||
|
const [{ icon: iconCode, description }] = currentWeather;
|
||||||
|
|
||||||
|
const rainFall = rain ? rain["1h"] : null;
|
||||||
|
const snowFall = snow ? snow["1h"] : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.currentTemp}>
|
||||||
|
{convertTemp(temp, tempUnit)}
|
||||||
|
<InlineIcon icon={degreesIcon} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.iconContainer}>
|
||||||
|
<div className={styles.weatherIcon}>
|
||||||
|
<InlineIcon icon={getWeatherIcon(iconCode)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.stats}>
|
||||||
|
<div>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div>
|
||||||
|
<InlineIcon icon={cloudIcon} />
|
||||||
|
</div>
|
||||||
|
<div>{humidity}%</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div>
|
||||||
|
<InlineIcon icon={strongWind} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.textUnit}>
|
||||||
|
<div>{convertSpeed(wind_speed, speedUnit)}</div>
|
||||||
|
<div className={styles.statUnit}>
|
||||||
|
{speedUnit === "mph" ? " mph" : " m/s"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div>
|
||||||
|
<InlineIcon icon={humidityIcon} />
|
||||||
|
</div>
|
||||||
|
<div>{clouds}%</div>
|
||||||
|
</div>
|
||||||
|
<Precipitation
|
||||||
|
snowFall={snowFall}
|
||||||
|
rainFall={rainFall}
|
||||||
|
lengthUnit={lengthUnit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.description}>
|
||||||
|
{capitalizeFirstLetter(description)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <div>no weather data / loading</div>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Precipitation, if any
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Number} props.rainFall
|
||||||
|
* @param {Number} props.snowFall
|
||||||
|
* @param {String} props.lengthUnit
|
||||||
|
* @returns {JSX.Element} precipitation component
|
||||||
|
*/
|
||||||
|
const Precipitation = ({ rainFall, snowFall, lengthUnit }) => {
|
||||||
|
if ((rainFall && rainFall !== 0) || (snowFall && snowFall !== 0)) {
|
||||||
|
return (
|
||||||
|
<div className={styles.statItem}>
|
||||||
|
<div>
|
||||||
|
<InlineIcon icon={rainFall ? rainIcon : snowIcon} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.textUnit}>
|
||||||
|
<div>{convertLength(rainFall ? rainFall : snowFall, lengthUnit)}</div>
|
||||||
|
<div>{lengthUnit}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Precipitation.propTypes = {
|
||||||
|
rainFall: PropTypes.number,
|
||||||
|
snowFall: PropTypes.number,
|
||||||
|
lengthUnit: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps weather codes to icons
|
||||||
|
* see https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
|
||||||
|
*
|
||||||
|
* @param {String} iconCode
|
||||||
|
* @returns {Object} Icon
|
||||||
|
*/
|
||||||
|
function getWeatherIcon(iconCode) {
|
||||||
|
switch (iconCode) {
|
||||||
|
case "01d":
|
||||||
|
return daySunny;
|
||||||
|
case "01n":
|
||||||
|
return nightClear;
|
||||||
|
case "02d":
|
||||||
|
return nightPartlyCloudy;
|
||||||
|
case "02n":
|
||||||
|
return nightAltPartlyCloudy;
|
||||||
|
case "03d":
|
||||||
|
return dayCloudy;
|
||||||
|
case "03n":
|
||||||
|
return nightAltCloudy;
|
||||||
|
case "04d":
|
||||||
|
return dayCloudy;
|
||||||
|
case "04n":
|
||||||
|
return nightAltCloudy;
|
||||||
|
case "09d":
|
||||||
|
return dayRain;
|
||||||
|
case "09n":
|
||||||
|
return nightRain;
|
||||||
|
case "10d":
|
||||||
|
return dayRain;
|
||||||
|
case "10n":
|
||||||
|
return nightRain;
|
||||||
|
case "11d":
|
||||||
|
return dayThunderstorm;
|
||||||
|
case "11n":
|
||||||
|
return nightAltThunderstorm;
|
||||||
|
case "13d":
|
||||||
|
return daySnow;
|
||||||
|
case "13n":
|
||||||
|
return nightAltSnow;
|
||||||
|
case "50d":
|
||||||
|
return dayFog;
|
||||||
|
case "50n":
|
||||||
|
return nightFog;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CurrentWeather;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"weather-icon current-temp stats"
|
||||||
|
"description description description"
|
||||||
|
}
|
||||||
|
|
||||||
|
.current-temp {
|
||||||
|
grid-area: current-temp;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
grid-area: weather-icon;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-icon {
|
||||||
|
font-size: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
grid-area: description;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
grid-area: stats;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item > div:first-child {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 5px;
|
||||||
|
width: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.uvi-label {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-unit {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-unit > div:last-child {
|
||||||
|
margin-left: 2px;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import React, { useContext } from "react";
|
||||||
|
import { AppContext } from "~/AppContext";
|
||||||
|
import Clock from "~/components/Clock";
|
||||||
|
import WeatherInfo from "~/components/WeatherInfo";
|
||||||
|
import ControlButtons from "~/components/ControlButtons";
|
||||||
|
import styles from "./styles.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Info Panel
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Info Panel
|
||||||
|
*/
|
||||||
|
const InfoPanel = () => {
|
||||||
|
const { darkMode } = useContext(AppContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${darkMode ? styles.dark : styles.light} ${styles.panel}`}>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.clockContainer}>
|
||||||
|
<Clock />
|
||||||
|
</div>
|
||||||
|
<div className={styles.weatherInfoContainer}>
|
||||||
|
<WeatherInfo />
|
||||||
|
</div>
|
||||||
|
<div className={styles.controls}>
|
||||||
|
<ControlButtons />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InfoPanel;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
.panel {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
height: calc(100% - 20px); /* subtract padding */
|
||||||
|
padding: 10px;
|
||||||
|
font-family: "Rubik", sans-serif;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto;
|
||||||
|
grid-template-rows: 90px auto 10px;
|
||||||
|
grid-template-areas:
|
||||||
|
"clock"
|
||||||
|
"weather-info"
|
||||||
|
"controls";
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .container {
|
||||||
|
color: #f6f6f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light .container {
|
||||||
|
color: #3a3938;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clock-container {
|
||||||
|
grid-area: clock;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-info-container {
|
||||||
|
grid-area: weather-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
grid-area: controls;
|
||||||
|
margin: -10px; /* subtract padding */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .controls{
|
||||||
|
border-top: 1px solid #757575;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light .controls{
|
||||||
|
border-top: 1px solid #f1f1f1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import React, { useEffect, useState, useContext } from "react";
|
||||||
|
import { AppContext } from "~/AppContext";
|
||||||
|
import { InlineIcon } from "@iconify/react";
|
||||||
|
import reverseGeocode from "~/services/reverseGeocode";
|
||||||
|
import locationIcon from "@iconify/icons-gridicons/location";
|
||||||
|
import styles from "./styles.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map location
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Location name
|
||||||
|
*/
|
||||||
|
const LocationName = () => {
|
||||||
|
const { currentWeatherData, reverseGeoApiKey } = useContext(AppContext);
|
||||||
|
const [name, setName] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentWeatherData && reverseGeoApiKey) {
|
||||||
|
const { lat, lon } = currentWeatherData;
|
||||||
|
reverseGeocode({ lat, lon, apiKey: reverseGeoApiKey })
|
||||||
|
.then((res) => {
|
||||||
|
setName(getName(res));
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setName(`${lat}, ${lon}`);
|
||||||
|
console.log("err!", err);
|
||||||
|
});
|
||||||
|
} else if (currentWeatherData && !reverseGeoApiKey) {
|
||||||
|
const { lat, lon } = currentWeatherData;
|
||||||
|
setName(`${lat}, ${lon}`);
|
||||||
|
}
|
||||||
|
}, [currentWeatherData, reverseGeoApiKey]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.container}`}>
|
||||||
|
{name ? (
|
||||||
|
<div>
|
||||||
|
<InlineIcon icon={locationIcon} /> {name}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses name data from results
|
||||||
|
*
|
||||||
|
* @param {Object} res
|
||||||
|
* @returns {String} Display name
|
||||||
|
*/
|
||||||
|
const getName = (res) => {
|
||||||
|
// eslint-disable-next-line babel/camelcase
|
||||||
|
const { city, country, state, country_code, county, region } = res.address;
|
||||||
|
// eslint-disable-next-line babel/camelcase
|
||||||
|
if (country_code === "us") {
|
||||||
|
if (city) {
|
||||||
|
return `${city}, ${state}`;
|
||||||
|
} else if (county) {
|
||||||
|
return `${county}, ${state}`;
|
||||||
|
} else if (state) {
|
||||||
|
return `${state}`;
|
||||||
|
} else {
|
||||||
|
return `${country}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (city) {
|
||||||
|
return `${city}, ${country}`;
|
||||||
|
} else {
|
||||||
|
return `${
|
||||||
|
county
|
||||||
|
? `${county}, `
|
||||||
|
: region
|
||||||
|
? `${region}, `
|
||||||
|
: state
|
||||||
|
? `${state}, `
|
||||||
|
: ""
|
||||||
|
}${country}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LocationName;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
.container {
|
||||||
|
max-height: 36px;
|
||||||
|
min-height: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.err {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(128, 128, 128, 0.6);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
.animate-enter {
|
||||||
|
-webkit-transform: translateX(-200%);
|
||||||
|
-moz-transform: translateX(-200%);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
.animate-enter-active {
|
||||||
|
-webkit-transform: translateX(0px);
|
||||||
|
-moz-transform: translateX(0px);
|
||||||
|
transform: translateX(0px);
|
||||||
|
-webkit-transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
.animate-exit {
|
||||||
|
-webkit-transform: translateX(0px);
|
||||||
|
-moz-transform: translateX(0px);
|
||||||
|
transform: translateX(0px);
|
||||||
|
-webkit-transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-exit-active {
|
||||||
|
-webkit-transform: translateX(-200%);
|
||||||
|
-moz-transform: translateX(-200%);
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
@@ -0,0 +1,425 @@
|
|||||||
|
import React, { useContext, useState, useEffect } from "react";
|
||||||
|
import styles from "./styles.css";
|
||||||
|
import { AppContext } from "~/AppContext";
|
||||||
|
import { CSSTransition } from "react-transition-group";
|
||||||
|
import { InlineIcon } from "@iconify/react";
|
||||||
|
import closeFilled from "@iconify/icons-carbon/close-filled";
|
||||||
|
import roundSaveAlt from "@iconify/icons-ic/round-save-alt";
|
||||||
|
import undoIcon from "@iconify/icons-dashicons/undo";
|
||||||
|
import closeSharp from "@iconify/icons-ion/close-sharp";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import "!style-loader!css-loader!./animations.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings page
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Settings page
|
||||||
|
*/
|
||||||
|
const Settings = () => {
|
||||||
|
const {
|
||||||
|
settingsMenuOpen,
|
||||||
|
weatherApiKey,
|
||||||
|
mapApiKey,
|
||||||
|
reverseGeoApiKey,
|
||||||
|
customLat,
|
||||||
|
customLon,
|
||||||
|
setSettingsMenuOpen,
|
||||||
|
mouseHide,
|
||||||
|
saveMouseHide,
|
||||||
|
} = useContext(AppContext);
|
||||||
|
|
||||||
|
const [mapsKey, setMapsKey] = useState(null);
|
||||||
|
const [weatherKey, setWeatherKey] = useState(null);
|
||||||
|
const [geoKey, setGeoKey] = useState(null);
|
||||||
|
const [lat, setLat] = useState(null);
|
||||||
|
const [lon, setLon] = useState(null);
|
||||||
|
|
||||||
|
const [currentMapsKey, setCurrentMapsKey] = useState(null);
|
||||||
|
const [currentWeatherKey, setCurrentWeatherKey] = useState(null);
|
||||||
|
const [currentGeoKey, setCurrentGeoKey] = useState(null);
|
||||||
|
const [currentLat, setCurrentLat] = useState(null);
|
||||||
|
const [currentLon, setCurrentLon] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentMapsKey(mapApiKey);
|
||||||
|
setCurrentWeatherKey(weatherApiKey);
|
||||||
|
setCurrentGeoKey(reverseGeoApiKey);
|
||||||
|
setCurrentLat(customLat);
|
||||||
|
setCurrentLon(customLon);
|
||||||
|
}, [
|
||||||
|
mapApiKey,
|
||||||
|
weatherApiKey,
|
||||||
|
reverseGeoApiKey,
|
||||||
|
customLat,
|
||||||
|
customLon,
|
||||||
|
currentGeoKey,
|
||||||
|
mouseHide,
|
||||||
|
saveMouseHide,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mapApiKey) {
|
||||||
|
setMapsKey(mapApiKey);
|
||||||
|
}
|
||||||
|
if (weatherApiKey) {
|
||||||
|
setWeatherKey(weatherApiKey);
|
||||||
|
}
|
||||||
|
if (reverseGeoApiKey) {
|
||||||
|
setGeoKey(reverseGeoApiKey);
|
||||||
|
}
|
||||||
|
if (customLat) {
|
||||||
|
setLat(customLat);
|
||||||
|
}
|
||||||
|
if (customLon) {
|
||||||
|
setLon(customLon);
|
||||||
|
}
|
||||||
|
}, [mapApiKey, weatherApiKey, reverseGeoApiKey, customLon, customLat]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CSSTransition
|
||||||
|
in={settingsMenuOpen}
|
||||||
|
unmountOnExit
|
||||||
|
timeout={300}
|
||||||
|
classNames="animate"
|
||||||
|
>
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.header}>SETTINGS</div>
|
||||||
|
<div
|
||||||
|
className={styles.closeButton}
|
||||||
|
onClick={() => {
|
||||||
|
setSettingsMenuOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InlineIcon icon={closeSharp} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.settingsContainer}>
|
||||||
|
<ToggleButtons />
|
||||||
|
<Input
|
||||||
|
label={"MAPS API KEY"}
|
||||||
|
val={mapsKey}
|
||||||
|
current={currentMapsKey}
|
||||||
|
cb={setMapsKey}
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={"WEATHER API KEY"}
|
||||||
|
val={weatherKey}
|
||||||
|
current={currentWeatherKey}
|
||||||
|
cb={setWeatherKey}
|
||||||
|
required={true}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={"GEOLOCATION API KEY"}
|
||||||
|
val={geoKey}
|
||||||
|
current={currentGeoKey}
|
||||||
|
cb={setGeoKey}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={"CUSTOM STARTING LATITUDE"}
|
||||||
|
val={lat}
|
||||||
|
cb={setLat}
|
||||||
|
current={currentLat}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label={"CUSTOM STARTING LONGITUDE"}
|
||||||
|
val={lon}
|
||||||
|
cb={setLon}
|
||||||
|
current={currentLon}
|
||||||
|
/>
|
||||||
|
<div className={styles.bottomButtonContainer}>
|
||||||
|
<div>
|
||||||
|
<div className={styles.label}>HIDE MOUSE</div>
|
||||||
|
<ToggleButton
|
||||||
|
button1Label={"ON"}
|
||||||
|
button2Label={"OFF"}
|
||||||
|
val={mouseHide}
|
||||||
|
button1Val={true}
|
||||||
|
button2Val={false}
|
||||||
|
cb={saveMouseHide}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.saveButtonContainer}>
|
||||||
|
<SaveButton
|
||||||
|
mapsKey={mapsKey}
|
||||||
|
weatherKey={weatherKey}
|
||||||
|
geoKey={geoKey}
|
||||||
|
lat={lat}
|
||||||
|
lon={lon}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CSSTransition>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save button
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {String} [props.mapsKey]
|
||||||
|
* @param {String} [props.weatherKey]
|
||||||
|
* @param {String} [props.geoKey]
|
||||||
|
* @param {String} [props.lat]
|
||||||
|
* @param {String} [props.lon]
|
||||||
|
* @returns {JSX.Element} Save button
|
||||||
|
*/
|
||||||
|
const SaveButton = ({ mapsKey, weatherKey, geoKey, lat, lon }) => {
|
||||||
|
const { saveSettingsToJson, setSettingsMenuOpen, mouseHide } = useContext(
|
||||||
|
AppContext
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.button} ${styles.saveButton} ${
|
||||||
|
!mouseHide ? styles.showMouse : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
saveSettingsToJson({ mapsKey, weatherKey, geoKey, lat, lon })
|
||||||
|
.then(() => {
|
||||||
|
setSettingsMenuOpen(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("err!", err);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className={styles.label}>SAVE</div>
|
||||||
|
<div>
|
||||||
|
<InlineIcon icon={roundSaveAlt} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
SaveButton.propTypes = {
|
||||||
|
mapsKey: PropTypes.string,
|
||||||
|
weatherKey: PropTypes.string,
|
||||||
|
geoKey: PropTypes.string,
|
||||||
|
lat: PropTypes.string,
|
||||||
|
lon: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle Buttons Group
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} A grouping of toggle buttons
|
||||||
|
*/
|
||||||
|
const ToggleButtons = () => {
|
||||||
|
const {
|
||||||
|
tempUnit,
|
||||||
|
saveTempUnit,
|
||||||
|
speedUnit,
|
||||||
|
saveSpeedUnit,
|
||||||
|
lengthUnit,
|
||||||
|
saveLengthUnit,
|
||||||
|
clockTime,
|
||||||
|
saveClockTime,
|
||||||
|
} = useContext(AppContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.label}>UNITS</div>
|
||||||
|
<div className={styles.toggleButtons}>
|
||||||
|
<div>
|
||||||
|
<ToggleButton
|
||||||
|
button1Label={"F"}
|
||||||
|
button2Label={"C"}
|
||||||
|
val={tempUnit}
|
||||||
|
button1Val={"f"}
|
||||||
|
button2Val={"c"}
|
||||||
|
cb={saveTempUnit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ToggleButton
|
||||||
|
button1Label={"mph"}
|
||||||
|
button2Label={"m/s"}
|
||||||
|
val={speedUnit}
|
||||||
|
button1Val={"mph"}
|
||||||
|
button2Val={"ms"}
|
||||||
|
cb={saveSpeedUnit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ToggleButton
|
||||||
|
button1Label={"in"}
|
||||||
|
button2Label={"mm"}
|
||||||
|
val={lengthUnit}
|
||||||
|
button1Val={"in"}
|
||||||
|
button2Val={"mm"}
|
||||||
|
cb={saveLengthUnit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ToggleButton
|
||||||
|
button1Label={"12h"}
|
||||||
|
button2Label={"24h"}
|
||||||
|
val={clockTime}
|
||||||
|
button1Val={"12"}
|
||||||
|
button2Val={"24"}
|
||||||
|
cb={saveClockTime}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle buttons
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {String} props.button1Label PropTypes.string.isRequired,
|
||||||
|
* @param {String} props.button2Label PropTypes.string.isRequired,
|
||||||
|
* @param {*} props.val PropTypes.string.isRequired,
|
||||||
|
* @param {*} props.button1Val PropTypes.string.isRequired,
|
||||||
|
* @param {*} props.button2Val PropTypes.string.isRequired,
|
||||||
|
* @param {Function} props.cb PropTypes.func.isRequired,
|
||||||
|
* @returns {JSX.Element} Toggle buttons
|
||||||
|
*/
|
||||||
|
const ToggleButton = ({
|
||||||
|
button1Label,
|
||||||
|
button2Label,
|
||||||
|
val,
|
||||||
|
button1Val,
|
||||||
|
button2Val,
|
||||||
|
cb,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.toggleContainer}>
|
||||||
|
<div
|
||||||
|
className={` ${styles.button} ${button1Val === val ? styles.down : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
cb(button1Val);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{button1Label}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={` ${styles.button} ${button2Val === val ? styles.down : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
cb(button2Val);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{button2Label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ToggleButton.propTypes = {
|
||||||
|
button1Label: PropTypes.string.isRequired,
|
||||||
|
button2Label: PropTypes.string.isRequired,
|
||||||
|
val: PropTypes.any.isRequired,
|
||||||
|
button1Val: PropTypes.any.isRequired,
|
||||||
|
button2Val: PropTypes.any.isRequired,
|
||||||
|
cb: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete button
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Function} props.cb callback
|
||||||
|
* @returns {JSX.Element} Delete button
|
||||||
|
*/
|
||||||
|
const DeleteButton = ({ cb }) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.button} onClick={cb}>
|
||||||
|
<InlineIcon icon={closeFilled} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DeleteButton.propTypes = {
|
||||||
|
cb: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Undo button, restores input to default value
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Function} props.cb callback
|
||||||
|
* @returns {JSX.Element} Undo button
|
||||||
|
*/
|
||||||
|
const UndoButton = ({ cb }) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.button} onClick={cb}>
|
||||||
|
<InlineIcon icon={undoIcon} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
UndoButton.propTypes = {
|
||||||
|
cb: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings input
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {String} props.label Label
|
||||||
|
* @param {String} props.val value
|
||||||
|
* @param {Function} props.cb change callback
|
||||||
|
* @param {String} props.current current default value
|
||||||
|
* @param {Boolean} [props.required] If input is required
|
||||||
|
* @returns {JSX.Element} Input
|
||||||
|
*/
|
||||||
|
const Input = ({ label, val, cb, required, current }) => {
|
||||||
|
const [inputValue, setInputValue] = useState(val);
|
||||||
|
const [defaultValue, setDefaultValue] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ((val || val === "") && (!defaultValue || defaultValue === "")) {
|
||||||
|
setDefaultValue(val);
|
||||||
|
}
|
||||||
|
setInputValue(val);
|
||||||
|
}, [val, defaultValue]);
|
||||||
|
return (
|
||||||
|
<div className={styles.settingsItem}>
|
||||||
|
<div className={styles.label}>{label}</div>
|
||||||
|
<div
|
||||||
|
className={`${styles.inputContainer} ${
|
||||||
|
required && !val ? styles.invalid : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={"None"}
|
||||||
|
value={inputValue || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const { value } = e.target;
|
||||||
|
setInputValue(value);
|
||||||
|
cb(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<DeleteButton
|
||||||
|
cb={() => {
|
||||||
|
setInputValue("");
|
||||||
|
cb("");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<UndoButton
|
||||||
|
cb={() => {
|
||||||
|
setInputValue(current);
|
||||||
|
cb(current);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Input.propTypes = {
|
||||||
|
label: PropTypes.string,
|
||||||
|
val: PropTypes.string,
|
||||||
|
cb: PropTypes.func.isRequired,
|
||||||
|
required: PropTypes.bool,
|
||||||
|
current: PropTypes.string,
|
||||||
|
};
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
.container {
|
||||||
|
height: calc(100vh - 20px); /* subtract padding */
|
||||||
|
width: calc(100vw - 320px); /* subtract info panel and padding */
|
||||||
|
background-color: rgba(0, 0, 0, 0.65);
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
padding: 10px;
|
||||||
|
color: #f6f6f4;
|
||||||
|
font-family: "Rubik", sans-serif;
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 0.15);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container input {
|
||||||
|
height: 30px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 800px;
|
||||||
|
margin-right: 10px;
|
||||||
|
outline: none;
|
||||||
|
border: 2px solid #5d5c5c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container.invalid input {
|
||||||
|
border: 2px solid #c20808;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container > div {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container > div:last-child {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
height: 34px;
|
||||||
|
width: 34px;
|
||||||
|
font-size: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(#5d5c5c, #4e4e4e);
|
||||||
|
border-top: 1px solid #757575;
|
||||||
|
border-left: 1px solid #757575;
|
||||||
|
border-right: 1px solid #4c4c4c;
|
||||||
|
border-bottom: 1px solid #4c4c4c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-mouse.button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.down {
|
||||||
|
color: #4e4e4e;
|
||||||
|
background: linear-gradient(#d6d6d6, #c7c7c7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:active {
|
||||||
|
background: linear-gradient(#4e4e4e, #5d5c5c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-item {
|
||||||
|
margin: 10px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-buttons {
|
||||||
|
display: flex;
|
||||||
|
max-width: 900px;
|
||||||
|
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-buttons > div {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-buttons > div:last-child {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-container > .button {
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-button-container {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button {
|
||||||
|
width: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button .label {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
font-size: 20px;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-mouse.close-button {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button:active {
|
||||||
|
color: #c7c7c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-button-container {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import React from "react";
|
||||||
|
import styles from "./styles.css";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
/*
|
||||||
|
https://github.com/tobiasahlin/SpinKit
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2020 Tobias Ahlin
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A spinner based off of https://github.com/tobiasahlin/SpinKit
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {String} props.color
|
||||||
|
* @param {String} props.size
|
||||||
|
* @returns {JSX.Element} Spinner
|
||||||
|
*/
|
||||||
|
function Spinner({ color, size }) {
|
||||||
|
const style = {
|
||||||
|
height: size,
|
||||||
|
width: size,
|
||||||
|
backgroundColor: color,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.spinner}>
|
||||||
|
<div className={styles.bounce1} style={style} />
|
||||||
|
<div className={styles.bounce2} style={style} />
|
||||||
|
<div className={styles.bounce3} style={style} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Spinner.propTypes = {
|
||||||
|
size: PropTypes.string,
|
||||||
|
color: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Spinner;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
.spinner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner > div {
|
||||||
|
border-radius: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
-webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||||
|
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner .bounce1 {
|
||||||
|
-webkit-animation-delay: -0.32s;
|
||||||
|
animation-delay: -0.32s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner .bounce2 {
|
||||||
|
-webkit-animation-delay: -0.16s;
|
||||||
|
animation-delay: -0.16s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes sk-bouncedelay {
|
||||||
|
0%,
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sk-bouncedelay {
|
||||||
|
0%,
|
||||||
|
80%,
|
||||||
|
100% {
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React, { useContext } from "react";
|
||||||
|
import { AppContext } from "~/AppContext";
|
||||||
|
import { InlineIcon } from "@iconify/react";
|
||||||
|
import bxsMoon from '@iconify/icons-bx/bxs-moon';
|
||||||
|
import bxsSun from '@iconify/icons-bx/bxs-sun';
|
||||||
|
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import styles from "./styles.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sunrise / Sunset component
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Sunrise / Sunset component
|
||||||
|
*/
|
||||||
|
const SunRiseSet = () => {
|
||||||
|
const { currentWeatherData, clockTime } = useContext(AppContext);
|
||||||
|
if (currentWeatherData) {
|
||||||
|
const {
|
||||||
|
sunrise: { value: sunrise },
|
||||||
|
sunset: { value: sunset },
|
||||||
|
} = currentWeatherData;
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div>
|
||||||
|
<InlineIcon icon={bxsSun} />
|
||||||
|
<span>
|
||||||
|
{format(new Date(sunrise), clockTime === "12" ? "p" : "HH:mm")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<InlineIcon icon={bxsMoon} />
|
||||||
|
<span>
|
||||||
|
{format(new Date(sunset), clockTime === "12" ? "p" : "HH:mm")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SunRiseSet;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
font-size: 14px;
|
||||||
|
/* padding-top: 5px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > div:first-child {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > div > span {
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import React, { useEffect, useContext, useState, useCallback } from "react";
|
||||||
|
import Spinner from "~/components/Spinner";
|
||||||
|
import { AppContext } from "~/AppContext";
|
||||||
|
import styles from "./styles.css";
|
||||||
|
import LocationName from "~/components/LocationName";
|
||||||
|
import CurrentWeather from "~/components/CurrentWeather";
|
||||||
|
import DailyChart from "~/components/weatherCharts/DailyChart";
|
||||||
|
import HourlyChart from "~/components/weatherCharts/HourlyChart";
|
||||||
|
|
||||||
|
const CURRENT_WEATHER_DATA_UPDATE_INTERVAL = 10 * 60 * 1000; //every 10 minutes
|
||||||
|
const HOURLY_WEATHER_DATA_UPDATE_INTERVAL = 60 * 60 * 1000; //every hour
|
||||||
|
const DAILY_WEATHER_DATA_UPDATE_INTERVAL = 24 * 60 * 60 * 1000; //every day
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an interval to call a weather update callback
|
||||||
|
*
|
||||||
|
* @param {Object} params
|
||||||
|
* @param {Object} params.stateInterval Interval in state
|
||||||
|
* @param {Function} params.stateIntervalSetter state interval setter
|
||||||
|
* @param {Function} params.cb callback to invoke on each interval
|
||||||
|
* @param {Number} params.intervalTime interval frequency, ms
|
||||||
|
* @param {String} params.weatherApiKey weather API key
|
||||||
|
* @param {Object} params.mapGeo coordinates to get weather for
|
||||||
|
*/
|
||||||
|
function createWeatherUpdateInterval({
|
||||||
|
stateInterval,
|
||||||
|
stateIntervalSetter,
|
||||||
|
cb,
|
||||||
|
intervalTime,
|
||||||
|
weatherApiKey,
|
||||||
|
mapGeo,
|
||||||
|
}) {
|
||||||
|
if (stateInterval) {
|
||||||
|
clearInterval(stateInterval);
|
||||||
|
stateIntervalSetter(null);
|
||||||
|
}
|
||||||
|
if (weatherApiKey && mapGeo) {
|
||||||
|
const interval = setInterval(cb, intervalTime);
|
||||||
|
cb();
|
||||||
|
stateIntervalSetter(interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays weather info
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Clock component
|
||||||
|
*/
|
||||||
|
const WeatherInfo = () => {
|
||||||
|
const {
|
||||||
|
getWeatherApiKey,
|
||||||
|
getReverseGeoApiKey,
|
||||||
|
reverseGeoApiKey,
|
||||||
|
updateCurrentWeatherData,
|
||||||
|
updateHourlyWeatherData,
|
||||||
|
updateDailyWeatherData,
|
||||||
|
mapGeo,
|
||||||
|
weatherApiKey,
|
||||||
|
currentWeatherDataErr,
|
||||||
|
currentWeatherDataErrMsg,
|
||||||
|
darkMode,
|
||||||
|
setSettingsMenuOpen,
|
||||||
|
currentWeatherData,
|
||||||
|
} = useContext(AppContext);
|
||||||
|
|
||||||
|
const [
|
||||||
|
currentWeatherUpdateInterval,
|
||||||
|
setCurrentWeatherUpdateInterval,
|
||||||
|
] = useState(null);
|
||||||
|
const [
|
||||||
|
hourlyWeatherUpdateInterval,
|
||||||
|
setHourlyWeatherUpdateInterval,
|
||||||
|
] = useState(null);
|
||||||
|
const [dailyWeatherUpdateInterval, setDailyWeatherUpdateInterval] = useState(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [err, setErr] = useState(null);
|
||||||
|
|
||||||
|
const hourlyWeatherUpdateCb = useCallback(() => {
|
||||||
|
updateHourlyWeatherData(mapGeo).catch((err) => {
|
||||||
|
console.log("err", err);
|
||||||
|
});
|
||||||
|
}, [updateHourlyWeatherData, mapGeo]);
|
||||||
|
|
||||||
|
const dailyWeatherUpdateCb = useCallback(() => {
|
||||||
|
updateDailyWeatherData(mapGeo).catch((err) => {
|
||||||
|
console.log("err", err);
|
||||||
|
});
|
||||||
|
}, [updateDailyWeatherData, mapGeo]);
|
||||||
|
|
||||||
|
const currentWeatherUpdateCb = useCallback(() => {
|
||||||
|
updateCurrentWeatherData(mapGeo).catch((err) => {
|
||||||
|
console.log("err", err);
|
||||||
|
});
|
||||||
|
}, [updateCurrentWeatherData, mapGeo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setErr(false);
|
||||||
|
if (!weatherApiKey) {
|
||||||
|
getWeatherApiKey().catch((err) => {
|
||||||
|
console.log("error getting weather api key:", err);
|
||||||
|
setErr(true);
|
||||||
|
setSettingsMenuOpen(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!reverseGeoApiKey) {
|
||||||
|
getReverseGeoApiKey().catch((err) => {
|
||||||
|
console.log("error getting reverse geo api key:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [weatherApiKey, reverseGeoApiKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
createWeatherUpdateInterval({
|
||||||
|
stateInterval: currentWeatherUpdateInterval,
|
||||||
|
stateIntervalSetter: setCurrentWeatherUpdateInterval,
|
||||||
|
cb: currentWeatherUpdateCb,
|
||||||
|
intervalTime: CURRENT_WEATHER_DATA_UPDATE_INTERVAL,
|
||||||
|
weatherApiKey,
|
||||||
|
mapGeo,
|
||||||
|
});
|
||||||
|
createWeatherUpdateInterval({
|
||||||
|
stateInterval: hourlyWeatherUpdateInterval,
|
||||||
|
stateIntervalSetter: setHourlyWeatherUpdateInterval,
|
||||||
|
cb: hourlyWeatherUpdateCb,
|
||||||
|
intervalTime: HOURLY_WEATHER_DATA_UPDATE_INTERVAL,
|
||||||
|
weatherApiKey,
|
||||||
|
mapGeo,
|
||||||
|
});
|
||||||
|
createWeatherUpdateInterval({
|
||||||
|
stateInterval: dailyWeatherUpdateInterval,
|
||||||
|
stateIntervalSetter: setDailyWeatherUpdateInterval,
|
||||||
|
cb: dailyWeatherUpdateCb,
|
||||||
|
intervalTime: DAILY_WEATHER_DATA_UPDATE_INTERVAL,
|
||||||
|
weatherApiKey,
|
||||||
|
mapGeo,
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
clearInterval(currentWeatherUpdateInterval);
|
||||||
|
clearInterval(hourlyWeatherUpdateInterval);
|
||||||
|
clearInterval(dailyWeatherUpdateInterval);
|
||||||
|
};
|
||||||
|
}, [weatherApiKey, mapGeo]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
if (currentWeatherData) {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.location}>
|
||||||
|
<LocationName />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<CurrentWeather />
|
||||||
|
</div>
|
||||||
|
<div className={styles.weatherChart}>
|
||||||
|
<HourlyChart />
|
||||||
|
</div>
|
||||||
|
<div className={styles.weatherChart}>
|
||||||
|
<DailyChart />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (currentWeatherData || currentWeatherDataErr || err) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${styles.errContainer} ${
|
||||||
|
darkMode ? styles.dark : styles.light
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div>Could not retrieve weather data.</div>
|
||||||
|
<div>Is your weather API key valid?</div>
|
||||||
|
{currentWeatherDataErr ? (
|
||||||
|
<div className={styles.message}>{currentWeatherDataErrMsg}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className={styles.loadingContainer}>
|
||||||
|
<Spinner size={"20px"} color={darkMode ? "#f6f6f444" : "#3a393844"} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WeatherInfo;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
.container {
|
||||||
|
padding: 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weather-chart {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.err-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 60px;
|
||||||
|
font-size: 12px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
color: #f6f6f444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
color: #3a393888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.err-container .message{
|
||||||
|
margin-top: 10px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import React, {
|
||||||
|
useEffect,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
} from "react";
|
||||||
|
import { Map, TileLayer, AttributionControl, Marker } from "react-leaflet";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import { AppContext } from "~/AppContext";
|
||||||
|
import debounce from "debounce";
|
||||||
|
import axios from "axios";
|
||||||
|
import styles from "./styles.css";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weather map
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Number} props.zoom zoom level
|
||||||
|
* @param {Boolean} [props.dark] dark mode
|
||||||
|
* @returns {JSX.Element} Weather map
|
||||||
|
*/
|
||||||
|
const WeatherMap = ({ zoom, dark }) => {
|
||||||
|
const MAP_CLICK_DEBOUNCE_TIME = 200; //ms
|
||||||
|
const {
|
||||||
|
setMapPosition,
|
||||||
|
panToCoords,
|
||||||
|
setPanToCoords,
|
||||||
|
browserGeo,
|
||||||
|
mapGeo,
|
||||||
|
mapApiKey,
|
||||||
|
getMapApiKey,
|
||||||
|
markerIsVisible,
|
||||||
|
animateWeatherMap,
|
||||||
|
} = useContext(AppContext);
|
||||||
|
const mapRef = useRef();
|
||||||
|
|
||||||
|
const mapClickHandler = useCallback(
|
||||||
|
debounce((e) => {
|
||||||
|
const { lat: latitude, lng: longitude } = e.latlng;
|
||||||
|
const newCoords = { latitude, longitude };
|
||||||
|
setMapPosition(newCoords);
|
||||||
|
}, MAP_CLICK_DEBOUNCE_TIME),
|
||||||
|
[setMapPosition]
|
||||||
|
);
|
||||||
|
|
||||||
|
const [mapTimestamps, setMapTimestamps] = useState(null);
|
||||||
|
const [mapTimestamp, setMapTimestamp] = useState(null);
|
||||||
|
const [currentMapTimestampIdx, setCurrentMapTimestampIdx] = useState(0);
|
||||||
|
|
||||||
|
const MAP_TIMESTAMP_REFRESH_FREQUENCY = 1000 * 60 * 10; //update every 10 minutes
|
||||||
|
const MAP_CYCLE_RATE = 1000; //ms
|
||||||
|
|
||||||
|
const getMapApiKeyCallback = useCallback(() => getMapApiKey(), [
|
||||||
|
getMapApiKey,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getMapApiKeyCallback().catch((err) => {
|
||||||
|
console.log("err!", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateTimeStamps = () => {
|
||||||
|
getMapTimestamps()
|
||||||
|
.then((res) => {
|
||||||
|
setMapTimestamps(res);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.log("err", err);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapTimestampsInterval = setInterval(
|
||||||
|
updateTimeStamps,
|
||||||
|
MAP_TIMESTAMP_REFRESH_FREQUENCY
|
||||||
|
);
|
||||||
|
updateTimeStamps(); //initial update
|
||||||
|
return () => {
|
||||||
|
clearInterval(mapTimestampsInterval);
|
||||||
|
};
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
// Pan the screen to a a specific location when `panToCoords` is updated with grid coordinates
|
||||||
|
useEffect(() => {
|
||||||
|
if (panToCoords && mapRef.current) {
|
||||||
|
const { leafletElement } = mapRef.current;
|
||||||
|
leafletElement.panTo([panToCoords.latitude, panToCoords.longitude]);
|
||||||
|
setPanToCoords(null); //reset back to null so we can observe a change next time its fired for the same coords
|
||||||
|
}
|
||||||
|
}, [panToCoords, mapRef]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const { latitude, longitude } = browserGeo || {};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mapTimestamps) {
|
||||||
|
setMapTimestamp(mapTimestamps[currentMapTimestampIdx]);
|
||||||
|
}
|
||||||
|
}, [currentMapTimestampIdx, mapTimestamps]);
|
||||||
|
|
||||||
|
// cycle through weather maps when animated is enabled
|
||||||
|
useEffect(() => {
|
||||||
|
if (mapTimestamps) {
|
||||||
|
if (animateWeatherMap) {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
let nextIdx;
|
||||||
|
if (currentMapTimestampIdx + 1 >= mapTimestamps.length) {
|
||||||
|
nextIdx = 0;
|
||||||
|
} else {
|
||||||
|
nextIdx = currentMapTimestampIdx + 1;
|
||||||
|
}
|
||||||
|
setCurrentMapTimestampIdx(nextIdx);
|
||||||
|
}, MAP_CYCLE_RATE);
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
setCurrentMapTimestampIdx(mapTimestamps.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentMapTimestampIdx, animateWeatherMap, mapTimestamps]);
|
||||||
|
|
||||||
|
if (!hasVal(latitude) || !hasVal(longitude) || !zoom || !mapApiKey) {
|
||||||
|
return (
|
||||||
|
<div className={`${styles.noMap} ${dark ? styles.dark : styles.light}`}>
|
||||||
|
<div>Cannot retrieve map data.</div>
|
||||||
|
<div>Did you enter an API key?</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const markerPosition = mapGeo ? [mapGeo.latitude, mapGeo.longitude] : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Map
|
||||||
|
ref={mapRef}
|
||||||
|
center={[latitude, longitude]}
|
||||||
|
zoom={zoom}
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
attributionControl={false}
|
||||||
|
touchZoom={true}
|
||||||
|
dragging={true}
|
||||||
|
fadeAnimation={false}
|
||||||
|
onClick={mapClickHandler}
|
||||||
|
>
|
||||||
|
<AttributionControl position={"bottomleft"} />
|
||||||
|
<TileLayer
|
||||||
|
attribution='© <a href="https://www.mapbox.com/feedback/">Mapbox</a>'
|
||||||
|
url={`https://api.mapbox.com/styles/v1/mapbox/${
|
||||||
|
dark ? "dark-v10" : "light-v10"
|
||||||
|
}/tiles/{z}/{x}/{y}?access_token={apiKey}`}
|
||||||
|
apiKey={mapApiKey}
|
||||||
|
/>
|
||||||
|
{mapTimestamp ? (
|
||||||
|
<TileLayer
|
||||||
|
attribution='<a href="https://www.rainviewer.com/">RainViewer</a>'
|
||||||
|
url={`https://tilecache.rainviewer.com/v2/radar/${mapTimestamp}/{size}/{z}/{x}/{y}/{color}/{smooth}_{snow}.png`}
|
||||||
|
opacity={0.3}
|
||||||
|
size={512}
|
||||||
|
color={6} // https://www.rainviewer.com/api.html#colorSchemes
|
||||||
|
smooth={1}
|
||||||
|
snow={1}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{markerIsVisible && markerPosition ? (
|
||||||
|
<Marker position={markerPosition} opacity={0.65}></Marker>
|
||||||
|
) : null}
|
||||||
|
</Map>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
WeatherMap.propTypes = {
|
||||||
|
zoom: PropTypes.number.isRequired,
|
||||||
|
dark: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weather layer
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {String} props.layer
|
||||||
|
* @param {String} props.weatherApiKey
|
||||||
|
* @returns {JSX.Element} Weather layer
|
||||||
|
*/
|
||||||
|
const WeatherLayer = ({ layer, weatherApiKey }) => {
|
||||||
|
return (
|
||||||
|
<TileLayer
|
||||||
|
attribution='&copy <a href="https://openweathermap.org/">OpenWeather</a>'
|
||||||
|
url={`https://tile.openweathermap.org/map/${layer}/{z}/{x}/{y}.png?appid=${weatherApiKey}`}
|
||||||
|
apiKey
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
WeatherLayer.propTypes = {
|
||||||
|
layer: PropTypes.string.isRequired,
|
||||||
|
weatherApiKey: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if truthy, but returns true for 0
|
||||||
|
*
|
||||||
|
* @param {*} i
|
||||||
|
* @returns {Boolean} If truthy or zero
|
||||||
|
*/
|
||||||
|
function hasVal(i) {
|
||||||
|
return !!(i || i === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get timestamps for weather map
|
||||||
|
*
|
||||||
|
* @returns {Promise} Promise of timestamps
|
||||||
|
*/
|
||||||
|
function getMapTimestamps() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios
|
||||||
|
.get("https://api.rainviewer.com/public/maps.json")
|
||||||
|
.then((res) => {
|
||||||
|
resolve(res.data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WeatherMap;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
.no-map {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: Rubik;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
color: #f6f6f444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
color: #3a393888;
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import React, { useContext, useState, useEffect, useCallback } from "react";
|
||||||
|
import { AppContext } from "~/AppContext";
|
||||||
|
import styles from "../styles.css";
|
||||||
|
import { Line } from "react-chartjs-2";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import {
|
||||||
|
convertTemp,
|
||||||
|
convertLength,
|
||||||
|
convertSpeed,
|
||||||
|
} from "~/services/conversions";
|
||||||
|
import { fontColor } from "../common";
|
||||||
|
|
||||||
|
const createChartOptions = ({
|
||||||
|
darkMode,
|
||||||
|
tempUnit,
|
||||||
|
speedUnit,
|
||||||
|
lengthUnit,
|
||||||
|
altMode,
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
responsive: true,
|
||||||
|
hoverMode: "index",
|
||||||
|
stacked: false,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: `7 Day ${
|
||||||
|
altMode
|
||||||
|
? `Wind Speed / Precipitation (${lengthUnit})`
|
||||||
|
: `Temp / Precipitation`
|
||||||
|
}`,
|
||||||
|
fontColor: fontColor(darkMode),
|
||||||
|
fontFamily: "Rubik, sans-serif",
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
fontColor: fontColor(darkMode),
|
||||||
|
fontFamily: "Rubik, sans-serif",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
type: "linear",
|
||||||
|
display: true,
|
||||||
|
position: "left",
|
||||||
|
id: "y-axis-1",
|
||||||
|
ticks: {
|
||||||
|
fontColor: fontColor(darkMode),
|
||||||
|
fontFamily: "Rubik, sans-serif",
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
callback: (val) => {
|
||||||
|
return altMode
|
||||||
|
? `${val} ${speedUnit === "mph" ? "mph" : "m/s"}`
|
||||||
|
: `${val} ${tempUnit.toUpperCase()}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "linear",
|
||||||
|
display: true,
|
||||||
|
position: "right",
|
||||||
|
id: "y-axis-2",
|
||||||
|
ticks: {
|
||||||
|
fontColor: fontColor(darkMode),
|
||||||
|
fontFamily: "Rubik, sans-serif",
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
suggestedMin: 0,
|
||||||
|
callback: (val) => {
|
||||||
|
return `${val}${altMode ? ` ${lengthUnit}` : "%"}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartColors = {
|
||||||
|
blue: "rgba(63, 127, 191, 0.5)",
|
||||||
|
gray: "rgba(127, 127, 127, 0.5)",
|
||||||
|
gray2: "rgba(127, 127, 127, 0.3)",
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapChartData = ({ data, tempUnit, speedUnit, altMode, lengthUnit }) => {
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
labels: data.map((e) => {
|
||||||
|
const date = new Date(e.observation_time.value);
|
||||||
|
const adjustedTimestamp =
|
||||||
|
date.getTime() + date.getTimezoneOffset() * 60 * 1000;
|
||||||
|
return format(new Date(adjustedTimestamp), "EEEEE");
|
||||||
|
}),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
radius: 0,
|
||||||
|
label: altMode ? "High Wind Speed" : "High Temp",
|
||||||
|
data: data.map((e) => {
|
||||||
|
return altMode
|
||||||
|
? convertSpeed(e.wind_speed[1].max.value, speedUnit)
|
||||||
|
: convertTemp(e.temp[1].max.value, tempUnit);
|
||||||
|
}),
|
||||||
|
yAxisID: "y-axis-1",
|
||||||
|
borderColor: chartColors.gray,
|
||||||
|
backgroundColor: chartColors.gray,
|
||||||
|
fill: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
radius: 0,
|
||||||
|
label: altMode ? "Low Wind Speed" : "Low Temp",
|
||||||
|
data: data.map((e) => {
|
||||||
|
return altMode
|
||||||
|
? convertSpeed(e.wind_speed[0].min.value, speedUnit)
|
||||||
|
: convertTemp(e.temp[0].min.value, tempUnit);
|
||||||
|
}),
|
||||||
|
yAxisID: "y-axis-1",
|
||||||
|
borderColor: chartColors.gray2,
|
||||||
|
backgroundColor: chartColors.gray2,
|
||||||
|
fill: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
radius: 0,
|
||||||
|
label: "Precipitation",
|
||||||
|
data: data.map((e) => {
|
||||||
|
return altMode
|
||||||
|
? convertLength(e.precipitation[0].max.value, lengthUnit)
|
||||||
|
: e.precipitation_probability.value;
|
||||||
|
}),
|
||||||
|
yAxisID: "y-axis-2",
|
||||||
|
borderColor: chartColors.blue,
|
||||||
|
backgroundColor: chartColors.blue,
|
||||||
|
fill: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daily forecast chart
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Hourly forecast chart
|
||||||
|
*/
|
||||||
|
const DailyChart = () => {
|
||||||
|
const {
|
||||||
|
dailyWeatherData,
|
||||||
|
dailyWeatherDataErr,
|
||||||
|
dailyWeatherDataErrMsg,
|
||||||
|
tempUnit,
|
||||||
|
darkMode,
|
||||||
|
lengthUnit,
|
||||||
|
speedUnit,
|
||||||
|
} = useContext(AppContext);
|
||||||
|
|
||||||
|
const [altMode, setAltMode] = useState(false);
|
||||||
|
const [chartData, setChartData] = useState(null);
|
||||||
|
const [chartOptions, setChartOptions] = useState(null);
|
||||||
|
|
||||||
|
const setChartDataCallback = useCallback((e) => setChartData(e), []);
|
||||||
|
const setChartOptionsCallback = useCallback((e) => setChartOptions(e), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dailyWeatherData) {
|
||||||
|
setChartDataCallback(
|
||||||
|
mapChartData({
|
||||||
|
data: dailyWeatherData,
|
||||||
|
tempUnit,
|
||||||
|
lengthUnit,
|
||||||
|
speedUnit,
|
||||||
|
altMode,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setChartOptionsCallback(
|
||||||
|
createChartOptions({
|
||||||
|
tempUnit,
|
||||||
|
darkMode,
|
||||||
|
lengthUnit,
|
||||||
|
speedUnit,
|
||||||
|
altMode,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
dailyWeatherData,
|
||||||
|
tempUnit,
|
||||||
|
lengthUnit,
|
||||||
|
altMode,
|
||||||
|
speedUnit,
|
||||||
|
darkMode,
|
||||||
|
setChartOptionsCallback,
|
||||||
|
setChartDataCallback,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (chartData && chartOptions) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
onClick={() => {
|
||||||
|
setAltMode(!altMode);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Line options={chartOptions} data={chartData} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (dailyWeatherDataErr) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${darkMode ? styles.dark : styles.light} ${
|
||||||
|
styles.errContainer
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div>Cannot get 7 day weather forecast</div>
|
||||||
|
<div className={styles.message}>{dailyWeatherDataErrMsg}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DailyChart;
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import React, { useContext, useState, useEffect } from "react";
|
||||||
|
import { AppContext } from "~/AppContext";
|
||||||
|
import styles from "../styles.css";
|
||||||
|
import { Line } from "react-chartjs-2";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import {
|
||||||
|
convertTemp,
|
||||||
|
convertLength,
|
||||||
|
convertSpeed,
|
||||||
|
} from "~/services/conversions";
|
||||||
|
import { fontColor } from "../common";
|
||||||
|
|
||||||
|
const chartOptions = ({
|
||||||
|
darkMode,
|
||||||
|
tempUnit,
|
||||||
|
speedUnit,
|
||||||
|
lengthUnit,
|
||||||
|
altMode,
|
||||||
|
}) => {
|
||||||
|
return {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
legend: {
|
||||||
|
display: false,
|
||||||
|
},
|
||||||
|
responsive: true,
|
||||||
|
hoverMode: "index",
|
||||||
|
stacked: false,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: `24 Hour ${
|
||||||
|
altMode
|
||||||
|
? `Wind Speed / Precipitation (${lengthUnit})`
|
||||||
|
: `Temp / Precipitation`
|
||||||
|
}`,
|
||||||
|
fontColor: fontColor(darkMode),
|
||||||
|
fontFamily: "Rubik, sans-serif",
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
xAxes: [
|
||||||
|
{
|
||||||
|
ticks: {
|
||||||
|
fontColor: fontColor(darkMode),
|
||||||
|
fontFamily: "Rubik, sans-serif",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
yAxes: [
|
||||||
|
{
|
||||||
|
type: "linear",
|
||||||
|
display: true,
|
||||||
|
position: "left",
|
||||||
|
id: "y-axis-1",
|
||||||
|
ticks: {
|
||||||
|
fontColor: fontColor(darkMode),
|
||||||
|
fontFamily: "Rubik, sans-serif",
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
callback: (val) => {
|
||||||
|
return altMode
|
||||||
|
? `${val} ${speedUnit === "mph" ? "mph" : "m/s"}`
|
||||||
|
: `${val} ${tempUnit.toUpperCase()}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "linear",
|
||||||
|
display: true,
|
||||||
|
position: "right",
|
||||||
|
id: "y-axis-2",
|
||||||
|
ticks: {
|
||||||
|
fontColor: fontColor(darkMode),
|
||||||
|
fontFamily: "Rubik, sans-serif",
|
||||||
|
maxTicksLimit: 5,
|
||||||
|
suggestedMin: 0,
|
||||||
|
callback: (val) => {
|
||||||
|
return `${val}${altMode ? ` ${lengthUnit}` : "%"}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gridLines: {
|
||||||
|
drawOnChartArea: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartColors = {
|
||||||
|
blue: "rgba(63, 127, 191, 0.5)",
|
||||||
|
gray: "rgba(127, 127, 127, 0.5)",
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapChartData = ({
|
||||||
|
data,
|
||||||
|
tempUnit,
|
||||||
|
speedUnit,
|
||||||
|
clockTime,
|
||||||
|
altMode,
|
||||||
|
lengthUnit,
|
||||||
|
}) => {
|
||||||
|
if (!data) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
labels: data.map((e) => {
|
||||||
|
if (clockTime === "12") {
|
||||||
|
return `${format(new Date(e.observation_time.value), "h")}${format(
|
||||||
|
new Date(e.observation_time.value),
|
||||||
|
"aaaaa"
|
||||||
|
)}`;
|
||||||
|
} else {
|
||||||
|
return `${format(new Date(e.observation_time.value), "HH")}`;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
radius: 0,
|
||||||
|
label: altMode ? "Wind Speed" : "Temp",
|
||||||
|
data: data.map((e) => {
|
||||||
|
return altMode
|
||||||
|
? convertSpeed(e.wind_speed.value, speedUnit)
|
||||||
|
: convertTemp(e.temp.value, tempUnit);
|
||||||
|
}),
|
||||||
|
yAxisID: "y-axis-1",
|
||||||
|
borderColor: chartColors.gray,
|
||||||
|
backgroundColor: chartColors.gray,
|
||||||
|
fill: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
radius: 0,
|
||||||
|
label: "Precipitation",
|
||||||
|
data: data.map((e) => {
|
||||||
|
return altMode
|
||||||
|
? convertLength(e.precipitation.value, lengthUnit)
|
||||||
|
: e.precipitation_probability.value;
|
||||||
|
}),
|
||||||
|
yAxisID: "y-axis-2",
|
||||||
|
borderColor: chartColors.blue,
|
||||||
|
backgroundColor: chartColors.blue,
|
||||||
|
fill: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hourly forecast chart
|
||||||
|
*
|
||||||
|
* @returns {JSX.Element} Hourly forecast chart
|
||||||
|
*/
|
||||||
|
const HourlyChart = () => {
|
||||||
|
const {
|
||||||
|
hourlyWeatherData,
|
||||||
|
tempUnit,
|
||||||
|
darkMode,
|
||||||
|
clockTime,
|
||||||
|
lengthUnit,
|
||||||
|
speedUnit,
|
||||||
|
hourlyWeatherDataErr,
|
||||||
|
hourlyWeatherDataErrMsg,
|
||||||
|
} = useContext(AppContext);
|
||||||
|
|
||||||
|
const [altMode, setAltMode] = useState(false);
|
||||||
|
const [chartData, setChartData] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (hourlyWeatherData) {
|
||||||
|
setChartData(
|
||||||
|
mapChartData({
|
||||||
|
data: hourlyWeatherData,
|
||||||
|
tempUnit,
|
||||||
|
clockTime,
|
||||||
|
lengthUnit,
|
||||||
|
speedUnit,
|
||||||
|
altMode,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [hourlyWeatherData, tempUnit, clockTime, lengthUnit, altMode, speedUnit]);
|
||||||
|
|
||||||
|
if (chartData) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
onClick={() => {
|
||||||
|
setAltMode(!altMode);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Line
|
||||||
|
data={chartData}
|
||||||
|
options={chartOptions({
|
||||||
|
tempUnit,
|
||||||
|
darkMode,
|
||||||
|
lengthUnit,
|
||||||
|
speedUnit,
|
||||||
|
altMode,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}else if (hourlyWeatherDataErr) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`${darkMode ? styles.dark : styles.light} ${
|
||||||
|
styles.errContainer
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div>Cannot get 24 hour weather forecast</div>
|
||||||
|
<div className={styles.message}>{hourlyWeatherDataErrMsg}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HourlyChart;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Returns the appropriate font color depending on whether or not dark mode is enabled or not
|
||||||
|
* see `InfoPanel/styles.css`
|
||||||
|
*
|
||||||
|
* @param {Boolean} darkMode
|
||||||
|
* @returns {String} font color
|
||||||
|
*/
|
||||||
|
export const fontColor = (darkMode) => {
|
||||||
|
return darkMode ? "rgba(246, 246, 244, 0.8)" : "rgba(58, 57, 56, 0.8)";
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
.container {
|
||||||
|
width: 255px;
|
||||||
|
height: 115px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
color: #f6f6f444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
color: #3a393888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.err-container {
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.err-container .message{
|
||||||
|
margin-top: 10px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
|
||||||
|
integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
|
||||||
|
crossorigin=""
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"
|
||||||
|
integrity="sha512-gZwIG9x3wUXg2hdXF6+rVkLF/0Vi9U8D2Ntg4Ga5I5BZpVkVxlJWbSQtXPSiUTtC0TjtGOmxa1AJPuV0CPthew=="
|
||||||
|
crossorigin=""
|
||||||
|
></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Rubik:wght@300&display=swap" rel="stylesheet">
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Pi Weather Station</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import App from "~/components/App";
|
||||||
|
import { AppContextProvider } from "~/AppContext";
|
||||||
|
import "~/styles";
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<AppContextProvider>
|
||||||
|
<App />
|
||||||
|
</AppContextProvider>,
|
||||||
|
document.getElementById("root")
|
||||||
|
);
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* Convert celsius to fahrenheit
|
||||||
|
*
|
||||||
|
* @param {Number} c degrees celsius
|
||||||
|
* @returns {Number} degrees fahrenheit
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const cToF = (c) => {
|
||||||
|
return c * (9/5) + 32;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert temperature to celsius if needed
|
||||||
|
*
|
||||||
|
* @param {Number} c temperature in celsius
|
||||||
|
* @param {String} units `f` for fahrenheit, `c` for celsius
|
||||||
|
* @returns {Number} converted temperature
|
||||||
|
*/
|
||||||
|
export const convertTemp = (c, units) => {
|
||||||
|
if (!c && c !== 0) {
|
||||||
|
console.log("missing input temp!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (units && units.toLowerCase() === "c") {
|
||||||
|
return parseInt(c);
|
||||||
|
} else if (units && units.toLowerCase() === "f") {
|
||||||
|
return parseInt(cToF(c));
|
||||||
|
} else {
|
||||||
|
console.log("Missing / invalid target unit!", units);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert m/s to mph
|
||||||
|
*
|
||||||
|
* @param {Number} ms
|
||||||
|
* @returns {Number} mph
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const msToMph = (ms) => {
|
||||||
|
return ms / 0.44704;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts speed
|
||||||
|
*
|
||||||
|
* @param {Number} speed
|
||||||
|
* @param {String} units mph or ms for m/s
|
||||||
|
* @returns {Number} converted speed
|
||||||
|
*/
|
||||||
|
export const convertSpeed = (speed, units) => {
|
||||||
|
if (!speed && speed !== 0) {
|
||||||
|
console.log("missing input speed");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (units && units.toLowerCase() === "mph") {
|
||||||
|
return parseInt(msToMph(speed));
|
||||||
|
} else if (units && units.toLowerCase() === "ms") {
|
||||||
|
return parseInt(speed);
|
||||||
|
} else {
|
||||||
|
console.log("Missing / invalid target unit!", units);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts mm to inches
|
||||||
|
*
|
||||||
|
* @param {Number} mm
|
||||||
|
* @returns {Number} inches
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
const mmToIn = (mm) => {
|
||||||
|
return mm / 25.4;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert length
|
||||||
|
*
|
||||||
|
* @param {Number} len mm
|
||||||
|
* @param {String} units in or mm
|
||||||
|
* @returns {Number} converted length
|
||||||
|
*/
|
||||||
|
export const convertLength = (len, units) => {
|
||||||
|
if (!len && len !== 0) {
|
||||||
|
console.log("missing input length!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (units && units.toLowerCase() === "in") {
|
||||||
|
return parseInt(mmToIn(len) * 100) / 100;
|
||||||
|
} else if (units && units.toLowerCase() === "mm") {
|
||||||
|
return len;
|
||||||
|
} else {
|
||||||
|
console.log("Missing / invalid target unit!", units);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Capitalizes the first letter of a string
|
||||||
|
*
|
||||||
|
* @param {String} string
|
||||||
|
* @returns {String} String with first letter capitalized
|
||||||
|
*/
|
||||||
|
export const capitalizeFirstLetter = (string) => {
|
||||||
|
if (string && typeof string === "string") {
|
||||||
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
||||||
|
} else {
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets coordinates from `navigator.geolocation` (currently not supported on aspbian Chromium)
|
||||||
|
*
|
||||||
|
* @returns {Promise} coordinates
|
||||||
|
*/
|
||||||
|
export function getCoordsFromBrowser() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (navigator.geolocation) {
|
||||||
|
navigator.geolocation.getCurrentPosition((pos) => {
|
||||||
|
if (!pos || (pos && !pos.coords)) {
|
||||||
|
reject("Could not get current position");
|
||||||
|
} else {
|
||||||
|
resolve(pos.coords);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets coordinates from an external API
|
||||||
|
*
|
||||||
|
* @returns {Promise} coordinates
|
||||||
|
*/
|
||||||
|
export function getCoordsFromApi() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios
|
||||||
|
.get("/geolocation")
|
||||||
|
.then((res) => {
|
||||||
|
const { latitude, longitude } = res.data;
|
||||||
|
if (
|
||||||
|
!latitude ||
|
||||||
|
(!latitude && latitude !== 0) ||
|
||||||
|
!longitude ||
|
||||||
|
(!longitude && longitude !== 0)
|
||||||
|
) {
|
||||||
|
reject("Could not get lan/lon");
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
let prevCoords = {};
|
||||||
|
let prevResult = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse geocode lookup
|
||||||
|
*
|
||||||
|
* @param {Object} params
|
||||||
|
* @param {Number} params.lat latitude
|
||||||
|
* @param {Number} params.lon longitude
|
||||||
|
* @param {String} params.apiKey api key
|
||||||
|
* @returns {Promise} API result
|
||||||
|
*/
|
||||||
|
function reverseGeocode({ lat, lon, apiKey }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (prevCoords.lat === lat && prevCoords.lon === lon) {
|
||||||
|
return resolve(prevResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
axios
|
||||||
|
.get(
|
||||||
|
`https://us1.locationiq.com/v1/reverse.php?key=${apiKey}&lat=${lat}&lon=${lon}&format=json`
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
prevCoords = { lat, lon };
|
||||||
|
prevResult = res.data;
|
||||||
|
resolve(res.data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// return reject(); //tempoarily disabling
|
||||||
|
// axios
|
||||||
|
// .get(
|
||||||
|
// `https://api.bigdatacloud.net/data/reverse-geocode?latitude=${lat}&longitude=${lon}&localityLanguage=en`
|
||||||
|
// )
|
||||||
|
// .then((res) => {
|
||||||
|
// prevCoords = { lat, lon };
|
||||||
|
// prevResult = res.data;
|
||||||
|
// resolve(res.data);
|
||||||
|
// })
|
||||||
|
// .catch((err) => {
|
||||||
|
// reject(err);
|
||||||
|
// });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default reverseGeocode;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets settings
|
||||||
|
*
|
||||||
|
* @returns {Promise} resolves settings
|
||||||
|
*/
|
||||||
|
export function getSettings() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
axios
|
||||||
|
.get("/settings")
|
||||||
|
.then((res) => {
|
||||||
|
resolve(res.data);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import("!style-loader!css-loader!~/styles/main.css");
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
body {
|
||||||
|
font-family: Rubik;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const webpack = require("webpack");
|
||||||
|
const HtmlWebPackPlugin = require("html-webpack-plugin");
|
||||||
|
|
||||||
|
module.exports = (env) => {
|
||||||
|
const PRODUCTION = !!(env && env.BUILD_PRODUCTION);
|
||||||
|
process.env.NODE_ENV = PRODUCTION ? "production" : "development";
|
||||||
|
|
||||||
|
const definePlugin = new webpack.DefinePlugin({
|
||||||
|
__PRODUCTION__: JSON.stringify(
|
||||||
|
JSON.parse(env ? env.BUILD_PRODUCTION || "false" : "false")
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
output: {
|
||||||
|
path: path.resolve(__dirname, "dist"),
|
||||||
|
filename: "bundle.min.js",
|
||||||
|
publicPath: "/",
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
enforce: "pre",
|
||||||
|
test: /\.js$/,
|
||||||
|
include: [path.resolve(__dirname, "src")],
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: "eslint-loader"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(js|jsx)$/,
|
||||||
|
include: [path.resolve(__dirname, "src")],
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: "babel-loader",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.html$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: "html-loader",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css$/,
|
||||||
|
use: [
|
||||||
|
"style-loader",
|
||||||
|
{
|
||||||
|
loader: "css-loader",
|
||||||
|
options: {
|
||||||
|
sourceMap: !PRODUCTION,
|
||||||
|
modules: {
|
||||||
|
exportLocalsConvention: "camelCase",
|
||||||
|
localIdentName: "[path][name]__[local]--[hash:base64:5]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ loader: "postcss-loader", options: { sourceMap: !PRODUCTION } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(png|svg|jpg|gif)$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: "url-loader",
|
||||||
|
options: {
|
||||||
|
limit: 8192,
|
||||||
|
sourceMap: !PRODUCTION,
|
||||||
|
name: PRODUCTION
|
||||||
|
? "[contenthash].[ext]"
|
||||||
|
: "[path][name].[ext]?[contenthash]"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: "file-loader",
|
||||||
|
options: {
|
||||||
|
sourceMap: !PRODUCTION,
|
||||||
|
name: PRODUCTION
|
||||||
|
? "[contenthash].[ext]"
|
||||||
|
: "[path][name].[ext]?[contenthash]"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: [".js", ".scss"],
|
||||||
|
alias: {
|
||||||
|
["~"]: path.resolve(__dirname, "src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebPackPlugin({
|
||||||
|
template: "./src/index.html",
|
||||||
|
filename: "./index.html",
|
||||||
|
}),
|
||||||
|
definePlugin,
|
||||||
|
],
|
||||||
|
watchOptions: {
|
||||||
|
ignored: /node_modules/
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
Generated
+609
@@ -0,0 +1,609 @@
|
|||||||
|
{
|
||||||
|
"name": "pi-weather-station",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": {
|
||||||
|
"version": "1.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
|
||||||
|
"integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
|
||||||
|
"requires": {
|
||||||
|
"mime-types": "~2.1.24",
|
||||||
|
"negotiator": "0.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"anymatch": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"picomatch": "^2.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"array-flatten": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
|
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
|
||||||
|
},
|
||||||
|
"async-limiter": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"axios": {
|
||||||
|
"version": "0.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz",
|
||||||
|
"integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==",
|
||||||
|
"requires": {
|
||||||
|
"follow-redirects": "1.5.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binary-extensions": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"body-parser": {
|
||||||
|
"version": "1.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
|
||||||
|
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
|
||||||
|
"requires": {
|
||||||
|
"bytes": "3.1.0",
|
||||||
|
"content-type": "~1.0.4",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "~1.1.2",
|
||||||
|
"http-errors": "1.7.2",
|
||||||
|
"iconv-lite": "0.4.24",
|
||||||
|
"on-finished": "~2.3.0",
|
||||||
|
"qs": "6.7.0",
|
||||||
|
"raw-body": "2.4.0",
|
||||||
|
"type-is": "~1.6.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"braces": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fill-range": "^7.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bytes": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
|
||||||
|
},
|
||||||
|
"chokidar": {
|
||||||
|
"version": "3.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz",
|
||||||
|
"integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"anymatch": "~3.1.1",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"fsevents": "~2.1.2",
|
||||||
|
"glob-parent": "~5.1.0",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connect-livereload": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/connect-livereload/-/connect-livereload-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-3R0kMOdL7CjJpU66fzAkCe6HNtd3AavCS4m+uW4KtJjrdGPT0SQEZieAYd+cm+lJoBznNQ4lqipYWkhBMgk00g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"content-disposition": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
|
||||||
|
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
|
||||||
|
"requires": {
|
||||||
|
"safe-buffer": "5.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"content-type": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
|
||||||
|
},
|
||||||
|
"cookie": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
|
||||||
|
},
|
||||||
|
"cookie-signature": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
|
"integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
|
||||||
|
},
|
||||||
|
"cors": {
|
||||||
|
"version": "2.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||||
|
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||||
|
"requires": {
|
||||||
|
"object-assign": "^4",
|
||||||
|
"vary": "^1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"requires": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"depd": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
|
||||||
|
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
|
||||||
|
},
|
||||||
|
"destroy": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||||
|
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
|
||||||
|
},
|
||||||
|
"ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
|
||||||
|
},
|
||||||
|
"encodeurl": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
|
||||||
|
},
|
||||||
|
"escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
|
||||||
|
},
|
||||||
|
"etag": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
|
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
|
||||||
|
},
|
||||||
|
"express": {
|
||||||
|
"version": "4.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
|
||||||
|
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
|
||||||
|
"requires": {
|
||||||
|
"accepts": "~1.3.7",
|
||||||
|
"array-flatten": "1.1.1",
|
||||||
|
"body-parser": "1.19.0",
|
||||||
|
"content-disposition": "0.5.3",
|
||||||
|
"content-type": "~1.0.4",
|
||||||
|
"cookie": "0.4.0",
|
||||||
|
"cookie-signature": "1.0.6",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "~1.1.2",
|
||||||
|
"encodeurl": "~1.0.2",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"etag": "~1.8.1",
|
||||||
|
"finalhandler": "~1.1.2",
|
||||||
|
"fresh": "0.5.2",
|
||||||
|
"merge-descriptors": "1.0.1",
|
||||||
|
"methods": "~1.1.2",
|
||||||
|
"on-finished": "~2.3.0",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"path-to-regexp": "0.1.7",
|
||||||
|
"proxy-addr": "~2.0.5",
|
||||||
|
"qs": "6.7.0",
|
||||||
|
"range-parser": "~1.2.1",
|
||||||
|
"safe-buffer": "5.1.2",
|
||||||
|
"send": "0.17.1",
|
||||||
|
"serve-static": "1.14.1",
|
||||||
|
"setprototypeof": "1.1.1",
|
||||||
|
"statuses": "~1.5.0",
|
||||||
|
"type-is": "~1.6.18",
|
||||||
|
"utils-merge": "1.0.1",
|
||||||
|
"vary": "~1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fill-range": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"to-regex-range": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"finalhandler": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
|
||||||
|
"requires": {
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"encodeurl": "~1.0.2",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"on-finished": "~2.3.0",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"statuses": "~1.5.0",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"follow-redirects": {
|
||||||
|
"version": "1.5.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
|
||||||
|
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
|
||||||
|
"requires": {
|
||||||
|
"debug": "=3.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"debug": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
|
||||||
|
"requires": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forwarded": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
|
||||||
|
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
|
||||||
|
},
|
||||||
|
"fresh": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
|
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
|
||||||
|
},
|
||||||
|
"fsevents": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"glob-parent": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http-errors": {
|
||||||
|
"version": "1.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
|
||||||
|
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
|
||||||
|
"requires": {
|
||||||
|
"depd": "~1.1.2",
|
||||||
|
"inherits": "2.0.3",
|
||||||
|
"setprototypeof": "1.1.1",
|
||||||
|
"statuses": ">= 1.5.0 < 2",
|
||||||
|
"toidentifier": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"iconv-lite": {
|
||||||
|
"version": "0.4.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"requires": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"inherits": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||||
|
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||||
|
},
|
||||||
|
"ipaddr.js": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="
|
||||||
|
},
|
||||||
|
"is-binary-path": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"binary-extensions": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-docker": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw=="
|
||||||
|
},
|
||||||
|
"is-extglob": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
|
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"is-glob": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-extglob": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is-number": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"livereload": {
|
||||||
|
"version": "0.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/livereload/-/livereload-0.9.1.tgz",
|
||||||
|
"integrity": "sha512-9g7sua11kkyZNo2hLRCG3LuZZwqexoyEyecSlV8cAsfAVVCZqLzVir6XDqmH0r+Vzgnd5LrdHDMyjtFnJQLAYw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"chokidar": "^3.3.0",
|
||||||
|
"livereload-js": "^3.1.0",
|
||||||
|
"opts": ">= 1.2.0",
|
||||||
|
"ws": "^6.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"livereload-js": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-CBu1gTEfzVhlOK1WASKAAJ9Qx1fHECTq0SUB67sfxwQssopTyvzqTlgl+c0h9pZ6V+Fzd2rc510ppuNusg9teQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"media-typer": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
|
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
|
||||||
|
},
|
||||||
|
"merge-descriptors": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
|
||||||
|
},
|
||||||
|
"methods": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
|
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
|
||||||
|
},
|
||||||
|
"mime": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
|
||||||
|
},
|
||||||
|
"mime-db": {
|
||||||
|
"version": "1.44.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz",
|
||||||
|
"integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg=="
|
||||||
|
},
|
||||||
|
"mime-types": {
|
||||||
|
"version": "2.1.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz",
|
||||||
|
"integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==",
|
||||||
|
"requires": {
|
||||||
|
"mime-db": "1.44.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
|
||||||
|
},
|
||||||
|
"negotiator": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
|
||||||
|
},
|
||||||
|
"normalize-path": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||||
|
},
|
||||||
|
"on-finished": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
|
"integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
|
||||||
|
"requires": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"open": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/open/-/open-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-lLPI5KgOwEYCDKXf4np7y1PBEkj7HYIyP2DY8mVDRnx0VIIu6bNrRB0R66TuO7Mack6EnTNLm4uvcl1UoklTpA==",
|
||||||
|
"requires": {
|
||||||
|
"is-docker": "^2.0.0",
|
||||||
|
"is-wsl": "^2.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"is-wsl": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||||
|
"requires": {
|
||||||
|
"is-docker": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"opts": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/opts/-/opts-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-rPleeyX48sBEc4aj7rAok5dCbvRdYpdbIdSRR4gnIK98a7Rvd4l3wlv4YHQr2mwPQTpKQiw8uipi/WoyItDINg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"parseurl": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
|
||||||
|
},
|
||||||
|
"path-to-regexp": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||||
|
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
|
||||||
|
},
|
||||||
|
"picomatch": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"proxy-addr": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==",
|
||||||
|
"requires": {
|
||||||
|
"forwarded": "~0.1.2",
|
||||||
|
"ipaddr.js": "1.9.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"qs": {
|
||||||
|
"version": "6.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
|
||||||
|
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
|
||||||
|
},
|
||||||
|
"range-parser": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
|
||||||
|
},
|
||||||
|
"raw-body": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
|
||||||
|
"requires": {
|
||||||
|
"bytes": "3.1.0",
|
||||||
|
"http-errors": "1.7.2",
|
||||||
|
"iconv-lite": "0.4.24",
|
||||||
|
"unpipe": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"readdirp": {
|
||||||
|
"version": "3.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz",
|
||||||
|
"integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||||
|
},
|
||||||
|
"safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
|
},
|
||||||
|
"send": {
|
||||||
|
"version": "0.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
|
||||||
|
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
|
||||||
|
"requires": {
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "~1.1.2",
|
||||||
|
"destroy": "~1.0.4",
|
||||||
|
"encodeurl": "~1.0.2",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"etag": "~1.8.1",
|
||||||
|
"fresh": "0.5.2",
|
||||||
|
"http-errors": "~1.7.2",
|
||||||
|
"mime": "1.6.0",
|
||||||
|
"ms": "2.1.1",
|
||||||
|
"on-finished": "~2.3.0",
|
||||||
|
"range-parser": "~1.2.1",
|
||||||
|
"statuses": "~1.5.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ms": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve-static": {
|
||||||
|
"version": "1.14.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
|
||||||
|
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
|
||||||
|
"requires": {
|
||||||
|
"encodeurl": "~1.0.2",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"send": "0.17.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setprototypeof": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
|
||||||
|
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
|
||||||
|
},
|
||||||
|
"to-regex-range": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"is-number": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"toidentifier": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
|
||||||
|
},
|
||||||
|
"type-is": {
|
||||||
|
"version": "1.6.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
|
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||||
|
"requires": {
|
||||||
|
"media-typer": "0.3.0",
|
||||||
|
"mime-types": "~2.1.24"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unpipe": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
|
||||||
|
},
|
||||||
|
"utils-merge": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
|
||||||
|
},
|
||||||
|
"vary": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
|
||||||
|
},
|
||||||
|
"ws": {
|
||||||
|
"version": "6.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
|
||||||
|
"integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"async-limiter": "~1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "pi-weather-station",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A weather station designed for the Raspberry Pi 7 inch touchscreen",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node ./server/index.js"
|
||||||
|
},
|
||||||
|
"author": "Eric Lewin",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.19.2",
|
||||||
|
"body-parser": "^1.19.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"open": "^7.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"connect-livereload": "^0.6.1",
|
||||||
|
"livereload": "^0.9.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Pi Weather Station
|
||||||
|
|
||||||
|
This is a weather station designed to be used with a Raspberry Pi on the official 7" 800x480 touchscreen.
|
||||||
|
|
||||||
|
The weather station will require you to have API keys from [Mapbox](https://www.mapbox.com/) and [ClimaCell](https://www.climacell.co/). Optionally, you can use an API key from [LocationIQ](https://locationiq.com/) to preform reverse geocoding.
|
||||||
|
|
||||||
|
Weather maps are provided by the [RainViewer](https://www.rainviewer.com/) API, which generously does not require an [API key](https://www.rainviewer.com/api.html).
|
||||||
|
|
||||||
|
> Be mindful of the plan limits for your API keys and understand the terms of each provider, as scrolling around the map and selecting different locations will incur API calls for every location. Additionally, the weather station will periodically make additional api calls to get weather updates throughout the day.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
To install, clone the repo and run
|
||||||
|
|
||||||
|
$ npm install
|
||||||
|
|
||||||
|
Start the server with
|
||||||
|
|
||||||
|
$ npm start
|
||||||
|
|
||||||
|
Now set point your browser to `http://localhost:8080` and put it in full screen mode (`F11` in Chromium).
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
- Your API keys are saved locally (in plain text) to `settings.json`.
|
||||||
|
- The server will attempt to get your default location, but if it cannot or you wish to choose a different default location, enter the latitude and longitude under `Custom Latitude` and `Custom Longitude` in settings, which can be accessed by tapping the gear button in the lower right hand corner.
|
||||||
|
- To hide the mouse cursor when using a touch screen, set `Hide Mouse` to `On`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2020 Eric Lewin
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
const axios = require("axios");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets coordinates from an external API
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function getCoords(req, res) {
|
||||||
|
axios
|
||||||
|
.get("https://freegeoip.app/json/")
|
||||||
|
.then((result) => {
|
||||||
|
return res.status(result.status).json(result.data).end();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return res.status(500).json(err).end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getCoords,
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const bodyParser = require("body-parser");
|
||||||
|
const path = require("path");
|
||||||
|
const cors = require("cors");
|
||||||
|
const open = require("open");
|
||||||
|
const ver = require("../package.json").version;
|
||||||
|
const appName = require("../package.json").name;
|
||||||
|
|
||||||
|
const settingsCtrl = require("./settingsCtrl");
|
||||||
|
const geolocationCtrl = require("./geolocationCtrl");
|
||||||
|
|
||||||
|
const {
|
||||||
|
getSettings,
|
||||||
|
setSetting,
|
||||||
|
deleteSetting,
|
||||||
|
createSettingsFile,
|
||||||
|
replaceSettings,
|
||||||
|
} = settingsCtrl;
|
||||||
|
const { getCoords } = geolocationCtrl;
|
||||||
|
|
||||||
|
const DIST_DIR = "/../client/dist";
|
||||||
|
const PORT = 8080;
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// ***** dev only:
|
||||||
|
// const livereload = require("livereload");
|
||||||
|
// const connectLivereload = require("connect-livereload");
|
||||||
|
// const liveReloadServer = livereload.createServer();
|
||||||
|
// liveReloadServer.watch(path.join(`${__dirname}/${DIST_DIR}`));
|
||||||
|
// liveReloadServer.server.once("connection", () => {
|
||||||
|
// setTimeout(() => {
|
||||||
|
// liveReloadServer.refresh("/");
|
||||||
|
// }, 100);
|
||||||
|
// });
|
||||||
|
// app.use(connectLivereload());
|
||||||
|
// *****
|
||||||
|
|
||||||
|
app.use(cors());
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
app.use(express.static(path.join(`${__dirname}/${DIST_DIR}`)));
|
||||||
|
app.listen(PORT, "localhost", async () => {
|
||||||
|
await open(`http://localhost:${PORT}`);
|
||||||
|
console.log(`${appName} v${ver} has started on port ${PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get("/settings", getSettings);
|
||||||
|
app.post("/settings", createSettingsFile);
|
||||||
|
app.put("/settings", replaceSettings);
|
||||||
|
app.patch("/setting", setSetting);
|
||||||
|
app.delete("/setting", deleteSetting);
|
||||||
|
|
||||||
|
app.get("/geolocation", getCoords);
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const SETTINGS_FILE = "../settings.json";
|
||||||
|
const FILE_PATH = path.join(`${__dirname}/${SETTINGS_FILE}`);
|
||||||
|
const ENCODING = "utf8";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the settings.json file
|
||||||
|
*
|
||||||
|
* @param {Object} callbacks
|
||||||
|
* @param {Function} callbacks.successCb
|
||||||
|
* @param {Function} callbacks.errorCb
|
||||||
|
*/
|
||||||
|
function readSettingsFile({ successCb, errorCb }) {
|
||||||
|
fs.readFile(FILE_PATH, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
errorCb(err);
|
||||||
|
} else {
|
||||||
|
successCb(JSON.parse(data));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a `settings.json` file
|
||||||
|
*
|
||||||
|
* @param {Object} req
|
||||||
|
* @param {Object} [req.body]
|
||||||
|
* @param {Object} res
|
||||||
|
*/
|
||||||
|
function createSettingsFile(req, res) {
|
||||||
|
const contents = req.body || {};
|
||||||
|
|
||||||
|
if (fs.existsSync(FILE_PATH)) {
|
||||||
|
return res.status(409).json("settings file already exists").end();
|
||||||
|
} else {
|
||||||
|
fs.writeFile(FILE_PATH, JSON.stringify(contents), ENCODING, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json(err).end();
|
||||||
|
} else {
|
||||||
|
return res.status(201).json(contents).end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the settings.json file
|
||||||
|
*/
|
||||||
|
function getSettings(req, res) {
|
||||||
|
if (!fs.existsSync(FILE_PATH)) {
|
||||||
|
return res.status(404).json("settings.json not found!").end();
|
||||||
|
}
|
||||||
|
|
||||||
|
readSettingsFile({
|
||||||
|
successCb: (data) => {
|
||||||
|
return res.status(200).json(data).end();
|
||||||
|
},
|
||||||
|
errorCb: (err) => {
|
||||||
|
return res.status(500).end();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a single setting. Creates a new `settings.json` file if none exists.
|
||||||
|
*
|
||||||
|
* @param {Object} req
|
||||||
|
* @param {Object} res
|
||||||
|
*/
|
||||||
|
function setSetting(req, res) {
|
||||||
|
const { key, val } = req.body;
|
||||||
|
if (!key || !val) {
|
||||||
|
return res.status(400).json("You must supply a key and val").end();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes file contents
|
||||||
|
*
|
||||||
|
* @param {Object} newSettings
|
||||||
|
* @param {Boolean} [newFile] If file is new
|
||||||
|
*/
|
||||||
|
const writeContents = (newSettings, newFile) => {
|
||||||
|
fs.writeFile(FILE_PATH, JSON.stringify(newSettings), ENCODING, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json(err).end();
|
||||||
|
} else {
|
||||||
|
return res
|
||||||
|
.status(newFile ? 201 : 200)
|
||||||
|
.json(newSettings)
|
||||||
|
.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read success callback
|
||||||
|
*
|
||||||
|
* @param {Object} currentSettings
|
||||||
|
*/
|
||||||
|
const readSuccess = (currentSettings) => {
|
||||||
|
const newSettings = {
|
||||||
|
...currentSettings,
|
||||||
|
[key]: val,
|
||||||
|
};
|
||||||
|
writeContents(newSettings);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read error callback
|
||||||
|
*
|
||||||
|
* @param {Object} [err]
|
||||||
|
*/
|
||||||
|
const readError = (err) => {
|
||||||
|
return res.status(500).json(err).end();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!fs.existsSync(FILE_PATH)) {
|
||||||
|
writeContents({ [key]: val }, true);
|
||||||
|
} else {
|
||||||
|
readSettingsFile({
|
||||||
|
successCb: readSuccess,
|
||||||
|
errorCb: readError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceSettings(req, res) {
|
||||||
|
const { body } = req;
|
||||||
|
if (!body) {
|
||||||
|
return res.status(400).json("You must provide settings contents").end();
|
||||||
|
}
|
||||||
|
const fileExists = fs.existsSync(FILE_PATH);
|
||||||
|
|
||||||
|
fs.writeFile(FILE_PATH, JSON.stringify(body), ENCODING, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json(err).end();
|
||||||
|
} else {
|
||||||
|
return res
|
||||||
|
.status(fileExists ? 200 : 201)
|
||||||
|
.json(body)
|
||||||
|
.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a specific setting
|
||||||
|
*
|
||||||
|
* @param {Object} req
|
||||||
|
* @param {Object} req.query
|
||||||
|
* @param {Object} req.query.key The key to be deleted
|
||||||
|
* @param {Object} res
|
||||||
|
*/
|
||||||
|
function deleteSetting(req, res) {
|
||||||
|
const { key } = req.query;
|
||||||
|
if (!key) {
|
||||||
|
return res.status(400).json("You must supply a key to delete").end();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read success callback
|
||||||
|
*
|
||||||
|
* @param {Object} currentSettings
|
||||||
|
*/
|
||||||
|
const readSuccess = (currentSettings) => {
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(currentSettings, key)) {
|
||||||
|
return res.status(404).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
delete currentSettings[key];
|
||||||
|
|
||||||
|
fs.writeFile(
|
||||||
|
FILE_PATH,
|
||||||
|
JSON.stringify(currentSettings),
|
||||||
|
ENCODING,
|
||||||
|
(err) => {
|
||||||
|
if (err) {
|
||||||
|
return res.status(500).json(err).end();
|
||||||
|
} else {
|
||||||
|
return res.status(200).json(currentSettings).end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error callback
|
||||||
|
*
|
||||||
|
* @param {Object} err
|
||||||
|
*/
|
||||||
|
const readError = (err) => {
|
||||||
|
return res.status(500).json(err).end();
|
||||||
|
};
|
||||||
|
|
||||||
|
readSettingsFile({
|
||||||
|
successCb: readSuccess,
|
||||||
|
errorCb: readError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getSettings,
|
||||||
|
setSetting,
|
||||||
|
deleteSetting,
|
||||||
|
createSettingsFile,
|
||||||
|
replaceSettings,
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"weatherApiKey": "key",
|
||||||
|
"mapApiKey": "key",
|
||||||
|
"reverseGeoApiKey": "key",
|
||||||
|
"startingLat": "32.7473",
|
||||||
|
"startingLon": "-97.0945"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user