├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── src ├── assets │ ├── earth-night.jpg │ └── night-sky.png ├── setupTests.js ├── App.test.js ├── index.css ├── lib │ ├── httpClient.js │ ├── utils.js │ └── countryList.js ├── index.js ├── components │ ├── Dialog.js │ ├── Modal.js │ ├── Chart.js │ ├── Drawer.js │ ├── MapChart.js │ └── Tab.js ├── logo.svg ├── App.js ├── pages │ └── home.js └── serviceWorker.js ├── docker-compose.yml ├── Dockerfile ├── .gitignore ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/covid19-banned-flights/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/covid19-banned-flights/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/covid19-banned-flights/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/assets/earth-night.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/covid19-banned-flights/HEAD/src/assets/earth-night.jpg -------------------------------------------------------------------------------- /src/assets/night-sky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maschad/covid19-banned-flights/HEAD/src/assets/night-sky.png -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | web: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | volumes: 9 | - '.:/app' 10 | - '/app/node_modules' 11 | ports: 12 | - "3001:3000" 13 | environment: 14 | - NODE_ENV=development -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.10.0-alpine 2 | 3 | # set working directory 4 | WORKDIR /app 5 | 6 | # add `/app/node_modules/.bin` to $PATH 7 | ENV PATH /app/node_modules/.bin:$PATH 8 | 9 | COPY yarn.lock /app/yarn.lock 10 | COPY . /app/ 11 | 12 | RUN yarn 13 | 14 | # start app 15 | CMD ["yarn", "start"] -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | .pnp 7 | 8 | .pnp 9 | .pnp.js 10 | 11 | # testing 12 | coverage 13 | 14 | # production 15 | fe/build 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | .env.development 28 | -------------------------------------------------------------------------------- /src/lib/httpClient.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import axios from "axios"; 4 | 5 | export const getData = async (url, cancel) => { 6 | try { 7 | const results = await axios(url, { cancelToken: cancel.token }); 8 | return results.data; 9 | } catch (error) { 10 | if (axios.isCancel(error)) { 11 | // Handle if request was cancelled 12 | console.log("Request canceled", error.message); 13 | } else { 14 | // Handle usual errors 15 | console.log("Something went wrong: ", error.message); 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want your app to work offline and load faster, you can change 15 | // unregister() to register() below. Note this comes with some pitfalls. 16 | // Learn more about service workers: https://bit.ly/CRA-PWA 17 | serviceWorker.unregister(); 18 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "COVID19 Travel Info", 3 | "name": "COVID19 Travel Information", 4 | "icons": [{ 5 | "src": "favicon.ico", 6 | "sizes": "64x64 32x32 24x24 16x16", 7 | "type": "image/x-icon" 8 | }, 9 | { 10 | "src": "logo192.png", 11 | "type": "image/png", 12 | "sizes": "192x192" 13 | }, 14 | { 15 | "src": "logo512.png", 16 | "type": "image/png", 17 | "sizes": "512x512" 18 | } 19 | ], 20 | "start_url": ".", 21 | "display": "standalone", 22 | "theme_color": "#000000", 23 | "background_color": "#ffffff" 24 | } -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | export const sanitizeCountryNamesForCOVIDStats = countryName => { 4 | switch (countryName) { 5 | case "Democratic Republic of the Congo": 6 | countryName = "Congo (Kinshasa)"; 7 | break; 8 | 9 | case "Republic of Congo": 10 | countryName = "Congo (Brazzaville)"; 11 | break; 12 | 13 | case "South Korea": 14 | countryName = "Korea, South"; 15 | break; 16 | 17 | case "United States of America": 18 | countryName = "US"; 19 | break; 20 | 21 | case "United Republic of Tanzania": 22 | countryName = "Tanzania"; 23 | break; 24 | default: 25 | break; 26 | } 27 | 28 | return countryName; 29 | }; 30 | 31 | export const sanitizeCountryNamesForFlightInfo = countryName => { 32 | switch (countryName) { 33 | case "United States of America": 34 | countryName = "United States"; 35 | break; 36 | default: 37 | break; 38 | } 39 | 40 | return countryName; 41 | }; 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "banned-corona-flights", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@iconify/icons-mdi": "^1.0.83", 7 | "@iconify/react": "^1.1.3", 8 | "@material-ui/core": "^4.9.7", 9 | "@material-ui/icons": "^4.9.1", 10 | "@nivo/axes": "^0.61.0", 11 | "@nivo/line": "^0.61.1", 12 | "@testing-library/jest-dom": "^4.2.4", 13 | "@testing-library/react": "^9.3.2", 14 | "@testing-library/user-event": "^7.1.2", 15 | "axios": "^1.6.0", 16 | "qrcode.react": "^1.0.0", 17 | "react": "^16.13.1", 18 | "react-dom": "^16.13.1", 19 | "react-globe.gl": "^2.7.6", 20 | "react-scripts": "3.4.1", 21 | "typeface-roboto": "^0.0.75" 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.2%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 1 chrome version", 40 | "last 1 firefox version", 41 | "last 1 safari version" 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/Dialog.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from "react"; 4 | import { 5 | AppBar, 6 | Dialog, 7 | IconButton, 8 | makeStyles, 9 | Slide, 10 | Toolbar, 11 | } from "@material-ui/core"; 12 | import CloseIcon from "@material-ui/icons/Close"; 13 | 14 | const useStyles = makeStyles((theme) => ({ 15 | appBar: { 16 | position: "relative", 17 | }, 18 | title: { 19 | marginLeft: theme.spacing(2), 20 | flex: 1, 21 | }, 22 | })); 23 | 24 | const Transition = React.forwardRef(function Transition(props, ref) { 25 | return ; 26 | }); 27 | 28 | const CustomDialog = ({ handleDialog, dialogContent, dialog }) => { 29 | const classes = useStyles(); 30 | return ( 31 |
32 | handleDialog(false)} 37 | TransitionComponent={Transition}> 38 | 39 | 40 | handleDialog(false)} 44 | aria-label='close'> 45 | 46 | 47 | 48 | 49 | {dialogContent} 50 | 51 |
52 | ); 53 | }; 54 | 55 | export default CustomDialog; 56 | -------------------------------------------------------------------------------- /src/components/Modal.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import React from "react"; 3 | import { makeStyles, Modal, Paper, Zoom } from "@material-ui/core"; 4 | 5 | const useStyles = makeStyles(theme => ({ 6 | modal: { 7 | flexGrow: 1, 8 | backgroundColor: theme.palette.background.paper, 9 | border: "2px solid #FFF", 10 | borderRadius: 10, 11 | boxShadow: theme.shadows[5], 12 | padding: theme.spacing(5), 13 | position: "absolute", 14 | [theme.breakpoints.up("lg")]: { 15 | top: "27%", 16 | left: "28%", 17 | transform: "translate(-30%, -30%)", 18 | width: 450 19 | }, 20 | [theme.breakpoints.down("md")]: { 21 | top: "20%", 22 | left: "25%", 23 | width: 450 24 | }, 25 | [theme.breakpoints.down("sm")]: { 26 | top: "18%", 27 | left: "10%", 28 | width: 268 29 | }, 30 | [theme.breakpoints.down("xs")]: { 31 | top: "14%", 32 | left: "5%", 33 | width: 202 34 | }, 35 | outline: "none" 36 | } 37 | })); 38 | 39 | const CustomModal = ({ handleModal, modal, modalContent }) => { 40 | const classes = useStyles(); 41 | 42 | const renderModalBody = ( 43 | 44 | {modalContent} 45 | 46 | ); 47 | 48 | return ( 49 | handleModal(false)} 52 | aria-labelledby='modal-title' 53 | aria-describedby='modal-description'> 54 | {renderModalBody} 55 | 56 | ); 57 | }; 58 | 59 | export default CustomModal; 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # See Coronavirus Travel Restrictions, Across the Globe 4 | 5 | ### [Live Site](https://www.covid19globalinfo.com/) 6 | 7 | This project utilizes [pomber's](https://github.com/pomber/covid19) time series JSON API as well as a scraper that I built [here](https://github.com/maschad/covid-scraper). As well as [vasturiano's](https://github.com/vasturiano/react-globe.gl) react globe library. 8 | 9 | ![gif demo](https://s5.gifyu.com/images/Screen-Recording-2020-04-03-at-9.gif) 10 | 11 | ## Available Scripts 12 | 13 | In the project directory, you can run: 14 | 15 | ### `yarn start` 16 | 17 | Runs the app in the development mode.
18 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 19 | 20 | The page will reload if you make edits.
21 | You will also see any lint errors in the console. 22 | 23 | ### `yarn test` 24 | 25 | Launches the test runner in the interactive watch mode.
26 | 27 | ### `yarn build` 28 | 29 | Builds the app for production to the `build` folder.
30 | It correctly bundles React in production mode and optimizes the build for the best performance. 31 | 32 | The build is minified and the filenames include the hashes.
33 | Your app is ready to be deployed! 34 | 35 | ## Setting up 36 | 37 | Because this project requires a backend, you can run the [scraper](https://github.com/maschad/covid-scraper) I mentioned before 38 | 39 | ## Docker Instructions 40 | 41 | If you have [Docker](https://www.docker.com/) then: 42 | 43 | - Build an image 44 | ``` 45 | docker build -t /banned-flights 46 | ``` 47 | - Run that image 48 | 49 | ``` 50 | docker run -d /banned-flights 51 | ``` 52 | 53 | - Ensure it's running 54 | ``` 55 | docker ps 56 | ``` 57 | -------------------------------------------------------------------------------- /src/components/Chart.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | import React from "react"; 3 | 4 | import { ResponsiveLine } from "@nivo/line"; 5 | 6 | const Chart = ({ data }) => { 7 | return ( 8 | 71 | ); 72 | }; 73 | 74 | export default Chart; 75 | -------------------------------------------------------------------------------- /src/components/Drawer.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from "react"; 4 | import { 5 | Drawer, 6 | List, 7 | ListItem, 8 | ListItemIcon, 9 | ListItemText, 10 | makeStyles, 11 | Typography 12 | } from "@material-ui/core"; 13 | 14 | import { Icon } from "@iconify/react"; 15 | import bitcoinIcon from "@iconify/icons-mdi/bitcoin"; 16 | import ethereumIcon from "@iconify/icons-mdi/ethereum"; 17 | 18 | const useStyles = makeStyles(() => ({ 19 | list: { 20 | width: 250 21 | }, 22 | titleText: { 23 | justifyContent: "center", 24 | alignItems: "center", 25 | inset: "auto" 26 | } 27 | })); 28 | 29 | const CustomDrawer = ({ drawerState, sendViaQR, toggleDrawer }) => { 30 | const classes = useStyles(); 31 | 32 | const cryptoList = ( 33 |
38 | 39 | 40 | 41 | 42 | Show some love without borders and bank fees :){" "} 43 | 44 | 45 | 46 | sendViaQR("Ethereum")}> 47 | 48 | 49 | 50 | 51 | 52 | sendViaQR("Bitcoin")}> 53 | 54 | 55 | 56 | 57 | 58 | 59 |
60 | ); 61 | 62 | return ( 63 | 64 | 65 | {cryptoList} 66 | 67 | 68 | ); 69 | }; 70 | 71 | export default CustomDrawer; 72 | -------------------------------------------------------------------------------- /src/components/MapChart.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { memo, useEffect, useRef, useState } from "react"; 4 | import Globe from "react-globe.gl"; 5 | import { sanitizeCountryNamesForCOVIDStats } from "../lib/utils"; 6 | 7 | const MapChart = ({ countryData, renderChart }) => { 8 | const globeEl = useRef(); 9 | const [hoverD, setHoverD] = useState(); 10 | const [countries, setCountries] = useState({ features: [] }); 11 | 12 | useEffect(() => { 13 | //Get Country info 14 | fetch("./countries.json") 15 | .then((res) => res.json()) 16 | .then(setCountries); 17 | }, []); 18 | 19 | const todaysData = (countryName) => { 20 | countryName = sanitizeCountryNamesForCOVIDStats(countryName); 21 | 22 | if (countryData[countryName] !== undefined) { 23 | return countryData[countryName][countryData[countryName].length - 1]; 24 | } else { 25 | return "No info"; 26 | } 27 | }; 28 | 29 | const getPolygonLabel = (data) => { 30 | if (todaysData(data.ADMIN) === "No info") return "No Info"; 31 | else 32 | return ` 33 | ${data.ADMIN}
34 | Confirmed: ${ 35 | todaysData(data.ADMIN).confirmed 36 | }
37 | Deaths: ${todaysData(data.ADMIN).deaths}
38 | Recovered: ${todaysData(data.ADMIN).recovered}
39 | `; 40 | }; 41 | 42 | return ( 43 | (d === hoverD ? 0.12 : 0.06)} 49 | polygonCapColor={(d) => (d === hoverD ? "green" : "black")} 50 | polygonSideColor={() => "rgba(0, 100, 0, 0.15)"} 51 | polygonStrokeColor={() => "#111"} 52 | polygonLabel={({ properties: d }) => getPolygonLabel(d)} 53 | onPolygonHover={setHoverD} 54 | onPolygonClick={({ properties: d }) => renderChart(d.ADMIN)} 55 | polygonsTransitionDuration={300} 56 | /> 57 | ); 58 | }; 59 | 60 | export default memo(MapChart); 61 | -------------------------------------------------------------------------------- /src/components/Tab.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React from "react"; 4 | import PropTypes from "prop-types"; 5 | import { makeStyles } from "@material-ui/core/styles"; 6 | import AppBar from "@material-ui/core/AppBar"; 7 | import Tabs from "@material-ui/core/Tabs"; 8 | import Tab from "@material-ui/core/Tab"; 9 | import Typography from "@material-ui/core/Typography"; 10 | import Box from "@material-ui/core/Box"; 11 | 12 | function TabPanel(props) { 13 | const { children, value, index, ...other } = props; 14 | 15 | return ( 16 | 25 | ); 26 | } 27 | 28 | TabPanel.propTypes = { 29 | children: PropTypes.node, 30 | index: PropTypes.any.isRequired, 31 | value: PropTypes.any.isRequired 32 | }; 33 | 34 | function a11yProps(index) { 35 | return { 36 | id: `simple-tab-${index}`, 37 | "aria-controls": `simple-tabpanel-${index}` 38 | }; 39 | } 40 | 41 | const useStyles = makeStyles(theme => ({ 42 | root: { 43 | flexGrow: 1, 44 | backgroundColor: theme.palette.background.paper 45 | } 46 | })); 47 | 48 | const CustomTab = ({ tabTitles, tabContents }) => { 49 | const classes = useStyles(); 50 | const [value, setValue] = React.useState(0); 51 | 52 | const handleChange = (event, newValue) => { 53 | setValue(newValue); 54 | }; 55 | 56 | return ( 57 |
58 | 59 | 63 | {tabTitles.map((title, index) => ( 64 | 65 | ))} 66 | 67 | 68 | {tabContents.map((content, index) => ( 69 | 70 | {content} 71 | 72 | ))} 73 |
74 | ); 75 | }; 76 | 77 | export default CustomTab; 78 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 26 | 30 | 31 | 40 | COVID19 Global Info 41 | 42 | 43 | 44 |
45 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useState } from "react"; 4 | import "typeface-roboto"; 5 | 6 | import { 7 | createMuiTheme, 8 | makeStyles, 9 | ThemeProvider 10 | } from "@material-ui/core/styles"; 11 | 12 | import { grey } from "@material-ui/core/colors"; 13 | import { 14 | AppBar, 15 | Button, 16 | Box, 17 | Hidden, 18 | IconButton, 19 | Toolbar, 20 | Typography, 21 | Grid 22 | } from "@material-ui/core"; 23 | 24 | import FileCopyIcon from "@material-ui/icons/FileCopy"; 25 | 26 | import FavoriteIcon from "@material-ui/icons/Favorite"; 27 | import GitHubIcon from "@material-ui/icons/GitHub"; 28 | 29 | import QRCode from "qrcode.react"; 30 | 31 | import CustomDrawer from "./components/Drawer"; 32 | import CustomModal from "./components/Modal"; 33 | import Home from "./pages/home"; 34 | 35 | const theme = createMuiTheme({ 36 | palette: { 37 | primary: { 38 | main: grey[900] 39 | }, 40 | secondary: { 41 | main: grey[50] 42 | } 43 | } 44 | }); 45 | 46 | const useStyles = makeStyles(theme => ({ 47 | addressText: { 48 | textAlign: "center", 49 | marginTop: 15, 50 | [theme.breakpoints.down("md")]: { 51 | flexWrap: "wrap", 52 | fontSize: 10, 53 | marginTop: 10 54 | } 55 | }, 56 | menuButton: { 57 | marginRight: theme.spacing(2) 58 | }, 59 | title: { 60 | flexGrow: 1 61 | } 62 | })); 63 | 64 | const BTCAddress = "bc1qppv3awnvxw7kas3zne6jcwj2w8a5uehaz3mn6q"; 65 | const ETHAddress = "0x6ed31d002338349E486daD57939E1e4A4A7a0007"; 66 | 67 | const App = () => { 68 | const classes = useStyles(); 69 | 70 | const [drawerState, setDrawerState] = useState(false); 71 | 72 | const [modal, setModal] = useState(false); 73 | const [modalContent, setModalContent] = useState(null); 74 | 75 | const [copied, setCopied] = useState(false); 76 | 77 | const copyToClipboard = value => { 78 | navigator.clipboard.writeText(value); 79 | setCopied(true); 80 | }; 81 | 82 | const sendViaQR = type => { 83 | let address = ""; 84 | if (type === "Bitcoin") { 85 | address = BTCAddress; 86 | } else { 87 | address = ETHAddress; 88 | } 89 | 90 | setModalContent( 91 | 92 | 93 | 94 | Scan this QR Code with your {type} wallet 95 | 96 | 97 | 98 | 99 | 100 | 145 | 146 | 147 | 148 | 153 | 154 | 159 | 160 | ); 161 | }; 162 | 163 | export default App; 164 | -------------------------------------------------------------------------------- /src/pages/home.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | import React, { useState, useEffect } from "react"; 4 | import axios from "axios"; 5 | 6 | import { 7 | CircularProgress, 8 | makeStyles, 9 | Typography, 10 | Grid, 11 | DialogContent, 12 | DialogContentText, 13 | } from "@material-ui/core"; 14 | 15 | import Chart from "../components/Chart"; 16 | import GlobeChart from "../components/MapChart"; 17 | 18 | import { getData } from "../lib/httpClient"; 19 | import CustomTab from "../components/Tab"; 20 | import CustomDialog from "../components/Dialog"; 21 | import { 22 | sanitizeCountryNamesForFlightInfo, 23 | sanitizeCountryNamesForCOVIDStats, 24 | } from "../lib/utils"; 25 | 26 | const url = `${process.env.REACT_APP_SCRAPER_URL}`; 27 | const pomberUrl = "https://pomber.github.io/covid19/timeseries.json"; 28 | 29 | const useStyles = makeStyles((theme) => ({ 30 | chart: { 31 | flexGrow: 1, 32 | height: 400, 33 | }, 34 | flightInfo: { 35 | flexGrow: 1, 36 | height: 100, 37 | }, 38 | root: { 39 | flexGrow: 1, 40 | }, 41 | progress: { 42 | alignSelf: "center", 43 | justifySelf: "center", 44 | margin: theme.spacing(2), 45 | }, 46 | modal: { 47 | top: "50%", 48 | left: "50%", 49 | transform: "translate(-50%, -50%)", 50 | position: "absolute", 51 | height: 500, 52 | width: 800, 53 | backgroundColor: theme.palette.background.paper, 54 | border: "2px solid #FFF", 55 | borderRadius: 10, 56 | boxShadow: theme.shadows[5], 57 | padding: theme.spacing(2, 4, 3), 58 | outline: "none", 59 | }, 60 | mobileModal: { 61 | top: "25%", 62 | left: "25%", 63 | transform: "translate(-25%, -25%)", 64 | height: 295, 65 | width: 292, 66 | }, 67 | })); 68 | 69 | const Home = () => { 70 | const classes = useStyles(); 71 | 72 | const [loading, setLoading] = useState(false); 73 | 74 | const [bannedCountries, setBannedCountries] = useState(""); 75 | const [countryData, setCountryData] = useState({}); 76 | 77 | const [chartData, setChartData] = useState([]); 78 | const [dialog, setDialog] = useState(false); 79 | const [dialogContent, setDialogContent] = useState({}); 80 | 81 | useEffect(() => { 82 | const cancel = axios.CancelToken.source(); 83 | 84 | const fetchData = async () => { 85 | setLoading(true); 86 | 87 | let results = await getData(url, cancel); 88 | setBannedCountries(results); 89 | 90 | results = await getData(pomberUrl, cancel); 91 | setCountryData(results); 92 | 93 | setLoading(false); 94 | }; 95 | 96 | fetchData(); 97 | 98 | return () => { 99 | cancel.cancel(); 100 | }; 101 | }, []); 102 | 103 | const handleDialog = (value) => { 104 | setDialog(value); 105 | }; 106 | 107 | const renderChart = (name) => { 108 | if (countryData[sanitizeCountryNamesForCOVIDStats(name)] !== undefined) { 109 | const getData = (type) => { 110 | return countryData[sanitizeCountryNamesForCOVIDStats(name)].map( 111 | (stat, index) => { 112 | return { 113 | x: `Day ${index}`, 114 | y: stat[type], 115 | }; 116 | } 117 | ); 118 | }; 119 | 120 | setChartData([ 121 | { 122 | id: "Confirmed", 123 | color: "hsl(40, 50%, 45%)", 124 | data: getData("confirmed"), 125 | }, 126 | { 127 | id: "Deaths", 128 | color: "hsl(0, 50%, 45%)", 129 | data: getData("deaths"), 130 | }, 131 | { 132 | id: "Recovered", 133 | color: "hsl(83, 70%, 50%)", 134 | data: getData("recovered"), 135 | }, 136 | ]); 137 | setDialogContent(getDialogContent(name, chartData)); 138 | handleDialog(true); 139 | } 140 | }; 141 | 142 | const getDialogContent = (name, chartData) => { 143 | const tabTitles = ["Flight Info", "Basic Stats"]; 144 | const tabContents = []; 145 | 146 | tabContents.push( 147 | 148 | {name} 149 | 150 | 151 | 152 | {bannedCountries[sanitizeCountryNamesForFlightInfo(name)] === 153 | undefined 154 | ? "No Flight Info" 155 | : bannedCountries[sanitizeCountryNamesForFlightInfo(name)]} 156 | 157 | 158 | 159 | 160 | ); 161 | 162 | tabContents.push( 163 | 164 | {name} 165 | 166 | 167 | ); 168 | 169 | return ( 170 | 171 | 172 | 173 | ); 174 | }; 175 | 176 | if (loading) 177 | return ( 178 |
179 | 180 |
181 | ); 182 | 183 | return ( 184 |
185 |
186 | 187 | 192 |
193 |
194 | ); 195 | }; 196 | 197 | export default Home; 198 | -------------------------------------------------------------------------------- /src/lib/countryList.js: -------------------------------------------------------------------------------- 1 | /** @format */ 2 | 3 | // List of all countries in a simple list / array. 4 | const countryList = [ 5 | "Afghanistan", 6 | "Albania", 7 | "Algeria", 8 | "American Samoa", 9 | "Andorra", 10 | "Angola", 11 | "Anguilla", 12 | "Antarctica", 13 | "Antigua and Barbuda", 14 | "Argentina", 15 | "Armenia", 16 | "Aruba", 17 | "Australia", 18 | "Austria", 19 | "Azerbaijan", 20 | "Bahamas (the)", 21 | "Bahrain", 22 | "Bangladesh", 23 | "Barbados", 24 | "Belarus", 25 | "Belgium", 26 | "Belize", 27 | "Benin", 28 | "Bermuda", 29 | "Bhutan", 30 | "Bolivia (Plurinational State of)", 31 | "Bonaire, Sint Eustatius and Saba", 32 | "Bosnia and Herzegovina", 33 | "Botswana", 34 | "Bouvet Island", 35 | "Brazil", 36 | "British Indian Ocean Territory (the)", 37 | "Brunei Darussalam", 38 | "Bulgaria", 39 | "Burkina Faso", 40 | "Burundi", 41 | "Cabo Verde", 42 | "Cambodia", 43 | "Cameroon", 44 | "Canada", 45 | "Cayman Islands (the)", 46 | "Central African Republic (the)", 47 | "Chad", 48 | "Chile", 49 | "China", 50 | "Christmas Island", 51 | "Cocos (Keeling) Islands (the)", 52 | "Colombia", 53 | "Comoros (the)", 54 | "Congo (the Democratic Republic of the)", 55 | "Congo (the)", 56 | "Cook Islands (the)", 57 | "Costa Rica", 58 | "Croatia", 59 | "Cuba", 60 | "Curaçao", 61 | "Cyprus", 62 | "Czechia", 63 | "Côte d'Ivoire", 64 | "Denmark", 65 | "Djibouti", 66 | "Dominica", 67 | "Dominican Republic (the)", 68 | "Ecuador", 69 | "Egypt", 70 | "El Salvador", 71 | "Equatorial Guinea", 72 | "Eritrea", 73 | "Estonia", 74 | "Eswatini", 75 | "Ethiopia", 76 | "Falkland Islands (the) [Malvinas]", 77 | "Faroe Islands (the)", 78 | "Fiji", 79 | "Finland", 80 | "France", 81 | "French Guiana", 82 | "French Polynesia", 83 | "French Southern Territories (the)", 84 | "Gabon", 85 | "Gambia (the)", 86 | "Georgia", 87 | "Germany", 88 | "Ghana", 89 | "Gibraltar", 90 | "Greece", 91 | "Greenland", 92 | "Grenada", 93 | "Guadeloupe", 94 | "Guam", 95 | "Guatemala", 96 | "Guernsey", 97 | "Guinea", 98 | "Guinea-Bissau", 99 | "Guyana", 100 | "Haiti", 101 | "Heard Island and McDonald Islands", 102 | "Holy See (the)", 103 | "Honduras", 104 | "Hong Kong", 105 | "Hungary", 106 | "Iceland", 107 | "India", 108 | "Indonesia", 109 | "Iran (Islamic Republic of)", 110 | "Iraq", 111 | "Ireland", 112 | "Isle of Man", 113 | "Israel", 114 | "Italy", 115 | "Jamaica", 116 | "Japan", 117 | "Jersey", 118 | "Jordan", 119 | "Kazakhstan", 120 | "Kenya", 121 | "Kiribati", 122 | "Korea (the Democratic People's Republic of)", 123 | "Korea (the Republic of)", 124 | "Kuwait", 125 | "Kyrgyzstan", 126 | "Lao People's Democratic Republic (the)", 127 | "Latvia", 128 | "Lebanon", 129 | "Lesotho", 130 | "Liberia", 131 | "Libya", 132 | "Liechtenstein", 133 | "Lithuania", 134 | "Luxembourg", 135 | "Macao", 136 | "Madagascar", 137 | "Malawi", 138 | "Malaysia", 139 | "Maldives", 140 | "Mali", 141 | "Malta", 142 | "Marshall Islands (the)", 143 | "Martinique", 144 | "Mauritania", 145 | "Mauritius", 146 | "Mayotte", 147 | "Mexico", 148 | "Micronesia (Federated States of)", 149 | "Moldova (the Republic of)", 150 | "Monaco", 151 | "Mongolia", 152 | "Montenegro", 153 | "Montserrat", 154 | "Morocco", 155 | "Mozambique", 156 | "Myanmar", 157 | "Namibia", 158 | "Nauru", 159 | "Nepal", 160 | "Netherlands (the)", 161 | "New Caledonia", 162 | "New Zealand", 163 | "Nicaragua", 164 | "Niger (the)", 165 | "Nigeria", 166 | "Niue", 167 | "Norfolk Island", 168 | "Northern Mariana Islands (the)", 169 | "Norway", 170 | "Oman", 171 | "Pakistan", 172 | "Palau", 173 | "Palestine, State of", 174 | "Panama", 175 | "Papua New Guinea", 176 | "Paraguay", 177 | "Peru", 178 | "Philippines (the)", 179 | "Pitcairn", 180 | "Poland", 181 | "Portugal", 182 | "Puerto Rico", 183 | "Qatar", 184 | "Republic of North Macedonia", 185 | "Romania", 186 | "Russian Federation (the)", 187 | "Rwanda", 188 | "Réunion", 189 | "Saint Barthélemy", 190 | "Saint Helena, Ascension and Tristan da Cunha", 191 | "Saint Kitts and Nevis", 192 | "Saint Lucia", 193 | "Saint Martin (French part)", 194 | "Saint Pierre and Miquelon", 195 | "Saint Vincent and the Grenadines", 196 | "Samoa", 197 | "San Marino", 198 | "Sao Tome and Principe", 199 | "Saudi Arabia", 200 | "Senegal", 201 | "Serbia", 202 | "Seychelles", 203 | "Sierra Leone", 204 | "Singapore", 205 | "Sint Maarten (Dutch part)", 206 | "Slovakia", 207 | "Slovenia", 208 | "Solomon Islands", 209 | "Somalia", 210 | "South Africa", 211 | "South Georgia and the South Sandwich Islands", 212 | "South Sudan", 213 | "Spain", 214 | "Sri Lanka", 215 | "Sudan (the)", 216 | "Suriname", 217 | "Svalbard and Jan Mayen", 218 | "Sweden", 219 | "Switzerland", 220 | "Syrian Arab Republic", 221 | "Taiwan (Province of China)", 222 | "Tajikistan", 223 | "Tanzania, United Republic of", 224 | "Thailand", 225 | "Timor-Leste", 226 | "Togo", 227 | "Tokelau", 228 | "Tonga", 229 | "Trinidad and Tobago", 230 | "Tunisia", 231 | "Turkey", 232 | "Turkmenistan", 233 | "Turks and Caicos Islands (the)", 234 | "Tuvalu", 235 | "Uganda", 236 | "Ukraine", 237 | "United Arab Emirates (the)", 238 | "United Kingdom of Great Britain and Northern Ireland (the)", 239 | "United States Minor Outlying Islands (the)", 240 | "United States of America (the)", 241 | "Uruguay", 242 | "Uzbekistan", 243 | "Vanuatu", 244 | "Venezuela (Bolivarian Republic of)", 245 | "Viet Nam", 246 | "Virgin Islands (British)", 247 | "Virgin Islands (U.S.)", 248 | "Wallis and Futuna", 249 | "Western Sahara", 250 | "Yemen", 251 | "Zambia", 252 | "Zimbabwe", 253 | "Åland Islands" 254 | ]; 255 | 256 | export default countryList; 257 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | --------------------------------------------------------------------------------