├── Procfile ├── public ├── robots.txt ├── favicon.ico ├── images │ ├── favicon.ico │ └── bg_gray_smoke.jpg ├── jquery.spin.min.js ├── index.css ├── page.js ├── repo.css └── app.js ├── .gitignore ├── package.json ├── README.md ├── server.coffee ├── styls └── index.styl └── views └── index.jade /Procfile: -------------------------------------------------------------------------------- 1 | web: coffee server.coffee -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitrun/poll/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitrun/poll/HEAD/public/images/favicon.ico -------------------------------------------------------------------------------- /public/images/bg_gray_smoke.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitrun/poll/HEAD/public/images/bg_gray_smoke.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | node_modules 15 | npm-debug.log -------------------------------------------------------------------------------- /public/jquery.spin.min.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | $.fn.spin = function(opts) { 3 | this.each(function() { 4 | var $this = $(this), 5 | data = $this.data(); 6 | 7 | if (data.spinner) { 8 | data.spinner.stop(); 9 | delete data.spinner; 10 | } 11 | if (opts !== false) { 12 | data.spinner = new Spinner($.extend({color: $this.css('color')}, opts)).spin(this); 13 | } 14 | }); 15 | return this; 16 | }; 17 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "hashobject team (team@hashobject.com)", 3 | "name": "gitpoll", 4 | "description": "GitHub poll app", 5 | "version": "0.1.0", 6 | "main": "server.coffee", 7 | "engines": { 8 | "node": "~0.10.22" 9 | }, 10 | "dependencies": { 11 | "express": "=3.4.6", 12 | "jade": "=0.35.0", 13 | "passport": ">= 0.0.0", 14 | "passport-github": ">= 0.0.0", 15 | "coffee-script": ">=1.6.3", 16 | "stylus": "=0.41.0", 17 | "nib": "=1.0.1" 18 | }, 19 | "devDependencies": {}, 20 | "optionalDependencies": {} 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gitpoll 2 | ========== 3 | 4 | Poll for Github issues. 5 | [poll.gitrun.com](http://poll.gitrun.com). 6 | 7 | LICENCE 8 | ========== 9 | 10 | (The MIT License) 11 | 12 | Copyright © 2012-2014 Hashobject Ltd (team@hashobject.com) 13 | 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the 'Software'), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all 22 | copies or substantial portions of the Software. 23 | 24 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 30 | SOFTWARE. 31 | -------------------------------------------------------------------------------- /server.coffee: -------------------------------------------------------------------------------- 1 | express = require "express" 2 | stylus = require "stylus" 3 | nib = require "nib" 4 | passport = require "passport" 5 | 6 | GitHubStrategy = require('passport-github').Strategy 7 | 8 | GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || "041e33b4c176a007f627" 9 | GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET || "f70b6f1468f4ef87ba18d5587a2be6d7c6c4ae98"; 10 | 11 | app = module.exports = express.createServer() 12 | 13 | 14 | passport.serializeUser (user, done) -> 15 | done(null, user); 16 | 17 | 18 | passport.deserializeUser (obj, done) -> 19 | done(null, obj); 20 | 21 | 22 | 23 | callbackURL = process.env.GITHUB_CALLBACK_URL || "http://localhost:8085/auth/callback" 24 | 25 | 26 | passport.use new GitHubStrategy { 27 | clientID: GITHUB_CLIENT_ID, 28 | clientSecret: GITHUB_CLIENT_SECRET, 29 | callbackURL: callbackURL 30 | }, 31 | (accessToken, refreshToken, profile, done) -> 32 | 33 | process.nextTick -> 34 | profile.accessToken = accessToken 35 | profile.avatar = profile._json.avatar_url 36 | return done(null, profile); 37 | 38 | 39 | # stylus compile function 40 | compile = (str, path) -> 41 | return stylus(str) 42 | .define("url", stylus.url({ paths: [__dirname + "/public"] })) 43 | .set("filename", path) 44 | .set("warn", true) 45 | .set("compress", false) 46 | .use(nib()) 47 | 48 | app.configure -> 49 | # stylus middleware 50 | app.use stylus.middleware 51 | src : __dirname + "/styls" # styl files should be placed inside this folder 52 | dest : __dirname + "/public" # CSS files will be complied to public directory 53 | compile: compile # compile function 54 | app.set('views', __dirname + '/views'); 55 | app.set('view engine', 'jade'); 56 | app.set('view options', {layout: false}); 57 | app.use(express.favicon(__dirname + "/public/favicon.ico")); 58 | app.use(express.logger()); 59 | app.use(express.cookieParser()); 60 | app.use(express.bodyParser()); 61 | app.use(express.methodOverride()); 62 | app.use(express.session({ secret: 'keyboard cat' })); 63 | 64 | app.use(passport.initialize()); 65 | app.use(passport.session()); 66 | app.use(app.router); 67 | app.use(express.static(__dirname + '/public')); 68 | 69 | 70 | app.get '/auth', 71 | passport.authenticate('github', scope: 'public_repo'), 72 | (req, res) -> 73 | 74 | app.get "/", (req, res) -> 75 | 76 | user = req.user 77 | if not user 78 | user = {username:'guest'} 79 | console.log "user", user 80 | res.render "index", {user: user} 81 | 82 | app.get "/:username/:repoName/issues/:number", (req, res) -> 83 | 84 | user = req.user 85 | if not user 86 | user = {username:'guest'} 87 | res.render "index", {user: user} 88 | 89 | app.get "/logout", (req, res) -> 90 | req.logout() 91 | res.redirect "/" 92 | 93 | app.get '/auth/callback', 94 | passport.authenticate('github', { failureRedirect: '/login' }), 95 | (req, res) -> 96 | res.redirect('/'); 97 | 98 | port = process.env.PORT || 8085 99 | app.listen port 100 | console.log "server started on port 8085. Open http://localhost:8085 in your browser" 101 | -------------------------------------------------------------------------------- /styls/index.styl: -------------------------------------------------------------------------------- 1 | @import 'nib' 2 | global-reset(); 3 | * 4 | box-sizing: border-box; 5 | 6 | html 7 | body 8 | background-color: white; 9 | font-family: Helvetica,arial,freesans,clean,sans-serif; 10 | font-size: 13px; 11 | color: #666; 12 | line-height: 1.6; 13 | .container 14 | width: 740px; 15 | margin: 0 auto; 16 | header.container 17 | margin-top: 10px; 18 | margin-bottom: 20px; 19 | height: 30px; 20 | & a 21 | & a:visited 22 | white-space: nowrap; 23 | text-decoration: none; 24 | color: #333; 25 | & a:hover 26 | color: white; 27 | #app-footer 28 | border-top: 1px solid #E0E0E0; 29 | position: fixed; 30 | bottom: 0; 31 | width: 100%; 32 | height: 100px; 33 | & .container 34 | margin-top: 20px; 35 | & p 36 | display: inline-block; 37 | vertical-align: top; 38 | margin: 10px 10px 0 0; 39 | #username 40 | color: #4A3E7B; 41 | line-height: 22px; 42 | font-weight: bold; 43 | text-shadow: 0 1px 0 white; 44 | &:hover 45 | text-decoration: underline; 46 | .page 47 | position: relative; 48 | width: 740px; 49 | .page-container 50 | margin-bottom: 15px; 51 | padding: 3px; 52 | background: #EEE; 53 | border-radius: 4px; 54 | .page-content 55 | padding: 10px; 56 | background: white; 57 | border: 1px solid #DDD; 58 | border-radius: 2px; 59 | position: relative; 60 | min-height: 160px; 61 | .title 62 | font: 13px Helvetica,arial,freesans,clean,sans-serif; 63 | position: relative; 64 | font-size: 14px; 65 | font-weight: bold; 66 | color: #333; 67 | #app-intro-page .title 68 | margin: 10px 0 15px; 69 | #issue-title 70 | width: 49%; 71 | font-size: 16px; 72 | font-weight: bold; 73 | color: #444; 74 | box-sizing: border-box; 75 | margin-right: 5px; 76 | padding: 6px; 77 | color: #666; 78 | background-repeat: no-repeat; 79 | background-position: right center; 80 | #issue-description 81 | margin-top: 15px; 82 | //margin-bottom: 70px; 83 | .info-bar 84 | display: inline-block; 85 | margin: 15px 0 -10px; 86 | width: 100%; 87 | padding: 10px 10px 4px; 88 | border: 1px solid #E5E5E5; 89 | border-left: none; 90 | border-right: none; 91 | background: whiteSmoke; 92 | position: absolute; 93 | left: 0; 94 | right: 0; 95 | bottom: 9px; 96 | textarea 97 | box-sizing: border-box; 98 | width: 100%; 99 | padding: 5px; 100 | height: 150px; 101 | font-size: 12px; 102 | .btn 103 | color: white; 104 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.4); 105 | background: #FF8A66; 106 | background: linear-gradient(#FF8A66, #ED4706); 107 | box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); 108 | display: inline-block; 109 | padding: 8px 15px; 110 | line-height: normal; 111 | position: relative; 112 | font-size: 12px; 113 | font-weight: bold; 114 | border-radius: 3px; 115 | border: 1px solid; 116 | border-color: #E46432; 117 | border-bottom-color: #C13D14; 118 | &:hover 119 | background: #EE6A36; 120 | background: linear-gradient(#EE6A36, #BE410E); 121 | border-color: #D64910; 122 | color: white; 123 | button#create-issue-button 124 | position: absolute; 125 | right: 5px; 126 | margin-top: 15px; 127 | #user-repos-select 128 | width: 49%; 129 | font-size: 16px; 130 | font-weight: bold; 131 | color: #444; 132 | box-sizing: border-box; 133 | padding: 6px 6px; 134 | color: #666; 135 | background-repeat: no-repeat; 136 | background-position: right center; 137 | border-color: rgba(102, 102, 102, 0.15); 138 | & option 139 | font-size: 12px; 140 | font-weight: normal; 141 | header 142 | color: #666; 143 | font-size: 16px; 144 | width: 100%; 145 | position: relative; 146 | & img 147 | width: 22px; 148 | vertical-align: -6px; 149 | margin: 0 3px 0 10px; 150 | border-radius: 11px; 151 | #login, #logout 152 | color: #333; 153 | padding: 0 10px 0 10px; 154 | font-weight: bold; 155 | line-height: 24px; 156 | text-shadow: 0 1px 0 white; 157 | border: 1px solid #D4D4D4; 158 | border-bottom-color: #BCBCBC; 159 | border-radius: 3px; 160 | background: #FAFAFA; 161 | background: linear-gradient(#FAFAFA,#EAEAEA); 162 | cursor: pointer; 163 | #logout 164 | position: absolute; 165 | right: 5px; 166 | bottom: -9px; 167 | #login:hover, #logout:hover 168 | color: white; 169 | text-decoration: none; 170 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.3); 171 | border-color: #1B0E53; 172 | border-bottom-color: #150A43; 173 | background: #716896; 174 | background: linear-gradient(#716896,#1D0D5A); 175 | #no-results 176 | margin-bottom: 10px; 177 | margin-top: 20px; 178 | #chart-panel 179 | margin: -35px auto 0; 180 | height: 400px; 181 | position: relative; 182 | #poll-description 183 | margin-top: 10px; 184 | margin-bottom: 10px; 185 | #yes, #no 186 | font-weight: bold; 187 | & span 188 | margin-left: 4px; 189 | .not-valid 190 | border: 1px solid red!important; 191 | ///colors for pie 192 | g.slice:nth-child(1) 193 | fill: #ED4706; 194 | g.slice:nth-child(2) 195 | fill: #232063; 196 | footer a 197 | color: #4A3E7B; 198 | text-decoration: none; 199 | &:hover 200 | text-decoration: underline; 201 | header svg 202 | width: auto; 203 | display: inline-block; 204 | & path 205 | fill: #4A3E7B; 206 | #home 207 | margin-right: 15px; 208 | display: inline-block; 209 | bottom: -8px; 210 | width: 30px; 211 | height: 30px; 212 | position: relative; 213 | &:hover path 214 | fill: #FF8A66; 215 | .visible 216 | display: block; 217 | .inline-visible 218 | display: inline-block; 219 | .invisible 220 | display: none; 221 | -------------------------------------------------------------------------------- /views/index.jade: -------------------------------------------------------------------------------- 1 | doctype 5 2 | html(lang="en") 3 | head 4 | meta(charset="utf-8") 5 | title GitPoll - vote on GitHub issue 6 | meta(name="description", content="Application for voting on GitHub issues") 7 | meta(name="keywords", content="gitpoll, github, issue, vote, poll, +1, -1") 8 | meta(name="author", content="Hashobject (team@hashobject.com)", itemprop="author") 9 | link(rel="stylesheet", href="/index.css") 10 | script(type="text/javascript") 11 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 12 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 13 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 14 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 15 | ga('create', 'UA-46214133-2', 'gitrun.com'); 16 | ga('send', 'pageview'); 17 | script 18 | var gp = {}; 19 | gp.user = { 20 | username: '#{user.username}', 21 | avatar: '#{user.avatar}', 22 | profileUrl: '#{user.profileUrl}', 23 | displayName: '#{user.displayName}', 24 | accessToken: '#{user.accessToken}' 25 | }; 26 | body 27 | header.container 28 | a(href="/")#home.visible 29 | svg(version="1.1", id="Layer_1", xmlns="http://www.w3.org/2000/svg", x="0px", y="0px", width="30px", height="30px", viewBox="0 0 100 100", enable-background="new 0 0 100 100") 30 | path(d="M92,46C92,20.595,71.405,0,46,0S0,20.595,0,46s20.595,46,46,46V46H92z") 31 | path(d="M54,100c25.405,0,46-20.595,46-46H54V100z") 32 | span#welcome.invisible Welcome 33 | img(src="").invisible 34 | a#username.invisible(href="", target="_blank", title="Opens in new window") 35 | button#logout.invisible Logout 36 | a#login.invisible(href="/auth") Login 37 | div.container 38 | div.page.invisible#app-intro-page 39 | div.page-container 40 | section.page-content 41 | h2.title Usage 42 | ol 43 | li 1. Create new issue on GitHub or GitPoll. 44 | li 2. Share link to issue with repository collaborators. 45 | li 3. Vote on issue and see results as on Ember.js repo. 46 | h2.title Feedback 47 | p If you run into some problem with app or need extra feature please create new issue on GitHub and we will vote on it. 48 | div.page.invisible#create-poll-page 49 | div.page-container 50 | section.page-content 51 | form(onsubmit="return false;") 52 | h1.title Title 53 | input#issue-title(placeholder="Name of issue...", required) 54 | select#user-repos-select(required) 55 | option Choose repository 56 | textarea#issue-description(rows="4", cols="50", placeholder="Add some description...") 57 | div.info-bar.invisible 58 | span Want to add somebody personally? 59 | input#mandatory-voter 60 | button#add-mandatory-voter-button Add 61 | div#mandatory-voter-container 62 | span#issue-labels Labels: 63 | input#issue-label 64 | button#add-issue-label-button Add 65 | div#issue-labels-container 66 | button#create-issue-button.btn Create poll 67 | div.page.invisible#poll-page 68 | div.page-container 69 | section.page-content 70 | p.error.invisible Sorry, there is no issue on given URL, please check, if URL is correct. 71 | a(href="", target="_blank") 72 | h1#poll-title.title 73 | div#poll-description 74 | div#no-results.invisible Nobody voted yet. Be the first! 75 | div#results 76 | div#yes Yes: 77 | span 78 | div#no No: 79 | span 80 | div#issue-comments 81 | div#vote-btns 82 | button#yes-btn.btn +1 83 | button#no-btn.btn -1 84 | div#chart-panel.invisible 85 | //- footer#app-footer 86 | //- div.container 87 | //- p GitPoll is not affiliated with GitHub. 88 | //- p Home icon and favicon symbol by The Noun Project team. 89 | //- p GitPoll brought to you by Hashobject team. 90 | //- p GitPoll is open source project hosted on GitHub. 91 | a(href="https://github.com/gitrun/poll") 92 | img(style="position: absolute; top: 0; right: 0; border: 0;", src="https://s3.amazonaws.com/github/ribbons/forkme_right_red_aa0000.png", alt="Fork me on GitHub") 93 | script(src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore-min.js") 94 | script(src="/page.js") 95 | script(src="https://code.jquery.com/jquery-1.11.2.min.js") 96 | script(src="https://cdnjs.cloudflare.com/ajax/libs/spin.js/2.0.1/spin.min.js") 97 | script(src="https://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js") 98 | script(src="/jquery.spin.min.js") 99 | script(src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.1/d3.min.js") 100 | script(src="https://cdnjs.cloudflare.com/ajax/libs/c3/0.4.7/c3.min.js") 101 | script(src="/app.js") -------------------------------------------------------------------------------- /public/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | span, 5 | applet, 6 | object, 7 | iframe, 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6, 14 | p, 15 | blockquote, 16 | pre, 17 | a, 18 | abbr, 19 | acronym, 20 | address, 21 | big, 22 | cite, 23 | code, 24 | del, 25 | dfn, 26 | em, 27 | img, 28 | ins, 29 | kbd, 30 | q, 31 | s, 32 | samp, 33 | small, 34 | strike, 35 | strong, 36 | sub, 37 | sup, 38 | tt, 39 | var, 40 | dl, 41 | dt, 42 | dd, 43 | ol, 44 | ul, 45 | li, 46 | fieldset, 47 | form, 48 | label, 49 | legend, 50 | table, 51 | caption, 52 | tbody, 53 | tfoot, 54 | thead, 55 | tr, 56 | th, 57 | td { 58 | margin: 0; 59 | padding: 0; 60 | border: 0; 61 | outline: 0; 62 | font-weight: inherit; 63 | font-style: inherit; 64 | font-family: inherit; 65 | font-size: 100%; 66 | vertical-align: baseline; 67 | } 68 | body { 69 | line-height: 1; 70 | color: #000; 71 | background: #fff; 72 | } 73 | ol, 74 | ul { 75 | list-style: none; 76 | } 77 | table { 78 | border-collapse: separate; 79 | border-spacing: 0; 80 | vertical-align: middle; 81 | } 82 | caption, 83 | th, 84 | td { 85 | text-align: left; 86 | font-weight: normal; 87 | vertical-align: middle; 88 | } 89 | a img { 90 | border: none; 91 | } 92 | * { 93 | -webkit-box-sizing: border-box; 94 | -moz-box-sizing: border-box; 95 | box-sizing: border-box; 96 | } 97 | html, 98 | body { 99 | background-color: #fff; 100 | font-family: Helvetica, arial, freesans, clean, sans-serif; 101 | font-size: 13px; 102 | color: #666; 103 | line-height: 1.6; 104 | } 105 | .container { 106 | width: 740px; 107 | margin: 0 auto; 108 | } 109 | header.container { 110 | margin-top: 10px; 111 | margin-bottom: 20px; 112 | height: 30px; 113 | } 114 | header.container a, 115 | header.container a:visited { 116 | white-space: nowrap; 117 | text-decoration: none; 118 | color: #333; 119 | } 120 | header.container a:hover { 121 | color: #fff; 122 | } 123 | #app-footer { 124 | border-top: 1px solid #e0e0e0; 125 | position: fixed; 126 | bottom: 0; 127 | width: 100%; 128 | height: 100px; 129 | } 130 | #app-footer .container { 131 | margin-top: 20px; 132 | } 133 | #app-footer .container p { 134 | display: inline-block; 135 | vertical-align: top; 136 | margin: 10px 10px 0 0; 137 | } 138 | #username { 139 | color: #4a3e7b; 140 | line-height: 22px; 141 | font-weight: bold; 142 | text-shadow: 0 1px 0 #fff; 143 | } 144 | #username:hover { 145 | text-decoration: underline; 146 | } 147 | .page { 148 | position: relative; 149 | width: 740px; 150 | } 151 | .page-container { 152 | margin-bottom: 15px; 153 | padding: 3px; 154 | background: #eee; 155 | -webkit-border-radius: 4px; 156 | border-radius: 4px; 157 | } 158 | .page-content { 159 | padding: 10px; 160 | background: #fff; 161 | border: 1px solid #ddd; 162 | -webkit-border-radius: 2px; 163 | border-radius: 2px; 164 | position: relative; 165 | min-height: 160px; 166 | } 167 | .title { 168 | font: 13px Helvetica, arial, freesans, clean, sans-serif; 169 | position: relative; 170 | font-size: 14px; 171 | font-weight: bold; 172 | color: #333; 173 | } 174 | #app-intro-page .title { 175 | margin: 10px 0 15px; 176 | } 177 | #issue-title { 178 | width: 49%; 179 | font-size: 16px; 180 | font-weight: bold; 181 | color: #444; 182 | -webkit-box-sizing: border-box; 183 | -moz-box-sizing: border-box; 184 | box-sizing: border-box; 185 | margin-right: 5px; 186 | padding: 6px; 187 | color: #666; 188 | background-repeat: no-repeat; 189 | background-position: right center; 190 | } 191 | #issue-description { 192 | margin-top: 15px; 193 | } 194 | .info-bar { 195 | display: inline-block; 196 | margin: 15px 0 -10px; 197 | width: 100%; 198 | padding: 10px 10px 4px; 199 | border: 1px solid #e5e5e5; 200 | border-left: none; 201 | border-right: none; 202 | background: whiteSmoke; 203 | position: absolute; 204 | left: 0; 205 | right: 0; 206 | bottom: 9px; 207 | } 208 | textarea { 209 | -webkit-box-sizing: border-box; 210 | -moz-box-sizing: border-box; 211 | box-sizing: border-box; 212 | width: 100%; 213 | padding: 5px; 214 | height: 150px; 215 | font-size: 12px; 216 | } 217 | .btn { 218 | color: #fff; 219 | text-shadow: 0 -1px 0 rgba(0,0,0,0.4); 220 | background: #ff8a66; 221 | background: -webkit-linear-gradient(#ff8a66, #ed4706); 222 | background: -moz-linear-gradient(#ff8a66, #ed4706); 223 | background: -o-linear-gradient(#ff8a66, #ed4706); 224 | background: -ms-linear-gradient(#ff8a66, #ed4706); 225 | background: linear-gradient(#ff8a66, #ed4706); 226 | -webkit-box-shadow: 0 1px 4px rgba(0,0,0,0.2); 227 | box-shadow: 0 1px 4px rgba(0,0,0,0.2); 228 | display: inline-block; 229 | padding: 8px 15px; 230 | line-height: normal; 231 | position: relative; 232 | font-size: 12px; 233 | font-weight: bold; 234 | -webkit-border-radius: 3px; 235 | border-radius: 3px; 236 | border: 1px solid; 237 | border-color: #e46432; 238 | border-bottom-color: #c13d14; 239 | } 240 | .btn:hover { 241 | background: #ee6a36; 242 | background: -webkit-linear-gradient(#ee6a36, #be410e); 243 | background: -moz-linear-gradient(#ee6a36, #be410e); 244 | background: -o-linear-gradient(#ee6a36, #be410e); 245 | background: -ms-linear-gradient(#ee6a36, #be410e); 246 | background: linear-gradient(#ee6a36, #be410e); 247 | border-color: #d64910; 248 | color: #fff; 249 | } 250 | button#create-issue-button { 251 | position: absolute; 252 | right: 5px; 253 | margin-top: 15px; 254 | } 255 | #user-repos-select { 256 | width: 49%; 257 | font-size: 16px; 258 | font-weight: bold; 259 | color: #444; 260 | -webkit-box-sizing: border-box; 261 | -moz-box-sizing: border-box; 262 | box-sizing: border-box; 263 | padding: 6px 6px; 264 | color: #666; 265 | background-repeat: no-repeat; 266 | background-position: right center; 267 | border-color: rgba(102,102,102,0.15); 268 | } 269 | #user-repos-select option { 270 | font-size: 12px; 271 | font-weight: normal; 272 | } 273 | header { 274 | color: #666; 275 | font-size: 16px; 276 | width: 100%; 277 | position: relative; 278 | } 279 | header img { 280 | width: 22px; 281 | vertical-align: -6px; 282 | margin: 0 3px 0 10px; 283 | -webkit-border-radius: 11px; 284 | border-radius: 11px; 285 | } 286 | #login, 287 | #logout { 288 | color: #333; 289 | padding: 0 10px 0 10px; 290 | font-weight: bold; 291 | line-height: 24px; 292 | text-shadow: 0 1px 0 #fff; 293 | border: 1px solid #d4d4d4; 294 | border-bottom-color: #bcbcbc; 295 | -webkit-border-radius: 3px; 296 | border-radius: 3px; 297 | background: #fafafa; 298 | background: -webkit-linear-gradient(#fafafa, #eaeaea); 299 | background: -moz-linear-gradient(#fafafa, #eaeaea); 300 | background: -o-linear-gradient(#fafafa, #eaeaea); 301 | background: -ms-linear-gradient(#fafafa, #eaeaea); 302 | background: linear-gradient(#fafafa, #eaeaea); 303 | cursor: pointer; 304 | } 305 | #logout { 306 | position: absolute; 307 | right: 5px; 308 | bottom: -9px; 309 | } 310 | #login:hover, 311 | #logout:hover { 312 | color: #fff; 313 | text-decoration: none; 314 | text-shadow: 0 -1px 0 rgba(0,0,0,0.3); 315 | border-color: #1b0e53; 316 | border-bottom-color: #150a43; 317 | background: #716896; 318 | background: -webkit-linear-gradient(#716896, #1d0d5a); 319 | background: -moz-linear-gradient(#716896, #1d0d5a); 320 | background: -o-linear-gradient(#716896, #1d0d5a); 321 | background: -ms-linear-gradient(#716896, #1d0d5a); 322 | background: linear-gradient(#716896, #1d0d5a); 323 | } 324 | #no-results { 325 | margin-bottom: 10px; 326 | margin-top: 20px; 327 | } 328 | #chart-panel { 329 | margin: -35px auto 0; 330 | height: 400px; 331 | position: relative; 332 | } 333 | #poll-description { 334 | margin-top: 10px; 335 | margin-bottom: 10px; 336 | } 337 | #yes, 338 | #no { 339 | font-weight: bold; 340 | } 341 | #yes span, 342 | #no span { 343 | margin-left: 4px; 344 | } 345 | .not-valid { 346 | border: 1px solid #f00 !important; 347 | } 348 | g.slice:nth-child(1) { 349 | fill: #ed4706; 350 | } 351 | g.slice:nth-child(2) { 352 | fill: #232063; 353 | } 354 | footer a { 355 | color: #4a3e7b; 356 | text-decoration: none; 357 | } 358 | footer a:hover { 359 | text-decoration: underline; 360 | } 361 | header svg { 362 | width: auto; 363 | display: inline-block; 364 | } 365 | header svg path { 366 | fill: #4a3e7b; 367 | } 368 | #home { 369 | margin-right: 15px; 370 | display: inline-block; 371 | bottom: -8px; 372 | width: 30px; 373 | height: 30px; 374 | position: relative; 375 | } 376 | #home:hover path { 377 | fill: #ff8a66; 378 | } 379 | .visible { 380 | display: block; 381 | } 382 | .inline-visible { 383 | display: inline-block; 384 | } 385 | .invisible { 386 | display: none; 387 | } 388 | -------------------------------------------------------------------------------- /public/page.js: -------------------------------------------------------------------------------- 1 | 2 | ;(function(){ 3 | 4 | /** 5 | * Perform initial dispatch. 6 | */ 7 | 8 | var dispatch = true; 9 | 10 | /** 11 | * Base path. 12 | */ 13 | 14 | var base = ''; 15 | 16 | /** 17 | * Running flag. 18 | */ 19 | 20 | var running; 21 | 22 | /** 23 | * Register `path` with callback `fn()`, 24 | * or route `path`, or `page.start()`. 25 | * 26 | * page('/user/:id', load, user); 27 | * page('/user/' + user.id, { some: 'thing' }); 28 | * page('/user/' + user.id); 29 | * page(); 30 | * 31 | * @param {String} path 32 | * @param {Function} fn... 33 | * @api public 34 | */ 35 | 36 | function page(path, fn) { 37 | // route to 38 | if ('function' == typeof fn) { 39 | var route = new Route(path); 40 | for (var i = 1; i < arguments.length; ++i) { 41 | page.callbacks.push(route.middleware(arguments[i])); 42 | } 43 | // show with [state] 44 | } else if ('string' == typeof path) { 45 | page.show(path, fn); 46 | // start [options] 47 | } else { 48 | page.start(path); 49 | } 50 | } 51 | 52 | /** 53 | * Callback functions. 54 | */ 55 | 56 | page.callbacks = []; 57 | 58 | /** 59 | * Get or set basepath to `path`. 60 | * 61 | * @param {String} path 62 | * @api public 63 | */ 64 | 65 | page.base = function(path){ 66 | if (0 == arguments.length) return base; 67 | base = path; 68 | }; 69 | 70 | /** 71 | * Bind with the given `options`. 72 | * 73 | * Options: 74 | * 75 | * - `click` bind to click events [true] 76 | * - `popstate` bind to popstate [true] 77 | * - `dispatch` perform initial dispatch [true] 78 | * 79 | * @param {Object} options 80 | * @api public 81 | */ 82 | 83 | page.start = function(options){ 84 | options = options || {}; 85 | if (running) return; 86 | running = true; 87 | if (false === options.dispatch) dispatch = false; 88 | if (false !== options.popstate) addEventListener('popstate', onpopstate, false); 89 | if (false !== options.click) addEventListener('click', onclick, false); 90 | if (!dispatch) return; 91 | page.replace(location.pathname, null, true, dispatch); 92 | }; 93 | 94 | /** 95 | * Unbind click and popstate event handlers. 96 | * 97 | * @api public 98 | */ 99 | 100 | page.stop = function(){ 101 | running = false; 102 | removeEventListener('click', onclick, false); 103 | removeEventListener('popstate', onpopstate, false); 104 | }; 105 | 106 | /** 107 | * Show `path` with optional `state` object. 108 | * 109 | * @param {String} path 110 | * @param {Object} state 111 | * @return {Context} 112 | * @api public 113 | */ 114 | 115 | page.show = function(path, state){ 116 | var ctx = new Context(path, state); 117 | page.dispatch(ctx); 118 | if (!ctx.unhandled) ctx.pushState(); 119 | return ctx; 120 | }; 121 | 122 | /** 123 | * Replace `path` with optional `state` object. 124 | * 125 | * @param {String} path 126 | * @param {Object} state 127 | * @return {Context} 128 | * @api public 129 | */ 130 | 131 | page.replace = function(path, state, init, dispatch){ 132 | var ctx = new Context(path, state); 133 | ctx.init = init; 134 | if (null == dispatch) dispatch = true; 135 | if (dispatch) page.dispatch(ctx); 136 | ctx.save(); 137 | return ctx; 138 | }; 139 | 140 | /** 141 | * Dispatch the given `ctx`. 142 | * 143 | * @param {Object} ctx 144 | * @api private 145 | */ 146 | 147 | page.dispatch = function(ctx){ 148 | var i = 0; 149 | 150 | function next() { 151 | var fn = page.callbacks[i++]; 152 | if (!fn) return unhandled(ctx); 153 | fn(ctx, next); 154 | } 155 | 156 | next(); 157 | }; 158 | 159 | /** 160 | * Unhandled `ctx`. When it's not the initial 161 | * popstate then redirect. If you wish to handle 162 | * 404s on your own use `page('*', callback)`. 163 | * 164 | * @param {Context} ctx 165 | * @api private 166 | */ 167 | 168 | function unhandled(ctx) { 169 | if (window.location.pathname == ctx.canonicalPath) return; 170 | page.stop(); 171 | ctx.unhandled = true; 172 | window.location = ctx.canonicalPath; 173 | } 174 | 175 | /** 176 | * Initialize a new "request" `Context` 177 | * with the given `path` and optional initial `state`. 178 | * 179 | * @param {String} path 180 | * @param {Object} state 181 | * @api public 182 | */ 183 | 184 | function Context(path, state) { 185 | if ('/' == path[0] && 0 != path.indexOf(base)) path = base + path; 186 | this.canonicalPath = path; 187 | this.path = path.replace(base, '') || '/'; 188 | this.title = document.title; 189 | this.state = state || {}; 190 | this.state.path = path; 191 | this.params = []; 192 | } 193 | 194 | /** 195 | * Push state. 196 | * 197 | * @api private 198 | */ 199 | 200 | Context.prototype.pushState = function(){ 201 | history.pushState(this.state, this.title, this.canonicalPath); 202 | }; 203 | 204 | /** 205 | * Save the context state. 206 | * 207 | * @api public 208 | */ 209 | 210 | Context.prototype.save = function(){ 211 | history.replaceState(this.state, this.title, this.canonicalPath); 212 | }; 213 | 214 | /** 215 | * Initialize `Route` with the given HTTP `path`, 216 | * and an array of `callbacks` and `options`. 217 | * 218 | * Options: 219 | * 220 | * - `sensitive` enable case-sensitive routes 221 | * - `strict` enable strict matching for trailing slashes 222 | * 223 | * @param {String} path 224 | * @param {Object} options. 225 | * @api private 226 | */ 227 | 228 | function Route(path, options) { 229 | options = options || {}; 230 | this.path = path; 231 | this.method = 'GET'; 232 | this.regexp = pathtoRegexp(path 233 | , this.keys = [] 234 | , options.sensitive 235 | , options.strict); 236 | } 237 | 238 | /** 239 | * Return route middleware with 240 | * the given callback `fn()`. 241 | * 242 | * @param {Function} fn 243 | * @return {Function} 244 | * @api public 245 | */ 246 | 247 | Route.prototype.middleware = function(fn){ 248 | var self = this; 249 | return function(ctx, next){ 250 | if (self.match(ctx.path, ctx.params)) return fn(ctx, next); 251 | next(); 252 | } 253 | }; 254 | 255 | /** 256 | * Check if this route matches `path`, if so 257 | * populate `params`. 258 | * 259 | * @param {String} path 260 | * @param {Array} params 261 | * @return {Boolean} 262 | * @api private 263 | */ 264 | 265 | Route.prototype.match = function(path, params){ 266 | var keys = this.keys 267 | , m = this.regexp.exec(path); 268 | 269 | if (!m) return false; 270 | 271 | for (var i = 1, len = m.length; i < len; ++i) { 272 | var key = keys[i - 1]; 273 | 274 | var val = 'string' == typeof m[i] 275 | ? decodeURIComponent(m[i]) 276 | : m[i]; 277 | 278 | if (key) { 279 | params[key.name] = undefined !== params[key.name] 280 | ? params[key.name] 281 | : val; 282 | } else { 283 | params.push(val); 284 | } 285 | } 286 | 287 | return true; 288 | }; 289 | 290 | /** 291 | * Normalize the given path string, 292 | * returning a regular expression. 293 | * 294 | * An empty array should be passed, 295 | * which will contain the placeholder 296 | * key names. For example "/user/:id" will 297 | * then contain ["id"]. 298 | * 299 | * @param {String|RegExp|Array} path 300 | * @param {Array} keys 301 | * @param {Boolean} sensitive 302 | * @param {Boolean} strict 303 | * @return {RegExp} 304 | * @api private 305 | */ 306 | 307 | function pathtoRegexp(path, keys, sensitive, strict) { 308 | if (path instanceof RegExp) return path; 309 | if (path instanceof Array) path = '(' + path.join('|') + ')'; 310 | path = path 311 | .concat(strict ? '' : '/?') 312 | .replace(/\/\(/g, '(?:/') 313 | .replace(/\+/g, '__plus__') 314 | .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional){ 315 | keys.push({ name: key, optional: !! optional }); 316 | slash = slash || ''; 317 | return '' 318 | + (optional ? '' : slash) 319 | + '(?:' 320 | + (optional ? slash : '') 321 | + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' 322 | + (optional || ''); 323 | }) 324 | .replace(/([\/.])/g, '\\$1') 325 | .replace(/__plus__/g, '(.+)') 326 | .replace(/\*/g, '(.*)'); 327 | return new RegExp('^' + path + '$', sensitive ? '' : 'i'); 328 | }; 329 | 330 | /** 331 | * Handle "populate" events. 332 | */ 333 | 334 | function onpopstate(e) { 335 | if (e.state) { 336 | var path = e.state.path; 337 | page.replace(path, e.state); 338 | } 339 | } 340 | 341 | /** 342 | * Handle "click" events. 343 | */ 344 | 345 | function onclick(e) { 346 | if (e.defaultPrevented) return; 347 | var el = e.target; 348 | while (el && 'A' != el.nodeName) el = el.parentNode; 349 | if (!el || 'A' != el.nodeName) return; 350 | var href = el.href; 351 | var path = el.pathname; 352 | if (el.hash) return; 353 | if (!sameOrigin(href)) return; 354 | var orig = path; 355 | path = path.replace(base, ''); 356 | if (base && orig == path) return; 357 | e.preventDefault(); 358 | page.show(orig); 359 | } 360 | 361 | /** 362 | * Check if `href` is the same origin. 363 | */ 364 | 365 | function sameOrigin(href) { 366 | var origin = location.protocol + '//' + location.hostname; 367 | if (location.port) origin += ':' + location.port; 368 | return 0 == href.indexOf(origin); 369 | } 370 | 371 | /** 372 | * Expose `page`. 373 | */ 374 | 375 | if ('undefined' == typeof module) { 376 | window.page = page; 377 | } else { 378 | module.exports = page; 379 | } 380 | 381 | })(); -------------------------------------------------------------------------------- /public/repo.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Repo.js 3 | * @author Darcy Clarke 4 | * 5 | * Copyright (c) 2012 Darcy Clarke 6 | * Dual licensed under the MIT and GPL licenses. 7 | * http://darcyclarke.me/ 8 | */ 9 | 10 | /*! 11 | * @mekwall's .vangogh() for Syntax Highlighting 12 | * 13 | * All code is open source and dual licensed under GPL and MIT. 14 | * Check the individual licenses for more information. 15 | * https://github.com/mekwall/jquery-vangogh/blob/master/GPL-LICENSE.txt 16 | * https://github.com/mekwall/jquery-vangogh/blob/master/MIT-LICENSE.txt 17 | */ 18 | 19 | @font-face { 20 | font-family: "Octicons Regular"; 21 | src: url("https://a248.e.akamai.net/assets.github.com/fonts/octicons/octicons-regular-webfont.eot?639c50d4"); 22 | src: url("https://a248.e.akamai.net/assets.github.com/fonts/octicons/octicons-regular-webfont.eot?639c50d4#iefix") format("embedded-opentype"), url("https://a248.e.akamai.net/assets.github.com/fonts/octicons/octicons-regular-webfont.woff?0605b255") format("woff"), url("https://a248.e.akamai.net/assets.github.com/fonts/octicons/octicons-regular-webfont.ttf?f82fcba7") format("truetype"), url("https://a248.e.akamai.net/assets.github.com/fonts/octicons/octicons-regular-webfont.svg?1f7afa21#newFontRegular") format("svg"); 23 | font-weight: normal; 24 | font-style: normal; 25 | } 26 | .repo, .repo * { 27 | -webkit-box-sizing: border-box; 28 | -moz-box-sizing: border-box; 29 | -ms-box-sizing: border-box; 30 | box-sizing: border-box; 31 | } 32 | .repo ul * { 33 | display: block; 34 | font-family: sans-serif; 35 | font-size: 13px; 36 | line-height: 18px; 37 | } 38 | .repo { 39 | width: 100%; 40 | margin: 0 0 15px 0; 41 | position: relative; 42 | padding-bottom: 1px; 43 | color: #555; 44 | overflow: hidden; 45 | height: 300px; 46 | -webkit-transition: height .25s; 47 | -moz-transition: height .25s; 48 | -o-transition: height .25s; 49 | -ms-transition: height .25s; 50 | transition: height .25s; 51 | } 52 | .repo .page { 53 | background: #f8f8f8; 54 | border: 4px solid rgba(0, 0, 0, 0.08); 55 | border-radius: 3px; 56 | -ms-filter: "alpha(opacity=0)"; 57 | filter: alpha(opacity=0); 58 | opacity: 0; 59 | left: 100%; 60 | width: 740px; 61 | position: absolute; 62 | -webkit-transition: all .25s; 63 | -moz-transition: all .25s; 64 | -o-transition: all .25s; 65 | -ms-transition: all .25s; 66 | transition: all .25s; 67 | } 68 | .repo .page.visible { 69 | left: 1%!important; 70 | -ms-filter: "alpha(opacity=100)"; 71 | filter: alpha(opacity=100); 72 | opacity: 1; 73 | display: block; 74 | padding: 10px; 75 | } 76 | .repo .page.left { 77 | left: -100%} 78 | .repo .loader { 79 | position: absolute; 80 | display: block; 81 | width: 100%; 82 | height: 300px; 83 | top: 0; 84 | left: 0; 85 | background: url(https://a248.e.akamai.net/assets.github.com/images/spinners/octocat-spinner-64.gif?1329872007) no-repeat center 50%} 86 | .repo.loaded .loader { 87 | display: none; 88 | } 89 | .repo h1 { 90 | padding: 0 0 0 10px; 91 | font-family: sans-serif; 92 | font-size: 20px; 93 | line-height: 26px; 94 | color: #000; 95 | font-weight: normal; 96 | } 97 | .repo h1 a:nth-of-type(1), .repo h1 a.active { 98 | font-weight: bold; 99 | } 100 | .repo h1 a.active, .repo h1 a.active:active, .repo h1 a.active:visited, .repo h1 a.active:hover { 101 | color: #000; 102 | } 103 | .repo h1 a, .repo h1 a:active, .repo h1 a:visited, .repo h1 a:hover { 104 | color: #4183c4; 105 | text-decoration: none; 106 | } 107 | .repo h1 a:after { 108 | content: "/"; 109 | color: #999; 110 | padding: 0 5px; 111 | font-weight: normal; 112 | } 113 | .repo h1 a:last-child:after { 114 | content: ""} 115 | .repo .page, .repo ul { 116 | zoom: 1; 117 | } 118 | .repo .page:before, .repo .page:after, .repo ul:before, .repo ul:after { 119 | content: ""; 120 | display: table; 121 | } 122 | .repo .page:after, .repo ul:after { 123 | clear: both; 124 | } 125 | .repo ul { 126 | border: 1px solid rgba(0, 0, 0, 0.25); 127 | margin: 0; 128 | padding: 0; 129 | } 130 | .repo li { 131 | width: 100%; 132 | margin: 0; 133 | padding: 0; 134 | float: left; 135 | border-bottom: 1px solid #ccc; 136 | position: relative; 137 | white-space: nowrap; 138 | } 139 | .repo li.titles { 140 | background: -webkit-linear-gradient(#fafafa, #eaeaea); 141 | background: -moz-linear-gradient(#fafafa, #eaeaea); 142 | background: -o-linear-gradient(#fafafa, #eaeaea); 143 | background: -ms-linear-gradient(#fafafa, #eaeaea); 144 | background: linear-gradient(#fafafa, #eaeaea); 145 | font-weight: bold; 146 | padding: 10px 10px 8px 36px; 147 | text-shadow: 0 1px 0 #fff; 148 | } 149 | .repo li:before { 150 | content: " "; 151 | font-family: "Octicons Regular"; 152 | position: absolute; 153 | top: 10px; 154 | left: 10px; 155 | font-size: 18px; 156 | -webkit-font-smoothing: antialiased; 157 | } 158 | .repo li.dir:before { 159 | content: " "; 160 | color: #80a6cd; 161 | } 162 | .repo li.titles:before, .repo li.back:before { 163 | content: ""} 164 | .repo li:last-child { 165 | border: 0; 166 | padding-bottom: none; 167 | margin: 0; 168 | } 169 | .repo li a, .repo li a:visited, .repo li a:active { 170 | color: #4183c4; 171 | width: 100%; 172 | padding: 10px 10px 8px 36px; 173 | display: block; 174 | text-decoration: none; 175 | } 176 | .repo li a:hover { 177 | text-decoration: underline; 178 | } 179 | .repo li span { 180 | display: inline-block; 181 | } 182 | .repo li span:nth-of-type(1) { 183 | width: 30%} 184 | .repo li span:nth-of-type(2) { 185 | width: 20%} 186 | .repo li span:nth-of-type(3) { 187 | width: 40%} 188 | .repo .vg-container { 189 | position: relative; 190 | overflow: auto; 191 | white-space: pre!important; 192 | word-wrap: normal!important; 193 | } 194 | .repo .vg-container, .repo .vg-code { 195 | border: 0; 196 | margin: 0; 197 | overflow: auto; 198 | } 199 | .repo .vg-code .vg-line, .repo .vg-gutter .vg-number { 200 | display: block; 201 | height: 1.5em; 202 | line-height: 1.5em!important; 203 | } 204 | .repo .vg-gutter { 205 | float: left; 206 | min-width: 20px; 207 | width: auto; 208 | -webkit-user-select: none; 209 | -moz-user-select: none; 210 | -ms-user-select: none; 211 | user-select: none; 212 | } 213 | .repo .vg-number { 214 | cursor: pointer; 215 | } 216 | .repo .vg-container { 217 | font-family: "Bitstream Vera Sans Mono", "Courier New", monospace; 218 | font-size: 13px; 219 | border: 1px solid #ddd; 220 | } 221 | .repo .vg-gutter { 222 | background-color: #ececec; 223 | border-right: 1px solid #ddd; 224 | text-align: right; 225 | color: #aaa; 226 | padding: 1em .5em; 227 | margin-right: .5em; 228 | } 229 | .repo .vg-code *::-moz-selection, .repo .vg-code *::-webkit-selection, .repo .vg-code *::selection, .repo .vg-line.vg-highlight { 230 | background-color: #ffc; 231 | } 232 | .repo .vg-line span.vg-highlight { 233 | color: blue; 234 | font-weight: bold; 235 | text-decoration: underline; 236 | } 237 | .repo .vg-container .vg-code { 238 | display: block; 239 | padding: 1em .5em; 240 | background: #fff; 241 | } 242 | .repo .vg-code { 243 | color: #000; 244 | background: #f8f8ff; 245 | border: 0; 246 | padding: .4em; 247 | } 248 | .repo .vg-code .comment, .repo .vg-code .template_comment, .repo .vg-code .diff .header, .repo .vg-code .javadoc { 249 | color: #998; 250 | font-style: italic; 251 | } 252 | .repo .vg-code .keyword, .repo .vg-code .css .rule .keyword, .repo .vg-code .winutils, .repo .vg-code .javascript .title, .repo .vg-code .lisp .title, .repo .vg-code .subst { 253 | color: #000; 254 | font-weight: bold; 255 | } 256 | .vg-code .number, .vg-code .hexcolor { 257 | color: #40a070; 258 | } 259 | .vg-code .string, .repo .vg-code .tag .value, .repo .vg-code .phpdoc, .repo .vg-code .tex .formula { 260 | color: #d14; 261 | } 262 | .repo .vg-code .title, .repo .vg-code .id { 263 | color: #900; 264 | font-weight: bold; 265 | } 266 | .repo .vg-code .javascript .title, .repo .vg-code .lisp .title, .repo .vg-code .subst { 267 | font-weight: normal; 268 | } 269 | .repo .vg-code .class .title, .repo .vg-code .haskell .label, .repo .vg-code .tex .command { 270 | color: #458; 271 | font-weight: bold; 272 | } 273 | .repo .vg-code .tag, .repo .vg-code .tag .title, .repo .vg-code .rules .property, .repo .vg-code .django .tag .keyword { 274 | color: #000080; 275 | font-weight: normal; 276 | } 277 | .repo .vg-code .attribute, .repo .vg-code .variable, .repo .vg-code .instancevar, .repo .vg-code .lisp .body { 278 | color: #008080; 279 | } 280 | .repo .vg-code .regexp { 281 | color: #009926; 282 | } 283 | .repo .vg-code .class { 284 | color: #458; 285 | font-weight: bold; 286 | } 287 | .repo .vg-code .symbol, .repo .vg-code .ruby .symbol .string, .repo .vg-code .ruby .symbol .keyword, .repo .vg-code .ruby .symbol .keymethods, .repo .vg-code .lisp .keyword, .repo .vg-code .tex .special, .repo .vg-code .input_number { 288 | color: #990073; 289 | } 290 | .repo .vg-code .builtin, .repo .vg-code .built_in, .repo .vg-code .lisp .title { 291 | color: #0086b3; 292 | } 293 | .repo .vg-code .codeprocessor, .repo .vg-code .pi, .repo .vg-code .doctype, .repo .vg-code .shebang, .repo .vg-code .cdata { 294 | color: #999; 295 | font-weight: bold; 296 | } 297 | .repo .vg-code .deletion { 298 | background: #fdd; 299 | } 300 | .repo .vg-code .addition { 301 | background: #dfd; 302 | } 303 | .repo .vg-code .diff .change { 304 | background: #0086b3; 305 | } 306 | .repo .vg-code .chunk { 307 | color: #aaa; 308 | } 309 | .repo .vg-code .tex .formula { 310 | -ms-filter: "alpha(opacity=50)"; 311 | filter: alpha(opacity=50); 312 | opacity: .5; 313 | } 314 | .repo .image { 315 | padding: 1em .5em; 316 | text-align: center; 317 | } 318 | .repo .image .border-wrap { 319 | background-color: #fff; 320 | border: 1px solid #999; 321 | line-height: 0; 322 | display: inline-block; 323 | } 324 | .repo .image img { 325 | border: 1px solid #fff; 326 | background: url(https://a248.e.akamai.net/assets.github.com/images/modules/commit/trans_bg.gif?); 327 | } -------------------------------------------------------------------------------- /public/app.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | 'use strict'; 3 | // LOGIN 4 | function checkIfUserLoggedIn() { 5 | var userInfo = getLocalStorageData("lsUserInfo"); //----- lsUserInfo - localStorage user (name of string in LS) 6 | if (userInfo == undefined) { 7 | if (gp.user.username == "guest") { 8 | return "guest"; 9 | } 10 | else { 11 | localStorage.setItem("lsUserInfo", JSON.stringify(gp.user)); 12 | } 13 | } else { 14 | gp.user = userInfo; 15 | } 16 | } 17 | 18 | function makeHtml(md){ 19 | var mdConverter = new Showdown.converter(); 20 | return mdConverter.makeHtml(md); 21 | } 22 | 23 | var userReposContainer = $('#user-repos-select'); 24 | function showUserRepos() { 25 | var urlUserRepos = defineUrl("/user/repos", gp.user.accessToken); 26 | $.getJSON(urlUserRepos, function(userRepos){ 27 | var html = ""; 28 | 29 | var m = 0; 30 | for (; m < userRepos.length; m++) { 31 | html += ""; 32 | } 33 | userReposContainer.append(html); 34 | }); 35 | 36 | var urlUserOrgs = defineUrl("/user/orgs", gp.user.accessToken); 37 | $.getJSON(urlUserOrgs, function(userOrgs){ 38 | var userOrgsArray = []; 39 | 40 | var i = 0; 41 | for (; i < userOrgs.length; i++) { 42 | userOrgsArray.push(userOrgs[i].login); 43 | } 44 | 45 | var k = 0; 46 | for (; k < userOrgsArray.length; k++) { 47 | var urlUserOrgsRepos = defineUrl("/orgs/" + userOrgsArray[k] + "/repos", gp.user.accessToken); 48 | $.getJSON(urlUserOrgsRepos, function(orgRepos){ 49 | var html =""; 50 | 51 | var l = 0; 52 | for (; l < orgRepos.length; l++) { 53 | html += ""; 54 | } 55 | userReposContainer.append(html); 56 | }); 57 | } 58 | }); 59 | } 60 | 61 | function checkValidity() { 62 | var isValid = true; 63 | if ($("#issue-title").get(0).checkValidity() == false) { 64 | $("#issue-title").addClass("not-valid"); 65 | isValid = false; 66 | } else { 67 | $("#issue-title").removeClass("not-valid"); 68 | } 69 | if ($("#user-repos-select").val() == "Choose repository") { 70 | $("#user-repos-select").addClass("not-valid"); 71 | isValid = false; 72 | } else { 73 | $("#user-repos-select").removeClass("not-valid"); 74 | } 75 | return isValid; 76 | } 77 | 78 | $("#create-issue-button").on("click", function(e) { 79 | e.preventDefault(); 80 | 81 | if (checkValidity() == true) { 82 | $("#create-poll-page .page-container").removeClass("visible").addClass("invisible"); 83 | $("#create-poll-page").spin(); 84 | 85 | var issueTitle = $("#issue-title").val(); 86 | var issueDescription = $("#issue-description").val(); 87 | var issueRepoFullname = $("#user-repos-select").val(); 88 | var issueMandatoryVoter = $("#mandatory-voter").val(); 89 | var issueLabel = $("issue-label").val(); 90 | 91 | var issueData = {"title": issueTitle, "body": issueDescription}; 92 | 93 | var url = defineUrl('/repos/' + issueRepoFullname + '/issues', gp.user.accessToken); 94 | 95 | $.post(url, JSON.stringify(issueData), function(data) { 96 | $("#create-poll-page .page-container").removeClass("invisible").addClass("visible"); 97 | $("#create-poll-page").spin(false); 98 | 99 | var url = data.html_url.split("/") 100 | 101 | page("/" + url[3] + "/" + url[4] + "/" + url[5] + "/" + url[6]); 102 | }); 103 | } 104 | }); 105 | 106 | function logOutUser() { 107 | localStorage.clear(); 108 | page('/logout'); 109 | } 110 | 111 | $("#logout").on("click", logOutUser); 112 | 113 | $("#home").on("click", function(e){ 114 | e.preventDefault() 115 | page("/"); 116 | }); 117 | // POLL PAGE 118 | 119 | function buildPollPage(urlIssue, urlComments){ 120 | $("#poll-page .page-container").removeClass("visible").addClass("invisible"); 121 | $("#poll-page").spin(); 122 | 123 | 124 | $.getJSON(urlIssue, function(issueData){ 125 | $("#poll-page .page-container").removeClass("invisible").addClass("visible"); 126 | $("#poll-page").spin(false); 127 | 128 | var pollTitleContainer = $('#poll-title'); 129 | var issueLinkContainer = pollTitleContainer.parent(); 130 | var pollDescriptionContainer = $("#poll-description"); 131 | 132 | pollTitleContainer.html(issueData.title); 133 | issueLinkContainer.attr("href", issueData.html_url); 134 | if (issueData.body) { 135 | var html = makeHtml(issueData.body); 136 | pollDescriptionContainer.html(html); 137 | } 138 | }).error(function() { 139 | $("#results").removeClass("visible").addClass("invisible"); 140 | $("#vote-btns").removeClass("visible").addClass("invisible"); 141 | $("p.error").removeClass("invisible").addClass("visible"); 142 | }); 143 | 144 | $.getJSON(urlComments, function(issueCommentsData){ 145 | var issueCommentsContainer = $('#issue-comments'); 146 | 147 | var yesArray = []; 148 | var noArray = []; 149 | 150 | var i = 0; 151 | for (; i < issueCommentsData.length; i++) { 152 | var commentText = issueCommentsData[i].body; 153 | if (commentText.indexOf("+1") > -1) { 154 | yesArray.push(issueCommentsData[i].user.login); 155 | } else if (commentText.indexOf("-1") > -1) { 156 | noArray.push(issueCommentsData[i].user.login); 157 | } 158 | } 159 | 160 | if ((yesArray.length == 0) && (noArray.length == 0)) { 161 | $("#no-results").removeClass("invisible").addClass("visible"); 162 | $("#results, #chart-panel").removeClass("visible").addClass("invisible"); 163 | $("#vote-btns").removeClass("invisible").addClass("visible"); 164 | } else { 165 | updatePollResultsView(yesArray, noArray); 166 | } 167 | 168 | var yesCommentBody = {"body": "+1"}; 169 | var noCommentBody = {"body": "-1"}; 170 | 171 | $('#vote-btns').on('click', 'button', function() { 172 | if ($(this).attr('id') == 'yes-btn') { 173 | $.post(urlComments, JSON.stringify(yesCommentBody)); 174 | 175 | yesArray.push(gp.user.username); 176 | updatePollResultsView(yesArray, noArray); 177 | } else if ($(this).attr('id') == 'no-btn') { 178 | $.post(urlComments, JSON.stringify(noCommentBody)); 179 | 180 | noArray.push(gp.user.username); 181 | updatePollResultsView(yesArray, noArray); 182 | } 183 | }); 184 | 185 | }).error(function() { 186 | $("#results").removeClass("visible").addClass("invisible"); 187 | $("#vote-btns").removeClass("visible").addClass("invisible"); 188 | $("p.error").removeClass("invisible").addClass("visible"); 189 | }); 190 | } 191 | 192 | // COMMON FUNCTIONS 193 | 194 | function getLocalStorageData(key) { 195 | var key = localStorage.getItem(key); 196 | if (key == undefined) { 197 | return undefined; 198 | } else { 199 | return JSON.parse(key); 200 | } 201 | } 202 | 203 | function showPage(pagename, functionDeclaration) { 204 | $(".page.visible").removeClass('visible').addClass('invisible'); 205 | 206 | $("#" + pagename).removeClass('invisible').addClass('visible'); 207 | 208 | if (functionDeclaration) { 209 | functionDeclaration(); 210 | } 211 | } 212 | 213 | function updatePollResultsView(yesArray, noArray) { 214 | var yesContainer = $('#yes span'); 215 | var noContainer = $('#no span'); 216 | 217 | yesArray = _.uniq(yesArray); 218 | noArray = _.uniq(noArray); 219 | 220 | yesContainer.text(yesArray.length); 221 | noContainer.text(noArray.length); 222 | 223 | var data = [{ key : "Poll Results", 224 | values : [{"label": "yes", "value": yesArray.length}, 225 | {"label": "no", "value": noArray.length}] 226 | }]; 227 | 228 | // pie(data, "#pie-chart"); 229 | c3.generate({ 230 | bindto: "#chart-panel", 231 | data: { 232 | // iris data from R 233 | columns: [ 234 | ['yes', yesArray.length], 235 | ['no', noArray.length], 236 | ], 237 | type : 'pie' 238 | } 239 | }); 240 | $("#results, #chart-panel").removeClass("invisible").addClass("visible"); 241 | } 242 | 243 | function defineUrl(relativePath, accessToken) { 244 | var path = "https://api.github.com" + relativePath; 245 | if (accessToken) { 246 | path += "?access_token=" + gp.user.accessToken; 247 | } 248 | return path; 249 | } 250 | 251 | // ROUTER 252 | 253 | page('/:user/:repoName/issues/:number', function(ctx){ 254 | if (checkIfUserLoggedIn() == 'guest') { 255 | $("#login").removeClass("invisible").addClass("inline-visible"); 256 | 257 | var urlIssue = defineUrl("/repos/" + ctx.params.user + "/" + ctx.params.repoName + "/issues/" + ctx.params.number); 258 | var urlComments = defineUrl("/repos/" + ctx.params.user + "/" + ctx.params.repoName + "/issues/" + ctx.params.number + "/comments"); 259 | 260 | $("#vote-btns").removeClass("visible").addClass("invisible"); 261 | } else { 262 | $("#login").removeClass("visible").addClass("invisible"); 263 | $("#welcome, header img, #username, #logout").removeClass("invisible").addClass("inline-visible"); 264 | $("header img").attr("src", gp.user.avatar); 265 | $("#username").attr("href", gp.user.profileUrl).text(gp.user.username); 266 | 267 | var urlIssue = defineUrl("/repos/" + ctx.params.user + "/" + ctx.params.repoName + "/issues/" + ctx.params.number, gp.user.accessToken); 268 | var urlComments = defineUrl("/repos/" + ctx.params.user + "/" + ctx.params.repoName + "/issues/" + ctx.params.number + "/comments", gp.user.accessToken); 269 | 270 | $("#vote-btns").removeClass("invisible").addClass("visible"); 271 | } 272 | 273 | showPage("poll-page", function(){ 274 | buildPollPage(urlIssue, urlComments); 275 | }); 276 | }); 277 | 278 | page('', function(){ 279 | if (checkIfUserLoggedIn() == 'guest') { 280 | $("#login").removeClass("invisible").addClass("inline-visible"); 281 | showPage("app-intro-page"); 282 | } else { 283 | $("#login").removeClass("visible").addClass("invisible"); 284 | $("#welcome, header img, #username, #logout").removeClass("invisible").addClass("inline-visible"); 285 | $("header img").attr("src", gp.user.avatar); 286 | $("#username").attr("href", gp.user.profileUrl).text(gp.user.username); 287 | 288 | showPage("create-poll-page", showUserRepos); 289 | } 290 | }); 291 | 292 | page.start({ click: false }); 293 | 294 | }); --------------------------------------------------------------------------------