├── .gitignore ├── .node-version ├── .npmrc ├── .travis.yml ├── .vscode └── settings.json ├── Gruntfile.coffee ├── LICENSE ├── README.md ├── app ├── coffee │ ├── controllerSpec.coffee │ ├── controllers │ │ ├── appCtrl.coffee │ │ ├── calcCtrl.coffee │ │ ├── headerCtrl.coffee │ │ ├── modelSelectionCtrl.coffee │ │ ├── questionaryCtrl.coffee │ │ └── shareCtrl.coffee │ ├── directives.coffee │ ├── init.coffee │ ├── initSpec.coffee │ ├── modelDefs │ │ ├── marketValueModel.coffee │ │ ├── marketValueModel.png │ │ ├── marketValueModel.svg │ │ ├── roleBasedModel.coffee │ │ ├── roleBasedModel.png │ │ ├── roleBasedModel.svg │ │ ├── weightedAspectsModel.coffee │ │ ├── weightedAspectsModel.png │ │ └── weightedAspectsModel.svg │ └── services │ │ ├── modelRepo.coffee │ │ ├── modelRepoSpec.coffee │ │ ├── trace.coffee │ │ └── traceSpec.coffee ├── error.html ├── images │ ├── background.png │ ├── copyrights.txt │ ├── favicon.ico │ ├── favicon.svg │ ├── piechopper_logo.png │ └── piechopper_logo.svg ├── index.html ├── partials │ ├── contrib-table.html │ ├── emphasis-table.html │ ├── footer.html │ ├── ga.html │ ├── header.html │ ├── model-selection.html │ ├── overview.html │ ├── questionary.html │ ├── results.html │ ├── scripts.html │ ├── styles.html │ └── tos.html ├── style │ └── style.scss └── vendor │ ├── fonts │ ├── chango.css │ └── chango.woff │ ├── jreject │ ├── browsers │ │ ├── background_browser.gif │ │ ├── browser_chrome.gif │ │ ├── browser_firefox.gif │ │ └── browser_msie.gif │ ├── jquery.reject.css │ ├── jquery.reject.js │ └── jquery.reject.min.js │ └── themes │ └── bootstrap-flatly.min.css ├── appveyor.yml ├── bower.json ├── circle.yml ├── config └── development.json ├── cypress.json ├── cypress └── integration │ └── app_spec.js ├── package-lock.json ├── package.json ├── renovate.json └── server ├── mgmt ├── crontab.sh ├── express │ ├── start.sh │ └── stop.sh ├── install.sh ├── memuse.sh └── mongo │ ├── backup.sh │ ├── cleanup.js │ ├── cleanup.sh │ ├── console.sh │ ├── start.sh │ └── stop.sh └── src ├── config.coffee └── server.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | !.gitkeep 2 | *.sublime-* 3 | build 4 | node_modules 5 | bower_components 6 | libpeerconnection.log 7 | npm-debug.log 8 | config/production.json 9 | config/staging.json 10 | config/deploy.json 11 | .sass-cache 12 | cypress/videos 13 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 16.13.1 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | ## our PieChopper is written 2 | ## in node so thats what we select. 3 | ## Cypress is agnostic to your apps 4 | ## backend language though. 5 | language: node_js 6 | 7 | node_js: 8 | - 12 9 | 10 | # https://github.com/cypress-io/cypress/issues/4069 11 | addons: 12 | apt: 13 | packages: 14 | - libgconf-2-4 15 | 16 | ## Cache NPM folder and Cypress binary 17 | ## to avoid downloading Cypress again and again 18 | cache: 19 | directories: 20 | - ~/.npm 21 | - ~/.cache 22 | 23 | install: 24 | - npm ci 25 | - npm run build 26 | 27 | before_script: 28 | ## runs the 'start' script which 29 | ## boots our local app server on port 8888 30 | ## which cypress expects to be running 31 | ## ----------------------------------- 32 | ## the '-- --silent' passes arguments 33 | ## to grunt serve which silences its output 34 | ## else our travis logs would be cluttered 35 | ## with output 36 | ## --------------------------------------- 37 | ## we use the '&' ampersand which tells 38 | ## travis to run this process in the background 39 | ## else it would block execution and hang travis 40 | - npm start -- --silent & 41 | 42 | script: 43 | ## now run cypress headlessly 44 | ## and record all of the tests. 45 | ## Cypress will search for a 46 | ## CYPRESS_RECORD_KEY environment 47 | ## variable by default and apply 48 | ## this to the run. 49 | - npm run cypress:run 50 | 51 | ## alternatively we could specify 52 | ## a specific record key to use 53 | ## like this without having to 54 | ## configure environment variables 55 | ## - cypress run --record --key 56 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": false 3 | } -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | acceptedTargets = ['development', 'staging', 'production'] 3 | targetName = "#{grunt.option('target') || 'development'}" 4 | if targetName not in acceptedTargets 5 | throw "Grunt target must be one of " + acceptedTargets 6 | 7 | readConfig = -> 8 | flattenObject = (obj) -> 9 | for own k, v of obj 10 | if (typeof obj[k]) is 'object' 11 | flattenObject(obj[k]) 12 | for own k2 of obj[k] 13 | obj["#{k}.#{k2}"] = obj[k][k2] 14 | obj 15 | 16 | getJsonConfig = (target) -> 17 | try 18 | return grunt.file.readJSON("config/#{target}.json") 19 | catch e 20 | return null 21 | 22 | config = 23 | deploy: getJsonConfig('deploy') or { address: '', username: '', password: ''} 24 | targetName: targetName 25 | timestamp: grunt.template.today('yyyy-mm-d--HH-MM-ss') 26 | targets: 27 | development: getJsonConfig('development') 28 | staging: getJsonConfig('staging') or {} 29 | production: getJsonConfig('production') or {} 30 | addCommonSetting: (key, value) -> 31 | for own k, v of config.targets 32 | v[key] = value 33 | config.addCommonSetting('deploy', config.deploy) 34 | config.addCommonSetting('timestamp', config.timestamp) 35 | config.target = config.targets[targetName] 36 | config.addCommonSetting('name', targetName) 37 | flattenObject(config) 38 | config = readConfig() 39 | 40 | 41 | # Project configuration. 42 | grunt.initConfig 43 | pkg: grunt.file.readJSON('package.json') 44 | config: config 45 | 46 | watch: 47 | all: 48 | files: [ 49 | 'app/**/*.coffee' 50 | 'app/**/*.scss' 51 | 'app/**/*.html' 52 | 'app/**/*.svg' 53 | 'app/**/*.ico' 54 | 'app/**/*.png' 55 | 'server/src/**/*' 56 | 'e2e/**/*' 57 | 'Gruntfile.coffee' 58 | 'config/**/*' 59 | ] 60 | tasks: 'default' 61 | 62 | clean: 63 | preBuild: ['build'] 64 | postBuild: ['build/preprocess'] 65 | distClean: config.target.removedFiles 66 | 67 | copy: 68 | lib: 69 | files: [ 70 | expand: true 71 | flatten: true 72 | src: [ 73 | 'bower_components/font-awesome/fonts/*' 74 | ] 75 | dest: 'build/served/vendor/fonts/' 76 | , 77 | expand: true 78 | flatten: true 79 | src: [ 80 | 'bower_components/bower-bootstrap-css/*.min.css' 81 | 'bower_components/font-awesome/css/font-awesome.min.css' 82 | 'bower_components/angular-ui/build/angular-ui.min.css' 83 | ] 84 | dest: 'build/served/vendor/css/' 85 | , 86 | expand: true 87 | flatten: true 88 | src: ['app/vendor/jreject/browsers/*.gif'] 89 | dest: 'build/served/vendor/images/' 90 | , 91 | expand: true 92 | flatten: true 93 | src: [ 94 | 'bower_components/jquery/*.js' 95 | 'bower_components/jquery/*.map' 96 | 'bower_components/angular/*.js' 97 | 'bower_components/angular-sanitize/*.js' 98 | 'bower_components/angular-sanitize/*.map' 99 | 'bower_components/angular-ui/build/*.js' 100 | 'bower_components/angular-ui-bootstrap-bower/*.js' 101 | 'bower_components/angular-mocks/*.js' 102 | 'bower_components/d3/*.js' 103 | 'app/vendor/**/*.js' 104 | ] 105 | dest: 'build/served/vendor/js/' 106 | , 107 | expand: true 108 | cwd: 'bower_components/angular-ui-bootstrap/template/' 109 | src: ['**'] 110 | dest: 'build/served/vendor/js/template/' 111 | , 112 | expand: true 113 | flatten: true 114 | src: [ 'app/vendor/**/*.css' ] 115 | dest: 'build/served/vendor/css/' 116 | , 117 | expand: true 118 | flatten: true 119 | src: [ 'app/vendor/**/*.woff' ] 120 | dest: 'build/served/vendor/fonts/' 121 | ] 122 | appImages: 123 | files: [ 124 | expand: true 125 | flatten: true 126 | src: [ 127 | 'app/**/*.ico' 128 | 'app/**/*.png' 129 | ] 130 | dest: 'build/served/app/img/' 131 | ] 132 | preProcess: 133 | files: [ 134 | expand: true 135 | flatten: false 136 | src: ['app/**/*', 'server/**/*', 'config/**/*', 'e2e/**/*'] 137 | dest: 'build/preprocess/' 138 | ] 139 | appHtmls: 140 | files: [ 141 | expand: true 142 | flatten: true 143 | src: ['build/preprocess/app/*.html'] 144 | dest: 'build/served/' 145 | ] 146 | serverMgmt: 147 | files: [ 148 | expand: true 149 | flatten: false 150 | cwd: 'build/preprocess/server/mgmt' 151 | src: ['**/*'] 152 | dest: 'build/server/mgmt/' 153 | ] 154 | 155 | 156 | preprocess: 157 | options: 158 | inline: true 159 | src: 160 | options: 161 | context: config 162 | src: [ 163 | 'build/preprocess/**/*.*' 164 | '!build/preprocess/app/vendor/**/*' 165 | ] 166 | 167 | coffee: 168 | options: 169 | sourceMap: false 170 | bare: true 171 | app: 172 | files: 173 | 'build/served/app/js/app.js': [ 174 | 'build/preprocess/app/coffee/init.coffee' 175 | 'build/preprocess/app/coffee/**/*.coffee' 176 | '!build/preprocess/app/coffee/**/*Spec.coffee' 177 | ] 178 | server: 179 | files: 180 | 'build/server/src/server.js': 'build/preprocess/server/src/server.coffee' 181 | 'build/server/src/db.js': 'build/preprocess/server/src/db.coffee' 182 | 'build/server/src/config.js': 'build/preprocess/server/src/config.coffee' 183 | 184 | ngmin: 185 | app: 186 | src: ['build/served/app/js/app.js'] 187 | dest: 'build/served/app/js/app.min.js' 188 | 189 | uglify: 190 | app: 191 | files: 192 | 'build/served/app/js/app.min.js': ['build/served/app/js/app.min.js'] 193 | 194 | cssmin: 195 | app: 196 | files: 197 | 'build/served/app/css/style.min.css': ['build/served/app/css/style.css'] 198 | 199 | sass: 200 | app: 201 | files: 202 | 'build/served/app/css/style.css': 'build/preprocess/app/style/*.scss' 203 | 204 | exec: 205 | browser: 206 | command: 'chrome http://localhost:8080 &' 207 | server: 208 | command: "./node_modules/.bin/nodemon --delay 2 build/server/src/server.js" 209 | 210 | sshconfig: 211 | server: 212 | host: config.deploy.address 213 | username: config.deploy.username 214 | password: config.deploy.password 215 | 216 | sshexec: 217 | options: 218 | config: 'server' 219 | path: '/' 220 | listVersions: 221 | command: "cd #{config.target.webServer.directory}; ls -1t versions/" 222 | createLinkToLatest: 223 | command: "cd #{config.target.webServer.directory}; rm -f build; ln -s versions/`ls versions -1t | head -1` build" 224 | restartServer: 225 | command: "cd #{config.target.webServer.directory}; sh build/server/mgmt/express/stop.sh; sleep #{config.target.webServer.restartDelaySecs}; sh build/server/mgmt/express/start.sh;" 226 | 227 | sftp: 228 | options: 229 | config: 'server' 230 | path: "#{config.target.webServer.directory}/versions/#{config.timestamp}/" 231 | srcBasePath: 'build/' 232 | createDirectories: true 233 | deploy: 234 | files: 235 | "./": 'build/**' 236 | 237 | 238 | grunt.loadNpmTasks 'grunt-contrib-watch' 239 | grunt.loadNpmTasks 'grunt-contrib-clean' 240 | grunt.loadNpmTasks 'grunt-contrib-copy' 241 | grunt.loadNpmTasks 'grunt-preprocess' 242 | grunt.loadNpmTasks 'grunt-contrib-coffee' 243 | grunt.loadNpmTasks 'grunt-exec' 244 | grunt.loadNpmTasks 'grunt-ssh' 245 | grunt.loadNpmTasks 'grunt-ngmin' 246 | grunt.loadNpmTasks 'grunt-contrib-uglify' 247 | grunt.loadNpmTasks 'grunt-contrib-cssmin' 248 | 249 | grunt.registerTask 'build', [ 250 | 'clean:preBuild' 251 | 'copy:lib' 252 | 'copy:preProcess' 253 | 'preprocess' 254 | 'copy:appImages' 255 | 'copy:appHtmls' 256 | 'copy:serverMgmt' 257 | 'coffee:app' 258 | 'coffee:server' 259 | 'ngmin:app' 260 | 'uglify:app' 261 | 'cssmin:app' 262 | 'clean:postBuild' 263 | 'clean:distClean' 264 | ] 265 | 266 | grunt.registerTask 'deploy', [ 267 | 'build' 268 | 'sftp:deploy' 269 | 'sshexec:createLinkToLatest' 270 | 'sshexec:restartServer' 271 | ] 272 | 273 | grunt.registerTask 'browse', ['exec:browser'] 274 | grunt.registerTask 'serve', ['exec:server'] 275 | grunt.registerTask 'default', ['build'] 276 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2013-2014 the PieChopper team, http://piechopper.com/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PieChopper [![Travis CI](https://travis-ci.org/cypress-io/cypress-example-piechopper.svg?branch=master)](https://travis-ci.org/cypress-io/cypress-example-piechopper) [![Circle CI](https://circleci.com/gh/cypress-io/cypress-example-piechopper.svg?style=svg)](https://circleci.com/gh/cypress-io/cypress-example-piechopper) [![Build status](https://ci.appveyor.com/api/projects/status/o6522037ibcaluj6?svg=true)](https://ci.appveyor.com/project/cypress-io/cypress-example-piechopper) [![renovate-app badge][renovate-badge]][renovate-app] 2 | 3 | ![piechopper-gif](https://cloud.githubusercontent.com/assets/1268976/12985444/ad14159c-d0c0-11e5-8e50-2b64a1d389ac.gif) 4 | 5 | ## Installing 6 | 7 | ```bash 8 | ## install the node_modules 9 | npm install 10 | ``` 11 | 12 | ## Development 13 | 14 | ```bash 15 | ## build the app files (once) 16 | npm run build 17 | 18 | ## start the local webserver 19 | npm start 20 | 21 | ## if you modify the app source files and 22 | ## want the files to auto build (optional) 23 | npm run watch 24 | ``` 25 | 26 | ## navigate your browser to 27 | http://localhost:8080 28 | 29 | ## Running Tests in Cypress 30 | 31 | - [Install Cypress](https://on.cypress.io/guides/installing-and-running#section-installing) 32 | - [Add the `cypress-example-piechopper` folder as a project](https://on.cypress.io/guides/installing-and-running#section-adding-projects) in Cypress. 33 | - Click `app_spec.js` or `Run All Tests` in the Cypress runner. 34 | - [Read how to setup Continous Integration in CircleCI](https://on.cypress.io/guides/continuous-integration). 35 | 36 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 37 | [renovate-app]: https://renovateapp.com/ 38 | -------------------------------------------------------------------------------- /app/coffee/controllerSpec.coffee: -------------------------------------------------------------------------------- 1 | describe 'AppCtrl', -> 2 | scope = null 3 | ctrl = null 4 | 5 | beforeEach -> 6 | module('piechopper') 7 | inject ($controller, $rootScope) -> 8 | scope = $rootScope.$new() 9 | ctrl = $controller('AppCtrl', $scope: scope) 10 | 11 | it "should have specific active model at the start", -> 12 | expect(scope.repo.activeModel).not.toBeUndefined() 13 | expect(scope.repo.activeModel.modelDef.id).toBe('6cfb9e85-90fc-4faa-954a-d99c8a9adc33') 14 | expect(scope.repo.activeModel).not.toBeUndefined() 15 | 16 | 17 | describe 'CalcCtrl', -> 18 | scope = null 19 | ctrl = null 20 | 21 | beforeEach -> 22 | module('piechopper') 23 | inject ($controller, $rootScope) -> 24 | $controller('AppCtrl', $scope: $rootScope) 25 | scope = $rootScope.$new() 26 | ctrl = $controller('CalcCtrl', $scope: scope) 27 | 28 | it "should automatically change member names in all models if one changes", -> 29 | testName = "Teemu Testi" 30 | scope.repo.activeModel.team.members[0].name = testName 31 | scope.$digest() 32 | for m in scope.repo.models 33 | do (m) -> 34 | expect(m.team.members[0].name).toBe(testName) 35 | -------------------------------------------------------------------------------- /app/coffee/controllers/appCtrl.coffee: -------------------------------------------------------------------------------- 1 | app.controller "AppCtrl", 2 | ($scope, $location, $http, $modal, $window, modelRepo, trace) => 3 | 4 | # from http://stackoverflow.com/a/2117523 5 | uuid = -> 6 | "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace /[xy]/g, (c) -> 7 | r = Math.random() * 16 | 0 8 | v = (if c is "x" then r else r & 0x3 | 0x8) 9 | v.toString 16 10 | localStorage.userId = uuid() if not localStorage.userId 11 | $scope.userId = localStorage.userId 12 | 13 | # expose commonly used pieces to scope 14 | $scope.repo = repo = modelRepo.createRepo(modelDefs.all) 15 | $scope.model = model = -> repo.activeModel 16 | $scope.members = members = -> repo.activeModel.team.members 17 | $scope.criteria = (id) -> repo.activeModel.modelDef.criterias[id] 18 | $scope.memberColors = ["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c"] 19 | $scope.trace = trace 20 | 21 | $scope.moreResultsWanted = false # does user want more results to be shown? 22 | $scope.showMoreResults = (show) -> $scope.moreResultsWanted = show 23 | 24 | _linkToSnapshot = null 25 | $scope.linkToSnapshot = (val) -> 26 | _linkToSnapshot = val if val != undefined 27 | _linkToSnapshot 28 | 29 | $scope.showMessage = (title, desc, timeout) -> 30 | html = """ 31 |
32 |

#{title}

33 | #{desc} 34 |
35 | """ 36 | dlg = $modal.open(template: html) 37 | if timeout 38 | $timeout((-> dlg.dismiss()), timeout) 39 | 40 | $scope.showTos = -> 41 | $scope.showMessage("Terms and Conditions", $('#tos').html()) 42 | $scope.trace.showTos() 43 | 44 | showUnknownProposalError = -> 45 | $scope.showMessage('Oops !', 46 | """ 47 |

The proposal you were looking for wasn't found.

48 |

49 | Please note that proposals older than a month are deleted. 50 | For more recent proposals, please check that your address is correct. 51 |

52 | """) 53 | 54 | showInvalidProposalError = -> 55 | $scope.showMessage('Ooops !', 56 | """ 57 |

58 | There was an error while loading the given address. 59 | Some parts of the proposal might not be correct! 60 |

61 |

62 | If your data is mission critical, mail to info@piechopper.com. 63 |

64 | """) 65 | 66 | addDefaultMembers = -> repo.addMember() for [1..2] 67 | pathParts = $location.path().split('/') 68 | 69 | if pathParts.length == 3 and pathParts[1] == 'p' 70 | $http.get("/api/1/proposals/#{ pathParts[2] }"). 71 | success((data, status, headers, config) -> 72 | success = repo.deserialize(data.repo) 73 | if not success 74 | showInvalidProposalError() 75 | else 76 | $scope.$broadcast('modelLoaded') 77 | if data.userId and (data.userId == $scope.userId) 78 | $scope.trace.loadOwnProposal() 79 | else 80 | $scope.trace.loadPartnerProposal() 81 | ). 82 | error (data, status, headers, config) -> 83 | addDefaultMembers() 84 | showUnknownProposalError() 85 | else if pathParts.length >= 2 86 | addDefaultMembers() 87 | showUnknownProposalError() 88 | else 89 | addDefaultMembers() 90 | -------------------------------------------------------------------------------- /app/coffee/controllers/calcCtrl.coffee: -------------------------------------------------------------------------------- 1 | app.controller "CalcCtrl", ($scope, $timeout, trace) -> 2 | updateEquities = () -> 3 | $scope.equities = [] 4 | for m in $scope.members() 5 | $scope.equities.push({ name: m.name, value: m.equity }) 6 | 7 | $scope.memberValues = -> 8 | (m.values for m in $scope.members()) 9 | scoreTimer = null 10 | score = -> 11 | $scope.linkToSnapshot(null) # since another round of scoring is done, link is invalid 12 | $timeout.cancel(scoreTimer) if scoreTimer 13 | doScore = -> 14 | $scope.model().score() 15 | updateEquities() 16 | scoreTimer = $timeout(doScore, 1) 17 | $scope.$watch('memberValues()', score, true) 18 | $scope.$watch('model().emphasis', score, true) 19 | 20 | # --- if team member's name change, update it to other models also --- 21 | syncMemberNames = -> 22 | $scope.repo.syncMemberNames($scope.model()) 23 | updateEquities() # to update names into piechart 24 | $scope.memberNames = -> (m.name for m in $scope.members()) 25 | $scope.$watch('memberNames()', syncMemberNames, true) 26 | 27 | # --- Imitate radio controls --- 28 | $scope.unselectOtherRadios = (id, member) -> 29 | $timeout -> # needs timeout to get member.values[id] -value set correctly by angular 30 | if member.values[id] 31 | for other in $scope.members() 32 | if other.id != member.id 33 | other.values[id] = false 34 | , 1 35 | -------------------------------------------------------------------------------- /app/coffee/controllers/headerCtrl.coffee: -------------------------------------------------------------------------------- 1 | app.controller "HeaderCtrl", ($scope) -> 2 | $scope.aboutVisible = false 3 | -------------------------------------------------------------------------------- /app/coffee/controllers/modelSelectionCtrl.coffee: -------------------------------------------------------------------------------- 1 | app.controller "ModelSelectionCtrl", ($scope, $timeout) -> 2 | # Models in repo could directly be used as slides; however if done so, 3 | # slide activation causes heavy DOM updates since models are tied to 4 | # tables via 2-way binding. Therefore slides are isolated as separate 5 | # objects, and their changes are reflected in the repo after a short delay 6 | 7 | class Slide 8 | constructor: (@model, @id, @active, @name, @image, @target, @desc) -> 9 | 10 | $scope.slides = slides = [] 11 | for m in $scope.repo.models 12 | slide = new Slide(m, m.modelDef.id, m.active, m.modelDef.name, 13 | m.modelDef.image, m.modelDef.target, m.modelDef.desc) 14 | slides.push(slide) 15 | 16 | slideChangeTimer = null 17 | onSlideChange = -> 18 | $timeout.cancel(slideChangeTimer) if slideChangeTimer 19 | slideChangeTimer = $timeout -> 20 | $scope.repo.activeModel = $scope.activeSlide().model 21 | console.log "in onSlideChange timed", $scope.repo.activeModel.modelDef.name 22 | , 1000 23 | 24 | $scope.activeSlide = -> 25 | for s in slides 26 | return s if s.active 27 | $scope.$on 'modelLoaded', () -> 28 | model = $scope.repo.activeModel 29 | console.log "in modelLoaded", model.modelDef.name 30 | $timeout.cancel(slideChangeTimer) if slideChangeTimer 31 | for s in slides 32 | s.active = (s.model == model) 33 | 34 | $scope.$watch($scope.activeSlide, onSlideChange) 35 | -------------------------------------------------------------------------------- /app/coffee/controllers/questionaryCtrl.coffee: -------------------------------------------------------------------------------- 1 | app.controller "QuestionaryCtrl", ($scope, $http, $timeout, $rootScope) -> 2 | $scope.questionary = {} 3 | $scope.questionarySent = false 4 | 5 | $scope.sendQuestionary = -> 6 | $scope.trace.sendQuestionary() 7 | doc = 8 | userId: $scope.userId 9 | questionary: $scope.questionary 10 | $http.post('/api/1/questionaries', doc). 11 | success((data, status, headers, config) -> 12 | $scope.questionarySent = true 13 | ). 14 | error (data, status, headers, config) -> 15 | $scope.showMessage('Oops !', 16 | """ 17 | There was an error while saving the questionary. 18 | 22 | """) 23 | -------------------------------------------------------------------------------- /app/coffee/controllers/shareCtrl.coffee: -------------------------------------------------------------------------------- 1 | app.controller "ShareCtrl", ($scope, $http, $location) -> 2 | $scope.linkToSnapshot(null) 3 | $scope.saveSnapshot = -> 4 | doc = 5 | repo: $scope.repo.serialize() 6 | userId: $scope.userId 7 | 8 | $http.post('/api/1/proposals', doc). 9 | success((data, status, headers, config) -> 10 | loc = $location.host() 11 | port = $location.port() 12 | if port and (port != 80) 13 | loc += ":#{$location.port()}" 14 | $scope.linkToSnapshot("http://#{loc}/#/p/#{data.id}") 15 | $scope.trace.shareProposal() 16 | ). 17 | error (data, status, headers, config) -> 18 | $scope.showMessage('Ooops !', 19 | """ 20 | We couldn't save the proposal. 21 | 25 | """) 26 | -------------------------------------------------------------------------------- /app/coffee/directives.coffee: -------------------------------------------------------------------------------- 1 | app.directive "contenteditable", () -> 2 | require: "ngModel" 3 | link: (scope, elm, attrs, ctrl) -> 4 | preventEmpty = attrs.preventEmpty or false 5 | 6 | ctrl.$render = -> 7 | elm.html(ctrl.$viewValue) 8 | 9 | elm.bind "blur", -> 10 | scope.$apply -> 11 | if preventEmpty and elm.html().replace(/^\s+|\s+$/g, "").length == 0 12 | elm.html(ctrl.$viewValue) 13 | else 14 | newVal = elm.html().replace(/
/g, ' ') # no linebreaks 15 | elm.html(newVal) 16 | ctrl.$setViewValue(newVal) 17 | 18 | 19 | app.directive "piegraph", -> 20 | restrict: "E" 21 | replace: true 22 | template: '
' 23 | scope: 24 | width: '@' 25 | height: '@' 26 | slices: '=' # function returning array of objects with keys {name, value} 27 | colors: '=' 28 | link: (scope, element, attrs) -> 29 | [width, height] = [parseInt(scope.width), parseInt(scope.height)] 30 | svg = d3.select(element[0]) 31 | .append("svg").attr("width", width).attr("height", height) 32 | .append("g").attr("transform", "translate(#{width / 2}, #{height / 2})") 33 | radius = Math.min(width, height) / 2 - 10 34 | pie = d3.layout.pie().sort(null).value((d) -> d.value) 35 | arc = d3.svg.arc().outerRadius(radius).innerRadius(0) 36 | color = d3.scale.ordinal().range(scope.colors) 37 | 38 | update = (slices) -> 39 | nonZeroSlices = (slice for slice in slices when slice.value > 0) 40 | svg.selectAll('*').remove(); 41 | if nonZeroSlices.length > 0 42 | g = svg.selectAll(".arc").data(pie(nonZeroSlices)).enter() 43 | .append("g").attr("class", "arc") 44 | g.append("path").attr("d", arc).style("fill", (d) -> color(d.data.name)) 45 | g.append("text").attr("transform", (d) -> 46 | "translate(#{arc.centroid(d)})" 47 | ).attr("dy", ".35em").style("text-anchor", "middle").text (d) -> 48 | d.data.name 49 | else 50 | svg.append("circle").attr("r", radius).attr('fill', '#fff') 51 | .attr('stroke', '#98abc5') 52 | 53 | scope.$watch('slices', update, true) 54 | 55 | 56 | app.directive "sliceGraph", ($compile) -> 57 | restrict: 'A' 58 | scope: 59 | width: '@' 60 | height: '@' 61 | slices: '=' # function returning array of objects with keys {name, value} 62 | colors: '=' 63 | link: (scope, element, attrs) -> 64 | update = -> 65 | $('.i-slice-graph').remove() 66 | html = [] 67 | html.push "
" 68 | prevY = 0 69 | for slice, i in scope.slices 70 | h = 0 71 | if slice.value != 0 72 | h = Math.round(+scope.height / 100 * slice.value) 73 | style = [] 74 | style.push "left: 0; top: #{prevY}px; width: #{+scope.width}px; height: #{h}px;" 75 | style.push "background-color: #{scope.colors[i]};" 76 | style = style.join(' ') 77 | html.push "
" 78 | prevY += h 79 | html.push "
" 80 | bars = $(html.join('')) 81 | element.append(bars) 82 | $compile(bars)(scope) 83 | scope.$watch('slices', update, true) 84 | 85 | 86 | app.directive "showInSections", ($timeout) -> 87 | restrict: 'A' 88 | link: (scope, element, attrs) -> 89 | w = $(window) 90 | offset = 60 91 | 92 | isViewed = (elemId) -> 93 | elem = $('#' + elemId) 94 | docViewTop = w.scrollTop() 95 | docViewBottom = docViewTop + w.height() 96 | elemTop = elem.offset().top 97 | elemBottom = elemTop + elem.height() 98 | (elemTop < docViewTop + offset) and (elemBottom > docViewTop) 99 | 100 | showHide = -> 101 | sections = attrs.showInSections.split(';') 102 | viewed = false 103 | for section in sections 104 | break if viewed = isViewed(section) 105 | if viewed 106 | $(element).fadeIn() 107 | else 108 | $(element).fadeOut() 109 | 110 | $(window).scroll -> showHide() 111 | $timeout (-> $(element).hide(); showHide()), 1 112 | 113 | 114 | 115 | app.directive "valueCell", ($compile) -> 116 | restrict: 'A' 117 | require: 'ngModel' 118 | link: (scope, element, attrs, ctrl) -> 119 | typeDef = scope.$eval(attrs.valueCell) 120 | # FIXME: on model changes, link function is called even though the model 121 | # is not yet settled causing errors on log. The fullowing 122 | # temporary fix prevents these errors happening (but there's still futile link-calls) 123 | return if not typeDef 124 | radioUnselector = element.attr('radio-unselector') 125 | html = [] 126 | switch typeDef.typeName 127 | when 'checkbox' 128 | html.push '' 131 | when 'radio' 132 | html.push '' 135 | when 'number' 136 | html.push '' 139 | if typeDef.unit 140 | html.push "#{typeDef.unit}" 141 | when 'enum' 142 | scope.selectOptions = ([v, k] for own k, v of typeDef.values) 143 | html.push "' 145 | 146 | html = html.join(' ') 147 | html = html.replace('@@@', "ng-model=\"#{ element.attr('ng-model') }\"") 148 | input = angular.element(html) 149 | element.append(input) 150 | $compile(input)(scope) 151 | 152 | if typeDef.typeName in ['checkbox', 'radio'] 153 | input.click (e) -> 154 | e.stopPropagation() # prevent td to take control 155 | 156 | $(element).click (e) -> 157 | scope.$apply -> 158 | newVal = !input.prop('checked') 159 | input.prop('checked', newVal) 160 | ctrl.$setViewValue(newVal) 161 | if element.attr('radio-unselector') and typeDef.typeName == 'radio' 162 | scope.$eval(radioUnselector) 163 | 164 | 165 | 166 | app.directive "scrollTo", ($timeout) -> 167 | restrict: "A" 168 | link: (scope, element, attrs) -> 169 | element.bind "click", -> 170 | $timeout -> # needs delay if there's ng-show before scroll 171 | offset = attrs.offset or 0 172 | target = $('#' + attrs.scrollTo) 173 | speed = attrs.speed or 500 174 | $("html,body").stop().animate 175 | scrollTop: target.offset().top - offset 176 | , speed 177 | , 1 178 | 179 | 180 | 181 | app.directive "fullyCentered", ($timeout) -> 182 | restrict: "A" 183 | link: (scope, element, attrs) -> 184 | $(window).resize -> 185 | $(element).css 186 | position: 'absolute' 187 | left: ($(window).width() - $(element).outerWidth()) / 2 188 | top: ($(window).height() - $(element).outerHeight()) / 2 189 | $timeout((-> $(window).resize()), 1) 190 | 191 | 192 | app.directive "selectOnFocus", () -> 193 | restrict: "A" 194 | link: (scope, element, attrs) -> 195 | $(element).on "click", -> 196 | $(element).select() 197 | 198 | 199 | app.directive "legacyBrowserRejector", ($window) -> 200 | restrict: "A" 201 | link: (scope, element, attrs) -> 202 | $window.onload = -> 203 | $.reject 204 | reject: 205 | all: false 206 | msie5: true 207 | msie6: true 208 | msie7: true 209 | msie8: true 210 | browserInfo: 211 | firefox: 212 | text: 'Mozilla Firefox' 213 | url: 'http://www.mozilla.com/firefox/' 214 | chrome: 215 | text: 'Google Chrome' 216 | url: 'http://www.google.com/chrome/' 217 | msie: 218 | text: 'Internet Explorer' 219 | url: 'http://www.microsoft.com/windows/Internet-explorer/' 220 | imagePath: attrs.browserImagePath 221 | display: ['firefox', 'chrome', 'msie'] 222 | -------------------------------------------------------------------------------- /app/coffee/init.coffee: -------------------------------------------------------------------------------- 1 | app = angular.module('piechopper', ['ui', 'ui.bootstrap', 'ngSanitize']) 2 | 3 | # The following are the globals in the app 4 | modelDefs = 5 | all: [] 6 | add: (newDef) -> 7 | for oldDef, i in modelDefs.all 8 | if newDef.priority < oldDef.priority 9 | modelDefs.all.splice(i, 0, newDef) 10 | newDef = null 11 | break 12 | if newDef 13 | modelDefs.all.push(newDef) 14 | 15 | types = 16 | checkbox: (options) -> 17 | typeName: 'checkbox' 18 | default: options?.default or false 19 | check: (value) -> 20 | if typeof(value) != 'boolean' 21 | return 'Value must be on / off' 22 | 23 | radio: (options) -> 24 | typeName: 'radio' 25 | default: false 26 | check: (value) -> 27 | if typeof(value) != 'boolean' 28 | return 'Value must be on / off' 29 | 30 | number: (options) -> 31 | typeName: 'number' 32 | default: options.default or 0 33 | min: options?.min 34 | max: options?.max 35 | unit: options?.unit 36 | check: (value) -> 37 | if typeof(value) != 'number' 38 | return 'Value must be a number' 39 | if options?.min and value < options.min 40 | return "Value must be bigger than #{options.min}" 41 | if options?.max and value > options.max 42 | return "Value must be smaller than #{options.max}" 43 | 44 | enum: (options) -> 45 | r = 46 | typeName: 'enum' 47 | values: options.values 48 | default: options.default 49 | check: (value) -> 50 | found = false 51 | for own k, v of options.values 52 | if value == v 53 | found = true 54 | if not found 55 | return "Value must be one of #{options.values}" 56 | if r.default == undefined 57 | r.default = (v for k, v in options.values)[0] 58 | r 59 | -------------------------------------------------------------------------------- /app/coffee/initSpec.coffee: -------------------------------------------------------------------------------- 1 | describe 'Init', -> 2 | describe "Model Definition", -> 3 | 4 | for md, i in modelDefs.all 5 | do (md, i) -> 6 | 7 | it "no. #{i} should have an id", -> 8 | expect(typeof md.id).toBe('string') 9 | 10 | it "no. #{i} should have a priority", -> 11 | expect(typeof md.priority).toBe('number') 12 | 13 | it "no. #{i} should have a name", -> 14 | expect(typeof md.name).toBe('string') 15 | 16 | it "no. #{i} should have a version", -> 17 | expect(typeof md.version).toBe('string') 18 | majorVersion = parseInt(md.version.split('.')[0]) 19 | expect(typeof majorVersion).toBe('number') 20 | expect(majorVersion).not.toBe(NaN) 21 | 22 | it "#{md.name} should have a target", -> 23 | expect(typeof md.target).toBe('string') 24 | 25 | it "#{md.name} should have a description", -> 26 | expect(typeof md.desc).toBe('string') 27 | 28 | it "#{md.name} should have criterias", -> 29 | expect(typeof md.criterias).toBe('object') 30 | 31 | for own c, v of md.criterias 32 | do (c, v) -> 33 | it "#{md.name}:criteria:#{c} should have correct subfields", -> 34 | expect(typeof v.text).toBe('string') 35 | expect(typeof v.type).toBe('object') 36 | expect((k for own k, kv of types)).toContain(v.type.typeName) 37 | if v.type in [types.radio, types.checkbox] 38 | expect(typeof v.default).toBe('boolean') 39 | else if v.type == types.number 40 | expect(typeof v.default).toBe('number') 41 | else if v.type == types.enum 42 | expect(typeof v.default).toBe('string') 43 | if v.info 44 | expect(typeof v.info).toBe('string') 45 | if v.constraint 46 | expect(typeof v.constraint).toBe('function') 47 | 48 | it "#{md.name} should have an scoring function", -> 49 | expect(typeof md.score).toBe('function') 50 | -------------------------------------------------------------------------------- /app/coffee/modelDefs/marketValueModel.coffee: -------------------------------------------------------------------------------- 1 | modelDefs.add 2 | 3 | id: 'd691b574-58d0-44dd-be19-7196d34d781f' 4 | name: 'Market Value' 5 | version: '1.0' 6 | image: 'marketValueModel.png' 7 | target: "If you want to match equity to monetary value of the contribution." 8 | priority: 20 9 | desc: """ 10 |

This method compares market price for each contribution in the company. 11 | It considers the opportunity cost of the lost salary, as well as assets and sales 12 | benefitting the project.

13 |

The method is inspired by the Slicing Pie website.

14 | """ 15 | 16 | criterias: 17 | marketSalary: 18 | text: "What is the member's market salary per month?" 19 | type: types.number(default: 0, min: 0, unit: '$') 20 | 21 | salary: 22 | text: "How much does the member get salary from the company per month?" 23 | type: types.number(default: 0, min: 0, unit: '$') 24 | 25 | hours: 26 | text: "How many hours has the member been working for the company?" 27 | type: types.number(default: 0, min: 0, unit: 'h') 28 | 29 | cash: 30 | text: "How much cash is the member investing?" 31 | type: types.number(default: 0, min: 0, unit: '$') 32 | 33 | otherItems: 34 | text: "How much does the member bring in other valuables (premises, tools etc.)?" 35 | type: types.number(default: 0, min: 0, unit: '$') 36 | 37 | sales: 38 | text: "How much sales revenue is the member bringing in?" 39 | type: types.number(default: 0, min: 0, unit: '$') 40 | 41 | salesCommissionPercent: 42 | text: "What is the sales commission percent that is usually paid on the market?" 43 | type: types.number(default: 0, min: 0, max: 100, unit: '%') 44 | 45 | salesCommissionPaid: 46 | text: "How much sales commission has been paid to the member?" 47 | type: types.number(default: 0, min: 0, unit: '$') 48 | 49 | emphasis: 50 | salary: 51 | text: "How much do you value contributed work?" 52 | type: types.number(default: 200, min: 0, unit: '%') 53 | 54 | cash: 55 | text: "How much do you value contributed cash?" 56 | type: types.number(default: 400, min: 0, unit: '%') 57 | 58 | otherItems: 59 | text: "How much do you value contributed other items?" 60 | type: types.number(default: 400, min: 0, unit: '%') 61 | 62 | sales: 63 | text: "How much do you value sales?" 64 | type: types.number(default: 200, min: 0, unit: '%') 65 | 66 | score: (self, members, messages, emphasis) -> 67 | pie = 0 68 | for member in members 69 | ms = member.scores = { slice: 0 } 70 | mv = member.values 71 | 72 | salaryPart = (mv.marketSalary - mv.salary) / 160 * mv.hours * (emphasis.salary / 100) 73 | if salaryPart < 0 74 | messages.push "#{member.name}'s salary is too high." 75 | salaryPart = 0 76 | ms.slice = salaryPart 77 | 78 | ms.slice += mv.cash * (emphasis.cash / 100) 79 | ms.slice += mv.otherItems * (emphasis.otherItems / 100) 80 | 81 | salesPart = ((mv.sales * mv.salesCommissionPercent / 100) - mv.salesCommissionPaid) * (emphasis.sales / 100) 82 | if salesPart < 0 83 | messages.push "#{member.name}'s sales commission is too high." 84 | salaryPart = 0 85 | 86 | ms.slice += salesPart 87 | pie += ms.slice 88 | 89 | for member in members 90 | member.equity = (Math.round(member.scores.slice / pie * 1000) / 10) or 0 91 | -------------------------------------------------------------------------------- /app/coffee/modelDefs/marketValueModel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-piechopper/143df856de3b982c077da99852e5bdb78330ccc4/app/coffee/modelDefs/marketValueModel.png -------------------------------------------------------------------------------- /app/coffee/modelDefs/marketValueModel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/coffee/modelDefs/roleBasedModel.coffee: -------------------------------------------------------------------------------- 1 | do -> 2 | valueEnum = -> types.enum(default: 0, values: { 3 | 'None': 0 , 'Little': 2, 'Some': 5, 'Plenty': 10 }) 4 | 5 | modelDefs.add 6 | id: '6cfb9e85-90fc-4faa-954a-d99c8a9adc33' 7 | name: 'Company Roles' 8 | version: '1.0' 9 | image: 'roleBasedModel.png' 10 | target: "If you want to share equity based on roles in the company." 11 | priority: 10 12 | desc: """ 13 |

This method considers executive, development and business roles in the company, 14 | and scores each team member based on their contributions for each role.

15 |

The method is inspired by the Foundrs.com website.

16 | """ 17 | 18 | criterias: 19 | idea: 20 | text: "Who had the original idea for the project?" 21 | type: types.checkbox() 22 | 23 | participation: 24 | text: "Compared to full time job, how much time the member contributes until you raise funding?" 25 | info: "100 = full time, 50 = half-time, 120 = 20% overtime" 26 | type: types.number(default: 100, min: 1, max: 250, unit: '%') 27 | 28 | techParticipation: 29 | text: "How much does the member participate into technical development?" 30 | type: valueEnum() 31 | 32 | techLead: 33 | text: "Who would lead the technical team if you would get more personnel?" 34 | type: types.radio() 35 | 36 | leaveTech: 37 | text: "If the member would leave the project, how much would it affect the development schedule?" 38 | type: valueEnum() 39 | 40 | leaveFunding: 41 | text: "If the member would leave the project, how much would it affect the chances of getting funded?" 42 | type: valueEnum() 43 | 44 | launch: 45 | text: "If the member would leave the project, how much would it affect the launch or initial traction?" 46 | type: valueEnum() 47 | 48 | revenue: 49 | text: "If the member would leave the project, how much would it affect generating the revenue quickly?" 50 | type: valueEnum() 51 | 52 | pr: 53 | text: "How much does the member participate to the creation of marketing materials?" 54 | type: valueEnum() 55 | score: (scores, value) -> 56 | scores.biz += value / 5 57 | 58 | features: 59 | text: "How much does the member contribute to the product features?" 60 | type: valueEnum() 61 | 62 | budget: 63 | text: "Who maintains the budgeting spreadsheets?" 64 | type: types.radio() 65 | 66 | expenses: 67 | text: "How much does the member contribute to the business expenses (business cards, web hosting...)?" 68 | type: valueEnum() 69 | 70 | pitch: 71 | text: "Who pitches investors?" 72 | type: types.radio() 73 | 74 | connections: 75 | text: "How well is the member connected with the target industry (potential customers, journalists, influencers)?" 76 | type: valueEnum() 77 | 78 | ceo: 79 | text: "Who is or becomes the CEO?" 80 | type: types.radio() 81 | 82 | emphasis: 83 | ceo: 84 | text: "How much do you value the executive role?" 85 | info: "100 = normal valuation, 50 = half-valuation, 200 = double valuation" 86 | type: types.number(default: 140, min: 1, max: 250, unit: '%') 87 | 88 | dev: 89 | text: "How much do you value the development role?" 90 | info: "100 = normal valuation, 50 = half-valuation, 200 = double valuation" 91 | type: types.number(default: 120, min: 1, max: 250, unit: '%') 92 | 93 | biz: 94 | text: "How much do you value the business and marketing role?" 95 | info: "100 = normal valuation, 50 = half-valuation, 200 = double valuation" 96 | type: types.number(default: 100, min: 1, max: 250, unit: '%') 97 | 98 | 99 | score: (self, members, messages, emphasis) -> 100 | theCeo = members.filter((m) -> m.values.ceo)[0] 101 | 102 | addScore = (target, vals) -> 103 | for own k, v of vals 104 | target[k] += v 105 | 106 | ts = { ceo: 0, dev: 0, biz: 0 } # teamScores 107 | for member in members 108 | ms = member.scores = { ceo: 0, dev: 0, biz: 0} 109 | mv = member.values 110 | 111 | addScore(ms, ceo: 5, dev: 3, biz: 3) if mv.idea 112 | addScore(ms, dev: mv.techParticipation) 113 | addScore(ms, ceo: 1, dev: 10) if mv.techLead 114 | addScore(ms, ceo: mv.leaveTech / 10, dev: mv.leaveTech) 115 | addScore(ms, ceo: mv.leaveFunding) 116 | addScore(ms, ceo: mv.launch / 3, dev: mv.launch / 3, biz: mv.launch / 3) 117 | addScore(ms, ceo: mv.revenue / 5, biz: mv.revenue) 118 | addScore(ms, biz: mv.pr / 5) 119 | addScore(ms, ceo: mv.features / 2, dev: mv.features / 5, biz: mv.features / 5) 120 | addScore(ms, ceo: 3, biz: 5) if mv.budget 121 | addScore(ms, ceo: mv.expenses / 5, biz: mv.expenses / 5) 122 | addScore(ms, ceo: 10, biz: 1) if mv.pitch 123 | addScore(ms, ceo: mv.connections / 3, biz: mv.connections) 124 | addScore(ms, ceo: 10) if mv.ceo 125 | 126 | for own id, value of ms 127 | val = ms[id] = ms[id] * (mv.participation / 100) 128 | ts[id] += val 129 | 130 | if not theCeo 131 | messages.push "You haven't selected the CEO." 132 | 133 | else if theCeo.scores.ceo < 35 134 | messages.push 'You appear to have a weak CEO.' 135 | 136 | if ts.dev < 25 137 | messages.push 'You should strengthen your development team.' 138 | 139 | if ts.biz < 20 140 | messages.push 'You should strengthen your business and marketing team.' 141 | 142 | eCeo = emphasis.ceo / 100 143 | eDev = emphasis.dev / 100 144 | eBiz = emphasis.biz / 100 145 | teamTotal = ts.ceo * eCeo + ts.dev * eDev + ts.biz * eBiz 146 | for member in members 147 | ms = member.scores 148 | memberTotal = ms.ceo * eCeo + ms.dev * eDev + ms.biz * eBiz 149 | member.equity = (Math.round(memberTotal / teamTotal * 1000) / 10) or 0 150 | 151 | if theCeo and (ms.ceo > theCeo.scores.ceo) 152 | messages.push "Maybe #{member.name} should be the CEO." 153 | -------------------------------------------------------------------------------- /app/coffee/modelDefs/roleBasedModel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-piechopper/143df856de3b982c077da99852e5bdb78330ccc4/app/coffee/modelDefs/roleBasedModel.png -------------------------------------------------------------------------------- /app/coffee/modelDefs/roleBasedModel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 45 | 47 | 48 | 50 | image/svg+xml 51 | 53 | 54 | 55 | 56 | 57 | 61 | 66 | 71 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /app/coffee/modelDefs/weightedAspectsModel.coffee: -------------------------------------------------------------------------------- 1 | modelDefs.add 2 | 3 | id: '6ee7289c-6eb6-4239-9fe1-5ded5dd511e4' 4 | name: 'Relative Importance' 5 | version: '1.0' 6 | image: 'weightedAspectsModel.png' 7 | target: "If you want something quick and simple" 8 | priority: 30 9 | desc: """ 10 |

This method considers relative importance of various aspect related to establishing a company. It's quick to fill, though highly subjective.

11 |

The method is inspired by the Founders Pie Calculator.

12 | """ 13 | 14 | criterias: 15 | idea: 16 | text: "How much has the member been contributing to the original idea? (0-10)" 17 | type: types.number(default: 0, min: 0, max: 10) 18 | 19 | businessPlan: 20 | text: "How much has the member been contributing to the business plan? (0-10)" 21 | type: types.number(default: 0, min: 0, max: 10) 22 | 23 | domainExpertise: 24 | text: "How well does the member know your target industry, and how well they are connected? (0-10)" 25 | type: types.number(default: 0, min: 0, max: 10) 26 | 27 | commitmentAndRisk: 28 | text: "How committed the member is in terms of consumed time and money? (0-10)" 29 | type: types.number(default: 0, min: 0, max: 10) 30 | 31 | responsibilities: 32 | text: "How demanding are the responsibilities of the member? (0-10)" 33 | type: types.number(default: 0, min: 0, max: 10) 34 | 35 | emphasis: 36 | idea: 37 | text: "How much do you value the idea? (0-10)" 38 | type: types.number(default: 5, min: 0, max: 10) 39 | 40 | businessPlan: 41 | text: "How much do you value the business plan? (0-10)" 42 | type: types.number(default: 5, min: 0, max: 10) 43 | 44 | domainExpertise: 45 | text: "How much do you value the domain expertise? (0-10)" 46 | type: types.number(default: 5, min: 0, max: 10) 47 | 48 | commitmentAndRisk: 49 | text: "How much do you value the commitment and risk? (0-10)" 50 | type: types.number(default: 5, min: 0, max: 10) 51 | 52 | responsibilities: 53 | text: "How much do you value the demandingness of responsibilities? (0-10)" 54 | type: types.number(default: 5, min: 0, max: 10) 55 | 56 | 57 | score: (self, members, messages, emphasis) -> 58 | pie = 0 59 | for member in members 60 | member.scores = { slice: 0 } 61 | for own k, v of emphasis 62 | member.scores.slice += member.values[k] * v 63 | pie += member.scores.slice 64 | for member in members 65 | member.equity = (Math.round(member.scores.slice / pie * 1000) / 10) or 0 66 | -------------------------------------------------------------------------------- /app/coffee/modelDefs/weightedAspectsModel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-piechopper/143df856de3b982c077da99852e5bdb78330ccc4/app/coffee/modelDefs/weightedAspectsModel.png -------------------------------------------------------------------------------- /app/coffee/modelDefs/weightedAspectsModel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/coffee/services/modelRepo.coffee: -------------------------------------------------------------------------------- 1 | app.service "modelRepo", () -> 2 | 3 | class Member 4 | constructor: (@id, @name, @modelDef) -> 5 | @values = {} # values per criteria (coming from UI) 6 | @scores = {} # updated during scoring 7 | @valueErrors = {} # error messages for given values 8 | for own id, crit of @modelDef.criterias 9 | @values[id] = crit.type.default 10 | @equity = 0 11 | 12 | 13 | # each model has own instance of a team 14 | class Team 15 | constructor: (@modelDef) -> 16 | @members = [] 17 | @messages = [] # messages comfing from grading function 18 | 19 | addMember: (id, name) -> 20 | member = new Member(id, name, @modelDef) 21 | @members.push(member) 22 | member 23 | 24 | removeMember: (index) -> 25 | @members.splice(index, 1) 26 | 27 | 28 | class Model 29 | constructor: (@modelDef) -> 30 | @emphasis = {} 31 | @emphasisErrors = {} 32 | @hasInputErrors = false 33 | @init() 34 | 35 | init: () -> 36 | @team = new Team(@modelDef) 37 | for own id, e of @modelDef.emphasis 38 | @emphasis[id] = e.type.default 39 | 40 | score: -> 41 | @hasInputErrors = false 42 | # check errors in the values 43 | for m in @team.members 44 | for own k, v of m.values 45 | t = @modelDef.criterias[k].type 46 | error = t.check(v) 47 | if error 48 | m.valueErrors[k] = error 49 | @hasInputErrors = true 50 | else if m.valueErrors[k] 51 | delete m.valueErrors[k] 52 | for own k, v of @emphasis 53 | t = @modelDef.emphasis[k].type 54 | error = t.check(v) 55 | if error 56 | @emphasisErrors[k] = error 57 | @hasInputErrors = true 58 | else if @emphasisErrors[k] 59 | delete @emphasisErrors[k] 60 | 61 | # score 62 | @team.messages = [] 63 | @modelDef.score(@modelDef, @team.members, @team.messages, @emphasis) 64 | 65 | # divide even if all get score 0 66 | allZeros = true 67 | for member in @team.members 68 | if member.equity != 0 69 | allZeros = false 70 | break 71 | if allZeros 72 | slice = Math.round(100 / @team.members.length * 10) / 10 73 | for member in @team.members 74 | member.equity = slice 75 | 76 | # angular seems to lose object key order in iteration, get order from here instead 77 | getCriteriaIds: -> (id for own id, v of @modelDef.criterias) 78 | getEmphasisIds: () -> (id for own id, v of @emphasis) 79 | 80 | class ModelRepo 81 | constructor: (@modelDefList) -> 82 | @defaultMemberIds = "ABCDEFGHIJKLMN" # used as an array 83 | @usedMemberIds = [] 84 | @models = [] 85 | for def in @modelDefList 86 | @createModel(def) 87 | @activeModel = @models[0] 88 | 89 | init: () -> 90 | @usedMemberIds = [] 91 | for model in @models 92 | model.init() 93 | 94 | createModel: (modelDef) -> 95 | @models.push(new Model(modelDef)) 96 | 97 | isTeamAtMax: -> @memberCount() >= 6 98 | isTeamAtMin: -> @memberCount() <= 2 99 | memberCount: -> @usedMemberIds.length 100 | 101 | getNextMemberId: -> 102 | for mid in @defaultMemberIds 103 | if mid not in @usedMemberIds 104 | return mid 105 | return 'X' 106 | 107 | addMember: (memberId, memberName) -> 108 | return if @isTeamAtMax() 109 | mid = memberId or @getNextMemberId() 110 | @usedMemberIds.push(mid) 111 | mname = memberName or "Member #{mid}" 112 | for model in @models 113 | model.team.addMember(mid, mname) 114 | 115 | removeMember: (index) -> 116 | return if @isTeamAtMin() 117 | @usedMemberIds.splice(index, 1) 118 | for model in @models 119 | model.team.removeMember(index) 120 | 121 | syncMemberNames: (fromModel) -> 122 | members = fromModel.team.members 123 | for model in @models 124 | if model != fromModel 125 | for m, i in model.team.members 126 | m.name = members[i].name 127 | 128 | serialize: -> 129 | sRepo = 130 | activeModelId: @activeModel.modelDef.id 131 | models: {} 132 | for model in @models 133 | sRepo.models[model.modelDef.id] = sModel = {} 134 | sModel.version = model.modelDef.version 135 | sModel.name = model.modelDef.name 136 | sModel.team = {} 137 | sModel.team.members = [] 138 | for member in model.team.members 139 | sMember = {} 140 | sMember.id = member.id 141 | sMember.name = member.name 142 | sMember.values = member.values 143 | sModel.team.members.push(sMember) 144 | sModel.emphasis = {} 145 | for own k, v of model.emphasis 146 | sModel.emphasis[k] = v 147 | return sRepo 148 | 149 | deserialize: (sRepo) -> 150 | @init() 151 | errors = [] 152 | assert = (field, val, typ) -> 153 | if typeof(val) != typ 154 | errors.push "field #{field}: #{val} is not of type #{typ}" 155 | try 156 | firstModel = true 157 | assert('sRepo', sRepo, 'object') 158 | assert('sRepo.activeModelId', sRepo.activeModelId, 'string') 159 | assert('sRepo', sRepo.models, 'object') 160 | for own k, v of sRepo.models 161 | [id, sModel] = [k, v] 162 | assert('sModel.id', id, 'string') 163 | assert('sModel', sModel, 'object') 164 | modelDeserialized = false 165 | for model in @models 166 | if model.modelDef.id != id 167 | continue 168 | if sRepo.activeModelId == id 169 | @activeModel = model 170 | 171 | assert('sModel.version', sModel.version, 'string') 172 | majorVersion = parseInt(model.modelDef.version.split('.')[0]) 173 | sMajorVersion = parseInt(sModel.version.split('.')[0]) 174 | if majorVersion != sMajorVersion 175 | errors.push "Major versions differ in model #{id}" 176 | assert('sModel.team', sModel.team, 'object') 177 | assert('sModel.team.members', sModel.team.members, 'object') 178 | for sMember in sModel.team.members 179 | if firstModel 180 | assert('sMember.id', sMember.id, 'string') 181 | @usedMemberIds.push(sMember.id) 182 | 183 | assert('sMember.name', sMember.name, 'string') 184 | member = model.team.addMember(sMember.id, sMember.name) 185 | 186 | assert('sMember.values', sMember.values, 'object') 187 | member.values = sMember.values 188 | 189 | if sModel.emphasis 190 | assert('sModel.emphasis', sModel.emphasis, 'object') 191 | for own k, v of sModel.emphasis 192 | model.emphasis[k] = v 193 | modelDeserialized = true 194 | 195 | if not modelDeserialized 196 | errors.push "Couldn't find model '#{name}' to deserialize into" 197 | firstModel = false 198 | if errors.length > 0 199 | console.error "Errors while deserializing:\n -", errors.join('\n - ') 200 | errors.length == 0 201 | 202 | @createRepo = (modelDefList) -> new ModelRepo(modelDefList) 203 | return @ 204 | -------------------------------------------------------------------------------- /app/coffee/services/modelRepoSpec.coffee: -------------------------------------------------------------------------------- 1 | describe 'modelRepo', -> 2 | testModelDef = 3 | id: 'test' 4 | name: 'test model' 5 | version: '0.1' 6 | summary: '' 7 | desc: '' 8 | image: '' 9 | initScores: (scores) -> scores.test = false 10 | criterias: 11 | test: 12 | text: "Testing" 13 | type: types.checkbox() 14 | score: (scores, value) -> scores.test = value 15 | emphasis: 16 | foo: 17 | text: "Foo" 18 | type: types.number(default: 100, min: 1, max: 250, unit: '%') 19 | grade: (team) -> 20 | for member in team.members 21 | member.equity = Math.round(100 / team.members.length) or 0 22 | team.messages.push 'Test message' 23 | 24 | defs = [testModelDef] 25 | createRepo = null 26 | repo = null 27 | 28 | beforeEach -> 29 | module('piechopper') 30 | inject (modelRepo) -> 31 | createRepo = modelRepo.createRepo 32 | repo = createRepo(defs) 33 | 34 | it "should have one activated model after init", -> 35 | expect(repo.models.length).toBe(1) 36 | expect(repo.activeModel.modelDef.id).toBe('test') 37 | 38 | it "should allow addition and removal of members", -> 39 | repo.addMember() for [1..5] 40 | expect(repo.memberCount()).toBe(5) 41 | expect(repo.isTeamAtMax()).toBe(false) 42 | expect(repo.isTeamAtMin()).toBe(false) 43 | 44 | repo.addMember() 45 | expect(repo.memberCount()).toBe(6) 46 | expect(repo.isTeamAtMax()).toBe(true) 47 | 48 | repo.addMember() 49 | expect(repo.memberCount()).toBe(6) 50 | expect(repo.isTeamAtMax()).toBe(true) 51 | 52 | repo.removeMember() for [1..3] 53 | expect(repo.memberCount()).toBe(3) 54 | expect(repo.isTeamAtMin()).toBe(false) 55 | 56 | repo.removeMember() 57 | expect(repo.memberCount()).toBe(2) 58 | expect(repo.isTeamAtMin()).toBe(true) 59 | 60 | repo.removeMember() 61 | expect(repo.memberCount()).toBe(2) 62 | expect(repo.isTeamAtMin()).toBe(true) 63 | 64 | it "should create unique member names on member additions", -> 65 | repo.addMember() for [1..2] 66 | members = repo.activeModel.team.members 67 | expect(members[0].name).toBe('Member A') 68 | expect(members[1].name).toBe('Member B') 69 | 70 | # are member names correctly re-used? 71 | repo.removeMember(1) 72 | repo.addMember() 73 | expect(members[1].name).toBe('Member B') 74 | 75 | it "should change member names in all models if one changes", -> 76 | testName = "Teemu Testi" 77 | repo.addMember() for [1..6] 78 | repo.activeModel.team.members[0].name = testName 79 | repo.syncMemberNames(repo.activeModel) 80 | for m in repo.models 81 | do (m) -> 82 | expect(m.team.members[0].name).toBe(testName) 83 | 84 | it "should serialize & deserialize properly", -> 85 | testName = "Teemu Testi" 86 | repo.addMember() for [1..3] 87 | members1 = repo.activeModel.team.members 88 | members1[0].values.test = true 89 | members1[1].values.test = false 90 | members1[0].name = testName 91 | repo.activeModel.emphasis.foo = 3 92 | 93 | sRepo = repo.serialize() 94 | expect(typeof(sRepo)).toBe('object') 95 | 96 | # create completely new repo and deserialize it 97 | repo2 = createRepo(defs) 98 | success = repo2.deserialize(sRepo) 99 | expect(success).toBe(true) 100 | expect(repo2.activeModel.modelDef.id).toBe(repo.activeModel.modelDef.id) 101 | members2 = repo2.activeModel.team.members 102 | expect(members2.length).toBe(3) 103 | expect(members2[0].values.test).toBe(true) 104 | expect(members2[1].values.test).toBe(false) 105 | expect(members2[0].name).toBe(testName) 106 | expect(repo2.memberCount()).toBe(3) 107 | expect(repo2.activeModel.emphasis.foo).toBe(3) 108 | -------------------------------------------------------------------------------- /app/coffee/services/trace.coffee: -------------------------------------------------------------------------------- 1 | app.service "trace", ($window) -> 2 | traceCounter = {} 3 | 4 | @createGaArray = (params, prio) -> 5 | params = params.split(':') 6 | if params[0].length == 0 7 | params = [] 8 | if params.length > 3 9 | params = params[0..2] 10 | while params.length < 3 11 | params.push null 12 | params.push(prio) 13 | ['_trackEvent'].concat(params) 14 | 15 | @trace = (params, prio) -> 16 | if traceCounter[params] == undefined 17 | traceCounter[params] = 1 18 | else 19 | traceCounter[params] += 1 20 | arr = @createGaArray(params, prio) 21 | if $window._gaq 22 | $window._gaq.push(arr) 23 | arr 24 | 25 | @traceFirst = (params, prio) -> 26 | return [] if traceCounter[params] == 1 27 | @trace(params, prio) 28 | 29 | @addMember = () -> @traceFirst('Click:Add / Remove Member', 10) 30 | @removeMember = () -> @traceFirst('Click:Add / Remove Member', 10) 31 | @changeMemberName = () -> @traceFirst('Click:Change Member Name', 20) 32 | @openEmail = () -> @traceFirst('Click:Open Email', 20) 33 | @openTwitter = () -> @traceFirst('Click:Open Twitter', 5) 34 | @openGithub = () -> @traceFirst('Click:Open Github', 5) 35 | @showAbout = () -> @traceFirst('Click:Show About', 5) 36 | @showTos = () -> @traceFirst('Click:Show TOS', 10) 37 | @wantMoreResults = () -> @trace('Click:Show More Results', 80) 38 | @cancelMoreResults = () -> @trace('Click:Cancel More Results', -80) 39 | @loadOwnProposal = () -> @trace('Persist:Load Own Proposal', 60) 40 | @loadPartnerProposal = () -> @trace('Persist:Load Partner Proposal', 100) 41 | @shareProposal = () -> @trace('Persist:Share Prososal', 80) 42 | @sendQuestionary = () -> @trace('Persist:Send Questionary', 150) 43 | 44 | 45 | # TODO: add these to tracing 46 | @changeContrib = () -> @traceFirst('Click:Change Contrib', 1) 47 | @changeEmphasis = () -> @traceFirst('Click:Change Emphasis', 2) 48 | @switchModel = () -> @traceFirst('Click:Switch Model', 10, 3) 49 | 50 | return @ 51 | -------------------------------------------------------------------------------- /app/coffee/services/traceSpec.coffee: -------------------------------------------------------------------------------- 1 | describe 'Trace', -> 2 | t = null 3 | 4 | beforeEach -> 5 | module('piechopper') 6 | inject (trace) -> 7 | t = trace 8 | 9 | it "Should form proper GA array", -> 10 | expect(t.createGaArray('category:action:opt_label', 1)).toEqual( 11 | [ '_trackEvent', 'category', 'action', 'opt_label', 1 ]) 12 | expect(t.createGaArray('category:action', 1)).toEqual( 13 | [ '_trackEvent', 'category', 'action', null, 1 ]) 14 | expect(t.createGaArray('category', 1)).toEqual( 15 | [ '_trackEvent', 'category', null, null, 1 ]) 16 | expect(t.createGaArray('', 1)).toEqual( 17 | [ '_trackEvent', null, null, null, 1 ]) 18 | expect(t.createGaArray('category:action:opt_label:extra', 1)).toEqual( 19 | [ '_trackEvent', 'category', 'action', 'opt_label', 1 ]) 20 | expect(t.createGaArray('category:action:opt_label:extra:extra2', 1)).toEqual( 21 | [ '_trackEvent', 'category', 'action', 'opt_label', 1 ]) 22 | 23 | it "Should trace properly", -> 24 | expect(t.trace('foo:bar', 2)).toEqual(['_trackEvent', 'foo', 'bar', null, 2]) 25 | expect(t.traceFirst('onlyOnce', 10)).toEqual(['_trackEvent', 'onlyOnce', null, null, 10]) 26 | expect(t.traceFirst('onlyOnce', 10)).toEqual([]) 27 | expect(t.traceFirst('onlyOnce', 20)).toEqual([]) 28 | 29 | it "Should handle facade methods the right way", -> 30 | expect(t.addMember()).toEqual(['_trackEvent', 'Click', 'Add / Remove Member', null, 10]) 31 | expect(t.addMember()).toEqual([]) 32 | -------------------------------------------------------------------------------- /app/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PieChopper - Oops 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
PieChopper logo
13 |
14 |

Oops, that page doesn't exist!

15 |

Don't worry though, click here to chop your pie.

16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-piechopper/143df856de3b982c077da99852e5bdb78330ccc4/app/images/background.png -------------------------------------------------------------------------------- /app/images/copyrights.txt: -------------------------------------------------------------------------------- 1 | background.png from http://subtlepatterns.com/debut-light/ -> license = http://creativecommons.org/licenses/by-sa/3.0/deed.en_US 2 | -------------------------------------------------------------------------------- /app/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-piechopper/143df856de3b982c077da99852e5bdb78330ccc4/app/images/favicon.ico -------------------------------------------------------------------------------- /app/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/images/piechopper_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-piechopper/143df856de3b982c077da99852e5bdb78330ccc4/app/images/piechopper_logo.png -------------------------------------------------------------------------------- /app/images/piechopper_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | image/svg+xml 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | PieChopper - Chop your startup equity 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 |
17 |
22 |
23 |
24 | 25 | 26 |
27 | 28 |
29 | 30 |
31 | 32 | 33 | 34 |
35 | 36 |
37 | 38 |
39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/partials/contrib-table.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

How do you contribute?

4 |

5 | Consider each member's contribution below. You may add or remove members (with and buttons) to match your team's headcount. You may also rename members by clicking their name. In case you feel that some rows don't apply in your situation, leave them blank. If you cannot find clear differences between member contributions, it might be a symptom of ill responsibility definition in the company. In that case, use some time to define the role of each member more precisely. 6 |

7 |
8 | 9 | 10 | 11 | 18 | 28 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 42 | 55 | 63 | 64 | 65 |
12 |
13 | 14 | 15 | 16 |
17 |
21 |
22 | 23 | 24 | 25 | 26 |
27 |
34 | {{ member.equity }} % 35 | - 36 |
43 |
44 |
45 | {{ criteria(id).text }} 46 |
47 |
51 | 52 |
53 |
54 |
61 |
{{ member.valueErrors[id] }}
62 |
66 |
67 |
68 | 69 | 70 |
71 |
72 |
73 | -------------------------------------------------------------------------------- /app/partials/emphasis-table.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Want to tweak it?

4 |

5 | In this section, you can tweak the selected method to match your team's opinion on fair equity sharing. You may want to adjust the default values due to the market situation in your location, your principles and ethics, or your financial situation. In case you don't have any preference, continue to the next section. 6 |

7 |
8 | 9 | 10 | 11 | 14 | 17 | 18 | 19 | 20 | 22 | 23 | 29 | 30 | 31 |
12 |
What to value?
13 |
15 |
Value
16 |
{{ model().modelDef.emphasis[id].text }} 27 |
{{ model().emphasisErrors[id] }}
28 |
32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /app/partials/footer.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /app/partials/ga.html: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | -------------------------------------------------------------------------------- /app/partials/header.html: -------------------------------------------------------------------------------- 1 | 20 | 21 |
22 |
23 |
24 |

25 | PieChopper assists startup teams to share their equity fair and square.
It's free to use, and its source code is available in GitHub under MIT license. 26 |

27 |
28 |
29 |
30 | -------------------------------------------------------------------------------- /app/partials/model-selection.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

How to chop it?

4 |
5 |

6 | There are various ways to divide the equity. Select the method that is the closest match to your situation and thinking. You may also try out alternative methods, and see how their outcomes differ. In case you don't have any preference, continue to the next section. 7 |

8 |
9 | 30 |
31 |
32 | -------------------------------------------------------------------------------- /app/partials/overview.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |

Chop your startup equity

7 |

Slice your founders' pie fairly.
Share your proposals.

8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /app/partials/questionary.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Want some more?

4 |
5 |

Piechopper is currently under development and benefits greatly from your help. 6 | Answering the questions below assists in setting the right direction, and it only takes a few seconds. 7 | Thank you for your help!

8 |
9 |
10 |
11 | Would you like to see some of the following features: 12 |
    13 |
  • 14 | Get legally binding agreement easily out from the service:
    15 | Yes 16 | No 17 |
  • 18 |
  • 19 | See vesting as part of the proposal:
    20 | Yes 21 | No 22 |
  • 23 |
  • 24 | Take investment rounds into account:
    25 | Yes 26 | No 27 |
  • 28 |
  • 29 | Adjust equity periodically (e.g. once per month) based on used effort:
    30 | Yes 31 | No 32 |
  • 33 |
  • 34 | See the difference between multiple proposals:
    35 | Yes 36 | No 37 |
  • 38 |
  • 39 | Comment your partner's proposal in detail:
    40 | Yes 41 | No 42 |
  • 43 |
  • 44 | Change the methods without programming:
    45 | Yes 46 | No 47 |
  • 48 |
49 |
50 |
51 |

52 | Would you be willing to pay on some of the features?
53 | 54 |

55 |

56 | Freeform feedback:
57 | 58 |

59 |

60 | Leave your email to get an invite to upcoming features:
61 | 62 |

63 |
64 |
65 |
66 | 67 | 68 |
69 |
70 |
71 |

Thank you for your feedback!

72 |

Click 73 | here if you want to modify the questionary.

74 |
75 |
76 | -------------------------------------------------------------------------------- /app/partials/results.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Results

4 |
5 |
6 |

Based on the given data, this is how the "{{ model().modelDef.name }}"-method suggests you to chop your equity.

7 |
8 |
9 |
10 |

Equity per Member

11 |
    12 |
  • 13 | {{ member.name }}: {{ member.equity }} % 14 |
  • 15 |
16 |
17 |
18 |

The Pie

19 | 20 |
21 |
22 |

Remarks

23 |
    24 |
  • 25 | No remarks 26 |
  • 27 |
  • 28 | {{ message }} 29 |
  • 30 |
31 |
32 |
33 |
34 |
35 |
36 |

Your input seems to contain errors. Please fix the inputs above, errors are pointed with red color.

37 |
38 |
39 |
40 |
41 | 42 | 43 |
44 | 52 |
53 |
54 |
55 | -------------------------------------------------------------------------------- /app/partials/scripts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/partials/styles.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 | 25 | -------------------------------------------------------------------------------- /app/partials/tos.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /app/style/style.scss: -------------------------------------------------------------------------------- 1 | // ------- generic ------- 2 | $hint-color: #ccc; 3 | $dark-bg: darken(#ecf0f1, 4%); 4 | $action-color: #18bc9c; 5 | $dark-highlight: #2c3e50; 6 | $medium-highlight: lighten($dark-highlight, 5%); 7 | $input-border-color: #dce4ec; 8 | $error-color: lighten(#e95d4e, 15%); 9 | 10 | hr { 11 | height: 1px; 12 | background-color: lighten($dark-highlight, 50%); 13 | } 14 | .clickable { cursor: pointer; } 15 | .dark-section { 16 | background-color: $dark-bg; 17 | hr { 18 | background-color: lighten($dark-highlight, 30%); 19 | } 20 | } 21 | #top { 22 | .brand { font-family: 'Chango', cursive; } 23 | .btn-navbar { margin-top: 13px; } 24 | } 25 | .action-colored { color: $action-color; } 26 | 27 | body { 28 | background-image:url('../img/background.png'); 29 | } 30 | 31 | // override bootstrap-responsive's percentage margins 32 | .no-intendation { 33 | margin-left: 0 !important; 34 | } 35 | 36 | .proceed-actions { 37 | @extend .no-intendation; 38 | margin: 40px 0 70px 0; 39 | text-align: center; 40 | .btn { 41 | min-width: 160px; 42 | margin: 0 10px 10px 10px; 43 | background-color: $medium-highlight; 44 | } 45 | } 46 | 47 | .error-dlg { 48 | padding: 5px 20px 20px 20px; 49 | background-color: $dark-bg; 50 | border-radius: 5px; 51 | } 52 | 53 | .model-section { 54 | padding: 25px 0 20px 0; 55 | h2 { 56 | padding-bottom: 10px; 57 | } 58 | .model-section-desc { 59 | margin-bottom: 30px; 60 | } 61 | } 62 | 63 | .carousel-control, .carousel-indicators li { 64 | cursor: pointer; 65 | } 66 | 67 | .model-selector { 68 | border: 1px solid lighten($dark-highlight, 50%); 69 | padding-top: 20px; 70 | } 71 | .model-selector-desc { 72 | padding-top: 20px; 73 | } 74 | 75 | table { 76 | background-color: #fff; 77 | .cell-centered { 78 | text-align: center; 79 | vertical-align: middle; 80 | } 81 | .cell-wrap { position: relative; } 82 | thead tr { height: 60px; } 83 | td { 84 | position: relative; 85 | &.invalid { 86 | background-color: $error-color !important; 87 | } 88 | 89 | input[type='number'], select { margin: 0; } 90 | input[type='number'] { width: 55%; } 91 | select { width: 75%; } 92 | .cell-error-msg { 93 | font-size: 80%; 94 | text-decoration: italic; 95 | margin-bottom: 5px; 96 | } 97 | 98 | span.value-unit { 99 | color: $hint-color; 100 | margin-left: 3px; 101 | font-size: 15px; 102 | } 103 | 104 | div.criteria-info { 105 | color: $hint-color; 106 | position: absolute; 107 | right: 10px; 108 | top: 0px; 109 | width: 100px; 110 | text-align: right; 111 | z-index: 10; 112 | } 113 | } 114 | } 115 | 116 | #slice-graph { 117 | position: fixed; 118 | } 119 | 120 | 121 | #about-section { 122 | text-align: center; 123 | padding: 10px 0 0 0; 124 | border-bottom: 1px solid; 125 | border-left: 1px solid; 126 | border-right: 1px solid; 127 | border-bottom-left-radius: 10px; 128 | border-bottom-right-radius: 10px; 129 | } 130 | 131 | #overview-section { 132 | text-align: center; 133 | padding-top: 100px; 134 | h1 { margin-bottom: 15px; } 135 | .overview-logo { 136 | width: 400px; 137 | } 138 | } 139 | 140 | #model-selection-section { 141 | .carousel-img { 142 | margin: auto; 143 | width: 300px; 144 | height: 200px; 145 | } 146 | .carousel-indicators li { 147 | background-color: white; 148 | &.active { 149 | background-color: $medium-highlight; 150 | } 151 | } 152 | } 153 | 154 | #contrib-section { 155 | tr.result { 156 | font-weight: 600; 157 | } 158 | .member-add-btn, .member-remove-btn { 159 | cursor: pointer; 160 | color: $action-color; 161 | position: absolute; 162 | z-index: 10; 163 | padding: 5px; 164 | } 165 | .member-add-btn { 166 | right: 0; 167 | top: -28px; 168 | } 169 | .member-remove-btn { 170 | top: -32px; 171 | right: 0; 172 | text-align: right; 173 | } 174 | } 175 | 176 | #questionary-section { 177 | form { 178 | margin: 30px 0 0 0; 179 | } 180 | ul { 181 | list-style-type: none; 182 | padding: 0; 183 | margin: 10px 0 20px 20px; 184 | li { 185 | margin-top: 7px; 186 | font-style: italic; 187 | input { 188 | margin: -2px 5px 0 10px; 189 | } 190 | } 191 | } 192 | textarea, input[type="email"] { 193 | margin-top: 5px; 194 | width: 95%; 195 | max-width: 420px; 196 | } 197 | textarea { 198 | height: 85px; 199 | } 200 | } 201 | 202 | #footer { 203 | background-color: $dark-highlight; 204 | padding: 10px 65px 50px 65px; 205 | .links a { 206 | padding-right: 20px; 207 | } 208 | } 209 | 210 | @media (max-width: 978px) { 211 | .container { 212 | padding: 0 15px 0 15px; 213 | margin: 0; 214 | } 215 | 216 | body { 217 | padding: 0; 218 | } 219 | 220 | .navbar-fixed-top, .navbar-fixed-bottom, .navbar-static-top { 221 | margin-left: 0; 222 | margin-right: 0; 223 | margin-bottom: 0; 224 | } 225 | 226 | .i-slice-graph { 227 | visibility: hidden; 228 | } 229 | } 230 | 231 | @media (max-width:767px) { 232 | #overview-section .overview-logo { max-width: 200px; } 233 | } -------------------------------------------------------------------------------- /app/vendor/fonts/chango.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Chango'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: local('Chango Regular'), local('Chango-Regular'), url(../fonts/chango.woff) format('woff'); 6 | } 7 | -------------------------------------------------------------------------------- /app/vendor/fonts/chango.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-piechopper/143df856de3b982c077da99852e5bdb78330ccc4/app/vendor/fonts/chango.woff -------------------------------------------------------------------------------- /app/vendor/jreject/browsers/background_browser.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-piechopper/143df856de3b982c077da99852e5bdb78330ccc4/app/vendor/jreject/browsers/background_browser.gif -------------------------------------------------------------------------------- /app/vendor/jreject/browsers/browser_chrome.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-piechopper/143df856de3b982c077da99852e5bdb78330ccc4/app/vendor/jreject/browsers/browser_chrome.gif -------------------------------------------------------------------------------- /app/vendor/jreject/browsers/browser_firefox.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-piechopper/143df856de3b982c077da99852e5bdb78330ccc4/app/vendor/jreject/browsers/browser_firefox.gif -------------------------------------------------------------------------------- /app/vendor/jreject/browsers/browser_msie.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cypress-io/cypress-example-piechopper/143df856de3b982c077da99852e5bdb78330ccc4/app/vendor/jreject/browsers/browser_msie.gif -------------------------------------------------------------------------------- /app/vendor/jreject/jquery.reject.css: -------------------------------------------------------------------------------- 1 | /* 2 | * jReject (jQuery Browser Rejection Plugin) 3 | * Version 1.0.0 4 | * URL: http://jreject.turnwheel.com/ 5 | * Description: jReject is a easy method of rejecting specific browsers on your site 6 | * Author: Steven Bower (TurnWheel Designs) http://turnwheel.com/ 7 | * Copyright: Copyright (c) 2009-2011 Steven Bower under dual MIT/GPL license. 8 | */ 9 | 10 | #jr_overlay { 11 | top: 0; 12 | left: 0; 13 | padding: 0; 14 | margin: 0; 15 | z-index: 200; 16 | position: absolute; 17 | } 18 | 19 | #jr_wrap { 20 | position: absolute; 21 | text-align: center; 22 | width: 100%; 23 | z-index: 300; 24 | padding: 0; 25 | margin: 0; 26 | } 27 | 28 | #jr_inner { 29 | font-family: "Lucida Grande","Lucida Sans Unicode",Arial,Verdana,sans-serif; 30 | font-size: 12px; 31 | background: #FFF; 32 | border: 1px solid #CCC; 33 | color: #4F4F4F; 34 | margin: 0 auto; 35 | height: auto; 36 | padding: 20px; 37 | position: relative; 38 | } 39 | 40 | #jr_header { 41 | display: block; 42 | color: #333; 43 | padding: 5px; 44 | padding-bottom: 0; 45 | margin: 0; 46 | font-family: Helvetica,Arial,sans-serif; 47 | font-weight: bold; 48 | text-align: left; 49 | font-size: 1.3em; 50 | margin-bottom: 0.5em; 51 | } 52 | 53 | #jr_inner p { 54 | text-align: left; 55 | padding: 5px; 56 | margin: 0; 57 | } 58 | 59 | #jr_inner ul { 60 | list-style-image: none; 61 | list-style-position: outside; 62 | list-style-type: none; 63 | margin: 0; 64 | padding: 0; 65 | } 66 | 67 | #jr_inner ul li { 68 | cursor: pointer; 69 | float: left; 70 | width: 120px; 71 | height: 122px; 72 | margin: 0 10px 10px 10px; 73 | padding: 0; 74 | text-align: center; 75 | } 76 | 77 | #jr_inner li a { 78 | color: #333; 79 | font-size: 0.8em; 80 | text-decoration: none; 81 | padding: 0; 82 | margin: 0; 83 | } 84 | 85 | #jr_inner li a:hover { 86 | text-decoration: underline; 87 | } 88 | 89 | #jr_inner .jr_icon { 90 | width: 100px; 91 | height: 100px; 92 | margin: 1px auto; 93 | padding: 0; 94 | background: transparent no-repeat scroll left top; 95 | cursor: pointer; 96 | } 97 | 98 | #jr_close { 99 | margin: 0 0 0 50px; 100 | clear: both; 101 | text-align: left; 102 | padding: 0; 103 | margin: 0; 104 | } 105 | 106 | #jr_close a { 107 | color: #000; 108 | display: block; 109 | width: auto; 110 | margin: 0; 111 | padding: 0; 112 | text-decoration: underline; 113 | } 114 | 115 | #jr_close p { 116 | padding: 10px 0 0 0; 117 | margin: 0; 118 | } 119 | -------------------------------------------------------------------------------- /app/vendor/jreject/jquery.reject.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jReject (jQuery Browser Rejection Plugin) 3 | * Version 1.0.2 4 | * URL: http://jreject.turnwheel.com/ 5 | * Description: jReject is a easy method of rejecting specific browsers on your site 6 | * Author: Steven Bower (TurnWheel Designs) http://turnwheel.com/ 7 | * Copyright: Copyright (c) 2009-2013 Steven Bower under dual MIT/GPLv2 license. 8 | */ 9 | 10 | (function($) { 11 | $.reject = function(options) { 12 | var opts = $.extend(true,{ 13 | reject : { // Rejection flags for specific browsers 14 | all: false, // Covers Everything (Nothing blocked) 15 | msie5: true, msie6: true // Covers MSIE 5-6 (Blocked by default) 16 | /* 17 | * Possibilities are endless... 18 | * 19 | * // MSIE Flags (Global, 5-8) 20 | * msie, msie5, msie6, msie7, msie8, 21 | * // Firefox Flags (Global, 1-3) 22 | * firefox, firefox1, firefox2, firefox3, 23 | * // Konqueror Flags (Global, 1-3) 24 | * konqueror, konqueror1, konqueror2, konqueror3, 25 | * // Chrome Flags (Global, 1-4) 26 | * chrome, chrome1, chrome2, chrome3, chrome4, 27 | * // Safari Flags (Global, 1-4) 28 | * safari, safari2, safari3, safari4, 29 | * // Opera Flags (Global, 7-10) 30 | * opera, opera7, opera8, opera9, opera10, 31 | * // Rendering Engines (Gecko, Webkit, Trident, KHTML, Presto) 32 | * gecko, webkit, trident, khtml, presto, 33 | * // Operating Systems (Win, Mac, Linux, Solaris, iPhone) 34 | * win, mac, linux, solaris, iphone, 35 | * unknown // Unknown covers everything else 36 | */ 37 | }, 38 | display: [], // What browsers to display and their order (default set below) 39 | browserShow: true, // Should the browser options be shown? 40 | browserInfo: { // Settings for which browsers to display 41 | firefox: { 42 | text: 'Mozilla Firefox', // Text below the icon 43 | url: 'http://www.mozilla.com/firefox/' // URL For icon/text link 44 | }, 45 | chrome: { 46 | text: 'Google Chrome', 47 | url: 'http://www.google.com/chrome/' 48 | }, 49 | safari: { 50 | text: 'Safari 5', 51 | url: 'http://www.apple.com/safari/download/' 52 | }, 53 | opera: { 54 | text: 'Opera 12', 55 | url: 'http://www.opera.com/download/' 56 | }, 57 | msie: { 58 | text: 'Internet Explorer 9', 59 | url: 'http://www.microsoft.com/windows/Internet-explorer/' 60 | }, 61 | gcf: { 62 | text: 'Google Chrome Frame', 63 | url: 'http://code.google.com/chrome/chromeframe/', 64 | // This browser option will only be displayed for MSIE 65 | allow: { all: false, msie: true } 66 | } 67 | }, 68 | 69 | // Header of pop-up window 70 | header: 'Did you know that your Internet Browser is out of date?', 71 | // Paragraph 1 72 | paragraph1: 'Your browser is out of date, and may not be compatible with '+ 73 | 'our website. A list of the most popular web browsers can be '+ 74 | 'found below.', 75 | // Paragraph 2 76 | paragraph2: 'Just click on the icons to get to the download page', 77 | close: true, // Allow closing of window 78 | // Message displayed below closing link 79 | closeMessage: 'By closing this window you acknowledge that your experience '+ 80 | 'on this website may be degraded', 81 | closeLink: 'Close This Window', // Text for closing link 82 | closeURL: '#', // Close URL 83 | closeESC: true, // Allow closing of window with esc key 84 | 85 | // If cookies should be used to remmember if the window was closed 86 | // See cookieSettings for more options 87 | closeCookie: false, 88 | // Cookie settings are only used if closeCookie is true 89 | cookieSettings: { 90 | // Path for the cookie to be saved on 91 | // Should be root domain in most cases 92 | path: '/', 93 | // Expiration Date (in seconds) 94 | // 0 (default) means it ends with the current session 95 | expires: 0 96 | }, 97 | 98 | imagePath: './images/', // Path where images are located 99 | overlayBgColor: '#000', // Background color for overlay 100 | overlayOpacity: 0.8, // Background transparency (0-1) 101 | 102 | // Fade in time on open ('slow','medium','fast' or integer in ms) 103 | fadeInTime: 'fast', 104 | // Fade out time on close ('slow','medium','fast' or integer in ms) 105 | fadeOutTime: 'fast', 106 | 107 | // Google Analytics Link Tracking (Optional) 108 | // Set to true to enable 109 | // Note: Analytics tracking code must be added separately 110 | analytics: false 111 | }, options); 112 | 113 | // Set default browsers to display if not already defined 114 | if (opts.display.length < 1) { 115 | opts.display = ['chrome','firefox','safari','opera','gcf','msie']; 116 | } 117 | 118 | // beforeRject: Customized Function 119 | if ($.isFunction(opts.beforeReject)) { 120 | opts.beforeReject(); 121 | } 122 | 123 | // Disable 'closeESC' if closing is disabled (mutually exclusive) 124 | if (!opts.close) { 125 | opts.closeESC = false; 126 | } 127 | 128 | // This function parses the advanced browser options 129 | var browserCheck = function(settings) { 130 | // Check 1: Look for 'all' forced setting 131 | // Check 2: Operating System (eg. 'win','mac','linux','solaris','iphone') 132 | // Check 3: Rendering engine (eg. 'webkit', 'gecko', 'trident') 133 | // Check 4: Browser name (eg. 'firefox','msie','chrome') 134 | // Check 5: Browser+major version (eg. 'firefox3','msie7','chrome4') 135 | return (settings['all'] ? true : false) || 136 | (settings[$.os.name] ? true : false) || 137 | (settings[$.layout.name] ? true : false) || 138 | (settings[$.browser.name] ? true : false) || 139 | (settings[$.browser.className] ? true : false); 140 | }; 141 | 142 | // Determine if we need to display rejection for this browser, or exit 143 | if (!browserCheck(opts.reject)) { 144 | // onFail: Customized Function 145 | if ($.isFunction(opts.onFail)) { 146 | opts.onFail(); 147 | } 148 | 149 | return false; 150 | } 151 | 152 | // If user can close and set to remmember close, initiate cookie functions 153 | if (opts.close && opts.closeCookie) { 154 | // Local global setting for the name of the cookie used 155 | var COOKIE_NAME = 'jreject-close'; 156 | 157 | // Cookies Function: Handles creating/retrieving/deleting cookies 158 | // Cookies are only used for opts.closeCookie parameter functionality 159 | var _cookie = function(name, value) { 160 | // Save cookie 161 | if (typeof value != 'undefined') { 162 | var expires = ''; 163 | 164 | // Check if we need to set an expiration date 165 | if (opts.cookieSettings.expires !== 0) { 166 | var date = new Date(); 167 | date.setTime(date.getTime()+(opts.cookieSettings.expires*1000)); 168 | expires = "; expires="+date.toGMTString(); 169 | } 170 | 171 | // Get path from settings 172 | var path = opts.cookieSettings.path || '/'; 173 | 174 | // Set Cookie with parameters 175 | document.cookie = name+'='+ 176 | encodeURIComponent((!value) ? '' : value)+expires+ 177 | '; path='+path; 178 | 179 | return true; 180 | } 181 | // Get cookie 182 | else { 183 | var cookie,val = null; 184 | 185 | if (document.cookie && document.cookie !== '') { 186 | var cookies = document.cookie.split(';'); 187 | 188 | // Loop through all cookie values 189 | var clen = cookies.length; 190 | for (var i = 0; i < clen; ++i) { 191 | cookie = $.trim(cookies[i]); 192 | 193 | // Does this cookie string begin with the name we want? 194 | if (cookie.substring(0,name.length+1) == (name+'=')) { 195 | var len = name.length; 196 | val = decodeURIComponent(cookie.substring(len+1)); 197 | break; 198 | } 199 | } 200 | } 201 | 202 | // Returns cookie value 203 | return val; 204 | } 205 | }; 206 | 207 | // If cookie is set, return false and don't display rejection 208 | if (_cookie(COOKIE_NAME)) { 209 | return false; 210 | } 211 | } 212 | 213 | // Load background overlay (jr_overlay) + Main wrapper (jr_wrap) + 214 | // Inner Wrapper (jr_inner) w/ opts.header (jr_header) + 215 | // opts.paragraph1/opts.paragraph2 if set 216 | var html = '
'+ 217 | '

'+opts.header+'

'+ 218 | (opts.paragraph1 === '' ? '' : '

'+opts.paragraph1+'

')+ 219 | (opts.paragraph2 === '' ? '' : '

'+opts.paragraph2+'

'); 220 | 221 | if (opts.browserShow) { 222 | html += '
    '; 223 | 224 | var displayNum = 0; 225 | 226 | // Generate the browsers to display 227 | for (var x in opts.display) { 228 | var browser = opts.display[x]; // Current Browser 229 | var info = opts.browserInfo[browser] || false; // Browser Information 230 | 231 | // If no info exists for this browser 232 | // or if this browser is not suppose to display to this user 233 | if (!info || (info['allow'] != undefined && !browserCheck(info['allow']))) { 234 | continue; 235 | } 236 | 237 | var url = info.url || '#'; // URL to link text/icon to 238 | 239 | // Generate HTML for this browser option 240 | html += '
  • '+ 241 | '
  • '; 243 | 244 | ++displayNum; 245 | } 246 | 247 | html += '
'; 248 | } 249 | 250 | // Close list and #jr_list 251 | html += '
'+ 252 | // Display close links/message if set 253 | (opts.close ? ''+opts.closeLink+''+ 254 | '

'+opts.closeMessage+'

' : '')+'
'+ 255 | // Close #jr_inner and #jr_wrap 256 | '
'; 257 | 258 | var element = $('
'+html+'
'); // Create element 259 | var size = _pageSize(); // Get page size 260 | var scroll = _scrollSize(); // Get page scroll 261 | 262 | // This function handles closing this reject window 263 | // When clicked, fadeOut and remove all elements 264 | element.bind('closejr', function() { 265 | // Make sure the permission to close is granted 266 | if (!opts.close) { 267 | return false; 268 | } 269 | 270 | // Customized Function 271 | if ($.isFunction(opts.beforeClose)) { 272 | opts.beforeClose(); 273 | } 274 | 275 | // Remove binding function so it 276 | // doesn't get called more than once 277 | $(this).unbind('closejr'); 278 | 279 | // Fade out background and modal wrapper 280 | $('#jr_overlay,#jr_wrap').fadeOut(opts.fadeOutTime,function() { 281 | $(this).remove(); // Remove element from DOM 282 | 283 | // afterClose: Customized Function 284 | if ($.isFunction(opts.afterClose)) { 285 | opts.afterClose(); 286 | } 287 | }); 288 | 289 | // Show elements that were hidden for layering issues 290 | var elmhide = 'embed.jr_hidden, object.jr_hidden, select.jr_hidden, applet.jr_hidden'; 291 | $(elmhide).show().removeClass('jr_hidden'); 292 | 293 | // Set close cookie for next run 294 | if (opts.closeCookie) { 295 | _cookie(COOKIE_NAME, 'true'); 296 | } 297 | 298 | return true; 299 | }); 300 | 301 | // Tracks clicks in Google Analytics (category 'External Links') 302 | // only if opts.analytics is enabled 303 | var analytics = function (url) { 304 | if (!opts.analytics) return false; 305 | 306 | // Get just the hostname 307 | var host = url.split(/\/+/g)[1]; 308 | 309 | // Send external link event to Google Analaytics 310 | // Attempts both versions of analytics code. (Newest first) 311 | try { 312 | // Newest analytics code 313 | _gaq.push(['_trackEvent', 'External Links', host, url]); 314 | } catch (e) { 315 | try { 316 | // Older analytics code 317 | pageTracker._trackEvent('External Links', host, url); 318 | } catch (e) { } 319 | } 320 | }; 321 | 322 | // Called onClick for browser links (and icons) 323 | // Opens link in new window 324 | var openBrowserLinks = function(url) { 325 | // Send link to analytics if enabled 326 | analytics(url); 327 | 328 | // Open window, generate random id value 329 | window.open(url, 'jr_'+ Math.round(Math.random()*11)); 330 | 331 | return false; 332 | }; 333 | 334 | /* 335 | * Trverse through element DOM and apply JS variables 336 | * All CSS elements that do not require JS will be in 337 | * css/jquery.jreject.css 338 | */ 339 | 340 | // Creates 'background' (div) 341 | element.find('#jr_overlay').css({ 342 | width: size[0], 343 | height: size[1], 344 | background: opts.overlayBgColor, 345 | opacity: opts.overlayOpacity 346 | }); 347 | 348 | // Wrapper for our pop-up (div) 349 | element.find('#jr_wrap').css({ 350 | top: scroll[1]+(size[3]/4), 351 | left: scroll[0] 352 | }); 353 | 354 | // Wrapper for inner centered content (div) 355 | element.find('#jr_inner').css({ 356 | minWidth: displayNum*100, 357 | maxWidth: displayNum*140, 358 | // min/maxWidth not supported by IE 359 | width: $.layout.name == 'trident' ? displayNum*155 : 'auto' 360 | }); 361 | 362 | element.find('#jr_inner li').css({ // Browser list items (li) 363 | background: 'transparent url("'+opts.imagePath+'background_browser.gif")'+ 364 | 'no-repeat scroll left top' 365 | }); 366 | 367 | element.find('#jr_inner li .jr_icon').each(function() { 368 | // Dynamically sets the icon background image 369 | var self = $(this); 370 | self.css('background','transparent url('+opts.imagePath+'browser_'+ 371 | (self.parent('li').attr('id').replace(/jr_/,''))+'.gif)'+ 372 | ' no-repeat scroll left top'); 373 | 374 | // Send link clicks to openBrowserLinks 375 | self.click(function () { 376 | var url = $(this).next('div').children('a').attr('href'); 377 | openBrowserLinks(url); 378 | }); 379 | }); 380 | 381 | element.find('#jr_inner li a').click(function() { 382 | openBrowserLinks($(this).attr('href')); 383 | return false; 384 | }); 385 | 386 | // Bind closing event to trigger closejr 387 | // to be consistant with ESC key close function 388 | element.find('#jr_close a').click(function() { 389 | $(this).trigger('closejr'); 390 | 391 | // If plain anchor is set, return false so there is no page jump 392 | if (opts.closeURL === '#') { 393 | return false; 394 | } 395 | }); 396 | 397 | // Set focus (fixes ESC key issues with forms and other focus bugs) 398 | $('#jr_overlay').focus(); 399 | 400 | // Hide elements that won't display properly 401 | $('embed, object, select, applet').each(function() { 402 | if ($(this).is(':visible')) { 403 | $(this).hide().addClass('jr_hidden'); 404 | } 405 | }); 406 | 407 | // Append element to body of document to display 408 | $('body').append(element.hide().fadeIn(opts.fadeInTime)); 409 | 410 | // Handle window resize/scroll events and update overlay dimensions 411 | $(window).bind('resize scroll',function() { 412 | var size = _pageSize(); // Get size 413 | 414 | // Update overlay dimensions based on page size 415 | $('#jr_overlay').css({ 416 | width: size[0], 417 | height: size[1] 418 | }); 419 | 420 | var scroll = _scrollSize(); // Get page scroll 421 | 422 | // Update modal position based on scroll 423 | $('#jr_wrap').css({ 424 | top: scroll[1] + (size[3]/4), 425 | left: scroll[0] 426 | }); 427 | }); 428 | 429 | // Add optional ESC Key functionality 430 | if (opts.closeESC) { 431 | $(document).bind('keydown',function(event) { 432 | // ESC = Keycode 27 433 | if (event.keyCode == 27) { 434 | element.trigger('closejr'); 435 | } 436 | }); 437 | } 438 | 439 | // afterReject: Customized Function 440 | if ($.isFunction(opts.afterReject)) { 441 | opts.afterReject(); 442 | } 443 | 444 | return true; 445 | }; 446 | 447 | // Based on compatibility data from quirksmode.com 448 | var _pageSize = function() { 449 | var xScroll = window.innerWidth && window.scrollMaxX ? 450 | window.innerWidth + window.scrollMaxX : 451 | (document.body.scrollWidth > document.body.offsetWidth ? 452 | document.body.scrollWidth : document.body.offsetWidth); 453 | 454 | var yScroll = window.innerHeight && window.scrollMaxY ? 455 | window.innerHeight + window.scrollMaxY : 456 | (document.body.scrollHeight > document.body.offsetHeight ? 457 | document.body.scrollHeight : document.body.offsetHeight); 458 | 459 | var windowWidth = window.innerWidth ? window.innerWidth : 460 | (document.documentElement && document.documentElement.clientWidth ? 461 | document.documentElement.clientWidth : document.body.clientWidth); 462 | 463 | var windowHeight = window.innerHeight ? window.innerHeight : 464 | (document.documentElement && document.documentElement.clientHeight ? 465 | document.documentElement.clientHeight : document.body.clientHeight); 466 | 467 | return [ 468 | xScroll < windowWidth ? xScroll : windowWidth, // Page Width 469 | yScroll < windowHeight ? windowHeight : yScroll, // Page Height 470 | windowWidth,windowHeight 471 | ]; 472 | }; 473 | 474 | 475 | // Based on compatibility data from quirksmode.com 476 | var _scrollSize = function() { 477 | return [ 478 | // scrollSize X 479 | window.pageXOffset ? window.pageXOffset : (document.documentElement && 480 | document.documentElement.scrollTop ? 481 | document.documentElement.scrollLeft : document.body.scrollLeft), 482 | 483 | // scrollSize Y 484 | window.pageYOffset ? window.pageYOffset : (document.documentElement && 485 | document.documentElement.scrollTop ? 486 | document.documentElement.scrollTop : document.body.scrollTop) 487 | ]; 488 | }; 489 | })(jQuery); 490 | 491 | /* 492 | * jQuery Browser Plugin 493 | * Version 2.4 / jReject 1.0.x 494 | * URL: http://jquery.thewikies.com/browser 495 | * Description: jQuery Browser Plugin extends browser detection capabilities and 496 | * can assign browser selectors to CSS classes. 497 | * Author: Nate Cavanaugh, Minhchau Dang, Jonathan Neal, & Gregory Waxman 498 | * Updated By: Steven Bower for use with jReject plugin 499 | * Copyright: Copyright (c) 2008 Jonathan Neal under dual MIT/GPL license. 500 | */ 501 | 502 | (function ($) { 503 | $.browserTest = function (a, z) { 504 | var u = 'unknown', 505 | x = 'X', 506 | m = function (r, h) { 507 | for (var i = 0; i < h.length; i = i + 1) { 508 | r = r.replace(h[i][0], h[i][1]); 509 | } 510 | 511 | return r; 512 | }, c = function (i, a, b, c) { 513 | var r = { 514 | name: m((a.exec(i) || [u, u])[1], b) 515 | }; 516 | 517 | r[r.name] = true; 518 | 519 | if (!r.opera) { 520 | r.version = (c.exec(i) || [x, x, x, x])[3]; 521 | } 522 | else { 523 | r.version = window.opera.version(); 524 | } 525 | 526 | if (/safari/.test(r.name)) { 527 | var safariversion = /(safari)(\/|\s)([a-z0-9\.\+]*?)(\;|dev|rel|\s|$)/; 528 | var res = safariversion.exec(i) 529 | if (res && res[3] && res[3] < 400) { 530 | r.version = '2.0'; 531 | } 532 | } 533 | 534 | else if (r.name === 'presto') { 535 | r.version = ($.browser.version > 9.27) ? 'futhark' : 'linear_b'; 536 | } 537 | 538 | r.versionNumber = parseFloat(r.version, 10) || 0; 539 | var minorStart = 1; 540 | 541 | if (r.versionNumber < 100 && r.versionNumber > 9) { 542 | minorStart = 2; 543 | } 544 | 545 | r.versionX = (r.version !== x) ? r.version.substr(0, minorStart) : x; 546 | r.className = r.name + r.versionX; 547 | 548 | return r; 549 | }; 550 | 551 | a = (/Opera|Navigator|Minefield|KHTML|Chrome|CriOS/.test(a) ? m(a, [ 552 | [/(Firefox|MSIE|KHTML,\slike\sGecko|Konqueror)/, ''], 553 | ['Chrome Safari', 'Chrome'], 554 | ['CriOS', 'Chrome'], 555 | ['KHTML', 'Konqueror'], 556 | ['Minefield', 'Firefox'], 557 | ['Navigator', 'Netscape'] 558 | ]) : a).toLowerCase(); 559 | 560 | $.browser = $.extend((!z) ? $.browser : {}, c(a, 561 | /(camino|chrome|crios|firefox|netscape|konqueror|lynx|msie|opera|safari)/, 562 | [], 563 | /(camino|chrome|crios|firefox|netscape|netscape6|opera|version|konqueror|lynx|msie|safari)(\/|\s)([a-z0-9\.\+]*?)(\;|dev|rel|\s|$)/)); 564 | 565 | $.layout = c(a, /(gecko|konqueror|msie|opera|webkit)/, [ 566 | ['konqueror', 'khtml'], 567 | ['msie', 'trident'], 568 | ['opera', 'presto'] 569 | ], /(applewebkit|rv|konqueror|msie)(\:|\/|\s)([a-z0-9\.]*?)(\;|\)|\s)/); 570 | 571 | $.os = { 572 | name: (/(win|mac|linux|sunos|solaris|iphone|ipad)/. 573 | exec(navigator.platform.toLowerCase()) || [u])[0].replace('sunos', 'solaris') 574 | }; 575 | 576 | if (!z) { 577 | $('html').addClass([$.os.name, $.browser.name, $.browser.className, 578 | $.layout.name, $.layout.className].join(' ')); 579 | } 580 | }; 581 | 582 | $.browserTest(navigator.userAgent); 583 | }(jQuery)); 584 | -------------------------------------------------------------------------------- /app/vendor/jreject/jquery.reject.min.js: -------------------------------------------------------------------------------- 1 | (function(b){b.reject=function(f){var a=b.extend(!0,{reject:{all:!1,msie5:!0,msie6:!0},display:[],browserShow:!0,browserInfo:{firefox:{text:"Firefox 16",url:"http://www.mozilla.com/firefox/"},safari:{text:"Safari 5",url:"http://www.apple.com/safari/download/"},opera:{text:"Opera 12",url:"http://www.opera.com/download/"},chrome:{text:"Chrome 22",url:"http://www.google.com/chrome/"},msie:{text:"Internet Explorer 9",url:"http://www.microsoft.com/windows/Internet-explorer/"},gcf:{text:"Google Chrome Frame", 2 | url:"http://code.google.com/chrome/chromeframe/",allow:{all:!1,msie:!0}}},header:"Did you know that your Internet Browser is out of date?",paragraph1:"Your browser is out of date, and may not be compatible with our website. A list of the most popular web browsers can be found below.",paragraph2:"Just click on the icons to get to the download page",close:!0,closeMessage:"By closing this window you acknowledge that your experience on this website may be degraded",closeLink:"Close This Window",closeURL:"#", 3 | closeESC:!0,closeCookie:!1,cookieSettings:{path:"/",expires:0},imagePath:"./images/",overlayBgColor:"#000",overlayOpacity:0.8,fadeInTime:"fast",fadeOutTime:"fast",analytics:!1},f);1>a.display.length&&(a.display="firefox chrome msie safari opera gcf".split(" "));b.isFunction(a.beforeReject)&&a.beforeReject();a.close||(a.closeESC=!1);f=function(a){return(a.all?!0:!1)||(a[b.os.name]?!0:!1)||(a[b.layout.name]?!0:!1)||(a[b.browser.name]?!0:!1)||(a[b.browser.className]?!0:!1)};if(!f(a.reject)){if(b.isFunction(a.onFail))a.onFail(); 4 | return!1}if(a.close&&a.closeCookie){var e="jreject-close",c=function(c,d){if("undefined"!=typeof d){var e="";0!==a.cookieSettings.expires&&(e=new Date,e.setTime(e.getTime()+1E3*a.cookieSettings.expires),e="; expires="+e.toGMTString());var f=a.cookieSettings.path||"/";document.cookie=c+"="+encodeURIComponent(!d?"":d)+e+"; path="+f}else{f=null;if(document.cookie&&""!==document.cookie)for(var g=document.cookie.split(";"),h=g.length,i=0;i"+(""===a.paragraph1?"":"

"+a.paragraph1+"

")+(""===a.paragraph2?"":"

"+a.paragraph2+"

");if(a.browserShow){var d=d+""}var d=d+('
'+(a.close?''+a.closeLink+"

"+a.closeMessage+"

":"")+"
"),g=b("
"+d+"
");j=h();f=l();g.bind("closejr",function(){if(!a.close)return!1;b.isFunction(a.beforeClose)&&a.beforeClose();b(this).unbind("closejr");b("#jr_overlay,#jr_wrap").fadeOut(a.fadeOutTime,function(){b(this).remove();b.isFunction(a.afterClose)&&a.afterClose()});b("embed.jr_hidden, object.jr_hidden, select.jr_hidden, applet.jr_hidden").show().removeClass("jr_hidden"); 7 | a.closeCookie&&c(e,"true");return!0});var n=function(b){if(a.analytics){var c=b.split(/\/+/g)[1];try{_gaq.push(["_trackEvent","External Links",c,b])}catch(e){try{pageTracker._trackEvent("External Links",c,b)}catch(d){}}}window.open(b,"jr_"+Math.round(11*Math.random()));return!1};g.find("#jr_overlay").css({width:j[0],height:j[1],background:a.overlayBgColor,opacity:a.overlayOpacity});g.find("#jr_wrap").css({top:f[1]+j[3]/4,left:f[0]});g.find("#jr_inner").css({minWidth:100*i,maxWidth:140*i,width:"trident"== 8 | b.layout.name?155*i:"auto"});g.find("#jr_inner li").css({background:'transparent url("'+a.imagePath+'background_browser.gif")no-repeat scroll left top'});g.find("#jr_inner li .jr_icon").each(function(){var c=b(this);c.css("background","transparent url("+a.imagePath+"browser_"+c.parent("li").attr("id").replace(/jr_/,"")+".gif) no-repeat scroll left top");c.click(function(){var a=b(this).next("div").children("a").attr("href");n(a)})});g.find("#jr_inner li a").click(function(){n(b(this).attr("href")); 9 | return!1});g.find("#jr_close a").click(function(){b(this).trigger("closejr");if("#"===a.closeURL)return!1});b("#jr_overlay").focus();b("embed, object, select, applet").each(function(){b(this).is(":visible")&&b(this).hide().addClass("jr_hidden")});b("body").append(g.hide().fadeIn(a.fadeInTime));b(window).bind("resize scroll",function(){var a=h();b("#jr_overlay").css({width:a[0],height:a[1]});var c=l();b("#jr_wrap").css({top:c[1]+a[3]/4,left:c[0]})});a.closeESC&&b(document).bind("keydown",function(a){27== 10 | a.keyCode&&g.trigger("closejr")});b.isFunction(a.afterReject)&&a.afterReject();return!0};var h=function(){var b=window.innerWidth&&window.scrollMaxX?window.innerWidth+window.scrollMaxX:document.body.scrollWidth>document.body.offsetWidth?document.body.scrollWidth:document.body.offsetWidth,a=window.innerHeight&&window.scrollMaxY?window.innerHeight+window.scrollMaxY:document.body.scrollHeight>document.body.offsetHeight?document.body.scrollHeight:document.body.offsetHeight,e=window.innerWidth?window.innerWidth: 11 | document.documentElement&&document.documentElement.clientWidth?document.documentElement.clientWidth:document.body.clientWidth,c=window.innerHeight?window.innerHeight:document.documentElement&&document.documentElement.clientHeight?document.documentElement.clientHeight:document.body.clientHeight;return[bc.versionNumber&&9 2 | 3 | // a custom assertion function 4 | // asserts the element is scrolled to the top of the viewport 5 | const expectScrolledIntoView = ($el) => { 6 | const el = $el[0] 7 | const win = el.ownerDocument.defaultView 8 | expect(el.offsetTop).closeTo(win.scrollY, 5) 9 | } 10 | 11 | describe('PieChopper', function(){ 12 | beforeEach(function(){ 13 | // Visiting before each test ensures the app 14 | // gets reset to a clean state before each test 15 | // 16 | // We've set our baseUrl to be http://localhost:8080 17 | // which is automatically prepended to cy.visit 18 | // 19 | // https://on.cypress.io/visit 20 | cy.visit('/') 21 | }) 22 | 23 | // to make assertions throughout our test 24 | // we're going to use the should command 25 | // https://on.cypress.io/should 26 | 27 | 28 | it('has correct title', function(){ 29 | // https://on.cypress.io/title 30 | cy.title().should('eq', 'PieChopper - Chop your startup equity') 31 | }) 32 | 33 | it('has correct h1', function(){ 34 | // https://on.cypress.io/get 35 | cy.get('h1').should('contain', 'Chop your startup equity') 36 | }) 37 | 38 | context('About', function(){ 39 | describe('desktop responsive', function(){ 40 | it('is collapsed by default', function(){ 41 | // https://on.cypress.io/parents 42 | cy.get('#about-section').parents('.collapse').should('not.be.visible') 43 | }) 44 | 45 | it('expands on click', function(){ 46 | // https://on.cypress.io/contains 47 | // https://on.cypress.io/click 48 | cy.contains('About').click() 49 | cy.get('#about-section') 50 | .should('be.visible') 51 | .should('contain', 'PieChopper assists startup teams to share their equity fair and square.') 52 | .parents('.collapse').should('have.css', 'height', '66px') 53 | }) 54 | }) 55 | 56 | describe('mobile responsive', function(){ 57 | beforeEach(function(){ 58 | // https://on.cypress.io/viewport 59 | cy.viewport('iphone-6') 60 | }) 61 | 62 | 63 | it('displays hamburger menu', function(){ 64 | // by default the About nav menu is hidden 65 | cy.contains('About').should('be.hidden') 66 | cy.get('#about-section').should('be.hidden') 67 | 68 | // now it should be visible after click 69 | cy.get('.icon-bar:first').parent().click() 70 | cy.contains('About').should('be.visible').click() 71 | 72 | // and the about section should now be visible 73 | cy.get('#about-section').should('be.visible') 74 | }) 75 | }) 76 | }) 77 | 78 | context('Begin button', function(){ 79 | // the viewport is reset before each test back to the default 80 | // as defined in our https://on.cypress.io/guides/configuration 81 | // so we are back to the desktop resolution 82 | 83 | it('scrolls to "How to chop it?"', function(){ 84 | // scroll behavior is difficult to test - but with some 85 | // basic DOM knowledge we can do this pretty easily 86 | // 87 | // to figure out that the window is being scrolled we can simply 88 | // check the '#model-selection-section' top offset and once that equals 89 | // the windows scrollY we know its been scrolled to the top (within 1 px) 90 | cy.contains('button', 'Begin').click() 91 | 92 | // you can pass a custom assertion function into `should` 93 | // this will retry until it passes or times out. 94 | cy.get('#model-selection-section') 95 | .should(expectScrolledIntoView) 96 | }) 97 | }) 98 | 99 | context('How to chop it?', function(){ 100 | it('defaults with Company Roles', function(){ 101 | cy.get('.carousel-inner .active').should('contain', 'Company Roles') 102 | cy.get('.model-selector-desc') 103 | .should('contain', 'The method is inspired by the Foundrs.com website.') 104 | 105 | // https://on.cypress.io/find 106 | .find('a').should('have.attr', 'href', 'http://foundrs.com/') 107 | }) 108 | 109 | it('can change carousel to Market Value', function(){ 110 | cy.get('.carousel-control.right').click() 111 | cy.get('.carousel-inner .active').should('contain', 'Market Value') 112 | cy.get('.model-selector-desc') 113 | .should('contain', 'The method is inspired by the Slicing Pie website.') 114 | .find('a').should('have.attr', 'href', 'http://www.slicingpie.com/') 115 | }) 116 | 117 | it('can change carousel to "Relative Important" using cy.wait', function(){ 118 | cy.get('.carousel-control.right').click() 119 | cy.get('.carousel-inner .active').should('contain', 'Market Value') 120 | cy.get('.carousel-control.right').click() 121 | cy.get('.carousel-inner .active').should('contain', 'Relative Importance') 122 | cy.get('.model-selector-desc') 123 | .should('contain', 'The method is inspired by the Founders Pie Calculator.') 124 | 125 | // https://on.cypress.io/and 126 | .find('a').should('have.attr', 'href').and('include', 'www.andrew.cmu.edu/user/fd0n/') 127 | }) 128 | 129 | it('can loop around forward + backwards', function(){ 130 | cy.get('.carousel-control.right').click() 131 | cy.get('.carousel-inner .active').should('contain', 'Market Value') 132 | cy.get('.carousel-control.right').click() 133 | cy.get('.carousel-inner .active').should('contain', 'Relative Importance') 134 | cy.get('.carousel-control.right').click() 135 | cy.get('.carousel-inner .active').should('contain', 'Company Roles') 136 | 137 | // verify the carousel indicators are correct 138 | // only 1 is active and its the first li 139 | cy.get('ol.carousel-indicators li').should(function($lis){ 140 | expect($lis.filter('.active')).to.have.length(1) 141 | expect($lis.first()).to.have.class('active') 142 | }) 143 | 144 | // loop back around 145 | cy.get('.carousel-control.left').click() 146 | cy.get('.carousel-inner .active').should('contain', 'Relative Importance') 147 | 148 | // verify the carousel indicators are correct 149 | // only 1 is active and its the last li 150 | cy.get('ol.carousel-indicators li').should(function($lis){ 151 | expect($lis.filter('.active')).to.have.length(1) 152 | expect($lis.last()).to.have.class('active') 153 | }) 154 | }) 155 | 156 | it('scrolls to How do you contribute?', function(){ 157 | // this shows an alternate approach to testing whether an 158 | // element has been scrolled. 159 | // 160 | // we take advantage of aliasing instead of using a closure 161 | // for referencing the window object as 'this.win' 162 | // 163 | // https://on.cypress.io/as 164 | cy.window().as('win') 165 | cy.get('#model-selection-section').contains('button', 'Continue').click() 166 | 167 | // you can pass a custom assertion function into `should` 168 | // this will retry until it passes or times out. 169 | cy.get('#contrib-section') 170 | .should(expectScrolledIntoView) 171 | }) 172 | }) 173 | 174 | context('How do you contribute?', function(){ 175 | beforeEach(function(){ 176 | cy.get('#contrib-section').as('contrib') 177 | }) 178 | 179 | // the form changes based on which algorithm 180 | // has been selected with 'Company Roles' being the default 181 | describe('Company Roles', function(){ 182 | it('can add a Member C', function(){ 183 | // https://on.cypress.io/within 184 | // do all of our work within this section 185 | cy.get('@contrib').within(function(){ 186 | cy.get('thead th').should('have.length', 3) 187 | cy.get('.member-add-btn').click() 188 | cy.get('thead th').should('have.length', 4) 189 | 190 | // https://on.cypress.io/last 191 | .last().should('contain', 'Member C') 192 | }) 193 | }) 194 | 195 | it('can remove a Member C', function(){ 196 | cy.get('@contrib').within(function(){ 197 | cy.get('.member-add-btn').click() 198 | cy.get('thead th').should('have.length', 4) 199 | cy.get('thead th').last().find('.member-remove-btn').click() 200 | cy.get('thead th').should('have.length', 3) 201 | .last().should('not.contain', 'Member C') 202 | }) 203 | }) 204 | 205 | it('hides button at max num of columns', function(){ 206 | cy.get('#contrib-section').find('table').find('th') 207 | .should('have.length', 3) 208 | cy.get('.member-add-btn') 209 | .click().click().click().click() 210 | .should('be.hidden') 211 | }) 212 | 213 | it('calculates the values between members A + B', function(){ 214 | // using contains here to select the with this content 215 | // so its much easier to understand which row we're focused on 216 | 217 | cy.ng('model', 'member.name').filter('span').as('members') 218 | 219 | // https://on.cypress.io/type 220 | cy.get('@members').first().type('{selectall}{backspace}Jane') 221 | cy.get('@members').last().type('{selectall}{backspace}John') 222 | 223 | // https://on.cypress.io/contains 224 | cy.contains('tr', 'Who had the original idea for the project?') 225 | 226 | // https://on.cypress.io/check 227 | .find('td:eq(1)').find(':checkbox').check() 228 | 229 | cy.contains('tr', 'How much does the member participate into technical development?').within(function(){ 230 | // https://on.cypress.io/select 231 | cy.get('td:eq(1) select').select('Some') 232 | cy.get('td:eq(2) select').select('Plenty') 233 | }) 234 | 235 | cy.contains('tr', 'Who would lead the technical team if you would get more personnel?').within(function(){ 236 | // this should uncheck the 1st after we check the 2nd 237 | cy.get('td:eq(1) :checkbox').check().as('chk1') 238 | cy.get('td:eq(2) :checkbox').check() 239 | cy.get('@chk1').should('not.be.checked') 240 | }) 241 | 242 | cy.contains('tr', 'How much does the member contribute to the business expenses').within(function(){ 243 | cy.get('td:eq(1) select').select('Some') 244 | cy.get('td:eq(2) select').select('Little') 245 | }) 246 | 247 | cy.contains('tr', 'Who is or becomes the CEO?').within(function(){ 248 | cy.get('td:eq(1) :checkbox').check() 249 | }) 250 | 251 | // now verify that the tfoot + the slice graph match 252 | cy.get('tfoot td:eq(1)').should('contain', '57.7 %') 253 | cy.get('tfoot td:eq(2)').should('contain', '42.3 %') 254 | 255 | cy.get('#slice-graph').within(function(){ 256 | cy.get('[popover="Jane: 57.7%"]') 257 | cy.get('[popover="John: 42.3%"]') 258 | }) 259 | }) 260 | 261 | it('updates Member A + B values in #slice-graph', function(){ 262 | cy.contains('tr', 'How much does the member contribute to the product features?').within(function(){ 263 | cy.get('td:eq(1) select').select('Little') 264 | cy.get('td:eq(2) select').select('Plenty') 265 | }) 266 | 267 | // when we click the first slice a popover should appear with this content 268 | cy.get('#slice-graph').trigger("mouseover") 269 | .find('[popover]').as('slices').first().click() 270 | cy.get('.popover-content').should('contain', 'Member A: 16.7%') 271 | 272 | // and we'll just check the [popover='...'] attr for the 2nd 273 | cy.get('@slices').last() 274 | .should('have.attr', 'popover', 'Member B: 83.3%') 275 | }) 276 | }) 277 | 278 | describe('Market Value', function(){ 279 | beforeEach(function(){ 280 | // swap to market value 281 | cy.get('.carousel-control.right').click() 282 | }) 283 | 284 | it('updates Member A + B values', function(){ 285 | cy.contains('tr', 'How much cash is the member investing?').within(function(){ 286 | 287 | // https://on.cypress.io/type 288 | cy.get('td:eq(1) input').type(50000) 289 | cy.get('td:eq(2) input').type(25000) 290 | }) 291 | 292 | cy.contains('tr', 'How much does the member bring in other valuables ').within(function(){ 293 | cy.get('td:eq(2) input').type(10000) 294 | }) 295 | 296 | cy.get('tfoot td:eq(1)').should('contain', '58.8 %') 297 | cy.get('tfoot td:eq(2)').should('contain', '41.2 %') 298 | }) 299 | 300 | it('validates input and displays errors', function(){ 301 | cy.contains('tr', 'What is the sales commission percent that is usually paid on the market?').within(function(){ 302 | cy.get('td:eq(1) input').type(500) 303 | .parent().should('have.class', 'invalid') 304 | .find('.cell-error-msg').should('contain', 'Value must be smaller than 100') 305 | }) 306 | 307 | cy.get('#results-section').should('contain', 'Your input seems to contain errors.') 308 | }) 309 | }) 310 | }) 311 | 312 | context('Sharing Results', function(){ 313 | beforeEach(function(){ 314 | // We want to start a server before each test 315 | // to control nerwork requests and responses 316 | 317 | // https://on.cypress.io/server 318 | cy.server() 319 | }) 320 | 321 | // simulate the server failing to respond to the share proposal 322 | it('displays error message in modal when server errors', function(){ 323 | // https://on.cypress.io/route 324 | cy.route({ 325 | method: 'POST', 326 | url: /proposals/, 327 | status: 500, 328 | response: '' 329 | }).as('proposal') 330 | cy.get('#results-section').contains('Share').click() 331 | 332 | // https://on.cypress.io/wait 333 | cy.wait('@proposal') 334 | cy.get('.modal').should('contain', 'We couldn\'t save the proposal.') 335 | .find('h2').should('contain', 'Ooops !') 336 | 337 | // after we click on the backdrop the modal should go away 338 | cy.get('.modal-backdrop').click().should('not.exist') 339 | }) 340 | 341 | it('sends up correct request JSON', function(){ 342 | // https://on.cypress.io/route 343 | cy.route('POST', /proposals/, {}).as('proposal') 344 | cy.get('#results-section').contains('Share').click() 345 | cy.wait('@proposal').its('requestBody').should(function(json){ 346 | expect(json.userId).to.be.a('string') 347 | 348 | // expect there to be 3 keys in models 349 | // https://on.cypress.io/underscore 350 | expect(Cypress._.keys(json.repo.models)).to.have.length(3) 351 | 352 | // make sure the activeModelId matches on of our repo.models 353 | var selected = json.repo.models[json.repo.activeModelId] 354 | expect(selected).to.exist 355 | expect(selected.name).to.eq('Company Roles') 356 | }) 357 | }) 358 | 359 | it('displays share link on successful response', function(){ 360 | var id = '12345-foo-bar' 361 | 362 | cy.route('POST', /proposals/, {id: id}).as('proposal') 363 | cy.get('#results-section').contains('Share').as('share').click() 364 | cy.wait('@proposal') 365 | 366 | // share button should now be disabled 367 | cy.get('@share').should('be.disabled') 368 | 369 | cy.get('#link-share-url').should('be.visible') 370 | 371 | // https://on.cypress.io/and 372 | .and('contain', 'The following link can be copied and pasted over IM or email.') 373 | 374 | cy.get('#sharedUrl').should('have.prop', 'value').and('include', id) 375 | }) 376 | }) 377 | }) 378 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-example-piechopper", 3 | "scripts": { 4 | "postinstall": "bower install --allow-root", 5 | "cypress:open": "cypress open", 6 | "cypress:run": "cypress run", 7 | "cypress:version": "cypress version", 8 | "cypress:verify": "cypress verify", 9 | "build": "grunt", 10 | "postbuild": "node-sass app/style/style.scss build/served/app/css/style.css", 11 | "prestart": "npm run build", 12 | "start": "grunt serve", 13 | "server": "nodemon --delay 2 build/server/src/server.js", 14 | "watch": "grunt watch", 15 | "pretest": "npm run build", 16 | "test": "run-p --race server cypress:run", 17 | "pretest:ci": "npm run build", 18 | "test:ci": "run-p --race server cypress:run" 19 | }, 20 | "devDependencies": { 21 | "bower": "1.8.13", 22 | "coffeescript": "1.12.7", 23 | "cypress": "9.3.1", 24 | "grunt": "^1.4.1", 25 | "grunt-cli": "^1.4.3", 26 | "grunt-contrib-clean": "^2.0.0", 27 | "grunt-contrib-coffee": "^1.0.0", 28 | "grunt-contrib-copy": "^1.0.0", 29 | "grunt-contrib-cssmin": "^4.0.0", 30 | "grunt-contrib-uglify": "^5.0.1", 31 | "grunt-contrib-watch": "^1.1.0", 32 | "grunt-exec": "^3.0.0", 33 | "grunt-ngmin": "^0.0.3", 34 | "grunt-preprocess": "^5.1.0", 35 | "grunt-ssh": "^0.12.9", 36 | "node-sass": "^6.0.1", 37 | "nodemon": "^2.0.15", 38 | "npm-run-all": "4.1.5" 39 | }, 40 | "dependencies": { 41 | "compression": "1.7.4", 42 | "express": "4.17.1" 43 | }, 44 | "private": true 45 | } 46 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "commitMessage": "{{semanticPrefix}}{{#if isPin}}Pin deps or locks{{else}}Update {{depName}} to {{newVersion}} 🌟{{/if}}", 7 | "prTitle": "{{semanticPrefix}}{{#if isPin}}Pin{{else}}Update{{/if}} dependency {{depName}} to version {{newVersion}} 🌟", 8 | "major": { 9 | "automerge": false 10 | }, 11 | "minor": { 12 | "automerge": false 13 | }, 14 | "prHourlyLimit": 2, 15 | "updateNotScheduled": false, 16 | "timezone": "America/New_York", 17 | "schedule": [ 18 | "after 10pm and before 5am on every weekday", 19 | "every weekend" 20 | ], 21 | "lockFileMaintenance": { 22 | "enabled": true 23 | }, 24 | "separatePatchReleases": true, 25 | "separateMultipleMajor": true, 26 | "masterIssue": true, 27 | "labels": [ 28 | "type: dependencies", 29 | "renovate" 30 | ], 31 | "ignoreDeps": [ 32 | "grunt", 33 | "grunt-cli", 34 | "grunt-contrib-clean", 35 | "grunt-contrib-coffee", 36 | "grunt-contrib-copy", 37 | "grunt-contrib-cssmin", 38 | "grunt-contrib-uglify", 39 | "grunt-contrib-watch", 40 | "grunt-exec", 41 | "grunt-ngmin", 42 | "grunt-preprocess", 43 | "grunt-ssh", 44 | "node-sass", 45 | "nodemon" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /server/mgmt/crontab.sh: -------------------------------------------------------------------------------- 1 | MGMT_DIR=/* @echo target.webServer.directory *//build/server/mgmt 2 | 3 | 8,18,28,38,48 * * * * sh $MGMT_DIR/express/start.sh > $HOME/cron_recent.log 2>&1 4 | 7,17,27,37,47 * * * * sh $MGMT_DIR/mongo/start.sh > $HOME/cron_recent.log 2>&1 5 | * 20 * * * sh $MGMT_DIR/mongo/backup.sh >> $HOME/cron_retain.log 2>&1 6 | * 21 * * * sh $MGMT_DIR/mongo/cleanup.sh >> $HOME/cron_retain.log 2>&1 7 | -------------------------------------------------------------------------------- /server/mgmt/express/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | pushd /* @echo target.webServer.directory */ 3 | mkdir -p /* @echo target.webServer.directory *//run 4 | pid=$(/sbin/pidof /* @echo target.webServer.directory *//bin/node) 5 | if echo "$pid" | grep -q " "; then 6 | pid="" 7 | fi 8 | if [ -n "$pid" ]; then 9 | user=$(ps -p $pid -o user | tail -n 1) 10 | if [ $user = "/* @echo target.deploy.username */" ]; then 11 | exit 0 12 | fi 13 | fi 14 | nohup /* @echo target.webServer.directory *//bin/node /* @echo target.webServer.directory *//build/server/src/server.js > /dev/null 2>&1 & 15 | /sbin/pidof /* @echo target.webServer.directory *//bin/node > /* @echo target.webServer.directory *//run/node.pid 16 | popd 17 | -------------------------------------------------------------------------------- /server/mgmt/express/stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mkdir -p /* @echo target.webServer.directory *//run 3 | pid=$(/sbin/pidof /* @echo target.webServer.directory *//bin/node) 4 | if echo "$pid" | grep -q " "; then 5 | pid="" 6 | fi 7 | if [ -n "$pid" ]; then 8 | user=$(ps -p $pid -o user | tail -n 1) 9 | if [ $user = "/* @echo target.deploy.username */" ]; then 10 | kill "$pid" 11 | rm -f /* @echo target.webServer.directory *//run/node.pid 12 | fi 13 | fi 14 | -------------------------------------------------------------------------------- /server/mgmt/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | pushd /* @echo target.webServer.directory */ 3 | /* @echo target.webServer.directory *//bin/npm install mongodb express 4 | popd 5 | -------------------------------------------------------------------------------- /server/mgmt/memuse.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ps -u /* @echo target.deploy.username */ -o pid,rss,command | awk '{print $0}{sum+=$2} END {print "Total", sum/1024, "MB"}' 3 | -------------------------------------------------------------------------------- /server/mgmt/mongo/backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | if test -n "/* @echo target.db.backupDir */"; then 3 | mkdir -p /* @echo target.db.backupDir */ 4 | uname="/* @echo target.db.username */" 5 | pword="/* @echo target.db.password */" 6 | if test -n "$uname"; then 7 | uname="-u $uname" 8 | fi 9 | if test -n "$pword"; then 10 | pword="-p $pword" 11 | fi 12 | /* @echo target.db.binsDir *//mongodump --port /* @echo target.db.port */ $uname $pword --db /* @echo target.db.name */ --out /* @echo target.db.backupDir *//`date "+%Y-%m-%d--%H-%M-%S"` 13 | fi 14 | -------------------------------------------------------------------------------- /server/mgmt/mongo/cleanup.js: -------------------------------------------------------------------------------- 1 | var curSecs = Math.floor((new Date()).getTime() / 1000); 2 | var secs31daysAgo = curSecs - (60 * 60 * 24 * 31); 3 | var hex = secs31daysAgo.toString(16); 4 | var oid = ObjectId(hex + "0000000000000000"); 5 | var cursor = db.getCollection('proposals').find({_id: {$lt: oid}}); 6 | //while (cursor.hasNext()) { printjsononeline(cursor.next()); } 7 | db.getCollection('proposals').remove({_id: {$lt: oid}}); 8 | -------------------------------------------------------------------------------- /server/mgmt/mongo/cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | uname="/* @echo target.db.username */" 3 | pword="/* @echo target.db.password */" 4 | if test -n "$uname"; then 5 | uname="-u $uname" 6 | fi 7 | if test -n "$pword"; then 8 | pword="-p $pword" 9 | fi 10 | /* @echo target.db.binsDir *//mongo $uname $pword localhost:/* @echo target.db.port *///* @echo target.db.name */ /* @echo target.webServer.directory *//build/server/mgmt/mongo/cleanup.js 11 | -------------------------------------------------------------------------------- /server/mgmt/mongo/console.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | uname="/* @echo target.db.username */" 3 | pword="/* @echo target.db.password */" 4 | if test -n "$uname"; then 5 | uname="-u $uname" 6 | fi 7 | if test -n "$pword"; then 8 | pword="-p $pword" 9 | fi 10 | /* @echo target.db.binsDir *//mongo $uname $pword localhost:/* @echo target.db.port *///* @echo target.db.name */ 11 | -------------------------------------------------------------------------------- /server/mgmt/mongo/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mkdir -p /* @echo target.db.binsDir *//run 3 | pid=$(/sbin/pidof /* @echo target.db.binsDir *//mongod) 4 | if echo "$pid" | grep -q " "; then 5 | pid="" 6 | fi 7 | if [ -n "$pid" ]; then 8 | user=$(ps -p $pid -o user | tail -n 1) 9 | if [ $user = "/* @echo target.deploy.username */" ]; then 10 | exit 0 11 | fi 12 | fi 13 | nohup /* @echo target.db.binsDir *//mongod --auth --dbpath /* @echo target.db.dataDir */ --port /* @echo target.db.port */ > /dev/null 2>&1 & 14 | /sbin/pidof /* @echo target.db.binsDir *//mongod > /* @echo target.db.binsDir *//run/mongod.pid 15 | -------------------------------------------------------------------------------- /server/mgmt/mongo/stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mkdir -p /* @echo target.db.binsDir *//run 3 | pid=$(/sbin/pidof /* @echo target.db.binsDir *//mongod) 4 | if echo "$pid" | grep -q " "; then 5 | pid="" 6 | fi 7 | if [ -n "$pid" ]; then 8 | user=$(ps -p $pid -o user | tail -n 1) 9 | if [ $user = "/* @echo target.deploy.username */" ]; then 10 | kill "$pid" 11 | rm -f /* @echo target.db.binsDir *//run/mongod.pid 12 | fi 13 | fi 14 | -------------------------------------------------------------------------------- /server/src/config.coffee: -------------------------------------------------------------------------------- 1 | CONFIG = 2 | # @include ../../../../config/development.json 3 | 4 | exports.config = CONFIG 5 | -------------------------------------------------------------------------------- /server/src/server.coffee: -------------------------------------------------------------------------------- 1 | config = require('./config').config 2 | 3 | express = require('express') 4 | compression = require('compression') 5 | 6 | server = express() 7 | server.use(compression()) 8 | server.use(express.static('./build/served/')) 9 | 10 | # The 404 Route (ALWAYS Keep this as the last route) 11 | server.get '*', (req, res) -> 12 | res.sendfile('./build/served/error.html', 404) 13 | 14 | 15 | server.listen(config.webServer.port) 16 | --------------------------------------------------------------------------------