├── .gitattributes ├── .gitignore ├── README.md ├── client ├── .bowerrc ├── .editorconfig ├── .jshintrc ├── .travis.yml ├── .yo-rc.json ├── Gruntfile.js ├── app │ ├── .buildignore │ ├── 404.html │ ├── images │ │ └── yeoman.png │ ├── index.html │ ├── robots.txt │ ├── scripts │ │ ├── app.js │ │ └── controllers │ │ │ ├── main.js │ │ │ ├── results.js │ │ │ └── vote.js │ ├── styles │ │ └── main.scss │ └── views │ │ ├── main.html │ │ ├── results.html │ │ └── vote.html ├── bower.json ├── package.json └── test │ ├── .jshintrc │ ├── karma.conf.js │ └── spec │ └── controllers │ └── main.js ├── img ├── material1.png ├── material2.png └── material3.png ├── license └── server ├── app └── models │ └── poll.js ├── config.js.example ├── package.json └── server.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .tmp 4 | .sass-cache 5 | bower_components 6 | server/config.js 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Material Poll 2 | ### This is a open source implementation of the strawpoll.me site. [DEMO](http://materialpoll.tk) 3 | * Server built with Node, Express, Mongo and Socket.io 4 | * Client built with Yo Angular and materializecss 5 | 6 | #####Notes: if deploying to server make sure to add rewrites for html5Mode or change url within app to add hash 7 |
 8 | NGINX
 9 |   server {
10 |     server_name my-app;
11 | 
12 |     root /path/to/app;
13 | 
14 |     location / {
15 |         try_files $uri $uri/ /index.html;
16 |     }
17 | }
18 | 19 | #### Make sure to rename config.js.example to config.js with your mongodb connection string. 20 | 21 | ##### Create Poll 22 | ![Material Poll](img/material1.png?raw=true "Material Poll") 23 | ##### Vote on Poll 24 | ![Material Poll](img/material2.png?raw=true "Material Poll") 25 | ##### See Results 26 | ![Material Poll](img/material3.png?raw=true "Material Poll") 27 | -------------------------------------------------------------------------------- /client/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /client/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /client/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "eqnull": true, 11 | "indent": 2, 12 | "latedef": true, 13 | "newcap": true, 14 | "noarg": true, 15 | "quotmark": "single", 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "angular": false, 23 | "$": false, 24 | "io": false, 25 | "Chart": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /client/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 'iojs' 5 | - '0.12' 6 | - '0.10' 7 | before_script: 8 | - 'npm install -g bower grunt-cli' 9 | - 'bower install' 10 | -------------------------------------------------------------------------------- /client/.yo-rc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /client/Gruntfile.js: -------------------------------------------------------------------------------- 1 | // Generated on 2015-06-16 using generator-angular 0.11.1 2 | 'use strict'; 3 | 4 | // # Globbing 5 | // for performance reasons we're only matching one level down: 6 | // 'test/spec/{,*/}*.js' 7 | // use this if you want to recursively match all subfolders: 8 | // 'test/spec/**/*.js' 9 | 10 | module.exports = function (grunt) { 11 | 12 | // Load grunt tasks automatically 13 | require('load-grunt-tasks')(grunt); 14 | 15 | // Time how long tasks take. Can help when optimizing build times 16 | require('time-grunt')(grunt); 17 | 18 | // Configurable paths for the application 19 | var appConfig = { 20 | app: require('./bower.json').appPath || 'app', 21 | dist: 'dist' 22 | }; 23 | 24 | // Define the configuration for all the tasks 25 | grunt.initConfig({ 26 | 27 | // Project settings 28 | yeoman: appConfig, 29 | 30 | // Watches files for changes and runs tasks based on the changed files 31 | watch: { 32 | bower: { 33 | files: ['bower.json'], 34 | tasks: ['wiredep'] 35 | }, 36 | js: { 37 | files: ['<%= yeoman.app %>/scripts/{,*/}*.js'], 38 | tasks: ['newer:jshint:all'], 39 | options: { 40 | livereload: '<%= connect.options.livereload %>' 41 | } 42 | }, 43 | jsTest: { 44 | files: ['test/spec/{,*/}*.js'], 45 | tasks: ['newer:jshint:test', 'karma'] 46 | }, 47 | compass: { 48 | files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], 49 | tasks: ['compass:server', 'autoprefixer'] 50 | }, 51 | gruntfile: { 52 | files: ['Gruntfile.js'] 53 | }, 54 | livereload: { 55 | options: { 56 | livereload: '<%= connect.options.livereload %>' 57 | }, 58 | files: [ 59 | '<%= yeoman.app %>/{,*/}*.html', 60 | '.tmp/styles/{,*/}*.css', 61 | '<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' 62 | ] 63 | } 64 | }, 65 | 66 | // The actual grunt server settings 67 | connect: { 68 | options: { 69 | port: 9000, 70 | // Change this to '0.0.0.0' to access the server from outside. 71 | hostname: '0.0.0.0', 72 | livereload: 35729 73 | }, 74 | livereload: { 75 | options: { 76 | open: true, 77 | middleware: function (connect) { 78 | return [ 79 | connect.static('.tmp'), 80 | connect().use( 81 | '/bower_components', 82 | connect.static('./bower_components') 83 | ), 84 | connect().use( 85 | '/app/styles', 86 | connect.static('./app/styles') 87 | ), 88 | connect.static(appConfig.app) 89 | ]; 90 | } 91 | } 92 | }, 93 | test: { 94 | options: { 95 | port: 9001, 96 | middleware: function (connect) { 97 | return [ 98 | connect.static('.tmp'), 99 | connect.static('test'), 100 | connect().use( 101 | '/bower_components', 102 | connect.static('./bower_components') 103 | ), 104 | connect.static(appConfig.app) 105 | ]; 106 | } 107 | } 108 | }, 109 | dist: { 110 | options: { 111 | open: true, 112 | base: '<%= yeoman.dist %>' 113 | } 114 | } 115 | }, 116 | 117 | // Make sure code styles are up to par and there are no obvious mistakes 118 | jshint: { 119 | options: { 120 | jshintrc: '.jshintrc', 121 | reporter: require('jshint-stylish') 122 | }, 123 | all: { 124 | src: [ 125 | 'Gruntfile.js', 126 | '<%= yeoman.app %>/scripts/{,*/}*.js' 127 | ] 128 | }, 129 | test: { 130 | options: { 131 | jshintrc: 'test/.jshintrc' 132 | }, 133 | src: ['test/spec/{,*/}*.js'] 134 | } 135 | }, 136 | 137 | // Empties folders to start fresh 138 | clean: { 139 | dist: { 140 | files: [{ 141 | dot: true, 142 | src: [ 143 | '.tmp', 144 | '<%= yeoman.dist %>/{,*/}*', 145 | '!<%= yeoman.dist %>/.git{,*/}*' 146 | ] 147 | }] 148 | }, 149 | server: '.tmp' 150 | }, 151 | 152 | // Add vendor prefixed styles 153 | autoprefixer: { 154 | options: { 155 | browsers: ['last 1 version'] 156 | }, 157 | server: { 158 | options: { 159 | map: true, 160 | }, 161 | files: [{ 162 | expand: true, 163 | cwd: '.tmp/styles/', 164 | src: '{,*/}*.css', 165 | dest: '.tmp/styles/' 166 | }] 167 | }, 168 | dist: { 169 | files: [{ 170 | expand: true, 171 | cwd: '.tmp/styles/', 172 | src: '{,*/}*.css', 173 | dest: '.tmp/styles/' 174 | }] 175 | } 176 | }, 177 | 178 | // Automatically inject Bower components into the app 179 | wiredep: { 180 | app: { 181 | src: ['<%= yeoman.app %>/index.html'], 182 | ignorePath: /\.\.\// 183 | }, 184 | test: { 185 | devDependencies: true, 186 | src: '<%= karma.unit.configFile %>', 187 | ignorePath: /\.\.\//, 188 | fileTypes:{ 189 | js: { 190 | block: /(([\s\t]*)\/{2}\s*?bower:\s*?(\S*))(\n|\r|.)*?(\/{2}\s*endbower)/gi, 191 | detect: { 192 | js: /'(.*\.js)'/gi 193 | }, 194 | replace: { 195 | js: '\'{{filePath}}\',' 196 | } 197 | } 198 | } 199 | }, 200 | sass: { 201 | src: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'], 202 | ignorePath: /(\.\.\/){1,2}bower_components\// 203 | } 204 | }, 205 | 206 | // Compiles Sass to CSS and generates necessary files if requested 207 | compass: { 208 | options: { 209 | sassDir: '<%= yeoman.app %>/styles', 210 | cssDir: '.tmp/styles', 211 | generatedImagesDir: '.tmp/images/generated', 212 | imagesDir: '<%= yeoman.app %>/images', 213 | javascriptsDir: '<%= yeoman.app %>/scripts', 214 | fontsDir: '<%= yeoman.app %>/styles/fonts', 215 | importPath: './bower_components', 216 | httpImagesPath: '/images', 217 | httpGeneratedImagesPath: '/images/generated', 218 | httpFontsPath: '/styles/fonts', 219 | relativeAssets: false, 220 | assetCacheBuster: false, 221 | raw: 'Sass::Script::Number.precision = 10\n' 222 | }, 223 | dist: { 224 | options: { 225 | generatedImagesDir: '<%= yeoman.dist %>/images/generated' 226 | } 227 | }, 228 | server: { 229 | options: { 230 | sourcemap: true 231 | } 232 | } 233 | }, 234 | 235 | // Renames files for browser caching purposes 236 | filerev: { 237 | dist: { 238 | src: [ 239 | '<%= yeoman.dist %>/scripts/{,*/}*.js', 240 | '<%= yeoman.dist %>/styles/{,*/}*.css', 241 | '<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', 242 | '<%= yeoman.dist %>/styles/fonts/*' 243 | ] 244 | } 245 | }, 246 | 247 | // Reads HTML for usemin blocks to enable smart builds that automatically 248 | // concat, minify and revision files. Creates configurations in memory so 249 | // additional tasks can operate on them 250 | useminPrepare: { 251 | html: '<%= yeoman.app %>/index.html', 252 | options: { 253 | dest: '<%= yeoman.dist %>', 254 | flow: { 255 | html: { 256 | steps: { 257 | js: ['concat', 'uglifyjs'], 258 | css: ['cssmin'] 259 | }, 260 | post: {} 261 | } 262 | } 263 | } 264 | }, 265 | 266 | // Performs rewrites based on filerev and the useminPrepare configuration 267 | usemin: { 268 | html: ['<%= yeoman.dist %>/{,*/}*.html'], 269 | css: ['<%= yeoman.dist %>/styles/{,*/}*.css'], 270 | options: { 271 | assetsDirs: [ 272 | '<%= yeoman.dist %>', 273 | '<%= yeoman.dist %>/images', 274 | '<%= yeoman.dist %>/styles' 275 | ] 276 | } 277 | }, 278 | cssmin: { 279 | minify: { 280 | src: '.tmp/styles/main.css', 281 | dest: 'dist/styles/main.css' 282 | } 283 | }, 284 | 285 | // The following *-min tasks will produce minified files in the dist folder 286 | // By default, your `index.html`'s will take care of 287 | // minification. These next options are pre-configured if you do not wish 288 | // to use the Usemin blocks. 289 | // cssmin: { 290 | // dist: { 291 | // files: { 292 | // '<%= yeoman.dist %>/styles/main.css': [ 293 | // '.tmp/styles/{,*/}*.css' 294 | // ] 295 | // } 296 | // } 297 | // }, 298 | // uglify: { 299 | // dist: { 300 | // files: { 301 | // '<%= yeoman.dist %>/scripts/scripts.js': [ 302 | // '<%= yeoman.dist %>/scripts/scripts.js' 303 | // ] 304 | // } 305 | // } 306 | // }, 307 | // concat: { 308 | // dist: {} 309 | // }, 310 | 311 | imagemin: { 312 | dist: { 313 | files: [{ 314 | expand: true, 315 | cwd: '<%= yeoman.app %>/images', 316 | src: '{,*/}*.{png,jpg,jpeg,gif}', 317 | dest: '<%= yeoman.dist %>/images' 318 | }] 319 | } 320 | }, 321 | 322 | svgmin: { 323 | dist: { 324 | files: [{ 325 | expand: true, 326 | cwd: '<%= yeoman.app %>/images', 327 | src: '{,*/}*.svg', 328 | dest: '<%= yeoman.dist %>/images' 329 | }] 330 | } 331 | }, 332 | 333 | htmlmin: { 334 | dist: { 335 | options: { 336 | collapseWhitespace: true, 337 | conservativeCollapse: true, 338 | collapseBooleanAttributes: true, 339 | removeCommentsFromCDATA: true, 340 | removeOptionalTags: true 341 | }, 342 | files: [{ 343 | expand: true, 344 | cwd: '<%= yeoman.dist %>', 345 | src: ['*.html', 'views/{,*/}*.html'], 346 | dest: '<%= yeoman.dist %>' 347 | }] 348 | } 349 | }, 350 | 351 | // ng-annotate tries to make the code safe for minification automatically 352 | // by using the Angular long form for dependency injection. 353 | ngAnnotate: { 354 | dist: { 355 | files: [{ 356 | expand: true, 357 | cwd: '.tmp/concat/scripts', 358 | src: '*.js', 359 | dest: '.tmp/concat/scripts' 360 | }] 361 | } 362 | }, 363 | 364 | // Replace Google CDN references 365 | cdnify: { 366 | dist: { 367 | html: ['<%= yeoman.dist %>/*.html'] 368 | } 369 | }, 370 | 371 | // Copies remaining files to places other tasks can use 372 | copy: { 373 | dist: { 374 | files: [{ 375 | expand: true, 376 | dot: true, 377 | cwd: '<%= yeoman.app %>', 378 | dest: '<%= yeoman.dist %>', 379 | src: [ 380 | '*.{ico,png,txt}', 381 | '.htaccess', 382 | '*.html', 383 | 'views/{,*/}*.html', 384 | 'images/{,*/}*.{webp}', 385 | 'styles/fonts/{,*/}*.*' 386 | ] 387 | }, { 388 | expand: true, 389 | cwd: '.tmp/images', 390 | dest: '<%= yeoman.dist %>/images', 391 | src: ['generated/*'] 392 | }, { 393 | expand: true, 394 | cwd: '.', 395 | src: 'bower_components/bootstrap-sass-official/assets/fonts/bootstrap/*', 396 | dest: '<%= yeoman.dist %>' 397 | }] 398 | }, 399 | styles: { 400 | expand: true, 401 | cwd: '<%= yeoman.app %>/styles', 402 | dest: '.tmp/styles/', 403 | src: '{,*/}*.css' 404 | } 405 | }, 406 | 407 | // Test settings 408 | karma: { 409 | unit: { 410 | configFile: 'test/karma.conf.js', 411 | singleRun: true 412 | } 413 | } 414 | }); 415 | 416 | 417 | grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { 418 | if (target === 'dist') { 419 | return grunt.task.run(['build', 'connect:dist:keepalive']); 420 | } 421 | 422 | grunt.task.run([ 423 | 'clean:server', 424 | 'wiredep', 425 | 'compass:server', 426 | 'autoprefixer:server', 427 | 'connect:livereload', 428 | 'watch' 429 | ]); 430 | }); 431 | 432 | grunt.registerTask('server', 'DEPRECATED TASK. Use the "serve" task instead', function (target) { 433 | grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.'); 434 | grunt.task.run(['serve:' + target]); 435 | }); 436 | 437 | grunt.registerTask('test', [ 438 | 'clean:server', 439 | 'wiredep', 440 | 'compass', 441 | 'autoprefixer', 442 | 'connect:test', 443 | 'karma' 444 | ]); 445 | 446 | grunt.registerTask('build', [ 447 | 'clean:dist', 448 | 'wiredep', 449 | 'useminPrepare', 450 | 'compass:dist', 451 | 'imagemin', 452 | 'svgmin', 453 | 'autoprefixer', 454 | 'concat', 455 | 'ngAnnotate', 456 | 'copy:dist', 457 | 'cdnify', 458 | 'cssmin', 459 | 'uglify', 460 | 'filerev', 461 | 'usemin', 462 | 'htmlmin' 463 | ]); 464 | 465 | grunt.registerTask('default', [ 466 | 'newer:jshint', 467 | 'test', 468 | 'build' 469 | ]); 470 | }; 471 | -------------------------------------------------------------------------------- /client/app/.buildignore: -------------------------------------------------------------------------------- 1 | *.coffee -------------------------------------------------------------------------------- /client/app/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
144 |

Not found :(

145 |

Sorry, but the page you were trying to view does not exist.

146 |

It looks like this was the result of either:

147 | 151 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /client/app/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearbycoder/materialpoll/269b52f9955ea07d7724becec78d730be243adec/client/app/images/yeoman.png -------------------------------------------------------------------------------- /client/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 | Fork me on GitHub 20 |
21 |
22 |
23 | 30 |
31 |
32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /client/app/robots.txt: -------------------------------------------------------------------------------- 1 | # robotstxt.org 2 | 3 | User-agent: * 4 | -------------------------------------------------------------------------------- /client/app/scripts/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc overview 5 | * @name materialPollApp 6 | * @description 7 | * # materialPollApp 8 | * 9 | * Main module of the application. 10 | */ 11 | angular 12 | .module('materialPollApp', [ 13 | 'ngAnimate', 14 | 'ui.router', 15 | 'ngSanitize', 16 | 'ngTouch' 17 | ]) 18 | .config(function ($stateProvider, $urlRouterProvider, $locationProvider) { 19 | $locationProvider.html5Mode(true); 20 | $urlRouterProvider.otherwise('/'); 21 | 22 | $stateProvider 23 | .state('/', { 24 | url: '/', 25 | templateUrl: 'views/main.html', 26 | controller: 'MainCtrl' 27 | }) 28 | .state('/:id', { 29 | url: '/:id', 30 | templateUrl: 'views/vote.html', 31 | controller: 'VoteCtrl' 32 | }) 33 | .state('/:id/v', { 34 | url: '/:id/v', 35 | templateUrl: 'views/results.html', 36 | controller: 'ResultsCtrl' 37 | }); 38 | }) 39 | .constant('myConfig', { 40 | 'backend': 'http://45.55.31.147:9090/api/poll/', 41 | 'version': 0.3 42 | }); 43 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name materialPollApp.controller:MainCtrl 6 | * @description 7 | * # MainCtrl 8 | * Controller of the materialPollApp 9 | */ 10 | angular.module('materialPollApp') 11 | .controller('MainCtrl', function ($scope, $http, myConfig) { 12 | $scope.options = [{},{},{}]; 13 | //submit poll 14 | $scope.submit = function() { 15 | var votes = []; 16 | var added = false; 17 | for(var x = 0; x < $scope.options.length; x++){ 18 | votes.push(0); 19 | if($scope.options[x].name !== undefined && $scope.title !== undefined){ 20 | added = true; 21 | } 22 | } 23 | if(added !== false){ 24 | $('.submitButton').hide(); 25 | $http.post(myConfig.backend,{answers: $scope.options, name: $scope.title, multiple: $scope.multiple, singleIP: $scope.singleIP, votes: votes}) 26 | .success(function(data){ 27 | $scope.linkto = data[1].poll.id; 28 | $scope.location = window.location.protocol + window.location.host + '/' + $scope.linkto; 29 | }); 30 | } 31 | }; 32 | //add new poll option automatically if focused on the next to last option and other fields are filled 33 | $scope.addOption = function() { 34 | var addOpt = false; 35 | for(var x = 0; x < $scope.options.length - 1; x++){ 36 | if($scope.options[x].name !== '' && $scope.options[x].name != null){ 37 | addOpt = true; 38 | }else { 39 | addOpt = false; 40 | } 41 | } 42 | if(addOpt === true){ 43 | $scope.options.push({}); 44 | } 45 | }; 46 | }); 47 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/results.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name materialPollApp.controller:MainCtrl 6 | * @description 7 | * # ResultsCtrl 8 | * Controller of the materialPollApp 9 | */ 10 | angular.module('materialPollApp') 11 | .controller('ResultsCtrl', function ($scope, $stateParams, $http, myConfig) { 12 | $scope.options = []; 13 | $scope.options.votes = []; 14 | $http.get(myConfig.backend + $stateParams.id) 15 | .success(function(data){ 16 | $scope.title = data.name; 17 | $scope.multiple = data.multiple || false; 18 | var totalvotes = 0; 19 | $.each(data.votes,function() { 20 | totalvotes += this; 21 | }); 22 | //setup initial votes based on data from db 23 | $scope.pieData = []; 24 | var colors = ['#7e57c2','#ec407a', '#42a5f5', '#66bb6a', '#d4e157', '#ff7043', '#78909c', '#ffa726', '#9ccc65', '#26a69a']; 25 | var highlight = ['#b39ddb','#f48fb1', '#90caf9', '#a5d6a7', '#e6ee9c', '#ffab91', '#b0bec5', '#ffcc80', '#c5e1a5', '#80cbc4']; 26 | for(var x = 0; x < data.answers.length; x++){ 27 | if(data.answers[x]){ 28 | data.answers[x].checked = false; 29 | $scope.options.push({name:data.answers[x].name, color : colors[x], highlight: highlight[x], checked : data.answers[x].checked, votes : data.votes[x], percent : data.votes[x] / totalvotes * 100 + '%'}); 30 | $scope.pieData.push({value: data.votes[x], color: colors[x], highlight: highlight[x], label: data.answers[x].name}); 31 | } 32 | 33 | } 34 | 35 | Chart.defaults.global.responsive = true; 36 | var ctx = $('#pieChart').get(0).getContext('2d'); 37 | // This will get the first returned node in the jQuery collection. 38 | var pieChart = new Chart(ctx).Pie($scope.pieData, { 39 | segmentShowStroke : false, 40 | segmentStrokeColor : '#fff', 41 | segmentStrokeWidth : 0, 42 | percentageInnerCutout : 0, 43 | animationSteps : 100, 44 | animationEasing : 'easeOutBounce', 45 | animateRotate : true, 46 | animateScale : false, 47 | legendTemplate : '' 48 | }); 49 | //setup socket connection for realtime connection to poll 50 | var socket = io.connect('http://45.55.31.147:9090', {'sync disconnect on unload':true}); 51 | socket.on('chat' + $stateParams.id, function(dat){ 52 | var socketvotes = 0; 53 | $.each(dat.votes,function() { 54 | socketvotes += this; 55 | }); 56 | for(var y = 0; y < dat.answers.length; y++){ 57 | if($scope.options[y]){ 58 | $scope.options[y].votes = dat.votes[y]; 59 | $scope.options[y].percent = dat.votes[y] / socketvotes * 100 + '%'; 60 | pieChart.segments[y].value = dat.votes[y]; 61 | pieChart.update(); 62 | } 63 | } 64 | $scope.$apply(); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /client/app/scripts/controllers/vote.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @ngdoc function 5 | * @name materialPollApp.controller:MainCtrl 6 | * @description 7 | * # VoteCtrl 8 | * Controller of the materialPollApp 9 | */ 10 | angular.module('materialPollApp') 11 | .controller('VoteCtrl', function ($scope, $state,$stateParams, $http, myConfig) { 12 | $http.get(myConfig.backend + $stateParams.id) 13 | .success(function(data){ 14 | $scope.title = data.name; 15 | $scope.multiple = data.multiple || false; 16 | $scope.options = []; 17 | for(var x = 0; x < data.answers.length; x++){ 18 | if(data.answers[x]){ 19 | data.answers[x].checked = false; 20 | $scope.options.push({name:data.answers[x].name, checked : data.answers[x].checked }); 21 | } 22 | } 23 | }); 24 | //check if at least one box was checked before allowing $http put 25 | $scope.vote = function(options) { 26 | var votes = []; 27 | var checked = false; 28 | angular.forEach(options, function(option) { 29 | if(option.checked === true){ 30 | checked = true; 31 | votes.push(1); 32 | }else{ 33 | votes.push(0); 34 | } 35 | }); 36 | if(checked === true){ 37 | $http.put(myConfig.backend + $stateParams.id,{votes : votes}) 38 | .success(function(){ 39 | $state.go('/:id/v',{id: $stateParams.id}); 40 | }); 41 | } 42 | }; 43 | //check if allowing multiple selections, if not reset all other checkboxes 44 | $scope.updateSelection = function(position, options) { 45 | if($scope.multiple === false){ 46 | angular.forEach(options, function(option, index) { 47 | if (position !== index) { 48 | option.checked = false; 49 | } 50 | }); 51 | } 52 | }; 53 | }); 54 | -------------------------------------------------------------------------------- /client/app/styles/main.scss: -------------------------------------------------------------------------------- 1 | 2 | // bower:scss 3 | // endbower 4 | #mainquestion { 5 | font-size: 30px; 6 | } 7 | #mainq { 8 | font-size:20px; 9 | } 10 | .mainoptions { 11 | font-size:20px; 12 | margin:0 5px; 13 | padding:0 0 15px 0; 14 | } 15 | .card { 16 | padding-bottom: 20px; 17 | } 18 | .progress { 19 | height:30px; 20 | } 21 | .determinate { 22 | height:30px; 23 | } 24 | .question { 25 | font-size:20px; 26 | position:absolute; 27 | left:10px; 28 | z-index:100; 29 | width: 100px; 30 | } 31 | #mainquestionlabel { 32 | font-size: 20px; 33 | } 34 | .card-action { 35 | margin-left:25px; 36 | margin-right:25px; 37 | } 38 | .questions { 39 | font-size:20px; 40 | margin:10px; 41 | } 42 | #pieChart { 43 | padding: 20px; 44 | } 45 | @media only screen and (max-device-width: 480px) { 46 | body { 47 | font-size: 14px; 48 | } 49 | nav .brand-logo { 50 | font-size: 18px; 51 | } 52 | #mainquestion { 53 | font-size: 15px!important; 54 | } 55 | .questions { 56 | font-size: 15px!important; 57 | } 58 | #mainquestionlabel { 59 | font-size: 15px!important; 60 | } 61 | .hide-small{ 62 | display:none; 63 | } 64 | } 65 | body { 66 | display: flex; 67 | min-height: 100vh; 68 | flex-direction: column; 69 | } 70 | .ribbon { 71 | background-color: #E17475; 72 | overflow: hidden; 73 | white-space: nowrap; 74 | /* top left corner */ 75 | position: absolute; 76 | right: -50px; 77 | top: 40px; 78 | z-index: 300; 79 | /* 45 deg ccw rotation */ 80 | -webkit-transform: rotate(45deg); 81 | -moz-transform: rotate(45deg); 82 | -ms-transform: rotate(45deg); 83 | -o-transform: rotate(45deg); 84 | transform: rotate(45deg); 85 | /* shadow */ 86 | -webkit-box-shadow: 0 0 10px #E4F2FD; 87 | -moz-box-shadow: 0 0 10px #E4F2FD; 88 | box-shadow: 0 0 10px #E4F2FD; 89 | } 90 | .ribbon a { 91 | border: 1px solid #faa; 92 | color: #fff; 93 | display: block; 94 | font: bold 81.25% 'Helvetica Neue', Helvetica, Arial, sans-serif; 95 | margin: 1px 0; 96 | padding: 10px 50px; 97 | text-align: center; 98 | text-decoration: none; 99 | /* shadow */ 100 | text-shadow: 0 0 5px #444; 101 | } 102 | 103 | 104 | main { 105 | flex: 1 0 auto; 106 | } 107 | 108 | /* label color */ 109 | .input-field label { 110 | color: #e3f2fd; 111 | } 112 | /* label focus color */ 113 | .input-field input[type=text]:focus + label { 114 | color: #e3f2fd; 115 | } 116 | /* label underline focus color */ 117 | .input-field input[type=text]:focus { 118 | border-bottom: 1px solid #e3f2fd; 119 | box-shadow: 0 1px 0 0 #e3f2fd; 120 | } 121 | /* valid color */ 122 | .input-field input[type=text].valid { 123 | border-bottom: 1px solid #e3f2fd; 124 | box-shadow: 0 1px 0 0 #e3f2fd; 125 | } 126 | /* invalid color */ 127 | .input-field input[type=text].invalid { 128 | border-bottom: 1px solid #e3f2fd; 129 | box-shadow: 0 1px 0 0 #e3f2fd; 130 | } 131 | /* icon prefix focus color */ 132 | .input-field .prefix.active { 133 | color: #e3f2fd; 134 | } 135 | 136 | [type="checkbox"]:checked+label:before { 137 | border-right: 2px solid #e3f2fd; 138 | border-bottom: 2px solid #e3f2fd; 139 | } 140 | .card { 141 | margin-top: 20px; 142 | } 143 | 144 | .card .card-action { 145 | border-top: 0px solid #e3f2fd; 146 | padding-bottom: 15px; 147 | padding-top: 0px; 148 | } 149 | .card .card-content { 150 | padding-bottom: 0px; 151 | } -------------------------------------------------------------------------------- /client/app/views/main.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 | 7 | 8 |
9 |
10 | 11 | 12 |
13 |
14 |
15 |
16 |

17 | 18 | 19 |

20 |

21 | 22 | 23 |

24 | Create Poll 25 | View Poll 26 |

Url: {{location}}

27 |
28 |
29 |
30 |
-------------------------------------------------------------------------------- /client/app/views/results.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

{{title}}

7 |
8 |
9 |
10 |

11 | 12 |

13 |
14 |

Votes: {{option.votes}}

15 |
16 |
17 |

18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 |
26 |
27 |
-------------------------------------------------------------------------------- /client/app/views/vote.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

{{title}}

7 |
8 |
9 |

10 | 11 | 12 |

13 |
14 |
15 |
16 |
17 | Submit Vote 18 |
19 |
20 |
21 |
-------------------------------------------------------------------------------- /client/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-poll", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "angular": "^1.3.0", 6 | "angular-animate": "^1.3.0", 7 | "angular-cookies": "^1.3.0", 8 | "angular-resource": "^1.3.0", 9 | "angular-route": "^1.3.0", 10 | "angular-sanitize": "^1.3.0", 11 | "angular-touch": "^1.3.0", 12 | "angular-ui-router": "~0.2.15", 13 | "materialize": "~0.96.1", 14 | "Chart.js": "~1.0.2" 15 | }, 16 | "devDependencies": { 17 | "angular-mocks": "^1.3.0", 18 | "angular-ui-router": "~0.2.15" 19 | }, 20 | "appPath": "app", 21 | "moduleName": "materialPollApp" 22 | } 23 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "materialpoll", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "es5-shim": "^4.1.7" 6 | }, 7 | "repository": {}, 8 | "devDependencies": { 9 | "grunt": "^0.4.5", 10 | "grunt-autoprefixer": "^2.0.0", 11 | "grunt-concurrent": "^1.0.0", 12 | "grunt-contrib-clean": "^0.6.0", 13 | "grunt-contrib-compass": "^1.0.0", 14 | "grunt-contrib-concat": "^0.5.0", 15 | "grunt-contrib-connect": "^0.9.0", 16 | "grunt-contrib-copy": "^0.7.0", 17 | "grunt-contrib-cssmin": "^0.12.0", 18 | "grunt-contrib-htmlmin": "^0.4.0", 19 | "grunt-contrib-imagemin": "^0.9.2", 20 | "grunt-contrib-jshint": "^0.11.0", 21 | "grunt-contrib-uglify": "^0.7.0", 22 | "grunt-contrib-watch": "^0.6.1", 23 | "grunt-filerev": "^2.1.2", 24 | "grunt-google-cdn": "^0.4.3", 25 | "grunt-karma": "*", 26 | "grunt-newer": "^1.1.0", 27 | "grunt-ng-annotate": "^0.9.2", 28 | "grunt-svgmin": "^2.0.0", 29 | "grunt-usemin": "^3.0.0", 30 | "grunt-wiredep": "^2.0.0", 31 | "jshint-stylish": "^1.0.0", 32 | "karma-jasmine": "*", 33 | "karma-phantomjs-launcher": "*", 34 | "load-grunt-tasks": "^3.1.0", 35 | "time-grunt": "^1.0.0" 36 | }, 37 | "engines": { 38 | "node": ">=0.10.0" 39 | }, 40 | "scripts": { 41 | "test": "grunt test" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /client/test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "jasmine": true, 22 | "globals": { 23 | "angular": false, 24 | "browser": false, 25 | "inject": false 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /client/test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.12/config/configuration-file.html 3 | // Generated on 2015-06-16 using 4 | // generator-karma 1.0.0 5 | 6 | module.exports = function(config) { 7 | 'use strict'; 8 | 9 | config.set({ 10 | // enable / disable watching file and executing tests whenever any file changes 11 | autoWatch: true, 12 | 13 | // base path, that will be used to resolve files and exclude 14 | basePath: '../', 15 | 16 | // testing framework to use (jasmine/mocha/qunit/...) 17 | // as well as any additional frameworks (requirejs/chai/sinon/...) 18 | frameworks: [ 19 | "jasmine" 20 | ], 21 | 22 | // list of files / patterns to load in the browser 23 | files: [ 24 | 'node_modules/es5-shim/es5-shim.js', 25 | // bower:js 26 | 'bower_components/jquery/dist/jquery.js', 27 | 'bower_components/angular/angular.js', 28 | 'bower_components/angular-animate/angular-animate.js', 29 | 'bower_components/angular-cookies/angular-cookies.js', 30 | 'bower_components/angular-resource/angular-resource.js', 31 | 'bower_components/angular-route/angular-route.js', 32 | 'bower_components/angular-sanitize/angular-sanitize.js', 33 | 'bower_components/angular-touch/angular-touch.js', 34 | 'bower_components/angular-ui-router/release/angular-ui-router.js', 35 | 'bower_components/materialize/bin/materialize.js', 36 | 'bower_components/Chart.js/Chart.js', 37 | 'bower_components/angular-mocks/angular-mocks.js', 38 | // endbower 39 | "app/scripts/**/*.js", 40 | "test/mock/**/*.js", 41 | "test/spec/**/*.js" 42 | ], 43 | 44 | // list of files / patterns to exclude 45 | exclude: [ 46 | ], 47 | 48 | // web server port 49 | port: 8080, 50 | 51 | // Start these browsers, currently available: 52 | // - Chrome 53 | // - ChromeCanary 54 | // - Firefox 55 | // - Opera 56 | // - Safari (only Mac) 57 | // - PhantomJS 58 | // - IE (only Windows) 59 | browsers: [ 60 | "PhantomJS" 61 | ], 62 | 63 | // Which plugins to enable 64 | plugins: [ 65 | "karma-phantomjs-launcher", 66 | "karma-jasmine" 67 | ], 68 | 69 | // Continuous Integration mode 70 | // if true, it capture browsers, run tests and exit 71 | singleRun: false, 72 | 73 | colors: true, 74 | 75 | // level of logging 76 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 77 | logLevel: config.LOG_INFO, 78 | 79 | // Uncomment the following lines if you are using grunt's server to run the tests 80 | // proxies: { 81 | // '/': 'http://localhost:9000/' 82 | // }, 83 | // URL root prevent conflicts with the site root 84 | // urlRoot: '_karma_' 85 | }); 86 | }; 87 | -------------------------------------------------------------------------------- /client/test/spec/controllers/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: MainCtrl', function () { 4 | 5 | // load the controller's module 6 | beforeEach(module('materialPollApp')); 7 | 8 | // Initialize the controller and a mock scope 9 | beforeEach(inject(function () { 10 | 11 | })); 12 | 13 | it('should attach a list of awesomeThings to the scope', function () { 14 | 15 | 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /img/material1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearbycoder/materialpoll/269b52f9955ea07d7724becec78d730be243adec/img/material1.png -------------------------------------------------------------------------------- /img/material2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearbycoder/materialpoll/269b52f9955ea07d7724becec78d730be243adec/img/material2.png -------------------------------------------------------------------------------- /img/material3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nearbycoder/materialpoll/269b52f9955ea07d7724becec78d730be243adec/img/material3.png -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Josh Hamilton 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 | -------------------------------------------------------------------------------- /server/app/models/poll.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var Schema = mongoose.Schema; 3 | 4 | var PollSchema = new Schema({ 5 | id: String, 6 | name: String, 7 | answers: Schema.Types.Mixed, 8 | votes: Schema.Types.Mixed, 9 | multiple: Boolean, 10 | singleIP: Boolean, 11 | IpAddresses: Schema.Types.Mixed, 12 | }); 13 | 14 | module.exports = mongoose.model('Poll', PollSchema); -------------------------------------------------------------------------------- /server/config.js.example: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | db: 'mongo connection here' 3 | } 4 | 5 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "materialpoll", 3 | "description": "Built with mongoose, node, express, and socketio", 4 | "version": "0.0.1", 5 | "homepage": "https://github.com/nearbycoder/materialpoll", 6 | "author": { 7 | "name": "nearbycoder", 8 | "email": "nearbycoder@gmail.com" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/nearbycoder/materialpoll.git" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/nearbycoder/materialpoll/issues" 16 | }, 17 | "main": "server.js", 18 | "dependencies": { 19 | "body-parser": "~1.0.1", 20 | "express": "~4.0.0", 21 | "mongoose": "~3.6.13", 22 | "morgan": "~1.0.0", 23 | "socket.io": "^1.3.5" 24 | }, 25 | "keywords": [ 26 | "materialpoll" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | // BASE SETUP 2 | // ============================================================================= 3 | 4 | // call the packages we need 5 | var express = require('express'); 6 | var bodyParser = require('body-parser'); 7 | var app = express(); 8 | var server = require('http').createServer(app); 9 | var io = require('socket.io')(server); 10 | var settings = require('./config'); 11 | 12 | var allowCrossDomain = function(req, res, next) { 13 | res.header('Access-Control-Allow-Origin', '*'); 14 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); 15 | res.header('Access-Control-Allow-Headers', 'Content-Type'); 16 | 17 | next(); 18 | } 19 | 20 | function makeid(){ 21 | var text = ""; 22 | var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 23 | 24 | for( var i=0; i < 5; i++ ) 25 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 26 | 27 | return text; 28 | } 29 | 30 | // configure app 31 | app.use(bodyParser.urlencoded({ extended: true })); 32 | app.use(bodyParser.json()); 33 | app.use(allowCrossDomain); 34 | 35 | var port = process.env.PORT || 9090; // set our port 36 | 37 | var mongoose = require('mongoose'); 38 | mongoose.connect(settings.db); // connect to our database 39 | var Poll = require('./app/models/poll'); 40 | 41 | // ROUTES FOR OUR API 42 | // ============================================================================= 43 | 44 | io.on('connection', function (socket) { 45 | console.log('connected'); 46 | }); 47 | 48 | // create our router 49 | var router = express.Router(); 50 | 51 | // middleware to use for all requests 52 | router.use(function(req, res, next) { 53 | // do logging 54 | console.log('Something is happening.'); 55 | next(); 56 | }); 57 | 58 | // test route to make sure everything is working (accessed at GET /api) 59 | router.get('/', function(req, res) { 60 | res.json({ message: 'hooray! welcome to our api!' }); 61 | }); 62 | 63 | //============================================================================= 64 | // on routes that end in /poll 65 | // ---------------------------------------------------- 66 | router.route('/poll') 67 | 68 | // create a poll (accessed at POST /poll) 69 | .post(function(req, res) { 70 | 71 | var poll = new Poll(); // create a new instance of the poll model 72 | poll.id = makeid(); 73 | poll.name = req.body.name; // set the poll name (comes from the request) 74 | poll.multiple = req.body.multiple; 75 | poll.singleIP = req.body.singleIP; 76 | poll.answers = req.body.answers; 77 | poll.IpAddresses = []; 78 | poll.votes = req.body.votes; 79 | poll.save(function(err, poll) { 80 | if (err) 81 | res.send(err); 82 | 83 | res.json([{ response: 'Poll created!' },{poll: poll}]); 84 | }); 85 | 86 | 87 | }) 88 | 89 | // get all the poll (accessed at GET /api/poll) 90 | .get(function(req, res) { 91 | Poll.find(function(err, poll) { 92 | if (err) 93 | res.send(err); 94 | 95 | res.json(poll); 96 | }); 97 | }); 98 | 99 | // on routes that end in /poll/:poll_id 100 | // ---------------------------------------------------- 101 | router.route('/poll/:poll_id') 102 | 103 | // get the poll with that id 104 | .get(function(req, res) { 105 | Poll.findOne({id :req.params.poll_id}, function(err, poll) { 106 | if (err) 107 | res.send(err); 108 | res.json(poll); 109 | }); 110 | }) 111 | 112 | // update the poll with this id 113 | .put(function(req, res) { 114 | Poll.findOne({id :req.params.poll_id}, function(err, poll) { 115 | if (err){ 116 | res.send(err); 117 | } 118 | if(poll.singleIP == true){ 119 | var ip = req.headers['x-forwarded-for'] || 120 | req.connection.remoteAddress || 121 | req.socket.remoteAddress || 122 | req.connection.socket.remoteAddress; 123 | 124 | if (poll.IpAddresses.indexOf(ip) > -1) { 125 | //In the array! 126 | } else { 127 | poll.IpAddresses.push(ip); 128 | for(x = 0; x < req.body.votes.length; x++){ 129 | if(req.body.votes[x] != null){ 130 | poll.votes[x] += req.body.votes[x]; 131 | } 132 | } 133 | poll.markModified('IpAddresses'); 134 | poll.markModified('votes'); 135 | } 136 | }else{ 137 | for(x = 0; x < req.body.votes.length; x++){ 138 | if(req.body.votes[x] != null){ 139 | poll.votes[x] += req.body.votes[x]; 140 | } 141 | } 142 | poll.markModified('votes'); 143 | } 144 | 145 | poll.save(function(err) { 146 | io.emit('chat' + req.params.poll_id, poll); 147 | if (err){ 148 | res.send(err); 149 | }else{ 150 | res.json(poll); 151 | } 152 | }); 153 | 154 | }); 155 | }) 156 | 157 | app.use('/api', router); 158 | 159 | server.listen(port); 160 | console.log('Magic happens on port ' + port); 161 | --------------------------------------------------------------------------------