diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5cf946d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "tabWidth": 2, + "useTabs": false, + "singleQuote": true, + "jsxSingleQuote": true, + "trailingComma": "all" +} diff --git a/app.js b/app.js index 378c940..cb0b11d 100644 --- a/app.js +++ b/app.js @@ -1,128 +1,248 @@ const express = require('express'); +const helmet = require('helmet'); const morgan = require('morgan'); - -const { getCountryTable, getJSONData, getJSONDataForCountry } = require('./lib/byCountry'); -const { getCompleteTable } = require('./lib/corona'); -const { lookupCountry } = require('./lib/helpers'); +const chalk = require('chalk'); + +const { + getCountryTable, + getJSONData, + getJSONDataForCountry, +} = require('./lib/byCountry'); +const { getCompleteTable, getGraph } = require('./lib/corona'); +const { lookupCountry, htmlTemplate, footer } = require('./lib/helpers'); const { getLiveUpdates } = require('./lib/reddit.js'); const { getWorldoMetersTable } = require('./lib/worldoMeters.js'); +const { getUsaStats } = require('./lib/country/us.js'); +const { + helpContent, + countryNotFound, + stateCountryNotFound, +} = require('./lib/constants'); const app = express(); const port = process.env.PORT || 3001; const IS_CURL_RE = /\bcurl\b/im; -function errorHandler(error, res) { +function errorHandler(error, req, res) { console.error(error); - return res.send(` + const body = ` I am sorry. Something went wrong. Please report it\n ${error.message} - `); + ${footer(new Date())} + `; + if (req.isCurl) { + return body; + } + return res.status(500).send(htmlTemplate(body)); } app.set('json escape', true); -app.use(morgan(':remote-addr :remote-user :method :url :status :res[content-length] - :response-time ms')); +app.use( + helmet({ + dnsPrefetchControl: false, + frameguard: { + action: 'deny', + }, + }), +); + +app.use( + helmet.hsts({ + force: true, + includeSubDomains: true, + maxAge: 63072000, // 2 years + preload: true, + }), +); + +app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' })); + +app.use( + morgan( + ':remote-addr :remote-user :method :url :status :res[content-length] - :response-time ms', + ), +); +app.use('/favicon.ico', express.static('./favicon.ico')); + app.use((req, res, next) => { - res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Cache-Control', 's-maxage=600'); + req.isCurl = IS_CURL_RE.test(req.headers['user-agent']); next(); }); app.get('/', (req, res) => { - const isCurl = IS_CURL_RE.test(req.headers['user-agent']); + const isCurl = req.isCurl; const format = req.query.format ? req.query.format : ''; const minimal = req.query.minimal === 'true'; const emojis = req.query.emojis === 'true'; const top = req.query.top ? Number(req.query.top) : 1000; - const source = req.query.source ? Number(req.query.source) : 1; + const source = req.query.source ? Number(req.query.source) : 2; - if (source === 2) { - return getWorldoMetersTable({ isCurl, emojis, minimal, top }) - .then(result => { - return res.send(result); - }).catch(error => errorHandler(error, res)); - } + if (source === 1) { + if (format.toLowerCase() === 'json') { + return getJSONData() + .then((result) => { + return res.json(result); + }) + .catch((error) => errorHandler(error, req, res)); + } - if (format.toLowerCase() === 'json') { - return getJSONData().then(result => { - return res.json(result); - }).catch(error => errorHandler(error, res)); + return getCompleteTable({ isCurl, emojis, minimal, top }) + .then((result) => { + return res.send(result); + }) + .catch((error) => errorHandler(error, req, res)); } - return getCompleteTable({ isCurl, emojis, minimal, top }) - .then(result => { + return getWorldoMetersTable({ isCurl, emojis, minimal, top, format }) + .then((result) => { return res.send(result); - }).catch(error => errorHandler(error, res)); + }) + .catch((error) => errorHandler(error, req, res)); }); app.get('/updates', (req, res) => { - const isCurl = IS_CURL_RE.test(req.headers['user-agent']); + const isCurl = req.isCurl; const format = req.query.format ? req.query.format : ''; if (format.toLowerCase() === 'json') { - return getLiveUpdates({ json: true, isCurl }).then(result => { - return res.json(result); - }).catch(error => errorHandler(error, res)); + return getLiveUpdates({ json: true, isCurl }) + .then((result) => { + return res.json(result); + }) + .catch((error) => errorHandler(error, req, res)); } - return getLiveUpdates({ json: false, isCurl }).then(result => { - return res.send(result); - }).catch(error => errorHandler(error, res)); + return getLiveUpdates({ json: false, isCurl }) + .then((result) => { + return res.send(result); + }) + .catch((error) => errorHandler(error, req, res)); +}); + +app.get(['/:country/graph', '/graph'], (req, res) => { + const { country } = req.params; + const isCurl = req.isCurl; + if (!country) { + return getGraph({ isCurl }) + .then((result) => res.send(result)) + .catch((error) => errorHandler(error, req, res)); + } + const lookupObj = lookupCountry(country); + + if (!lookupObj) { + return res.status(404).send(countryNotFound(isCurl)); + } + return getGraph({ countryCode: lookupObj.iso2, isCurl }) + .then((result) => res.send(result)) + .catch((error) => errorHandler(error, req, res)); +}); + +app.get('/help', (req, res) => { + const isCurl = req.isCurl; + if (!isCurl) { + return res.send(htmlTemplate(helpContent)); + } + return res.send(chalk.green(helpContent)); +}); + +app.get('/states/:country', (req, res) => { + const { country } = req.params; + const isCurl = req.isCurl; + const format = req.query.format ? req.query.format : ''; + const minimal = req.query.minimal === 'true'; + const top = req.query.top ? Number(req.query.top) : 1000; + + const lookupObj = lookupCountry(country); + + if (!lookupObj) { + return res.status(404).send(stateCountryNotFound(isCurl)); + } + if (lookupObj.iso2 === 'US') { + return getUsaStats({ isCurl, minimal, top, format }) + .then((result) => { + return res.send(result); + }) + .catch((error) => errorHandler(error, req, res)); + } }); app.get('/:country', (req, res) => { const { country } = req.params; - const isCurl = IS_CURL_RE.test(req.headers['user-agent']); + const isCurl = req.isCurl; const format = req.query.format ? req.query.format : ''; const minimal = req.query.minimal === 'true'; const emojis = req.query.emojis === 'true'; - const source = req.query.source ? Number(req.query.source) : 1; + const source = req.query.source ? Number(req.query.source) : 2; - if (!country || country.toUpperCase() === 'ALL') { + if (!country || country.toUpperCase() === 'ALL' || country.includes(',')) { if (format.toLowerCase() === 'json') { - return getJSONData().then(result => { - return res.json(result); - }).catch(error => errorHandler(error, res)); + return getWorldoMetersTable({ + countryCode: country, + isCurl, + emojis, + minimal, + format, + }) + .then((result) => { + return res.json(result); + }) + .catch((error) => errorHandler(error, req, res)); } - return getCompleteTable({ isCurl, emojis, minimal }) - .then(result => { + return getWorldoMetersTable({ + countryCode: country, + isCurl, + emojis, + minimal, + }) + .then((result) => { return res.send(result); - }).catch(error => errorHandler(error, res)); + }) + .catch((error) => errorHandler(error, req, res)); } + if (source === 1) { + const lookupObj = lookupCountry(country); - const lookupObj = lookupCountry(country); + if (!lookupObj) { + return res.status(404).send(countryNotFound(isCurl)); + } + const { iso2 } = lookupObj; - if (!lookupObj) { - return res.send(` - Country not found. - Try the full country name or country code. - Example: - - /UK: for United Kingdom - - /US: for United States of America. - - /Italy: for Italy. - `); + if (format.toLowerCase() === 'json') { + return getJSONDataForCountry(iso2) + .then((result) => { + return res.json(result); + }) + .catch((error) => errorHandler(error, req, res)); + } + return getCountryTable({ countryCode: iso2, isCurl, emojis, minimal }) + .then((result) => { + return res.send(result); + }) + .catch((error) => errorHandler(error, req, res)); } + const lookupObj = lookupCountry(country); - const { iso2 } = lookupObj; - - if (source === 2) { - return getWorldoMetersTable({ countryCode: iso2, isCurl, emojis, minimal }) - .then(result => { - return res.send(result); - }).catch(error => errorHandler(error, res)); + if (!lookupObj) { + return res.status(404).send(countryNotFound(isCurl)); } - if (format.toLowerCase() === 'json') { - return getJSONDataForCountry(iso2).then(result => { - return res.json(result); - }).catch(error => errorHandler(error, res)); - } + const { iso2 } = lookupObj; - return getCountryTable({ countryCode: iso2, isCurl, emojis, minimal }) - .then(result => { + return getWorldoMetersTable({ + countryCode: iso2, + isCurl, + emojis, + minimal, + format, + }) + .then((result) => { return res.send(result); - }).catch(error => errorHandler(error, res)); + }) + .catch((error) => errorHandler(error, req, res)); }); app.listen(port, () => console.log(`Running on ${port}`)); diff --git a/bin/index.js b/bin/index.js index 4aaab8b..624d7f1 100755 --- a/bin/index.js +++ b/bin/index.js @@ -3,10 +3,11 @@ require('yargonaut').style('green'); const yargs = require('yargs'); const chalk = require('chalk'); -const { getCompleteTable } = require('../lib/corona'); +const { getCompleteTable, getGraph } = require('../lib/corona'); const { getCountryTable } = require('../lib/byCountry'); const { getWorldoMetersTable } = require('../lib/worldoMeters'); const { lookupCountry } = require('../lib/helpers'); +const { getUsaStats } = require('../lib/country/us'); const { argv } = yargs .command('$0 [country]', 'Tool to track COVID-19 statistics from terminal', yargs => @@ -38,7 +39,7 @@ const { argv } = yargs s: { alias: 'source', describe: 'fetch data from other source', - default: 1, + default: 2, type: 'int' }, e: { @@ -62,22 +63,55 @@ const { argv } = yargs alias: 'top', describe: 'Filter table by rank', type: 'int' + }, + g: { + alias: 'graph', + describe: 'Get graph', + type: 'boolean', + default: false, + }, + st: { + alias: 'states', + describe: 'Get state level data of country ', + type: 'string', } }) .strict() .help('help'); argv.countryCode = argv.country; -if (argv.source === 2) { - getWorldoMetersTable(argv) - .then(console.log) - .catch(console.error); -} else { +if (argv.states) { + const country = lookupCountry(argv.states); + if (!country) { + let error = `Country '${argv.states}' not found.\n`; + error += 'Try full country name or country code.\n'; + error += 'Ex:\n'; + error += '- UK: for United Kingdom \n'; + error += '- US: for United States of America.\n'; + error += '- Italy: for Italy.\n'; + throw new Error(chalk.red.bold(error)); + } + argv.countryCode = country.iso2; + if (argv.countryCode === 'US') { + getUsaStats(argv).then(result => { + console.log(result); + process.exit(1); + }).catch(error => { + console.error(error); + process.exit(0); + }); + } +} + +if (argv.source === 1) { ( argv.country === 'ALL' ? getCompleteTable(argv) : getCountryTable(argv) - ) - .then(console.log) - .catch(console.error); + ).then(console.log).catch(console.error); +} +else if (argv.graph === true) { + getGraph(argv).then(console.log).catch(console.error); +} else { + getWorldoMetersTable(argv).then(console.log).catch(console.error); } diff --git a/changelog.md b/changelog.md index c0d352a..2aa9b5f 100644 --- a/changelog.md +++ b/changelog.md @@ -1,26 +1,38 @@ # Changelog +## Version 0.9.1 + +* Added US states api `corona --states=US` + +## Version 0.9.0 + +* Changed default source to worldoMeters. i.e source 2 is now default + +## Version 0.8.0 + +* Added confirmed cases graphs `corona -g` or `corona italy -g` + ## Version 0.7.0 -* Added new source to fetch realtime data. ``corona --source=2`` +* Added new source to fetch realtime data. `corona --source=2` * Code refactored and some bug fixes. ## Version 0.6.0 -* Added filter to show top N countries. ``corona --top=20`` +* Added filter to show top N countries. `corona --top=20` ## Version 0.5.0 -* Added minimal / comapct table command. ``corona --minimal`` +* Added minimal / comapct table command. `corona --minimal` * Added world total stats at the bottom of the table too. * Refactor: moved table formatting functions to helpers. * Added total stats object when using `?format=json` ## Version 0.4.0 -* Added country filter. Ex: ``corona Italy`` -* Added command to show emojis. Ex: ``corona --emojis`` -* Added command to disable colors using. Ex: ``corona --color=false`` +* Added country filter. Ex: `corona Italy` +* Added command to show emojis. Ex: `corona --emojis` +* Added command to disable colors using. Ex: `corona --color=false` ## Version 0.2.0 @@ -28,4 +40,4 @@ ## Version 0.1.0 -* Lauched command `corona` \ No newline at end of file +* Lauched command `corona` diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..804a1ac Binary files /dev/null and b/favicon.ico differ diff --git a/lib/api.js b/lib/api.js index 33f7cfb..d2fb154 100644 --- a/lib/api.js +++ b/lib/api.js @@ -1,17 +1,32 @@ +/** + * Data Source File + * 1. Fetch data from source. + * 2. Process data. + * 3. Add to cache and return + */ + const NodeCache = require('node-cache'); const axios = require('axios'); -const { countryNameMap } = require('./constants'); const myCache = new NodeCache({ stdTTL: 100, checkperiod: 600 }); +/** + * John Hopkins Univ. data source API. + * The data needs to be flattened. + * @todo Move the data processing from `view` files to this `data` file. + * @returns {Object} JHUDataObject + */ exports.getCoronaData = async () => { const CORONA_ALL_KEY = 'coronaAll'; const cache = myCache.get(CORONA_ALL_KEY); if (cache) { + console.log('cache', CORONA_ALL_KEY); return cache; } - const result = await axios('https://coronavirus-tracker-api.herokuapp.com/all'); + const result = await axios( + 'https://coronavirus-tracker-api.herokuapp.com/all', + ); if (!result || !result.data) { throw new Error('Source API failure.'); @@ -20,7 +35,12 @@ exports.getCoronaData = async () => { return result.data; }; -/** Fetch Worldometers Data */ +/** + * Fetch Worldometers Data. + * As JHU data updates once a day, this was added. + * This API scrapes data from `https://www.worldometers.info/coronavirus/` + * and updates very frequenly. + * */ exports.getWorldoMetersData = async (countryCode = 'ALL') => { const key = `worldMetersData_${countryCode}`; const cache = myCache.get(key); @@ -29,40 +49,80 @@ exports.getWorldoMetersData = async (countryCode = 'ALL') => { console.log('cache', key); return cache; } - const result = await axios('https://corona.lmao.ninja/countries'); + const result = await axios( + 'https://disease.sh/v2/countries?sort=cases', + ); if (!result || !result.data) { throw new Error('WorldoMeters Source API failure'); } - const worldStats = result.data.reduce((acc, countryObj) => { - acc.cases += countryObj.cases; - acc.todayCases += countryObj.todayCases; - acc.deaths += countryObj.deaths; - acc.todayDeaths += countryObj.todayDeaths; - acc.recovered += countryObj.recovered; - acc.active += countryObj.active; - acc.critical += countryObj.critical; - return acc; - }, { - countryName: 'World', - cases: 0, - todayCases: 0, - deaths: 0, - todayDeaths: 0, - recovered: 0, - active: 0, - critical: 0, - }); + const worldStats = result.data.reduce( + (acc, countryObj) => { + acc.cases += countryObj.cases; + acc.todayCases += countryObj.todayCases; + acc.deaths += countryObj.deaths; + acc.todayDeaths += countryObj.todayDeaths; + acc.recovered += countryObj.recovered; + acc.active += countryObj.active; + acc.critical += countryObj.critical; + return acc; + }, + { + country: 'World', + countryCode: 'World', + cases: 0, + todayCases: 0, + deaths: 0, + todayDeaths: 0, + recovered: 0, + active: 0, + critical: 0, + }, + ); - result.data.forEach(obj => obj.countryCode = countryNameMap[obj.country]); + result.data.forEach((obj) => { + obj.confirmed = obj.cases; + obj.countryCode = obj.countryInfo.iso2 || ''; + }); worldStats.casesPerOneMillion = (worldStats.cases / 7794).toFixed(2); - let finalData = result.data; - console.log(countryCode); + worldStats.confirmed = worldStats.cases; + let finalData = result.data; if (countryCode && countryCode !== 'ALL') { - finalData = finalData.filter(obj => obj.countryCode === countryCode); + // extra filter to cater for trailing comma, ie /gb, + finalData = finalData.filter((obj) => countryCode.toLowerCase().split(',').filter((obj) => obj.length > 1).includes(obj.countryCode.toLowerCase())); } const returnObj = { data: finalData, worldStats }; myCache.set(key, returnObj, 60 * 15); return returnObj; }; + +exports.usaStats = async () => { + const key = 'usaStats'; + const cache = myCache.get(key); + + if (cache) { + console.log('cache', key); + return cache; + } + const result = await axios('https://disease.sh/v2/states'); + if (!result || !result.data) { + throw new Error('usa stats API failure'); + } + return result; +}; + +// exports.bingApi = async (countryCode = 'ALL') => { +// const key = 'bingData'; +// const cache = myCache.get(key); + +// if (cache) { +// console.log('cache', key); +// return cache; +// } +// const result = await axios('https://bing.com/covid/data'); +// if (!result || !result.data) { +// throw new Error('bing api faliure'); +// } +// return result; +// }; diff --git a/lib/byCountry.js b/lib/byCountry.js index 7f50d05..8e7e87e 100644 --- a/lib/byCountry.js +++ b/lib/byCountry.js @@ -2,14 +2,12 @@ const Table = require('cli-table3'); const _ = require('lodash'); const helpers = require('./helpers'); const api = require('./api'); + const { extraStats, getConfirmed, - getActive, getDeaths, - getRecovered, getMortalityPer, - getRecoveredPer, getEmoji, getOneDayChange, getOneWeekChange, @@ -18,11 +16,10 @@ const { htmlTemplate, } = require('./helpers'); -function getDataByState(confirmed, deaths, recovered) { +function getDataByState(confirmed, deaths) { const countryMap = {}; const lastUpdated = confirmed.last_updated; const confirmedMap = _.keyBy(confirmed.locations, (i) => i.country + i.province); - const recoveredMap = _.keyBy(recovered.locations, (i) => i.country + i.province); const deathsMap = _.keyBy(deaths.locations, (i) => i.country + i.province); confirmed.locations.forEach(obj => { const countryName = obj.country; @@ -34,10 +31,8 @@ function getDataByState(confirmed, deaths, recovered) { province: provinceName, countryCode: obj.country_code, confirmed: confirmedMap[mapKey].latest, - recovered: recoveredMap[mapKey].latest, deaths: deathsMap[mapKey].latest, confirmedByDay: helpers.historyObjToArr(confirmedMap[mapKey].history), - recoveredByDay: helpers.historyObjToArr(recoveredMap[mapKey].history), deathsByDay: helpers.historyObjToArr(deathsMap[mapKey].history), lastUpdated, }; @@ -51,8 +46,8 @@ function getDataByState(confirmed, deaths, recovered) { exports.getJSONData = async () => { const data = await api.getCoronaData(); - const { confirmed, deaths, recovered } = data; - const countryData = getDataByState(confirmed, deaths, recovered); + const { confirmed, deaths } = data; + const countryData = getDataByState(confirmed, deaths); const totalStats = getTotalStats(countryData); totalStats.country = 'World'; return countryData.concat(totalStats); @@ -60,8 +55,8 @@ exports.getJSONData = async () => { exports.getJSONDataForCountry = async (countryCode) => { const data = await api.getCoronaData(); - const { confirmed, deaths, recovered } = data; - const countryData = getDataByState(confirmed, deaths, recovered) + const { confirmed, deaths } = data; + const countryData = getDataByState(confirmed, deaths) .filter(obj => obj.countryCode === countryCode); return countryData; }; @@ -78,8 +73,8 @@ exports.getCountryTable = async ({ style: helpers.getTableStyles(minimal), }); const data = await api.getCoronaData(); - const { confirmed, deaths, recovered } = data; - const countryData = getDataByState(confirmed, deaths, recovered) + const { confirmed, deaths } = data; + const countryData = getDataByState(confirmed, deaths) .filter(obj => obj.countryCode === countryCode); if (countryData.length === 0) { @@ -91,11 +86,8 @@ exports.getCountryTable = async ({ [countryData[0].country]: [ 'Total', getConfirmed(totalStats.confirmed), - getRecovered(totalStats.recovered), getDeaths(totalStats.deaths), - getActive(totalStats.active), getMortalityPer(totalStats.mortalityPer), - getRecoveredPer(totalStats.recoveredPer), getOneDayChange(totalStats), getOneWeekChange(totalStats), ] @@ -108,11 +100,8 @@ exports.getCountryTable = async ({ const values = [ cd.province, getConfirmed(cd.confirmed), - getRecovered(cd.recovered), getDeaths(cd.deaths), - getActive(cd.active), getMortalityPer(cd.mortalityPer), - getRecoveredPer(cd.recoveredPer), getOneDayChange(cd), getOneWeekChange(cd), ...(emojis ? [countryEmoji] : []) diff --git a/lib/constants.js b/lib/constants.js index 6d9d756..028316a 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -1,183 +1,127 @@ -exports.countryNameMap = { - China: 'CN', - UK: 'GB', - Martinique: 'MQ', - Liechtenstein: 'LI', - 'RΓ©union': 'RE', - Ukraine: 'UA', - Honduras: 'HN', - Afghanistan: 'AF', - Bangladesh: 'BD', - Macao: 'MO', - Bolivia: 'BO', - Cuba: 'CU', - Netherlands: 'NL', - Jamaica: 'JM', - 'French Guiana': 'GF', - DRC: 'CD', - Cameroon: 'CM', - Maldives: 'MV', - Montenegro: 'ME', - Paraguay: 'PY', - Nigeria: 'NG', - Guam: 'GU', - 'French Polynesia': 'PF', - Austria: 'AT', - Ghana: 'GH', - Rwanda: 'RW', - Monaco: 'MC', - Gibraltar: 'GI', - Guatemala: 'GT', - 'Ivory Coast': 'CI', - Ethiopia: 'ET', - Togo: 'TG', - 'Trinidad and Tobago': 'TT', - Kenya: 'KE', - Belgium: 'BE', - Mauritius: 'MU', - 'Equatorial Guinea': 'GQ', - Kyrgyzstan: 'KG', - Mongolia: 'MN', - 'Puerto Rico': 'PR', - Seychelles: 'SC', - Tanzania: 'TZ', - Guyana: 'GY', - Aruba: 'AW', - Barbados: 'BB', - Norway: 'NO', - Mayotte: 'YT', - 'Cayman Islands': 'KY', - 'CuraΓ§ao': 'CW', - Bahamas: 'BS', - Congo: 'CD', - Gabon: 'GA', - Namibia: 'NA', - 'St. Barth': 'BL', - 'Saint Martin': 'MF', - 'U.S. Virgin Islands': 'VI', - Sweden: 'SE', - Sudan: 'SD', - Benin: 'BJ', - Bermuda: 'BM', - Bhutan: 'BT', - CAR: 'CF', - Greenland: 'GL', - Haiti: 'HT', - Liberia: 'LR', - Mauritania: 'MR', - 'New Caledonia': 'NC', - Denmark: 'DK', - 'Saint Lucia': 'LC', - Zambia: 'ZM', - Nepal: 'NP', - Angola: 'AO', - 'Antigua and Barbuda': 'AG', - 'Cabo Verde': 'CV', - Chad: 'TD', - Djibouti: 'DJ', - 'El Salvador': 'SV', - Fiji: 'FJ', - Japan: 'JP', - Gambia: 'GM', - Guinea: 'GN', - 'Vatican City': 'VA', - 'Isle of Man': 'IM', - Montserrat: 'MS', - Nicaragua: 'NI', - Niger: 'NE', - 'St. Vincent Grenadines': 'VC', - 'Sint Maarten': 'SX', - Somalia: 'SO', - Malaysia: 'MY', - Suriname: 'SR', - Eswatini: 'SZ', - Australia: 'AU', - Italy: 'IT', - Canada: 'CA', - Portugal: 'PT', - Czechia: 'CZ', - Israel: 'IL', - Brazil: 'BR', - Luxembourg: 'LU', - Ireland: 'IE', - Greece: 'GR', - Qatar: 'QA', - Pakistan: 'PK', - Iran: 'IR', - Finland: 'FI', - Poland: 'PL', - Turkey: 'TR', - Singapore: 'SG', - Chile: 'CL', - Iceland: 'IS', - Thailand: 'TH', - Slovenia: 'SI', - Indonesia: 'ID', - Bahrain: 'BH', - Spain: 'ES', - Romania: 'RO', - 'Saudi Arabia': 'SA', - Estonia: 'EE', - Ecuador: 'EC', - Egypt: 'EG', - Peru: 'PE', - Philippines: 'PH', - 'Hong Kong': 'HK', - India: 'IN', - Russia: 'RU', - Germany: 'DE', - Iraq: 'IQ', - Mexico: 'MX', - Lebanon: 'LB', - 'South Africa': 'ZA', - Kuwait: 'KW', - 'San Marino': 'SM', - UAE: 'AE', - Panama: 'PA', - Armenia: 'AM', - Taiwan: 'TW', - USA: 'US', - Argentina: 'AR', - Colombia: 'CO', - Slovakia: 'SK', - Serbia: 'RS', - Croatia: 'HR', - Bulgaria: 'BG', - Uruguay: 'UY', - Algeria: 'DZ', - 'Costa Rica': 'CR', - Latvia: 'LV', - France: 'FR', - Hungary: 'HU', - Vietnam: 'VN', - 'Faeroe Islands': 'FO', - Andorra: 'AD', - Brunei: 'BN', - Belarus: 'BY', - Jordan: 'JO', - Cyprus: 'CY', - 'Sri Lanka': 'LK', - Albania: 'AL', - 'S. Korea': 'KR', - 'Bosnia and Herzegovina': 'BA', - Morocco: 'MA', - Malta: 'MT', - 'North Macedonia': 'MK', - Moldova: 'MD', - Kazakhstan: 'KZ', - Lithuania: 'LT', - Oman: 'OM', - Cambodia: 'KH', - Palestine: 'PS', - Switzerland: 'CH', - Guadeloupe: 'GP', - Azerbaijan: 'AZ', - Georgia: 'GE', - Venezuela: 'VE', - Tunisia: 'TN', - 'New Zealand': 'NZ', - Senegal: 'SN', - 'Dominican Republic': 'DO', - 'Burkina Faso': 'BF', - Uzbekistan: 'UZ', +const { htmlTemplate, footer } = require('./helpers'); + +exports.helpContent = ` + +/$$ /$$ /$$$$$$$$ /$$ /$$$$$$$ +| $$ | $$| $$_____/| $$ | $$____$$ +| $$ | $$| $$ | $$ | $$ $$ +| $$$$$$$$| $$$$$ | $$ | $$$$$$$/ +| $$__ $$| $$__/ | $$ | $$____/ +| $$ | $$| $$ | $$ | $$ +| $$ | $$| $$$$$$$$| $$$$$$$$| $$ +|__/ |__/|________/|________/|__/ + +--------------------------------------------------------------------------------- + +# Source 1 stats - updated once a day from John Hopkins University +https://corona-stats.online + +--------------------------------------------------------------------------------- + +(DEFAULT SOURCE) +# Source 2 stats - updated every 15 minutes from worldometers.info +https://corona-stats.online?source=2 + +--------------------------------------------------------------------------------- + +# Country wise stats + +## Format: +https://corona-stats.online/[countryCode] +https://corona-stats.online/[countryName] + +## Example: From source 1 +https://corona-stats.online/Italy?source=1 +https://corona-stats.online/UK?source=1 + +## Example: From source 2 (DEFAULT) +https://corona-stats.online/italy +https://corona-stats.online/italy?source=2 +https://corona-stats.online/UK?source=2 +https://corona-stats.online/UK + +--------------------------------------------------------------------------------- + +# State wise api (Only for US as of now) + +## Format: +https://corona-stats.online/states/[countryCode] +https://corona-stats.online/states/[countryName] + +## Example: From source 1 +https://corona-stats.online/us +https://corona-stats.online/USA?format=json +https://corona-stats.online/USA?minimal=true + +--------------------------------------------------------------------------------- + +# Minimal Mode - remove the borders and padding from table + +## Example: +https://corona-stats.online?minimal=true +https://corona-stats.online/Italy?minimal=true (with country filter) +https://corona-stats.online?minimal=true&source=1 (with source) +https://corona-stats.online/uk?source=2&minimal=true (with source and country) + +--------------------------------------------------------------------------------- + +# Get data as JSON - Add ?format=json + +## Example: +https://corona-stats.online?format=json +https://corona-stats.online/Italy?format=json (with country filter) +https://corona-stats.online/?source=2&format=json (with source) +https://corona-stats.online/uk?source=2&format=json (with source and country) + +--------------------------------------------------------------------------------- + +# Get top N countries - Add ?top=N + +## Example: +https://corona-stats.online?top=25 +https://corona-stats.online?source=1&top=10 (with source) +https://corona-stats.online/uk?minimal=true&top=20 (with minimal) + + +--------------------------------------------------------------------------------- + +# Confirmed Cases Graph (WIP) + +## Format: +https://corona-stats.online/[countryName]/graph +https://corona-stats.online/[countryCode]/graph + +## Example: +https://corona-stats.online/italy/graph +https://corona-stats.online/china/graph + + +------------- Any issues or feedback - Hit me up on twitter @ekrysis -------------- + +`; + +exports.countryNotFound = (isCurl) => { + const body = ` + Country not found. + Try the full country name or country code. + Example: + - /UK: for United Kingdom + - /US: for United States of America. + - /Italy: for Italy. + + ${footer(new Date)} + `; + return isCurl ? body : htmlTemplate(body); +}; + +exports.stateCountryNotFound = (isCurl) => { + const body = ` + State wise api is only available for: + - US + Try: + /US or /USA + + ${footer(new Date)} + `; + return isCurl ? body : htmlTemplate(body); }; \ No newline at end of file diff --git a/lib/corona.js b/lib/corona.js index 1581125..be1cafa 100644 --- a/lib/corona.js +++ b/lib/corona.js @@ -1,17 +1,16 @@ const Table = require('cli-table3'); const _ = require('lodash'); const helpers = require('./helpers'); +const asciichart = require('asciichart'); const api = require('./api'); +const chalk = require('chalk'); const { extraStats, getCountry, getConfirmed, - getActive, getDeaths, - getRecovered, getMortalityPer, - getRecoveredPer, getEmoji, getOneDayChange, getOneWeekChange, @@ -20,11 +19,10 @@ const { htmlTemplate, } = require('./helpers'); -function getDataByCountry(confirmed, deaths, recovered) { +function getDataByCountry(confirmed, deaths) { const countryMap = {}; const lastUpdated = confirmed.last_updated; const confirmedMap = _.keyBy(confirmed.locations, (i) => i.country + i.province); - const recoveredMap = _.keyBy(recovered.locations, (i) => i.country + i.province); const deathsMap = _.keyBy(deaths.locations, (i) => i.country + i.province); confirmed.locations.forEach(obj => { const countryName = obj.country; @@ -35,25 +33,18 @@ function getDataByCountry(confirmed, deaths, recovered) { country: countryName, countryCode: obj.country_code, confirmed: confirmedMap[mapKey].latest, - recovered: recoveredMap[mapKey].latest, deaths: deathsMap[mapKey].latest, confirmedByDay: helpers.historyObjToArr(confirmedMap[mapKey].history), - recoveredByDay: helpers.historyObjToArr(recoveredMap[mapKey].history), deathsByDay: helpers.historyObjToArr(deathsMap[mapKey].history), lastUpdated, }; } else { countryMap[countryName].confirmed += confirmedMap[mapKey].latest; - countryMap[countryName].recovered += recoveredMap[mapKey].latest; countryMap[countryName].deaths += deathsMap[mapKey].latest; countryMap[countryName].confirmedByDay = helpers.addArr( countryMap[countryName].confirmedByDay, helpers.historyObjToArr(confirmedMap[mapKey].history) ); - countryMap[countryName].recoveredByDay = helpers.addArr( - countryMap[countryName].recoveredByDay, - helpers.historyObjToArr(recoveredMap[mapKey].history) - ); countryMap[countryName].deathsByDay = helpers.addArr( countryMap[countryName].deathsByDay, helpers.historyObjToArr(deathsMap[mapKey].history) @@ -66,6 +57,38 @@ function getDataByCountry(confirmed, deaths, recovered) { return _.sortBy(countryArr, (o) => -o.confirmed); } +exports.getGraph = async ({ countryCode = 'ALL', isCurl = true}) => { + const data = await api.getCoronaData(); + const { confirmed, deaths, } = data; + const countryData = getDataByCountry(confirmed, deaths,); + const worldStats = getTotalStats(countryData); + worldStats.countryCode = 'ALL'; + worldStats.countryName = 'World'; + countryData.push(worldStats); + + // const graphLength = ' '; + const graphLength = ' Confirmed Cases Graph '; + const padding = ' '; + const graphConfig = { + height: 30, + offset: 2, + padding, + }; + const singleCountryData = countryData.filter(obj => obj.countryCode === countryCode); + const { confirmedByDay } = singleCountryData[0]; + + const confirmedGraph = asciichart.plot(confirmedByDay, graphConfig); + const body = chalk.greenBright(confirmedGraph) + + chalk.cyanBright('\n\n' +padding + '22 Feb' + graphLength + 'Today') + '\n'; + + if (!isCurl) { + return htmlTemplate(body); + } + return body; + + +}; + exports.getCompleteTable = async ({ isCurl = true, emojis = false, @@ -78,17 +101,14 @@ exports.getCompleteTable = async ({ style: helpers.getTableStyles(minimal), }); const data = await api.getCoronaData(); - const { confirmed, deaths, recovered } = data; - const countryData = getDataByCountry(confirmed, deaths, recovered); + const { confirmed, deaths, } = data; + const countryData = getDataByCountry(confirmed, deaths); const worldStats = getTotalStats(countryData); const worldRow = [ 'World', getConfirmed(worldStats.confirmed), - getRecovered(worldStats.recovered), getDeaths(worldStats.deaths), - getActive(worldStats.active), getMortalityPer(worldStats.mortalityPer), - getRecoveredPer(worldStats.recoveredPer), getOneDayChange(worldStats), getOneWeekChange(worldStats), ...(emojis ? ['π'] : []) @@ -101,11 +121,8 @@ exports.getCompleteTable = async ({ const values = [ getCountry(`${cd.country} (${cd.countryCode})`), getConfirmed(cd.confirmed), - getRecovered(cd.recovered), getDeaths(cd.deaths), - getActive(cd.active), getMortalityPer(cd.mortalityPer), - getRecoveredPer(cd.recoveredPer), getOneDayChange(cd), getOneWeekChange(cd), ...(emojis ? [countryEmoji] : []) diff --git a/lib/country/us.js b/lib/country/us.js new file mode 100644 index 0000000..60e5b4d --- /dev/null +++ b/lib/country/us.js @@ -0,0 +1,57 @@ +const Table = require('cli-table3'); +const chalk = require('chalk'); +const helpers = require('../helpers'); +const api = require('../api'); +const { cFormatter } = helpers; + + +const getUsaStatsHeaders = (emojis, secondColumnName) => { + const head = [ + 'Rank', + secondColumnName, + `Total Cases ${emojis ? ' β ' : ''}`, + 'New Cases β²', + `Total Deaths${emojis ? ' π' : ''}`, + `New Deaths β²${emojis ? ' π' : ''}`, + `Active${emojis ? ' π·' : ''}`, + + ]; + return head; +}; + + +exports.getUsaStats = async ({ + isCurl = true, + minimal = false, + top = 1000, + format, +}) => { + const secondColumnName = 'US States'; + const table = new Table({ + head: getUsaStatsHeaders(null, secondColumnName), + chars: helpers.getTableBorders(minimal), + style: helpers.getTableStyles(minimal), + }); + const { data } = await api.usaStats(); + if (format === 'json') { + return { data }; + } + + let rank = 1; + data.some(cd => { + const values = [ + cFormatter(cd.state , chalk.cyanBright), + cFormatter(cd.cases, chalk.green, 'right', true), + cFormatter(cd.todayCases, chalk.cyanBright, 'right', true, ' β²'), + cFormatter(cd.deaths, chalk.whiteBright, 'right', true), + cFormatter(cd.todayDeaths, chalk.redBright, 'right', true, ' β²'), + cFormatter(cd.active, chalk.blueBright , 'right', true), + ]; + table.push({ [rank++]: values }); + return rank === top + 1; + }); + + const lastUpdated = new Date(); + const ret = table.toString() + helpers.footer(lastUpdated); + return isCurl ? ret : helpers.htmlTemplate(ret); +}; \ No newline at end of file diff --git a/lib/helpers.js b/lib/helpers.js index c5a5295..566986f 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -42,7 +42,7 @@ e.getDeaths = (deaths) => { e.getActive = (active) => { return { content: chalk.magentaBright(h(active)), - hAlign: 'right' + hAlign: 'right', }; }; @@ -63,9 +63,12 @@ e.getEmoji = (countryCode) => { return ''; }; -e.calActive = ({ confirmed, recovered, deaths }) => confirmed - (recovered + deaths); -e.calMortalityPer = ({ confirmed, deaths }) => ((deaths / confirmed) * 100).toFixed(2); -e.calRecoveredPer = ({ confirmed, recovered }) => ((recovered / confirmed) * 100).toFixed(2); +e.calActive = ({ confirmed, recovered, deaths }) => + confirmed - (recovered + deaths); +e.calMortalityPer = ({ confirmed, deaths }) => + ((deaths / confirmed) * 100).toFixed(2); +e.calRecoveredPer = ({ confirmed, recovered }) => + ((recovered / confirmed) * 100).toFixed(2); /** historyObj = { @@ -78,11 +81,11 @@ e.calRecoveredPer = ({ confirmed, recovered }) => ((recovered / confirmed) * 100 */ e.historyObjToArr = (historyObj) => { const sortedTimestampArr = _.sortBy( - Object.keys(historyObj).map(date => new Date(date).getTime()), - Number + Object.keys(historyObj).map((date) => new Date(date).getTime()), + Number, ); - return sortedTimestampArr.map(timestamp => { + return sortedTimestampArr.map((timestamp) => { const dateFormatted = moment(timestamp).format('M/D/YY'); return historyObj[dateFormatted]; }); @@ -92,7 +95,7 @@ e.historyObjToArr = (historyObj) => { * Given both arr1 and arr2 has same number of elements * Returns -> sum[n] = arr1[n] + arr2[n] * -*/ + */ e.addArr = (arr1, arr2) => { if (arr1.length === 0) { return arr2; @@ -126,45 +129,50 @@ e.getOneWeekChange = ({ confirmedByDay }) => { }; e.getTotalStats = (countryData) => { - const worldStats = countryData.reduce((acc, countryObj) => { - acc.confirmed += countryObj.confirmed; - acc.recovered += countryObj.recovered; - acc.deaths += countryObj.deaths; - acc.confirmedByDay = e.addArr(acc.confirmedByDay, countryObj.confirmedByDay); - acc.recoveredByDay = e.addArr(acc.recoveredByDay, countryObj.recoveredByDay); - acc.deathsByDay = e.addArr(acc.deathsByDay, countryObj.deathsByDay); - return acc; - }, { - confirmed: 0, - recovered: 0, - deaths: 0, - confirmedByDay: [], - recoveredByDay: [], - deathsByDay: [], - }); + const worldStats = countryData.reduce( + (acc, countryObj) => { + acc.confirmed += countryObj.confirmed; + acc.deaths += countryObj.deaths; + acc.confirmedByDay = e.addArr( + acc.confirmedByDay, + countryObj.confirmedByDay, + ); + acc.deathsByDay = e.addArr(acc.deathsByDay, countryObj.deathsByDay); + return acc; + }, + { + confirmed: 0, + deaths: 0, + confirmedByDay: [], + deathsByDay: [], + }, + ); - worldStats.active = e.calActive(worldStats); - worldStats.recoveredPer = e.calRecoveredPer(worldStats); worldStats.mortalityPer = e.calMortalityPer(worldStats); - return worldStats; }; -e.countryUpperCase = country => { +e.countryUpperCase = (country) => { if (country.length > 2) { - return country.toLowerCase().split(/\s+/).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); + return country + .toLowerCase() + .split(/\s+/) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); } return country; }; -e.lookupCountry = country => { +e.lookupCountry = (country) => { country = e.countryUpperCase(country); try { - return lookup.byIso(country) - || lookup.byFips(country) - || lookup.byCountry(country); + return ( + lookup.byIso(country) || + lookup.byFips(country) || + lookup.byCountry(country) + ); } catch (error) { return lookup.byFips(country) || lookup.byCountry(country); } @@ -172,35 +180,72 @@ e.lookupCountry = country => { e.footer = (lastUpdated) => ` -β οΈ ${chalk.cyanBright('Stay safe. Stay inside.')} - -π» ${chalk.greenBright('Code')}: ${chalk.blueBright('https://github.com/sagarkarira/coronavirus-tracker-cli')} -β‘οΈ ${chalk.greenBright('Twitter')}: ${chalk.blueBright('https://twitter.com/ekrysis')} - -β³ ${chalk.magentaBright('Last Updated on:')} ${moment(lastUpdated).utc().format('DD-MMM-YYYY HH:MM')} UTC - -${chalk.red.bold.underline('NEW UPDATE (REALTIME STATS)')}: ${chalk.blueBright('curl https://corona-stats.online?source=2')} +${chalk.greenBright('Code')}: ${chalk.blueBright( + 'https://github.com/sagarkarira/coronavirus-tracker-cli', +)} +${chalk.greenBright('Twitter')}: ${chalk.blueBright( + 'https://twitter.com/ekrysis', +)} + +${chalk.magentaBright('Last Updated on:')} ${moment(lastUpdated) + .utc() + .format('DD-MMM-YYYY HH:MM')} UTC + +${chalk.red.bold('US STATES API')}: ${chalk.blueBright( + 'https://corona-stats.online/states/us', +)} +${chalk.red.bold('HELP')}: ${chalk.blueBright( + 'https://corona-stats.online/help', +)} +${chalk.red.bold('SPONSORED BY')}: ${chalk.blueBright('Vercel')} +${chalk.greenBright( + 'Checkout fun new side project I am working on', +)}: ${chalk.bold.blueBright( + 'https://messagink.com', +)} `; -e.getTableBorders = minimal => { +e.getTableBorders = (minimal) => { if (minimal) { return { - 'top': '', 'top-mid': '', 'top-left': '', 'top-right': '', - 'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '', - 'left': '', 'left-mid': '', 'mid': '', 'mid-mid': '', - 'right': '', 'right-mid': '', 'middle': ' ' + top: '', + 'top-mid': '', + 'top-left': '', + 'top-right': '', + bottom: '', + 'bottom-mid': '', + 'bottom-left': '', + 'bottom-right': '', + left: '', + 'left-mid': '', + mid: '', + 'mid-mid': '', + right: '', + 'right-mid': '', + middle: ' ', }; } return { - 'top': 'β', 'top-mid': 'β€', 'top-left': 'β', 'top-right': 'β', - 'bottom': 'β', 'bottom-mid': 'β§', 'bottom-left': 'β', 'bottom-right': 'β', - 'left': 'β', 'left-mid': 'β', 'mid': 'β', 'mid-mid': 'βΌ', - 'right': 'β', 'right-mid': 'β’', 'middle': 'β', + top: 'β', + 'top-mid': 'β€', + 'top-left': 'β', + 'top-right': 'β', + bottom: 'β', + 'bottom-mid': 'β§', + 'bottom-left': 'β', + 'bottom-right': 'β', + left: 'β', + 'left-mid': 'β', + mid: 'β', + 'mid-mid': 'βΌ', + right: 'β', + 'right-mid': 'β’', + middle: 'β', }; }; -e.getTableStyles = minimal => { +e.getTableStyles = (minimal) => { if (minimal) { return { 'padding-left': 0, 'padding-right': 0 }; } @@ -211,11 +256,8 @@ e.getTableHeaders = (emojis, secondColumnName) => { 'Rank', secondColumnName, `Confirmed ${emojis ? ' β ' : ''}`, - `Recovered${emojis ? ' π' : ''}`, `Deaths${emojis ? ' π' : ''}`, - `Active${emojis ? ' π·' : ''}`, - 'Mortality %', - 'Recovered %', + 'CFR %', '1 Day β²', '1 Week β²', ...(emojis ? ['π³'] : []), @@ -239,14 +281,11 @@ e.getTableHeadersV2 = (emojis, secondColumnName) => { ]; return head; }; - e.extraStats = (dataArr) => { - return dataArr.map(obj => { + return dataArr.map((obj) => { return { ...obj, - active: e.calActive(obj), mortalityPer: e.calMortalityPer(obj), - recoveredPer: e.calRecoveredPer(obj) }; }); }; @@ -259,18 +298,37 @@ e.htmlTemplate = (body) => {
${body}
+ ${body}
+