├── .browserslistrc ├── .commitlintrc.json ├── .env ├── .eslintrc ├── .gitignore ├── .huskyrc ├── .lintstagedrc ├── .prettierrc ├── README.md ├── _templates └── component │ └── simple │ └── component.ejs.t ├── jsconfig.json ├── now.json ├── package.json ├── postcss.config.js ├── public ├── _redirects ├── favicon.ico ├── icon-192.png ├── icon-512.png ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.js ├── Components │ ├── Card.js │ ├── Content.js │ ├── Link.js │ ├── Logo.js │ ├── Navbar.js │ ├── Stat.js │ └── Stats.js ├── Constants │ ├── COUNTRIES.js │ └── RECHARTS_OVERRIDES.js ├── Requests │ └── fetchData.js ├── Utils │ ├── findCountryByName.js │ └── formatNumber.js ├── Widgets │ ├── PandemicStatus.js │ └── TotalCasesProgression.js ├── index.css ├── index.js └── setupTests.js ├── tailwind.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | [production] 2 | > 0.2% 3 | not dead 4 | not op_mini all 5 | 6 | [development] 7 | last 1 chrome version 8 | last 1 safari version 9 | last 1 firefox version 10 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"], 3 | "rules": { 4 | "type-case": [2, "always", "camel-case"], 5 | "scope-case": [2, "always", "pascal-case"], 6 | "subject-case": [2, "always", "sentence-case"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_NAME=$npm_package_name 2 | REACT_APP_VERSION=$npm_package_version 3 | 4 | # REACT_APP_API_URL= 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "no-use-before-define": [ 5 | "error", 6 | { 7 | "variables": true, 8 | "functions": false 9 | } 10 | ], 11 | "object-curly-newline": [ 12 | "error", 13 | { 14 | "ObjectExpression": { 15 | "minProperties": 1 16 | } 17 | } 18 | ], 19 | "arrow-body-style": ["error", "always"], 20 | "no-else-return": [ 21 | "error", 22 | { 23 | "allowElseIf": false 24 | } 25 | ], 26 | "padding-line-between-statements": [ 27 | "error", 28 | { 29 | "blankLine": "always", 30 | "prev": "let", 31 | "next": "*" 32 | }, 33 | { 34 | "blankLine": "never", 35 | "prev": "singleline-let", 36 | "next": "singleline-let" 37 | }, 38 | { 39 | "blankLine": "always", 40 | "prev": "expression", 41 | "next": "*" 42 | }, 43 | { 44 | "blankLine": "never", 45 | "prev": "expression", 46 | "next": "expression" 47 | }, 48 | { 49 | "blankLine": "always", 50 | "prev": "multiline-expression", 51 | "next": "*" 52 | }, 53 | { 54 | "blankLine": "always", 55 | "prev": "*", 56 | "next": "multiline-expression" 57 | }, 58 | { 59 | "blankLine": "always", 60 | "prev": "block-like", 61 | "next": "*" 62 | }, 63 | { 64 | "blankLine": "always", 65 | "prev": "*", 66 | "next": "return" 67 | } 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | .DS_Store 3 | 4 | # Yarn 5 | /.pnp 6 | .pnp.js 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # NPM 11 | /node_modules 12 | npm-debug.log* 13 | 14 | # CRA 15 | /coverage 16 | /build 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | # PostCSS 23 | /src/index.final.css 24 | -------------------------------------------------------------------------------- /.huskyrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged", 4 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{md,mdx,html,scss,json}": "prettier --write", 3 | "*.js": "eslint --fix" 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Covid 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode. 10 | 11 | ### `yarn test` 12 | 13 | Launches the test runner in the interactive watch mode. 14 | 15 | ### `yarn build` 16 | 17 | Builds the app for production to the `build` folder. 18 | -------------------------------------------------------------------------------- /_templates/component/simple/component.ejs.t: -------------------------------------------------------------------------------- 1 | --- 2 | to: src/<%= m %>/Components/<%= name %>.js 3 | --- 4 | import React from "react" 5 | 6 | export function <%= name %>(props) { 7 | return
{props.children}
8 | } 9 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "compilerOptions": { 4 | "baseUrl": "src" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "alias": "covid.hnordt.app" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hnordt/covid", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "prestart": "npm run build:css", 6 | "pretest": "NODE_ENV=production npm run build:css", 7 | "prebuild": "NODE_ENV=production npm run build:css", 8 | "start": "react-scripts start", 9 | "test": "react-scripts test", 10 | "build": "react-scripts build", 11 | "build:css": "postcss src/index.css -o src/index.final.css", 12 | "gen:component": "hygen component" 13 | }, 14 | "dependencies": { 15 | "axios": "0.21.1", 16 | "classnames": "2.2.6", 17 | "lodash": "4.17.15", 18 | "numeral": "2.0.6", 19 | "react": "16.13.1", 20 | "react-dom": "16.13.1", 21 | "react-scripts": "3.4.1", 22 | "recharts": "1.8.5" 23 | }, 24 | "devDependencies": { 25 | "@commitlint/cli": "8.3.5", 26 | "@commitlint/config-conventional": "8.3.4", 27 | "@fullhuman/postcss-purgecss": "2.1.0", 28 | "@tailwindcss/ui": "0.1.3", 29 | "@testing-library/jest-dom": "5.1.1", 30 | "@testing-library/react": "10.0.1", 31 | "@testing-library/user-event": "10.0.0", 32 | "husky": "4.2.3", 33 | "hygen": "5.0.3", 34 | "lint-staged": "10.0.8", 35 | "postcss-cli": "7.1.0", 36 | "prettier": "1.19.1", 37 | "tailwindcss": "1.2.0" 38 | }, 39 | "private": true 40 | } 41 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require("tailwindcss"), 4 | ...(process.env.NODE_ENV === "production" 5 | ? [ 6 | require("@fullhuman/postcss-purgecss")({ 7 | content: ["./src/**/*.js", "./public/**/*.html"], 8 | defaultExtractor: content => { 9 | return content.match(/[\w-/.:]+(? 2 | 3 | 4 | 5 | 9 | 10 | Covid 11 | 12 | 13 | 17 | 23 | 24 | 25 | 26 |
27 | 31 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Covid", 3 | "short_name": "Covid", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "type": "image/x-icon", 8 | "sizes": "16x16 32x32 48x48" 9 | }, 10 | { 11 | "src": "icon-192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "icon-512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "theme_color": "#000", 22 | "background_color": "#fff", 23 | "display": "standalone", 24 | "start_url": "." 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react" 2 | import * as L from "lodash/fp" 3 | import { COUNTRIES } from "Constants/COUNTRIES" 4 | import { Navbar } from "Components/Navbar" 5 | import { Content } from "Components/Content" 6 | import { PandemicStatus } from "Widgets/PandemicStatus" 7 | import { TotalCasesProgression } from "Widgets/TotalCasesProgression" 8 | import { fetchData } from "Requests/fetchData" 9 | 10 | export function App() { 11 | let [data, setData] = useState([]) 12 | let [error, setError] = useState(null) 13 | 14 | let totalCasesByCountry = COUNTRIES.reduce((acc, country) => { 15 | return { 16 | ...acc, 17 | [country.name]: L.pipe( 18 | L.filter(L.propEq("location", country.name)), 19 | L.orderBy("date", "asc"), 20 | L.map(L.prop("totalCases")) 21 | )(data) 22 | } 23 | }, {}) 24 | 25 | let totalsInBrazil = 26 | L.pipe( 27 | L.filter(L.propEq("location", "Brazil")), 28 | L.orderBy("date", "asc"), 29 | L.last 30 | )(data) ?? {} 31 | 32 | useEffect(() => { 33 | fetchData() 34 | .then(setData) 35 | .catch(setError) 36 | }, []) 37 | 38 | if (error) { 39 | // TODO: allow the user to retry 40 | } 41 | 42 | return ( 43 | <> 44 | 45 | 46 |
47 | 48 | 49 |
50 |
51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/Components/Card.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Link } from "Components/Link" 3 | 4 | export function Card(props) { 5 | return ( 6 |
7 |
8 |

9 | {props.title} 10 |

11 |

12 | {props.description} 13 |

14 |
15 |
{props.children}
16 |
17 | {props.elaboration && ( 18 |

19 | Explicação detalhada: {props.elaboration} 20 |

21 | )} 22 |

23 | Fonte:{" "} 24 | 25 | {props.dataSource.name} 26 | 27 |

28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/Components/Content.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export function Content(props) { 4 | return
{props.children}
5 | } 6 | -------------------------------------------------------------------------------- /src/Components/Link.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export function Link(props) { 4 | return ( 5 | 15 | {props.children} 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/Components/Logo.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import cn from "classnames" 3 | 4 | export function Logo(props) { 5 | return ( 6 |
7 | 12 | 13 | 14 | 15 |

Covid

16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/Components/Navbar.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Logo } from "Components/Logo" 3 | 4 | export function Navbar() { 5 | return ( 6 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/Components/Stat.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export function Stat(props) { 4 | return ( 5 |
6 |

7 | {props.value} 8 |

9 |

10 | {props.label} 11 |

12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/Components/Stats.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export function Stats(props) { 4 | return
{props.children}
5 | } 6 | -------------------------------------------------------------------------------- /src/Constants/COUNTRIES.js: -------------------------------------------------------------------------------- 1 | export let COUNTRIES = [ 2 | { 3 | name: "Brazil", 4 | nameInPortuguese: "Brasil", 5 | color: "#38a169", 6 | primary: true 7 | }, 8 | { 9 | name: "Italy", 10 | nameInPortuguese: "Itália", 11 | color: "#d53f8c", 12 | primary: false 13 | }, 14 | { 15 | name: "Spain", 16 | nameInPortuguese: "Espanha", 17 | color: "#d69e2e", 18 | primary: false 19 | }, 20 | { 21 | name: "China", 22 | nameInPortuguese: "China", 23 | color: "#e53e3e", 24 | primary: false 25 | }, 26 | { 27 | name: "United States", 28 | nameInPortuguese: "EUA", 29 | color: "#3182ce", 30 | primary: false 31 | }, 32 | { 33 | name: "South Korea", 34 | nameInPortuguese: "Coreia do Sul", 35 | color: "#718096", 36 | primary: false 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /src/Constants/RECHARTS_OVERRIDES.js: -------------------------------------------------------------------------------- 1 | export let RECHARTS_OVERRIDES = { 2 | lineChart: { 3 | className: "text-gray-900 text-sm", 4 | margin: { 5 | top: 0, 6 | bottom: 0, 7 | left: 0, 8 | right: 0 9 | } 10 | }, 11 | line: { 12 | dot: false 13 | }, 14 | xAxis: { 15 | tickLine: false 16 | }, 17 | yAxis: { 18 | tickLine: false 19 | }, 20 | legend: { 21 | wrapperStyle: { 22 | paddingTop: 20 23 | } 24 | }, 25 | tooltip: { 26 | separator: ": ", 27 | labelStyle: { 28 | fontWeight: 500 29 | }, 30 | itemStyle: { 31 | marginBottom: -5 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Requests/fetchData.js: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import * as L from "lodash/fp" 3 | 4 | export async function fetchData() { 5 | let response = await axios.get( 6 | "https://covid.ourworldindata.org/data/ecdc/full_data.csv" 7 | ) 8 | 9 | return ( 10 | response.data 11 | // Extract lines 12 | .split("\n") 13 | // Ignore the first line 14 | .slice(1) 15 | // Extract "columns" for each line 16 | .map(L.split(",")) 17 | // Transform data 18 | .map(([date, location, newCases, newDeaths, totalCases, totalDeaths]) => { 19 | return { 20 | date, 21 | location, 22 | newCases: Number(newCases), 23 | newDeaths: Number(newDeaths), 24 | totalCases: Number(totalCases), 25 | totalDeaths: Number(totalDeaths) 26 | } 27 | }) 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/Utils/findCountryByName.js: -------------------------------------------------------------------------------- 1 | import * as L from "lodash/fp" 2 | import { COUNTRIES } from "Constants/COUNTRIES" 3 | 4 | export function findCountryByName(name) { 5 | return COUNTRIES.find(L.propEq("name", name)) 6 | } 7 | -------------------------------------------------------------------------------- /src/Utils/formatNumber.js: -------------------------------------------------------------------------------- 1 | import numeral from "numeral" 2 | import "numeral/locales/pt-br" 3 | 4 | numeral.locale("pt-br") 5 | 6 | export function formatNumber(number, format = "0,0") { 7 | return numeral(number).format(format) 8 | } 9 | -------------------------------------------------------------------------------- /src/Widgets/PandemicStatus.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Card } from "Components/Card" 3 | import { Stats } from "Components/Stats" 4 | import { Stat } from "Components/Stat" 5 | import { formatNumber } from "Utils/formatNumber" 6 | 7 | export function PandemicStatus(props) { 8 | return ( 9 | 17 | 18 | 22 | 26 | 35 | 36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/Widgets/TotalCasesProgression.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { 3 | ResponsiveContainer, 4 | LineChart, 5 | Line, 6 | XAxis, 7 | YAxis, 8 | Legend, 9 | Tooltip 10 | } from "recharts" 11 | import * as L from "lodash/fp" 12 | import { COUNTRIES } from "Constants/COUNTRIES" 13 | import { RECHARTS_OVERRIDES } from "Constants/RECHARTS_OVERRIDES" 14 | import { Card } from "Components/Card" 15 | import { findCountryByName } from "Utils/findCountryByName" 16 | import { formatNumber } from "Utils/formatNumber" 17 | 18 | export function TotalCasesProgression(props) { 19 | // Number of days to show in the chart 20 | let NUMBER_OF_DAYS = 21 | props.totalCasesByCountry.Brazil.filter(totalCases => { 22 | return totalCases >= 100 23 | }).length + 15 24 | 25 | return ( 26 | 35 | 36 | { 38 | return { 39 | day: i + 1, 40 | ...COUNTRIES.reduce((acc, country) => { 41 | return { 42 | ...acc, 43 | [country.name]: props.totalCasesByCountry[ 44 | country.name 45 | ].filter(totalCases => { 46 | return totalCases >= 100 47 | })[i] 48 | } 49 | }, {}) 50 | } 51 | })} 52 | {...RECHARTS_OVERRIDES.lineChart} 53 | > 54 | {COUNTRIES.map(country => { 55 | return ( 56 | 64 | ) 65 | })} 66 | 67 | 68 | { 71 | return findCountryByName(countryName).nameInPortuguese 72 | }} 73 | {...RECHARTS_OVERRIDES.legend} 74 | /> 75 | { 77 | return `Casos no ${day}º dia` 78 | }} 79 | formatter={(totalCases, countryName) => { 80 | return [ 81 | formatNumber(totalCases), 82 | findCountryByName(countryName).nameInPortuguese 83 | ] 84 | }} 85 | {...RECHARTS_OVERRIDES.tooltip} 86 | /> 87 | 88 | 89 | 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* purgecss start ignore */ 2 | @tailwind base; 3 | @tailwind components; 4 | /* purgecss end ignore */ 5 | 6 | @tailwind utilities; 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { render } from "react-dom" 3 | import { App } from "./App" 4 | import "./index.final.css" 5 | 6 | render(, document.getElementById("root")) 7 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom/extend-expect" 2 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | let defaultTheme = require("tailwindcss/defaultTheme") 2 | 3 | module.exports = { 4 | theme: { 5 | extend: { 6 | fontFamily: { 7 | sans: ["Inter", ...defaultTheme.fontFamily.sans] 8 | } 9 | } 10 | }, 11 | plugins: [require("@tailwindcss/ui")] 12 | } 13 | --------------------------------------------------------------------------------