├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── Procfile ├── Procfile.local ├── README.md ├── Vagrantfile ├── _Design Assets └── Logo.sketch │ ├── Data │ ├── QuickLook │ ├── Preview.png │ └── Thumbnail.png │ ├── fonts │ ├── metadata │ └── version ├── app.js ├── cluster_app.js ├── components ├── GithubContributors.js └── MarkdownParser.js ├── config ├── passport.js ├── secrets.js └── userlist.js ├── constants.js ├── controllers ├── api.js ├── chat.js ├── comments.js ├── contact.js ├── home.js ├── issues.js ├── news.js ├── user.js └── votes.js ├── forever.js ├── gulpfile.js ├── hackathon-starter-readme.md ├── models ├── Comment.js ├── Issue.js ├── NewsItem.js ├── User.js └── Vote.js ├── package.json ├── public ├── css │ ├── lib │ │ ├── animate.css │ │ ├── bootstrap │ │ │ ├── alerts.less │ │ │ ├── badges.less │ │ │ ├── bootstrap.less │ │ │ ├── breadcrumbs.less │ │ │ ├── button-groups.less │ │ │ ├── buttons.less │ │ │ ├── carousel.less │ │ │ ├── close.less │ │ │ ├── code.less │ │ │ ├── component-animations.less │ │ │ ├── dropdowns.less │ │ │ ├── forms.less │ │ │ ├── glyphicons.less │ │ │ ├── grid.less │ │ │ ├── input-groups.less │ │ │ ├── jumbotron.less │ │ │ ├── labels.less │ │ │ ├── list-group.less │ │ │ ├── media.less │ │ │ ├── mixins.less │ │ │ ├── modals.less │ │ │ ├── navbar.less │ │ │ ├── navs.less │ │ │ ├── normalize.less │ │ │ ├── pager.less │ │ │ ├── pagination.less │ │ │ ├── panels.less │ │ │ ├── popovers.less │ │ │ ├── print.less │ │ │ ├── progress-bars.less │ │ │ ├── responsive-utilities.less │ │ │ ├── scaffolding.less │ │ │ ├── tables.less │ │ │ ├── theme.less │ │ │ ├── thumbnails.less │ │ │ ├── tooltip.less │ │ │ ├── type.less │ │ │ ├── utilities.less │ │ │ ├── variables.less │ │ │ └── wells.less │ │ ├── font-awesome.min.css │ │ └── jquery.atwho.css │ ├── styles.less │ └── themes │ │ ├── default.less │ │ ├── flatly.less │ │ └── ios7.less ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ └── fontawesome-webfont.woff ├── humans.txt ├── img │ ├── appicon.png │ ├── favicon.ico │ ├── hacker_news.png │ ├── paypal.png │ └── sendgrid.png └── js │ ├── globals.js │ ├── keen.js │ ├── lib │ ├── ansi_up.js │ ├── bootstrap.min.js │ ├── jquery-2.1.0.min.js │ ├── jquery.atwho.js │ ├── jquery.caret.js │ └── keen.min.js │ ├── logs.js │ └── main.js ├── tasks ├── db_clear_down.js └── db_seed.js ├── test ├── NewsItem.test.js ├── config.userlist.test.js └── voteCounts.test.js ├── utils └── index.js ├── vagrant ├── ansible-windows.sh ├── playbook.yml └── tasks │ ├── essentials.yml │ ├── mongo.yml │ ├── nodejs.yml │ └── setup_app.yml └── views ├── 404.jade ├── about.jade ├── account ├── comments.jade ├── contributions.jade ├── news.jade ├── settings.jade └── signup.jade ├── analytics.jade ├── bookmarklet.jade ├── chat └── index.jade ├── comments ├── comment.jade ├── index.jade ├── listcomments.jade └── postcomment.jade ├── contact.jade ├── error.jade ├── howtosignup.jade ├── issues ├── index.jade └── show.jade ├── layout.jade ├── logs.jade ├── mixins └── delete.jade ├── news ├── index.jade ├── newsitem.jade ├── newslist.jade ├── show.jade └── submit.jade ├── partials ├── data.jade ├── flash.jade ├── footer.jade ├── joinsite.jade ├── navigation.jade ├── nocomments.jade ├── noscript.jade └── profile.jade └── rss.jade /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | *.swp 10 | 11 | pids 12 | logs 13 | results 14 | tmp 15 | 16 | .env 17 | 18 | npm-debug.log 19 | node_modules 20 | .idea 21 | *.iml 22 | .DS_Store 23 | Thumbs.db 24 | 25 | public/vendor 26 | public/css/styles.css 27 | 28 | .vagrant/ 29 | vagrant_ansible_inventory_default 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | services: 5 | - mongodb 6 | before_script: 7 | - npm install gulp -g 8 | deploy: 9 | provider: heroku 10 | api_key: 11 | secure: yNgE0NqIUewmXddVOx6WS64EbBBbr9OwkpWDLQWugiH/eKGMqAGSlMsGcpiqC2tOR6M0tq0FINEcWBduGWs9OhFOY4ggFe+RSFnQgiAoKwzQLlla3v/vHA+SJ15xidENEWdrBfy4i66gn/j2jpWWcpgnY0gsaU6claJc+aHlbKE= 12 | app: pullup 13 | on: 14 | repo: larvalabs/pullup 15 | git: 16 | depth: 10 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | The only way to join the pullup community is via pull request. Please note 4 | you must **add a feature** or **fix a bug**, along with adding your username 5 | to our [userlist file](https://github.com/larvalabs/pullup/blob/master/config/userlist.js). 6 | In general, we prefer the following for your first pull request: 7 | 8 | * It includes a user facing feature or bug fix. 9 | * It is not a refactor, re-org of the code, or change to how the project functions. 10 | * It works :) 11 | 12 | Save your big refactors and "how it should work" changes until after you're 13 | a member. This seems to cut down on debate and keep the focus on moving 14 | forward. Other than that, go for it! 15 | 16 | # Additional Resources 17 | 18 | * [Current bugs and requested features](https://github.com/larvalabs/pullup/issues?state=open) 19 | * [Setup and general instructions](https://github.com/larvalabs/pullup/blob/master/README.md) 20 | * IRC: #pullup on freenode [(join via Webchat)](https://webchat.freenode.net?channels=%23pullup) 21 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var dbClearDown = require('./tasks/db_clear_down'), 2 | dbSeed = require('./tasks/db_seed'); 3 | 4 | module.exports = function(grunt) { 5 | grunt.registerTask('db-clear', dbClearDown.task); 6 | grunt.registerTask('db-seed', dbClearDown.task, dbSeed.task); 7 | }; 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Larva Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node forever.js 2 | -------------------------------------------------------------------------------- /Procfile.local: -------------------------------------------------------------------------------- 1 | web: node forever.js 2 | mongo: mongod 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PullUp [![Build Status](https://travis-ci.org/larvalabs/pullup.svg?branch=master)](https://travis-ci.org/larvalabs/pullup) [![Code Climate](https://codeclimate.com/repos/52fba7f66956805f68002062/badges/b1a62e6c14008de1ff3c/gpa.svg)](https://codeclimate.com/repos/52fba7f66956805f68002062/feed) [![Gitter chat](https://badges.gitter.im/larvalabs/pullup.png)](https://gitter.im/larvalabs/pullup) 2 | ====== 3 | 4 | A website you join via pull request. See it live at http://pullup.io 5 | 6 | What would it be like if every user of a site had contributed some code? Let's find out! Right now the site is little more 7 | than a terrible Hacker News type thing, but let's see if it can grow into something more. 8 | 9 | How to join 10 | ----------- 11 | 12 | Summary: 13 | - Fork and set up project for development (see below) 14 | - Add your GitHub username to the [authorized users list](https://github.com/larvalabs/pullup/blob/master/config/userlist.js). 15 | - Add a feature, fix a bug, improve the design, etc. 16 | - Submit a pull request! When we merge, you'll be allowed to log in. 17 | 18 | Development Setup 19 | --------------- 20 | 21 | Prerequisites: [Node.js](http://nodejs.org/) and [MongoDB](http://mongodb.org/). 22 | 23 | First, [register a new developer application on GitHub](https://github.com/settings/applications/new). Set the **Homepage URL** to your development server (example: `http://localhost:3000`) and set your **Authorization callback URL** to `http://localhost:3000/auth/github/callback`. Take note of the **Client ID** and **Client Secret**, as you will need them in the next steps. 24 | 25 | ```bash 26 | # Fetch only the latest commits. 27 | git clone git@github.com:larvalabs/pullup.git 28 | 29 | cd pullup 30 | 31 | # Install NPM dependencies 32 | npm install 33 | npm install -g gulp 34 | 35 | # Set the following environment variables: 36 | 37 | export GITHUB_CLIENTID=CLIENTID 38 | export GITHUB_SECRET=SECRET 39 | 40 | # Or, on Windows... 41 | 42 | SET GITHUB_CLIENTID=CLIENTID 43 | SET GITHUB_SECRET=SECRET 44 | ``` 45 | 46 | Once those are set you can run the local development version: 47 | 48 | node app.js 49 | 50 | Or start the app with [foreman](https://github.com/ddollar/foreman): 51 | 52 | foreman start -f Procfile.local 53 | 54 | And perform build tasks and linting with: 55 | 56 | gulp 57 | 58 | 59 | You can find out more technical details in the [Readme for Hackathon Starter](https://github.com/larvalabs/pullup/blob/master/hackathon-starter-readme.md). 60 | 61 | Seeding The Database 62 | -------------------- 63 | 64 | Firstly make sure you have installed ```grunt-cli``` globally 65 | 66 | ```bash 67 | npm install -g grunt-cli 68 | ``` 69 | 70 | Once this has installed you can seed the database by running the following grunt task. 71 | 72 | ```bash 73 | grunt db-seed 74 | ``` 75 | 76 | You may also wish to completely clear down the database, you can do so by running the ```db-clear``` task. 77 | 78 | ```bash 79 | grunt db-clear 80 | ``` 81 | 82 | Pullup Dev Community 83 | -------------------- 84 | 85 | We hang out on [Gitter](https://gitter.im/larvalabs/pullup) 86 | 87 | Using the Vagrant-based Development Environment 88 | ----------------------- 89 | 90 | You'll need [VirtualBox](https://www.virtualbox.org/wiki/Downloads), [Vagrant](http://www.vagrantup.com/downloads.html), and [Ansible](https://devopsu.com/guides/ansible-mac-osx.html) installed to use this environment. 91 | 92 | **Note**: Windows users do not need Ansible installed. (A script will run and install Ansible on the guest machine for you) 93 | 94 | Update the GitHub environment variables in `vagrant/tasks/setup_app.yml` 95 | 96 | Fire up the Vagrant VM: 97 | 98 | vagrant up 99 | 100 | Ensure Ansible has run successfully and provisioned the boxes. If not, try again using `vagrant provision` 101 | 102 | Then, ssh in and follow the installation steps: 103 | 104 | vagrant ssh 105 | cd /vagrant/ 106 | npm install 107 | node app.js 108 | 109 | #### Vagrant Development on a Windows Host 110 | Windows lacks support for symlinks in synced folders. Use `npm install --no-bin-links` instead of `npm install` when installing. 111 | 112 | Credits 113 | ------- 114 | 115 | This project is based on the awesome [Hackathon Starter project](https://github.com/sahat/hackathon-starter). Thanks @sahat! 116 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | CUSTOM_CONFIG = { 5 | "BOX_NAME" => "precise64", 6 | "BOX_URL" => "http://files.vagrantup.com/precise64.box", 7 | "HEADLESS" => false 8 | } 9 | 10 | Vagrant.configure("2") do |config| 11 | # Change this to the name of your Vagrant base box. 12 | config.vm.box = CUSTOM_CONFIG['BOX_NAME'] 13 | 14 | # Change this to a URL from which the base box can be downloaded, if you like. 15 | config.vm.box_url = CUSTOM_CONFIG['BOX_URL'] 16 | 17 | # we will run node.js on port 3000 18 | config.vm.network :forwarded_port, guest: 3000, host: 3000 19 | 20 | # headless? uncomment this to have the VM's window available 21 | config.vm.provider :virtualbox do |vb| 22 | vb.gui = CUSTOM_CONFIG['HEADLESS'] 23 | end 24 | 25 | # forward SSH keys 26 | config.ssh.forward_agent = true 27 | 28 | if (/cygwin|mswin|mingw|bccwin|wince|emx/ =~ RUBY_PLATFORM) 29 | # provisioning with ansible on windows 30 | config.vm.provision "shell", path: "./vagrant/ansible-windows.sh" 31 | else 32 | # provisioning with ansible 33 | config.vm.provision :ansible do |ansible| 34 | ansible.playbook = "./vagrant/playbook.yml" 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /_Design Assets/Logo.sketch/Data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larvalabs/pullup/e4e3778d3debf7b7682e2a72b7d6d9f80c2c10cd/_Design Assets/Logo.sketch/Data -------------------------------------------------------------------------------- /_Design Assets/Logo.sketch/QuickLook/Preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larvalabs/pullup/e4e3778d3debf7b7682e2a72b7d6d9f80c2c10cd/_Design Assets/Logo.sketch/QuickLook/Preview.png -------------------------------------------------------------------------------- /_Design Assets/Logo.sketch/QuickLook/Thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larvalabs/pullup/e4e3778d3debf7b7682e2a72b7d6d9f80c2c10cd/_Design Assets/Logo.sketch/QuickLook/Thumbnail.png -------------------------------------------------------------------------------- /_Design Assets/Logo.sketch/fonts: -------------------------------------------------------------------------------- 1 | Lato-Regular -------------------------------------------------------------------------------- /_Design Assets/Logo.sketch/metadata: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | app 6 | com.bohemiancoding.sketch 7 | build 8 | 5302 9 | commit 10 | 9460a4bc62af5e9ba50dd4143578fd9401710ce5 11 | version 12 | 18 13 | 14 | 15 | -------------------------------------------------------------------------------- /_Design Assets/Logo.sketch/version: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /cluster_app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var os = require('os'); 6 | var cluster = require('cluster'); 7 | 8 | /** 9 | * Cluster setup. 10 | */ 11 | 12 | // Setup the cluster to use app.js 13 | cluster.setupMaster({ 14 | exec: 'app.js' 15 | }); 16 | 17 | // Listen for dying workers 18 | cluster.on('exit', function(worker) { 19 | console.log('Worker ' + worker.id + ' died'); 20 | // Replace the dead worker 21 | cluster.fork(); 22 | }); 23 | 24 | // Fork a worker for each available CPU 25 | for (var i = 0; i < os.cpus().length; i++) { 26 | cluster.fork(); 27 | } 28 | -------------------------------------------------------------------------------- /components/GithubContributors.js: -------------------------------------------------------------------------------- 1 | 2 | var NodeCache = require( "node-cache" ); 3 | var github = require('octonode'); 4 | 5 | /** 6 | * Used to retrieve and manage contributors list requested through GitHub 7 | * API. 8 | */ 9 | function GithubContributors() { 10 | 11 | /* Unauthenticated GitHub API requests a rate limited to 60 per hour. 12 | TTL of cache should be set appropriately */ 13 | this.cache = new NodeCache({ stdTTL: 15 * 60, checkperiod: 15 * 60 }); 14 | }; 15 | 16 | /** 17 | * Looks for contributors list in cache. In the event of a cache miss makes 18 | * a GitHub API request using the current users GitHub authentication 19 | * information. 20 | * @param {Object} args May contain on error and on success callbacks 21 | */ 22 | GithubContributors.prototype.getContributors = function(args) { 23 | var that = this; 24 | var errorCallback = args.onError || function() {}; 25 | var successCallback = args.onSuccess || function() {}; 26 | 27 | this.cache.get('githubContributors', function (err, value) { 28 | 29 | if (err) { 30 | errorCallback(); 31 | } else if (!Object.keys(value).length) { 32 | /* Since contributors data isn't in the cache, request it via the 33 | GitHub API */ 34 | console.log ('getUserGithubData: cache miss'); 35 | 36 | var client = github.client(); 37 | var ghrepo = client.repo('larvalabs/pullup'); 38 | ghrepo.contributors (function(err, data, headers) { 39 | if (err) { 40 | errorCallback(); 41 | return; 42 | } 43 | successCallback (data); 44 | 45 | // cache the results 46 | that.cache.set('githubContributors', data, 47 | function (err, success) { 48 | 49 | if(!err && success) { 50 | console.log ('cached github contributors'); 51 | } else { 52 | console.warn ('failed to cache github contributors'); 53 | } 54 | }); 55 | }); 56 | 57 | } else { 58 | console.log ('getUserGithubData: cache hit'); 59 | successCallback(value); 60 | } 61 | 62 | }); 63 | }; 64 | 65 | /** 66 | * @param {String} username Name of the user for which the number of 67 | * contributions should be returned 68 | * @param {Array} contributors The response of the GitHub API request made 69 | * in getContributors() 70 | * @return {Number} 71 | */ 72 | GithubContributors.prototype.getContributions = function( 73 | username, contributors) { 74 | 75 | var contributions = 0; 76 | for (var i in contributors) { 77 | if (contributors[i].login === username) { 78 | contributions = contributors[i].contributions; 79 | break; 80 | } 81 | } 82 | return contributions; 83 | }; 84 | 85 | GithubContributors.prototype.getIssues = function(onSuccess, onError) { 86 | var that = this; 87 | 88 | this.cache.get('githubIssues', function (err, value) { 89 | if (err) { 90 | onError(); 91 | } else if (!Object.keys(value).length) { 92 | var client = github.client(), 93 | ghrepo = client.repo('larvalabs/pullup'); 94 | 95 | ghrepo.issues({state: 'all'}, function (err, data, headers) { 96 | if (err) { 97 | onError(); 98 | return; 99 | } 100 | 101 | onSuccess(data); 102 | 103 | that.cache.set('githubIssues', data, 104 | function (err, success) { 105 | 106 | if (!err && success) { 107 | console.log('cached github issues'); 108 | } else { 109 | console.warn('failed to cache github issues'); 110 | } 111 | }); 112 | }); 113 | } else { 114 | console.log('getUserGithubIssueData: cache hit'); 115 | onSuccess(value.githubIssues); 116 | } 117 | }); 118 | }; 119 | 120 | GithubContributors.prototype.getIssuesForUser = function(username, allIssues) { 121 | var issues = []; 122 | for (var i in allIssues) { 123 | if (allIssues[i].user.login === username) { 124 | issues.push(allIssues[i]); 125 | } 126 | } 127 | return issues; 128 | }; 129 | 130 | module.exports = new GithubContributors(); -------------------------------------------------------------------------------- /components/MarkdownParser.js: -------------------------------------------------------------------------------- 1 | var marked = require('marked'); 2 | 3 | marked.setOptions({ 4 | sanitize: true, 5 | silent: true 6 | }); 7 | 8 | module.exports = marked; -------------------------------------------------------------------------------- /config/secrets.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | db: process.env.MONGOHQ_URL || 'localhost', 3 | 4 | sendgrid: { 5 | user: process.env.SENDGRID_USERNAME, 6 | password: process.env.SENDGRID_PASSWORD 7 | }, 8 | 9 | facebook: { 10 | clientID: 'Your App ID', 11 | clientSecret: 'Your App Secret', 12 | callbackURL: '/auth/facebook/callback', 13 | passReqToCallback: true 14 | }, 15 | 16 | github: { 17 | clientID: process.env.GITHUB_CLIENTID, 18 | clientSecret: process.env.GITHUB_SECRET, 19 | callbackURL: process.env.GITHUB_CALLBACK || '/auth/github/callback', 20 | scope: ['user:email'], 21 | passReqToCallback: true 22 | }, 23 | 24 | heroku: { 25 | email: process.env.HEROKU_EMAIL || false, 26 | authToken: process.env.HEROKU_AUTH_TOKEN || false, 27 | app: process.env.HEROKU_APP || false 28 | }, 29 | 30 | twitter: { 31 | consumerKey: 'Your Consumer Key', 32 | consumerSecret: 'Your Consumer Secret', 33 | callbackURL: '/auth/twitter/callback', 34 | passReqToCallback: true 35 | }, 36 | 37 | google: { 38 | clientID: 'Your Client ID', 39 | clientSecret: 'Your Client Secret', 40 | callbackURL: '/auth/google/callback', 41 | passReqToCallback: true 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /config/userlist.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | users: [ // ADD YOUR USERNAME IN ITS ALPHABETICAL SLOT 3 | 'amac441', 4 | 'andersos', 5 | 'arminkhoshbin', 6 | 'avaynshtok', 7 | 'barretts', 8 | 'bluetidepro', 9 | 'bobnisco', 10 | 'briansoule', 11 | 'bwlang', 12 | 'Caraheacock', 13 | 'cgroner', 14 | 'chall8908', 15 | 'ChristianGaertner', 16 | 'cpdean', 17 | 'crandles', 18 | 'crankeye', 19 | 'd7p', 20 | 'dcancel', 21 | 'dck273', 22 | 'dfjones', 23 | 'djstearns', 24 | 'DusanSacha', 25 | 'dylnclrk', 26 | 'eik3', 27 | 'euank', 28 | 'evsie001', 29 | 'exadeci', 30 | 'fridays', 31 | 'gwillen', 32 | 'hemanth', 33 | 'humd', 34 | 'ianwalter', 35 | 'isaaczafuta', 36 | 'jackcarter', 37 | 'jeffawang', 38 | 'jensenbox', 39 | 'jeromegn', 40 | 'jgillich', 41 | 'jmcelwain', 42 | 'joewright', 43 | 'jordancalder', 44 | 'josegonzalez', 45 | 'josephwegner', 46 | 'jphatton', 47 | 'julbaxter', 48 | 'kalmi', 49 | 'kanakiyajay', 50 | 'kasperlewau', 51 | 'kckolz', 52 | 'KeenRivals', 53 | 'kennethrapp', 54 | 'kevinsearle', 55 | 'lablayers', 56 | 'LeandroLovisolo', 57 | 'lgarron', 58 | 'lukemiles', 59 | 'marco-fiset', 60 | 'markbao', 61 | 'mattclaw', 62 | 'megamattron', 63 | 'michaelnovakjr', 64 | 'mreinhardt', 65 | 'msied', 66 | 'naturalethic', 67 | 'negitivezero', 68 | 'omphalosskeptic', 69 | 'parenparen', 70 | 'pents90', 71 | 'pksjce', 72 | 'qguv', 73 | 'rickhanlonii', 74 | 'ritvik1512', 75 | 'roryokane', 76 | 'rtfeldman', 77 | 'ryanwi', 78 | 'samcal', 79 | 'shedd', 80 | 'singularperturbation', 81 | 'sorpaas', 82 | 'sp1d3rx', 83 | 'sriehl', 84 | 'suprememoocow', 85 | 'swelham', 86 | 'tdhsmith', 87 | 'tejohnso', 88 | 'testuser1', 89 | 'theprofessor117', 90 | 'thomascate', 91 | 'tito0224', 92 | 'travishorn', 93 | 'treygriffith', 94 | 'utdemir', 95 | 'vallyath', 96 | 'vellip', 97 | 'vvayne', 98 | 'williamle8300', 99 | 'willurd', 100 | 'zhanglun' 101 | ] 102 | }; 103 | -------------------------------------------------------------------------------- /constants.js: -------------------------------------------------------------------------------- 1 | 2 | var constants = {}; 3 | 4 | Object.defineProperty (constants, "DEBUG", { 5 | value: false, 6 | writable: false 7 | }); 8 | 9 | module.exports = constants; 10 | -------------------------------------------------------------------------------- /controllers/api.js: -------------------------------------------------------------------------------- 1 | var secrets = require('../config/secrets'); 2 | var User = require('../models/User'); 3 | var querystring = require('querystring'); 4 | var async = require('async'); 5 | var cheerio = require('cheerio'); 6 | var request = require('request'); 7 | var _ = require('underscore'); 8 | var marked = require('marked'); 9 | var userlist = require('../config/userlist.js'); 10 | 11 | marked.setOptions({ 12 | sanitize: true, 13 | silent: true 14 | }); 15 | 16 | exports.getMarkdown = function(req, res) { 17 | if (!req.user) { 18 | return res.send({ 19 | messages: [{ msg: 'Only members can upvote items.' }] 20 | }); 21 | } 22 | 23 | if(typeof req.body.source == 'undefined') { 24 | return res.send({ 25 | messages: [{ msg: "You didn't send any markdown."}] 26 | }); 27 | } 28 | 29 | if(req.body.source === ''){ 30 | return res.send({ 31 | result: '' 32 | }); 33 | } 34 | 35 | return res.send({ 36 | result: marked(req.body.source) 37 | }); 38 | }; 39 | 40 | exports.getUserList = function(req, res){ 41 | return res.send(userlist); 42 | }; -------------------------------------------------------------------------------- /controllers/chat.js: -------------------------------------------------------------------------------- 1 | 2 | exports.index = function (req, res) { 3 | res.render('chat/index'); 4 | }; -------------------------------------------------------------------------------- /controllers/contact.js: -------------------------------------------------------------------------------- 1 | var secrets = require('../config/secrets'); 2 | var sendgrid = require('sendgrid')(secrets.sendgrid.user, secrets.sendgrid.password); 3 | 4 | /** 5 | * GET /contact 6 | * Contact form page. 7 | */ 8 | 9 | exports.getContact = function(req, res) { 10 | res.render('contact', { 11 | title: 'Contact' 12 | }); 13 | }; 14 | 15 | /** 16 | * POST /contact 17 | * Send a contact form via SendGrid. 18 | * @param {string} email 19 | * @param {string} name 20 | * @param {string} message 21 | */ 22 | 23 | exports.postContact = function(req, res) { 24 | req.assert('name', 'Name cannot be blank').notEmpty(); 25 | req.assert('email', 'Email cannot be blank').notEmpty(); 26 | req.assert('email', 'Email is not valid').isEmail(); 27 | req.assert('message', 'Message cannot be blank').notEmpty(); 28 | 29 | var errors = req.validationErrors(); 30 | 31 | if (errors) { 32 | req.flash('errors', errors); 33 | return res.redirect('/contact'); 34 | } 35 | 36 | var from = req.body.email; 37 | var name = req.body.name; 38 | var body = req.body.message; 39 | var to = 'you@email.com'; 40 | var subject = 'API Example | Contact Form'; 41 | 42 | var email = new sendgrid.Email({ 43 | to: to, 44 | from: from, 45 | subject: subject, 46 | text: body + '\n\n' + name 47 | }); 48 | 49 | sendgrid.send(email, function(err) { 50 | if (err) { 51 | req.flash('errors', { msg: err.message }); 52 | return res.redirect('/contact'); 53 | } 54 | req.flash('success', { msg: 'Email has been sent successfully!' }); 55 | res.redirect('/contact'); 56 | }); 57 | }; 58 | -------------------------------------------------------------------------------- /controllers/home.js: -------------------------------------------------------------------------------- 1 | /** 2 | * GET / 3 | * Home page. 4 | */ 5 | 6 | exports.index = function(req, res) { 7 | res.redirect('/news'); 8 | }; 9 | 10 | exports.signup = function(req, res) { 11 | res.render('howtosignup', { 12 | title: 'Signup' 13 | }); 14 | }; 15 | 16 | exports.about = function(req, res) { 17 | res.render('about', { 18 | title: 'About' 19 | }); 20 | }; 21 | 22 | exports.analytics = function(req, res) { 23 | res.render('analytics', { 24 | title: 'Analytics' 25 | }); 26 | }; 27 | 28 | exports.logs = function(req, res) { 29 | res.render('logs', { 30 | title: 'Logs' 31 | }); 32 | }; 33 | 34 | exports.bookmarklet = function(req, res) { 35 | res.render('bookmarklet', { 36 | title: 'Bookmarklet' 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /controllers/user.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var passport = require('passport'); 3 | var _ = require('underscore'); 4 | var User = require('../models/User'); 5 | var Comment = require('../models/Comment'); 6 | var githubContributors = require('../components/GithubContributors'); 7 | var async = require('async'); 8 | var news = require('./news'); 9 | var markdownParser = require('../components/MarkdownParser'); 10 | var utils = require('../utils'); 11 | 12 | 13 | /** 14 | * GET /signup 15 | * Signup page. 16 | */ 17 | 18 | exports.getSignup = function(req, res) { 19 | if (req.user) return res.redirect('/'); 20 | res.render('account/signup', { 21 | title: 'Create Account' 22 | }); 23 | }; 24 | 25 | /** 26 | * GET /account 27 | * Profile page. 28 | */ 29 | 30 | exports.getAccount = function(req, res) { 31 | console.log (res.locals.user); 32 | res.render('account/settings', { 33 | title: 'Account Management' 34 | }); 35 | }; 36 | 37 | /** 38 | * POST /account/profile 39 | * Update profile information. 40 | */ 41 | 42 | exports.postUpdateProfile = function(req, res, next) { 43 | User.findById(req.user.id, function(err, user) { 44 | if (err) return next(err); 45 | user.email = req.body.email || ''; 46 | user.profile.name = req.body.name || ''; 47 | user.profile.location = req.body.location || ''; 48 | user.profile.bio = req.body.bio || ''; 49 | 50 | if(req.body.description.length < 32){ 51 | user.profile.description = req.body.description || ''; 52 | } 53 | 54 | if (req.body.website.match(/https?:\/\//i)) { 55 | user.profile.website = req.body.website; 56 | } else if (user.profile.website) { 57 | user.profile.website = 'http://' + req.body.website; 58 | } else { 59 | user.profile.website = ''; 60 | } 61 | 62 | user.save(function(err) { 63 | if (err) return next(err); 64 | req.flash('success', { msg: 'Profile information updated.' }); 65 | res.redirect('/account'); 66 | }); 67 | }); 68 | }; 69 | 70 | /** 71 | * POST /account/password 72 | * Update current password. 73 | * @param {string} password 74 | */ 75 | 76 | exports.postUpdatePassword = function(req, res, next) { 77 | req.assert('password', 'Password must be at least 4 characters long').len(4); 78 | req.assert('confirmPassword', 'Passwords do not match').equals(req.body.password); 79 | 80 | var errors = req.validationErrors(); 81 | 82 | if (errors) { 83 | req.flash('errors', errors); 84 | return res.redirect('/account'); 85 | } 86 | 87 | User.findById(req.user.id, function(err, user) { 88 | if (err) return next(err); 89 | 90 | user.password = req.body.password; 91 | 92 | user.save(function(err) { 93 | if (err) return next(err); 94 | req.flash('success', { msg: 'Password has been changed.' }); 95 | res.redirect('/account'); 96 | }); 97 | }); 98 | }; 99 | 100 | /** 101 | * POST /account/delete 102 | * Delete user account. 103 | * @param {string} id 104 | */ 105 | 106 | exports.postDeleteAccount = function(req, res, next) { 107 | User.remove({ _id: req.user.id }, function(err) { 108 | if (err) return next(err); 109 | req.logout(); 110 | res.redirect('/'); 111 | }); 112 | }; 113 | 114 | /** 115 | * GET /account/unlink/:provider 116 | * Unlink OAuth2 provider from the current user. 117 | * @param {string} provider 118 | * @param {string} id 119 | */ 120 | 121 | exports.getOauthUnlink = function(req, res, next) { 122 | var provider = req.params.provider; 123 | User.findById(req.user.id, function(err, user) { 124 | if (err) return next(err); 125 | 126 | user[provider] = undefined; 127 | user.tokens = _.reject(user.tokens, function(token) { return token.kind === provider; }); 128 | 129 | user.save(function(err) { 130 | if (err) return next(err); 131 | req.flash('info', { msg: provider + ' account has been unlinked.' }); 132 | res.redirect('/account'); 133 | }); 134 | }); 135 | }; 136 | 137 | function getUserOrRespond(req, resp, next, callback){ 138 | User 139 | .findOne({'username': req.params.id}) 140 | .exec(function(err, user) { 141 | 142 | if (err) return next(err); 143 | 144 | if (!user) { 145 | req.flash('errors', {msg: "That user does not exist. "}); 146 | return res.redirect('/'); 147 | } 148 | 149 | user.profile.bio = markdownParser(user.profile.bio); 150 | 151 | githubContributors.getContributors({ 152 | onError: function() { 153 | callback(user); 154 | }, 155 | onSuccess: function(data) { 156 | user.profile.contributions_count = githubContributors.getContributions(user.username, data); 157 | callback(user); 158 | } 159 | }); 160 | }); 161 | } 162 | 163 | function getCommentsForUser(user, callback){ 164 | async.waterfall([ 165 | function (cb) { 166 | Comment 167 | .find({'poster': user.id}) 168 | .sort('-created') 169 | .limit(30) 170 | .populate('poster') 171 | .exec(cb); 172 | }, 173 | function (comments, cb) { 174 | news.getNewsItemsForComments(comments, user, cb); 175 | }, 176 | function (comments, cb){ 177 | _.each(comments, function (comment,i,l) { 178 | comment.contents = markdownParser(utils.replaceUserMentions(comment.contents)); 179 | }); 180 | cb(null, comments); 181 | } 182 | ], callback); 183 | } 184 | 185 | /** 186 | * GET /user/:id 187 | */ 188 | exports.user = function(req, res, next) { 189 | getUserOrRespond(req, res, next, function(user){ 190 | news.getNewsItems({'poster': user.id}, req.user, function(err, newsItems){ 191 | if (err) return next(err); 192 | res.render('account/news', { 193 | title: 'Posts by ' + user.username, 194 | tab: 'news', 195 | items: newsItems, 196 | userView: user 197 | }); 198 | }); 199 | }); 200 | }; 201 | 202 | /** 203 | * GET /user/:id/comments 204 | */ 205 | exports.userComments = function(req, res, next) { 206 | getUserOrRespond(req, res, next, function(user){ 207 | getCommentsForUser(user, function (err, comments) { 208 | if (err) return next(err); 209 | 210 | res.render('account/comments', { 211 | title: 'Comments by ' + user.username, 212 | tab: 'comments', 213 | comments: comments, 214 | userView: user 215 | }); 216 | }); 217 | }); 218 | }; 219 | 220 | /** 221 | * GET /user/:id/comments 222 | */ 223 | exports.userContributions = function(req, res, next) { 224 | getUserOrRespond(req, res, next, function(user){ 225 | githubContributors.getIssues(function(allIssues) { 226 | var contributions = githubContributors.getIssuesForUser(user.username, allIssues); 227 | 228 | res.render('account/contributions', { 229 | title: 'Contributions by ' + user.username, 230 | tab: 'contributions', 231 | contributions: contributions, 232 | userView: user 233 | }); 234 | }); 235 | }); 236 | }; 237 | 238 | /** 239 | * GET /logout 240 | * Log out. 241 | */ 242 | 243 | exports.logout = function(req, res) { 244 | req.logout(); 245 | res.redirect('/'); 246 | }; 247 | -------------------------------------------------------------------------------- /controllers/votes.js: -------------------------------------------------------------------------------- 1 | var Vote = require('../models/Vote'); 2 | var votesController = exports; 3 | 4 | // generic vote controller 5 | exports.voteFor = function (type, root) { 6 | 7 | return function (req, res, next) { 8 | 9 | req.assert('amount', 'Items can only be upvoted.').equals('1'); 10 | 11 | var errors = req.validationErrors(); 12 | 13 | if (errors) { 14 | return res.send({ 15 | messages: errors 16 | }); 17 | } 18 | 19 | if (!req.user) { 20 | return res.send({ 21 | messages: [{ msg: 'Only members can upvote items.' }] 22 | }); 23 | } 24 | 25 | var vote = new Vote({ 26 | item: req.params.id, 27 | voter: req.user.id, 28 | amount: req.body.amount, 29 | itemType: type 30 | }); 31 | 32 | vote.save(function (err) { 33 | if (err) { 34 | var errors = []; 35 | 36 | if (err.code === 11000) { 37 | errors.push({ msg: 'You can only upvote an item once.' }); 38 | } 39 | console.warn(err); 40 | 41 | return res.send({ 42 | messages: errors, 43 | }); 44 | } 45 | 46 | return res.send({ 47 | messages: [{ msg: 'Item upvoted. Awesome!' }], 48 | success: true 49 | }); 50 | }); 51 | }; 52 | 53 | }; 54 | 55 | // generic vote retrieval 56 | /** 57 | * Query the Votes collection to find votes for item(s) 58 | * @param {String} type Type of item. Should be `news`, `comment`, or `issue` 59 | * @param {String | Array} id Either a string or an array of strings (or object id's) defining the item(s) to be queried about 60 | * @param {Function} callback Evaluated with an error as the first parameter and the votes found as the second 61 | */ 62 | exports.retrieveVotesFor = function (type, id, callback) { 63 | 64 | if(arguments.length === 1) { 65 | return function (id, callback) { 66 | return votesController.retrieveVotesFor(type, id, callback); 67 | }; 68 | } 69 | 70 | // special case of allowing a null type for news 71 | if(type === 'news') { 72 | type = { $in: ['news', null] }; 73 | } 74 | 75 | // pass an array of ids to find votes for all of them 76 | if(Array.isArray(id)) { 77 | id = { $in: id }; 78 | } 79 | 80 | Vote 81 | .find({ 82 | item: id, 83 | itemType: type 84 | }) 85 | .populate('voter') 86 | .exec(callback); 87 | }; 88 | 89 | // add a `votes` count and `votedFor` property for a particular item, vote array, and user 90 | exports.addVotesToItem = function (item, item_id, user, votes) { 91 | 92 | item = typeof item.toObject === 'function' ? item.toObject() : item; 93 | item_id = item_id.toString(); 94 | 95 | item.votes = votes 96 | .filter(function (vote) { 97 | return vote.item.toString() === item_id; 98 | }) 99 | .reduce(function (prev, curr, i) { 100 | 101 | // count this item as voted for if the logged in user has a vote tallied 102 | if(user && user.id && curr.voter.id.toString() === user.id.toString()) { 103 | item.votedFor = true; 104 | } 105 | 106 | return prev + curr.amount; 107 | }, 0); 108 | 109 | item.voters = votes.filter(function(vote){ 110 | return vote.item.toString() === item_id; 111 | }) 112 | .map(function (vote) { 113 | return vote.voter.username; 114 | }); 115 | 116 | return item; 117 | }; 118 | 119 | // all-in-one query 120 | /** 121 | * Add a `votes` property to an Array of Objects based on their type, `id` property, and the current user 122 | * @param {String} type Type of item. Should be `news`, `comment`, or `issue` 123 | * @param {Array | Object} items Array of objects (or a single object) that votes should be added to. This converts mongoose objects into plain objects. 124 | * @param {User} user A user object with an `id` property 125 | * @param {Function} callback Function to be evaluated with an error as the first parameter, and the modified objects as the second 126 | */ 127 | exports.addVotesFor = function (type, items, user, callback) { 128 | 129 | var isArray; 130 | 131 | if(arguments.length === 1) { 132 | return function (items, user, callback) { 133 | return votesController.addVotesFor(type, items, user, callback); 134 | }; 135 | } 136 | 137 | wasArray = Array.isArray(items); 138 | 139 | // make a non-array argument behave as an array for now to simplify our dealing with it 140 | if(!wasArray) items = [items]; 141 | 142 | votesController.retrieveVotesFor(type, items, function (err, votes) { 143 | 144 | if(err) return callback(err); 145 | 146 | // add the votes to the objects themselves 147 | items = items.map(function (item) { 148 | return votesController.addVotesToItem(item, item._id, user, votes); 149 | }); 150 | 151 | // convert back if it wasn't passed in as an array 152 | if(!wasArray) items = items[0]; 153 | 154 | callback(null, items); 155 | }); 156 | }; 157 | -------------------------------------------------------------------------------- /forever.js: -------------------------------------------------------------------------------- 1 | var forever = require('forever-monitor'); 2 | 3 | var child = new (forever.Monitor)('app.js', { 4 | max: 3 5 | }); 6 | 7 | child.on('exit', function () { 8 | console.warn('app.js has exited after 3 restarts'); 9 | }); 10 | 11 | child.start(); 12 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var jshint = require('gulp-jshint'); 3 | var mocha = require('gulp-mocha'); 4 | 5 | var paths = { 6 | scripts: ['./*.js', './config/*.js', 'controllers/*.js', 'models/*.js', 'public/js/*.js', '!public/js/lib/**/*.js', 'test/*.js'], 7 | tests: ['./test/*.js'] 8 | }; 9 | 10 | gulp.task('jshint', function () { 11 | gulp.src(paths.scripts) 12 | .pipe(jshint()) 13 | .pipe(jshint.reporter('default')) 14 | .pipe(jshint.reporter('fail')); 15 | }); 16 | 17 | gulp.task('mocha-tests', function () { 18 | gulp.src(paths.tests) 19 | .pipe(mocha({reporter: 'spec'})); 20 | }); 21 | 22 | gulp.task('test', ['jshint', 'mocha-tests']); 23 | gulp.task('default', ['test']); 24 | -------------------------------------------------------------------------------- /models/Comment.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'), Schema = mongoose.Schema; 2 | var userSchema = require('./User.js'); 3 | 4 | var commentSchema = new mongoose.Schema({ 5 | item: { 6 | type: Schema.Types.ObjectId, 7 | required: true 8 | }, 9 | itemType: { 10 | type: String, 11 | required: true, 12 | enum: ['news', 'comment'] 13 | }, 14 | contents: { 15 | type: String, 16 | default: '', 17 | required: true 18 | }, 19 | poster: { 20 | type: String, 21 | ref: 'User' 22 | }, 23 | vote_count: { 24 | type: Number, 25 | default: 0 26 | }, 27 | created: { 28 | type: Date, 29 | default: Date.now 30 | } 31 | }); 32 | 33 | var User = mongoose.model('User', userSchema); 34 | var Comment; 35 | module.exports = Comment = mongoose.model('Comment', commentSchema); 36 | -------------------------------------------------------------------------------- /models/Issue.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'), Schema = mongoose.Schema; 2 | 3 | var issueSchema = new mongoose.Schema({ 4 | number: { 5 | type: Number, 6 | required: true, 7 | unique: true 8 | }, 9 | poster: { 10 | type: Schema.Types.ObjectId 11 | }, 12 | vote_count: { 13 | type: Number, 14 | default: 0 15 | } 16 | }); 17 | 18 | var Issue; 19 | module.exports = Issue = mongoose.model('Issue', issueSchema); 20 | -------------------------------------------------------------------------------- /models/NewsItem.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'), Schema = mongoose.Schema; 2 | var userSchema = require('./User.js'); 3 | 4 | var newsItemSchema = new mongoose.Schema({ 5 | title: { type: String, default: '' }, 6 | url: { type: String, unique: true }, 7 | summary: { type: String }, 8 | source: { type: String }, 9 | poster: { type: String, ref: 'User' }, 10 | vote_count: {type: Number, default: 0 }, 11 | created: { type: Date, default: Date.now } 12 | }); 13 | 14 | newsItemSchema.methods.isSelfPost = function() { 15 | return this.url === '/news/' + this._id; 16 | }; 17 | 18 | /** 19 | * Returns a properly formatted URL in case it is missing information, i.e. protocol 20 | * @param {string} url URL to format 21 | * @returns {string} url Formatted URL 22 | */ 23 | newsItemSchema.statics.formatUrl = function(url) { 24 | return /^(http:\/\/|https:\/\/)/.test(url) ? url : 'http://' + url; 25 | }; 26 | 27 | // Logic to be executed before a model is saved to Mongo 28 | newsItemSchema.pre('save', function (next) { 29 | if(!this.isSelfPost()) { 30 | this.url = NewsItem.formatUrl(this.url); 31 | } 32 | 33 | next(); 34 | }); 35 | 36 | var User = mongoose.model('User', userSchema); 37 | 38 | var NewsItem; 39 | module.exports = NewsItem = mongoose.model('NewsItem', newsItemSchema); 40 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var bcrypt = require('bcrypt-nodejs'); 3 | 4 | var userSchema = new mongoose.Schema({ 5 | username: { type: String, unique: true }, // = github username 6 | email: { type: String, default:'' }, 7 | password: String, 8 | 9 | facebook: { type: String, unique: true, sparse: true }, 10 | twitter: { type: String, unique: true, sparse: true }, 11 | google: { type: String, unique: true, sparse: true }, 12 | github: { type: String, unique: true, sparse: true }, 13 | tokens: [ { kind: String, accessToken: String, tokenSecret: String } ], 14 | 15 | profile: { 16 | name: { type: String, default: '' }, 17 | location: { type: String, default: '' }, 18 | website: { type: String, default: '' }, 19 | picture: { type: String, default: '' }, 20 | description: { type: String, default: '' }, 21 | bio: { type: String, default: '' } 22 | } 23 | }); 24 | 25 | /** 26 | * Hash the password for security. 27 | */ 28 | 29 | userSchema.pre('save', function(next) { 30 | var user = this; 31 | var SALT_FACTOR = 5; 32 | 33 | if (!user.isModified('password')) return next(); 34 | 35 | bcrypt.genSalt(SALT_FACTOR, function(err, salt) { 36 | if (err) return next(err); 37 | 38 | bcrypt.hash(user.password, salt, null, function(err, hash) { 39 | if (err) return next(err); 40 | user.password = hash; 41 | next(); 42 | }); 43 | }); 44 | }); 45 | 46 | userSchema.methods.comparePassword = function(candidatePassword, cb) { 47 | bcrypt.compare(candidatePassword, this.password, function(err, isMatch) { 48 | if(err) return cb(err); 49 | cb(null, isMatch); 50 | }); 51 | }; 52 | 53 | module.exports = mongoose.model('User', userSchema); 54 | -------------------------------------------------------------------------------- /models/Vote.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'), 2 | Schema = mongoose.Schema, 3 | 4 | Comment = require('./Comment.js'), 5 | Issue = require('./Issue.js'), 6 | NewsItem = require('./NewsItem.js'); 7 | 8 | 9 | var voteSchema = new mongoose.Schema({ 10 | item: { 11 | type: Schema.Types.ObjectId, 12 | required: true 13 | }, 14 | itemType: { 15 | type: String, 16 | required: true, 17 | enum: ['news', 'comment', 'issue'], 18 | default: 'news' 19 | }, 20 | voter: { 21 | type: Schema.Types.ObjectId, 22 | ref: 'User', 23 | required: true 24 | }, 25 | amount: { 26 | type: Number, 27 | default: 1, 28 | required: true, 29 | min: 1, 30 | max: 1 31 | } 32 | }); 33 | 34 | // each user can only vote on an item once 35 | voteSchema.index({ item: 1, voter: 1 }, { unique: true }); 36 | 37 | // increment item vote count on save. 38 | voteSchema.pre('save', function(next) { 39 | 40 | var initem = this; 41 | 42 | // ensure this isn't a duplicate vote, as this happens before the 43 | // unique index is enforced for the new vote. 44 | Vote.findOne({item: initem.item, voter: initem.voter}, function(err, dup) { 45 | if ( dup === null) { 46 | incrementVote(initem.item, initem.itemType, 1, next); 47 | } else { 48 | next(); 49 | } 50 | }); 51 | 52 | }); 53 | 54 | // decrement item vote count on remove. 55 | voteSchema.pre('remove', function(next) { 56 | incrementVote(this.item, this.itemType, -1, next); 57 | }); 58 | 59 | // increment/decrement the aggregated vote count by amount. 60 | function incrementVote(itemId, type, amount, next) { 61 | 62 | var item; 63 | 64 | if (type === 'issue') { 65 | item = Issue; 66 | } else if (type === 'comment') { 67 | item = Comment; 68 | } else { 69 | item = NewsItem; 70 | } 71 | 72 | 73 | item.findById(itemId, function(err, item){ 74 | 75 | if (err) { next(err); } 76 | 77 | item.vote_count += amount; 78 | item.save(function(err) { 79 | 80 | if (err) { 81 | next(err); 82 | } else { 83 | next(); 84 | } 85 | }); 86 | }); 87 | 88 | } 89 | 90 | var Vote; 91 | module.exports = Vote = mongoose.model('Vote', voteSchema); 92 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pullup", 3 | "version": "0.0.0", 4 | "description": "A website you join via pull request.", 5 | "license": { 6 | "type": "MIT", 7 | "url": "https://github.com/larvalabs/pullup/blob/master/LICENSE" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/larvalabs/pullup.git" 12 | }, 13 | "scripts": { 14 | "test": "gulp test" 15 | }, 16 | "dependencies": { 17 | "async": "~0.2.10", 18 | "bcrypt-nodejs": "~0.0.3", 19 | "cheerio": "~0.13.1", 20 | "express": "~3.4.8", 21 | "express-flash": "~0.0.2", 22 | "express-validator": "~1.0.1", 23 | "forever-monitor": "~1.2", 24 | "github": "~0.1", 25 | "jade": "~1.1.5", 26 | "less-middleware": "~0.1.15", 27 | "mongoose": "~3.8.5", 28 | "node-cache": "~0.4.0", 29 | "octonode": "~0.5.0", 30 | "passport": "~0.2.0", 31 | "passport-facebook": "~1.0.2", 32 | "passport-github": "alexgorbatchev/passport-github#9fef321dc7237c977b81fbf4a4f9589fc8ae9aaf", 33 | "passport-google-oauth": "~0.1.5", 34 | "passport-local": "~0.1.6", 35 | "passport-oauth": "~1.0.0", 36 | "passport-twitter": "~1.0.2", 37 | "request": "~2.33.0", 38 | "sendgrid": "~0.4.6", 39 | "underscore": "~1.5.2", 40 | "connect-mongo": "~0.4.0", 41 | "optimist": "latest", 42 | "timeago": "~0.1.0", 43 | "xmldom": "~0.1.19", 44 | "marked": "~0.3.1" 45 | }, 46 | "devDependencies": { 47 | "gulp": "~3.5.2", 48 | "gulp-util": "~2.2.14", 49 | "gulp-jshint": "~1.4.0", 50 | "gulp-mocha": "~0.4.1", 51 | "mocha": "~1.17.1", 52 | "should": "~3.1.2", 53 | "marked": "~0.3.1", 54 | "grunt": "^0.4.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/alerts.less: -------------------------------------------------------------------------------- 1 | // 2 | // Alerts 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // ------------------------- 8 | 9 | .alert { 10 | padding: @alert-padding; 11 | margin-bottom: @line-height-computed; 12 | border: 1px solid transparent; 13 | border-radius: @alert-border-radius; 14 | 15 | // Headings for larger alerts 16 | h4 { 17 | margin-top: 0; 18 | // Specified for the h4 to prevent conflicts of changing @headings-color 19 | color: inherit; 20 | } 21 | // Provide class for links that match alerts 22 | .alert-link { 23 | font-weight: @alert-link-font-weight; 24 | } 25 | 26 | // Improve alignment and spacing of inner content 27 | > p, 28 | > ul { 29 | margin-bottom: 0; 30 | } 31 | > p + p { 32 | margin-top: 5px; 33 | } 34 | } 35 | 36 | // Dismissable alerts 37 | // 38 | // Expand the right padding and account for the close button's positioning. 39 | 40 | .alert-dismissable { 41 | padding-right: (@alert-padding + 20); 42 | 43 | // Adjust close link position 44 | .close { 45 | position: relative; 46 | top: -2px; 47 | right: -21px; 48 | color: inherit; 49 | } 50 | } 51 | 52 | // Alternate styles 53 | // 54 | // Generate contextual modifier classes for colorizing the alert. 55 | 56 | .alert-success { 57 | .alert-variant(@alert-success-bg; @alert-success-border; @alert-success-text); 58 | } 59 | .alert-info { 60 | .alert-variant(@alert-info-bg; @alert-info-border; @alert-info-text); 61 | } 62 | .alert-warning { 63 | .alert-variant(@alert-warning-bg; @alert-warning-border; @alert-warning-text); 64 | } 65 | .alert-danger { 66 | .alert-variant(@alert-danger-bg; @alert-danger-border; @alert-danger-text); 67 | } 68 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/badges.less: -------------------------------------------------------------------------------- 1 | // 2 | // Badges 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base classes 7 | .badge { 8 | display: inline-block; 9 | min-width: 10px; 10 | padding: 3px 7px; 11 | font-size: @font-size-small; 12 | font-weight: @badge-font-weight; 13 | color: @badge-color; 14 | line-height: @badge-line-height; 15 | vertical-align: baseline; 16 | white-space: nowrap; 17 | text-align: center; 18 | background-color: @badge-bg; 19 | border-radius: @badge-border-radius; 20 | 21 | // Empty badges collapse automatically (not available in IE8) 22 | &:empty { 23 | display: none; 24 | } 25 | 26 | // Quick fix for badges in buttons 27 | .btn & { 28 | position: relative; 29 | top: -1px; 30 | } 31 | .btn-xs & { 32 | top: 0; 33 | padding: 1px 5px; 34 | } 35 | } 36 | 37 | // Hover state, but only for links 38 | a.badge { 39 | &:hover, 40 | &:focus { 41 | color: @badge-link-hover-color; 42 | text-decoration: none; 43 | cursor: pointer; 44 | } 45 | } 46 | 47 | // Account for counters in navs 48 | a.list-group-item.active > .badge, 49 | .nav-pills > .active > a > .badge { 50 | color: @badge-active-color; 51 | background-color: @badge-active-bg; 52 | } 53 | .nav-pills > li > a > .badge { 54 | margin-left: 3px; 55 | } 56 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/bootstrap.less: -------------------------------------------------------------------------------- 1 | // Core variables and mixins 2 | @import "variables.less"; 3 | @import "mixins.less"; 4 | 5 | // Reset 6 | @import "normalize.less"; 7 | @import "print.less"; 8 | 9 | // Core CSS 10 | @import "scaffolding.less"; 11 | @import "type.less"; 12 | @import "code.less"; 13 | @import "grid.less"; 14 | @import "tables.less"; 15 | @import "forms.less"; 16 | @import "buttons.less"; 17 | 18 | // Components 19 | @import "component-animations.less"; 20 | @import "glyphicons.less"; 21 | @import "dropdowns.less"; 22 | @import "button-groups.less"; 23 | @import "input-groups.less"; 24 | @import "navs.less"; 25 | @import "navbar.less"; 26 | @import "breadcrumbs.less"; 27 | @import "pagination.less"; 28 | @import "pager.less"; 29 | @import "labels.less"; 30 | @import "badges.less"; 31 | @import "jumbotron.less"; 32 | @import "thumbnails.less"; 33 | @import "alerts.less"; 34 | @import "progress-bars.less"; 35 | @import "media.less"; 36 | @import "list-group.less"; 37 | @import "panels.less"; 38 | @import "wells.less"; 39 | @import "close.less"; 40 | 41 | // Components w/ JavaScript 42 | @import "modals.less"; 43 | @import "tooltip.less"; 44 | @import "popovers.less"; 45 | @import "carousel.less"; 46 | 47 | // Utility classes 48 | @import "utilities.less"; 49 | @import "responsive-utilities.less"; 50 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/breadcrumbs.less: -------------------------------------------------------------------------------- 1 | // 2 | // Breadcrumbs 3 | // -------------------------------------------------- 4 | 5 | 6 | .breadcrumb { 7 | padding: @breadcrumb-padding-vertical @breadcrumb-padding-horizontal; 8 | margin-bottom: @line-height-computed; 9 | list-style: none; 10 | background-color: @breadcrumb-bg; 11 | border-radius: @border-radius-base; 12 | 13 | > li { 14 | display: inline-block; 15 | 16 | + li:before { 17 | content: "@{breadcrumb-separator}\00a0"; // Unicode space added since inline-block means non-collapsing white-space 18 | padding: 0 5px; 19 | color: @breadcrumb-color; 20 | } 21 | } 22 | 23 | > .active { 24 | color: @breadcrumb-active-color; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/button-groups.less: -------------------------------------------------------------------------------- 1 | // 2 | // Button groups 3 | // -------------------------------------------------- 4 | 5 | // Make the div behave like a button 6 | .btn-group, 7 | .btn-group-vertical { 8 | position: relative; 9 | display: inline-block; 10 | vertical-align: middle; // match .btn alignment given font-size hack above 11 | > .btn { 12 | position: relative; 13 | float: left; 14 | // Bring the "active" button to the front 15 | &:hover, 16 | &:focus, 17 | &:active, 18 | &.active { 19 | z-index: 2; 20 | } 21 | &:focus { 22 | // Remove focus outline when dropdown JS adds it after closing the menu 23 | outline: none; 24 | } 25 | } 26 | } 27 | 28 | // Prevent double borders when buttons are next to each other 29 | .btn-group { 30 | .btn + .btn, 31 | .btn + .btn-group, 32 | .btn-group + .btn, 33 | .btn-group + .btn-group { 34 | margin-left: -1px; 35 | } 36 | } 37 | 38 | // Optional: Group multiple button groups together for a toolbar 39 | .btn-toolbar { 40 | margin-left: -5px; // Offset the first child's margin 41 | &:extend(.clearfix all); 42 | 43 | .btn-group, 44 | .input-group { 45 | float: left; 46 | } 47 | > .btn, 48 | > .btn-group, 49 | > .input-group { 50 | margin-left: 5px; 51 | } 52 | } 53 | 54 | .btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { 55 | border-radius: 0; 56 | } 57 | 58 | // Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match 59 | .btn-group > .btn:first-child { 60 | margin-left: 0; 61 | &:not(:last-child):not(.dropdown-toggle) { 62 | .border-right-radius(0); 63 | } 64 | } 65 | // Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it 66 | .btn-group > .btn:last-child:not(:first-child), 67 | .btn-group > .dropdown-toggle:not(:first-child) { 68 | .border-left-radius(0); 69 | } 70 | 71 | // Custom edits for including btn-groups within btn-groups (useful for including dropdown buttons within a btn-group) 72 | .btn-group > .btn-group { 73 | float: left; 74 | } 75 | .btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { 76 | border-radius: 0; 77 | } 78 | .btn-group > .btn-group:first-child { 79 | > .btn:last-child, 80 | > .dropdown-toggle { 81 | .border-right-radius(0); 82 | } 83 | } 84 | .btn-group > .btn-group:last-child > .btn:first-child { 85 | .border-left-radius(0); 86 | } 87 | 88 | // On active and open, don't show outline 89 | .btn-group .dropdown-toggle:active, 90 | .btn-group.open .dropdown-toggle { 91 | outline: 0; 92 | } 93 | 94 | 95 | // Sizing 96 | // 97 | // Remix the default button sizing classes into new ones for easier manipulation. 98 | 99 | .btn-group-xs > .btn { .btn-xs(); } 100 | .btn-group-sm > .btn { .btn-sm(); } 101 | .btn-group-lg > .btn { .btn-lg(); } 102 | 103 | 104 | // Split button dropdowns 105 | // ---------------------- 106 | 107 | // Give the line between buttons some depth 108 | .btn-group > .btn + .dropdown-toggle { 109 | padding-left: 8px; 110 | padding-right: 8px; 111 | } 112 | .btn-group > .btn-lg + .dropdown-toggle { 113 | padding-left: 12px; 114 | padding-right: 12px; 115 | } 116 | 117 | // The clickable button for toggling the menu 118 | // Remove the gradient and set the same inset shadow as the :active state 119 | .btn-group.open .dropdown-toggle { 120 | .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); 121 | 122 | // Show no shadow for `.btn-link` since it has no other button styles. 123 | &.btn-link { 124 | .box-shadow(none); 125 | } 126 | } 127 | 128 | 129 | // Reposition the caret 130 | .btn .caret { 131 | margin-left: 0; 132 | } 133 | // Carets in other button sizes 134 | .btn-lg .caret { 135 | border-width: @caret-width-large @caret-width-large 0; 136 | border-bottom-width: 0; 137 | } 138 | // Upside down carets for .dropup 139 | .dropup .btn-lg .caret { 140 | border-width: 0 @caret-width-large @caret-width-large; 141 | } 142 | 143 | 144 | // Vertical button groups 145 | // ---------------------- 146 | 147 | .btn-group-vertical { 148 | > .btn, 149 | > .btn-group, 150 | > .btn-group > .btn { 151 | display: block; 152 | float: none; 153 | width: 100%; 154 | max-width: 100%; 155 | } 156 | 157 | // Clear floats so dropdown menus can be properly placed 158 | > .btn-group { 159 | &:extend(.clearfix all); 160 | > .btn { 161 | float: none; 162 | } 163 | } 164 | 165 | > .btn + .btn, 166 | > .btn + .btn-group, 167 | > .btn-group + .btn, 168 | > .btn-group + .btn-group { 169 | margin-top: -1px; 170 | margin-left: 0; 171 | } 172 | } 173 | 174 | .btn-group-vertical > .btn { 175 | &:not(:first-child):not(:last-child) { 176 | border-radius: 0; 177 | } 178 | &:first-child:not(:last-child) { 179 | border-top-right-radius: @border-radius-base; 180 | .border-bottom-radius(0); 181 | } 182 | &:last-child:not(:first-child) { 183 | border-bottom-left-radius: @border-radius-base; 184 | .border-top-radius(0); 185 | } 186 | } 187 | .btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { 188 | border-radius: 0; 189 | } 190 | .btn-group-vertical > .btn-group:first-child:not(:last-child) { 191 | > .btn:last-child, 192 | > .dropdown-toggle { 193 | .border-bottom-radius(0); 194 | } 195 | } 196 | .btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { 197 | .border-top-radius(0); 198 | } 199 | 200 | 201 | 202 | // Justified button groups 203 | // ---------------------- 204 | 205 | .btn-group-justified { 206 | display: table; 207 | width: 100%; 208 | table-layout: fixed; 209 | border-collapse: separate; 210 | > .btn, 211 | > .btn-group { 212 | float: none; 213 | display: table-cell; 214 | width: 1%; 215 | } 216 | > .btn-group .btn { 217 | width: 100%; 218 | } 219 | } 220 | 221 | 222 | // Checkbox and radio options 223 | [data-toggle="buttons"] > .btn > input[type="radio"], 224 | [data-toggle="buttons"] > .btn > input[type="checkbox"] { 225 | display: none; 226 | } 227 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/buttons.less: -------------------------------------------------------------------------------- 1 | // 2 | // Buttons 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base styles 7 | // -------------------------------------------------- 8 | 9 | .btn { 10 | display: inline-block; 11 | margin-bottom: 0; // For input.btn 12 | font-weight: @btn-font-weight; 13 | text-align: center; 14 | vertical-align: middle; 15 | cursor: pointer; 16 | background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214 17 | border: 1px solid transparent; 18 | white-space: nowrap; 19 | .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @border-radius-base); 20 | .user-select(none); 21 | 22 | &:focus { 23 | .tab-focus(); 24 | } 25 | 26 | &:hover, 27 | &:focus { 28 | color: @btn-default-color; 29 | text-decoration: none; 30 | } 31 | 32 | &:active, 33 | &.active { 34 | outline: 0; 35 | background-image: none; 36 | .box-shadow(inset 0 3px 5px rgba(0,0,0,.125)); 37 | } 38 | 39 | &.disabled, 40 | &[disabled], 41 | fieldset[disabled] & { 42 | cursor: not-allowed; 43 | pointer-events: none; // Future-proof disabling of clicks 44 | .opacity(.65); 45 | .box-shadow(none); 46 | } 47 | } 48 | 49 | 50 | // Alternate buttons 51 | // -------------------------------------------------- 52 | 53 | .btn-default { 54 | .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border); 55 | } 56 | .btn-primary { 57 | .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border); 58 | } 59 | // Success appears as green 60 | .btn-success { 61 | .button-variant(@btn-success-color; @btn-success-bg; @btn-success-border); 62 | } 63 | // Info appears as blue-green 64 | .btn-info { 65 | .button-variant(@btn-info-color; @btn-info-bg; @btn-info-border); 66 | } 67 | // Warning appears as orange 68 | .btn-warning { 69 | .button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border); 70 | } 71 | // Danger and error appear as red 72 | .btn-danger { 73 | .button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border); 74 | } 75 | 76 | 77 | // Link buttons 78 | // ------------------------- 79 | 80 | // Make a button look and behave like a link 81 | .btn-link { 82 | color: @link-color; 83 | font-weight: normal; 84 | cursor: pointer; 85 | border-radius: 0; 86 | 87 | &, 88 | &:active, 89 | &[disabled], 90 | fieldset[disabled] & { 91 | background-color: transparent; 92 | .box-shadow(none); 93 | } 94 | &, 95 | &:hover, 96 | &:focus, 97 | &:active { 98 | border-color: transparent; 99 | } 100 | &:hover, 101 | &:focus { 102 | color: @link-hover-color; 103 | text-decoration: underline; 104 | background-color: transparent; 105 | } 106 | &[disabled], 107 | fieldset[disabled] & { 108 | &:hover, 109 | &:focus { 110 | color: @btn-link-disabled-color; 111 | text-decoration: none; 112 | } 113 | } 114 | } 115 | 116 | 117 | // Button Sizes 118 | // -------------------------------------------------- 119 | 120 | .btn-lg { 121 | // line-height: ensure even-numbered height of button next to large input 122 | .button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large); 123 | } 124 | .btn-sm { 125 | // line-height: ensure proper height of button next to small input 126 | .button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small); 127 | } 128 | .btn-xs { 129 | .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @border-radius-small); 130 | } 131 | 132 | 133 | // Block button 134 | // -------------------------------------------------- 135 | 136 | .btn-block { 137 | display: block; 138 | width: 100%; 139 | padding-left: 0; 140 | padding-right: 0; 141 | } 142 | 143 | // Vertically space out multiple block buttons 144 | .btn-block + .btn-block { 145 | margin-top: 5px; 146 | } 147 | 148 | // Specificity overrides 149 | input[type="submit"], 150 | input[type="reset"], 151 | input[type="button"] { 152 | &.btn-block { 153 | width: 100%; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/carousel.less: -------------------------------------------------------------------------------- 1 | // 2 | // Carousel 3 | // -------------------------------------------------- 4 | 5 | 6 | // Wrapper for the slide container and indicators 7 | .carousel { 8 | position: relative; 9 | } 10 | 11 | .carousel-inner { 12 | position: relative; 13 | overflow: hidden; 14 | width: 100%; 15 | 16 | > .item { 17 | display: none; 18 | position: relative; 19 | .transition(.6s ease-in-out left); 20 | 21 | // Account for jankitude on images 22 | > img, 23 | > a > img { 24 | .img-responsive(); 25 | line-height: 1; 26 | } 27 | } 28 | 29 | > .active, 30 | > .next, 31 | > .prev { display: block; } 32 | 33 | > .active { 34 | left: 0; 35 | } 36 | 37 | > .next, 38 | > .prev { 39 | position: absolute; 40 | top: 0; 41 | width: 100%; 42 | } 43 | 44 | > .next { 45 | left: 100%; 46 | } 47 | > .prev { 48 | left: -100%; 49 | } 50 | > .next.left, 51 | > .prev.right { 52 | left: 0; 53 | } 54 | 55 | > .active.left { 56 | left: -100%; 57 | } 58 | > .active.right { 59 | left: 100%; 60 | } 61 | 62 | } 63 | 64 | // Left/right controls for nav 65 | // --------------------------- 66 | 67 | .carousel-control { 68 | position: absolute; 69 | top: 0; 70 | left: 0; 71 | bottom: 0; 72 | width: @carousel-control-width; 73 | .opacity(@carousel-control-opacity); 74 | font-size: @carousel-control-font-size; 75 | color: @carousel-control-color; 76 | text-align: center; 77 | text-shadow: @carousel-text-shadow; 78 | // We can't have this transition here because WebKit cancels the carousel 79 | // animation if you trip this while in the middle of another animation. 80 | 81 | // Set gradients for backgrounds 82 | &.left { 83 | #gradient > .horizontal(@start-color: rgba(0,0,0,.5); @end-color: rgba(0,0,0,.0001)); 84 | } 85 | &.right { 86 | left: auto; 87 | right: 0; 88 | #gradient > .horizontal(@start-color: rgba(0,0,0,.0001); @end-color: rgba(0,0,0,.5)); 89 | } 90 | 91 | // Hover/focus state 92 | &:hover, 93 | &:focus { 94 | outline: none; 95 | color: @carousel-control-color; 96 | text-decoration: none; 97 | .opacity(.9); 98 | } 99 | 100 | // Toggles 101 | .icon-prev, 102 | .icon-next, 103 | .glyphicon-chevron-left, 104 | .glyphicon-chevron-right { 105 | position: absolute; 106 | top: 50%; 107 | z-index: 5; 108 | display: inline-block; 109 | } 110 | .icon-prev, 111 | .glyphicon-chevron-left { 112 | left: 50%; 113 | } 114 | .icon-next, 115 | .glyphicon-chevron-right { 116 | right: 50%; 117 | } 118 | .icon-prev, 119 | .icon-next { 120 | width: 20px; 121 | height: 20px; 122 | margin-top: -10px; 123 | margin-left: -10px; 124 | font-family: serif; 125 | } 126 | 127 | .icon-prev { 128 | &:before { 129 | content: '\2039';// SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039) 130 | } 131 | } 132 | .icon-next { 133 | &:before { 134 | content: '\203a';// SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A) 135 | } 136 | } 137 | } 138 | 139 | // Optional indicator pips 140 | // 141 | // Add an unordered list with the following class and add a list item for each 142 | // slide your carousel holds. 143 | 144 | .carousel-indicators { 145 | position: absolute; 146 | bottom: 10px; 147 | left: 50%; 148 | z-index: 15; 149 | width: 60%; 150 | margin-left: -30%; 151 | padding-left: 0; 152 | list-style: none; 153 | text-align: center; 154 | 155 | li { 156 | display: inline-block; 157 | width: 10px; 158 | height: 10px; 159 | margin: 1px; 160 | text-indent: -999px; 161 | border: 1px solid @carousel-indicator-border-color; 162 | border-radius: 10px; 163 | cursor: pointer; 164 | 165 | // IE8-9 hack for event handling 166 | // 167 | // Internet Explorer 8-9 does not support clicks on elements without a set 168 | // `background-color`. We cannot use `filter` since that's not viewed as a 169 | // background color by the browser. Thus, a hack is needed. 170 | // 171 | // For IE8, we set solid black as it doesn't support `rgba()`. For IE9, we 172 | // set alpha transparency for the best results possible. 173 | background-color: #000 \9; // IE8 174 | background-color: rgba(0,0,0,0); // IE9 175 | } 176 | .active { 177 | margin: 0; 178 | width: 12px; 179 | height: 12px; 180 | background-color: @carousel-indicator-active-bg; 181 | } 182 | } 183 | 184 | // Optional captions 185 | // ----------------------------- 186 | // Hidden by default for smaller viewports 187 | .carousel-caption { 188 | position: absolute; 189 | left: 15%; 190 | right: 15%; 191 | bottom: 20px; 192 | z-index: 10; 193 | padding-top: 20px; 194 | padding-bottom: 20px; 195 | color: @carousel-caption-color; 196 | text-align: center; 197 | text-shadow: @carousel-text-shadow; 198 | & .btn { 199 | text-shadow: none; // No shadow for button elements in carousel-caption 200 | } 201 | } 202 | 203 | 204 | // Scale up controls for tablets and up 205 | @media screen and (min-width: @screen-sm-min) { 206 | 207 | // Scale up the controls a smidge 208 | .carousel-control { 209 | .glyphicons-chevron-left, 210 | .glyphicons-chevron-right, 211 | .icon-prev, 212 | .icon-next { 213 | width: 30px; 214 | height: 30px; 215 | margin-top: -15px; 216 | margin-left: -15px; 217 | font-size: 30px; 218 | } 219 | } 220 | 221 | // Show and left align the captions 222 | .carousel-caption { 223 | left: 20%; 224 | right: 20%; 225 | padding-bottom: 30px; 226 | } 227 | 228 | // Move up the indicators 229 | .carousel-indicators { 230 | bottom: 20px; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/close.less: -------------------------------------------------------------------------------- 1 | // 2 | // Close icons 3 | // -------------------------------------------------- 4 | 5 | 6 | .close { 7 | float: right; 8 | font-size: (@font-size-base * 1.5); 9 | font-weight: @close-font-weight; 10 | line-height: 1; 11 | color: @close-color; 12 | text-shadow: @close-text-shadow; 13 | .opacity(.2); 14 | 15 | &:hover, 16 | &:focus { 17 | color: @close-color; 18 | text-decoration: none; 19 | cursor: pointer; 20 | .opacity(.5); 21 | } 22 | 23 | // Additional properties for button version 24 | // iOS requires the button element instead of an anchor tag. 25 | // If you want the anchor version, it requires `href="#"`. 26 | button& { 27 | padding: 0; 28 | cursor: pointer; 29 | background: transparent; 30 | border: 0; 31 | -webkit-appearance: none; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/code.less: -------------------------------------------------------------------------------- 1 | // 2 | // Code (inline and block) 3 | // -------------------------------------------------- 4 | 5 | 6 | // Inline and block code styles 7 | code, 8 | kbd, 9 | pre, 10 | samp { 11 | font-family: @font-family-monospace; 12 | } 13 | 14 | // Inline code 15 | code { 16 | padding: 2px 4px; 17 | font-size: 90%; 18 | color: @code-color; 19 | background-color: @code-bg; 20 | white-space: nowrap; 21 | border-radius: @border-radius-base; 22 | } 23 | 24 | // User input typically entered via keyboard 25 | kbd { 26 | padding: 2px 4px; 27 | font-size: 90%; 28 | color: @kbd-color; 29 | background-color: @kbd-bg; 30 | border-radius: @border-radius-small; 31 | box-shadow: inset 0 -1px 0 rgba(0,0,0,.25); 32 | } 33 | 34 | // Blocks of code 35 | pre { 36 | display: block; 37 | padding: ((@line-height-computed - 1) / 2); 38 | margin: 0 0 (@line-height-computed / 2); 39 | font-size: (@font-size-base - 1); // 14px to 13px 40 | line-height: @line-height-base; 41 | word-break: break-all; 42 | word-wrap: break-word; 43 | color: @pre-color; 44 | background-color: @pre-bg; 45 | border: 1px solid @pre-border-color; 46 | border-radius: @border-radius-base; 47 | 48 | // Account for some code outputs that place code tags in pre tags 49 | code { 50 | padding: 0; 51 | font-size: inherit; 52 | color: inherit; 53 | white-space: pre-wrap; 54 | background-color: transparent; 55 | border-radius: 0; 56 | } 57 | } 58 | 59 | // Enable scrollable blocks of code 60 | .pre-scrollable { 61 | max-height: @pre-scrollable-max-height; 62 | overflow-y: scroll; 63 | } 64 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/component-animations.less: -------------------------------------------------------------------------------- 1 | // 2 | // Component animations 3 | // -------------------------------------------------- 4 | 5 | // Heads up! 6 | // 7 | // We don't use the `.opacity()` mixin here since it causes a bug with text 8 | // fields in IE7-8. Source: https://github.com/twitter/bootstrap/pull/3552. 9 | 10 | .fade { 11 | opacity: 0; 12 | .transition(opacity .15s linear); 13 | &.in { 14 | opacity: 1; 15 | } 16 | } 17 | 18 | .collapse { 19 | display: none; 20 | &.in { 21 | display: block; 22 | } 23 | } 24 | .collapsing { 25 | position: relative; 26 | height: 0; 27 | overflow: hidden; 28 | .transition(height .35s ease); 29 | } 30 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/dropdowns.less: -------------------------------------------------------------------------------- 1 | // 2 | // Dropdown menus 3 | // -------------------------------------------------- 4 | 5 | 6 | // Dropdown arrow/caret 7 | .caret { 8 | display: inline-block; 9 | width: 0; 10 | height: 0; 11 | margin-left: 2px; 12 | vertical-align: middle; 13 | border-top: @caret-width-base solid; 14 | border-right: @caret-width-base solid transparent; 15 | border-left: @caret-width-base solid transparent; 16 | } 17 | 18 | // The dropdown wrapper (div) 19 | .dropdown { 20 | position: relative; 21 | } 22 | 23 | // Prevent the focus on the dropdown toggle when closing dropdowns 24 | .dropdown-toggle:focus { 25 | outline: 0; 26 | } 27 | 28 | // The dropdown menu (ul) 29 | .dropdown-menu { 30 | position: absolute; 31 | top: 100%; 32 | left: 0; 33 | z-index: @zindex-dropdown; 34 | display: none; // none by default, but block on "open" of the menu 35 | float: left; 36 | min-width: 160px; 37 | padding: 5px 0; 38 | margin: 2px 0 0; // override default ul 39 | list-style: none; 40 | font-size: @font-size-base; 41 | background-color: @dropdown-bg; 42 | border: 1px solid @dropdown-fallback-border; // IE8 fallback 43 | border: 1px solid @dropdown-border; 44 | border-radius: @border-radius-base; 45 | .box-shadow(0 6px 12px rgba(0,0,0,.175)); 46 | background-clip: padding-box; 47 | 48 | // Aligns the dropdown menu to right 49 | // 50 | // Deprecated as of 3.1.0 in favor of `.dropdown-menu-[dir]` 51 | &.pull-right { 52 | right: 0; 53 | left: auto; 54 | } 55 | 56 | // Dividers (basically an hr) within the dropdown 57 | .divider { 58 | .nav-divider(@dropdown-divider-bg); 59 | } 60 | 61 | // Links within the dropdown menu 62 | > li > a { 63 | display: block; 64 | padding: 3px 20px; 65 | clear: both; 66 | font-weight: normal; 67 | line-height: @line-height-base; 68 | color: @dropdown-link-color; 69 | white-space: nowrap; // prevent links from randomly breaking onto new lines 70 | } 71 | } 72 | 73 | // Hover/Focus state 74 | .dropdown-menu > li > a { 75 | &:hover, 76 | &:focus { 77 | text-decoration: none; 78 | color: @dropdown-link-hover-color; 79 | background-color: @dropdown-link-hover-bg; 80 | } 81 | } 82 | 83 | // Active state 84 | .dropdown-menu > .active > a { 85 | &, 86 | &:hover, 87 | &:focus { 88 | color: @dropdown-link-active-color; 89 | text-decoration: none; 90 | outline: 0; 91 | background-color: @dropdown-link-active-bg; 92 | } 93 | } 94 | 95 | // Disabled state 96 | // 97 | // Gray out text and ensure the hover/focus state remains gray 98 | 99 | .dropdown-menu > .disabled > a { 100 | &, 101 | &:hover, 102 | &:focus { 103 | color: @dropdown-link-disabled-color; 104 | } 105 | } 106 | // Nuke hover/focus effects 107 | .dropdown-menu > .disabled > a { 108 | &:hover, 109 | &:focus { 110 | text-decoration: none; 111 | background-color: transparent; 112 | background-image: none; // Remove CSS gradient 113 | .reset-filter(); 114 | cursor: not-allowed; 115 | } 116 | } 117 | 118 | // Open state for the dropdown 119 | .open { 120 | // Show the menu 121 | > .dropdown-menu { 122 | display: block; 123 | } 124 | 125 | // Remove the outline when :focus is triggered 126 | > a { 127 | outline: 0; 128 | } 129 | } 130 | 131 | // Menu positioning 132 | // 133 | // Add extra class to `.dropdown-menu` to flip the alignment of the dropdown 134 | // menu with the parent. 135 | .dropdown-menu-right { 136 | left: auto; // Reset the default from `.dropdown-menu` 137 | right: 0; 138 | } 139 | // With v3, we enabled auto-flipping if you have a dropdown within a right 140 | // aligned nav component. To enable the undoing of that, we provide an override 141 | // to restore the default dropdown menu alignment. 142 | // 143 | // This is only for left-aligning a dropdown menu within a `.navbar-right` or 144 | // `.pull-right` nav component. 145 | .dropdown-menu-left { 146 | left: 0; 147 | right: auto; 148 | } 149 | 150 | // Dropdown section headers 151 | .dropdown-header { 152 | display: block; 153 | padding: 3px 20px; 154 | font-size: @font-size-small; 155 | line-height: @line-height-base; 156 | color: @dropdown-header-color; 157 | } 158 | 159 | // Backdrop to catch body clicks on mobile, etc. 160 | .dropdown-backdrop { 161 | position: fixed; 162 | left: 0; 163 | right: 0; 164 | bottom: 0; 165 | top: 0; 166 | z-index: (@zindex-dropdown - 10); 167 | } 168 | 169 | // Right aligned dropdowns 170 | .pull-right > .dropdown-menu { 171 | right: 0; 172 | left: auto; 173 | } 174 | 175 | // Allow for dropdowns to go bottom up (aka, dropup-menu) 176 | // 177 | // Just add .dropup after the standard .dropdown class and you're set, bro. 178 | // TODO: abstract this so that the navbar fixed styles are not placed here? 179 | 180 | .dropup, 181 | .navbar-fixed-bottom .dropdown { 182 | // Reverse the caret 183 | .caret { 184 | border-top: 0; 185 | border-bottom: @caret-width-base solid; 186 | content: ""; 187 | } 188 | // Different positioning for bottom up menu 189 | .dropdown-menu { 190 | top: auto; 191 | bottom: 100%; 192 | margin-bottom: 1px; 193 | } 194 | } 195 | 196 | 197 | // Component alignment 198 | // 199 | // Reiterate per navbar.less and the modified component alignment there. 200 | 201 | @media (min-width: @grid-float-breakpoint) { 202 | .navbar-right { 203 | .dropdown-menu { 204 | .dropdown-menu-right(); 205 | } 206 | // Necessary for overrides of the default right aligned menu. 207 | // Will remove come v4 in all likelihood. 208 | .dropdown-menu-left { 209 | .dropdown-menu-left(); 210 | } 211 | } 212 | } 213 | 214 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/grid.less: -------------------------------------------------------------------------------- 1 | // 2 | // Grid system 3 | // -------------------------------------------------- 4 | 5 | 6 | // Container widths 7 | // 8 | // Set the container width, and override it for fixed navbars in media queries. 9 | 10 | .container { 11 | .container-fixed(); 12 | 13 | @media (min-width: @screen-sm-min) { 14 | width: @container-sm; 15 | } 16 | @media (min-width: @screen-md-min) { 17 | width: @container-md; 18 | } 19 | @media (min-width: @screen-lg-min) { 20 | width: @container-lg; 21 | } 22 | } 23 | 24 | 25 | // Fluid container 26 | // 27 | // Utilizes the mixin meant for fixed width containers, but without any defined 28 | // width for fluid, full width layouts. 29 | 30 | .container-fluid { 31 | .container-fixed(); 32 | } 33 | 34 | 35 | // Row 36 | // 37 | // Rows contain and clear the floats of your columns. 38 | 39 | .row { 40 | .make-row(); 41 | } 42 | 43 | 44 | // Columns 45 | // 46 | // Common styles for small and large grid columns 47 | 48 | .make-grid-columns(); 49 | 50 | 51 | // Extra small grid 52 | // 53 | // Columns, offsets, pushes, and pulls for extra small devices like 54 | // smartphones. 55 | 56 | .make-grid-columns-float(xs); 57 | .make-grid(@grid-columns, xs, width); 58 | .make-grid(@grid-columns, xs, pull); 59 | .make-grid(@grid-columns, xs, push); 60 | .make-grid(@grid-columns, xs, offset); 61 | 62 | 63 | // Small grid 64 | // 65 | // Columns, offsets, pushes, and pulls for the small device range, from phones 66 | // to tablets. 67 | 68 | @media (min-width: @screen-sm-min) { 69 | .make-grid-columns-float(sm); 70 | .make-grid(@grid-columns, sm, width); 71 | .make-grid(@grid-columns, sm, pull); 72 | .make-grid(@grid-columns, sm, push); 73 | .make-grid(@grid-columns, sm, offset); 74 | } 75 | 76 | 77 | // Medium grid 78 | // 79 | // Columns, offsets, pushes, and pulls for the desktop device range. 80 | 81 | @media (min-width: @screen-md-min) { 82 | .make-grid-columns-float(md); 83 | .make-grid(@grid-columns, md, width); 84 | .make-grid(@grid-columns, md, pull); 85 | .make-grid(@grid-columns, md, push); 86 | .make-grid(@grid-columns, md, offset); 87 | } 88 | 89 | 90 | // Large grid 91 | // 92 | // Columns, offsets, pushes, and pulls for the large desktop device range. 93 | 94 | @media (min-width: @screen-lg-min) { 95 | .make-grid-columns-float(lg); 96 | .make-grid(@grid-columns, lg, width); 97 | .make-grid(@grid-columns, lg, pull); 98 | .make-grid(@grid-columns, lg, push); 99 | .make-grid(@grid-columns, lg, offset); 100 | } 101 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/input-groups.less: -------------------------------------------------------------------------------- 1 | // 2 | // Input groups 3 | // -------------------------------------------------- 4 | 5 | // Base styles 6 | // ------------------------- 7 | .input-group { 8 | position: relative; // For dropdowns 9 | display: table; 10 | border-collapse: separate; // prevent input groups from inheriting border styles from table cells when placed within a table 11 | 12 | // Undo padding and float of grid classes 13 | &[class*="col-"] { 14 | float: none; 15 | padding-left: 0; 16 | padding-right: 0; 17 | } 18 | 19 | .form-control { 20 | // IE9 fubars the placeholder attribute in text inputs and the arrows on 21 | // select elements in input groups. To fix it, we float the input. Details: 22 | // https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855 23 | float: left; 24 | 25 | width: 100%; 26 | margin-bottom: 0; 27 | } 28 | } 29 | 30 | // Sizing options 31 | // 32 | // Remix the default form control sizing classes into new ones for easier 33 | // manipulation. 34 | 35 | .input-group-lg > .form-control, 36 | .input-group-lg > .input-group-addon, 37 | .input-group-lg > .input-group-btn > .btn { .input-lg(); } 38 | .input-group-sm > .form-control, 39 | .input-group-sm > .input-group-addon, 40 | .input-group-sm > .input-group-btn > .btn { .input-sm(); } 41 | 42 | 43 | // Display as table-cell 44 | // ------------------------- 45 | .input-group-addon, 46 | .input-group-btn, 47 | .input-group .form-control { 48 | display: table-cell; 49 | 50 | &:not(:first-child):not(:last-child) { 51 | border-radius: 0; 52 | } 53 | } 54 | // Addon and addon wrapper for buttons 55 | .input-group-addon, 56 | .input-group-btn { 57 | width: 1%; 58 | white-space: nowrap; 59 | vertical-align: middle; // Match the inputs 60 | } 61 | 62 | // Text input groups 63 | // ------------------------- 64 | .input-group-addon { 65 | padding: @padding-base-vertical @padding-base-horizontal; 66 | font-size: @font-size-base; 67 | font-weight: normal; 68 | line-height: 1; 69 | color: @input-color; 70 | text-align: center; 71 | background-color: @input-group-addon-bg; 72 | border: 1px solid @input-group-addon-border-color; 73 | border-radius: @border-radius-base; 74 | 75 | // Sizing 76 | &.input-sm { 77 | padding: @padding-small-vertical @padding-small-horizontal; 78 | font-size: @font-size-small; 79 | border-radius: @border-radius-small; 80 | } 81 | &.input-lg { 82 | padding: @padding-large-vertical @padding-large-horizontal; 83 | font-size: @font-size-large; 84 | border-radius: @border-radius-large; 85 | } 86 | 87 | // Nuke default margins from checkboxes and radios to vertically center within. 88 | input[type="radio"], 89 | input[type="checkbox"] { 90 | margin-top: 0; 91 | } 92 | } 93 | 94 | // Reset rounded corners 95 | .input-group .form-control:first-child, 96 | .input-group-addon:first-child, 97 | .input-group-btn:first-child > .btn, 98 | .input-group-btn:first-child > .btn-group > .btn, 99 | .input-group-btn:first-child > .dropdown-toggle, 100 | .input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), 101 | .input-group-btn:last-child > .btn-group:not(:last-child) > .btn { 102 | .border-right-radius(0); 103 | } 104 | .input-group-addon:first-child { 105 | border-right: 0; 106 | } 107 | .input-group .form-control:last-child, 108 | .input-group-addon:last-child, 109 | .input-group-btn:last-child > .btn, 110 | .input-group-btn:last-child > .btn-group > .btn, 111 | .input-group-btn:last-child > .dropdown-toggle, 112 | .input-group-btn:first-child > .btn:not(:first-child), 113 | .input-group-btn:first-child > .btn-group:not(:first-child) > .btn { 114 | .border-left-radius(0); 115 | } 116 | .input-group-addon:last-child { 117 | border-left: 0; 118 | } 119 | 120 | // Button input groups 121 | // ------------------------- 122 | .input-group-btn { 123 | position: relative; 124 | // Jankily prevent input button groups from wrapping with `white-space` and 125 | // `font-size` in combination with `inline-block` on buttons. 126 | font-size: 0; 127 | white-space: nowrap; 128 | 129 | // Negative margin for spacing, position for bringing hovered/focused/actived 130 | // element above the siblings. 131 | > .btn { 132 | position: relative; 133 | + .btn { 134 | margin-left: -1px; 135 | } 136 | // Bring the "active" button to the front 137 | &:hover, 138 | &:focus, 139 | &:active { 140 | z-index: 2; 141 | } 142 | } 143 | 144 | // Negative margin to only have a 1px border between the two 145 | &:first-child { 146 | > .btn, 147 | > .btn-group { 148 | margin-right: -1px; 149 | } 150 | } 151 | &:last-child { 152 | > .btn, 153 | > .btn-group { 154 | margin-left: -1px; 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/jumbotron.less: -------------------------------------------------------------------------------- 1 | // 2 | // Jumbotron 3 | // -------------------------------------------------- 4 | 5 | 6 | .jumbotron { 7 | padding: @jumbotron-padding; 8 | margin-bottom: @jumbotron-padding; 9 | color: @jumbotron-color; 10 | background-color: @jumbotron-bg; 11 | 12 | h1, 13 | .h1 { 14 | color: @jumbotron-heading-color; 15 | } 16 | p { 17 | margin-bottom: (@jumbotron-padding / 2); 18 | font-size: @jumbotron-font-size; 19 | font-weight: 200; 20 | } 21 | 22 | .container & { 23 | border-radius: @border-radius-large; // Only round corners at higher resolutions if contained in a container 24 | } 25 | 26 | .container { 27 | max-width: 100%; 28 | } 29 | 30 | @media screen and (min-width: @screen-sm-min) { 31 | padding-top: (@jumbotron-padding * 1.6); 32 | padding-bottom: (@jumbotron-padding * 1.6); 33 | 34 | .container & { 35 | padding-left: (@jumbotron-padding * 2); 36 | padding-right: (@jumbotron-padding * 2); 37 | } 38 | 39 | h1, 40 | .h1 { 41 | font-size: (@font-size-base * 4.5); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/labels.less: -------------------------------------------------------------------------------- 1 | // 2 | // Labels 3 | // -------------------------------------------------- 4 | 5 | .label { 6 | display: inline; 7 | padding: .2em .6em .3em; 8 | font-size: 75%; 9 | font-weight: bold; 10 | line-height: 1; 11 | color: @label-color; 12 | text-align: center; 13 | white-space: nowrap; 14 | vertical-align: baseline; 15 | border-radius: .25em; 16 | 17 | // Add hover effects, but only for links 18 | &[href] { 19 | &:hover, 20 | &:focus { 21 | color: @label-link-hover-color; 22 | text-decoration: none; 23 | cursor: pointer; 24 | } 25 | } 26 | 27 | // Empty labels collapse automatically (not available in IE8) 28 | &:empty { 29 | display: none; 30 | } 31 | 32 | // Quick fix for labels in buttons 33 | .btn & { 34 | position: relative; 35 | top: -1px; 36 | } 37 | } 38 | 39 | // Colors 40 | // Contextual variations (linked labels get darker on :hover) 41 | 42 | .label-default { 43 | .label-variant(@label-default-bg); 44 | } 45 | 46 | .label-primary { 47 | .label-variant(@label-primary-bg); 48 | } 49 | 50 | .label-success { 51 | .label-variant(@label-success-bg); 52 | } 53 | 54 | .label-info { 55 | .label-variant(@label-info-bg); 56 | } 57 | 58 | .label-warning { 59 | .label-variant(@label-warning-bg); 60 | } 61 | 62 | .label-danger { 63 | .label-variant(@label-danger-bg); 64 | } 65 | -------------------------------------------------------------------------------- /public/css/lib/bootstrap/list-group.less: -------------------------------------------------------------------------------- 1 | // 2 | // List groups 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base class 7 | // 8 | // Easily usable on