├── .github
├── dependabot.yml
└── workflows
│ └── greetings.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── calculator
├── README.md
├── packages
│ └── calc
│ │ └── eval
│ │ ├── eval.js
│ │ └── package.json
└── web
│ ├── index.html
│ └── logo.png
├── chat
├── README.md
├── chatroom_demo-1.0.0-swagger.yaml
├── lib
│ └── validation.js
├── packages
│ ├── chatadmin
│ │ ├── create
│ │ │ ├── .include
│ │ │ └── index.js
│ │ ├── reset
│ │ │ ├── .include
│ │ │ └── index.js
│ │ └── who
│ │ │ ├── .include
│ │ │ └── index.js
│ └── chatroom
│ │ ├── delAllMessages
│ │ ├── .include
│ │ └── index.js
│ │ ├── delAllUsers
│ │ ├── .include
│ │ └── index.js
│ │ ├── delByUserId
│ │ ├── .include
│ │ └── index.js
│ │ ├── delMessage
│ │ ├── .include
│ │ └── index.js
│ │ ├── getMessages
│ │ ├── .include
│ │ └── index.js
│ │ ├── getUserList
│ │ ├── .include
│ │ └── index.js
│ │ ├── newUserId
│ │ ├── .include
│ │ └── index.js
│ │ └── postMessage
│ │ ├── .include
│ │ └── index.js
├── project.yml
└── web
│ ├── .gitignore
│ ├── .include
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
│ └── src
│ ├── actions
│ └── index.js
│ ├── components
│ ├── App.js
│ ├── Form.js
│ ├── Header.js
│ ├── Main.js
│ ├── MessageInput.js
│ ├── Messages.js
│ └── Sidebar.js
│ ├── constants
│ └── index.js
│ ├── images
│ ├── Nimbella-Logo.svg
│ ├── userlist-active.png
│ └── userlist.png
│ ├── index.js
│ ├── reducers
│ ├── index.js
│ ├── messages.js
│ └── userlist.js
│ ├── serviceWorker.js
│ └── stylesheets
│ ├── Form.css
│ ├── Header.css
│ ├── MessageInput.css
│ ├── Messages.css
│ ├── Sidebar.css
│ ├── dashboard.css
│ └── style.css
├── election
├── .env-template
├── .gitignore
├── README.md
├── packages
│ ├── .gitignore
│ └── ge2020
│ │ ├── counties
│ │ ├── .include
│ │ └── index.js
│ │ ├── divisions
│ │ ├── __mocks__
│ │ │ └── axios.js
│ │ ├── __tests__
│ │ │ └── index.spec.js
│ │ ├── index.js
│ │ └── package.json
│ │ ├── elections
│ │ ├── __mocks__
│ │ │ └── axios.js
│ │ ├── __tests__
│ │ │ └── index.spec.js
│ │ ├── index.js
│ │ └── package.json
│ │ ├── exitpolls
│ │ ├── .include
│ │ ├── __tests__
│ │ │ ├── empty.spec.js
│ │ │ ├── exists.spec.js
│ │ │ └── valid.spec.js
│ │ ├── index.js
│ │ └── package.json
│ │ ├── news
│ │ ├── __mocks__
│ │ │ └── rss.js
│ │ ├── __tests__
│ │ │ ├── index.spec.js
│ │ │ ├── rss.spec.js
│ │ │ └── sort.spec.js
│ │ ├── index.js
│ │ └── package.json
│ │ ├── reps
│ │ ├── __mocks__
│ │ │ └── axios.js
│ │ ├── __tests__
│ │ │ └── index.spec.js
│ │ ├── index.js
│ │ └── package.json
│ │ ├── resources
│ │ ├── .include
│ │ └── index.js
│ │ ├── results
│ │ ├── all-states.js
│ │ ├── election-contract.js
│ │ ├── index.js
│ │ ├── package.json
│ │ └── web3.js
│ │ ├── state_counties
│ │ ├── .include
│ │ └── index.js
│ │ ├── timeseries
│ │ ├── .include
│ │ └── index.js
│ │ └── voterinfo
│ │ ├── __mocks__
│ │ └── axios.js
│ │ ├── __tests__
│ │ └── index.spec.js
│ │ ├── index.js
│ │ └── package.json
├── project.yml
├── raw-data
│ ├── 1976-2016-president.csv
│ ├── 1976-2020-president.csv
│ ├── countypres_2000-2016.csv
│ └── exitpoll.csv
├── transformed-data
│ ├── counties.json
│ ├── counties2000.json
│ ├── counties2004.json
│ ├── counties2008.json
│ ├── counties2012.json
│ ├── exitpolls.json
│ ├── resources.json
│ ├── state_county_wise.json
│ ├── timeseries.json
│ └── total.json
├── transformers
│ ├── .gitignore
│ ├── counties.js
│ ├── package.json
│ ├── state_county_wise.js
│ ├── timeseries.js
│ └── total.js
└── web
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── .include
│ ├── LICENSE.md
│ ├── build.sh
│ ├── package.json
│ ├── public
│ ├── Nimbella Election API.postman_collection.json
│ ├── Nimbella Election API.yaml
│ ├── favicon.ico
│ ├── fonts
│ │ └── Archia
│ │ │ ├── archia-bold-webfont.woff2
│ │ │ ├── archia-medium-webfont.woff2
│ │ │ └── archia-semibold-webfont.woff2
│ ├── icon.png
│ ├── index.html
│ ├── manifest.json
│ ├── robots.txt
│ ├── site.webmanifest
│ └── sitemap.xml
│ ├── resources.md
│ ├── rollup.config.js
│ ├── scripts
│ └── project-maps.sh
│ └── src
│ ├── App.js
│ ├── App.scss
│ ├── ProgressBar.scss
│ ├── SplitElectoralVotes.scss
│ ├── animations.js
│ ├── components
│ ├── API.js
│ ├── Actions.js
│ ├── ActionsPanel.js
│ ├── Build.js
│ ├── Cell.js
│ ├── Contest.js
│ ├── CountyRow.js
│ ├── DeltaBarGraph.js
│ ├── Divisions.js
│ ├── Elections.js
│ ├── ExitPolls.js
│ ├── Footer.js
│ ├── HeaderCell.js
│ ├── Home.js
│ ├── Level.js
│ ├── MapExplorer.js
│ ├── MapLegend.js
│ ├── MapSwitcher.js
│ ├── MapVisualizer.js
│ ├── Minigraphs.js
│ ├── Navbar.js
│ ├── ProgressBar.js
│ ├── Representatives.js
│ ├── Resources.js
│ ├── Row.js
│ ├── Search.js
│ ├── SplitElectoralVotes.js
│ ├── State.js
│ ├── StateDropdown.js
│ ├── StateHeader.js
│ ├── StateMeta.js
│ ├── StateMetaCard.js
│ ├── Table.js
│ ├── Timeline.js
│ ├── Timer.js
│ ├── Timeseries.js
│ ├── TimeseriesExplorer.js
│ ├── Tooltip.js
│ ├── Trends.js
│ ├── USAMap.js
│ ├── USAState.js
│ ├── Updates.js
│ ├── Voter.js
│ ├── loaders
│ │ ├── MapVisualizer.js
│ │ ├── Table.js
│ │ └── Timeseries.js
│ └── snippets
│ │ └── TableDeltaHelper.js
│ ├── constants.js
│ ├── data
│ ├── API_Response
│ │ ├── elections.json
│ │ ├── representatives.json
│ │ └── voterInfo.json
│ ├── historical-maps.js
│ ├── historical-maps.json
│ ├── resources.js
│ ├── resources.json
│ ├── usa-map-dimensions.js
│ ├── usa-states-dimensions.json
│ └── usa-states-labels.json
│ ├── hooks
│ ├── useIsVisible.js
│ ├── useResizeObserver.js
│ ├── useScript.js
│ ├── useStickySWR.js
│ ├── useTime.js
│ └── useTimeout.js
│ ├── img
│ ├── 270-marker.png
│ ├── Election2020ContestBanner.png
│ └── Timeline.png
│ ├── index.js
│ ├── placeholder.js
│ ├── serviceWorker.js
│ ├── setupTests.js
│ ├── swBuild.js
│ ├── swTemplate.js
│ ├── tests
│ ├── components
│ │ ├── Level.test.js
│ │ └── Row.test.js
│ ├── mapAndApiStateNames.test.js
│ └── utils
│ │ └── index.js
│ ├── utils
│ ├── commonFunctions.js
│ ├── genericTable.js
│ ├── loader.js
│ └── noResult.js
│ ├── wdyr.js
│ └── workers
│ └── getCounties.js
├── hello-dotnet
├── README.md
├── packages
│ └── default
│ │ └── hello
│ │ ├── .gitignore
│ │ ├── .include
│ │ ├── Hello.cs
│ │ ├── build.cmd
│ │ └── build.sh
└── project.yml
├── hello-typescript
├── .gitignore
├── README.md
├── lib
│ ├── .jestrc.json
│ ├── build.sh
│ ├── package.json
│ ├── src
│ │ └── hello
│ │ │ ├── Hello.ts
│ │ │ └── webpack.config.js
│ ├── test
│ │ └── hello
│ │ │ └── Hello.test.ts
│ └── tsconfig.json
└── packages
│ └── hello-ts
│ └── hello
│ ├── build.sh
│ └── package.json
├── images
├── ocrtutorial-44e74a5a.svg
├── ocrtutorial-5b2cba72.svg
├── ocrtutorial-cb781cec.svg
├── qrcodetutorial-d1d54d1b.svg
└── visitstutorial-cc3f0c43.svg
├── jokes
├── README.md
├── commands.yaml
└── packages
│ └── default
│ └── joke
│ ├── __main__.py
│ ├── build.sh
│ └── requirements.txt
├── mongo-music
├── .gitignore
├── LICENSE
├── README.md
├── packages
│ └── mongo-music
│ │ ├── CreateAlbum
│ │ ├── .include
│ │ ├── MongoMusic.API
│ │ │ ├── CreateAlbum.cs
│ │ │ ├── Models
│ │ │ │ └── Album.cs
│ │ │ └── MongoMusic.API.csproj
│ │ ├── build.cmd
│ │ └── build.sh
│ │ └── GetAlbum
│ │ ├── .include
│ │ ├── MongoMusic.API
│ │ ├── GetAlbum.cs
│ │ ├── Models
│ │ │ └── Album.cs
│ │ └── MongoMusic.API.csproj
│ │ ├── build.cmd
│ │ └── build.sh
└── project.yml
├── numbers-to-words
├── .gitignore
├── README.md
└── packages
│ └── default
│ └── n2w
│ ├── .ignores
│ ├── build.sh
│ ├── composer.json
│ └── index.php
├── ocr
├── .gitignore
├── .slack-env.template
├── README.md
├── commander
│ └── ocr.js
├── packages
│ ├── ocr
│ │ ├── acceptImage
│ │ │ ├── .include
│ │ │ ├── package.json
│ │ │ └── workflow.js
│ │ ├── credential
│ │ │ └── index.js
│ │ ├── imageToText
│ │ │ ├── .include
│ │ │ ├── index.js
│ │ │ └── package.json
│ │ ├── progress
│ │ │ └── index.js
│ │ └── textToSpeech
│ │ │ └── index.js
│ └── utils
│ │ └── slack.js
├── project-slack-notification.yml
├── project.yml
└── web
│ ├── .gitignore
│ ├── .include
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
│ └── src
│ ├── actions
│ └── index.js
│ ├── components
│ ├── App.js
│ ├── FileUpload.js
│ ├── Header.js
│ ├── ImageDisplay.js
│ ├── Result.js
│ └── TextDisplay.js
│ ├── images
│ ├── Nimbella-Logo.svg
│ ├── covert-icon.svg
│ └── upload-icon.svg
│ ├── index.js
│ ├── serviceWorker.js
│ └── stylesheets
│ ├── FileUpload.css
│ ├── Header.css
│ ├── ImageDisplay.css
│ ├── Result.css
│ ├── TextDisplay.css
│ └── style.css
├── printer
├── .gitignore
├── packages
│ ├── admin
│ │ └── flush
│ │ │ └── index.js
│ └── printer
│ │ ├── create
│ │ ├── .include
│ │ ├── __mocks__
│ │ │ └── @nimbella
│ │ │ │ └── sdk.js
│ │ ├── package.json
│ │ ├── src
│ │ │ └── create.js
│ │ └── test
│ │ │ └── create.test.js
│ │ ├── get
│ │ ├── .include
│ │ ├── __mocks__
│ │ │ └── @nimbella
│ │ │ │ └── sdk.js
│ │ ├── package.json
│ │ ├── src
│ │ │ └── get.js
│ │ └── test
│ │ │ └── get.test.js
│ │ ├── list
│ │ ├── .include
│ │ ├── __mocks__
│ │ │ └── @nimbella
│ │ │ │ └── sdk.js
│ │ ├── package.json
│ │ ├── src
│ │ │ └── list.js
│ │ └── test
│ │ │ └── list.test.js
│ │ ├── notify
│ │ └── slack.js
│ │ └── update
│ │ ├── .include
│ │ ├── __mocks__
│ │ └── @nimbella
│ │ │ └── sdk.js
│ │ ├── package.json
│ │ ├── src
│ │ └── update.js
│ │ └── test
│ │ └── update.test.js
├── project.yml
└── web
│ ├── .built
│ ├── .gitignore
│ ├── .include
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
│ └── src
│ ├── actions
│ └── index.js
│ ├── components
│ ├── App.js
│ ├── FileUpload.js
│ ├── Header.js
│ ├── ImageDisplay.js
│ └── Result.js
│ ├── images
│ ├── Nimbella-Logo.svg
│ ├── covert-icon.svg
│ └── upload-icon.svg
│ ├── index.js
│ ├── serviceWorker.js
│ └── stylesheets
│ ├── FileUpload.css
│ ├── Header.css
│ ├── ImageDisplay.css
│ ├── Result.css
│ └── style.css
├── qrcode-multi-lang
├── README.md
├── packages
│ └── default
│ │ ├── qr-java
│ │ ├── .gitignore
│ │ ├── build.cmd
│ │ ├── build.gradle
│ │ ├── build.sh
│ │ ├── pom.xml
│ │ ├── settings.gradle
│ │ └── src
│ │ │ └── main
│ │ │ └── java
│ │ │ └── qr
│ │ │ └── Generate.java
│ │ └── qr
│ │ ├── package.json
│ │ └── qr.js
├── project.yml
└── web
│ ├── index.html
│ └── logo.png
├── qrcode
├── README.md
├── packages
│ └── default
│ │ └── qr
│ │ ├── package.json
│ │ └── qr.js
└── web
│ ├── index.html
│ └── logo.png
├── trade
├── README.md
├── packages
│ └── tradedemo
│ │ ├── buyStock.js
│ │ ├── getCashBalance.js
│ │ ├── getPositions.js
│ │ ├── getStockDescription.js
│ │ ├── getStockHistory.js
│ │ ├── newAccount.js
│ │ └── sellStock.js
├── project.yml
├── trading_demo-1.0.0-swagger.yaml
└── web
│ ├── .gitignore
│ ├── .include
│ ├── package.json
│ ├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
│ └── src
│ ├── action
│ ├── numberFormat.js
│ └── services.js
│ ├── components
│ ├── About.js
│ ├── App.js
│ ├── Charts.js
│ ├── ErrorBoundary.js
│ ├── Header.js
│ ├── Sidebar.js
│ ├── Summary.js
│ ├── Ticker.js
│ └── submit
│ │ ├── Buy.js
│ │ ├── Sell.js
│ │ └── Submit.js
│ ├── css
│ ├── Charts.css
│ ├── Header.css
│ ├── Sidebar.css
│ └── Submit.css
│ ├── images
│ └── Nimbella-Logo.svg
│ ├── index.js
│ ├── serviceWorker.js
│ └── style.css
└── visits
├── README.md
├── packages
└── visits
│ ├── counter.php
│ └── info.php
└── web
├── index.html
└── logo.png
/.github/workflows/greetings.yml:
--------------------------------------------------------------------------------
1 | name: Greetings
2 |
3 | on: [issues]
4 |
5 | jobs:
6 | greeting:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/first-interaction@v1
10 | with:
11 | repo-token: ${{ secrets.GITHUB_TOKEN }}
12 | issue-message: >
13 | Hi there! 👋🏼
14 | As you're new to this repo, we'd like to suggest that you read our [code of conduct](https://github.com/nimbella/demo-projects/blob/master/CODE_OF_CONDUCT.md)
15 | as well as our [contribution guidelines](https://github.com/nimbella/demo-projects/blob/master/CONTRIBUTING.md).
16 | Thanks a bunch for opening your first issue! 🙏
17 | pr-message: >
18 | Congratulations on opening your first pull request! We'll get back to you as soon as possible. In the meantime, please make sure you've updated the documentation to reflect your changes and have added test automation as needed. Thanks! 🙏🏼
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | node_modules
3 | package-lock.json
4 | yarn.lock
5 | __deployer__.zip
6 | .nimbella
7 | .project
8 | .settings
9 | .idea
10 | demo-projects.iml
11 | pnpm-lock.yaml
12 | vendor
13 | composer.lock
14 |
--------------------------------------------------------------------------------
/calculator/README.md:
--------------------------------------------------------------------------------
1 | # Calculator Demo
2 | A simple calculator showing how to use state, nodejs packages, html, and functions.
--------------------------------------------------------------------------------
/calculator/packages/calc/eval/eval.js:
--------------------------------------------------------------------------------
1 | const expeval = require('expression-eval')
2 | const nim = require('@nimbella/sdk')
3 | const key = 'counter'
4 |
5 | function main(args) {
6 | let expr = args['text']
7 | let result = evaluate(expr)
8 | let redis = nim.redis()
9 | return redis.getAsync(key)
10 | .then(reply => { return updateAndReply(redis, asCount(reply), result) })
11 | .catch(err => { return updateAndReply(redis, 0, result) } )
12 | }
13 |
14 | function evaluate(exprStr) {
15 | try {
16 | let ast = expeval.parse(exprStr)
17 | return expeval.eval(ast)
18 | } catch {
19 | return "error evaluating expression"
20 | }
21 | }
22 | function asCount(s) {
23 | if (Number.isInteger(s)) { return s }
24 | let v = parseInt(s, 10)
25 | return isNaN(v) ? 0 : v
26 | }
27 |
28 | function updateAndReply(redis, count, text) {
29 | return redis.setAsync(key, count+1)
30 | .then(reply => { return { count: count, result: text } })
31 | .catch(err => { return { count: count, result: text } })
32 | }
33 |
34 | exports.main = main
35 |
--------------------------------------------------------------------------------
/calculator/packages/calc/eval/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "calc",
3 | "version": "1.0.0",
4 | "description": "Serverless calculator demo",
5 | "main": "eval.js",
6 | "dependencies": {
7 | "expression-eval": "^2.0.0"
8 | },
9 | "devDependencies": {
10 | "@nimbella/sdk": "^1.2.7"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/calculator/web/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/calculator/web/logo.png
--------------------------------------------------------------------------------
/chat/README.md:
--------------------------------------------------------------------------------
1 | # Chatroom
2 | This example demonstrates how to simply create a chat application with system managed state.
3 |
--------------------------------------------------------------------------------
/chat/lib/validation.js:
--------------------------------------------------------------------------------
1 | const validation = {
2 | secretKey: 'This is a secret key',
3 | username: (s) => {
4 | if(!s) {
5 | return { result: false, message: 'Username is required' };
6 | }
7 | if(!/^[0-9a-zA-z_.-]+$/.test(s)) {
8 | return { result: false, message: 'Username contains numbers, English letters, dashes, dot only'};
9 | }
10 | return { result: true };
11 | },
12 | message: (s) => {
13 | if(!s) {
14 | return { result: false, message: 'Message is required' };
15 | }
16 | if(/[<>]/g.test(s)) {
17 | return { result: false, message: 'Message may contains harmful content'}
18 | }
19 | return { result: true }
20 | },
21 | admin: (p) => {
22 | if(!p) {
23 | return { result: false, message: 'Password is required' };
24 | }
25 | if(!/^[0-9a-zA-z_.-]+$/.test(p)) {
26 | return { result: false, message: 'Password contains numbers, English letters, dashes, dot only'};
27 | }
28 | return { result: true }
29 | }
30 | };
31 |
32 | module.exports = validation;
33 |
--------------------------------------------------------------------------------
/chat/packages/chatadmin/create/.include:
--------------------------------------------------------------------------------
1 | index.js
2 | ../../../lib/validation.js
3 |
--------------------------------------------------------------------------------
/chat/packages/chatadmin/reset/.include:
--------------------------------------------------------------------------------
1 | index.js
2 | ../../../lib/validation.js
3 |
--------------------------------------------------------------------------------
/chat/packages/chatadmin/reset/index.js:
--------------------------------------------------------------------------------
1 | const { redis } = require('@nimbella/sdk');
2 | const validation = require('./validation');
3 |
4 | const main = (params) => {
5 | const key = params.key;
6 | const pair = validation.secretKey;
7 | const adminKey = 'chat_demo_admin/';
8 |
9 | if(!key) {
10 | return { returnCode: -1, message: 'Please provide secret key' };
11 | }
12 |
13 | if(key === pair) {
14 | return redis().delAsync(adminKey).then(() => {
15 | return { returnCode: 0, message: 'Admin account has been reset' };
16 | });
17 | } else {
18 | return { returnCode: -1, message: 'The key is not matched' };
19 | }
20 | };
21 |
22 | exports.main = main;
23 |
--------------------------------------------------------------------------------
/chat/packages/chatadmin/who/.include:
--------------------------------------------------------------------------------
1 | index.js
2 | ../../../lib/validation.js
3 |
--------------------------------------------------------------------------------
/chat/packages/chatadmin/who/index.js:
--------------------------------------------------------------------------------
1 | const { redis } = require('@nimbella/sdk');
2 |
3 | const main = () => {
4 | const adminKey = 'chat_demo_admin/';
5 |
6 | return redis().getAsync(adminKey).then(res => {
7 | if(res) {
8 | const admin = JSON.parse(res).username;
9 | return { returnCode: 0, admin };
10 | }
11 | return { returnCode: -1, message: "Admin is not set yet." }
12 | })
13 | };
14 |
15 | exports.main = main;
16 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/delAllMessages/.include:
--------------------------------------------------------------------------------
1 | index.js
2 | ../../../lib/validation.js
3 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/delAllUsers/.include:
--------------------------------------------------------------------------------
1 | index.js
2 | ../../../lib/validation.js
3 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/delByUserId/.include:
--------------------------------------------------------------------------------
1 | index.js
2 | ../../../lib/validation.js
3 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/delByUserId/index.js:
--------------------------------------------------------------------------------
1 | const { redis } = require('@nimbella/sdk');
2 | const validation = require('./validation');
3 |
4 | const handleDel = (username) => {
5 | const userListKey = 'chat_demo_user_list/';
6 | return redis().sismemberAsync(userListKey, username).then(userExists => {
7 | if(userExists) {
8 | return redis().sremAsync(userListKey, username).then(() => {
9 | return { "returnCode": 0, "username": username };
10 | })
11 | }
12 | return { "returnCode": -1, "message": "username is not existed" };
13 | })
14 | };
15 |
16 | const handleValidate = async (username) => {
17 | const usernameValidate = validation.username(username);
18 |
19 | if(!usernameValidate.result) {
20 | return { returnCode: -1, message: usernameValidate.message }
21 | }
22 |
23 | return { returnCode: 0 }
24 | };
25 |
26 | const main = (params) => {
27 | const username = params.username;
28 |
29 | return handleValidate(username).then(res => {
30 | if(res.returnCode === 0) {
31 | return handleDel(username).then(result => {
32 | return result
33 | });
34 | } else {
35 | return res;
36 | }
37 | });
38 | };
39 |
40 | exports.main = main;
41 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/delMessage/.include:
--------------------------------------------------------------------------------
1 | index.js
2 | ../../../lib/validation.js
3 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/delMessage/index.js:
--------------------------------------------------------------------------------
1 | const { redis } = require('@nimbella/sdk');
2 |
3 | const main = (params) => {
4 | const messagePoolKey = 'chat_demo_message_pool';
5 | return redis().lrangeAsync(messagePoolKey, 0, -1).then((list) => {
6 | return redis().lremAsync('chat_demo_message_pool', 1, JSON.stringify(params.item)).then(res => {
7 | if(res === 0) {
8 | return { returnCode: -1, message: "Message data is mismatched" }
9 | }
10 | return { returnCode: 0, timestamp: params.item.timestamp }
11 | });
12 | });
13 | };
14 |
15 | exports.main = main;
16 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/getMessages/.include:
--------------------------------------------------------------------------------
1 | index.js
2 | ../../../lib/validation.js
3 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/getMessages/index.js:
--------------------------------------------------------------------------------
1 | const { redis } = require('@nimbella/sdk');
2 |
3 | const main = (params) => {
4 | const messagePoolKey = 'chat_demo_message_pool';
5 |
6 | return redis().llenAsync(messagePoolKey).then(total => {
7 | let limit = 10;
8 | let page = (params.page) ? parseInt(params.page) : 1;
9 | let maxPage = Math.round(total/limit);
10 | let from = (maxPage > page) ? (total - limit * page) : 0;
11 | let to = (page === 1) ? total : (total - ((page - 1) * limit) - 1);
12 |
13 | if(!params.page) { // get all message list if params is not available
14 | from = 0;
15 | to = -1;
16 | maxPage = 1;
17 | page = 1;
18 | }
19 |
20 | if(page <= maxPage && page > 0) {
21 | return redis().lrangeAsync(messagePoolKey, from, to).then(res => {
22 | const messages = res.map(i => {
23 | return JSON.parse(i);
24 | });
25 | return { messages, returnCode: 0, total };
26 | })
27 | } else {
28 | return { returnCode: -1, message: 'page over the range limit', maxPage, page }
29 | }
30 |
31 | });
32 |
33 | };
34 |
35 | exports.main = main;
36 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/getUserList/.include:
--------------------------------------------------------------------------------
1 | index.js
2 | ../../../lib/validation.js
3 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/getUserList/index.js:
--------------------------------------------------------------------------------
1 | const { redis } = require('@nimbella/sdk');
2 |
3 | const main = () => {
4 | const userListKey = 'chat_demo_user_list/';
5 |
6 | return redis().smembersAsync(userListKey).then(userlist => {
7 | return { returnCode: 0, userlist };
8 | })
9 | };
10 |
11 | exports.main = main;
12 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/newUserId/.include:
--------------------------------------------------------------------------------
1 | index.js
2 | ../../../lib/validation.js
3 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/newUserId/index.js:
--------------------------------------------------------------------------------
1 | const { redis } = require('@nimbella/sdk');
2 | const validation = require('./validation');
3 |
4 | const handleAddUser = (username) => {
5 | const userListKey = 'chat_demo_user_list/';
6 |
7 | return redis().sismemberAsync(userListKey, username).then(userExists => {
8 | if(userExists) {
9 | return { "returnCode": -1, "message": 'username has benn taken' };
10 | }
11 | return redis().saddAsync('chat_demo_user_list/', username).then(() => {
12 | return { "returnCode": 0, "username": username };
13 | })
14 | })
15 | };
16 |
17 | const handleValidate = async (username) => {
18 | const usernameValidate = validation.username(username);
19 |
20 | if(!usernameValidate.result) {
21 | return { returnCode: -1, message: usernameValidate.message }
22 | }
23 |
24 | return { returnCode: 0 }
25 | };
26 |
27 | const main = (params) => {
28 | const username = params.username;
29 |
30 | return handleValidate(username).then(res => {
31 | if(res.returnCode === 0) {
32 | return handleAddUser(username).then(status => {
33 | return status
34 | });
35 | } else {
36 | return res;
37 | }
38 | });
39 | };
40 |
41 |
42 | exports.main = main;
43 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/postMessage/.include:
--------------------------------------------------------------------------------
1 | index.js
2 | ../../../lib/validation.js
3 |
--------------------------------------------------------------------------------
/chat/packages/chatroom/postMessage/index.js:
--------------------------------------------------------------------------------
1 | const { redis } = require('@nimbella/sdk');
2 | const validation = require('./validation');
3 |
4 | const handleAddMessage = (username, message, timestamp) => {
5 | const messagePoolKey = 'chat_demo_message_pool';
6 | const userListKey = 'chat_demo_user_list/';
7 |
8 | return redis().sismemberAsync(userListKey, username).then(userExists => {
9 | if(userExists) {
10 | return redis().rpushAsync(messagePoolKey, JSON.stringify({ message, timestamp, username })).then(() => {
11 | return { "returnCode": 0, username, message, timestamp };
12 | })
13 | } else {
14 | return { "returnCode": -1, message: 'Username is not existed' };
15 | }
16 | })
17 | };
18 |
19 | const handleValidate = async (username, message) => {
20 | const usernameValidate = validation.username(username);
21 | const messageValidate = validation.message(message);
22 |
23 | if(!usernameValidate.result) {
24 | return { returnCode: -1, message: usernameValidate.message }
25 | }
26 |
27 | if(!messageValidate.result) {
28 | return { returnCode: -1, message: messageValidate.message }
29 | }
30 | return { returnCode: 0 }
31 | };
32 |
33 | const main = (params) => {
34 | const username = params.username;
35 | const message = params.message;
36 | const timestamp = new Date();
37 |
38 | return handleValidate(username, message).then(res => {
39 | if(res.returnCode === 0) {
40 | return handleAddMessage(username, message, timestamp).then(result => {
41 | return result;
42 | });
43 | } else {
44 | return res;
45 | }
46 | });
47 | };
48 |
49 | exports.main = main;
50 |
--------------------------------------------------------------------------------
/chat/project.yml:
--------------------------------------------------------------------------------
1 | bucket:
2 | strip: 1
3 |
--------------------------------------------------------------------------------
/chat/web/.gitignore:
--------------------------------------------------------------------------------
1 | /*.js
2 | /*.css
3 | /*.html
4 | /*.ico
5 | /static/
6 | /build/
7 | /*manifest.json
--------------------------------------------------------------------------------
/chat/web/.include:
--------------------------------------------------------------------------------
1 | build
2 |
--------------------------------------------------------------------------------
/chat/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chatdemo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "bootstrap": "^3.4.1",
7 | "jquery": "^3.3.1",
8 | "prop-types": "^15.7.2",
9 | "react": "^16.8.3",
10 | "react-dom": "^16.8.3",
11 | "react-emojione": "^5.0.1",
12 | "react-redux": "^6.0.1",
13 | "react-scripts": "^3.0.0",
14 | "redux": "^4.0.1",
15 | "redux-thunk": "^2.3.0",
16 | "typescript": "^3.3.3333"
17 | },
18 | "scripts": {
19 | "start": "PORT=3001 react-scripts start",
20 | "build": "react-scripts build",
21 | "test": "react-scripts test",
22 | "eject": "react-scripts eject"
23 | },
24 | "eslintConfig": {
25 | "extends": "react-app"
26 | },
27 | "browserslist": [
28 | ">0.2%",
29 | "not dead",
30 | "not ie <= 11",
31 | "not op_mini all"
32 | ],
33 | "devDependencies": {
34 | "redux-devtools-extension": "^2.13.8",
35 | "redux-logger": "^3.0.6"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/chat/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/chat/web/public/favicon.ico
--------------------------------------------------------------------------------
/chat/web/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
25 | Nimbella Chatroom
26 |
27 |
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/chat/web/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": ">Nimbella Chatroom",
3 | "name": ">Nimbella Chatroom Application",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#122A2E",
14 | "background_color": "#222222"
15 | }
16 |
--------------------------------------------------------------------------------
/chat/web/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 | import { connect } from "react-redux";
3 |
4 | import Header from './Header';
5 | import Sidebar from './Sidebar';
6 | import Main from './Main';
7 | import Form from './Form';
8 |
9 | class App extends Component {
10 | constructor(props) {
11 | super(props);
12 |
13 | this.state = {
14 | showForm: false,
15 | showSidebar: false
16 | }
17 | }
18 |
19 | componentDidMount() {
20 | const username = localStorage.getItem('username');
21 | if(!username) {
22 | this.setState({
23 | showForm: true
24 | })
25 | }
26 | }
27 |
28 | componentWillReceiveProps(nextProps, nextContext) {
29 | const username = localStorage.getItem('username');
30 | if(nextProps.userlist.filter(curr => curr.username === username).length) {
31 | this.setState({ showForm: false });
32 | } else {
33 | this.setState({ showForm: true });
34 | }
35 | };
36 |
37 | handleCloseForm = () => {
38 | this.setState({
39 | showForm: false
40 | })
41 | };
42 |
43 | handleSidebar = () => {
44 | this.setState({ showSidebar: !this.state.showSidebar });
45 | };
46 |
47 | render() {
48 | const { showForm, showSidebar } = this.state;
49 | return (
50 |
51 | { showForm && }
52 |
53 |
59 |
60 | );
61 | }
62 | }
63 |
64 | const mapStateToProps = (state) => {
65 | return {
66 | userlist: state.userlist
67 | }
68 | };
69 |
70 | export default connect(mapStateToProps, null)(App);
71 |
--------------------------------------------------------------------------------
/chat/web/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import logo from '../images/Nimbella-Logo.svg';
3 | import '../stylesheets/Header.css';
4 | import PropTypes from 'prop-types';
5 | import { connect } from "react-redux";
6 |
7 | class Header extends Component {
8 | handleSidebar = (e) => {
9 | e.preventDefault();
10 | this.props.fn();
11 | };
12 |
13 | render() {
14 | const { show, userlist } = this.props;
15 | const userNum = userlist.length;
16 | return (
17 |
26 | );
27 | }
28 | }
29 |
30 | Header.propTypes = {
31 | fn: PropTypes.func.isRequired,
32 | show: PropTypes.bool.isRequired
33 | };
34 |
35 | const mapStateToProps = (state) => {
36 | return{
37 | userlist: state.userlist,
38 | }
39 | };
40 |
41 | export default connect(mapStateToProps, {})(Header);
42 |
--------------------------------------------------------------------------------
/chat/web/src/components/Main.js:
--------------------------------------------------------------------------------
1 | import React, { Component, Fragment } from 'react';
2 |
3 | import Messages from './Messages';
4 | import MessageInput from './MessageInput';
5 |
6 | class Main extends Component {
7 | render() {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 | }
17 | }
18 |
19 | export default Main;
--------------------------------------------------------------------------------
/chat/web/src/components/MessageInput.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { connect } from "react-redux";
4 | import { submitMessage } from "../actions";
5 |
6 | import '../stylesheets/MessageInput.css';
7 |
8 | class Message extends Component {
9 | constructor(props) {
10 | super(props);
11 |
12 | this.state = {
13 | isLoading: false
14 | }
15 | };
16 |
17 | handleLoading = () => {
18 | this.setState({ isLoading: true });
19 | };
20 |
21 | handleUnloading = () => {
22 | this.setState({ isLoading: false });
23 | };
24 |
25 | handleEnter = (e) => {
26 | let message = this.refs.message.value;
27 | let username = localStorage.getItem('username');
28 | if(e.key === 'Enter' && message) {
29 | this.handleLoading();
30 | this.props.submitMessage({ message, username })
31 | .then(res => {
32 | if(res.returnCode === 0) {
33 | this.handleUnloading();
34 | this.refs.message.focus();
35 | this.refs.message.value = '';
36 | }
37 | })
38 | .catch(err => console.log(err));
39 | }
40 | };
41 |
42 | render() {
43 | const { isLoading } = this.state;
44 | return (
45 |
50 | );
51 | }
52 | }
53 |
54 | Message.propTypes = {
55 | submitMessage: PropTypes.func.isRequired
56 |
57 | };
58 |
59 | export default connect(null, { submitMessage })(Message);
--------------------------------------------------------------------------------
/chat/web/src/constants/index.js:
--------------------------------------------------------------------------------
1 | export const SET_USERNAME = 'SET_USERNAME';
2 | export const GET_USERLIST = 'GET_USERLIST';
3 | export const DEL_USERNAME = 'DEL_USERNAME';
4 |
5 | export const POST_MESSAGE = 'POST_MESSAGE';
6 | export const GET_MESSAGES = 'GET_MESSAGES';
7 | export const DEL_MESSAGE = 'DEL_MESSAGE';
8 |
9 | export const GET_LOOPUSERLIST = 'GET_LOOPUSERLIST';
10 | export const GET_LOOPMESSAGES = 'GET_LOOPMESSAGES';
11 |
--------------------------------------------------------------------------------
/chat/web/src/images/userlist-active.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/chat/web/src/images/userlist-active.png
--------------------------------------------------------------------------------
/chat/web/src/images/userlist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/chat/web/src/images/userlist.png
--------------------------------------------------------------------------------
/chat/web/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './components/App';
4 | import * as serviceWorker from './serviceWorker';
5 |
6 | import { Provider } from 'react-redux';
7 | import { createStore, applyMiddleware } from "redux";
8 | import rootReducer from './reducers';
9 | import { composeWithDevTools } from "redux-devtools-extension";
10 | import { createLogger } from 'redux-logger';
11 | import thunk from 'redux-thunk';
12 |
13 | import { GET_LOOPUSERLIST, GET_LOOPMESSAGES } from "./constants";
14 |
15 | import 'bootstrap/dist/css/bootstrap.min.css';
16 | import './stylesheets/dashboard.css';
17 | import './stylesheets/style.css';
18 |
19 | const logger = createLogger({
20 | predicate: (getState, action) => action.type !== GET_LOOPMESSAGES && action.type !== GET_LOOPUSERLIST
21 | });
22 |
23 | const store = createStore(
24 | rootReducer,
25 | composeWithDevTools(
26 | applyMiddleware(thunk, logger)
27 | )
28 | );
29 |
30 | ReactDOM.render(, document.getElementById('root'));
31 |
32 | // If you want your app to work offline and load faster, you can change
33 | // unregister() to register() below. Note this comes with some pitfalls.
34 | // Learn more about service workers: http://bit.ly/CRA-PWA
35 | serviceWorker.unregister();
36 |
--------------------------------------------------------------------------------
/chat/web/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import userlist from './userlist';
3 | import messages from './messages'
4 |
5 | export default combineReducers({
6 | userlist,
7 | messages
8 | });
--------------------------------------------------------------------------------
/chat/web/src/reducers/messages.js:
--------------------------------------------------------------------------------
1 | import { POST_MESSAGE, GET_MESSAGES, GET_LOOPMESSAGES, DEL_MESSAGE } from "../constants";
2 |
3 | const messages = (state = [], action = {}) => {
4 | switch(action.type) {
5 | case POST_MESSAGE:
6 | return [
7 | ...state,
8 | action.messageData
9 | ];
10 | case DEL_MESSAGE:
11 | return state.filter((curr, i , arr) => {
12 | return curr.timestamp !== action.timestamp;
13 | });
14 | case GET_MESSAGES:
15 | return action.messages;
16 | default: return state;
17 | case GET_LOOPMESSAGES:
18 | return action.messages;
19 | }
20 | };
21 |
22 | export default messages;
--------------------------------------------------------------------------------
/chat/web/src/reducers/userlist.js:
--------------------------------------------------------------------------------
1 | import { SET_USERNAME, GET_USERLIST, GET_LOOPUSERLIST, DEL_USERNAME } from "../constants";
2 |
3 | const userlist = (state = [], action = {}) => {
4 | switch(action.type) {
5 | case SET_USERNAME:
6 | return [
7 | ...state,
8 | action.username
9 | ];
10 | case DEL_USERNAME:
11 | return state.filter((curr, i , arr) => {
12 | return curr.username !== action.username;
13 | });
14 | case GET_USERLIST:
15 | return action.userlist;
16 | default: return state;
17 | case GET_LOOPUSERLIST:
18 | return action.userlist;
19 | }
20 | };
21 |
22 | export default userlist;
--------------------------------------------------------------------------------
/chat/web/src/stylesheets/Form.css:
--------------------------------------------------------------------------------
1 | .mask{ display: inline-block; width: 100%; height: 100%; background-color: rgba(4,28,33,.8); position: fixed; top: 0; left: 0; z-index: 9999; }
2 |
3 | .mask .header h3{ display: inline;}
4 |
5 | .mask .form-wrap{ background-color: white; margin-top: 5%; padding: 2% 3%; border-radius: 5px; }
6 |
7 | .form-group.one-line span { display: inline-block; }
8 |
9 | .form-group.has-error div:first-child{ border-color: #a94442; }
--------------------------------------------------------------------------------
/chat/web/src/stylesheets/Header.css:
--------------------------------------------------------------------------------
1 | .navbar .logo-text{ display: inline-block; margin:0; padding: 20px 0; min-height:50px; }
2 | .navbar .navbar-brand{ padding: 0; height: 55px; }
3 | .navbar .navbar-brand:hover{ color: #55BFD1; }
4 | .navbar-nav{ list-style: none; float: right; min-height: 50px; padding: 20px; margin-right: 15px; }
5 | .navbar-nav .nav-item{ margin-left: 40px; }
6 | .navbar .list-icon{ position: relative; display: line-block; float: right;margin-top: 15px; }
7 | .navbar .list-icon::before{ content: ''; position: absolute; right: 15px; top: -12px; background: transparent url(../images/userlist.png) center center no-repeat; background-size: 50px; display: inline-block; width: 50px; height: 50px; }
8 | .navbar .list-icon:hover, .navbar .list-icon.active{ color: #55BFD1; }
9 | .navbar .list-icon:hover::before, .navbar .list-icon.active::before{ background-image: url(../images/userlist-active.png); }
10 |
11 |
12 | @media only screen and (max-width: 736px) {
13 | .navbar-nav{ padding: 12px 12px 0 0; margin: 0; float: right !important; }
14 | }
15 |
16 | @media only screen and (max-width: 414px) {
17 | .navbar-brand{ padding: 15px 5px; }
18 | .navbar-collapse{ float: left; }
19 | .navbar-collapse .nav-item{ margin-left: 10px; }
20 | }
--------------------------------------------------------------------------------
/chat/web/src/stylesheets/MessageInput.css:
--------------------------------------------------------------------------------
1 | .input-wrap{ position: fixed !important; bottom: 0; background-color: white; padding: 20px; border-top: 1px solid #efefef; }
2 | .input-wrap .form-group{ margin: 0; }
--------------------------------------------------------------------------------
/chat/web/src/stylesheets/Messages.css:
--------------------------------------------------------------------------------
1 | .messages-wrap { list-style: none; padding-left: 0; padding-bottom: 60px; overflow-y: auto; height: 1000px; }
2 | .messages-wrap .message{ margin: 10px auto; }
3 |
4 | .messages-wrap .message .init{ display: inline-block; margin-right: 10px; vertical-align: top; margin-top: 15px; }
5 | .messages-wrap .message .info{ display: inline-block; width: 60%; }
6 | .messages-wrap .message .info span:first-of-type{ margin-right: 10px; }
7 | .messages-wrap .message .info .message-box{ display: inline-block; background-color: #E2F6F9; border: 1px solid #CDE2E5; padding: 10px; width: 100%; border-radius: 5px; word-wrap: break-word; word-break: break-word; }
8 |
9 | .messages-wrap .message.me .info{ margin-left: 90px; position: relative; }
10 | .messages-wrap .message.me .info .del-btn{ position: absolute; right: -25px; top: 50%; cursor: pointer; transition: opacity .3s ease-in-out; opacity: 0; }
11 | .messages-wrap .message.me .info:hover .del-btn{ opacity: 0.5; }
12 | .messages-wrap .message.me .info .message-box{ background-color: #E4F9E2; border-color: #D1E5CF; }
--------------------------------------------------------------------------------
/chat/web/src/stylesheets/Sidebar.css:
--------------------------------------------------------------------------------
1 | .sidebar-wrap .sidebar{ display: block; background-color: #122A2E; }
2 | .sidebar-wrap .nav-sidebar{ padding-top: 10px; padding-bottom: 20px; }
3 | .sidebar-wrap .nav-sidebar > li.nav-item{ padding-left: 20px; padding-right: 20px; margin:10px 0; }
4 | .sidebar-wrap .nav-sidebar li.nav-item span:nth-of-type(2) { margin-left: 10px; }
5 | .sidebar-wrap .nav-sidebar li.nav-item.active span:nth-of-type(2) { color: #55BFD1; }
6 | .sidebar-wrap .person{ position: fixed; bottom: 0; left: 0; text-align: center; }
7 | .sidebar-wrap .person span{ background-color: #122A2E; width: 100%; display: inline-block; padding-top: 20px; padding-bottom: 20px; }
8 | .sidebar-wrap .del-btn{ margin-top: 12px; cursor: pointer; opacity: 0; transition: opacity .3s ease-in-out; }
9 | .sidebar-wrap .nav-item:hover .del-btn{ opacity: 1; }
10 |
11 | @media only screen and (max-width: 768px) {
12 | .sidebar-wrap{ padding-top: 0; padding-bottom: 0; }
13 | .sidebar-wrap .sidebar { background-color: #122A2E; }
14 | }
15 |
16 | @media only screen and (max-width: 767px) {
17 | .sidebar-wrap{ position: fixed; z-index: 999; background-color: rgba(18,42,46, .9); padding-top: 20px; padding-bottom: 20px; }
18 | .sidebar-wrap .sidebar{ overflow-y: auto; overflow-x: hidden; background-color: transparent; }
19 | .sidebar-wrap .nav-sidebar{ padding-bottom: 0; }
20 | .sidebar-wrap .nav-sidebar > li.nav-item{ padding-left: 20px; padding-right: 0px; margin:5px 0; }
21 | }
--------------------------------------------------------------------------------
/election/.env-template:
--------------------------------------------------------------------------------
1 | GOOGLE_CIVIC_API_TOKEN=
2 | INFURA_MAINNET_HTTP=
3 |
--------------------------------------------------------------------------------
/election/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | .nimbella
3 | coverage
4 | web/.built
5 |
--------------------------------------------------------------------------------
/election/packages/.gitignore:
--------------------------------------------------------------------------------
1 | attic/
2 | __pycache__/
3 | virtualenv/
4 | *.zip
5 | *.pyc
6 | *.done
7 |
--------------------------------------------------------------------------------
/election/packages/ge2020/counties/.include:
--------------------------------------------------------------------------------
1 | ../../../transformed-data/counties.json
2 | ../../../transformed-data/counties2000.json
3 | ../../../transformed-data/counties2004.json
4 | ../../../transformed-data/counties2008.json
5 | ../../../transformed-data/counties2012.json
6 | index.js
7 |
--------------------------------------------------------------------------------
/election/packages/ge2020/counties/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | function main(args) {
4 |
5 | const year = args.year || '';
6 | const contents = fs.readFileSync(__dirname + `/counties${year}.json`, 'UTF-8');
7 |
8 | return { body: JSON.parse(contents) };
9 |
10 | }
11 |
12 | exports.main = main;
13 |
--------------------------------------------------------------------------------
/election/packages/ge2020/divisions/__mocks__/axios.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | get: jest.fn(() => Promise.resolve({ data: {} }))
3 | };
4 |
--------------------------------------------------------------------------------
/election/packages/ge2020/divisions/__tests__/index.spec.js:
--------------------------------------------------------------------------------
1 | const axios = require("../__mocks__/axios")
2 | const { main } = require('../index')
3 |
4 | it("fetches divisions data", async () => {
5 | // arrange
6 | const response =
7 | {
8 | "kind": "civicinfo#divisionSearchResponse",
9 | "results": [
10 | {
11 | "ocdId": "testOcdId",
12 | "name": "testName",
13 | "aliases": [
14 | "testAlias"
15 | ]
16 | }
17 | ]
18 | }
19 |
20 |
21 | axios.get.mockImplementationOnce(() =>
22 | Promise.resolve({ data: response }),
23 | );
24 |
25 | // act
26 | const data = await main({})
27 |
28 | // assert
29 | expect(data).toStrictEqual({ body: response })
30 | expect(axios.get).toHaveBeenCalledTimes(1)
31 | });
32 |
33 |
34 | it('returns error message on failure', async () => {
35 | // arrange
36 | const errorMessage = 'Network Error';
37 | const message = { "body": errorMessage }
38 |
39 | axios.get.mockImplementationOnce(() =>
40 | Promise.reject(new Error(errorMessage)),
41 | );
42 |
43 | // act
44 | const data = await main({})
45 |
46 | // assert
47 | expect(data).toStrictEqual(message)
48 | });
49 |
--------------------------------------------------------------------------------
/election/packages/ge2020/divisions/index.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 | const google_api_token = process.env.GOOGLE_CIVIC_API_TOKEN
3 |
4 | async function main(args) {
5 | const { query = '' } = args
6 | const path = `/civicinfo/v2/divisions?query=${encodeURI(query)}&key=${google_api_token}`
7 |
8 | return axios
9 | .get(`https://www.googleapis.com/${path}`)
10 | .then(res => ({ body: res.data }))
11 | .catch(error => ({ body: error.message }))
12 | }
13 |
14 | exports.main = main
15 |
--------------------------------------------------------------------------------
/election/packages/ge2020/divisions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "divisions",
3 | "version": "1.0.0",
4 | "description": "List political divisions by their natural name or OCD ID",
5 | "main": "index.js",
6 | "devDependencies": {
7 | "jest": "^26.5.3"
8 | },
9 | "scripts": {
10 | "test": "jest"
11 | },
12 | "jest": {
13 | "collectCoverage": true,
14 | "coverageReporters": [
15 | "html"
16 | ]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/election/packages/ge2020/elections/__mocks__/axios.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | get: jest.fn(() => Promise.resolve({ data: {} }))
3 | };
4 |
--------------------------------------------------------------------------------
/election/packages/ge2020/elections/__tests__/index.spec.js:
--------------------------------------------------------------------------------
1 | const axios = require("../__mocks__/axios")
2 | const { main } = require('../index')
3 |
4 | it("fetches election data", async () => {
5 | // arrange
6 | const response = {
7 | "elections": [
8 | {
9 | "id": "2000",
10 | "name": "VIP Test Election",
11 | "electionDay": "2021-06-06",
12 | "ocdDivisionId": "ocd-division/country:us"
13 | },
14 | {
15 | "id": "4955",
16 | "name": "West Virginia Presidential and State Primary Election",
17 | "electionDay": "2020-06-09",
18 | "ocdDivisionId": "ocd-division/country:us/state:wv"
19 | },
20 | {
21 | "id": "4958",
22 | "name": "Georgia Presidential and State Primary Election",
23 | "electionDay": "2020-06-09",
24 | "ocdDivisionId": "ocd-division/country:us/state:ga"
25 | }]
26 | }
27 |
28 | axios.get.mockImplementationOnce(() =>
29 | Promise.resolve({ data: response }),
30 | );
31 |
32 | // act
33 | const data = await main()
34 |
35 | // assert
36 | expect(data).toStrictEqual({ body: response })
37 | expect(axios.get).toHaveBeenCalledTimes(1)
38 | });
39 |
40 |
41 | it('returns error message on failure', async () => {
42 | // arrange
43 | const errorMessage = 'Network Error';
44 | const message = { "body": errorMessage }
45 |
46 | axios.get.mockImplementationOnce(() =>
47 | Promise.reject(new Error(errorMessage)),
48 | );
49 |
50 | // act
51 | const data = await main()
52 |
53 | // assert
54 | expect(data).toStrictEqual(message)
55 | });
56 |
--------------------------------------------------------------------------------
/election/packages/ge2020/elections/index.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 | const google_api_token = process.env.GOOGLE_CIVIC_API_TOKEN
3 |
4 | const path = `https://www.googleapis.com/civicinfo/v2/elections?key=${google_api_token}`
5 |
6 | async function main(args) {
7 | return axios
8 | .get(path)
9 | .then(res => ({ body: res.data }))
10 | .catch(error => ({ body: error.message }))
11 | }
12 |
13 | module.exports = { main }
14 |
--------------------------------------------------------------------------------
/election/packages/ge2020/elections/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elections",
3 | "version": "1.0.0",
4 | "description": "Upcoming elections across USA",
5 | "main": "index.js",
6 | "devDependencies": {
7 | "jest": "^26.5.3"
8 | },
9 | "scripts": {
10 | "test": "jest"
11 | },
12 | "jest": {
13 | "collectCoverage": true,
14 | "coverageReporters": [
15 | "html"
16 | ]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/election/packages/ge2020/exitpolls/.include:
--------------------------------------------------------------------------------
1 | ../../../transformed-data/exitpolls.json
2 | index.js
3 |
--------------------------------------------------------------------------------
/election/packages/ge2020/exitpolls/__tests__/empty.spec.js:
--------------------------------------------------------------------------------
1 | const { main } = require('../index')
2 |
3 | jest.resetModules()
4 | jest.mock('fs', () => ({
5 | readFileSync: jest.fn(() => '{}'),
6 | }));
7 |
8 | describe('main method call', () => {
9 | test('should return empty json response', () => {
10 | // act
11 | const output = main()
12 |
13 | // assert
14 | expect(output).toStrictEqual({ body: {} });
15 | })
16 | })
17 |
--------------------------------------------------------------------------------
/election/packages/ge2020/exitpolls/__tests__/exists.spec.js:
--------------------------------------------------------------------------------
1 |
2 | const fs = require('fs')
3 |
4 | test('should check existence of data file', () => {
5 | expect(fs.existsSync('../../../transformed-data/exitpolls.json')).toBe(true)
6 | });
7 |
--------------------------------------------------------------------------------
/election/packages/ge2020/exitpolls/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const contents = fs.readFileSync(__dirname + '/exitpolls.json', 'UTF-8');
4 | const body = JSON.parse(contents);
5 | function main(args) {
6 | return { body };
7 | }
8 |
9 | module.exports = { main }
10 |
--------------------------------------------------------------------------------
/election/packages/ge2020/exitpolls/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "test": "jest"
4 | },
5 | "devDependencies": {
6 | "jest": "^26.5.3"
7 | },
8 | "jest": {
9 | "testEnvironment": "node"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/election/packages/ge2020/news/__tests__/sort.spec.js:
--------------------------------------------------------------------------------
1 | const { sortByLatest } = require('../index');
2 |
3 | describe('sort function', () => {
4 | test('it should sort by latest date', () => {
5 | const input = [
6 | { id: 3, created: 1602569513559 },
7 | { id: 1, created: 1602569508015 },
8 | { id: 2, created: 1602569511570 },
9 | ];
10 |
11 | const output = [
12 | { id: 3, created: 1602569513559 },
13 | { id: 2, created: 1602569511570 },
14 | { id: 1, created: 1602569508015 },
15 | ];
16 |
17 | expect(input.sort(sortByLatest('created'))).toEqual(output);
18 | });
19 |
20 | test('it should sort by latest id', () => {
21 | const input = [
22 | { id: 3, created: 1602569513559 },
23 | { id: 1, created: 1602569513559 },
24 | { id: 2, created: 1602569511570 },
25 | ];
26 |
27 | const output = [
28 | { id: 3, created: 1602569513559 },
29 | { id: 2, created: 1602569511570 },
30 | { id: 1, created: 1602569513559 },
31 | ];
32 |
33 | expect(input.sort(sortByLatest('id'))).toEqual(output);
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/election/packages/ge2020/news/index.js:
--------------------------------------------------------------------------------
1 | const GoogleNewsRss = require("google-news-rss")
2 |
3 | function main(args) {
4 | const googleNews = new GoogleNewsRss()
5 | return googleNews.search("us election", 50, "en", {
6 | scoring: "n",
7 | }).then((resp) => {
8 | resp.forEach(obj => obj.created = new Date(obj.pubDate).getTime())
9 | sorted = resp.sort(sortByLatest("created")); // scoring not working
10 | return {
11 | body: sorted,
12 | }
13 | });
14 | }
15 |
16 | function sortByLatest(property) {
17 | return function (a, b) {
18 | if (a[property] > b[property]) return -1
19 | else if (a[property] < b[property]) return 1
20 | else return 0
21 | };
22 | }
23 |
24 | module.exports = { main, sortByLatest }
25 |
--------------------------------------------------------------------------------
/election/packages/ge2020/news/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "news",
3 | "version": "1.0.0",
4 | "description": "Election Related News",
5 | "main": "index.js",
6 | "dependencies": {
7 | "google-news-rss": "^0.4.1"
8 | },
9 | "devDependencies": {
10 | "jest": "^26.5.3",
11 | "nock": "^13.0.4"
12 | },
13 | "scripts": {
14 | "test": "jest"
15 | },
16 | "jest": {
17 | "collectCoverage": true,
18 | "coverageReporters": [
19 | "html"
20 | ]
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/election/packages/ge2020/reps/__mocks__/axios.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | get: jest.fn(() => Promise.resolve({ data: {} }))
3 | };
4 |
--------------------------------------------------------------------------------
/election/packages/ge2020/reps/index.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 | const google_api_token = process.env.GOOGLE_CIVIC_API_TOKEN
3 |
4 | async function main(args) {
5 | const { ocdId = '', address = '', levels = '', roles = '', includeOffices = true, recursive = true } = args
6 | let path = `/civicinfo/v2/representatives?address=${encodeURI(address)}&includeOffices=${includeOffices}${levels ? `&levels=${levels}` : ''}${roles ? `&roles=${roles}` : ''}&key=${google_api_token}`
7 |
8 | if (ocdId) {
9 | path = `/civicinfo/v2/representatives/${encodeURI(ocdId)}?recursive=${recursive}${levels ? `&levels=${levels}` : ''}${roles ? `&roles=${roles}` : ''}&key=${google_api_token}`
10 | }
11 |
12 | return axios
13 | .get(`https://www.googleapis.com/${path}`)
14 | .then(res => ({ body: res.data }))
15 | .catch(error => ({ body: error.message }))
16 | }
17 |
18 | exports.main = main
19 |
--------------------------------------------------------------------------------
/election/packages/ge2020/reps/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "reps",
3 | "version": "1.0.0",
4 | "description": "Representatives for a given address",
5 | "main": "index.js",
6 | "devDependencies": {
7 | "jest": "^26.5.3"
8 | },
9 | "scripts": {
10 | "test": "jest"
11 | },
12 | "jest": {
13 | "collectCoverage": true,
14 | "coverageReporters": [
15 | "html"
16 | ]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/election/packages/ge2020/resources/.include:
--------------------------------------------------------------------------------
1 | ../../../transformed-data/resources.json
2 | index.js
3 |
--------------------------------------------------------------------------------
/election/packages/ge2020/resources/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const contents = fs.readFileSync(__dirname + '/resources.json', 'UTF-8');
4 | const body = JSON.parse(contents);
5 |
6 | function main(args) {
7 |
8 | return { body };
9 |
10 | }
11 |
12 | exports.main = main;
--------------------------------------------------------------------------------
/election/packages/ge2020/results/all-states.js:
--------------------------------------------------------------------------------
1 | const allStates = [
2 | "US",
3 | "AK",
4 | "AL",
5 | "AR",
6 | "AZ",
7 | "CA",
8 | "CO",
9 | "CT",
10 | "DC",
11 | "DE",
12 | "FL",
13 | "GA",
14 | "HI",
15 | "IA",
16 | "ID",
17 | "IL",
18 | "IN",
19 | "KS",
20 | "KY",
21 | "LA",
22 | "MA",
23 | "MD",
24 | "ME",
25 | "MI",
26 | "MN",
27 | "MO",
28 | "MS",
29 | "MT",
30 | "NC",
31 | "ND",
32 | "NE",
33 | "NH",
34 | "NJ",
35 | "NM",
36 | "NV",
37 | "NY",
38 | "OH",
39 | "OK",
40 | "OR",
41 | "PA",
42 | "RI",
43 | "SC",
44 | "SD",
45 | "TN",
46 | "TX",
47 | "UT",
48 | "VA",
49 | "VT",
50 | "WA",
51 | "WI",
52 | "WV",
53 | "WY",
54 | ];
55 |
56 | module.exports = { allStates }
57 |
--------------------------------------------------------------------------------
/election/packages/ge2020/results/index.js:
--------------------------------------------------------------------------------
1 | const { allStates } = require('./all-states')
2 | const { mainnetContract } = require('./election-contract')
3 |
4 | async function main(_args) {
5 | const result = {}
6 | const fetchAllStates =
7 | allStates.map((state) => {
8 | return mainnetContract.methods
9 | .presidentialWinners(state)
10 | .call()
11 | .then((winner) => {
12 | if (winner.winner === 'Trump')
13 | result[state] = 'red'
14 | else if (winner.winner === 'Biden')
15 | result[state] = '#007bff'
16 | else
17 | result[state] = 'grey'
18 | });
19 | });
20 | return Promise.all(fetchAllStates).then(_res => ({ body: result })).catch(error => ({ body: error.message }));
21 | }
22 |
23 | module.exports = { main }
24 |
--------------------------------------------------------------------------------
/election/packages/ge2020/results/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "results",
3 | "version": "1.0.0",
4 | "description": "Election results live",
5 | "main": "index.js",
6 | "dependencies": {
7 | "web3": "^1.3.0"
8 | },
9 | "devDependencies": {
10 | "jest": "^26.5.3"
11 | },
12 | "scripts": {
13 | "test": "jest"
14 | },
15 | "jest": {
16 | "collectCoverage": true,
17 | "coverageReporters": [
18 | "html"
19 | ]
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/election/packages/ge2020/results/web3.js:
--------------------------------------------------------------------------------
1 | const Web3 = require("web3");
2 | const web3 = new Web3(
3 | new Web3.providers.HttpProvider(process.env.INFURA_MAINNET_HTTP || 'https://mainnet.infura.io/v3/d390dc5f79ce4cf38314163a516084a1')
4 | );
5 | module.exports = { web3 }
6 |
--------------------------------------------------------------------------------
/election/packages/ge2020/state_counties/.include:
--------------------------------------------------------------------------------
1 | ../../../transformed-data/state_county_wise.json
2 | index.js
3 |
--------------------------------------------------------------------------------
/election/packages/ge2020/state_counties/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const contents = fs.readFileSync(__dirname + '/state_county_wise.json', 'UTF-8');
4 | const body = JSON.parse(contents);
5 |
6 | function main(args) {
7 |
8 | return { body };
9 | }
10 |
11 | exports.main = main;
--------------------------------------------------------------------------------
/election/packages/ge2020/timeseries/.include:
--------------------------------------------------------------------------------
1 | ../../../transformed-data/timeseries.json
2 | index.js
3 |
--------------------------------------------------------------------------------
/election/packages/ge2020/timeseries/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const contents = fs.readFileSync(__dirname + '/timeseries.json', 'UTF-8');
4 | const body = JSON.parse(contents);
5 |
6 | function main(args) {
7 |
8 | return { body };
9 | }
10 |
11 | exports.main = main;
--------------------------------------------------------------------------------
/election/packages/ge2020/voterinfo/__mocks__/axios.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | get: jest.fn(() => Promise.resolve({ data: {} }))
3 | };
4 |
--------------------------------------------------------------------------------
/election/packages/ge2020/voterinfo/index.js:
--------------------------------------------------------------------------------
1 | const axios = require('axios')
2 | const google_api_token = process.env.GOOGLE_CIVIC_API_TOKEN
3 |
4 | async function main(args) {
5 | const { address = '', electionId = 0, officialOnly = true, returnAllAvailableData = true } = args
6 | let path = `/civicinfo/v2/voterinfo?address=${encodeURI(address)}&electionId=${electionId}&officialOnly=${officialOnly}&returnAllAvailableData=${returnAllAvailableData}&key=${google_api_token}`
7 |
8 | return axios
9 | .get(`https://www.googleapis.com/${path}`)
10 | .then(res => ({ body: res.data }))
11 | .catch(error => ({ body: error.message }))
12 | }
13 |
14 | exports.main = main
15 |
--------------------------------------------------------------------------------
/election/packages/ge2020/voterinfo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "voterinfo",
3 | "version": "1.0.0",
4 | "description": "Voter information for a given address",
5 | "main": "index.js",
6 | "devDependencies": {
7 | "jest": "^26.5.3"
8 | },
9 | "scripts": {
10 | "test": "jest"
11 | },
12 | "jest": {
13 | "collectCoverage": true,
14 | "coverageReporters": [
15 | "html"
16 | ]
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/election/project.yml:
--------------------------------------------------------------------------------
1 | bucket:
2 | strip: 1
3 |
4 | packages:
5 | - name: ge2020
6 | actions:
7 | - name: news
8 | - name: timeseries
9 | - name: counties
10 | - name: exitpolls
11 | - name: state_counties
12 | - name: resources
13 | - name: divisions
14 | - name: elections
15 | - name: reps
16 | - name: voterinfo
17 | - name: results
18 |
--------------------------------------------------------------------------------
/election/transformers/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules/
5 | npm-debug.log*
6 |
--------------------------------------------------------------------------------
/election/transformers/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "transformers",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "total.js",
6 | "dependencies": {
7 | },
8 | "devDependencies": {
9 | "node-fetch": "^2.6.1",
10 | "fast-csv": "^4.3.6"
11 | },
12 | "scripts": {
13 | "test": "echo \"Error: no test specified\" && exit 1"
14 | },
15 | "author": "",
16 | "license": "ISC"
17 | }
18 |
--------------------------------------------------------------------------------
/election/transformers/timeseries.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const csv = require('fast-csv');
4 |
5 | const data = {}
6 | let years = {}
7 | let total = {}
8 | let meta = {}
9 | let candidates = {}
10 |
11 | let changers = { counter: 0 }
12 |
13 | fs.createReadStream(path.resolve(__dirname, '..', 'raw-data', '1976-2020-president.csv'))
14 | .pipe(csv.parse({ headers: true }))
15 | .on('error', error => console.error(error))
16 | .on('data', row => {
17 | if (changers.counter == 0) {
18 | changers.state = row.state_po
19 | changers.year = row.year
20 | }
21 | changers.counter = changers.counter + 1
22 |
23 | if (changers.year != row.year) {
24 | years[changers.year] = { total, meta, candidates }
25 | total = {}
26 | meta = {}
27 | candidates = {}
28 | changers.year = row.year
29 | }
30 |
31 | if (changers.state != row.state_po) {
32 | years[changers.year] = { total, meta, candidates }
33 | data[changers.state] = Object.assign({}, data[changers.state])
34 | data[changers.state].years = Object.assign(years, data[changers.state].years)
35 | years = {}
36 | total = {}
37 | meta = {}
38 | candidates = {}
39 | changers.state = row.state_po
40 | }
41 | total[(row.party_simplified || 'others').toLowerCase()] = row.candidatevotes
42 | meta['total'] = row.totalvotes
43 | candidates[(row.party_simplified || 'others').toLowerCase()] = row.candidate
44 |
45 | }
46 |
47 | )
48 | .on('end', rowCount => {
49 | console.log(JSON.stringify(data, null, 4))
50 | fs.writeFileSync('timeseries.json', JSON.stringify(data, null, 4))
51 | console.log(`Parsed ${rowCount} rows`)
52 | });
53 |
--------------------------------------------------------------------------------
/election/transformers/total.js:
--------------------------------------------------------------------------------
1 | const fetch = require('node-fetch');
2 | const fs = require('fs');
3 | let url = "https://devchand-hfo8lwtg1e5-apigcp.nimbella.io/api/ge2020/timeseries";
4 |
5 | let settings = { method: "Get" };
6 | const TT = {}
7 | years = ['1976',
8 | '1980',
9 | '1984',
10 | '1988',
11 | '1992',
12 | '1996',
13 | '2000',
14 | '2004',
15 | '2008',
16 | '2012',
17 | '2016',
18 | '2020'
19 | ]
20 | const states = [
21 | 'AK', 'AL', 'AR', 'AZ', 'CA', 'CO', 'CT',
22 | 'DC', 'DE', 'FL', 'GA', 'HI', 'IA', 'ID',
23 | 'IL', 'IN', 'KS', 'KY', 'LA', 'MA', 'MD',
24 | 'ME', 'MI', 'MN', 'MO', 'MS', 'MT', 'NC',
25 | 'ND', 'NE', 'NH', 'NJ', 'NM', 'NV', 'NY',
26 | 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD',
27 | 'TN', 'TX', 'UT', 'VA', 'VT', 'WA', 'WI',
28 | 'WV', 'WY'
29 | ]
30 | fetch(url, settings)
31 | .then(res => res.json())
32 | .then((json) => {
33 | years.forEach(year => {
34 | const counter = {democrat:0,libertarian:0,other:0,republican:0}
35 | states.forEach(state => {
36 | Object.entries(json[state].years[year].total).forEach(e => {
37 | counter[e[0]] = counter[e[0]] + Number(e[1])
38 | TT.years = Object.assign({}, TT.years)
39 | TT.years[year] = Object.assign({}, TT.years[year])
40 | TT.years[year][e[0]] = counter[e[0]] //Object.assign(0, TT.years[year][e[1]])
41 | // TT.years[year][e[0]] = Number(TT.years[year][e[0]]) + Number(e[1])
42 |
43 | TT.years[year].total = Object.assign({}, TT.years[year].total)
44 | TT.years[year].total[e[0]] = counter[e[0]] //Object.assign(0, TT.years[year].total[e[0]])
45 | });
46 |
47 | });
48 | });
49 | fs.writeFileSync(`total.json`, JSON.stringify(TT, null, 4))
50 | });
51 |
--------------------------------------------------------------------------------
/election/web/.eslintignore:
--------------------------------------------------------------------------------
1 | build/**
2 | node_modules/**
3 |
--------------------------------------------------------------------------------
/election/web/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es6: true,
5 | },
6 | extends: ['plugin:react/recommended', 'google', 'prettier'],
7 | globals: {
8 | Atomics: 'readonly',
9 | SharedArrayBuffer: 'readonly',
10 | },
11 | parser: 'babel-eslint',
12 | parserOptions: {
13 | ecmaFeatures: {
14 | jsx: true,
15 | },
16 | ecmaVersion: 2018,
17 | sourceType: 'module',
18 | },
19 | plugins: ['react', 'prettier', 'react-hooks', 'import'],
20 | rules: {
21 | 'require-jsdoc': 0,
22 | 'prettier/prettier': 0,
23 | 'no-invalid-this': 0,
24 | 'react/prop-types': 0,
25 | quotes: [0, "single", { "avoidEscape": true }],
26 | 'react-hooks/rules-of-hooks': 'warn',
27 | 'react-hooks/exhaustive-deps': 'error',
28 | 'import/no-unresolved': [2, {commonjs: true, amd: true}],
29 | 'import/named': 2,
30 | 'import/default': 2,
31 | 'import/export': 2,
32 | 'import/order': [
33 | 0,
34 | {
35 | groups: [
36 | 'index',
37 | 'sibling',
38 | 'parent',
39 | 'internal',
40 | 'external',
41 | 'builtin',
42 | ],
43 | 'newlines-between': 'always',
44 | alphabetize: {
45 | order: 'asc',
46 | caseInsensitive: true,
47 | },
48 | },
49 | ],
50 | },
51 | settings: {
52 | react: {
53 | version: 'detect',
54 | },
55 | },
56 | };
57 |
--------------------------------------------------------------------------------
/election/web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules/
5 | /.pnp
6 | .pnp.js
7 |
8 | #IDE file
9 | .idea
10 | .vscode
11 |
12 | # testing
13 | /coverage
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | .history
21 | .env.local
22 | .env.development.local
23 | .env.test.local
24 | .env.production.local
25 | /server/.env
26 |
27 | npm-debug.log*
28 | yarn-debug.log*
29 | yarn-error.log*
30 |
31 | # vim swap files
32 | *.swp
33 | *.swo
34 |
--------------------------------------------------------------------------------
/election/web/.include:
--------------------------------------------------------------------------------
1 | build
2 |
--------------------------------------------------------------------------------
/election/web/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Jeremy Philemon
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/election/web/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | npm install --production
3 | npm run build
4 |
--------------------------------------------------------------------------------
/election/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/election/web/public/favicon.ico
--------------------------------------------------------------------------------
/election/web/public/fonts/Archia/archia-bold-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/election/web/public/fonts/Archia/archia-bold-webfont.woff2
--------------------------------------------------------------------------------
/election/web/public/fonts/Archia/archia-medium-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/election/web/public/fonts/Archia/archia-medium-webfont.woff2
--------------------------------------------------------------------------------
/election/web/public/fonts/Archia/archia-semibold-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/election/web/public/fonts/Archia/archia-semibold-webfont.woff2
--------------------------------------------------------------------------------
/election/web/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/election/web/public/icon.png
--------------------------------------------------------------------------------
/election/web/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "ge2020usa",
3 | "name": "ge2020usa",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "48x48",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "/",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff",
15 | "orientation": "portrait"
16 | }
17 |
--------------------------------------------------------------------------------
/election/web/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | Sitemap: https://www.ge2020usa.org/sitemap.xml
3 | User-agent: *
4 | Disallow:
5 |
--------------------------------------------------------------------------------
/election/web/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "General Election 2020 | USA",
3 | "short_name": "General Election 2020 | USA",
4 | "icons": [{
5 | "src": "/android-chrome-192x192.png",
6 | "sizes": "192x192",
7 | "type": "image/png"
8 | }, {
9 | "src": "/android-chrome-512x512.png",
10 | "sizes": "512x512",
11 | "type": "image/png"
12 | }],
13 | "theme_color": "#ffffff",
14 | "background_color": "#ffffff",
15 | "display": "standalone"
16 | }
17 |
--------------------------------------------------------------------------------
/election/web/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | https://www.ge2020usa.org/
5 |
6 |
7 | https://www.ge2020usa.org/map/
8 |
9 |
10 | https://www.ge2020usa.org/exitpolls/
11 |
12 |
13 | https://www.ge2020usa.org/api/
14 |
15 |
16 | https://www.ge2020usa.org/resources/
17 |
18 |
19 | https://www.ge2020usa.org/state/FL
20 |
21 |
22 |
--------------------------------------------------------------------------------
/election/web/rollup.config.js:
--------------------------------------------------------------------------------
1 | import copy from 'rollup-plugin-copy';
2 |
3 | export default {
4 | input: './src/placeholder.js',
5 | plugins: [
6 | copy({
7 | targets: [
8 | {
9 | src: 'public/projected_maps/*.json',
10 | dest: 'build/mini_maps',
11 | transform: (contents) => JSON.stringify(JSON.parse(contents)),
12 | },
13 | ],
14 | }),
15 | ],
16 | };
17 |
--------------------------------------------------------------------------------
/election/web/scripts/project-maps.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | WIDTH=432
4 | HEIGHT=488
5 |
6 | SCRIPT_DIR=$(dirname $0)
7 | MAPS_DIR=${SCRIPT_DIR}/../public/maps
8 | OUTPUT_DIR=${SCRIPT_DIR}/../public/projected_maps
9 |
10 | mkdir -p "${OUTPUT_DIR}"
11 |
12 | for file in ${MAPS_DIR}/*.json; do
13 | fn=$(basename ${file})
14 | topo2geo counties=- -i "$file" | geoproject "d3.geoMercator().fitSize([${WIDTH}, ${HEIGHT}], d)" | geo2topo counties=- -o "${OUTPUT_DIR}/${fn}"
15 | done
16 |
17 | topomerge states=counties -k 'd.properties.st_nm' "${OUTPUT_DIR}/india.json" -o "${OUTPUT_DIR}/india_merged.json"
18 |
19 | mv "${OUTPUT_DIR}/india_merged.json" "${OUTPUT_DIR}/india.json"
20 | prettier --loglevel silent --write "$OUTPUT_DIR"
21 |
22 |
--------------------------------------------------------------------------------
/election/web/src/ProgressBar.scss:
--------------------------------------------------------------------------------
1 |
2 | .progress-bar-labels {
3 | margin-top: 50px;
4 | width: 1000px;
5 | font-size: 20px;
6 | font-weight: bold;
7 | }
8 |
9 | .dem-label {
10 | float: left;
11 | width: 50%;
12 | color: #007bff;
13 | }
14 |
15 | .rep-label {
16 | float: left;
17 | width: 50%;
18 | text-align: right;
19 | color: red;
20 | }
21 |
22 | .num-label {
23 | font-size: 40px;
24 | font-weight: bold;
25 | }
26 |
27 | .progress-bar {
28 | position: relative;
29 | height: 18%;
30 | width: 100%;
31 | background: #d3d3d3;
32 | border-radius: 50px;
33 | border: 1px solid #333;
34 | display: inline-block;
35 | overflow: hidden;
36 | }
37 |
38 | .blank-filler {
39 | float: left;
40 | background: #d3d3d3;
41 | height: 100%;
42 | border-radius: inherit;
43 | transition: width 0.2s ease-in;
44 | color: black;
45 | font-weight: bold;
46 | padding-left: -1px;
47 | padding-right: -1px;
48 | padding-top: 5px;
49 | }
50 |
51 | .dem-filler {
52 | float: left;
53 | background: #007bff;
54 | height: 100%;
55 | min-width: 0px;
56 | border-radius: inherit;
57 | transition: width 0.2s ease-in;
58 | }
59 |
60 | .rep-filler {
61 | float: left;
62 | background: red;
63 | height: 100%;
64 | min-width: 0px;
65 | border-radius: inherit;
66 | transition: width 0.2s ease-in;
67 | }
68 |
69 | .marker {
70 | margin-left: 48.5%;
71 | }
72 |
--------------------------------------------------------------------------------
/election/web/src/SplitElectoralVotes.scss:
--------------------------------------------------------------------------------
1 | .btn-group {
2 | float: left;
3 | margin-right:2%;
4 | }
5 |
6 | .btn-group button {
7 | height: 30px;
8 | width: 30px;
9 | background-color: #d3d3d3; /* Green background */
10 | border: 1px solid black; /* Green border */
11 | color: black; /* White text */
12 | font-weight: bold;
13 | padding: 2px 2px;
14 | cursor: pointer; /* Pointer/hand icon */
15 | float: left; /* Float the buttons side by side */
16 | }
17 |
18 | .state {
19 | width: 60px !important;
20 | }
21 |
22 | /* Clear floats (clearfix hack) */
23 | .btn-group:after {
24 | content: "";
25 | clear: both;
26 | display: table;
27 | }
28 |
29 | /* Add a background color on hover */
30 | .btn-group button:hover {
31 | opacity: 0.5;
32 | cursor: pointer;
33 | }
34 |
35 | .red {
36 | background-color: red !important;
37 | }
38 |
39 | .blue {
40 | background-color: #007bff !important;
41 | }
42 |
--------------------------------------------------------------------------------
/election/web/src/animations.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Navbar
3 | */
4 |
5 | export const SLIDE_IN = {
6 | position: 'absolute',
7 | transform: 'translate3d(-20rem, 0, 0)',
8 | height: '100vh',
9 | zIndex: -1,
10 | };
11 |
12 | export const SLIDE_OUT = {
13 | position: 'absolute',
14 | transform: 'translate3d(10rem, 0, 0)',
15 | };
16 |
17 | export const SLIDE_IN_MOBILE = {
18 | opacity: 1,
19 | position: 'absolute',
20 | height: '100vh',
21 | top: 64,
22 | zIndex: 999,
23 | };
24 |
25 | export const SLIDE_OUT_MOBILE = {
26 | opacity: 1,
27 | position: 'absolute',
28 | height: '100vh',
29 | top: 64,
30 | zIndex: 999,
31 | };
32 |
33 | /*
34 | * Tooltip
35 | */
36 |
37 | export const TOOLTIP_FADE_IN = {
38 | opacity: 1,
39 | transform: 'translate3d(0, 0px, 0)',
40 | zIndex: 999,
41 | position: 'absolute',
42 | pointerEvents: 'none',
43 | };
44 |
45 | export const TOOLTIP_FADE_OUT = {
46 | opacity: 0,
47 | transform: 'translate3d(0, 2px, 0)',
48 | zIndex: 999,
49 | position: 'absolute',
50 | pointerEvents: 'none',
51 | };
52 | /*
53 | * Table
54 | */
55 |
56 | export const TABLE_FADE_IN = {
57 | opacity: 1,
58 | transform: 'translate3d(0, 0px, 0)',
59 | height: 200,
60 | };
61 |
62 | export const TABLE_FADE_OUT = {
63 | opacity: 0,
64 | transform: 'translate3d(0, 2px, 0)',
65 | height: 0,
66 | };
67 |
--------------------------------------------------------------------------------
/election/web/src/components/API.js:
--------------------------------------------------------------------------------
1 | import Footer from './Footer';
2 |
3 | import { RedocStandalone } from 'redoc';
4 | import useScript from '../hooks/useScript';
5 | import React from 'react';
6 | import { Helmet } from 'react-helmet';
7 | import useDarkMode from 'use-dark-mode';
8 |
9 | function API() {
10 | const darkMode = useDarkMode(false);
11 |
12 | useScript(null, `(function (p, o, s, t, m, a, n) {
13 | !p[s] && (p[s] = function () { (p[t] || (p[t] = [])).push(arguments); })
14 | !o.getElementById(s + t) && o.getElementsByTagName("head")[0].appendChild((
15 | (n = o.createElement("script")),
16 | (n.id = s + t), (n.async = 1), (n.src = m), n
17 | ));
18 |
19 | }(window, document, "_pm", "PostmanRunObject", "https://run.pstmn.io/button.js"));
20 | `);
21 | return (
22 |
23 |
24 | Nimbella API - General Election 2020 USA
25 |
29 |
30 |
31 |
Nimbella API for General Election 2020 USA
32 |
33 | Look up the representatives, polling places, early vote location,
34 | candidate data, and other election official information.
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | }
47 | export default API;
48 |
--------------------------------------------------------------------------------
/election/web/src/components/Actions.js:
--------------------------------------------------------------------------------
1 | import ActionsPanel from './ActionsPanel';
2 | import {API_ROOT_URL} from '../constants';
3 | import {fetcher} from '../utils/commonFunctions';
4 |
5 | import React, {useState, useEffect, lazy, Suspense} from 'react';
6 | import {useLocalStorage} from 'react-use';
7 | import useSWR from 'swr';
8 |
9 | const Updates = lazy(() => import('./Updates'));
10 |
11 | const Actions = ({setYear, years}) => {
12 | const [showUpdates, setShowUpdates] = useState(false);
13 | const [newUpdate, setNewUpdate] = useLocalStorage('newUpdate', false);
14 | const [lastViewedLog, setLastViewedLog] = useLocalStorage('lastViewedLog', 0);
15 | const [isTimelineMode, setIsTimelineMode] = useState(false);
16 | const {data: updates} = useSWR(
17 | `${API_ROOT_URL}/news`,
18 | fetcher,
19 | {
20 | revalidateOnFocus: true,
21 | }
22 | );
23 |
24 | useEffect(() => {
25 | if (updates && updates.length> 0) {
26 | const lastTimestamp = updates.slice()[0].created;
27 | if (lastTimestamp !== lastViewedLog) {
28 | setNewUpdate(true);
29 | setLastViewedLog(lastTimestamp);
30 | }
31 | }
32 | }, [lastViewedLog, updates, setLastViewedLog, setNewUpdate]);
33 |
34 | return (
35 |
36 |
49 |
50 | {showUpdates && (
51 | }>
52 |
53 |
54 | )}
55 |
56 | );
57 | };
58 |
59 | const isEqual = (prevProps, currProps) => {
60 | return true;
61 | };
62 |
63 | export default React.memo(Actions, isEqual);
64 |
--------------------------------------------------------------------------------
/election/web/src/components/Contest.js:
--------------------------------------------------------------------------------
1 | import Footer from './Footer';
2 | import React from 'react';
3 | import {Helmet} from 'react-helmet';
4 | import banner from '../img/Election2020ContestBanner.png';
5 |
6 | function Contest() {
7 | return (
8 |
9 |
10 | Contest - build, extend and end the year 2020 on a winning note.
11 |
15 |
16 |
17 |
Don't let the year 2020 be a drag
18 |
Extend these election-related APIs/app and build your own election app to inspire your friends, family 👪, and everyone you know to win 🏆 $2020.
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 | export default Contest;
28 |
--------------------------------------------------------------------------------
/election/web/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {Twitter, GitHub, Mail, Slack} from 'react-feather';
3 |
4 | function Footer() {
5 | return (
6 |
55 | );
56 | }
57 |
58 | export default React.memo(Footer);
59 |
--------------------------------------------------------------------------------
/election/web/src/components/HeaderCell.js:
--------------------------------------------------------------------------------
1 | import {STATISTIC_CONFIGS} from '../constants';
2 | import {toTitleCase} from '../utils/commonFunctions';
3 |
4 | import {FilterIcon} from '@primer/octicons-v2-react';
5 | import classnames from 'classnames';
6 | import equal from 'fast-deep-equal';
7 | import produce from 'immer';
8 | import React from 'react';
9 | import {useLongPress} from 'react-use';
10 |
11 | function StateHeaderCell({handleSort, sortData, setSortData, statistic}) {
12 | const onLongPress = () => {
13 | if (sortData.sortColumn === statistic) {
14 | setSortData(
15 | produce(sortData, (sortDataDraft) => {
16 | sortDataDraft.delta = !sortData.delta;
17 | })
18 | );
19 | }
20 | };
21 | const longPressEvent = useLongPress(onLongPress, {isPreventDefault: false});
22 |
23 | return (
24 |
29 | {sortData.sortColumn === statistic && (
30 |
36 |
37 |
38 | )}
39 |
{toTitleCase(STATISTIC_CONFIGS[statistic].displayName)}
40 |
41 | );
42 | }
43 |
44 | const isStateHeaderCellEqual = (prevProps, currProps) => {
45 | if (!equal(prevProps.sortData, currProps.sortData)) {
46 | return false;
47 | } else {
48 | return true;
49 | }
50 | };
51 |
52 | export default React.memo(StateHeaderCell, isStateHeaderCellEqual);
53 |
--------------------------------------------------------------------------------
/election/web/src/components/ProgressBar.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 |
3 | import '../ProgressBar.scss';
4 | import marker from '../img/270-marker.png';
5 |
6 | class ProgressBar extends Component {
7 | render() {
8 | return (
9 |
10 |
11 |
12 | Democrat {' '}
13 | {this.props.demVotes}
14 |
15 |
16 | {' '}
17 | {this.props.repVotes}{' '}
18 | Republican{' '}
19 |
20 |
21 |
22 |

23 |
24 |
25 |
26 |
27 |
28 |
32 |
33 |
34 |
35 | );
36 | }
37 | }
38 |
39 | const DemFiller = (props) => {
40 | return (
41 |
42 | );
43 | };
44 |
45 | const BlankFiller = (props) => {
46 | return (
47 |
48 |
49 | );
50 | };
51 |
52 | const RepFiller = (props) => {
53 | return (
54 |
55 | );
56 | };
57 |
58 | export default ProgressBar;
59 |
--------------------------------------------------------------------------------
/election/web/src/components/Resources.js:
--------------------------------------------------------------------------------
1 | import Footer from './Footer';
2 | import Links from '../data/resources';
3 | import React, {useEffect} from 'react';
4 | import {Helmet} from 'react-helmet';
5 |
6 | function Resources() {
7 | useEffect(() => {
8 | getResources();
9 | }, []);
10 |
11 | useEffect(() => {
12 | window.scrollTo(0, 0);
13 | }, []);
14 |
15 | const getResources = () => {
16 | return Links.resources;
17 | };
18 |
19 | return (
20 |
21 |
22 | Resources - US General Election 2020
23 |
27 |
28 |
29 |
Useful Links Related to US General Election
30 | APIs, Datasets, and Websites
31 |
32 |
33 | {getResources().map((resource, index) => {
34 | return (
35 |
47 | );
48 | })}
49 |
50 |
51 |
52 |
53 | );
54 | }
55 | export default Resources;
56 |
--------------------------------------------------------------------------------
/election/web/src/components/SplitElectoralVotes.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import '../SplitElectoralVotes.scss';
3 |
4 | class SplitElectoralVotes extends Component {
5 | render() {
6 | return (
7 |
8 |
Split Electoral Votes
9 |
10 |
17 |
24 |
31 |
32 |
33 |
40 |
47 |
54 |
61 |
62 |
63 | );
64 | }
65 | }
66 |
67 | export default SplitElectoralVotes;
68 |
--------------------------------------------------------------------------------
/election/web/src/components/StateHeader.js:
--------------------------------------------------------------------------------
1 | import StateDropdown from './StateDropdown';
2 | import {formatDate} from '../utils/commonFunctions';
3 | import React, {useMemo} from 'react';
4 |
5 | function StateHeader({data, stateCode}) {
6 | const trail = useMemo(() => {
7 | const styles = [];
8 |
9 | [0, 0, 0].map((element, index) => {
10 | styles.push({
11 | animationDelay: `${index * 250}ms`,
12 | });
13 | return null;
14 | });
15 |
16 | return styles;
17 | }, []);
18 |
19 |
20 | return (
21 |
22 |
23 |
24 | {data?.meta?.['last_updated'] && (
25 |
26 | {`Last Updated on ${formatDate(
27 | data.meta.last_updated,
28 | 'dd MMM, p'
29 | )} IST`}
30 |
31 | )}
32 |
33 |
34 | {/*
35 |
{'Total'}
36 |
37 | {spring.total.interpolate((total) => formatNumber(Math.floor(total)))}
38 |
39 |
*/}
40 |
41 | );
42 | }
43 |
44 | export default React.memo(StateHeader);
45 |
--------------------------------------------------------------------------------
/election/web/src/components/StateMeta.js:
--------------------------------------------------------------------------------
1 | import {formatNumber} from '../utils/commonFunctions';
2 |
3 | import React from 'react';
4 |
5 | function StateMeta({stateCode, data}) {
6 | return (
7 |
8 |
9 |
10 |
Total
11 | {formatNumber(data[stateCode]?.meta?.total)}
12 |
13 |
14 |
15 | );
16 | }
17 |
18 | const isEqual = (prevProps, currProps) => {
19 | if (currProps.timeseries && !prevProps.timeseries) {
20 | return false;
21 | } else if (prevProps.stateCode !== currProps.stateCode) {
22 | return false;
23 | }
24 | return true;
25 | };
26 |
27 | export default React.memo(StateMeta, isEqual);
28 |
--------------------------------------------------------------------------------
/election/web/src/components/StateMetaCard.js:
--------------------------------------------------------------------------------
1 | import Tooltip from './Tooltip';
2 |
3 | import {InfoIcon} from '@primer/octicons-v2-react';
4 | import React from 'react';
5 |
6 | function StateMetaCard({
7 | title,
8 | statistic,
9 | total,
10 | formula,
11 | date,
12 | description,
13 | className,
14 | }) {
15 | return (
16 |
17 |
18 |
{title}
19 |
20 |
21 |
22 |
23 |
{statistic}
24 |
{date}
25 | {total &&
{`India has ${total} CPM`}
}
26 |
{description}
27 |
28 | );
29 | }
30 |
31 | export default StateMetaCard;
32 |
--------------------------------------------------------------------------------
/election/web/src/components/Timer.js:
--------------------------------------------------------------------------------
1 | import {useTime} from '../hooks/useTime';
2 |
3 | import {DateTime} from 'luxon';
4 | import React from 'react';
5 | const Timer = ({end}) => {
6 | const now = useTime(1000);
7 | end = end || DateTime.fromISO('2020-11-03T10:30');
8 | const diff = end.diff(now);
9 | const formattedDuration = diff.toFormat(
10 | "d 'Days' h 'Hours' m 'Minutes' s 'Seconds'"
11 | );
12 | return (
13 | {formattedDuration}
14 | );
15 | };
16 | export default Timer;
17 |
--------------------------------------------------------------------------------
/election/web/src/components/Tooltip.js:
--------------------------------------------------------------------------------
1 | import {TOOLTIP_FADE_IN, TOOLTIP_FADE_OUT} from '../animations';
2 |
3 | import React, {useCallback, useState} from 'react';
4 | import {useTransition, animated} from 'react-spring';
5 |
6 | const Tooltip = ({data, children}) => {
7 | const [isTooltipVisible, setIsTooltipVisible] = useState(false);
8 |
9 | const transitions = useTransition(isTooltipVisible, null, {
10 | from: TOOLTIP_FADE_OUT,
11 | enter: TOOLTIP_FADE_IN,
12 | leave: TOOLTIP_FADE_OUT,
13 | config: {
14 | mass: 1,
15 | tension: 210,
16 | friction: 20,
17 | },
18 | });
19 |
20 | const handleClick = useCallback((e) => e.stopPropagation(), []);
21 |
22 | return (
23 |
30 | {children}
31 |
32 | {transitions.map(({item, key, props}) =>
33 | item ? (
34 |
35 |
36 |
'),
39 | }}
40 | >
41 |
42 |
43 | ) : (
44 |
45 | )
46 | )}
47 |
48 | );
49 | };
50 |
51 | export default Tooltip;
52 |
--------------------------------------------------------------------------------
/election/web/src/components/USAState.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const USAState = (props) => {
4 | return (
5 |
12 | {props.stateName}
13 |
14 | );
15 | };
16 | export default USAState;
17 |
--------------------------------------------------------------------------------
/election/web/src/components/loaders/MapVisualizer.js:
--------------------------------------------------------------------------------
1 | import {STATISTIC_CONFIGS, MAP_LEGEND_HEIGHT} from '../../constants';
2 |
3 | import React from 'react';
4 | import ContentLoader from 'react-content-loader';
5 |
6 | // Margins as declared in CSS
7 | export const MAP_BUFFER_MARGINS = 42;
8 | export const INDIA_ASPECT_RATIO = 0.885;
9 |
10 | const MapVisualizerLoader = ({width, statistic}) => {
11 | const windowWidth = window.innerWidth;
12 | // Default width for loader
13 | if (!width) width = windowWidth > 769 ? 480 : windowWidth;
14 | // From CSS
15 | const scalingFactor = windowWidth > 769 ? 0.9 : 1;
16 | const mapHeight = (scalingFactor * width) / INDIA_ASPECT_RATIO;
17 | const height = MAP_BUFFER_MARGINS + MAP_LEGEND_HEIGHT + mapHeight;
18 | return (
19 |
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default MapVisualizerLoader;
34 |
--------------------------------------------------------------------------------
/election/web/src/components/loaders/Table.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ContentLoader from 'react-content-loader';
3 |
4 | const TableLoader = () => {
5 | const windowWidth = window.innerWidth;
6 | const width = windowWidth > 769 ? 448 : windowWidth;
7 | const height = 135;
8 |
9 | return (
10 |
11 |
18 |
19 |
20 |
21 |
22 |
23 | );
24 | };
25 |
26 | export default TableLoader;
27 |
--------------------------------------------------------------------------------
/election/web/src/components/loaders/Timeseries.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ContentLoader from 'react-content-loader';
3 |
4 | const TimeseriesLoader = () => {
5 | const windowWidth = window.innerWidth;
6 | const width = windowWidth > 769 ? 480 : windowWidth;
7 | const height = 160 * 5;
8 |
9 | return (
10 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default TimeseriesLoader;
24 |
--------------------------------------------------------------------------------
/election/web/src/components/snippets/TableDeltaHelper.js:
--------------------------------------------------------------------------------
1 | import {TABLE_STATISTICS} from '../../constants';
2 |
3 | import {FilterIcon} from '@primer/octicons-v2-react';
4 | import classnames from 'classnames';
5 | import React, {useEffect, useState} from 'react';
6 |
7 | const TableDeltaHelper = () => {
8 | const [statisticIndex, setStatisticIndex] = useState(0);
9 |
10 | useEffect(() => {
11 | const id = window.setTimeout(() => {
12 | setStatisticIndex((prevStatisticIndex) =>
13 | prevStatisticIndex === TABLE_STATISTICS.length - 1
14 | ? 0
15 | : prevStatisticIndex + 1
16 | );
17 | window.clearTimeout(id);
18 | }, 1000);
19 | }, [statisticIndex]);
20 |
21 | return (
22 |
23 |
24 |
25 |
26 | Sort by Delta [long press]
27 |
28 | );
29 | };
30 |
31 | export default TableDeltaHelper;
32 |
--------------------------------------------------------------------------------
/election/web/src/data/API_Response/elections.json:
--------------------------------------------------------------------------------
1 | {
2 | "elections": [
3 | {
4 | "id": "2000",
5 | "name": "VIP Test Election",
6 | "electionDay": "2021-06-06",
7 | "ocdDivisionId": "ocd-division/country:us"
8 | },
9 | {
10 | "id": "4955",
11 | "name": "West Virginia Presidential and State Primary Election",
12 | "electionDay": "2020-06-09",
13 | "ocdDivisionId": "ocd-division/country:us/state:wv"
14 | },
15 | {
16 | "id": "4958",
17 | "name": "Georgia Presidential and State Primary Election",
18 | "electionDay": "2020-06-09",
19 | "ocdDivisionId": "ocd-division/country:us/state:ga"
20 | },
21 | {
22 | "id": "4969",
23 | "name": "North Dakota State Primary Election",
24 | "electionDay": "2020-06-09",
25 | "ocdDivisionId": "ocd-division/country:us/state:nd"
26 | },
27 | {
28 | "id": "4970",
29 | "name": "Nevada State Primary Election",
30 | "electionDay": "2020-06-09",
31 | "ocdDivisionId": "ocd-division/country:us/state:nv"
32 | },
33 | {
34 | "id": "4971",
35 | "name": "South Carolina State Primary Election",
36 | "electionDay": "2020-06-09",
37 | "ocdDivisionId": "ocd-division/country:us/state:sc"
38 | },
39 | {
40 | "id": "4977",
41 | "name": "Colorado State Primary Election",
42 | "electionDay": "2020-06-30",
43 | "ocdDivisionId": "ocd-division/country:us/state:co"
44 | },
45 | {
46 | "id": "4990",
47 | "name": "Louisiana Presidential and Municipal Primary Election",
48 | "electionDay": "2020-07-11",
49 | "ocdDivisionId": "ocd-division/country:us/state:la"
50 | }
51 | ],
52 | "kind": "civicinfo#electionsQueryResponse"
53 | }
--------------------------------------------------------------------------------
/election/web/src/data/historical-maps.js:
--------------------------------------------------------------------------------
1 | import HistoricalMaps from './historical-maps.json';
2 |
3 | export default HistoricalMaps;
4 |
--------------------------------------------------------------------------------
/election/web/src/data/resources.js:
--------------------------------------------------------------------------------
1 | import Links from './resources.json';
2 |
3 | export default Links;
4 |
--------------------------------------------------------------------------------
/election/web/src/data/usa-map-dimensions.js:
--------------------------------------------------------------------------------
1 | import data from "./usa-states-dimensions.json";
2 |
3 | export default data;
4 |
--------------------------------------------------------------------------------
/election/web/src/data/usa-states-labels.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/election/web/src/data/usa-states-labels.json
--------------------------------------------------------------------------------
/election/web/src/hooks/useIsVisible.js:
--------------------------------------------------------------------------------
1 | import {useState, useEffect} from 'react';
2 |
3 | const OPTIONS = {
4 | root: null,
5 | rootMargin: '0px 0px 0px 0px',
6 | threshold: 0,
7 | };
8 |
9 | const useIsVisible = (elementRef) => {
10 | const [isVisible, setIsVisible] = useState(false);
11 |
12 | useEffect(() => {
13 | if (elementRef.current) {
14 | const observer = new IntersectionObserver((entries, observer) => {
15 | entries.forEach((entry) => {
16 | if (entry.isIntersecting) {
17 | setIsVisible(true);
18 | observer.unobserve(elementRef.current);
19 | }
20 | });
21 | }, OPTIONS);
22 | observer.observe(elementRef.current);
23 | }
24 | }, [elementRef]);
25 |
26 | return isVisible;
27 | };
28 |
29 | export default useIsVisible;
30 |
--------------------------------------------------------------------------------
/election/web/src/hooks/useResizeObserver.js:
--------------------------------------------------------------------------------
1 | import {ResizeObserver} from '@juggle/resize-observer';
2 | import {useState, useEffect} from 'react';
3 |
4 | export const useResizeObserver = (ref) => {
5 | const [dimensions, setDimensions] = useState(null);
6 | useEffect(() => {
7 | const observeTarget = ref.current;
8 | const resizeObserver = new ResizeObserver((entries, observer) => {
9 | entries.forEach((entry, index) => {
10 | setDimensions(entry.contentRect);
11 | });
12 | });
13 | resizeObserver.observe(observeTarget);
14 | return () => {
15 | resizeObserver.unobserve(observeTarget);
16 | };
17 | }, [ref]);
18 | return dimensions;
19 | };
20 |
21 | export default useResizeObserver;
22 |
--------------------------------------------------------------------------------
/election/web/src/hooks/useScript.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 |
3 | const useScript = (url, body) => {
4 | useEffect(() => {
5 | const script = document.createElement('script');
6 | if (url) {
7 | script.src = url;
8 | script.async = true;
9 | }
10 | if (body) {
11 | const scriptText = document.createTextNode(body);
12 | script.appendChild(scriptText);
13 | }
14 | document.body.appendChild(script);
15 |
16 | return () => {
17 | document.body.removeChild(script);
18 | }
19 | }, [url, body]);
20 | };
21 |
22 | export default useScript;
23 |
--------------------------------------------------------------------------------
/election/web/src/hooks/useStickySWR.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 | import {useState} from 'react';
3 | import {useUpdateEffect} from 'react-use';
4 | import useSWR from 'swr';
5 |
6 | export function useStickySWR(key, fetcher, swrOptions, ...args) {
7 | const [options, setOptions] = useState(swrOptions);
8 |
9 | const {data, isValidating, error, ...rest} = useSWR(
10 | key,
11 | fetcher,
12 | options,
13 | ...args
14 | );
15 |
16 | useUpdateEffect(() => {
17 | setOptions(
18 | produce(options, (draftOptions) => {
19 | draftOptions.initialData = data;
20 | })
21 | );
22 | }, [data]);
23 |
24 | return {
25 | ...rest,
26 | isValidating,
27 | error,
28 | data,
29 | };
30 | }
31 |
32 | export default useStickySWR;
33 |
--------------------------------------------------------------------------------
/election/web/src/hooks/useTime.js:
--------------------------------------------------------------------------------
1 | //
2 | // useTime hook
3 | //
4 | import {DateTime} from 'luxon';
5 | import {useEffect, useState} from 'react';
6 |
7 | export const getTime = () => {
8 | // This implementation uses Luxon: https://moment.github.io/luxon/
9 | return DateTime.local();
10 |
11 | // You can also use moment: https://momentjs.com
12 | // return moment();
13 |
14 | // Or just use native Date objects (in general, not a good move)
15 | // return new Date();
16 |
17 | // Or just use unix epoch timestamps (integers, no timezones)
18 | // return (new Date()).getTime();
19 | };
20 |
21 | export const useTime = (refreshCycle = 100) => {
22 | // Returns the current time
23 | // and queues re-renders every `refreshCycle` milliseconds (default: 100ms)
24 |
25 | const [now, setNow] = useState(getTime());
26 |
27 | useEffect(() => {
28 | // Regularly set time in state
29 | // (this will cause your component to re-render frequently)
30 | const intervalId = setInterval(() => setNow(getTime()), refreshCycle);
31 |
32 | // Cleanup interval
33 | return () => clearInterval(intervalId);
34 |
35 | // Specify dependencies for useEffect
36 | }, [refreshCycle, setNow]);
37 |
38 | return now;
39 | };
40 |
--------------------------------------------------------------------------------
/election/web/src/hooks/useTimeout.js:
--------------------------------------------------------------------------------
1 | import {useEffect} from 'react';
2 |
3 | //
4 | // useTimeout React Hook
5 | //
6 | // React hook for delaying calls with time
7 |
8 | export const useTimeout = (
9 | callback, // function to call. No args passed.
10 | timeout = 0, // delay, ms (default: immediately put into JS Event Queue)
11 | {
12 | // manage re-render behavior.
13 | // by default, a re-render in your component will re-define the callback,
14 | // which will cause this timeout to cancel itself.
15 | // to avoid cancelling on re-renders (but still cancel on unmounts),
16 | // set `persistRenders: true,`.
17 | persistRenders = false,
18 | } = {},
19 | // These dependencies are injected for testing purposes.
20 | // (pure functions - where all dependencies are arguments - is often easier to test)
21 | _setTimeout = setTimeout,
22 | _clearTimeout = clearTimeout,
23 | _useEffect = useEffect
24 | ) => {
25 | let timeoutId;
26 | const cancel = () => timeoutId && _clearTimeout(timeoutId);
27 |
28 | _useEffect(
29 | () => {
30 | timeoutId = _setTimeout(callback, timeout);
31 | return cancel;
32 | },
33 | persistRenders
34 | ? [_setTimeout, _clearTimeout]
35 | : [callback, timeout, _setTimeout, _clearTimeout]
36 | );
37 |
38 | return cancel;
39 | };
40 |
--------------------------------------------------------------------------------
/election/web/src/img/270-marker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/election/web/src/img/270-marker.png
--------------------------------------------------------------------------------
/election/web/src/img/Election2020ContestBanner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/election/web/src/img/Election2020ContestBanner.png
--------------------------------------------------------------------------------
/election/web/src/img/Timeline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/election/web/src/img/Timeline.png
--------------------------------------------------------------------------------
/election/web/src/index.js:
--------------------------------------------------------------------------------
1 | import * as serviceWorker from './serviceWorker';
2 | import React, { Suspense, lazy } from 'react';
3 | import { render } from 'react-dom';
4 | import { HashRouter as Router } from 'react-router-dom';
5 |
6 | const App = lazy(() => import('./App'));
7 | const rootElement = document.getElementById('root');
8 |
9 | const main = () =>
10 | render(
11 | }>
12 |
13 |
14 |
15 | ,
16 | rootElement
17 | );
18 |
19 | const browserSupportsAllFeatures = () => {
20 | return window.requestIdleCallback && window.IntersectionObserver;
21 | };
22 |
23 | const loadScript = (src, done) => {
24 | const js = document.createElement('script');
25 | js.src = src;
26 | js.onload = function () {
27 | done();
28 | };
29 | js.onerror = function () {
30 | done(new Error('Failed to load script ' + src));
31 | };
32 | document.head.appendChild(js);
33 | };
34 |
35 | if (browserSupportsAllFeatures()) {
36 | main();
37 | } else {
38 | loadScript(
39 | 'https://polyfill.io/v3/polyfill.min.js?version=3.52.1&features=requestIdleCallback%2CIntersectionObserver',
40 | main
41 | );
42 | }
43 |
44 | serviceWorker.register();
45 |
--------------------------------------------------------------------------------
/election/web/src/placeholder.js:
--------------------------------------------------------------------------------
1 | export default () => {};
2 |
--------------------------------------------------------------------------------
/election/web/src/setupTests.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom/extend-expect';
2 | import {configure} from 'enzyme';
3 | import Adapter from 'enzyme-adapter-react-16';
4 |
5 | configure({adapter: new Adapter()});
6 |
--------------------------------------------------------------------------------
/election/web/src/swBuild.js:
--------------------------------------------------------------------------------
1 | const workboxBuild = require('workbox-build');
2 |
3 | const buildSW = () => {
4 | workboxBuild
5 | .injectManifest({
6 | swSrc: 'src/swTemplate.js',
7 | swDest: 'build/service-worker.js',
8 | globDirectory: 'build',
9 | globPatterns: ['**/*.{html,woff2,js,css}'],
10 | })
11 | .then(({count, size, warnings}) => {
12 | // Optionally, log any warnings and details.
13 | warnings.forEach(console.warn);
14 | console.log(
15 | `${count} files will be precached, totaling ${size / 1000} KBs.`
16 | );
17 | })
18 | .catch(console.error);
19 | };
20 |
21 | buildSW();
22 |
--------------------------------------------------------------------------------
/election/web/src/swTemplate.js:
--------------------------------------------------------------------------------
1 | if (typeof importScripts === 'function') {
2 | importScripts(
3 | 'https://storage.googleapis.com/workbox-cdn/releases/5.0.0/workbox-sw.js'
4 | );
5 |
6 | /* global workbox */
7 | if (workbox) {
8 | console.log('Workbox is loaded 🚀');
9 | workbox.core.skipWaiting();
10 |
11 | /* injection point for manifest files. */
12 | workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
13 |
14 | /* custom cache rules */
15 | workbox.routing.registerRoute(
16 | new workbox.routing.NavigationRoute(
17 | new workbox.strategies.NetworkFirst({
18 | cacheName: 'PRODUCTION',
19 | })
20 | )
21 | );
22 |
23 | // Adding staleWhileRevalidate for all js files. Provide faster access from cache while revalidating in the background
24 | workbox.routing.registerRoute(
25 | /.*\.js$/,
26 | new workbox.strategies.StaleWhileRevalidate()
27 | );
28 |
29 | // Adding staleWhileRevalidate for all html files
30 | workbox.routing.registerRoute(
31 | /.*\.html/,
32 | new workbox.strategies.StaleWhileRevalidate()
33 | );
34 |
35 | // Adding staleWhileRevalidate for all css files
36 | workbox.routing.registerRoute(
37 | /.*\.css/,
38 | new workbox.strategies.StaleWhileRevalidate()
39 | );
40 |
41 | // Adding networkFirst for all json data. In offline mode will be fetched from cache
42 | workbox.routing.registerRoute(
43 | new RegExp('https://api\\.ge2020usa\\.org/.*\\.json'),
44 | new workbox.strategies.NetworkFirst(),
45 | 'GET'
46 | );
47 | } else {
48 | console.log('Workbox could not be loaded. Hence, no offline support.');
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/election/web/src/tests/components/Level.test.js:
--------------------------------------------------------------------------------
1 | import Level from '../../components/Level';
2 |
3 | import {render} from '@testing-library/react';
4 | import React from 'react';
5 |
6 | const data = {
7 | delta: {
8 | republican: 153,
9 | libertarian: 1,
10 | green: 2,
11 | constitution: {
12 | samples: 2544,
13 | },
14 | },
15 | meta: {
16 | constitution: {
17 | last_updated: '2020-09-06',
18 | source: 'https://docs.cdn.yougov.com/8nwf5tw7g2/econTabReport.pdf',
19 | },
20 | },
21 | total: {
22 | republican: 883,
23 | libertarian: 3,
24 | other: 3,
25 | green: 5,
26 | constitution: 27688,
27 | },
28 | };
29 |
30 | test('Level renders total state data', () => {
31 | const {container} = render();
32 |
33 | expect(container).toHaveTextContent(
34 | 'Republican+153883Democrat 872Green+25Libertarian+13'
35 | );
36 | });
37 |
--------------------------------------------------------------------------------
/election/web/src/tests/mapAndApiStateNames.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | getStatesAndCountiesFromAPI,
3 | getStatesAndCountiesFromMaps,
4 | } from './utils/index';
5 |
6 | describe('Compare the map and the API states and counties', () => {
7 | test('for any discrepancies', async () => {
8 | const statesAndCountiesFromAPI = await getStatesAndCountiesFromAPI();
9 | const statesAndCountiesFromMaps = await getStatesAndCountiesFromMaps();
10 |
11 | const statesFromAPI = Object.keys(statesAndCountiesFromAPI);
12 | const statesFromMaps = Object.keys(statesAndCountiesFromMaps);
13 |
14 | expect(statesFromAPI).toContain(...statesFromMaps);
15 |
16 | statesFromAPI.forEach((state) => {
17 | const expectedCounties = statesAndCountiesFromMaps[state]?.sort();
18 | const receivedCounties = statesAndCountiesFromAPI[state]?.sort();
19 |
20 | if (expectedCounties !== undefined && receivedCounties.length !== 0) {
21 | expect(expectedCounties).toContain(...receivedCounties);
22 | }
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/election/web/src/tests/utils/index.js:
--------------------------------------------------------------------------------
1 | import {promises as fs} from 'fs';
2 | import {API_ROOT_URL} from '../constants';
3 |
4 | export function removeFileExtension(fileName) {
5 | return fileName.substr(0, fileName.lastIndexOf('.'));
6 | }
7 |
8 | function removeUnknown(e) {
9 | return e !== 'Unknown';
10 | }
11 |
12 | export async function getStatesAndCountiesFromAPI() {
13 | const url =
14 | `${API_ROOT_URL}/state_counties`;
15 | const stateCountyWiseResponse = await (await fetch(url)).json();
16 | const states = Object.keys(stateCountyWiseResponse).filter(removeUnknown);
17 | const result = {};
18 | states.map((stateName) => {
19 | result[stateName] = Object.keys(
20 | stateCountyWiseResponse[stateName]['countyData']
21 | ).filter(removeUnknown);
22 | });
23 | return result;
24 | }
25 |
26 | export async function getStatesAndCountiesFromMaps() {
27 | const dir = await fs.opendir('public/maps');
28 | const counties = {};
29 | for await (const dirent of dir) {
30 | const fileName = dirent.name;
31 | const fileNameWithoutExtension = removeFileExtension(fileName);
32 |
33 | if (
34 | fileName === 'india.json' ||
35 | fileName === 'india_counties.json' ||
36 | fileName === 'dnh-and-dd.json'
37 | )
38 | continue;
39 | const data = JSON.parse(
40 | await fs.readFile(`public/maps/${fileName}`, 'binary')
41 | );
42 |
43 | const stateName =
44 | data.objects[`${fileNameWithoutExtension}_county`].geometries[0]
45 | .properties.st_nm;
46 |
47 | const result = data.objects[
48 | `${fileNameWithoutExtension}_county`
49 | ].geometries.map((e) => e.properties.county);
50 |
51 | counties[stateName] = result;
52 | }
53 | return counties;
54 | }
55 |
--------------------------------------------------------------------------------
/election/web/src/utils/loader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CircularProgress from '@material-ui/core/CircularProgress';
3 |
4 | export default function Loader(props) {
5 | return (
6 | props.show ?
7 |
8 |
: ''
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/election/web/src/utils/noResult.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function NoResult(props) {
4 | return (
5 | props.show ? No Results Found
: ''
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/election/web/src/wdyr.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | if (process.env.NODE_ENV === 'development') {
4 | const whyDidYouRender = require('@welldone-software/why-did-you-render');
5 | whyDidYouRender(React, {
6 | trackAllPureComponents: true,
7 | // logOwnerReasons: true,
8 | // logOnDifferentValues: true,
9 | });
10 | }
11 |
--------------------------------------------------------------------------------
/election/web/src/workers/getCounties.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer';
2 |
3 | export const getCounties = (data) => {
4 | let counties = {};
5 |
6 | Object.keys(data).map((stateCode) => {
7 | Object.keys(data[stateCode]?.counties || {}).map((countyName) => {
8 | counties = produce(counties || {}, (draftCounties) => {
9 | const countyKey = `${countyName}-${stateCode}`;
10 | draftCounties[countyKey] = data[stateCode].counties[countyName];
11 | draftCounties[countyKey].countyName = countyName;
12 | draftCounties[countyKey].stateCode = stateCode;
13 | });
14 | return null;
15 | });
16 | return null;
17 | });
18 |
19 | postMessage(counties);
20 | };
21 |
--------------------------------------------------------------------------------
/hello-dotnet/packages/default/hello/.gitignore:
--------------------------------------------------------------------------------
1 | Apache.OpenWhisk.Example.Dotnet/
2 | *.pdb
3 | *.dll
4 | *.json
5 |
--------------------------------------------------------------------------------
/hello-dotnet/packages/default/hello/.include:
--------------------------------------------------------------------------------
1 | Apache.OpenWhisk.Example.Dotnet.deps.json
2 | Apache.OpenWhisk.Example.Dotnet.pdb
3 | Apache.OpenWhisk.Example.Dotnet.dll
4 | Newtonsoft.Json.dll
5 |
--------------------------------------------------------------------------------
/hello-dotnet/packages/default/hello/Hello.cs:
--------------------------------------------------------------------------------
1 | /*
2 | * Licensed to the Apache Software Foundation (ASF) under one or more
3 | * contributor license agreements. See the NOTICE file distributed with
4 | * this work for additional information regarding copyright ownership.
5 | * The ASF licenses this file to You under the Apache License, Version 2.0
6 | * (the "License"); you may not use this file except in compliance with
7 | * the License. You may obtain a copy of the License at
8 | *
9 | * http://www.apache.org/licenses/LICENSE-2.0
10 | *
11 | * Unless required by applicable law or agreed to in writing, software
12 | * distributed under the License is distributed on an "AS IS" BASIS,
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | * See the License for the specific language governing permissions and
15 | * limitations under the License.
16 | */
17 |
18 | using System;
19 | using Newtonsoft.Json.Linq;
20 |
21 | namespace Apache.OpenWhisk.Example.Dotnet
22 | {
23 | public class Hello
24 | {
25 | public JObject Main(JObject args)
26 | {
27 | string name = "stranger";
28 | if (args.ContainsKey("name")) {
29 | name = args["name"].ToString();
30 | }
31 | JObject message = new JObject();
32 | message.Add("statusCode", new JValue(200));
33 | message.Add("body", new JValue($"Hello, {name}!"));
34 | return (message);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/hello-dotnet/packages/default/hello/build.cmd:
--------------------------------------------------------------------------------
1 | dotnet new classlib -n Apache.OpenWhisk.Example.Dotnet -lang C# -f netstandard2.1
2 | cd Apache.OpenWhisk.Example.Dotnet
3 | dotnet add package Newtonsoft.Json -v 12.0.2
4 | copy ..\hello.cs .
5 | dotnet publish -c Release -o ..
6 |
--------------------------------------------------------------------------------
/hello-dotnet/packages/default/hello/build.sh:
--------------------------------------------------------------------------------
1 | dotnet new classlib -n Apache.OpenWhisk.Example.Dotnet -lang C# -f netstandard2.1
2 | cd Apache.OpenWhisk.Example.Dotnet
3 | dotnet add package Newtonsoft.Json -v 12.0.2
4 | cp ../hello.cs .
5 | dotnet publish -c Release -o ..
6 |
--------------------------------------------------------------------------------
/hello-dotnet/project.yml:
--------------------------------------------------------------------------------
1 | packages:
2 | - name: default
3 | actions:
4 | - name: hello
5 | main: "Apache.OpenWhisk.Example.Dotnet::Apache.OpenWhisk.Example.Dotnet.Hello::Main"
6 | runtime: "dotnet:3.1"
--------------------------------------------------------------------------------
/hello-typescript/.gitignore:
--------------------------------------------------------------------------------
1 | lib/src/**/*.js
2 | lib/src/**/*d.ts
3 | lib/test/**/*d.ts
4 | lib/test/**/*.js
5 | lib/coverage
6 | packages/hello-ts/hello/index.js
7 |
--------------------------------------------------------------------------------
/hello-typescript/lib/.jestrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "collectCoverage": true,
3 | "coverageDirectory": "coverage",
4 | "collectCoverageFrom": [
5 | "src/**/*.ts"
6 | ],
7 | "transform": {
8 | "^.+\\.(ts|tsx)$": "ts-jest"
9 | },
10 | "testPathIgnorePatterns": [
11 | "/node_modules/",
12 | "(/__tests__/.*|(\\.|/)(test|spec))\\.js$"
13 | ],
14 | "coverageThreshold": {
15 | "global": {
16 | "branches": 0,
17 | "lines": 0,
18 | "statements": 0
19 | }
20 | },
21 | "testEnvironment": "node"
22 | }
23 |
--------------------------------------------------------------------------------
/hello-typescript/lib/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | SELFDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
6 | TSC=tsc
7 |
8 | if type -P "${TSC}" &>/dev/null; then
9 | echo "tsc is in path, will use global install"
10 | else
11 | echo "tsc is not in path, will use local install"
12 | TSC=${SELFDIR}/node_modules/.bin/tsc
13 | fi
14 |
15 | (cd "${SELFDIR}" && npm install && "${TSC}")
16 | (cd "${SELFDIR}/src/hello" && npx webpack)
17 |
--------------------------------------------------------------------------------
/hello-typescript/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hello-typescript",
3 | "version": "1.0.0",
4 | "description": "Example project using TypeScript",
5 | "scripts": {
6 | "build": "tsc",
7 | "test": "jest --config=./.jestrc.json ./test",
8 | "clean": "find src -type f \\( -iname '*.d.ts' -or -iname '*.js' ! -iname 'webpack.*' \\) | xargs rm",
9 | "clear": "jest --clearCache"
10 | },
11 | "dependencies": {
12 | "@types/jest": "^29.2.3",
13 | "jest": "^29.3.1",
14 | "ts-jest": "^29.0.3",
15 | "ts-loader": "^9.4.1",
16 | "tslib": "^2.4.1",
17 | "typescript": "^4.9.3",
18 | "webpack": "^5.75.0",
19 | "webpack-cli": "^5.0.0"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/hello-typescript/lib/src/hello/Hello.ts:
--------------------------------------------------------------------------------
1 | export function main(args: {}): {} {
2 | let name: string = args['name'] || 'stranger'
3 | let greeting: string = 'Hello, ' + name + '!'
4 | console.log(greeting)
5 | return { body: greeting }
6 | }
7 |
--------------------------------------------------------------------------------
/hello-typescript/lib/src/hello/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const actionDir = '../../../packages/hello-ts/hello/'
3 |
4 | module.exports = {
5 | entry: './Hello.ts',
6 | mode: 'production',
7 | target: 'node',
8 | node: false,
9 | module: {
10 | rules: [
11 | {
12 | test: /\.tsx?$/,
13 | use: 'ts-loader',
14 | exclude: /node_modules/,
15 | },
16 | ],
17 | },
18 | resolve: {
19 | extensions: [ '.tsx', '.ts', '.js' ],
20 | },
21 | externals: function(context, request, callback) {
22 | if (/^[^.]/.test(request)){
23 | return callback(null, 'commonjs2 ' + request)
24 | }
25 | callback()
26 | },
27 | output: {
28 | filename: 'index.js',
29 | path: path.resolve(actionDir),
30 | libraryTarget: 'commonjs-module'
31 | },
32 | optimization: {
33 | minimize: false
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/hello-typescript/lib/test/hello/Hello.test.ts:
--------------------------------------------------------------------------------
1 | import { main } from '../../src/hello/Hello'
2 |
3 | describe('hello', () => {
4 | it('should respond with standard greeting', async () => {
5 | expect.assertions(1)
6 | const res = await main({})
7 | expect(res).toEqual({'body': 'Hello, stranger!'})
8 | })
9 |
10 | it('should respond with name in greeting', async () => {
11 | expect.assertions(1)
12 | const res = await main({name: 'jest'})
13 | expect(res).toEqual({'body': 'Hello, jest!'})
14 | })
15 | })
16 |
17 |
--------------------------------------------------------------------------------
/hello-typescript/lib/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "declaration": true,
4 | "importHelpers": true,
5 | "module": "commonjs",
6 | "target": "es2017",
7 | "rootDir": "./"
8 | },
9 | "include": [
10 | "src/**/*.ts",
11 | "test/**/*.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/hello-typescript/packages/hello-ts/hello/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | SELFDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
6 |
7 | ../../../lib/build.sh
8 | npm install --production
9 |
--------------------------------------------------------------------------------
/hello-typescript/packages/hello-ts/hello/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hello-typescript",
3 | "version": "1.0.0",
4 | "description": "Example project using TypeScript",
5 | "main": "index.js",
6 | "dependencies": {}
7 | }
8 |
--------------------------------------------------------------------------------
/jokes/commands.yaml:
--------------------------------------------------------------------------------
1 | commands:
2 | joke:
3 | description: joke generator
4 |
--------------------------------------------------------------------------------
/jokes/packages/default/joke/__main__.py:
--------------------------------------------------------------------------------
1 | import pyjokes
2 |
3 | def main(args):
4 | joke = pyjokes.get_joke()
5 | return {
6 | 'body': {
7 | 'response_type': 'in_channel',
8 | 'text': joke
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/jokes/packages/default/joke/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | virtualenv virtualenv
6 | source virtualenv/bin/activate
7 | pip install -r requirements.txt
8 | deactivate
9 |
--------------------------------------------------------------------------------
/jokes/packages/default/joke/requirements.txt:
--------------------------------------------------------------------------------
1 | pyjokes==0.6.0
2 |
--------------------------------------------------------------------------------
/mongo-music/.gitignore:
--------------------------------------------------------------------------------
1 | *.pdb
2 | *.dll
3 | *.json
4 | *.zip
5 | [Dd]ebug/
6 | [Dd]ebugPublic/
7 | [Rr]elease/
8 | [Rr]eleases/
9 | x64/
10 | x86/
11 | [Aa][Rr][Mm]/
12 | [Aa][Rr][Mm]64/
13 | bld/
14 | [Bb]in/
15 | [Oo]bj/
16 | [Ll]og/
17 | [Ll]ogs/
18 |
--------------------------------------------------------------------------------
/mongo-music/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Will Velida
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/mongo-music/packages/mongo-music/CreateAlbum/.include:
--------------------------------------------------------------------------------
1 | MongoMusic.API.deps.json
2 | MongoMusic.API.dll
3 | MongoMusic.API.pdb
4 | MongoDB.Driver.dll
5 | MongoDB.Driver.Core.dll
6 | MongoDB.Bson.dll
7 | MongoDB.Libmongocrypt.dll
8 | Newtonsoft.Json.dll
9 | SharpCompress.dll
10 | System.Text.Encoding.CodePages.dll
11 | System.Runtime.CompilerServices.Unsafe.dll
12 | DnsClient.dll
13 | System.Buffers.dll
14 | System.Xml.ReaderWriter.dll
15 | System.Xml.XDocument.dll
16 | System.Threading.dll
17 | System.Threading.Thread.dll
18 | System.Threading.ThreadPool.dll
19 | System.Text.RegularExpressions.dll
20 | System.Threading.Tasks.Extensions.dll
21 | System.Security.Cryptography.Primitives.dll
22 | System.Security.Principal.dll
23 | System.Security.Cryptography.OpenSsl.dll
24 | System.Security.Claims.dll
25 | System.Runtime.Serialization.Primitives.dll
26 | System.Runtime.InteropServices.RuntimeInformation.dll
27 | System.Runtime.Numerics.dll
28 | System.Runtime.Serialization.Formatters.dll
29 | System.Reflection.TypeExtensions.dll
30 | System.Reflection.Emit.dll
31 | System.Reflection.Emit.Lightweight.dll
32 | System.ObjectModel.dll
33 | System.Reflection.Emit.ILGeneration.dll
34 | System.Linq.Expressions.dll
35 | System.Linq.dll
36 | System.IO.FileSystem.Primitives.dll
37 | System.IO.Compression.ZipFile.dll
38 | System.Diagnostics.DiagnosticSource.dll
39 | System.AppContext.dll
40 | System.Collections.Concurrent.dll
41 | System.Linq.Queryable.dll
42 | System.Dynamic.Runtime.dll
43 | System.Diagnostics.TextWriterTraceListener.dll
44 | System.ComponentModel.TypeConverter.dll
45 | System.ComponentModel.dll
46 | System.Collections.NonGeneric.dll
47 | System.Collections.Specialized.dll
48 | System.ComponentModel.Primitives.dll
49 |
--------------------------------------------------------------------------------
/mongo-music/packages/mongo-music/CreateAlbum/MongoMusic.API/Models/Album.cs:
--------------------------------------------------------------------------------
1 | using MongoDB.Bson;
2 | using MongoDB.Bson.Serialization.Attributes;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Text;
6 |
7 | namespace MongoMusic.API.Models
8 | {
9 | public class Album
10 | {
11 | [BsonId]
12 | [BsonRepresentation(BsonType.ObjectId)]
13 | public string Id { get; set; }
14 |
15 | [BsonElement("Name")]
16 | public string AlbumName { get; set; }
17 | public string Artist { get; set; }
18 | public double Price { get; set; }
19 | public DateTime ReleaseDate { get; set; }
20 | public string Genre { get; set; }
21 |
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/mongo-music/packages/mongo-music/CreateAlbum/MongoMusic.API/MongoMusic.API.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.1
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/mongo-music/packages/mongo-music/CreateAlbum/build.cmd:
--------------------------------------------------------------------------------
1 | cd MongoMusic.API
2 | dotnet publish -c Release -o ..
3 |
--------------------------------------------------------------------------------
/mongo-music/packages/mongo-music/CreateAlbum/build.sh:
--------------------------------------------------------------------------------
1 | cd MongoMusic.API
2 | dotnet publish -c Release -o ..
3 |
--------------------------------------------------------------------------------
/mongo-music/packages/mongo-music/GetAlbum/.include:
--------------------------------------------------------------------------------
1 | MongoMusic.API.deps.json
2 | MongoMusic.API.dll
3 | MongoMusic.API.pdb
4 | MongoDB.Driver.dll
5 | MongoDB.Driver.Core.dll
6 | MongoDB.Bson.dll
7 | MongoDB.Libmongocrypt.dll
8 | Newtonsoft.Json.dll
9 | SharpCompress.dll
10 | System.Text.Encoding.CodePages.dll
11 | System.Runtime.CompilerServices.Unsafe.dll
12 | DnsClient.dll
13 | System.Buffers.dll
14 | System.Xml.ReaderWriter.dll
15 | System.Xml.XDocument.dll
16 | System.Threading.dll
17 | System.Threading.Thread.dll
18 | System.Threading.ThreadPool.dll
19 | System.Text.RegularExpressions.dll
20 | System.Threading.Tasks.Extensions.dll
21 | System.Security.Cryptography.Primitives.dll
22 | System.Security.Principal.dll
23 | System.Security.Cryptography.OpenSsl.dll
24 | System.Security.Claims.dll
25 | System.Runtime.Serialization.Primitives.dll
26 | System.Runtime.InteropServices.RuntimeInformation.dll
27 | System.Runtime.Numerics.dll
28 | System.Runtime.Serialization.Formatters.dll
29 | System.Reflection.TypeExtensions.dll
30 | System.Reflection.Emit.dll
31 | System.Reflection.Emit.Lightweight.dll
32 | System.ObjectModel.dll
33 | System.Reflection.Emit.ILGeneration.dll
34 | System.Linq.Expressions.dll
35 | System.Linq.dll
36 | System.IO.FileSystem.Primitives.dll
37 | System.IO.Compression.ZipFile.dll
38 | System.Diagnostics.DiagnosticSource.dll
39 | System.AppContext.dll
40 | System.Collections.Concurrent.dll
41 | System.Linq.Queryable.dll
42 | System.Dynamic.Runtime.dll
43 | System.Diagnostics.TextWriterTraceListener.dll
44 | System.ComponentModel.TypeConverter.dll
45 | System.ComponentModel.dll
46 | System.Collections.NonGeneric.dll
47 | System.Collections.Specialized.dll
48 | System.ComponentModel.Primitives.dll
49 |
--------------------------------------------------------------------------------
/mongo-music/packages/mongo-music/GetAlbum/MongoMusic.API/Models/Album.cs:
--------------------------------------------------------------------------------
1 | using MongoDB.Bson;
2 | using MongoDB.Bson.Serialization.Attributes;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Text;
6 |
7 | namespace MongoMusic.API.Models
8 | {
9 | public class Album
10 | {
11 | [BsonId]
12 | [BsonRepresentation(BsonType.ObjectId)]
13 | public string Id { get; set; }
14 |
15 | [BsonElement("Name")]
16 | public string AlbumName { get; set; }
17 | public string Artist { get; set; }
18 | public double Price { get; set; }
19 | public DateTime ReleaseDate { get; set; }
20 | public string Genre { get; set; }
21 |
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/mongo-music/packages/mongo-music/GetAlbum/MongoMusic.API/MongoMusic.API.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.1
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/mongo-music/packages/mongo-music/GetAlbum/build.cmd:
--------------------------------------------------------------------------------
1 | cd MongoMusic.API
2 | dotnet publish -c Release -o ..
3 |
--------------------------------------------------------------------------------
/mongo-music/packages/mongo-music/GetAlbum/build.sh:
--------------------------------------------------------------------------------
1 | cd MongoMusic.API
2 | dotnet publish -c Release -o ..
3 |
--------------------------------------------------------------------------------
/mongo-music/project.yml:
--------------------------------------------------------------------------------
1 | packages:
2 | - name: mongo-music
3 | clean: true
4 | parameters: $(
5 | MONGO_CONNECTION_STRING
6 | DATABASE_NAME
7 | COLLECTION_NAME
8 | )
9 | actions:
10 | - name: GetAlbum
11 | main: "MongoMusic.API::MongoMusic.API.GetAlbum::Main"
12 | runtime: "dotnet:3.1"
13 | - name: CreateAlbum
14 | main: "MongoMusic.API::MongoMusic.API.CreateAlbum::Main"
15 | runtime: "dotnet:3.1"
--------------------------------------------------------------------------------
/numbers-to-words/.gitignore:
--------------------------------------------------------------------------------
1 | packages/default/n2w/composer.lock
2 | packages/default/n2w/vendor/
3 |
--------------------------------------------------------------------------------
/numbers-to-words/README.md:
--------------------------------------------------------------------------------
1 | ## Numbers to Words
2 |
3 | This project creates an API implemented in PHP and which requires installing a third party library.
4 |
5 | ### Project file structure
6 |
7 | The GitHub project has the file structure that Nimbella uses to deploy the project with minimal configuration.
8 | - There is a single API implemented in [./packages/default/n2w](./packages/default/n2w).
9 | - The required library is specified in [./packages/default/n2w/composer.json](./packages/default/n2w/composer.json).
10 | - There is a [`build.sh`](./packages/default/n2w/build.sh) to install the required library during a Nimbella project deployment.
11 |
12 | ### Deploy this project to the Nimbella Cloud
13 |
14 | If you have the [Nimbella command line tool called `nim`](https://docs.nimbella.com/cli) installed, you can deploy this project directly from GitHub. Or, you can clone this repository and deploy from the clone.
15 |
16 | - To deploy from GitHub
17 |
18 | `nim project deploy github:nimbella/demo-projects/numbers-to-words`
19 |
20 | - If you have cloned the repository
21 |
22 | `nim project deploy /path/to/numbers-to-words`
23 |
24 | The output of this command will include a link to where the application is running in the cloud for your account.
25 |
--------------------------------------------------------------------------------
/numbers-to-words/packages/default/n2w/.ignores:
--------------------------------------------------------------------------------
1 | build.sh
2 |
--------------------------------------------------------------------------------
/numbers-to-words/packages/default/n2w/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | composer install
6 |
--------------------------------------------------------------------------------
/numbers-to-words/packages/default/n2w/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": {
3 | "nineteenfeet/nf-number-to-word": "1.1.1"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/numbers-to-words/packages/default/n2w/index.php:
--------------------------------------------------------------------------------
1 | 'Please supply a number.']);
8 | }
9 |
10 | $number = (int)($args['number']);
11 | $words = (new NumberToWords)->toWords($number);
12 |
13 | return [
14 | 'body' => $words,
15 | ];
16 | }
17 |
18 | function wrap(array $args) : array
19 | {
20 | return ["body" => $args];
21 | }
22 |
--------------------------------------------------------------------------------
/ocr/.gitignore:
--------------------------------------------------------------------------------
1 | .slack-env
2 | web/.built
3 | packages/ocr/imageToText/eng.traineddata
4 | packages/ocr/acceptImage/annotations.json
5 | packages/ocr/acceptImage/index.js
6 |
--------------------------------------------------------------------------------
/ocr/.slack-env.template:
--------------------------------------------------------------------------------
1 | token="your slack token"
2 | username="notifications"
3 | url="https://slack.com/api/chat.postMessage"
4 | channel="#general"
5 |
--------------------------------------------------------------------------------
/ocr/commander/ocr.js:
--------------------------------------------------------------------------------
1 | // jshint esversion: 9
2 | // jshint asi:true
3 |
4 | const openwhisk = require('openwhisk')
5 |
6 | /**
7 | * @description null
8 | * @param {ParamsType} params list of command parameters
9 | * @param {?string} commandText slack text message
10 | * @param {!object} [secrets = {}] list of secrets
11 | * @return {Promise} Slack response body
12 | */
13 | async function _command(params, commandText, secrets = {}) {
14 | const wsk = openwhisk({api_key: secrets.api_key, namespace: secrets.namespace})
15 | return wsk
16 | .activations
17 | .result({name: params.id})
18 | .then(activation => {
19 | console.log(activation)
20 | return {
21 | response_type: 'in_channel',
22 | text: activation.result.body.text
23 | }
24 | })
25 | }
26 |
27 | /**
28 | * @typedef {object} SlackBodyType
29 | * @property {string} text
30 | * @property {'in_channel'|'ephemeral'} [response_type]
31 | */
32 |
33 | const main = async ({__secrets = {}, commandText, ...params}) => ({body: await _command(params, commandText, __secrets)})
34 | module.exports = main
35 |
--------------------------------------------------------------------------------
/ocr/packages/ocr/acceptImage/.include:
--------------------------------------------------------------------------------
1 | index.js
2 |
--------------------------------------------------------------------------------
/ocr/packages/ocr/acceptImage/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "workflow",
3 | "version": "0.0.1",
4 | "main": "index.js",
5 | "scripts": {
6 | "build": "npx compose --js workflow.js -o index.js",
7 | "viz": "npx compose --ast workflow.js | jq {\"conductor\":.} > annotations.json"
8 | },
9 | "dependencies": {},
10 | "devDependencies": {
11 | "openwhisk-composer": "^0.12.0"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/ocr/packages/ocr/acceptImage/workflow.js:
--------------------------------------------------------------------------------
1 | const composer = require('openwhisk-composer')
2 |
3 | module.exports =
4 | composer.sequence(
5 | // save parameters to use later
6 | composer.retain(
7 | ({ id, lang }) => ({
8 | text: `Starting OCR id ${id} (${lang})`
9 | }),
10 | // send slack notification
11 | 'utils/slack'
12 | ),
13 | // restore saved parameters
14 | saved => saved.params,
15 | // invoke image-to-text on saved params
16 | 'ocr/imageToText',
17 | ({ body: { id, activation } }) => ({
18 | text: `Finished OCR id ${id} (${activation})`
19 | }),
20 | // send result to slack
21 | 'utils/slack'
22 | )
23 |
--------------------------------------------------------------------------------
/ocr/packages/ocr/credential/index.js:
--------------------------------------------------------------------------------
1 | async function getSignedUrl(filename) {
2 | const nimbella = require('@nimbella/sdk')
3 | const bucket = await nimbella.storage()
4 |
5 | const file = bucket.file(filename)
6 | const expiration = 15 * 60 * 1000 // 15 minutes
7 |
8 | const putOptions = {
9 | version: 'v4',
10 | action: 'write',
11 | contentType: 'multipart/formdata; charset=UTF-8',
12 | expires: Date.now() + expiration
13 | }
14 |
15 | const getOptions = {
16 | version: 'v4',
17 | action: 'read',
18 | expires: Date.now() + expiration
19 | }
20 |
21 | return Promise
22 | .all([ file.getSignedUrl(putOptions), file.getSignedUrl(getOptions) ])
23 | .then(([[signedPutUrl], [signedGetUrl]]) => {
24 | return {
25 | body: { signedPutUrl, signedGetUrl, bucketName: bucket.id }
26 | }
27 | })
28 | .catch(error => {
29 | console.log(error)
30 | return errorResponse(error.message)
31 | })
32 | }
33 |
34 | const main = (args) => {
35 | if (args.filename) {
36 | return getSignedUrl(args.filename)
37 | } else return errorResponse('filename required')
38 | }
39 |
40 | function errorResponse(msg) {
41 | return {
42 | statusCode: 400,
43 | body: {
44 | error: msg
45 | }
46 | }
47 | }
48 |
49 | exports.main = main
50 |
--------------------------------------------------------------------------------
/ocr/packages/ocr/imageToText/.include:
--------------------------------------------------------------------------------
1 | index.js
2 |
--------------------------------------------------------------------------------
/ocr/packages/ocr/imageToText/index.js:
--------------------------------------------------------------------------------
1 | const worker = require('tesseract.js'),
2 | nimbella = require('@nimbella/sdk'),
3 | redis = nimbella.redis()
4 |
5 | const ocr = (url, lang, id, promise) => {
6 | worker
7 | .recognize(url, lang)
8 | .progress(res => {
9 | if (res) {
10 | let { status, progress } = res
11 | console.log(`${status}...${progress}`)
12 | progress = status == 'recognizing text' ? progress : 0
13 | redis
14 | .setAsync(id, JSON.stringify({ progress, status }))
15 | .catch(console.log)
16 | }
17 | })
18 | .then(result => {
19 | const lines = result.lines
20 | const textOverlay = lines.map(i => ({ bbox: i.bbox }))
21 | redis.setAsync(id, JSON.stringify({ text: JSON.parse(JSON.stringify(result.text)), textOverlay, progress: 1, status: 'done' })).then(() => {
22 | promise.resolve({ body: { id, lang, text: result.text, textOverlay, activation: process.env.__OW_ACTIVATION_ID } })
23 | })
24 | })
25 | .catch(error => {
26 | console.log(error)
27 | promise.reject(errorResponse(error.message))
28 | })
29 | }
30 |
31 | const main = (args) => {
32 | const { url, lang, id } = args
33 | return new Promise((resolve, reject) => {
34 | ocr(url, lang, id, { resolve, reject })
35 | })
36 | }
37 |
38 | const errorResponse = (msg, code) => {
39 | return {
40 | statusCode: code || 400,
41 | body: {
42 | error: msg
43 | }
44 | }
45 | }
46 |
47 | exports.main = main
48 |
49 |
--------------------------------------------------------------------------------
/ocr/packages/ocr/imageToText/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "imagetotext",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "devDependencies": {
6 | "tesseract.js": "^1.0.19"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/ocr/packages/ocr/progress/index.js:
--------------------------------------------------------------------------------
1 | const nimbella = require('@nimbella/sdk'),
2 | redis = nimbella.redis()
3 |
4 | const main = args => {
5 | const { id } = args
6 | if (id) {
7 | return redis
8 | .getAsync(id)
9 | .then(res => {
10 | if (res) {
11 | const { progress = 0, status = '', text = '', textOverlay = [] } = JSON.parse(res)
12 | return {
13 | body: {
14 | progress,
15 | status,
16 | text,
17 | textOverlay
18 | }
19 | }
20 | } else {
21 | return {
22 | body: {
23 | progress: 0,
24 | status: 'waiting...'
25 | }
26 | }
27 | }
28 | })
29 | .catch(error => {
30 | console.log(error)
31 | return errorResponse(error.message)
32 | })
33 | } else return errorResponse('The param id is required.')
34 | }
35 |
36 | const errorResponse = (msg, code) => {
37 | return {
38 | statusCode: code || 400,
39 | body: {
40 | error: msg
41 | }
42 | }
43 | }
44 |
45 | exports.main = main
46 |
--------------------------------------------------------------------------------
/ocr/packages/ocr/textToSpeech/index.js:
--------------------------------------------------------------------------------
1 | const handleText = (t, l) => {
2 | let truncated = []
3 | const arr = t.match(/.{1,195}(\s|$)/g)
4 | arr.forEach(i => {
5 | truncated.push(`http://translate.google.com/translate_tts?ie=UTF-8&q=${i.replace(/ /g, '+')}&tl=${l}&client=tw-ob`)
6 | })
7 | return truncated
8 | }
9 |
10 | const main = args => {
11 | const { text, lang } = args
12 | const url = handleText(text, lang)
13 | return { body: { url } }
14 | }
15 |
16 | exports.main = main
17 |
--------------------------------------------------------------------------------
/ocr/project-slack-notification.yml:
--------------------------------------------------------------------------------
1 | bucket:
2 | strip: 1
3 | packages:
4 | - name: ocr
5 | actions:
6 | - name: imageToText
7 | runtime: tessjs:10
8 | limits:
9 | memory: 768
10 | timeout: 120000
11 | - name: utils
12 | actions:
13 | - name: slack
14 | parameters: ${<.slack-env}
15 |
--------------------------------------------------------------------------------
/ocr/project.yml:
--------------------------------------------------------------------------------
1 | bucket:
2 | strip: 1
3 | packages:
4 | - name: ocr
5 | actions:
6 | - name: imageToText
7 | runtime: tessjs:10
8 | limits:
9 | memory: 768
10 | timeout: 120000
11 |
--------------------------------------------------------------------------------
/ocr/web/.gitignore:
--------------------------------------------------------------------------------
1 | /*.js
2 | /*.css
3 | /*.html
4 | /*.ico
5 | /static/
6 | /build/
7 | /*manifest.json
8 |
--------------------------------------------------------------------------------
/ocr/web/.include:
--------------------------------------------------------------------------------
1 | build
2 |
--------------------------------------------------------------------------------
/ocr/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ocrdemo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.21.1",
7 | "bootstrap": "^3.4.1",
8 | "konva": "^4.0.12",
9 | "prop-types": "^15.7.2",
10 | "react": "^16.8.3",
11 | "react-card-flip": "^1.0.10",
12 | "react-dom": "^16.8.3",
13 | "react-file-uploader": "^1.0.0",
14 | "react-html5-camera-photo": "^1.3.0",
15 | "react-konva": "^16.9.0-1",
16 | "react-scripts": "^3.0.1",
17 | "react-uuid": "^1.0.2",
18 | "reactstrap": "^8.1.1"
19 | },
20 | "scripts": {
21 | "start": "PORT=4000 react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": "react-app"
28 | },
29 | "browserslist": [
30 | ">0.2%",
31 | "not dead",
32 | "not ie <= 11",
33 | "not op_mini all"
34 | ],
35 | "proxy": "http://localhost:3000",
36 | "devDependencies": {}
37 | }
38 |
--------------------------------------------------------------------------------
/ocr/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/ocr/web/public/favicon.ico
--------------------------------------------------------------------------------
/ocr/web/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
25 | Nimbella - OCR Demo
26 |
27 |
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/ocr/web/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "OCR Demo",
3 | "name": "Nimballe - OCR Demo",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#13292d",
14 | "background_color": "#222222"
15 | }
16 |
--------------------------------------------------------------------------------
/ocr/web/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import logo from '../images/Nimbella-Logo.svg';
3 |
4 | import '../stylesheets/Header.css';
5 |
6 | class Header extends Component {
7 | render() {
8 | return (
9 |
17 | );
18 | }
19 | }
20 |
21 | export default Header;
22 |
--------------------------------------------------------------------------------
/ocr/web/src/images/covert-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/ocr/web/src/images/upload-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
--------------------------------------------------------------------------------
/ocr/web/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './components/App';
4 |
5 | import 'bootstrap/dist/css/bootstrap.css';
6 | import './stylesheets/style.css';
7 |
8 | import * as serviceWorker from './serviceWorker';
9 |
10 | ReactDOM.render(, document.getElementById('root'));
11 |
12 | // If you want your app to work offline and load faster, you can change
13 | // unregister() to register() below. Note this comes with some pitfalls.
14 | // Learn more about service workers: http://bit.ly/CRA-PWA
15 | serviceWorker.unregister();
16 |
--------------------------------------------------------------------------------
/ocr/web/src/stylesheets/FileUpload.css:
--------------------------------------------------------------------------------
1 | .file-upload{ width: 100%; height: 400px; min-height: 100px; border: 1px dashed #A2B5B8; border-radius: 5px; position: relative; overflow-y: auto; background-color: #222222; box-shadow: inset 4px 4px 10px rgba(0,0,0,1); display: flex; align-items: center; box-sizing: border-box; }
2 |
3 | .file-upload .spinner-border{ position: absolute; z-index: 99; margin-left: calc(50% - 1.5rem); }
4 | .dnd{ width: 100%; height: 395px; padding: 10px; display: table; text-align: center; box-sizing: border-box; }
5 | .dnd .content{ display: table-cell; vertical-align: middle; }
6 | .dnd .upload-icon{ display: inline-block; width: 128px; height: 77px; background: transparent url(../images/upload-icon.svg) center center no-repeat; clear: both; }
7 |
8 | .react-html5-camera-photo{ position: fixed; top: 0; left: 0; width: 100%; height: 100%; padding-top: 0; padding-bottom: 40px; z-index: 9999; background-color: #222222; }
9 | .react-html5-camera-photo video{ width: 100%; }
10 |
11 | .file-upload .close{ position: fixed; top: 20px; right: 20px; z-index: 99999; opacity: .5; -webkit-transition: opacity .3s ease-in;-moz-transition: opacity .3s ease-in;-ms-transition: opacity .3s ease-in;-o-transition: opacity .3s ease-in;transition: opacity .3s ease-in;}
12 | .file-upload .close:hover{ opacity: 1; }
13 | .file-upload .close span{ color: white; font-size: 60px; }
14 |
15 | @media only screen and (max-width: 1023px) {
16 | .file-upload{ border: none; }
17 | .file-upload.mobile{ background-color: transparent; box-shadow: none; }
18 | }
19 |
20 | @media only screen and (max-width: 812px) {
21 | .file-upload{ height: fit-content; margin-bottom: 20px; }
22 | .dnd{ height: fit-content; }
23 | }
24 |
--------------------------------------------------------------------------------
/ocr/web/src/stylesheets/Header.css:
--------------------------------------------------------------------------------
1 | .navbar{ border-radius: 0; }
2 | .navbar .logo-text{ display: inline-block; margin:0; padding: 20px 0; min-height:50px; }
3 | .navbar .navbar-brand{ padding: 0; height: 55px; }
4 | .navbar .navbar-brand:hover{ color: #55BFD1; }
5 | .navbar-nav{ list-style: none; float: right; min-height: 50px; padding: 20px; margin-right: 15px; }
6 | .navbar-nav .nav-item{ margin-left: 40px; }
7 |
8 |
9 | @media only screen and (max-width: 736px) {
10 | .navbar-nav{ padding: 12px 12px 0 0; margin: 0; float: right !important; }
11 | }
12 |
13 | @media only screen and (max-width: 414px) {
14 | .navbar-brand{ padding: 15px 5px; }
15 | .navbar-collapse{ float: left; }
16 | .navbar-collapse .nav-item{ margin-left: 10px; }
17 | }
--------------------------------------------------------------------------------
/ocr/web/src/stylesheets/Result.css:
--------------------------------------------------------------------------------
1 | .result-section .result-item {
2 | margin-top: 3rem;
3 | width: 100%;
4 | height: 400px;
5 | }
6 |
7 | .result-section .result-item .display-error {
8 | font-size: 1.2em;
9 | text-align: center;
10 | }
11 |
12 | .result-section .image-display {
13 | padding: 5px;
14 | }
15 |
16 | @media only screen and (max-width: 812px) {
17 | .result-section .result-item { height: 300px; margin-bottom: 20px; }
18 | .result-section .react-card-flipper {
19 | margin-bottom: 20px;
20 | margin-top: 90px !important;
21 | }
22 | .result-section .col-md-4:first-of-type .react-card-flipper {
23 | margin-top: 0 !important;
24 | margin-bottom: 20px;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ocr/web/src/stylesheets/TextDisplay.css:
--------------------------------------------------------------------------------
1 | .text-display{ display: inline-block; height: 400px; border: 1px solid #A2B5B8; width: calc(100% - 1px); padding: 20px; border-radius: 5px; position: relative; white-space: pre-line; background-color: #222222; }
2 | .text-display .content{ overflow-y: auto; height: 310px; }
3 | .text-display .btn-wrap { margin-top: 10px; }
4 | .text-display .btn-wrap .btn { width: 100%; margin: 0; }
5 |
6 | /*Equalizer Animation*/
7 | .eq-icon { align-items: flex-end; display: flex; width: 20px; height: 12px; overflow: hidden; opacity: 0.8; float: right; }
8 | .eq-icon .eq-col { flex: 1; position: relative; height: 100%; margin-right: 1px; }
9 | .eq-icon .eq-col div { animation-name: ani-eq; animation-timing-function: linear; animation-iteration-count: infinite; animation-direction: alternate; background-color: #a2b5b8; position: absolute; width: 100%; height: 100%; transform: translateY(100%); will-change: transform; }
10 | .eq-item-1-a { animation-duration: 0.3s; }
11 | .eq-item-1-b { animation-duration: 0.45s; }
12 | .eq-item-2-a { animation-duration: 0.5s; }
13 | .eq-item-2-b { animation-duration: 0.4s; }
14 | .eq-item-3-a { animation-duration: 0.3s; }
15 | .eq-item-3-b { animation-duration: 0.35s; }
16 | .eq-item-4-a { animation-duration: 0.4s; }
17 | .eq-item-4-b { animation-duration: 0.25s; }
18 |
19 | @keyframes ani-eq {
20 | 0% { transform: translateY(100%); }
21 | 100% { transform: translateY(0); }
22 | }
23 |
24 | @media only screen and (max-width: 991px) {
25 | .text-display .content{ height: 200px; text-align: center; }
26 | .text-display { height: 300px; }
27 | .btn-wrap .btn{ width: 100% !important; }
28 | .btn-wrap .btn:first-of-type{ margin-bottom: 10px; }
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/printer/.gitignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | __deployer__.zip
3 | .slack-env
4 | **/__mocks__/nim.js
5 |
--------------------------------------------------------------------------------
/printer/packages/admin/flush/index.js:
--------------------------------------------------------------------------------
1 | const nimbella = require('@nimbella/sdk'),
2 | redis = nimbella.redis()
3 |
4 | function main(args) {
5 | if (args && args.flush) {
6 | return redis.flushallAsync('ASYNC').then(_ => {
7 | console.log(_)
8 | return { ok: 'flushed' }
9 | })
10 | } else return { ok: true }
11 | }
12 |
--------------------------------------------------------------------------------
/printer/packages/printer/create/.include:
--------------------------------------------------------------------------------
1 | src
2 | package.json
3 | node_modules
4 |
--------------------------------------------------------------------------------
/printer/packages/printer/create/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "createPrintJob",
3 | "version": "1.0.0",
4 | "main": "./src/create.js",
5 | "dependencies": {
6 | "uuid": "^3.3.3"
7 | },
8 | "scripts": {
9 | "test": "jest"
10 | },
11 | "jest": {
12 | "collectCoverage": true,
13 | "collectCoverageFrom": [
14 | "src/*.js"
15 | ],
16 | "coverageThreshold": {
17 | "global": {
18 | "branches": 0,
19 | "lines": 0,
20 | "statements": 0
21 | }
22 | }
23 | },
24 | "devDependencies": {
25 | "chai": "^4.2.0",
26 | "jest": "^24.9.0",
27 | "@nimbella/sdk": "^1.2.7"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/printer/packages/printer/create/src/create.js:
--------------------------------------------------------------------------------
1 | const uuidv4 = require('uuid/v4'),
2 | nimbella = require('@nimbella/sdk'),
3 | redis = nimbella.redis()
4 |
5 | async function getSignedUrl(filename) {
6 | const bucket = await nimbella.storage()
7 | const file = bucket.file(filename)
8 | const expiration = 60 * 60 * 1000 // 60 minutes
9 |
10 | const putOptions = {
11 | version: 'v4',
12 | action: 'write',
13 | contentType: 'multipart/formdata; charset=UTF-8',
14 | expires: Date.now() + expiration
15 | }
16 |
17 | return file
18 | .getSignedUrl(putOptions)
19 | .then(_ => _[0])
20 | }
21 |
22 | const response = (body, statusCode) => ({ statusCode: statusCode || 200, body })
23 |
24 | async function main(args) {
25 | if (args && args.filename) {
26 | const fileId = uuidv4()
27 | const signedPutUrl = await getSignedUrl(fileId)
28 |
29 | const form = {
30 | id: fileId,
31 | filename: args.filename,
32 | material: args.material,
33 | customer: args.customer,
34 | process: args.process,
35 | costEstimate: args.cost,
36 | timeEstimate: args.time,
37 | status: 'created'
38 | }
39 |
40 | return redis
41 | .setAsync(fileId, JSON.stringify(form))
42 | .then(_ => redis.sadd('created', fileId))
43 | .then(_ => response({
44 | id: fileId,
45 | filename: args.filename,
46 | status: 'created',
47 | signedPutUrl
48 | }))
49 | .catch(e => {
50 | console.error(e)
51 | return response({ error: 'Internal error.' }, 500)
52 | })
53 | } else {
54 | return response({ error: 'Filename required.' }, 400)
55 | }
56 | }
57 |
58 | module.exports = {
59 | main: main,
60 | getSignedUrl: getSignedUrl
61 | }
62 |
--------------------------------------------------------------------------------
/printer/packages/printer/get/.include:
--------------------------------------------------------------------------------
1 | src
2 | package.json
3 | node_modules
4 |
--------------------------------------------------------------------------------
/printer/packages/printer/get/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "getPrintJob",
3 | "version": "1.0.0",
4 | "main": "src/get.js",
5 | "dependencies": {},
6 | "scripts": {
7 | "test": "jest"
8 | },
9 | "jest": {
10 | "collectCoverage": true,
11 | "collectCoverageFrom": [
12 | "src/*.js"
13 | ],
14 | "coverageThreshold": {
15 | "global": {
16 | "branches": 0,
17 | "lines": 0,
18 | "statements": 0
19 | }
20 | }
21 | },
22 | "devDependencies": {
23 | "chai": "^4.2.0",
24 | "jest": "^24.9.0",
25 | "@nimbella/sdk": "^1.2.7"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/printer/packages/printer/get/src/get.js:
--------------------------------------------------------------------------------
1 | const nimbella = require('@nimbella/sdk'),
2 | redis = nimbella.redis()
3 |
4 | const response = (body, statusCode) => ({ statusCode: statusCode || 200, body })
5 |
6 | async function getSignedUrl(filename) {
7 | const bucket = await nimbella.storage()
8 | const file = bucket.file(filename)
9 | const expiration = 10 * 60 * 1000 // 10 minutes
10 |
11 | const getOptions = {
12 | version: 'v4',
13 | action: 'read',
14 | expires: Date.now() + expiration
15 | }
16 |
17 | return file
18 | .getSignedUrl(getOptions)
19 | .then(_ => _[0])
20 | }
21 |
22 | function main(args) {
23 | if (args && args.id) {
24 | return redis
25 | .getAsync(args.id)
26 | .then(async (_) => {
27 | let form = JSON.parse(_)
28 | form.image = await getSignedUrl(form.id)
29 | return response(form)
30 | })
31 | .catch(error => {
32 | console.log(error)
33 | return response({ error: 'Invalid file id.' }, 404)
34 | })
35 | } else {
36 | return response({ error: 'File id required.' }, 400)
37 | }
38 | }
39 |
40 | module.exports = {
41 | main: main
42 | }
43 |
--------------------------------------------------------------------------------
/printer/packages/printer/get/test/get.test.js:
--------------------------------------------------------------------------------
1 | const chai = require('chai'),
2 | nimbella = require('@nimbella/sdk'),
3 | { main } = require('../src/get'),
4 | expect = chai.expect,
5 | assert = chai.assert
6 |
7 | describe('get print job by id', function() {
8 |
9 | it('generate bad request if missing file id', function() {
10 | let res = main()
11 | expect(res.statusCode).to.equal(400)
12 | expect(res.body).to.deep.equal({ error: 'File id required.' })
13 |
14 | res = main({})
15 | expect(res.statusCode).to.equal(400)
16 | expect(res.body).to.deep.equal({ error: 'File id required.' })
17 | })
18 |
19 | it('generate bad request if file id is invalid', function() {
20 | return main({id: 'bad id'})
21 | .then(res => {
22 | expect(res.statusCode).to.equal(404)
23 | expect(res.body).to.deep.equal({ error: 'Invalid file id.' })
24 | })
25 | })
26 |
27 | it('get print job by id', function() {
28 | let redis = nimbella.redis()
29 | let id = 'some id'
30 |
31 | redis.set(id, JSON.stringify({
32 | id,
33 | filename: 'some.file',
34 | status: 'created'
35 | }))
36 |
37 | return main({ id })
38 | .then(res => {
39 | expect(res.statusCode).to.equal(200)
40 | expect(res.body.id).to.equal(id)
41 | expect(res.body.filename).to.equal('some.file')
42 | expect(res.body.status).to.equal('created')
43 | expect(res.body.image).to.equal(`signed-${id}-read`)
44 | })
45 | })
46 | })
47 |
--------------------------------------------------------------------------------
/printer/packages/printer/list/.include:
--------------------------------------------------------------------------------
1 | src
2 | package.json
3 |
--------------------------------------------------------------------------------
/printer/packages/printer/list/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "listPrintJob",
3 | "version": "1.0.0",
4 | "main": "src/list.js",
5 | "dependencies": {},
6 | "scripts": {
7 | "test": "jest"
8 | },
9 | "jest": {
10 | "collectCoverage": true,
11 | "collectCoverageFrom": [
12 | "src/*.js"
13 | ],
14 | "coverageThreshold": {
15 | "global": {
16 | "branches": 0,
17 | "lines": 0,
18 | "statements": 0
19 | }
20 | }
21 | },
22 | "devDependencies": {
23 | "chai": "^4.2.0",
24 | "jest": "^24.9.0",
25 | "@nimbella/sdk": "^1.2.7"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/printer/packages/printer/list/src/list.js:
--------------------------------------------------------------------------------
1 | const nimbella = require('@nimbella/sdk'),
2 | redis = nimbella.redis()
3 |
4 | const response = (body, statusCode) => ({ statusCode: statusCode || 200, body })
5 |
6 | function isValidStatus(status) {
7 | return status === 'created' || status === 'approved' || status === 'rejected'
8 | }
9 |
10 | async function main(args) {
11 | let status = (args || {}).status
12 | if (isValidStatus(status)) {
13 | let list = {}
14 | list[status] = await redis.smembersAsync(status)
15 | return response(list)
16 | } else return response({ error: 'Valid status required.' }, 400)
17 | }
18 |
19 | module.exports = {
20 | main: main
21 | }
22 |
--------------------------------------------------------------------------------
/printer/packages/printer/list/test/list.test.js:
--------------------------------------------------------------------------------
1 | const chai = require('chai'),
2 | nimbella = require('@nimbella/sdk'),
3 | { main } = require('../src/list'),
4 | expect = chai.expect,
5 | assert = chai.assert
6 |
7 | describe('list print jobs by status', function() {
8 |
9 | it('generate bad request if no args given', function() {
10 | return main().then(res => {
11 | expect(res.statusCode).to.equal(400)
12 | expect(res.body).to.deep.equal({ error: 'Valid status required.' })
13 | })
14 | })
15 |
16 | it('generate bad request if empty args given', function() {
17 | return main({}).then(res => {
18 | expect(res.statusCode).to.equal(400)
19 | expect(res.body).to.deep.equal({ error: 'Valid status required.' })
20 | })
21 | })
22 |
23 | it('generate bad request if invalid status given', function() {
24 | return main({status: 'y'}).then(res => {
25 | expect(res.statusCode).to.equal(400)
26 | expect(res.body).to.deep.equal({ error: 'Valid status required.' })
27 | })
28 | })
29 |
30 | let statusSet = {
31 | 'created': [1, 2],
32 | 'approved': [3, 4],
33 | 'rejected': [5, 6]
34 | }
35 |
36 | Object.keys(statusSet).forEach(status => {
37 | it(`list ids for ${status} status`, function() {
38 | let redis = nimbella.redis()
39 |
40 | statusSet[status].forEach(v => redis.sadd(status, v))
41 |
42 | return main({ status })
43 | .then(res => {
44 | expect(res.statusCode).to.equal(200)
45 | expect(res.body[status]).to.deep.equal(statusSet[status])
46 | })
47 | })
48 | })
49 | })
50 |
--------------------------------------------------------------------------------
/printer/packages/printer/update/.include:
--------------------------------------------------------------------------------
1 | src
2 | package.json
3 | node_modules
4 |
--------------------------------------------------------------------------------
/printer/packages/printer/update/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "updatePrintJob",
3 | "version": "1.0.0",
4 | "main": "src/update.js",
5 | "dependencies": {},
6 | "scripts": {
7 | "test": "jest"
8 | },
9 | "jest": {
10 | "collectCoverage": true,
11 | "collectCoverageFrom": [
12 | "src/*.js"
13 | ],
14 | "coverageThreshold": {
15 | "global": {
16 | "branches": 0,
17 | "lines": 0,
18 | "statements": 0
19 | }
20 | }
21 | },
22 | "devDependencies": {
23 | "chai": "^4.2.0",
24 | "jest": "^24.9.0",
25 | "@nimbella/sdk": "^1.2.7"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/printer/packages/printer/update/src/update.js:
--------------------------------------------------------------------------------
1 | const nimbella = require('@nimbella/sdk'),
2 | redis = nimbella.redis()
3 |
4 | const response = (body, statusCode) => ({ statusCode: statusCode || 200, body })
5 |
6 | function isValidStatus(status) {
7 | return status === 'created' || status === 'approved' || status === 'rejected'
8 | }
9 |
10 | function main(args) {
11 | if (args && args.id && isValidStatus(args.status)) {
12 | return redis
13 | .getAsync(args.id)
14 | .then(async (_) => {
15 | let form = JSON.parse(_)
16 | let fileId = form.id
17 | let newStatus = args.status
18 | let oldStatus = form.status
19 | form.status = newStatus
20 | return redis
21 | .setAsync(fileId, JSON.stringify(form))
22 | .then(_ => {
23 | redis.sadd(newStatus, fileId)
24 | redis.srem(oldStatus, fileId)
25 | return response({
26 | id: fileId,
27 | filename: form.filename,
28 | status: newStatus
29 | })
30 | })
31 | })
32 | .catch(error => {
33 | console.log(error)
34 | return response({ error: 'Invalid file id.' }, 404)
35 | })
36 | } else {
37 | return response({ error: 'File id and valid status required.' }, 400)
38 | }
39 | }
40 |
41 | module.exports = {
42 | main: main
43 | }
44 |
--------------------------------------------------------------------------------
/printer/project.yml:
--------------------------------------------------------------------------------
1 | bucket:
2 | strip: 1
3 | packages:
4 | - name: admin
5 | actions:
6 | - name: flush
7 | web: false
8 |
9 | - name: printer
10 | actions:
11 | - name: notify
12 | parameters: ${<.slack-env}
13 |
--------------------------------------------------------------------------------
/printer/web/.built:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/printer/web/.built
--------------------------------------------------------------------------------
/printer/web/.gitignore:
--------------------------------------------------------------------------------
1 | /*.js
2 | /*.css
3 | /*.html
4 | /*.ico
5 | /static/
6 | /build/
7 | /*manifest.json
--------------------------------------------------------------------------------
/printer/web/.include:
--------------------------------------------------------------------------------
1 | build
2 |
--------------------------------------------------------------------------------
/printer/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "printer",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.21.1",
7 | "bootstrap": "^3.4.1",
8 | "konva": "^4.0.12",
9 | "prop-types": "^15.7.2",
10 | "react": "^16.8.3",
11 | "react-dom": "^16.8.3",
12 | "react-file-uploader": "^1.0.0",
13 | "react-html5-camera-photo": "^1.3.0",
14 | "react-konva": "^16.9.0-1",
15 | "react-scripts": "^3.0.1"
16 | },
17 | "scripts": {
18 | "start": "PORT=4000 react-scripts start",
19 | "build": "react-scripts build",
20 | "test": "react-scripts test",
21 | "eject": "react-scripts eject"
22 | },
23 | "eslintConfig": {
24 | "extends": "react-app"
25 | },
26 | "browserslist": [
27 | ">0.2%",
28 | "not dead",
29 | "not ie <= 11",
30 | "not op_mini all"
31 | ],
32 | "proxy": "http://localhost:3000",
33 | "devDependencies": {}
34 | }
35 |
--------------------------------------------------------------------------------
/printer/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/printer/web/public/favicon.ico
--------------------------------------------------------------------------------
/printer/web/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
25 | Nimbella - Job Processing Demo
26 |
27 |
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/printer/web/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Job Processing Demo",
3 | "name": "Nimballe - Job Processing Demo",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#13292d",
14 | "background_color": "#222222"
15 | }
16 |
--------------------------------------------------------------------------------
/printer/web/src/actions/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | const apiRoot = `/api/printer`;
3 |
4 | export const upload = async (file) => {
5 | console.log('uploading:', file)
6 | const { id, signedPutUrl } = await getSignedUrl(file.name);
7 | return axios.put(
8 | signedPutUrl,
9 | file,
10 | {
11 | headers: {
12 | 'Content-Type': 'multipart/formdata; charset=UTF-8',
13 | 'Access-Control-Allow-Origin': '*',
14 | 'Cross-Domain': true
15 | }
16 | }
17 | ).then(res => {
18 | return notify(id).then(_ => ({ statusCode: 200, fileId: id }))
19 | })
20 | };
21 |
22 | export const getSignedUrl = (filename) => {
23 | return fetch(`${apiRoot}/create?filename=${filename}`).then(_ => _.json())
24 | };
25 |
26 | export const notify = (fileId) => {
27 | console.log('sending notification for', fileId)
28 | return fetch(`${apiRoot}/notify.json`, {
29 | method: 'POST',
30 | body: JSON.stringify({
31 | text: `Job created with id ${fileId}.`
32 | }),
33 | headers: {
34 | 'Content-Type': 'application/json'
35 | }
36 | }).then(handleResponse)
37 | };
38 |
39 | const handleResponse = (res) => {
40 | if(res.ok) {
41 | return res.json();
42 | } else {
43 | try{
44 | let error = new Error(res.statusText);
45 | error['response'] = res;
46 | return error;
47 | }
48 | catch(err) {};
49 | }
50 | };
51 |
--------------------------------------------------------------------------------
/printer/web/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import logo from '../images/Nimbella-Logo.svg';
3 |
4 | import '../stylesheets/Header.css';
5 |
6 | class Header extends Component {
7 | render() {
8 | return (
9 |
17 | );
18 | }
19 | }
20 |
21 | export default Header;
22 |
--------------------------------------------------------------------------------
/printer/web/src/images/covert-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/printer/web/src/images/upload-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
--------------------------------------------------------------------------------
/printer/web/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './components/App';
4 |
5 | import 'bootstrap/dist/css/bootstrap.css';
6 | import './stylesheets/style.css';
7 |
8 | import * as serviceWorker from './serviceWorker';
9 |
10 | ReactDOM.render(, document.getElementById('root'));
11 |
12 | // If you want your app to work offline and load faster, you can change
13 | // unregister() to register() below. Note this comes with some pitfalls.
14 | // Learn more about service workers: http://bit.ly/CRA-PWA
15 | serviceWorker.unregister();
16 |
--------------------------------------------------------------------------------
/printer/web/src/stylesheets/FileUpload.css:
--------------------------------------------------------------------------------
1 | .file-upload{ width: 100%; height: 400px; min-height: 100px; border: 1px dashed #A2B5B8; border-radius: 5px; position: relative; overflow-y: auto; background-color: #222222; box-shadow: inset 4px 4px 10px rgba(0,0,0,1); display: flex; align-items: center; }
2 |
3 | .file-upload .spinner-border{ position: absolute; z-index: 99; margin-left: calc(50% - 1.5rem); }
4 | .dnd{ width: 100%; height: 400px; padding: 10px; display: table; text-align: center; }
5 | .dnd .content{ display: table-cell; vertical-align: middle; }
6 | .dnd .upload-icon{ display: inline-block; width: 128px; height: 77px; background: transparent url(../images/upload-icon.svg) center center no-repeat; clear: both; }
7 |
8 | .react-html5-camera-photo{ position: fixed; top: 0; left: 0; width: 100%; height: 100%; padding-top: 0; padding-bottom: 40px; z-index: 9999; background-color: #222222; }
9 | .react-html5-camera-photo video{ width: 100%; }
10 |
11 | .file-upload .close{ position: fixed; top: 20px; right: 20px; z-index: 99999; opacity: .5; -webkit-transition: opacity .3s ease-in;-moz-transition: opacity .3s ease-in;-ms-transition: opacity .3s ease-in;-o-transition: opacity .3s ease-in;transition: opacity .3s ease-in;}
12 | .file-upload .close:hover{ opacity: 1; }
13 | .file-upload .close span{ color: white; font-size: 60px; }
14 |
15 | @media only screen and (max-width: 1023px) {
16 | .file-upload{ border: none; }
17 | .file-upload.mobile{ background-color: transparent; box-shadow: none; }
18 | }
19 |
20 | @media only screen and (max-width: 812px) {
21 | .file-upload{ height: auto; margin-bottom: 20px; }
22 | .dnd{ height: auto; }
23 | }
24 |
--------------------------------------------------------------------------------
/printer/web/src/stylesheets/Header.css:
--------------------------------------------------------------------------------
1 | .navbar{ border-radius: 0; }
2 | .navbar .logo-text{ display: inline-block; margin:0; padding: 20px 0; min-height:50px; }
3 | .navbar .navbar-brand{ padding: 0; height: 55px; }
4 | .navbar .navbar-brand:hover{ color: #55BFD1; }
5 | .navbar-nav{ list-style: none; float: right; min-height: 50px; padding: 20px; margin-right: 15px; }
6 | .navbar-nav .nav-item{ margin-left: 40px; }
7 |
8 |
9 | @media only screen and (max-width: 736px) {
10 | .navbar-nav{ padding: 12px 12px 0 0; margin: 0; float: right !important; }
11 | }
12 |
13 | @media only screen and (max-width: 414px) {
14 | .navbar-brand{ padding: 15px 5px; }
15 | .navbar-collapse{ float: left; }
16 | .navbar-collapse .nav-item{ margin-left: 10px; }
17 | }
--------------------------------------------------------------------------------
/printer/web/src/stylesheets/ImageDisplay.css:
--------------------------------------------------------------------------------
1 | .image-display{ width: 100%; height: 400px; min-height: 100px; border: 1px dashed #A2B5B8; border-radius: 5px; position: relative; overflow-y: auto; background-color: #222222; box-shadow: inset 4px 4px 10px rgba(0,0,0,1); margin-bottom: 20px; }
2 | .image-display.mobile{ background-color: transparent; box-shadow: none; }
3 | .image-display .spinner-border{ position: absolute; z-index: 99; }
4 | .dnd{ width: 100%; height: 400px; padding: 10px; display: table; text-align: center; }
5 | .dnd .content{ display: table-cell; vertical-align: middle; }
6 | .dnd .upload-icon{ display: inline-block; width: 128px; height: 77px; background: transparent url(../images/upload-icon.svg) center center no-repeat; clear: both; }
7 |
8 | /*textOverlay*/
9 | .image-display{ display: flex; align-items: center; justify-content: center; }
10 | .image-display .image{ margin: auto; }
11 |
12 | .react-html5-camera-photo{ position: fixed; top: 0; left: 0; width: 100%; height: 100%; padding-top: 0; padding-bottom: 40px; z-index: 9999; background-color: #222222; }
13 | .react-html5-camera-photo video{ width: 100%; }
14 | .image-display .close{ position: fixed; top: 20px; right: 20px; z-index: 99999; opacity: .5; -webkit-transition: opacity .3s ease-in;-moz-transition: opacity .3s ease-in;-ms-transition: opacity .3s ease-in;-o-transition: opacity .3s ease-in;transition: opacity .3s ease-in;}
15 | .image-display .close:hover{ opacity: 1; }
16 | .image-display .close span{ color: white; font-size: 60px; }
17 |
18 | @media only screen and (max-width: 768px) {
19 | .image-display{ border: none; }
20 | }
21 |
22 | @media only screen and (max-width: 812px) {
23 | .image-display{ height: auto; margin-bottom: 20px; }
24 | .dnd{ height: auto; }
25 | }
26 |
--------------------------------------------------------------------------------
/printer/web/src/stylesheets/Result.css:
--------------------------------------------------------------------------------
1 | .result{ display: inline-block; height: 400px; border: 1px solid #A2B5B8; width: 100%; padding: 20px; border-radius: 5px; position: relative; white-space: pre-line; margin-bottom: 20px; background-color: #222222; box-shadow: inset 4px 4px 10px rgba(0,0,0,1); }
2 | .result .content{ overflow-y: auto; height: 360px; }
3 |
4 | /*Equalizer Animation*/
5 | .eq-icon { align-items: flex-end; display: flex; width: 20px; height: 12px; overflow: hidden; opacity: 0.8; float: right; }
6 | .eq-icon .eq-col { flex: 1; position: relative; height: 100%; margin-right: 1px; }
7 | .eq-icon .eq-col div { animation-name: ani-eq; animation-timing-function: linear; animation-iteration-count: infinite; animation-direction: alternate; background-color: #a2b5b8; position: absolute; width: 100%; height: 100%; transform: translateY(100%); will-change: transform; }
8 | .eq-item-1-a { animation-duration: 0.3s; }
9 | .eq-item-1-b { animation-duration: 0.45s; }
10 | .eq-item-2-a { animation-duration: 0.5s; }
11 | .eq-item-2-b { animation-duration: 0.4s; }
12 | .eq-item-3-a { animation-duration: 0.3s; }
13 | .eq-item-3-b { animation-duration: 0.35s; }
14 | .eq-item-4-a { animation-duration: 0.4s; }
15 | .eq-item-4-b { animation-duration: 0.25s; }
16 |
17 | @keyframes ani-eq {
18 | 0% { transform: translateY(100%); }
19 | 100% { transform: translateY(0); }
20 | }
21 |
22 | @media only screen and (max-width: 991px) {
23 | .result, .result .content{ height: auto; text-align: center; }
24 | .btn-wrap .btn{ width: 100% !important; }
25 | .btn-wrap .btn:first-of-type{ margin-bottom: 10px; }
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/qrcode-multi-lang/packages/default/qr-java/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | build
3 | .gradle
4 | .include
5 |
--------------------------------------------------------------------------------
/qrcode-multi-lang/packages/default/qr-java/build.cmd:
--------------------------------------------------------------------------------
1 | SET BUILD="maven"
2 |
3 | IF BUILD == "maven" THEN
4 | goto :mavenBuild
5 |
6 | IF BUILD == "gradle" THEN
7 | goto :gradleBuild
8 | ELSE
9 | echo Unknown Build
10 |
11 | :gradleBuild
12 | gradlew jar
13 | @echo build/libs/qr-java-1.0.jar > .include
14 | goto :eof
15 |
16 | :mavenBuild
17 | mvn install
18 | @echo target/qr-1.0.0-jar-with-dependencies.jar > .include
19 | goto :eof
--------------------------------------------------------------------------------
/qrcode-multi-lang/packages/default/qr-java/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'java'
2 |
3 | sourceCompatibility = 1.8
4 | targetCompatibility = 1.8
5 |
6 | version = '1.0'
7 |
8 | repositories {
9 | mavenCentral()
10 | }
11 |
12 | configurations {
13 | provided
14 | compile.extendsFrom provided
15 | }
16 |
17 | dependencies {
18 | provided 'com.google.code.gson:gson:2.6.2'
19 | compile 'com.google.zxing:core:3.3.0'
20 | compile 'com.google.zxing:javase:3.3.0'
21 | }
22 |
23 | jar {
24 | dependsOn configurations.runtime
25 |
26 | from {
27 | (configurations.runtime - configurations.provided).collect {
28 | it.isDirectory() ? it : zipTree(it)
29 | }
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/qrcode-multi-lang/packages/default/qr-java/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | # set to 'maven' to use maven
6 | # set to 'gradle' to use gradle
7 | BUILD="maven"
8 |
9 | if [ $BUILD == "maven" ]; then
10 | mvn install
11 | echo target/qr-1.0.0-jar-with-dependencies.jar > .include
12 | else
13 | if [ $BUILD == "gradle" ]; then
14 | gradle jar
15 | echo build/libs/qr-java-1.0.jar > .include
16 | else
17 | echo unknown builder
18 | exit -1
19 | fi
20 | fi
21 |
--------------------------------------------------------------------------------
/qrcode-multi-lang/packages/default/qr-java/settings.gradle:
--------------------------------------------------------------------------------
1 | // Deliberately empty
2 |
--------------------------------------------------------------------------------
/qrcode-multi-lang/packages/default/qr-java/src/main/java/qr/Generate.java:
--------------------------------------------------------------------------------
1 | package qr;
2 |
3 | import java.io.*;
4 | import java.util.Base64;
5 |
6 | import com.google.gson.JsonObject;
7 |
8 | import com.google.zxing.*;
9 | import com.google.zxing.client.j2se.MatrixToImageWriter;
10 | import com.google.zxing.common.BitMatrix;
11 |
12 | public class Generate {
13 | public static JsonObject main(JsonObject args) throws Exception {
14 | String property = "text";
15 | String text = "Hello. Try with a 'text' value next time.";
16 | if (args.has(property)) {
17 | text = args.get(property).toString();
18 | }
19 |
20 | ByteArrayOutputStream baos = new ByteArrayOutputStream();
21 | OutputStream b64os = Base64.getEncoder().wrap(baos);
22 |
23 | BitMatrix matrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, 300, 300);
24 | MatrixToImageWriter.writeToStream(matrix, "png", b64os);
25 | b64os.close();
26 |
27 | String output = baos.toString("utf-8");
28 |
29 | JsonObject response = new JsonObject();
30 | JsonObject headers = new JsonObject();
31 | headers.addProperty("content-type", "text/html; charset=UTF-8");
32 | response.add("headers", headers);
33 | response.addProperty("body", "data:image/png;base64," + output);
34 | return response;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/qrcode-multi-lang/packages/default/qr/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qr",
3 | "version": "1.0.0",
4 | "description": "Serverless QR generator",
5 | "main": "qr.js",
6 | "dependencies": {
7 | "qrcode": "^1.3.2"
8 | },
9 | "devDependencies": {}
10 | }
11 |
--------------------------------------------------------------------------------
/qrcode-multi-lang/packages/default/qr/qr.js:
--------------------------------------------------------------------------------
1 | const qrcode = require('qrcode')
2 |
3 | exports.main = (args) => {
4 | return qrcode.toDataURL(args.text).then(res => ({
5 | headers: { 'content-type': 'text/html; charset=UTF-8' },
6 | body: args.img == undefined ? res : `
`
7 | }))
8 | }
9 |
10 | if (process.env.TEST) exports.main({text:"hello"}).then(console.log)
11 |
--------------------------------------------------------------------------------
/qrcode-multi-lang/project.yml:
--------------------------------------------------------------------------------
1 | packages:
2 | - name: default
3 | actions:
4 | - name: qr-java
5 | runtime: java:default
6 | main: qr.Generate#main
7 | - name: qr
8 | runtime: nodejs:default
9 |
--------------------------------------------------------------------------------
/qrcode-multi-lang/web/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/qrcode-multi-lang/web/logo.png
--------------------------------------------------------------------------------
/qrcode/packages/default/qr/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qr",
3 | "version": "1.0.0",
4 | "description": "Serverless QR generator",
5 | "main": "qr.js",
6 | "dependencies": {
7 | "qrcode": "^1.3.2"
8 | },
9 | "devDependencies": {}
10 | }
11 |
--------------------------------------------------------------------------------
/qrcode/packages/default/qr/qr.js:
--------------------------------------------------------------------------------
1 | const qrcode = require('qrcode')
2 |
3 | exports.main = (args) => {
4 | return qrcode.toDataURL(args.text).then(res => ({
5 | headers: { 'content-type': 'text/html; charset=UTF-8' },
6 | body: args.img == undefined ? res : `
`
7 | }))
8 | }
9 |
10 | if (process.env.TEST) exports.main({text:"hello"}).then(console.log)
11 |
--------------------------------------------------------------------------------
/qrcode/web/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/qrcode/web/logo.png
--------------------------------------------------------------------------------
/trade/README.md:
--------------------------------------------------------------------------------
1 | ## Nimbella Trading Demo ##
2 |
3 | This demo allows a user to buy and sell stock (virtually), shows updated stock positions and also displays information about a given stock.
4 |
5 | The front-end is written in React.
6 |
7 | The back-end is a set of serverless functions written in JavaScript.
8 |
9 | The back-end functions connect to the IEX Cloud to obtain live stock information. The IEX Cloud requires an API token to get stock information. An IEX Cloud API token is available for free by:
10 |
11 | 1. Visit https://iexcloud.io
12 | 2. Click 'Get started'
13 | 3. Enter an email address and password
14 | 4. Select the '$0/Start' option
15 | 5. Verify your email address by click on the link in the email sent to you
16 | 6. Click 'API tokens' in the IEX web interface that comes up
17 | 7. In the interface, you will see your secret API token
18 |
19 | Place the API token for the IEX Cloud into the environment as environment variable `IEXCLOUD_API_TOKEN` when using the Nimbella deployer to deploy this project. One way to do this is to use a `.env` file as described in the Nimbella deployer documentation.
20 |
21 | The functions use the Nimbella Redis key-value store to manage persisted data.
22 |
23 | A Swagger API definition for the back-end functions is contained in the swagger .yaml file
24 |
25 | ## License ##
26 |
27 | Copyright 2019 Nimbella Corp
28 |
29 | Licensed under the Apache License, Version 2.0 (the "License");
30 | you may not use this file except in compliance with the License.
31 | You may obtain a copy of the License at
32 |
33 | https://www.apache.org/licenses/LICENSE-2.0
34 |
35 | Unless required by applicable law or agreed to in writing, software
36 | distributed under the License is distributed on an "AS IS" BASIS,
37 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
38 | See the License for the specific language governing permissions and
39 | limitations under the License.
40 |
--------------------------------------------------------------------------------
/trade/packages/tradedemo/getCashBalance.js:
--------------------------------------------------------------------------------
1 | const nim = require('@nimbella/sdk')
2 |
3 | function getCashBalance(rc, account_id) {
4 | const balanceKey = 'trade_demo_balance/' + account_id
5 | return rc.getAsync(balanceKey).then(function(balance) {
6 | if (balance != undefined) {
7 | balance = parseFloat(balance).toFixed(2)
8 | }
9 | return { balance }
10 | })
11 | }
12 |
13 | function main(params) {
14 | let account_id = params.account_id
15 |
16 | const rc = nim.redis()
17 | return getCashBalance(rc, account_id).then(function(balance) {
18 | return balance
19 | })
20 | }
21 |
--------------------------------------------------------------------------------
/trade/packages/tradedemo/getStockDescription.js:
--------------------------------------------------------------------------------
1 | const needle = require('needle')
2 |
3 | function main(params) {
4 | let apitoken = params.apitoken
5 | let ticker = params.ticker.toUpperCase()
6 |
7 | let func = `https://cloud.iexapis.com/stable/stock/market/batch?token=${apitoken}&types=stats,quote,company&symbols=`
8 |
9 | return needle('get', func + ticker)
10 | .then(function(response) {
11 | let r = response.body[ticker]
12 | return {
13 | ticker: ticker,
14 | desc: r.company.description,
15 | gain: r.quote.change,
16 | gainP: r.quote.changePercent,
17 | prevClose: r.quote.previousClose,
18 | open: r.quote.open,
19 | close: r.quote.close,
20 | peR: r.quote.peRatio,
21 | divY: r.stats.dividendYield,
22 | mktCap: r.quote.marketCap,
23 | volume: r.quote.latestVolume,
24 | aveVolume: r.quote.avgTotalVolume,
25 | wkH: r.quote.week52High,
26 | wkL: r.quote.week52Low
27 | }
28 | })
29 | .catch(function(err) {
30 | return { "status": "failed, " + err }
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/trade/packages/tradedemo/getStockHistory.js:
--------------------------------------------------------------------------------
1 | function main(params) {
2 | let apitoken = params.apitoken
3 | let ticker = params.ticker.toUpperCase()
4 |
5 | let func = `https://cloud.iexapis.com/stable/stock/${ticker}/chart/1y?token=${apitoken}`
6 | var needle = require('needle')
7 | return needle('get', func)
8 | .then(function(response) {
9 | return { prices: response.body }
10 | })
11 | .catch(function(err) {
12 | return { "status": "failed, " + err }
13 | })
14 | }
15 |
--------------------------------------------------------------------------------
/trade/packages/tradedemo/newAccount.js:
--------------------------------------------------------------------------------
1 | const nim = require('@nimbella/sdk')
2 |
3 | function newAccountBalance(rc, account_id, newBalance) {
4 | const balanceKey = 'trade_demo_balance/' + account_id
5 | return rc.setAsync(balanceKey, 10000).then(function() {
6 | return { account_id }
7 | })
8 | }
9 |
10 | function main(params) {
11 | const crypto = require("crypto")
12 | const account_id = crypto.randomBytes(16).toString("hex")
13 |
14 | const rc = nim.redis()
15 | return newAccountBalance(rc, account_id, 10000).then(function(res) {
16 | return res
17 | })
18 | }
19 |
--------------------------------------------------------------------------------
/trade/project.yml:
--------------------------------------------------------------------------------
1 | bucket:
2 | strip: 1
3 | packages:
4 | - name: tradedemo
5 | parameters:
6 | apitoken: ${IEXCLOUD_API_TOKEN}
7 |
8 |
--------------------------------------------------------------------------------
/trade/web/.gitignore:
--------------------------------------------------------------------------------
1 | /*.js
2 | /*.css
3 | /*.html
4 | /*.ico
5 | /static/
6 | /build/
7 | /*manifest.json
--------------------------------------------------------------------------------
/trade/web/.include:
--------------------------------------------------------------------------------
1 | build
2 |
--------------------------------------------------------------------------------
/trade/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "demo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "axios": "^0.21.1",
7 | "d3-format": "^1.3.2",
8 | "d3-scale": "^2.2.2",
9 | "d3-shape": "^1.3.3",
10 | "prop-types": "^15.6.2",
11 | "react": "^16.7.0",
12 | "react-dom": "^16.7.0",
13 | "react-number-format": "^4.0.6",
14 | "react-scripts": "2.1.3",
15 | "react-select": "^2.3.0",
16 | "react-stockcharts": "^0.7.8",
17 | "react-tooltip": "^3.9.2"
18 | },
19 | "scripts": {
20 | "start": "react-scripts start",
21 | "build": "react-scripts build"
22 | },
23 | "eslintConfig": {
24 | "extends": "react-app"
25 | },
26 | "browserslist": [
27 | ">0.2%",
28 | "not dead",
29 | "not ie <= 11",
30 | "not op_mini all"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/trade/web/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/trade/web/public/favicon.ico
--------------------------------------------------------------------------------
/trade/web/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Nimbella Demo",
3 | "name": "Nimbella Trade Demo",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#132a2d",
14 | "background_color": "#090d16"
15 | }
16 |
--------------------------------------------------------------------------------
/trade/web/src/action/numberFormat.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import NumberFormat from 'react-number-format';
3 |
4 | export const dollarSignWithDecimal = (val, decimal) => {
5 | return (
6 | (val !== 0 && !val)?'N/A':
7 |
15 | )
16 | };
17 |
18 | export const dollarPositiveSignWithDecimal = (val, decimal) => {
19 | return (
20 | (val !== 0 && !val)?'N/A':
21 | 0)?'+$':'$' }
26 | decimalScale={ decimal }
27 | fixedDecimalScale={ true }
28 | />
29 | )
30 | };
31 |
32 | export const decimalOnly = (val, decimal) => {
33 | return (
34 | (val === 0 || !val)?'N/A':
35 |
42 | )
43 | };
44 |
45 | export const percentWithDecimal = (val, decimal) => {
46 | return (
47 | (val !== 0 && !val)?'N/A':
48 |
55 | )
56 | };
57 |
58 | export const shortNum = (num, decimal) => {
59 | let val = Math.abs(Number(num)) >= 1.0e+9
60 | ? (Math.abs(Number(num)) / 1.0e+9).toFixed(decimal) + "B"
61 | : Math.abs(Number(num)) >= 1.0e+6
62 | ? (Math.abs(Number(num)) / 1.0e+6).toFixed(decimal) + "M"
63 | : Math.abs(Number(num)) >= 1.0e+3
64 | ? (Math.abs(Number(num)) / 1.0e+3).toFixed(decimal) + "K"
65 | : Math.abs(Number(num));
66 | return (val === 0)?'N/A':val;
67 | };
--------------------------------------------------------------------------------
/trade/web/src/components/About.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | class About extends Component {
5 | static propTypes = {
6 | text: PropTypes.string.isRequired
7 | };
8 | render() {
9 | return (
10 |
11 |
12 |
About
13 |
{ this.props.text }
14 |
15 |
16 | );
17 | }
18 | }
19 |
20 | export default About;
--------------------------------------------------------------------------------
/trade/web/src/components/ErrorBoundary.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 |
3 | export default class ErrorBoundary extends Component {
4 | constructor(props) {
5 | super(props);
6 | this.state = { error: null, errorInfo: null };
7 | }
8 |
9 | componentDidCatch(error, errorInfo) {
10 | // You can also log the error to an error reporting service
11 | this.setState({
12 | error: error,
13 | errorInfo: errorInfo
14 | })
15 | }
16 |
17 | render() {
18 | if (this.state.errorInfo) {
19 | // Error path
20 | return (
21 |
22 |
Oops, something went wrong.
23 |
24 | { this.state.error && this.state.error.toString() }
25 |
26 | { this.state.errorInfo.componentStack }
27 |
28 |
29 | );
30 | }
31 | // Normally, just render children
32 | return this.props.children;
33 | }
34 | }
--------------------------------------------------------------------------------
/trade/web/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { dollarSignWithDecimal } from "../action/numberFormat";
3 | import PropTypes from 'prop-types';
4 |
5 | import '../css/Header.css';
6 | import logo from '../images/Nimbella-Logo.svg';
7 |
8 | class Header extends Component {
9 | constructor(props) {
10 | super(props);
11 |
12 | this.state = {
13 | isExpand: false
14 | }
15 | }
16 |
17 | static propTypes = {
18 | data: PropTypes.object.isRequired
19 | };
20 |
21 | render() {
22 | const { data } = this.props;
23 | const { isExpand } = this.state;
24 | return (
25 |
41 | );
42 | }
43 | }
44 |
45 | export default Header;
46 |
--------------------------------------------------------------------------------
/trade/web/src/components/Ticker.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { dollarSignWithDecimal, percentWithDecimal } from "../action/numberFormat";
4 |
5 | class Ticker extends Component {
6 | static propTypes = {
7 | ticker: PropTypes.string.isRequired,
8 | gain: PropTypes.number.isRequired,
9 | gainP: PropTypes.number.isRequired
10 | };
11 | render() {
12 | const { ticker, gain, gainP } = this.props;
13 | return (
14 |
15 |
16 |
17 |
{ ticker }
18 |
19 |
20 |
21 | Gain = 0)? 'text-positive': 'text-negative' }`}>
22 | { dollarSignWithDecimal(gain, 2) }
23 | ({ percentWithDecimal(gainP, 2) })
24 |
25 |
26 |
27 |
28 | );
29 | }
30 | }
31 |
32 | export default Ticker;
--------------------------------------------------------------------------------
/trade/web/src/components/submit/Submit.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Buy from './Buy';
4 | import Sell from './Sell';
5 |
6 | class Submit extends Component {
7 | constructor(props) {
8 | super(props);
9 |
10 | this.state = {
11 | buy: false,
12 | sell: false
13 | }
14 | }
15 |
16 | handleShowBuyForm = (e) => {
17 | e.preventDefault();
18 | this.setState({ buy: true })
19 | };
20 |
21 | handleShowSellForm = (e) => {
22 | e.preventDefault();
23 | this.setState({ sell: true })
24 | };
25 |
26 | handleClose = () => {
27 | this.setState({ buy: false, sell: false })
28 | };
29 |
30 | static propTypes = {
31 | handleDataUpdate: PropTypes.func.isRequired,
32 | ifData: PropTypes.bool.isRequired,
33 | tickerList: PropTypes.array.isRequired
34 | };
35 |
36 | render() {
37 | const { buy, sell } = this.state;
38 | const { handleDataUpdate, ifData, tickerList } = this.props;
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | { ifData && }
50 |
51 |
52 |
53 | );
54 | }
55 | }
56 |
57 | export default Submit;
--------------------------------------------------------------------------------
/trade/web/src/css/Charts.css:
--------------------------------------------------------------------------------
1 | .react-stockchart{ background-color: #090D16 }
2 | .tick link{stroke: #B2B2B2 !important;}
3 | .tick text{fill: #B2B2B2 !important;}
4 | .list-group .list-group-item{ background-color: transparent; border: none; color: #B2B2B2;}
5 | .list-group .list-group-item .badge{background-color: transparent; border-radius:0; font-size:inherit; color: #B2B2B2;}
--------------------------------------------------------------------------------
/trade/web/src/css/Header.css:
--------------------------------------------------------------------------------
1 | .navbar .logo-text{ display: inline-block; margin:0; padding: 20px 0; min-height:50px; }
2 | .navbar .navbar-brand{ padding: 0; height: 55px; }
3 | .navbar .navbar-brand:hover{ color: #55BFD1; }
4 | .navbar-nav{ list-style: none; float: right; min-height: 50px; padding: 20px; margin-right: 15px; }
5 | .navbar-nav .nav-item{ margin-left: 40px; }
6 | .more-icon{ width: 30px; height: 30px; margin-top: 10px; float: right; }
7 |
8 | @media only screen and (max-width: 736px) {
9 | .navbar-nav{ padding: 12px 12px 0 0; margin: 0; float: right !important; }
10 | }
11 |
12 | @media only screen and (max-width: 414px) {
13 | .navbar-brand{ padding: 15px 5px; }
14 | .navbar-collapse{ float: left; }
15 | .navbar-collapse .nav-item{ margin-left: 10px; }
16 | }
--------------------------------------------------------------------------------
/trade/web/src/css/Sidebar.css:
--------------------------------------------------------------------------------
1 | .sidebar-wrap li{ display: inline-table; width: 100%; padding: 20px 10px; border-bottom: 1px solid #090D16; cursor: pointer; position: relative; }
2 |
3 | .sidebar-wrap li:hover, .sidebar-wrap li.active{ background-color: #090D16; }
4 | .sidebar-wrap li:hover span.symbol, .sidebar-wrap li.active span.symbol{ color: #55BFD1; }
5 |
6 | .sidebar-wrap li span.text-box{ font-size: 14px; color: white; padding: 5px 10px; border-radius: 5px; font-weight: 300; float: right; min-width: 80px; text-align: center; }
7 |
8 | .sidebar-wrap li span.text-box.positive{ background-color: #417505; }
9 | .sidebar-wrap li span.text-box.negative{ background-color: #9F2223; }
10 |
11 | .sidebar-wrap .ttip{ background-color: rgba(85,191,209, .5); }
12 | .sidebar-wrap .ttip.place-bottom::after{ border-bottom-color: rgba(85,191,209, .5); }
13 |
14 | .sidebar-wrap .nav-sidebar{ padding-bottom: 50px; }
15 |
16 | @media only screen and (max-width: 414px) {
17 | .sidebar-wrap .nav-sidebar{ padding-bottom: 0; padding-top: 50px; }
18 | }
--------------------------------------------------------------------------------
/trade/web/src/css/Submit.css:
--------------------------------------------------------------------------------
1 | .submit-wrap{ position: fixed; bottom: 0; z-index: 9999; }
2 | .submit-wrap .btn-wrap.bg{ padding: 15px; background-color: #132a2d; }
3 | .mask{ display: inline-block; width: 100%; height: 100%; background-color: rgba(4,28,33,.8); position: fixed; top: 0; left: 0; z-index: 9999; visibility: hidden; opacity: 0; }
4 | .mask.show{ visibility: visible; opacity: 1; }
5 | .mask.hide{ visibility: hidden; opacity: 0; }
6 |
7 | .mask .header h3{ display: inline;}
8 |
9 | .mask .form-wrap{ background-color: white; margin-top: 5%; padding: 2% 3%; border-radius: 5px; }
10 |
11 | .form-group.one-line span { display: inline-block; }
12 |
13 | .form-group.has-error div:first-child{ border-color: #a94442; }
--------------------------------------------------------------------------------
/trade/web/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 |
4 | import * as serviceWorker from './serviceWorker';
5 |
6 | import App from './components/App'
7 |
8 | ReactDOM.render(, document.getElementById('root'));
9 |
10 | // If you want your app to work offline and load faster, you can change
11 | // unregister() to register() below. Note this comes with some pitfalls.
12 | // Learn more about service workers: http://bit.ly/CRA-PWA
13 | serviceWorker.unregister();
14 |
--------------------------------------------------------------------------------
/visits/packages/visits/counter.php:
--------------------------------------------------------------------------------
1 | redis();
12 |
13 | function main(array $args) : array {
14 | global $redis;
15 |
16 | $uuid = Uuid::uuid4();
17 | $count = $redis->incr(COUNTER);
18 | return [
19 | 'headers' => [
20 | 'Set-Cookie' => 'VisitID=' . $uuid->toString()
21 | ],
22 | 'body' => $count
23 | ];
24 | }
25 |
--------------------------------------------------------------------------------
/visits/packages/visits/info.php:
--------------------------------------------------------------------------------
1 | storage();
14 | $file = $bucket->object(FILENAME);
15 | $since = false; // locally cache the up date
16 |
17 | function main() : array {
18 | global $since, $bucket, $file;
19 |
20 | if ($since) {
21 | // date is locally cached, use it
22 | return [ 'body' => $since ];
23 | } else if ($file->exists()) {
24 | // fetch the date from the object store
25 | // and cache it for future use
26 | $since = $file->downloadAsString();
27 | return [ 'body' => $since ];
28 | } else {
29 | // date file doesn't exist, create it
30 | // and 'now' becomes the since date
31 | echo 'creating info file';
32 | $since = date('m/d/Y');
33 | $bucket->upload($since, [
34 | 'name' => FILENAME
35 | ]);
36 | return [ 'body' => $since ];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/visits/web/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Your Serverless Cloud. Beautiful.
5 |
6 |
7 |
8 |
18 |
19 |
20 |
21 |
22 |
28 |
29 |
30 |
31 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/visits/web/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nimbella/demo-projects/72740cd30fdbc8acba8b13d1199b124d3345e271/visits/web/logo.png
--------------------------------------------------------------------------------