├── .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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------