├── .gitignore ├── public ├── assets │ ├── 404.gif │ ├── help.gif │ ├── love.gif │ └── loader.gif ├── css │ ├── auth-icons.png │ ├── font-awesome-4.5.0 │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ │ ├── less │ │ │ ├── fixed-width.less │ │ │ ├── larger.less │ │ │ ├── list.less │ │ │ ├── core.less │ │ │ ├── font-awesome.less │ │ │ ├── stacked.less │ │ │ ├── bordered-pulled.less │ │ │ ├── rotated-flipped.less │ │ │ ├── path.less │ │ │ ├── animated.less │ │ │ └── mixins.less │ │ ├── scss │ │ │ ├── _fixed-width.scss │ │ │ ├── _larger.scss │ │ │ ├── _list.scss │ │ │ ├── font-awesome.scss │ │ │ ├── _core.scss │ │ │ ├── _stacked.scss │ │ │ ├── _bordered-pulled.scss │ │ │ ├── _rotated-flipped.scss │ │ │ ├── _path.scss │ │ │ ├── _animated.scss │ │ │ └── _mixins.scss │ │ └── HELP-US-OUT.txt │ ├── login.css │ ├── dashboard.css │ ├── main.css │ ├── roboto-slab-stylesheet.css │ └── auth-buttons.css ├── js │ ├── models │ │ ├── gitcard.js │ │ ├── nav-stats.js │ │ └── input.js │ ├── config.js │ ├── app │ │ └── main.js │ ├── views │ │ ├── my-profile-view.js │ │ ├── nav-stats.js │ │ ├── profile-view.mustache │ │ └── profile-view.js │ ├── lib │ │ ├── countUp.min.js │ │ ├── require.js │ │ ├── underscore-min.js │ │ ├── text.js │ │ ├── backbone-min.js │ │ └── mustache.js │ ├── collection-views │ │ └── profile-collection-view.js │ └── collections │ │ └── profile-collection.js ├── templates │ ├── empty-search.mustache │ ├── small-card.mustache │ ├── base-card.mustache │ └── large-card.mustache ├── sass │ ├── login.scss │ └── main.scss ├── pages │ └── login.html └── old-css │ └── css │ └── stylesheet.css ├── server ├── config │ ├── database.js │ └── passport.js ├── models │ └── user.js ├── views │ ├── card.mustache │ ├── login.mustache │ └── app.mustache ├── index.js └── routes.js ├── LICENSE ├── package.json ├── README.md └── gulpfile.js /.gitignore: -------------------------------------------------------------------------------- 1 | public/css/*.css* 2 | node_modules 3 | *.DS_Store 4 | *.swp 5 | -------------------------------------------------------------------------------- /public/assets/404.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shikkic/git-dash/HEAD/public/assets/404.gif -------------------------------------------------------------------------------- /public/assets/help.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shikkic/git-dash/HEAD/public/assets/help.gif -------------------------------------------------------------------------------- /public/assets/love.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shikkic/git-dash/HEAD/public/assets/love.gif -------------------------------------------------------------------------------- /public/assets/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shikkic/git-dash/HEAD/public/assets/loader.gif -------------------------------------------------------------------------------- /public/css/auth-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shikkic/git-dash/HEAD/public/css/auth-icons.png -------------------------------------------------------------------------------- /server/config/database.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | 'url' : 'mongodb://127.0.0.1:27017/data/db' 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shikkic/git-dash/HEAD/public/css/font-awesome-4.5.0/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /public/js/models/gitcard.js: -------------------------------------------------------------------------------- 1 | define(['backbone'], function(Backbone){ 2 | 3 | var GitCard = Backbone.Model.extend({}); 4 | 5 | return GitCard; 6 | }); 7 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shikkic/git-dash/HEAD/public/css/font-awesome-4.5.0/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shikkic/git-dash/HEAD/public/css/font-awesome-4.5.0/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shikkic/git-dash/HEAD/public/css/font-awesome-4.5.0/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Shikkic/git-dash/HEAD/public/css/font-awesome-4.5.0/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /public/templates/empty-search.mustache: -------------------------------------------------------------------------------- 1 |

Could not find "{{searchValue}}" in your followers list!

2 |

Press enter to search github

3 | -------------------------------------------------------------------------------- /public/js/models/nav-stats.js: -------------------------------------------------------------------------------- 1 | define(['backbone'], function(Backbone) { 2 | 3 | var NavStats = Backbone.Model.extend({ 4 | url: '/userNavBarStats' 5 | }); 6 | 7 | return NavStats; 8 | }); 9 | -------------------------------------------------------------------------------- /public/js/models/input.js: -------------------------------------------------------------------------------- 1 | define(['backbone'], function(Backbone) { 2 | 3 | var InputModel = Backbone.Model.extend({ 4 | initialize: function() { 5 | console.log(this); 6 | } 7 | }); 8 | 9 | return InputModel; 10 | }); 11 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | var userSchema = mongoose.Schema({ 4 | github : { 5 | id : String, 6 | token : String, 7 | name: String, 8 | username : String, 9 | url : String, 10 | } 11 | }); 12 | 13 | module.exports = mongoose.model('User', userSchema); 14 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/HELP-US-OUT.txt: -------------------------------------------------------------------------------- 1 | I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project, 2 | Fonticons (https://fonticons.com). It makes it easy to put the perfect icons on your website. Choose from our awesome, 3 | comprehensive icon sets or copy and paste your own. 4 | 5 | Please. Check it out. 6 | 7 | -Dave Gandy 8 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /public/templates/small-card.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

{{user}}

5 |

{{date}}

6 | 7 | 8 |

{{#watch}}{{watch}}{{/watch}}

9 |
10 | 11 |

{{repoName}}

12 |
13 | 14 |

{{commitSha}} {{commitMsg}}

15 |
-------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /public/css/login.css: -------------------------------------------------------------------------------- 1 | body { 2 | position: absolute; 3 | height: 100%; 4 | width: 100%; 5 | font-family: 'Roboto Slab'; 6 | font-weight: 300; 7 | margin-top: 0px; 8 | margin-left: 0; } 9 | body #title { 10 | font-size: 35px; 11 | margin-bottom: 0px; 12 | margin-top: 20px; 13 | text-align: center; } 14 | body #title h1 { 15 | font-weight: 300; 16 | letter-spacing: 7px; 17 | font-family: 'Roboto Slab'; 18 | margin-bottom: 0px; 19 | font-size: 83px; } 20 | body #title h3 { 21 | font-size: 18px; 22 | font-weight: 200; 23 | color: #26C6DA; 24 | margin-bottom: 0px; 25 | margin-top: 20px; 26 | letter-spacing: 1px; } 27 | body #title #login-button { 28 | margin-top: 50px; } 29 | -------------------------------------------------------------------------------- /public/templates/base-card.mustache: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 |

    {{user}}

    5 |
    6 |

    {{date}}

    7 | 8 | 9 |

    {{#watch}}{{watch}}{{/watch}}

    10 |
    11 | 12 |

    {{repoName}}

    13 |
    14 | 15 |

    {{commitSha}} {{commitMsg}}

    16 |
    17 |
  • -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /server/views/card.mustache: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 |

    {{user}} 5 |

    6 |

    {{date}}

    7 | 8 | 9 |

    {{#watch}}{{watch}}{{/watch}}

    10 |
    11 | 12 |

    {{repoName}}

    {{commitSha}} {{commitMsg}}

    13 | 14 |
  • 15 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/sass/login.scss: -------------------------------------------------------------------------------- 1 | body { 2 | position: absolute; 3 | height: 100%; 4 | width: 100%; 5 | font-family: 'Roboto Slab'; 6 | font-weight: 300; 7 | margin-top: 0px; 8 | margin-left: 0; 9 | 10 | #title { 11 | font-size: 35px; 12 | margin-bottom: 0px; 13 | margin-top: 20px; 14 | text-align: center; 15 | 16 | h1 { 17 | font-weight: 300; 18 | letter-spacing: 7px; 19 | font-family: 'Roboto Slab'; 20 | margin-bottom: 0px; 21 | font-size: 83px; 22 | } 23 | 24 | h3 { 25 | font-size: 18px; 26 | font-weight: 200; 27 | color: #26C6DA; 28 | margin-bottom: 0px; 29 | margin-top: 20px; 30 | letter-spacing: 1px; 31 | } 32 | 33 | #login-button { 34 | margin-top: 50px; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/pages/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Github - Dashboard 5 | 6 | 7 | 8 | 9 | 10 | 11 |
    12 |
    13 |

    Git Dashboard

    14 |

    See what your friends are up to on Github in one easy place

    15 | 20 |
    21 |
    22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /public/js/config.js: -------------------------------------------------------------------------------- 1 | requirejs.config({ 2 | 3 | baseUrl: "js/lib", 4 | 5 | paths: { 6 | "app": "../app", 7 | "text": "text", 8 | "jquery": "jquery-2.1.4.min", 9 | "materialize": "materialize.min", 10 | "moments": "moment", 11 | "mustache": "mustache", 12 | "underscore": "underscore-min", 13 | "backbone": "backbone-min", 14 | "stickit": "backbone.stickit", 15 | "countup": "countUp.min" 16 | }, 17 | 18 | shim: { 19 | 'underscore': { 20 | exports: '_' 21 | }, 22 | 'backbone': { 23 | //These script dependencies should be loaded before loading 24 | //backbone.js 25 | deps: ['underscore', 'jquery'], 26 | //Once loaded, use the global 'Backbone' as the 27 | //module value. 28 | exports: 'Backbone' 29 | } 30 | } 31 | 32 | }); 33 | 34 | // Load the main app module to start the app 35 | requirejs(["app/main"]); 36 | -------------------------------------------------------------------------------- /public/js/app/main.js: -------------------------------------------------------------------------------- 1 | define([ 2 | "jquery", 3 | "moments", 4 | "mustache", 5 | "underscore", 6 | "backbone", 7 | "../../../js/collections/profile-collection", 8 | "../../../js/collection-views/profile-collection-view", 9 | "../../../js/views/nav-stats", 10 | "../../../js/views/my-profile-view" 11 | ], function($, Moments, Mustache, _, Backbone, ProfileCollection, ProfileCollectionView, NavStatsView, MyProfileView) { 12 | 13 | var PageView = Backbone.View.extend({ 14 | 15 | initialize: function(options) { 16 | console.log("Creating Collection View"); 17 | this.navStatsView = new NavStatsView({}); 18 | this.myProfile = new MyProfileView({}); 19 | this.profileCollectionView = new ProfileCollectionView({}); 20 | } 21 | 22 | }); 23 | 24 | // Instantiate our Page View 25 | var pageView = new PageView({el: $('body')}); 26 | }); 27 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | .fa-icon-rotate(@degrees, @rotation) { 15 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation); 16 | -webkit-transform: rotate(@degrees); 17 | -ms-transform: rotate(@degrees); 18 | transform: rotate(@degrees); 19 | } 20 | 21 | .fa-icon-flip(@horiz, @vert, @rotation) { 22 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=@rotation, mirror=1); 23 | -webkit-transform: scale(@horiz, @vert); 24 | -ms-transform: scale(@horiz, @vert); 25 | transform: scale(@horiz, @vert); 26 | } 27 | -------------------------------------------------------------------------------- /public/css/font-awesome-4.5.0/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Dan Cadden 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /public/js/views/my-profile-view.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'backbone', 3 | 'mustache', 4 | ], function(Backbone, Mustache) { 5 | 6 | var MyProfileView = Backbone.View.extend({ 7 | 8 | el: $('body'), 9 | 10 | events : { 11 | 'click .fa-bars': 'toggleProfile' 12 | }, 13 | 14 | toggleProfile: function() { 15 | if ($('#my-profile').hasClass('show-my-profile')) { 16 | this.hideProfile(); 17 | } else { 18 | this.showProfile(); 19 | } 20 | }, 21 | 22 | showProfile: function() { 23 | $('#my-profile').removeClass("hide-my-profile"); 24 | $('#container').removeClass('hide-profile'); 25 | $('#container').addClass('show-profile'); 26 | $('#my-profile').addClass("show-my-profile"); 27 | }, 28 | 29 | hideProfile: function() { 30 | $('#my-profile').removeClass("show-my-profile"); 31 | $('#container').removeClass('show-profile'); 32 | $('#my-profile').addClass('hide-my-profile'); 33 | $('#container').addClass('hide-profile'); 34 | } 35 | 36 | }); 37 | 38 | return MyProfileView; 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /public/templates/large-card.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 |

    4 | {{user}} 5 |

    6 | 24 |

    {{currentStreak}}

    25 | 26 | 27 |

    {{#watch}}{{watch}}{{/watch}} 29 |

    30 |
    31 | 32 |

    {{repoName}}

    33 |
    34 | 35 |

    {{commitSha}} {{commitMsg}}

    36 |
    -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-dash", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/Shikkic/git-dash" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/Shikkic/git-dash/issues" 17 | }, 18 | "homepage": "https://github.com/Shikkic/git-dash", 19 | "dependencies": { 20 | "async": "^1.2.0", 21 | "body-parser": "^1.13.2", 22 | "bower": "^1.5.2", 23 | "cookie-parser": "^1.3.5", 24 | "express": "^4.12.4", 25 | "express-session": "^1.11.3", 26 | "gh-scrape": "^1.0.3", 27 | "gulp": "^3.9.0", 28 | "gulp-sass": "^2.1.1", 29 | "gulp-util": "^3.0.7", 30 | "moment": "^2.10.6", 31 | "mongoose": "^4.0.7", 32 | "morgan": "^1.5.3", 33 | "mustache": "^2.1.3", 34 | "mustache-express": "^1.2.1", 35 | "passport": "^0.2.2", 36 | "passport-github": "^0.1.5", 37 | "passport-github2": "^0.1.9", 38 | "path": "^0.12.7", 39 | "request": "^2.57.0", 40 | "underscore": "^1.8.3" 41 | }, 42 | "devDependencies": { 43 | "gulp": "^3.9.0", 44 | "gulp-jshint": "^1.11.2", 45 | "jshint-stylish": "^2.0.1" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server/views/login.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Github - Dashboard 5 | 6 | 7 | 8 | 9 | 10 | 11 | Fork me on GitHub 12 |
    13 |
    14 |

    Git Dashboard

    15 |

    See what your friends are up to on Github in one easy place

    16 | 21 |
    22 |
    23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Git-dash 2 | 3 | Git dash is a dashboard interface for staying current with what your freinds on Github are working on! 4 | The goal was to make it easier to stay relevant with peoples interests and projects to help make reaching out easier. 5 | 6 | ![gitdash](http://i.imgur.com/7EM9Yr6h.gif) 7 | 8 | ## TODOS 9 | 10 | - FE should be completely refactor to use AMD 11 | - Implement Collections and actuallly use models/views correctly 12 | - Implement a popup/modal-offclick overlay 13 | - Design and Implemeant large profile cards 14 | - Lookinto left hand column user profile 15 | 16 | ## Dev Installation 17 | 18 | **Note**: Github uses the [Github API](https://developer.github.com/v3/), a [developer account and app application](https://developer.github.com/program/) are required 19 | 20 | [Fork this repo](http://www.github.com/git-dash/fork), clone it and then run: 21 | 22 | ``` 23 | npm install 24 | ``` 25 | 26 | ... which installs the dependencies, and you should be good to go! 27 | 28 | ``` 29 | gulp 30 | ``` 31 | 32 | ### Warnings 33 | 34 | - You will need a `Github Client ID` and a `Github Client Secret` stored as enviroment variables. 35 | 36 | Ex: bash_profile 37 | ```bash 38 | export GITHUB_CLIENT_ID='[oiwhpiuqhniqo2y8;ioq2u8oqdhoihakjsdkjasgiuawh' 39 | export GITHUB_CLIENT_SECRET=';epoajslknzlaspidjhqwlkdnwlakdnasndlkasbdl' 40 | ``` 41 | 42 | I hope you like it! 43 | 44 | ![Thank you](http://i.giphy.com/FG5wEpsAiqqCk.gif) 45 | 46 | 47 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var gulp = require('gulp'), 4 | exec = require('child_process').exec, 5 | jshint = require('gulp-jshint'), 6 | gutil = require('gulp-util'), 7 | sass = require('gulp-sass'); 8 | 9 | gulp.task('default', ['run', 'watch']); 10 | 11 | gulp.task('run', function(cb) { 12 | exec('mongod', function (err, stdout, stderr) { 13 | cb(err); 14 | }); 15 | var nodeChild = exec('node ./server/index.js'); 16 | nodeChild.stdout.on('data', function(data) { 17 | console.log('node: ' + data); 18 | }); 19 | nodeChild.stderr.on('data', function(data) { 20 | console.log('node: ' + data); 21 | }); 22 | nodeChild.on('close', function(code) { 23 | console.log('closing code: ' + code); 24 | }); 25 | }); 26 | 27 | gulp.task('ngrok', function(cb) { 28 | exec('ngrok http 3000', function (err, stdout, stderr) { 29 | cb(err); 30 | }); 31 | }); 32 | 33 | gulp.task('jshint', function() { 34 | return gulp.src('./public/js/app.js') 35 | .pipe(jshint()) 36 | .pipe(jshint.reporter('jshint-stylish', {verbose: true})); 37 | }); 38 | 39 | gulp.task('sass:build', function() { 40 | gulp.src('./public/sass/*.scss') 41 | .pipe(sass().on('error', sass.logError)) 42 | .pipe(gulp.dest('./public/css')); 43 | }); 44 | 45 | gulp.task('watch', function() { 46 | gulp.watch('./public/js/app.js', ['jshint']); 47 | gulp.watch('./public/sass/*.scss', ['sass:build']); 48 | }); 49 | -------------------------------------------------------------------------------- /public/js/views/nav-stats.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'backbone', 3 | 'mustache', 4 | 'countup', 5 | '../models/nav-stats' 6 | ], function(Backbone, Mustache, CountUp, NavStatsModel) { 7 | 8 | var NavStatsView = Backbone.View.extend({ 9 | 10 | initialize: function() { 11 | this.model = new NavStatsModel({}); 12 | 13 | this.listenTo(this.model, 'change', this.render); 14 | 15 | this.model.fetch(); 16 | }, 17 | 18 | render: function() { 19 | console.log("MADE IT TO RENDER", this.model); 20 | this.renderTotalContributrions(); 21 | this.renderLongestStreak(); 22 | this.renderCurrentStreak(); 23 | }, 24 | 25 | renderTotalContributrions: function() { 26 | this.slowCount("userFollowerCount", this.model.get('totalContributions')); 27 | }, 28 | 29 | renderLongestStreak: function() { 30 | this.slowCount("userFollowingCount", this.model.get('longestStreak')); 31 | }, 32 | 33 | renderCurrentStreak: function() { 34 | this.slowCount("publicRepoCount", this.model.get('currentStreak')); 35 | }, 36 | 37 | slowCount: function(elementName, value) { 38 | var options = { 39 |   useEasing : true, 40 |   useGrouping : true, 41 |   separator : ',', 42 |   decimal : '.', 43 |   prefix : '', 44 |   suffix : '' 45 | }; 46 | var demo = new CountUp(elementName, 0, value, 0, 2.5, options); 47 | demo.start(); 48 | } 49 | }); 50 | 51 | return NavStatsView; 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | /////////////////////////////////// 3 | // Import Libraries /// 4 | /////////////////////////////////// 5 | */ 6 | var path = require('path'), 7 | express = require('express'), 8 | app = express(), 9 | request = require('request'), 10 | async = require('async'), 11 | passport = require('passport'), 12 | mongoose = require('mongoose'), 13 | morgan = require('morgan'), 14 | cookieParser = require('cookie-parser'), 15 | bodyParser = require('body-parser'), 16 | session = require('express-session'), 17 | mustacheExpress = require('mustache-express'), 18 | gh = require('gh-scrape'); 19 | 20 | /* 21 | ///////////////////////////////////// 22 | // Set up DB // 23 | ///////////////////////////////////// 24 | */ 25 | var configDB = require('./config/database.js'); 26 | mongoose.connect(configDB.url); // connect to our database 27 | require('./config/passport.js')(passport); 28 | 29 | /* 30 | ///////////////////////////////////// 31 | // MIDDLEWARE // 32 | ///////////////////////////////////// 33 | */ 34 | // Setting up Mustache Rendering 35 | app.engine('mustache', mustacheExpress()); 36 | app.set('view engine', 'mustache'); 37 | app.set('views', path.join(__dirname, 'views')); 38 | 39 | app.set('port', (process.env.PORT || 3000)); 40 | app.use(morgan('dev')); 41 | app.use(express.static('./public')); 42 | app.use(cookieParser()); 43 | app.use(bodyParser.json());; 44 | app.use(session({ 45 | secret: 'iloveketchup', 46 | resave: false, 47 | saveUninitialized: true 48 | })); 49 | app.use(passport.initialize()); 50 | app.use(passport.session()); 51 | 52 | /* 53 | ////////////////////////////////////// 54 | // ROUTES // 55 | ////////////////////////////////////// 56 | */ 57 | require('./routes.js')(app, request, async, passport); 58 | 59 | app.listen(app.get('port'), function() { 60 | console.log("listening on port: " + app.get('port')); 61 | }); 62 | -------------------------------------------------------------------------------- /public/css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Base structure 3 | */ 4 | 5 | /* Move down content because we have a fixed navbar that is 50px tall */ 6 | body { 7 | padding-top: 50px; 8 | } 9 | 10 | 11 | /* 12 | * Global add-ons 13 | */ 14 | 15 | .sub-header { 16 | padding-bottom: 10px; 17 | border-bottom: 1px solid #eee; 18 | } 19 | 20 | /* 21 | * Top navigation 22 | * Hide default border to remove 1px line. 23 | */ 24 | .navbar-fixed-top { 25 | border: 0; 26 | } 27 | 28 | /* 29 | * Sidebar 30 | */ 31 | 32 | /* Hide for mobile, show later */ 33 | .sidebar { 34 | display: none; 35 | } 36 | @media (min-width: 768px) { 37 | .sidebar { 38 | position: fixed; 39 | top: 51px; 40 | bottom: 0; 41 | left: 0; 42 | z-index: 1000; 43 | display: block; 44 | padding: 20px; 45 | overflow-x: hidden; 46 | overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ 47 | background-color: #f5f5f5; 48 | border-right: 1px solid #eee; 49 | } 50 | } 51 | 52 | /* Sidebar navigation */ 53 | .nav-sidebar { 54 | margin-right: -21px; /* 20px padding + 1px border */ 55 | margin-bottom: 20px; 56 | margin-left: -20px; 57 | } 58 | .nav-sidebar > li > a { 59 | padding-right: 20px; 60 | padding-left: 20px; 61 | } 62 | .nav-sidebar > .active > a, 63 | .nav-sidebar > .active > a:hover, 64 | .nav-sidebar > .active > a:focus { 65 | color: #fff; 66 | background-color: #428bca; 67 | } 68 | 69 | 70 | /* 71 | * Main content 72 | */ 73 | 74 | .main { 75 | padding: 20px; 76 | } 77 | @media (min-width: 768px) { 78 | .main { 79 | padding-right: 40px; 80 | padding-left: 40px; 81 | } 82 | } 83 | .main .page-header { 84 | margin-top: 0; 85 | } 86 | 87 | 88 | /* 89 | * Placeholder dashboard ideas 90 | */ 91 | 92 | .placeholders { 93 | margin-bottom: 30px; 94 | text-align: center; 95 | } 96 | .placeholders h4 { 97 | margin-bottom: 0; 98 | } 99 | .placeholder { 100 | margin-bottom: 20px; 101 | } 102 | .placeholder img { 103 | display: inline-block; 104 | border-radius: 50%; 105 | } -------------------------------------------------------------------------------- /public/js/views/profile-view.mustache: -------------------------------------------------------------------------------- 1 | define([ 2 | 3 | ], function() { 4 | 5 | var ProfileView = Backbone.View.extend({ 6 | el: $('#container'), 7 | 8 | events: { 9 | 'dblclick' : 'transform', 10 | 'click .fa-times' : 'normal' 11 | }, 12 | 13 | tagName: 'li', 14 | 15 | baseCard: "", 16 | 17 | smallCardTemplate: "", 18 | 19 | largeCardTemplate: " 20 | 21 | initialize: function(){ 22 | this.render(); 23 | this.setElement('#'+this.model.get('userID')); 24 | }, 25 | 26 | render: function() { 27 | this.$el.append((Mustache.render(this.baseCard, this.model.toJSON()))); 28 | return this; 29 | }, 30 | 31 | smallCardRender: function() { 32 | this.$el.html((Mustache.render(this.smallCardTemplate, this.model.toJSON()))); 33 | }, 34 | 35 | largeCardRender: function() { 36 | this.$el.html((Mustache.render(this.largeCardTemplate, this.model.toJSON()))); 37 | }, 38 | 39 | hide: function() { 40 | this.$el.hide(); 41 | }, 42 | 43 | show: function() { 44 | this.$el.show(); 45 | }, 46 | 47 | transform: function() { 48 | this.$el.empty(); 49 | this.largeCardRender(); 50 | this.$el.css("position", "fixed"); 51 | this.$el.css("top", "70px"); 52 | this.$el.height("600px"); 53 | this.$el.width("600px"); 54 | /*this.$el.animate({ 55 | height: '500px', 56 | width: '600px' 57 | }, 500);*/ 58 | this.$el.css("left", "50%"); 59 | this.$el.css("margin-left", "-300px"); 60 | this.$el.addClass("z-depth-5"); 61 | this.$el.removeClass("hoverable"); 62 | this.$el.css("z-index", "2"); 63 | }, 64 | 65 | normal: function() { 66 | //$('#'+this.model.get('userID')).hide(); 67 | this.smallCardRender(); 68 | this.hide(); 69 | console.log(this.$el); 70 | $('#'+this.model.get('userID')).height('210px'); 71 | $('#'+this.model.get('userID')).width('410px'); 72 | this.$el.css({ 73 | "left": "0", 74 | "margin-left": "15px", 75 | }); 76 | 77 | this.$el.css("display", "inline-block"); 78 | this.$el.removeClass("z-depth-5"); 79 | this.$el.css("position", "static"); 80 | this.$el.addClass("hoverable"); 81 | this.show(); 82 | } 83 | }); 84 | 85 | return 86 | -------------------------------------------------------------------------------- /server/config/passport.js: -------------------------------------------------------------------------------- 1 | // config/passport.js 2 | 3 | // load all the things we need 4 | var GithubStrategy = require('passport-github2').Strategy; 5 | // load up the user model 6 | var User = require('../models/user'); 7 | 8 | var GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID; 9 | var GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET; 10 | 11 | // expose this function to our app using module.exports 12 | module.exports = function(passport) { 13 | 14 | // ========================================================================= 15 | // passport session setup ================================================== 16 | // ========================================================================= 17 | // required for persistent login sessions 18 | // passport needs ability to serialize and unserialize users out of session 19 | 20 | // used to serialize the user for the session 21 | passport.serializeUser(function(user, done) { 22 | done(null, user.id); 23 | }); 24 | 25 | // used to deserialize the user 26 | passport.deserializeUser(function(id, done) { 27 | User.findById(id, function(err, user) { 28 | done(err, user); 29 | }); 30 | }); 31 | 32 | passport.use(new GithubStrategy({ 33 | clientID: GITHUB_CLIENT_ID, 34 | clientSecret: GITHUB_CLIENT_SECRET, 35 | callbackURL: "http://127.0.0.1:3000/auth/github/callback" 36 | }, 37 | function(token, refreshToken, profile, done) { 38 | User.findOne({ githubId: profile.id }, function (err, user) { 39 | if (err) { 40 | return done(err); 41 | } 42 | // if the user is found, then log them in 43 | if (user) { 44 | return done(null, user); // user found, return that user 45 | } else { 46 | // if there is no user found with that facebook id, create them 47 | var newUser = new User(); 48 | 49 | // set all of the facebook information in our user model 50 | newUser.github.id = profile.id; // set the users facebook id 51 | newUser.github.token = token; // we will save the token that facebook provides to the user 52 | newUser.github.name = profile.displayName; 53 | newUser.github.username = profile.username; 54 | newUser.github.url = profile.profileUrl; 55 | // save our user to the database 56 | newUser.save(function(err) { 57 | if (err) 58 | throw err; 59 | 60 | // if successful, return the new user 61 | return done(null, newUser); 62 | }); 63 | } 64 | 65 | 66 | }); 67 | } 68 | )); 69 | 70 | }; 71 | -------------------------------------------------------------------------------- /public/js/views/profile-view.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'backbone', 3 | 'mustache', 4 | "text!../../../templates/base-card.mustache", 5 | "text!../../../templates/small-card.mustache", 6 | "text!../../../templates/large-card.mustache" 7 | ], function(Backbone, Mustache, BaseCardTemplate, SmallCardTemplate, LargeCardTemplate) { 8 | 9 | var ProfileView = Backbone.View.extend({ 10 | 11 | el: $('#container'), 12 | 13 | events: { 14 | 'dblclick' : 'transform', 15 | 'click .fa-times' : 'normal' 16 | }, 17 | 18 | tagName: 'li', 19 | 20 | baseCard: BaseCardTemplate, 21 | 22 | smallCardTemplate: SmallCardTemplate, 23 | 24 | largeCardTemplate: LargeCardTemplate, 25 | 26 | initialize: function(){ 27 | this.render(); 28 | this.setElement('#'+this.model.get('userID')); 29 | }, 30 | 31 | render: function() { 32 | this.$el.append((Mustache.render(this.baseCard, this.model.toJSON()))); 33 | return this; 34 | }, 35 | 36 | smallCardRender: function() { 37 | this.$el.html((Mustache.render(this.smallCardTemplate, this.model.toJSON()))); 38 | }, 39 | 40 | largeCardRender: function() { 41 | this.$el.html((Mustache.render(this.largeCardTemplate, this.model.toJSON()))); 42 | }, 43 | 44 | hide: function() { 45 | this.$el.hide(); 46 | }, 47 | 48 | show: function() { 49 | this.$el.show(); 50 | }, 51 | 52 | transform: function() { 53 | this.$el.empty(); 54 | this.largeCardRender(); 55 | this.$el.css("position", "fixed"); 56 | this.$el.css("top", "70px"); 57 | this.$el.height("600px"); 58 | this.$el.width("600px"); 59 | /*this.$el.animate({ 60 | height: '500px', 61 | width: '600px' 62 | }, 500);*/ 63 | this.$el.css("left", "50%"); 64 | this.$el.css("margin-left", "-300px"); 65 | this.$el.addClass("z-depth-5"); 66 | this.$el.removeClass("hoverable"); 67 | this.$el.css("z-index", "2"); 68 | }, 69 | 70 | normal: function() { 71 | //$('#'+this.model.get('userID')).hide(); 72 | this.smallCardRender(); 73 | this.hide(); 74 | console.log(this.$el); 75 | $('#'+this.model.get('userID')).height('210px'); 76 | $('#'+this.model.get('userID')).width('410px'); 77 | this.$el.css({ 78 | "left": "0", 79 | "margin-left": "15px", 80 | }); 81 | 82 | this.$el.css("display", "inline-block"); 83 | this.$el.removeClass("z-depth-5"); 84 | this.$el.css("position", "static"); 85 | this.$el.addClass("hoverable"); 86 | this.show(); 87 | } 88 | }); 89 | 90 | return ProfileView; 91 | 92 | }); 93 | -------------------------------------------------------------------------------- /server/views/app.mustache: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Github - Dashboard 11 | 12 | 13 | 42 |
    43 |
    44 | 55 | 58 |
    59 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /public/js/lib/countUp.min.js: -------------------------------------------------------------------------------- 1 | !function(t,a){"function"==typeof define&&define.amd?define(a):"object"==typeof exports?module.exports=a(require,exports,module):t.CountUp=a()}(this,function(t,a,e){/* 2 | 3 | countUp.js 4 | (c) 2014-2015 @inorganik 5 | Licensed under the MIT license. 6 | 7 | */ 8 | var n=function(t,a,e,n,i,r){for(var o=0,s=["webkit","moz","ms","o"],u=0;uthis.endVal,this.frameVal=this.startVal,this.decimals=Math.max(0,n||0),this.dec=Math.pow(10,this.decimals),this.duration=1e3*Number(i)||2e3;var l=this;this.version=function(){return"1.6.1"},this.printValue=function(t){var a=isNaN(t)?"--":l.formatNumber(t);"INPUT"==l.d.tagName?this.d.value=a:"text"==l.d.tagName||"tspan"==l.d.tagName?this.d.textContent=a:this.d.innerHTML=a},this.easeOutExpo=function(t,a,e,n){return e*(-Math.pow(2,-10*t/n)+1)*1024/1023+a},this.count=function(t){l.startTime||(l.startTime=t),l.timestamp=t;var a=t-l.startTime;l.remaining=l.duration-a,l.options.useEasing?l.countDown?l.frameVal=l.startVal-l.easeOutExpo(a,0,l.startVal-l.endVal,l.duration):l.frameVal=l.easeOutExpo(a,l.startVal,l.endVal-l.startVal,l.duration):l.countDown?l.frameVal=l.startVal-(l.startVal-l.endVal)*(a/l.duration):l.frameVal=l.startVal+(l.endVal-l.startVal)*(a/l.duration),l.countDown?l.frameVal=l.frameVall.endVal?l.endVal:l.frameVal,l.frameVal=Math.floor(l.frameVal*l.dec)/l.dec,l.printValue(l.frameVal),al.endVal,l.rAF=requestAnimationFrame(l.count)},this.formatNumber=function(t){t=t.toFixed(l.decimals),t+="";var a,e,n,i;if(a=t.split("."),e=a[0],n=a.length>1?l.options.decimal+a[1]:"",i=/(\d+)(\d{3})/,l.options.useGrouping)for(;i.test(e);)e=e.replace(i,"$1"+l.options.separator+"$2");return l.options.prefix+e+n+l.options.suffix},l.printValue(l.startVal)};return n}); -------------------------------------------------------------------------------- /public/js/collection-views/profile-collection-view.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'backbone', 3 | 'mustache', 4 | 'stickit', 5 | '../../../js/models/input', 6 | "../views/profile-view", 7 | "../collections/profile-collection", 8 | "text!../../../templates/empty-search.mustache" 9 | ], function(Backbone, Mustache, Stickit, InputModel, ProfileView, ProfileCollection, EmptySearchTemplate) { 10 | 11 | var ProfileCollectionView = Backbone.View.extend({ 12 | 13 | viewCollection: null, 14 | 15 | el: 'body', 16 | 17 | bindings: { 18 | '#search': 'inputVal' 19 | }, 20 | 21 | initialize: function() { 22 | _.bindAll(this, 'visibleItemsEmpty'); 23 | this.collection = new ProfileCollection; 24 | this.toggleLoader({init: true}); 25 | 26 | // TODO Fix this 27 | this.model = new InputModel({}); 28 | 29 | this.listenTo(this.model, {'change:inputVal':this.updateText}); 30 | this.listenTo(this.collection, 'update', this.toggleLoader); 31 | 32 | 33 | this.collection.fetch({ 34 | success: _.bind(function() { 35 | this.trigger("update"); 36 | }, this) 37 | }); 38 | 39 | this.inputVal = $("#search").val(); 40 | this.stickit(); 41 | }, 42 | 43 | updateText: function(e) { 44 | this.inputVal = $("#search").val(); 45 | this.filter(); 46 | }, 47 | 48 | render: function() { 49 | var viewCollection = []; 50 | // Use different _.each instead 51 | this.collection.forEach(function(item) { 52 | var view = new ProfileView({model: item}); 53 | viewCollection.push(view); 54 | }); 55 | this.viewCollection = viewCollection; 56 | }, 57 | 58 | filter: function() { 59 | var inputValue = this.inputVal.toLowerCase(); 60 | if (this.viewCollection) { 61 | for(var i = 0; i < this.viewCollection.length; i++) { 62 | var item = this.viewCollection[i]; 63 | var username = item.model.attributes.userID.toLowerCase(); 64 | if(username.indexOf(inputValue) > -1 || username.length === 0) { 65 | $("#"+username).show(); 66 | } else { 67 | $("#"+username).hide(); 68 | } 69 | } 70 | console.log(this.visibleItemsEmpty()); 71 | if (this.visibleItemsEmpty()) { 72 | // RENDER MUSTACHE TEMPLATE HERE 73 | this.renderEmptySearch(inputValue); 74 | } else { 75 | this.removeEmptySearch(); 76 | } 77 | } 78 | }, 79 | 80 | visibleItemsEmpty: function() { 81 | for(var i = 0; i< this.viewCollection.length; i++) { 82 | var username = this.viewCollection[i].model.get('userID').toLowerCase(); 83 | if ($('#'+username).is(':visible')) { 84 | return false; 85 | } 86 | } 87 | return true; 88 | }, 89 | 90 | toggleLoader: function(options) { 91 | //TODO Might want these to be properties of the collection-view, not the page view 92 | options = (options || {}); 93 | if (!this.collection.length && options.init) { 94 | // Toggle has been called, but it's the initial call 95 | $('#spinner').show(); 96 | } else if (!this.collection.length && !options.init) { 97 | // Collection has fetched, but it's empty 98 | this.toggleEmptyView(); 99 | } else { 100 | // We have a colletion to render that is NOT Empty 101 | $('#spinner').hide(); 102 | this.render(); 103 | } 104 | }, 105 | 106 | renderEmptySearch(searchValue) { 107 | $('#emptyView').html(Mustache.render(EmptySearchTemplate, {'searchValue': searchValue})).removeClass("no-border"); 108 | }, 109 | 110 | removeEmptySearch() { 111 | $('#emptyView').html('').addClass("no-border"); 112 | }, 113 | 114 | toggleEmptyView : function() { 115 | // TODO REFACTOR THIS TO RENDER AN EMPTY TEMPLATE 116 | // TODO Might want these to be properties of the collection-view, not the page view 117 | // TODO DELETE THIS IT"S CAUSING PERFORMANCE ISSUES 118 | $("#spinner").hide(); 119 | $("#img-spinner").attr("src", "../assets/help.gif"); 120 | $("#img-spinner").attr("id", "no-friends"); 121 | $("#info").append("Looks like you don't have any friends on Github, try adding some and come back! <3this.depCount&&!this.defined){if(G(l)){if(this.events.error&&this.map.isDefine||e.onError!==ca)try{f=h.execCb(c,l,b,f)}catch(d){a=d}else f=h.execCb(c,l,b,f);this.map.isDefine&& 19 | void 0===f&&((b=this.module)?f=b.exports:this.usingExports&&(f=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else f=l;this.exports=f;if(this.map.isDefine&&!this.ignore&&(q[c]=f,e.onResourceLoad))e.onResourceLoad(h,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete= 20 | !0)}}else t(h.defQueueMap,c)||this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=i(a.prefix);this.depMaps.push(d);s(d,"defined",u(this,function(f){var l,d;d=n(aa,this.map.id);var g=this.map.name,P=this.map.parentMap?this.map.parentMap.name:null,p=h.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(g=f.normalize(g,function(a){return c(a,P,!0)})||""),f=i(a.prefix+"!"+g,this.map.parentMap),s(f,"defined",u(this,function(a){this.init([],function(){return a}, 21 | null,{enabled:!0,ignore:!0})})),d=n(m,f.id)){this.depMaps.push(f);if(this.events.error)d.on("error",u(this,function(a){this.emit("error",a)}));d.enable()}}else d?(this.map.url=h.nameToUrl(d),this.load()):(l=u(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),l.error=u(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];A(m,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),l.fromText=u(this,function(f,c){var d=a.name,g=i(d),P=M;c&&(f=c);P&& 22 | (M=!1);r(g);t(k.config,b)&&(k.config[d]=k.config[b]);try{e.exec(f)}catch(m){return w(B("fromtexteval","fromText eval for "+b+" failed: "+m,m,[b]))}P&&(M=!0);this.depMaps.push(g);h.completeLoad(d);p([d],l)}),f.load(a.name,p,l,k))}));h.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){V[this.map.id]=this;this.enabling=this.enabled=!0;v(this.depMaps,u(this,function(a,b){var c,f;if("string"===typeof a){a=i(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c= 23 | n(L,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;s(a,"defined",u(this,function(a){this.undefed||(this.defineDep(b,a),this.check())}));this.errback?s(a,"error",u(this,this.errback)):this.events.error&&s(a,"error",u(this,function(a){this.emit("error",a)}))}c=a.id;f=m[c];!t(L,c)&&(f&&!f.enabled)&&h.enable(a,this)}));A(this.pluginMaps,u(this,function(a){var b=n(m,a.id);b&&!b.enabled&&h.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]= 24 | []);c.push(b)},emit:function(a,b){v(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};h={config:k,contextName:b,registry:m,defined:q,urlFetched:S,defQueue:C,defQueueMap:{},Module:Z,makeModuleMap:i,nextTick:e.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=k.shim,c={paths:!0,bundles:!0,config:!0,map:!0};A(a,function(a,b){c[b]?(k[b]||(k[b]={}),U(k[b],a,!0,!0)):k[b]=a});a.bundles&&A(a.bundles,function(a,b){v(a, 25 | function(a){a!==b&&(aa[a]=b)})});a.shim&&(A(a.shim,function(a,c){H(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=h.makeShimExports(a);b[c]=a}),k.shim=b);a.packages&&v(a.packages,function(a){var b,a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(k.paths[b]=a.location);k.pkgs[b]=a.name+"/"+(a.main||"main").replace(ha,"").replace(Q,"")});A(m,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=i(b,null,!0))});if(a.deps||a.callback)h.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b; 26 | a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,j){function g(c,d,p){var k,n;j.enableBuildCallback&&(d&&G(d))&&(d.__requireJsBuild=!0);if("string"===typeof c){if(G(d))return w(B("requireargs","Invalid require call"),p);if(a&&t(L,c))return L[c](m[a.id]);if(e.get)return e.get(h,c,a,g);k=i(c,a,!1,!0);k=k.id;return!t(q,k)?w(B("notloaded",'Module name "'+k+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):q[k]}J();h.nextTick(function(){J(); 27 | n=r(i(null,a));n.skipMap=j.skipMap;n.init(c,d,p,{enabled:!0});D()});return g}j=j||{};U(g,{isBrowser:z,toUrl:function(b){var d,e=b.lastIndexOf("."),j=b.split("/")[0];if(-1!==e&&(!("."===j||".."===j)||1g.attachEvent.toString().indexOf("[native code"))&&!Y?(M=!0,g.attachEvent("onreadystatechange",b.onScriptLoad)):(g.addEventListener("load",b.onScriptLoad,!1),g.addEventListener("error",b.onScriptError,!1));g.src=d;J=g;D?y.insertBefore(g,D):y.appendChild(g);J=null;return g}if(ea)try{importScripts(d),b.completeLoad(c)}catch(i){b.onError(B("importscripts", 35 | "importScripts failed for "+c+" at "+d,i,[c]))}};z&&!s.skipDataMain&&T(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(I=b.getAttribute("data-main"))return r=I,s.baseUrl||(E=r.split("/"),r=E.pop(),O=E.length?E.join("/")+"/":"./",s.baseUrl=O),r=r.replace(Q,""),e.jsExtRegExp.test(r)&&(r=I),s.deps=s.deps?s.deps.concat(r):[r],!0});define=function(b,c,d){var e,g;"string"!==typeof b&&(d=c,c=b,b=null);H(c)||(d=c,c=null);!c&&G(d)&&(c=[],d.length&&(d.toString().replace(ja,"").replace(ka, 36 | function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(M){if(!(e=J))N&&"interactive"===N.readyState||T(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return N=b}),e=N;e&&(b||(b=e.getAttribute("data-requiremodule")),g=F[e.getAttribute("data-requirecontext")])}g?(g.defQueue.push([b,c,d]),g.defQueueMap[b]=!0):R.push([b,c,d])};define.amd={jQuery:!0};e.exec=function(b){return eval(b)};e(s)}})(this); 37 | -------------------------------------------------------------------------------- /public/js/lib/underscore-min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.8.3 2 | // http://underscorejs.org 3 | // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 4 | // Underscore may be freely distributed under the MIT license. 5 | (function(){function n(n){function t(t,r,e,u,i,o){for(;i>=0&&o>i;i+=n){var a=u?u[i]:i;e=r(e,t[a],a,t)}return e}return function(r,e,u,i){e=b(e,i,4);var o=!k(r)&&m.keys(r),a=(o||r).length,c=n>0?0:a-1;return arguments.length<3&&(u=r[o?o[c]:c],c+=n),t(r,e,u,o,c,a)}}function t(n){return function(t,r,e){r=x(r,e);for(var u=O(t),i=n>0?0:u-1;i>=0&&u>i;i+=n)if(r(t[i],i,t))return i;return-1}}function r(n,t,r){return function(e,u,i){var o=0,a=O(e);if("number"==typeof i)n>0?o=i>=0?i:Math.max(i+a,o):a=i>=0?Math.min(i+1,a):i+a+1;else if(r&&i&&a)return i=r(e,u),e[i]===u?i:-1;if(u!==u)return i=t(l.call(e,o,a),m.isNaN),i>=0?i+o:-1;for(i=n>0?o:a-1;i>=0&&a>i;i+=n)if(e[i]===u)return i;return-1}}function e(n,t){var r=I.length,e=n.constructor,u=m.isFunction(e)&&e.prototype||a,i="constructor";for(m.has(n,i)&&!m.contains(t,i)&&t.push(i);r--;)i=I[r],i in n&&n[i]!==u[i]&&!m.contains(t,i)&&t.push(i)}var u=this,i=u._,o=Array.prototype,a=Object.prototype,c=Function.prototype,f=o.push,l=o.slice,s=a.toString,p=a.hasOwnProperty,h=Array.isArray,v=Object.keys,g=c.bind,y=Object.create,d=function(){},m=function(n){return n instanceof m?n:this instanceof m?void(this._wrapped=n):new m(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=m),exports._=m):u._=m,m.VERSION="1.8.3";var b=function(n,t,r){if(t===void 0)return n;switch(null==r?3:r){case 1:return function(r){return n.call(t,r)};case 2:return function(r,e){return n.call(t,r,e)};case 3:return function(r,e,u){return n.call(t,r,e,u)};case 4:return function(r,e,u,i){return n.call(t,r,e,u,i)}}return function(){return n.apply(t,arguments)}},x=function(n,t,r){return null==n?m.identity:m.isFunction(n)?b(n,t,r):m.isObject(n)?m.matcher(n):m.property(n)};m.iteratee=function(n,t){return x(n,t,1/0)};var _=function(n,t){return function(r){var e=arguments.length;if(2>e||null==r)return r;for(var u=1;e>u;u++)for(var i=arguments[u],o=n(i),a=o.length,c=0;a>c;c++){var f=o[c];t&&r[f]!==void 0||(r[f]=i[f])}return r}},j=function(n){if(!m.isObject(n))return{};if(y)return y(n);d.prototype=n;var t=new d;return d.prototype=null,t},w=function(n){return function(t){return null==t?void 0:t[n]}},A=Math.pow(2,53)-1,O=w("length"),k=function(n){var t=O(n);return"number"==typeof t&&t>=0&&A>=t};m.each=m.forEach=function(n,t,r){t=b(t,r);var e,u;if(k(n))for(e=0,u=n.length;u>e;e++)t(n[e],e,n);else{var i=m.keys(n);for(e=0,u=i.length;u>e;e++)t(n[i[e]],i[e],n)}return n},m.map=m.collect=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=Array(u),o=0;u>o;o++){var a=e?e[o]:o;i[o]=t(n[a],a,n)}return i},m.reduce=m.foldl=m.inject=n(1),m.reduceRight=m.foldr=n(-1),m.find=m.detect=function(n,t,r){var e;return e=k(n)?m.findIndex(n,t,r):m.findKey(n,t,r),e!==void 0&&e!==-1?n[e]:void 0},m.filter=m.select=function(n,t,r){var e=[];return t=x(t,r),m.each(n,function(n,r,u){t(n,r,u)&&e.push(n)}),e},m.reject=function(n,t,r){return m.filter(n,m.negate(x(t)),r)},m.every=m.all=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(!t(n[o],o,n))return!1}return!0},m.some=m.any=function(n,t,r){t=x(t,r);for(var e=!k(n)&&m.keys(n),u=(e||n).length,i=0;u>i;i++){var o=e?e[i]:i;if(t(n[o],o,n))return!0}return!1},m.contains=m.includes=m.include=function(n,t,r,e){return k(n)||(n=m.values(n)),("number"!=typeof r||e)&&(r=0),m.indexOf(n,t,r)>=0},m.invoke=function(n,t){var r=l.call(arguments,2),e=m.isFunction(t);return m.map(n,function(n){var u=e?t:n[t];return null==u?u:u.apply(n,r)})},m.pluck=function(n,t){return m.map(n,m.property(t))},m.where=function(n,t){return m.filter(n,m.matcher(t))},m.findWhere=function(n,t){return m.find(n,m.matcher(t))},m.max=function(n,t,r){var e,u,i=-1/0,o=-1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],e>i&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(u>o||u===-1/0&&i===-1/0)&&(i=n,o=u)});return i},m.min=function(n,t,r){var e,u,i=1/0,o=1/0;if(null==t&&null!=n){n=k(n)?n:m.values(n);for(var a=0,c=n.length;c>a;a++)e=n[a],i>e&&(i=e)}else t=x(t,r),m.each(n,function(n,r,e){u=t(n,r,e),(o>u||1/0===u&&1/0===i)&&(i=n,o=u)});return i},m.shuffle=function(n){for(var t,r=k(n)?n:m.values(n),e=r.length,u=Array(e),i=0;e>i;i++)t=m.random(0,i),t!==i&&(u[i]=u[t]),u[t]=r[i];return u},m.sample=function(n,t,r){return null==t||r?(k(n)||(n=m.values(n)),n[m.random(n.length-1)]):m.shuffle(n).slice(0,Math.max(0,t))},m.sortBy=function(n,t,r){return t=x(t,r),m.pluck(m.map(n,function(n,r,e){return{value:n,index:r,criteria:t(n,r,e)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.index-t.index}),"value")};var F=function(n){return function(t,r,e){var u={};return r=x(r,e),m.each(t,function(e,i){var o=r(e,i,t);n(u,e,o)}),u}};m.groupBy=F(function(n,t,r){m.has(n,r)?n[r].push(t):n[r]=[t]}),m.indexBy=F(function(n,t,r){n[r]=t}),m.countBy=F(function(n,t,r){m.has(n,r)?n[r]++:n[r]=1}),m.toArray=function(n){return n?m.isArray(n)?l.call(n):k(n)?m.map(n,m.identity):m.values(n):[]},m.size=function(n){return null==n?0:k(n)?n.length:m.keys(n).length},m.partition=function(n,t,r){t=x(t,r);var e=[],u=[];return m.each(n,function(n,r,i){(t(n,r,i)?e:u).push(n)}),[e,u]},m.first=m.head=m.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:m.initial(n,n.length-t)},m.initial=function(n,t,r){return l.call(n,0,Math.max(0,n.length-(null==t||r?1:t)))},m.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:m.rest(n,Math.max(0,n.length-t))},m.rest=m.tail=m.drop=function(n,t,r){return l.call(n,null==t||r?1:t)},m.compact=function(n){return m.filter(n,m.identity)};var S=function(n,t,r,e){for(var u=[],i=0,o=e||0,a=O(n);a>o;o++){var c=n[o];if(k(c)&&(m.isArray(c)||m.isArguments(c))){t||(c=S(c,t,r));var f=0,l=c.length;for(u.length+=l;l>f;)u[i++]=c[f++]}else r||(u[i++]=c)}return u};m.flatten=function(n,t){return S(n,t,!1)},m.without=function(n){return m.difference(n,l.call(arguments,1))},m.uniq=m.unique=function(n,t,r,e){m.isBoolean(t)||(e=r,r=t,t=!1),null!=r&&(r=x(r,e));for(var u=[],i=[],o=0,a=O(n);a>o;o++){var c=n[o],f=r?r(c,o,n):c;t?(o&&i===f||u.push(c),i=f):r?m.contains(i,f)||(i.push(f),u.push(c)):m.contains(u,c)||u.push(c)}return u},m.union=function(){return m.uniq(S(arguments,!0,!0))},m.intersection=function(n){for(var t=[],r=arguments.length,e=0,u=O(n);u>e;e++){var i=n[e];if(!m.contains(t,i)){for(var o=1;r>o&&m.contains(arguments[o],i);o++);o===r&&t.push(i)}}return t},m.difference=function(n){var t=S(arguments,!0,!0,1);return m.filter(n,function(n){return!m.contains(t,n)})},m.zip=function(){return m.unzip(arguments)},m.unzip=function(n){for(var t=n&&m.max(n,O).length||0,r=Array(t),e=0;t>e;e++)r[e]=m.pluck(n,e);return r},m.object=function(n,t){for(var r={},e=0,u=O(n);u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},m.findIndex=t(1),m.findLastIndex=t(-1),m.sortedIndex=function(n,t,r,e){r=x(r,e,1);for(var u=r(t),i=0,o=O(n);o>i;){var a=Math.floor((i+o)/2);r(n[a])i;i++,n+=r)u[i]=n;return u};var E=function(n,t,r,e,u){if(!(e instanceof t))return n.apply(r,u);var i=j(n.prototype),o=n.apply(i,u);return m.isObject(o)?o:i};m.bind=function(n,t){if(g&&n.bind===g)return g.apply(n,l.call(arguments,1));if(!m.isFunction(n))throw new TypeError("Bind must be called on a function");var r=l.call(arguments,2),e=function(){return E(n,e,t,this,r.concat(l.call(arguments)))};return e},m.partial=function(n){var t=l.call(arguments,1),r=function(){for(var e=0,u=t.length,i=Array(u),o=0;u>o;o++)i[o]=t[o]===m?arguments[e++]:t[o];for(;e=e)throw new Error("bindAll must be passed function names");for(t=1;e>t;t++)r=arguments[t],n[r]=m.bind(n[r],n);return n},m.memoize=function(n,t){var r=function(e){var u=r.cache,i=""+(t?t.apply(this,arguments):e);return m.has(u,i)||(u[i]=n.apply(this,arguments)),u[i]};return r.cache={},r},m.delay=function(n,t){var r=l.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},m.defer=m.partial(m.delay,m,1),m.throttle=function(n,t,r){var e,u,i,o=null,a=0;r||(r={});var c=function(){a=r.leading===!1?0:m.now(),o=null,i=n.apply(e,u),o||(e=u=null)};return function(){var f=m.now();a||r.leading!==!1||(a=f);var l=t-(f-a);return e=this,u=arguments,0>=l||l>t?(o&&(clearTimeout(o),o=null),a=f,i=n.apply(e,u),o||(e=u=null)):o||r.trailing===!1||(o=setTimeout(c,l)),i}},m.debounce=function(n,t,r){var e,u,i,o,a,c=function(){var f=m.now()-o;t>f&&f>=0?e=setTimeout(c,t-f):(e=null,r||(a=n.apply(i,u),e||(i=u=null)))};return function(){i=this,u=arguments,o=m.now();var f=r&&!e;return e||(e=setTimeout(c,t)),f&&(a=n.apply(i,u),i=u=null),a}},m.wrap=function(n,t){return m.partial(t,n)},m.negate=function(n){return function(){return!n.apply(this,arguments)}},m.compose=function(){var n=arguments,t=n.length-1;return function(){for(var r=t,e=n[t].apply(this,arguments);r--;)e=n[r].call(this,e);return e}},m.after=function(n,t){return function(){return--n<1?t.apply(this,arguments):void 0}},m.before=function(n,t){var r;return function(){return--n>0&&(r=t.apply(this,arguments)),1>=n&&(t=null),r}},m.once=m.partial(m.before,2);var M=!{toString:null}.propertyIsEnumerable("toString"),I=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"];m.keys=function(n){if(!m.isObject(n))return[];if(v)return v(n);var t=[];for(var r in n)m.has(n,r)&&t.push(r);return M&&e(n,t),t},m.allKeys=function(n){if(!m.isObject(n))return[];var t=[];for(var r in n)t.push(r);return M&&e(n,t),t},m.values=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=n[t[u]];return e},m.mapObject=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=u.length,o={},a=0;i>a;a++)e=u[a],o[e]=t(n[e],e,n);return o},m.pairs=function(n){for(var t=m.keys(n),r=t.length,e=Array(r),u=0;r>u;u++)e[u]=[t[u],n[t[u]]];return e},m.invert=function(n){for(var t={},r=m.keys(n),e=0,u=r.length;u>e;e++)t[n[r[e]]]=r[e];return t},m.functions=m.methods=function(n){var t=[];for(var r in n)m.isFunction(n[r])&&t.push(r);return t.sort()},m.extend=_(m.allKeys),m.extendOwn=m.assign=_(m.keys),m.findKey=function(n,t,r){t=x(t,r);for(var e,u=m.keys(n),i=0,o=u.length;o>i;i++)if(e=u[i],t(n[e],e,n))return e},m.pick=function(n,t,r){var e,u,i={},o=n;if(null==o)return i;m.isFunction(t)?(u=m.allKeys(o),e=b(t,r)):(u=S(arguments,!1,!1,1),e=function(n,t,r){return t in r},o=Object(o));for(var a=0,c=u.length;c>a;a++){var f=u[a],l=o[f];e(l,f,o)&&(i[f]=l)}return i},m.omit=function(n,t,r){if(m.isFunction(t))t=m.negate(t);else{var e=m.map(S(arguments,!1,!1,1),String);t=function(n,t){return!m.contains(e,t)}}return m.pick(n,t,r)},m.defaults=_(m.allKeys,!0),m.create=function(n,t){var r=j(n);return t&&m.extendOwn(r,t),r},m.clone=function(n){return m.isObject(n)?m.isArray(n)?n.slice():m.extend({},n):n},m.tap=function(n,t){return t(n),n},m.isMatch=function(n,t){var r=m.keys(t),e=r.length;if(null==n)return!e;for(var u=Object(n),i=0;e>i;i++){var o=r[i];if(t[o]!==u[o]||!(o in u))return!1}return!0};var N=function(n,t,r,e){if(n===t)return 0!==n||1/n===1/t;if(null==n||null==t)return n===t;n instanceof m&&(n=n._wrapped),t instanceof m&&(t=t._wrapped);var u=s.call(n);if(u!==s.call(t))return!1;switch(u){case"[object RegExp]":case"[object String]":return""+n==""+t;case"[object Number]":return+n!==+n?+t!==+t:0===+n?1/+n===1/t:+n===+t;case"[object Date]":case"[object Boolean]":return+n===+t}var i="[object Array]"===u;if(!i){if("object"!=typeof n||"object"!=typeof t)return!1;var o=n.constructor,a=t.constructor;if(o!==a&&!(m.isFunction(o)&&o instanceof o&&m.isFunction(a)&&a instanceof a)&&"constructor"in n&&"constructor"in t)return!1}r=r||[],e=e||[];for(var c=r.length;c--;)if(r[c]===n)return e[c]===t;if(r.push(n),e.push(t),i){if(c=n.length,c!==t.length)return!1;for(;c--;)if(!N(n[c],t[c],r,e))return!1}else{var f,l=m.keys(n);if(c=l.length,m.keys(t).length!==c)return!1;for(;c--;)if(f=l[c],!m.has(t,f)||!N(n[f],t[f],r,e))return!1}return r.pop(),e.pop(),!0};m.isEqual=function(n,t){return N(n,t)},m.isEmpty=function(n){return null==n?!0:k(n)&&(m.isArray(n)||m.isString(n)||m.isArguments(n))?0===n.length:0===m.keys(n).length},m.isElement=function(n){return!(!n||1!==n.nodeType)},m.isArray=h||function(n){return"[object Array]"===s.call(n)},m.isObject=function(n){var t=typeof n;return"function"===t||"object"===t&&!!n},m.each(["Arguments","Function","String","Number","Date","RegExp","Error"],function(n){m["is"+n]=function(t){return s.call(t)==="[object "+n+"]"}}),m.isArguments(arguments)||(m.isArguments=function(n){return m.has(n,"callee")}),"function"!=typeof/./&&"object"!=typeof Int8Array&&(m.isFunction=function(n){return"function"==typeof n||!1}),m.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},m.isNaN=function(n){return m.isNumber(n)&&n!==+n},m.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"===s.call(n)},m.isNull=function(n){return null===n},m.isUndefined=function(n){return n===void 0},m.has=function(n,t){return null!=n&&p.call(n,t)},m.noConflict=function(){return u._=i,this},m.identity=function(n){return n},m.constant=function(n){return function(){return n}},m.noop=function(){},m.property=w,m.propertyOf=function(n){return null==n?function(){}:function(t){return n[t]}},m.matcher=m.matches=function(n){return n=m.extendOwn({},n),function(t){return m.isMatch(t,n)}},m.times=function(n,t,r){var e=Array(Math.max(0,n));t=b(t,r,1);for(var u=0;n>u;u++)e[u]=t(u);return e},m.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))},m.now=Date.now||function(){return(new Date).getTime()};var B={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},T=m.invert(B),R=function(n){var t=function(t){return n[t]},r="(?:"+m.keys(n).join("|")+")",e=RegExp(r),u=RegExp(r,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};m.escape=R(B),m.unescape=R(T),m.result=function(n,t,r){var e=null==n?void 0:n[t];return e===void 0&&(e=r),m.isFunction(e)?e.call(n):e};var q=0;m.uniqueId=function(n){var t=++q+"";return n?n+t:t},m.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var K=/(.)^/,z={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},D=/\\|'|\r|\n|\u2028|\u2029/g,L=function(n){return"\\"+z[n]};m.template=function(n,t,r){!t&&r&&(t=r),t=m.defaults({},t,m.templateSettings);var e=RegExp([(t.escape||K).source,(t.interpolate||K).source,(t.evaluate||K).source].join("|")+"|$","g"),u=0,i="__p+='";n.replace(e,function(t,r,e,o,a){return i+=n.slice(u,a).replace(D,L),u=a+t.length,r?i+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":e?i+="'+\n((__t=("+e+"))==null?'':__t)+\n'":o&&(i+="';\n"+o+"\n__p+='"),t}),i+="';\n",t.variable||(i="with(obj||{}){\n"+i+"}\n"),i="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+i+"return __p;\n";try{var o=new Function(t.variable||"obj","_",i)}catch(a){throw a.source=i,a}var c=function(n){return o.call(this,n,m)},f=t.variable||"obj";return c.source="function("+f+"){\n"+i+"}",c},m.chain=function(n){var t=m(n);return t._chain=!0,t};var P=function(n,t){return n._chain?m(t).chain():t};m.mixin=function(n){m.each(m.functions(n),function(t){var r=m[t]=n[t];m.prototype[t]=function(){var n=[this._wrapped];return f.apply(n,arguments),P(this,r.apply(m,n))}})},m.mixin(m),m.each(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=o[n];m.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!==n&&"splice"!==n||0!==r.length||delete r[0],P(this,r)}}),m.each(["concat","join","slice"],function(n){var t=o[n];m.prototype[n]=function(){return P(this,t.apply(this._wrapped,arguments))}}),m.prototype.value=function(){return this._wrapped},m.prototype.valueOf=m.prototype.toJSON=m.prototype.value,m.prototype.toString=function(){return""+this._wrapped},"function"==typeof define&&define.amd&&define("underscore",[],function(){return m})}).call(this); 6 | //# sourceMappingURL=underscore-min.map -------------------------------------------------------------------------------- /public/js/lib/text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license RequireJS text 2.0.14 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved. 3 | * Available via the MIT or new BSD license. 4 | * see: http://github.com/requirejs/text for details 5 | */ 6 | /*jslint regexp: true */ 7 | /*global require, XMLHttpRequest, ActiveXObject, 8 | define, window, process, Packages, 9 | java, location, Components, FileUtils */ 10 | 11 | define(['module'], function (module) { 12 | 'use strict'; 13 | 14 | var text, fs, Cc, Ci, xpcIsWindows, 15 | progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], 16 | xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, 17 | bodyRegExp = /]*>\s*([\s\S]+)\s*<\/body>/im, 18 | hasLocation = typeof location !== 'undefined' && location.href, 19 | defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''), 20 | defaultHostName = hasLocation && location.hostname, 21 | defaultPort = hasLocation && (location.port || undefined), 22 | buildMap = {}, 23 | masterConfig = (module.config && module.config()) || {}; 24 | 25 | text = { 26 | version: '2.0.14', 27 | 28 | strip: function (content) { 29 | //Strips declarations so that external SVG and XML 30 | //documents can be added to a document without worry. Also, if the string 31 | //is an HTML document, only the part inside the body tag is returned. 32 | if (content) { 33 | content = content.replace(xmlRegExp, ""); 34 | var matches = content.match(bodyRegExp); 35 | if (matches) { 36 | content = matches[1]; 37 | } 38 | } else { 39 | content = ""; 40 | } 41 | return content; 42 | }, 43 | 44 | jsEscape: function (content) { 45 | return content.replace(/(['\\])/g, '\\$1') 46 | .replace(/[\f]/g, "\\f") 47 | .replace(/[\b]/g, "\\b") 48 | .replace(/[\n]/g, "\\n") 49 | .replace(/[\t]/g, "\\t") 50 | .replace(/[\r]/g, "\\r") 51 | .replace(/[\u2028]/g, "\\u2028") 52 | .replace(/[\u2029]/g, "\\u2029"); 53 | }, 54 | 55 | createXhr: masterConfig.createXhr || function () { 56 | //Would love to dump the ActiveX crap in here. Need IE 6 to die first. 57 | var xhr, i, progId; 58 | if (typeof XMLHttpRequest !== "undefined") { 59 | return new XMLHttpRequest(); 60 | } else if (typeof ActiveXObject !== "undefined") { 61 | for (i = 0; i < 3; i += 1) { 62 | progId = progIds[i]; 63 | try { 64 | xhr = new ActiveXObject(progId); 65 | } catch (e) {} 66 | 67 | if (xhr) { 68 | progIds = [progId]; // so faster next time 69 | break; 70 | } 71 | } 72 | } 73 | 74 | return xhr; 75 | }, 76 | 77 | /** 78 | * Parses a resource name into its component parts. Resource names 79 | * look like: module/name.ext!strip, where the !strip part is 80 | * optional. 81 | * @param {String} name the resource name 82 | * @returns {Object} with properties "moduleName", "ext" and "strip" 83 | * where strip is a boolean. 84 | */ 85 | parseName: function (name) { 86 | var modName, ext, temp, 87 | strip = false, 88 | index = name.lastIndexOf("."), 89 | isRelative = name.indexOf('./') === 0 || 90 | name.indexOf('../') === 0; 91 | 92 | if (index !== -1 && (!isRelative || index > 1)) { 93 | modName = name.substring(0, index); 94 | ext = name.substring(index + 1); 95 | } else { 96 | modName = name; 97 | } 98 | 99 | temp = ext || modName; 100 | index = temp.indexOf("!"); 101 | if (index !== -1) { 102 | //Pull off the strip arg. 103 | strip = temp.substring(index + 1) === "strip"; 104 | temp = temp.substring(0, index); 105 | if (ext) { 106 | ext = temp; 107 | } else { 108 | modName = temp; 109 | } 110 | } 111 | 112 | return { 113 | moduleName: modName, 114 | ext: ext, 115 | strip: strip 116 | }; 117 | }, 118 | 119 | xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/, 120 | 121 | /** 122 | * Is an URL on another domain. Only works for browser use, returns 123 | * false in non-browser environments. Only used to know if an 124 | * optimized .js version of a text resource should be loaded 125 | * instead. 126 | * @param {String} url 127 | * @returns Boolean 128 | */ 129 | useXhr: function (url, protocol, hostname, port) { 130 | var uProtocol, uHostName, uPort, 131 | match = text.xdRegExp.exec(url); 132 | if (!match) { 133 | return true; 134 | } 135 | uProtocol = match[2]; 136 | uHostName = match[3]; 137 | 138 | uHostName = uHostName.split(':'); 139 | uPort = uHostName[1]; 140 | uHostName = uHostName[0]; 141 | 142 | return (!uProtocol || uProtocol === protocol) && 143 | (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) && 144 | ((!uPort && !uHostName) || uPort === port); 145 | }, 146 | 147 | finishLoad: function (name, strip, content, onLoad) { 148 | content = strip ? text.strip(content) : content; 149 | if (masterConfig.isBuild) { 150 | buildMap[name] = content; 151 | } 152 | onLoad(content); 153 | }, 154 | 155 | load: function (name, req, onLoad, config) { 156 | //Name has format: some.module.filext!strip 157 | //The strip part is optional. 158 | //if strip is present, then that means only get the string contents 159 | //inside a body tag in an HTML string. For XML/SVG content it means 160 | //removing the declarations so the content can be inserted 161 | //into the current doc without problems. 162 | 163 | // Do not bother with the work if a build and text will 164 | // not be inlined. 165 | if (config && config.isBuild && !config.inlineText) { 166 | onLoad(); 167 | return; 168 | } 169 | 170 | masterConfig.isBuild = config && config.isBuild; 171 | 172 | var parsed = text.parseName(name), 173 | nonStripName = parsed.moduleName + 174 | (parsed.ext ? '.' + parsed.ext : ''), 175 | url = req.toUrl(nonStripName), 176 | useXhr = (masterConfig.useXhr) || 177 | text.useXhr; 178 | 179 | // Do not load if it is an empty: url 180 | if (url.indexOf('empty:') === 0) { 181 | onLoad(); 182 | return; 183 | } 184 | 185 | //Load the text. Use XHR if possible and in a browser. 186 | if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) { 187 | text.get(url, function (content) { 188 | text.finishLoad(name, parsed.strip, content, onLoad); 189 | }, function (err) { 190 | if (onLoad.error) { 191 | onLoad.error(err); 192 | } 193 | }); 194 | } else { 195 | //Need to fetch the resource across domains. Assume 196 | //the resource has been optimized into a JS module. Fetch 197 | //by the module name + extension, but do not include the 198 | //!strip part to avoid file system issues. 199 | req([nonStripName], function (content) { 200 | text.finishLoad(parsed.moduleName + '.' + parsed.ext, 201 | parsed.strip, content, onLoad); 202 | }); 203 | } 204 | }, 205 | 206 | write: function (pluginName, moduleName, write, config) { 207 | if (buildMap.hasOwnProperty(moduleName)) { 208 | var content = text.jsEscape(buildMap[moduleName]); 209 | write.asModule(pluginName + "!" + moduleName, 210 | "define(function () { return '" + 211 | content + 212 | "';});\n"); 213 | } 214 | }, 215 | 216 | writeFile: function (pluginName, moduleName, req, write, config) { 217 | var parsed = text.parseName(moduleName), 218 | extPart = parsed.ext ? '.' + parsed.ext : '', 219 | nonStripName = parsed.moduleName + extPart, 220 | //Use a '.js' file name so that it indicates it is a 221 | //script that can be loaded across domains. 222 | fileName = req.toUrl(parsed.moduleName + extPart) + '.js'; 223 | 224 | //Leverage own load() method to load plugin value, but only 225 | //write out values that do not have the strip argument, 226 | //to avoid any potential issues with ! in file names. 227 | text.load(nonStripName, req, function (value) { 228 | //Use own write() method to construct full module value. 229 | //But need to create shell that translates writeFile's 230 | //write() to the right interface. 231 | var textWrite = function (contents) { 232 | return write(fileName, contents); 233 | }; 234 | textWrite.asModule = function (moduleName, contents) { 235 | return write.asModule(moduleName, fileName, contents); 236 | }; 237 | 238 | text.write(pluginName, nonStripName, textWrite, config); 239 | }, config); 240 | } 241 | }; 242 | 243 | if (masterConfig.env === 'node' || (!masterConfig.env && 244 | typeof process !== "undefined" && 245 | process.versions && 246 | !!process.versions.node && 247 | !process.versions['node-webkit'] && 248 | !process.versions['atom-shell'])) { 249 | //Using special require.nodeRequire, something added by r.js. 250 | fs = require.nodeRequire('fs'); 251 | 252 | text.get = function (url, callback, errback) { 253 | try { 254 | var file = fs.readFileSync(url, 'utf8'); 255 | //Remove BOM (Byte Mark Order) from utf8 files if it is there. 256 | if (file[0] === '\uFEFF') { 257 | file = file.substring(1); 258 | } 259 | callback(file); 260 | } catch (e) { 261 | if (errback) { 262 | errback(e); 263 | } 264 | } 265 | }; 266 | } else if (masterConfig.env === 'xhr' || (!masterConfig.env && 267 | text.createXhr())) { 268 | text.get = function (url, callback, errback, headers) { 269 | var xhr = text.createXhr(), header; 270 | xhr.open('GET', url, true); 271 | 272 | //Allow plugins direct access to xhr headers 273 | if (headers) { 274 | for (header in headers) { 275 | if (headers.hasOwnProperty(header)) { 276 | xhr.setRequestHeader(header.toLowerCase(), headers[header]); 277 | } 278 | } 279 | } 280 | 281 | //Allow overrides specified in config 282 | if (masterConfig.onXhr) { 283 | masterConfig.onXhr(xhr, url); 284 | } 285 | 286 | xhr.onreadystatechange = function (evt) { 287 | var status, err; 288 | //Do not explicitly handle errors, those should be 289 | //visible via console output in the browser. 290 | if (xhr.readyState === 4) { 291 | status = xhr.status || 0; 292 | if (status > 399 && status < 600) { 293 | //An http 4xx or 5xx error. Signal an error. 294 | err = new Error(url + ' HTTP status: ' + status); 295 | err.xhr = xhr; 296 | if (errback) { 297 | errback(err); 298 | } 299 | } else { 300 | callback(xhr.responseText); 301 | } 302 | 303 | if (masterConfig.onXhrComplete) { 304 | masterConfig.onXhrComplete(xhr, url); 305 | } 306 | } 307 | }; 308 | xhr.send(null); 309 | }; 310 | } else if (masterConfig.env === 'rhino' || (!masterConfig.env && 311 | typeof Packages !== 'undefined' && typeof java !== 'undefined')) { 312 | //Why Java, why is this so awkward? 313 | text.get = function (url, callback) { 314 | var stringBuffer, line, 315 | encoding = "utf-8", 316 | file = new java.io.File(url), 317 | lineSeparator = java.lang.System.getProperty("line.separator"), 318 | input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), 319 | content = ''; 320 | try { 321 | stringBuffer = new java.lang.StringBuffer(); 322 | line = input.readLine(); 323 | 324 | // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 325 | // http://www.unicode.org/faq/utf_bom.html 326 | 327 | // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: 328 | // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 329 | if (line && line.length() && line.charAt(0) === 0xfeff) { 330 | // Eat the BOM, since we've already found the encoding on this file, 331 | // and we plan to concatenating this buffer with others; the BOM should 332 | // only appear at the top of a file. 333 | line = line.substring(1); 334 | } 335 | 336 | if (line !== null) { 337 | stringBuffer.append(line); 338 | } 339 | 340 | while ((line = input.readLine()) !== null) { 341 | stringBuffer.append(lineSeparator); 342 | stringBuffer.append(line); 343 | } 344 | //Make sure we return a JavaScript string and not a Java string. 345 | content = String(stringBuffer.toString()); //String 346 | } finally { 347 | input.close(); 348 | } 349 | callback(content); 350 | }; 351 | } else if (masterConfig.env === 'xpconnect' || (!masterConfig.env && 352 | typeof Components !== 'undefined' && Components.classes && 353 | Components.interfaces)) { 354 | //Avert your gaze! 355 | Cc = Components.classes; 356 | Ci = Components.interfaces; 357 | Components.utils['import']('resource://gre/modules/FileUtils.jsm'); 358 | xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc); 359 | 360 | text.get = function (url, callback) { 361 | var inStream, convertStream, fileObj, 362 | readData = {}; 363 | 364 | if (xpcIsWindows) { 365 | url = url.replace(/\//g, '\\'); 366 | } 367 | 368 | fileObj = new FileUtils.File(url); 369 | 370 | //XPCOM, you so crazy 371 | try { 372 | inStream = Cc['@mozilla.org/network/file-input-stream;1'] 373 | .createInstance(Ci.nsIFileInputStream); 374 | inStream.init(fileObj, 1, 0, false); 375 | 376 | convertStream = Cc['@mozilla.org/intl/converter-input-stream;1'] 377 | .createInstance(Ci.nsIConverterInputStream); 378 | convertStream.init(inStream, "utf-8", inStream.available(), 379 | Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); 380 | 381 | convertStream.readString(inStream.available(), readData); 382 | convertStream.close(); 383 | inStream.close(); 384 | callback(readData.value); 385 | } catch (e) { 386 | throw new Error((fileObj && fileObj.path || '') + ': ' + e); 387 | } 388 | }; 389 | } 390 | return text; 391 | }); 392 | -------------------------------------------------------------------------------- /server/routes.js: -------------------------------------------------------------------------------- 1 | var path = require('path'), 2 | fs = require('fs'), 3 | gh = require('gh-scrape'), 4 | moment = require('moment'), 5 | LOGIN_PAGE = path.join(__dirname, '..', 'public', 'pages', 'login.html'), 6 | _ = require('underscore'); 7 | 8 | module.exports = function(app, request, async, passport) { 9 | 10 | /////////////////////////////////////////// 11 | // Returns Nav Bar User Stats // 12 | /////////////////////////////////////////// 13 | app.get('/userNavBarStats', function(req, res) { 14 | //var name = req.user.github.username; 15 | gh.scrapeContributionStats("https://github.com/shikkic", function(data) { 16 | res.send(data); 17 | }); 18 | }); 19 | 20 | /////////////////////////////////////////// 21 | /// Default landing page / login /// 22 | /////////////////////////////////////////// 23 | app.get('/', isLoggedIn, function(req, res) { 24 | // Parse name and user token and set request options 25 | var name = req.user.github.username, 26 | userToken = req.user.github.token, 27 | gitUrl = "https://api.github.com/user?access_token="+userToken, 28 | options = { 29 | url: gitUrl, 30 | headers: { 31 | 'User-Agent': name 32 | } 33 | }; 34 | 35 | // Make requests for nav bar 36 | // TODO REMOVE THIS AND MAKE FE GRAB THIS DATA 37 | request.get(options, function(error, response, userData) { 38 | if (!error) { 39 | res.render('app', {data: JSON.parse(userData)}); 40 | } 41 | }); 42 | }); 43 | 44 | ////////////////////////////////////////// 45 | /// AUTHENTICATION /// 46 | ////////////////////////////////////////// 47 | app.get('/auth/github', passport.authenticate('github', { scope: ['user:email']})); 48 | 49 | ///////////////////////////////////////// 50 | /// CALL BACK /// 51 | ///////////////////////////////////////// 52 | 53 | app.get('/auth/github/callback', 54 | passport.authenticate('github', { failureRedirect : '/'}), 55 | function(req, res) { 56 | res.redirect('/'); 57 | }); 58 | 59 | ////////////////////////////////////////// 60 | /// LOGOUT /// 61 | ////////////////////////////////////////// 62 | app.get('/logout', function(req, res) { 63 | req.logout(); 64 | res.redirect('/'); 65 | }); 66 | 67 | /////////////////////////////////////////// 68 | /// LOGIN /// 69 | /////////////////////////////////////////// 70 | app.get('/login', function(req, res) { 71 | res.status(200).set('Content-Type', 'text/html').sendFile(LOGIN_PAGE); 72 | }); 73 | 74 | ///////////////////////////////////// 75 | /// Returns a single user's info /// 76 | ///////////////////////////////////// 77 | app.get('/git', function(req, res) { 78 | var name = req.query.name; 79 | var gitUrl = "https://api.github.com/users/"+ name +"/events?access_token="+ght; 80 | var options = { 81 | url: gitUrl, 82 | headers: { 83 | 'User-Agent': name 84 | } 85 | }; 86 | getRequest(gitUrl, options, 1, function(gitObj) { 87 | res.send(gitObj); 88 | }); 89 | }); 90 | 91 | /////////////////////////////////////// 92 | /// Returns info of users following /// 93 | /////////////////////////////////////// 94 | app.get('/geet', isLoggedIn, function(req, res) { 95 | var name = req.user.github.username; 96 | var userToken = req.user.github.token; 97 | getFriendsList(name, userToken, function(names) { 98 | var names = names; 99 | getFriendsEventData(name, userToken, names, function(data){ 100 | getFriendsProfileData(name, names, data, function(results){ 101 | getFriendsPersonalData(name, userToken, names, function(personalData) { 102 | var userList = []; 103 | // TODO create a function for this! 104 | for (var i = 0; i < names.length; i++) { 105 | var user = { 106 | contributions: results[i], 107 | eventData: data[i], 108 | personalData: personalData[i] 109 | }; 110 | userList.push(user); 111 | } 112 | res.send(userList); 113 | }); 114 | }); 115 | }); 116 | }); 117 | }); 118 | 119 | 120 | function getFriendsEventData(name, userToken, friendsList, callback) { 121 | var count = 0; 122 | async.times(friendsList.length, function(_, next) { 123 | var gitUrl = 'https://api.github.com/users/'+friendsList[count]+'/events?access_token='+userToken; 124 | var options = { 125 | url: gitUrl, 126 | headers: { 127 | 'User-Agent': name 128 | } 129 | }; 130 | count++; 131 | request.get(options, function(error, response, body) { 132 | var data = JSON.parse(body); 133 | var pushEventNum = findPush(data); 134 | var watchEventNum = findWatch(data); 135 | 136 | var pushEvent = data[pushEventNum]; 137 | var watchEventNum = data[watchEventNum]; 138 | return next(error, ({pushEvents: pushEvent, watchEvents: watchEventNum})); 139 | }); 140 | }, function(err, results) { 141 | callback(results); 142 | }); 143 | }; 144 | 145 | function getFriendsPersonalData(name, userToken, friendsList, callback) { 146 | var count = 0; 147 | async.times(friendsList.length, function(_, next) { 148 | var gitUrl = 'https://api.github.com/users/'+friendsList[count]+'?access_token='+userToken; 149 | var options = { 150 | url: gitUrl, 151 | headers: { 152 | 'User-Agent': name 153 | } 154 | }; 155 | count++; 156 | request.get(options, function(error, response, body) { 157 | var data = JSON.parse(body); 158 | return next(error, ({personalData: data})); 159 | }); 160 | }, function(err, results) { 161 | callback(results); 162 | }); 163 | }; 164 | 165 | function getFriendsProfileData(name, friendsList, eventData, callback) { 166 | var count = 0; 167 | async.times(friendsList.length, function(_, next) { 168 | var name = friendsList[count]; 169 | var gitUrl = "https://github.com/"+friendsList[count]; 170 | var options = { 171 | url: gitUrl, 172 | headers: { 173 | 'User-Agent': name 174 | } 175 | }; 176 | count++; 177 | request.get(options, function(error, response, body) { 178 | scrapeCon(body, function(body) { 179 | var data = body; 180 | var total = data.total; 181 | var longestStreak = data.longestStreak; 182 | var currentStreak = data.currentStreak; 183 | var contributions = {totals: total, longestStreaks: longestStreak, currentStreaks: currentStreak}; 184 | 185 | return next(null, contributions); 186 | }); 187 | }); 188 | }, function(err, results) { 189 | callback(results); 190 | }); 191 | }; 192 | 193 | function createModels(data) { 194 | var userArray = []; 195 | for(var i in data) { 196 | if(data[i].pushEvents ) { 197 | var gitUser = { 198 | imgUrl: data[i].pushEvents.actor.avatar_url, 199 | user: data[i].pushEvents.actor.login, 200 | userID: data[i].pushEvents.actor.login.toLowerCase(), 201 | repoName: data[i].pushEvents.repo.name, 202 | commitMsg: data[i].pushEvents.payload.commits[0].message, 203 | commitSha: data[i].pushEvents.payload.commits[0].sha.slice(0,5), 204 | commitUrl: 'http://github.com/'+data[i].pushEvents.repo.name+'/commit/'+data[i].pushEvents.payload.commits[0].sha, 205 | date: "Last pushed "+moment(data[i].pushEvents.created_at).fromNow(), 206 | dateString: data[i].pushEvents.created_at 207 | }; 208 | if(data[i].watchEvents) { 209 | gitUser.watch = data[i].watchEvents.repo.name; 210 | gitUser.watchUrl = 'http://www.github.com/'+data[i].watchEvents.repo.name; 211 | }; 212 | userArray.push(gitUser); 213 | } 214 | } 215 | return userArray; 216 | }; 217 | 218 | function userData(name, names, userToken, callback) { 219 | var count = 0; 220 | async.times(names.length, function(_, next) { 221 | var gitUrl = 'https://api.github.com/users/'+names[count]+'/events?access_token='+userToken; 222 | var options = { 223 | url: gitUrl, 224 | headers: { 225 | 'User-Agent': name 226 | } 227 | }; 228 | count++; 229 | request.get(options, function(error, response, body) { 230 | var data = JSON.parse(body); 231 | var pushEventNum = findPush(data); 232 | var watchEventNum = findWatch(data); 233 | 234 | var pushEvent = data[pushEventNum]; 235 | var watchEventNum = data[watchEventNum]; 236 | return next(error, ({pushEvents: pushEvent, watchEvents: watchEventNum})); 237 | }); 238 | }, function(err, results) { 239 | //console.log(results); 240 | callback(results); 241 | }); 242 | }; 243 | 244 | function getFriendsList(name, userToken, callback) { 245 | var page = 1; 246 | var done = false; 247 | var names = []; 248 | async.whilst( 249 | function () { return done === false }, 250 | function (next) { 251 | var gitUrl = 'https://api.github.com/users/'+ name +"/following?access_token="+userToken+'&page='+page; 252 | var options = { 253 | url: gitUrl, 254 | headers: { 255 | 'User-Agent': name 256 | } 257 | }; 258 | request.get(options, function(error, response, body) { 259 | var data = JSON.parse(body); 260 | for (i in data) { 261 | names.push(data[i].login); 262 | } 263 | if (data.length > 0) { 264 | console.log("Data is NOT empty"); 265 | page++; 266 | 267 | } else { 268 | done = true; 269 | } 270 | return next(error, ({friends: names})); 271 | }); 272 | }, 273 | function (err) { 274 | callback(names); 275 | }); 276 | }; 277 | 278 | function getRequest(url, options, page, callback) { 279 | request.get(options, function(error, response, body){ 280 | if(!error) { 281 | var data = JSON.parse(body); 282 | var count = 0; 283 | var found = false; 284 | while(found != true && count < data.length) { 285 | if(data[count].type == "PushEvent") { 286 | found = true; 287 | break; 288 | } 289 | count++; 290 | } 291 | callback(data[count]); 292 | } 293 | return error; 294 | }); 295 | }; 296 | 297 | // TODO This is a fucked function remove 298 | function getRequest2(gitUrl, callback) { 299 | var options = { 300 | url: gitUrl 301 | }; 302 | request.get(options, function(error, response, body){ 303 | if(!error) { 304 | callback(body); 305 | } 306 | return error; 307 | }); 308 | }; 309 | 310 | function isLoggedIn(req, res, next) { 311 | if (req.isAuthenticated()) { 312 | return next(); 313 | } 314 | res.redirect('/login'); 315 | }; 316 | 317 | function findPush(data) { 318 | var count = 0; 319 | var found = false; 320 | while(found != true && count < data.length) { 321 | if(data[count].type == "PushEvent") { 322 | found = true; 323 | break; 324 | } 325 | count++; 326 | } 327 | return count; 328 | }; 329 | 330 | function findWatch(data) { 331 | var count = 0; 332 | var found = false; 333 | while(found != true && count < data.length) { 334 | if(data[count].type == "WatchEvent") { 335 | found = true; 336 | break; 337 | } 338 | count++; 339 | } 340 | return count; 341 | }; 342 | 343 | function scrapeCon(html, callback) { 344 | var user = {}, 345 | j = 0, 346 | z = 0, 347 | index = 0, 348 | character = '', 349 | word = "", 350 | found = false, 351 | len = html.length - 1; 352 | 353 | for (i = 0; i < len; i++) { 354 | // Loop over every char on the page 355 | character = html[i]; 356 | // Finding Contributions in the last year 357 | if (character === 'C') { 358 | index = i + 30; 359 | word = html.slice(i, index); 360 | // Contributions in the last year 361 | if (word === "Contributions in the last year") { 362 | j = i + 30; 363 | found = false; 364 | while (j < len && !found) { 365 | character = html[j]; 366 | if (character === 'c') { 367 | word = html.slice(j, j + 14); 368 | if (word === "contrib-number") { 369 | found = true; 370 | word = ""; 371 | z = j + 16; 372 | character = html[z]; 373 | while (character != '<') { 374 | word += character; 375 | z++; 376 | character = html[z]; 377 | } 378 | user.total = word; 379 | //console.log("total = ", user.total); 380 | } 381 | } 382 | j++; 383 | } 384 | } 385 | index = i + 14; 386 | word = html.slice(i, index); 387 | if (word === "Current streak") { 388 | j = i + 14; 389 | found = false; 390 | while (j < len && !found) { 391 | character = html[j]; 392 | if (character === 'c') { 393 | word = html.slice(j, j +14); 394 | if (word === "contrib-number") { 395 | found = true; 396 | word = ""; 397 | z = j + 16; 398 | character = html[z]; 399 | while (character != '<') { 400 | word += character; 401 | z++; 402 | character = html[z]; 403 | } 404 | user.currentStreak = word; 405 | //console.log("Current streak = ", user.currentStreak); 406 | } 407 | } 408 | j++; 409 | } 410 | } 411 | } 412 | // Finding Longest Streak 413 | if (character === 'L') { 414 | index = i + 14; 415 | word = html.slice(i, index); 416 | if (word === 'Longest streak') { 417 | j = i + 15; 418 | found = false; 419 | while (j < len && !found) { 420 | character = html[j]; 421 | if (character === 'c') { 422 | word = html.slice(j, j +14); 423 | if (word === "contrib-number") { 424 | found = true; 425 | word = ""; 426 | z = j + 16; 427 | character = html[z]; 428 | while (character != '<') { 429 | word += character; 430 | z++; 431 | character = html[z]; 432 | } 433 | user.longestStreak = word; 434 | //console.log("Longest Streak = ", user.streak); 435 | } 436 | } 437 | j++; 438 | } 439 | } 440 | } 441 | } 442 | callback(user); 443 | }; 444 | 445 | }; 446 | -------------------------------------------------------------------------------- /public/js/lib/backbone-min.js: -------------------------------------------------------------------------------- 1 | (function(t){var e=typeof self=="object"&&self.self==self&&self||typeof global=="object"&&global.global==global&&global;if(typeof define==="function"&&define.amd){define(["underscore","jquery","exports"],function(i,r,n){e.Backbone=t(e,n,i,r)})}else if(typeof exports!=="undefined"){var i=require("underscore"),r;try{r=require("jquery")}catch(n){}t(e,exports,i,r)}else{e.Backbone=t(e,{},e._,e.jQuery||e.Zepto||e.ender||e.$)}})(function(t,e,i,r){var n=t.Backbone;var s=Array.prototype.slice;e.VERSION="1.2.3";e.$=r;e.noConflict=function(){t.Backbone=n;return this};e.emulateHTTP=false;e.emulateJSON=false;var a=function(t,e,r){switch(t){case 1:return function(){return i[e](this[r])};case 2:return function(t){return i[e](this[r],t)};case 3:return function(t,n){return i[e](this[r],h(t,this),n)};case 4:return function(t,n,s){return i[e](this[r],h(t,this),n,s)};default:return function(){var t=s.call(arguments);t.unshift(this[r]);return i[e].apply(i,t)}}};var o=function(t,e,r){i.each(e,function(e,n){if(i[n])t.prototype[n]=a(e,n,r)})};var h=function(t,e){if(i.isFunction(t))return t;if(i.isObject(t)&&!e._isModel(t))return u(t);if(i.isString(t))return function(e){return e.get(t)};return t};var u=function(t){var e=i.matches(t);return function(t){return e(t.attributes)}};var l=e.Events={};var c=/\s+/;var f=function(t,e,r,n,s){var a=0,o;if(r&&typeof r==="object"){if(n!==void 0&&"context"in s&&s.context===void 0)s.context=n;for(o=i.keys(r);a7);this._useHashChange=this._wantsHashChange&&this._hasHashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.history&&this.history.pushState);this._usePushState=this._wantsPushState&&this._hasPushState;this.fragment=this.getFragment();this.root=("/"+this.root+"/").replace(O,"/");if(this._wantsHashChange&&this._wantsPushState){if(!this._hasPushState&&!this.atRoot()){var e=this.root.slice(0,-1)||"/";this.location.replace(e+"#"+this.getPath());return true}else if(this._hasPushState&&this.atRoot()){this.navigate(this.getHash(),{replace:true})}}if(!this._hasHashChange&&this._wantsHashChange&&!this._usePushState){this.iframe=document.createElement("iframe");this.iframe.src="javascript:0";this.iframe.style.display="none";this.iframe.tabIndex=-1;var r=document.body;var n=r.insertBefore(this.iframe,r.firstChild).contentWindow;n.document.open();n.document.close();n.location.hash="#"+this.fragment}var s=window.addEventListener||function(t,e){return attachEvent("on"+t,e)};if(this._usePushState){s("popstate",this.checkUrl,false)}else if(this._useHashChange&&!this.iframe){s("hashchange",this.checkUrl,false)}else if(this._wantsHashChange){this._checkUrlInterval=setInterval(this.checkUrl,this.interval)}if(!this.options.silent)return this.loadUrl()},stop:function(){var t=window.removeEventListener||function(t,e){return detachEvent("on"+t,e)};if(this._usePushState){t("popstate",this.checkUrl,false)}else if(this._useHashChange&&!this.iframe){t("hashchange",this.checkUrl,false)}if(this.iframe){document.body.removeChild(this.iframe);this.iframe=null}if(this._checkUrlInterval)clearInterval(this._checkUrlInterval);M.started=false},route:function(t,e){this.handlers.unshift({route:t,callback:e})},checkUrl:function(t){var e=this.getFragment();if(e===this.fragment&&this.iframe){e=this.getHash(this.iframe.contentWindow)}if(e===this.fragment)return false;if(this.iframe)this.navigate(e);this.loadUrl()},loadUrl:function(t){if(!this.matchRoot())return false;t=this.fragment=this.getFragment(t);return i.some(this.handlers,function(e){if(e.route.test(t)){e.callback(t);return true}})},navigate:function(t,e){if(!M.started)return false;if(!e||e===true)e={trigger:!!e};t=this.getFragment(t||"");var i=this.root;if(t===""||t.charAt(0)==="?"){i=i.slice(0,-1)||"/"}var r=i+t;t=this.decodeFragment(t.replace(U,""));if(this.fragment===t)return;this.fragment=t;if(this._usePushState){this.history[e.replace?"replaceState":"pushState"]({},document.title,r)}else if(this._wantsHashChange){this._updateHash(this.location,t,e.replace);if(this.iframe&&t!==this.getHash(this.iframe.contentWindow)){var n=this.iframe.contentWindow;if(!e.replace){n.document.open();n.document.close()}this._updateHash(n.location,t,e.replace)}}else{return this.location.assign(r)}if(e.trigger)return this.loadUrl(t)},_updateHash:function(t,e,i){if(i){var r=t.href.replace(/(javascript:|#).*$/,"");t.replace(r+"#"+e)}else{t.hash="#"+e}}});e.history=new M;var q=function(t,e){var r=this;var n;if(t&&i.has(t,"constructor")){n=t.constructor}else{n=function(){return r.apply(this,arguments)}}i.extend(n,r,e);var s=function(){this.constructor=n};s.prototype=r.prototype;n.prototype=new s;if(t)i.extend(n.prototype,t);n.__super__=r.prototype;return n};y.extend=x.extend=$.extend=I.extend=M.extend=q;var F=function(){throw new Error('A "url" property or function must be specified')};var z=function(t,e){var i=e.error;e.error=function(r){if(i)i.call(e.context,t,r,e);t.trigger("error",t,r,e)}};return e}); 2 | //# sourceMappingURL=backbone-min.map -------------------------------------------------------------------------------- /public/js/lib/mustache.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * mustache.js - Logic-less {{mustache}} templates with JavaScript 3 | * http://github.com/janl/mustache.js 4 | */ 5 | 6 | /*global define: false Mustache: true*/ 7 | 8 | (function defineMustache (global, factory) { 9 | if (typeof exports === 'object' && exports && typeof exports.nodeName !== 'string') { 10 | factory(exports); // CommonJS 11 | } else if (typeof define === 'function' && define.amd) { 12 | define(['exports'], factory); // AMD 13 | } else { 14 | global.Mustache = {}; 15 | factory(global.Mustache); // script, wsh, asp 16 | } 17 | }(this, function mustacheFactory (mustache) { 18 | 19 | var objectToString = Object.prototype.toString; 20 | var isArray = Array.isArray || function isArrayPolyfill (object) { 21 | return objectToString.call(object) === '[object Array]'; 22 | }; 23 | 24 | function isFunction (object) { 25 | return typeof object === 'function'; 26 | } 27 | 28 | /** 29 | * More correct typeof string handling array 30 | * which normally returns typeof 'object' 31 | */ 32 | function typeStr (obj) { 33 | return isArray(obj) ? 'array' : typeof obj; 34 | } 35 | 36 | function escapeRegExp (string) { 37 | return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'); 38 | } 39 | 40 | /** 41 | * Null safe way of checking whether or not an object, 42 | * including its prototype, has a given property 43 | */ 44 | function hasProperty (obj, propName) { 45 | return obj != null && typeof obj === 'object' && (propName in obj); 46 | } 47 | 48 | // Workaround for https://issues.apache.org/jira/browse/COUCHDB-577 49 | // See https://github.com/janl/mustache.js/issues/189 50 | var regExpTest = RegExp.prototype.test; 51 | function testRegExp (re, string) { 52 | return regExpTest.call(re, string); 53 | } 54 | 55 | var nonSpaceRe = /\S/; 56 | function isWhitespace (string) { 57 | return !testRegExp(nonSpaceRe, string); 58 | } 59 | 60 | var entityMap = { 61 | '&': '&', 62 | '<': '<', 63 | '>': '>', 64 | '"': '"', 65 | "'": ''', 66 | '/': '/', 67 | '`': '`', 68 | '=': '=' 69 | }; 70 | 71 | function escapeHtml (string) { 72 | return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) { 73 | return entityMap[s]; 74 | }); 75 | } 76 | 77 | var whiteRe = /\s*/; 78 | var spaceRe = /\s+/; 79 | var equalsRe = /\s*=/; 80 | var curlyRe = /\s*\}/; 81 | var tagRe = /#|\^|\/|>|\{|&|=|!/; 82 | 83 | /** 84 | * Breaks up the given `template` string into a tree of tokens. If the `tags` 85 | * argument is given here it must be an array with two string values: the 86 | * opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of 87 | * course, the default is to use mustaches (i.e. mustache.tags). 88 | * 89 | * A token is an array with at least 4 elements. The first element is the 90 | * mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag 91 | * did not contain a symbol (i.e. {{myValue}}) this element is "name". For 92 | * all text that appears outside a symbol this element is "text". 93 | * 94 | * The second element of a token is its "value". For mustache tags this is 95 | * whatever else was inside the tag besides the opening symbol. For text tokens 96 | * this is the text itself. 97 | * 98 | * The third and fourth elements of the token are the start and end indices, 99 | * respectively, of the token in the original template. 100 | * 101 | * Tokens that are the root node of a subtree contain two more elements: 1) an 102 | * array of tokens in the subtree and 2) the index in the original template at 103 | * which the closing tag for that section begins. 104 | */ 105 | function parseTemplate (template, tags) { 106 | if (!template) 107 | return []; 108 | 109 | var sections = []; // Stack to hold section tokens 110 | var tokens = []; // Buffer to hold the tokens 111 | var spaces = []; // Indices of whitespace tokens on the current line 112 | var hasTag = false; // Is there a {{tag}} on the current line? 113 | var nonSpace = false; // Is there a non-space char on the current line? 114 | 115 | // Strips all whitespace tokens array for the current line 116 | // if there was a {{#tag}} on it and otherwise only space. 117 | function stripSpace () { 118 | if (hasTag && !nonSpace) { 119 | while (spaces.length) 120 | delete tokens[spaces.pop()]; 121 | } else { 122 | spaces = []; 123 | } 124 | 125 | hasTag = false; 126 | nonSpace = false; 127 | } 128 | 129 | var openingTagRe, closingTagRe, closingCurlyRe; 130 | function compileTags (tagsToCompile) { 131 | if (typeof tagsToCompile === 'string') 132 | tagsToCompile = tagsToCompile.split(spaceRe, 2); 133 | 134 | if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) 135 | throw new Error('Invalid tags: ' + tagsToCompile); 136 | 137 | openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*'); 138 | closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1])); 139 | closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1])); 140 | } 141 | 142 | compileTags(tags || mustache.tags); 143 | 144 | var scanner = new Scanner(template); 145 | 146 | var start, type, value, chr, token, openSection; 147 | while (!scanner.eos()) { 148 | start = scanner.pos; 149 | 150 | // Match any text between tags. 151 | value = scanner.scanUntil(openingTagRe); 152 | 153 | if (value) { 154 | for (var i = 0, valueLength = value.length; i < valueLength; ++i) { 155 | chr = value.charAt(i); 156 | 157 | if (isWhitespace(chr)) { 158 | spaces.push(tokens.length); 159 | } else { 160 | nonSpace = true; 161 | } 162 | 163 | tokens.push([ 'text', chr, start, start + 1 ]); 164 | start += 1; 165 | 166 | // Check for whitespace on the current line. 167 | if (chr === '\n') 168 | stripSpace(); 169 | } 170 | } 171 | 172 | // Match the opening tag. 173 | if (!scanner.scan(openingTagRe)) 174 | break; 175 | 176 | hasTag = true; 177 | 178 | // Get the tag type. 179 | type = scanner.scan(tagRe) || 'name'; 180 | scanner.scan(whiteRe); 181 | 182 | // Get the tag value. 183 | if (type === '=') { 184 | value = scanner.scanUntil(equalsRe); 185 | scanner.scan(equalsRe); 186 | scanner.scanUntil(closingTagRe); 187 | } else if (type === '{') { 188 | value = scanner.scanUntil(closingCurlyRe); 189 | scanner.scan(curlyRe); 190 | scanner.scanUntil(closingTagRe); 191 | type = '&'; 192 | } else { 193 | value = scanner.scanUntil(closingTagRe); 194 | } 195 | 196 | // Match the closing tag. 197 | if (!scanner.scan(closingTagRe)) 198 | throw new Error('Unclosed tag at ' + scanner.pos); 199 | 200 | token = [ type, value, start, scanner.pos ]; 201 | tokens.push(token); 202 | 203 | if (type === '#' || type === '^') { 204 | sections.push(token); 205 | } else if (type === '/') { 206 | // Check section nesting. 207 | openSection = sections.pop(); 208 | 209 | if (!openSection) 210 | throw new Error('Unopened section "' + value + '" at ' + start); 211 | 212 | if (openSection[1] !== value) 213 | throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); 214 | } else if (type === 'name' || type === '{' || type === '&') { 215 | nonSpace = true; 216 | } else if (type === '=') { 217 | // Set the tags for the next time around. 218 | compileTags(value); 219 | } 220 | } 221 | 222 | // Make sure there are no open sections when we're done. 223 | openSection = sections.pop(); 224 | 225 | if (openSection) 226 | throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); 227 | 228 | return nestTokens(squashTokens(tokens)); 229 | } 230 | 231 | /** 232 | * Combines the values of consecutive text tokens in the given `tokens` array 233 | * to a single token. 234 | */ 235 | function squashTokens (tokens) { 236 | var squashedTokens = []; 237 | 238 | var token, lastToken; 239 | for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { 240 | token = tokens[i]; 241 | 242 | if (token) { 243 | if (token[0] === 'text' && lastToken && lastToken[0] === 'text') { 244 | lastToken[1] += token[1]; 245 | lastToken[3] = token[3]; 246 | } else { 247 | squashedTokens.push(token); 248 | lastToken = token; 249 | } 250 | } 251 | } 252 | 253 | return squashedTokens; 254 | } 255 | 256 | /** 257 | * Forms the given array of `tokens` into a nested tree structure where 258 | * tokens that represent a section have two additional items: 1) an array of 259 | * all tokens that appear in that section and 2) the index in the original 260 | * template that represents the end of that section. 261 | */ 262 | function nestTokens (tokens) { 263 | var nestedTokens = []; 264 | var collector = nestedTokens; 265 | var sections = []; 266 | 267 | var token, section; 268 | for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { 269 | token = tokens[i]; 270 | 271 | switch (token[0]) { 272 | case '#': 273 | case '^': 274 | collector.push(token); 275 | sections.push(token); 276 | collector = token[4] = []; 277 | break; 278 | case '/': 279 | section = sections.pop(); 280 | section[5] = token[2]; 281 | collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; 282 | break; 283 | default: 284 | collector.push(token); 285 | } 286 | } 287 | 288 | return nestedTokens; 289 | } 290 | 291 | /** 292 | * A simple string scanner that is used by the template parser to find 293 | * tokens in template strings. 294 | */ 295 | function Scanner (string) { 296 | this.string = string; 297 | this.tail = string; 298 | this.pos = 0; 299 | } 300 | 301 | /** 302 | * Returns `true` if the tail is empty (end of string). 303 | */ 304 | Scanner.prototype.eos = function eos () { 305 | return this.tail === ''; 306 | }; 307 | 308 | /** 309 | * Tries to match the given regular expression at the current position. 310 | * Returns the matched text if it can match, the empty string otherwise. 311 | */ 312 | Scanner.prototype.scan = function scan (re) { 313 | var match = this.tail.match(re); 314 | 315 | if (!match || match.index !== 0) 316 | return ''; 317 | 318 | var string = match[0]; 319 | 320 | this.tail = this.tail.substring(string.length); 321 | this.pos += string.length; 322 | 323 | return string; 324 | }; 325 | 326 | /** 327 | * Skips all text until the given regular expression can be matched. Returns 328 | * the skipped string, which is the entire tail if no match can be made. 329 | */ 330 | Scanner.prototype.scanUntil = function scanUntil (re) { 331 | var index = this.tail.search(re), match; 332 | 333 | switch (index) { 334 | case -1: 335 | match = this.tail; 336 | this.tail = ''; 337 | break; 338 | case 0: 339 | match = ''; 340 | break; 341 | default: 342 | match = this.tail.substring(0, index); 343 | this.tail = this.tail.substring(index); 344 | } 345 | 346 | this.pos += match.length; 347 | 348 | return match; 349 | }; 350 | 351 | /** 352 | * Represents a rendering context by wrapping a view object and 353 | * maintaining a reference to the parent context. 354 | */ 355 | function Context (view, parentContext) { 356 | this.view = view; 357 | this.cache = { '.': this.view }; 358 | this.parent = parentContext; 359 | } 360 | 361 | /** 362 | * Creates a new context using the given view with this context 363 | * as the parent. 364 | */ 365 | Context.prototype.push = function push (view) { 366 | return new Context(view, this); 367 | }; 368 | 369 | /** 370 | * Returns the value of the given name in this context, traversing 371 | * up the context hierarchy if the value is absent in this context's view. 372 | */ 373 | Context.prototype.lookup = function lookup (name) { 374 | var cache = this.cache; 375 | 376 | var value; 377 | if (cache.hasOwnProperty(name)) { 378 | value = cache[name]; 379 | } else { 380 | var context = this, names, index, lookupHit = false; 381 | 382 | while (context) { 383 | if (name.indexOf('.') > 0) { 384 | value = context.view; 385 | names = name.split('.'); 386 | index = 0; 387 | 388 | /** 389 | * Using the dot notion path in `name`, we descend through the 390 | * nested objects. 391 | * 392 | * To be certain that the lookup has been successful, we have to 393 | * check if the last object in the path actually has the property 394 | * we are looking for. We store the result in `lookupHit`. 395 | * 396 | * This is specially necessary for when the value has been set to 397 | * `undefined` and we want to avoid looking up parent contexts. 398 | **/ 399 | while (value != null && index < names.length) { 400 | if (index === names.length - 1) 401 | lookupHit = hasProperty(value, names[index]); 402 | 403 | value = value[names[index++]]; 404 | } 405 | } else { 406 | value = context.view[name]; 407 | lookupHit = hasProperty(context.view, name); 408 | } 409 | 410 | if (lookupHit) 411 | break; 412 | 413 | context = context.parent; 414 | } 415 | 416 | cache[name] = value; 417 | } 418 | 419 | if (isFunction(value)) 420 | value = value.call(this.view); 421 | 422 | return value; 423 | }; 424 | 425 | /** 426 | * A Writer knows how to take a stream of tokens and render them to a 427 | * string, given a context. It also maintains a cache of templates to 428 | * avoid the need to parse the same template twice. 429 | */ 430 | function Writer () { 431 | this.cache = {}; 432 | } 433 | 434 | /** 435 | * Clears all cached templates in this writer. 436 | */ 437 | Writer.prototype.clearCache = function clearCache () { 438 | this.cache = {}; 439 | }; 440 | 441 | /** 442 | * Parses and caches the given `template` and returns the array of tokens 443 | * that is generated from the parse. 444 | */ 445 | Writer.prototype.parse = function parse (template, tags) { 446 | var cache = this.cache; 447 | var tokens = cache[template]; 448 | 449 | if (tokens == null) 450 | tokens = cache[template] = parseTemplate(template, tags); 451 | 452 | return tokens; 453 | }; 454 | 455 | /** 456 | * High-level method that is used to render the given `template` with 457 | * the given `view`. 458 | * 459 | * The optional `partials` argument may be an object that contains the 460 | * names and templates of partials that are used in the template. It may 461 | * also be a function that is used to load partial templates on the fly 462 | * that takes a single argument: the name of the partial. 463 | */ 464 | Writer.prototype.render = function render (template, view, partials) { 465 | var tokens = this.parse(template); 466 | var context = (view instanceof Context) ? view : new Context(view); 467 | return this.renderTokens(tokens, context, partials, template); 468 | }; 469 | 470 | /** 471 | * Low-level method that renders the given array of `tokens` using 472 | * the given `context` and `partials`. 473 | * 474 | * Note: The `originalTemplate` is only ever used to extract the portion 475 | * of the original template that was contained in a higher-order section. 476 | * If the template doesn't use higher-order sections, this argument may 477 | * be omitted. 478 | */ 479 | Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate) { 480 | var buffer = ''; 481 | 482 | var token, symbol, value; 483 | for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { 484 | value = undefined; 485 | token = tokens[i]; 486 | symbol = token[0]; 487 | 488 | if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate); 489 | else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate); 490 | else if (symbol === '>') value = this.renderPartial(token, context, partials, originalTemplate); 491 | else if (symbol === '&') value = this.unescapedValue(token, context); 492 | else if (symbol === 'name') value = this.escapedValue(token, context); 493 | else if (symbol === 'text') value = this.rawValue(token); 494 | 495 | if (value !== undefined) 496 | buffer += value; 497 | } 498 | 499 | return buffer; 500 | }; 501 | 502 | Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate) { 503 | var self = this; 504 | var buffer = ''; 505 | var value = context.lookup(token[1]); 506 | 507 | // This function is used to render an arbitrary template 508 | // in the current context by higher-order sections. 509 | function subRender (template) { 510 | return self.render(template, context, partials); 511 | } 512 | 513 | if (!value) return; 514 | 515 | if (isArray(value)) { 516 | for (var j = 0, valueLength = value.length; j < valueLength; ++j) { 517 | buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate); 518 | } 519 | } else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') { 520 | buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate); 521 | } else if (isFunction(value)) { 522 | if (typeof originalTemplate !== 'string') 523 | throw new Error('Cannot use higher-order sections without the original template'); 524 | 525 | // Extract the portion of the original template that the section contains. 526 | value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); 527 | 528 | if (value != null) 529 | buffer += value; 530 | } else { 531 | buffer += this.renderTokens(token[4], context, partials, originalTemplate); 532 | } 533 | return buffer; 534 | }; 535 | 536 | Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate) { 537 | var value = context.lookup(token[1]); 538 | 539 | // Use JavaScript's definition of falsy. Include empty arrays. 540 | // See https://github.com/janl/mustache.js/issues/186 541 | if (!value || (isArray(value) && value.length === 0)) 542 | return this.renderTokens(token[4], context, partials, originalTemplate); 543 | }; 544 | 545 | Writer.prototype.renderPartial = function renderPartial (token, context, partials) { 546 | if (!partials) return; 547 | 548 | var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; 549 | if (value != null) 550 | return this.renderTokens(this.parse(value), context, partials, value); 551 | }; 552 | 553 | Writer.prototype.unescapedValue = function unescapedValue (token, context) { 554 | var value = context.lookup(token[1]); 555 | if (value != null) 556 | return value; 557 | }; 558 | 559 | Writer.prototype.escapedValue = function escapedValue (token, context) { 560 | var value = context.lookup(token[1]); 561 | if (value != null) 562 | return mustache.escape(value); 563 | }; 564 | 565 | Writer.prototype.rawValue = function rawValue (token) { 566 | return token[1]; 567 | }; 568 | 569 | mustache.name = 'mustache.js'; 570 | mustache.version = '2.2.1'; 571 | mustache.tags = [ '{{', '}}' ]; 572 | 573 | // All high-level mustache.* functions use this writer. 574 | var defaultWriter = new Writer(); 575 | 576 | /** 577 | * Clears all cached templates in the default writer. 578 | */ 579 | mustache.clearCache = function clearCache () { 580 | return defaultWriter.clearCache(); 581 | }; 582 | 583 | /** 584 | * Parses and caches the given template in the default writer and returns the 585 | * array of tokens it contains. Doing this ahead of time avoids the need to 586 | * parse templates on the fly as they are rendered. 587 | */ 588 | mustache.parse = function parse (template, tags) { 589 | return defaultWriter.parse(template, tags); 590 | }; 591 | 592 | /** 593 | * Renders the `template` with the given `view` and `partials` using the 594 | * default writer. 595 | */ 596 | mustache.render = function render (template, view, partials) { 597 | if (typeof template !== 'string') { 598 | throw new TypeError('Invalid template! Template should be a "string" ' + 599 | 'but "' + typeStr(template) + '" was given as the first ' + 600 | 'argument for mustache#render(template, view, partials)'); 601 | } 602 | 603 | return defaultWriter.render(template, view, partials); 604 | }; 605 | 606 | // This is here for backwards compatibility with 0.4.x., 607 | /*eslint-disable */ // eslint wants camel cased function name 608 | mustache.to_html = function to_html (template, view, partials, send) { 609 | /*eslint-enable*/ 610 | 611 | var result = mustache.render(template, view, partials); 612 | 613 | if (isFunction(send)) { 614 | send(result); 615 | } else { 616 | return result; 617 | } 618 | }; 619 | 620 | // Export the escaping function so that the user may override it. 621 | // See https://github.com/janl/mustache.js/issues/244 622 | mustache.escape = escapeHtml; 623 | 624 | // Export these mainly for testing, but also for advanced usage. 625 | mustache.Scanner = Scanner; 626 | mustache.Context = Context; 627 | mustache.Writer = Writer; 628 | 629 | })); 630 | --------------------------------------------------------------------------------