diff --git a/package-lock.json b/package-lock.json index c8489b4..6a3edaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5130,9 +5130,9 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "eventemitter3": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", - "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz", + "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==" }, "events": { "version": "3.1.0", @@ -6297,9 +6297,9 @@ "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" }, "http-proxy": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", - "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "requires": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", @@ -10990,6 +10990,18 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "react-redux": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.0.tgz", + "integrity": "sha512-EvCAZYGfOLqwV7gh849xy9/pt55rJXPwmYvI4lilPM5rUT/1NxuuN59ipdBksRVSvz0KInbPnp4IfoXJXCqiDA==", + "requires": { + "@babel/runtime": "^7.5.5", + "hoist-non-react-statics": "^3.3.0", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^16.9.0" + } + }, "react-router": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz", @@ -11198,6 +11210,15 @@ "strip-indent": "^3.0.0" } }, + "redux": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz", + "integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, "regenerate": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", @@ -11418,6 +11439,11 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "reselect": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.0.0.tgz", + "integrity": "sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==" + }, "resolve": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.0.tgz", @@ -12659,6 +12685,11 @@ "util.promisify": "~1.0.0" } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", diff --git a/package.json b/package.json index 28d6450..ac57993 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,11 @@ "novelcovid": "^1.2.7", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-redux": "^7.2.0", "react-router-dom": "^5.2.0", - "react-scripts": "3.4.1" + "react-scripts": "3.4.1", + "redux": "^4.0.5", + "reselect": "^4.0.0" }, "scripts": { "start": "react-scripts start", diff --git a/src/API/API.js b/src/API/API.js index 2681fa3..9111625 100644 --- a/src/API/API.js +++ b/src/API/API.js @@ -1,95 +1,202 @@ -import { NovelCovid } from 'novelcovid'; +import { NovelCovid } from "novelcovid"; const track = new NovelCovid(); -const WORLD_POPULATION = 7_784_000_000; //Approximately the world population now. Should be constant for the sake of this progam. +const WORLD_POPULATION = 7_784_000_000; //Approximately the world population now. Should be constant for the sake of this program. export async function getCountryData(country) { - try { - const response = await track.countries(country); //unfortunately, has to fetch all countries even if one is needed to find the ranking - console.log(response) - if (response.message === "Country not found or doesn't have any cases") { + if ( + response.message === "Country not found or doesn't have any cases" + ) { throw response.message; } - let res = { //TEMP + let res = { + //TEMP cases: response.cases, recovered: response.recovered, deaths: response.deaths, - casepermil: response.casesPerOneMillion, - testpermil: response.testsPerOneMillion, + casesPerOneMillion: response.casesPerOneMillion, + testsPerOneMillion: response.testsPerOneMillion, tests: response.tests, todayCases: response.todayCases, - todayDeaths: response.todayDeaths + todayDeaths: response.todayDeaths, }; - let restructuredResponse = { //TEMP, until rankings are added and the fetch request rewritten. + let restructuredResponse = { + //TEMP, until rankings are added and the fetch request rewritten. covidinfo: { - cases: { title: "Total Cases", number: res.cases, ranking: null, daily: (res.todayCases > 0 && "+") + res.todayCases }, - recovered: { title: "Recovered", number: res.recovered, ranking: null, daily: null }, - deaths: { title: "Died", number: res.deaths, ranking: null, daily: (res.todayDeaths > 0 && "+") + res.todayDeaths }, - casepermil: { title: "Cases per million", number: res.casepermil, ranking: null, daily: null}, - recoverate: {title: "Recovery rate", number: res.cases > 0 ? (100 * res.recovered / res.cases).toFixed(2) + '%' : null}, - mortality: { title: "Mortality", number: res.cases > 0 ? (100 * res.deaths / res.cases).toFixed(2) + '%' : "N/A", ranking: null, daily: null }, - tests: { title: "Tests", number: res.tests, ranking: null, daily: null }, - testpermil: {title: "Tests per million", number: res.testpermil, ranking: null, daily: null}, - testrate: { title: "Positive Tests", number: res.cases > 0 ? (100 * res.cases / res.tests).toFixed(2) + '%' : "N/A", ranking: null, daily: null } + cases: { + title: "Total Cases", + number: res.cases, + ranking: null, + daily: (res.todayCases > 0 && "+") + res.todayCases, + }, + recovered: { + title: "Recovered", + number: res.recovered, + ranking: null, + daily: null, + }, + deaths: { + title: "Died", + number: res.deaths, + ranking: null, + daily: (res.todayDeaths > 0 && "+") + res.todayDeaths, + }, + casesPerOneMillion: { + title: "Cases per million", + number: res.casesPerOneMillion, + ranking: null, + daily: null, + }, + recoverate: { + title: "Recovery rate", + number: + res.cases > 0 + ? ((100 * res.recovered) / res.cases).toFixed(2) + + "%" + : null, + }, + mortality: { + title: "Mortality", + number: + res.cases > 0 + ? ((100 * res.deaths) / res.cases).toFixed(2) + "%" + : "N/A", + ranking: null, + daily: null, + }, + tests: { + title: "Tests", + number: res.tests, + ranking: null, + daily: null, + }, + testsPerOneMillion: { + title: "Tests per million", + number: res.testsPerOneMillion, + ranking: null, + daily: null, + }, + testrate: { + title: "Positive Tests", + number: + res.cases > 0 + ? ((100 * res.cases) / res.tests).toFixed(2) + "%" + : "N/A", + ranking: null, + daily: null, + }, }, country: response.country, - countryIcon: response.countryInfo.flag + countryIcon: response.countryInfo.flag, }; return restructuredResponse; - } catch (reason) { - console.log(`The reason is: ${reason}`); return null; } - - -}; +} export async function getEachCountryData(country) { - try { - const response = await track.countries(); - // console.log(`Sample response ${response}`); - console.log(response); let result = []; for (let entry of response) { if (entry.country.toLowerCase().startsWith(country.toLowerCase())) { result.push({ covidinfo: { - cases: { title: "Total Cases", number: entry.cases, ranking: null, daily: null }, - recovered: { title: "Recovered", number: entry.recovered, ranking: null, daily: null }, - deaths: { title: "Died", number: entry.deaths, ranking: null, daily: null }, - tests: { title: "Tests", number: entry.tests, ranking: null, daily: null } + cases: { + title: "Total Cases", + number: entry.cases, + ranking: null, + daily: null, + }, + recovered: { + title: "Recovered", + number: entry.recovered, + ranking: null, + daily: null, + }, + deaths: { + title: "Died", + number: entry.deaths, + ranking: null, + daily: null, + }, + casesPerOneMillion: { + title: "Cases per million", + number: entry.casesPerOneMillion, + ranking: null, + daily: null, + }, + recoverate: { + title: "Recovery rate", + number: + entry.cases > 0 + ? ( + (100 * entry.recovered) / + entry.cases + ).toFixed(2) + "%" + : null, + }, + mortality: { + title: "Mortality", + number: + entry.cases > 0 + ? ( + (100 * entry.deaths) / + entry.cases + ).toFixed(2) + "%" + : "N/A", + ranking: null, + daily: null, + }, + tests: { + title: "Tests", + number: entry.tests, + ranking: null, + daily: null, + }, + testsPerOneMillion: { + title: "Tests per million", + number: entry.testsPerOneMillion, + ranking: null, + daily: null, + }, + testrate: { + title: "Positive Tests", + number: + entry.cases > 0 + ? ( + (100 * entry.cases) / + entry.tests + ).toFixed(2) + "%" + : "N/A", + ranking: null, + daily: null, + }, }, country: entry.country, - countryIcon: entry.countryInfo.flag + iso2: entry.countryInfo.iso2 || "", + iso3: entry.countryInfo.iso3 || "", + countryIcon: entry.countryInfo.flag, }); } } - console.log(result) if (result.length === 0) { throw new Error("No match"); } return result; - } catch (reason) { - console.log(`The reason is: ${reason}`); return null; } -}; - +} export async function getGlobalData() { - try { - const response = await track.countries(); - console.log(response); let res = response.reduce((x, y) => ({ cases: x.cases + y.cases, recovered: x.recovered + y.recovered, @@ -98,29 +205,90 @@ export async function getGlobalData() { testsPerOneMillion: x.testsPerOneMillion + y.testsPerOneMillion, tests: x.tests + y.tests, todayCases: x.todayCases + y.todayCases, - todayDeaths: x.todayDeaths + y.todayDeaths - })) - console.log(res); + todayDeaths: x.todayDeaths + y.todayDeaths, + })); let restructuredResponse = { covidinfo: { - cases: { title: "Total Cases", number: res.cases, ranking: null, daily: (res.todayCases > 0 && "+") + res.todayCases }, - recovered: { title: "Recovered", number: res.recovered, ranking: null, daily: null }, - deaths: { title: "Died", number: res.deaths, ranking: null, daily: (res.todayDeaths > 0 && "+") + res.todayDeaths }, - casepermil: { title: "Cases per million", number: (100_000_0 * res.cases / WORLD_POPULATION).toFixed(0), ranking: null, daily: null}, - recoverate: { title: "Recovery rate", number: res.cases > 0 ? (100 * res.recovered / res.cases).toFixed(2) + '%' : null}, - mortality: { title: "Mortality", number: res.cases > 0 ? (100 * res.deaths / res.cases).toFixed(2) + '%' : "N/A", ranking: null, daily: null }, - tests: { title: "Tests", number: res.tests, ranking: null, daily: null }, - testpermil: { title: "Tests per million", number: (100_000_0 * res.tests / WORLD_POPULATION).toFixed(0), ranking: null, daily: null}, - testrate: { title: "Positive Tests", number: res.cases > 0 ? (100 * res.cases / res.tests).toFixed(2) + '%' : "N/A", ranking: null, daily: null } - } + cases: { + title: "Total Cases", + number: res.cases, + ranking: null, + daily: (res.todayCases > 0 && "+") + res.todayCases, + }, + recovered: { + title: "Recovered", + number: res.recovered, + ranking: null, + daily: null, + }, + deaths: { + title: "Died", + number: res.deaths, + ranking: null, + daily: (res.todayDeaths > 0 && "+") + res.todayDeaths, + }, + casesPerOneMillion: { + title: "Cases per million", + number: ( + (100_000_0 * res.cases) / + WORLD_POPULATION + ).toFixed(0), + ranking: null, + daily: null, + }, + recoverate: { + title: "Recovery rate", + number: + res.cases > 0 + ? ((100 * res.recovered) / res.cases).toFixed(2) + + "%" + : null, + }, + mortality: { + title: "Mortality", + number: + res.cases > 0 + ? ((100 * res.deaths) / res.cases).toFixed(2) + "%" + : "N/A", + ranking: null, + daily: null, + }, + tests: { + title: "Tests", + number: res.tests, + ranking: null, + daily: null, + }, + testsPerOneMillion: { + title: "Tests per million", + number: ( + (100_000_0 * res.tests) / + WORLD_POPULATION + ).toFixed(0), + ranking: null, + daily: null, + }, + testrate: { + title: "Positive Tests", + number: + res.cases > 0 + ? ((100 * res.cases) / res.tests).toFixed(2) + "%" + : "N/A", + ranking: null, + daily: null, + }, + }, }; return restructuredResponse; - } catch (reason) { - console.log(`The reason is: ${reason}`); return null; } +} - -}; \ No newline at end of file +export async function getEachCountryAndGlobal(country) { + return { + countries: await getEachCountryData(country), + global: await getGlobalData(), + }; +} diff --git a/src/App.js b/src/App.js index f91655a..ae7d3f9 100644 --- a/src/App.js +++ b/src/App.js @@ -1,91 +1,61 @@ -import React from 'react'; -import { Body, Search, Navbar, Ranking } from './components'; -import { getGlobalData, getEachCountryData } from './API/API.js'; -import { Switch, Route } from 'react-router-dom'; -import './App.css'; - +import React from "react"; +import { Body, Search, Navbar, Ranking } from "./components"; +import { getEachCountryAndGlobal } from "./API/API.js"; +import { Switch, Route, BrowserRouter } from "react-router-dom"; +import { Provider } from "react-redux"; +import "./App.css"; +import { createStore, combineReducers } from "redux"; +import { countryReducer, inputReducer, navigationReducer } from "./reducers"; +import { updateCountryData } from "./actions"; + +const store = createStore( + combineReducers({ + data: countryReducer, + input: inputReducer, + nav: navigationReducer, + }) +); +//TODO handle history through react-redux-router, async data directly through redux. class App extends React.Component { - constructor() { - super(); - this.state = { - data: null, - rating_data: null, - path_changed: false - } - } - - async componentDidMount() { - - console.log("Awaited"); - this.setState({ - rating_data: await getEachCountryData(""), //displays all countries by default - data: await getGlobalData() //displays global data by default - }); - } - - handleChangeCountry = async (info) => { - - if (window.location.pathname === "/") { - this.setState({ - data: await info - }); - } else { - this.setState({ - rating_data: await info - }) + async componentDidMount() { + let response = await getEachCountryAndGlobal(""); //fetches all countries by default + store.dispatch(updateCountryData(response)); } - console.log("The state has been reset"); - } - handleStateFlush = async (addr) => { - - if (addr === "/") { - this.setState({ - data: await getGlobalData() - }); - } else { - this.setState({ - rating_data: await getEachCountryData("") - }); + render() { + //Notice the components wrapped in routes. This is one way to pass a common history to each component + return ( + + +
+ +
+
+

Covid-19 Tracker

+
+
+ +
+
+ + + + + + +
+
+
+
+
+ ); } - - } - - render() { - - if (this.state.data === null || this.state.rating_data === null) { - return null; - } - - - return ( - -
- -
-
-

- Covid-19 Tracker -

-
-
- -
-
- - - - - - - - -
-
-
- ); - } - -}; +} export default App; diff --git a/src/actions/creators.js b/src/actions/creators.js new file mode 100644 index 0000000..dbdc6ba --- /dev/null +++ b/src/actions/creators.js @@ -0,0 +1,21 @@ +import {UPDATE_COUNTRIES, UPDATE_QUERY, CHANGE_LOCATION, SET_ERROR} from './'; + +export const updateCountryData = (payload) => ({ + type: UPDATE_COUNTRIES, + payload: {...payload} +}) + +export const updateInputField = (payload) => ({ + type: UPDATE_QUERY, + payload: { value: payload } +}) + +export const updateLocation = (payload) => ({ + type: CHANGE_LOCATION, + payload: { location : payload } +}) + +export const updateError = (payload) => ({ + type: SET_ERROR, + payload: { error : payload } +}) \ No newline at end of file diff --git a/src/actions/index.js b/src/actions/index.js new file mode 100644 index 0000000..0e53704 --- /dev/null +++ b/src/actions/index.js @@ -0,0 +1,2 @@ +export { UPDATE_COUNTRIES, UPDATE_QUERY, SUBMIT_QUERY, CHANGE_LOCATION, SET_ERROR } from './types.js'; +export { updateCountryData, updateInputField, updateLocation, updateError } from './creators.js'; \ No newline at end of file diff --git a/src/actions/types.js b/src/actions/types.js new file mode 100644 index 0000000..60ae23d --- /dev/null +++ b/src/actions/types.js @@ -0,0 +1,9 @@ +export const UPDATE_COUNTRIES = "UPDATE_COUNTRIES"; + +export const UPDATE_QUERY = "UPDATE_QUERY"; + +export const SUBMIT_QUERY = "SUBMIT_QUERY"; //Not used yet. + +export const CHANGE_LOCATION = "CHANGE_LOCATION"; + +export const SET_ERROR = "SET_ERROR"; \ No newline at end of file diff --git a/src/components/Body/Body.js b/src/components/Body/Body.js index d890f54..7e31ab4 100644 --- a/src/components/Body/Body.js +++ b/src/components/Body/Body.js @@ -1,19 +1,24 @@ -import React from 'react'; -import './Body.css' -import { Card } from '../Cards/Cards.js'; +import React from "react"; +import { singleCountrySelector } from "../../selectors/selectors"; +import { connect } from "react-redux"; +import { Card } from "../Cards/Cards.js"; +import "./Body.css"; -export class Body extends React.Component { - - // fetchCountryData = () => { - - // this.setState({ - // data: getCountryData() - // }); - // } +//TODO Rewrite async data handling through redux. +class Body extends React.Component { render() { + let countryData; + const isGlobal = + !("country" in this.props.match.params) || + ["global", "world"].indexOf(this.props.match.params["country"]) > -1; + + countryData = this.props.country; //for backwards compatibility + + if (!countryData || JSON.stringify(countryData) === "{}") { + console.log("Caught"); + return null; //Either an empty object or untruthy. + } - console.log("Im here"); - const isGlobal = !this.props.data["country"]; let heading; if (isGlobal) { heading = ( @@ -26,14 +31,13 @@ export class Body extends React.Component {
COUNTRY:
- -

{this.props.data["country"]}

+ +

{countryData.country}

); } - // console.log(`Before returning, this.props.data == ${this.props.data}, this.props.data.country == ${this.props.data["country"]}, this.props.data.cases == ${this.props.data.cases} `) - // {console.log(`Trying to get covidinfo, ${JSON.stringify(this.props.data)}; ${JSON.stringify(this.props.data.covidinfo)}`)} + return (
@@ -42,11 +46,23 @@ export class Body extends React.Component {
- {Object.values(this.props.data.covidinfo).map((entry) => - - )} + {Object.values(countryData.covidinfo).map((entry) => ( + + ))}
); - }; -}; \ No newline at end of file + } +} + +const mapStateToProps = (state, props) => ({ + country: singleCountrySelector(state, props) //This allows for memoization of selectors +}); + +export default connect(mapStateToProps)(Body); diff --git a/src/components/Cards/Cards.js b/src/components/Cards/Cards.js index 2803aa7..289d610 100644 --- a/src/components/Cards/Cards.js +++ b/src/components/Cards/Cards.js @@ -2,7 +2,7 @@ import React from 'react'; import './Cards.css'; export const Card = (props) => { - console.log(`${JSON.stringify(props.title)}, ${JSON.stringify(props.number)}`); + return (

{props.title}

{props.ranking && Ranked {props.ranking}} diff --git a/src/components/Navbar/Navbar.css b/src/components/Navbar/Navbar.css index 2000749..eae4195 100644 --- a/src/components/Navbar/Navbar.css +++ b/src/components/Navbar/Navbar.css @@ -1,24 +1,7 @@ -/* .nav-container-old { - display: grid; - grid-template-rows: 1fr; - grid-template-columns: 1fr 1fr; - justify-items: start; - justify-self: start; - gap: 10px; - padding: 4px; - background-color: #00d39a; - color: whitesmoke; - margin-bottom: 10px; - font-family: sans-serif; - font-weight: bold; - font-size: large; - width: 100%; -} */ - .nav-container { background-color: #333; overflow: hidden; - /* box-shadow: 0 4px 20px 0 rgba(0,0,0,.14), 0 7px 12px -5px rgba(24, 73, 56, 0.46); */ + } .nav-container a { @@ -54,20 +37,3 @@ background-color: #00d39a; color: white; } -/* -a:link { - font-family: sans-serif; - text-decoration: none; - color: whitesmoke; -} - -a:active { - background: whitesmoke; - color: #00d39a; -} - -.link { - font-family: sans-serif; - color: whitesmoke; - -} */ \ No newline at end of file diff --git a/src/components/Navbar/Navbar.js b/src/components/Navbar/Navbar.js index 4ae960e..044c918 100644 --- a/src/components/Navbar/Navbar.js +++ b/src/components/Navbar/Navbar.js @@ -1,17 +1,28 @@ import React from 'react'; import { Link } from 'react-router-dom'; import './Navbar.css' +import { connect } from 'react-redux'; +import { updateLocation } from '../../actions/'; -export function Navbar(props) { +//TODO use react-redux-router extension to syncronize history through redux state. +function Navbar(props) { return ( ); -} \ No newline at end of file +} + +const mapStateToProps = (state) => ({ + location: state.nav.location +}) + +const mapDispatchToProps = (dispatch) => ({ + setLocation: (location) => dispatch(updateLocation(location)) +}) + +export default connect(mapStateToProps, mapDispatchToProps)(Navbar); \ No newline at end of file diff --git a/src/components/Ranking/Ranking.js b/src/components/Ranking/Ranking.js index 5e6fd01..f67549e 100644 --- a/src/components/Ranking/Ranking.js +++ b/src/components/Ranking/Ranking.js @@ -1,27 +1,64 @@ -import React from 'react'; -import { Card } from '../Cards/Cards.js'; -import './Ranking.css' +import React from "react"; +import { Card } from "../Cards/Cards.js"; +import "./Ranking.css"; +import { similarCountriesSelector } from "../../selectors/selectors.js"; +import { connect } from "react-redux"; +import { updateError } from "../../actions/"; -export class Ranking extends React.Component { +class Ranking extends React.Component { render() { + + if (!this.props.countries || JSON.stringify(this.props.countries) === "{}") { + console.log("Caught"); + return null; //Either an empty object or untruthy. + } + return (
- {this.props.data.map((entry) => + {this.props.countries.map((entry) => (
- +
{entry.country}
- {Object.values(entry.covidinfo).map((data_entry) => - - )} + {Object.values(entry.covidinfo) + .filter( + (elem) => + [ + "Total Cases", + "Recovered", + "Died", + "Tests", + ].indexOf(elem.title) > -1 + ) + .map((dataEntry) => ( + + ))}
- )} + ))}
- ); //Placeholder, to be implemented. + ); } -}; +} + +const mapStateToProps = (state, props) => ({ + countries: similarCountriesSelector(state, props) +}); + +const mapDispatchToProps = (dispatch) => ({ + setError: (value) => dispatch(updateError(value)), +}); -export default Ranking; \ No newline at end of file +export default connect(mapStateToProps, mapDispatchToProps)(Ranking); diff --git a/src/components/Search/Search.js b/src/components/Search/Search.js index 3e04b2f..87ea33c 100644 --- a/src/components/Search/Search.js +++ b/src/components/Search/Search.js @@ -1,73 +1,66 @@ -import React from 'react'; -import { getCountryData, getEachCountryData } from "../../API/API.js"; -import './Search.css' +import React from "react"; +import "./Search.css"; +import { connect } from "react-redux"; +import { updateInputField, updateError } from "../../actions"; +import { selectClosestMatch } from "../../selectors/selectors.js"; -export class Search extends React.Component { +class Search extends React.Component { + //redux refator -- done - constructor(props) { - super(props); - this.state = { - value: "", - error: false - }; + shouldComponentUpdate(nextProps) { + return this.props.error !== nextProps.error; //Otherwise, no need to re-render. } - shouldComponentUpdate(nextProps, nextState) { - return this.state.error !== nextState.error; //Otherwise, no need to re-render. - } - - handleSubmit = async (event) => { - + handleSubmit = (event) => { if (event.type === "click" || event.key === "Enter") { + //Much simpler now with redux! - const country = this.state.value; - let result; - if (window.location.pathname === "/ranking") { - result = await getEachCountryData(country); + const countryQuery = this.props.getCountry(this.props.value); + if (!countryQuery) { + this.props.setError(true); } else { - result = await getCountryData(country); - } - console.log(`The location is ${window.location.pathname}`) - - console.log(`country ${country} is null ${result == null}`) - if (result != null) { - if (this.state.error) { - this.setState({ - error: false - }); - } - this.props.onCountryChange(result); //perform a callback to the parent component - } else { - console.log("Error should occur"); - this.setState({ - error: true - }); - console.log(this.state.error); + this.props.setError(false); + this.props.history.push(this.props.location + this.props.value); } } }; handleChange = (event) => { - - this.setState({ - value: event.target.value - }); - - } + this.props.setValue(event.target.value); + }; componentDidUpdate() { - console.log("---Component has been updated."); + console.log("---Component has been updated.---"); } render() { return (
- - - - - {/* {this.state.error && } */} + +
); } -} \ No newline at end of file +} + +const mapStateToProps = (state) => ({ + location: state.nav.location, + error: state.input.error, + value: state.input.value, + getCountry: (country) => selectClosestMatch(country, state.data), +}); + +const mapDispatchToProps = (dispatch) => ({ + setValue: (value) => dispatch(updateInputField(value)), + setError: (value) => dispatch(updateError(value)), +}); + +export default connect(mapStateToProps, mapDispatchToProps)(Search); diff --git a/src/components/index.js b/src/components/index.js index ba573c3..137d323 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,6 +1,10 @@ -export {Body} from './Body/Body.js'; -export {Search} from './Search/Search.js'; -// export {Cards} from './Body/Body.js'; -export { Card } from './Cards/Cards.js' -export { Ranking } from './Ranking/Ranking.js' -export { Navbar } from './Navbar/Navbar.js' \ No newline at end of file +import Body from './Body/Body.js'; +import Search from './Search/Search.js'; +import Navbar from './Navbar/Navbar.js'; +import Ranking from './Ranking/Ranking.js'; + +export { Body }; +export { Search }; +export { Navbar }; +export { Card } from './Cards/Cards.js'; +export { Ranking }; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 6c894e5..00545e4 100644 --- a/src/index.js +++ b/src/index.js @@ -3,12 +3,9 @@ import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; -import {BrowserRouter} from 'react-router-dom'; ReactDOM.render( - - - , + , document.getElementById('root') ); diff --git a/src/reducers/index.js b/src/reducers/index.js new file mode 100644 index 0000000..d06d448 --- /dev/null +++ b/src/reducers/index.js @@ -0,0 +1 @@ +export { countryReducer, navigationReducer, inputReducer } from "./reducers.js"; \ No newline at end of file diff --git a/src/reducers/reducers.js b/src/reducers/reducers.js new file mode 100644 index 0000000..3883fe9 --- /dev/null +++ b/src/reducers/reducers.js @@ -0,0 +1,61 @@ +import { CHANGE_LOCATION, UPDATE_COUNTRIES, UPDATE_QUERY, SET_ERROR } from '../actions/'; + +const initialCountryState = { + countries: [], + global: {} +} + +export const countryReducer = (state = initialCountryState, { type, payload }) => { + switch (type) { + + case UPDATE_COUNTRIES: + return { + ...state, + countries: [...state.countries, ...payload.countries], + global: { ...payload.global } + }; + + default: + return state; + } +} + +const initialInputState = { + inputField: "", + error: false +} + +export const inputReducer = (state = initialInputState, { type, payload }) => { + switch (type) { + case UPDATE_QUERY: + return { + ...state, + ...payload + }; + case SET_ERROR: + return { + ...state, + ...payload + }; + default: + return state; + } +}; + +const initialNavigationState = { + location: "/" +}; + +export const navigationReducer = (state = initialNavigationState, { type, payload }) => { + switch (type) { + case CHANGE_LOCATION: + return { + ...state, + ...payload + }; + + default: + return state; + } +}; + diff --git a/src/selectors/selectors.js b/src/selectors/selectors.js new file mode 100644 index 0000000..049a3a0 --- /dev/null +++ b/src/selectors/selectors.js @@ -0,0 +1,59 @@ +import { createSelector } from 'reselect'; + +//SELECT country, countryIcon, covidinfo FROM countries WHERE country ILIKE '%countryName%; -- SQL analogy. +export function selectCountriesLike(countryName, countries) { + countryName = countryName.toLowerCase(); + if ( + countryName === "" || + countryName === "global" || + countryName === "world" + ) { + return countries; + } + return countries.filter( + (entry) => + entry.country.toLowerCase().startsWith(countryName) || + entry.iso2.toLowerCase().startsWith(countryName) || + entry.iso3.toLowerCase().startsWith(countryName) + ); +} + +export function selectClosestMatch(countryName, { countries, global }) { + + if ( //two additional checks redundant, but for the sake of brewity/compatibility, they're kept + countryName === "" || + countryName === "global" || + countryName === "world" + ) { + return global; + } + let match; + for (let entry of countries) { + if ( + entry.country.toLowerCase() === countryName || + entry.iso2.toLowerCase() === countryName || + entry.iso3.toLowerCase() === countryName + ) { + return entry; + } else if ( + entry.country.toLowerCase().startsWith(countryName) || + entry.iso2.toLowerCase().startsWith(countryName) || + entry.iso3.toLowerCase().startsWith(countryName) + ) { + match = entry; + } + } + return match; +} + +export const singleCountrySelector = createSelector( + (state, props) => ("country" in props.match.params && ["world", "global"].indexOf(props.match.params.country) <= -1) ? props.match.params.country : "global", + (state) => state.data, + (country, data) => selectClosestMatch(country, data) +) + +export const similarCountriesSelector = createSelector( + (state, props) => ("country" in props.match.params && ["world", "global"].indexOf(props.match.params.country) <= -1) ? props.match.params.country : "global", + (state) => state.data.countries, + (country, data) => selectCountriesLike(country, data) +)