Merge remote-tracking branch 'upstream/master'

Conflicts:
	package.json
This commit is contained in:
Matt Roberts
2014-02-27 16:16:13 +00:00
32 changed files with 2051 additions and 1297 deletions

8
.travis.yml Normal file
View File

@ -0,0 +1,8 @@
language: node_js
services:
- mongodb
node_js:
- '0.11'
- '0.10'

165
README.md
View File

@ -1,5 +1,5 @@
![Alt](https://lh4.googleusercontent.com/-PVw-ZUM9vV8/UuWeH51os0I/AAAAAAAAD6M/0Ikg7viJftQ/w1286-h566-no/hackathon-starter-logo.jpg)
Hackathon Starter [![Dependency Status](https://david-dm.org/sahat/hackathon-starter.png?theme=shields.io)](https://david-dm.org/sahat/hackathon-starter)
Hackathon Starter [![Dependency Status](https://david-dm.org/sahat/hackathon-starter.png?theme=shields.io)](https://david-dm.org/sahat/hackathon-starter) [![Build Status](https://travis-ci.org/sahat/hackathon-starter.png)](https://travis-ci.org/sahat/hackathon-starter)
=================
A boilerplate for **Node.js** web applications.
@ -35,6 +35,7 @@ Table of Contents
- [Getting Started](#getting-started)
- [Obtaining API Keys](#obtaining-api-keys)
- [Project Structure](#project-structure)
- [List of Packages](#list-of-packages)
- [Useful Tools](#useful-tools)
- [Recommended Design](#recommended-design)
- [Recommended Node.js Libraries](#recommended-nodejs-libraries)
@ -66,6 +67,7 @@ Features
- Change Password
- Link multiple OAuth strategies to one account
- Delete Account
- Forgot Password
- **API Examples**: Facebook, Foursquare, Last.fm, Tumblr, Twitter, PayPal, and more.
Prerequisites
@ -89,7 +91,7 @@ Getting Started
The easiest way to get started is to clone the repository:
```bash
# Fetch only the latest commits.
# Fetch only the latest commits
git clone --depth=1 git@github.com:sahat/hackathon-starter.git my-project
cd my-project
@ -100,12 +102,12 @@ npm install
node app.js
```
>:exclamation: **Note**: I strongly recommend installing nodemon `sudo npm install -g nodemon`.
>It will monitor for any changes in your node.js
>application and automatically restart the server. Once installed, instead of `node app.js` use `nodemon app.js`.
>It is a big time saver in the long run.
:exclamation: **Note**: I strongly recommend installing nodemon `sudo npm install -g nodemon`.
It will monitor for any changes in your node.js
application and automatically restart the server. Once installed, instead of `node app.js` use `nodemon app.js`.
It will save you a lot of time in the long run, because you won't need to manually restart the server each time you make a change.
Next up, if you want to use any of the APIs or OAuth authentication methods, you will need to obtain
Next, if you want to use any of the included APIs or OAuth authentication methods, you will need to obtain
appropriate credentials: Client ID, Client Secret, API Key, or Username & Password. You will
need to go through each provider to generate new credentials.
@ -114,6 +116,13 @@ Obtaining API Keys
:pushpin: You could support all 5 authentication methods by setting up OAuth keys, but you don't have to. If you would only like to have **Facebook sign-in** and **Local sign-in** with email and password, in **secrets.js** set `googleAuth: false`, `twitterOauth: false`, `githubAuth: false`. By doing so, *Google, Twitter and Github* buttons will not show up on the *Login* page. If you set `localAuth: false`, users will not be able to login/create an account with email and password or change password in the *Account Management* page.
:bulb: Alternatively, if you would like to completely remove authentication methods that you do not plan on using, you will need to manually delete the code yourself. Let's say you want to keep only **Local authentication**. Start by deleting *FacebookStrategy, TwitterStrategy, GitHubStrategy, GoogleStrategy* `require` lines and their corresponding defined strategies in **passport.js**. Then in **login.jade** template delete the entire `.btn-group`, leaving only the form with Email and Password.
Update **User.js** model by deleting the following fields: `facebook`, `github`, `google`, `twitter`. In your **profile.jade** template delete the entire code starting with **h3 Linked Accounts**. And finally delete the corresponding routes that have **/auth/provider** and **/auth/provider/callback**, for example:
```js
app.get('/auth/facebook', passport.authenticate('facebook', { scope: ['email', 'user_location'] }));
app.get('/auth/facebook/callback', passport.authenticate('facebook', { successRedirect: '/', failureRedirect: '/login' }));
```
<img src="http://images.google.com/intl/en_ALL/images/srpr/logo6w.png" width="200">
- Visit [Google Cloud Console](https://cloud.google.com/console/project)
- Click **CREATE PROJECT** button
@ -250,10 +259,42 @@ Project Structure
| app.js | Main application file. |
| cluster_app.js | Runs multiple instances of `app.js` using <a href="http://nodejs.org/api/cluster.html" target="_blank">Node.js clusters</a>.|
:exclamation: **Note:** There is no preference how you name or structure your views. You could place all your templates in a top-level `views` directory without having a nested folder structure, if that makes things easier for you. Just don't forget to update `extends ../layout` and corresponding `res.render()` method in controllers.
:exclamation: **Note:** There is no difference how you name or structure your views. You could place all your templates in a top-level `views` directory without having a nested folder structure, if that makes things easier for you. Just don't forget to update `extends ../layout` and corresponding `res.render()` method in controllers. For smaller apps, I find having a flat folder structure to be easier to work with.
List of Packages
----------------
| Package | Description |
| ------------- |:-------------:|
| async | Utility library that provides asynchronous control flow. |
| bcrypt-nodejs | Library for hashing and salting user passwords. |
| cheerio | Scrape web pages using jQuery-style syntax. |
| connect-mongo | MongoDB session store for Express. |
| connect-assets | Compiles LESS stylesheets, concatenates/minifies JavaScript. |
| express | Web framework. |
| express-flash | Provides flash messages for Express. Uses connect-flash internally. |
| express-validator | Easy form validation for Express. Uses node-validator internally. |
| fbgraph | Facebook Graph API library |
| github-api | GitHub API library |
| jade | Template engine for node.js |
| lastfm | Last.fm API library |
| less | LESS compiler. Used implicitly by connect-assets. |
| mongoose | MongoDB object modeling tool |
| node-foursquare | Foursquare API library |
| nodemailer | Node.js library for sending emails |
| passport | Simple and elegant authentication library for node.js |
| passport-facebook | Sign-in with Facebook plugin. |
| passport-github | Sign-in with GitHub plugin. |
| passport-google-oauth | Sign-in with Google plugin. |
| passport-twitter | Sign-in with Twitter plugin. |
| passport-local | Sign-in with Username and Password plugin. |
| passport-oauth | Allows you to set up your own OAuth 1.0a and OAuth 2.0 strategies. |
| request | Simplified HTTP request library. |
| tumblr.js | Tumblr API library. |
| underscore | Handy JavaScript utlities library. |
| paypal-rest-sdk | PayPal API library. |
| twilio | Twilio API library. |
| validator | Used in conjunction with express-validator in **controllers/api.js**. |
:bangbang: **Note:** Although your main template - **layout.jade** only knows about `/css/styles.css` file, you should be editing **styles.less** stylesheet. Express will automatically generate minified **styles.css** whenever there are changes in LESS file. This is done via [less-middleware](https://github.com/emberfeather/less.js-middleware) node.js library.
Useful Tools
------------
@ -273,6 +314,7 @@ Recommended Design
- [Creative Link Effects](http://tympanus.net/Development/CreativeLinkEffects/) - Beautiful link effects in CSS.
- [Medium Scroll Effect](http://codepen.io/andreasstorm/pen/pyjEh) - Fade in/out header background image as you scroll.
- [HTML5UP](http://html5up.net/) - Beautifully designed HTML templates.
- [Progre(c)ss](https://github.com/jh3y/progre-c-ss) - Pure CSS progress bars.
Recommended Node.js Libraries
-----------------------------
@ -281,6 +323,7 @@ Recommended Node.js Libraries
- [Nodemailer](https://github.com/andris9/Nodemailer) - send emails with node.js (without sendgrid or mailgun).
- [filesize.js](http://filesizejs.com/) - make file size pretty, e.g. `filesize(265318); // "265.32 kB"`.
- [Numeral.js](http://numeraljs.com) - a javascript library for formatting and manipulating numbers.
- [Node Inspector](https://github.com/node-inspector/node-inspector) - Node.js debugger based on Chrome Developer Tools.
Recommended Client-Side libraries
---------------------------------
@ -297,15 +340,15 @@ Recommended Client-Side libraries
- [select.js](http://github.hubspot.com/select/docs/welcome/) - Styleable select elements.
- [drop.js](http://github.hubspot.com/drop/docs/welcome/) - Powerful Javascript and CSS library for creating dropdowns and other floating displays.
- [scrollReveal.js](https://github.com/julianlloyd/scrollReveal.js) - Declarative on-scroll reveal animations.
- [InstantClick](http://instantclick.io) - Makes your pages load instantly by pre-loading them on mouse hover.
Pro Tips
--------
- When you install a new npm package, add a *--save* flag and it will be automatially
- When installing an NPM package, add a *--save* flag, and it will be automatially
added to `package.json` as well. For example, `npm install --save moment`.
- Use [async.parallel()](https://github.com/caolan/async#parallel) when you neeed to run multiple
asynchronous tasks, and then render a page, but only when all tasks are completed. For example, you might
want to scrape 3 different websites for some data (async operation) and render the results
on a page after all 3 websites have been scraped.
want to scrape 3 different websites for some data (async operation) and render the results in a template after all 3 websites have been scraped.
- Need to find a specific object inside an Array? Use [_.findWhere](http://underscorejs.org/#findWhere) function from Underscore.js. For example, this is how you would retrieve a Twitter token from database: `var token = _.findWhere(req.user.tokens, { kind: 'twitter' });`, where `req.user.tokens` is an Array, and a second parameter is an object with a given key/value.
- If you right click and select **View Page Source**, notice how *Express*
minified HTML for you. If you would like to see non-minified markup,
@ -316,9 +359,12 @@ FAQ
### Why do I get `403 Error: Forbidden` when submitting a POST form?
You need to add this hidden input element to your form. This has been added in the
pull request [#40](https://github.com/sahat/hackathon-starter/pull/40).
```
input(type='hidden', name='_csrf', value=token)
```
You can read more about [CSRF protection middleware](http://expressjs.com/api.html#csrf) at the Express API Reference.
### What is `cluster_app.js`?
From the [Node.js Documentation](http://nodejs.org/api/cluster.html#cluster_how_it_works):
@ -346,12 +392,15 @@ script(src='/js/application.js')
```
As soon as you start bringing in more JavaScript libraries, the benefits of concatenating and minifying
JavaScript files will be even greater.
Using connect-assets library it's as as simple as:
```jade
Using **connect-assets** library, it is as as simple as declaring these two lines:
```
!= css('styles') // expects public/css/styles.less
!= js('application') // expects public/js/application.js
```
:bulb: **Tip:** This works because in *connect-assets* middleware we have specified `helperContext: app.locals`.
The only thing you need to remember is to define your JavaScript files inside `public/js/application.js` using this
strange syntax notation (Sprockets-style) borrowed from Rails. I know it's an extra thing to learn
for someone who has never seen Rails asset pipeline before, but in this case, I think benefits outweigh the costs.
@ -468,14 +517,14 @@ app.get('/escape-velocity', homeController.escapeVelocity);
Restart the server (if you are not using **nodemon**), then you should see the new template at [http://localhost:3000/escape-velocity](http://localhost:3000/escape-velocity).
I will stop here, but if you would like to use this template as more than just a single page, take a look at how these Jade templates work: `layout.jade` - base template, `index.jade` - home page, `partials/navigation.jade` - Bootstrap navbar, `partials/footer.jade` - sticky footer. You will have to manually break it apart into smaller pieces. Figure out which part of the template you want to keep the same on all pages - that's your new `layout.jade`.
I will stop right here, but if you would like to use this template as more than just a single page, take a look at how these Jade templates work: `layout.jade` - base template, `index.jade` - home page, `partials/navigation.jade` - Bootstrap navbar, `partials/footer.jade` - sticky footer. You will have to manually break it apart into smaller pieces. Figure out which part of the template you want to keep the same on all pages - that's your new `layout.jade`.
Then, each page that changes, be it `index.jade`, `about.jade`, `contact.jade`
will be embedded in the new `layout.jade` via `block content`.
will be embedded in your new `layout.jade` via `block content`. Use existing templates as a reference.
This is a lengthy process, I know, and templates you get from outside **HTML5**UP,
will have yet another grid system. That's why I chose Bootstrap CSS for the Hackathon Starter.
Most people are familiar with Bootstrap, it's easy to get started, very extendable.
You can also buy a Bootstrap theme drop it in into your project, and everything looks great without a single change to your markup or CSS class names. However, if you would like to go with a completely custom design, there you have it!
This is a rather lengthy process, and templates you get from elsewhere,
might have yet another grid system. That's why I chose *Bootstrap* for the Hackathon Starter.
Many people are already familiar with *Bootstrap*, plus it's easy to get started with it if you have never used *Bootstrap*.
You can also buy many beautifully designed *Bootstrap* themes at [Themeforest](http://themeforest.net/), and use them as a drop-in replacement for Hackathon Starter. However, if you would like to go with a completely custom HTML/CSS design, this should help you to get started!
<hr>
@ -562,11 +611,11 @@ or send a pull request if you would like to include something that I missed.
<hr>
###:snowman: How do I create a new page?
A more correct way to be to say "How do I create a route". The main file `app.js` contains all the routes.
Each route has a callback function (aka controller) associated with it. Sometimes you will see 3 or more arguments
to routes. In cases like that, the first argument is still a URL string, the middle arguments
A more correct way to be to say "How do I create a new route". The main file `app.js` contains all the routes.
Each route has a callback function associated with it. Sometimes you will see 3 or more arguments
to routes. In cases like that, the first argument is still a URL string, while middle arguments
are what's called middleware. Think of middleware as a door. If this door prevents you from
continuing forward, well, you won't get to your callback function (aka controller). One such example is authentication.
continuing forward, you won't get to your callback function. One such example is a route that requires authentication.
```js
app.get('/account', passportConf.isAuthenticated, userController.getAccount);
@ -577,14 +626,15 @@ checks if you are authenticated:
```js
exports.isAuthenticated = function(req, res, next) {
if (req.isAuthenticated()) return next();
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
};
```
If you are authenticated, you let this visitor pass through your "door" by calling `return next();`. It then proceeds to the
next middleware until it reaches the last argument which is a callback function that usually renders a template,
or responds with a JSON data, if you are building a REST API. But in this example it simply renders a page and nothing more:
next middleware until it reaches the last argument, which is a callback function that typically renders a template on `GET` requests or redirects on `POST` requests. In this case, if you are authenticated, then you will see *Account Management* page, otherwise you will be redirected to *Login* page.
```js
exports.getAccount = function(req, res) {
@ -597,7 +647,7 @@ exports.getAccount = function(req, res) {
Express.js has `app.get`, `app.post`, `app.put`, `app.del`, but for the most part you will only use the first two.
If you just want to display a page, then use `GET`, if you are submitting a form, sending a file then use `POST`.
Here is a typical workflow of adding new routes to your application. Let's say we are building
Here is a typical workflow for adding new routes to your application. Let's say we are building
a page that lists all books from database.
**Step 1.** Start by defining a route.
@ -730,7 +780,16 @@ io.sockets.on('connection', function(socket) {
});
```
We are done with the server-side business.
One last thing left to change:
```js
app.listen(app.get('port'), function() {
```
to
```js
server.listen(app.get('port'), function() {
```
At this point we are done with the back-end.
You now have a choice - to include your JavaScript code in Jade templates or have all your client-side
JavaScript in a separate file - in `main.js`. I will admit, when I first started out with Node.js and JavaScript in general,
@ -781,6 +840,30 @@ And that's it, we are done!
If you want to see a really cool real-time dashboard check out this [live example](http://hackathonstarter.herokuapp.com/dashboard). Refer to the [pull request #23](https://github.com/sahat/hackathon-starter/pull/23/files) to see how it is implemented.
### How does “Forgot your password” feature work?
There are **4** routes in total that handle forgot password and reset password:
```js
app.get('/forgot', forgotController.getForgot);
app.post('/forgot', forgotController.postForgot);
app.get('/reset/:token', resetController.getReset);
app.post('/reset/:token', resetController.postReset);
```
The first step begins at the get `GET /forgot` when user clicks on **Forgot your password?** link on the *Login* page. The `POST /forgot` handles the form submission. If email address is valid, it creates a random 20-bit hash, finds that users email in the database and sets `resetPasswordToken` field to the newly generated random 20-bit hash, additionally `resetPasswordExpires` is set to 1 hour into the future. That means from the moment you receive an email, that reset link will be valid only for one hour (for security reasons its a good practice to expire reset password links). If 1 hour is too short for your needs, feel free to increase it. The final step is to actually send an email with a reset link. This is all elegantly done using **async.waterfall** control flow.
Notice how it handles the case when no email address exists:
```js
if (!user) {
req.flash('errors', { msg: 'No account with that email address exists.' });
return res.redirect('/forgot');
}
```
Some people might find this approach to be less secure. Maybe a better approach might have been to let the user know “If there is an account with provided e-mail address, we will send you a reset link”. Again, feel free to change it based on your application needs.
The second step involves resetting a password. After clicking on a reset link, it redirects you to a page where you can set a new password. The token validity check is performed twice - on `GET` request when you click on a reset link and on `POST` request after you submit a new password. After selecting a new password, both `passwordResetToken` and `resetPasswordExpire` fields are deleted from the database. This is easily done by setting their value to `undefined`; *Mongoose* will run `$unset` internally. And finally, user is logged in with the new password and a confirmation email is sent notifying about the password change.
Mongoose Cheatsheet
-------------------
#### Find all users:
@ -886,12 +969,28 @@ Add this to `package.json`, after *name* and *version*. This is necessary becaus
- And you are done! (Not quite as simple as Heroku, huh?)
<img src="https://www.nodejitsu.com/img/media/nodejitsu-transparent.png" width="200">
TODO: Will be added soon.
- To install **jitsu**, open a terminal and type: `sudo npm install -g jitsu`
- Run `jitsu login` and enter your login credentials
- From your app directory, run `jitsu deploy`
- This will create a new application snapshot, generate and/or update project metadata
- Done!
<img src="http://upload.wikimedia.org/wikipedia/en/f/ff/Windows_Azure_logo.png" width="200">
TODO: Will be added soon.
- Login to [Windows Azure Management Portal](http://manage.windowsazure.com/)
- Click the **+ NEW** button on the bottom left of the portal
- Click **WEB SITE**, then **QUICK CREATE**
- Enter a name for **URL** and select the datacenter **REGION** for your web site
- Click on **CREATE WEB SITE** button
- Once the web site status changes to *Running*, click on the name of the web site to access the Dashboard
- At the bottom right of the Quickstart page, select **Set up a deployment from source control**
- Select **Local Git repository** from the list, and then click the arrow
- To enable Git publishing, Azure will ask you to create a user name and password
- Once the Git repository is ready, you will be presented with a **GIT URL**
- Inside your *Hackathon Starter* directory, run `git remote add azure [Azure Git URL]`
- To push your changes simply run `git push azure master`
- **Note:** *You will be prompted for the password you created earlier*
- On **Deployments** tab of your Windows Azure Web Site, you will see the deployment history
TODO
----
@ -899,7 +998,7 @@ TODO
Contributing
------------
If something is unclear, confusing, or needs to be refactored, please let me know. Pull requests are always welcome, but due to the opinionated nature of this project, I cannot accept every pull request. Please open an issue before submitting a pull request. This project uses [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) with a few exceptions.
If something is unclear, confusing, or needs to be refactored, please let me know. Pull requests are always welcome, but due to the opinionated nature of this project, I cannot accept every pull request. Please open an issue before submitting a pull request. This project uses [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) with a few minor exceptions. If you are submitting a pull request that involves Jade templates, please make sure you are using *spaces*, not tabs.
License
-------

15
app.js
View File

@ -9,6 +9,7 @@ var path = require('path');
var mongoose = require('mongoose');
var passport = require('passport');
var expressValidator = require('express-validator');
var connectAssets = require('connect-assets');
@ -20,6 +21,8 @@ var homeController = require('./controllers/home');
var userController = require('./controllers/user');
var apiController = require('./controllers/api');
var contactController = require('./controllers/contact');
var forgotController = require('./controllers/forgot');
var resetController = require('./controllers/reset');
/**
* API keys + Passport configuration.
@ -55,8 +58,8 @@ var month = (day * 30);
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(require('connect-assets')({
src: 'public',
app.use(connectAssets({
paths: ['public/css', 'public/js'],
helperContext: app.locals
}));
app.use(express.compress());
@ -70,7 +73,7 @@ app.use(express.methodOverride());
app.use(express.session({
secret: secrets.sessionSecret,
store: new MongoStore({
db: mongoose.connection.db,
url: secrets.db,
auto_reconnect: true
})
}));
@ -100,6 +103,10 @@ app.get('/', homeController.index);
app.get('/login', userController.getLogin);
app.post('/login', userController.postLogin);
app.get('/logout', userController.logout);
app.get('/forgot', forgotController.getForgot);
app.post('/forgot', forgotController.postForgot);
app.get('/reset/:token', resetController.getReset);
app.post('/reset/:token', resetController.postReset);
app.get('/signup', userController.getSignup);
app.post('/signup', userController.postSignup);
app.get('/contact', contactController.getContact);
@ -167,3 +174,5 @@ app.get('/auth/venmo/callback', passport.authorize('venmo', { failureRedirect: '
app.listen(app.get('port'), function() {
console.log("✔ Express server listening on port %d in %s mode", app.get('port'), app.settings.env);
});
module.exports = app;

View File

@ -1,14 +1,14 @@
var _ = require('underscore');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var OAuthStrategy = require('passport-oauth').OAuthStrategy;
var OAuth2Strategy = require('passport-oauth').OAuth2Strategy;
var FacebookStrategy = require('passport-facebook').Strategy;
var TwitterStrategy = require('passport-twitter').Strategy;
var GitHubStrategy = require('passport-github').Strategy;
var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
var OAuthStrategy = require('passport-oauth').OAuthStrategy; // Tumblr
var OAuth2Strategy = require('passport-oauth').OAuth2Strategy; // Venmo, Foursquare
var User = require('../models/User');
var secrets = require('./secrets');
var _ = require('underscore');
passport.serializeUser(function(user, done) {
done(null, user.id);
@ -20,6 +20,10 @@ passport.deserializeUser(function(id, done) {
});
});
/**
* Sign in using Email and Password.
*/
passport.use(new LocalStrategy({ usernameField: 'email' }, function(email, password, done) {
User.findOne({ email: email }, function(err, user) {
if (!user) return done(null, false, { message: 'Email ' + email + ' not found'});
@ -59,8 +63,12 @@ passport.use(new FacebookStrategy(secrets.facebook, function(req, accessToken, r
});
} else {
User.findOne({ facebook: profile.id }, function(err, existingUser) {
console.log(profile)
if (existingUser) return done(null, existingUser);
User.findOne({ email: profile._json.email }, function(err, existingEmailUser) {
if (existingEmailUser) {
req.flash('errors', { msg: 'There is already an account using this email address. Sign in to that account and link it with Facebook manually from Account Settings.' });
done(err);
} else {
var user = new User();
user.email = profile._json.email;
user.facebook = profile.id;
@ -72,6 +80,8 @@ passport.use(new FacebookStrategy(secrets.facebook, function(req, accessToken, r
user.save(function(err) {
done(err, user);
});
}
});
});
}
}));
@ -104,6 +114,11 @@ passport.use(new GitHubStrategy(secrets.github, function(req, accessToken, refre
} else {
User.findOne({ github: profile.id }, function(err, existingUser) {
if (existingUser) return done(null, existingUser);
User.findOne({ email: profile._json.email }, function(err, existingEmailUser) {
if (existingEmailUser) {
req.flash('errors', { msg: 'There is already an account using this email address. Sign in to that account and link it with GitHub manually from Account Settings.' });
done(err);
} else {
var user = new User();
user.email = profile._json.email;
user.github = profile.id;
@ -115,6 +130,8 @@ passport.use(new GitHubStrategy(secrets.github, function(req, accessToken, refre
user.save(function(err) {
done(err, user);
});
}
});
});
}
}));
@ -191,6 +208,11 @@ passport.use(new GoogleStrategy(secrets.google, function(req, accessToken, refre
} else {
User.findOne({ google: profile.id }, function(err, existingUser) {
if (existingUser) return done(null, existingUser);
User.findOne({ email: profile._json.email }, function(err, existingEmailUser) {
if (existingEmailUser) {
req.flash('errors', { msg: 'There is already an account using this email address. Sign in to that account and link it with Google manually from Account Settings.' });
done(err);
} else {
var user = new User();
user.email = profile._json.email;
user.google = profile.id;
@ -201,10 +223,17 @@ passport.use(new GoogleStrategy(secrets.google, function(req, accessToken, refre
user.save(function(err) {
done(err, user);
});
}
});
});
}
}));
/**
* Tumblr API
* Uses OAuth 1.0a Strategy.
*/
passport.use('tumblr', new OAuthStrategy({
requestTokenURL: 'http://www.tumblr.com/oauth/request_token',
accessTokenURL: 'http://www.tumblr.com/oauth/access_token',
@ -224,6 +253,11 @@ passport.use('tumblr', new OAuthStrategy({
}
));
/**
* Foursquare API
* Uses OAuth 2.0 Strategy.
*/
passport.use('foursquare', new OAuth2Strategy({
authorizationURL: 'https://foursquare.com/oauth2/authorize',
tokenURL: 'https://foursquare.com/oauth2/access_token',
@ -242,6 +276,11 @@ passport.use('foursquare', new OAuth2Strategy({
}
));
/**
* Venmo API
* Uses OAuth 2.0 Strategy.
*/
passport.use('venmo', new OAuth2Strategy({
authorizationURL: 'https://api.venmo.com/v1/oauth/authorize',
tokenURL: 'https://api.venmo.com/v1/oauth/access_token',
@ -260,11 +299,19 @@ passport.use('venmo', new OAuth2Strategy({
}
));
/**
* Login Required middleware.
*/
exports.isAuthenticated = function(req, res, next) {
if (req.isAuthenticated()) return next();
res.redirect('/login');
};
/**
* Authorization Required middleware.
*/
exports.isAuthorized = function(req, res, next) {
var provider = req.path.split('/').slice(-1)[0];
if (_.findWhere(req.user.tokens, { kind: provider })) next();

View File

@ -124,7 +124,7 @@ exports.getScraping = function(req, res, next) {
if (err) return next(err);
var $ = cheerio.load(body);
var links = [];
$('.title a').each(function() {
$(".title a[href^='http'], a[href^='https']").each(function() {
links.push($(this));
});
res.render('api/scraping', {

85
controllers/forgot.js Normal file
View File

@ -0,0 +1,85 @@
var async = require('async');
var crypto = require('crypto');
var nodemailer = require("nodemailer");
var User = require('../models/User');
var secrets = require('../config/secrets');
/**
* GET /forgot
* Forgot Password page.
*/
exports.getForgot = function(req, res) {
if (req.isAuthenticated()) {
return res.redirect('/');
}
res.render('account/forgot', {
title: 'Forgot Password'
});
};
/**
* POST /forgot
* Create a random token, then the send user an email with a reset link.
* @param email
*/
exports.postForgot = function(req, res, next) {
req.assert('email', 'Please enter a valid email address.').isEmail();
var errors = req.validationErrors();
if (errors) {
req.flash('errors', errors);
return res.redirect('/forgot');
}
async.waterfall([
function(done) {
crypto.randomBytes(20, function(err, buf) {
var token = buf.toString('hex');
done(err, token);
});
},
function(token, done) {
User.findOne({ email: req.body.email.toLowerCase() }, function(err, user) {
if (!user) {
req.flash('errors', { msg: 'No account with that email address exists.' });
return res.redirect('/forgot');
}
user.resetPasswordToken = token;
user.resetPasswordExpires = Date.now() + 3600000; // 1 hour
user.save(function(err) {
done(err, token, user);
});
});
},
function(token, user, done) {
var smtpTransport = nodemailer.createTransport('SMTP', {
service: 'SendGrid',
auth: {
user: secrets.sendgrid.user,
pass: secrets.sendgrid.password
}
});
var mailOptions = {
to: user.profile.name + ' <' + user.email + '>',
from: 'hackathon@starter.com',
subject: 'Hackathon Starter Password Reset',
text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' +
'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
'http://' + req.headers.host + '/reset/' + token + '\n\n' +
'If you did not request this, please ignore this email and your password will remain unchanged.\n'
};
smtpTransport.sendMail(mailOptions, function(err) {
req.flash('info', { msg: 'An e-mail has been sent to ' + user.email + ' with further instructions.' });
done(err, 'done');
});
}
], function(err) {
if (err) return next(err);
res.redirect('/forgot');
});
};

93
controllers/reset.js Normal file
View File

@ -0,0 +1,93 @@
var async = require('async');
var nodemailer = require('nodemailer');
var User = require('../models/User');
var secrets = require('../config/secrets');
/**
* GET /reset/:token
* Reset Password page.
*/
exports.getReset = function(req, res) {
if (req.isAuthenticated()) {
return res.redirect('/');
}
User
.findOne({ resetPasswordToken: req.params.token })
.where('resetPasswordExpires').gt(Date.now())
.exec(function(err, user) {
if (!user) {
req.flash('errors', { msg: 'Password reset token is invalid or has expired.' });
return res.redirect('/forgot');
}
res.render('account/reset', {
title: 'Password Reset'
});
});
};
/**
* POST /reset/:token
* Process the reset password request.
*/
exports.postReset = function(req, res, next) {
req.assert('password', 'Password must be at least 4 characters long.').len(4);
req.assert('confirm', 'Passwords must match.').equals(req.body.password);
var errors = req.validationErrors();
if (errors) {
req.flash('errors', errors);
return res.redirect('back');
}
async.waterfall([
function(done) {
User
.findOne({ resetPasswordToken: req.params.token })
.where('resetPasswordExpires').gt(Date.now())
.exec(function(err, user) {
if (!user) {
req.flash('errors', { msg: 'Password reset token is invalid or has expired.' });
return res.redirect('back');
}
user.password = req.body.password;
user.resetPasswordToken = undefined;
user.resetPasswordExpires = undefined;
user.save(function(err) {
if (err) return next(err);
req.logIn(user, function(err) {
done(err, user);
});
});
});
},
function(user, done) {
var smtpTransport = nodemailer.createTransport('SMTP', {
service: 'SendGrid',
auth: {
user: secrets.sendgrid.user,
pass: secrets.sendgrid.password
}
});
var mailOptions = {
to: user.profile.name + ' <' + user.email + '>',
from: 'hackathon@starter.com',
subject: 'Your Hackathon Starter password has been changed',
text: 'Hello,\n\n' +
'This is a confirmation that the password for your account ' + user.email + ' has just been changed.\n'
};
smtpTransport.sendMail(mailOptions, function(err) {
req.flash('success', { msg: 'Success! Your password has been changed.' });
done(err);
});
}
], function(err) {
if (err) return next(err);
res.redirect('/');
});
};

View File

@ -18,20 +18,23 @@ var userSchema = new mongoose.Schema({
location: { type: String, default: '' },
website: { type: String, default: '' },
picture: { type: String, default: '' }
}
},
resetPasswordToken: String,
resetPasswordExpires: Date
});
/**
* Hash the password for security.
* "Pre" is a Mongoose middleware that executes before each user.save() call.
*/
userSchema.pre('save', function(next) {
var user = this;
var SALT_FACTOR = 5;
if (!user.isModified('password')) return next();
bcrypt.genSalt(SALT_FACTOR, function(err, salt) {
bcrypt.genSalt(5, function(err, salt) {
if (err) return next(err);
bcrypt.hash(user.password, salt, null, function(err, hash) {
@ -42,6 +45,11 @@ userSchema.pre('save', function(next) {
});
});
/**
* Validate user's password.
* Used by Passport-Local Strategy for password validation.
*/
userSchema.methods.comparePassword = function(candidatePassword, cb) {
bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
if (err) return cb(err);
@ -50,12 +58,18 @@ userSchema.methods.comparePassword = function(candidatePassword, cb) {
};
/**
* Get a URL to a user's Gravatar email.
* Get URL to a user's gravatar.
* Used in Navbar and Account Management page.
*/
userSchema.methods.gravatar = function(size, defaults) {
if (!size) size = 200;
if (!defaults) defaults = 'retro';
if (!this.email) {
return 'https://gravatar.com/avatar/?s=' + size + '&d=' + defaults;
}
var md5 = crypto.createHash('md5').update(this.email);
return 'https://gravatar.com/avatar/' + md5.digest('hex').toString() + '?s=' + size + '&d=' + defaults;
};

View File

@ -1,14 +1,19 @@
{
"name": "hackathon-starter",
"version": "0.0.0",
"repository": {
"type" : "git",
"url" : "https://github.com/sahat/hackathon-starter.git"
},
"scripts": {
"start": "node app.js"
"start": "node app.js",
"test": "mocha"
},
"dependencies": {
"async": "~0.2.10",
"bcrypt-nodejs": "~0.0.3",
"cheerio": "~0.13.1",
"connect-assets": "~2.5.4",
"connect-assets": "~3.0.0-beta1",
"express": "~3.4.8",
"express-flash": "~0.0.2",
"express-validator": "~1.0.1",
@ -27,14 +32,21 @@
"passport-local": "~0.1.6",
"passport-oauth": "~1.0.0",
"passport-twitter": "~1.0.2",
"request": "~2.33.0",
"request": "~2.34.0",
"tumblr.js": "~0.0.4",
"twit": "~1.1.12",
"underscore": "~1.6.0",
"paypal-rest-sdk": "~0.6.4",
"connect-mongo": "~0.4.0",
"twilio": "~1.5.0",
"validator": "~3.2.1",
"clockwork": "0.1.0"
"clockwork": ">=0.1.0",
"validator": "~3.3.0",
"csso": "~1.3.11",
"uglify-js": "~2.4.12"
},
"devDependencies": {
"chai": "~1.9.0",
"mocha": "~1.17.1",
"supertest": "~0.9.0"
}
}

View File

@ -38,7 +38,8 @@
// Optional: Group multiple button groups together for a toolbar
.btn-toolbar {
margin-left: -5px; // Offset the first child's margin
&:extend(.clearfix all);
&:extend(.clearfix all)
;
.btn-group,
.input-group {
@ -62,6 +63,7 @@
.border-right-radius(0);
}
}
// Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it
.btn-group > .btn:last-child:not(:first-child),
.btn-group > .dropdown-toggle:not(:first-child) {
@ -72,15 +74,18 @@
.btn-group > .btn-group {
float: left;
}
.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {
border-radius: 0;
}
.btn-group > .btn-group:first-child {
> .btn:last-child,
> .dropdown-toggle {
.border-right-radius(0);
}
}
.btn-group > .btn-group:last-child > .btn:first-child {
.border-left-radius(0);
}
@ -91,15 +96,24 @@
outline: 0;
}
// Sizing
//
// Remix the default button sizing classes into new ones for easier manipulation.
.btn-group-xs > .btn { .btn-xs(); }
.btn-group-sm > .btn { .btn-sm(); }
.btn-group-lg > .btn { .btn-lg(); }
.btn-group-xs > .btn {
&:extend(.btn-xs)
;
}
.btn-group-sm > .btn {
&:extend(.btn-sm)
;
}
.btn-group-lg > .btn {
&:extend(.btn-lg)
;
}
// Split button dropdowns
// ----------------------
@ -109,6 +123,7 @@
padding-left: 8px;
padding-right: 8px;
}
.btn-group > .btn-lg + .dropdown-toggle {
padding-left: 12px;
padding-right: 12px;
@ -125,22 +140,22 @@
}
}
// Reposition the caret
.btn .caret {
margin-left: 0;
}
// Carets in other button sizes
.btn-lg .caret {
border-width: @caret-width-large @caret-width-large 0;
border-bottom-width: 0;
}
// Upside down carets for .dropup
.dropup .btn-lg .caret {
border-width: 0 @caret-width-large @caret-width-large;
}
// Vertical button groups
// ----------------------
@ -156,7 +171,8 @@
// Clear floats so dropdown menus can be properly placed
> .btn-group {
&:extend(.clearfix all);
&:extend(.clearfix all)
;
> .btn {
float: none;
}
@ -184,21 +200,22 @@
.border-top-radius(0);
}
}
.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {
border-radius: 0;
}
.btn-group-vertical > .btn-group:first-child:not(:last-child) {
> .btn:last-child,
> .dropdown-toggle {
.border-bottom-radius(0);
}
}
.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {
.border-top-radius(0);
}
// Justified button groups
// ----------------------
@ -218,7 +235,6 @@
}
}
// Checkbox and radio options
[data-toggle="buttons"] > .btn > input[type="radio"],
[data-toggle="buttons"] > .btn > input[type="checkbox"] {

View File

@ -2,7 +2,6 @@
// Buttons
// --------------------------------------------------
// Base styles
// --------------------------------------------------
@ -19,9 +18,13 @@
.button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @border-radius-base);
.user-select(none);
&,
&:active,
&.active {
&:focus {
.tab-focus();
}
}
&:hover,
&:focus {
@ -46,34 +49,37 @@
}
}
// Alternate buttons
// --------------------------------------------------
.btn-default {
.button-variant(@btn-default-color; @btn-default-bg; @btn-default-border);
}
.btn-primary {
.button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border);
}
// Success appears as green
.btn-success {
.button-variant(@btn-success-color; @btn-success-bg; @btn-success-border);
}
// Info appears as blue-green
.btn-info {
.button-variant(@btn-info-color; @btn-info-bg; @btn-info-border);
}
// Warning appears as orange
.btn-warning {
.button-variant(@btn-warning-color; @btn-warning-bg; @btn-warning-border);
}
// Danger and error appear as red
.btn-danger {
.button-variant(@btn-danger-color; @btn-danger-bg; @btn-danger-border);
}
// Link buttons
// -------------------------
@ -113,7 +119,6 @@
}
}
// Button Sizes
// --------------------------------------------------
@ -121,15 +126,16 @@
// line-height: ensure even-numbered height of button next to large input
.button-size(@padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);
}
.btn-sm {
// line-height: ensure proper height of button next to small input
.button-size(@padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @border-radius-small);
}
.btn-xs {
.button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @border-radius-small);
}
// Block button
// --------------------------------------------------

View File

@ -2,7 +2,6 @@
// Carousel
// --------------------------------------------------
// Wrapper for the slide container and indicators
.carousel {
position: relative;
@ -21,14 +20,17 @@
// Account for jankitude on images
> img,
> a > img {
.img-responsive();
&:extend(.img-responsive)
;
line-height: 1;
}
}
> .active,
> .next,
> .prev { display: block; }
> .prev {
display: block;
}
> .active {
left: 0;
@ -200,14 +202,13 @@
}
}
// Scale up controls for tablets and up
@media screen and (min-width: @screen-sm-min) {
// Scale up the controls a smidge
.carousel-control {
.glyphicons-chevron-left,
.glyphicons-chevron-right,
.glyphicon-chevron-left,
.glyphicon-chevron-right,
.icon-prev,
.icon-next {
width: 30px;

View File

@ -2,7 +2,6 @@
// Forms
// --------------------------------------------------
// Normalize non-controls
//
// Restyle and baseline non-control form elements.
@ -35,7 +34,6 @@ label {
font-weight: bold;
}
// Normalize form controls
//
// While most of our form styles require extra classes, some basic normalization
@ -88,7 +86,6 @@ output {
color: @input-color;
}
// Common form controls
//
// Shared size and type resets for form controls. Apply `.form-control` to any
@ -133,9 +130,10 @@ output {
.placeholder();
// Disabled and read-only inputs
// Note: HTML5 says that controls under a fieldset > legend:first-child won't
// be disabled if the fieldset is disabled. Due to implementation difficulty,
// we don't honor that edge case; we style them as disabled anyway.
//
// HTML5 says that controls under a fieldset > legend:first-child won't be
// disabled if the fieldset is disabled. Due to implementation difficulty, we
// don't honor that edge case; we style them as disabled anyway.
&[disabled],
&[readonly],
fieldset[disabled] & {
@ -150,15 +148,26 @@ output {
}
}
// Search inputs in iOS
//
// This overrides the extra rounded corners on search inputs in iOS so that our
// `.form-control` class can properly style them. Note that this cannot simply
// be added to `.form-control` as it's not specific enough. For details, see
// https://github.com/twbs/bootstrap/issues/11586.
input[type="search"] {
-webkit-appearance: none;
}
// Special styles for iOS date input
//
// In Mobile Safari, date inputs require a pixel line-height that matches the
// given height of the input.
input[type="date"] {
line-height: @input-height-base;
}
// Form groups
//
// Designed to help with the organization and spacing of vertical forms. For
@ -168,7 +177,6 @@ input[type="date"] {
margin-bottom: 15px;
}
// Checkboxes and radios
//
// Indent the labels to position radios/checkboxes as hanging controls.
@ -186,6 +194,7 @@ input[type="date"] {
cursor: pointer;
}
}
.radio input[type="radio"],
.radio-inline input[type="radio"],
.checkbox input[type="checkbox"],
@ -193,6 +202,7 @@ input[type="date"] {
float: left;
margin-left: -20px;
}
.radio + .radio,
.checkbox + .checkbox {
margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing
@ -208,6 +218,7 @@ input[type="date"] {
font-weight: normal;
cursor: pointer;
}
.radio-inline + .radio-inline,
.checkbox-inline + .checkbox-inline {
margin-top: 0;
@ -229,7 +240,6 @@ input[type="checkbox"],
}
}
// Form control sizing
//
// Build on `.form-control` with modifier classes to decrease or increase the
@ -243,7 +253,6 @@ input[type="checkbox"],
.input-size(@input-height-large; @padding-large-vertical; @padding-large-horizontal; @font-size-large; @line-height-large; @border-radius-large);
}
// Form control feedback states
//
// Apply contextual and semantic states to individual form controls.
@ -274,14 +283,15 @@ input[type="checkbox"],
.has-success {
.form-control-validation(@state-success-text; @state-success-text; @state-success-bg);
}
.has-warning {
.form-control-validation(@state-warning-text; @state-warning-text; @state-warning-bg);
}
.has-error {
.form-control-validation(@state-danger-text; @state-danger-text; @state-danger-bg);
}
// Static form control text
//
// Apply class to a `p` element to make any string of text align with labels in
@ -291,7 +301,6 @@ input[type="checkbox"],
margin-bottom: 0; // Remove default margin from `p`
}
// Help text
//
// Apply to any element you wish to create light text for placement immediately
@ -304,8 +313,6 @@ input[type="checkbox"],
color: lighten(@text-color, 25%); // lighten the text some for contrast
}
// Inline forms
//
// Make forms appear inline(-block) by adding the `.form-inline` class. Inline
@ -335,6 +342,11 @@ input[type="checkbox"],
vertical-align: middle;
}
// Input groups need that 100% width though
.input-group > .form-control {
width: 100%;
}
.control-label {
margin-bottom: 0;
vertical-align: middle;
@ -351,6 +363,7 @@ input[type="checkbox"],
padding-left: 0;
vertical-align: middle;
}
.radio input[type="radio"],
.checkbox input[type="checkbox"] {
float: none;
@ -367,7 +380,6 @@ input[type="checkbox"],
}
}
// Horizontal forms
//
// Horizontal forms are built on grid classes and allow you to create forms with

View File

@ -2,7 +2,6 @@
// Grid system
// --------------------------------------------------
// Container widths
//
// Set the container width, and override it for fixed navbars in media queries.
@ -21,7 +20,6 @@
}
}
// Fluid container
//
// Utilizes the mixin meant for fixed width containers, but without any defined
@ -31,7 +29,6 @@
.container-fixed();
}
// Row
//
// Rows contain and clear the floats of your columns.
@ -40,25 +37,18 @@
.make-row();
}
// Columns
//
// Common styles for small and large grid columns
.make-grid-columns();
// Extra small grid
//
// Columns, offsets, pushes, and pulls for extra small devices like
// smartphones.
.make-grid-columns-float(xs);
.make-grid(@grid-columns, xs, width);
.make-grid(@grid-columns, xs, pull);
.make-grid(@grid-columns, xs, push);
.make-grid(@grid-columns, xs, offset);
.make-grid(xs);
// Small grid
//
@ -66,35 +56,21 @@
// to tablets.
@media (min-width: @screen-sm-min) {
.make-grid-columns-float(sm);
.make-grid(@grid-columns, sm, width);
.make-grid(@grid-columns, sm, pull);
.make-grid(@grid-columns, sm, push);
.make-grid(@grid-columns, sm, offset);
.make-grid(sm);
}
// Medium grid
//
// Columns, offsets, pushes, and pulls for the desktop device range.
@media (min-width: @screen-md-min) {
.make-grid-columns-float(md);
.make-grid(@grid-columns, md, width);
.make-grid(@grid-columns, md, pull);
.make-grid(@grid-columns, md, push);
.make-grid(@grid-columns, md, offset);
.make-grid(md);
}
// Large grid
//
// Columns, offsets, pushes, and pulls for the large desktop device range.
@media (min-width: @screen-lg-min) {
.make-grid-columns-float(lg);
.make-grid(@grid-columns, lg, width);
.make-grid(@grid-columns, lg, pull);
.make-grid(@grid-columns, lg, push);
.make-grid(@grid-columns, lg, offset);
.make-grid(lg);
}

View File

@ -17,6 +17,11 @@
}
.form-control {
// Ensure that the input is always above the *appended* addon button for
// proper border colors.
position: relative;
z-index: 2;
// IE9 fubars the placeholder attribute in text inputs and the arrows on
// select elements in input groups. To fix it, we float the input. Details:
// https://github.com/twbs/bootstrap/issues/11561#issuecomment-28936855
@ -34,11 +39,15 @@
.input-group-lg > .form-control,
.input-group-lg > .input-group-addon,
.input-group-lg > .input-group-btn > .btn { .input-lg(); }
.input-group-lg > .input-group-btn > .btn {
.input-lg();
}
.input-group-sm > .form-control,
.input-group-sm > .input-group-addon,
.input-group-sm > .input-group-btn > .btn { .input-sm(); }
.input-group-sm > .input-group-btn > .btn {
.input-sm();
}
// Display as table-cell
// -------------------------
@ -51,6 +60,7 @@
border-radius: 0;
}
}
// Addon and addon wrapper for buttons
.input-group-addon,
.input-group-btn {
@ -101,9 +111,11 @@
.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {
.border-right-radius(0);
}
.input-group-addon:first-child {
border-right: 0;
}
.input-group .form-control:last-child,
.input-group-addon:last-child,
.input-group-btn:last-child > .btn,
@ -113,6 +125,7 @@
.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {
.border-left-radius(0);
}
.input-group-addon:last-child {
border-left: 0;
}

View File

@ -2,7 +2,6 @@
// Mixins
// --------------------------------------------------
// Utilities
// -------------------------
@ -48,17 +47,26 @@
width: @width;
height: @height;
}
.square(@size) {
.size(@size; @size);
}
// Placeholder text
.placeholder(@color: @input-color-placeholder) {
&:-moz-placeholder { color: @color; } // Firefox 4-18
&::-moz-placeholder { color: @color; // Firefox 19+
opacity: 1; } // See https://github.com/twbs/bootstrap/pull/11526
&:-ms-input-placeholder { color: @color; } // Internet Explorer 10+
&::-webkit-input-placeholder { color: @color; } // Safari and Chrome
&::-moz-placeholder {
color: @color; // Firefox
opacity: 1;
}
// See https://github.com/twbs/bootstrap/pull/11526
&:-ms-input-placeholder {
color: @color;
}
// Internet Explorer 10+
&::-webkit-input-placeholder {
color: @color;
}
// Safari and Chrome
}
// Text overflow
@ -86,13 +94,12 @@
background-color: transparent;
border: 0;
}
// New mixin to use as of v3.0.1
.text-hide() {
.hide-text();
}
// CSS3 PROPERTIES
// --------------------------------------------------
@ -101,14 +108,17 @@
border-top-right-radius: @radius;
border-top-left-radius: @radius;
}
.border-right-radius(@radius) {
border-bottom-right-radius: @radius;
border-top-right-radius: @radius;
}
.border-bottom-radius(@radius) {
border-bottom-right-radius: @radius;
border-bottom-left-radius: @radius;
}
.border-left-radius(@radius) {
border-bottom-left-radius: @radius;
border-top-left-radius: @radius;
@ -129,18 +139,22 @@
-webkit-transition: @transition;
transition: @transition;
}
.transition-property(@transition-property) {
-webkit-transition-property: @transition-property;
transition-property: @transition-property;
}
.transition-delay(@transition-delay) {
-webkit-transition-delay: @transition-delay;
transition-delay: @transition-delay;
}
.transition-duration(@transition-duration) {
-webkit-transition-duration: @transition-duration;
transition-duration: @transition-duration;
}
.transition-transform(@transition) {
-webkit-transition: -webkit-transform @transition;
-moz-transition: -moz-transform @transition;
@ -154,21 +168,25 @@
-ms-transform: rotate(@degrees); // IE9 only
transform: rotate(@degrees);
}
.scale(@ratio; @ratio-y...) {
-webkit-transform: scale(@ratio, @ratio-y);
-ms-transform: scale(@ratio, @ratio-y); // IE9 only
transform: scale(@ratio, @ratio-y);
}
.translate(@x; @y) {
-webkit-transform: translate(@x, @y);
-ms-transform: translate(@x, @y); // IE9 only
transform: translate(@x, @y);
}
.skew(@x; @y) {
-webkit-transform: skew(@x, @y);
-ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+
transform: skew(@x, @y);
}
.translate3d(@x; @y; @z) {
-webkit-transform: translate3d(@x, @y, @z);
transform: translate3d(@x, @y, @z);
@ -179,21 +197,25 @@
-ms-transform: rotateX(@degrees); // IE9 only
transform: rotateX(@degrees);
}
.rotateY(@degrees) {
-webkit-transform: rotateY(@degrees);
-ms-transform: rotateY(@degrees); // IE9 only
transform: rotateY(@degrees);
}
.perspective(@perspective) {
-webkit-perspective: @perspective;
-moz-perspective: @perspective;
perspective: @perspective;
}
.perspective-origin(@perspective) {
-webkit-perspective-origin: @perspective;
-moz-perspective-origin: @perspective;
perspective-origin: @perspective;
}
.transform-origin(@origin) {
-webkit-transform-origin: @origin;
-moz-transform-origin: @origin;
@ -206,26 +228,32 @@
-webkit-animation: @animation;
animation: @animation;
}
.animation-name(@name) {
-webkit-animation-name: @name;
animation-name: @name;
}
.animation-duration(@duration) {
-webkit-animation-duration: @duration;
animation-duration: @duration;
}
.animation-timing-function(@timing-function) {
-webkit-animation-timing-function: @timing-function;
animation-timing-function: @timing-function;
}
.animation-delay(@delay) {
-webkit-animation-delay: @delay;
animation-delay: @delay;
}
.animation-iteration-count(@iteration-count) {
-webkit-animation-iteration-count: @iteration-count;
animation-iteration-count: @iteration-count;
}
.animation-direction(@direction) {
-webkit-animation-direction: @direction;
animation-direction: @direction;
@ -253,7 +281,6 @@
-webkit-user-select: @select;
-moz-user-select: @select;
-ms-user-select: @select; // IE10+
-o-user-select: @select;
user-select: @select;
}
@ -291,8 +318,6 @@
filter: ~"alpha(opacity=@{opacity-ie})";
}
// GRADIENTS
// --------------------------------------------------
@ -356,8 +381,6 @@
filter: e(%("progid:DXImageTransform.Microsoft.gradient(enabled = false)"));
}
// Retina images
//
// Short retina mixin for setting background-image and -size
@ -365,19 +388,12 @@
.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {
background-image: url("@{file-1x}");
@media
only screen and (-webkit-min-device-pixel-ratio: 2),
only screen and ( min--moz-device-pixel-ratio: 2),
only screen and ( -o-min-device-pixel-ratio: 2/1),
only screen and ( min-device-pixel-ratio: 2),
only screen and ( min-resolution: 192dpi),
only screen and ( min-resolution: 2dppx) {
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and ( min--moz-device-pixel-ratio: 2), only screen and ( -o-min-device-pixel-ratio: 2/1), only screen and ( min-device-pixel-ratio: 2), only screen and ( min-resolution: 192dpi), only screen and ( min-resolution: 2dppx) {
background-image: url("@{file-2x}");
background-size: @width-1x @height-1x;
}
}
// Responsive image
//
// Keep images from scaling beyond the width of their parents.
@ -388,7 +404,6 @@
height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching
}
// COMPONENT MIXINS
// --------------------------------------------------
@ -476,7 +491,9 @@
a& {
color: @color;
.list-group-item-heading { color: inherit; }
.list-group-item-heading {
color: inherit;
}
&:hover,
&:focus {
@ -622,20 +639,22 @@
// More easily include all the states for responsive-utilities.less.
.responsive-visibility() {
display: block !important;
table& { display: table; }
tr& { display: table-row !important; }
table& {
display: table;
}
tr& {
display: table-row !important;
}
th&,
td& { display: table-cell !important; }
td& {
display: table-cell !important;
}
}
.responsive-invisibility() {
&,
tr&,
th&,
td& { display: none !important; }
display: none !important;
}
// Grid System
// -----------
@ -645,14 +664,16 @@
margin-left: auto;
padding-left: (@grid-gutter-width / 2);
padding-right: (@grid-gutter-width / 2);
&:extend(.clearfix all);
&:extend(.clearfix all)
;
}
// Creates a wrapper for a series of columns
.make-row(@gutter: @grid-gutter-width) {
margin-left: (@gutter / -2);
margin-right: (@gutter / -2);
&:extend(.clearfix all);
&:extend(.clearfix all)
;
}
// Generate the extra small columns
@ -664,23 +685,25 @@
padding-left: (@gutter / 2);
padding-right: (@gutter / 2);
}
.make-xs-column-offset(@columns) {
@media (min-width: @screen-xs-min) {
margin-left: percentage((@columns / @grid-columns));
}
}
.make-xs-column-push(@columns) {
@media (min-width: @screen-xs-min) {
left: percentage((@columns / @grid-columns));
}
}
.make-xs-column-pull(@columns) {
@media (min-width: @screen-xs-min) {
right: percentage((@columns / @grid-columns));
}
}
// Generate the small columns
.make-sm-column(@columns; @gutter: @grid-gutter-width) {
position: relative;
@ -693,23 +716,25 @@
width: percentage((@columns / @grid-columns));
}
}
.make-sm-column-offset(@columns) {
@media (min-width: @screen-sm-min) {
margin-left: percentage((@columns / @grid-columns));
}
}
.make-sm-column-push(@columns) {
@media (min-width: @screen-sm-min) {
left: percentage((@columns / @grid-columns));
}
}
.make-sm-column-pull(@columns) {
@media (min-width: @screen-sm-min) {
right: percentage((@columns / @grid-columns));
}
}
// Generate the medium columns
.make-md-column(@columns; @gutter: @grid-gutter-width) {
position: relative;
@ -722,23 +747,25 @@
width: percentage((@columns / @grid-columns));
}
}
.make-md-column-offset(@columns) {
@media (min-width: @screen-md-min) {
margin-left: percentage((@columns / @grid-columns));
}
}
.make-md-column-push(@columns) {
@media (min-width: @screen-md-min) {
left: percentage((@columns / @grid-columns));
}
}
.make-md-column-pull(@columns) {
@media (min-width: @screen-md-min) {
right: percentage((@columns / @grid-columns));
}
}
// Generate the large columns
.make-lg-column(@columns; @gutter: @grid-gutter-width) {
position: relative;
@ -751,23 +778,25 @@
width: percentage((@columns / @grid-columns));
}
}
.make-lg-column-offset(@columns) {
@media (min-width: @screen-lg-min) {
margin-left: percentage((@columns / @grid-columns));
}
}
.make-lg-column-push(@columns) {
@media (min-width: @screen-lg-min) {
left: percentage((@columns / @grid-columns));
}
}
.make-lg-column-pull(@columns) {
@media (min-width: @screen-lg-min) {
right: percentage((@columns / @grid-columns));
}
}
// Framework grid generation
//
// Used only by Bootstrap to generate the correct number of grid classes given
@ -775,15 +804,18 @@
.make-grid-columns() {
// Common styles for all sizes of grid columns, widths 1-12
.col(@index) when (@index = 1) { // initial
.col(@index) when (@index = 1) {
// initial
@item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}";
.col((@index + 1), @item);
}
.col(@index, @list) when (@index =< @grid-columns) { // general; "=<" isn't a typo
.col(@index, @list) when (@index =< @grid-columns) {
// general; "=<" isn't a typo
@item: ~".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}";
.col((@index + 1), ~"@{list}, @{item}");
}
.col(@index, @list) when (@index > @grid-columns) { // terminal
.col(@index, @list) when (@index > @grid-columns) {
// terminal
@{list} {
position: relative;
// Prevent columns from collapsing when empty
@ -796,16 +828,19 @@
.col(1); // kickstart it
}
.make-grid-columns-float(@class) {
.col(@index) when (@index = 1) { // initial
.float-grid-columns(@class) {
.col(@index) when (@index = 1) {
// initial
@item: ~".col-@{class}-@{index}";
.col((@index + 1), @item);
}
.col(@index, @list) when (@index =< @grid-columns) { // general
.col(@index, @list) when (@index =< @grid-columns) {
// general
@item: ~".col-@{class}-@{index}";
.col((@index + 1), ~"@{list}, @{item}");
}
.col(@index, @list) when (@index > @grid-columns) { // terminal
.col(@index, @list) when (@index > @grid-columns) {
// terminal
@{list} {
float: left;
}
@ -813,34 +848,45 @@
.col(1); // kickstart it
}
.calc-grid(@index, @class, @type) when (@type = width) and (@index > 0) {
.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {
.col-@{class}-@{index} {
width: percentage((@index / @grid-columns));
}
}
.calc-grid(@index, @class, @type) when (@type = push) {
.calc-grid-column(@index, @class, @type) when (@type = push) {
.col-@{class}-push-@{index} {
left: percentage((@index / @grid-columns));
}
}
.calc-grid(@index, @class, @type) when (@type = pull) {
.calc-grid-column(@index, @class, @type) when (@type = pull) {
.col-@{class}-pull-@{index} {
right: percentage((@index / @grid-columns));
}
}
.calc-grid(@index, @class, @type) when (@type = offset) {
.calc-grid-column(@index, @class, @type) when (@type = offset) {
.col-@{class}-offset-@{index} {
margin-left: percentage((@index / @grid-columns));
}
}
// Basic looping in LESS
.make-grid(@index, @class, @type) when (@index >= 0) {
.calc-grid(@index, @class, @type);
.loop-grid-columns(@index, @class, @type) when (@index >= 0) {
.calc-grid-column(@index, @class, @type);
// next iteration
.make-grid((@index - 1), @class, @type);
.loop-grid-columns((@index - 1), @class, @type);
}
// Create grid for specific class
.make-grid(@class) {
.float-grid-columns(@class);
.loop-grid-columns(@grid-columns, @class, width);
.loop-grid-columns(@grid-columns, @class, pull);
.loop-grid-columns(@grid-columns, @class, push);
.loop-grid-columns(@grid-columns, @class, offset);
}
// Form validation states
//

View File

@ -34,7 +34,9 @@
.translate(0, -25%);
.transition-transform(~"0.3s ease-out");
}
&.in .modal-dialog { .translate(0, 0)}
&.in .modal-dialog {
.translate(0, 0)
}
}
// Shell div to position the modal with bottom padding
@ -67,8 +69,12 @@
z-index: @zindex-modal-background;
background-color: @modal-backdrop-bg;
// Fade for backdrop
&.fade { .opacity(0); }
&.in { .opacity(@modal-backdrop-opacity); }
&.fade {
.opacity(0);
}
&.in {
.opacity(@modal-backdrop-opacity);
}
}
// Modal header
@ -78,6 +84,7 @@
border-bottom: 1px solid @modal-header-border-color;
min-height: (@modal-title-padding + @modal-title-line-height);
}
// Close icon
.modal-header .close {
margin-top: -2px;
@ -102,7 +109,8 @@
padding: (@modal-inner-padding - 1) @modal-inner-padding @modal-inner-padding;
text-align: right; // right align buttons
border-top: 1px solid @modal-footer-border-color;
&:extend(.clearfix all); // clear it in case folks use .pull-* classes on buttons
&:extend(.clearfix all)
; // clear it in case folks use .pull-* classes on buttons
// Properly space out buttons
.btn + .btn {
@ -121,18 +129,24 @@
// Scale up the modal
@media (min-width: @screen-sm-min) {
// Automatically set modal's width for larger viewports
.modal-dialog {
width: @modal-md;
margin: 30px auto;
}
.modal-content {
.box-shadow(0 5px 15px rgba(0, 0, 0, .5));
}
// Modal sizes
.modal-sm { width: @modal-sm; }
.modal-lg { width: @modal-lg; }
.modal-sm {
width: @modal-sm;
}
}
@media (min-width: @screen-md-min) {
.modal-lg {
width: @modal-lg;
}
}

View File

@ -2,7 +2,6 @@
// Navbars
// --------------------------------------------------
// Wrapper and base class
//
// Provide a static navbar from which we expand to create full-width, fixed, and
@ -15,28 +14,28 @@
border: 1px solid transparent;
// Prevent floats from breaking the navbar
&:extend(.clearfix all);
&:extend(.clearfix all)
;
@media (min-width: @grid-float-breakpoint) {
border-radius: @navbar-border-radius;
}
}
// Navbar heading
//
// Groups `.navbar-brand` and `.navbar-toggle` into a single component for easy
// styling of responsive aspects.
.navbar-header {
&:extend(.clearfix all);
&:extend(.clearfix all)
;
@media (min-width: @grid-float-breakpoint) {
float: left;
}
}
// Navbar collapse (body)
//
// Group your navbar content into this for easy collapsing and expanding across
@ -54,7 +53,8 @@
padding-left: @navbar-padding-horizontal;
border-top: 1px solid transparent;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1);
&:extend(.clearfix all);
&:extend(.clearfix all)
;
-webkit-overflow-scrolling: touch;
&.in {
@ -88,7 +88,6 @@
}
}
// Both navbar header and collapse
//
// When a container is present, change the behavior of the header and collapse.
@ -107,7 +106,6 @@
}
}
//
// Navbar alignment options
//
@ -137,17 +135,18 @@
border-radius: 0;
}
}
.navbar-fixed-top {
top: 0;
border-width: 0 0 1px;
}
.navbar-fixed-bottom {
bottom: 0;
margin-bottom: 0; // override .navbar defaults
border-width: 1px 0 0;
}
// Brand/project name
.navbar-brand {
@ -155,7 +154,7 @@
padding: @navbar-padding-vertical @navbar-padding-horizontal;
font-size: @font-size-large;
line-height: @line-height-computed;
height: @line-height-computed;
height: @navbar-height;
&:hover,
&:focus {
@ -170,7 +169,6 @@
}
}
// Navbar toggle
//
// Custom button for toggling the `.navbar-collapse`, powered by the collapse
@ -209,7 +207,6 @@
}
}
// Navbar nav links
//
// Builds on top of the `.nav` components with its own modifier class to make
@ -267,7 +264,6 @@
}
}
// Component alignment
//
// Repurpose the pull utilities as their own navbar utilities to avoid specificity
@ -275,10 +271,14 @@
// though so that navbar contents properly stack and align in mobile.
@media (min-width: @grid-float-breakpoint) {
.navbar-left { .pull-left(); }
.navbar-right { .pull-right(); }
.navbar-left {
.pull-left();
}
.navbar-right {
.pull-right();
}
}
// Navbar form
//
@ -323,7 +323,6 @@
}
}
// Dropdown menus
// Menu position and menu carets
@ -331,12 +330,12 @@
margin-top: 0;
.border-top-radius(0);
}
// Menu position and menu caret support for dropups via extra dropup class
.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {
.border-bottom-radius(0);
}
// Buttons in navbars
//
// Vertically center a button within a navbar (when *not* in a form).
@ -352,7 +351,6 @@
}
}
// Text in navbars
//
// Add a class to make any element properly align itself vertically within the navbars.
@ -480,7 +478,6 @@
}
}
// Links in navbars
//
// Add a class to ensure links outside the navbar nav are colored correctly.

View File

@ -2,7 +2,6 @@
// Panels
// --------------------------------------------------
// Base class
.panel {
margin-bottom: @line-height-computed;
@ -15,9 +14,40 @@
// Panel contents
.panel-body {
padding: @panel-body-padding;
&:extend(.clearfix all);
&:extend(.clearfix all)
;
}
// Optional heading
.panel-heading {
padding: 10px 15px;
border-bottom: 1px solid transparent;
.border-top-radius((@panel-border-radius - 1));
> .dropdown .dropdown-toggle {
color: inherit;
}
}
// Within heading, strip any `h*` tag of its default margins for spacing.
.panel-title {
margin-top: 0;
margin-bottom: 0;
font-size: ceil((@font-size-base * 1.125));
color: inherit;
> a {
color: inherit;
}
}
// Optional footer (stays gray in every modifier class)
.panel-footer {
padding: 10px 15px;
background-color: @panel-footer-bg;
border-top: 1px solid @panel-inner-border;
.border-bottom-radius((@panel-border-radius - 1));
}
// List groups in panels
//
@ -27,30 +57,29 @@
.panel {
> .list-group {
margin-bottom: 0;
.list-group-item {
border-width: 1px 0;
border-radius: 0;
&:first-child {
border-top: 0;
}
&:last-child {
border-bottom: 0;
}
}
// Add border top radius for first one
&:first-child {
.list-group-item:first-child {
border-top: 0;
.border-top-radius((@panel-border-radius - 1));
}
}
// Add border bottom radius for last one
&:last-child {
.list-group-item:last-child {
border-bottom: 0;
.border-bottom-radius((@panel-border-radius - 1));
}
}
}
}
// Collapse space between when there's no additional content.
.panel-heading + .list-group {
.list-group-item:first-child {
@ -58,7 +87,6 @@
}
}
// Tables in panels
//
// Place a non-bordered `.table` within a panel (not within a `.panel-body`) and
@ -72,6 +100,8 @@
// Add border top radius for first one
> .table:first-child,
> .table-responsive:first-child > .table:first-child {
.border-top-radius((@panel-border-radius - 1));
> thead:first-child,
> tbody:first-child {
> tr:first-child {
@ -89,6 +119,8 @@
// Add border bottom radius for last one
> .table:last-child,
> .table-responsive:last-child > .table:last-child {
.border-bottom-radius((@panel-border-radius - 1));
> tbody:last-child,
> tfoot:last-child {
> tr:last-child {
@ -126,12 +158,22 @@
> td:last-child {
border-right: 0;
}
&:first-child > th,
&:first-child > td {
border-top: 0;
}
&:last-child > th,
&:last-child > td {
}
> thead,
> tbody {
> tr:first-child {
> td,
> th {
border-bottom: 0;
}
}
}
> tbody,
> tfoot {
> tr:last-child {
> td,
> th {
border-bottom: 0;
}
}
@ -143,39 +185,6 @@
}
}
// Optional heading
.panel-heading {
padding: 10px 15px;
border-bottom: 1px solid transparent;
.border-top-radius((@panel-border-radius - 1));
> .dropdown .dropdown-toggle {
color: inherit;
}
}
// Within heading, strip any `h*` tag of its default margins for spacing.
.panel-title {
margin-top: 0;
margin-bottom: 0;
font-size: ceil((@font-size-base * 1.125));
color: inherit;
> a {
color: inherit;
}
}
// Optional footer (stays gray in every modifier class)
.panel-footer {
padding: 10px 15px;
background-color: @panel-footer-bg;
border-top: 1px solid @panel-inner-border;
.border-bottom-radius((@panel-border-radius - 1));
}
// Collapsable panels (aka, accordion)
//
// Wrap a series of panels in `.panel-group` to turn them into an accordion with
@ -208,23 +217,27 @@
}
}
// Contextual variations
.panel-default {
.panel-variant(@panel-default-border; @panel-default-text; @panel-default-heading-bg; @panel-default-border);
}
.panel-primary {
.panel-variant(@panel-primary-border; @panel-primary-text; @panel-primary-heading-bg; @panel-primary-border);
}
.panel-success {
.panel-variant(@panel-success-border; @panel-success-text; @panel-success-heading-bg; @panel-success-border);
}
.panel-info {
.panel-variant(@panel-info-border; @panel-info-text; @panel-info-heading-bg; @panel-info-border);
}
.panel-warning {
.panel-variant(@panel-warning-border; @panel-warning-text; @panel-warning-heading-bg; @panel-warning-border);
}
.panel-danger {
.panel-variant(@panel-danger-border; @panel-danger-text; @panel-danger-heading-bg; @panel-danger-border);
}

View File

@ -2,7 +2,6 @@
// Popovers
// --------------------------------------------------
.popover {
position: absolute;
top: 0;
@ -23,10 +22,18 @@
white-space: normal;
// Offset the popover to account for the popover arrow
&.top { margin-top: -10px; }
&.right { margin-left: 10px; }
&.bottom { margin-top: 10px; }
&.left { margin-left: -10px; }
&.top {
margin-top: -@popover-arrow-width;
}
&.right {
margin-left: @popover-arrow-width;
}
&.bottom {
margin-top: @popover-arrow-width;
}
&.left {
margin-left: -@popover-arrow-width;
}
}
.popover-title {
@ -48,7 +55,7 @@
//
// .arrow is outer, .arrow:after is inner
.popover .arrow {
.popover > .arrow {
&,
&:after {
position: absolute;
@ -59,16 +66,18 @@
border-style: solid;
}
}
.popover .arrow {
.popover > .arrow {
border-width: @popover-arrow-outer-width;
}
.popover .arrow:after {
.popover > .arrow:after {
border-width: @popover-arrow-width;
content: "";
}
.popover {
&.top .arrow {
&.top > .arrow {
left: 50%;
margin-left: -@popover-arrow-outer-width;
border-bottom-width: 0;
@ -83,7 +92,7 @@
border-top-color: @popover-arrow-color;
}
}
&.right .arrow {
&.right > .arrow {
top: 50%;
left: -@popover-arrow-outer-width;
margin-top: -@popover-arrow-outer-width;
@ -98,7 +107,7 @@
border-right-color: @popover-arrow-color;
}
}
&.bottom .arrow {
&.bottom > .arrow {
left: 50%;
margin-left: -@popover-arrow-outer-width;
border-top-width: 0;
@ -114,7 +123,7 @@
}
}
&.left .arrow {
&.left > .arrow {
top: 50%;
right: -@popover-arrow-outer-width;
margin-top: -@popover-arrow-outer-width;

View File

@ -2,7 +2,6 @@
// Responsive: Utility classes
// --------------------------------------------------
// IE10 in Windows (Phone) 8
//
// Support for responsive views via media queries is kind of borked in IE10, for
@ -21,32 +20,33 @@
width: device-width;
}
// Visibility utilities
.visible-xs {
.visible-xs,
.visible-sm,
.visible-md,
.visible-lg {
.responsive-invisibility();
}
.visible-xs {
@media (max-width: @screen-xs-max) {
.responsive-visibility();
}
}
.visible-sm {
.responsive-invisibility();
.visible-sm {
@media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {
.responsive-visibility();
}
}
.visible-md {
.responsive-invisibility();
.visible-md {
@media (min-width: @screen-md-min) and (max-width: @screen-md-max) {
.responsive-visibility();
}
}
.visible-lg {
.responsive-invisibility();
.visible-lg {
@media (min-width: @screen-lg-min) {
.responsive-visibility();
}
@ -57,23 +57,25 @@
.responsive-invisibility();
}
}
.hidden-sm {
@media (min-width: @screen-sm-min) and (max-width: @screen-sm-max) {
.responsive-invisibility();
}
}
.hidden-md {
@media (min-width: @screen-md-min) and (max-width: @screen-md-max) {
.responsive-invisibility();
}
}
.hidden-lg {
@media (min-width: @screen-lg-min) {
.responsive-invisibility();
}
}
// Print utilities
//
// Media queries are placed on the inside to be mixin-friendly.

View File

@ -2,7 +2,6 @@
// Thumbnails
// --------------------------------------------------
// Mixin and adjust the regular image class
.thumbnail {
display: block;
@ -16,7 +15,8 @@
> img,
a > img {
.img-responsive();
&:extend(.img-responsive)
;
margin-left: auto;
margin-right: auto;
}

View File

@ -2,7 +2,6 @@
// Typography
// --------------------------------------------------
// Headings
// -------------------------
@ -32,6 +31,7 @@ h3, .h3 {
font-size: 65%;
}
}
h4, .h4,
h5, .h5,
h6, .h6 {
@ -44,13 +44,29 @@ h6, .h6 {
}
}
h1, .h1 { font-size: @font-size-h1; }
h2, .h2 { font-size: @font-size-h2; }
h3, .h3 { font-size: @font-size-h3; }
h4, .h4 { font-size: @font-size-h4; }
h5, .h5 { font-size: @font-size-h5; }
h6, .h6 { font-size: @font-size-h6; }
h1, .h1 {
font-size: @font-size-h1;
}
h2, .h2 {
font-size: @font-size-h2;
}
h3, .h3 {
font-size: @font-size-h3;
}
h4, .h4 {
font-size: @font-size-h4;
}
h5, .h5 {
font-size: @font-size-h5;
}
h6, .h6 {
font-size: @font-size-h6;
}
// Body text
// -------------------------
@ -70,39 +86,58 @@ p {
}
}
// Emphasis & misc
// -------------------------
// Ex: 14px base font * 85% = about 12px
small,
.small { font-size: 85%; }
.small {
font-size: 85%;
}
// Undo browser default styling
cite { font-style: normal; }
cite {
font-style: normal;
}
// Alignment
.text-left { text-align: left; }
.text-right { text-align: right; }
.text-center { text-align: center; }
.text-justify { text-align: justify; }
.text-left {
text-align: left;
}
.text-right {
text-align: right;
}
.text-center {
text-align: center;
}
.text-justify {
text-align: justify;
}
// Contextual colors
.text-muted {
color: @text-muted;
}
.text-primary {
.text-emphasis-variant(@brand-primary);
}
.text-success {
.text-emphasis-variant(@state-success-text);
}
.text-info {
.text-emphasis-variant(@state-info-text);
}
.text-warning {
.text-emphasis-variant(@state-warning-text);
}
.text-danger {
.text-emphasis-variant(@state-danger-text);
}
@ -116,20 +151,23 @@ cite { font-style: normal; }
color: #fff;
.bg-variant(@brand-primary);
}
.bg-success {
.bg-variant(@state-success-bg);
}
.bg-info {
.bg-variant(@state-info-bg);
}
.bg-warning {
.bg-variant(@state-warning-bg);
}
.bg-danger {
.bg-variant(@state-danger-bg);
}
// Page header
// -------------------------
@ -139,7 +177,6 @@ cite { font-style: normal; }
border-bottom: 1px solid @page-header-border-color;
}
// Lists
// --------------------------------------------------
@ -165,15 +202,12 @@ ol {
// Inline turns list items into inline-block
.list-inline {
.list-unstyled();
margin-left: -5px;
> li {
display: inline-block;
padding-left: 5px;
padding-right: 5px;
&:first-child {
padding-left: 0;
}
}
}
@ -182,13 +216,16 @@ dl {
margin-top: 0; // Remove browser default
margin-bottom: @line-height-computed;
}
dt,
dd {
line-height: @line-height-base;
}
dt {
font-weight: bold;
}
dd {
margin-left: 0; // Undo browser default
}
@ -209,7 +246,8 @@ dd {
}
dd {
margin-left: @component-offset-horizontal;
&:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present
&:extend(.clearfix all)
; // Clear the floated `dt` if an empty `dd` is present
}
}
}
@ -224,6 +262,7 @@ abbr[data-original-title] {
cursor: help;
border-bottom: 1px dotted @abbr-border-color;
}
.initialism {
font-size: 90%;
text-transform: uppercase;
@ -233,7 +272,7 @@ abbr[data-original-title] {
blockquote {
padding: (@line-height-computed / 2) @line-height-computed;
margin: 0 0 @line-height-computed;
font-size: (@font-size-base * 1.25);
font-size: @blockquote-font-size;
border-left: 5px solid @blockquote-border-color;
p,
@ -275,7 +314,9 @@ blockquote.pull-right {
footer,
small,
.small {
&:before { content: ''; }
&:before {
content: '';
}
&:after {
content: '\00A0 \2014'; // nbsp, em dash
}

View File

@ -2,16 +2,20 @@
// Variables
// --------------------------------------------------
//== Colors
//
//## Gray and brand colors for use across Bootstrap.
@gray-darker: lighten(#000, 13.5%); // #222
@gray-dark: lighten(#000, 20%); // #333
@gray: lighten(#000, 33.5%); // #555
@gray-light: lighten(#000, 60%); // #999
@gray-lighter: lighten(#000, 93.5%); // #eee
@gray-darker: lighten(#000, 13.5%);
// #222
@gray-dark: lighten(#000, 20%);
// #333
@gray: lighten(#000, 33.5%);
// #555
@gray-light: lighten(#000, 60%);
// #999
@gray-lighter: lighten(#000, 93.5%);
// #eee
@brand-primary: #428bca;
@brand-success: #5cb85c;
@ -19,7 +23,6 @@
@brand-warning: #f0ad4e;
@brand-danger: #d9534f;
//== Scaffolding
//
// ## Settings for some of the most global styles.
@ -34,7 +37,6 @@
//** Link hover color set via `darken()` function.
@link-hover-color: darken(@link-color, 15%);
//== Typography
//
//## Font, line-height, and color for body text, headings, and more.
@ -46,20 +48,29 @@
@font-family-base: @font-family-sans-serif;
@font-size-base: 14px;
@font-size-large: ceil((@font-size-base * 1.25)); // ~18px
@font-size-small: ceil((@font-size-base * 0.85)); // ~12px
@font-size-large: ceil((@font-size-base * 1.25));
// ~18px
@font-size-small: ceil((@font-size-base * 0.85));
// ~12px
@font-size-h1: floor((@font-size-base * 2.6)); // ~36px
@font-size-h2: floor((@font-size-base * 2.15)); // ~30px
@font-size-h3: ceil((@font-size-base * 1.7)); // ~24px
@font-size-h4: ceil((@font-size-base * 1.25)); // ~18px
@font-size-h1: floor((@font-size-base * 2.6));
// ~36px
@font-size-h2: floor((@font-size-base * 2.15));
// ~30px
@font-size-h3: ceil((@font-size-base * 1.7));
// ~24px
@font-size-h4: ceil((@font-size-base * 1.25));
// ~18px
@font-size-h5: @font-size-base;
@font-size-h6: ceil((@font-size-base * 0.85)); // ~12px
@font-size-h6: ceil((@font-size-base * 0.85));
// ~12px
//** Unit-less `line-height` for use in components like buttons.
@line-height-base: 1.428571429; // 20/14
@line-height-base: 1.428571429;
// 20/14
//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
@line-height-computed: floor((@font-size-base * @line-height-base)); // ~20px
@line-height-computed: floor((@font-size-base * @line-height-base));
// ~20px
//** By default, this inherits from the `<body>`.
@headings-font-family: inherit;
@ -67,7 +78,6 @@
@headings-line-height: 1.1;
@headings-color: inherit;
//-- Iconography
//
//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower.
@ -109,7 +119,6 @@
//** Carets increase slightly in size for larger components.
@caret-width-large: 5px;
//== Tables
//
//## Customizes the `.table` component with basic values, each used across all table variations.
@ -130,7 +139,6 @@
//** Border color for table and cell borders.
@table-border-color: #ddd;
//== Buttons
//
//## For each of Bootstrap's buttons, define text, background and border color.
@ -163,7 +171,6 @@
@btn-link-disabled-color: @gray-light;
//== Forms
//
//##
@ -200,7 +207,6 @@
//** Border color for textual input addons
@input-group-addon-border-color: @input-border;
//== Dropdowns
//
//## Dropdown menu container and contents.
@ -235,7 +241,6 @@
// Note: Deprecated @dropdown-caret-color as of v3.1.0
@dropdown-caret-color: #000;
//-- Z-index master list
//
// Warning: Avoid customizing these values. They're used for a bird's eye view
@ -251,7 +256,6 @@
@zindex-modal-background: 1040;
@zindex-modal: 1050;
//== Media queries breakpoints
//
//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
@ -285,7 +289,6 @@
@screen-sm-max: (@screen-md-min - 1);
@screen-md-max: (@screen-lg-min - 1);
//== Grid system
//
//## Define your custom responsive grid.
@ -300,6 +303,24 @@
//** Point at which the navbar begins collapsing.
@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
//== Container sizes
//
//## Define the maximum width of `.container` for different screen sizes.
// Small screen / tablet
@container-tablet: ((720px + @grid-gutter-width));
//** For `@screen-sm-min` and up.
@container-sm: @container-tablet;
// Medium screen / desktop
@container-desktop: ((940px + @grid-gutter-width));
//** For `@screen-md-min` and up.
@container-md: @container-desktop;
// Large screen / wide desktop
@container-large-desktop: ((1140px + @grid-gutter-width));
//** For `@screen-lg-min` and up.
@container-lg: @container-large-desktop;
//== Navbar
//
@ -336,7 +357,6 @@
@navbar-default-toggle-icon-bar-bg: #888;
@navbar-default-toggle-border-color: #ddd;
// Inverted navbar
// Reset inverted navbar basics
@navbar-inverse-color: @gray-light;
@ -362,7 +382,6 @@
@navbar-inverse-toggle-icon-bar-bg: #fff;
@navbar-inverse-toggle-border-color: #333;
//== Navs
//
//##
@ -393,7 +412,6 @@
@nav-pills-active-link-hover-bg: @component-active-bg;
@nav-pills-active-link-hover-color: @component-active-color;
//== Pagination
//
//##
@ -414,7 +432,6 @@
@pagination-disabled-bg: #fff;
@pagination-disabled-border: #ddd;
//== Pager
//
//##
@ -430,7 +447,6 @@
@pager-disabled-color: @pagination-disabled-color;
//== Jumbotron
//
//##
@ -441,7 +457,6 @@
@jumbotron-heading-color: inherit;
@jumbotron-font-size: ceil((@font-size-base * 1.5));
//== Form states and alerts
//
//## Define colors for form feedback states and, by default, alerts.
@ -462,7 +477,6 @@
@state-danger-bg: #f2dede;
@state-danger-border: darken(spin(@state-danger-bg, -10), 5%);
//== Tooltips
//
//##
@ -480,7 +494,6 @@
//** Tooltip arrow color
@tooltip-arrow-color: @tooltip-bg;
//== Popovers
//
//##
@ -505,10 +518,9 @@
//** Popover outer arrow width
@popover-arrow-outer-width: (@popover-arrow-width + 1);
//** Popover outer arrow color
@popover-arrow-outer-color: rgba(0,0,0,.25);
@popover-arrow-outer-color: fadein(@popover-border-color, 5%);
//** Popover outer arrow fallback color
@popover-arrow-outer-fallback-color: #999;
@popover-arrow-outer-fallback-color: darken(@popover-fallback-border-color, 20%);
//== Labels
//
@ -532,7 +544,6 @@
//** Default text color of a linked label
@label-link-hover-color: #fff;
//== Modals
//
//##
@ -565,7 +576,6 @@
@modal-md: 600px;
@modal-sm: 300px;
//== Alerts
//
//## Define alert colors, border radius, and padding.
@ -590,7 +600,6 @@
@alert-danger-text: @state-danger-text;
@alert-danger-border: @state-danger-border;
//== Progress bars
//
//##
@ -611,7 +620,6 @@
//** Info progress bar color
@progress-bar-info-bg: @brand-info;
//== List group
//
//##
@ -636,7 +644,6 @@
@list-group-link-color: #555;
@list-group-link-heading-color: #333;
//== Panels
//
//##
@ -673,7 +680,6 @@
@panel-danger-border: @state-danger-border;
@panel-danger-heading-bg: @state-danger-bg;
//== Thumbnails
//
//##
@ -692,7 +698,6 @@
//** Padding around the thumbnail caption
@thumbnail-caption-padding: 9px;
//== Wells
//
//##
@ -700,7 +705,6 @@
@well-bg: #f5f5f5;
@well-border: darken(@well-bg, 7%);
//== Badges
//
//##
@ -719,7 +723,6 @@
@badge-line-height: 1;
@badge-border-radius: 10px;
//== Breadcrumbs
//
//##
@ -735,7 +738,6 @@
//** Textual separator for between breadcrumb elements
@breadcrumb-separator: "/";
//== Carousel
//
//##
@ -752,7 +754,6 @@
@carousel-caption-color: #fff;
//== Close
//
//##
@ -761,7 +762,6 @@
@close-color: #000;
@close-text-shadow: 0 1px 0 #fff;
//== Code
//
//##
@ -777,7 +777,6 @@
@pre-border-color: #ccc;
@pre-scrollable-max-height: 340px;
//== Type
//
//##
@ -790,12 +789,13 @@
@headings-small-color: @gray-light;
//** Blockquote small color
@blockquote-small-color: @gray-light;
//** Blockquote font size
@blockquote-font-size: (@font-size-base * 1.25);
//** Blockquote border color
@blockquote-border-color: @gray-lighter;
//** Page header border color
@page-header-border-color: @gray-lighter;
//== Miscellaneous
//
//##
@ -805,23 +805,3 @@
//** Horizontal offset for forms and lists.
@component-offset-horizontal: 180px;
//== Container sizes
//
//## Define the maximum width of `.container` for different screen sizes.
// Small screen / tablet
@container-tablet: ((720px + @grid-gutter-width));
//** For `@screen-sm-min` and up.
@container-sm: @container-tablet;
// Medium screen / desktop
@container-desktop: ((940px + @grid-gutter-width));
//** For `@screen-md-min` and up.
@container-md: @container-desktop;
// Large screen / wide desktop
@container-large-desktop: ((1140px + @grid-gutter-width));
//** For `@screen-lg-min` and up.
@container-lg: @container-large-desktop;

View File

@ -32,6 +32,10 @@
background-image: linear-gradient(to bottom, #ffffff 60%, #f8f8f8 100%);
}
.btn-link {
box-shadow: none;
}
// Forms
// -------------------------

View File

@ -1,8 +1,5 @@
// Flatly 3.0.3
// --------------------------------------------------
// Global values
// Flatly 3.1.1
// Variables
// --------------------------------------------------
#footer {
@ -11,74 +8,91 @@
border-top: 1px solid @navbar-default-border;
}
// Grays
// -------------------------
//== Colors
//
//## Gray and brand colors for use across Bootstrap.
@gray-darker: lighten(#000, 13.5%); // #222
@gray-dark: #7b8a8b;
// #333
@gray: #95a5a6;
// #555
@gray-light: #b4bcc2;
// #999
@gray-lighter: #ecf0f1;
// Brand colors
// -------------------------
// #eee
@brand-primary: #2C3E50;
@brand-success: #18BC9C;
@brand-info: #3498DB;
@brand-warning: #F39C12;
@brand-danger: #E74C3C;
@brand-info: #3498DB;
// Scaffolding
// -------------------------
//== Scaffolding
//
// ## Settings for some of the most global styles.
//** Background color for `<body>`.
@body-bg: #fff;
//** Global text color on `<body>`.
@text-color: @brand-primary;
// Links
// -------------------------
//** Global textual link color.
@link-color: @brand-success;
//** Link hover color set via `darken()` function.
@link-hover-color: @link-color;
// Typography
// -------------------------
//== Typography
//
//## Font, line-height, and color for body text, headings, and more.
@font-family-sans-serif: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif;
@font-family-serif: Georgia, "Times New Roman", Times, serif;
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
@font-family-base: @font-family-sans-serif;
@font-size-base: 15px;
@font-size-large: ceil(@font-size-base * 1.25); // ~18px
@font-size-small: ceil(@font-size-base * 0.85); // ~12px
@font-size-large: ceil((@font-size-base * 1.25));
// ~18px
@font-size-small: ceil((@font-size-base * 0.85));
// ~12px
@font-size-h1: floor(@font-size-base * 2.6); // ~36px
@font-size-h2: floor(@font-size-base * 2.15); // ~30px
@font-size-h3: ceil(@font-size-base * 1.7); // ~24px
@font-size-h4: ceil(@font-size-base * 1.25); // ~18px
@font-size-h1: floor((@font-size-base * 2.6));
// ~36px
@font-size-h2: floor((@font-size-base * 2.15));
// ~30px
@font-size-h3: ceil((@font-size-base * 1.7));
// ~24px
@font-size-h4: ceil((@font-size-base * 1.25));
// ~18px
@font-size-h5: @font-size-base;
@font-size-h6: ceil(@font-size-base * 0.85); // ~12px
@font-size-h6: ceil((@font-size-base * 0.85));
// ~12px
//** Unit-less `line-height` for use in components like buttons.
@line-height-base: 1.428571429; // 20/14
@line-height-computed: floor(@font-size-base * @line-height-base); // ~20px
//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
@line-height-computed: floor((@font-size-base * @line-height-base));
// ~20px
//** By default, this inherits from the `<body>`.
@headings-font-family: @font-family-base;
@headings-font-weight: 500;
@headings-font-weight: 400;
@headings-line-height: 1.1;
@headings-color: inherit;
// Iconography
// -------------------------
//-- Iconography
//
//## Specify custom locations of the include Glyphicons icon font. Useful for those including Bootstrap via Bower.
@icon-font-path: "../fonts/";
@icon-font-name: "glyphicons-halflings-regular";
@icon-font-svg-id: "glyphicons_halflingsregular";
// Components
// -------------------------
// Based on 14px font-size and 1.428 line-height (~20px to start)
//== Components
//
//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
@padding-base-vertical: 10px;
@padding-base-horizontal: 15px;
@ -99,28 +113,39 @@
@border-radius-large: 6px;
@border-radius-small: 3px;
//** Global color for active items (e.g., navs or dropdowns).
@component-active-color: #fff;
//** Global background color for active items (e.g., navs or dropdowns).
@component-active-bg: @brand-primary;
//** Width of the `border` for generating carets that indicator dropdowns.
@caret-width-base: 4px;
//** Carets increase slightly in size for larger components.
@caret-width-large: 5px;
// Tables
// -------------------------
//== Tables
//
//## Customizes the `.table` component with basic values, each used across all table variations.
//** Padding for `<th>`s and `<td>`s.
@table-cell-padding: 8px;
//** Padding for cells in `.table-condensed`.
@table-condensed-cell-padding: 5px;
@table-bg: transparent; // overall background-color
@table-bg-accent: #f9f9f9; // for striping
//** Default background color used for all tables.
@table-bg: transparent;
//** Background color used for `.table-striped`.
@table-bg-accent: #f9f9f9;
//** Background color used for `.table-hover`.
@table-bg-hover: @gray-lighter;
@table-bg-active: @table-bg-hover;
@table-border-color: @gray-lighter; // table and cell border
//** Border color for table and cell borders.
@table-border-color: @gray-lighter;
// Buttons
// -------------------------
//== Buttons
//
//## For each of Bootstrap's buttons, define text, background and border color.
@btn-font-weight: normal;
@ -136,6 +161,10 @@
@btn-success-bg: @brand-success;
@btn-success-border: @btn-success-bg;
@btn-info-color: @btn-default-color;
@btn-info-bg: @brand-info;
@btn-info-border: @btn-info-bg;
@btn-warning-color: @btn-default-color;
@btn-warning-bg: @brand-warning;
@btn-warning-border: @btn-warning-bg;
@ -144,65 +173,84 @@
@btn-danger-bg: @brand-danger;
@btn-danger-border: @btn-danger-bg;
@btn-info-color: @btn-default-color;
@btn-info-bg: @brand-info;
@btn-info-border: @btn-info-bg;
@btn-link-disabled-color: @gray-light;
//== Forms
//
//##
// Forms
// -------------------------
//** `<input>` background color
@input-bg: #fff;
//** `<input disabled>` background color
@input-bg-disabled: @gray-lighter;
//** Text color for `<input>`s
@input-color: @text-color;
//** `<input>` border color
@input-border: #dce4ec;
//** `<input>` border radius
@input-border-radius: @border-radius-base;
//** Border color for inputs on focus
@input-border-focus: #1abc9c;
//** Placeholder text color
@input-color-placeholder: #acb6c0;
//** Default `.form-control` height
@input-height-base: (@line-height-computed + (@padding-base-vertical * 2) + 2);
//** Large `.form-control` height
@input-height-large: (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
//** Small `.form-control` height
@input-height-small: (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
@legend-color: @text-color;
@legend-border-color: #e5e5e5;
//** Background color for textual input addons
@input-group-addon-bg: @gray-lighter;
//** Border color for textual input addons
@input-group-addon-border-color: @input-border;
//== Dropdowns
//
//## Dropdown menu container and contents.
// Dropdowns
// -------------------------
//** Background for the dropdown menu.
@dropdown-bg: #fff;
//** Dropdown menu `border-color`.
@dropdown-border: rgba(0,0,0,.15);
//** Dropdown menu `border-color` **for IE8**.
@dropdown-fallback-border: #ccc;
//** Divider color for between dropdown items.
@dropdown-divider-bg: #e5e5e5;
//** Dropdown link text color.
@dropdown-link-color: @gray-dark;
//** Hover color for dropdown links.
@dropdown-link-hover-color: #fff;
//** Hover background for dropdown links.
@dropdown-link-hover-bg: @dropdown-link-active-bg;
//** Active dropdown menu item text color.
@dropdown-link-active-color: #fff;
//** Active dropdown menu item background color.
@dropdown-link-active-bg: @component-active-bg;
//** Disabled dropdown menu item background color.
@dropdown-link-disabled-color: @text-muted;
//** Text color for headers within dropdown menus.
@dropdown-header-color: @text-muted;
// Note: Deprecated @dropdown-caret-color as of v3.1.0
@dropdown-caret-color: #000;
// COMPONENT VARIABLES
// --------------------------------------------------
// Z-index master list
// -------------------------
// Used for a bird's eye view of components dependent on the z-axis
// Try to avoid customizing these :)
//-- Z-index master list
//
// Warning: Avoid customizing these values. They're used for a bird's eye view
// of components dependent on the z-axis and are designed to all work together.
//
// Note: These variables are not generated into the Customizer.
@zindex-navbar: 1000;
@zindex-dropdown: 1000;
@ -212,8 +260,9 @@
@zindex-modal-background: 1040;
@zindex-modal: 1050;
// Media queries breakpoints
// --------------------------------------------------
//== Media queries breakpoints
//
//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
// Extra small screen / phone
// Note: Deprecated @screen-xs and @screen-phone as of v3.0.1
@ -244,32 +293,50 @@
@screen-sm-max: (@screen-md-min - 1);
@screen-md-max: (@screen-lg-min - 1);
//== Grid system
//
//## Define your custom responsive grid.
// Grid system
// --------------------------------------------------
// Number of columns in the grid system
//** Number of columns in the grid.
@grid-columns: 12;
// Padding, to be divided by two and applied to the left and right of all columns
//** Padding between columns. Gets divided in half for the left and right.
@grid-gutter-width: 30px;
// Navbar collapse
// Point at which the navbar becomes uncollapsed
//** Point at which the navbar becomes uncollapsed.
@grid-float-breakpoint: @screen-sm-min;
// Point at which the navbar begins collapsing
//** Point at which the navbar begins collapsing.
@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
//== Container sizes
//
//## Define the maximum width of `.container` for different screen sizes.
// Navbar
// -------------------------
// Small screen / tablet
@container-tablet: ((720px + @grid-gutter-width));
//** For `@screen-sm-min` and up.
@container-sm: @container-tablet;
// Medium screen / desktop
@container-desktop: ((940px + @grid-gutter-width));
//** For `@screen-md-min` and up.
@container-md: @container-desktop;
// Large screen / wide desktop
@container-large-desktop: ((1140px + @grid-gutter-width));
//** For `@screen-lg-min` and up.
@container-lg: @container-large-desktop;
//== Navbar
//
//##
// Basics of a navbar
@navbar-height: 60px;
@navbar-margin-bottom: @line-height-computed;
@navbar-border-radius: @border-radius-base;
@navbar-padding-horizontal: floor(@grid-gutter-width / 2); // ~15px
@navbar-padding-horizontal: floor((@grid-gutter-width / 2));
@navbar-padding-vertical: ((@navbar-height - @line-height-computed) / 2);
@navbar-collapse-max-height: 340px;
@navbar-default-color: #777;
@navbar-default-bg: @brand-primary;
@ -296,7 +363,6 @@
// Inverted navbar
//
// Reset inverted navbar basics
@navbar-inverse-color: #fff;
@navbar-inverse-bg: @brand-success;
@ -321,10 +387,11 @@
@navbar-inverse-toggle-icon-bar-bg: #fff;
@navbar-inverse-toggle-border-color: darken(@navbar-inverse-bg, 10%);
//== Navs
//
//##
// Navs
// -------------------------
//=== Shared nav styles
@nav-link-padding: 10px 15px;
@nav-link-hover-bg: @gray-lighter;
@ -333,7 +400,7 @@
@nav-open-link-hover-color: #fff;
// Tabs
//== Tabs
@nav-tabs-border-color: @gray-lighter;
@nav-tabs-link-hover-border-color: @gray-lighter;
@ -345,45 +412,60 @@
@nav-tabs-justified-link-border-color: @gray-lighter;
@nav-tabs-justified-active-link-border-color: @body-bg;
// Pills
//== Pills
@nav-pills-border-radius: @border-radius-base;
@nav-pills-active-link-hover-bg: @component-active-bg;
@nav-pills-active-link-hover-color: @component-active-color;
//== Pagination
//
//##
// Pagination
// -------------------------
@pagination-color: #fff;
@pagination-bg: @brand-success;
@pagination-border: transparent;
@pagination-hover-color: #fff;
@pagination-hover-bg: darken(@brand-success, 15%);
@pagination-hover-border: transparent;
@pagination-active-bg: darken(@brand-success, 15%);
@pagination-active-color: #fff;
@pagination-active-bg: darken(@brand-success, 15%);
@pagination-active-border: transparent;
@pagination-disabled-color: @gray-lighter;
@pagination-disabled-bg: lighten(@brand-success, 15%);
;
@pagination-disabled-border: transparent;
//== Pager
//
//##
// Pager
// -------------------------
@pager-bg: @pagination-bg;
@pager-border: @pagination-border;
@pager-border-radius: 15px;
@pager-hover-bg: @pagination-hover-bg;
@pager-active-bg: @pagination-active-bg;
@pager-active-color: @pagination-active-color;
@pager-disabled-color: #fff;
// Jumbotron
// -------------------------
//== Jumbotron
//
//##
@jumbotron-padding: 30px;
@jumbotron-color: inherit;
@jumbotron-bg: @gray-lighter;
@jumbotron-heading-color: inherit;
@jumbotron-font-size: ceil(@font-size-base * 1.5);
@jumbotron-font-size: ceil((@font-size-base * 1.5));
// Form states and alerts
// -------------------------
//== Form states and alerts
//
//## Define colors for form feedback states and, by default, alerts.
@state-success-text: #fff;
@state-success-bg: @brand-success;
@ -401,66 +483,109 @@
@state-danger-bg: @brand-danger;
@state-danger-border: @brand-danger;
//== Tooltips
//
//##
// Tooltips
// -------------------------
//** Tooltip max width
@tooltip-max-width: 200px;
//** Tooltip text color
@tooltip-color: #fff;
//** Tooltip background color
@tooltip-bg: rgba(0,0,0,.9);
@tooltip-opacity: .9;
//** Tooltip arrow width
@tooltip-arrow-width: 5px;
//** Tooltip arrow color
@tooltip-arrow-color: @tooltip-bg;
//== Popovers
//
//##
// Popovers
// -------------------------
//** Popover body background color
@popover-bg: #fff;
//** Popover maximum width
@popover-max-width: 276px;
//** Popover border color
@popover-border-color: rgba(0,0,0,.2);
//** Popover fallback border color
@popover-fallback-border-color: #ccc;
//** Popover title background color
@popover-title-bg: darken(@popover-bg, 3%);
//** Popover arrow width
@popover-arrow-width: 10px;
//** Popover arrow color
@popover-arrow-color: #fff;
//** Popover outer arrow width
@popover-arrow-outer-width: (@popover-arrow-width + 1);
@popover-arrow-outer-color: rgba(0,0,0,.25);
@popover-arrow-outer-fallback-color: #999;
//** Popover outer arrow color
@popover-arrow-outer-color: fadein(@popover-border-color, 5%);
//** Popover outer arrow fallback color
@popover-arrow-outer-fallback-color: darken(@popover-fallback-border-color, 20%);
//== Labels
//
//##
// Labels
// -------------------------
//** Default label background color
@label-default-bg: @btn-default-bg;
//** Primary label background color
@label-primary-bg: @brand-primary;
//** Success label background color
@label-success-bg: @brand-success;
//** Info label background color
@label-info-bg: @brand-info;
//** Warning label background color
@label-warning-bg: @brand-warning;
//** Danger label background color
@label-danger-bg: @brand-danger;
//** Default label text color
@label-color: #fff;
//** Default text color of a linked label
@label-link-hover-color: #fff;
//== Modals
//
//##
// Modals
// -------------------------
//** Padding applied to the modal body
@modal-inner-padding: 20px;
//** Padding applied to the modal title
@modal-title-padding: 15px;
//** Modal title line-height
@modal-title-line-height: @line-height-base;
//** Background color of modal content area
@modal-content-bg: #fff;
//** Modal content border color
@modal-content-border-color: rgba(0,0,0,.2);
//** Modal content border color **for IE8**
@modal-content-fallback-border-color: #999;
//** Modal backdrop background color
@modal-backdrop-bg: #000;
//** Modal backdrop opacity
@modal-backdrop-opacity: .5;
//** Modal header border color
@modal-header-border-color: #e5e5e5;
//** Modal footer border color
@modal-footer-border-color: @modal-header-border-color;
@modal-lg: 900px;
@modal-md: 600px;
@modal-sm: 300px;
//== Alerts
//
//## Define alert colors, border radius, and padding.
// Alerts
// -------------------------
@alert-padding: 15px;
@alert-border-radius: @border-radius-base;
@alert-link-font-weight: bold;
@ -481,39 +606,60 @@
@alert-danger-text: @state-danger-text;
@alert-danger-border: @state-danger-border;
//== Progress bars
//
//##
// Progress bars
// -------------------------
//** Background color of the whole progress component
@progress-bg: @gray-lighter;
//** Progress bar text color
@progress-bar-color: #fff;
//** Default progress bar color
@progress-bar-bg: @brand-primary;
//** Success progress bar color
@progress-bar-success-bg: @brand-success;
//** Warning progress bar color
@progress-bar-warning-bg: @brand-warning;
//** Danger progress bar color
@progress-bar-danger-bg: @brand-danger;
//** Info progress bar color
@progress-bar-info-bg: @brand-info;
//== List group
//
//##
// List group
// -------------------------
//** Background color on `.list-group-item`
@list-group-bg: #fff;
//** `.list-group-item` border color
@list-group-border: @gray-lighter;
//** List group border radius
@list-group-border-radius: @border-radius-base;
//** Background color of single list elements on hover
@list-group-hover-bg: @gray-lighter;
//** Text color of active list elements
@list-group-active-color: @component-active-color;
//** Background color of active list elements
@list-group-active-bg: @component-active-bg;
//** Border color of active list elements
@list-group-active-border: @list-group-active-bg;
@list-group-active-text-color: lighten(@list-group-active-bg, 40%);
@list-group-link-color: #555;
@list-group-link-heading-color: #333;
//== Panels
//
//##
// Panels
// -------------------------
@panel-bg: #fff;
@panel-inner-border: @gray-lighter;
@panel-body-padding: 15px;
@panel-border-radius: @border-radius-base;
//** Border color for elements within panels
@panel-inner-border: @gray-lighter;
@panel-footer-bg: @gray-lighter;
@panel-default-text: @text-color;
@ -528,6 +674,10 @@
@panel-success-border: @state-success-border;
@panel-success-heading-bg: @state-success-bg;
@panel-info-text: @state-info-text;
@panel-info-border: @state-info-border;
@panel-info-heading-bg: @state-info-bg;
@panel-warning-text: @state-warning-text;
@panel-warning-border: @state-warning-border;
@panel-warning-heading-bg: @state-warning-bg;
@ -536,51 +686,67 @@
@panel-danger-border: @state-danger-border;
@panel-danger-heading-bg: @state-danger-bg;
@panel-info-text: @state-info-text;
@panel-info-border: @state-info-border;
@panel-info-heading-bg: @state-info-bg;
//== Thumbnails
//
//##
// Thumbnails
// -------------------------
//** Padding around the thumbnail image
@thumbnail-padding: 4px;
//** Thumbnail background color
@thumbnail-bg: @body-bg;
//** Thumbnail border color
@thumbnail-border: @gray-lighter;
//** Thumbnail border radius
@thumbnail-border-radius: @border-radius-base;
//** Custom text color for thumbnail captions
@thumbnail-caption-color: @text-color;
//** Padding around the thumbnail caption
@thumbnail-caption-padding: 9px;
//== Wells
//
//##
// Wells
// -------------------------
@well-bg: @gray-lighter;
@well-border: transparent;
//== Badges
//
//##
// Badges
// -------------------------
@badge-color: #fff;
//** Linked badge text color on hover
@badge-link-hover-color: #fff;
@badge-bg: @gray;
//** Badge text color in active nav link
@badge-active-color: @link-color;
//** Badge background color in active nav link
@badge-active-bg: #fff;
@badge-font-weight: bold;
@badge-line-height: 1;
@badge-border-radius: 10px;
//== Breadcrumbs
//
//##
// Breadcrumbs
// -------------------------
@breadcrumb-padding-vertical: 8px;
@breadcrumb-padding-horizontal: 15px;
//** Breadcrumb background color
@breadcrumb-bg: @gray-lighter;
//** Breadcrumb text color
@breadcrumb-color: #ccc;
//** Text color of current page in the breadcrumb
@breadcrumb-active-color: @gray;
//** Textual separator for between breadcrumb elements
@breadcrumb-separator: "/";
// Carousel
// ------------------------
//== Carousel
//
//##
@carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6);
@ -594,63 +760,59 @@
@carousel-caption-color: #fff;
//== Close
//
//##
// Close
// ------------------------
@close-font-weight: bold;
@close-color: #000;
@close-text-shadow: 0 1px 0 #fff;
//== Code
//
//##
// Code
// ------------------------
@code-color: #c7254e;
@code-bg: #f9f2f4;
@kbd-color: #fff;
@kbd-bg: #333;
@pre-bg: @gray-lighter;
@pre-color: @gray-dark;
@pre-border-color: #ccc;
@pre-scrollable-max-height: 340px;
// Type
// ------------------------
//== Type
//
//##
//** Text muted color
@text-muted: @gray-light;
//** Abbreviations and acronyms border color
@abbr-border-color: @gray-light;
//** Headings small color
@headings-small-color: @gray-light;
//** Blockquote small color
@blockquote-small-color: @gray-light;
//** Blockquote font size
@blockquote-font-size: (@font-size-base * 1.25);
//** Blockquote border color
@blockquote-border-color: @gray-lighter;
//** Page header border color
@page-header-border-color: @gray-lighter;
// Miscellaneous
// -------------------------
//== Miscellaneous
//
//##
// Hr border color
//** Horizontal line color.
@hr-border: @gray-lighter;
// Horizontal forms & lists
//** Horizontal offset for forms and lists.
@component-offset-horizontal: 180px;
// Container sizes
// --------------------------------------------------
// Small screen / tablet
@container-tablet: ((720px + @grid-gutter-width));
@container-sm: @container-tablet;
// Medium screen / desktop
@container-desktop: ((940px + @grid-gutter-width));
@container-md: @container-desktop;
// Large screen / wide desktop
@container-large-desktop: ((1140px + @grid-gutter-width));
@container-lg: @container-large-desktop;
// Flatly 3.0.3
// Bootswatch
// -----------------------------------------------------
@import url("//fonts.googleapis.com/css?family=Lato:400,700,900,400italic");
@import url("//fonts.googleapis.com/css?family=Lato:400,700,400italic");
// Navbar =====================================================================
@ -700,6 +862,24 @@
tr.danger {
color: #fff;
}
> thead > tr > th,
> tbody > tr > th,
> tfoot > tr > th,
> thead > tr > td,
> tbody > tr > td,
> tfoot > tr > td {
border: none;
}
&-bordered > thead > tr > th,
&-bordered > tbody > tr > th,
&-bordered > tfoot > tr > th,
&-bordered > thead > tr > td,
&-bordered > tbody > tr > td,
&-bordered > tfoot > tr > td {
border: 1px solid @table-border-color;
}
}
// Forms ======================================================================
@ -774,23 +954,6 @@ input[type="color"],
}
}
.pagination {
a,
a:hover {
color: #fff;
}
.disabled {
&>a,
&>a:hover,
&>a:focus,
&>span {
background-color: lighten(@brand-success, 15%);
}
}
}
.pager {
a,
a:hover {
@ -802,7 +965,7 @@ input[type="color"],
&>a:hover,
&>a:focus,
&>span {
background-color: lighten(@brand-success, 15%);
background-color: @pagination-disabled-bg;
}
}
}
@ -829,5 +992,4 @@ input[type="color"],
.well {
.box-shadow(none);
border-width: 0;
}

19
test/app_test.js Normal file
View File

@ -0,0 +1,19 @@
var request = require('supertest');
var app = require('../app.js');
describe('GET /', function() {
it('should return 200 OK', function(done) {
request(app)
.get('/')
.expect(200, done);
});
});
describe('GET /reset', function() {
it('should return 404', function(done) {
request(app)
.get('/reset')
.expect(404, done);
// this will fail
});
});

1
test/mocha.opts Normal file
View File

@ -0,0 +1 @@
--reporter spec

43
test/user_test.js Normal file
View File

@ -0,0 +1,43 @@
var chai = require('chai');
var should = chai.should();
var User = require('../models/User');
describe('User attributes', function() {
before(function(done) {
user = new User({
email: 'janedoe@gmail.com',
password: 'password'
});
done();
});
it('email should be a string', function() {
user.email.should.be.a('string');
});
it('password should be a string', function() {
user.password.should.be.a('string');
});
it('should save a user', function(done) {
user.save();
done();
});
it('should find our newly created user', function(done) {
User.findOne({ email: user.email }, function(err, user) {
should.exist(user);
user.email.should.equal('janedoe@gmail.com');
done();
});
});
it('should not allow users with duplicate emails', function(done) {
user.save(function(err) {
if (err) {
err.code.should.equal(11000);
}
done();
});
});
});

15
views/account/forgot.jade Normal file
View File

@ -0,0 +1,15 @@
extends ../layout
block content
.col-sm-8.col-sm-offset-2
form(method='POST')
legend Forgot Password
input(type='hidden', name='_csrf', value=token)
.form-group
p Enter your email address below and we will send you password reset instructions.
label.control-label(for='email') Email
input.form-control(type='email', name='email', id='email', placeholder='Email', autofocus=true)
.form-group
button.btn.btn-primary(type='submit')
i.fa.fa-key
| Reset Password

View File

@ -3,8 +3,8 @@ extends ../layout
block content
.col-sm-8.col-sm-offset-2
form(method='POST')
input(type='hidden', name='_csrf', value=token)
legend Sign In
input(type='hidden', name='_csrf', value=token)
.form-group
.btn-group.btn-group-justified
if secrets.facebookAuth
@ -34,4 +34,4 @@ block content
button.btn.btn-primary(type='submit')
i.fa.fa-unlock-alt
| Login
a.btn.btn-link(href='/forgot') Forgot your password?

17
views/account/reset.jade Normal file
View File

@ -0,0 +1,17 @@
extends ../layout
block content
.col-sm-8.col-sm-offset-2
form(method='POST')
legend Reset Password
input(type='hidden', name='_csrf', value=token)
.form-group
label(for='password') New Password
input.form-control(type='password', name='password', value='', placeholder='New password', autofocus=true)
.form-group
label(for='confirm') Confirm Password
input.form-control(type='password', name='confirm', value='', placeholder='Confirm password')
.form-group
button.btn.btn-primary.btn-reset(type='submit')
i.fa.fa-keyboard-o
| Update Password