├── .github └── workflows │ ├── budget.json │ ├── ci.yml │ ├── deploy.yml │ ├── lighthouse-ci.json │ └── update-data.yml ├── .gitignore ├── .prettierrc ├── .rancher-pipeline.yml ├── Assets ├── Icons │ ├── Readme.txt │ ├── [PLD] Media [P] Academia XX v1.0.svg │ ├── [PLD] Media [P] Isolation XX v1.0.svg │ ├── [PLD] Media [P] Outdoors XX v1.0.svg │ ├── [PLD] Media [P] Power XX v1.0.svg │ ├── [PLD] Media [P] Shopping XX v1.0.svg │ ├── [PLD] Media [P] Telecom XX v1.0.svg │ ├── [PLD] Media [P] Water XX v1.0.svg │ ├── [PLD] Media [P] Work XX v1.0.svg │ ├── travel-flight.svg │ ├── travel-land.svg │ └── travel-sea.svg ├── Readme.txt ├── logo-dark.png ├── logo.png └── offline-icon.svg ├── Dockerfile ├── LICENSE ├── README.md ├── azure ├── .funcignore ├── .gitignore ├── GenerateLockdownsSnapshot │ ├── function.json │ ├── index.js │ ├── readme.md │ └── sample.dat ├── GenerateReportHttpHandler │ ├── function.json │ ├── index.js │ └── sample.dat ├── SendNotificationAboutLockdownReport │ ├── function.json │ ├── index.js │ ├── readme.md │ └── sample.dat ├── host.json ├── package.json └── proxies.json ├── backend ├── .deployment ├── README.md ├── api.yaml ├── babel.config.js ├── data │ └── population.json ├── nodemon.json ├── package-lock.json ├── package.json ├── src │ ├── config.js │ ├── index.js │ ├── loaders │ │ ├── index.js │ │ ├── lockdown │ │ │ ├── googlesheet.js │ │ │ ├── lockdown.js │ │ │ ├── parsers │ │ │ │ └── lockdownParser.js │ │ │ ├── snapshot │ │ │ │ └── processor.js │ │ │ └── updates.js │ │ ├── totals │ │ │ ├── corona.js │ │ │ ├── territories.js │ │ │ └── totals.js │ │ └── worldmap │ │ │ ├── base.json │ │ │ └── worldmap.js │ ├── main.js │ ├── repositories │ │ ├── Database.js │ │ ├── SnapshotRepository.js │ │ └── index.js │ ├── server.js │ ├── services │ │ ├── CacheService.js │ │ ├── MessagesService.js │ │ └── SnapshotsService.js │ ├── types │ │ ├── DataPoint.js │ │ ├── Entry.js │ │ ├── Measure.js │ │ ├── Snapshot.js │ │ └── Travel.js │ └── utils │ │ ├── CustomGoogleSpreadsheet.js │ │ ├── SimpleGrid.js │ │ ├── dataHelper.js │ │ ├── errors.js │ │ ├── file.js │ │ ├── logger.js │ │ ├── moment.js │ │ ├── sheet.js │ │ └── typeHelper.js ├── tests │ ├── data │ │ └── lockdown_summary.json │ ├── dataHelper.test.js │ └── loaders │ │ ├── spreadsheet.test.js │ │ └── totals.test.js └── web.config ├── budget.json ├── data ├── boundaries-adm0-v3.json ├── population.json ├── territoriesData.js ├── totals.json ├── updates.json ├── worldmap.json ├── worldmap_small.json └── worldpopulation.json ├── deployment.yaml ├── index.html ├── local_dependencies ├── es6-promise.auto.min.js ├── es6-promise.min.js └── i18n.min.js ├── manifest-dark.json ├── manifest.json ├── nginx.conf ├── package.json ├── rollup.config.js ├── shared └── types.js ├── src ├── assets │ ├── apple-touch-icon-dark.png │ ├── apple-touch-icon.png │ ├── favicon-16x16-dark.png │ ├── favicon-16x16.png │ ├── favicon-32x32-dark.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── icons │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── arrow-left.js │ │ ├── arrow-right.js │ │ ├── burger.svg.js │ │ ├── calendar.svg │ │ ├── calendar.svg.js │ │ ├── chevron-down.js │ │ ├── chevron-down.svg │ │ ├── chevron-right.svg │ │ ├── chevron-up.js │ │ ├── circle-plus.svg │ │ ├── contribute.svg.js │ │ ├── download.svg │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── icons.js │ │ ├── info.svg │ │ ├── info.svg.js │ │ ├── list.svg │ │ ├── list.svg.js │ │ ├── loading.svg │ │ ├── loading.svg.js │ │ ├── lock.svg │ │ ├── lock.svg.js │ │ ├── logo.svg │ │ ├── logo.svg.js │ │ ├── magnify.svg │ │ ├── magnify.svg.js │ │ ├── measures.svg.js │ │ ├── menu.svg │ │ ├── menu.svg.js │ │ ├── menu │ │ │ ├── black.svg │ │ │ └── white.svg │ │ ├── mstile-150x150.png │ │ ├── offline.svg.js │ │ ├── refresh.svg │ │ ├── settings.svg │ │ ├── settings.svg.js │ │ ├── ticker.svg.js │ │ ├── travel-flight.svg.js │ │ ├── travel-land.svg.js │ │ ├── travel-sea.svg.js │ │ ├── true.svg │ │ ├── true.svg.js │ │ ├── twitter.svg │ │ ├── unlock.svg │ │ ├── unlock.svg.js │ │ ├── virus.svg │ │ ├── virus.svg.js │ │ ├── viruslock.svg │ │ ├── viruslock.svg.js │ │ ├── world.js │ │ ├── x.svg │ │ └── x.svg.js │ ├── images │ │ ├── logo.png │ │ ├── pld-report-lrg.png │ │ ├── pld-report-med.png │ │ ├── pld-report-sm.png │ │ └── stripes-pattern-2.png │ ├── lockdown-logo.svg │ └── pwa │ │ ├── apple-icon-120.png │ │ ├── apple-icon-152.png │ │ ├── apple-icon-167.png │ │ ├── apple-icon-180.png │ │ ├── apple-splash-1125-2436.png │ │ ├── apple-splash-1136-640.png │ │ ├── apple-splash-1242-2208.png │ │ ├── apple-splash-1242-2688.png │ │ ├── apple-splash-1334-750.png │ │ ├── apple-splash-1536-2048.png │ │ ├── apple-splash-1668-2224.png │ │ ├── apple-splash-1668-2388.png │ │ ├── apple-splash-1792-828.png │ │ ├── apple-splash-2048-1536.png │ │ ├── apple-splash-2048-2732.png │ │ ├── apple-splash-2208-1242.png │ │ ├── apple-splash-2224-1668.png │ │ ├── apple-splash-2388-1668.png │ │ ├── apple-splash-2436-1125.png │ │ ├── apple-splash-2688-1242.png │ │ ├── apple-splash-2732-2048.png │ │ ├── apple-splash-640-1136.png │ │ ├── apple-splash-750-1334.png │ │ ├── apple-splash-828-1792.png │ │ ├── apple-splash-dark-1125-2436.png │ │ ├── apple-splash-dark-1136-640.png │ │ ├── apple-splash-dark-1242-2208.png │ │ ├── apple-splash-dark-1242-2688.png │ │ ├── apple-splash-dark-1334-750.png │ │ ├── apple-splash-dark-1536-2048.png │ │ ├── apple-splash-dark-1668-2224.png │ │ ├── apple-splash-dark-1668-2388.png │ │ ├── apple-splash-dark-1792-828.png │ │ ├── apple-splash-dark-2048-1536.png │ │ ├── apple-splash-dark-2048-2732.png │ │ ├── apple-splash-dark-2208-1242.png │ │ ├── apple-splash-dark-2224-1668.png │ │ ├── apple-splash-dark-2388-1668.png │ │ ├── apple-splash-dark-2436-1125.png │ │ ├── apple-splash-dark-2688-1242.png │ │ ├── apple-splash-dark-2732-2048.png │ │ ├── apple-splash-dark-640-1136.png │ │ ├── apple-splash-dark-750-1334.png │ │ ├── apple-splash-dark-828-1792.png │ │ ├── manifest-icon-192.png │ │ └── manifest-icon-512.png ├── bootstrap.js ├── components │ ├── App.js │ ├── CountriesSearcher.js │ ├── CountryInfo.js │ ├── CountryInfo.styles.js │ ├── DatePicker.js │ ├── Dialog.js │ ├── Expandable.js │ ├── Header.js │ ├── LanguageSelector.js │ ├── Lazy.js │ ├── Legend.js │ ├── Menu.js │ ├── Settings.js │ ├── Tabs.js │ ├── TerritoryTabs.js │ ├── Ticker.js │ ├── TimeSlider.js │ ├── Totals.js │ ├── WorldMap.js │ ├── Wrappers │ │ └── withMobileDetection.js │ ├── pwa-install-button.js │ ├── pwa-update-available.js │ └── tool-tip.js ├── lazy-resources.js ├── libs │ └── rslider.min.js ├── locale │ ├── availableLanguages.js │ ├── i18nUtils.js │ ├── index.js │ └── translations │ │ ├── ar │ │ └── index.js │ │ ├── en-US │ │ └── index.js │ │ ├── en │ │ └── index.js │ │ ├── es-ES │ │ └── index.js │ │ ├── es-MX │ │ └── index.js │ │ ├── es │ │ └── index.js │ │ ├── it │ │ └── index.js │ │ ├── pt-BR │ │ └── index.js │ │ ├── pt │ │ └── index.js │ │ ├── ru │ │ └── index.js │ │ ├── zh-CN │ │ └── index.js │ │ └── zh-HK │ │ └── index.js ├── manifest.json ├── router.js ├── services │ ├── coronaTrackerService.js │ ├── coronastatusService.js │ ├── countryDetailService.js │ ├── dialogService.js │ ├── populationService.js │ ├── services.js │ ├── servicesConfiguration.js │ ├── totalsService.js │ ├── travelAdviceService.js │ └── updatesService.js ├── style │ ├── main.css │ └── shared.styles.js ├── utils │ ├── EventTargetShim.js │ ├── addPwaUpdateListener.js │ └── setFavIcon.js └── version.js ├── sw.js └── webpack.config.js /.github/workflows/budget.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "/*", 4 | "timings": [ 5 | { 6 | "metric": "interactive", 7 | "budget": 3000 8 | }, 9 | { 10 | "metric": "first-meaningful-paint", 11 | "budget": 1000 12 | } 13 | ], 14 | "resourceSizes": [ 15 | { 16 | "resourceType": "script", 17 | "budget": 250000 18 | }, 19 | { 20 | "resourceType": "total", 21 | "budget": 720000 22 | } 23 | ], 24 | "resourceCounts": [ 25 | {"resourceType": "third-party","budget": 27} 26 | ] 27 | } 28 | ] 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | ci: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | with: 12 | persist-credentials: false 13 | 14 | - name: Install 15 | run: | 16 | npm install 17 | 18 | - name: Format 19 | run: | 20 | npm run format 21 | 22 | - name: Lint 23 | run: | 24 | npm run lint 25 | 26 | - name: Build 27 | run: | 28 | npm run build 29 | 30 | - name: Lighthouse 31 | uses: treosh/lighthouse-ci-action@v2 32 | with: 33 | configPath: './.github/workflows/lighthouse-ci.json' 34 | budgetPath: './.github/workflows/budget.json' # test performance budgets 35 | uploadArtifacts: true # save results as an action artifacts 36 | temporaryPublicStorage: true # upload lighthouse report to the temporary storage 37 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 🛎️ 12 | uses: actions/checkout@v2 13 | with: 14 | persist-credentials: true 15 | 16 | - name: Install and Build 🔧 17 | run: | 18 | npm install 19 | npm run build 20 | 21 | - name: Deploy 🚀 22 | uses: JamesIves/github-pages-deploy-action@releases/v3 23 | with: 24 | ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | BRANCH: gh-pages # The branch the action should deploy to. 26 | FOLDER: build # The folder the action should deploy. 27 | -------------------------------------------------------------------------------- /.github/workflows/lighthouse-ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "ci": { 3 | "collect": { 4 | "staticDistDir": "./build" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/workflows/update-data.yml: -------------------------------------------------------------------------------- 1 | name: Update data 2 | 3 | on: 4 | schedule: 5 | - cron: '*/10 * * * *' 6 | 7 | jobs: 8 | update-data: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | with: 14 | persist-credentials: false 15 | 16 | - name: Checkout branch 17 | run: | 18 | git fetch 19 | git checkout origin/master 20 | 21 | - name: Update data 22 | run: | 23 | export MONGO_DB_PASSWORD="${{ secrets.MONGO_DB_PASSWORD }}" 24 | export MONGO_DB="${{ secrets.MONGO_DB }}" 25 | export MONGO_DB_USER="${{ secrets.MONGO_DB_USER }}" 26 | export AZURE_SERVICEBUS_CACHE_QUEUE=${{ secrets.AZURE_SERVICEBUS_CACHE_QUEUE }} 27 | export AZURE_SERVICEBUS_CONNECTION_STRING="${{ secrets.AZURE_SERVICEBUS_CONNECTION_STRING }}" 28 | export GOOGLE_SERVICE_ACCOUNT_EMAIL=${{ secrets.GOOGLE_API_EMAIL }} 29 | export GOOGLE_PRIVATE_KEY="${{ secrets.GOOGLE_API_KEY }}" 30 | cd backend 31 | npm install 32 | npm run start:job 33 | 34 | - name: Commit changes 35 | run: | 36 | git config user.email "lockdown-bot@non-existing.com" 37 | git config user.name "lockdown-bot" 38 | git add data 39 | changed_files=$(git status --porcelain --untracked-files=no | wc -l) 40 | 41 | if [ $changed_files -gt 0 ] 42 | then 43 | git commit -m "Update data" 44 | fi 45 | - name: Push changes 46 | uses: ad-m/github-push-action@master 47 | with: 48 | github_token: ${{ secrets.GITHUB_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | package-lock.json 4 | .vscode 5 | .DS_Store 6 | size-plugin.json 7 | **/venv/ 8 | script/token.pickle 9 | script/credentials.json 10 | .idea/ 11 | backend/node_modules 12 | backend/credentials.json 13 | backend/package-lock.json 14 | backend/dist 15 | *.code-workspace 16 | .env 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 140 4 | } 5 | -------------------------------------------------------------------------------- /.rancher-pipeline.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - name: Publish 3 | steps: 4 | - publishImageConfig: 5 | dockerfilePath: ./Dockerfile 6 | buildContext: . 7 | tag: cbase-staging:${CICD_EXECUTION_SEQUENCE} 8 | - name: Deploy 9 | steps: 10 | - applyYamlConfig: 11 | path: ./deployment.yaml 12 | notification: {} 13 | -------------------------------------------------------------------------------- /Assets/Icons/Readme.txt: -------------------------------------------------------------------------------- 1 | Null 2 | -------------------------------------------------------------------------------- /Assets/Icons/[PLD] Media [P] Academia XX v1.0.svg: -------------------------------------------------------------------------------- 1 | Artboard 65 -------------------------------------------------------------------------------- /Assets/Icons/[PLD] Media [P] Isolation XX v1.0.svg: -------------------------------------------------------------------------------- 1 | Artboard 63 -------------------------------------------------------------------------------- /Assets/Icons/[PLD] Media [P] Outdoors XX v1.0.svg: -------------------------------------------------------------------------------- 1 | Artboard 64 -------------------------------------------------------------------------------- /Assets/Icons/[PLD] Media [P] Power XX v1.0.svg: -------------------------------------------------------------------------------- 1 | Artboard 31 -------------------------------------------------------------------------------- /Assets/Icons/[PLD] Media [P] Shopping XX v1.0.svg: -------------------------------------------------------------------------------- 1 | Artboard 66 -------------------------------------------------------------------------------- /Assets/Icons/[PLD] Media [P] Telecom XX v1.0.svg: -------------------------------------------------------------------------------- 1 | Artboard 69 -------------------------------------------------------------------------------- /Assets/Icons/[PLD] Media [P] Water XX v1.0.svg: -------------------------------------------------------------------------------- 1 | Artboard 68 -------------------------------------------------------------------------------- /Assets/Icons/[PLD] Media [P] Work XX v1.0.svg: -------------------------------------------------------------------------------- 1 | Artboard 60 -------------------------------------------------------------------------------- /Assets/Icons/travel-flight.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Assets/Icons/travel-land.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Assets/Icons/travel-sea.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Assets/Readme.txt: -------------------------------------------------------------------------------- 1 | This folder contains all the assets used in Project Lockdown. 2 | -------------------------------------------------------------------------------- /Assets/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/Assets/logo-dark.png -------------------------------------------------------------------------------- /Assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/Assets/logo.png -------------------------------------------------------------------------------- /Assets/offline-icon.svg: -------------------------------------------------------------------------------- 1 | Asset 2 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1 - the build process 2 | FROM node:10-alpine as build-deps 3 | WORKDIR /usr/src/app 4 | COPY . ./ 5 | RUN npm i && npm run build 6 | 7 | # Stage 2 - the production environment 8 | FROM nginx:1.17-alpine 9 | RUN rm /etc/nginx/conf.d/default.conf 10 | COPY nginx.conf /etc/nginx/conf.d/000-react.conf 11 | COPY --from=build-deps /usr/src/app/build /usr/share/nginx/html 12 | RUN sed -i -r "s/\/lockdown\//\//g" /usr/share/nginx/html/index.html 13 | EXPOSE 80 14 | CMD ["nginx", "-g", "daemon off;"] 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![logo](https://user-images.githubusercontent.com/9198668/85232285-68543380-b430-11ea-8353-1aafb79baf78.png) 3 | *** 4 | 5 | 6 | ## Please note that this repository has been transitioned to https://github.com/TheIOFoundation/ProjectLockdown 7 | 8 | The information contained here is outdated and kept provisionally for reference purposes only. 9 | 10 | # Project Lockdown 11 | 12 | **Project Lockdown** (an initiative from The IO Foundation) is a civic tech, interactive platform providing an overview of the state of Human and Digital Rights around the globe. It evaluates policies obtained from high quality sources that may impact their observance. It provides, among other tools, a layered map interface that allows for a visual representation of the policies adopted, assisting a broad range of stakeholders in understanding the global state of their Rights. This empowers them to become active agents of global change. 13 | 14 | The project is the result of the collaborative effort from a global network of partners and volunteers who are dedicating their time and resources to ensure that we do not degrade the Rights we currently enjoy while simultaneously preparing them to become a new generation of Rights defenders. 15 | 16 | The objective is to provide an overview of the state of the world to citizens and assist journalists and Human Rights defenders in their reporting and overseeing tasks. 17 | 18 | https://ProjectLockdown.world 19 | 20 | The IO Foundation: www.TheIOFoundation.org 21 | 22 | ## Scripts 23 | 24 | ```bash 25 | # install dependencies 26 | npm install 27 | 28 | # start for local development 29 | npm run start 30 | 31 | # build for production with minification 32 | npm run build 33 | 34 | # test the production build locally 35 | npm run start:build 36 | 37 | # format the code 38 | npm run format 39 | ``` 40 | 41 | ## Technologies used 42 | 43 | ### Frontend 44 | - [Preact](https://preactjs.com/)/[htm](https://github.com/developit/htm) 45 | - [csz](https://github.com/lukejacksonn/csz) 46 | - [leafletjs](https://leafletjs.com/) (/mapbox api for tiles) 47 | - [pwa-helper-components](https://github.com/thepassle/pwa-helpers) 48 | 49 | ### Dev 50 | - [prettier](https://prettier.io/) 51 | - [es-dev-server](https://open-wc.org/developing/es-dev-server.html) 52 | - [github actions](https://github.com/features/actions) 53 | - [netlify](https://www.netlify.com/) 54 | 55 | ### Build 56 | - [rollup](https://rollupjs.org/guide/en/) 57 | - [@rollup/plugin-node-resolve](https://www.npmjs.com/package/@rollup/plugin-node-resolve) 58 | - [@open-wc/rollup-plugin-html](https://github.com/open-wc/open-wc/tree/master/packages/rollup-plugin-html) 59 | - [rollup-plugin-terser](https://www.npmjs.com/package/rollup-plugin-terser) 60 | - [rollup-plugin-babel](https://github.com/rollup/rollup-plugin-babel) 61 | - [rollup-plugin-copy](https://www.npmjs.com/package/rollup-plugin-copy) 62 | - [rollup-plugin-workbox](https://www.npmjs.com/package/rollup-plugin-workbox) 63 | - [rollup-plugin-apply-sw-registration](https://github.com/thepassle/rollup-plugin-apply-sw-registration) 64 | - [@rollup/plugin-replace](https://www.npmjs.com/package/@rollup/plugin-replace) 65 | - [uglifycss](https://www.npmjs.com/package/uglifycss) 66 | - [pwa-asset-generator](https://github.com/onderceylan/pwa-asset-generator) 67 | 68 | ## Contributing 69 | 70 | Did you find a bug? Feel free create a PR, and we'll look at it as soon as we can. Please run `npm run format` before pushing 🙂. 71 | -------------------------------------------------------------------------------- /azure/.funcignore: -------------------------------------------------------------------------------- 1 | *.js.map 2 | *.ts 3 | .git* 4 | .vscode 5 | local.settings.json 6 | test 7 | tsconfig.json -------------------------------------------------------------------------------- /azure/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | # TypeScript output 87 | dist 88 | out 89 | 90 | # Azure Functions artifacts 91 | bin 92 | obj 93 | appsettings.json 94 | local.settings.json -------------------------------------------------------------------------------- /azure/GenerateLockdownsSnapshot/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "name": "myTimer", 5 | "type": "timerTrigger", 6 | "direction": "in", 7 | "schedule": "0 0 0 * * *" 8 | }, 9 | { 10 | "type": "blob", 11 | "direction": "out", 12 | "name": "reportOutput", 13 | "path": "lockdown/reports/{rand-guid}.txt", 14 | "connection": "AzureWebJobsStorage" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /azure/GenerateLockdownsSnapshot/index.js: -------------------------------------------------------------------------------- 1 | async function connect() { 2 | const MongoClient = require('mongodb').MongoClient; 3 | const uri = `mongodb://${process.env.MONGO_DB_USER}:${process.env.MONGO_DB_PASSWORD}@${process.env.MONGO_DB}`; 4 | const client = new MongoClient(uri, { useNewUrlParser: true }); 5 | const connection = await client.connect(); 6 | return connection.db("lockdown"); 7 | } 8 | 9 | function compare(currentData, snapshotData) { 10 | const result = {}; 11 | const currentDate = new Date(); 12 | currentData.forEach(element => { 13 | element.snapshot_date = currentDate; 14 | var founded = snapshotData 15 | .filter(e => e.unique_id == element.unique_id); 16 | if (founded.length == 0) { 17 | result[element.iso3] = {}; 18 | element.measures.forEach(m => result[element.iso3][m.label] = {}) 19 | } 20 | }); 21 | 22 | const StringBuilder = require("string-builder"); 23 | let sb = new StringBuilder(); 24 | 25 | sb.appendLine(`Report generated: ${new Date()}`) 26 | 27 | Object.keys(result).forEach(countryKey => { 28 | const country = result[countryKey]; 29 | 30 | sb.appendLine(`${countryKey}: `); 31 | Object.keys(country).forEach(key => { 32 | sb.append(`${key}, `); 33 | }); 34 | }); 35 | 36 | return { data: result, report: sb.toString() }; 37 | } 38 | 39 | module.exports = async function (context, myTimer, updateSnapshotInput) { 40 | const appInsights = require("applicationinsights"); 41 | appInsights.setup(process.env.APP_INSIGHTS); 42 | appInsights.start(); 43 | 44 | const db = await connect(); 45 | const snapshots = db.collection("snapshots"); 46 | const snapshotsCopy = db.collection("snapshots_copy"); 47 | 48 | const snapshotsRecords = await snapshotsCopy.find().toArray(); 49 | const allRecords = await snapshots.find().toArray(); 50 | 51 | const result = compare(allRecords, snapshotsRecords); 52 | 53 | await snapshotsCopy.remove(); 54 | await snapshotsCopy.insertMany(allRecords); 55 | 56 | if (Object.keys(result.data).length > 0) { 57 | return { reportOutput: result.report } 58 | } 59 | 60 | return {}; 61 | }; -------------------------------------------------------------------------------- /azure/GenerateLockdownsSnapshot/readme.md: -------------------------------------------------------------------------------- 1 | # TimerTrigger - JavaScript 2 | 3 | The `TimerTrigger` makes it incredibly easy to have your functions executed on a schedule. This sample demonstrates a simple use case of calling your function every 5 minutes. 4 | 5 | ## How it works 6 | 7 | For a `TimerTrigger` to work, you provide a schedule in the form of a [cron expression](https://en.wikipedia.org/wiki/Cron#CRON_expression)(See the link for full details). A cron expression is a string with 6 separate expressions which represent a given schedule via patterns. The pattern we use to represent every 5 minutes is `0 */5 * * * *`. This, in plain text, means: "When seconds is equal to 0, minutes is divisible by 5, for any hour, day of the month, month, day of the week, or year". 8 | 9 | ## Learn more 10 | 11 | Documentation -------------------------------------------------------------------------------- /azure/GenerateLockdownsSnapshot/sample.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/azure/GenerateLockdownsSnapshot/sample.dat -------------------------------------------------------------------------------- /azure/GenerateReportHttpHandler/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "function", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "methods": [ 9 | "get", 10 | "post" 11 | ] 12 | }, 13 | { 14 | "type": "http", 15 | "direction": "out", 16 | "name": "res" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /azure/GenerateReportHttpHandler/index.js: -------------------------------------------------------------------------------- 1 | async function connect() { 2 | const MongoClient = require('mongodb').MongoClient; 3 | const uri = `mongodb://${process.env.MONGO_DB_USER}:${process.env.MONGO_DB_PASSWORD}@${process.env.MONGO_DB}`; 4 | const client = new MongoClient(uri, { useNewUrlParser: true }); 5 | const connection = await client.connect(); 6 | return connection.db("lockdown"); 7 | } 8 | 9 | function compare(currentData, snapshotData) { 10 | const result = {}; 11 | const currentDate = new Date(); 12 | const humanizeDuration = require('humanize-duration') 13 | 14 | currentData.forEach(element => { 15 | var founded = snapshotData 16 | .filter(e => e.unique_id == element.unique_id); 17 | if (founded.length == 0) { 18 | result[element.iso3] = {}; 19 | element.measures.forEach(m => result[element.iso3][m.label] = {}) 20 | } 21 | }); 22 | 23 | const report = []; 24 | 25 | const snapshot = snapshotData[0] || { snapshot_date: new Date('2019-11-01') }; 26 | var humanDuration = humanizeDuration(currentDate - snapshot.snapshot_date); 27 | 28 | report.push(`# Changes for the last: ${humanDuration}`); 29 | const StringBuilder = require("string-builder"); 30 | 31 | let total = 0; 32 | let sb = new StringBuilder(); 33 | Object.keys(result).forEach(countryKey => { 34 | const country = result[countryKey]; 35 | 36 | sb.append(`* ${countryKey}: `); 37 | Object.keys(country).forEach(key => { 38 | sb.append(`\`${key}\`, `); 39 | }); 40 | if (total % 5 == 0) { 41 | report.push(sb.toString()); 42 | sb = new StringBuilder(); 43 | } else { 44 | sb.appendLine(); 45 | } 46 | 47 | total++; 48 | }); 49 | 50 | return { data: result, report: report }; 51 | } 52 | 53 | module.exports = async function (context, req) { 54 | const db = await connect(); 55 | const snapshots = db.collection("snapshots"); 56 | const snapshotsCopy = db.collection("snapshots_copy"); 57 | 58 | const snapshotsRecords = await snapshotsCopy.find().toArray(); 59 | const allRecords = await snapshots.find().toArray(); 60 | 61 | const result = compare(allRecords, snapshotsRecords); 62 | const snapshot = snapshotsRecords[0] || { snapshot_date: new Date('2019-11-01') }; 63 | 64 | let report = result.report; 65 | if (Object.keys(result.data).length == 0) { 66 | const humanizeDuration = require('humanize-duration'); 67 | var humanDuration = humanizeDuration(new Date() - snapshot.snapshot_date); 68 | report = [`> No Changes for the last: ${humanDuration}`]; 69 | } 70 | 71 | context.res = { 72 | // status: 200, /* Defaults to 200 */ 73 | body: { 74 | "blocks": report.map(r => { 75 | return { 76 | "type": "section", 77 | "text": { 78 | "type": "mrkdwn", 79 | "text": r 80 | } 81 | } 82 | }) 83 | }, 84 | headers: { 85 | 'Content-Type': 'application/json' 86 | } 87 | }; 88 | }; -------------------------------------------------------------------------------- /azure/GenerateReportHttpHandler/sample.dat: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Azure" 3 | } -------------------------------------------------------------------------------- /azure/SendNotificationAboutLockdownReport/function.json: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "name": "myBlob", 5 | "type": "blobTrigger", 6 | "direction": "in", 7 | "path": "lockdown/reports/{name}", 8 | "connection": "AzureWebJobsStorage" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /azure/SendNotificationAboutLockdownReport/index.js: -------------------------------------------------------------------------------- 1 | module.exports = async function (context, myBlob) { 2 | const SlackWebhook = require('slack-webhook') 3 | const slack = new SlackWebhook(process.env.SLACK_WEBHOOK, { 4 | defaults: { 5 | username: 'Bot', 6 | channel: '#prj-lockdown-editorlog', 7 | icon_emoji: ':robot_face:' 8 | } 9 | }); 10 | 11 | slack.send({ 12 | text: `Report was generated: ${context.bindingData.uri}` 13 | }) 14 | }; -------------------------------------------------------------------------------- /azure/SendNotificationAboutLockdownReport/readme.md: -------------------------------------------------------------------------------- 1 | # BlobTrigger - JavaScript 2 | 3 | The `BlobTrigger` makes it incredibly easy to react to new Blobs inside of Azure Blob Storage. This sample demonstrates a simple use case of processing data from a given Blob using JavaScript. 4 | 5 | ## How it works 6 | 7 | For a `BlobTrigger` to work, you provide a path which dictates where the blobs are located inside your container, and can also help restrict the types of blobs you wish to return. For instance, you can set the path to `samples/{name}.png` to restrict the trigger to only the samples path and only blobs with ".png" at the end of their name. 8 | 9 | ## Learn more 10 | 11 | Documentation -------------------------------------------------------------------------------- /azure/SendNotificationAboutLockdownReport/sample.dat: -------------------------------------------------------------------------------- 1 | samples-workitems/workitem.txt -------------------------------------------------------------------------------- /azure/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensionBundle": { 4 | "id": "Microsoft.Azure.Functions.ExtensionBundle", 5 | "version": "[1.*, 2.0.0)" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /azure/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "azure", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "func start", 7 | "test": "echo \"No tests yet...\"" 8 | }, 9 | "dependencies": { 10 | "mongodb": "^3.5.6", 11 | "string-builder": "^0.1.6", 12 | "slack-webhook": "^1.0.0", 13 | "applicationinsights": "^1.7.5", 14 | "humanize-duration": "^3.22.0" 15 | }, 16 | "devDependencies": {} 17 | } 18 | -------------------------------------------------------------------------------- /azure/proxies.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /backend/.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | SCM_DO_BUILD_DURING_DEPLOYMENT=true -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | ## Backend scripts to load and transform data 2 | 3 | ### Requirements: 4 | Minimum node version: 8.5 5 | 6 | #### Installation: 7 | For google sheet access, you will need either: 8 | - A credentials.json generated from the google developer for sheets api v4 in the base folder 9 | - Set `GOOGLE_SERVICE_ACCOUNT_EMAIL` and `GOOGLE_PRIVATE_KEY` environment variable 10 | 11 | For MongoDB please set `MONGO_DB_USER` and `MONGO_DB_PASSWORD` and `MONGO_DB` in env.variables 12 | ``` 13 | npm install 14 | ``` 15 | 16 | 17 | #### Build & run: 18 | ``` 19 | npm start 20 | ``` 21 | 22 | #### Debug run 23 | *Note: Still replaces data* 24 | ``` 25 | npm run dev 26 | ``` 27 | 28 | #### Test 29 | ``` 30 | npm run test 31 | ``` -------------------------------------------------------------------------------- /backend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", { 5 | "targets": { 6 | "node": "current" 7 | } 8 | } 9 | ] 10 | ], 11 | "env": { 12 | "debug": { 13 | "sourceMaps": "inline", 14 | "retainLines": true 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": true, 3 | "restartable": "rs", 4 | "ignore": [ 5 | "CHANGELOG.md", "README.md", 6 | "docs/*", "coverage/*", ".github/*", ".nyc_output", 7 | "test/*", ".env-example", ".gitignore", ".nvmrc", 8 | ".travis.yml", "dist/*" 9 | ], 10 | "exec": "node ./src/server.js | pino-pretty --colorize --translateTime" 11 | } 12 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lockdown-backend", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "npm run build && node dist/server.js", 7 | "start:job": "npm run build && node dist/index.js", 8 | "build": "babel src -d dist --copy-files", 9 | "dev": "babel-node src/index.js", 10 | "test": "jest ./tests", 11 | "debug": "babel-node debug src/index.js", 12 | "server": "nodemon --exec babel-node src/server.js", 13 | "server:debug": "nodemon --inspect src/server.js" 14 | }, 15 | "dependencies": { 16 | "@azure/service-bus": "^1.1.7", 17 | "@babel/node": "^7.8.7", 18 | "axios": "^0.19.2", 19 | "compression": "^1.7.4", 20 | "cors": "^2.8.5", 21 | "dotenv": "^8.2.0", 22 | "express": "^4.17.1", 23 | "google-spreadsheet": "^3.0.10", 24 | "moment-range": "^4.0.2", 25 | "moment-timezone": "^0.5.28", 26 | "mongodb": "^3.5.6", 27 | "node-cache": "^5.1.0", 28 | "swagger-ui-express": "^4.1.4", 29 | "yamljs": "^0.3.0" 30 | }, 31 | "devDependencies": { 32 | "@babel/cli": "^7.8.4", 33 | "@babel/core": "^7.9.0", 34 | "@babel/preset-env": "^7.9.0", 35 | "babel-jest": "^25.2.3", 36 | "dotenv": "^8.2.0", 37 | "jest": "^25.2.3", 38 | "nodemon": "^2.0.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /backend/src/config.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | export const lockdownSheetId = '1mVyQxxLxAF3E1dw870WHXTOLgYzmumojvzIekpgvLV0'; 5 | export const lockdownSheetUrl = `https://docs.google.com/spreadsheets/d/${lockdownSheetId}`; 6 | 7 | export const googleServiceCredentialsJson = (() => { 8 | // Attempt to get from env 9 | if (process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL && process.env.GOOGLE_PRIVATE_KEY) { 10 | return { 11 | client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL, 12 | // fix escaped newlines in CI 13 | private_key: process.env.GOOGLE_PRIVATE_KEY.replace(/\\n/gm, '\n') 14 | }; 15 | } 16 | 17 | // Attempt to get from local file 18 | const credsFullpath = path.join(__dirname, '../credentials.json'); 19 | if (fs.existsSync(credsFullpath)) { 20 | const content = require(credsFullpath); 21 | return content; 22 | } 23 | })(); 24 | 25 | // Use API key -> Doesn't work for now 26 | export const googleSpreadsheetAPIKey = process.env.GOOGLESHEET_API_KEY; 27 | -------------------------------------------------------------------------------- /backend/src/index.js: -------------------------------------------------------------------------------- 1 | // Entrypoint 2 | import './main'; -------------------------------------------------------------------------------- /backend/src/loaders/index.js: -------------------------------------------------------------------------------- 1 | import { performance } from 'perf_hooks'; 2 | import lockdownLoader from './lockdown/lockdown'; 3 | import worldmapLoader from './worldmap/worldmap'; 4 | import totalsLoader from './totals/totals'; 5 | import updatesLoader from './lockdown/updates'; 6 | import logger from '../utils/logger'; 7 | 8 | /** 9 | * Execute all loaders 10 | */ 11 | async function executeLoaders() { 12 | const t0 = performance.now(); 13 | 14 | logger.log('[Lockdown] start'); 15 | const { lockdownStatusByTerritory } = await lockdownLoader(); 16 | 17 | logger.log('[WorldMap + Total + Updates] start'); 18 | await Promise.all([ 19 | totalsLoader(lockdownStatusByTerritory), 20 | updatesLoader(), 21 | ]); 22 | 23 | const t1 = performance.now(); 24 | logger.log(`Completed, took ${Math.round(t1 - t0)} milleseconds`); 25 | } 26 | 27 | executeLoaders(); 28 | -------------------------------------------------------------------------------- /backend/src/loaders/lockdown/googlesheet.js: -------------------------------------------------------------------------------- 1 | import GoogleSpreadsheetWorksheet from 'google-spreadsheet/lib/GoogleSpreadsheetWorksheet'; 2 | import { lockdownSheetId, googleServiceCredentialsJson } from '../../config'; 3 | import { CustomGoogleSpreadsheet } from '../../utils/CustomGoogleSpreadsheet'; 4 | 5 | const doc = new CustomGoogleSpreadsheet(lockdownSheetId); 6 | var initialized = false; 7 | 8 | /** 9 | * Initialize google spreadsheet object with cache 10 | */ 11 | async function init() { 12 | if (initialized) return; 13 | 14 | if (googleServiceCredentialsJson) { 15 | await doc.useServiceAccountAuth(googleServiceCredentialsJson); 16 | } else { 17 | throw Exception('No API key / service account credentials defined!'); 18 | } 19 | 20 | await doc.loadInfo(); // loads document properties and worksheets 21 | initialized = true; 22 | } 23 | 24 | /** 25 | * Gets a single worksheet by its title. 26 | * If duplicate, will return first found 27 | * @param {string} title 28 | * @returns {GoogleSpreadsheetWorksheet} 29 | */ 30 | export async function getWorksheetByTitle(title) { 31 | await init(); 32 | return doc.sheetsByIndex.find(sheet => sheet['_rawProperties']['title'] == title); 33 | } 34 | 35 | /** 36 | * Returns document 37 | * @returns {CustomGoogleSpreadsheet} 38 | */ 39 | export default async function getDocument() { 40 | await init(); 41 | return doc; 42 | } -------------------------------------------------------------------------------- /backend/src/loaders/lockdown/snapshot/processor.js: -------------------------------------------------------------------------------- 1 | import Moment, { MomentRange } from '../../../utils/moment'; 2 | import { extendMoment } from 'moment-range'; 3 | import { isEntryActive } from '../../../utils/typeHelper'; 4 | import Entry from '../../../types/Entry'; 5 | import get from 'lodash/get'; 6 | 7 | export const MEASURES = [ 8 | 'max_gathering', 9 | 'measure.lockdown_status', 10 | 'measure.city_movement_restriction', 11 | 'measure.attending_religious_sites', 12 | 'measure.going_to_work', 13 | 'measure.military_not_deployed', 14 | 'measure.academia_allowed', 15 | 'measure.going_to_shops', 16 | 'measure.electricity_nominal', 17 | 'measure.water_nominal', 18 | 'measure.internet_nominal', 19 | 'land.local', 20 | 'land.nationals_inbound', 21 | 'land.nationals_outbound', 22 | 'land.foreigners_inbound', 23 | 'land.foreigners_outbound', 24 | 'land.cross_border_workers', 25 | 'land.commerce', 26 | 'land.stopovers', 27 | 'flight.local', 28 | 'flight.nationals_inbound', 29 | 'flight.nationals_outbound', 30 | 'flight.foreigners_inbound', 31 | 'flight.foreigners_outbound', 32 | 'flight.cross_border_workers', 33 | 'flight.commerce', 34 | 'flight.stopovers', 35 | 'sea.local', 36 | 'sea.nationals_inbound', 37 | 'sea.nationals_outbound', 38 | 'sea.foreigners_inbound', 39 | 'sea.foreigners_outbound', 40 | 'sea.cross_border_workers', 41 | 'sea.commerce', 42 | 'sea.stopovers' 43 | ]; 44 | 45 | const moment = extendMoment(Moment); 46 | 47 | export const GLOBAL_FIRST_DATE = moment('01-11-2019', 'DD-MM-YYYY'); 48 | export const GLOBAL_LAST_DATE = moment('30-04-2022', 'DD-MM-YYYY'); 49 | 50 | /** 51 | * Gets snapshots in range 52 | * TODO: Handle case where entry start/end dates are more than globals? 53 | * @param {Entry[]} entries 54 | * @param {MomentRange} startDate 55 | * @param {MomentRange} endDate 56 | */ 57 | export function getSnapshots(entries) { 58 | // Filter to only "Active" sheets 59 | const activeEntries = entries.filter(entry => isEntryActive(entry.status)); 60 | 61 | // Sort according to date of issuance (latest first) 62 | activeEntries.sort((a, b) => b.source_date_of_issuance?.unix() - a.source_date_of_issuance?.unix()) 63 | 64 | let ranges = []; 65 | activeEntries.forEach(entry => { 66 | fillRanges(ranges, entry); 67 | }); 68 | 69 | return ranges; 70 | } 71 | 72 | function fillRanges(ranges, entry) { 73 | MEASURES.forEach(measureKey => { 74 | let dataPoint = getDataPoint(entry, measureKey); 75 | if (!dataPoint) { 76 | 77 | return; 78 | } 79 | 80 | let range = ranges 81 | .find(r => 82 | moment(r.start_date).isSame(moment(dataPoint.start_date)) 83 | && moment(r.end_date).isSame(moment(dataPoint.end_date)) 84 | && r.unique_id == dataPoint.unique_id); 85 | 86 | var measure = { 87 | value: dataPoint.value, 88 | label: dataPoint.label, 89 | }; 90 | 91 | if (range) { 92 | range.measures.push(measure); 93 | } else { 94 | ranges.push({ 95 | source_name: dataPoint.source_name, 96 | source_url: dataPoint.source_url, 97 | unique_id: dataPoint.unique_id, 98 | issue_id: dataPoint.issue_id, 99 | start_date: dataPoint.start_date, 100 | end_date: dataPoint.end_date, 101 | unique_id: dataPoint.unique_id, 102 | measures: [measure] 103 | }) 104 | } 105 | }) 106 | } 107 | 108 | function getDataPoint(entry, key) { 109 | const dataPoint = get(entry, key); 110 | 111 | // No value defined, skip this entry 112 | // TODO: Or should it break and let the value as undefined? 113 | // Current behaviour is it will search in entries beneath 114 | if (dataPoint == undefined || (typeof dataPoint == 'object' && dataPoint.value == undefined)) { 115 | return; 116 | } 117 | 118 | const entryStartDate = entry.source_start_date ?? entry.source_date_of_issuance ?? GLOBAL_FIRST_DATE; 119 | const entryEndDate = entry.source_end_date ?? GLOBAL_LAST_DATE; 120 | 121 | // Datapoint dates with default to entry 122 | const dataPointStartDate = dataPoint?.start_date ?? entryStartDate; 123 | const dataPointEndDate = dataPoint?.end_date ?? entryEndDate; 124 | 125 | return { 126 | start_date: dataPointStartDate.toDate(), 127 | end_date: dataPointEndDate.toDate(), 128 | value: dataPoint.value, 129 | label: key, 130 | unique_id: entry.entry_uid, 131 | issue_id: entry.issue_id, 132 | source_name: entry.source_name, 133 | source_url: entry.source_url 134 | } 135 | } -------------------------------------------------------------------------------- /backend/src/loaders/lockdown/updates.js: -------------------------------------------------------------------------------- 1 | import { getWorksheetByTitle } from './googlesheet'; 2 | import { transposeRows } from '../../utils/dataHelper'; 3 | import { writeJSON } from '../../utils/file'; 4 | import { isUpdateReady, toUpdateType } from '../../utils/typeHelper'; 5 | import moment from '../../utils/moment'; 6 | 7 | /** 8 | * Gets updates 9 | * @returns {array} 10 | */ 11 | export async function getUpdates() { 12 | const sheet = await getWorksheetByTitle('Updates'); 13 | const rows = await sheet.getCellsInRange(`A2:G${sheet.rowCount}`); 14 | const updates = transposeRows(['timestamp', 'status', 'type', 'geo', 'title', 'text', 'link'], rows); 15 | 16 | // Filter ready & title is required 17 | const updatesReady = updates.filter(update => { 18 | return isUpdateReady(update.status) 19 | && update.title != undefined; 20 | }); 21 | 22 | // Sort by latest first 23 | updatesReady.sort((a, b) => { 24 | return new Date(b.timestamp) - new Date(a.timestamp); 25 | }); 26 | 27 | // Reformat structure 28 | return updatesReady.map(update => { 29 | return { 30 | type: toUpdateType(update.type), 31 | title: update.title, 32 | content: update.text, 33 | link: update.link, 34 | date: moment(update.timestamp, 'DD/MM/YYYY hh:mm:ss').format('DD-MM-YYYY hh:mm'), 35 | } 36 | }); 37 | } 38 | 39 | export default async function loadData() { 40 | const updates = await getUpdates(); 41 | writeJSON('updates', { 42 | updates: updates 43 | }); 44 | } -------------------------------------------------------------------------------- /backend/src/loaders/totals/corona.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const params = { 4 | where: '1=1', 5 | geometryType: 'esriGeometryEnvelope', 6 | spatialRel: 'esriSpatialRelIntersects', 7 | resultType: 'none', 8 | distance: '0.0', 9 | units: 'esriSRUnit_Meter', 10 | returnGeodetic: 'false', 11 | outFields: '*', 12 | returnGeometry: 'false', 13 | featureEncoding: 'esriDefault', 14 | multipatchOption: 'xyFootprint', 15 | applyVCSProjection: 'false', 16 | returnIdsOnly: 'false', 17 | returnUniqueIdsOnly: 'false', 18 | returnCountOnly: 'false', 19 | returnExtentOnly: 'false', 20 | returnQueryGeometry: 'false', 21 | returnDistinctValues: 'false', 22 | cacheHint: 'false', 23 | returnZ: 'false', 24 | returnM: 'false', 25 | returnExceededLimitFeatures: 'true', 26 | sqlFormat: 'none', 27 | f: 'pjson' 28 | } 29 | 30 | export async function sumCorona() { 31 | const url = 'https://api.covid19api.com/world/total'; 32 | const response = await axios.get(url, { 33 | params 34 | }); 35 | const data = response.data; 36 | 37 | return { 38 | 'confirmed': data.TotalConfirmed, 39 | 'deaths': data.TotalDeaths 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /backend/src/loaders/totals/territories.js: -------------------------------------------------------------------------------- 1 | import { isLockdown } from '../../utils/typeHelper.js'; 2 | import moment from 'moment-timezone'; 3 | 4 | /** 5 | * Totals up locked down territories 6 | * @param {array} lockdownStatusByTerritory 7 | */ 8 | export function sumLockdown(lockdownStatusByTerritory) { 9 | var affected = 0; 10 | var population = require('../../../../data/population.json'); 11 | // TODO: changes needed for time slider 12 | const total = Object.values(lockdownStatusByTerritory).reduce((prev, territory) => { 13 | if (isLockdown(territory.lockdown_status)) { 14 | affected += +population[territory.ISO].Population; 15 | return prev + 1; 16 | } 17 | return prev; 18 | }, 0); 19 | 20 | return {total, affected}; 21 | } -------------------------------------------------------------------------------- /backend/src/loaders/totals/totals.js: -------------------------------------------------------------------------------- 1 | import { writeJSON } from '../../utils/file.js'; 2 | import { sumLockdown } from './territories'; 3 | import { sumCorona } from './corona'; 4 | 5 | /** 6 | * Loads totals for corona and lockdown count 7 | * @param {array} lockdownStatusByTerritory 8 | */ 9 | export default async function loadData(lockdownStatusByTerritory) { 10 | const corona = await sumCorona(); 11 | writeJSON('totals', { 12 | corona: { 13 | confirmed: corona.confirmed, 14 | date: corona.date, 15 | deaths: corona.deaths, 16 | } 17 | }); 18 | 19 | } -------------------------------------------------------------------------------- /backend/src/loaders/worldmap/worldmap.js: -------------------------------------------------------------------------------- 1 | import { writeJSON } from '../../utils/file.js'; 2 | 3 | // Default lockdown status if data doesnt exist 4 | const defaultLockdownStatus = null; 5 | 6 | /** 7 | * Appends lockdown_status to each territory 8 | * @param {array} lockdownStatusByTerritory 9 | */ 10 | export function appendLockdownStatus(lockdownStatusByTerritory) { 11 | const baseData = require('./base.json'); 12 | const updatedFeatures = []; 13 | // TODO: changes needed for time slider 14 | baseData['features'].forEach(feature => { 15 | let lockdownStatus = lockdownStatusByTerritory[feature.properties.iso2]?.lockdown?.lockdown_status; 16 | 17 | updatedFeatures.push({ 18 | ...feature, 19 | properties: { 20 | ...feature['properties'], 21 | lockdown_status: lockdownStatus ?? defaultLockdownStatus, 22 | } 23 | }); 24 | }); 25 | 26 | return { 27 | ...baseData, 28 | features: updatedFeatures 29 | } 30 | } 31 | 32 | export default async function loadData(lockdownStatusByTerritory) { 33 | Object.keys(lockdownStatusByTerritory).forEach(key => { 34 | var lockdown = lockdownStatusByTerritory[key]; 35 | const statusForDate = appendLockdownStatus(lockdown); 36 | writeJSON(`worldMap/${key}`, statusForDate); 37 | }) 38 | } -------------------------------------------------------------------------------- /backend/src/main.js: -------------------------------------------------------------------------------- 1 | import './loaders'; -------------------------------------------------------------------------------- /backend/src/repositories/Database.js: -------------------------------------------------------------------------------- 1 | import SnapshotRepository from "./SnapshotRepository"; 2 | 3 | export default class Database { 4 | constructor(connection) { 5 | /** @private */ 6 | this._connection = connection; 7 | this._db = connection.db("lockdown"); 8 | 9 | /**@type {SnapshotRepository} */ 10 | this.snapshotRepository = new SnapshotRepository(this._db); 11 | } 12 | 13 | close(){ 14 | this._connection.close(); 15 | } 16 | } -------------------------------------------------------------------------------- /backend/src/repositories/index.js: -------------------------------------------------------------------------------- 1 | import Database from './Database'; 2 | 3 | require('dotenv').config(); 4 | 5 | if (!process.env.MONGO_DB_USER || !process.env.MONGO_DB_PASSWORD || !process.env.MONGO_DB){ 6 | throw "Please set MONGO_DB_USER and MONGO_DB_PASSWORD and MONGO_DB in env.variables"; 7 | } 8 | 9 | const MongoClient = require('mongodb').MongoClient; 10 | const uri = `mongodb://${process.env.MONGO_DB_USER}:${process.env.MONGO_DB_PASSWORD}@${process.env.MONGO_DB}`; 11 | const client = new MongoClient(uri, { useNewUrlParser: true }); 12 | 13 | /** 14 | * @returns {Database} 15 | */ 16 | export async function connect() { 17 | let connection = await client.connect(); 18 | return new Database(connection); 19 | } 20 | 21 | -------------------------------------------------------------------------------- /backend/src/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { connect } from './repositories'; 3 | import SnapshotsService from './services/SnapshotsService'; 4 | import CacheService from './services/CacheService'; 5 | import cors from 'cors'; 6 | import compression from 'compression'; 7 | import swaggerUi from 'swagger-ui-express'; 8 | import YAML from 'yamljs'; 9 | import { MessagesService } from './services/MessagesService'; 10 | 11 | const swaggerDocument = YAML.load('./api.yaml'); 12 | 13 | const app = express(); 14 | 15 | app.use(cors()) 16 | app.use(compression()); 17 | 18 | const ttl = 60 * 60 * 1; 19 | const cacheService = new CacheService(ttl); 20 | 21 | connect().then(database => { 22 | 23 | const snapshotService = new SnapshotsService(database); 24 | if (process.env.AZURE_SERVICEBUS_CONNECTION_STRING && process.env.AZURE_SERVICEBUS_CACHE_QUEUE) { 25 | var cacheMessageBus = new MessagesService(process.env.AZURE_SERVICEBUS_CONNECTION_STRING, process.env.AZURE_SERVICEBUS_CACHE_QUEUE); 26 | cacheMessageBus.addReceiver(message => { 27 | cacheService.flush(); 28 | }); 29 | } 30 | 31 | app.listen(process.env.PORT || 3000, function () { 32 | console.log(`listening on ${process.env.PORT || 3000}`) 33 | }); 34 | 35 | app.get('/status/world/:date/', function (req, res, next) { 36 | let date = new Date(req.params.date); 37 | 38 | cacheService.get(`status_world_${date}}`, () => { 39 | return snapshotService.getWorldLockdownSnaphots(date); 40 | }).then(result => { 41 | res.json(result); 42 | }).catch(next); 43 | }); 44 | 45 | app.get('/status/world/:startDate/:endDate', function (req, res, next) { 46 | let startDate = new Date(req.params.startDate); 47 | let endDate = new Date(req.params.endDate); 48 | 49 | cacheService.get(`status_world_${startDate}${endDate}}`, () => { 50 | return snapshotService.getWorldLockdownSnaphotsByRange(startDate, endDate); 51 | }).then(result => { 52 | res.json(result); 53 | }).catch(next); 54 | }); 55 | 56 | app.get('/status/:iso/:date', function (req, res, next) { 57 | let iso = req.params.iso; 58 | let date = new Date(req.params.date); 59 | 60 | cacheService.get(`${iso}${date}`, () => { 61 | return snapshotService.getSnapshot(iso, date); 62 | }).then(result => { 63 | res.json(result); 64 | }).catch(next); 65 | }); 66 | 67 | app.get('/status/:iso/:startDate/:endDate', function (req, res, next) { 68 | let iso = req.params.iso; 69 | let startDate = new Date(req.params.startDate); 70 | let endDate = new Date(req.params.endDate); 71 | 72 | cacheService.get(`${iso}${startDate}${endDate}`, () => { 73 | return snapshotService.getSnapshots(iso, startDate, endDate); 74 | }).then(result => { 75 | res.json(result); 76 | }).catch(next); 77 | }); 78 | 79 | app.get('/measures/:iso/:startDate/:endDate', function (req, res, next) { 80 | let iso = req.params.iso; 81 | let startDate = new Date(req.params.startDate); 82 | let endDate = new Date(req.params.endDate); 83 | let measures = req.query.measures; 84 | 85 | cacheService.get(`${iso}${startDate}${endDate}${measures}`, () => { 86 | return snapshotService.getSnapshotsByMeasures(iso, startDate, endDate, measures); 87 | }).then(result => { 88 | res.json(result); 89 | }).catch(next); 90 | }); 91 | 92 | 93 | app.get('/totals/lockdown/:startDate/:endDate', function (req, res, next) { 94 | let startDate = new Date(req.params.startDate); 95 | let endDate = new Date(req.params.endDate); 96 | 97 | cacheService.get(`totals_lockdown_${startDate}${endDate}`, () => { 98 | return snapshotService.getTotalsLockdown(startDate, endDate); 99 | }).then(result => { 100 | res.json(result); 101 | }).catch(next); 102 | }); 103 | 104 | app.use('/', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); 105 | }); -------------------------------------------------------------------------------- /backend/src/services/CacheService.js: -------------------------------------------------------------------------------- 1 | import NodeCache from 'node-cache'; 2 | 3 | export default class CacheService { 4 | 5 | constructor(ttlSeconds) { 6 | this.cache = new NodeCache({ stdTTL: ttlSeconds, checkperiod: ttlSeconds * 0.2, useClones: false }); 7 | } 8 | 9 | get(key, storeFunction) { 10 | const value = this.cache.get(key); 11 | if (value) { 12 | return Promise.resolve(value); 13 | } 14 | 15 | return storeFunction().then((result) => { 16 | this.cache.set(key, result); 17 | return result; 18 | }); 19 | } 20 | 21 | del(keys) { 22 | this.cache.del(keys); 23 | } 24 | 25 | delStartWith(startStr = '') { 26 | if (!startStr) { 27 | return; 28 | } 29 | 30 | const keys = this.cache.keys(); 31 | for (const key of keys) { 32 | if (key.indexOf(startStr) === 0) { 33 | this.del(key); 34 | } 35 | } 36 | } 37 | 38 | flush() { 39 | this.cache.flushAll(); 40 | } 41 | } -------------------------------------------------------------------------------- /backend/src/services/MessagesService.js: -------------------------------------------------------------------------------- 1 | import { ServiceBusClient, ReceiveMode } from "@azure/service-bus"; 2 | 3 | export class MessagesService { 4 | constructor(connectionString, queueName) { 5 | this.sbClient = ServiceBusClient.createFromConnectionString(connectionString); 6 | this.queueClient = this.sbClient.createQueueClient(queueName); 7 | this.sender = this.queueClient.createSender(); 8 | this.receiver = this.queueClient.createReceiver(ReceiveMode.receiveAndDelete); 9 | } 10 | 11 | async sendMessage(body, label, properties) { 12 | const message = { 13 | body: body, 14 | label: label, 15 | userProperties: properties 16 | }; 17 | console.log(`Sending message: ${message.body}`); 18 | await this.sender.send(message); 19 | } 20 | 21 | addReceiver(onRecieve) { 22 | this.receiver.registerMessageHandler( 23 | (message) => { 24 | console.log("Received message"); 25 | onRecieve(message); 26 | }, 27 | (error) => { 28 | console.log(error); 29 | 30 | } 31 | ); 32 | } 33 | 34 | async close() { 35 | await this.queueClient.close(); 36 | await this.sbClient.close(); 37 | } 38 | } -------------------------------------------------------------------------------- /backend/src/types/DataPoint.js: -------------------------------------------------------------------------------- 1 | import { MomentRange } from 'moment-range'; 2 | 3 | export default class DataPoint { 4 | constructor() { 5 | /** @type {any} */ 6 | this.value = null; 7 | 8 | /** @type {MomentRange} */ 9 | this.start_date = null; 10 | 11 | /** @type {MomentRange} */ 12 | this.end_date = null; 13 | } 14 | } -------------------------------------------------------------------------------- /backend/src/types/Entry.js: -------------------------------------------------------------------------------- 1 | import DataPoint from './DataPoint'; 2 | import Measure from './Measure'; 3 | import Travel from './Travel'; 4 | 5 | export default class Entry { 6 | constructor() { 7 | /** @type {string} */ 8 | this.entry_uid = null; 9 | 10 | /** @type {string} */ 11 | this.timestamp = null; 12 | 13 | /** @type {string} */ 14 | this.issue_id = null; 15 | 16 | /** @type {string} */ 17 | this.status = null; 18 | 19 | /** @type {string} */ 20 | this.editor = null; 21 | 22 | /** @type {string} */ 23 | this.reviewed_by = null; 24 | 25 | /** @type {string} */ 26 | this.type = null; 27 | 28 | /** @type {string} */ 29 | this.source_name = null; 30 | 31 | /** @type {string} */ 32 | this.source_url = null; 33 | 34 | /** @type {string} */ 35 | this.source_title_of_status = null; 36 | 37 | /** @type {string} */ 38 | this.source_date_of_issuance = null; 39 | 40 | /** @type {string} */ 41 | this.source_start_date = null; 42 | 43 | /** @type {string} */ 44 | this.source_end_date = null; 45 | 46 | /** @type {DataPoint} */ 47 | this.max_gathering = null; 48 | 49 | /** @type {Measure} */ 50 | this.measure = null; 51 | 52 | /** @type {Travel} */ 53 | this.land = null; 54 | 55 | /** @type {Travel} */ 56 | this.flight = null; 57 | 58 | /** @type {Travel} */ 59 | this.sea = null; 60 | } 61 | } -------------------------------------------------------------------------------- /backend/src/types/Measure.js: -------------------------------------------------------------------------------- 1 | import DataPoint from './DataPoint'; 2 | 3 | export default class Measure { 4 | constructor() { 5 | /** @type {DataPoint} */ 6 | this.lockdown_status = null; 7 | 8 | /** @type {DataPoint} */ 9 | this.city_movement_restriction = null; 10 | 11 | /** @type {DataPoint} */ 12 | this.attending_religious_sites = null; 13 | 14 | /** @type {DataPoint} */ 15 | this.going_to_work = null; 16 | 17 | /** @type {DataPoint} */ 18 | this.military_not_deployed = null; 19 | 20 | /** @type {DataPoint} */ 21 | this.academia_allowed = null; 22 | 23 | /** @type {DataPoint} */ 24 | this.going_to_shops = null; 25 | 26 | /** @type {DataPoint} */ 27 | this.electricity_nominal = null; 28 | 29 | /** @type {DataPoint} */ 30 | this.water_nominal = null; 31 | 32 | /** @type {DataPoint} */ 33 | this.internet_nominal = null; 34 | } 35 | } -------------------------------------------------------------------------------- /backend/src/types/Snapshot.js: -------------------------------------------------------------------------------- 1 | import Entry from "./Entry"; 2 | 3 | export default class Snapshot{ 4 | constructor(){ 5 | /**@type {Entry} */ 6 | this.lockdown = null; 7 | } 8 | } -------------------------------------------------------------------------------- /backend/src/types/Travel.js: -------------------------------------------------------------------------------- 1 | import DataPoint from './DataPoint'; 2 | 3 | export default class Travel { 4 | constructor() { 5 | /** @type {DataPoint} */ 6 | this.local = null; 7 | 8 | /** @type {DataPoint} */ 9 | this.nationals_inbound = null; 10 | 11 | /** @type {DataPoint} */ 12 | this.nationals_outbound = null; 13 | 14 | /** @type {DataPoint} */ 15 | this.foreigners_inbound = null; 16 | 17 | /** @type {DataPoint} */ 18 | this.foreigners_outbound = null; 19 | 20 | /** @type {DataPoint} */ 21 | this.cross_border_workers = null; 22 | 23 | /** @type {DataPoint} */ 24 | this.commerce = null; 25 | 26 | /** @type {DataPoint} */ 27 | this.stopovers = null; 28 | } 29 | } -------------------------------------------------------------------------------- /backend/src/utils/CustomGoogleSpreadsheet.js: -------------------------------------------------------------------------------- 1 | import { GoogleSpreadsheet } from 'google-spreadsheet'; 2 | 3 | export class CustomGoogleSpreadsheet extends GoogleSpreadsheet { 4 | /** 5 | * Added capability to execute batchGet API: 6 | * https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchGet 7 | * @param {string[]} ranges 8 | */ 9 | async batchGetGridRanges(ranges) { 10 | // 'ranges' key is multiple in this request 11 | const params = new URLSearchParams(); 12 | params.append('majorDimension', 'ROWS'); 13 | params.append('valueRenderOption', 'FORMATTED_VALUE'); 14 | params.append('dateTimeRenderOption', 'FORMATTED_STRING'); 15 | ranges.forEach(range => { 16 | params.append('ranges', range); 17 | }); 18 | 19 | const result = await this.axios.get(`/values:batchGet?${params.toString()}`); 20 | 21 | return (result.data.valueRanges || []).map(d => d['values']); 22 | } 23 | } -------------------------------------------------------------------------------- /backend/src/utils/SimpleGrid.js: -------------------------------------------------------------------------------- 1 | import { letterToColumn } from 'google-spreadsheet/lib/utils'; 2 | import GoogleSpreadsheetWorksheet from 'google-spreadsheet/lib/GoogleSpreadsheetWorksheet'; 3 | import { getRowAndColumnIndexA1 } from './sheet'; 4 | import get from 'lodash/get'; 5 | 6 | /** 7 | * 2d sparse grid that supports A1 queries. 8 | * Not drop-in compatible with GoogleSpreadsheetWorksheet. Borrowed some from 9 | * https://github.com/theoephraim/node-google-spreadsheet/blob/master/lib/GoogleSpreadsheetWorksheet.js 10 | */ 11 | export class SimpleGrid { 12 | constructor(range, data, rowCount, columnCount) { 13 | const [startA1, endA1] = range.split(':'); 14 | const [startRow, startColumn] = getRowAndColumnIndexA1(startA1); 15 | 16 | this._cells = []; 17 | this._rowCount = rowCount; 18 | this._columnCount = columnCount; 19 | 20 | this._fillCellData(data, startRow, startColumn, rowCount, columnCount); 21 | } 22 | 23 | get rowCount() { 24 | return this._rowCount; 25 | } 26 | 27 | get columnCount() { 28 | return this._columnCount; 29 | } 30 | 31 | getCellByA1(a1Address) { 32 | const split = a1Address.match(/([A-Z]+)([0-9]+)/); 33 | const columnIndex = letterToColumn(split[1]); 34 | const rowIndex = parseInt(split[2]); 35 | return this.getCell(rowIndex - 1, columnIndex - 1); 36 | } 37 | 38 | getCell(rowIndex, columnIndex) { 39 | if (rowIndex < 0 || columnIndex < 0) throw new Error('Min coordinate is 0, 0'); 40 | if (rowIndex >= this.rowCount || columnIndex >= this.columnCount) { 41 | throw new Error(`Out of bounds, grid is ${this.rowCount} by ${this.columnCount}, but requested ${rowIndex+1}:${columnIndex+1}`); 42 | } 43 | 44 | // Currently does not check if data is loaded or just empty. 45 | return get(this._cells, `[${rowIndex}][${columnIndex}]`); 46 | } 47 | 48 | _fillCellData(data, startRow, startColumn, numRows, numColumns) { 49 | // update cell data for entire range 50 | for (let i = 0; i < numRows; i++) { 51 | // TODO: Check if following logic applies 52 | const actualRow = startRow + i; 53 | for (let j = 0; j < numColumns; j++) { 54 | const actualColumn = startColumn + j; 55 | // if the row has not been initialized yet, do it 56 | if (!this._cells[actualRow]) this._cells[actualRow] = []; 57 | 58 | const cellData = get(data, `[${i}][${j}]`); 59 | // update the cell object or create it 60 | if (cellData != undefined) { 61 | this._cells[actualRow][actualColumn] = cellData; 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | export class SimpleGridSheet extends SimpleGrid { 69 | /** 70 | * 71 | * @param {string} range 72 | * @param {array} data 73 | * @param {GoogleSpreadsheetWorksheet} sheet 74 | */ 75 | constructor(range, data, sheet) { 76 | super(range, data, sheet['gridProperties']['rowCount'], sheet['gridProperties']['columnCount']); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /backend/src/utils/dataHelper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parses rows into associative arrays 3 | * @param {array} headers 4 | * @param {array} rows 5 | */ 6 | export function transposeRows(headers, rows) { 7 | return rows.map((row) => { 8 | var obj = {}; 9 | for (var i = 0; i < headers.length; i++) { 10 | obj[headers[i]] = row[i]; 11 | } 12 | return obj; 13 | }); 14 | } 15 | 16 | /** 17 | * Parses columns into associative arrays 18 | * @param {array} headers 19 | * @param {array} columns 20 | * @param {boolean?} isFirstColumnOnly 21 | */ 22 | export function transposeColumns(headers, columns, isFirstColumnOnly = false) { 23 | var obj = {}; 24 | for (var i = 0; i < headers.length; i++) { 25 | if (columns[i]) 26 | obj[headers[i]] = isFirstColumnOnly ? columns[i][0] : columns[i]; 27 | } 28 | return obj; 29 | } -------------------------------------------------------------------------------- /backend/src/utils/errors.js: -------------------------------------------------------------------------------- 1 | export class BackendError { 2 | constructor(messageOrError = null) { 3 | this.message = null; 4 | this.previous = null; 5 | 6 | if (typeof messageOrError == 'string') { 7 | this.message = message; 8 | } else if (messageOrError && messageOrError.message) { 9 | this.message = messageOrError.message; 10 | this.previous = messageOrError; 11 | } 12 | } 13 | } 14 | 15 | export class GoogleAPIError extends BackendError {} 16 | export class LoaderNetworkError extends BackendError {} -------------------------------------------------------------------------------- /backend/src/utils/file.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | export const dataPath = path.join(__dirname, '../../../data'); 5 | 6 | export async function writeJSON(name, data) { 7 | return new Promise((resolve, reject) => { 8 | try { 9 | fs.writeFile(`${dataPath}/${name}.json`, JSON.stringify(data), resolve); 10 | } catch (e) { 11 | reject(e); 12 | } 13 | }) 14 | } -------------------------------------------------------------------------------- /backend/src/utils/logger.js: -------------------------------------------------------------------------------- 1 | // TODO: detect environment and set logging appropriately 2 | class logger { 3 | static log(string) { 4 | console.log(string); 5 | } 6 | 7 | static error(exception) { 8 | console.dir(exception); 9 | } 10 | } 11 | 12 | export default logger; -------------------------------------------------------------------------------- /backend/src/utils/moment.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment-timezone'; 2 | 3 | // Sets default timezone 4 | moment.tz.setDefault('UTC'); 5 | export default moment -------------------------------------------------------------------------------- /backend/src/utils/sheet.js: -------------------------------------------------------------------------------- 1 | import { letterToColumn } from 'google-spreadsheet/lib/utils'; 2 | import GoogleSpreadsheetWorksheet from 'google-spreadsheet/lib/GoogleSpreadsheetWorksheet'; 3 | import { CustomGoogleSpreadsheet } from './CustomGoogleSpreadsheet'; 4 | import { SimpleGridSheet } from './SimpleGrid'; 5 | 6 | /** 7 | * This utility only gets the precached cells 8 | * Set isStripNull to true to follow sheet.getCellsInRange behaviour 9 | * TODO: Probably should accept interface when we convert to TS 10 | * @param {GoogleSpreadsheetWorksheet|SimpleGrid} sheet 11 | * @param {string} range 12 | * @param {boolean} isStripNull 13 | */ 14 | export function getCachedCellsRange(sheet, range, isStripNull = false) { 15 | const [startCellA1, endCellA1] = range.split(':'); 16 | const [startCellRow, startCellColumn] = getRowAndColumnIndexA1(startCellA1); 17 | const [endCellRow, endCellColumn] = getRowAndColumnIndexA1(endCellA1); 18 | 19 | const rows = []; 20 | // Loop through all sheet's cells 21 | for (var rowIndex = startCellRow; rowIndex <= endCellRow; rowIndex++) { 22 | let columns = []; 23 | for (var columnIndex = startCellColumn; columnIndex <= endCellColumn; columnIndex++) { 24 | let cell = sheet.getCell(rowIndex, columnIndex); 25 | let value = cell && cell.formattedValue ? cell.formattedValue : cell; 26 | 27 | // Strips null cell values 28 | if (value === null && isStripNull) { 29 | continue; 30 | } 31 | columns.push(value); 32 | } 33 | rows.push(columns); 34 | } 35 | return rows; 36 | } 37 | 38 | /** 39 | * Converts A1 address to row & column indexes 40 | * @param {string} a1Address 41 | */ 42 | export function getRowAndColumnIndexA1(a1Address) { 43 | const split = a1Address.match(/([A-Z]+)([0-9]+)/); 44 | const columnIndex = letterToColumn(split[1]); 45 | const rowIndex = parseInt(split[2]); 46 | return [ 47 | rowIndex - 1, 48 | columnIndex - 1 49 | ]; 50 | } -------------------------------------------------------------------------------- /backend/src/utils/typeHelper.js: -------------------------------------------------------------------------------- 1 | import { MEASURE, TRAVEL, UPDATE_STATUS, COUNTRY_STATUS, DATA_ENTRY_STATUS } from '../../../shared/types'; 2 | import moment from './moment'; 3 | 4 | // Options are the literal values set in google sheet 5 | const OPTION_MEASURE = { 6 | 'Yes': MEASURE.YES, 7 | 'No': MEASURE.NO, 8 | 'Partially': MEASURE.PARTIAL, 9 | 'Unclear': MEASURE.UNCLEAR, 10 | } 11 | 12 | const OPTION_TRAVEL = { 13 | 'Yes': TRAVEL.YES, 14 | 'No': TRAVEL.NO, 15 | 'Partially': TRAVEL.PARTIALLY, 16 | 'Unclear': TRAVEL.UNCLEAR, 17 | 'N/A': TRAVEL.NA, 18 | } 19 | 20 | const OPTION_COUNTRY_STATUS = { 21 | 'Unclear': COUNTRY_STATUS.UNCLEAR, 22 | 'N/A': COUNTRY_STATUS.NA, 23 | 'State of Calamity': COUNTRY_STATUS.STATE_OF_CALAMITY, 24 | 'State of Emergency': COUNTRY_STATUS.STATE_OF_EMERGENCY, 25 | 'State of Alert': COUNTRY_STATUS.STATE_OF_ALERT, 26 | 'Other (Yes)': COUNTRY_STATUS.OTHER_YES, 27 | 'Other (No)': COUNTRY_STATUS.OTHER_NO, 28 | 'State of Natural Disaster': COUNTRY_STATUS.STATE_OF_NATURAL_DISASTER, 29 | 'State of National Disaster': COUNTRY_STATUS.STATE_OF_NATIONAL_DISASTER, 30 | } 31 | 32 | const OPTION_DATA_ENTRY_STATUS = { 33 | 'Demo': DATA_ENTRY_STATUS.DEMO, 34 | 'Flagged': DATA_ENTRY_STATUS.FLAGGED, 35 | 'Ready': DATA_ENTRY_STATUS.READY, 36 | 'Standby': DATA_ENTRY_STATUS.STANDBY, 37 | } 38 | 39 | const OPTION_UPDATE_STATUS = { 40 | 'Standby': UPDATE_STATUS.STANDBY, 41 | 'Ready': UPDATE_STATUS.READY, 42 | 'Demo': UPDATE_STATUS.DEMO, 43 | 'Flagged': UPDATE_STATUS.FLAGGED, 44 | } 45 | 46 | const OPTION_UPDATE_TYPE = { 47 | 'New Entry': 'new_entry', 48 | 'Announcement': 'announcement', 49 | 'Rectification': 'rectification', 50 | 'Promoting Project': 'promoting_project', 51 | 'Promoting Petition': 'promoting_petition', 52 | } 53 | 54 | export function toMeasureType(value) { 55 | return OPTION_MEASURE[value] ?? null; 56 | } 57 | 58 | export function toTravelType(value) { 59 | return OPTION_TRAVEL[value] ?? null; 60 | } 61 | 62 | export function toCountryStatus(value) { 63 | return OPTION_COUNTRY_STATUS[value] ?? null; 64 | } 65 | 66 | export function toUpdateType(value) { 67 | return OPTION_UPDATE_TYPE[value] ?? null; 68 | } 69 | 70 | // Special note on lockdown: its label is "Are citizens allowed to leave their homes?" 71 | // Thus the value is actually opposite, i.e: "No" means lockdown, and vice versa 72 | export function toLockdownType(value) { 73 | switch (value) { 74 | case 'No': 75 | return MEASURE.YES 76 | case 'Yes': 77 | return MEASURE.NO 78 | } 79 | return OPTION_MEASURE[value] ?? null; 80 | } 81 | 82 | export function toInteger(value) { 83 | const int = parseInt(value); 84 | return Number.isInteger(int) ? int : null; 85 | } 86 | 87 | /** 88 | * Date check for entry. ex valid: '1 April 2020' 89 | * @param {string} value 90 | */ 91 | export function toEntryDate(value) { 92 | const d = moment(value, 'D MMMM Y'); 93 | return d.isValid() ? d : null; 94 | } 95 | 96 | export function isEntryActive(value) { 97 | const status = OPTION_DATA_ENTRY_STATUS[value]; 98 | return status === DATA_ENTRY_STATUS.READY || status === DATA_ENTRY_STATUS.FLAGGED; 99 | } 100 | 101 | export function isUpdateReady(value) { 102 | const status = OPTION_UPDATE_STATUS[value]; 103 | return status === UPDATE_STATUS.READY; 104 | } 105 | 106 | export function isLockdown(value) { 107 | return value === MEASURE.YES || value === MEASURE.PARTIAL; 108 | } 109 | -------------------------------------------------------------------------------- /backend/tests/dataHelper.test.js: -------------------------------------------------------------------------------- 1 | import { transposeRows, transposeColumns } from '../src/utils/dataHelper'; 2 | 3 | test('should transpose rows', async () => { 4 | const data = transposeRows(['title','desc'], [ 5 | ['data-title-1', 'data-desc-1'], 6 | ['data-title-2', 'data-desc-2'], 7 | ]); 8 | expect(data).toEqual([ 9 | { 10 | 'title': 'data-title-1', 11 | 'desc': 'data-desc-1', 12 | }, 13 | { 14 | 'title': 'data-title-2', 15 | 'desc': 'data-desc-2', 16 | } 17 | ]); 18 | }); 19 | 20 | test('should transpose columns with single value', async () => { 21 | const data = transposeColumns( 22 | ['name', 'url', 'title', 'date'], 23 | [ 24 | [ 'The act of killing coronavirus' ], 25 | [ 'https://www.google.com' ], 26 | [ 'State of Emergency' ], 27 | [ '1 March 2020' ] 28 | ], true); 29 | 30 | expect(data).toEqual({ 31 | name: 'The act of killing coronavirus', 32 | url: 'https://www.google.com', 33 | title: 'State of Emergency', 34 | date: '1 March 2020' 35 | }); 36 | }); 37 | 38 | test('should transpose columns with multiple values', async () => { 39 | const data = transposeColumns( 40 | ['max_gathering', 41 | 'lockdown_status', 42 | 'city_movement_restriction'], 43 | [ { start: '', end: '', value: '2' }, 44 | { start: '', end: '', value: 'No' }, 45 | { start: '', end: '', value: 'Yes' }, 46 | { start: '', end: '', value: 'Partial' }]); 47 | 48 | expect(data).toEqual({ 49 | max_gathering: { start: '', end: '', value: '2' }, 50 | lockdown_status: { start: '', end: '', value: 'No' }, 51 | city_movement_restriction: { start: '', end: '', value: 'Yes' } 52 | }); 53 | }) -------------------------------------------------------------------------------- /backend/tests/loaders/spreadsheet.test.js: -------------------------------------------------------------------------------- 1 | test('googlesheet asm structure', async () => { 2 | expect(true).toBe(true); 3 | }) -------------------------------------------------------------------------------- /backend/tests/loaders/totals.test.js: -------------------------------------------------------------------------------- 1 | import { sumLockdown } from "../../src/loaders/totals/territories"; 2 | import { isLockdown } from "../../src/utils/typeHelper"; 3 | import { sumCorona } from "../../src/loaders/totals/corona"; 4 | 5 | test('total locked down territories', async () => { 6 | const territories = require('../data/lockdown_summary.json'); 7 | 8 | const assertTotal = Object.values(territories).reduce((prev, territory) => { 9 | if (isLockdown(territory.lockdown.lockdown_status)) { 10 | return prev + 1; 11 | } 12 | return prev; 13 | }, 0); 14 | 15 | expect(sumLockdown(territories)).toEqual(assertTotal); 16 | }) 17 | 18 | test('total corona', async () => { 19 | const corona = await sumCorona(); 20 | 21 | expect(typeof corona.confirmed).toEqual('number'); 22 | expect(typeof corona.deaths).toEqual('number'); 23 | }); -------------------------------------------------------------------------------- /backend/web.config: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /budget.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "path": "/*", 4 | "timings": [ 5 | { 6 | "metric": "interactive", 7 | "budget": 3000 8 | }, 9 | { 10 | "metric": "first-meaningful-paint", 11 | "budget": 1000 12 | } 13 | ], 14 | "resourceSizes": [ 15 | { 16 | "resourceType": "script", 17 | "budget": 230 18 | }, 19 | { 20 | "resourceType": "total", 21 | "budget": 700 22 | } 23 | ], 24 | "resourceCounts": [ 25 | { 26 | "resourceType": "third-party", 27 | "budget": 17 28 | } 29 | ] 30 | } 31 | ] 32 | -------------------------------------------------------------------------------- /data/totals.json: -------------------------------------------------------------------------------- 1 | {"corona":{"confirmed":177682872,"deaths":3851268}} -------------------------------------------------------------------------------- /data/updates.json: -------------------------------------------------------------------------------- 1 | {"updates":[{"type":"announcement","title":"Project Lockdown v1.0 is out!","content":"Check our features and tell us what you think!","link":"https://www.ua.es/","date":"04-04-2020 08:13"},{"type":"new_entry","title":"PHL has new data!","content":"dsfasfas","link":"https://www.google.com/search?clien","date":"04-04-2020 08:08"}]} -------------------------------------------------------------------------------- /deployment.yaml: -------------------------------------------------------------------------------- 1 | kind: Service 2 | apiVersion: v1 3 | metadata: 4 | name: lockdown-app 5 | spec: 6 | selector: 7 | app: lockdown-app 8 | type: NodePort 9 | ports: 10 | - protocol: TCP 11 | port: 80 12 | targetPort: 80 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: lockdown 18 | labels: 19 | app: lockdown-app 20 | spec: 21 | replicas: 1 22 | selector: 23 | matchLabels: 24 | app: lockdown-app 25 | template: 26 | metadata: 27 | labels: 28 | app: lockdown-app 29 | spec: 30 | imagePullSecrets: 31 | - name: pipeline-docker-registry 32 | containers: 33 | - name: lockdown-app 34 | image: ${CICD_IMAGE}:${CICD_EXECUTION_SEQUENCE} 35 | ports: 36 | - containerPort: 80 37 | -------------------------------------------------------------------------------- /local_dependencies/i18n.min.js: -------------------------------------------------------------------------------- 1 | (function(){var e,t,n,r=function(e,t){return function(){return e.apply(t,arguments)}};e=function(){function e(){this.translate=r(this.translate,this);this.data={values:{},contexts:[]};this.globalContext={}}e.prototype.translate=function(e,t,n,r,i){var s,o,u,a;if(i==null){i=this.globalContext}u=function(e){var t;t=typeof e;return t==="function"||t==="object"&&!!e};if(u(t)){s=null;a=null;o=t;i=n||this.globalContext}else{if(typeof t==="number"){s=null;a=t;o=n;i=r||this.globalContext}else{s=t;if(typeof n==="number"){a=n;o=r;i=i}else{a=null;o=n;i=r||this.globalContext}}}if(u(e)){if(u(e["i18n"])){e=e["i18n"]}return this.translateHash(e,i)}else{return this.translateText(e,a,o,i,s)}};e.prototype.add=function(e){var t,n,r,i,s,o,u,a;if(e.values!=null){o=e.values;for(n in o){r=o[n];this.data.values[n]=r}}if(e.contexts!=null){u=e.contexts;a=[];for(i=0,s=u.length;i=s[0]||s[0]===null)&&(t<=s[1]||s[1]===null)){i=this.applyFormatting(s[2].replace("-%n",String(-t)),t,n);return this.applyFormatting(i.replace("%n",String(t)),t,n)}}}}return null};e.prototype.getContextData=function(e,t){var n,r,i,s,o,u,a,f;if(e.contexts==null){return null}a=e.contexts;for(o=0,u=a.length;o', 24 | `${initialImports.map(i => ``)}` 25 | ); 26 | } 27 | 28 | export default [ 29 | { 30 | input: 'sw.js', 31 | output: { 32 | format: 'es', 33 | dir: 'build' 34 | }, 35 | plugins: [replace({ 'process.env.NODE_ENV': '"production"' }), resolve(), terser({ output: { comments: false } })] 36 | }, 37 | { 38 | input: 'index.html', 39 | output: { 40 | entryFileNames: '[hash].js', 41 | chunkFileNames: '[hash].js', 42 | format: 'es', 43 | dir: 'build' 44 | }, 45 | 46 | plugins: [ 47 | { 48 | name: 'version', 49 | load(id) { 50 | // replace the version module with a live version from the package.json 51 | if (id === versionModulePath) { 52 | return `export default '${packageJson.version}'`; 53 | } 54 | } 55 | }, 56 | resolve(), 57 | html({ 58 | transform: [preloadInitialImports] 59 | }), 60 | babel({ 61 | babelHelpers: 'bundled', 62 | presets: [require.resolve('@babel/preset-modules')], 63 | plugins: [ 64 | [require.resolve('babel-plugin-htm'), { import: 'preact' }], 65 | [require.resolve('babel-plugin-bundled-import-meta'), { importStyle: 'baseURI' }], 66 | require.resolve('@babel/plugin-proposal-optional-chaining'), 67 | require.resolve('@babel/plugin-proposal-nullish-coalescing-operator') 68 | ] 69 | }), 70 | terser({ output: { comments: false } }), 71 | copy({ 72 | hook: 'buildStart', 73 | targets: [ 74 | { src: 'src/assets/**/*', dest: 'build/src' }, 75 | { src: 'src/style/**/*.css', dest: 'build/src' } 76 | ], 77 | flatten: false 78 | }), 79 | copy({ 80 | hook: 'buildStart', 81 | targets: [{ src: 'data/**/*', dest: 'build/data' }], 82 | flatten: false 83 | }), 84 | copy({ 85 | hook: 'buildStart', 86 | targets: [{ src: 'src/locale/translations', dest: 'build/' }], 87 | flatten: true 88 | }), 89 | copy({ 90 | hook: 'buildStart', 91 | targets: [{ src: 'local_dependencies', dest: 'build/' }], 92 | flatten: true 93 | }), 94 | copy({ 95 | hook: 'buildStart', 96 | targets: [ 97 | { src: 'manifest.json', dest: 'build/' }, 98 | { src: 'manifest-dark.json', dest: 'build/' } 99 | ], 100 | flatten: false 101 | }), 102 | injectManifest({ 103 | swSrc: 'build/sw.js', 104 | swDest: 'build/sw.js', 105 | globDirectory: 'build/', 106 | mode: 'production' 107 | // modifyURLPrefix: { 108 | // '': '/lockdown/' 109 | // } 110 | }), 111 | applySwRegistration({ 112 | htmlFileName: 'index.html' 113 | // base: 'lockdown/' 114 | }), 115 | { 116 | name: 'minify-css', 117 | writeBundle() { 118 | const filepath = path.resolve('./build/src/style/'); 119 | fs.readdirSync(filepath) 120 | .map(file => path.join(filepath, file)) 121 | .forEach(file => { 122 | fs.writeFileSync(file, uglifycss.processFiles([file])); 123 | }); 124 | } 125 | } 126 | ] 127 | } 128 | ]; 129 | -------------------------------------------------------------------------------- /shared/types.js: -------------------------------------------------------------------------------- 1 | // Referrence: https://docs.google.com/spreadsheets/d/1mVyQxxLxAF3E1dw870WHXTOLgYzmumojvzIekpgvLV0/edit#gid=663521510 2 | const DATA_ENTRY_STATUS = { 3 | STANDBY: '1', 4 | READY: '2', 5 | DEMO: '3', 6 | FLAGGED: '4', 7 | }; 8 | 9 | const DATA_ENTRY = { 10 | OFFICIAL_PROCLAMATION: '1', 11 | MEDIA: '2', 12 | OTHER: '3', 13 | }; 14 | 15 | // AKA Answer 16 | const MEASURE = { 17 | YES: '1', 18 | PARTIAL: '2', 19 | NO: '3', 20 | UNCLEAR: '4', 21 | }; 22 | 23 | // const LOCKDOWN = { 24 | // NO: '1', 25 | // PARTIAL: '2', 26 | // YES: '3', 27 | // UNCLEAR: '4', 28 | // } 29 | 30 | const COUNTRY_STATUS = { 31 | UNCLEAR: '1', 32 | NA: '2', 33 | STATE_OF_CALAMITY: '3', 34 | STATE_OF_EMERGENCY: '4', 35 | STATE_OF_ALERT: '5', 36 | OTHER_YES: '6', 37 | OTHER_NO: '7', 38 | STATE_OF_NATURAL_DISASTER: '8', 39 | STATE_OF_NATIONAL_DISASTER: '9', 40 | } 41 | 42 | // AKA In & Out 43 | const TRAVEL = { 44 | YES: '1', 45 | PARTIALLY: '2', 46 | NO: '3', 47 | UNCLEAR: '4', 48 | NA: '5', 49 | }; 50 | 51 | const GLOBAL_COUNTRY_STATUS = { 52 | NO_DATA: '1', 53 | ACTIVE: '2', 54 | DEMO: '3', 55 | DELETE: '4', 56 | }; 57 | 58 | const UPDATE = { 59 | NEW_ENTRY: '1', 60 | ANNOUNCEMENTS: '2', 61 | RECTIFICATIONS: '3', 62 | PROMOTING_OTHER_PROJECTS: '4', 63 | PROMOTING_PETITIONS: '5', 64 | }; 65 | 66 | const UPDATE_STATUS = { 67 | STANDBY: '1', 68 | READY: '2', 69 | DEMO: '3', 70 | FLAGGED: '4', 71 | } 72 | 73 | module.exports = { 74 | DATA_ENTRY_STATUS: DATA_ENTRY_STATUS, 75 | DATA_ENTRY: DATA_ENTRY, 76 | MEASURE: MEASURE, 77 | COUNTRY_STATUS: COUNTRY_STATUS, 78 | TRAVEL: TRAVEL, 79 | GLOBAL_COUNTRY_STATUS: GLOBAL_COUNTRY_STATUS, 80 | UPDATE: UPDATE, 81 | UPDATE_STATUS: UPDATE_STATUS, 82 | // LOCKDOWN: LOCKDOWN, 83 | } -------------------------------------------------------------------------------- /src/assets/apple-touch-icon-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/apple-touch-icon-dark.png -------------------------------------------------------------------------------- /src/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/favicon-16x16-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/favicon-16x16-dark.png -------------------------------------------------------------------------------- /src/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/favicon-32x32-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/favicon-32x32-dark.png -------------------------------------------------------------------------------- /src/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/assets/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/assets/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/icons/arrow-left.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const arrowLeft = html` 4 | 5 | `; 6 | -------------------------------------------------------------------------------- /src/assets/icons/arrow-right.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const arrowRight = html` 4 | 5 | `; 6 | -------------------------------------------------------------------------------- /src/assets/icons/burger.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const burger = html` 4 | 5 | 12 | 13 | `; 14 | -------------------------------------------------------------------------------- /src/assets/icons/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/calendar.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const calendar = html` 4 | 12 | 18 | 29 | `; 30 | -------------------------------------------------------------------------------- /src/assets/icons/chevron-down.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const chevronDown = html` 15 | 16 | 17 | `; 18 | -------------------------------------------------------------------------------- /src/assets/icons/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/chevron-up.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const chevronUp = html` 4 | 5 | `; 6 | -------------------------------------------------------------------------------- /src/assets/icons/circle-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/icons/download.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/icons/favicon-16x16.png -------------------------------------------------------------------------------- /src/assets/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/icons/favicon-32x32.png -------------------------------------------------------------------------------- /src/assets/icons/icons.js: -------------------------------------------------------------------------------- 1 | export { info } from './info.svg.js'; 2 | export { settings } from './settings.svg.js'; 3 | export { refresh } from './ticker.svg.js'; 4 | export { add } from './contribute.svg.js'; 5 | export { close } from './x.svg.js'; 6 | export { trues } from './true.svg.js'; 7 | export { logo } from './logo.svg.js'; 8 | export { offline } from './offline.svg.js'; 9 | export { loading } from './loading.svg.js'; 10 | export { travelLand } from './travel-land.svg.js'; 11 | export { travelSea } from './travel-sea.svg.js'; 12 | export { travelFlight } from './travel-flight.svg.js'; 13 | export { arrowLeft } from './arrow-left'; 14 | export { arrowRight } from './arrow-right'; 15 | export { burger } from './burger.svg.js'; 16 | export { menuLeft, menuRight } from './menu.svg.js'; 17 | export { list } from './list.svg.js'; 18 | export { unlock } from './unlock.svg.js'; 19 | export { lock } from './lock.svg.js'; 20 | export { virus } from './virus.svg.js'; 21 | export { viruslock } from './viruslock.svg.js'; 22 | export { calendar } from './calendar.svg.js'; 23 | export { chevronDown } from './chevron-down.js'; 24 | export { chevronUp } from './chevron-up.js'; 25 | export { world } from './world.js'; 26 | export { magnify } from './magnify.svg.js'; 27 | 28 | export { home, citymovement, religion, work, military, academia, shops, electricity, water, internet } from './measures.svg.js'; 29 | -------------------------------------------------------------------------------- /src/assets/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/info.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const info = html` 4 | 24 | 33 | 37 | 38 | `; 39 | const settings = html` 40 | 54 | settings 55 | 56 | 59 | 60 | 61 | `; 62 | -------------------------------------------------------------------------------- /src/assets/icons/list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/list.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const list = html` 4 | 8 | 12 | 16 | 20 | 24 | 28 | 32 | 36 | `; 37 | -------------------------------------------------------------------------------- /src/assets/icons/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/loading.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const loading = html` 4 | 5 | 6 | 7 | `; 8 | -------------------------------------------------------------------------------- /src/assets/icons/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/lock.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const lock = html` 4 | 8 | `; 9 | -------------------------------------------------------------------------------- /src/assets/icons/magnify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/magnify.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const magnify = html` 4 | 8 | `; 9 | -------------------------------------------------------------------------------- /src/assets/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/icons/menu.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | // ? When the sidebar is hide 4 | export const menuRight = html` 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | `; 25 | 26 | // ? When the sidebar is visible 27 | export const menuLeft = html` 28 | 29 | 30 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | `; 48 | -------------------------------------------------------------------------------- /src/assets/icons/menu/black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/icons/menu/white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/assets/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/icons/mstile-150x150.png -------------------------------------------------------------------------------- /src/assets/icons/offline.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const offline = html` 4 | 5 | 6 | 12 | 13 | Asset 2 14 | 15 | 16 | 20 | 21 | 22 | 23 | `; 24 | -------------------------------------------------------------------------------- /src/assets/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/settings.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const settings = html` 4 | 25 | 35 | 39 | 40 | `; 41 | -------------------------------------------------------------------------------- /src/assets/icons/ticker.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const refresh = html` 4 | 23 | 32 | 36 | 37 | `; 38 | -------------------------------------------------------------------------------- /src/assets/icons/travel-flight.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const travelFlight = html` 4 | 5 | 6 | 9 | 10 | 11 | 12 | `; 13 | -------------------------------------------------------------------------------- /src/assets/icons/travel-land.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const travelLand = html` 4 | 5 | 8 | 9 | 10 | `; 11 | -------------------------------------------------------------------------------- /src/assets/icons/travel-sea.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const travelSea = html` 4 | 5 | 6 | 9 | 10 | `; 11 | -------------------------------------------------------------------------------- /src/assets/icons/true.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/true.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const trues = html` 4 | 15 | 16 | 17 | `; 18 | -------------------------------------------------------------------------------- /src/assets/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/viruslock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/viruslock.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const viruslock = html` 4 | 8 | `; 9 | -------------------------------------------------------------------------------- /src/assets/icons/world.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const world = html` 4 | 8 | `; 9 | -------------------------------------------------------------------------------- /src/assets/icons/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/x.svg.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | 3 | export const close = html` 4 | 5 | 6 | 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/pld-report-lrg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/images/pld-report-lrg.png -------------------------------------------------------------------------------- /src/assets/images/pld-report-med.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/images/pld-report-med.png -------------------------------------------------------------------------------- /src/assets/images/pld-report-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/images/pld-report-sm.png -------------------------------------------------------------------------------- /src/assets/images/stripes-pattern-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/images/stripes-pattern-2.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-icon-120.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-icon-152.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-icon-167.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-icon-180.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-1125-2436.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-1125-2436.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-1136-640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-1136-640.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-1242-2208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-1242-2208.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-1242-2688.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-1242-2688.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-1334-750.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-1334-750.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-1536-2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-1536-2048.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-1668-2224.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-1668-2224.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-1668-2388.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-1668-2388.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-1792-828.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-1792-828.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-2048-1536.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-2048-1536.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-2048-2732.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-2048-2732.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-2208-1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-2208-1242.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-2224-1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-2224-1668.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-2388-1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-2388-1668.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-2436-1125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-2436-1125.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-2688-1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-2688-1242.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-2732-2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-2732-2048.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-640-1136.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-640-1136.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-750-1334.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-750-1334.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-828-1792.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-828-1792.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-1125-2436.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-1125-2436.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-1136-640.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-1136-640.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-1242-2208.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-1242-2208.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-1242-2688.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-1242-2688.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-1334-750.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-1334-750.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-1536-2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-1536-2048.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-1668-2224.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-1668-2224.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-1668-2388.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-1668-2388.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-1792-828.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-1792-828.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-2048-1536.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-2048-1536.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-2048-2732.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-2048-2732.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-2208-1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-2208-1242.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-2224-1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-2224-1668.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-2388-1668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-2388-1668.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-2436-1125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-2436-1125.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-2688-1242.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-2688-1242.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-2732-2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-2732-2048.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-640-1136.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-640-1136.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-750-1334.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-750-1334.png -------------------------------------------------------------------------------- /src/assets/pwa/apple-splash-dark-828-1792.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/apple-splash-dark-828-1792.png -------------------------------------------------------------------------------- /src/assets/pwa/manifest-icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/manifest-icon-192.png -------------------------------------------------------------------------------- /src/assets/pwa/manifest-icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/assets/pwa/manifest-icon-512.png -------------------------------------------------------------------------------- /src/bootstrap.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | import { render } from 'preact'; 3 | import { installMediaQueryWatcher } from 'pwa-helpers/media-query.js'; 4 | import { App } from './components/App.js'; 5 | import version from './version'; 6 | import { setFavIcon } from './utils/setFavIcon.js'; 7 | import './lazy-resources.js'; 8 | 9 | console.log(`🌐 Project Lockdown, version: ${version}`); 10 | 11 | window.addEventListener('appinstalled', () => { 12 | window.location.reload(); 13 | }); 14 | 15 | installMediaQueryWatcher(`(prefers-color-scheme: dark)`, (preference) => { 16 | const localStorageDarkmode = localStorage.getItem('darkmode'); 17 | const darkmodePreferenceExists = localStorageDarkmode !== null; 18 | const darkMode = localStorageDarkmode === 'true'; 19 | 20 | // on initial pageload, decide darkmode on users system preference 21 | if (!darkmodePreferenceExists) { 22 | if (preference) { 23 | document.documentElement.classList.add('dark'); 24 | setFavIcon(true); 25 | } else { 26 | document.documentElement.classList.remove('dark'); 27 | setFavIcon(false); 28 | } 29 | } else { 30 | // on subsequent pageloads, decide darkmode on users chosen preference 31 | if (darkMode) { 32 | document.documentElement.classList.add('dark'); 33 | setFavIcon(true); 34 | } 35 | } 36 | }); 37 | 38 | render(html` <${App} /> `, document.getElementById('app')); 39 | -------------------------------------------------------------------------------- /src/components/Expandable.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | import { useState, useRef, useEffect } from 'preact/compat'; 3 | import css from 'csz'; 4 | 5 | export function Expandable(props) { 6 | const [expanded, setExpanded] = useState(false); 7 | const detail = useRef(null); 8 | 9 | useEffect(() => { 10 | if (expanded) { 11 | detail.current.focus({ preventScroll: true }); 12 | } 13 | }, [expanded]); 14 | 15 | useEffect(() => { 16 | if (props.currentDropdown !== props.toggle) { 17 | setExpanded(false); 18 | } 19 | }, [props.currentDropdown]); 20 | 21 | return html` 22 |
23 |
24 | 37 | 38 |
44 | ${props.detail} 45 |
46 |
47 |
48 | `; 49 | } 50 | 51 | const styles = css` 52 | .ld-expandable { 53 | width: 100%; 54 | height: auto; 55 | border-bottom: lightgrey solid 1px; 56 | .dark & { 57 | border-bottom: var(--ld-gray-2) solid 1px; 58 | } 59 | } 60 | 61 | .ld-expandable a { 62 | color: var(--ld-active); 63 | } 64 | 65 | .ld-expandable--icon { 66 | height: 60px; 67 | display: flex; 68 | align-items: center; 69 | } 70 | 71 | .ld-expandable--toggle-content { 72 | flex: 1; 73 | font-family: 'Montserrat', sans-serif; 74 | display: flex; 75 | align-items: center; 76 | height: 60px; 77 | } 78 | 79 | .ld-expandable button { 80 | color: var(--ld-text); 81 | text-align: left; 82 | width: 100%; 83 | display: flex; 84 | 85 | border: none; 86 | margin: 0; 87 | padding: 0; 88 | overflow: visible; 89 | background: transparent; 90 | } 91 | 92 | .ld-expandable--toggle { 93 | height: 60px; 94 | } 95 | 96 | .ld-expandable--toggle:hover { 97 | background-color: var(--ld-hover); 98 | } 99 | 100 | .ld-expandable--detail { 101 | } 102 | 103 | .ld-expandable--expanded { 104 | display: block; 105 | padding: 10px; 106 | } 107 | 108 | .ld-expandable--closed { 109 | display: none; 110 | } 111 | `; 112 | 113 | const chevronRight = html` 114 | 126 | 127 | 128 | 129 | `; 130 | 131 | const chevronDown = html` 132 | 144 | 145 | 146 | 147 | `; 148 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | import css from 'csz'; 3 | import Totals from './Totals.js'; 4 | import { logo } from '../assets/icons/icons.js'; 5 | 6 | const styles = css` 7 | @keyframes fadeOutUp { 8 | from { 9 | opacity: 1; 10 | } 11 | 12 | to { 13 | opacity: 0; 14 | transform: translate3d(0, -100%, 0); 15 | } 16 | } 17 | @keyframes fadeInDown { 18 | from { 19 | opacity: 0; 20 | transform: translate3d(0, -100%, 0); 21 | } 22 | 23 | to { 24 | opacity: 1; 25 | transform: translate3d(0, 0, 0); 26 | } 27 | } 28 | & { 29 | position: fixed; 30 | top: 0; 31 | left: 0; 32 | right: 0; 33 | background-color: var(--ld-bg); 34 | color: var(--ld-text); 35 | /* needs to be higher than the map */ 36 | z-index: 1200; 37 | display: flex; 38 | align-items: center; 39 | justify-content: space-around; 40 | /*height: 60px;*/ 41 | height: 55px; 42 | width: 100%; 43 | animation: 1s fadeInDown forwards; 44 | display:none; 45 | @media (max-width: 899px) { 46 | &.hide{ 47 | animation: 1s fadeOutUp forwards; 48 | } 49 | } 50 | } 51 | 52 | &::after { 53 | position: absolute; 54 | bottom: 0; 55 | left: 0; 56 | width: 100%; 57 | height: 100%; 58 | content: ''; 59 | box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.12), 0 8px 8px 0 rgba(0, 0, 0, 0.24); 60 | } 61 | 62 | a { 63 | position: relative; 64 | display: block; 65 | flex-shrink: 0; 66 | margin-left: auto; 67 | margin-right: auto; 68 | max-width: 65%; 69 | } 70 | 71 | a p { 72 | position: absolute; 73 | color: var(--ld-active); 74 | top: -4px; 75 | right: -45px; 76 | margin: 0; 77 | font-size: 12px; 78 | } 79 | 80 | .totals { 81 | margin-left: auto; 82 | margin-right: auto; 83 | display: block; 84 | } 85 | 86 | .ld-logo-wrapper { 87 | display: flex; 88 | width: 320px; 89 | & a svg{ 90 | width: 100% 91 | height: 100% 92 | } 93 | } 94 | 95 | img { 96 | display: block; 97 | margin-left: auto; 98 | margin-right: auto; 99 | } 100 | 101 | @media (max-width: 899px) { 102 | .totals { 103 | display: none; 104 | } 105 | } 106 | `; 107 | 108 | export const Header = (_) => html` 109 | 110 |
111 | 118 |
119 | <${Totals} selectedDate=${_.selectedDate} startDate=${_.startDate} endDate=${_.endDate} i18n=${_.i18n} /> 120 |
121 |
122 | `; 123 | -------------------------------------------------------------------------------- /src/components/LanguageSelector.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'preact'; 2 | import { html } from 'htm/preact'; 3 | import css from 'csz'; 4 | 5 | import { world } from '../assets/icons/icons.js'; 6 | 7 | const selectedLang = css` 8 | & { 9 | position: absolute; 10 | top: 60px; 11 | @media (max-width: 590px) { 12 | top: 65px; 13 | } 14 | @media (max-width: 330px) { 15 | top: 75px; 16 | } 17 | right: 2.5vw; 18 | height: 30px; 19 | min-width: 30px; 20 | padding: 0px; 21 | padding-left: 10px; 22 | border-radius: 15px; 23 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.25); 24 | color: #333333; 25 | background-color: #fff; 26 | display: flex; 27 | justify-content: space-around; 28 | align-items: center; 29 | font-weight: 600; 30 | font-size: 12px; 31 | letter-spacing: 0.05em; 32 | &:hover { 33 | cursor: pointer; 34 | } 35 | .dark & { 36 | background-color: #333333; 37 | color: #fff; 38 | } 39 | & .circleBtn { 40 | margin-left: 5px; 41 | height: 30px; 42 | min-width: 30px; 43 | border-radius: 50%; 44 | color: #333333; 45 | display: flex; 46 | justify-content: center; 47 | align-items: center; 48 | font-weight: 600; 49 | font-size: 12px; 50 | letter-spacing: 0.05em; 51 | .dark & { 52 | background-color: #333333; 53 | color: #fff; 54 | } 55 | } 56 | } 57 | `; 58 | 59 | const langOptions = css` 60 | & { 61 | background: transparent; 62 | display: flex; 63 | align-items: center; 64 | position: absolute; 65 | top: 100px; 66 | @media (max-width: 330px) { 67 | top: 110px; 68 | } 69 | right: 2.5vw; 70 | max-width: 0px; 71 | overflow: hidden; 72 | transition: 0.5s; 73 | padding: 5px 0px; 74 | flex-wrap: no-wrap; 75 | &.show { 76 | max-width: 50vw; 77 | flex-wrap: wrap; 78 | } 79 | & .langOpt { 80 | display: flex; 81 | text-align: center; 82 | align-items: center; 83 | justify-content: center; 84 | height: 30px; 85 | width: 30px; 86 | border-radius: 50%; 87 | background-color: #fff; 88 | color: #333333; 89 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.25); 90 | overflow: hidden; 91 | margin-left: 10px; 92 | font-weight: 500; 93 | margin-bottom: 10px; 94 | &:hover { 95 | cursor: pointer; 96 | border: 0px solid rgba(51, 51, 51, 0.5); 97 | background-color: #e0e0e0; 98 | } 99 | &.active { 100 | border: 0px solid #333333; 101 | background-color: #e0e0e0; 102 | } 103 | .dark &.active { 104 | border: 0px solid #fff; 105 | background-color: #828282; 106 | } 107 | .dark & { 108 | background-color: #333333; 109 | color: #fff; 110 | } 111 | .dark &:hover { 112 | background-color: #828282; 113 | } 114 | } 115 | } 116 | `; 117 | 118 | class LanguageSelector extends Component { 119 | constructor() { 120 | super(); 121 | this.state = { 122 | showLangOpt: false, 123 | }; 124 | this.toggleLangOpts = this.toggleLangOpts.bind(this); 125 | this.changeCurrentLanguage = this.changeCurrentLanguage.bind(this); 126 | } 127 | toggleLangOpts() { 128 | this.setState({ 129 | showLangOpt: !this.state.showLangOpt, 130 | }); 131 | } 132 | changeCurrentLanguage(lang) { 133 | this.toggleLangOpts(); 134 | this.props.onLocateChange(lang); 135 | } 136 | render({ i18n, languages }, { showLangOpt }) { 137 | return html` 138 |
139 | ${i18n.locale?.toUpperCase()} 140 |
${world}
141 |
142 |
143 | ${languages.map((language) => { 144 | return html`
this.changeCurrentLanguage(language)} 147 | > 148 | ${language?.toUpperCase().replace('-', '\n')} 149 |
`; 150 | })} 151 |
152 | `; 153 | } 154 | } 155 | 156 | export default LanguageSelector; 157 | -------------------------------------------------------------------------------- /src/components/Lazy.js: -------------------------------------------------------------------------------- 1 | import { Component, h } from 'preact'; 2 | 3 | export class Lazy extends Component { 4 | async componentWillMount() { 5 | const module = await this.props.component(); 6 | const Component = module.default || module; 7 | this.setState({ Component }); 8 | } 9 | 10 | render({ props }, { Component }) { 11 | return h(Component, props); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Tabs.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | import { Component } from 'preact'; 3 | import { info, settings, refresh, add } from '../assets/icons/icons.js'; 4 | import { addPwaUpdateListener } from '../utils/addPwaUpdateListener.js'; 5 | import { installMediaQueryWatcher } from 'pwa-helpers/media-query.js'; 6 | import { dialogService } from '../services/dialogService.js'; 7 | 8 | const ICONS = { 9 | info, 10 | settings, 11 | updates: refresh, 12 | contribute: add, 13 | }; 14 | const KEYCODE_LEFT = 37; 15 | const KEYCODE_RIGHT = 39; 16 | 17 | export default class Tabs extends Component { 18 | constructor() { 19 | super(); 20 | this.state = { 21 | updateAvailable: false, 22 | index: 0, 23 | }; 24 | 25 | this.tabRefs = {}; 26 | 27 | this.__onFocusMove = this.__onFocusMove.bind(this); 28 | this.__onTabClick = this.__onTabClick.bind(this); 29 | } 30 | 31 | componentDidMount() { 32 | installMediaQueryWatcher(`(min-width: 960px)`, (matches) => { 33 | this.setState({ isMobile: !matches }); 34 | }); 35 | 36 | dialogService.addEventListener('close', (e) => { 37 | if (e.detail.menuDialogClosed) { 38 | this.tabRefs[`tab${this.state.index}`].focus(); 39 | } 40 | }); 41 | 42 | addPwaUpdateListener((updateAvailable) => { 43 | this.setState({ 44 | updateAvailable, 45 | }); 46 | }); 47 | } 48 | 49 | updateIndex(i, type) { 50 | if (type === 'settings' && this.state.updateAvailable) { 51 | this.setState({ 52 | updateAvailable: false, 53 | }); 54 | } 55 | 56 | this.setState({ index: i }); 57 | this.tabRefs[`tab${i}`].focus(); 58 | if (!this.state.isMobile) { 59 | this.commit(type); 60 | } 61 | } 62 | 63 | __onTabClick(i, type) { 64 | this.updateIndex(i, type); 65 | this.commit(type); 66 | } 67 | 68 | commit(type) { 69 | this.props.switchContent(type.toLowerCase()); 70 | } 71 | 72 | __onFocusMove(e) { 73 | const currIndex = this.state.index; 74 | 75 | switch (e.keyCode) { 76 | case KEYCODE_LEFT: 77 | if (currIndex !== 0) { 78 | this.updateIndex(currIndex - 1, this.tabRefs[`tab${currIndex - 1}`].getAttribute('data-label').toLowerCase()); 79 | } 80 | break; 81 | case KEYCODE_RIGHT: 82 | if (currIndex !== this.props.children.length - 1) { 83 | this.updateIndex(currIndex + 1, this.tabRefs[`tab${currIndex + 1}`].getAttribute('data-label').toLowerCase()); 84 | } 85 | break; 86 | } 87 | } 88 | 89 | render(_, { index }) { 90 | return html` 91 |
    92 | ${this.props.children.map((child, i) => { 93 | return html` 94 | 110 | `; 111 | })} 112 |
113 | `; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/components/TerritoryTabs.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-for-All/lockdown/7f2f3f61130f541537b89b150be49feeb5587dfa/src/components/TerritoryTabs.js -------------------------------------------------------------------------------- /src/components/Totals.js: -------------------------------------------------------------------------------- 1 | import { html } from 'htm/preact'; 2 | import { installMediaQueryWatcher } from 'pwa-helpers/media-query.js'; 3 | import css from 'csz'; 4 | import { Component } from 'preact'; 5 | import { totalsService } from '../services/totalsService.js'; 6 | import { format } from 'date-fns/esm'; 7 | 8 | // ? Wrappers 9 | import withMobileDetection from './Wrappers/withMobileDetection'; 10 | 11 | const styles = css` 12 | dl { 13 | display: flex; 14 | height: 100%; 15 | padding: 0; 16 | margin: 0; 17 | text-align: center; 18 | text-transform: uppercase; 19 | width:400px; 20 | @media (max-width: 590px) { 21 | width:100%; 22 | } 23 | } 24 | 25 | dl div { 26 | position: relative; 27 | display: flex; 28 | flex-direction: column; 29 | justify-content: space-between; 30 | width: 50%; 31 | @media (max-width: 899px) { 32 | width: 50%; 33 | } 34 | padding: 5px 16px; 35 | @media (max-width: 590px) { 36 | padding: 2px 6px; 37 | } 38 | margin: 5px 0; 39 | } 40 | 41 | dt { 42 | display: block; 43 | } 44 | 45 | dd { 46 | margin-left: 0px; 47 | display: block; 48 | margin-top:5px; 49 | } 50 | 51 | div:not(:last-of-type)::after { 52 | content: ''; 53 | position: absolute; 54 | top: 0; 55 | bottom: 0; 56 | right: 0; 57 | width: 1px; 58 | background-color: #e0e0e0; 59 | } 60 | `; 61 | 62 | const SHOW_STATS = true; 63 | 64 | class Totals extends Component { 65 | constructor() { 66 | super(); 67 | 68 | this.state = { totals: {} }; 69 | } 70 | 71 | async componentDidUpdate(prevProps) { 72 | if (this.props.selectedDate !== prevProps.selectedDate) { 73 | const { startDate, endDate, selectedDate, daysRange } = this.props; 74 | this.setState({ 75 | totals: await totalsService.getTotals({ date: selectedDate, startDate, endDate, daysRange }), 76 | }); 77 | } 78 | } 79 | 80 | async componentDidMount() { 81 | installMediaQueryWatcher(`(min-width: 900px)`, (matches) => { 82 | this.setState({ desktop: matches }); 83 | }); 84 | const { daysRange } = this.props; 85 | const totals = await totalsService.getTotals({ 86 | date: this.props.selectedDate, 87 | startDate: this.props.startDate, 88 | endDate: this.props.endDate, 89 | daysRange, 90 | }); 91 | 92 | this.setState({ 93 | totals: totals, 94 | }); 95 | } 96 | 97 | // { 98 | // description: i18n.t('header.totals.cases'), 99 | // value: Number(totals.corona?.confirmed || 0).toLocaleString(), 100 | // }, 101 | // { 102 | // description: i18n.t('header.totals.deaths'), 103 | // value: Number(totals.corona?.deaths || 0).toLocaleString(), 104 | // }, 105 | 106 | render({ selectedDate, i18n }, { totals, desktop }) { 107 | const items = [ 108 | { 109 | description: i18n.t('header.totals.territoriesLockdown'), 110 | value: Number(totals.territories?.lockdown || 0).toLocaleString(), 111 | }, 112 | { 113 | description: i18n.t('header.totals.peopleAffected'), 114 | value: Number(totals.territories?.affected || 0).toLocaleString(), 115 | }, 116 | ]; 117 | return html` 118 |
119 | ${SHOW_STATS 120 | ? html`
121 | ${(desktop ? items : items.slice(0, 2)).map( 122 | (item) => html` 123 |
124 |
${item.description}
125 |
${item.value}
126 |
127 | ` 128 | )} 129 |
` 130 | : null} 131 |
132 | `; 133 | } 134 | } 135 | 136 | export default withMobileDetection(Totals); 137 | -------------------------------------------------------------------------------- /src/components/Wrappers/withMobileDetection.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'preact'; 2 | import { html } from 'htm/preact'; 3 | 4 | export default function withMobileDetection(WrappedComponent) { 5 | return class extends Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | isMobile: false, 10 | daysRange: 70, 11 | }; 12 | } 13 | 14 | componentWillMount() { 15 | let width = window.innerWidth || window.clientWidth; 16 | let isMobile = false; 17 | let daysRange = 80; 18 | if (width <= 960) { 19 | isMobile = true; 20 | daysRange = 70; 21 | } 22 | this.setState({ 23 | isMobile, 24 | daysRange, 25 | }); 26 | } 27 | 28 | render(_) { 29 | return html`<${WrappedComponent} isMobile=${this.state.isMobile} daysRange=${this.state.daysRange} ...${this.props} />`; 30 | } 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/components/pwa-install-button.js: -------------------------------------------------------------------------------- 1 | let deferredPrompt; 2 | let installable; 3 | 4 | window.addEventListener('beforeinstallprompt', (e) => { 5 | e.preventDefault(); 6 | installable = true; 7 | deferredPrompt = e; 8 | }); 9 | 10 | class PwaInstallButton extends HTMLElement { 11 | constructor() { 12 | super(); 13 | const shadow = this.attachShadow({ mode: 'open' }); 14 | shadow.innerHTML = ``; 15 | } 16 | 17 | connectedCallback() { 18 | this.setAttribute('hidden', ''); 19 | 20 | this.addEventListener('click', this._handlePrompt.bind(this)); 21 | window.addEventListener('beforeinstallprompt', (e) => { 22 | e.preventDefault(); 23 | deferredPrompt = e; 24 | this.removeAttribute('hidden'); 25 | this.dispatchEvent(new CustomEvent('pwa-installable', { detail: true })); 26 | }); 27 | 28 | if (installable) { 29 | this.removeAttribute('hidden'); 30 | } 31 | } 32 | 33 | async _handlePrompt(e) { 34 | e.preventDefault(); 35 | deferredPrompt.prompt(); 36 | const { outcome } = await deferredPrompt.userChoice; 37 | if (outcome === 'accepted') { 38 | this.dispatchEvent(new CustomEvent('pwa-installed', { detail: true })); 39 | this.setAttribute('hidden', ''); 40 | deferredPrompt = null; 41 | } else { 42 | this.dispatchEvent(new CustomEvent('pwa-installed', { detail: false })); 43 | } 44 | } 45 | } 46 | 47 | customElements.define('pwa-install-button', PwaInstallButton); 48 | -------------------------------------------------------------------------------- /src/components/pwa-update-available.js: -------------------------------------------------------------------------------- 1 | class PwaUpdateAvailable extends HTMLElement { 2 | constructor() { 3 | super(); 4 | const shadow = this.attachShadow({ mode: 'open' }); 5 | shadow.innerHTML = ``; 6 | this._refreshing = false; 7 | } 8 | 9 | async connectedCallback() { 10 | this.addEventListener('click', this._postMessage.bind(this)); 11 | 12 | navigator.serviceWorker.addEventListener('controllerchange', () => { 13 | if (this._refreshing) return; 14 | window.location.reload(); 15 | this._refreshing = true; 16 | }); 17 | } 18 | 19 | async _postMessage(e) { 20 | e.preventDefault(); 21 | const reg = await navigator.serviceWorker.getRegistration(); 22 | reg.waiting.postMessage({ type: 'SKIP_WAITING' }); 23 | } 24 | } 25 | 26 | customElements.define('pwa-update-available', PwaUpdateAvailable); 27 | -------------------------------------------------------------------------------- /src/components/tool-tip.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | class Tooltip extends HTMLElement { 18 | /** 19 | * The constructor does work that needs to be executed _exactly_ once. 20 | */ 21 | constructor() { 22 | super(); 23 | 24 | // These functions are used in a bunch of places, and always need to 25 | // bind the correct `this` reference, so do it once. 26 | this._show = this._show.bind(this); 27 | this._hide = this._hide.bind(this); 28 | } 29 | 30 | /** 31 | * `connectedCallback()` fires when the element is inserted into the DOM. 32 | * It's a good place to set the initial `role`, `tabindex`, internal state, 33 | * and install event listeners. 34 | */ 35 | connectedCallback() { 36 | if (!this.hasAttribute('role')) this.setAttribute('role', 'tooltip'); 37 | 38 | if (!this.hasAttribute('tabindex')) this.setAttribute('tabindex', -1); 39 | 40 | this._hide(); 41 | 42 | // The element that triggers the tooltip references the tooltip 43 | // element with `aria-describedby`. 44 | this._target = document.querySelector('[aria-describedby=' + this.id + ']'); 45 | if (!this._target) return; 46 | // The tooltip needs to listen to `focus`/`blur` events from the target, 47 | // as well as `hover` events over the target. 48 | this._target.addEventListener('focus', this._show); 49 | this._target.addEventListener('blur', this._hide); 50 | this._target.addEventListener('mouseenter', this._show); 51 | this._target.addEventListener('mouseleave', this._hide); 52 | } 53 | 54 | /** 55 | * `disconnectedCallback()` unregisters the event listeners that were set up 56 | * in `connectedCallback()`. 57 | */ 58 | disconnectedCallback() { 59 | if (!this._target) return; 60 | 61 | // Remove the existing listeners, so that they don't trigger even though 62 | // there's no tooltip to show. 63 | this._target.removeEventListener('focus', this._show); 64 | this._target.removeEventListener('blur', this._hide); 65 | this._target.removeEventListener('mouseenter', this._show); 66 | this._target.removeEventListener('mouseleave', this._hide); 67 | this._target = null; 68 | } 69 | 70 | _show() { 71 | this.hidden = false; 72 | } 73 | 74 | _hide() { 75 | this.hidden = true; 76 | } 77 | } 78 | 79 | window.customElements.define('tool-tip', Tooltip); 80 | -------------------------------------------------------------------------------- /src/lazy-resources.js: -------------------------------------------------------------------------------- 1 | const onInitialLoad = 'requestIdleCallback' in window ? window.requestIdleCallback : (cb) => setTimeout(cb, 1000); 2 | 3 | onInitialLoad(() => { 4 | import('./components/Dialog.js'); 5 | import('./components/CountryInfo.js'); 6 | }); 7 | -------------------------------------------------------------------------------- /src/locale/availableLanguages.js: -------------------------------------------------------------------------------- 1 | export default ['ar', 'en', 'en-US', 'es', 'es-ES', 'es-MX', 'it', 'pt', 'pt-BR', 'ru', 'zh-CN', 'zh-HK']; 2 | -------------------------------------------------------------------------------- /src/locale/i18nUtils.js: -------------------------------------------------------------------------------- 1 | export const encodeJsonTranslation = (json) => { 2 | if (typeof json === 'object') { 3 | let newJson = {}; 4 | let jsonKeys = Object.keys(json); 5 | jsonKeys.forEach((jsonKey) => { 6 | let a = json_traverse(json[jsonKey], jsonKey); 7 | if (Array.isArray(a)) { 8 | let newResponse = {}; 9 | for (let jsonItem of a) { 10 | newResponse = { ...newResponse, ...jsonItem }; 11 | } 12 | a = newResponse; 13 | } 14 | newJson = { ...newJson, ...a }; 15 | }); 16 | return newJson; 17 | } else { 18 | return json; 19 | } 20 | json_traverse(json); 21 | }; 22 | function json_traverse(o, prevKey) { 23 | var type = typeof o; 24 | if (type == 'object' && !Array.isArray(o)) { 25 | let results = []; 26 | for (let key in o) { 27 | let response; 28 | if (prevKey) { 29 | response = json_traverse(o[key], `${prevKey}_${key}`); 30 | } else { 31 | response = json_traverse(o[key], key); 32 | } 33 | if (Array.isArray(response)) { 34 | let newResponse = {}; 35 | for (let jsonItem of response) { 36 | newResponse = { ...newResponse, ...jsonItem }; 37 | } 38 | response = newResponse; 39 | } 40 | results.push(response); 41 | } 42 | return results; 43 | } else { 44 | let returnObject = {}; 45 | returnObject[prevKey] = o.replace(/{(.*?)}/g, ''); 46 | return returnObject; 47 | } 48 | } 49 | export const getAllFNSLanguages = async (objetiveLanguages) => { 50 | let languages = await import('date-fns/locale'); 51 | return { ...languages }; 52 | }; 53 | -------------------------------------------------------------------------------- /src/locale/index.js: -------------------------------------------------------------------------------- 1 | // ? Translations source 2 | import AvailableLanguages from './availableLanguages.js'; 3 | 4 | // ? Translations utils 5 | import { encodeJsonTranslation } from './i18nUtils'; 6 | 7 | const getAll = async () => { 8 | let finalTranslationsJSON = {}; 9 | let totals = 0; 10 | for (let i = 0; i < AvailableLanguages.length; i++) { 11 | try { 12 | let idiom = await import('./translations/' + AvailableLanguages[i] + '/index.js'); 13 | idiom = idiom.default; 14 | if (idiom.languageId === AvailableLanguages[i]) { 15 | finalTranslationsJSON[AvailableLanguages[i]] = encodeJsonTranslation(idiom); 16 | } else { 17 | console.warn(`The iso value of ${AvailableLanguages[i]} not match`); 18 | } 19 | if (i === AvailableLanguages.length - 1) { 20 | return finalTranslationsJSON; 21 | } 22 | } catch (e) { 23 | console.log(e); 24 | totals++; 25 | if (i === AvailableLanguages.length - 1) { 26 | return finalTranslationsJSON; 27 | } 28 | continue; 29 | } 30 | } 31 | if (totals === AvailableLanguages - 1) { 32 | console.log(finalTranslationsJSON); 33 | return finalTranslationsJSON; 34 | } 35 | }; 36 | 37 | export default getAll; 38 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lockdown", 3 | "short_name": "Lockdown", 4 | "start_url": "/", 5 | "display": "standalone", 6 | "orientation": "portrait", 7 | "background_color": "#fff", 8 | "theme_color": "#673ab8", 9 | "icons": [ 10 | { 11 | "src": "/assets/icons/android-chrome-192x192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "/assets/icons/android-chrome-512x512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import { EventTargetShim } from './utils/EventTargetShim.js'; 2 | 3 | class Router extends EventTargetShim { 4 | constructor() { 5 | super(); 6 | 7 | window.addEventListener('click', this.__onClick.bind(this), true); 8 | window.addEventListener('popstate', this.__onPathChanged.bind(this)); 9 | this.url = new URL(location); 10 | } 11 | 12 | setPath(path) { 13 | window.history.pushState(null, '', path); 14 | this.__onPathChanged(); 15 | } 16 | 17 | setSearchParam(key, value) { 18 | const params = new URLSearchParams(location.search); 19 | if (value) { 20 | params.set(key, value); 21 | } else { 22 | params.delete(key); 23 | } 24 | const paramsString = params.toString(); 25 | 26 | this.setPath(`${location.pathname}${paramsString ? `?${paramsString}` : ''}`); 27 | } 28 | 29 | __onPathChanged() { 30 | this.url = new URL(location); 31 | this.dispatchEvent(new Event('path-changed')); 32 | 33 | /** 34 | * Checks if a new service worker is available on SPA navigations 35 | * Otherwise if a user has their tab open indefinitely, they wont get updates 36 | */ 37 | if ('serviceWorker' in navigator) { 38 | navigator.serviceWorker.getRegistration().then((registration) => { 39 | if (registration) { 40 | registration.update(); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | __onClick(e) { 47 | if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey) { 48 | return; 49 | } 50 | 51 | const a = e.composedPath().find((el) => el.tagName === 'A'); 52 | 53 | if (!a || !a.href) return; 54 | if (a.hasAttribute('download') || a.href.includes('mailto:')) return; 55 | const target = a.getAttribute('target'); 56 | if (target && target !== '' && target !== '_self') return; 57 | 58 | const url = new URL(a.href); 59 | 60 | if (url.hash) { 61 | return; 62 | } 63 | 64 | if (!url.href.startsWith(document.baseURI)) { 65 | // navigate outside app 66 | return; 67 | } 68 | // navigate within app 69 | e.preventDefault(); 70 | this.setPath(url.pathname); 71 | } 72 | } 73 | 74 | export const router = new Router(); 75 | -------------------------------------------------------------------------------- /src/services/coronaTrackerService.js: -------------------------------------------------------------------------------- 1 | import { EventTargetShim } from '../utils/EventTargetShim.js'; 2 | import format from 'date-fns/format'; 3 | import addDays from 'date-fns/addDays'; 4 | 5 | const currentRange = 80; 6 | 7 | class CoronaTrackerService extends EventTargetShim { 8 | constructor() { 9 | super(); 10 | this.cache = {}; 11 | } 12 | 13 | async getCountry(opts) { 14 | let { iso2, date } = opts; 15 | let startDate = opts.startDate; 16 | let endDate = opts.endDate; 17 | iso2 = encodeURI(iso2); 18 | 19 | startDate = startDate ? format(startDate, 'yyyy-MM-dd') : format(addDays(new Date(), -14), 'yyyy-MM-dd'); 20 | endDate = endDate ? format(endDate, 'yyyy-MM-dd') : format(addDays(new Date(), currentRange), 'yyyy-MM-dd'); 21 | 22 | if (!/^[a-zA-Z]{2}$/.test(iso2)) { 23 | return; 24 | } 25 | 26 | const cackeKey = `${iso2}${startDate}${endDate}`; 27 | 28 | if (opts.forceRefresh || this._shouldInvalidate() || this.cache[cackeKey]?.status === 'failed' || !this.cache[cackeKey]) { 29 | try { 30 | this.cache[cackeKey] = {}; 31 | const res = await ( 32 | await fetch( 33 | // `https://api.coronatracker.com/v3/analytics/trend/country?countryCode=${iso2}&startDate=${startDate}&endDate=${endDate}` 34 | `https://api.coronatracker.com/v5/analytics/trend/country?countryCode=${iso2}&startDate=${startDate}&endDate=${endDate}` 35 | // https://api.coronatracker.com/v5/analytics/trend/country 36 | 37 | ) 38 | ).json(); 39 | this.cache[cackeKey] = { 40 | status: 'success', 41 | data: res, 42 | }; 43 | this.__lastUpdate = Date.now(); 44 | } catch { 45 | this.cache[cackeKey] = { 46 | status: 'failed', 47 | }; 48 | } 49 | 50 | this.dispatchEvent(new Event('change')); 51 | } 52 | return this.cache[cackeKey]; 53 | } 54 | } 55 | 56 | export const coronaTrackerService = new CoronaTrackerService(); 57 | -------------------------------------------------------------------------------- /src/services/coronastatusService.js: -------------------------------------------------------------------------------- 1 | import { EventTargetShim } from '../utils/EventTargetShim.js'; 2 | 3 | class CoronaStatusService extends EventTargetShim { 4 | async getCoronaStatus(forceRefresh) { 5 | if (forceRefresh || !this.__coronastatus) { 6 | this.__coronastatus = fetch(new URL('../../data/coronastatus.json', import.meta.url)).then((r) => r.json()); 7 | await this.__coronastatus; 8 | this.dispatchEvent(new Event('change')); 9 | } 10 | return this.__coronastatus; 11 | } 12 | } 13 | 14 | export const coronaStatusService = new CoronaStatusService(); 15 | -------------------------------------------------------------------------------- /src/services/countryDetailService.js: -------------------------------------------------------------------------------- 1 | import { EventTargetShim } from '../utils/EventTargetShim.js'; 2 | import format from 'date-fns/format'; 3 | import addDays from 'date-fns/addDays'; 4 | import constats from './servicesConfiguration'; 5 | 6 | const { apiEndpoint } = constats; 7 | 8 | class CountryDetailService extends EventTargetShim { 9 | constructor() { 10 | super(); 11 | this.cache = {}; 12 | } 13 | 14 | async getDetails(opts) { 15 | let { iso2, date, daysRange } = opts; 16 | let startDate = opts.startDate; 17 | let endDate = opts.endDate; 18 | iso2 = encodeURI(iso2); 19 | 20 | startDate = startDate ? format(startDate, 'yyyy-MM-dd') : format(addDays(new Date(), -14), 'yyyy-MM-dd'); 21 | endDate = endDate ? format(endDate, 'yyyy-MM-dd') : format(addDays(new Date(), daysRange), 'yyyy-MM-dd'); 22 | 23 | if (!/^[a-zA-Z]{2}$/.test(iso2)) { 24 | return; 25 | } 26 | 27 | let cacheKey = `${iso2}${startDate}${endDate}`; 28 | 29 | if (opts.forceRefresh || this._shouldInvalidate() || this.cache[cacheKey]?.status === 'failed' || !this.cache[cacheKey]) { 30 | try { 31 | this.cache[cacheKey] = {}; 32 | const res = await (await fetch(`${apiEndpoint}/status/${iso2}/${startDate}/${endDate}`)).json(); 33 | this.cache[cacheKey] = res; 34 | } catch (_) { 35 | this.cache[cacheKey] = { 36 | status: 'failed', 37 | }; 38 | } 39 | } 40 | var data = this.cache[cacheKey]; 41 | if (data.status === 'failed') { 42 | this.dispatchEvent(new Event('change')); 43 | return data; 44 | } 45 | 46 | const travel = {}; 47 | var dateFormatted = format(date, 'yyyy-MM-dd'); 48 | 49 | var res = data[dateFormatted]; 50 | for (const type of ['land', 'flight', 'sea']) { 51 | for (const { label, value } of res.lockdown[type]) { 52 | if (Array.isArray(travel[label])) { 53 | travel[label].push(value); 54 | } else { 55 | travel[label] = [value]; 56 | } 57 | } 58 | } 59 | 60 | var result = { 61 | status: 'success', 62 | date: res.lockdown.date, 63 | measures: res.lockdown.measure, 64 | travel, 65 | max_gathering: res.lockdown.max_gathering[0].value, 66 | }; 67 | this.__lastUpdate = Date.now(); 68 | this.dispatchEvent(new Event('change')); 69 | return result; 70 | } 71 | } 72 | 73 | export const countryDetailService = new CountryDetailService(); 74 | -------------------------------------------------------------------------------- /src/services/dialogService.js: -------------------------------------------------------------------------------- 1 | import { EventTargetShim } from '../utils/EventTargetShim.js'; 2 | 3 | class DialogService extends EventTargetShim { 4 | close(status) { 5 | this.dispatchEvent(new CustomEvent('close', { detail: status })); 6 | } 7 | } 8 | 9 | export const dialogService = new DialogService(); 10 | -------------------------------------------------------------------------------- /src/services/populationService.js: -------------------------------------------------------------------------------- 1 | import { EventTargetShim } from '../utils/EventTargetShim.js'; 2 | 3 | class PopulationService extends EventTargetShim { 4 | constructor() { 5 | super(); 6 | this.cache = {}; 7 | } 8 | 9 | async getPopulation(forceRefresh) { 10 | if (forceRefresh || !this.__population) { 11 | try { 12 | this.__population = await fetch(new URL('../../data/population.json', import.meta.url)).then((r) => r.json()); 13 | } catch { 14 | return { 15 | status: 'failed', 16 | }; 17 | } 18 | } 19 | this.dispatchEvent(new Event('change')); 20 | return { 21 | status: 'success', 22 | data: this.__population ?? 0, 23 | }; 24 | } 25 | } 26 | 27 | export const populationService = new PopulationService(); 28 | -------------------------------------------------------------------------------- /src/services/services.js: -------------------------------------------------------------------------------- 1 | export { coronaStatusService } from './coronastatusService.js'; 2 | export { coronaTrackerService } from './coronaTrackerService.js'; 3 | export { countryDetailService } from './countryDetailService.js'; 4 | export { populationService } from './populationService.js'; 5 | export { totalsService } from './totalsService.js'; 6 | export { travelAdviceService } from './travelAdviceService.js'; 7 | -------------------------------------------------------------------------------- /src/services/servicesConfiguration.js: -------------------------------------------------------------------------------- 1 | export default { 2 | apiEndpoint: 'https://lockdownsnapshots.azurewebsites.net', 3 | }; 4 | -------------------------------------------------------------------------------- /src/services/totalsService.js: -------------------------------------------------------------------------------- 1 | import { EventTargetShim } from '../utils/EventTargetShim.js'; 2 | import format from 'date-fns/format'; 3 | import addDays from 'date-fns/addDays'; 4 | import constats from './servicesConfiguration'; 5 | 6 | const { apiEndpoint } = constats; 7 | 8 | class TotalsService extends EventTargetShim { 9 | constructor() { 10 | super(); 11 | this.cache = {}; 12 | this.corona = {}; 13 | fetch(new URL('../../data/totals.json', import.meta.url)) 14 | .then((r) => r.json()) 15 | .then((json) => { 16 | this.corona = json.corona; 17 | }); 18 | } 19 | 20 | async getTotals(opts) { 21 | let { date, daysRange } = opts; 22 | let startDate = opts.startDate; 23 | let endDate = opts.endDate; 24 | 25 | startDate = startDate ? format(startDate, 'yyyy-MM-dd') : format(addDays(new Date(), -14), 'yyyy-MM-dd'); 26 | endDate = endDate ? format(endDate, 'yyyy-MM-dd') : format(addDays(new Date(), daysRange), 'yyyy-MM-dd'); 27 | const cacheKey = `${startDate}${endDate}`; 28 | 29 | if (opts.forceRefresh || this.cache[cacheKey]?.status === 'failed' || !this.cache[cacheKey]) { 30 | try { 31 | // this.cache[cacheKey] = {}; 32 | const res = await (await fetch(`${apiEndpoint}/totals/lockdown/${startDate}/${endDate}`)).json(); 33 | this.cache[cacheKey] = res; 34 | } catch (_) { 35 | this.cache[cacheKey] = { 36 | status: 'failed', 37 | }; 38 | } 39 | 40 | var data = this.cache[cacheKey]; 41 | if (data.status === 'failed') { 42 | this.dispatchEvent(new Event('change')); 43 | return data; 44 | } 45 | } 46 | 47 | return { 48 | status: 'success', 49 | corona: this.corona, 50 | territories: this.cache[cacheKey][date], 51 | }; 52 | } 53 | } 54 | 55 | export const totalsService = new TotalsService(); 56 | -------------------------------------------------------------------------------- /src/services/travelAdviceService.js: -------------------------------------------------------------------------------- 1 | import { EventTargetShim } from '../utils/EventTargetShim.js'; 2 | 3 | class TravelAdviceService extends EventTargetShim { 4 | constructor() { 5 | super(); 6 | this.cache = {}; 7 | } 8 | 9 | async getAdvice(opts) { 10 | let { iso2 } = opts; 11 | iso2 = encodeURI(iso2); 12 | 13 | if (!/^[a-zA-Z]{2}$/.test(iso2)) { 14 | return; 15 | } 16 | if (opts.forceRefresh || this.cache[iso2]?.status === 'failed' || !this.cache[iso2]) { 17 | try { 18 | this.cache[iso2] = {}; 19 | const res = await (await fetch(`https://www.travel-advisory.info/api?countrycode=${iso2}`)).json(); 20 | this.cache[iso2] = { 21 | status: 'success', 22 | advice: res.data[iso2].advisory.message, 23 | score: `${res.data[iso2].advisory.score}/5`, 24 | }; 25 | 26 | return this.cache[iso2]; 27 | } catch (_) { 28 | this.cache[iso2] = { 29 | status: 'failed', 30 | }; 31 | } 32 | this.dispatchEvent(new Event('change')); 33 | return this.cache[iso2]; 34 | } 35 | return this.cache[iso2]; 36 | } 37 | } 38 | 39 | export const travelAdviceService = new TravelAdviceService(); 40 | -------------------------------------------------------------------------------- /src/services/updatesService.js: -------------------------------------------------------------------------------- 1 | import { EventTargetShim } from '../utils/EventTargetShim.js'; 2 | 3 | class UpdatesService extends EventTargetShim { 4 | async getUpdates(forceRefresh) { 5 | if (forceRefresh || !this.updates) { 6 | try { 7 | this.updates = await fetch(new URL('../../data/updates.json', import.meta.url)).then((r) => r.json()); 8 | await this.updates; 9 | } catch { 10 | return { 11 | status: 'failed', 12 | }; 13 | } 14 | } 15 | this.dispatchEvent(new Event('change')); 16 | return { 17 | status: 'success', 18 | data: this.updates, 19 | }; 20 | } 21 | } 22 | 23 | export const updatesService = new UpdatesService(); 24 | -------------------------------------------------------------------------------- /src/style/shared.styles.js: -------------------------------------------------------------------------------- 1 | import css from 'csz'; 2 | 3 | export const loadingStyles = css` 4 | & { 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | text-align: center; 9 | flex-direction: column; 10 | height: calc(100% - 60px); 11 | } 12 | 13 | svg { 14 | width: 120px; 15 | height: 120px; 16 | animation: rotate 2000ms linear infinite; 17 | transform-origin: center center; 18 | margin: auto; 19 | } 20 | 21 | circle { 22 | stroke-dasharray: 85, 200; 23 | /* 0px is requires for edge 15 and lower */ 24 | stroke-dashoffset: 0px; 25 | animation: dash 2000ms ease-in-out infinite; 26 | stroke-linecap: round; 27 | stroke-width: var(--spinner-stroke-width, 4px); 28 | stroke-miterlimit: 10; 29 | fill: none; 30 | stroke: #828282; 31 | } 32 | 33 | @keyframes rotate { 34 | 100% { 35 | transform: rotate(360deg); 36 | } 37 | } 38 | 39 | @keyframes dash { 40 | 0% { 41 | stroke-dasharray: 1, 200; 42 | /* 0px is requires for edge 15 and lower */ 43 | stroke-dashoffset: 0px; 44 | } 45 | 50% { 46 | stroke-dasharray: 89, 200; 47 | stroke-dashoffset: -35px; 48 | } 49 | 100% { 50 | stroke-dasharray: 89, 200; 51 | stroke-dashoffset: -124px; 52 | } 53 | } 54 | 55 | /* Animating SVG does not work on IE11. Use a fallback animation. */ 56 | @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { 57 | svg { 58 | animation-duration: 1500ms; 59 | } 60 | 61 | circle { 62 | stroke-linecap: square; 63 | } 64 | } 65 | 66 | @media (prefers-reduced-motion: reduce) { 67 | svg { 68 | animation-duration: 20000ms; 69 | } 70 | 71 | circle { 72 | animation: dash 20000ms ease-in-out infinite; 73 | } 74 | } 75 | `; 76 | 77 | export const offlineStyles = css` 78 | & { 79 | display: flex; 80 | align-items: center; 81 | justify-content: center; 82 | text-align: center; 83 | flex-direction: column; 84 | height: calc(100% - 60px); 85 | } 86 | 87 | svg { 88 | width: 120px; 89 | margin-bottom: 20px; 90 | } 91 | `; 92 | -------------------------------------------------------------------------------- /src/utils/EventTargetShim.js: -------------------------------------------------------------------------------- 1 | export class EventTargetShim { 2 | constructor() { 3 | const delegate = document.createDocumentFragment(); 4 | this.addEventListener = delegate.addEventListener.bind(delegate); 5 | this.dispatchEvent = delegate.dispatchEvent.bind(delegate); 6 | this.removeEventListener = delegate.removeEventListener.bind(delegate); 7 | this.__lastUpdate = Date.now(); 8 | } 9 | 10 | _shouldInvalidate() { 11 | const FIVE_MIN = 5 * 60 * 1000; 12 | return new Date() - this.__lastUpdate > FIVE_MIN; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/addPwaUpdateListener.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Fires a callback when a PWA update is available 3 | * @param {(updateAvailable: Boolean) => void} callback 4 | */ 5 | export function addPwaUpdateListener(callback) { 6 | let newWorker; 7 | 8 | if ('serviceWorker' in navigator) { 9 | navigator.serviceWorker.getRegistration().then((reg) => { 10 | if (reg) { 11 | reg.addEventListener('updatefound', () => { 12 | newWorker = reg.installing; 13 | newWorker.addEventListener('statechange', () => { 14 | if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { 15 | callback(true); 16 | } 17 | }); 18 | }); 19 | 20 | if (reg.waiting && navigator.serviceWorker.controller) { 21 | callback(true); 22 | newWorker = reg.waiting; 23 | } 24 | } 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/setFavIcon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Boolean} darkmode 3 | */ 4 | export function setFavIcon(darkmode) { 5 | const [iconBig, iconSmall] = [...document.querySelectorAll("link[rel='icon']")]; 6 | const manifest = document.querySelector("link[rel='manifest']"); 7 | const theme_color = document.querySelector("meta[name='theme-color']"); 8 | 9 | if (darkmode) { 10 | manifest.href = '/manifest-dark.json'; 11 | iconBig.href = 'src/assets/favicon-32x32-dark.png'; 12 | iconSmall.href = 'src/assets/favicon-16x16-dark.png'; 13 | theme_color.setAttribute('content', '#303136'); 14 | } else { 15 | manifest.href = '/manifest.json'; 16 | iconBig.href = 'src/assets/favicon-32x32.png'; 17 | iconSmall.href = 'src/assets/favicon-16x16.png'; 18 | theme_color.setAttribute('content', '#ffffff'); 19 | } 20 | 21 | document.getElementsByTagName('head')[0].appendChild(manifest); 22 | document.getElementsByTagName('head')[0].appendChild(iconBig); 23 | document.getElementsByTagName('head')[0].appendChild(iconSmall); 24 | document.getElementsByTagName('head')[0].appendChild(theme_color); 25 | } 26 | -------------------------------------------------------------------------------- /src/version.js: -------------------------------------------------------------------------------- 1 | export default 'dev'; 2 | -------------------------------------------------------------------------------- /sw.js: -------------------------------------------------------------------------------- 1 | // TODO: Update the sw implementation to save on cache only the fonts,Google Analitics, Api (World Status) response and the languages 2 | 3 | // import { registerRoute, NavigationRoute } from 'workbox-routing'; 4 | // import { NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies'; 5 | // import { precacheAndRoute } from 'workbox-precaching'; 6 | // import { CacheableResponsePlugin } from 'workbox-cacheable-response'; 7 | // import { ExpirationPlugin } from 'workbox-expiration'; 8 | // import { createHandlerBoundToURL } from 'workbox-precaching'; 9 | 10 | // /* Install new SW when user clicks 'Update app' */ 11 | // self.addEventListener('message', (event) => { 12 | // if (event.data && event.data.type === 'SKIP_WAITING') { 13 | // self.skipWaiting(); 14 | // } 15 | // }); 16 | 17 | // /* Cache the Google Fonts stylesheets with a stale-while-revalidate strategy. */ 18 | // registerRoute( 19 | // /^https:\/\/fonts\.googleapis\.com/, 20 | // new StaleWhileRevalidate({ 21 | // cacheName: 'google-fonts-stylesheets', 22 | // }) 23 | // ); 24 | 25 | // /* Cache the underlying font files with a cache-first strategy for 1 year. */ 26 | // registerRoute( 27 | // /^https:\/\/fonts\.gstatic\.com/, 28 | // new CacheFirst({ 29 | // cacheName: 'google-fonts-webfonts', 30 | // plugins: [ 31 | // new CacheableResponsePlugin({ 32 | // statuses: [0, 200], 33 | // }), 34 | // new ExpirationPlugin({ 35 | // maxAgeSeconds: 60 * 60 * 24 * 365, 36 | // maxEntries: 30, 37 | // }), 38 | // ], 39 | // }) 40 | // ); 41 | 42 | // /** 43 | // * Runtime country json data files with a network-first strategy, we want up to date data, 44 | // * but in the case of no connection, return cached data 45 | // */ 46 | // registerRoute( 47 | // new RegExp('.*data/territories/.*.json'), 48 | // new NetworkFirst({ 49 | // cacheName: 'territories', 50 | // plugins: [ 51 | // new ExpirationPlugin({ 52 | // maxEntries: 5, 53 | // purgeOnQuotaError: true 54 | // }), 55 | // ], 56 | // }) 57 | // ); 58 | 59 | // /** 60 | // * Runtime json data files with a network-first strategy, we want up to date data, 61 | // * but in the case of no connection, return cached data 62 | // */ 63 | // registerRoute( 64 | // new RegExp('.*data/.*.json'), 65 | // new NetworkFirst({ 66 | // cacheName: 'datafiles' 67 | // }) 68 | // ); 69 | 70 | // /** 71 | // * Corona tracker, network first 72 | // * This can start hogging up storage if users click on a lot of 73 | // * countries, so we restrict to 20 max 74 | // */ 75 | // registerRoute( 76 | // new RegExp('https://api.coronatracker.com/v3/.*'), 77 | // new NetworkFirst({ 78 | // cacheName: 'coronatracker', 79 | // plugins: [ 80 | // new ExpirationPlugin({ 81 | // maxEntries: 5, 82 | // purgeOnQuotaError: true 83 | // }), 84 | // ], 85 | // }) 86 | // ); 87 | 88 | // /* Precache manifest */ 89 | // precacheAndRoute(self.__WB_MANIFEST); 90 | 91 | // /* Return index.html on navigations */ 92 | // const handler = createHandlerBoundToURL('/index.html'); 93 | // const navigationRoute = new NavigationRoute(handler, {}); 94 | // registerRoute(navigationRoute); 95 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | { 2 | "resolve": { 3 | "alias": { 4 | "react": "preact-compat", 5 | "react-dom": "preact-compat" 6 | } 7 | } 8 | } --------------------------------------------------------------------------------