├── .all-contributorsrc
├── .editorconfig
├── .env.example
├── .eslintrc
├── .gitignore
├── .husky
└── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── package.json
├── public
├── css
│ ├── create.css
│ ├── footer.css
│ ├── poll-results-compact.css
│ ├── poll-results.css
│ ├── polls.css
│ ├── qrcode.css
│ ├── style.css
│ └── vote.css
└── js
│ ├── classes
│ └── poll.js
│ ├── qrcode.js
│ └── sketches
│ ├── create.js
│ ├── poll.js
│ └── vote.js
├── server
├── api.js
├── helpers
│ ├── broadcaster.js
│ ├── createNewPoll.js
│ └── database.js
├── index.js
├── validation
│ ├── antipollspam.js
│ ├── antipollspam_test.js
│ └── basicauth.js
└── web.js
└── views
├── create.pug
├── footer.pug
├── index.pug
├── notfound.pug
├── poll.pug
├── qrcode.pug
└── vote.pug
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "README.md"
4 | ],
5 | "imageSize": 100,
6 | "commit": false,
7 | "contributors": [
8 | {
9 | "login": "crunchypi",
10 | "name": "crunchypi",
11 | "avatar_url": "https://avatars2.githubusercontent.com/u/53178205?v=4",
12 | "profile": "https://github.com/crunchypi",
13 | "contributions": [
14 | "code",
15 | "ideas"
16 | ]
17 | },
18 | {
19 | "login": "jriegraf",
20 | "name": "Julian",
21 | "avatar_url": "https://avatars1.githubusercontent.com/u/16071323?v=4",
22 | "profile": "https://github.com/jriegraf",
23 | "contributions": [
24 | "code",
25 | "ideas"
26 | ]
27 | },
28 | {
29 | "login": "manthanabc",
30 | "name": "Manthan",
31 | "avatar_url": "https://avatars2.githubusercontent.com/u/48511543?v=4",
32 | "profile": "https://github.com/manthanabc",
33 | "contributions": [
34 | "code",
35 | "design"
36 | ]
37 | },
38 | {
39 | "login": "simon-tiger",
40 | "name": "Simon Tiger",
41 | "avatar_url": "https://avatars1.githubusercontent.com/u/21979673?v=4",
42 | "profile": "https://simontiger.com",
43 | "contributions": [
44 | "code",
45 | "ideas"
46 | ]
47 | },
48 | {
49 | "login": "pieterdeschepper",
50 | "name": "Pieter De Schepper",
51 | "avatar_url": "https://avatars0.githubusercontent.com/u/4106097?v=4",
52 | "profile": "https://github.com/pieterdeschepper",
53 | "contributions": [
54 | "design",
55 | "code"
56 | ]
57 | },
58 | {
59 | "login": "D-T-666",
60 | "name": "Dimitri Tabatadze",
61 | "avatar_url": "https://avatars1.githubusercontent.com/u/35934791?v=4",
62 | "profile": "https://github.com/D-T-666",
63 | "contributions": [
64 | "code",
65 | "ideas"
66 | ]
67 | },
68 | {
69 | "login": "ShawKai91",
70 | "name": "Shaw Kai",
71 | "avatar_url": "https://avatars3.githubusercontent.com/u/66273574?v=4",
72 | "profile": "https://github.com/ShawKai91",
73 | "contributions": [
74 | "code",
75 | "ideas"
76 | ]
77 | },
78 | {
79 | "login": "BeeryShklar",
80 | "name": "Beery Shklar",
81 | "avatar_url": "https://avatars3.githubusercontent.com/u/52495055?v=4",
82 | "profile": "https://github.com/BeeryShklar",
83 | "contributions": [
84 | "code"
85 | ]
86 | },
87 | {
88 | "login": "dipamsen",
89 | "name": "Fun Planet",
90 | "avatar_url": "https://avatars2.githubusercontent.com/u/59444569?v=4",
91 | "profile": "https://github.com/dipamsen",
92 | "contributions": [
93 | "ideas",
94 | "code"
95 | ]
96 | },
97 | {
98 | "login": "Samuel-Martineau",
99 | "name": "Samuel Martineau",
100 | "avatar_url": "https://avatars3.githubusercontent.com/u/44237969?v=4",
101 | "profile": "https://smartineau.me",
102 | "contributions": [
103 | "ideas"
104 | ]
105 | },
106 | {
107 | "login": "shiffman",
108 | "name": "Daniel Shiffman",
109 | "avatar_url": "https://avatars0.githubusercontent.com/u/191758?v=4",
110 | "profile": "http://www.shiffman.net",
111 | "contributions": [
112 | "code"
113 | ]
114 | },
115 | {
116 | "login": "johntalton",
117 | "name": "John",
118 | "avatar_url": "https://avatars1.githubusercontent.com/u/13648537?v=4",
119 | "profile": "https://github.com/johntalton",
120 | "contributions": [
121 | "ideas"
122 | ]
123 | },
124 | {
125 | "login": "adriaan1313",
126 | "name": "Bunnygamers",
127 | "avatar_url": "https://avatars0.githubusercontent.com/u/19620346?v=4",
128 | "profile": "https://github.com/adriaan1313",
129 | "contributions": [
130 | "ideas"
131 | ]
132 | },
133 | {
134 | "login": "TheCBKM",
135 | "name": "Rajaram Joshi",
136 | "avatar_url": "https://avatars1.githubusercontent.com/u/38382861?v=4",
137 | "profile": "http://cbkm.in",
138 | "contributions": [
139 | "doc"
140 | ]
141 | },
142 | {
143 | "login": "younesaassila",
144 | "name": "Younes Aassila",
145 | "avatar_url": "https://avatars1.githubusercontent.com/u/47226184?v=4",
146 | "profile": "http://aassila.com",
147 | "contributions": [
148 | "doc"
149 | ]
150 | },
151 | {
152 | "login": "GiggioG",
153 | "name": "GiggioG",
154 | "avatar_url": "https://avatars3.githubusercontent.com/u/47040505?v=4",
155 | "profile": "https://giggiog.github.com",
156 | "contributions": [
157 | "code",
158 | "design"
159 | ]
160 | },
161 | {
162 | "login": "elunico",
163 | "name": "Tom",
164 | "avatar_url": "https://avatars3.githubusercontent.com/u/10181211?v=4",
165 | "profile": "http://eluni.co",
166 | "contributions": [
167 | "code",
168 | "design"
169 | ]
170 | }
171 | ],
172 | "contributorsPerLine": 7,
173 | "projectName": "Live-Poll",
174 | "projectOwner": "CodingTrain",
175 | "repoType": "github",
176 | "repoHost": "https://github.com",
177 | "skipCi": true
178 | }
179 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | end_of_line = lf
3 | insert_final_newline = true
4 |
5 | [*.js]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 2
9 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | LOGIN_USERNAME=codingtrain
2 | LOGIN_PASSWORD=rainbow
3 | PORT=3000
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "commonjs": true,
5 | "es2021": true,
6 | "node": true
7 | },
8 | "plugins": [
9 | "prettier"
10 | ],
11 | "extends": [
12 | "eslint:recommended"
13 | ],
14 | "parserOptions": {
15 | "ecmaVersion": 12
16 | },
17 | "rules": {
18 | "prettier/prettier": [
19 | "error",
20 | {
21 | "endOfLine": "auto"
22 | }
23 | ]
24 | }
25 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | database.db
4 | .env
5 | .idea
6 | package-lock.json
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "bracketSpacing": true,
4 | "endOfLine": "lf",
5 | "printWidth": 80,
6 | "proseWrap": "preserve",
7 | "quoteProps": "consistent",
8 | "semi": true,
9 | "singleQuote": false,
10 | "useTabs": false
11 | }
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Coding Train
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Live-Poll
2 |
3 |
4 |
5 | [](#contributors-)
6 |
7 |
8 |
9 | ## Set Up
10 |
11 | 1. Clone repository
12 | 1. Run `npm install`
13 | 1. Copy and rename `.env.example` to `.env`
14 | 1. Run `npm run postinstall` (optional)
15 | 1. Run `npm run dev` (development mode) or `npm start` (production mode)
16 | 1. Open the page in your browser (https://localhost:3000). It will list all the active polls.
17 | 1. Type in the username and password found in `.env`
18 |
19 | ## Available Routes
20 |
21 | - http://localhost:3000/create to create a new poll
22 | - http://localhost:3000/newest to show the results of the newest poll
23 | - http://localhost:3000/vote/{poll_id} to vote in a poll
24 | - http://localhost:3000/poll/{poll_id} to show the results of a poll
25 | - http://localhost:3000/qrcode to show a qrcode with the url to newest poll
26 |
27 | ## Additional URL-Parameters
28 |
29 | - monotone=boolean
30 | - applies a reduced color scheme for voting bars
31 | - ignored if _overlay=true_ is used
32 | - simple=boolean
33 | - applies a basic font type
34 | - overlay=boolean
35 | - overwrites _monotone_ parameter
36 | - applies a compact view especially for live streams / OBS
37 |
38 | Example usage
39 |
40 | ```
41 | /poll/{poll_id}?monotone=true&simple=true&overlay=true
42 | ```
43 |
44 | ## Contributors ✨
45 |
46 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
47 |
48 |
49 |
50 |
51 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
83 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "live-poll",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server/index.js",
6 | "scripts": {
7 | "start": "node server/index.js",
8 | "test": "echo \"Error: no test specified\" && exit 1",
9 | "dev": "nodemon server/index.js",
10 | "lint": "eslint server/**/*.js public/**/*.js",
11 | "lint:fix": "npm run lint --fix",
12 | "lint:staged": "lint-staged",
13 | "postinstall": "husky install"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/CodingTrain/Live-Poll.git"
18 | },
19 | "keywords": [],
20 | "author": "",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/CodingTrain/Live-Poll/issues"
24 | },
25 | "homepage": "https://github.com/CodingTrain/Live-Poll#readme",
26 | "dependencies": {
27 | "dotenv": "^8.2.0",
28 | "express": "^4.17.1",
29 | "nedb": "^1.8.0",
30 | "nedb-promises": "^4.0.4",
31 | "pug": "^3.0.0",
32 | "socket.io": "^3.0.3"
33 | },
34 | "devDependencies": {
35 | "eslint": "^7.14.0",
36 | "eslint-plugin-prettier": "^3.1.4",
37 | "husky": "^5.0.4",
38 | "lint-staged": "^10.5.2",
39 | "nodemon": "^2.0.4",
40 | "prettier": "^2.2.0"
41 | },
42 | "lint-staged": {
43 | "*.js": [
44 | "eslint . --fix"
45 | ]
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/public/css/create.css:
--------------------------------------------------------------------------------
1 | /* put create poll page styles here */
2 | #main-create {
3 | margin: 0 20px;
4 | }
5 |
6 | #main-create textarea,
7 | input {
8 | font-family: inherit;
9 | }
10 |
11 | #main-create .options input,
12 | textarea {
13 | font-size: 16px;
14 | position: relative;
15 | margin-bottom: 0.2em;
16 | width: 300px;
17 | padding: 0.5em 1em;
18 | border: 1px solid lightblue;
19 | border-radius: 5px;
20 | left: 50%;
21 | transform: translate(-50%);
22 | }
23 |
24 | #main-create .options {
25 | display: flex;
26 | flex-direction: column;
27 | }
28 |
29 | #main-create textarea {
30 | font-size: 20px;
31 | height: 80px;
32 | }
33 |
34 | #main-create .buttonsArray {
35 | margin-top: 2em;
36 | display: flex;
37 | flex-direction: column;
38 | }
39 |
40 | #main-create button {
41 | font-size: 20px;
42 | position: relative;
43 | width: 300px;
44 | height: 40px;
45 | margin-top: 0.5em;
46 | background-color: var(--submit-button);
47 | cursor: pointer;
48 | border-radius: 8px;
49 | left: 50%;
50 | transform: translate(-50%) scale(1);
51 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);
52 | transition: box-shadow 200ms ease, transform 200ms ease;
53 | }
54 |
55 | #main-create button:hover {
56 | box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.5);
57 | transform: translate(-50%) scale(1.02);
58 | }
59 |
60 | #main-create button:active {
61 | transform: translate(-50%) scale(1);
62 | }
63 |
64 | #main-create .addOption {
65 | background-color: rgb(81, 219, 81);
66 | }
67 |
68 | #main-create .removeLastOption {
69 | background-color: rgb(219, 97, 81);
70 | }
71 |
72 | #main-create svg {
73 | margin-top: 20px;
74 | }
75 |
--------------------------------------------------------------------------------
/public/css/footer.css:
--------------------------------------------------------------------------------
1 | /* footer */
2 | #footer {
3 | position: fixed;
4 | bottom: 30px;
5 | width: 320px;
6 | left: 50%;
7 | transform: translate(-50%);
8 | }
9 |
10 | #options-wrapper {
11 | display: grid;
12 | grid-template-columns: repeat(3, minmax(min-content, 1fr));
13 | justify-items: center;
14 | height: 45px;
15 | margin-top: 1em;
16 | }
17 |
18 | div#views ul{
19 | list-style: none;
20 | display: flex;
21 | flex-direction: row;
22 | justify-content: space-evenly;
23 | }
24 |
25 | .btn-options input {
26 | position: absolute;
27 | left: -400px;
28 | visibility: hidden;
29 | overflow: hidden;
30 | }
31 |
32 | .btn-options label {
33 | display: inline-block;
34 | position: relative;
35 | padding: 0.2em 0.7em;
36 | height: 100%;
37 | width: 100px;
38 | text-align: center;
39 | background-color: var(--disabled);
40 | color: white;
41 | border-radius: 8px;
42 | cursor: pointer;
43 | box-shadow: inset 0px 0px 5px rgba(0,0,0,0.5);
44 | transition: box-shadow 300ms ease,
45 | background-color 300ms ease;
46 | }
47 |
48 | .btn-options input:checked + label {
49 | background-color: var(--enabled);
50 | }
51 |
52 | .btn-options label:hover, .btn-options input:checked + label:hover {
53 | box-shadow: inset 0px 0px 15px rgba(0,0,0,0.5);
54 | }
55 |
56 | #vote-main .btn-selection input:checked + label:hover {
57 | background-color: var(--enabled);
58 | }
59 |
--------------------------------------------------------------------------------
/public/css/poll-results-compact.css:
--------------------------------------------------------------------------------
1 | /* poll results */
2 | body {
3 | background: transparent !important;
4 | }
5 |
6 | #poll-result-main {
7 | width: calc(100% - 20px);
8 | text-align: left;
9 | max-width: 500px;
10 | }
11 |
12 | #poll-result-main h1 {
13 | color: white;
14 | text-shadow: 0px 0px 3px rgba(0, 0, 0, 1);
15 | }
16 |
17 | #poll-result-main h2 {
18 | margin: 0px 0px 2px 0px;
19 | font-size: 18px;
20 | color: white;
21 | text-shadow: 0px 0px 3px rgba(0, 0, 0, 1);
22 | }
23 |
24 | #poll-result-main .option {
25 | position: relative;
26 | height: 32px;
27 | background-color: rgba(0, 0, 0, 0.3);
28 | margin-bottom: 4px;
29 | }
30 |
31 | #poll-result-main .bar-title {
32 | position: absolute;
33 | display: inline;
34 | right: 0;
35 | font-size: 20px;
36 | font-family: var(--codingtrain-fontface);
37 | color: white;
38 | margin: 3px 12px 2px 0px;
39 | text-transform: uppercase;
40 | z-index: 2;
41 | }
42 |
43 | .progressBar {
44 | position: absolute;
45 | top: 0;
46 | left: 0;
47 | height: 100%;
48 | background-color: rgb(32, 145, 170);
49 | transition: width 1s ease, background-color 1s ease;
50 | }
51 |
52 | .leading {
53 | background-color: rgb(50, 168, 82);
54 | }
55 |
56 | .progressBar p {
57 | position: absolute;
58 | width: 200px;
59 | top: 0;
60 | font-size: 20px;
61 | margin: 3px 0px 2px 12px;
62 | color: white;
63 | font-family: var(--codingtrain-fontface);
64 | }
65 |
66 | #totalVotes {
67 | margin: 3px 6px 2px 10px;
68 | font-size: 20px;
69 | font-family: var(--codingtrain-fontface);
70 | color: white;
71 | text-shadow: 0px 0px 3px rgba(0, 0, 0, 1);
72 | }
73 |
74 | @media only screen and (max-width: 768px) {
75 | #poll-result-main {
76 | text-align: left;
77 | margin: 0 auto;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/public/css/poll-results.css:
--------------------------------------------------------------------------------
1 | /* poll results */
2 | #poll-result-main {
3 | width: calc(100% - 20px);
4 | text-align: left;
5 | }
6 |
7 | .bar-title {
8 | text-align: left;
9 | font-size: 20px;
10 | }
11 |
12 | .progressBar {
13 | position: relative;
14 | min-width: 21px;
15 | margin: 0 5px 5px 0px;
16 | padding: 2px 15px;
17 | margin-bottom: 10px;
18 | color: var(--progressbar-color);
19 | background: var(--monochrome-gradient);
20 | transition: width 1s;
21 | font-family: var(--codingtrain-fontface);
22 | font-size: larger;
23 | text-shadow: -1px 1px #000;
24 | border-radius: 100px;
25 | border: 1px solid black;
26 | }
27 |
28 | .progressBar p {
29 | width: 220px;
30 | }
31 |
32 | .gradient {
33 | background: var(--gradient);
34 | }
35 |
36 | #totalVotes {
37 | margin: 5px;
38 | font-size: 22px;
39 | }
40 |
41 | @media only screen and (max-width: 768px) {
42 | #poll-result-main {
43 | text-align: left;
44 | margin: 0 auto;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/public/css/polls.css:
--------------------------------------------------------------------------------
1 | /* polls */
2 | #pool-main {
3 | width: 100%;
4 | }
5 |
6 | #pool-main p {
7 | padding: 1em 0;
8 | }
9 |
10 | #pool-main ul {
11 | margin-bottom: 20px;
12 | }
13 |
14 | #pool-main #poll-container li {
15 | display: grid;
16 | grid-template-columns: [s] 2fr [m] 1fr [e];
17 | grid-template-rows: [t] 1fr [b] 1fr [e];
18 |
19 | margin: 0.5em auto;
20 | padding: 0.5em 1em;
21 | width: calc(100vw - 40px);
22 | background-color: var(--enabled);
23 | border-radius: 8px;
24 | }
25 |
26 | #pool-main .title {
27 | text-align: left;
28 | grid-column: s/m;
29 | grid-row: t/b;
30 | text-decoration: none;
31 | text-transform: uppercase;
32 | color: white;
33 | font-size: 18px;
34 | }
35 |
36 | #pool-main .date {
37 | grid-column: s/m;
38 | grid-row: b/e;
39 | text-align: left;
40 | text-transform: lowercase;
41 | color: rgba(0, 0, 0, 0.5);
42 | font-size: 14px;
43 | }
44 |
45 | #pool-main .vote-btn {
46 | grid-column: m/e;
47 | grid-row: 1/-1;
48 | justify-self: start;
49 | align-self: center;
50 | padding: 0.5em 1.2em;
51 | height: 35px;
52 | text-decoration: none;
53 | text-align: center;
54 | color: black;
55 | background-color: wheat;
56 | border-radius: 8px;
57 | box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
58 | }
59 |
60 | #pool-main .delete-btn {
61 | grid-row: 1/-1;
62 | grid-column: m/e;
63 | align-self: center;
64 | padding: 0.5em 1.2em;
65 | height: 35px;
66 | justify-self: end;
67 | background-color: var(--disabled);
68 | color: white;
69 | border-radius: 8px;
70 | box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5);
71 | cursor: pointer;
72 | }
73 |
74 | #pool-main #create {
75 | position: relative;
76 | text-decoration: none;
77 | color: black;
78 | text-transform: uppercase;
79 | font-size: 20px;
80 | padding: 0.3em 0.7em;
81 | width: 300px;
82 | height: 40px;
83 | background-color: var(--submit-button);
84 | cursor: pointer;
85 | border-radius: 8px;
86 | box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.5);
87 | }
88 |
--------------------------------------------------------------------------------
/public/css/qrcode.css:
--------------------------------------------------------------------------------
1 | #placeHolder {
2 | width: 400px;
3 | height: 400px;
4 | }
5 |
6 | body {
7 | background-color: rgba(255, 255, 255, 0);
8 | margin: 20px 20px;
9 | }
10 |
11 | .content {
12 | display: flex;
13 | width: 100%;
14 | height: 100%;
15 | }
16 |
17 | .qr-question {
18 | position: relative;
19 | font-size: 20px;
20 | margin: 0;
21 | }
22 |
23 | .left {
24 | width: 75%;
25 | }
26 |
27 | .right {
28 | width: 20%;
29 | }
30 |
31 | .left>iframe {
32 | width: 100%;
33 | height: 100%
34 | }
35 |
--------------------------------------------------------------------------------
/public/css/style.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "cubanoregular";
3 | src: url("https://thecodingtrain.com/assets/fonts/cubano-regular-webfont.woff2");
4 | }
5 | :root {
6 | --codingtrain-fontface: "cubanoregular";
7 | --gradient: linear-gradient(90deg, red, yellow, green, blue, violet);
8 | --monochrome-gradient: repeating-linear-gradient(
9 | 60deg,
10 | #a42963,
11 | #a42963 10px,
12 | #f063a4 10px,
13 | #f063a4 20px
14 | );
15 | --no-gradient: black;
16 | --progressbar-color: white;
17 | --disabled: rgb(189, 65, 65);
18 | --enabled: rgb(52, 168, 91);
19 | --submit-button: rgb(98, 159, 238);
20 | }
21 |
22 | /* resets browser default styling */
23 | *,
24 | ::after,
25 | ::before {
26 | border: 0;
27 | margin: 0;
28 | padding: 0;
29 | box-sizing: border-box;
30 | }
31 |
32 | /* main */
33 | html,
34 | body {
35 | background-color: transparent;
36 | }
37 |
38 | body {
39 | font-family: "Open Sans";
40 | margin: 20px;
41 | }
42 |
43 | main {
44 | text-align: center;
45 | width: 100%;
46 | }
47 |
48 | h1,
49 | h2,
50 | button,
51 | .question,
52 | #total-votes,
53 | #create {
54 | font-family: var(--codingtrain-fontface);
55 | }
56 |
57 | h1 {
58 | margin-bottom: 0.3em;
59 | }
60 |
61 | h2 {
62 | margin-bottom: 0.5em;
63 | }
64 |
65 | h1::before {
66 | content: "🚂 ";
67 | }
68 | h1::after {
69 | content: " 🌈";
70 | }
71 |
72 | .noselect {
73 | -webkit-touch-callout: none; /* iOS Safari */
74 | -webkit-user-select: none; /* Safari */
75 | -khtml-user-select: none; /* Konqueror HTML */
76 | -moz-user-select: none; /* Old versions of Firefox */
77 | -ms-user-select: none; /* Internet Explorer/Edge */
78 | user-select: none; /* Non-prefixed version, currently
79 | supported by Chrome, Edge, Opera and Firefox */
80 | }
81 |
82 | @media only screen and (max-width: 768px) {
83 | body {
84 | margin: 20px 0px 0px 0px;
85 | }
86 |
87 | h1,
88 | h2 {
89 | font-size: 20px;
90 | }
91 |
92 | h1 {
93 | text-align: center;
94 | margin-bottom: 40px;
95 | }
96 | }
97 |
98 | @media only screen and (max-width: 330px) {
99 | h1 {
100 | font-size: 16px;
101 | color: red;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/public/css/vote.css:
--------------------------------------------------------------------------------
1 | /* add styles for the voting page here */
2 | #vote-main {
3 | margin: 0 20px;
4 | }
5 |
6 | #vote-main #vote {
7 | position: absolute;
8 | width: 70%;
9 | left: 50%;
10 | transform: translate(-50%);
11 | }
12 |
13 | #vote-main h2 {
14 | position: relative;
15 | width: 100vw;
16 | left: 50%;
17 | transform: translate(-50%);
18 | }
19 |
20 | #vote-main #vote li {
21 | list-style: none;
22 | }
23 |
24 | #vote-main #vote li input {
25 | visibility: hidden;
26 | position: absolute;
27 | overflow: hidden;
28 | left: -500px;
29 | }
30 |
31 | #vote-main .btn-selection label {
32 | display: inline-block;
33 | position: relative;
34 | padding: 0.6em 1em;
35 | margin: 0.3em 0;
36 | width: 100%;
37 | background-color: whitesmoke;
38 | text-transform: uppercase;
39 | text-align: center;
40 | color: black;
41 | border-radius: 8px;
42 | cursor: pointer;
43 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);
44 | transform: scale(1);
45 | transition: box-shadow 200ms ease, background-color 400ms ease,
46 | transform 200ms ease, color 200ms ease;
47 | }
48 |
49 | #vote-main .btn-selection input:checked + label {
50 | background-color: var(--enabled);
51 | color: white;
52 | }
53 |
54 | #vote-main .btn-selection label:hover {
55 | box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.5);
56 | transform: scale(1.02);
57 | }
58 |
59 | #vote-main .submit-vote {
60 | border: 0;
61 | box-sizing: border-box;
62 | position: relative;
63 | padding: 0.5em 1em;
64 | margin-top: 1em;
65 | text-transform: uppercase;
66 | width: 33%;
67 | font-size: 18px;
68 | font-family: var(--codingtrain-fontface);
69 | border-radius: 8px;
70 | cursor: pointer;
71 | background-color: var(--submit-button);
72 | box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);
73 | transform: scale(1);
74 | transition: box-shadow 200ms ease, transform 200ms ease;
75 | }
76 |
77 | #vote-main .submit-vote:hover {
78 | box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.5);
79 | transform: scale(1.02);
80 | }
81 |
82 | @media only screen and (max-width: 768px) {
83 | #vote-main #vote {
84 | position: relative;
85 | width: 100%;
86 | left: 0%;
87 | transform: translate(0);
88 | }
89 |
90 | #vote-main .btn-selection label {
91 | padding: 1.2em 1em;
92 | margin: 0.6em 0;
93 | font-size: 16px;
94 | }
95 |
96 | #vote-main .submit-vote {
97 | padding: 1.2em 1em;
98 | margin-top: 2em;
99 | width: 100%;
100 | }
101 |
102 | #footer {
103 | display: none;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/public/js/classes/poll.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | /* eslint-disable no-undef */
3 |
4 | class Poll {
5 | constructor() {
6 | const pollId = document.querySelector("[data-id]").dataset.id;
7 |
8 | this.pollId = pollId;
9 |
10 | this.socket = io({ pollId: this.pollId });
11 |
12 | this.socket.on(
13 | "connect",
14 | function () {
15 | this.socket.emit("listenForPoll", this.pollId);
16 | }.bind(this)
17 | );
18 |
19 | this.socket.on("updatePoll", this.updatePollResults.bind(this));
20 | }
21 |
22 | async initPoll() {
23 | await this.fetchPollResults();
24 | this.updatePollResults();
25 | }
26 |
27 | updatePollResults(poll) {
28 | if (poll) {
29 | this.pollDetails = poll;
30 | }
31 | const pollDetails = this.getPollVotesStats();
32 |
33 | const totalText =
34 | pollDetails.totalVotes == 1
35 | ? `${pollDetails.totalVotes} vote`
36 | : `${pollDetails.totalVotes} votes`;
37 | select("#totalVotes").html(totalText);
38 |
39 | // find leading element
40 | const leadingIndex = this.indexOfMax(this.pollDetails.votes);
41 |
42 | // checks if it's a tie
43 | const tie =
44 | this.pollDetails.votes.filter(
45 | (value) => value == this.pollDetails.votes[leadingIndex]
46 | ).length != 1;
47 |
48 | for (let i = 0; i < this.pollDetails.votes.length; i++) {
49 | let count = this.pollDetails.votes[i];
50 |
51 | // calculate percentage values
52 | const percent =
53 | pollDetails.totalVotes == 0
54 | ? 0
55 | : (count / pollDetails.totalVotes) * 100;
56 |
57 | // Get the progress bar element
58 | // Set the width by percentage
59 | const progressBar = select("#progressBar_" + i);
60 | progressBar.style("width", percent + "%");
61 |
62 | const voteText = count == 1 ? `${count} vote` : `${count} votes`;
63 |
64 | if (count > 0) {
65 | progressBar.html(`${voteText} (${Math.round(percent)}%)
`);
66 | } else {
67 | progressBar.html(`${voteText}
`);
68 | }
69 |
70 | if (!tie && i == leadingIndex) {
71 | progressBar.addClass("leading");
72 | } else {
73 | progressBar.removeClass("leading");
74 | }
75 | }
76 | }
77 |
78 | indexOfMax(arr) {
79 | if (arr.length === 0) {
80 | return -1;
81 | }
82 |
83 | var max = arr[0];
84 | var maxIndex = 0;
85 |
86 | for (var i = 1; i < arr.length; i++) {
87 | if (arr[i] > max) {
88 | maxIndex = i;
89 | max = arr[i];
90 | }
91 | }
92 | return maxIndex;
93 | }
94 |
95 | async fetchPollResults() {
96 | // Fetch the poll
97 | const response = await fetch(`/api/poll/${this.pollId}`);
98 |
99 | // Extract the json
100 | poll = await response.json();
101 |
102 | // Throw an error if the poll has an error message
103 | if (poll.message) {
104 | throw new Error(poll.message);
105 | }
106 |
107 | // After all, display the results
108 | this.pollDetails = poll;
109 | }
110 |
111 | getPollVotesStats() {
112 | // Get the number of votes that the most voted option has.
113 | let maxVotes = this.pollDetails.votes.reduce((a, b) => (a > b ? a : b));
114 |
115 | // Get the total number of votes.
116 | let totalVotes = this.pollDetails.votes.reduce((a, b) => a + b);
117 |
118 | return { maxVotes, totalVotes };
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/public/js/qrcode.js:
--------------------------------------------------------------------------------
1 | //---------------------------------------------------------------------
2 | //
3 | // QR Code Generator for JavaScript
4 | //
5 | // Copyright (c) 2009 Kazuhiko Arase
6 | //
7 | // URL: http://www.d-project.com/
8 | //
9 | // Licensed under the MIT license:
10 | // http://www.opensource.org/licenses/mit-license.php
11 | //
12 | // The word 'QR Code' is registered trademark of
13 | // DENSO WAVE INCORPORATED
14 | // http://www.denso-wave.com/qrcode/faqpatent-e.html
15 | //
16 | //---------------------------------------------------------------------
17 |
18 | var qrcode = function() {
19 |
20 | //---------------------------------------------------------------------
21 | // qrcode
22 | //---------------------------------------------------------------------
23 |
24 | /**
25 | * qrcode
26 | * @param typeNumber 1 to 40
27 | * @param errorCorrectionLevel 'L','M','Q','H'
28 | */
29 | var qrcode = function(typeNumber, errorCorrectionLevel) {
30 |
31 | var PAD0 = 0xEC;
32 | var PAD1 = 0x11;
33 |
34 | var _typeNumber = typeNumber;
35 | var _errorCorrectionLevel = QRErrorCorrectionLevel[errorCorrectionLevel];
36 | var _modules = null;
37 | var _moduleCount = 0;
38 | var _dataCache = null;
39 | var _dataList = [];
40 |
41 | var _this = {};
42 |
43 | var makeImpl = function(test, maskPattern) {
44 |
45 | _moduleCount = _typeNumber * 4 + 17;
46 | _modules = function(moduleCount) {
47 | var modules = new Array(moduleCount);
48 | for (var row = 0; row < moduleCount; row += 1) {
49 | modules[row] = new Array(moduleCount);
50 | for (var col = 0; col < moduleCount; col += 1) {
51 | modules[row][col] = null;
52 | }
53 | }
54 | return modules;
55 | }(_moduleCount);
56 |
57 | setupPositionProbePattern(0, 0);
58 | setupPositionProbePattern(_moduleCount - 7, 0);
59 | setupPositionProbePattern(0, _moduleCount - 7);
60 | setupPositionAdjustPattern();
61 | setupTimingPattern();
62 | setupTypeInfo(test, maskPattern);
63 |
64 | if (_typeNumber >= 7) {
65 | setupTypeNumber(test);
66 | }
67 |
68 | if (_dataCache == null) {
69 | _dataCache = createData(_typeNumber, _errorCorrectionLevel, _dataList);
70 | }
71 |
72 | mapData(_dataCache, maskPattern);
73 | };
74 |
75 | var setupPositionProbePattern = function(row, col) {
76 |
77 | for (var r = -1; r <= 7; r += 1) {
78 |
79 | if (row + r <= -1 || _moduleCount <= row + r) continue;
80 |
81 | for (var c = -1; c <= 7; c += 1) {
82 |
83 | if (col + c <= -1 || _moduleCount <= col + c) continue;
84 |
85 | if ( (0 <= r && r <= 6 && (c == 0 || c == 6) )
86 | || (0 <= c && c <= 6 && (r == 0 || r == 6) )
87 | || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) {
88 | _modules[row + r][col + c] = true;
89 | } else {
90 | _modules[row + r][col + c] = false;
91 | }
92 | }
93 | }
94 | };
95 |
96 | var getBestMaskPattern = function() {
97 |
98 | var minLostPoint = 0;
99 | var pattern = 0;
100 |
101 | for (var i = 0; i < 8; i += 1) {
102 |
103 | makeImpl(true, i);
104 |
105 | var lostPoint = QRUtil.getLostPoint(_this);
106 |
107 | if (i == 0 || minLostPoint > lostPoint) {
108 | minLostPoint = lostPoint;
109 | pattern = i;
110 | }
111 | }
112 |
113 | return pattern;
114 | };
115 |
116 | var setupTimingPattern = function() {
117 |
118 | for (var r = 8; r < _moduleCount - 8; r += 1) {
119 | if (_modules[r][6] != null) {
120 | continue;
121 | }
122 | _modules[r][6] = (r % 2 == 0);
123 | }
124 |
125 | for (var c = 8; c < _moduleCount - 8; c += 1) {
126 | if (_modules[6][c] != null) {
127 | continue;
128 | }
129 | _modules[6][c] = (c % 2 == 0);
130 | }
131 | };
132 |
133 | var setupPositionAdjustPattern = function() {
134 |
135 | var pos = QRUtil.getPatternPosition(_typeNumber);
136 |
137 | for (var i = 0; i < pos.length; i += 1) {
138 |
139 | for (var j = 0; j < pos.length; j += 1) {
140 |
141 | var row = pos[i];
142 | var col = pos[j];
143 |
144 | if (_modules[row][col] != null) {
145 | continue;
146 | }
147 |
148 | for (var r = -2; r <= 2; r += 1) {
149 |
150 | for (var c = -2; c <= 2; c += 1) {
151 |
152 | if (r == -2 || r == 2 || c == -2 || c == 2
153 | || (r == 0 && c == 0) ) {
154 | _modules[row + r][col + c] = true;
155 | } else {
156 | _modules[row + r][col + c] = false;
157 | }
158 | }
159 | }
160 | }
161 | }
162 | };
163 |
164 | var setupTypeNumber = function(test) {
165 |
166 | var bits = QRUtil.getBCHTypeNumber(_typeNumber);
167 |
168 | for (var i = 0; i < 18; i += 1) {
169 | var mod = (!test && ( (bits >> i) & 1) == 1);
170 | _modules[Math.floor(i / 3)][i % 3 + _moduleCount - 8 - 3] = mod;
171 | }
172 |
173 | for (var i = 0; i < 18; i += 1) {
174 | var mod = (!test && ( (bits >> i) & 1) == 1);
175 | _modules[i % 3 + _moduleCount - 8 - 3][Math.floor(i / 3)] = mod;
176 | }
177 | };
178 |
179 | var setupTypeInfo = function(test, maskPattern) {
180 |
181 | var data = (_errorCorrectionLevel << 3) | maskPattern;
182 | var bits = QRUtil.getBCHTypeInfo(data);
183 |
184 | // vertical
185 | for (var i = 0; i < 15; i += 1) {
186 |
187 | var mod = (!test && ( (bits >> i) & 1) == 1);
188 |
189 | if (i < 6) {
190 | _modules[i][8] = mod;
191 | } else if (i < 8) {
192 | _modules[i + 1][8] = mod;
193 | } else {
194 | _modules[_moduleCount - 15 + i][8] = mod;
195 | }
196 | }
197 |
198 | // horizontal
199 | for (var i = 0; i < 15; i += 1) {
200 |
201 | var mod = (!test && ( (bits >> i) & 1) == 1);
202 |
203 | if (i < 8) {
204 | _modules[8][_moduleCount - i - 1] = mod;
205 | } else if (i < 9) {
206 | _modules[8][15 - i - 1 + 1] = mod;
207 | } else {
208 | _modules[8][15 - i - 1] = mod;
209 | }
210 | }
211 |
212 | // fixed module
213 | _modules[_moduleCount - 8][8] = (!test);
214 | };
215 |
216 | var mapData = function(data, maskPattern) {
217 |
218 | var inc = -1;
219 | var row = _moduleCount - 1;
220 | var bitIndex = 7;
221 | var byteIndex = 0;
222 | var maskFunc = QRUtil.getMaskFunction(maskPattern);
223 |
224 | for (var col = _moduleCount - 1; col > 0; col -= 2) {
225 |
226 | if (col == 6) col -= 1;
227 |
228 | while (true) {
229 |
230 | for (var c = 0; c < 2; c += 1) {
231 |
232 | if (_modules[row][col - c] == null) {
233 |
234 | var dark = false;
235 |
236 | if (byteIndex < data.length) {
237 | dark = ( ( (data[byteIndex] >>> bitIndex) & 1) == 1);
238 | }
239 |
240 | var mask = maskFunc(row, col - c);
241 |
242 | if (mask) {
243 | dark = !dark;
244 | }
245 |
246 | _modules[row][col - c] = dark;
247 | bitIndex -= 1;
248 |
249 | if (bitIndex == -1) {
250 | byteIndex += 1;
251 | bitIndex = 7;
252 | }
253 | }
254 | }
255 |
256 | row += inc;
257 |
258 | if (row < 0 || _moduleCount <= row) {
259 | row -= inc;
260 | inc = -inc;
261 | break;
262 | }
263 | }
264 | }
265 | };
266 |
267 | var createBytes = function(buffer, rsBlocks) {
268 |
269 | var offset = 0;
270 |
271 | var maxDcCount = 0;
272 | var maxEcCount = 0;
273 |
274 | var dcdata = new Array(rsBlocks.length);
275 | var ecdata = new Array(rsBlocks.length);
276 |
277 | for (var r = 0; r < rsBlocks.length; r += 1) {
278 |
279 | var dcCount = rsBlocks[r].dataCount;
280 | var ecCount = rsBlocks[r].totalCount - dcCount;
281 |
282 | maxDcCount = Math.max(maxDcCount, dcCount);
283 | maxEcCount = Math.max(maxEcCount, ecCount);
284 |
285 | dcdata[r] = new Array(dcCount);
286 |
287 | for (var i = 0; i < dcdata[r].length; i += 1) {
288 | dcdata[r][i] = 0xff & buffer.getBuffer()[i + offset];
289 | }
290 | offset += dcCount;
291 |
292 | var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount);
293 | var rawPoly = qrPolynomial(dcdata[r], rsPoly.getLength() - 1);
294 |
295 | var modPoly = rawPoly.mod(rsPoly);
296 | ecdata[r] = new Array(rsPoly.getLength() - 1);
297 | for (var i = 0; i < ecdata[r].length; i += 1) {
298 | var modIndex = i + modPoly.getLength() - ecdata[r].length;
299 | ecdata[r][i] = (modIndex >= 0)? modPoly.getAt(modIndex) : 0;
300 | }
301 | }
302 |
303 | var totalCodeCount = 0;
304 | for (var i = 0; i < rsBlocks.length; i += 1) {
305 | totalCodeCount += rsBlocks[i].totalCount;
306 | }
307 |
308 | var data = new Array(totalCodeCount);
309 | var index = 0;
310 |
311 | for (var i = 0; i < maxDcCount; i += 1) {
312 | for (var r = 0; r < rsBlocks.length; r += 1) {
313 | if (i < dcdata[r].length) {
314 | data[index] = dcdata[r][i];
315 | index += 1;
316 | }
317 | }
318 | }
319 |
320 | for (var i = 0; i < maxEcCount; i += 1) {
321 | for (var r = 0; r < rsBlocks.length; r += 1) {
322 | if (i < ecdata[r].length) {
323 | data[index] = ecdata[r][i];
324 | index += 1;
325 | }
326 | }
327 | }
328 |
329 | return data;
330 | };
331 |
332 | var createData = function(typeNumber, errorCorrectionLevel, dataList) {
333 |
334 | var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectionLevel);
335 |
336 | var buffer = qrBitBuffer();
337 |
338 | for (var i = 0; i < dataList.length; i += 1) {
339 | var data = dataList[i];
340 | buffer.put(data.getMode(), 4);
341 | buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) );
342 | data.write(buffer);
343 | }
344 |
345 | // calc num max data.
346 | var totalDataCount = 0;
347 | for (var i = 0; i < rsBlocks.length; i += 1) {
348 | totalDataCount += rsBlocks[i].dataCount;
349 | }
350 |
351 | if (buffer.getLengthInBits() > totalDataCount * 8) {
352 | throw 'code length overflow. ('
353 | + buffer.getLengthInBits()
354 | + '>'
355 | + totalDataCount * 8
356 | + ')';
357 | }
358 |
359 | // end code
360 | if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
361 | buffer.put(0, 4);
362 | }
363 |
364 | // padding
365 | while (buffer.getLengthInBits() % 8 != 0) {
366 | buffer.putBit(false);
367 | }
368 |
369 | // padding
370 | while (true) {
371 |
372 | if (buffer.getLengthInBits() >= totalDataCount * 8) {
373 | break;
374 | }
375 | buffer.put(PAD0, 8);
376 |
377 | if (buffer.getLengthInBits() >= totalDataCount * 8) {
378 | break;
379 | }
380 | buffer.put(PAD1, 8);
381 | }
382 |
383 | return createBytes(buffer, rsBlocks);
384 | };
385 |
386 | _this.addData = function(data, mode) {
387 |
388 | mode = mode || 'Byte';
389 |
390 | var newData = null;
391 |
392 | switch(mode) {
393 | case 'Numeric' :
394 | newData = qrNumber(data);
395 | break;
396 | case 'Alphanumeric' :
397 | newData = qrAlphaNum(data);
398 | break;
399 | case 'Byte' :
400 | newData = qr8BitByte(data);
401 | break;
402 | case 'Kanji' :
403 | newData = qrKanji(data);
404 | break;
405 | default :
406 | throw 'mode:' + mode;
407 | }
408 |
409 | _dataList.push(newData);
410 | _dataCache = null;
411 | };
412 |
413 | _this.isDark = function(row, col) {
414 | if (row < 0 || _moduleCount <= row || col < 0 || _moduleCount <= col) {
415 | throw row + ',' + col;
416 | }
417 | return _modules[row][col];
418 | };
419 |
420 | _this.getModuleCount = function() {
421 | return _moduleCount;
422 | };
423 |
424 | _this.make = function() {
425 | if (_typeNumber < 1) {
426 | var typeNumber = 1;
427 |
428 | for (; typeNumber < 40; typeNumber++) {
429 | var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, _errorCorrectionLevel);
430 | var buffer = qrBitBuffer();
431 |
432 | for (var i = 0; i < _dataList.length; i++) {
433 | var data = _dataList[i];
434 | buffer.put(data.getMode(), 4);
435 | buffer.put(data.getLength(), QRUtil.getLengthInBits(data.getMode(), typeNumber) );
436 | data.write(buffer);
437 | }
438 |
439 | var totalDataCount = 0;
440 | for (var i = 0; i < rsBlocks.length; i++) {
441 | totalDataCount += rsBlocks[i].dataCount;
442 | }
443 |
444 | if (buffer.getLengthInBits() <= totalDataCount * 8) {
445 | break;
446 | }
447 | }
448 |
449 | _typeNumber = typeNumber;
450 | }
451 |
452 | makeImpl(false, getBestMaskPattern() );
453 | };
454 |
455 | _this.createTableTag = function(cellSize, margin) {
456 |
457 | cellSize = cellSize || 2;
458 | margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
459 |
460 | var qrHtml = '';
461 |
462 | qrHtml += '';
467 | qrHtml += '';
468 |
469 | for (var r = 0; r < _this.getModuleCount(); r += 1) {
470 |
471 | qrHtml += '';
472 |
473 | for (var c = 0; c < _this.getModuleCount(); c += 1) {
474 | qrHtml += ' | ';
484 | }
485 |
486 | qrHtml += '
';
487 | }
488 |
489 | qrHtml += '';
490 | qrHtml += '
';
491 |
492 | return qrHtml;
493 | };
494 |
495 | _this.createSvgTag = function(cellSize, margin, alt, title) {
496 |
497 | var opts = {};
498 | if (typeof arguments[0] == 'object') {
499 | // Called by options.
500 | opts = arguments[0];
501 | // overwrite cellSize and margin.
502 | cellSize = opts.cellSize;
503 | margin = opts.margin;
504 | alt = opts.alt;
505 | title = opts.title;
506 | }
507 |
508 | cellSize = cellSize || 2;
509 | margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
510 |
511 | // Compose alt property surrogate
512 | alt = (typeof alt === 'string') ? {text: alt} : alt || {};
513 | alt.text = alt.text || null;
514 | alt.id = (alt.text) ? alt.id || 'qrcode-description' : null;
515 |
516 | // Compose title property surrogate
517 | title = (typeof title === 'string') ? {text: title} : title || {};
518 | title.text = title.text || null;
519 | title.id = (title.text) ? title.id || 'qrcode-title' : null;
520 |
521 | var size = _this.getModuleCount() * cellSize + margin * 2;
522 | var c, mc, r, mr, qrSvg='', rect;
523 |
524 | rect = 'l' + cellSize + ',0 0,' + cellSize +
525 | ' -' + cellSize + ',0 0,-' + cellSize + 'z ';
526 |
527 | qrSvg += '';
553 |
554 | return qrSvg;
555 | };
556 |
557 | _this.createDataURL = function(cellSize, margin) {
558 |
559 | cellSize = cellSize || 2;
560 | margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
561 |
562 | var size = _this.getModuleCount() * cellSize + margin * 2;
563 | var min = margin;
564 | var max = size - margin;
565 |
566 | return createDataURL(size, size, function(x, y) {
567 | if (min <= x && x < max && min <= y && y < max) {
568 | var c = Math.floor( (x - min) / cellSize);
569 | var r = Math.floor( (y - min) / cellSize);
570 | return _this.isDark(r, c)? 0 : 1;
571 | } else {
572 | return 1;
573 | }
574 | } );
575 | };
576 |
577 | _this.createImgTag = function(cellSize, margin, alt) {
578 |
579 | cellSize = cellSize || 2;
580 | margin = (typeof margin == 'undefined')? cellSize * 4 : margin;
581 |
582 | var size = _this.getModuleCount() * cellSize + margin * 2;
583 |
584 | var img = '';
585 | img += '
';
601 |
602 | return img;
603 | };
604 |
605 | var escapeXml = function(s) {
606 | var escaped = '';
607 | for (var i = 0; i < s.length; i += 1) {
608 | var c = s.charAt(i);
609 | switch(c) {
610 | case '<': escaped += '<'; break;
611 | case '>': escaped += '>'; break;
612 | case '&': escaped += '&'; break;
613 | case '"': escaped += '"'; break;
614 | default : escaped += c; break;
615 | }
616 | }
617 | return escaped;
618 | };
619 |
620 | var _createHalfASCII = function(margin) {
621 | var cellSize = 1;
622 | margin = (typeof margin == 'undefined')? cellSize * 2 : margin;
623 |
624 | var size = _this.getModuleCount() * cellSize + margin * 2;
625 | var min = margin;
626 | var max = size - margin;
627 |
628 | var y, x, r1, r2, p;
629 |
630 | var blocks = {
631 | '██': '█',
632 | '█ ': '▀',
633 | ' █': '▄',
634 | ' ': ' '
635 | };
636 |
637 | var blocksLastLineNoMargin = {
638 | '██': '▀',
639 | '█ ': '▀',
640 | ' █': ' ',
641 | ' ': ' '
642 | };
643 |
644 | var ascii = '';
645 | for (y = 0; y < size; y += 2) {
646 | r1 = Math.floor((y - min) / cellSize);
647 | r2 = Math.floor((y + 1 - min) / cellSize);
648 | for (x = 0; x < size; x += 1) {
649 | p = '█';
650 |
651 | if (min <= x && x < max && min <= y && y < max && _this.isDark(r1, Math.floor((x - min) / cellSize))) {
652 | p = ' ';
653 | }
654 |
655 | if (min <= x && x < max && min <= y+1 && y+1 < max && _this.isDark(r2, Math.floor((x - min) / cellSize))) {
656 | p += ' ';
657 | }
658 | else {
659 | p += '█';
660 | }
661 |
662 | // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square.
663 | ascii += (margin < 1 && y+1 >= max) ? blocksLastLineNoMargin[p] : blocks[p];
664 | }
665 |
666 | ascii += '\n';
667 | }
668 |
669 | if (size % 2 && margin > 0) {
670 | return ascii.substring(0, ascii.length - size - 1) + Array(size+1).join('▀');
671 | }
672 |
673 | return ascii.substring(0, ascii.length-1);
674 | };
675 |
676 | _this.createASCII = function(cellSize, margin) {
677 | cellSize = cellSize || 1;
678 |
679 | if (cellSize < 2) {
680 | return _createHalfASCII(margin);
681 | }
682 |
683 | cellSize -= 1;
684 | margin = (typeof margin == 'undefined')? cellSize * 2 : margin;
685 |
686 | var size = _this.getModuleCount() * cellSize + margin * 2;
687 | var min = margin;
688 | var max = size - margin;
689 |
690 | var y, x, r, p;
691 |
692 | var white = Array(cellSize+1).join('██');
693 | var black = Array(cellSize+1).join(' ');
694 |
695 | var ascii = '';
696 | var line = '';
697 | for (y = 0; y < size; y += 1) {
698 | r = Math.floor( (y - min) / cellSize);
699 | line = '';
700 | for (x = 0; x < size; x += 1) {
701 | p = 1;
702 |
703 | if (min <= x && x < max && min <= y && y < max && _this.isDark(r, Math.floor((x - min) / cellSize))) {
704 | p = 0;
705 | }
706 |
707 | // Output 2 characters per pixel, to create full square. 1 character per pixels gives only half width of square.
708 | line += p ? white : black;
709 | }
710 |
711 | for (r = 0; r < cellSize; r += 1) {
712 | ascii += line + '\n';
713 | }
714 | }
715 |
716 | return ascii.substring(0, ascii.length-1);
717 | };
718 |
719 | _this.renderTo2dContext = function(context, cellSize) {
720 | cellSize = cellSize || 2;
721 | var length = _this.getModuleCount();
722 | for (var row = 0; row < length; row++) {
723 | for (var col = 0; col < length; col++) {
724 | context.fillStyle = _this.isDark(row, col) ? 'black' : 'white';
725 | context.fillRect(row * cellSize, col * cellSize, cellSize, cellSize);
726 | }
727 | }
728 | }
729 |
730 | return _this;
731 | };
732 |
733 | //---------------------------------------------------------------------
734 | // qrcode.stringToBytes
735 | //---------------------------------------------------------------------
736 |
737 | qrcode.stringToBytesFuncs = {
738 | 'default' : function(s) {
739 | var bytes = [];
740 | for (var i = 0; i < s.length; i += 1) {
741 | var c = s.charCodeAt(i);
742 | bytes.push(c & 0xff);
743 | }
744 | return bytes;
745 | }
746 | };
747 |
748 | qrcode.stringToBytes = qrcode.stringToBytesFuncs['default'];
749 |
750 | //---------------------------------------------------------------------
751 | // qrcode.createStringToBytes
752 | //---------------------------------------------------------------------
753 |
754 | /**
755 | * @param unicodeData base64 string of byte array.
756 | * [16bit Unicode],[16bit Bytes], ...
757 | * @param numChars
758 | */
759 | qrcode.createStringToBytes = function(unicodeData, numChars) {
760 |
761 | // create conversion map.
762 |
763 | var unicodeMap = function() {
764 |
765 | var bin = base64DecodeInputStream(unicodeData);
766 | var read = function() {
767 | var b = bin.read();
768 | if (b == -1) throw 'eof';
769 | return b;
770 | };
771 |
772 | var count = 0;
773 | var unicodeMap = {};
774 | while (true) {
775 | var b0 = bin.read();
776 | if (b0 == -1) break;
777 | var b1 = read();
778 | var b2 = read();
779 | var b3 = read();
780 | var k = String.fromCharCode( (b0 << 8) | b1);
781 | var v = (b2 << 8) | b3;
782 | unicodeMap[k] = v;
783 | count += 1;
784 | }
785 | if (count != numChars) {
786 | throw count + ' != ' + numChars;
787 | }
788 |
789 | return unicodeMap;
790 | }();
791 |
792 | var unknownChar = '?'.charCodeAt(0);
793 |
794 | return function(s) {
795 | var bytes = [];
796 | for (var i = 0; i < s.length; i += 1) {
797 | var c = s.charCodeAt(i);
798 | if (c < 128) {
799 | bytes.push(c);
800 | } else {
801 | var b = unicodeMap[s.charAt(i)];
802 | if (typeof b == 'number') {
803 | if ( (b & 0xff) == b) {
804 | // 1byte
805 | bytes.push(b);
806 | } else {
807 | // 2bytes
808 | bytes.push(b >>> 8);
809 | bytes.push(b & 0xff);
810 | }
811 | } else {
812 | bytes.push(unknownChar);
813 | }
814 | }
815 | }
816 | return bytes;
817 | };
818 | };
819 |
820 | //---------------------------------------------------------------------
821 | // QRMode
822 | //---------------------------------------------------------------------
823 |
824 | var QRMode = {
825 | MODE_NUMBER : 1 << 0,
826 | MODE_ALPHA_NUM : 1 << 1,
827 | MODE_8BIT_BYTE : 1 << 2,
828 | MODE_KANJI : 1 << 3
829 | };
830 |
831 | //---------------------------------------------------------------------
832 | // QRErrorCorrectionLevel
833 | //---------------------------------------------------------------------
834 |
835 | var QRErrorCorrectionLevel = {
836 | L : 1,
837 | M : 0,
838 | Q : 3,
839 | H : 2
840 | };
841 |
842 | //---------------------------------------------------------------------
843 | // QRMaskPattern
844 | //---------------------------------------------------------------------
845 |
846 | var QRMaskPattern = {
847 | PATTERN000 : 0,
848 | PATTERN001 : 1,
849 | PATTERN010 : 2,
850 | PATTERN011 : 3,
851 | PATTERN100 : 4,
852 | PATTERN101 : 5,
853 | PATTERN110 : 6,
854 | PATTERN111 : 7
855 | };
856 |
857 | //---------------------------------------------------------------------
858 | // QRUtil
859 | //---------------------------------------------------------------------
860 |
861 | var QRUtil = function() {
862 |
863 | var PATTERN_POSITION_TABLE = [
864 | [],
865 | [6, 18],
866 | [6, 22],
867 | [6, 26],
868 | [6, 30],
869 | [6, 34],
870 | [6, 22, 38],
871 | [6, 24, 42],
872 | [6, 26, 46],
873 | [6, 28, 50],
874 | [6, 30, 54],
875 | [6, 32, 58],
876 | [6, 34, 62],
877 | [6, 26, 46, 66],
878 | [6, 26, 48, 70],
879 | [6, 26, 50, 74],
880 | [6, 30, 54, 78],
881 | [6, 30, 56, 82],
882 | [6, 30, 58, 86],
883 | [6, 34, 62, 90],
884 | [6, 28, 50, 72, 94],
885 | [6, 26, 50, 74, 98],
886 | [6, 30, 54, 78, 102],
887 | [6, 28, 54, 80, 106],
888 | [6, 32, 58, 84, 110],
889 | [6, 30, 58, 86, 114],
890 | [6, 34, 62, 90, 118],
891 | [6, 26, 50, 74, 98, 122],
892 | [6, 30, 54, 78, 102, 126],
893 | [6, 26, 52, 78, 104, 130],
894 | [6, 30, 56, 82, 108, 134],
895 | [6, 34, 60, 86, 112, 138],
896 | [6, 30, 58, 86, 114, 142],
897 | [6, 34, 62, 90, 118, 146],
898 | [6, 30, 54, 78, 102, 126, 150],
899 | [6, 24, 50, 76, 102, 128, 154],
900 | [6, 28, 54, 80, 106, 132, 158],
901 | [6, 32, 58, 84, 110, 136, 162],
902 | [6, 26, 54, 82, 110, 138, 166],
903 | [6, 30, 58, 86, 114, 142, 170]
904 | ];
905 | var G15 = (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0);
906 | var G18 = (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0);
907 | var G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1);
908 |
909 | var _this = {};
910 |
911 | var getBCHDigit = function(data) {
912 | var digit = 0;
913 | while (data != 0) {
914 | digit += 1;
915 | data >>>= 1;
916 | }
917 | return digit;
918 | };
919 |
920 | _this.getBCHTypeInfo = function(data) {
921 | var d = data << 10;
922 | while (getBCHDigit(d) - getBCHDigit(G15) >= 0) {
923 | d ^= (G15 << (getBCHDigit(d) - getBCHDigit(G15) ) );
924 | }
925 | return ( (data << 10) | d) ^ G15_MASK;
926 | };
927 |
928 | _this.getBCHTypeNumber = function(data) {
929 | var d = data << 12;
930 | while (getBCHDigit(d) - getBCHDigit(G18) >= 0) {
931 | d ^= (G18 << (getBCHDigit(d) - getBCHDigit(G18) ) );
932 | }
933 | return (data << 12) | d;
934 | };
935 |
936 | _this.getPatternPosition = function(typeNumber) {
937 | return PATTERN_POSITION_TABLE[typeNumber - 1];
938 | };
939 |
940 | _this.getMaskFunction = function(maskPattern) {
941 |
942 | switch (maskPattern) {
943 |
944 | case QRMaskPattern.PATTERN000 :
945 | return function(i, j) { return (i + j) % 2 == 0; };
946 | case QRMaskPattern.PATTERN001 :
947 | return function(i, j) { return i % 2 == 0; };
948 | case QRMaskPattern.PATTERN010 :
949 | return function(i, j) { return j % 3 == 0; };
950 | case QRMaskPattern.PATTERN011 :
951 | return function(i, j) { return (i + j) % 3 == 0; };
952 | case QRMaskPattern.PATTERN100 :
953 | return function(i, j) { return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0; };
954 | case QRMaskPattern.PATTERN101 :
955 | return function(i, j) { return (i * j) % 2 + (i * j) % 3 == 0; };
956 | case QRMaskPattern.PATTERN110 :
957 | return function(i, j) { return ( (i * j) % 2 + (i * j) % 3) % 2 == 0; };
958 | case QRMaskPattern.PATTERN111 :
959 | return function(i, j) { return ( (i * j) % 3 + (i + j) % 2) % 2 == 0; };
960 |
961 | default :
962 | throw 'bad maskPattern:' + maskPattern;
963 | }
964 | };
965 |
966 | _this.getErrorCorrectPolynomial = function(errorCorrectLength) {
967 | var a = qrPolynomial([1], 0);
968 | for (var i = 0; i < errorCorrectLength; i += 1) {
969 | a = a.multiply(qrPolynomial([1, QRMath.gexp(i)], 0) );
970 | }
971 | return a;
972 | };
973 |
974 | _this.getLengthInBits = function(mode, type) {
975 |
976 | if (1 <= type && type < 10) {
977 |
978 | // 1 - 9
979 |
980 | switch(mode) {
981 | case QRMode.MODE_NUMBER : return 10;
982 | case QRMode.MODE_ALPHA_NUM : return 9;
983 | case QRMode.MODE_8BIT_BYTE : return 8;
984 | case QRMode.MODE_KANJI : return 8;
985 | default :
986 | throw 'mode:' + mode;
987 | }
988 |
989 | } else if (type < 27) {
990 |
991 | // 10 - 26
992 |
993 | switch(mode) {
994 | case QRMode.MODE_NUMBER : return 12;
995 | case QRMode.MODE_ALPHA_NUM : return 11;
996 | case QRMode.MODE_8BIT_BYTE : return 16;
997 | case QRMode.MODE_KANJI : return 10;
998 | default :
999 | throw 'mode:' + mode;
1000 | }
1001 |
1002 | } else if (type < 41) {
1003 |
1004 | // 27 - 40
1005 |
1006 | switch(mode) {
1007 | case QRMode.MODE_NUMBER : return 14;
1008 | case QRMode.MODE_ALPHA_NUM : return 13;
1009 | case QRMode.MODE_8BIT_BYTE : return 16;
1010 | case QRMode.MODE_KANJI : return 12;
1011 | default :
1012 | throw 'mode:' + mode;
1013 | }
1014 |
1015 | } else {
1016 | throw 'type:' + type;
1017 | }
1018 | };
1019 |
1020 | _this.getLostPoint = function(qrcode) {
1021 |
1022 | var moduleCount = qrcode.getModuleCount();
1023 |
1024 | var lostPoint = 0;
1025 |
1026 | // LEVEL1
1027 |
1028 | for (var row = 0; row < moduleCount; row += 1) {
1029 | for (var col = 0; col < moduleCount; col += 1) {
1030 |
1031 | var sameCount = 0;
1032 | var dark = qrcode.isDark(row, col);
1033 |
1034 | for (var r = -1; r <= 1; r += 1) {
1035 |
1036 | if (row + r < 0 || moduleCount <= row + r) {
1037 | continue;
1038 | }
1039 |
1040 | for (var c = -1; c <= 1; c += 1) {
1041 |
1042 | if (col + c < 0 || moduleCount <= col + c) {
1043 | continue;
1044 | }
1045 |
1046 | if (r == 0 && c == 0) {
1047 | continue;
1048 | }
1049 |
1050 | if (dark == qrcode.isDark(row + r, col + c) ) {
1051 | sameCount += 1;
1052 | }
1053 | }
1054 | }
1055 |
1056 | if (sameCount > 5) {
1057 | lostPoint += (3 + sameCount - 5);
1058 | }
1059 | }
1060 | };
1061 |
1062 | // LEVEL2
1063 |
1064 | for (var row = 0; row < moduleCount - 1; row += 1) {
1065 | for (var col = 0; col < moduleCount - 1; col += 1) {
1066 | var count = 0;
1067 | if (qrcode.isDark(row, col) ) count += 1;
1068 | if (qrcode.isDark(row + 1, col) ) count += 1;
1069 | if (qrcode.isDark(row, col + 1) ) count += 1;
1070 | if (qrcode.isDark(row + 1, col + 1) ) count += 1;
1071 | if (count == 0 || count == 4) {
1072 | lostPoint += 3;
1073 | }
1074 | }
1075 | }
1076 |
1077 | // LEVEL3
1078 |
1079 | for (var row = 0; row < moduleCount; row += 1) {
1080 | for (var col = 0; col < moduleCount - 6; col += 1) {
1081 | if (qrcode.isDark(row, col)
1082 | && !qrcode.isDark(row, col + 1)
1083 | && qrcode.isDark(row, col + 2)
1084 | && qrcode.isDark(row, col + 3)
1085 | && qrcode.isDark(row, col + 4)
1086 | && !qrcode.isDark(row, col + 5)
1087 | && qrcode.isDark(row, col + 6) ) {
1088 | lostPoint += 40;
1089 | }
1090 | }
1091 | }
1092 |
1093 | for (var col = 0; col < moduleCount; col += 1) {
1094 | for (var row = 0; row < moduleCount - 6; row += 1) {
1095 | if (qrcode.isDark(row, col)
1096 | && !qrcode.isDark(row + 1, col)
1097 | && qrcode.isDark(row + 2, col)
1098 | && qrcode.isDark(row + 3, col)
1099 | && qrcode.isDark(row + 4, col)
1100 | && !qrcode.isDark(row + 5, col)
1101 | && qrcode.isDark(row + 6, col) ) {
1102 | lostPoint += 40;
1103 | }
1104 | }
1105 | }
1106 |
1107 | // LEVEL4
1108 |
1109 | var darkCount = 0;
1110 |
1111 | for (var col = 0; col < moduleCount; col += 1) {
1112 | for (var row = 0; row < moduleCount; row += 1) {
1113 | if (qrcode.isDark(row, col) ) {
1114 | darkCount += 1;
1115 | }
1116 | }
1117 | }
1118 |
1119 | var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5;
1120 | lostPoint += ratio * 10;
1121 |
1122 | return lostPoint;
1123 | };
1124 |
1125 | return _this;
1126 | }();
1127 |
1128 | //---------------------------------------------------------------------
1129 | // QRMath
1130 | //---------------------------------------------------------------------
1131 |
1132 | var QRMath = function() {
1133 |
1134 | var EXP_TABLE = new Array(256);
1135 | var LOG_TABLE = new Array(256);
1136 |
1137 | // initialize tables
1138 | for (var i = 0; i < 8; i += 1) {
1139 | EXP_TABLE[i] = 1 << i;
1140 | }
1141 | for (var i = 8; i < 256; i += 1) {
1142 | EXP_TABLE[i] = EXP_TABLE[i - 4]
1143 | ^ EXP_TABLE[i - 5]
1144 | ^ EXP_TABLE[i - 6]
1145 | ^ EXP_TABLE[i - 8];
1146 | }
1147 | for (var i = 0; i < 255; i += 1) {
1148 | LOG_TABLE[EXP_TABLE[i] ] = i;
1149 | }
1150 |
1151 | var _this = {};
1152 |
1153 | _this.glog = function(n) {
1154 |
1155 | if (n < 1) {
1156 | throw 'glog(' + n + ')';
1157 | }
1158 |
1159 | return LOG_TABLE[n];
1160 | };
1161 |
1162 | _this.gexp = function(n) {
1163 |
1164 | while (n < 0) {
1165 | n += 255;
1166 | }
1167 |
1168 | while (n >= 256) {
1169 | n -= 255;
1170 | }
1171 |
1172 | return EXP_TABLE[n];
1173 | };
1174 |
1175 | return _this;
1176 | }();
1177 |
1178 | //---------------------------------------------------------------------
1179 | // qrPolynomial
1180 | //---------------------------------------------------------------------
1181 |
1182 | function qrPolynomial(num, shift) {
1183 |
1184 | if (typeof num.length == 'undefined') {
1185 | throw num.length + '/' + shift;
1186 | }
1187 |
1188 | var _num = function() {
1189 | var offset = 0;
1190 | while (offset < num.length && num[offset] == 0) {
1191 | offset += 1;
1192 | }
1193 | var _num = new Array(num.length - offset + shift);
1194 | for (var i = 0; i < num.length - offset; i += 1) {
1195 | _num[i] = num[i + offset];
1196 | }
1197 | return _num;
1198 | }();
1199 |
1200 | var _this = {};
1201 |
1202 | _this.getAt = function(index) {
1203 | return _num[index];
1204 | };
1205 |
1206 | _this.getLength = function() {
1207 | return _num.length;
1208 | };
1209 |
1210 | _this.multiply = function(e) {
1211 |
1212 | var num = new Array(_this.getLength() + e.getLength() - 1);
1213 |
1214 | for (var i = 0; i < _this.getLength(); i += 1) {
1215 | for (var j = 0; j < e.getLength(); j += 1) {
1216 | num[i + j] ^= QRMath.gexp(QRMath.glog(_this.getAt(i) ) + QRMath.glog(e.getAt(j) ) );
1217 | }
1218 | }
1219 |
1220 | return qrPolynomial(num, 0);
1221 | };
1222 |
1223 | _this.mod = function(e) {
1224 |
1225 | if (_this.getLength() - e.getLength() < 0) {
1226 | return _this;
1227 | }
1228 |
1229 | var ratio = QRMath.glog(_this.getAt(0) ) - QRMath.glog(e.getAt(0) );
1230 |
1231 | var num = new Array(_this.getLength() );
1232 | for (var i = 0; i < _this.getLength(); i += 1) {
1233 | num[i] = _this.getAt(i);
1234 | }
1235 |
1236 | for (var i = 0; i < e.getLength(); i += 1) {
1237 | num[i] ^= QRMath.gexp(QRMath.glog(e.getAt(i) ) + ratio);
1238 | }
1239 |
1240 | // recursive call
1241 | return qrPolynomial(num, 0).mod(e);
1242 | };
1243 |
1244 | return _this;
1245 | };
1246 |
1247 | //---------------------------------------------------------------------
1248 | // QRRSBlock
1249 | //---------------------------------------------------------------------
1250 |
1251 | var QRRSBlock = function() {
1252 |
1253 | var RS_BLOCK_TABLE = [
1254 |
1255 | // L
1256 | // M
1257 | // Q
1258 | // H
1259 |
1260 | // 1
1261 | [1, 26, 19],
1262 | [1, 26, 16],
1263 | [1, 26, 13],
1264 | [1, 26, 9],
1265 |
1266 | // 2
1267 | [1, 44, 34],
1268 | [1, 44, 28],
1269 | [1, 44, 22],
1270 | [1, 44, 16],
1271 |
1272 | // 3
1273 | [1, 70, 55],
1274 | [1, 70, 44],
1275 | [2, 35, 17],
1276 | [2, 35, 13],
1277 |
1278 | // 4
1279 | [1, 100, 80],
1280 | [2, 50, 32],
1281 | [2, 50, 24],
1282 | [4, 25, 9],
1283 |
1284 | // 5
1285 | [1, 134, 108],
1286 | [2, 67, 43],
1287 | [2, 33, 15, 2, 34, 16],
1288 | [2, 33, 11, 2, 34, 12],
1289 |
1290 | // 6
1291 | [2, 86, 68],
1292 | [4, 43, 27],
1293 | [4, 43, 19],
1294 | [4, 43, 15],
1295 |
1296 | // 7
1297 | [2, 98, 78],
1298 | [4, 49, 31],
1299 | [2, 32, 14, 4, 33, 15],
1300 | [4, 39, 13, 1, 40, 14],
1301 |
1302 | // 8
1303 | [2, 121, 97],
1304 | [2, 60, 38, 2, 61, 39],
1305 | [4, 40, 18, 2, 41, 19],
1306 | [4, 40, 14, 2, 41, 15],
1307 |
1308 | // 9
1309 | [2, 146, 116],
1310 | [3, 58, 36, 2, 59, 37],
1311 | [4, 36, 16, 4, 37, 17],
1312 | [4, 36, 12, 4, 37, 13],
1313 |
1314 | // 10
1315 | [2, 86, 68, 2, 87, 69],
1316 | [4, 69, 43, 1, 70, 44],
1317 | [6, 43, 19, 2, 44, 20],
1318 | [6, 43, 15, 2, 44, 16],
1319 |
1320 | // 11
1321 | [4, 101, 81],
1322 | [1, 80, 50, 4, 81, 51],
1323 | [4, 50, 22, 4, 51, 23],
1324 | [3, 36, 12, 8, 37, 13],
1325 |
1326 | // 12
1327 | [2, 116, 92, 2, 117, 93],
1328 | [6, 58, 36, 2, 59, 37],
1329 | [4, 46, 20, 6, 47, 21],
1330 | [7, 42, 14, 4, 43, 15],
1331 |
1332 | // 13
1333 | [4, 133, 107],
1334 | [8, 59, 37, 1, 60, 38],
1335 | [8, 44, 20, 4, 45, 21],
1336 | [12, 33, 11, 4, 34, 12],
1337 |
1338 | // 14
1339 | [3, 145, 115, 1, 146, 116],
1340 | [4, 64, 40, 5, 65, 41],
1341 | [11, 36, 16, 5, 37, 17],
1342 | [11, 36, 12, 5, 37, 13],
1343 |
1344 | // 15
1345 | [5, 109, 87, 1, 110, 88],
1346 | [5, 65, 41, 5, 66, 42],
1347 | [5, 54, 24, 7, 55, 25],
1348 | [11, 36, 12, 7, 37, 13],
1349 |
1350 | // 16
1351 | [5, 122, 98, 1, 123, 99],
1352 | [7, 73, 45, 3, 74, 46],
1353 | [15, 43, 19, 2, 44, 20],
1354 | [3, 45, 15, 13, 46, 16],
1355 |
1356 | // 17
1357 | [1, 135, 107, 5, 136, 108],
1358 | [10, 74, 46, 1, 75, 47],
1359 | [1, 50, 22, 15, 51, 23],
1360 | [2, 42, 14, 17, 43, 15],
1361 |
1362 | // 18
1363 | [5, 150, 120, 1, 151, 121],
1364 | [9, 69, 43, 4, 70, 44],
1365 | [17, 50, 22, 1, 51, 23],
1366 | [2, 42, 14, 19, 43, 15],
1367 |
1368 | // 19
1369 | [3, 141, 113, 4, 142, 114],
1370 | [3, 70, 44, 11, 71, 45],
1371 | [17, 47, 21, 4, 48, 22],
1372 | [9, 39, 13, 16, 40, 14],
1373 |
1374 | // 20
1375 | [3, 135, 107, 5, 136, 108],
1376 | [3, 67, 41, 13, 68, 42],
1377 | [15, 54, 24, 5, 55, 25],
1378 | [15, 43, 15, 10, 44, 16],
1379 |
1380 | // 21
1381 | [4, 144, 116, 4, 145, 117],
1382 | [17, 68, 42],
1383 | [17, 50, 22, 6, 51, 23],
1384 | [19, 46, 16, 6, 47, 17],
1385 |
1386 | // 22
1387 | [2, 139, 111, 7, 140, 112],
1388 | [17, 74, 46],
1389 | [7, 54, 24, 16, 55, 25],
1390 | [34, 37, 13],
1391 |
1392 | // 23
1393 | [4, 151, 121, 5, 152, 122],
1394 | [4, 75, 47, 14, 76, 48],
1395 | [11, 54, 24, 14, 55, 25],
1396 | [16, 45, 15, 14, 46, 16],
1397 |
1398 | // 24
1399 | [6, 147, 117, 4, 148, 118],
1400 | [6, 73, 45, 14, 74, 46],
1401 | [11, 54, 24, 16, 55, 25],
1402 | [30, 46, 16, 2, 47, 17],
1403 |
1404 | // 25
1405 | [8, 132, 106, 4, 133, 107],
1406 | [8, 75, 47, 13, 76, 48],
1407 | [7, 54, 24, 22, 55, 25],
1408 | [22, 45, 15, 13, 46, 16],
1409 |
1410 | // 26
1411 | [10, 142, 114, 2, 143, 115],
1412 | [19, 74, 46, 4, 75, 47],
1413 | [28, 50, 22, 6, 51, 23],
1414 | [33, 46, 16, 4, 47, 17],
1415 |
1416 | // 27
1417 | [8, 152, 122, 4, 153, 123],
1418 | [22, 73, 45, 3, 74, 46],
1419 | [8, 53, 23, 26, 54, 24],
1420 | [12, 45, 15, 28, 46, 16],
1421 |
1422 | // 28
1423 | [3, 147, 117, 10, 148, 118],
1424 | [3, 73, 45, 23, 74, 46],
1425 | [4, 54, 24, 31, 55, 25],
1426 | [11, 45, 15, 31, 46, 16],
1427 |
1428 | // 29
1429 | [7, 146, 116, 7, 147, 117],
1430 | [21, 73, 45, 7, 74, 46],
1431 | [1, 53, 23, 37, 54, 24],
1432 | [19, 45, 15, 26, 46, 16],
1433 |
1434 | // 30
1435 | [5, 145, 115, 10, 146, 116],
1436 | [19, 75, 47, 10, 76, 48],
1437 | [15, 54, 24, 25, 55, 25],
1438 | [23, 45, 15, 25, 46, 16],
1439 |
1440 | // 31
1441 | [13, 145, 115, 3, 146, 116],
1442 | [2, 74, 46, 29, 75, 47],
1443 | [42, 54, 24, 1, 55, 25],
1444 | [23, 45, 15, 28, 46, 16],
1445 |
1446 | // 32
1447 | [17, 145, 115],
1448 | [10, 74, 46, 23, 75, 47],
1449 | [10, 54, 24, 35, 55, 25],
1450 | [19, 45, 15, 35, 46, 16],
1451 |
1452 | // 33
1453 | [17, 145, 115, 1, 146, 116],
1454 | [14, 74, 46, 21, 75, 47],
1455 | [29, 54, 24, 19, 55, 25],
1456 | [11, 45, 15, 46, 46, 16],
1457 |
1458 | // 34
1459 | [13, 145, 115, 6, 146, 116],
1460 | [14, 74, 46, 23, 75, 47],
1461 | [44, 54, 24, 7, 55, 25],
1462 | [59, 46, 16, 1, 47, 17],
1463 |
1464 | // 35
1465 | [12, 151, 121, 7, 152, 122],
1466 | [12, 75, 47, 26, 76, 48],
1467 | [39, 54, 24, 14, 55, 25],
1468 | [22, 45, 15, 41, 46, 16],
1469 |
1470 | // 36
1471 | [6, 151, 121, 14, 152, 122],
1472 | [6, 75, 47, 34, 76, 48],
1473 | [46, 54, 24, 10, 55, 25],
1474 | [2, 45, 15, 64, 46, 16],
1475 |
1476 | // 37
1477 | [17, 152, 122, 4, 153, 123],
1478 | [29, 74, 46, 14, 75, 47],
1479 | [49, 54, 24, 10, 55, 25],
1480 | [24, 45, 15, 46, 46, 16],
1481 |
1482 | // 38
1483 | [4, 152, 122, 18, 153, 123],
1484 | [13, 74, 46, 32, 75, 47],
1485 | [48, 54, 24, 14, 55, 25],
1486 | [42, 45, 15, 32, 46, 16],
1487 |
1488 | // 39
1489 | [20, 147, 117, 4, 148, 118],
1490 | [40, 75, 47, 7, 76, 48],
1491 | [43, 54, 24, 22, 55, 25],
1492 | [10, 45, 15, 67, 46, 16],
1493 |
1494 | // 40
1495 | [19, 148, 118, 6, 149, 119],
1496 | [18, 75, 47, 31, 76, 48],
1497 | [34, 54, 24, 34, 55, 25],
1498 | [20, 45, 15, 61, 46, 16]
1499 | ];
1500 |
1501 | var qrRSBlock = function(totalCount, dataCount) {
1502 | var _this = {};
1503 | _this.totalCount = totalCount;
1504 | _this.dataCount = dataCount;
1505 | return _this;
1506 | };
1507 |
1508 | var _this = {};
1509 |
1510 | var getRsBlockTable = function(typeNumber, errorCorrectionLevel) {
1511 |
1512 | switch(errorCorrectionLevel) {
1513 | case QRErrorCorrectionLevel.L :
1514 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0];
1515 | case QRErrorCorrectionLevel.M :
1516 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1];
1517 | case QRErrorCorrectionLevel.Q :
1518 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2];
1519 | case QRErrorCorrectionLevel.H :
1520 | return RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3];
1521 | default :
1522 | return undefined;
1523 | }
1524 | };
1525 |
1526 | _this.getRSBlocks = function(typeNumber, errorCorrectionLevel) {
1527 |
1528 | var rsBlock = getRsBlockTable(typeNumber, errorCorrectionLevel);
1529 |
1530 | if (typeof rsBlock == 'undefined') {
1531 | throw 'bad rs block @ typeNumber:' + typeNumber +
1532 | '/errorCorrectionLevel:' + errorCorrectionLevel;
1533 | }
1534 |
1535 | var length = rsBlock.length / 3;
1536 |
1537 | var list = [];
1538 |
1539 | for (var i = 0; i < length; i += 1) {
1540 |
1541 | var count = rsBlock[i * 3 + 0];
1542 | var totalCount = rsBlock[i * 3 + 1];
1543 | var dataCount = rsBlock[i * 3 + 2];
1544 |
1545 | for (var j = 0; j < count; j += 1) {
1546 | list.push(qrRSBlock(totalCount, dataCount) );
1547 | }
1548 | }
1549 |
1550 | return list;
1551 | };
1552 |
1553 | return _this;
1554 | }();
1555 |
1556 | //---------------------------------------------------------------------
1557 | // qrBitBuffer
1558 | //---------------------------------------------------------------------
1559 |
1560 | var qrBitBuffer = function() {
1561 |
1562 | var _buffer = [];
1563 | var _length = 0;
1564 |
1565 | var _this = {};
1566 |
1567 | _this.getBuffer = function() {
1568 | return _buffer;
1569 | };
1570 |
1571 | _this.getAt = function(index) {
1572 | var bufIndex = Math.floor(index / 8);
1573 | return ( (_buffer[bufIndex] >>> (7 - index % 8) ) & 1) == 1;
1574 | };
1575 |
1576 | _this.put = function(num, length) {
1577 | for (var i = 0; i < length; i += 1) {
1578 | _this.putBit( ( (num >>> (length - i - 1) ) & 1) == 1);
1579 | }
1580 | };
1581 |
1582 | _this.getLengthInBits = function() {
1583 | return _length;
1584 | };
1585 |
1586 | _this.putBit = function(bit) {
1587 |
1588 | var bufIndex = Math.floor(_length / 8);
1589 | if (_buffer.length <= bufIndex) {
1590 | _buffer.push(0);
1591 | }
1592 |
1593 | if (bit) {
1594 | _buffer[bufIndex] |= (0x80 >>> (_length % 8) );
1595 | }
1596 |
1597 | _length += 1;
1598 | };
1599 |
1600 | return _this;
1601 | };
1602 |
1603 | //---------------------------------------------------------------------
1604 | // qrNumber
1605 | //---------------------------------------------------------------------
1606 |
1607 | var qrNumber = function(data) {
1608 |
1609 | var _mode = QRMode.MODE_NUMBER;
1610 | var _data = data;
1611 |
1612 | var _this = {};
1613 |
1614 | _this.getMode = function() {
1615 | return _mode;
1616 | };
1617 |
1618 | _this.getLength = function(buffer) {
1619 | return _data.length;
1620 | };
1621 |
1622 | _this.write = function(buffer) {
1623 |
1624 | var data = _data;
1625 |
1626 | var i = 0;
1627 |
1628 | while (i + 2 < data.length) {
1629 | buffer.put(strToNum(data.substring(i, i + 3) ), 10);
1630 | i += 3;
1631 | }
1632 |
1633 | if (i < data.length) {
1634 | if (data.length - i == 1) {
1635 | buffer.put(strToNum(data.substring(i, i + 1) ), 4);
1636 | } else if (data.length - i == 2) {
1637 | buffer.put(strToNum(data.substring(i, i + 2) ), 7);
1638 | }
1639 | }
1640 | };
1641 |
1642 | var strToNum = function(s) {
1643 | var num = 0;
1644 | for (var i = 0; i < s.length; i += 1) {
1645 | num = num * 10 + chatToNum(s.charAt(i) );
1646 | }
1647 | return num;
1648 | };
1649 |
1650 | var chatToNum = function(c) {
1651 | if ('0' <= c && c <= '9') {
1652 | return c.charCodeAt(0) - '0'.charCodeAt(0);
1653 | }
1654 | throw 'illegal char :' + c;
1655 | };
1656 |
1657 | return _this;
1658 | };
1659 |
1660 | //---------------------------------------------------------------------
1661 | // qrAlphaNum
1662 | //---------------------------------------------------------------------
1663 |
1664 | var qrAlphaNum = function(data) {
1665 |
1666 | var _mode = QRMode.MODE_ALPHA_NUM;
1667 | var _data = data;
1668 |
1669 | var _this = {};
1670 |
1671 | _this.getMode = function() {
1672 | return _mode;
1673 | };
1674 |
1675 | _this.getLength = function(buffer) {
1676 | return _data.length;
1677 | };
1678 |
1679 | _this.write = function(buffer) {
1680 |
1681 | var s = _data;
1682 |
1683 | var i = 0;
1684 |
1685 | while (i + 1 < s.length) {
1686 | buffer.put(
1687 | getCode(s.charAt(i) ) * 45 +
1688 | getCode(s.charAt(i + 1) ), 11);
1689 | i += 2;
1690 | }
1691 |
1692 | if (i < s.length) {
1693 | buffer.put(getCode(s.charAt(i) ), 6);
1694 | }
1695 | };
1696 |
1697 | var getCode = function(c) {
1698 |
1699 | if ('0' <= c && c <= '9') {
1700 | return c.charCodeAt(0) - '0'.charCodeAt(0);
1701 | } else if ('A' <= c && c <= 'Z') {
1702 | return c.charCodeAt(0) - 'A'.charCodeAt(0) + 10;
1703 | } else {
1704 | switch (c) {
1705 | case ' ' : return 36;
1706 | case '$' : return 37;
1707 | case '%' : return 38;
1708 | case '*' : return 39;
1709 | case '+' : return 40;
1710 | case '-' : return 41;
1711 | case '.' : return 42;
1712 | case '/' : return 43;
1713 | case ':' : return 44;
1714 | default :
1715 | throw 'illegal char :' + c;
1716 | }
1717 | }
1718 | };
1719 |
1720 | return _this;
1721 | };
1722 |
1723 | //---------------------------------------------------------------------
1724 | // qr8BitByte
1725 | //---------------------------------------------------------------------
1726 |
1727 | var qr8BitByte = function(data) {
1728 |
1729 | var _mode = QRMode.MODE_8BIT_BYTE;
1730 | var _data = data;
1731 | var _bytes = qrcode.stringToBytes(data);
1732 |
1733 | var _this = {};
1734 |
1735 | _this.getMode = function() {
1736 | return _mode;
1737 | };
1738 |
1739 | _this.getLength = function(buffer) {
1740 | return _bytes.length;
1741 | };
1742 |
1743 | _this.write = function(buffer) {
1744 | for (var i = 0; i < _bytes.length; i += 1) {
1745 | buffer.put(_bytes[i], 8);
1746 | }
1747 | };
1748 |
1749 | return _this;
1750 | };
1751 |
1752 | //---------------------------------------------------------------------
1753 | // qrKanji
1754 | //---------------------------------------------------------------------
1755 |
1756 | var qrKanji = function(data) {
1757 |
1758 | var _mode = QRMode.MODE_KANJI;
1759 | var _data = data;
1760 |
1761 | var stringToBytes = qrcode.stringToBytesFuncs['SJIS'];
1762 | if (!stringToBytes) {
1763 | throw 'sjis not supported.';
1764 | }
1765 | !function(c, code) {
1766 | // self test for sjis support.
1767 | var test = stringToBytes(c);
1768 | if (test.length != 2 || ( (test[0] << 8) | test[1]) != code) {
1769 | throw 'sjis not supported.';
1770 | }
1771 | }('\u53cb', 0x9746);
1772 |
1773 | var _bytes = stringToBytes(data);
1774 |
1775 | var _this = {};
1776 |
1777 | _this.getMode = function() {
1778 | return _mode;
1779 | };
1780 |
1781 | _this.getLength = function(buffer) {
1782 | return ~~(_bytes.length / 2);
1783 | };
1784 |
1785 | _this.write = function(buffer) {
1786 |
1787 | var data = _bytes;
1788 |
1789 | var i = 0;
1790 |
1791 | while (i + 1 < data.length) {
1792 |
1793 | var c = ( (0xff & data[i]) << 8) | (0xff & data[i + 1]);
1794 |
1795 | if (0x8140 <= c && c <= 0x9FFC) {
1796 | c -= 0x8140;
1797 | } else if (0xE040 <= c && c <= 0xEBBF) {
1798 | c -= 0xC140;
1799 | } else {
1800 | throw 'illegal char at ' + (i + 1) + '/' + c;
1801 | }
1802 |
1803 | c = ( (c >>> 8) & 0xff) * 0xC0 + (c & 0xff);
1804 |
1805 | buffer.put(c, 13);
1806 |
1807 | i += 2;
1808 | }
1809 |
1810 | if (i < data.length) {
1811 | throw 'illegal char at ' + (i + 1);
1812 | }
1813 | };
1814 |
1815 | return _this;
1816 | };
1817 |
1818 | //=====================================================================
1819 | // GIF Support etc.
1820 | //
1821 |
1822 | //---------------------------------------------------------------------
1823 | // byteArrayOutputStream
1824 | //---------------------------------------------------------------------
1825 |
1826 | var byteArrayOutputStream = function() {
1827 |
1828 | var _bytes = [];
1829 |
1830 | var _this = {};
1831 |
1832 | _this.writeByte = function(b) {
1833 | _bytes.push(b & 0xff);
1834 | };
1835 |
1836 | _this.writeShort = function(i) {
1837 | _this.writeByte(i);
1838 | _this.writeByte(i >>> 8);
1839 | };
1840 |
1841 | _this.writeBytes = function(b, off, len) {
1842 | off = off || 0;
1843 | len = len || b.length;
1844 | for (var i = 0; i < len; i += 1) {
1845 | _this.writeByte(b[i + off]);
1846 | }
1847 | };
1848 |
1849 | _this.writeString = function(s) {
1850 | for (var i = 0; i < s.length; i += 1) {
1851 | _this.writeByte(s.charCodeAt(i) );
1852 | }
1853 | };
1854 |
1855 | _this.toByteArray = function() {
1856 | return _bytes;
1857 | };
1858 |
1859 | _this.toString = function() {
1860 | var s = '';
1861 | s += '[';
1862 | for (var i = 0; i < _bytes.length; i += 1) {
1863 | if (i > 0) {
1864 | s += ',';
1865 | }
1866 | s += _bytes[i];
1867 | }
1868 | s += ']';
1869 | return s;
1870 | };
1871 |
1872 | return _this;
1873 | };
1874 |
1875 | //---------------------------------------------------------------------
1876 | // base64EncodeOutputStream
1877 | //---------------------------------------------------------------------
1878 |
1879 | var base64EncodeOutputStream = function() {
1880 |
1881 | var _buffer = 0;
1882 | var _buflen = 0;
1883 | var _length = 0;
1884 | var _base64 = '';
1885 |
1886 | var _this = {};
1887 |
1888 | var writeEncoded = function(b) {
1889 | _base64 += String.fromCharCode(encode(b & 0x3f) );
1890 | };
1891 |
1892 | var encode = function(n) {
1893 | if (n < 0) {
1894 | // error.
1895 | } else if (n < 26) {
1896 | return 0x41 + n;
1897 | } else if (n < 52) {
1898 | return 0x61 + (n - 26);
1899 | } else if (n < 62) {
1900 | return 0x30 + (n - 52);
1901 | } else if (n == 62) {
1902 | return 0x2b;
1903 | } else if (n == 63) {
1904 | return 0x2f;
1905 | }
1906 | throw 'n:' + n;
1907 | };
1908 |
1909 | _this.writeByte = function(n) {
1910 |
1911 | _buffer = (_buffer << 8) | (n & 0xff);
1912 | _buflen += 8;
1913 | _length += 1;
1914 |
1915 | while (_buflen >= 6) {
1916 | writeEncoded(_buffer >>> (_buflen - 6) );
1917 | _buflen -= 6;
1918 | }
1919 | };
1920 |
1921 | _this.flush = function() {
1922 |
1923 | if (_buflen > 0) {
1924 | writeEncoded(_buffer << (6 - _buflen) );
1925 | _buffer = 0;
1926 | _buflen = 0;
1927 | }
1928 |
1929 | if (_length % 3 != 0) {
1930 | // padding
1931 | var padlen = 3 - _length % 3;
1932 | for (var i = 0; i < padlen; i += 1) {
1933 | _base64 += '=';
1934 | }
1935 | }
1936 | };
1937 |
1938 | _this.toString = function() {
1939 | return _base64;
1940 | };
1941 |
1942 | return _this;
1943 | };
1944 |
1945 | //---------------------------------------------------------------------
1946 | // base64DecodeInputStream
1947 | //---------------------------------------------------------------------
1948 |
1949 | var base64DecodeInputStream = function(str) {
1950 |
1951 | var _str = str;
1952 | var _pos = 0;
1953 | var _buffer = 0;
1954 | var _buflen = 0;
1955 |
1956 | var _this = {};
1957 |
1958 | _this.read = function() {
1959 |
1960 | while (_buflen < 8) {
1961 |
1962 | if (_pos >= _str.length) {
1963 | if (_buflen == 0) {
1964 | return -1;
1965 | }
1966 | throw 'unexpected end of file./' + _buflen;
1967 | }
1968 |
1969 | var c = _str.charAt(_pos);
1970 | _pos += 1;
1971 |
1972 | if (c == '=') {
1973 | _buflen = 0;
1974 | return -1;
1975 | } else if (c.match(/^\s$/) ) {
1976 | // ignore if whitespace.
1977 | continue;
1978 | }
1979 |
1980 | _buffer = (_buffer << 6) | decode(c.charCodeAt(0) );
1981 | _buflen += 6;
1982 | }
1983 |
1984 | var n = (_buffer >>> (_buflen - 8) ) & 0xff;
1985 | _buflen -= 8;
1986 | return n;
1987 | };
1988 |
1989 | var decode = function(c) {
1990 | if (0x41 <= c && c <= 0x5a) {
1991 | return c - 0x41;
1992 | } else if (0x61 <= c && c <= 0x7a) {
1993 | return c - 0x61 + 26;
1994 | } else if (0x30 <= c && c <= 0x39) {
1995 | return c - 0x30 + 52;
1996 | } else if (c == 0x2b) {
1997 | return 62;
1998 | } else if (c == 0x2f) {
1999 | return 63;
2000 | } else {
2001 | throw 'c:' + c;
2002 | }
2003 | };
2004 |
2005 | return _this;
2006 | };
2007 |
2008 | //---------------------------------------------------------------------
2009 | // gifImage (B/W)
2010 | //---------------------------------------------------------------------
2011 |
2012 | var gifImage = function(width, height) {
2013 |
2014 | var _width = width;
2015 | var _height = height;
2016 | var _data = new Array(width * height);
2017 |
2018 | var _this = {};
2019 |
2020 | _this.setPixel = function(x, y, pixel) {
2021 | _data[y * _width + x] = pixel;
2022 | };
2023 |
2024 | _this.write = function(out) {
2025 |
2026 | //---------------------------------
2027 | // GIF Signature
2028 |
2029 | out.writeString('GIF87a');
2030 |
2031 | //---------------------------------
2032 | // Screen Descriptor
2033 |
2034 | out.writeShort(_width);
2035 | out.writeShort(_height);
2036 |
2037 | out.writeByte(0x80); // 2bit
2038 | out.writeByte(0);
2039 | out.writeByte(0);
2040 |
2041 | //---------------------------------
2042 | // Global Color Map
2043 |
2044 | // black
2045 | out.writeByte(0x00);
2046 | out.writeByte(0x00);
2047 | out.writeByte(0x00);
2048 |
2049 | // white
2050 | out.writeByte(0xff);
2051 | out.writeByte(0xff);
2052 | out.writeByte(0xff);
2053 |
2054 | //---------------------------------
2055 | // Image Descriptor
2056 |
2057 | out.writeString(',');
2058 | out.writeShort(0);
2059 | out.writeShort(0);
2060 | out.writeShort(_width);
2061 | out.writeShort(_height);
2062 | out.writeByte(0);
2063 |
2064 | //---------------------------------
2065 | // Local Color Map
2066 |
2067 | //---------------------------------
2068 | // Raster Data
2069 |
2070 | var lzwMinCodeSize = 2;
2071 | var raster = getLZWRaster(lzwMinCodeSize);
2072 |
2073 | out.writeByte(lzwMinCodeSize);
2074 |
2075 | var offset = 0;
2076 |
2077 | while (raster.length - offset > 255) {
2078 | out.writeByte(255);
2079 | out.writeBytes(raster, offset, 255);
2080 | offset += 255;
2081 | }
2082 |
2083 | out.writeByte(raster.length - offset);
2084 | out.writeBytes(raster, offset, raster.length - offset);
2085 | out.writeByte(0x00);
2086 |
2087 | //---------------------------------
2088 | // GIF Terminator
2089 | out.writeString(';');
2090 | };
2091 |
2092 | var bitOutputStream = function(out) {
2093 |
2094 | var _out = out;
2095 | var _bitLength = 0;
2096 | var _bitBuffer = 0;
2097 |
2098 | var _this = {};
2099 |
2100 | _this.write = function(data, length) {
2101 |
2102 | if ( (data >>> length) != 0) {
2103 | throw 'length over';
2104 | }
2105 |
2106 | while (_bitLength + length >= 8) {
2107 | _out.writeByte(0xff & ( (data << _bitLength) | _bitBuffer) );
2108 | length -= (8 - _bitLength);
2109 | data >>>= (8 - _bitLength);
2110 | _bitBuffer = 0;
2111 | _bitLength = 0;
2112 | }
2113 |
2114 | _bitBuffer = (data << _bitLength) | _bitBuffer;
2115 | _bitLength = _bitLength + length;
2116 | };
2117 |
2118 | _this.flush = function() {
2119 | if (_bitLength > 0) {
2120 | _out.writeByte(_bitBuffer);
2121 | }
2122 | };
2123 |
2124 | return _this;
2125 | };
2126 |
2127 | var getLZWRaster = function(lzwMinCodeSize) {
2128 |
2129 | var clearCode = 1 << lzwMinCodeSize;
2130 | var endCode = (1 << lzwMinCodeSize) + 1;
2131 | var bitLength = lzwMinCodeSize + 1;
2132 |
2133 | // Setup LZWTable
2134 | var table = lzwTable();
2135 |
2136 | for (var i = 0; i < clearCode; i += 1) {
2137 | table.add(String.fromCharCode(i) );
2138 | }
2139 | table.add(String.fromCharCode(clearCode) );
2140 | table.add(String.fromCharCode(endCode) );
2141 |
2142 | var byteOut = byteArrayOutputStream();
2143 | var bitOut = bitOutputStream(byteOut);
2144 |
2145 | // clear code
2146 | bitOut.write(clearCode, bitLength);
2147 |
2148 | var dataIndex = 0;
2149 |
2150 | var s = String.fromCharCode(_data[dataIndex]);
2151 | dataIndex += 1;
2152 |
2153 | while (dataIndex < _data.length) {
2154 |
2155 | var c = String.fromCharCode(_data[dataIndex]);
2156 | dataIndex += 1;
2157 |
2158 | if (table.contains(s + c) ) {
2159 |
2160 | s = s + c;
2161 |
2162 | } else {
2163 |
2164 | bitOut.write(table.indexOf(s), bitLength);
2165 |
2166 | if (table.size() < 0xfff) {
2167 |
2168 | if (table.size() == (1 << bitLength) ) {
2169 | bitLength += 1;
2170 | }
2171 |
2172 | table.add(s + c);
2173 | }
2174 |
2175 | s = c;
2176 | }
2177 | }
2178 |
2179 | bitOut.write(table.indexOf(s), bitLength);
2180 |
2181 | // end code
2182 | bitOut.write(endCode, bitLength);
2183 |
2184 | bitOut.flush();
2185 |
2186 | return byteOut.toByteArray();
2187 | };
2188 |
2189 | var lzwTable = function() {
2190 |
2191 | var _map = {};
2192 | var _size = 0;
2193 |
2194 | var _this = {};
2195 |
2196 | _this.add = function(key) {
2197 | if (_this.contains(key) ) {
2198 | throw 'dup key:' + key;
2199 | }
2200 | _map[key] = _size;
2201 | _size += 1;
2202 | };
2203 |
2204 | _this.size = function() {
2205 | return _size;
2206 | };
2207 |
2208 | _this.indexOf = function(key) {
2209 | return _map[key];
2210 | };
2211 |
2212 | _this.contains = function(key) {
2213 | return typeof _map[key] != 'undefined';
2214 | };
2215 |
2216 | return _this;
2217 | };
2218 |
2219 | return _this;
2220 | };
2221 |
2222 | var createDataURL = function(width, height, getPixel) {
2223 | var gif = gifImage(width, height);
2224 | for (var y = 0; y < height; y += 1) {
2225 | for (var x = 0; x < width; x += 1) {
2226 | gif.setPixel(x, y, getPixel(x, y) );
2227 | }
2228 | }
2229 |
2230 | var b = byteArrayOutputStream();
2231 | gif.write(b);
2232 |
2233 | var base64 = base64EncodeOutputStream();
2234 | var bytes = b.toByteArray();
2235 | for (var i = 0; i < bytes.length; i += 1) {
2236 | base64.writeByte(bytes[i]);
2237 | }
2238 | base64.flush();
2239 |
2240 | return 'data:image/gif;base64,' + base64;
2241 | };
2242 |
2243 | //---------------------------------------------------------------------
2244 | // returns qrcode function.
2245 |
2246 | return qrcode;
2247 | }();
2248 |
2249 | // multibyte support
2250 | !function() {
2251 |
2252 | qrcode.stringToBytesFuncs['UTF-8'] = function(s) {
2253 | // http://stackoverflow.com/questions/18729405/how-to-convert-utf8-string-to-byte-array
2254 | function toUTF8Array(str) {
2255 | var utf8 = [];
2256 | for (var i=0; i < str.length; i++) {
2257 | var charcode = str.charCodeAt(i);
2258 | if (charcode < 0x80) utf8.push(charcode);
2259 | else if (charcode < 0x800) {
2260 | utf8.push(0xc0 | (charcode >> 6),
2261 | 0x80 | (charcode & 0x3f));
2262 | }
2263 | else if (charcode < 0xd800 || charcode >= 0xe000) {
2264 | utf8.push(0xe0 | (charcode >> 12),
2265 | 0x80 | ((charcode>>6) & 0x3f),
2266 | 0x80 | (charcode & 0x3f));
2267 | }
2268 | // surrogate pair
2269 | else {
2270 | i++;
2271 | // UTF-16 encodes 0x10000-0x10FFFF by
2272 | // subtracting 0x10000 and splitting the
2273 | // 20 bits of 0x0-0xFFFFF into two halves
2274 | charcode = 0x10000 + (((charcode & 0x3ff)<<10)
2275 | | (str.charCodeAt(i) & 0x3ff));
2276 | utf8.push(0xf0 | (charcode >>18),
2277 | 0x80 | ((charcode>>12) & 0x3f),
2278 | 0x80 | ((charcode>>6) & 0x3f),
2279 | 0x80 | (charcode & 0x3f));
2280 | }
2281 | }
2282 | return utf8;
2283 | }
2284 | return toUTF8Array(s);
2285 | };
2286 |
2287 | }();
2288 |
2289 | (function (factory) {
2290 | if (typeof define === 'function' && define.amd) {
2291 | define([], factory);
2292 | } else if (typeof exports === 'object') {
2293 | module.exports = factory();
2294 | }
2295 | }(function () {
2296 | return qrcode;
2297 | }));
2298 |
--------------------------------------------------------------------------------
/public/js/sketches/create.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | /* eslint-disable no-unused-vars */
3 | // Feel free to give a better name to this file
4 | // This Javascript is powering /createPoll
5 |
6 | function setup() {
7 | noCanvas();
8 | const textarea = createElement("textarea");
9 | textarea.attribute("placeholder", "Enter question here");
10 |
11 | const defaultOptions = 5;
12 | const inputsDiv = createElement("div").addClass("options");
13 | inputsDiv.id("inputsDiv");
14 | for (let i = 0; i < defaultOptions; i++) {
15 | const newInput = createInput().attribute(
16 | "placeholder",
17 | "Option " + (i + 1)
18 | );
19 |
20 | inputsDiv.child(newInput);
21 | }
22 |
23 | const buttonsDiv = createElement("div").addClass("buttonsArray");
24 |
25 | const addOption = createButton("Add Option").addClass("addOption");
26 | const removeLastOption =
27 | createButton("Remove last Option").addClass("removeLastOption");
28 | const submit = createButton("Create!");
29 |
30 | buttonsDiv.child(addOption);
31 | buttonsDiv.child(removeLastOption);
32 | buttonsDiv.child(submit);
33 |
34 | addOption.mousePressed(async () => {
35 | const currentOptionsLength =
36 | document.getElementById("inputsDiv").children.length;
37 |
38 | const newInput = createInput().attribute(
39 | "placeholder",
40 | "Option " + (currentOptionsLength + 1)
41 | );
42 |
43 | inputsDiv.child(newInput);
44 | });
45 |
46 | removeLastOption.mousePressed(async () => {
47 | const currentOptionsLength =
48 | document.getElementById("inputsDiv").children.length;
49 |
50 | if (currentOptionsLength > 2) {
51 | var list = document.getElementById("inputsDiv");
52 | list.removeChild(list.childNodes[currentOptionsLength - 1]);
53 | }
54 | });
55 |
56 | submit.mousePressed(async () => {
57 | const question = textarea.value();
58 | let options = [];
59 |
60 | const optionsDiv = document.getElementsByClassName("options")[0];
61 |
62 | for (let option of optionsDiv.children) {
63 | options.push(option.value);
64 | }
65 |
66 | if (!question) return alert("You need to enter a question.");
67 | let actualOptions = [];
68 | for (let option of options) {
69 | if (option != "") {
70 | actualOptions.push(option);
71 | }
72 | }
73 | if (actualOptions.length < 2)
74 | return alert("You need to mention at least two valid options.");
75 |
76 | const response = await fetch("/api/new", {
77 | method: "POST",
78 | headers: {
79 | "Content-Type": "application/json",
80 | },
81 | body: JSON.stringify({
82 | question,
83 | actualOptions,
84 | }),
85 | });
86 | const { id } = await response.json();
87 |
88 | textarea.hide();
89 | inputsDiv.hide();
90 | addOption.hide();
91 | removeLastOption.hide();
92 | submit.hide();
93 |
94 | const voteLink = location.origin + "/vote/" + id;
95 | const pollLink = location.origin + "/poll/" + id;
96 | createDiv(`
97 | Poll Created Successfully.
98 | Poll ID: ${id}
99 | Poll Voting Link: ${voteLink}
100 | Poll Results Link: ${pollLink}
101 | `);
102 |
103 | // add qrcode
104 | const typeNumber = 4;
105 | const errorCorrectionLevel = "L";
106 | const qr = qrcode(typeNumber, errorCorrectionLevel);
107 | qr.addData(voteLink);
108 | qr.make();
109 |
110 | createDiv(qr.createSvgTag(5, 5));
111 | });
112 | }
113 |
--------------------------------------------------------------------------------
/public/js/sketches/poll.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | /* eslint-disable no-unused-vars */
3 |
4 | async function setup() {
5 | noCanvas();
6 | poll = new Poll();
7 | poll.initPoll();
8 |
9 |
10 | // from https://github.com/CodingTrain/LateNight/issues/1
11 | const colorPairs = [
12 | ['#9253a1', '#70327e'],
13 | ['#f063a4', '#ec015a'],
14 | ['#2dc5f4', '#0b6a88'],
15 | ['#fcee21', '#f89e4f'],
16 | ['#f16164', '#ec015a'],
17 | ['#70327e', '#9253a1'],
18 | ['#a42963', '#ec015a'],
19 | ['#0b6a88', '#2dc5f4'],
20 | ['#f89e4f', '#fcee21'],
21 | ['#ec015a', '#a42963'],
22 | ];
23 |
24 | const [first, second] = random(colorPairs);
25 | const degrees = random(35, 75) * (random() < 0.5 ? -1 : 1);
26 |
27 | const gradient = `repeating-linear-gradient(${nf(degrees, 0, 1)}deg, ${first}, ${first} 10px, ${second} 10px, ${second} 20px`;
28 |
29 | const root = document.documentElement;
30 | root.style.setProperty('--monochrome-gradient', gradient);
31 |
32 | if (green(color(first)) > 127 && green(color(second)) > 127) {
33 | root.style.setProperty('--progressbar-color', 'black');
34 | }
35 | }
--------------------------------------------------------------------------------
/public/js/sketches/vote.js:
--------------------------------------------------------------------------------
1 | // marked for deletion?
--------------------------------------------------------------------------------
/server/api.js:
--------------------------------------------------------------------------------
1 | const database = require("./helpers/database");
2 | const {requiresAuthentication} = require("./validation/basicauth");
3 | const createNewPoll = require("./helpers/createNewPoll");
4 | const express = require("express");
5 | const router = express.Router();
6 |
7 | router.post("/new", requiresAuthentication, async (request, response) => {
8 | // request body should be in this form
9 | // {
10 | // question: string,
11 | // options: string[]
12 | // }
13 |
14 | let { question, options } = request.body;
15 |
16 | // Truthy filter (falsy values will be removed from the array)
17 | options = options.filter((x) => x);
18 |
19 | // Create a poll object, insert it in the database and get the ID back
20 | let pollID = await createNewPoll(question, options);
21 |
22 | // Send a response carryinh the poll id
23 | response.send({
24 | status: "success",
25 | message: "Poll created successfully!",
26 | id: pollID,
27 | });
28 | });
29 |
30 | // GET newest poll data
31 | // Have to put this route before the generic one because of how express uses routes
32 |
33 | // GET Poll Data for specific POLL ID
34 | router.get("/poll/:pollId", async (request, response) => {
35 | const _id = request.params.pollId;
36 |
37 | const poll = await database.findOne({ _id });
38 |
39 | response.send(
40 | poll || {
41 | status: "error",
42 | message: "Poll not found",
43 | }
44 | );
45 | });
46 |
47 |
48 | //End point to delete a poll if authenticated
49 | router.delete('/poll/:pollId', requiresAuthentication, async function(req, res) {
50 | // get poll from url
51 | const _id = req.params.pollId;
52 | const poll = await database.findOne({ _id });
53 |
54 | if (!poll) {
55 | res.status(404);
56 | res.json({
57 | status: 'error',
58 | message: 'Poll not found'
59 | })
60 | return;
61 | }
62 |
63 | let count = await database.remove({ _id });
64 |
65 | if (count == 0) {
66 | res.status(500);
67 | res.json({
68 | status: 'error',
69 | message: 'Poll not found'
70 | })
71 | } else {
72 | res.json({
73 | status: 'success',
74 | message: 'Poll deleted successfully'
75 | })
76 | }
77 |
78 |
79 | });
80 |
81 |
82 | module.exports = router;
83 |
--------------------------------------------------------------------------------
/server/helpers/broadcaster.js:
--------------------------------------------------------------------------------
1 | class Broadcaster {
2 | constructor() {
3 | this.sockets = []
4 | }
5 |
6 | registerSocket(pollId, socket) {
7 | if(!this.sockets[pollId]) {
8 | this.sockets[pollId] = {};
9 | }
10 |
11 | this.sockets[pollId][socket.id] = socket;
12 | }
13 |
14 | unregisterSocket(socketId) {
15 | if (this.sockets) {
16 | for (let pollId in this.sockets) {
17 | if (this.sockets[pollId][socketId]) {
18 | delete this.sockets[pollId][socketId];
19 | }
20 | }
21 | }
22 | }
23 |
24 | updatePoll(poll) {
25 | const sockets = this.sockets[poll._id];
26 | if (sockets) {
27 | for(let socketId in sockets) {
28 | sockets[socketId].emit('updatePoll', poll);
29 | }
30 | }
31 | }
32 | }
33 |
34 | module.exports = new Broadcaster();
35 |
--------------------------------------------------------------------------------
/server/helpers/createNewPoll.js:
--------------------------------------------------------------------------------
1 | // TODO: Move into a separate file which interacts with the database
2 |
3 | const database = require("./database");
4 |
5 | async function createNewPoll(question, options) {
6 | // Poll structure (in database):
7 | // {
8 | // question: string,
9 | // options: string[],
10 | // votes: number[],
11 | // timestamp: created time (in ms) (since UNIX epoch)
12 | // _id: autogenerated by NeDB
13 | // }
14 |
15 | // If no values passed then we use default values
16 | let { _id } = await database.insert({
17 | question: question || "What should we do now?",
18 | options: options || [
19 | "Live Poll 📄",
20 | "Community Contributions 🎡",
21 | "Bots 🤖",
22 | ],
23 | votes: new Array(options ? options.length : 3).fill(0),
24 | timestamp: Date.now(),
25 | });
26 | return _id;
27 | }
28 |
29 | module.exports = createNewPoll;
30 |
--------------------------------------------------------------------------------
/server/helpers/database.js:
--------------------------------------------------------------------------------
1 | const Datastore = require("nedb-promises");
2 | module.exports = Datastore.create("database.db");
3 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | require("dotenv").config();
3 | const express = require("express");
4 | const app = express();
5 | const http = require('http').createServer(app)
6 | const io = require("socket.io")(http);
7 | broadcaster = require("./helpers/broadcaster");
8 |
9 | app.set("views", "./views");
10 | app.set("view engine", "pug");
11 | app.set("broadcaster", broadcaster);
12 |
13 | app.use(express.static("public"));
14 | app.use(express.json()); // For parsing application/json
15 | app.use(express.urlencoded({ extended: true })); // to support URL-encoded bodies
16 |
17 | // Routes
18 | const webRoutes = require("./web");
19 | app.use("/", webRoutes);
20 |
21 | const apiRoutes = require("./api");
22 | app.use("/api", apiRoutes);
23 |
24 | // const createNewPoll = require("./helpers/createNewPoll");
25 | // createNewPoll("Your question here", ["Option A", "Option B", "Option C"]);
26 |
27 | io.on('connection', (socket) => {
28 | socket.on('listenForPoll', (pollId) => {
29 | broadcaster.registerSocket(pollId, socket);
30 | });
31 | socket.on('disconnect', () => {
32 | broadcaster.unregisterSocket(socket.id)
33 | })
34 | });
35 |
36 | const port = process.env.PORT || 3000;
37 | http.listen(port, () =>
38 | console.log(`Server running at http://localhost:${port}`)
39 | );
40 |
--------------------------------------------------------------------------------
/server/validation/antipollspam.js:
--------------------------------------------------------------------------------
1 | // # This package contains a simple time-based anti-spam
2 | // # implementation.
3 | // #
4 | // # Details:
5 | // # The approach is to register ids (for example ip
6 | // # hashes) to a hashtable (JS object) as keys with
7 | // # timestamps as values. Vals are used for flushing.
8 | // #
9 | // # This is done while calling the main interface/func
10 | // # -- if a record already exists, then
11 | // # this func will return false, else true. On each call,
12 | // # old entries will be removed if their timestamp is
13 | // # too stale (dictated by integer),
14 | // # though scans are done on a interval specified with
15 | // # integer.
16 | // #
17 | // # Main interface:
18 | // # check() -- this is the default export.
19 | // #
20 | // #
21 |
22 | // # Holds access state. Keys are IDs while
23 | // # values are timestampts.
24 | let accessState = {};
25 |
26 | // # How many seconds before a record becomes
27 | // # stale and should be deleted.
28 | const deltaSecondsFlushID = 60 * 60 * 24; //Only 1 vote per day
29 |
30 | // # How many seconds before
31 | // # is scanned for stale records. Used to
32 | // # reduce unnecessary scans.
33 | const deltaSecondsFlushScan = 60;
34 | // # timestamp for last flush.
35 | let timestampLastFlush = nowUnixSeconds();
36 |
37 | // # Unixtime: milliseconds -> seconds.
38 | function nowUnixSeconds() {
39 | return Date.now() / 1000;
40 | }
41 |
42 | // # Flush records in (pkg lvl var) if they are
43 | // # older than (pkg lvl var). This
44 | // # procedure is aborted if not enough time has passed,
45 | // # which is specified by (pkg lvl var).
46 | function tryFlushStale() {
47 | // # Abort flush procedure if not enough time has passed.
48 | if (timestampLastFlush + deltaSecondsFlushScan > nowUnixSeconds()) return;
49 |
50 | // # Look through all items.
51 | Object.keys(accessState).forEach((key) => {
52 | const timestamp = accessState[key];
53 | // # Delete if stale.
54 | if (timestamp + deltaSecondsFlushID <= nowUnixSeconds()) {
55 | delete accessState[key];
56 | }
57 | });
58 | // # Update timestamp for last flush.
59 | timestampLastFlush = nowUnixSeconds();
60 | }
61 |
62 | // # Check if an id has used up its access.
63 | // # Each call to this func will also remove
64 | // # all stale records (see
65 | // # in this pkg).
66 | // #
67 | // # Recommended usage for this live-poll app:
68 | // # id=ip -- scope: global.
69 | // # id=poll/ip -- scope: poll.
70 | // # id=poll/option/ip -- scope: option.
71 | function check(id) {
72 | // # Remove old.
73 | tryFlushStale();
74 |
75 | return accessState[id] == undefined;
76 | }
77 |
78 | // # Check if an id has used up its access.
79 | // # If it is new, then it will be registered
80 | // # and true will be returned, else false.
81 | // #
82 | // # Each call to this func will also remove
83 | // # all stale records (see
84 | // # in this pkg).
85 | // #
86 | // # Recommended usage for this live-poll app:
87 | // # id=ip -- scope: global.
88 | // # id=poll/ip -- scope: poll.
89 | // # id=poll/option/ip -- scope: option.
90 | function checkAndRegister(id) {
91 | // # Remove old.
92 | tryFlushStale();
93 |
94 | // # id is not registered; register & exit.
95 | if (accessState[id] == undefined) {
96 | accessState[id] = nowUnixSeconds();
97 | return true;
98 | }
99 |
100 | // # Registered exists.
101 | return false;
102 | }
103 |
104 | // # Export of numbers is done for unit testing.
105 | module.exports = {
106 | check,
107 | checkAndRegister,
108 | deltaSecondsFlushID,
109 | deltaSecondsFlushScan,
110 | };
111 |
--------------------------------------------------------------------------------
/server/validation/antipollspam_test.js:
--------------------------------------------------------------------------------
1 | // # This pkg functions as a unit-test for antipollspam.js.
2 |
3 | const {
4 | check,
5 | deltaSecondsFlushID,
6 | deltaSecondsFlushScan,
7 | } = require("./antipollspam.js");
8 |
9 | console.log(`
10 | Note: Make sure to reduce and
11 | in './antipollspam.js'
12 | to a low integer before running this test.
13 | Not doing so will make this test time-expensive.
14 |
15 | Current : ${deltaSecondsFlushID}
16 | Current : ${deltaSecondsFlushScan}
17 | `);
18 |
19 | // # Generic sleep func.
20 | function sleep(ms) {
21 | return new Promise((resolve) => setTimeout(resolve, ms));
22 | }
23 |
24 | // # Verify that an id check passes on first introduction
25 | // # and fails on a second check.
26 | async function testInstantDoubleRegister() {
27 | const id = "test_instantDoubleRegister";
28 | // # Guard false negative.
29 | if (check(id) == false) return "test_instantDoubleRegister: fail 1";
30 |
31 | // # Guard false positive.
32 | if (check(id) == true) return "test_instantDoubleRegister: fail 2";
33 |
34 | // # ok.
35 | return "test_instantDoubleRegister: ok";
36 | }
37 |
38 | // # Verify that flushing works (won't want a
39 | // # memory leak).
40 | async function testFlushing() {
41 | const id = "test_flushing";
42 | // # Guard false negative.
43 | if (check(id) == false) return "test_flushing: fail 1";
44 |
45 | // # Use largest waiting delta;
46 | // # Should guarantee record flush.
47 | let seconds = Math.max(deltaSecondsFlushID, deltaSecondsFlushScan);
48 | // # Wait until flush.
49 | await sleep(1000 * seconds + 1);
50 |
51 | // # Guard false negative. -- should act as new
52 | // # introduction since the old record is gone.
53 | if (check(id) == false) return "test_flushing: fail 2";
54 |
55 | // ok.
56 | return "test_flushing: ok";
57 | }
58 |
59 | // # Run all tests.
60 | function test() {
61 | // # Collection.
62 | const funcs = [testFlushing, testInstantDoubleRegister];
63 | // # Run & print.
64 | funcs.forEach((f) => {
65 | f().then((r) => {
66 | console.log(r);
67 | });
68 | });
69 | }
70 |
71 | test();
72 |
--------------------------------------------------------------------------------
/server/validation/basicauth.js:
--------------------------------------------------------------------------------
1 | // Middleware function that blocks the route unless basicauth headers are present
2 | function requiresAuthentication(req, res, next) {
3 | // parse login and password from headers
4 | const b64auth = (req.headers.authorization || "").split(" ")[1] || "";
5 | const [login, password] = Buffer.from(b64auth, "base64")
6 | .toString()
7 | .split(":");
8 |
9 | // Verify login and password are set and correct and allow the request to continue
10 | if (
11 | login &&
12 | password &&
13 | login === process.env.LOGIN_USERNAME &&
14 | password === process.env.LOGIN_PASSWORD
15 | ) {
16 | next();
17 | } else {
18 | // Access denied! end the request-response cycle
19 | res.set("WWW-Authenticate", 'Basic realm="401"'); // change this
20 | res.status(401).send("Authentication required."); // custom message
21 | }
22 | }
23 |
24 | module.exports = {requiresAuthentication};
25 |
--------------------------------------------------------------------------------
/server/web.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const database = require("./helpers/database");
4 | const floodChecker = require("./validation/antipollspam");
5 | const { requiresAuthentication } = require("./validation/basicauth");
6 |
7 | //Index page to have an overview of active polls (and be able to manage them perhaps) - might need some password protection
8 | router.get("/", requiresAuthentication, async (req, res) => {
9 | res.render("index", {
10 | polls: await database.find({}).sort({ timestamp: -1 }),
11 | styling: req.query,
12 | });
13 | });
14 |
15 | //Page to create a new poll
16 | router.get("/create", requiresAuthentication, function (req, res) {
17 | res.render("create", { styling: req.query });
18 | });
19 |
20 | //Route for getting the newest poll
21 | router.get("/newest", async (req, res) => {
22 | // Get all polls, sort descending by timestamp, get the first poll
23 | const poll = (await database.find({}).sort({ timestamp: -1 }))[0];
24 |
25 | if (!poll) {
26 | res.status(404);
27 | res.render("notfound", { styling: req.query });
28 | } else {
29 | res.render("poll", { poll: poll, styling: req.query });
30 | }
31 | });
32 |
33 | //Route for getting the newest poll
34 | router.get("/vote-now", async (req, res) => {
35 | // Get all polls, sort descending by timestamp, get the first poll
36 | const poll = (await database.find({}).sort({ timestamp: -1 }))[0];
37 |
38 | if (!poll) {
39 | res.status(404);
40 | res.render("notfound", { styling: req.query });
41 | } else {
42 | res.redirect("/vote/" + poll._id);
43 | }
44 | });
45 |
46 | //Page to see the results of a poll
47 | router.get("/poll/:pollId", async function (req, res) {
48 | const _id = req.params.pollId;
49 | const poll = await database.findOne({ _id });
50 |
51 | if (!poll) {
52 | res.status(404);
53 | res.render("notfound", { styling: req.query });
54 | } else {
55 | res.render("poll", { poll: poll, styling: req.query });
56 | }
57 | });
58 |
59 | //Page to add a vote to a poll
60 | router.get("/vote/:pollId", async function (req, res) {
61 | const _id = req.params.pollId;
62 |
63 | let ip = req.ip;
64 | if (req.headers["x-forwarded-for"]) {
65 | ip = req.headers["x-forwarded-for"];
66 | }
67 |
68 | const floodCheckId = _id + "_" + ip;
69 | const hasVoted = !floodChecker.check(floodCheckId);
70 |
71 | // uncomment this to disable this check
72 | // const hasVoted = false
73 |
74 | //Forward user to poll results page if already voted
75 | if (hasVoted) {
76 | res.redirect("/poll/" + _id);
77 | return;
78 | }
79 |
80 | const poll = await database.findOne({ _id });
81 |
82 | if (!poll) {
83 | res.status(404);
84 | res.render("notfound");
85 | } else {
86 | res.render("vote", { poll: poll, styling: req.query });
87 | }
88 | });
89 |
90 | //Post request to do a vote
91 | router.post("/vote/:pollId", async function (req, res) {
92 | const _id = req.params.pollId;
93 |
94 | let ip = req.ip;
95 | if (req.headers["x-forwarded-for"]) {
96 | ip = req.headers["x-forwarded-for"];
97 | }
98 |
99 | const floodCheckId = _id + "_" + ip;
100 | const isValidVote = floodChecker.checkAndRegister(floodCheckId);
101 |
102 | // uncomment this to disable this check
103 | // const isValidVote = true
104 |
105 | //Forward user to poll results page if already voted
106 | if (!isValidVote) {
107 | res.redirect("/poll/" + _id);
108 | return;
109 | }
110 |
111 | const poll = await database.findOne({ _id });
112 |
113 | if (!poll) {
114 | res.status(404);
115 | res.render("notfound");
116 | return;
117 | }
118 |
119 | const choice = req.body.vote;
120 | // If the choice is out of range from the possible options,
121 | // send and error response
122 | if (choice < 0 || choice >= poll.options.length) {
123 | res.status(404);
124 | res.render("notfound");
125 | return;
126 | }
127 |
128 | // Update the votes
129 | poll.votes[choice]++;
130 |
131 | // Push the update to the database
132 | database.update({ _id }, poll);
133 |
134 | //Push update to all connected clients
135 | req.app.get("broadcaster").updatePoll(poll);
136 |
137 | // Forward user to poll results page
138 | res.redirect("/poll/" + _id);
139 | });
140 |
141 | //Page to view the latest qrcode
142 | router.get("/qrcode/", async (req, res) => {
143 | // Get all polls, sort descending by timestamp, get the first poll
144 |
145 | const poll = (await database.find({}).sort({ timestamp: -1 }))[0];
146 |
147 | const hostAddress = req.get("host");
148 | const pollURL = `http://${hostAddress}/vote/${poll._id}`;
149 |
150 | if (!poll) {
151 | res.status(404);
152 | res.render("notfound");
153 | } else {
154 | res.render("qrcode", { pollURL, question: poll.question });
155 | }
156 | });
157 |
158 | module.exports = router;
159 |
--------------------------------------------------------------------------------
/views/create.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset="UTF-8")
5 | meta(name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover")
6 | meta(http-equiv="X-UA-Compatible" content="ie=edge")
7 | title Live Coding Train Poll
8 | link(rel="stylesheet" href="css/style.css")
9 | link(rel="stylesheet" href="css/create.css")
10 | script(src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js")
11 | script(src="/js/qrcode.js")
12 |
13 | body#main-create
14 | main
15 | h1 The Coding Train Live Poll
16 | h2 Create a poll
17 |
18 | script(src="/js/sketches/create.js")
19 |
20 | //- template(data-styling=styling)
21 | //- include footer.pug
22 |
--------------------------------------------------------------------------------
/views/footer.pug:
--------------------------------------------------------------------------------
1 | head
2 | link(rel="stylesheet" href="/css/footer.css")
3 |
4 | div(id="footer")
5 | h3 View options
6 | a(style="font-size: 0.7em" onclick="this.parentNode.parentNode.remove()")
7 | i [close]
8 |
9 | div#options-wrapper
10 | div.btn-options(class="noselect")
11 | input(type="checkbox" id="font-toggle")
12 | label(for="font-toggle") simplier font
13 | div.btn-options(class="noselect")
14 | input(type="checkbox" id="gradient-toggle")
15 | label(for="gradient-toggle") monotone gradient
16 | div.btn-options(class="noselect")
17 | input(type="checkbox" checked id="overlay-toggle")
18 | label(for="overlay-toggle") overlay mode
19 |
20 | script.
21 |
22 | // checkboxes
23 | const fontToggle = document.querySelector('#font-toggle');
24 | const gradientToggle = document.querySelector('#gradient-toggle');
25 | const overlayToggle = document.querySelector('#overlay-toggle');
26 |
27 | // first check url-parameters
28 | // extracts url-styling parameters from poll.pug
29 | const styling = document.querySelector('[data-styling]').dataset.styling;
30 | const urlStyling = (JSON.parse(styling));
31 |
32 | // check if any url-parameter was submitted (if not)
33 | if(Object.keys(urlStyling).length === 0) {
34 |
35 | // initial state from localStorage
36 | fontToggle.checked = localStorage.getItem('font-simple') == 'true';
37 | gradientToggle.checked = localStorage.getItem('no-gradient') == 'true';
38 | overlayToggle.checked = localStorage.getItem('overlay-on') != 'false';
39 |
40 | } else {
41 |
42 | // removes local storage if url-parameters are used
43 | localStorage.removeItem('font-simple');
44 | localStorage.removeItem('no-gradient');
45 | localStorage.removeItem('overlay-on');
46 |
47 | // set states from url-paramaters
48 | fontToggle.checked = urlStyling.simple == 'true';
49 | gradientToggle.checked = urlStyling.monotone == 'true';
50 | overlayToggle.checked = urlStyling.overlay == 'true';
51 | }
52 |
53 | changeFonts(fontToggle.checked);
54 | changeGradient(gradientToggle.checked);
55 | changeOverlay(overlayToggle.checked);
56 |
57 | // input handlers
58 | fontToggle.oninput = function() {
59 | localStorage.setItem('font-simple', fontToggle.checked);
60 | changeFonts(fontToggle.checked);
61 | };
62 |
63 | gradientToggle.oninput = function() {
64 | localStorage.setItem('no-gradient', gradientToggle.checked);
65 | changeGradient(gradientToggle.checked);
66 | };
67 |
68 | overlayToggle.oninput = function() {
69 | localStorage.setItem('overlay-on', overlayToggle.checked);
70 | changeOverlay(overlayToggle.checked);
71 | };
72 |
73 | // toggle font by switching the variable in css
74 | function changeFonts(simple) {
75 | if (simple)
76 | document.documentElement.style.setProperty('--codingtrain-fontface', 'Open Sans');
77 | else
78 | document.documentElement.style.setProperty('--codingtrain-fontface', 'cubanoregular');
79 | }
80 |
81 |
82 | // remove gradient by removing or adding the correct class to the progress bar
83 | function changeGradient(off) {
84 | if (off) {
85 | let elements = document.getElementsByClassName('progressBar');
86 | for (let element of elements) {
87 | element.classList.remove('gradient')
88 | }
89 | } else {
90 | let elements = document.getElementsByClassName('progressBar');
91 | for (let element of elements) {
92 | element.classList.add('gradient')
93 | }
94 | }
95 | }
96 |
97 | // toggle the overlay
98 | // default value is a transparency = 50
99 | function changeOverlay(overlay) {
100 | const style = document.querySelector('#poll-result-style');
101 | if(!style) return;
102 |
103 | if(overlay) {
104 | document.querySelector('#poll-result-style').href = "/css/poll-results-compact.css";
105 | } else {
106 | document.querySelector('#poll-result-style').href = "/css/poll-results.css";
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/views/index.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset="UTF-8")
5 | meta(name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover")
6 | meta(http-equiv="X-UA-Compatible" content="ie=edge")
7 | title Live Coding Train Poll
8 | link(rel="stylesheet" href="css/style.css")
9 | link(rel="stylesheet" href="css/polls.css")
10 | script(src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js")
11 |
12 | body
13 | main#pool-main
14 | h1 The Coding Train Live Poll
15 | h2 Currently Active Polls
16 |
17 | if polls.length
18 | ul(id="poll-container")
19 | each poll in polls
20 | li(id=`poll-${poll._id}`)
21 | a(class="title" href=`/poll/${poll._id}`)=`${poll.question}`
22 | br
23 | span(class="date")=`Created on: ${new Date(poll.timestamp).toDateString()}`
24 | br
25 | a(class="vote-btn" href=`/vote/${poll._id}`)="Go to vote"
26 | input(id=`delete_${poll._id}` class="delete-btn" data-poll-id=`${poll._id}` type="button" value="Delete poll")
27 |
28 | a(id='create' href="/create") Create a Poll
29 | else
30 | p No Polls Found!
31 | a(id="create" href="/create") Create One Here
32 |
33 | template(data-styling=styling)
34 |
35 | include footer.pug
36 |
37 | script.
38 | let delBTN = document.querySelectorAll('.delete-btn');
39 |
40 | for (let button of delBTN) {
41 | button.onclick = function() {
42 |
43 | let confirmed = confirm('Are you sure you want to delete this poll?');
44 | if (!confirmed) {
45 | return ;
46 | }
47 |
48 | let id = this.getAttribute('data-poll-id');
49 | fetch('/api/poll/' + id, {
50 | method: 'DELETE'
51 | }).then(resp => resp.json()).then(data => {
52 | if (data.status == 'success') {
53 | alert('Deleted!');
54 | let pollContainer = document.querySelector('#poll-container');
55 | let pollNode = document.querySelector(`#poll-${id}`);
56 | pollContainer.removeChild(pollNode);
57 | } else {
58 | alert('Failed to delete poll')
59 | }
60 | }).catch(err => {
61 | alert('Failed to delete poll because of error');
62 | alert(err);
63 | })
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/views/notfound.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset="UTF-8")
5 | meta(name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover")
6 | meta(http-equiv="X-UA-Compatible" content="ie=edge")
7 | title Live Coding Train Poll
8 | link(rel="stylesheet" href="/css/style.css")
9 | link(rel="stylesheet" id="poll-result-style" href="/css/poll-results.css")
10 |
11 | body
12 | main#poll-result-main
13 | h1 The Coding Train Live Poll
14 | h2 There is no ongoing vote
15 |
16 | template(data-styling=styling)
17 |
18 | include footer.pug
--------------------------------------------------------------------------------
/views/poll.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset="UTF-8")
5 | meta(name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover")
6 | meta(http-equiv="X-UA-Compatible" content="ie=edge")
7 | title Live Coding Train Poll
8 | link(rel="stylesheet" href="/css/style.css")
9 | link(rel="stylesheet" id="poll-result-style" href="/css/poll-results.css")
10 | script(src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js")
11 | script(src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/3.0.3/socket.io.min.js")
12 |
13 | body
14 | main#poll-result-main
15 | h1 The Coding Train Live Poll
16 | h2=poll.question
17 |
18 | div#results
19 | each val, index in poll.options
20 | div.option
21 | div.bar-title=val
22 | div(id="progressBar_" + index class="progressBar")
23 | p#totalVotes
24 |
25 | template(data-styling=styling, data-id=poll._id)
26 |
27 | include footer.pug
28 |
29 | script(src="/js/classes/poll.js")
30 | script(src="/js/sketches/poll.js")
31 |
--------------------------------------------------------------------------------
/views/qrcode.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | title Live Coding Train Poll
5 | link(rel="stylesheet" href="css/style.css")
6 | link(rel="stylesheet" href="css/qrcode.css")
7 | script(src="/js/qrcode.js")
8 |
9 | body
10 | h1 The Coding Train Live Poll
11 | h2 QR-Code to newest Poll
12 | section.content
13 | section.left
14 | iframe#results()
15 | section.right
16 | h3.qr-question=question
17 | div#placeHolder
18 | template(data-pollurl=pollURL)
19 |
20 |
21 | script.
22 | var typeNumber = 4;
23 | var errorCorrectionLevel = 'L';
24 | var qr = qrcode(typeNumber, errorCorrectionLevel);
25 | var pollURL = document.querySelector('[data-pollurl]').dataset.pollurl;
26 | results.src = pollURL.replace('/vote/', '/poll/');
27 | qr.addData(pollURL);
28 | qr.make();
29 | document.querySelector('#placeHolder').innerHTML = qr.createSvgTag(5, 5);
30 |
--------------------------------------------------------------------------------
/views/vote.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html
3 | head
4 | meta(charset="UTF-8")
5 | meta(name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover")
6 | meta(http-equiv="X-UA-Compatible" content="ie=edge")
7 | title Live Coding Train Poll
8 | link(rel="stylesheet" href="/css/style.css")
9 | link(rel="stylesheet" href="/css/vote.css")
10 | script(src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js")
11 |
12 | body#vote-main
13 | main
14 | h1 The Coding Train Live Poll
15 | h2=poll.question
16 |
17 | form(action="/vote/" + poll._id method="post")
18 | ul#vote
19 | each val, index in poll.options
20 | li.btn-selection
21 | input(type="radio" name="vote" id="option_" + index value=index)
22 | label(for="option_" + index)=val
23 | input(class="submit-vote" type="submit" value="Vote")
24 |
25 | template(data-styling=styling, data-id=poll._id)
26 |
27 | script(src="/js/classes/poll.js")
28 | script(src="/js/sketches/vote.js")
29 |
30 | include footer.pug
31 |
--------------------------------------------------------------------------------