├── .bowerrc
├── .gitignore
├── .jscsrc
├── .jshintrc
├── Dockerfile-mysql
├── Dockerfile-web-app
├── Gruntfile.js
├── LICENSE
├── README.md
├── Schema.sql
├── UBUNTU.md
├── bower.json
├── docker-compose.yml
├── grunt
├── concat.js
├── copy.js
├── jade.js
├── jscs.js
├── jshint.js
├── less.js
├── pleeease.js
├── replace.js
├── uglify.js
└── watch.js
├── package.json
├── public_html
├── Default.html
├── api
│ ├── add-custom-entry.php
│ ├── add-entries.php
│ ├── add-user.php
│ ├── bbi-ballot.php
│ ├── bbi-names.php
│ ├── check-user.php
│ ├── codes.php
│ ├── config_sample.php
│ ├── create-graph.php
│ ├── delete-ballot.php
│ ├── delete-entries.php
│ ├── delete-users.php
│ ├── delete-vote.php
│ ├── delete-votes.php
│ ├── duplicate-ballot.php
│ ├── get-ballots.php
│ ├── get-candidates.php
│ ├── get-key-ballot.php
│ ├── get-settings.php
│ ├── get-votes.php
│ ├── login.php
│ ├── mysql-calls.php
│ ├── new-ballot.php
│ ├── no-mysql
│ │ ├── add-entries.php
│ │ ├── add-user.php
│ │ ├── config_sample.php
│ │ ├── delete-ballot.php
│ │ ├── delete-entries.php
│ │ ├── delete-votes.php
│ │ ├── get-ballots.php
│ │ ├── get-candidates.php
│ │ ├── get-key-ballot.php
│ │ ├── get-votes.php
│ │ ├── new-ballot.php
│ │ ├── update-ballot.php
│ │ └── vote.php
│ ├── php_errorlog
│ ├── rcvis_new.php
│ ├── rcvis_notes.php
│ ├── rcvis_patch.php
│ ├── rcvis_slug.php
│ ├── results.txt
│ ├── sql.php
│ ├── unsecure.php
│ ├── update-ballot.php
│ ├── validate-voter-code.php
│ └── vote.php
├── apple-touch-icon.png
├── favicon.ico
├── fonts
│ ├── FontAwesome.otf
│ ├── fontawesome-webfont.eot
│ ├── fontawesome-webfont.svg
│ ├── fontawesome-webfont.ttf
│ ├── fontawesome-webfont.woff
│ ├── fontawesome-webfont.woff2
│ ├── glyphicons-halflings-regular.eot
│ ├── glyphicons-halflings-regular.svg
│ ├── glyphicons-halflings-regular.ttf
│ ├── glyphicons-halflings-regular.woff
│ └── glyphicons-halflings-regular.woff2
├── github-fork.png
├── img
│ └── every-vote-counts.png
├── inc
│ ├── angular.js
│ ├── lib.js
│ ├── main.css
│ ├── main.js
│ ├── main_alt.css
│ ├── ranked-choices.css
│ └── timezone-picker.js
├── index.html
├── results_json.html
├── secure-elections-instructions.html
├── sw.js
└── terms-of-service.html
└── src
├── .htaccess
├── api
├── add-entries.php
├── add-user.php
├── config_sample.php
├── delete-ballot.php
├── delete-entries.php
├── delete-votes.php
├── get-ballots.php
├── get-candidates.php
├── get-key-ballot.php
├── get-votes.php
├── new-ballot.php
├── no-mysql
│ ├── add-entries.php
│ ├── add-user.php
│ ├── config_sample.php
│ ├── delete-ballot.php
│ ├── delete-entries.php
│ ├── delete-votes.php
│ ├── get-ballots.php
│ ├── get-candidates.php
│ ├── get-key-ballot.php
│ ├── get-votes.php
│ ├── new-ballot.php
│ ├── update-ballot.php
│ └── vote.php
├── update-ballot.php
└── vote.php
├── apple-touch-icon.png
├── favicon.ico
├── fonts
└── .gitkeep
├── github-fork.png
├── jade
├── index.jade
├── pages
│ ├── about.jade
│ ├── create.jade
│ ├── home.jade
│ ├── profile.jade
│ ├── register.html
│ ├── results.jade
│ └── vote.jade
└── partials
│ ├── congrats.jade
│ ├── edit.jade
│ ├── footer.jade
│ ├── form.jade
│ ├── head.jade
│ ├── mixins.jade
│ ├── navbar.jade
│ ├── new-entries.jade
│ ├── other-nav-items.jade
│ ├── shortcode.jade
│ └── shuffle.jade
├── js
├── app.js
├── main.js
└── services
│ ├── VoteFactory.js
│ └── mc.js
└── less
├── all.less
├── common.less
├── elections.less
├── fair-vote.less
├── jquery-ui.less
├── main-theme.less
└── pages.less
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "lib"
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Folders to ignore
2 | bower_components
3 | node_modules/
4 | dist/
5 | lib
6 |
7 | # Extensions
8 | *.orig
9 | *.log
10 |
11 | # Config files
12 | /src/api/config.php
13 | .ftppass
14 |
15 | # OS / Editor Files
16 | Thumbs.db
17 | .cache
18 | .project
19 | .settings
20 | *.sublime-project
21 | *.sublime-workspace
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "requireCurlyBraces": [
3 | "if",
4 | "else",
5 | "for",
6 | "while",
7 | "do",
8 | "try",
9 | "catch"
10 | ],
11 | "requireSpaceBeforeKeywords": [
12 | "else",
13 | "while",
14 | "catch"
15 | ],
16 | "requireSpaceAfterKeywords": [
17 | "do",
18 | "for",
19 | "if",
20 | "else",
21 | "switch",
22 | "case",
23 | "try",
24 | "catch",
25 | "void",
26 | "while",
27 | "with",
28 | "return",
29 | "typeof"
30 | ],
31 | "requireSpaceBeforeBlockStatements": true,
32 | "requireParenthesesAroundIIFE": true,
33 | "requireSpacesInConditionalExpression": true,
34 | "requireSpacesInAnonymousFunctionExpression": {
35 | "beforeOpeningCurlyBrace": true
36 | },
37 | "disallowSpacesInAnonymousFunctionExpression": {
38 | "beforeOpeningRoundBrace": true
39 | },
40 | "requireSpacesInNamedFunctionExpression": {
41 | "beforeOpeningCurlyBrace": true
42 | },
43 | "disallowSpacesInNamedFunctionExpression": {
44 | "beforeOpeningRoundBrace": true
45 | },
46 | "requireSpacesInFunctionDeclaration": {
47 | "beforeOpeningCurlyBrace": true
48 | },
49 | "disallowSpacesInFunctionDeclaration": {
50 | "beforeOpeningRoundBrace": true
51 | },
52 | "requireSpacesInFunction": {
53 | "beforeOpeningCurlyBrace": true
54 | },
55 | "disallowSpacesInFunction": {
56 | "beforeOpeningRoundBrace": true
57 | },
58 | "disallowOperatorBeforeLineBreak": ["."],
59 | "disallowMultipleLineBreaks": true,
60 | "disallowSpacesInCallExpression": true,
61 | //"requireMultipleVarDecl": true,
62 | "requireBlocksOnNewline": true,
63 | "requirePaddingNewlinesBeforeKeywords": [
64 | "if",
65 | "do",
66 | "for",
67 | "switch",
68 | "try",
69 | "void",
70 | "while",
71 | "with",
72 | "return"
73 | ],
74 | "disallowAnonymousFunctions": true,
75 | "disallowEmptyBlocks": true,
76 | "disallowSpacesInsideObjectBrackets": "all",
77 | "disallowSpacesInsideArrayBrackets": "all",
78 | //"disallowSpacesInsideParentheses": true,
79 | //"safeContextKeyword": ["me", "view", "model", "router", "component", "app"],
80 | "disallowQuotedKeysInObjects": "allButReserved",
81 | "disallowSpaceAfterObjectKeys": true,
82 | "requireSpaceBeforeObjectValues": true,
83 | "requireCommaBeforeLineBreak": true,
84 | "requireOperatorBeforeLineBreak": true,
85 | "disallowSpaceAfterPrefixUnaryOperators": true,
86 | "disallowSpaceBeforePostfixUnaryOperators": true,
87 | "requireSpaceBeforeBinaryOperators": true,
88 | "requireSpaceAfterBinaryOperators": true,
89 | "disallowMixedSpacesAndTabs": true,
90 | "disallowTrailingComma": true,
91 | "disallowKeywordsOnNewLine": ["else"],
92 | "requireCapitalizedConstructors": true,
93 | "requireDotNotation": true,
94 | "disallowYodaConditions": true,
95 | "disallowNewlineBeforeBlockStatements": true,
96 | "validateQuoteMarks": { "mark": "'", "escape": true },
97 | "validateIndentation": "\t",
98 | "validateParameterSeparator": ", ",
99 | "disallowPaddingNewlinesInBlocks": true
100 | }
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "curly": true,
3 | "camelcase": false,
4 | "newcap": true,
5 | "quotmark": "single",
6 | "browser": true,
7 | //"maxstatements": 12,
8 | //"maxparams": 3,
9 | "maxdepth": 3,
10 | "maxcomplexity": 14
11 | }
12 |
--------------------------------------------------------------------------------
/Dockerfile-mysql:
--------------------------------------------------------------------------------
1 | FROM mysql
2 |
3 | ADD Schema.sql /docker-entrypoint-initdb.d
4 |
5 | EXPOSE 3306
6 |
--------------------------------------------------------------------------------
/Dockerfile-web-app:
--------------------------------------------------------------------------------
1 | #TODO: probably shouldn't run as root user, but eh.
2 | FROM alpine:3.16.1
3 |
4 | # update alpine; use php7
5 | RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories &&\
6 | apk update && apk upgrade
7 |
8 | # Install system deps
9 | RUN apk add --update \
10 | git \
11 | npm \
12 | php7-mcrypt \
13 | php7-json \
14 | php7 \
15 | php7-pdo_mysql &&\
16 | npm install npm@latest -g && \
17 | npm install -g bower grunt-cli
18 |
19 | # make app dir
20 | RUN mkdir -p /usr/src/app
21 | WORKDIR /usr/src/app
22 |
23 | # install app deps
24 | # keep this separate so we can cache.
25 | COPY package.json /usr/src/app
26 | COPY bower.json /usr/src/app
27 | COPY .bowerrc /usr/src/app
28 | RUN npm install &&\
29 | bower install --allow-root
30 |
31 | # copy the app source over.
32 | COPY . /usr/src/app
33 |
34 | # build the app
35 | RUN grunt
36 | WORKDIR dist
37 |
38 | EXPOSE 1337
39 | CMD ["php7", "-S", "0.0.0.0:1337"]
40 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 | // load in package.json to reference any data from it (like the version number)
3 | grunt.initConfig({
4 | pkg: grunt.file.readJSON('package.json')
5 | });
6 |
7 | // Define other properties of the config object
8 | // Define the root directory for distribution
9 | var distRoot = 'dist',
10 | // Build main distribution path. NOTE: There is no slash at the end
11 | distPath = distRoot + '/inc/',
12 | // Define banner for css and js files
13 | banner = '/*!\n' +
14 | ' * <%= pkg.description %> - v<%= pkg.version %> \n' +
15 | ' * Build Date: <%= grunt.template.today("yyyy.mm.dd") %> \n' +
16 | ' * Docs: <%= pkg.homepage %> \n' +
17 | ' * Coded @ <%= pkg.author %> \n' +
18 | ' */ \n \n';
19 |
20 | // Set distribution path to grunt config object
21 | // This should be used when defining output files
22 | // Access this config option using <%= distRoot %>
23 | grunt.config.set('distRoot', distRoot);
24 | // Access this config option using <%= distPath %>
25 | grunt.config.set('distPath', distPath);
26 | // Access this config option using <%= banner %>
27 | grunt.config.set('banner', banner);
28 |
29 | // Load Grunt plugins from the config files in the grunt/ directory
30 | grunt.loadTasks('grunt');
31 |
32 | // default is just dev
33 | grunt.registerTask('dev', [
34 | 'default',
35 | 'watch'
36 | ]);
37 |
38 | // default is just dev
39 | grunt.registerTask('nomysql', [
40 | 'default',
41 | 'copy:noMySql'
42 | ]);
43 |
44 | // Register task for generating unminified output files
45 | // This is safe to run on bamboo
46 | // Note: all subtasks of the sass and autoprefixer tasks will run. This will generate a minified and unminified version of the css.
47 | // If you run this task all of the example html files and generated documentation will reference the unminified version of Edge UI's css and js
48 | grunt.registerTask('default', [
49 | 'jade',
50 | 'less:dev',
51 | 'copy:main',
52 | 'replace:dev',
53 | 'pleeease:dev',
54 | 'concat',
55 | 'jshint'
56 | ]);
57 |
58 | // Register task for generating minified (Prod ready) output files
59 | // Note: all subtasks of the sass and autoprefixer tasks will run. This will generate a minified and unminified version of the css.
60 | // If you run this task all of the example html files and generated documentation will reference the minified version of Edge UI's css and js
61 | grunt.registerTask('prod', [
62 | 'jade',
63 | 'less:prod',
64 | 'copy:main',
65 | 'replace:prod',
66 | 'pleeease:prod',
67 | 'concat'
68 | ]);
69 | };
70 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 - 2020 David Moritz
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 | # RCV - Ranked Choice Voting
2 | ### [RankedChoices.com](https://rankedchoices.com)
3 | This Ranked Choice Voting app is free to use, free to improve, and free to share with the general public!
4 |
5 | Here are a few advantages to RCV:
6 | * Better representation
7 | * No “Settling”
8 | * No wasted votes
9 | * Vote by preference
10 | * Great for multi-seat elections
11 | * Easy to use
12 |
13 | For a short, 1-minute explanation of RCV; check out this video:
14 |
15 | [](https://youtu.be/oHRPMJmzBBw)
16 |
17 | Ranked Choice voting is about representing the people in the best way possible. The key is allowing the voter to choose more than just their favorite candidate. The problem with only voting for one choice, is that if that choice does not come in first or second, it could be considered a “wasted vote.” Therefore, people are more likely to vote for their second or third choice on the idea that it has a higher chance of winning and it’s better than their last choice.
18 |
19 | With Ranked Choice Voting, there is no issue with voting for your first choice first, second choice second, and third choice third. Because if your first choice doesn’t win, then your vote automatically gets transferred to your second choice! And that is the beauty of the system.
20 |
21 | Another thing it works really well for voting for more than one position. Everyone’s vote is still counted as one vote, but if their first choice is elected, then a portion of their vote goes to second place. To better illustrate this point, there is a video that describes the use of ranked choice voting in the Animal Kingdom:
22 |
23 | Click here to watch
24 |
25 | [](https://youtu.be/l8XOZJkozfI)
26 |
27 | ## Consuming
28 | This project is designed to create the files necessary to run the site from a PHP server with MySQL database (e.g. LAMP). The database can be externally located without any issue. Make sure you specify the location of the database in the *src/api/config.php* file.
29 |
30 | The project uses Nodejs and Grunt to build a *dist/* folder whose contents will be copied into the root http folder of the server. If you don't have PHP or MySQL installed, please review .
31 |
32 | The following steps assume that you have PHP, MySQL, Nodejs and Grunt-cli installed and cwd is the project root:
33 |
34 | 1) run `npm install`
35 | 2) create "src/api/config.php" from "src/api/config_sample.php"
36 | 3) input your credentials to the MySQL database in "src/api/config.php"
37 | 4) use "Schema.sql" to build the MySQL database
38 | 5) run `grunt` (if you don't have MySQL installed, run `grunt nomysql`)
39 | 6) run `cd dist/`
40 | 7) run `php -S localhost:1337`
41 | 8) go to "localhost:1337" in your browser
42 |
43 | Let me know if you have any issues!
44 |
45 | After years of editing the dist folder on the live site, this repo became very far removed from the end product. Look at the `public_html` folder to see what is currently in production.
46 | If someone would like to convert the current site back into source code, I would welcome a pull request.
47 |
48 | Good luck!
49 |
50 | ## Docker Compose Consumption
51 |
52 | The provided docker file is meant for development / getting up and running quickly.
53 |
54 | 1. create "src/api/config.php" from "src/api/config_sample.php" (make sure the MySQL location is "db" instead of "localhost")
55 | An example that should work with the default docker-compose file as written;
56 |
57 | ```php
58 | true));
69 | } catch (PDOException $e) {
70 | die($e->getMessage());
71 | }
72 |
73 | ?>
74 | ```
75 | 2. Install docker and docker-compose; [Follow these instructions per your OS](https://docs.docker.com/compose/install/)
76 | 3. If you have docker compose installed cd to the project root and run `docker-compose up`
77 | 4. go to "localhost:1337" in your browser
78 | 5. profit
79 |
80 | ## Contributing
81 |
82 | If you are interested in joining the cause and contributing, I am very appreciative! One area that I would like focused efforts on is the ability for someone to register and manage their different ballots. There was work toward this efforts, but not finished. Please contact me before putting in significant effort to avoid duplicate work. Thanks!
83 |
--------------------------------------------------------------------------------
/Schema.sql:
--------------------------------------------------------------------------------
1 | -- phpMyAdmin SQL Dump
2 | -- version 4.0.10.18
3 | -- https://www.phpmyadmin.net
4 | --
5 | -- Host: localhost:3306
6 | -- Generation Time: Apr 25, 2017 at 04:45 PM
7 | -- Server version: 5.6.28-76.1-log
8 | -- PHP Version: 5.6.30
9 |
10 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
11 | SET time_zone = "+00:00";
12 |
13 | --
14 | -- Database: `iptheate_rcv`
15 | --
16 | CREATE DATABASE IF NOT EXISTS `rcv_db` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
17 | USE `rcv_db`;
18 |
19 | -- --------------------------------------------------------
20 |
21 | --
22 | -- Table structure for table `ballots`
23 | --
24 |
25 | DROP TABLE IF EXISTS `ballots`;
26 | CREATE TABLE IF NOT EXISTS `ballots` (
27 | `id` int(11) NOT NULL AUTO_INCREMENT,
28 | `name` varchar(64) NOT NULL,
29 | `key` varchar(16) NOT NULL,
30 | `positions` varchar(10) NOT NULL,
31 | `createdBy` varchar(64) NOT NULL,
32 | `requireSignIn` tinyint(1) NOT NULL,
33 | `tieBreak` varchar(16) NOT NULL,
34 | `maxVotes` smallint(6) NOT NULL,
35 | `voteCutoff` datetime NOT NULL,
36 | `resultsRelease` datetime NOT NULL,
37 | `timeCreated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
38 | PRIMARY KEY (`id`)
39 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
40 |
41 | -- --------------------------------------------------------
42 |
43 | --
44 | -- Table structure for table `entries`
45 | --
46 |
47 | DROP TABLE IF EXISTS `entries`;
48 | CREATE TABLE IF NOT EXISTS `entries` (
49 | `ballotId` int(11) NOT NULL,
50 | `name` varchar(128) NOT NULL,
51 | `entry_id` int(11) NOT NULL AUTO_INCREMENT,
52 | PRIMARY KEY (`entry_id`)
53 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
54 |
55 | -- --------------------------------------------------------
56 |
57 | --
58 | -- Table structure for table `users`
59 | --
60 |
61 | DROP TABLE IF EXISTS `users`;
62 | CREATE TABLE IF NOT EXISTS `users` (
63 | `id` bigint(20) NOT NULL,
64 | `name` varchar(64) NOT NULL,
65 | `email` varchar(64) NOT NULL,
66 | `image` varchar(256) NOT NULL
67 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
68 |
69 | -- --------------------------------------------------------
70 |
71 | --
72 | -- Table structure for table `votes`
73 | --
74 |
75 | DROP TABLE IF EXISTS `votes`;
76 | CREATE TABLE IF NOT EXISTS `votes` (
77 | `ballotId` int(11) NOT NULL,
78 | `vote` text NOT NULL,
79 | `ipAddress` varchar(64) NOT NULL,
80 | `vote_id` int(11) NOT NULL AUTO_INCREMENT,
81 | PRIMARY KEY (`vote_id`)
82 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
83 |
--------------------------------------------------------------------------------
/UBUNTU.md:
--------------------------------------------------------------------------------
1 | # Steps with Ubuntu
2 | **Note**: The `$` and `mysql>` portions within the commands are ***not*** part of the command, but are simply used to denote the difference between the bash prompt and the MySQL CLI.
3 |
4 | ## Install MySQL
5 | ```
6 | $ sudo apt-get install mysql-server
7 | ```
8 |
9 | ## Install PHP
10 | ```
11 | $ sudo apt-get install php
12 | ```
13 |
14 | ## Create Database
15 | The following steps are used to create a MySQL database using the MySQL CLI within Ubuntu. Run the below steps from the projects root directory where `Schema.sql` is located.
16 |
17 | ### Enter MySQL command line prompt
18 | Typing the command below will prompt you for your password after you hit enter.
19 | ```
20 | $ mysql -u root -p
21 | ```
22 |
23 | Type your password then hit enter.
24 |
25 | ### Create the database
26 | **Note** the difference in the command prompt.
27 | ```
28 | mysql> source Schema.sql
29 | ```
30 |
31 | ### Confirm database creation
32 | When executing the prior command you should have seen a series of output messages. Those were the result of creating the database, but to confirm that the database has been created, run:
33 |
34 | ```
35 | mysql> SHOW DATABASES;
36 | ```
37 |
38 | This should return to you something like this with our desired `rcv_db` created:
39 | ```
40 | +--------------------+
41 | | Database |
42 | +--------------------+
43 | | information_schema |
44 | | mysql |
45 | | performance_schema |
46 | | rcv_db |
47 | | sys |
48 | +--------------------+
49 | ````
50 |
51 | Your setup within the MySQL CLI is now complete! To exit the CLI, simply type `mysql> \q` and hit Enter.
52 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "init-angular",
3 | "version": "0.0.1",
4 | "dependencies": {
5 | "angular": "1.5.3",
6 | "angular-bootstrap": "~1.2.1",
7 | "angular-ui-sortable": "~0.13.4",
8 | "bootstrap": "3.3.6",
9 | "fontawesome": "4.5.0",
10 | "jquery": "2.2.1",
11 | "jquery-ui": "1.11.4",
12 | "lodash": "4.5.1",
13 | "moment": "2.11.2",
14 | "ng-pattern-restrict": "~0.2.1",
15 | "touch-punch": "https://github.com/furf/jquery-ui-touch-punch.git",
16 | "angular-animate": "^1.5.3"
17 | },
18 | "resolutions": {
19 | "angular": "1.5.3"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | db:
4 | build:
5 | context: .
6 | dockerfile: Dockerfile-mysql
7 | volumes:
8 | - db_data:/var/lib/mysql
9 | environment:
10 | - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-superSecretPassword}
11 | web-app:
12 | links:
13 | - db:db
14 | build:
15 | context: .
16 | dockerfile: Dockerfile-web-app
17 | volumes:
18 | - ./src:/usr/src/app/src/
19 | ports:
20 | - ${WEB_APP_PORT:-1337}:1337
21 | volumes:
22 | db_data:
23 |
--------------------------------------------------------------------------------
/grunt/concat.js:
--------------------------------------------------------------------------------
1 | /**
2 | * concatenate scripts together for dev
3 | */
4 | module.exports = function exportConcat(grunt) {
5 | grunt.config.set('concat', {
6 | js: {
7 | options: {
8 | stripBanners: true,
9 | banner: '<%= banner %>',
10 | },
11 | files: {
12 | '<%= distPath %>angular.js': [
13 | 'lib/jquery/dist/jquery.min.js',
14 | 'lib/angular/angular.min.js'
15 | ],
16 | '<%= distPath %>lib.js': [
17 | 'lib/jquery-ui/jquery-ui.min.js',
18 | 'lib/bootstrap/dist/js/bootstrap.min.js',
19 | 'lib/lodash/dist/lodash.min.js',
20 | 'lib/moment/min/moment.min.js',
21 | 'lib/touch-punch/jquery.ui.touch-punch.min.js',
22 | 'lib/angular-ui-sortable/sortable.min.js',
23 | 'lib/angular-animate/animate.min.js',
24 | 'lib/angular-bootstrap/ui-bootstrap-tpls.min.js',
25 | 'lib/ng-pattern-restrict/src/ng-pattern-restrict.min.js',
26 | 'src/external/**/*.js'
27 | ],
28 | '<%= distPath %>main.js': [
29 | 'src/services/mc.js',
30 | 'src/js/app.js',
31 | 'src/services/*.js',
32 | 'src/js/**/*.js'
33 | ]
34 | }
35 | }
36 | });
37 |
38 | grunt.loadNpmTasks('grunt-contrib-concat');
39 | };
40 |
--------------------------------------------------------------------------------
/grunt/copy.js:
--------------------------------------------------------------------------------
1 | module.exports = function exportCopy(grunt) {
2 | grunt.config('copy', {
3 | main: {
4 | files: [
5 | // copy all bootstrap fonts
6 | {
7 | expand: true,
8 | cwd: 'lib/bootstrap/fonts/',
9 | src: ['**'],
10 | dest: '<%= distRoot %>/fonts/'
11 | },
12 |
13 | // copy all fontawesome fonts
14 | {
15 | expand: true,
16 | cwd: 'lib/fontawesome/fonts/',
17 | src: ['**'],
18 | dest: '<%= distRoot %>/fonts/'
19 | },
20 |
21 | // copy all custom fonts
22 | {
23 | expand: true,
24 | cwd: 'src/fonts/',
25 | src: ['**'],
26 | dest: '<%= distRoot %>/fonts/'
27 | },
28 |
29 | // copy all api files too
30 | {
31 | expand: true,
32 | cwd: 'src/api',
33 | src: ['**'],
34 | dest: '<%= distRoot %>/api'
35 | },
36 |
37 | // copy favicon & apple-icon & htaccess
38 | {
39 | expand: true,
40 | cwd: 'src/',
41 | src: ['favicon.ico', 'apple-touch-icon.png', 'github-fork.png', '.htaccess'],
42 | dest: '<%= distRoot %>/'
43 | }
44 | ]
45 | },
46 | noMySql: {
47 | files: [
48 | // copy all no-mysql api files
49 | {
50 | expand: true,
51 | cwd: 'src/api/no-mysql',
52 | src: ['**'],
53 | dest: '<%= distRoot %>/api'
54 | }
55 | ]
56 | }
57 | });
58 |
59 | grunt.loadNpmTasks('grunt-contrib-copy');
60 | };
61 |
--------------------------------------------------------------------------------
/grunt/jade.js:
--------------------------------------------------------------------------------
1 | module.exports = function exportJade(grunt) {
2 | grunt.config('jade', {
3 | compile: {
4 | options: {
5 | pretty: true
6 | },
7 | files: {
8 | 'dist/index.html': 'src/jade/index.jade'
9 | }
10 | }
11 | });
12 |
13 | grunt.loadNpmTasks('grunt-contrib-jade');
14 | };
15 |
--------------------------------------------------------------------------------
/grunt/jscs.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 | grunt.config.set('jscs', {
3 | src: 'src/**/*.js',
4 | options: {
5 | config: '.jscsrc',
6 | fix: true
7 | }
8 | });
9 |
10 | grunt.loadNpmTasks('grunt-jscs');
11 | };
--------------------------------------------------------------------------------
/grunt/jshint.js:
--------------------------------------------------------------------------------
1 | module.exports = function exportJshint(grunt) {
2 | grunt.config('jshint', {
3 | options: {
4 | reporter: require('jshint-stylish'),
5 | jshintrc: '.jshintrc'
6 | },
7 | all: [
8 | 'src/**/*.js',
9 | 'grunt/**/*.js',
10 | '!src/external/**/*'
11 | ]
12 | });
13 |
14 | grunt.loadNpmTasks('grunt-contrib-jshint');
15 | };
16 |
--------------------------------------------------------------------------------
/grunt/less.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Make less into css file in the dist directory
3 | */
4 | module.exports = function exportLess(grunt) {
5 | grunt.config('less', {
6 | dev: {
7 | files: [{
8 | '<%= distPath %>main.css': [
9 | 'src/less/all.less'
10 | ]
11 | }]
12 | },
13 | prod: {
14 | options: {
15 | cleancss: true,
16 | compress: true
17 | },
18 | files: [{
19 | '<%= distPath %>main.css': [
20 | 'src/less/all.less'
21 | ]
22 | }]
23 | }
24 | });
25 |
26 | grunt.loadNpmTasks('grunt-contrib-less');
27 | };
28 |
--------------------------------------------------------------------------------
/grunt/pleeease.js:
--------------------------------------------------------------------------------
1 | module.exports = function exportPleeease(grunt) {
2 | grunt.config('pleeease', {
3 | dev: {
4 | options: {
5 | optimizers: {
6 | minifier: false
7 | }
8 | },
9 | files: {
10 | '<%= distPath %>main.css': '<%= distPath %>main.css'
11 | }
12 | },
13 | prod: {
14 | options: {
15 | optimizers: {
16 | minifier: true
17 | }
18 | },
19 | files: {
20 | '<%= distPath %>main.css': '<%= distPath %>main.css'
21 | }
22 | }
23 | });
24 |
25 | grunt.loadNpmTasks('grunt-pleeease');
26 | };
27 |
--------------------------------------------------------------------------------
/grunt/replace.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Replaces all references for typography.com and edge css / js
3 | *
4 | * Any file ending in '.html' with the exception of files ending in
5 | * '-documentation.html' will be searched for the patterns defined below.
6 | * This module searches for every instance of a 'match' defined below that has a
7 | * @@ in front of it. For example, the two matches defined below have
8 | * placeholders in the html pages that look like.
9 | * - @@typographyUrl
10 | * - @@majorVersion
11 | * - @@edgeuiFileName
12 | *
13 | * Using typographyUrl as an example:
14 | * Every instance of @@typographyUrl is replaced with the value in replacement
15 | * option for the typographyUrl match. The replacement option for typographUrl
16 | * is set to <%= typographyFontUrl %>. This value is defined in the Gruntfile.js
17 | * file.
18 | *
19 | * @@majorVersion and @@edgeuiFileName work the same way.
20 | *
21 | * After the placeholders are replaced with the final values the html files are
22 | * moved to the distrubtion root (this is also defined in the Gruntfile.js)
23 | *
24 | * Grunt plugin documentation: https://github.com/yoniholmes/grunt-text-replace
25 | */
26 | module.exports = function exportReplace(grunt) {
27 | grunt.config('replace', {
28 | dev: {
29 | options: {
30 | patterns: [
31 | {
32 | match: 'javascriptReplaceFiles',
33 | replacement: ''
34 | }
35 | ]
36 | },
37 | files: [
38 | {
39 | expand: true,
40 | flatten: true,
41 | src: [
42 | '<%= distRoot %>/*.html'
43 | ],
44 | dest: '<%= distRoot %>/'
45 | }
46 | ]
47 | },
48 | prod: {
49 | options: {
50 | patterns: [
51 | {
52 | match: 'javascriptReplaceFiles',
53 | replacement: ''
54 | }
55 | ]
56 | },
57 | files: [
58 | {
59 | expand: true,
60 | flatten: true,
61 | src: [
62 | '<%= distRoot %>/*.html'
63 | ],
64 | dest: '<%= distRoot %>/'
65 | }
66 | ]
67 | }
68 | });
69 |
70 | grunt.loadNpmTasks('grunt-replace');
71 | };
72 |
--------------------------------------------------------------------------------
/grunt/uglify.js:
--------------------------------------------------------------------------------
1 | /**
2 | * concat and minify scripts
3 | */
4 | module.exports = function exportUglify(grunt) {
5 | grunt.config.set('uglify', {
6 | prod: {
7 | options: {
8 | banner: '<%= banner %>',
9 | },
10 | files: {
11 | '<%= distPath %>main.js': '<%= distPath %>main.js'
12 | }
13 | }
14 | });
15 |
16 | grunt.loadNpmTasks('grunt-contrib-uglify');
17 | };
18 |
--------------------------------------------------------------------------------
/grunt/watch.js:
--------------------------------------------------------------------------------
1 | module.exports = function exportWatch(grunt) {
2 | grunt.config.set('watch', {
3 | src: {
4 | files: [
5 | 'src/**/*.*'
6 | ],
7 | tasks: [
8 | 'dev'
9 | ]
10 | }
11 | });
12 |
13 | grunt.loadNpmTasks('grunt-contrib-watch');
14 | };
15 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "new-angular",
3 | "version": "0.0.1",
4 | "description": "Initualize a new Angular app",
5 | "homepage": "http://moritzcompany.com",
6 | "author": "Moritz Company",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/davidmoritz/init-angular.git"
10 | },
11 | "bugs": {
12 | "url": "https://github.com/davidmoritz/init-angular/issues"
13 | },
14 | "scripts": {
15 | "install": "bower install"
16 | },
17 | "devDependencies": {
18 | "bower": "latest",
19 | "grunt": "latest",
20 | "grunt-cli": "latest",
21 | "grunt-contrib-concat": "latest",
22 | "grunt-contrib-copy": "latest",
23 | "grunt-contrib-jade": "latest",
24 | "grunt-contrib-jshint": "latest",
25 | "grunt-contrib-less": "latest",
26 | "grunt-contrib-uglify": "latest",
27 | "grunt-contrib-watch": "latest",
28 | "grunt-pleeease": "~0.0.5",
29 | "grunt-replace": "latest",
30 | "grunt-jscs": "latest",
31 | "jshint-stylish": "latest"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/public_html/api/add-custom-entry.php:
--------------------------------------------------------------------------------
1 | prepare($checkQ);
21 | $sth->execute();
22 | $check=$sth->fetchAll(PDO::FETCH_ASSOC);
23 |
24 | if(strlen(json_encode($check)) > 5 && !empty($entry)) {
25 | $id = $check[0]['id'];
26 |
27 | $query = "
28 | INSERT INTO
29 | entries (`ballotId`, `name`, `image`)
30 | VALUES ('$id', '$entry', '');
31 | ";
32 |
33 | $sth = $dbh->prepare($query);
34 | $sth->execute();
35 |
36 | // checking for blank values.
37 | $query2 = "
38 | SELECT
39 | *
40 | FROM
41 | `entries`
42 | ORDER BY
43 | entry_id DESC
44 | LIMIT
45 | 1;";
46 | $sth = $dbh->prepare($query2);
47 | $sth->execute();
48 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
49 | print json_encode($results);
50 | } else {
51 | echo 'None found';
52 | }
53 | } else {
54 | echo 'Please provde id and key';
55 | }
56 | ?>
57 |
--------------------------------------------------------------------------------
/public_html/api/add-entries.php:
--------------------------------------------------------------------------------
1 | prepare($query);
41 | $sth->execute();
42 | echo "Success";
43 | }
44 | ?>
45 |
--------------------------------------------------------------------------------
/public_html/api/add-user.php:
--------------------------------------------------------------------------------
1 | $val) {
13 | if(!in_array($key, $acceptableFields))
14 | continue;
15 | else if(!empty($columns)) {
16 | $columns .= ", ";
17 | $values .= ", ";
18 | $update .= ", ";
19 | }
20 | $columns .= "`$key`";
21 | $values .= "'$val'";
22 | $update .= "`$key` = VALUES(`$key`)";
23 | }
24 |
25 | if(!empty($columns)) {
26 | // checking for blank values.
27 | $query = "
28 | INSERT INTO
29 | users ($columns)
30 | VALUES
31 | ($values)
32 | ON DUPLICATE KEY UPDATE id=id
33 | ";
34 | $sth = $dbh->prepare($query);
35 | $sth->execute();
36 | } else {
37 | echo "failed to supply info";
38 | $query = "
39 | show tables;";
40 | $sth = $dbh->prepare($query);
41 | $sth->execute();
42 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
43 |
44 | print json_encode($results);
45 | }
46 | ?>
47 |
--------------------------------------------------------------------------------
/public_html/api/bbi-ballot.php:
--------------------------------------------------------------------------------
1 | prepare($query);
23 | $sth->execute();
24 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
25 |
26 | print json_encode($results);
27 | }
28 | ?>
29 |
--------------------------------------------------------------------------------
/public_html/api/bbi-names.php:
--------------------------------------------------------------------------------
1 | prepare($query);
15 | $sth->execute();
16 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
17 |
18 | print json_encode($results);
19 | ?>
20 |
--------------------------------------------------------------------------------
/public_html/api/check-user.php:
--------------------------------------------------------------------------------
1 | prepare($query);
16 | $sth->execute();
17 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
18 |
19 | print json_encode($results);
20 | }
21 | ?>
22 |
--------------------------------------------------------------------------------
/public_html/api/codes.php:
--------------------------------------------------------------------------------
1 | prepare($query);
14 | $sth->execute();
15 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
16 |
17 | print json_encode($results);
18 | ?>
19 |
--------------------------------------------------------------------------------
/public_html/api/config_sample.php:
--------------------------------------------------------------------------------
1 | true));
14 | } catch (PDOException $e) {
15 | die($e->getMessage());
16 | }
17 |
18 | ?>
19 |
--------------------------------------------------------------------------------
/public_html/api/create-graph.php:
--------------------------------------------------------------------------------
1 | prepare($query);
17 | $sth->execute();
18 | echo $query;
19 | }
20 | ?>
21 |
--------------------------------------------------------------------------------
/public_html/api/delete-ballot.php:
--------------------------------------------------------------------------------
1 | prepare($query2);
26 | $sth2->execute();
27 | echo $query2;
28 | $query = "
29 | DELETE FROM
30 | `ballots`
31 | WHERE
32 | `createdBy` = $createdBy
33 | AND
34 | `id` = $ballotId;";
35 | $sth = $dbh->prepare($query);
36 | $sth->execute();
37 | } else {
38 | echo "failed to supply ballotId";
39 | }
40 | ?>
41 |
--------------------------------------------------------------------------------
/public_html/api/delete-entries.php:
--------------------------------------------------------------------------------
1 | prepare($query);
26 | $sth->execute();
27 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
28 | } else {
29 | echo "failed to supply ballotId";
30 | }
31 | ?>
32 |
--------------------------------------------------------------------------------
/public_html/api/delete-users.php:
--------------------------------------------------------------------------------
1 | prepare($query);
32 | $sth->execute();
33 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
34 | if(count(results) = 0) {
35 |
36 | }
37 | } else {
38 | echo "failed to supply either userId, username, or password";
39 | }
40 | ?>
41 |
--------------------------------------------------------------------------------
/public_html/api/delete-vote.php:
--------------------------------------------------------------------------------
1 | prepare($query);
39 | $sth->execute();
40 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
41 | echo $query;
42 | } else {
43 | echo "failed to supply ballotId";
44 | }
45 | }
46 | ?>
47 |
--------------------------------------------------------------------------------
/public_html/api/delete-votes.php:
--------------------------------------------------------------------------------
1 | prepare($query);
34 | $sth->execute();
35 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
36 | } else {
37 | echo "failed to supply ballotId";
38 | }
39 | ?>
40 |
--------------------------------------------------------------------------------
/public_html/api/duplicate-ballot.php:
--------------------------------------------------------------------------------
1 | prepare($query);
39 | $sth->execute();
40 | echo $query;
41 | }
42 | ?>
43 |
--------------------------------------------------------------------------------
/public_html/api/get-ballots.php:
--------------------------------------------------------------------------------
1 | prepare($query);
31 | $sth->execute();
32 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
33 |
34 | if(strtolower($createdBy) == "guest") {
35 | echo "'guest' is not a valid entry";
36 | } else {
37 | print json_encode($results);
38 | }
39 | } else {
40 | echo "failed to supply Created By";
41 | }
42 | ?>
43 |
--------------------------------------------------------------------------------
/public_html/api/get-candidates.php:
--------------------------------------------------------------------------------
1 | prepare("SET time_zone = '+0:00'");
10 | $sth->execute();
11 | $query = "
12 | SELECT
13 | b.key, b.name, b.positions, b.register, b.voteCutoff, b.allowCustom, e.entry_id, e.image, e.color, e.name AS 'candidate'
14 | FROM
15 | entries e
16 | JOIN
17 | ballots b
18 | ON
19 | e.ballotId = b.id
20 | WHERE
21 | b.key = '$key'
22 | $editText
23 | ";
24 | $sth = $dbh->prepare($query);
25 | $sth->execute();
26 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
27 |
28 | if(empty($results) && !$edit)
29 | echo "Either shortcode is incorrect or voting has already been cutoff.";
30 | else
31 | print json_encode($results);
32 | } else {
33 | echo "Failed to supply Shortcode";
34 | }
35 | ?>
36 |
--------------------------------------------------------------------------------
/public_html/api/get-key-ballot.php:
--------------------------------------------------------------------------------
1 | prepare($query);
18 | $sth->execute();
19 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
20 | print json_encode($results);
21 | } else {
22 | echo "failed to supply Key";
23 | }
24 | ?>
25 |
--------------------------------------------------------------------------------
/public_html/api/get-settings.php:
--------------------------------------------------------------------------------
1 | prepare($query);
28 | $sth->execute();
29 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
30 | print json_encode($results);
31 | } else {
32 | $query = "
33 | SELECT
34 | *
35 | FROM
36 | `settings`
37 | WHERE
38 | `clearance` = 1";
39 | $sth = $dbh->prepare($query);
40 | $sth->execute();
41 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
42 | print json_encode($results);
43 | }
44 | ?>
45 |
--------------------------------------------------------------------------------
/public_html/api/get-votes.php:
--------------------------------------------------------------------------------
1 | prepare($query);
20 | $sth->execute();
21 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
22 |
23 | // $query2 = "
24 | // SELECT
25 | // name
26 | // FROM
27 | // entries
28 | // WHERE
29 | // ballotId = '$key'";
30 | // $sth2 = $dbh->prepare($query2);
31 | // $sth2->execute();
32 | // // THIS DOESN'T WORK YET
33 | // array_push($results, $sth->fetchAll(PDO::FETCH_ASSOC));
34 |
35 | if(empty($results))
36 | echo "Either shortcode is incorrect or no one has voted yet.";
37 | else
38 | print json_encode($results);
39 | } else {
40 | echo "Failed to supply key";
41 | }
42 | ?>
43 |
--------------------------------------------------------------------------------
/public_html/api/login.php:
--------------------------------------------------------------------------------
1 | prepare($query);
27 | $sth->execute();
28 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
29 |
30 | if(empty($results))
31 | echo "Incorrect username and/or password";
32 | else
33 | print json_encode($results);
34 | }
--------------------------------------------------------------------------------
/public_html/api/mysql-calls.php:
--------------------------------------------------------------------------------
1 | ballots.resultsRelease;";
17 | $sth = $dbh->prepare($query);
18 | $sth->execute();
19 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
20 | print json_encode($results);
21 | ?>
--------------------------------------------------------------------------------
/public_html/api/new-ballot.php:
--------------------------------------------------------------------------------
1 | prepare("SET time_zone = '+0:00'");
71 | $sth->execute();
72 | $query = "
73 | INSERT INTO
74 | ballots (`name`, `timeCreated`, `key`, `positions`, `createdBy`, `resultsRelease`, `voteCutoff`, `requireSignIn`, `tieBreak`, `register`, `allowCustom`, `hideNames`, `showGraph`, `maxVotes`)
75 | VALUES
76 | ('". addslashes($_POST['name']) ."', NOW(), '". addslashes($_POST['key']) ."',". $_POST['positions'] .",'". $_POST['createdBy'] ."', '". $_POST['sqlResultsRelease'] ."', '". $_POST['sqlVoteCutoff'] ."', $requireSignIn, '$tieBreak', $register, $allowCustom, $hideNames, $showGraph, $maxVotes);";
77 |
78 | $sth = $dbh->prepare($query);
79 | // echo $query;
80 | $sth->execute();
81 | echo $dbh->lastInsertId();
82 | }
83 | ?>
84 |
--------------------------------------------------------------------------------
/public_html/api/no-mysql/add-entries.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public_html/api/no-mysql/add-user.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public_html/api/no-mysql/config_sample.php:
--------------------------------------------------------------------------------
1 | true));
12 | } catch (PDOException $e) {
13 | die($e->getMessage());
14 | }
15 |
16 | ?>
17 |
--------------------------------------------------------------------------------
/public_html/api/no-mysql/delete-ballot.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public_html/api/no-mysql/delete-entries.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public_html/api/no-mysql/delete-votes.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public_html/api/no-mysql/get-ballots.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public_html/api/no-mysql/get-candidates.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public_html/api/no-mysql/get-key-ballot.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public_html/api/no-mysql/get-votes.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public_html/api/no-mysql/new-ballot.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public_html/api/no-mysql/update-ballot.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public_html/api/no-mysql/vote.php:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/public_html/api/rcvis_new.php:
--------------------------------------------------------------------------------
1 | $cfile));
14 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Token ' . APIKEY, 'Content-Type: multipart/form-data'));
15 |
16 | // grab URL and pass it to the browser
17 | echo curl_exec($ch);
18 |
19 | // echo '{"slug":"basic-ballot-creation-test-1","id":5782,"movieHorizontal":null,"movieVertical":null,"movieGenerationStatus":0,"numRounds":1,"numCandidates":1,"title":"Basic Ballot creation test","jsonFile":"https://rcvjsons-prod-us-east-1.s3.amazonaws.com/jsonFile?AWSAccessKeyId=AKIA4WE7J45B2W6TDHXU&Signature=clFhsx8LbTEBNWTGXgeIHjX78Uo%3D&Expires=1691356818","owner":"https://www.rcvis.com/api/users/656/","visualizeUrl":"https://www.rcvis.com/v/basic-ballot-creation-test-1","embedUrl":"https://www.rcvis.com/vo/basic-ballot-creation-test-1/bar","embedSankeyUrl":"https://www.rcvis.com/vo/basic-ballot-creation-test-1/sankey","embedTableUrl":"https://www.rcvis.com/vo/basic-ballot-creation-test-1/table","oembedEndpointUrl":"https://www.rcvis.com/oembed?url=https://www.rcvis.com/v/basic-ballot-creation-test-1"}1';
20 |
21 | // close cURL resource, and free up system resources
22 | curl_close($ch);
23 |
24 | ?>
--------------------------------------------------------------------------------
/public_html/api/rcvis_notes.php:
--------------------------------------------------------------------------------
1 | $_FILES["jsonFile"]);
18 |
19 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
20 | // set URL and other appropriate options
21 | curl_setopt($ch, CURLOPT_URL, "https://www.rcvis.com/api/visualizations/5778/");
22 | curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
23 | // curl_setopt($ch, CURLOPT_POSTFIELDS, array('jsonFile' => $_FILES['jsonFile']));
24 | curl_setopt($ch, CURLOPT_POSTFIELDS, array('jsonFile' => $cstringfile));
25 | // curl_setopt($ch, CURLOPT_POSTFIELDS, ['format' => 'json', 'data' => $contents]);
26 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Token ' . APIKEY, 'Content-Type: multipart/form-data'));
27 | // var_dump(trim(preg_replace('/\s+/', ' ', file_get_contents($_FILES['jsonFile']['tmp_name']))));
28 | // grab URL and pass it to the browser
29 | echo curl_exec($ch);
30 | // var_dump($_REQUEST);
31 | // close cURL resource, and free up system resources
32 | curl_close($ch);
33 |
34 | ?>
--------------------------------------------------------------------------------
/public_html/api/rcvis_patch.php:
--------------------------------------------------------------------------------
1 | $cfile));
16 | curl_setopt($ch, CURLOPT_HTTPHEADER, array('Authorization: Token ' . APIKEY, 'Content-Type: multipart/form-data'));
17 |
18 | // grab URL and pass it to the browser
19 | echo curl_exec($ch);
20 |
21 | // close cURL resource, and free up system resources
22 | curl_close($ch);
23 |
24 | ?>
--------------------------------------------------------------------------------
/public_html/api/rcvis_slug.php:
--------------------------------------------------------------------------------
1 | prepare($query);
23 | $sth->execute();
24 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
25 |
26 | print json_encode($results);
27 | }
28 | ?>
--------------------------------------------------------------------------------
/public_html/api/results.txt:
--------------------------------------------------------------------------------
1 | { "config": { "contest": "Test BBI Presidential Election RCV Straw Poll FIND PHP", "date": "2023-08-10", "jurisdiction": "BBI Straw Pole", "office": "President", "threshold": 4 }, "results": [ { "round": 1, "tally": { "D - Kennedy": "3", "R - Hurd": "1", "D - Williamson": "1", "R - DeSantis": "1", "D - Biden": "1" }, "tallyResults": [ { "eliminated": "R - Hurd", "transfers": { "D - Kennedy": "0", "D - Williamson": "1", "R - DeSantis": "0", "D - Biden": "0" } } ] }, { "round": 2, "tally": { "D - Kennedy": "3", "D - Williamson": "2", "R - DeSantis": "1", "D - Biden": "1" }, "tallyResults": [ { "eliminated": "R - DeSantis", "transfers": { "D - Kennedy": "0", "D - Williamson": "0", "D - Biden": "1" } } ] }, { "round": 3, "tally": { "D - Kennedy": "3", "D - Williamson": "2", "D - Biden": "2" }, "tallyResults": [ { "eliminated": "D - Biden", "transfers": { "D - Williamson": "1", "D - Kennedy": "0" } } ] }, { "round": 4, "tally": { "D - Williamson": "3", "D - Kennedy": "3" }, "tallyResults": [ { "eliminated": "D - Kennedy", "transfers": { "D - Williamson": "1" } } ] }, { "round": 5, "tally": { "D - Williamson": "4" }, "tallyResults": [ { "elected": "D - Williamson" } ] } ] }
--------------------------------------------------------------------------------
/public_html/api/sql.php:
--------------------------------------------------------------------------------
1 | prepare($query);
21 | $sth->execute();
22 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
23 |
24 | print json_encode($results);
25 | ?>
26 |
--------------------------------------------------------------------------------
/public_html/api/unsecure.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | query($query2);
10 | $num=$mysqli->mysqli_num_rows($result);?>
11 |
12 |
13 |
14 | Value1
15 |
16 |
17 | Value2
18 |
19 |
20 | Value3
21 |
22 |
23 | Value4
24 |
25 |
26 | Value5
27 |
28 |
29 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/public_html/api/update-ballot.php:
--------------------------------------------------------------------------------
1 | prepare($query);
95 | $sth->execute();
96 | echo $query;
97 | }
98 | ?>
99 |
--------------------------------------------------------------------------------
/public_html/api/validate-voter-code.php:
--------------------------------------------------------------------------------
1 | prepare($query);
25 | $sth->execute();
26 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
27 |
28 | if ($results)
29 | echo $code;
30 | }
31 | ?>
32 |
--------------------------------------------------------------------------------
/public_html/api/vote.php:
--------------------------------------------------------------------------------
1 | prepare($query);
44 | $sth->execute();
45 | }
46 | ?>
47 |
--------------------------------------------------------------------------------
/public_html/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/apple-touch-icon.png
--------------------------------------------------------------------------------
/public_html/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/favicon.ico
--------------------------------------------------------------------------------
/public_html/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/public_html/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/public_html/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/public_html/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/public_html/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/public_html/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/public_html/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/public_html/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/public_html/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/public_html/github-fork.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/github-fork.png
--------------------------------------------------------------------------------
/public_html/img/every-vote-counts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/public_html/img/every-vote-counts.png
--------------------------------------------------------------------------------
/public_html/inc/ranked-choices.css:
--------------------------------------------------------------------------------
1 | #sortable .candidate-item {
2 | background: #eee;
3 | }
4 | @media screen and (max-width: 992px) {
5 | #sortable {max-width: 92%;}
6 | }
7 | .fork-me-on-github {
8 | position: absolute;
9 | top: 0;
10 | right: 0;
11 | border: 0;
12 | z-index: 9;
13 | }
14 | .choice-image {
15 | margin-right: 5px;
16 | max-height: 150px;
17 | max-width: 100px;
18 | float: left;
19 | }
20 | #sortable li:after {
21 | clear: both;
22 | display: block;
23 | content: '';
24 | }
25 |
--------------------------------------------------------------------------------
/public_html/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Ranked Choice Free Voting App
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
31 |
32 |
33 |
34 |
35 |
36 |
Ranked Choice Voting App
37 | A better way of voting
38 |
39 |
40 |
52 |
53 |
54 |
55 |
56 |
Register
57 |
58 |
94 |
95 |
Facebook
96 |
138 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 | Login
153 |
154 |
155 |
156 | Username
157 |
158 |
159 |
160 | Password
161 |
162 |
163 |
164 |
165 | Remember me (30 days)
166 |
167 |
168 |
169 | Login
170 |
171 |
172 |
173 |
174 |
175 | Create New Account
176 |
177 |
178 |
179 | Username
180 |
181 |
182 |
183 | Password
184 | (no password recovery at this time, so don't forget)
185 |
186 |
187 |
188 |
189 | Create
190 |
191 |
192 |
193 |
194 |
195 | Thank you for signing in as {{user.name}}.
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
RCV
204 |
Ranked Choice Vote
205 |
Ranked Choice Vote
206 |
Always-free RCV! A better way to vote. Whether voting for one position or multiple, ranked choice voting allows for the greatest representation of the voters. Click "About" to learn more!
Create a Ballot
207 |
208 |
209 |
210 |
211 |
212 |
If you've found value from this free app, please consider a small donation. Thanks!
🧸 Buy me a bear
213 |
214 |
215 |
216 |
217 |
218 |
219 |
About
220 |
221 | It is my belief that RCV offers several benefits:
222 |
223 |
224 | Better representation
225 | No “Settling”
226 | No wasted votes
227 | Vote by preference
228 | Great for multi-seat elections
229 | Easy to use
230 |
231 |
Proponents of Ranked Choice voting believe this system is about representing the people in the best way possible. The key is allowing the voter to choose more than just their favorite candidate. The problem with only voting for one choice, is that if that choice does not come in first or second, it could be considered a “wasted vote.” Therefore, people are more likely to vote for their second or third choice on the idea that it has a higher chance of winning and it’s better than their last choice.
232 |
With Ranked Choice Voting, there is no issue with voting for your first choice first, second choice second, and third choice third. Because if your first choice doesn’t win, then your vote automatically gets transferred to your second choice! And that is the beauty of the system.
233 |
Another thing RCV works really well for is implementing a fair method in voting for more multiple positions at once. Everyone’s vote is still counted as one vote, but after their first choice is elected, the unused portion of their vote (equal to the amount not absolutely necessary to get their 1st choice elected) goes to their second choice candidate. To better illustrate this point, there is a video that describes the use of ranked choice voting in the Animal Kingdom:
Click here to watch
234 |
235 | Feedback
236 |
237 |
238 | I welcome questions, concerns, suggestions, and accolades! Please send an email to davidmoritz@gmail.com
239 |
240 |
241 | Secure Elections
242 |
243 |
244 | If you would like to use this site for a secure election, please review: Secure Election Instructions
245 |
246 |
247 | Support
248 |
249 |
250 | I built this free app to help raise awareness of ranked choice voting. If you would like to show your appreciation, please send contributions to paypal.me/rankedchoices . Thank you so much!
251 |
252 |
253 | Terms of Service
254 |
255 |
256 | Please view our terms of service to understand proper use of this site: Terms of Service
257 |
258 |
259 |
260 |
261 |
262 |
Edit Ballot: {{ballot.name}}
263 |
264 |
265 | Options
266 |
267 |
268 | Entries
269 |
270 |
271 |
407 |
408 |
409 |
417 |
Current Entry List
418 |
{{$index + 1}} {{entry}} {{images[$index]}}
419 |
420 |
421 |
422 |
Thank you for creating a ballot!
423 |
Send this url to people for voting:
424 |
{{origin}}/{{ballot.key}} Click here to vote yourself
425 |
426 |
427 |
428 |
429 |
430 |
431 |
Please register before voting!
432 |
480 |
481 |
482 |
Ballot: {{ballot.name}}
483 |
484 |
Order by Preference (remove any undesired using )
485 |
(Drag & Drop)
486 |
{{ballot.positions}} positions available
487 |
498 |
499 |
500 |
501 |
Ballot: {{ballot.name}}
502 |
Thank you for voting!
503 |
RESET
504 |
505 |
506 |
507 |
508 |
509 |
521 |
522 |
Ballot: {{ballot.name}}
523 |
524 |
Your Name* :
525 |
526 |
528 |
529 |
530 |
531 |
New Entry?:
532 |
533 |
534 |
535 |
536 | Add
537 |
538 |
539 |
540 |
541 |
Enter Your Name to View the Candidates.
542 |
543 |
Order by Preference (remove any undesired using )
544 |
(Drag & Drop)
545 |
{{ballot.positions}} positions available
546 |
557 |
558 |
Voting URL: {{origin}}/{{ballot.key}}
559 |
560 |
561 |
Ballot: {{ballot.name}}
562 |
Thank you for voting!
563 |
566 |
567 | Voting URL: {{origin}}/{{ballot.key}}
568 |
569 |
570 |
571 |
572 |
573 |
574 |
Results
575 |
586 |
587 |
588 | The winner (so far) : {{elected[0]}}
589 |
590 |
591 |
The winners, in order of most votes (so far) :
592 |
593 | {{result}}
594 |
595 |
596 |
597 |
598 |
599 | A graph of the results will be made available after voting has been cut off.
600 |
601 |
602 |
603 |
604 |
605 | {{!showText ? "Show" : "Hide"}} details
606 |
607 |
608 | Voting URL: {{origin}}/{{shortcode}}
609 |
610 |
617 |
{{errors.shortcode}}
618 |
619 |
620 |
621 |
622 |
623 |
Profile
624 |
Name: {{user.name}}
625 |
626 |
Current Ballots
627 |
628 |
629 |
630 | Shortcode
631 | Name
632 | Vote Cutoff ({{moment.tz(moment.tz.guess()).format('z')}})
633 | Total votes
634 | Admin Tools
635 |
636 |
637 |
638 |
639 | {{ballot.key}}
640 | {{ballot.name}}
641 | {{ballot.voteCutoff.year() > 2100 ? 'never' : ballot.voteCutoff.tz(moment.tz.guess()).format('MMM Do YYYY, h:mm a')}}{{ballot.voteCutoff}}
642 | {{ballot.totalVotes}}
643 |
644 |
645 |
646 |
647 |
648 |
649 |
650 |
651 |
652 |
653 |
654 |
655 |
656 |
657 |
Detailed Results
658 |
659 |
660 |
661 |
665 |
666 |
667 |
668 |
669 |
675 |
676 |
677 | URL must point to an image (e.g. ending with png or jpg)
678 |
679 |
680 |
681 |
685 |
686 |
687 |
688 |
689 |
690 |
691 |
697 |
698 |
699 | New Ballot Name
700 | {{errors.name}}
701 |
702 |
713 |
714 |
718 |
719 |
720 |
721 |
722 |
723 |
724 |
--------------------------------------------------------------------------------
/public_html/results_json.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Ranked Choice Vote Calculator App
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/public_html/secure-elections-instructions.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 | Secure Elections
12 | Thanks for stopping by! These instructions are for those who are looking for a way to make a ranked choice ballot fast, easy, inexpensive, and secure while using RankedChoices.com!
13 |
14 | I recommend registering an account first, then the organizer can easily remove any fraudulent votes.
15 |
16 | Open Election:
17 |
18 | Everyone's vote is public. The only concern here is if a different person voted under someone else's name, it would be hard to identify who the real vote is vs the fake.
19 |
20 |
21 | Make a list of randomly generated numbers.
22 | Assign a number to each person not showing others.
23 | Choose "Voter Names Required" from the advanced options.
24 | Instruct each voter to enter that number next to their name.
25 | Use advanced options to release results after voting is cut-off.
26 |
27 |
28 | By taking this approach, the organizer can verify that the name of the voter and their assigned number match if there are any disputes.
29 |
30 | Blind Election:
31 |
32 | Everyone's vote is hidden from other voters, but not the organizer. The concern here is not allowing someone to anonymously vote twice.
33 |
34 |
35 | Option 1:
36 |
37 |
38 | Choose "Voter Names Required" from the advanced options.
39 | Choose "Hide voter names when displaying results (except from owner)" from the advanced options.
40 |
41 |
42 | Option 2:
43 |
44 |
45 | Make a list of randomly generated numbers.
46 | Assign a number to each person not showing others.
47 | Choose "Voter Names Required" from the advanced options.
48 | Use advanced options to release results after voting is cut-off.
49 | Instruct each voter to enter that number INSTEAD OF their name.
50 |
51 |
52 | By taking this approach, the organizer can verify that every vote is legitmate because if an entered number does not match the list of generated numebrs, the vote can be thrown out.
53 |
54 | Double-Blind Election:
55 |
56 | Everyone's vote is hidden from other voters, including the organizer. The concern here is not allowing someone to anonymously vote twice nor allowing any way to trace a vote back to the voter.
57 |
58 |
59 | Make a list of randomly generated numbers.
60 | Copy the list of randomly generated numbers and separate them (e.g. cut them out and place them into a bowl).
61 | Allow each voter to secretly receive their number without others knowing (e.g. discretely drawing from a bowl).
62 | The voter keeps this number a secret during the entire election and beyond.
63 | Choose "Voter Names Required" from the advanced options.
64 | Use advanced options to release results after voting is cut-off.
65 | Instruct each voter to enter that number INSTEAD OF their name.
66 |
67 |
68 | By taking this approach, the organizer can verify that every vote is legitmate because if an entered number does not match the list of generated numbers, the vote can be thrown out.
69 |
70 | Also, the organizer has no way of tracing a number back to a voter without that voter's concent.
71 |
72 |
73 |
--------------------------------------------------------------------------------
/public_html/sw.js:
--------------------------------------------------------------------------------
1 | importScripts('/cache-polyfill.js');
2 |
3 | self.addEventListener('install', function(e) {
4 | e.waitUntil(
5 | caches.open('airhorner').then(function(cache) {
6 | return cache.addAll([
7 | 'index.html',
8 | 'wrap.css',
9 | 'wrap.js',
10 | 'favicon.ico',
11 | 's1.png',
12 | 's2.png',
13 | 's3.png',
14 | 'lib/',
15 | 'lib/bootstrap.min.js',
16 | 'lib/bootstrap.min.css',
17 | 'lib/jquery.slim.min.js',
18 | 'lib/tether.min.js'
19 | ]);
20 | })
21 | );
22 | });
23 |
24 | self.addEventListener('fetch', function(event) {
25 |
26 | console.log(event.request.url);
27 |
28 | event.respondWith(
29 |
30 | caches.match(event.request).then(function(response) {
31 |
32 | return response || fetch(event.request);
33 |
34 | })
35 |
36 | );
37 |
38 | });
--------------------------------------------------------------------------------
/public_html/terms-of-service.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 | TERMS OF USE
12 | Thanks for stopping by! These Terms of Use set out the rules for your use of this app / access to this website. Before using rankedchoices.com, you're required to read and agreed to these Terms of Use. Remember that just by using rankedchoices.com, you're automatically agreeing to all of these terms and conditions.
13 | Basics:
14 |
15 | These terms and conditions of use ("Terms") govern your access to and use of rankedchoices.com, including any content, functionality, and services offered on or through rankedchoices.com or its subdomains (referred to collectively as the "Website"). By accessing, viewing, or using the Website or any of its tools, you are agreeing to be bound by these Terms.
16 | If you don't agree with any term or condition outlined in these Terms, then you may not access the Website.
17 | We may change the Terms at any time in our sole discretion. All changes are effective immediately when posted, and apply to all access to and use of the Website thereafter. Your continued use of the Website following the posting of revised Terms means that you accept and agree to the changes. You're expected to check this page when you access the Website so you're aware of any changes, because they are binding on you.
18 | These Terms constitute the entire agreement between you and RankedChoices.com and replace all previous agreements under this title.
19 | The Website is offered and available to users who are at least 13 years old and in the United States.
20 |
21 | Website:
22 |
23 | Your use of the Website is at your sole risk. The Website is provided on an "as is" and "as available" basis.
24 | We may add to, stop, or make changes to this Website, and any service or material we provide on the Website, in our sole discretion without notice. Any new, removed, or changed features are subject to the Terms. Continued use of the Website after any such changes means you have agreed to those changes.
25 | We will not be liable if for any reason all or any part of the Website is unavailable at any time or for any period. Periodically, we may restrict access to some parts of the Website, or the entire Website, to users.
26 | You're responsible for making any arrangements necessary for you to have access to the Website, and for ensuring that all persons who access the Website through your internet connection are aware of these Terms and comply with them.
27 | To access the Website or some of the resources it offers, you may be asked to give certain registration data or other information. It is a condition of your use of the Website that the information you provide on the Website is correct, current, and complete.
28 | If you choose, or are given, a username, password, or any other piece of information as part of our security procedures, you must treat such information as confidential, and not disclose it. You also acknowledge that your account is personal to you and agree not to provide any other person with access to this Website or portions of it using your username, password, or other security information. You agree to notify us immediately of any unauthorized access to or use of your username or password or any other breach of security. You also agree to ensure that you exit from your account at the end of each session. You should use particular caution when accessing your account from a public or shared computer so that others are not able to view or record your password or other personal information. We have the right to disable any username, password, or other identifier, whether chosen by you or provided by us, at any time in our sole discretion for any or no reason, including if, in our opinion, you have violated any provision of these Terms of Use.
29 |
30 | Prohibited Uses:
31 | You may use the Website only for lawful purposes and in accordance with these Terms. You agree not to use the Website:
32 |
33 | In any way that violates any applicable law or regulation (including, without limitation, any laws regarding the export of data or software to and from the US or other countries).
34 | For the purpose of exploiting, harming, or attempting to exploit or harm minors in any way by exposing them to inappropriate content, asking for personally identifiable information, or otherwise.
35 | To transmit, or procure the sending of, any advertising or promotional material, including any "junk mail," "chain letter," "spam," or any other similar solicitation.
36 | To impersonate or attempt to impersonate someone else, or otherwise use the Website to engage in any act of deception.
37 | In any manner that could disable, overburden, damage, or impair the site or interfere with any other party's use of the Website, including their ability to engage in real time activities through the Website.
38 |
39 | You also agree not to use any manual or automatic device or process to monitor or copy any of the material on the Website or for any other purpose not expressly authorized in the Terms, or attempt to gain unauthorized access to or to disruption of any part of the Website, or to otherwise interfere or attempt to interfere with the proper working of the Website.
40 | We have the right (but no obligation) to screen, review, filter, change, reject, or remove any content or accounts from the Website at our sole discretion.
41 |
User Content:
42 |
43 | The Website may contain certain interactive features (collectively, "Interactive Services") that allow users to post, publish, or share with others (hereinafter, "post") content or materials (collectively, "User Content") on or through the Website.
44 | Any User Content you post to the site will be considered non-confidential and non-proprietary. By providing any User Content on the Website, you grant us and our licensees, successors, and assigns the right to use, reproduce, modify, perform, display, distribute, and otherwise disclose to third parties any such material.
45 | You represent, warrant, and agree that you own or control all rights in and to the User Content and have the right to grant the license granted above to us.
46 | You represent, warrant, and agree that all of your User Content does and will comply with these Terms.
47 | You understand and agree that you are responsible for any User Content you submit or contribute, and you (not us), have full responsibility for such content, including its legality, reliability, accuracy, and appropriateness.
48 | We are not responsible or liable to any third party for the content or accuracy of any User Content posted by you or any other user of the Website.
49 |
50 | Reliance on Information Posted:
51 |
52 | The information presented on or through the Website is made available solely for general information purposes. We do not warrant the accuracy, completeness, or usefulness of this information. Any reliance you place on such information is strictly at your own risk. We disclaim all liability and responsibility arising from any reliance placed on such materials by you or any other visitor to the Website, or by anyone who may be informed of any of its contents.
53 | This Website may include content provided by third parties, including materials provided by other users. We are not responsible, or liable to you or any third party, for the content or accuracy of any materials provided by any third parties.
54 |
55 | Geographic Restrictions:
56 |
57 | The owner of the Website is based in the State of Iowa in the United States. We provide this Website for use only by persons located in the United States. We make no claims that the Website or any of its content is accessible or appropriate outside of the United States. Access to the Website may not be legal by certain persons or in certain countries. If you access the Website from outside the United States, you do so on your own risk and are solely responsible for compliance with local laws.
58 |
59 | Disclaimer of Warranties:
60 |
61 | You understand that we cannot and do not guarantee or warrant that the Website will be free of viruses or other destructive code. You are responsible for implementing sufficient procedures and checkpoints to satisfy your particular requirements for anti-virus protection and accuracy of data input and output, and for maintaining a means external to our site for any reconstruction of any lost data.
62 | We do not warrant that the Website will meet your requirements or expectations, or that the Website will be delivered timely, securely, uninterrupted, or error-free. We do not warrant that any results that may be obtained through the Website will be accurate or reliable. The Website should not be used or relied upon for critical responses or voting results.
63 | TO THE FULLEST EXTENT PROVIDED BY LAW, WE WILL NOT BE LIABLE FOR ANY LOSS OR DAMAGE CAUSED BY A DISTRIBUTED DENIAL-OF-SERVICE ATTACK, VIRUSES, OR OTHER TECHNOLOGICALLY HARMFUL MATERIAL THAT MAY INFECT YOUR COMPUTER EQUIPMENT, COMPUTER PROGRAMS, DATA, OR OTHER PROPRIETARY MATERIAL DUE TO YOUR USE OF THE WEBSITE OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE WEBSITE OR TO YOUR DOWNLOADING OF ANY MATERIAL POSTED ON IT, OR ON ANY WEBSITE LINKED TO IT.
64 | YOUR USE OF THE WEBSITE, ITS CONTENT, AND ANY SERVICES OR ITEMS OBTAINED THROUGH THE WEBSITE IS AT YOUR OWN RISK. THE WEBSITE, ITS CONTENT, AND ANY SERVICES OR ITEMS OBTAINED THROUGH THE WEBSITE ARE PROVIDED ON AN "AS IS" AND "AS AVAILABLE" BASIS, WITHOUT ANY WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED.
65 | RANKEDCHOICES.COM MAKES ANY WARRANTY OR REPRESENTATION WITH RESPECT TO THE COMPLETENESS, SECURITY, RELIABILITY, QUALITY, ACCURACY, OR AVAILABILITY OF THE WEBSITE.
66 | WITHOUT LIMITING THE FOREGOING, NEITHER THE COMPANY NOR ANYONE ASSOCIATED WITH THE COMPANY REPRESENTS OR WARRANTS THAT THE WEBSITE, ITS CONTENT, OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE WEBSITE WILL BE ACCURATE, RELIABLE, ERROR-FREE, OR UNINTERRUPTED, THAT DEFECTS WILL BE CORRECTED, THAT OUR SITE OR THE SERVER THAT MAKES IT AVAILABLE ARE FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS, OR THAT THE WEBSITE OR ANY SERVICES OR ITEMS OBTAINED THROUGH THE WEBSITE WILL OTHERWISE MEET YOUR NEEDS OR EXPECTATIONS.
67 | TO THE FULLEST EXTENT PROVIDED BY LAW, THE COMPANY HEREBY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, STATUTORY, OR OTHERWISE, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND FITNESS FOR PARTICULAR PURPOSE.
68 | THE FOREGOING DOES NOT AFFECT ANY WARRANTIES THAT CANNOT BE EXCLUDED OR LIMITED UNDER APPLICABLE LAW.
69 |
70 | Limitation on Liability:
71 |
72 | TO THE FULLEST EXTENT ALLOWED BY LAW, IN NO EVENT WILL THE COMPANY, ITS AFFILIATES, OR THEIR LICENSORS, SERVICE PROVIDERS, EMPLOYEES, AGENTS, OFFICERS, OR DIRECTORS BE LIABLE FOR DAMAGES OF ANY KIND, UNDER ANY LEGAL THEORY, ARISING OUT OF OR IN CONNECTION WITH YOUR USE, OR INABILITY TO USE, THE WEBSITE, ANY WEBSITES LINKED TO IT, ANY CONTENT ON THE WEBSITE OR SUCH OTHER WEBSITES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES, INCLUDING BUT NOT LIMITED TO, PERSONAL INJURY, PAIN AND SUFFERING, EMOTIONAL DISTRESS, LOSS OF REVENUE, LOSS OF PROFITS, LOSS OF BUSINESS OR ANTICIPATED SAVINGS, LOSS OF USE, LOSS OF GOODWILL, LOSS OF DATA, AND WHETHER CAUSED BY TORT (INCLUDING NEGLIGENCE), BREACH OF CONTRACT, OR OTHERWISE, EVEN IF FORESEEABLE.
73 |
74 | Indemnification:
75 |
76 | You agree to defend, indemnify, and hold harmless RankedChoices.com from and against any claims, liabilities, damages, judgments, awards, losses, costs, expenses, or fees (including reasonable attorneys' fees) arising out of or relating to your violation of these Terms of Use or your use of the Website, including, but not limited to, your User Content, any use of the Website's content or services other than as expressly authorized in these Terms , or your use of any information obtained from the Website.
77 |
78 | Governing Law and Jurisdiction:
79 |
80 | All matters relating to the Website and these Terms, and any dispute or claim arising therefrom or related thereto (in each case, including non-contractual disputes or claims), shall be governed by and construed in accordance with the internal laws of the United States and/or the State of Iowa without giving effect to any choice or conflict of law provision or rule.
81 | Any legal suit, action, or proceeding arising out of, or related to, these Terms or the Website shall be instituted exclusively in the federal courts of the United States or the courts of the State of Iowa, in each case located in the City of Des Moines and County of Polk. You waive any and all objections to the exercise of jurisdiction over you by such courts and to venue in such courts.
82 |
83 |
84 |
--------------------------------------------------------------------------------
/src/.htaccess:
--------------------------------------------------------------------------------
1 | RewriteEngine on
2 |
3 | RewriteCond %{REQUEST_FILENAME} -f
4 | RewriteRule ^.*$ - [L]
5 |
6 | RewriteRule ^/?([^/]+)/?$ #$1 [L,QSA]
7 |
8 |
9 | order allow,deny
10 | allow from all
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/api/add-entries.php:
--------------------------------------------------------------------------------
1 | prepare($query);
38 | $sth->execute();
39 | echo "Success";
40 | }
41 | ?>
--------------------------------------------------------------------------------
/src/api/add-user.php:
--------------------------------------------------------------------------------
1 | $val) {
13 | if(!in_array($key, $acceptableFields))
14 | continue;
15 | else if(!empty($columns)) {
16 | $columns .= ", ";
17 | $values .= ", ";
18 | $update .= ", ";
19 | }
20 | $columns .= "`$key`";
21 | $values .= "'$val'";
22 | $update .= "`$key` = VALUES(`$key`)";
23 | }
24 |
25 | if(!empty($columns)) {
26 | // checking for blank values.
27 | $query = "
28 | INSERT INTO
29 | users ($columns)
30 | VALUES
31 | ($values)
32 | ON DUPLICATE KEY UPDATE
33 | $update";
34 | $sth = $dbh->prepare($query);
35 | $sth->execute();
36 | } else {
37 | echo "failed to supply info";
38 | }
39 | ?>
--------------------------------------------------------------------------------
/src/api/config_sample.php:
--------------------------------------------------------------------------------
1 | true));
12 | } catch (PDOException $e) {
13 | die($e->getMessage());
14 | }
15 |
16 | ?>
17 |
--------------------------------------------------------------------------------
/src/api/delete-ballot.php:
--------------------------------------------------------------------------------
1 | prepare($query);
19 | $sth->execute();
20 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
21 | } else {
22 | echo "failed to supply ballotId";
23 | }
24 | ?>
--------------------------------------------------------------------------------
/src/api/delete-entries.php:
--------------------------------------------------------------------------------
1 | prepare($query);
25 | $sth->execute();
26 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
27 | } else {
28 | echo "failed to supply ballotId";
29 | }
30 | ?>
--------------------------------------------------------------------------------
/src/api/delete-votes.php:
--------------------------------------------------------------------------------
1 | prepare($query);
26 | $sth->execute();
27 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
28 | } else {
29 | echo "failed to supply ballotId";
30 | }
31 | ?>
--------------------------------------------------------------------------------
/src/api/get-ballots.php:
--------------------------------------------------------------------------------
1 | prepare($query);
25 | $sth->execute();
26 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
27 |
28 | if(strtolower($createdBy) == "guest") {
29 | echo "'guest' is not a valid entry";
30 | } else {
31 | print json_encode($results);
32 | }
33 | } else {
34 | echo "failed to supply Created By";
35 | }
36 | ?>
--------------------------------------------------------------------------------
/src/api/get-candidates.php:
--------------------------------------------------------------------------------
1 | prepare($query);
21 | $sth->execute();
22 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
23 |
24 | if(empty($results))
25 | echo "Either shortcode is incorrect or voting has already been cutoff";
26 | else
27 | print json_encode($results);
28 | } else {
29 | echo "Failed to supply Shortcode";
30 | }
31 | ?>
--------------------------------------------------------------------------------
/src/api/get-key-ballot.php:
--------------------------------------------------------------------------------
1 | prepare($query);
18 | $sth->execute();
19 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
20 | print json_encode($results);
21 | } else {
22 | echo "failed to supply Key";
23 | }
24 | ?>
--------------------------------------------------------------------------------
/src/api/get-votes.php:
--------------------------------------------------------------------------------
1 | ballots.resultsRelease;";
21 | $sth = $dbh->prepare($query);
22 | $sth->execute();
23 | $results=$sth->fetchAll(PDO::FETCH_ASSOC);
24 |
25 | // $query2 = "
26 | // SELECT
27 | // name
28 | // FROM
29 | // entries
30 | // WHERE
31 | // ballotId = '$key'";
32 | // $sth2 = $dbh->prepare($query2);
33 | // $sth2->execute();
34 | // // THIS DOESN'T WORK YET
35 | // array_push($results, $sth->fetchAll(PDO::FETCH_ASSOC));
36 |
37 | if(empty($results))
38 | echo "Either shortcode is incorrect or results aren't ready to be released";
39 | else
40 | print json_encode($results);
41 | } else {
42 | echo "Failed to supply key";
43 | }
44 | ?>
--------------------------------------------------------------------------------
/src/api/new-ballot.php:
--------------------------------------------------------------------------------
1 | prepare($query);
57 | $sth->execute();
58 | echo $dbh->lastInsertId();
59 | }
60 | ?>
--------------------------------------------------------------------------------
/src/api/no-mysql/add-entries.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/no-mysql/add-user.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/no-mysql/config_sample.php:
--------------------------------------------------------------------------------
1 | true));
12 | } catch (PDOException $e) {
13 | die($e->getMessage());
14 | }
15 |
16 | ?>
17 |
--------------------------------------------------------------------------------
/src/api/no-mysql/delete-ballot.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/no-mysql/delete-entries.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/no-mysql/delete-votes.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/no-mysql/get-ballots.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/no-mysql/get-candidates.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/no-mysql/get-key-ballot.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/no-mysql/get-votes.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/no-mysql/new-ballot.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/no-mysql/update-ballot.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/no-mysql/vote.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/api/update-ballot.php:
--------------------------------------------------------------------------------
1 | prepare($query);
60 | $sth->execute();
61 | echo $query;
62 | }
63 | ?>
--------------------------------------------------------------------------------
/src/api/vote.php:
--------------------------------------------------------------------------------
1 | prepare($query);
36 | $sth->execute();
37 | }
38 | ?>
--------------------------------------------------------------------------------
/src/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/src/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/src/favicon.ico
--------------------------------------------------------------------------------
/src/fonts/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/src/fonts/.gitkeep
--------------------------------------------------------------------------------
/src/github-fork.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DavidMoritz/rcv/4326537486d92d0371da6aaa00ee2438647266eb/src/github-fork.png
--------------------------------------------------------------------------------
/src/jade/index.jade:
--------------------------------------------------------------------------------
1 | include partials/mixins
2 |
3 | doctype html
4 | html(ng-app='mainApp')
5 | include partials/head
6 | body(ng-cloak ng-controller='MainCtrl')
7 | a(href="https://github.com/davidmoritz/rcv")
8 | img(style="position: absolute; top: 0; right: 0; border: 0; z-index: 100;" src="github-fork.png" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_green_007200.png")
9 | .container
10 | h2 Ranked Choice Vote Calculator
11 | h5 A better way of voting
12 | include partials/navbar
13 | .container
14 | .panel.panel-default(ng-show="activeLink == 'register'")
15 | .panel-body
16 | include pages/register.html
17 | .panel.panel-default(ng-show="activeLink == 'home'")
18 | .panel-body
19 | include pages/home
20 | .panel.panel-default(ng-show="activeLink == 'about'")
21 | .panel-body
22 | include pages/about
23 | .panel.panel-default(ng-show="activeLink == 'create'")
24 | .panel-body
25 | include pages/create
26 | .panel.panel-default(ng-show="activeLink == 'vote'")
27 | .panel-body
28 | include pages/vote
29 | .panel.panel-default(ng-show="activeLink == 'results'")
30 | .panel-body
31 | include pages/results
32 | .panel.panel-default(ng-show="activeLink == 'profile'")
33 | .panel-body
34 | include pages/profile
35 | .panel.panel-default(ng-show="activeLink == 'results' && showText")
36 | .panel-heading
37 | h3.panel-title Detailed Results
38 | .panel-body(ng-bind-html="bodyText")
39 | include partials/footer
40 |
--------------------------------------------------------------------------------
/src/jade/pages/about.jade:
--------------------------------------------------------------------------------
1 | h2 About
2 | ul
3 | li Better representation
4 | li No “Settling”
5 | li No wasted votes
6 | li Vote by preference
7 | li Great for multi-seat elections
8 | li Easy to use
9 | p.
10 | Ranked Choice voting is about representing the people in the best way possible. The key is allowing the voter to choose more than just their favorite candidate. The problem with only voting for one choice, is that if that choice does not come in first or second, it could be considered a “wasted vote.” Therefore, people are more likely to vote for their second or third choice on the idea that it has a higher chance of winning and it’s better than their last choice.
11 | p.
12 | With Ranked Choice Voting, there is no issue with voting for your first choice first, second choice second, and third choice third. Because if your first choice doesn’t win, then your vote automatically gets transferred to your second choice! And that is the beauty of the system.
13 | p.
14 | Another thing it works really well for voting for more than one position. Everyone’s vote is still counted as one vote, but if their first choice is elected, then a portion of their vote goes to second place. To better illustrate this point, there is a video that describes the use of ranked choice voting in the Animal Kingdom:
15 | a(href="https://youtu.be/l8XOZJkozfI" target="_blank") Click here to watch
16 | //- .fluid-video-wrapper
17 | //- iframe(src="https://www.youtube.com/embed/l8XOZJkozfI" frameborder="0" allowfullscreen)
--------------------------------------------------------------------------------
/src/jade/pages/create.jade:
--------------------------------------------------------------------------------
1 | h1 Ballot: {{ballot.name}}
2 | include ../partials/form
3 | div(ng-show="entries && !congrats")
4 | include ../partials/new-entries
5 | div(ng-show="congrats")
6 | include ../partials/congrats
--------------------------------------------------------------------------------
/src/jade/pages/home.jade:
--------------------------------------------------------------------------------
1 | .row
2 | .col-sm-6.text-center
3 | h1 RCV
4 | h2.hidden-xs.hidden-sm Ranked Choice Vote
5 | h3.hidden-md.hidden-lg Ranked Choice Vote
6 | p A better way to vote. Whether voting for one position or multiple, ranked choice voting allows for the greatest representation of the voters. Click "About" to learn more!
7 | a.btn.btn-primary(href="create") Create a Ballot
8 | .col-sm-6
9 | a(href="create")
10 | img.img.img-responsive.vote-button(src="http://lwv.org/files/every_vote_counts_button_0.jpg")
--------------------------------------------------------------------------------
/src/jade/pages/profile.jade:
--------------------------------------------------------------------------------
1 | h1 Profile
2 | //-img(ng-if='user.image' src='user.image')
3 | h3(ng-show='user.name') Name: {{user.name}}
4 | h3(ng-show='user.email') Email: {{user.email}}
5 | div(ng-show='allBallots.length')
6 | h3 Current Ballots
7 | table.table.ballots
8 | thead
9 | tr
10 | th Shortcode
11 | th Name
12 | th Vote Cutoff
13 | th Total votes
14 | th Admin Tools
15 | tbody
16 | tr(ng-repeat="ballot in allBallots track by ballot.id")
17 | td {{ballot.key}}
18 | td {{ballot.name}}
19 | td {{ballot.voteCutoff | date:"MMM d, y - h:mm a"}}
20 | td {{ballot.totalVotes}}
21 | td
22 | a.fa.fa-list-ol.pointer(href="/results#{{ballot.key}}" tooltip-placement="top-left" uib-tooltip="See Results")
23 | i.fa.fa-refresh.pointer(ng-click="deleteVotes(ballot)" tooltip-placement="top" uib-tooltip="Reset Votes")
24 | i.fa.fa-pencil.pointer(ng-click="editBallot(ballot)" tooltip-placement="top" uib-tooltip="Edit Ballot")
25 | i.fa.fa-trash.pointer(ng-click="deleteBallot(ballot)" tooltip-placement="top-right" uib-tooltip="Delete Ballot")
--------------------------------------------------------------------------------
/src/jade/pages/register.html:
--------------------------------------------------------------------------------
1 | Register
2 |
3 |
39 |
40 |
Facebook
41 |
83 |
84 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | Thank you for signing in as {{user.name}}.
98 |
--------------------------------------------------------------------------------
/src/jade/pages/results.jade:
--------------------------------------------------------------------------------
1 | h1 Results
2 | span.ballot-name
3 | div(ng-hide="final")
4 | include ../partials/shortcode
5 | div(ng-show="final")
6 | h2 The winner{{elected.length == 1 ? "" : "s"}}:
7 | span(ng-repeat="result in elected") {{result}}
8 | span(ng-if="!$last") ,
9 | button.btn.btn-default.hidden-xs(ng-click="showText = !showText") {{!showText ? "Show" : "Hide"}} details
--------------------------------------------------------------------------------
/src/jade/pages/vote.jade:
--------------------------------------------------------------------------------
1 | div(ng-hide='candidates')
2 | h1 Vote!
3 | include ../partials/shortcode
4 | div(ng-if='candidates' ng-hide="thanks")
5 | h1 Ballot: {{ballot.name}}
6 | h2 Order by Preference
7 | h3(ng-show='ballot.positions > 1') {{ballot.positions}} positions available
8 | form(name='voteForm' ng-submit='submitVote()')
9 | ul#sortable(ui-sortable ng-model='candidates')
10 | li(ng-repeat='item in candidates track by $index' data-id='{{item.entry_id}}') {{item.name}}
11 | i.fa.fa-close(ng-click="removeCandidate($index)")
12 | button.btn.btn-primary.clearfix(type='submit') Vote!
13 | button.btn.btn-default(type="button" ng-click='resetCandidates()') Reset
14 | div(ng-show="thanks")
15 | h1 Ballot: {{ballot.name}}
16 | h3 Thank you for voting!
--------------------------------------------------------------------------------
/src/jade/partials/congrats.jade:
--------------------------------------------------------------------------------
1 | h3 Thank you for creating a ballot!
2 | h4 Send this url to people for voting:
3 | h4.text-success {{origin}}/{{ballot.key}}
4 | a(href="{{origin}}/vote#{{ballot.key}}") Click here to vote yourself
--------------------------------------------------------------------------------
/src/jade/partials/edit.jade:
--------------------------------------------------------------------------------
1 | form(name='ballotForm' ng-submit='editBallot()')
2 | .form-group
3 | label Ballot Name
4 | input.form-control(name="name" ng-model="ballot.name")
5 | span.text-danger(ng-show='errors.name') {{errors.name}}
6 | .form-group
7 | label Positions to be Elected
8 | input.form-control(type="number" name="positions" ng-model="ballot.positions")
9 | span.text-danger(ng-show='errors.positions') {{errors.positions}}
10 | .form-group
11 | label Delete all votes and reset Ballot
12 | input(type="checkbox" ng-model="showDelete")
13 | input.btn.btn-danger(ng-show="showDelete" type="button" value="Are you sure?" ng-click="deleteVotes(ballot)")
14 | span.text-warning(ng-show='deleted') Votes have been deleted.
15 | .form-group
16 | h4 Voting Cutoff: {{ballot.voteCutoff | date: "MMM d, y"}} {{ballot.voteCutoff | date: "h:mm a" }}
17 | label Edit Time
18 | input(type="checkbox" ng-model="editTime")
19 | uib-timepicker(ng-show="editTime" ng-model="ballot.voteCutoff" minute-step="1")
20 | label Edit Date
21 | input(type="checkbox" ng-model="editDate")
22 | .input-group(ng-show="editDate")
23 | input.form-control(type="text" ng-focus="releaseOpen = true" uib-datepicker-popup="{{pickerFormat}}" datepicker-options="pickerOptions" ng-model="ballot.voteCutoff" is-open="releaseOpen")
24 | span.input-group-btn
25 | button.btn.btn-default(type="button" ng-click="releaseOpen = !releaseOpen")
26 | i.fa.fa-calendar
27 | .form-group
28 | label Release Results:
29 | span(ng-hide="showRelease") after voting (check box for custom)
30 | input(type="checkbox" ng-model="showRelease" ng-click="sameTime()")
31 | small.text-muted(ng-show="showRelease && ballot.resultsRelease") {{ballot.resultsRelease | date: dateFormat }}
32 | div(ng-show="showRelease")
33 | .input-group
34 | input.form-control(type="text" ng-focus="cutoffOpen = true" uib-datepicker-popup="{{pickerFormat}}" datepicker-options="pickerOptions" ng-model="ballot.resultsRelease" is-open="cutoffOpen")
35 | span.input-group-btn
36 | button.btn.btn-default(type="button" ng-click="cutoffOpen = !cutoffOpen")
37 | i.fa.fa-calendar
38 | uib-timepicker(ng-model="ballot.resultsRelease" minute-step="15")
39 | .form-group
40 | label Require Voter Sign-in:
41 | span(ng-hide="ballot.requireSignIn") (with either Google or Facebook)
42 | input(type="checkbox" ng-model="ballot.requireSignIn")
43 | div(ng-show="ballot.requireSignIn")
44 | .input-group
45 | span.input-group-addon Max votes per person
46 | input.form-control(type="number" ng-model="ballot.maxVotes")
47 | input.btn.btn-info(type="submit" value="Submit")
--------------------------------------------------------------------------------
/src/jade/partials/footer.jade:
--------------------------------------------------------------------------------
1 | footer
2 | small © 2016 Ranked Choices Vote Calculator
--------------------------------------------------------------------------------
/src/jade/partials/form.jade:
--------------------------------------------------------------------------------
1 | form(name='ballotForm' ng-submit='newBallot()' ng-hide='entries')
2 | .form-group
3 | label Ballot Name
4 | input.form-control(name="name" ng-model="ballot.name")
5 | span.text-danger(ng-show='errors.name') {{errors.name}}
6 | .form-group
7 | label Shortcode (for link {{origin}}/{{ballot.key}})
8 | .input-group
9 | input.form-control(name="key" ng-model="ballot.key" ng-pattern-restrict pattern="\\w*" ng-change="checkAvailability()" maxlength=16 ng-readonly="editBallot")
10 | span.input-group-btn
11 | button.btn.btn-secondary(type="button" ng-click="generateRandomKey()" ng-disabled="editBallot") New Shortcode
12 | span.text-danger(ng-show='errors.key') {{errors.key}}
13 | span.text-success(ng-show='success.key') {{success.key}}
14 | .form-group
15 | label Advanced Options
16 | input(type="checkbox" ng-model="advancedOptions")
17 | div(ng-show="advancedOptions")
18 | .form-group
19 | label Positions to be Elected
20 | input.form-control(type="number" name="positions" min="1" ng-model="ballot.positions")
21 | span.text-danger(ng-show='errors.positions') {{errors.positions}}
22 | .form-group(ng-show="user.name")
23 | label Created By {{user.name}}
24 | .form-group(ng-show="ballot.createdBy == 'guest'")
25 | label Created By
26 | small.text-muted using default "guest" will restrict future editing. Please
27 | a.btn-link(href="register") Sign in
28 | | to allow future editing.
29 | input.form-control(name="createdBy" ng-model="ballot.createdBy" readonly)
30 | span.text-danger(ng-show='errors.createdBy') {{errors.createdBy}}
31 | .form-group
32 | h4(ng-hide="editDate || editTime") Voting Cutoff: never (check box for custom)
33 | h4(ng-show="editDate || editTime") Voting Cutoff: {{editDate ? (ballot.voteCutoff | date: "MMM d, y") : "Today, at"}} {{ballot.voteCutoff | date: "h:mm a" }}
34 | label Edit Time
35 | input(type="checkbox" ng-model="editTime")
36 | uib-timepicker(ng-show="editTime" ng-model="ballot.voteCutoff" minute-step="1")
37 | label Edit Date
38 | input(type="checkbox" ng-model="editDate")
39 | .input-group(ng-show="editDate")
40 | input.form-control(type="text" ng-focus="releaseOpen = true" uib-datepicker-popup="{{pickerFormat}}" datepicker-options="pickerOptions" ng-model="ballot.voteCutoff" is-open="releaseOpen")
41 | span.input-group-btn
42 | button.btn.btn-default(type="button" ng-click="releaseOpen = !releaseOpen")
43 | i.fa.fa-calendar
44 | .form-group
45 | label Release Results:
46 | span(ng-hide="showRelease || editDate || editTime") immediately (check box for custom)
47 | span(ng-hide="showRelease || (!editDate && !editTime)") after voting (check box for custom)
48 | input(type="checkbox" ng-model="showRelease" ng-click="sameTime()")
49 | small.text-muted(ng-show="showRelease && ballot.resultsRelease") {{ballot.resultsRelease | date: dateFormat }}
50 | div(ng-show="showRelease")
51 | .input-group
52 | input.form-control(type="text" ng-focus="cutoffOpen = true" uib-datepicker-popup="{{pickerFormat}}" datepicker-options="pickerOptions" ng-model="ballot.resultsRelease" is-open="cutoffOpen")
53 | span.input-group-btn
54 | button.btn.btn-default(type="button" ng-click="cutoffOpen = !cutoffOpen")
55 | i.fa.fa-calendar
56 | uib-timepicker(ng-model="ballot.resultsRelease" minute-step="15")
57 | .form-group
58 | label Tie-Break Method:
59 | div
60 | label.radio-inline(title="Randomly determine the order of candidates being eliminated")
61 | input(type="radio" value="random" name="tie-break-method" ng-model="ballot.tieBreak")
62 | | Random (official)
63 | label.radio-inline(title="Observe the preference of secondary choices to determine")
64 | input(type="radio" value="weighted" name="tie-break-method" ng-model="ballot.tieBreak")
65 | | Weighted
66 | label.radio-inline(title="All candidates in a tie are simultaneously eliminated")
67 | input(type="radio" value="none" name="tie-break-method" ng-model="ballot.tieBreak")
68 | | No Winner
69 | .form-group(ng-hide="true")
70 | label Require Voter Sign-in:
71 | span(ng-hide="ballot.requireSignIn") (with either Google or Facebook)
72 | input(type="checkbox" ng-model="ballot.requireSignIn")
73 | div(ng-show="ballot.requireSignIn")
74 | .input-group
75 | span.input-group-addon Max votes per person
76 | input.form-control(type="number" ng-model="ballot.maxVotes")
77 | input.btn.btn-info(type="submit" value="Submit" ng-disabled="errors.key")
78 |
--------------------------------------------------------------------------------
/src/jade/partials/head.jade:
--------------------------------------------------------------------------------
1 | head
2 | script(src='https://apis.google.com/js/platform.js' async defer)
3 | meta(charset='UTF-8')
4 | meta(name='description', content='RCV Calculator App')
5 | meta(name='author', content='Moritz Company')
6 | meta(name='viewport', content='width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no')
7 | meta(name='apple-mobile-web-app-capable' content='yes')
8 | meta(name='apple-touch-fullscreen' content='yes')
9 | meta(name='google-signin-client_id' content='828084230115-98814hb54sbrs34jjiecfirgak4svths.apps.googleusercontent.com')
10 | title Ranked Choice Vote Calculator App
11 | link(rel='shortcut icon', href='favicon.ico')
12 | link(rel='stylesheet', href='inc/main.css')
13 | script(src='inc/angular.js')
14 | script(src='inc/lib.js')
15 | script(src='inc/main.js?v=10')
16 | base(href='/')
17 | //- include analytics:
18 |
28 |
--------------------------------------------------------------------------------
/src/jade/partials/mixins.jade:
--------------------------------------------------------------------------------
1 | //- all 4 sizes are passed into the mixin in case a different ratio size is provided for the various sizes
2 | mixin col(xs, ms, sm, md, lg)
3 | - items = []
4 | - (xs > 1 && xs <= 12) ? items.push('col-xs-' + xs) : undefined
5 | - (ms > 1 && ms <= 12) ? items.push('col-ms-' + ms) : undefined
6 | - (sm > 1 && sm <= 12) ? items.push('col-sm-' + sm) : undefined
7 | - (md > 1 && md <= 12) ? items.push('col-md-' + md) : undefined
8 | - (lg > 1 && lg <= 12) ? items.push('col-lg-' + lg) : undefined
9 | - (!items.length) ? items.push('col-xs-12') : undefined
10 | section(class=items.join(' '))&attributes(attributes)
11 | if block
12 | block
--------------------------------------------------------------------------------
/src/jade/partials/navbar.jade:
--------------------------------------------------------------------------------
1 | nav.navbar-fairvote.navbar(role="navigation")
2 | .container
3 | .row
4 | //- Brand and toggle get grouped for better mobile display
5 | .navbar-header
6 | button.navbar-toggle.collapsed(type="button" data-toggle="collapse" data-target="#navbar" aria-expanded="false")
7 | span.sr-only Toggle navigation
8 | span.icon-bar
9 | span.icon-bar
10 | span.icon-bar
11 | //- a.navbar-brand(href="/home") RCV
12 | //- Collect the nav links, forms, and other content for toggle
13 | #navbar.collapse.navbar-collapse
14 | ul.nav.navbar-nav
15 | li(ng-repeat="nav in navItems" ng-class="{'active': nav.link == activeLink}" ng-hide="nav.hide")
16 | a(href="/{{nav.link}}") {{nav.text}}
17 | span.sr-only(ng-if="nav.link == activeLink") (current)
--------------------------------------------------------------------------------
/src/jade/partials/new-entries.jade:
--------------------------------------------------------------------------------
1 | form(name='entriesForm' ng-submit='addEntry()')
2 | .form-group
3 | label New Entry (use ENTER to add)
4 | input.form-control(name="entryInput" ng-model="entryInput")
5 | span(ng-show='errorEntry') {{errorEntry}}
6 | button.btn.btn-info(type='submit') Add Entry
7 | button.btn.btn-default(type='button' ng-click="submitEntries()") Submit All Entries
8 | h2 Current Entry List
9 | h5(ng-repeat="entry in entries track by $index")
10 | strong {{$index + 1}}
11 | | {{entry}}
12 | i.fa.fa-close.pointer(ng-click="removeEntry($index)")
--------------------------------------------------------------------------------
/src/jade/partials/other-nav-items.jade:
--------------------------------------------------------------------------------
1 | form.navbar-form.navbar-left(role="search")
2 | .form-group
3 | input.form-control(type="text" placeholder="Search")
4 | button.btn.btn-default(type="submit") Submit
5 | ul.nav.navbar-nav.navbar-right
6 | li
7 | a(href="#") Link
8 | li.dropdown
9 | a.dropdown-toggle(href="#" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false") Dropdown
10 | span.caret
11 | ul.dropdown-menu
12 | li
13 | a(href='#') Home
14 | li
15 | a(href='#') About
16 | li
17 | a(href='#') Create a new ballot!
18 | li
19 | a(href='#') Edit Ballot!
20 | li
21 | a(href='#') Results
22 | li
23 | a(href='#') Vote!
--------------------------------------------------------------------------------
/src/jade/partials/shortcode.jade:
--------------------------------------------------------------------------------
1 | form(ng-submit="submitShortcode()")
2 | .row
3 | .form-group.col-sm-6.col-md-4.col-lg-3
4 | label Enter Ballot Shortcode
5 | input.form-control(name="ballot" ng-model="shortcode")
6 | span.text-danger(ng-show='errors.shortcode') {{errors.shortcode}}
7 | input.btn.btn-success(type="submit" value="Submit")
--------------------------------------------------------------------------------
/src/jade/partials/shuffle.jade:
--------------------------------------------------------------------------------
1 | div(ng-hide="thanks || congrats")
2 | div(ng-show="editBallot")
3 | select.form-control(ng-model="ballot" ng-options="item as item.name for item in allBallots track by item.id" ng-change="changeEditBallot()")
4 | h1 Ballot: {{ballot.name}}
5 | div(ng-show="createBallot")
6 | div(ng-show='updated && !candidates')
7 | include partials/new-entries
8 | div(ng-show="congrats")
9 | include partials/congrats
--------------------------------------------------------------------------------
/src/js/app.js:
--------------------------------------------------------------------------------
1 | var mainApp = angular.module('mainApp', [
2 | 'ui.sortable',
3 | 'ui.bootstrap'
4 | ]);
5 |
6 | mainApp.config(function($locationProvider) {
7 | $locationProvider.html5Mode(true);
8 | });
9 |
10 | mainApp.run(function runWithDependencies($rootScope) {
11 | $rootScope._ = _;
12 | $rootScope.moment = moment;
13 | $rootScope.mc = mc;
14 | });
15 |
16 | function onSignIn(googleUser) {
17 | var profile = googleUser.getBasicProfile();
18 | console.log('ID: ' + profile.getId()); // Do not send to your backend! Use an ID token instead.
19 | console.log('Name: ' + profile.getName());
20 | console.log('Image URL: ' + profile.getImageUrl());
21 | console.log('Email: ' + profile.getEmail());
22 | setUser({
23 | id: profile.getId(),
24 | name: profile.getName(),
25 | email: profile.getEmail(),
26 | image: profile.getImageUrl()
27 | });
28 | }
29 |
30 | window.fbAsyncInit = function() {
31 | FB.init({
32 | appId: '916767641776363',
33 | xfbml: true,
34 | version: 'v2.5'
35 | });
36 | FB.getLoginStatus(function(response) {
37 | statusChangeCallback(response);
38 | });
39 | };
40 |
41 | // (function(d, s, id){
42 | // var js, fjs = d.getElementsByTagName(s)[0];
43 | // if (d.getElementById(id)) {return;}
44 | // js = d.createElement(s); js.id = id;
45 | // js.src = '//connect.facebook.net/en_US/sdk.js';
46 | // fjs.parentNode.insertBefore(js, fjs);
47 | // }(document, 'script', 'facebook-jssdk'));
--------------------------------------------------------------------------------
/src/js/main.js:
--------------------------------------------------------------------------------
1 | mainApp.controller('MainCtrl', [
2 | '$scope',
3 | '$location',
4 | '$timeout',
5 | '$interval',
6 | '$http',
7 | '$sce',
8 | 'VoteFactory',
9 | function MainCtrl($s, $loc, $timeout, $interval, $http, $sce, VF) {
10 | 'use strict';
11 |
12 | //during development
13 | window.$s = $s;
14 | $s.user = $s.user || {};
15 |
16 | var getVoteParam = function(param) {
17 | var temp = $loc.$$absUrl.split('/').pop();
18 |
19 | if (!temp) {
20 | $s.navigate('home');
21 | } else {
22 | temp = temp.split('#');
23 | param = temp[0];
24 | }
25 |
26 | if (_.find($s.navItems, {link: param})) {
27 | $s.navigate(param, temp[1]);
28 |
29 | return '';
30 | }
31 |
32 | return param;
33 | };
34 |
35 | var updateTime = function(dateObj) {
36 | var mom = moment(dateObj);
37 |
38 | if (mom.isDST()) {
39 | mom.utcOffset(-5);
40 | } else {
41 | mom.utcOffset(-6);
42 | }
43 |
44 | return mom.format('YYYY-MM-DD HH:mm:ss');
45 | };
46 |
47 | var deleteThis = function(data, item) {
48 | $http({
49 | method: 'POST',
50 | url: '/api/delete-' + item + '.php',
51 | data: data,
52 | headers: {'Content-Type': 'application/x-www-form-urlencoded'}
53 | }).success(function(resp) {
54 | $s.deleted = true;
55 | });
56 | };
57 |
58 | var resetBallot = function() {
59 | $s.generateRandomKey();
60 | $s.entries = null;
61 |
62 | return {
63 | positions: 1,
64 | createdBy: $s.user.id || 'guest',
65 | maxVotes: 1,
66 | tieBreak: 'random',
67 | voteCutoff: roundResultsRelease()
68 | };
69 | };
70 |
71 | var getBallots = function() {
72 | // we need to get ballots based on user signin
73 | if ($s.user.id) {
74 | $http({
75 | method: 'POST',
76 | url: '/api/get-ballots.php',
77 | data: $s.user,
78 | headers: {'Content-Type': 'application/x-www-form-urlencoded'}
79 | }).then(function(resp) {
80 | $s.now = new Date();
81 | $s.allBallots = resp.data.map(function(ballot) {
82 | ballot.voteCutoff = new Date(ballot.voteCutoff);
83 | ballot.resultsRelease = new Date(ballot.resultsRelease);
84 |
85 | return ballot;
86 | });
87 | });
88 | } else {
89 | setTimeout(getBallots, 500);
90 | }
91 | };
92 |
93 | var resetNav = function(hide) {
94 | $s.navItems.map(function(item) {
95 | if (item.link == 'profile') {
96 | item.hide = !hide;
97 | } else if (item.link == 'register') {
98 | item.hide = !!hide;
99 | }
100 | });
101 | };
102 |
103 | $s.$watch(function() {
104 | return window.location.pathname;
105 | }, getVoteParam);
106 |
107 | // initialize scoped variables
108 | _.assign($s, {
109 | activeLink: 'home',
110 | navItems: [
111 | {
112 | link: 'home',
113 | text: 'Home'
114 | },{
115 | link: 'about',
116 | text: 'About'
117 | },{
118 | link: 'create',
119 | text: 'Create a new Ballot!'
120 | },{
121 | link: 'profile',
122 | text: 'Profile',
123 | hide: true
124 | },{
125 | link: 'results',
126 | text: 'Results'
127 | },{
128 | link: 'register',
129 | text: 'Register'
130 | },{
131 | link: 'vote',
132 | text: 'Vote!'
133 | }
134 | ],
135 | ballot: {},
136 | errors: {},
137 | success: {},
138 | dateFormat: 'MMM d, y h:mm a',
139 | pickerFormat: 'fullDate',
140 | pickerOptions: {
141 | showWeeks: false
142 | },
143 | origin: window.location.origin
144 | });
145 |
146 | _.extend($s, VF);
147 |
148 | function roundResultsRelease() {
149 | var now = new Date();
150 | var m = now.getMinutes();
151 | var offset = parseInt((m + 25) / 15) * 15;
152 | now = new Date(now.setSeconds(0));
153 |
154 | // vote ends 15 minutes after it starts round to the nearest quarter
155 | return new Date(now.setMinutes(offset));
156 | }
157 |
158 | $s.navigate = function(link, shortcode) {
159 | var title = _.find($s.navItems, {link: link}).text;
160 | $s.activeLink = link;
161 |
162 | if ($('.navbar-collapse').hasClass('in')) {
163 | $('.navbar-collapse').collapse('hide');
164 | }
165 |
166 | switch (link) {
167 | case 'create':
168 | $s.ballot = resetBallot();
169 | $s.congrats = false;
170 | break;
171 | case 'profile':
172 | getBallots();
173 | }
174 |
175 | if (shortcode) {
176 | $s.shortcode = shortcode;
177 | $s.submitShortcode();
178 | } else {
179 | $s.shortcode = '';
180 | }
181 | };
182 |
183 | $s.signOut = function() {
184 | var auth2 = gapi.auth2.getAuthInstance();
185 | auth2.signOut().then(function() {
186 | console.log('User signed out.');
187 | resetNav();
188 | });
189 | };
190 |
191 | $s.sameTime = function() {
192 | $s.ballot.resultsRelease = new Date($s.ballot.voteCutoff.getTime());
193 | };
194 |
195 | $s.updateUser = function(user) {
196 | $s.user = user;
197 | resetNav(true);
198 | $s.$apply();
199 | $http({
200 | method: 'POST',
201 | url: '/api/add-user.php',
202 | data: $s.user,
203 | headers: {'Content-Type': 'application/x-www-form-urlencoded'}
204 | });
205 | };
206 |
207 | $s.getCandidates = function() {
208 | $http.get('/api/get-candidates.php?key=' + $s.shortcode)
209 | .then(function(resp) {
210 | if (typeof resp.data == 'string') {
211 | $s.errors.shortcode = resp.data;
212 |
213 | return;
214 | }
215 |
216 | $s.originalCandidates = resp.data.map(function(entry) {
217 | $s.ballot = entry;
218 | $s.ballot.positions = parseInt($s.ballot.positions);
219 |
220 | return {
221 | name: entry.candidate,
222 | id: entry.entry_id
223 | };
224 | });
225 | $s.activeLink = 'vote';
226 | $s.resetCandidates();
227 | })
228 | ;
229 | };
230 |
231 | $s.getResults = function() {
232 | var key = $s.shortcode || $s.ballot.key;
233 | $http.get('/api/get-votes.php?key=' + key)
234 | .then(function(resp) {
235 | if (typeof resp.data == 'string') {
236 | $s.errors.shortcode = resp.data;
237 |
238 | return;
239 | }
240 |
241 | $s.votes = resp.data.map(function(result) {
242 | $s.seats = parseInt(result.positions);
243 | $s.tieBreakMethod = result.tieBreakMethod;
244 |
245 | if (result.vote) {
246 | return JSON.parse(result.vote);
247 | }
248 | // THIS DOESN'T WORK YET
249 | //$s.names = result;
250 | });
251 | $('.ballot-name').text(' for ' + resp.data[0].name);
252 | $s.names = _.uniq(_.flatten($s.votes));
253 | $s.runTheCode();
254 | $s.bodyText = $sce.trustAsHtml($s.outputstring);
255 | $s.final = true;
256 | })
257 | ;
258 | };
259 |
260 | $s.generateRandomKey = function(len) {
261 | len = len || 4;
262 | var key = Math.random().toString(36).substr(2, len);
263 | $http.get('/api/get-key-ballot.php?key=' + key)
264 | .then(function(resp) {
265 | if (resp.data.length) {
266 | $s.generateRandomKey(++len);
267 | } else {
268 | $s.errors.key = null;
269 | $s.success.key = null;
270 | $s.ballot.key = key;
271 | }
272 | })
273 | ;
274 | };
275 |
276 | $s.changeEditBallot = function() {
277 | $s.createBallot = true;
278 | $s.ballot.positions = parseInt($s.ballot.positions);
279 | $s.ballot.resultsRelease = new Date($s.ballot.resultsRelease);
280 | $s.ballot.voteCutoff = new Date($s.ballot.voteCutoff);
281 |
282 | $http.get('/api/get-candidates.php?key=' + $s.ballot.key)
283 | .then(function(resp) {
284 | if (resp.data) {
285 | $s.entries = resp.data.map(function(entry) {
286 | return entry.candidate;
287 | });
288 | }
289 | })
290 | ;
291 | };
292 |
293 | $s.checkAvailability = _.debounce(function() {
294 | $http.get('/api/get-key-ballot.php?key=' + $s.ballot.key)
295 | .then(function(resp) {
296 | if (resp.data.length) {
297 | $s.success.key = null;
298 |
299 | if ($s.ballot.key) {
300 | $s.errors.key = $s.ballot.key + ' is already in use';
301 | } else {
302 | $s.errors.key = 'Shortcode is required';
303 | }
304 | } else {
305 | $s.errors.key = null;
306 | $s.success.key = $s.ballot.key + ' is available';
307 | }
308 | })
309 | ;
310 | }, 250);
311 |
312 | $s.removeEntry = function(idx) {
313 | $s.entries.splice(idx, 1);
314 | };
315 |
316 | $s.removeCandidate = function(idx) {
317 | $s.candidates.splice(idx, 1);
318 | };
319 |
320 | $s.resetCandidates = function() {
321 | $s.candidates = _.shuffle($s.originalCandidates);
322 | };
323 |
324 | $s.newBallot = function() {
325 | if (!$s.editTime && !$s.editDate) {
326 | $s.ballot.resultsRelease = updateTime(new Date());
327 | $s.ballot.voteCutoff = updateTime(new Date('2199-12-31T23:59:59'));
328 | } else {
329 | $s.sameTime();
330 | $s.ballot.voteCutoff = updateTime($s.ballot.voteCutoff);
331 | }
332 |
333 | if ($s.showRelease) {
334 | $s.ballot.resultsRelease = updateTime($s.ballot.resultsRelease);
335 | }
336 |
337 | $s.ballot.createdBy = $s.user.id || 'guest';
338 | $http({
339 | method: 'POST',
340 | url: '/api/' + ($s.editBallot ? 'update' : 'new') + '-ballot.php',
341 | data: $s.ballot,
342 | headers: {'Content-Type': 'application/x-www-form-urlencoded'}
343 | }).success(function(resp) {
344 | if (resp.errors) {
345 | $s.errors = resp.errors;
346 | } else {
347 | if (!$s.editBallot) {
348 | $s.ballot.id = resp;
349 | }
350 | $s.entries = [];
351 | }
352 | });
353 | };
354 |
355 | $s.submitEntries = function() {
356 | if ($s.entries.length < 2) {
357 | $s.errorEntry = 'Must have at least 2 entries';
358 |
359 | return;
360 | }
361 |
362 | if ($s.editBallot) {
363 | $http.get('/api/delete-entries.php?ballotId=' + $s.ballot.id)
364 | .then(function(resp) {
365 | $s.editBallot = false;
366 | $s.submitEntries();
367 | })
368 | ;
369 |
370 | return;
371 | }
372 | $http({
373 | method: 'POST',
374 | url: '/api/add-entries.php',
375 | data: {
376 | entries: $s.entries,
377 | ballotId: $s.ballot.id
378 | },
379 | headers: {'Content-Type': 'application/x-www-form-urlencoded'}
380 | }).success(function(resp) {
381 | if (resp.errors) {
382 | $s.errors = resp.errors;
383 | } else {
384 | $s.congrats = true;
385 | }
386 | });
387 | };
388 |
389 | $s.submitVote = function() {
390 | $http({
391 | method: 'POST',
392 | url: '/api/vote.php',
393 | data: {
394 | vote: JSON.stringify($s.candidates.map(function(cand) {
395 | return cand.name;
396 | })),
397 | key: $s.ballot.key
398 | },
399 | headers: {'Content-Type': 'application/x-www-form-urlencoded'}
400 | }).success(function(resp) {
401 | $s.thanks = true;
402 | console.log(resp);
403 | });
404 | };
405 |
406 | $s.deleteBallot = function(ballot) {
407 | if (confirm('Delete ' + ballot.name + ' ballot?\nThis action cannot be undone')) {
408 | deleteThis(ballot, 'ballot');
409 | _.remove($s.allBallots, ballot);
410 | }
411 | };
412 |
413 | $s.deleteVotes = function(ballot) {
414 | if (confirm('Delete all ' + ballot.name + ' votes?\nThis action cannot be undone')) {
415 | deleteThis(ballot, 'votes');
416 | ballot.totalVotes = 0;
417 | }
418 | };
419 |
420 | $s.voteNow = function() {
421 | $s.congrats = false;
422 | $s.originalCandidates = $s.entries;
423 | $s.resetCandidates();
424 | };
425 |
426 | $s.showResults = function() {
427 | $s.thanks = true;
428 | $s.final = true;
429 | $s.getResults();
430 | };
431 |
432 | $s.submitShortcode = function() {
433 | if ($s.activeLink == 'results') {
434 | $s.getResults();
435 | } else {
436 | $s.getCandidates();
437 | }
438 | };
439 |
440 | $s.addEntry = function() {
441 | if (!$s.entryInput.length) {
442 | $s.errorEntry = 'Entries must not be blank';
443 | } else if ($s.entries.indexOf($s.entryInput) !== -1) {
444 | $s.errorEntry = 'No duplicate entries allowed';
445 | } else {
446 | $s.errorEntry = '';
447 | $s.entries.push($s.entryInput);
448 | $s.entryInput = '';
449 | }
450 | };
451 |
452 | $s.shortcode = getVoteParam();
453 |
454 | if ($s.shortcode) {
455 | $s.activeLink = 'vote';
456 | $s.getCandidates();
457 | }
458 | }
459 | ]);
460 |
--------------------------------------------------------------------------------
/src/js/services/VoteFactory.js:
--------------------------------------------------------------------------------
1 | mainApp.factory('VoteFactory', [
2 | function VoteFactory() {
3 | 'use strict';
4 |
5 | return {
6 | wincount: 0,
7 | roundnum: 0,
8 | elected: [],
9 |
10 | runTheCode: function() {
11 | this.renewQuota();
12 | this.outputstring = this.createHeader();
13 | this.anotherRound();
14 | },
15 |
16 | renewQuota: function() {
17 | this.quota = _.round(this.votes.length / (this.seats + 1), 2);
18 | this.voteweight = _.range(1, this.votes.length + 1, 0);
19 | },
20 |
21 | displayVotes: function() {
22 | var model = this;
23 | this.outputstring += '';
24 | _.each(this.votes, function(vote, idx) {
25 | model.outputstring += 'Vote ' + (idx + 1) + ': ';
26 | var colspan = model.names.length - vote.length;
27 | _.each(vote, function(name, idx2) {
28 | if (idx2 === 0) {
29 | model.outputstring += '' + name + ' ';
30 | } else {
31 | model.outputstring += '' + name + ' ';
32 | }
33 | });
34 |
35 | if (colspan) {
36 | model.outputstring += ' ';
37 | }
38 |
39 | if (model.seats > 1) {
40 | model.outputstring += 'vote-value = ' + _.round(model.voteweight[idx], 4) + ' ';
41 | }
42 | });
43 | this.outputstring += '
';
44 | },
45 |
46 | createHeader: function() {
47 | if (this.seats > 1) {
48 | return 'Candidates: ' + this.names.length + ' Seats: ' + this.seats + ' Votes: ' + this.votes.length + ' Quota: ' + this.quota + ' ';
49 | } else {
50 | return 'Candidates: ' + this.names.length + ' Votes: ' + this.votes.length + ' ';
51 | }
52 | },
53 |
54 | // Check for 'end' conditions otherwise count votes again
55 | anotherRound: function() {
56 | if (this.wincount == this.seats) {
57 | this.finishElection();
58 | } else {
59 | this.outputstring += '
Round ' + (++this.roundnum) + ' votes ';
60 | this.displayVotes();
61 | this.countVotes();
62 | }
63 | },
64 |
65 | countVotes: function() {
66 | var quotacount = 0;
67 | var model = this;
68 | this.votenum = _.range(0, this.names.length, 0);
69 |
70 | _.each(this.votes, function(vote, idx) {
71 | var choice = model.names.indexOf(vote[0]);
72 | model.votenum[choice] += model.voteweight[idx];
73 | });
74 |
75 | _.each(this.names, function(name, idx) {
76 | model.outputstring += name + ' = ' + _.round(model.votenum[idx], 4) + ' ';
77 |
78 | if (model.votenum[idx] > model.quota) {
79 | quotacount++;
80 | }
81 | });
82 |
83 | this.buildDataForOutcome(quotacount);
84 | },
85 |
86 | buildDataForOutcome: function(data) {
87 | if (data) {
88 | data = {
89 | math: 'max',
90 | class: 'elected',
91 | elect: true,
92 | text: {
93 | count: 'Most votes currently held',
94 | total: 'greatest number of',
95 | tie: 'says the first surplus to be re-allocated',
96 | result: 'has exceeded the quota and is elected. If there are seats remaining to be filled, the surplus will now be reallocated'
97 | }
98 | };
99 | } else {
100 | data = {
101 | math: 'min',
102 | class: 'eliminated',
103 | text: {
104 | count: 'Fewest votes won',
105 | total: 'fewest',
106 | tie: 'loser',
107 | result: 'is eliminated'
108 | }
109 | };
110 | }
111 |
112 | this.determineOutcome(data);
113 | },
114 |
115 | // Show results for either winning candidate or losing candidate.
116 | determineOutcome: function(data) {
117 | // TODO: Eliminate every candidate with zero votes in the first round.
118 | // apex = votes needed to either be elected or be eliminated
119 | var apex = this.votenum.reduce(function(prev, current) {
120 | return current ? Math[data.math](prev, current) : prev;
121 | }, this.quota);
122 | var count = this.votenum.filter(function(num) {
123 | return num == apex;
124 | }).length;
125 | var chosen = this.votenum.indexOf(apex);
126 |
127 | this.outputstring += ' ' + data.text.count + ' by a candidate = ' + _.round(apex, 4) + '.';
128 | this.outputstring += ' Number of candidates with the ' + data.text.total + ' votes = ' + count + '.';
129 |
130 | if (count > 1) {
131 | chosen = this.tieBreakMethod == 'weighted' ? this.breakTieWeighted : this.breakTieRandom(apex);
132 | this.outputstring += ' The random tiebreaker ' + data.text.tie + ' is ' + this.names[chosen] + ' \'s.';
133 | }
134 |
135 | this.outputstring += '' + this.names[chosen] + ' ' + data.text.result + '.';
136 | this.removeChosen(chosen, data.class, data.elect);
137 | },
138 |
139 | // remove either the elected or eliminated candidates from votes
140 | removeChosen: function(chosen, className, elect) {
141 | if (elect) {
142 | this.elected[this.wincount++] = this.names[chosen];
143 | }
144 | var model = this;
145 | _.each(this.votes, function(vote, index) {
146 | var found = vote.indexOf(model.names[chosen]);
147 |
148 | if (found !== -1) {
149 | if (found === 0) {
150 | if (elect) {
151 | model.voteweight[index] *= 1 - model.quota / model.votenum[chosen];
152 | }
153 | vote.push('' + vote[found] + ' ');
154 | }
155 | vote.splice(found, 1);
156 | }
157 | });
158 | this.anotherRound();
159 | },
160 |
161 | // Analyses which candidates are marginally stronger for the purpose of breaking ties
162 | breakTieWeighted: function(value) {
163 | var model = this;
164 | var tieArray = [];
165 | var i;
166 | // length of longest vote array
167 | var voteSize = this.votes.reduce(function(voteSize, vote) {
168 | return Math.max(voteSize, vote.length);
169 | }, 0);
170 | var calculateValue = function(voteArr, idx) {
171 | var tie = _.find(tieArray, {name: voteArr[i]});
172 |
173 | if (tie) {
174 | // 2nd place votes are exponentially greater than 3rd place votes etc.
175 | tie.value += model.voteweight[idx] / Math.pow(10, i);
176 | }
177 | };
178 | // populate tieArray only with tie breakers
179 | this.votenum.map(function(val, idx) {
180 | if (val == value) {
181 | tieArray.push({
182 | index: idx,
183 | name: model.names[idx],
184 | value: 0
185 | });
186 | }
187 | });
188 |
189 | for (i = 1; i < voteSize; i++) {
190 | this.votes.map(calculateValue);
191 | }
192 | // sort by ascending vote value
193 | tieArray.sort(function(a, b) {
194 | return a.value > b.value;
195 | });
196 |
197 | return tieArray[0].index;
198 | },
199 |
200 | // creative way to achieve repeatable randomizing
201 | breakTieRandom: function(value) {
202 | var model = this;
203 | var tieArray = [];
204 | var randomize = function(string) {
205 | string = model.votes.length + string.replace(/\W/g, '') + model.roundnum;
206 | // algorithm supplied by http://indiegamr.com/generate-repeatable-random-numbers-in-js/
207 | return (parseInt(string, 36) * 9301 + 49297) % 233280;
208 | };
209 | // populate tieArray only with tie breakers
210 | this.votenum.map(function(val, idx) {
211 | if (val == value) {
212 | tieArray.push({
213 | index: idx,
214 | rand: randomize(model.names[idx] + idx)
215 | });
216 | }
217 | });
218 |
219 | // sort by ascending random value
220 | tieArray.sort(function(a, b) {
221 | return a.rand > b.rand;
222 | });
223 |
224 | return tieArray[0].index;
225 | },
226 |
227 | // Finish election and announce the winner(s).
228 | finishElection: function() {
229 | var model = this;
230 | this.outputstring += 'The election is complete and the elected candidates are';
231 | _.each(this.elected, function(name) {
232 | model.outputstring += ' (' + name + ')';
233 | });
234 | this.outputstring += '.
';
235 | }
236 | };
237 | }
238 | ]);
239 |
--------------------------------------------------------------------------------
/src/js/services/mc.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Moritz Company utility functions (for use anywhere) with mc.pluralize()
3 | **/
4 |
5 | var mc = {
6 | pluralize: function pluralize(str) {
7 | return str.replace(/y$/, 'ie') + 's';
8 | },
9 |
10 | camelToTitle: function camelToTitle(str) { // convert camelCaseString to Title Case String
11 | return _.capitalize(str.replace(/([A-Z])/g, ' $1')).trim();
12 | },
13 |
14 | randomDigits: function randomDigits(min, max) {
15 | min = min === undefined ? 1 : min;
16 | max = max || 999;
17 |
18 | return Math.floor(Math.random() * (max - min + 1) + min);
19 | },
20 |
21 | alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''),
22 |
23 | isAngularObjectEqual: function isAngularObjectEqual(object1, object2) {
24 | return _.isEqual(_.omit(object1, '$$hashKey'), _.omit(object2, '$$hashKey'));
25 | },
26 |
27 | expandArray: function expandArray(array, times) { // turns [1,2,3] into [1,2,3,1,2,3,1,2,3];
28 | times = times || 3; // default number of times to expand it by
29 |
30 | var expandedArray = [];
31 |
32 | for (var i = 0; i < times; i++) {
33 | expandedArray = expandedArray.concat(angular.copy(array));
34 | }
35 |
36 | return expandedArray;
37 | },
38 |
39 | calculateAge: function calculateAge(dateOfBirth) {
40 | var age;
41 |
42 | if (dateOfBirth) {
43 | var year = Number(dateOfBirth.substr(0, 4));
44 | var month = Number(dateOfBirth.substr(5, 2)) - 1;
45 | var day = Number(dateOfBirth.substr(8, 2));
46 | var today = new Date();
47 | age = today.getFullYear() - year;
48 |
49 | if (today.getMonth() < month || (today.getMonth() == month && today.getDate() < day)) {
50 | age--;
51 | }
52 | }
53 |
54 | return age || 0;
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/src/less/all.less:
--------------------------------------------------------------------------------
1 | /*** Main stylesheets ***/
2 | @import "main-theme";
3 | @import (less) "../../lib/jquery-ui/themes/smoothness/jquery-ui.css";
4 | @import "../../lib/fontawesome/less/font-awesome";
5 | @import "common";
6 | @import "jquery-ui";
7 | @import "elections";
8 | @import "pages";
9 | @import "fair-vote";
--------------------------------------------------------------------------------
/src/less/common.less:
--------------------------------------------------------------------------------
1 | /***** Common CSS ******/
2 |
3 | .pos-rel {
4 | position: relative;
5 | }
6 | .centered {
7 | margin: auto;
8 | }
9 | .pointer {
10 | cursor: pointer;
11 | }
12 | .upper {
13 | text-transform: uppercase;
14 | }
15 | [hidden] {
16 | display: none;
17 | }
18 | .btn {
19 | margin-top: 2px;
20 | .input-group-btn & {
21 | margin-top: 0;
22 | }
23 | }
24 | .btn-icon {
25 | margin-left: 6px;
26 | font-size: 115%;
27 | }
28 | .alert-flip {
29 | margin-bottom: 0px;
30 | margin-top: 20px;
31 | }
32 | .block {
33 | display: block;
34 | }
35 | .inline-block,
36 | .inline-block.collapse.in {
37 | display: inline-block;
38 | }
39 | .inline,
40 | .inline.collapse.in {
41 | display: inline;
42 | }
43 | #performance-gauge {width: 85%;}
44 |
45 | // add Bootstrap ms size
46 | .col-ms-1,
47 | .col-ms-2,
48 | .col-ms-3,
49 | .col-ms-4,
50 | .col-ms-5,
51 | .col-ms-6,
52 | .col-ms-7,
53 | .col-ms-8,
54 | .col-ms-9,
55 | .col-ms-10,
56 | .col-ms-11,
57 | .col-ms-12 {
58 | position: relative;
59 | min-height: 1px;
60 | padding-left: 15px;
61 | padding-right: 15px; }
62 |
63 | @media (min-width: @screen-xs) and (max-width: @screen-xs-max) {
64 | .visible-xs-only {
65 | display: none !important;
66 | }
67 | .visible-ms {
68 | display: block;
69 | }
70 | .visible-ms-block {
71 | display: block !important;
72 | }
73 | .visible-ms-inline-block {
74 | display: inline-block !important;
75 | }
76 | .visible-ms-inline {
77 | display: inline !important;
78 | }
79 | .hidden-ms {
80 | display: none !important;
81 | }
82 | .col-ms-1,
83 | .col-ms-2,
84 | .col-ms-3,
85 | .col-ms-4,
86 | .col-ms-5,
87 | .col-ms-6,
88 | .col-ms-7,
89 | .col-ms-8,
90 | .col-ms-9,
91 | .col-ms-10,
92 | .col-ms-11 {
93 | float: left; }
94 |
95 | .col-ms-1 {
96 | width: 8.33333%; }
97 |
98 | .col-ms-2 {
99 | width: 16.66667%; }
100 |
101 | .col-ms-3 {
102 | width: 25%; }
103 |
104 | .col-ms-4 {
105 | width: 33.33333%; }
106 |
107 | .col-ms-5 {
108 | width: 41.66667%; }
109 |
110 | .col-ms-6 {
111 | width: 50%; }
112 |
113 | .col-ms-7 {
114 | width: 58.33333%; }
115 |
116 | .col-ms-8 {
117 | width: 66.66667%; }
118 |
119 | .col-ms-9 {
120 | width: 75%; }
121 |
122 | .col-ms-10 {
123 | width: 83.33333%; }
124 |
125 | .col-ms-11 {
126 | width: 91.66667%; }
127 |
128 | .col-ms-12 {
129 | width: 100%; }
130 |
131 | .col-ms-push-1 {
132 | left: 8.33333%; }
133 |
134 | .col-ms-push-2 {
135 | left: 16.66667%; }
136 |
137 | .col-ms-push-3 {
138 | left: 25%; }
139 |
140 | .col-ms-push-4 {
141 | left: 33.33333%; }
142 |
143 | .col-ms-push-5 {
144 | left: 41.66667%; }
145 |
146 | .col-ms-push-6 {
147 | left: 50%; }
148 |
149 | .col-ms-push-7 {
150 | left: 58.33333%; }
151 |
152 | .col-ms-push-8 {
153 | left: 66.66667%; }
154 |
155 | .col-ms-push-9 {
156 | left: 75%; }
157 |
158 | .col-ms-push-10 {
159 | left: 83.33333%; }
160 |
161 | .col-ms-push-11 {
162 | left: 91.66667%; }
163 |
164 | .col-ms-pull-1 {
165 | right: 8.33333%; }
166 |
167 | .col-ms-pull-2 {
168 | right: 16.66667%; }
169 |
170 | .col-ms-pull-3 {
171 | right: 25%; }
172 |
173 | .col-ms-pull-4 {
174 | right: 33.33333%; }
175 |
176 | .col-ms-pull-5 {
177 | right: 41.66667%; }
178 |
179 | .col-ms-pull-6 {
180 | right: 50%; }
181 |
182 | .col-ms-pull-7 {
183 | right: 58.33333%; }
184 |
185 | .col-ms-pull-8 {
186 | right: 66.66667%; }
187 |
188 | .col-ms-pull-9 {
189 | right: 75%; }
190 |
191 | .col-ms-pull-10 {
192 | right: 83.33333%; }
193 |
194 | .col-ms-pull-11 {
195 | right: 91.66667%; }
196 |
197 | .col-ms-offset-1 {
198 | margin-left: 8.33333%; }
199 |
200 | .col-ms-offset-2 {
201 | margin-left: 16.66667%; }
202 |
203 | .col-ms-offset-3 {
204 | margin-left: 25%; }
205 |
206 | .col-ms-offset-4 {
207 | margin-left: 33.33333%; }
208 |
209 | .col-ms-offset-5 {
210 | margin-left: 41.66667%; }
211 |
212 | .col-ms-offset-6 {
213 | margin-left: 50%; }
214 |
215 | .col-ms-offset-7 {
216 | margin-left: 58.33333%; }
217 |
218 | .col-ms-offset-8 {
219 | margin-left: 66.66667%; }
220 |
221 | .col-ms-offset-9 {
222 | margin-left: 75%; }
223 |
224 | .col-ms-offset-10 {
225 | margin-left: 83.33333%; }
226 |
227 | .col-ms-offset-11 {
228 | margin-left: 91.66667%; }
229 | }
230 |
231 | @media (min-width: @screen-xs) and (max-width: @screen-xs-max) {
232 | .container {
233 | max-width: @screen-sm;
234 | }
235 |
236 | .form-horizontal .form-group .control-label {text-align:right;}
237 | }
--------------------------------------------------------------------------------
/src/less/elections.less:
--------------------------------------------------------------------------------
1 | .elected {
2 | color: green;
3 | font-weight: bold;
4 | }
5 | .eliminated {
6 | color: red;
7 | text-decoration: line-through;
8 | }
9 | .next-vote {
10 | color: darkred;
11 | font-style: italic;
12 | font-weight: bold;
13 | }
14 | body {
15 | background-color: rgba(255, 255, 255, 0);
16 | }
--------------------------------------------------------------------------------
/src/less/jquery-ui.less:
--------------------------------------------------------------------------------
1 | #sortable {
2 | list-style-type: none;
3 | margin: 0;
4 | padding: 0;
5 | cursor: move;
6 | position: relative;
7 | li {
8 | margin: 0 3px 3px 3px;
9 | padding: 5px 20px 5px 5px;
10 | font-size: 130%;
11 | border: solid 1px #709670;
12 | border-radius: 5px;
13 | background: rgba(245, 245, 245, 0.75);
14 | span {
15 | position: absolute;
16 | margin-left: -1.3em;
17 | }
18 | i {
19 | position: absolute;
20 | right: 5px;
21 | cursor: pointer;
22 | padding: 4px;
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/src/less/main-theme.less:
--------------------------------------------------------------------------------
1 | /* Import the Base Bootstrap Files */
2 | @import "../../lib/bootstrap/less/bootstrap.less";
3 |
4 | // Variables
5 |
6 | @gray-darker: lighten(#000, 13.5%);
7 | @gray-dark: lighten(#000, 20%);
8 | @gray: lighten(#000, 33.5%);
9 | @gray-light: lighten(#000, 60%);
10 | @gray-lighter: lighten(#000, 93.5%);
11 | @gray-lightest: lighten(#000, 97.25%);
12 | @brand-primary: #428bca;
13 | @brand-success: #5cb85c;
14 | @brand-info: #5bc0de;
15 | @brand-warning: #f0ad4e;
16 | @brand-danger: #d9534f;
17 |
18 | // Global Styles
19 |
20 | body {
21 | background-color: @gray-lightest;
22 | }
23 |
24 | // Wrappers
25 |
26 | #wrapper {
27 | width: 100%;
28 | }
29 |
30 | #page-wrapper {
31 | padding: 0 15px;
32 | min-height: 568px;
33 | background-color: white;
34 | }
35 |
36 | @media(min-width:768px) {
37 | #page-wrapper {
38 | position: inherit;
39 | margin: 0 0 0 250px;
40 | padding: 0 30px;
41 | border-left: 1px solid darken(@gray-lightest, 6.5%);
42 | }
43 | }
44 |
45 | // Navigation
46 |
47 | // --Topbar
48 |
49 | .navbar-top-links {
50 | margin-right: 0;
51 | }
52 |
53 | .navbar-top-links li {
54 | display: inline-block;
55 | }
56 |
57 | .navbar-top-links li:last-child {
58 | margin-right: 15px;
59 | }
60 |
61 | .navbar-top-links li a {
62 | padding: 15px;
63 | min-height: 50px;
64 | }
65 |
66 | .navbar-top-links .dropdown-menu li {
67 | display: block;
68 | }
69 |
70 | .navbar-top-links .dropdown-menu li:last-child {
71 | margin-right: 0;
72 | }
73 |
74 | .navbar-top-links .dropdown-menu li a {
75 | padding: 3px 20px;
76 | min-height: 0;
77 | }
78 |
79 | .navbar-top-links .dropdown-menu li a div {
80 | white-space: normal;
81 | }
82 |
83 | .navbar-top-links .dropdown-messages,
84 | .navbar-top-links .dropdown-tasks,
85 | .navbar-top-links .dropdown-alerts {
86 | width: 310px;
87 | min-width: 0;
88 | }
89 |
90 | .navbar-top-links .dropdown-messages {
91 | margin-left: 5px;
92 | }
93 |
94 | .navbar-top-links .dropdown-tasks {
95 | margin-left: -59px;
96 | }
97 |
98 | .navbar-top-links .dropdown-alerts {
99 | margin-left: -123px;
100 | }
101 |
102 | .navbar-top-links .dropdown-user {
103 | right: 0;
104 | left: auto;
105 | }
106 |
107 | // --Sidebar
108 |
109 | .sidebar {
110 | .sidebar-nav.navbar-collapse {
111 | padding-left: 0;
112 | padding-right: 0;
113 | }
114 | }
115 |
116 | .sidebar .sidebar-search {
117 | padding: 15px;
118 | }
119 |
120 | .sidebar ul li {
121 | border-bottom: 1px solid darken(@gray-lightest, 6.5%);
122 | a {
123 | &.active {
124 | background-color: @gray-lighter;
125 | }
126 | }
127 | }
128 |
129 | .sidebar .arrow {
130 | float: right;
131 | }
132 |
133 | .sidebar .fa.arrow:before {
134 | content: "\f104";
135 | }
136 |
137 | .sidebar .active > a > .fa.arrow:before {
138 | content: "\f107";
139 | }
140 |
141 | .sidebar .nav-second-level li,
142 | .sidebar .nav-third-level li {
143 | border-bottom: none !important;
144 | }
145 |
146 | .sidebar .nav-second-level li a {
147 | padding-left: 37px;
148 | }
149 |
150 | .sidebar .nav-third-level li a {
151 | padding-left: 52px;
152 | }
153 |
154 | @media(min-width:768px) {
155 | .sidebar {
156 | z-index: 1;
157 | position: absolute;
158 | width: 250px;
159 | margin-top: 51px;
160 | }
161 |
162 | .navbar-top-links .dropdown-messages,
163 | .navbar-top-links .dropdown-tasks,
164 | .navbar-top-links .dropdown-alerts {
165 | margin-left: auto;
166 | }
167 | }
168 |
169 | // Buttons
170 |
171 | .btn-outline {
172 | color: inherit;
173 | background-color: transparent;
174 | transition: all .5s;
175 | }
176 |
177 | .btn-primary.btn-outline {
178 | color: @brand-primary;
179 | }
180 |
181 | .btn-success.btn-outline {
182 | color: @brand-success;
183 | }
184 |
185 | .btn-info.btn-outline {
186 | color: @brand-info;
187 | }
188 |
189 | .btn-warning.btn-outline {
190 | color: @brand-warning;
191 | }
192 |
193 | .btn-danger.btn-outline {
194 | color: @brand-danger;
195 | }
196 |
197 | .btn-primary.btn-outline:hover,
198 | .btn-success.btn-outline:hover,
199 | .btn-info.btn-outline:hover,
200 | .btn-warning.btn-outline:hover,
201 | .btn-danger.btn-outline:hover {
202 | color: white;
203 | }
204 |
205 | // Chat Widget
206 |
207 | .chat {
208 | margin: 0;
209 | padding: 0;
210 | list-style: none;
211 | }
212 |
213 | .chat li {
214 | margin-bottom: 10px;
215 | padding-bottom: 5px;
216 | border-bottom: 1px dotted @gray-light;
217 | }
218 |
219 | .chat li.left .chat-body {
220 | margin-left: 60px;
221 | }
222 |
223 | .chat li.right .chat-body {
224 | margin-right: 60px;
225 | }
226 |
227 | .chat li .chat-body p {
228 | margin: 0;
229 | }
230 |
231 | .panel .slidedown .glyphicon,
232 | .chat .glyphicon {
233 | margin-right: 5px;
234 | }
235 |
236 | .chat-panel .panel-body {
237 | height: 350px;
238 | overflow-y: scroll;
239 | }
240 |
241 | // Login Page
242 |
243 | .login-panel {
244 | margin-top: 25%;
245 | }
246 |
247 | // Flot Charts Containers
248 |
249 | .flot-chart {
250 | display: block;
251 | height: 400px;
252 | }
253 |
254 | .flot-chart-content {
255 | width: 100%;
256 | height: 100%;
257 | }
258 |
259 | // DataTables Overrides
260 |
261 | table.dataTable thead .sorting,
262 | table.dataTable thead .sorting_asc,
263 | table.dataTable thead .sorting_desc,
264 | table.dataTable thead .sorting_asc_disabled,
265 | table.dataTable thead .sorting_desc_disabled {
266 | background: transparent;
267 | }
268 |
269 | table.dataTable thead .sorting_asc:after {
270 | content: "\f0de";
271 | float: right;
272 | font-family: fontawesome;
273 | }
274 |
275 | table.dataTable thead .sorting_desc:after {
276 | content: "\f0dd";
277 | float: right;
278 | font-family: fontawesome;
279 | }
280 |
281 | table.dataTable thead .sorting:after {
282 | content: "\f0dc";
283 | float: right;
284 | font-family: fontawesome;
285 | color: rgba(50,50,50,.5);
286 | }
287 |
288 | // Circle Buttons
289 |
290 | .btn-circle {
291 | width: 30px;
292 | height: 30px;
293 | padding: 6px 0;
294 | border-radius: 15px;
295 | text-align: center;
296 | font-size: 12px;
297 | line-height: 1.428571429;
298 | }
299 |
300 | .btn-circle.btn-lg {
301 | width: 50px;
302 | height: 50px;
303 | padding: 10px 16px;
304 | border-radius: 25px;
305 | font-size: 18px;
306 | line-height: 1.33;
307 | }
308 |
309 | .btn-circle.btn-xl {
310 | width: 70px;
311 | height: 70px;
312 | padding: 10px 16px;
313 | border-radius: 35px;
314 | font-size: 24px;
315 | line-height: 1.33;
316 | }
317 |
318 | // Grid Demo Elements
319 |
320 | .show-grid [class^="col-"] {
321 | padding-top: 10px;
322 | padding-bottom: 10px;
323 | border: 1px solid #ddd;
324 | background-color: #eee !important;
325 | }
326 |
327 | .show-grid {
328 | margin: 15px 0;
329 | }
330 |
331 | // Custom Colored Panels
332 |
333 | .huge {
334 | font-size: 40px;
335 | }
336 |
337 | .panel-green {
338 | border-color: @brand-success;
339 | .panel-heading {
340 | border-color: @brand-success;
341 | color: white;
342 | background-color: @brand-success;
343 | }
344 | a {
345 | color: @brand-success;
346 | &:hover {
347 | color: darken(@brand-success, 15%);
348 | }
349 | }
350 | }
351 |
352 | .panel-red {
353 | border-color: @brand-danger;
354 | .panel-heading {
355 | border-color: @brand-danger;
356 | color: white;
357 | background-color: @brand-danger;
358 | }
359 | a {
360 | color: @brand-danger;
361 | &:hover {
362 | color: darken(@brand-danger, 15%);
363 | }
364 | }
365 | }
366 |
367 | .panel-yellow {
368 | border-color: @brand-warning;
369 | .panel-heading {
370 | border-color: @brand-warning;
371 | color: white;
372 | background-color: @brand-warning;
373 | }
374 | a {
375 | color: @brand-warning;
376 | &:hover {
377 | color: darken(@brand-warning, 15%);
378 | }
379 | }
380 | }
--------------------------------------------------------------------------------
/src/less/pages.less:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #ebebeb !important;
3 | }
4 |
5 | .navbar {
6 | background-color: #553285;
7 | }
8 |
9 | .fluid-video-wrapper {
10 | padding: 56.25% 0 0;
11 | width: 100%;
12 | position: relative;
13 | iframe {
14 | position: absolute;
15 | top: 0;
16 | left: 0;
17 | width: 100%;
18 | height: 100%;
19 | }
20 | }
21 |
22 | .vote-button {
23 | max-height: 300px;
24 | margin: auto;
25 | }
26 |
27 | table.ballots {
28 | .fa {
29 | padding: 3px;
30 | color: inherit;
31 | &:hover {
32 | color: inherit;
33 | text-decoration: inherit;
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------