├── README.md
├── app.js
├── bin
└── www
├── config.js
├── models
├── comment.js
├── db.js
├── post.js
├── user.js
└── vote.js
├── package.json
├── proxy
└── post.js
├── public
├── favicon.ico
├── fonts
│ └── fontello
│ │ ├── LICENSE.txt
│ │ ├── README.txt
│ │ ├── config.json
│ │ ├── css
│ │ ├── animation.css
│ │ ├── fontello-codes.css
│ │ ├── fontello-embedded.css
│ │ ├── fontello-ie7-codes.css
│ │ ├── fontello-ie7.css
│ │ └── fontello.css
│ │ ├── demo.html
│ │ └── font
│ │ ├── fontello.eot
│ │ ├── fontello.svg
│ │ ├── fontello.ttf
│ │ ├── fontello.woff
│ │ └── fontello.woff2
├── images
│ ├── avatar.png
│ ├── bg.png
│ ├── bg2.png
│ └── fork-me-on-github.png
├── javascripts
│ ├── configs
│ │ └── configs.js
│ ├── controllers
│ │ └── controllers.js
│ ├── directives
│ │ └── directives.js
│ ├── mean-blog.min.js
│ ├── services
│ │ └── services.js
│ └── vendor
│ │ ├── angular-1.1.5
│ │ ├── angular-resource.min.js
│ │ ├── angular.js
│ │ └── angular.min.js
│ │ ├── angular-1.5.0
│ │ ├── angular-animate.min.js
│ │ ├── angular-resource.min.js
│ │ ├── angular-route.min.js
│ │ └── angular.min.js
│ │ ├── html5shiv.min.js
│ │ ├── imagesloaded.pkgd.min.js
│ │ ├── jquery-2.2.2.min.js
│ │ ├── markdown.min.js
│ │ ├── modernizr.min.js
│ │ └── nprogress.js
├── stylesheets
│ ├── grids-responsive-min.css
│ ├── mean-blog.min.css
│ ├── ng-animation.css
│ ├── normalize.css
│ ├── nprogress.css
│ ├── pure-min.css
│ ├── side-menu.css
│ └── style.css
└── uploads
│ └── images
│ └── d8187d9836e494faec7be0433ce9d74b.png
├── routes
├── api.js
├── auth.js
├── blog.js
├── comments.js
├── index.js
├── posts.js
└── users.js
├── utility
├── hash.js
├── redisClient.js
├── restrict.js
├── tool.js
└── validator.js
└── views
├── about
├── contact.jade
└── index.jade
├── auth
├── login.jade
└── register.jade
├── blog
├── create.jade
├── edit.jade
├── index.jade
└── show.jade
├── error.jade
├── index.jade
├── layout.jade
└── user
├── index.jade
└── show.jade
/README.md:
--------------------------------------------------------------------------------
1 | # MEAN-BLOG
2 | MEAN-BLOG is a blog project written in MEAN - MongoDB, ExpressJS, AngularJS, NodeJS.
3 |
4 | If you have a problem or have some good advice?Welcome to star and submit PR or submit a issue.❤
5 | ## Online DEMO
6 |
7 | ###[Click here](http://114.215.164.12:3000)
8 |
9 | ## Requirements and Environment
10 | * Node.js
11 | * Express 4
12 | * Mongodb
13 | * AngularJS
14 | * Redis
15 |
16 | ## Installation
17 |
18 | ### 1. Clone the repo
19 |
20 | git clone https://github.com/icyse/mean-blog.git
21 |
22 | ### 2. NPM install
23 |
24 | cd mean-blog
25 | npm install
26 |
27 | ### 3. Database stuff
28 |
29 | vi config.js
30 |
31 | Ajust your Database config and Redis config
32 |
33 | ### 4. Start the server
34 |
35 | npm start
36 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var path = require('path');
3 | var favicon = require('serve-favicon');
4 | var logger = require('morgan');
5 | var cookieParser = require('cookie-parser');
6 | var bodyParser = require('body-parser');
7 |
8 | var config = require('./config');
9 | var session = require('express-session');
10 | var RedisStore = require('connect-redis')(session);
11 |
12 | var routes = require('./routes/index');
13 | var users = require('./routes/users');
14 |
15 | var auth = require('./routes/auth');
16 | var api = require('./routes/api');
17 | var blog = require('./routes/blog');
18 | var posts = require('./routes/posts');
19 | var comments = require('./routes/comments');
20 |
21 | var app = express();
22 |
23 | // config
24 | // view engine setup
25 | app.set('views', path.join(__dirname, 'views'));
26 | app.set('view engine', 'jade');
27 |
28 | // middleware
29 | // uncomment after placing your favicon in /public
30 | app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
31 | app.use(logger('dev'));
32 | app.use(bodyParser.json());
33 | app.use(bodyParser.urlencoded({ extended: false }));
34 | app.use(cookieParser());
35 | app.use(express.static(path.join(__dirname, 'public')));
36 |
37 | app.use(session({
38 | resave: false, // don't save session if unmodified
39 | saveUninitialized: false, // don't create session until something stored
40 | cookie: {
41 | maxAge: 24 * 60 * 60 * 1000
42 | },
43 | secret: config.cookieSecret,
44 | store: new RedisStore({
45 | host: config.RedisHost,
46 | port: config.RedisPort,
47 | pass: config.RedisPass,
48 | ttl: config.sessionExpiration,
49 | prefix: 'sess'
50 | })
51 | }));
52 | // Session-persisted message middleware
53 | app.use(function(req, res, next) {
54 | var err = req.session.error;
55 | var msg = req.session.success;
56 | delete req.session.error;
57 | delete req.session.success;
58 | res.locals.message = '';
59 | if (err) res.locals.message = '
' + err + '
';
60 | if (msg) res.locals.message = '' + msg + '
';
61 | next();
62 | });
63 |
64 | app.use('/', routes);
65 | app.use('/users', users);
66 |
67 | app.use('/auth', auth);
68 | app.use('/api', api);
69 | app.use('/blog', blog);
70 | app.use('/posts', posts);
71 | app.use('/comments', comments);
72 |
73 | // catch 404 and forward to error handler
74 | app.use(function(req, res, next) {
75 | var err = new Error('Not Found');
76 | err.status = 404;
77 | next(err);
78 | });
79 |
80 | // error handlers
81 |
82 | // development error handler
83 | // will print stacktrace
84 | if (app.get('env') === 'development') {
85 | /*app.use(function(err, req, res, next) {
86 | res.status(err.status || 500);
87 | res.render('error', {
88 | message: err.message,
89 | error: err
90 | });
91 | });*/
92 | app.use(function(err, req, res, next) {
93 | res.status(err.status || 500);
94 | res.json({
95 | status: 'fail',
96 | error: err.message
97 | });
98 | });
99 | }
100 |
101 | // production error handler
102 | // no stacktraces leaked to user
103 | app.use(function(err, req, res, next) {
104 | res.status(err.status || 500);
105 | res.render('error', {
106 | message: err.message,
107 | error: {}
108 | });
109 | });
110 |
111 |
112 | module.exports = app;
113 |
--------------------------------------------------------------------------------
/bin/www:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Module dependencies.
5 | */
6 |
7 | var app = require('../app');
8 | var debug = require('debug')('mean-blog:server');
9 | var http = require('http');
10 |
11 | /**
12 | * Get port from environment and store in Express.
13 | */
14 |
15 | var port = normalizePort(process.env.PORT || '3000');
16 | app.set('port', port);
17 |
18 | /**
19 | * Create HTTP server.
20 | */
21 |
22 | var server = http.createServer(app);
23 |
24 | /**
25 | * Listen on provided port, on all network interfaces.
26 | */
27 |
28 | server.listen(port);
29 | server.on('error', onError);
30 | server.on('listening', onListening);
31 |
32 | /**
33 | * Normalize a port into a number, string, or false.
34 | */
35 |
36 | function normalizePort(val) {
37 | var port = parseInt(val, 10);
38 |
39 | if (isNaN(port)) {
40 | // named pipe
41 | return val;
42 | }
43 |
44 | if (port >= 0) {
45 | // port number
46 | return port;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Event listener for HTTP server "error" event.
54 | */
55 |
56 | function onError(error) {
57 | if (error.syscall !== 'listen') {
58 | throw error;
59 | }
60 |
61 | var bind = typeof port === 'string'
62 | ? 'Pipe ' + port
63 | : 'Port ' + port;
64 |
65 | // handle specific listen errors with friendly messages
66 | switch (error.code) {
67 | case 'EACCES':
68 | console.error(bind + ' requires elevated privileges');
69 | process.exit(1);
70 | break;
71 | case 'EADDRINUSE':
72 | console.error(bind + ' is already in use');
73 | process.exit(1);
74 | break;
75 | default:
76 | throw error;
77 | }
78 | }
79 |
80 | /**
81 | * Event listener for HTTP server "listening" event.
82 | */
83 |
84 | function onListening() {
85 | var addr = server.address();
86 | var bind = typeof addr === 'string'
87 | ? 'pipe ' + addr
88 | : 'port ' + addr.port;
89 | debug('Listening on ' + bind);
90 | }
91 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | /*mongodb://user:pwd@host:port/database*/
3 | "DbPath": "mongodb://mean-blog:cccccc@localhost:27017/mean-blog",
4 | "RedisHost": "127.0.0.1",
5 | "RedisPort": 6379,
6 | "RedisPass": "cccccc",
7 | "CacheExpired": 3600,
8 | "cookieSecret": "mean-blog-by-cai",
9 | "sessionExpiration": 86400,
10 | "salt": "OdwqENkHnCse8JNt3QCCFlrnvRLxZXUP"
11 | }
12 |
--------------------------------------------------------------------------------
/models/comment.js:
--------------------------------------------------------------------------------
1 | var db = require('./db'),
2 | mongoose = db.mongoose;
3 |
4 | var commentSchema = new mongoose.Schema({
5 | content: String,
6 | post: { type: mongoose.Schema.Types.ObjectId, ref: 'post' },
7 | user: { type: Object },
8 | voteCount: { type: Number, default: 0 },
9 | voteList: { type: Array, default: [] },
10 | softDelete: { type: Boolean, default: false },
11 | createdTime: { type: Date, default: Date.now() },
12 | updatedTime: { type: Date, default: Date.now() }
13 | });
14 |
15 | exports.commentModel = mongoose.model('comment', commentSchema, 'comment');
16 |
--------------------------------------------------------------------------------
/models/db.js:
--------------------------------------------------------------------------------
1 | var dbPath = require('../config').DbPath;
2 | var mongoose = require('mongoose');
3 |
4 | mongoose.connect(dbPath);
5 | var db = mongoose.connection;
6 | db.on('error', function(err) {
7 | console.error('MongoDB连接错误: ' + err);
8 | process.exit(1);
9 | });
10 | exports.mongoose = mongoose;
11 |
12 | var base = new mongoose.Schema({
13 | /*_id: {type: String, unique: true},*/
14 | createdTime: { type: Date, default: Date.now() },
15 | updatedTime: { type: Date, default: Date.now() }
16 | });
17 | exports.base = base;
18 |
--------------------------------------------------------------------------------
/models/post.js:
--------------------------------------------------------------------------------
1 | var db = require('./db'),
2 | mongoose = db.mongoose;
3 |
4 | var postSchema = new mongoose.Schema({
5 | title: String,
6 | alias: String,
7 | summary: String,
8 | source: String,
9 | content: String,
10 | imgList: [],
11 | labels: [],
12 | url: String,
13 | category: { type: Object },
14 | user: { type: Object },
15 | /*user: {type: mongoose.Schema.Types.ObjectId, ref: 'user'},*/
16 | commentList: { type: Array, default: [] },
17 | voteList: { type: Array, default: [] },
18 | viewCount: { type: Number, default: 0 },
19 | commentCount: { type: Number, default: 0 },
20 | voteCount: { type: Number, default: 0 },
21 | isDraft: { type: Boolean, default: false },
22 | isActive: { type: Boolean, default: true },
23 | softDelete: { type: Boolean, default: false },
24 | createdTime: { type: Date, default: Date.now() },
25 | updatedTime: { type: Date, default: Date.now() }
26 | });
27 |
28 | exports.postModel = mongoose.model('post', postSchema, 'post');
29 |
--------------------------------------------------------------------------------
/models/user.js:
--------------------------------------------------------------------------------
1 | var db = require('./db'),
2 | mongoose = db.mongoose;
3 |
4 | var userSchema = new mongoose.Schema({
5 | username: {
6 | type: String,
7 | unique: true,
8 | required: true,
9 | },
10 | email: {
11 | type: String,
12 | unique: true,
13 | required: true,
14 | validate: {
15 | validator: function(v) {
16 | return /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((.[a-zA-Z0-9_-]{2,3}){1,2})$/.test(v);
17 | },
18 | message: '{VALUE} is not a valid email!'
19 | }
20 | },
21 | password: {
22 | type: String,
23 | required: true
24 | },
25 | role: { type: String, default: 'admin' },
26 | avatar: { type: String, default: "/images/avatar.png" },
27 | isActive: { type: Boolean, default: true },
28 | /*postList: [{type: mongoose.Schema.Types.ObjectId, ref: 'post'}],*/
29 | postList: [],
30 | createdTime: { type: Date, default: Date.now() },
31 | updatedTime: { type: Date, default: Date.now() }
32 | });
33 |
34 | exports.userModel = mongoose.model('user', userSchema, 'user');
35 |
--------------------------------------------------------------------------------
/models/vote.js:
--------------------------------------------------------------------------------
1 | var db = require('./db'),
2 | mongoose = db.mongoose;
3 |
4 | var voteSchema = new mongoose.Schema({
5 | user: { type: mongoose.Schema.Types.ObjectId, ref: 'user' },
6 | target: { id: mongoose.Schema.Types.ObjectId, model: String },
7 | createdTime: { type: Date, default: Date.now() },
8 | updatedTime: { type: Date, default: Date.now() }
9 | });
10 |
11 | exports.postModel = mongoose.model('vote', voteSchema, 'vote');
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mean-blog",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "node ./bin/www"
7 | },
8 | "dependencies": {
9 | "body-parser": "~1.13.2",
10 | "cookie-parser": "~1.3.5",
11 | "debug": "~2.2.0",
12 | "ejs": "~2.3.3",
13 | "express": "~4.13.1",
14 | "express-session": "~1.13.0",
15 | "eventproxy": "^0.3.4",
16 | "formidable": "~1.0.15",
17 | "jade": "~1.11.0",
18 | "morgan": "~1.6.1",
19 | "serve-favicon": "~2.3.0",
20 | "mongoose": "^4.3.7",
21 | "redis": "^2.4.2",
22 | "connect-redis": "~2.4.1"
23 | }
24 | }
--------------------------------------------------------------------------------
/proxy/post.js:
--------------------------------------------------------------------------------
1 | var postModel = require('../models/post').postModel;
2 | var redisClient = require('../utility/redisClient');
3 | var tool = require('../utility/tool');
4 |
5 | /**
6 | * 为首页数据查询构建条件对象
7 | * @param params 查询参数对象
8 | * @returns {{}}
9 | */
10 | function getPostsQuery(params) {
11 | var query = {};
12 | query.isActive = true;
13 | query.isDraft = false;
14 |
15 | params.cateId && (query.category._id = params.cateId);
16 | params.userId && (query.user._id = params.userId);
17 | if (params.searchText) {
18 | switch (params.filterType) {
19 | case '1':
20 | query.title = { "$regex": params.searchText, "$options": "gi" };
21 | break;
22 | case '2':
23 | query.labels = { "$regex": params.searchText, "$options": "gi" };
24 | break;
25 | case '3':
26 | query.createdTime = { "$regex": params.searchText, "$options": "gi" };
27 | break;
28 | default:
29 | query.$or = [{
30 | "title": {
31 | "$regex": params.searchText,
32 | "$options": "gi"
33 | }
34 | }, {
35 | 'labels': {
36 | "$regex": params.searchText,
37 | "$options": "gi"
38 | }
39 | }, {
40 | 'summary': {
41 | "$regex": params.searchText,
42 | "$options": "gi"
43 | }
44 | }, {
45 | 'content': {
46 | "$regex": params.searchText,
47 | "$options": "gi"
48 | }
49 | }];
50 | }
51 | }
52 | return query;
53 | }
54 |
55 | /**
56 | * 获取首页的文章数据
57 | * @param params 参数对象
58 | * @param callback 回调函数
59 | */
60 | exports.getPosts = function(params, callback) {
61 | var cache_key = tool.generateKey('posts', params);
62 | redisClient.getItem(cache_key, function(err, posts) {
63 | if (err) {
64 | return callback(err);
65 | }
66 | if (posts) {
67 | return callback(null, posts);
68 | }
69 | params.limit = params.limit || 0;
70 | params.skip = params.skip || 0;
71 |
72 | var options = {};
73 | options.skip = parseInt(params.skip);
74 | options.limit = parseInt(params.limit);
75 | if (params.sortOrder == 'asc') {
76 | options.sort = params.sortName === 'title' ? 'title -createdTime' : '-createdTime';
77 | } else {
78 | options.sort = params.sortName === 'title' ? '-title -createdTime' : '-createdTime';
79 | }
80 | var query = getPostsQuery(params);
81 | postModel.find(query, {}, options, function(err, posts) {
82 | if (err) {
83 | return callback(err);
84 | }
85 | if (posts) {
86 | redisClient.setItem(cache_key, posts, redisClient.defaultExpired, function(err) {
87 | if (err) {
88 | return callback(err);
89 | }
90 | })
91 | }
92 | return callback(null, posts);
93 | });
94 | });
95 | };
96 |
97 | /**
98 | * 获取首页的文章数
99 | * @param params 参数对象
100 | * @param callback 回调函数
101 | */
102 | exports.getPostsCount = function(params, callback) {
103 | var cache_key = tool.generateKey('posts_count', params);
104 | redisClient.getItem(cache_key, function(err, count) {
105 | if (err) {
106 | return callback(err);
107 | }
108 | if (count) {
109 | return callback(null, count);
110 | }
111 | var query = getPostsQuery(params);
112 | postModel.count(query, function(err, count) {
113 | if (err) {
114 | return callback(err);
115 | }
116 | redisClient.setItem(cache_key, count, redisClient.defaultExpired, function(err) {
117 | if (err) {
118 | return callback(err);
119 | }
120 | });
121 | return callback(null, count);
122 | });
123 | });
124 | };
125 |
126 | /**
127 | * 根据alias获取文章
128 | * @param alias 文章alias
129 | * @param callback 回调函数
130 | */
131 | exports.getPostByAlias = function(alias, callback) {
132 | var cache_key = 'post_' + alias;
133 | //此处不需要等待MongoDB的响应,所以不想传一个回调函数,但如果不传回调函数,则必须在调用Query对象上的exec()方法!
134 | //postModel.update({"Alias": alias}, {"ViewCount": 1}, function () {});
135 | postModel.update({ "alias": alias }, { "$inc": { "viewCount": 1 } }).exec();
136 | redisClient.getItem(cache_key, function(err, post) {
137 | if (err) {
138 | return callback(err);
139 | }
140 | if (post) {
141 | return callback(null, post);
142 | }
143 | postModel.findOne({ "alias": alias }, function(err, post) {
144 | if (err) {
145 | return callback(err);
146 | }
147 | if (post) {
148 | redisClient.setItem(cache_key, post, redisClient.defaultExpired, function(err) {
149 | if (err) {
150 | return callback(err);
151 | }
152 | });
153 | }
154 | return callback(null, post);
155 | })
156 | });
157 | };
158 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caihuabin/mean-blog/ced946ce6adbcc609dc4ea47e2aaed584e37a48d/public/favicon.ico
--------------------------------------------------------------------------------
/public/fonts/fontello/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Font license info
2 |
3 |
4 | ## Font Awesome
5 |
6 | Copyright (C) 2012 by Dave Gandy
7 |
8 | Author: Dave Gandy
9 | License: SIL ()
10 | Homepage: http://fortawesome.github.com/Font-Awesome/
11 |
12 |
13 | ## Typicons
14 |
15 | (c) Stephen Hutchings 2012
16 |
17 | Author: Stephen Hutchings
18 | License: SIL (http://scripts.sil.org/OFL)
19 | Homepage: http://typicons.com/
20 |
21 |
22 |
--------------------------------------------------------------------------------
/public/fonts/fontello/README.txt:
--------------------------------------------------------------------------------
1 | This webfont is generated by http://fontello.com open source project.
2 |
3 |
4 | ================================================================================
5 | Please, note, that you should obey original font licenses, used to make this
6 | webfont pack. Details available in LICENSE.txt file.
7 |
8 | - Usually, it's enough to publish content of LICENSE.txt file somewhere on your
9 | site in "About" section.
10 |
11 | - If your project is open-source, usually, it will be ok to make LICENSE.txt
12 | file publicly available in your repository.
13 |
14 | - Fonts, used in Fontello, don't require a clickable link on your site.
15 | But any kind of additional authors crediting is welcome.
16 | ================================================================================
17 |
18 |
19 | Comments on archive content
20 | ---------------------------
21 |
22 | - /font/* - fonts in different formats
23 |
24 | - /css/* - different kinds of css, for all situations. Should be ok with
25 | twitter bootstrap. Also, you can skip style and assign icon classes
26 | directly to text elements, if you don't mind about IE7.
27 |
28 | - demo.html - demo file, to show your webfont content
29 |
30 | - LICENSE.txt - license info about source fonts, used to build your one.
31 |
32 | - config.json - keeps your settings. You can import it back into fontello
33 | anytime, to continue your work
34 |
35 |
36 | Why so many CSS files ?
37 | -----------------------
38 |
39 | Because we like to fit all your needs :)
40 |
41 | - basic file, .css - is usually enough, it contains @font-face
42 | and character code definitions
43 |
44 | - *-ie7.css - if you need IE7 support, but still don't wish to put char codes
45 | directly into html
46 |
47 | - *-codes.css and *-ie7-codes.css - if you like to use your own @font-face
48 | rules, but still wish to benefit from css generation. That can be very
49 | convenient for automated asset build systems. When you need to update font -
50 | no need to manually edit files, just override old version with archive
51 | content. See fontello source code for examples.
52 |
53 | - *-embedded.css - basic css file, but with embedded WOFF font, to avoid
54 | CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain.
55 | We strongly recommend to resolve this issue by `Access-Control-Allow-Origin`
56 | server headers. But if you ok with dirty hack - this file is for you. Note,
57 | that data url moved to separate @font-face to avoid problems with -1) {
38 | data['model'] = field.substring(0, index);
39 | data['field'] = field.substring(index + 1);
40 | } else {
41 | data['model'] = 'user';
42 | data['field'] = field;
43 | }
44 | value[data['field']] = eval('scope.' + attrs.ngModel);
45 | if (modelLastIndex > -1) {
46 | value['_id'] = scope[attrs.ngModel.substring(0, modelLastIndex)]['_id'];
47 | }
48 | data['value'] = value;
49 |
50 | $http({
51 | method: 'POST',
52 | url: '/api/checkUnique',
53 | data: data
54 | }).success(function(data, status, headers, cfg) {
55 | ctrl.$setValidity('unique', data.isUnique);
56 | }).error(function(data, status, headers, cfg) {
57 | ctrl.$setValidity('unique', false);
58 | });
59 | });
60 | }
61 | };
62 | });
63 | directives.directive('currentMessage', function() {
64 | return {
65 | restrict: 'A',
66 | //若直接写在文档会有闪现问题
67 | template: '{{ currentMessage.data.message }}
',
68 | link: function(scope) {
69 |
70 | }
71 | }
72 | });
73 |
74 | directives.directive('showMessage', function() {
75 | return {
76 | restrict: 'A',
77 | require: 'ngModel',
78 | link: function(scope, elem, attrs, ctrl) {
79 | var data = JSON.parse(attrs.showMessage);
80 | scope.$watch(attrs.ngModel, function() {
81 | if (ctrl.$viewValue === true) {
82 | scope.setCurrentMessage(data);
83 | }
84 | });
85 | }
86 | }
87 | });
88 |
89 | directives.directive('authDialog', ['AUTH_EVENTS', function(AUTH_EVENTS) {
90 | return {
91 | restrict: 'A',
92 | //若直接写在文档会有闪现问题
93 | template: '' +
94 | '
' +
95 | '
' +
96 | '
',
98 | link: function(scope) {
99 | scope.visible = false;
100 | scope.toggle = true;
101 | var showDialog = function(args, toggle) {
102 | scope.visible = true;
103 | (toggle === 'login') ? (scope.toggle = true) : (scope.toggle = false);
104 | };
105 | var closeDialog = function() {
106 | scope.visible = false;
107 | };
108 | scope.closeDialog = closeDialog;
109 | scope.$on(AUTH_EVENTS.notAuthenticated, showDialog);
110 | scope.$on(AUTH_EVENTS.sessionTimeout, showDialog);
111 | scope.$on(AUTH_EVENTS.loginSuccess, closeDialog);
112 | }
113 | };
114 | }]);
115 | directives.directive('blogDialog', ['CUSTOM_EVENTS', '$sce', function(CUSTOM_EVENTS, $sce) {
116 | return {
117 | restrict: 'A',
118 | template: '' +
119 | '
' +
120 | '
',
123 | link: function(scope) {
124 | scope.visible = false;
125 | var showDialog = function(args, toggle) {
126 | scope.visible = true;
127 | if (scope.post.content) {
128 | scope.previewContent = $sce.trustAsHtml(markdown.toHTML(scope.post.content));
129 | }
130 |
131 | };
132 | var closeDialog = function() {
133 | scope.visible = false;
134 | };
135 | scope.closeDialog = closeDialog;
136 | scope.$on(CUSTOM_EVENTS.blogPreviewOpen, showDialog);
137 | scope.$on(CUSTOM_EVENTS.blogPreviewClose, closeDialog);
138 | }
139 | };
140 | }]);
141 |
142 | directives.directive('markDown', ['$sce', function($sce) {
143 | return {
144 | restrict: 'A',
145 | link: function(scope) {
146 | if (scope.post.content) {
147 | scope.previewContent = $sce.trustAsHtml(markdown.toHTML(scope.post.content));
148 | }
149 | }
150 | }
151 | }]);
152 |
153 | directives.directive('menuLink', ['$window', 'DOM_EVENTS', function($window, DOM_EVENTS) {
154 | return {
155 | restrict: 'A',
156 | link: function(scope, element, attrs) {
157 | var elem = element[0];
158 |
159 | elem[DOM_EVENTS.onclick] = function(e) {
160 | var active = 'active';
161 | e.preventDefault();
162 | $window.document.getElementById('layout').classList.toggle(active);
163 | $window.document.getElementById('menu').classList.toggle(active);
164 | this.classList.toggle(active);
165 | };
166 |
167 | }
168 | };
169 | }]);
170 | directives.directive('onScroll', ['DOM_EVENTS', 'CUSTOM_EVENTS', '$window', function(DOM_EVENTS, CUSTOM_EVENTS, $window) {
171 | return {
172 | restrict: 'A',
173 | link: function(scope, element, attrs) {
174 | //var elem = element[0];
175 | var documentElement = $window.document.documentElement;
176 | var body = $window.document.body;
177 | var bindScroll = function() {
178 | IsScrollToBottom() && scope.$broadcast(CUSTOM_EVENTS.loadMore);
179 | };
180 | //elem[DOM_EVENTS.onscroll] = bindScroll;
181 | angular.element($window).bind('scroll', bindScroll);
182 | scope.$on(CUSTOM_EVENTS.loading, function() {
183 | //elem[DOM_EVENTS.scroll] = null;
184 | angular.element($window).unbind('scroll', bindScroll);
185 | });
186 | scope.$on(CUSTOM_EVENTS.loaded, function() {
187 | //elem[DOM_EVENTS.scroll] = bindScroll;
188 | angular.element($window).bind('scroll', bindScroll);
189 | });
190 | scope.$on('$routeChangeStart', function(evt, next, current) {
191 | angular.element($window).unbind('scroll', bindScroll);
192 | });
193 |
194 | function IsScrollToBottom() {
195 | var scrollTop = 0;
196 | var clientHeight = 0;
197 | var scrollHeight = Math.max(body.scrollHeight, documentElement.scrollHeight) || 0;
198 | if (documentElement && documentElement.scrollTop) {
199 | scrollTop = documentElement.scrollTop;
200 | } else if (body) {
201 | scrollTop = body.scrollTop;
202 | }
203 | if (body.clientHeight && documentElement.clientHeight) {
204 | clientHeight = (body.clientHeight < documentElement.clientHeight) ? body.clientHeight : documentElement.clientHeight;
205 | } else {
206 | clientHeight = (body.clientHeight > documentElement.clientHeight) ? body.clientHeight : documentElement.clientHeight;
207 | }
208 | return scrollTop + clientHeight >= scrollHeight ? true : false;
209 | }
210 | }
211 | };
212 | }]);
213 | directives.directive('scrollTo', ['$window', function($window) {
214 | return {
215 | restrict: 'A',
216 | link: function(scope, element, attrs) {
217 | var documentElement = $window.document.documentElement;
218 | var body = $window.document.body;
219 |
220 | scope.scrollTo = function(pos) {
221 | if (pos === 0) {
222 | body.scrollTop = 0;
223 | documentElement.scrollTop = 0;
224 | } else if (pos === -1) {
225 | var scrollHeight = Math.max(body.scrollHeight, documentElement.scrollHeight) || 0;
226 | var clientHeight = 0;
227 | if (body.clientHeight && documentElement.clientHeight) {
228 | clientHeight = (body.clientHeight < documentElement.clientHeight) ? body.clientHeight : documentElement.clientHeight;
229 | } else {
230 | clientHeight = (body.clientHeight > documentElement.clientHeight) ? body.clientHeight : documentElement.clientHeight;
231 | }
232 | body.scrollTop = scrollHeight - clientHeight;
233 | if (!body.scrollTop) {
234 | documentElement.scrollTop = scrollHeight - clientHeight;
235 | }
236 | }
237 | }
238 | }
239 | }
240 | }]);
241 | directives.directive('scrollInto', ['$window', function($window) {
242 | return {
243 | restrict: 'A',
244 | link: function(scope, element, attrs) {
245 | scope.scrollInto = function() {
246 | $window.document.getElementById(attrs.scrollInto).scrollIntoView();
247 | }
248 | }
249 | }
250 | }]);
251 | directives.directive('fileModel', ['$parse', 'DOM_EVENTS', function($parse, DOM_EVENTS) {
252 | return {
253 | restrict: 'A',
254 | scope: false,
255 | link: function(scope, element, attrs) {
256 | var optionsObj = {};
257 | /*if (scope.success) {
258 | optionsObj.success = function() {
259 | scope.$apply(function() {
260 | scope.success({e: e, data: data});
261 | });
262 | };
263 | }
264 | if (scope.error) {
265 | optionsObj.error = function() {
266 | scope.$apply(function() {
267 | scope.error({e: e, data: data});
268 | });
269 | };
270 | }
271 | if (scope.progress) {
272 | optionsObj.progress = function(e, data) {
273 | scope.$apply(function() {
274 | scope.progress({e: e, data: data});
275 | });
276 | }
277 | }*/
278 | var value = attrs.fileModel;
279 | var model = $parse('files');
280 | var modelSetter = model.assign;
281 | element[0][DOM_EVENTS.onchange] = function(e) {
282 | scope.$apply(function() {
283 | modelSetter(scope, element[0].files);
284 | });
285 | /*scope.files = (e.srcElement || e.target).files[0];*/
286 | //show images
287 | if (value == 'readAndUpload') {
288 | scope.readFiles(optionsObj);
289 | }
290 | scope.uploadFiles(optionsObj);
291 | }
292 |
293 | }
294 | };
295 | }]);
296 |
297 | directives.directive('focus', function() {
298 | return {
299 | link: function(scope, element, attrs) {
300 | element[0].focus();
301 | }
302 | };
303 | });
304 |
--------------------------------------------------------------------------------
/public/javascripts/mean-blog.min.js:
--------------------------------------------------------------------------------
1 | "use strict";var configs=angular.module("mean.configs",[]);configs.constant("AUTH_EVENTS",{loginSuccess:"auth-login-success",loginFailed:"auth-login-failed",logoutSuccess:"auth-logout-success",sessionTimeout:"auth-session-timeout",notAuthenticated:"auth-not-authenticated",notAuthorized:"auth-not-authorized"}),configs.constant("USER_ROLES",{all:"*",admin:"admin",editor:"editor",guest:"guest"}),configs.constant("DOM_EVENTS",{onclick:"onclick",onscroll:"onscroll",onchange:"onchange",click:"click",scroll:"scroll",change:"change"}),configs.constant("CUSTOM_EVENTS",{loadMore:"load-more",loading:"loading",loaded:"loaded",loadOver:"load-over",readFilesSuccess:"read-files-success",uploadFilesSuccess:"upload-files-success",blogPreviewOpen:"blog-preview-open",blogPreviewClose:"blog-preview-close"});var services=angular.module("mean.services",["ngResource","mean.configs"]);services.factory("AuthInterceptor",["$rootScope","$q","AUTH_EVENTS",function(a,b,c){return{responseError:function(d){return a.$broadcast({401:c.notAuthenticated,403:c.notAuthorized,419:c.sessionTimeout,440:c.sessionTimeout}[d.status],d),b.reject(d)}}}]),services.service("Session",function(){return this.create=function(a,b,c){this.id=a,this.userId=b,this.userRole=c},this.destroy=function(){this.id=null,this.userId=null,this.userRole=null},this}),services.factory("AuthService",["$http","Session",function(a,b){var c={};return c.login=function(c){return a.post("/auth/login",c).then(function(a){var c=a.data.data;return b.create(c.user._id,c.user._id,c.user.role),c})},c.isAuthenticated=function(){return!!b.userId},c.isAuthorized=function(a){return angular.isArray(a)||(a=[a]),c.isAuthenticated()&&-1!==a.indexOf(b.userRole)},c}]),services.factory("Post",["$resource",function(a){return a("/posts/:id",{id:"@_id"},{update:{method:"PUT"}})}]),services.factory("MultiPostLoader",["Post","$q",function(a,b){return function(c){var d=b.defer();return a.query(c,function(a){d.resolve(a)},function(){d.reject("Unable to fetch posts")}),d.promise}}]),services.factory("PostLoader",["Post","$route","$q",function(a,b,c){return function(){var d=c.defer();return a.get({id:b.current.params.postId},function(a){d.resolve(a)},function(){d.reject("Unable to fetch post "+b.current.params.postId)}),d.promise}}]),services.factory("fileReader",["$q",function(a){var b=function(a,b,c){return function(){c.$apply(function(){b.resolve(a.result)})}},c=function(a,b,c){return function(){c.$apply(function(){b.reject(a.result)})}},d=function(a,d){var e=new FileReader;return e.onload=b(e,a,d),e.onerror=c(e,a,d),e},e=function(b,c){var e=a.defer(),f=d(e,c);return f.readAsDataURL(b),e.promise};return{readAsDataUrl:e}}]),services.service("fileUpload",["$http",function(a){this.uploadToUrl=function(b,c){var d=new FormData;angular.forEach(c,function(a,b){d.append(b,a)});var e={method:"POST",url:b,data:d,headers:{"Content-Type":void 0},transformRequest:angular.identity};return a(e)}}]);var directives=angular.module("mean.directives",["mean.configs"]);directives.directive("pwCheck",[function(){return{restrict:"A",require:"ngModel",link:function(a,b,c,d){var e=b.inheritedData("$formController")[c.pwCheck];d.$parsers.push(function(a){return d.$setValidity("pwconfirm",a===e.$viewValue),a}),e.$parsers.push(function(a){return d.$setValidity("pwconfirm",a===d.$viewValue),a})}}}]),directives.directive("ensureUnique",function($http){return{restrict:"A",require:"ngModel",link:function(scope,elem,attrs,ctrl){scope.$watch(attrs.ngModel,function(){var data={},field=attrs.ensureUnique,index=field.indexOf("."),value={};index>-1?(data.model=field.substring(0,index),data.field=field.substring(index+1)):(data.model="user",data.field=field),value[data.field]=eval("scope."+attrs.ngModel),data.value=value,$http({method:"POST",url:"/api/checkUnique",data:data}).success(function(a,b,c,d){ctrl.$setValidity("unique",a.isUnique)}).error(function(a,b,c,d){ctrl.$setValidity("unique",!1)})})}}}),directives.directive("authDialog",["AUTH_EVENTS",function(a){return{restrict:"A",template:'',link:function(b){b.visible=!1,b.toggle=!0;var c=function(a,c){b.visible=!0,"login"===c?b.toggle=!0:b.toggle=!1},d=function(){b.visible=!1};b.closeDialog=d,b.$on(a.notAuthenticated,c),b.$on(a.sessionTimeout,c),b.$on(a.loginSuccess,d)}}}]),directives.directive("blogDialog",["CUSTOM_EVENTS","$sce",function(a,b){return{restrict:"A",template:'',link:function(c){c.visible=!1;var d=function(a,d){c.visible=!0,c.post.content&&(c.previewContent=b.trustAsHtml(markdown.toHTML(c.post.content)))},e=function(){c.visible=!1};c.closeDialog=e,c.$on(a.blogPreviewOpen,d),c.$on(a.blogPreviewClose,e)}}}]),directives.directive("markDown",["$sce",function(a){return{restrict:"A",link:function(b){b.post.content&&(b.previewContent=a.trustAsHtml(markdown.toHTML(b.post.content)))}}}]),directives.directive("menuLink",["$window","DOM_EVENTS",function(a,b){return{restrict:"A",link:function(c,d,e){var f=d[0];f[b.onclick]=function(b){var c="active";b.preventDefault(),a.document.getElementById("layout").classList.toggle(c),a.document.getElementById("menu").classList.toggle(c),this.classList.toggle(c)}}}}]),directives.directive("whenScrolled",["DOM_EVENTS","CUSTOM_EVENTS","$window",function(a,b,c){return{restrict:"A",link:function(d,e,f){function g(){var a=0,b=0,c=Math.max(j.scrollHeight,i.scrollHeight)||0;return i&&i.scrollTop?a=i.scrollTop:j&&(a=j.scrollTop),b=j.clientHeight&&i.clientHeight?j.clientHeighti.clientHeight?j.clientHeight:i.clientHeight,a+b>=c?!0:!1}var h=e[0],i=c.document.documentElement,j=c.document.body,k=function(){g()&&d.$broadcast(b.loadMore)};h[a.onscroll]=k,d.$on(b.loading,function(){h[a.scroll]=null}),d.$on(b.loaded,function(){h[a.scroll]=k})}}}]),directives.directive("scrollTo",["$window",function(a){return{restrict:"A",link:function(b,c,d){var e=a.document.documentElement,f=a.document.body;b.scrollTo=function(a){if(0===a)f.scrollTop=0;else if(-1===a){var b=Math.max(f.scrollHeight,e.scrollHeight)||0,c=0;c=f.clientHeight&&e.clientHeight?f.clientHeighte.clientHeight?f.clientHeight:e.clientHeight,f.scrollTop=b-c}}}}}]),directives.directive("fileModel",["$parse","DOM_EVENTS",function(a,b){return{restrict:"A",scope:!1,link:function(c,d,e){var f={},g=e.fileModel,h=a("files"),i=h.assign;d[0][b.onchange]=function(a){c.$apply(function(){i(c,d[0].files)}),"readAndUpload"==g&&c.readFiles(f),c.uploadFiles(f)}}}}]),directives.directive("focus",function(){return{link:function(a,b,c){b[0].focus()}}});var app=angular.module("mean",["ngRoute","ngAnimate","mean.directives","mean.services","mean.configs"]);app.config(["$routeProvider","$locationProvider","$httpProvider","USER_ROLES",function(a,b,c,d){a.when("/about",{templateUrl:"/about"}).when("/contact",{templateUrl:"/contact"}).when("/auth/restricted",{templateUrl:"/auth/restricted",data:{authorizedRoles:[d.admin,d.editor,d.guest]}}).when("/blog",{controller:"BlogListCtrl",resolve:{posts:["MultiPostLoader",function(a){return function(b){return a(b)}}]},templateUrl:"/blog/index"}).when("/blog/show/:postId",{controller:"BlogShowCtrl",resolve:{post:["PostLoader",function(a){return a()}]},templateUrl:"/blog/show"}).when("/blog/create",{controller:"BlogCreateCtrl",templateUrl:"/blog/create",data:{authorizedRoles:[d.admin,d.editor]}}).when("/blog/edit/:postId",{controller:"BlogEditCtrl",resolve:{post:["PostLoader",function(a){return a()}]},templateUrl:"/blog/edit",data:{authorizedRoles:[d.admin,d.editor]}}).otherwise({redirectTo:"/blog"}),c.interceptors.push(["$injector",function(a){return a.get("AuthInterceptor")}])}]),app.run(["$rootScope","$location","AuthService","AUTH_EVENTS",function(a,b,c,d){a.$on("$routeChangeStart",function(b,e,f){NProgress.start();var g=e.$$route?e.$$route.data:null;if(g){var h=g.authorizedRoles;c.isAuthorized(h)||(b.preventDefault(),NProgress.done(),c.isAuthenticated()?(a.$broadcast(d.notAuthorized),console.log("user is not allowed")):a.$broadcast(d.notAuthenticated,"login"))}}),a.$on("$routeChangeSuccess",function(a,b,c){NProgress.done()}),a.$on("$routeChangeError",function(a,b,c){NProgress.done()})}]),app.controller("ApplicationController",["$scope","USER_ROLES","AuthService","Session","$window","AUTH_EVENTS","CUSTOM_EVENTS",function(a,b,c,d,e,f,g){a.currentUser=null,a.BlogCount=null,a.userRoles=b,a.isAuthorized=c.isAuthorized,a.currentRoutePath="/blog";var h=e.clientUser;a.$on("$routeChangeSuccess",function(b,c,d){c.$$route&&(a.currentRoutePath=c.$$route.originalPath)}),a.authDialog=function(b){a.$broadcast(f.notAuthenticated,b)},a.blogDialog=function(){a.$broadcast(g.blogPreviewOpen)},a.setCurrentUser=function(b){a.currentUser=b},a.setBlogCount=function(b){a.BlogCount=b},h&&(d.create(h._id,h._id,h.role),a.setCurrentUser(h))}]),app.controller("LoginCtrl",["$scope","$rootScope","$location","$window","AuthService","AUTH_EVENTS",function(a,b,c,d,e,f){a.credentials={username:"",password:""},a.login=function(c){e.login(c).then(function(c){b.$broadcast(f.loginSuccess,c),a.setCurrentUser(c.user)},function(a){b.$broadcast(f.loginFailed,a),console.log(a)})}}]),app.controller("RegisterCtrl",["$scope","$location","$http","$rootScope",function(a,b,c,d){a.user={username:"",password:"",password_confirmation:"",email:""},a.register=function(){c.post("/auth/register",{username:a.user.username,password:a.user.password,password_confirmation:a.user.password_confirmation,email:a.user.email}).success(function(a){d.$broadcast(AUTH_EVENTS.loginSuccess,a)}).error(function(a){console.log(data)})}}]),app.controller("BlogListCtrl",["$scope","posts","CUSTOM_EVENTS",function(a,b,c){function d(c){return b(c).then(function(b){a.posts=a.posts.concat(b),a.setBlogCount(a.posts.length)},function(a){})}a.posts=[],d({skip:0,limit:12}),a.$on(c.loadMore,function(b){a.$emit(c.loading),NProgress.start(),setTimeout(function(){d({skip:a.posts.length,limit:12}).then(function(){a.$emit(c.loaded),NProgress.done()})},1e3)})}]),app.controller("BlogCreateCtrl",["$scope","$location","Post","CUSTOM_EVENTS",function(a,b,c,d){a.post=new c,a.imageDataUrlList=[],a.$on(d.readFilesSuccess,function(b,c){a.imageDataUrlList=a.imageDataUrlList.concat(c)}),a.$on(d.uploadFilesSuccess,function(b,c){var d="\n";angular.forEach(c,function(a,b){d+="'}),a.post.content=a.post.content?a.post.content+d:d,a.post.imgList=angular.isArray(a.post.imgList)?a.post.imgList.concat(c):c}),a.save=function(){a.post.user=a.currentUser,a.post.$save(function(a){var c=a.data;b.path("/blog/show/"+c._id)})}}]),app.controller("BlogShowCtrl",["$scope","$location","post",function(a,b,c){a.post=c.data}]),app.controller("BlogEditCtrl",["$scope","$location","post","Post","CUSTOM_EVENTS",function(a,b,c,d,e){a.post=new d(c.data),a.imageDataUrlList=[],a.$on(e.readFilesSuccess,function(b,c){a.imageDataUrlList=a.imageDataUrlList.concat(c)}),a.$on(e.uploadFilesSuccess,function(b,c){var d="\n";angular.forEach(c,function(a,b){d+="'}),a.post.content=a.post.content?a.post.content+d:d,a.post.imgList=angular.isArray(a.post.imgList)?a.post.imgList.concat(c):c});var f=c.data._id;a.update=function(){a.post.$update(function(a){a.data;b.path("/blog/show/"+f)})},a.remove=function(){1==confirm("Are you sure to delete it ? ")&&a.post.$delete(function(){b.path("/blog")})}}]),app.controller("UploaderController",["$scope","fileReader","fileUpload","$rootScope","CUSTOM_EVENTS",function(a,b,c,d,e){a.readFiles=function(c){var f=[],g=a.files;angular.forEach(g,function(g,h,i){b.readAsDataUrl(g,a).then(function(a){f.push(a),i.length-1==h&&(d.$broadcast(e.readFilesSuccess,f),c.success&&c.success())},function(a){c.error&&c.error()})})},a.uploadFinished=function(a,b){console.log("We just finished uploading this baby...")},a.uploadFiles=function(b){var f,g=a.files;c.uploadToUrl("/posts/upload",g).then(function(a){var c=a.data;f=c.data,d.$broadcast(e.uploadFilesSuccess,f),b.success&&b.success()},function(){console.log("error"),b.error&&b.error()})}}]),app.controller("IngredientsCtrl",["$scope",function(a){a.removeImageDataUrl=function(b){a.imageDataUrlList.splice(b,1),a.post.imgList.splice(b,1)}}]);
--------------------------------------------------------------------------------
/public/javascripts/services/services.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var services = angular.module('mean.services', ['ngResource', 'mean.configs']);
4 |
5 | services.factory('AuthInterceptor', ['$rootScope', '$q', 'AUTH_EVENTS', function($rootScope, $q, AUTH_EVENTS) {
6 | return {
7 | responseError: function(response) {
8 | $rootScope.$broadcast({
9 | 401: AUTH_EVENTS.notAuthenticated,
10 | 403: AUTH_EVENTS.notAuthorized,
11 | 419: AUTH_EVENTS.sessionTimeout,
12 | 440: AUTH_EVENTS.sessionTimeout
13 | }[response.status], response);
14 | return $q.reject(response);
15 | }
16 | };
17 | }]);
18 |
19 | services.service('Session', function() {
20 | this.create = function(sessionId, userId, userRole) {
21 | this.id = sessionId;
22 | this.userId = userId;
23 | this.userRole = userRole;
24 | };
25 | this.destroy = function() {
26 | this.id = null;
27 | this.userId = null;
28 | this.userRole = null;
29 | };
30 | return this;
31 | });
32 |
33 | services.factory('AuthService', ['$http', 'Session', function($http, Session) {
34 | var authService = {};
35 | authService.login = function(credentials) {
36 | return $http.post('/auth/login', credentials).then(function(res) {
37 | var data = res.data.data;
38 | Session.create(data.user._id, data.user._id, data.user.role);
39 | return data;
40 | });
41 | };
42 |
43 | authService.isAuthenticated = function() {
44 | return !!Session.userId;
45 | };
46 |
47 | authService.isAuthorized = function(authorizedRoles) {
48 | if (!angular.isArray(authorizedRoles)) {
49 | authorizedRoles = [authorizedRoles];
50 | }
51 | return (authService.isAuthenticated() && authorizedRoles.indexOf(Session.userRole) !== -1);
52 | };
53 | authService.isOwner = function(id) {
54 | return (authService.isAuthenticated() && Session.userId === id);
55 | };
56 | return authService;
57 | }]);
58 |
59 | services.factory('Post', ['$resource', function($resource) {
60 | return $resource('/posts/:id', { id: '@_id' }, { update: { method: 'PUT' }, vote: { method: 'PUT', url: '/posts/vote/:id' } });
61 | }]);
62 |
63 | services.factory('MultiPostLoader', ['Post', '$q', function(Post, $q) {
64 | return function(params) {
65 | var delay = $q.defer();
66 | Post.query(params, function(posts) {
67 | delay.resolve(posts);
68 | }, function() {
69 | delay.reject('Unable to fetch posts');
70 | });
71 | return delay.promise;
72 | };
73 | }]);
74 |
75 | services.factory('PostLoader', ['Post', '$route', '$q', function(Post, $route, $q) {
76 | return function() {
77 | var delay = $q.defer();
78 | Post.get({ id: $route.current.params.postId }, function(post) {
79 | delay.resolve(post);
80 | }, function() {
81 | delay.reject('Unable to fetch post ' + $route.current.params.postId);
82 | });
83 | return delay.promise;
84 | };
85 | }]);
86 |
87 | services.factory('User', ['$resource', function($resource) {
88 | return $resource('/users/:id', { id: '@_id' }, { update: { method: 'PUT' } });
89 | }]);
90 |
91 | services.factory('UserLoader', ['User', '$route', '$q', function(User, $route, $q) {
92 | return function() {
93 | var delay = $q.defer();
94 | User.get({ id: $route.current.params.userId }, function(user) {
95 | delay.resolve(user);
96 | }, function() {
97 | delay.reject('Unable to fetch user ' + $route.current.params.userId);
98 | });
99 | return delay.promise;
100 | };
101 | }]);
102 |
103 | services.factory('Comment', ['$resource', function($resource) {
104 | return $resource('/comments/:id', { id: '@_id' }, { update: { method: 'PUT' } });
105 | }]);
106 |
107 | services.factory('fileReader', ["$q", function($q) {
108 | var onLoad = function(reader, deferred, scope) {
109 | return function() {
110 | scope.$apply(function() {
111 | deferred.resolve(reader.result);
112 | });
113 | };
114 | };
115 | var onError = function(reader, deferred, scope) {
116 | return function() {
117 | scope.$apply(function() {
118 | deferred.reject(reader.result);
119 | });
120 | };
121 | };
122 | var getReader = function(deferred, scope) {
123 | var reader = new FileReader();
124 | reader.onload = onLoad(reader, deferred, scope);
125 | reader.onerror = onError(reader, deferred, scope);
126 | return reader;
127 | };
128 | var readAsDataURL = function(file, scope) {
129 | var deferred = $q.defer();
130 | var reader = getReader(deferred, scope);
131 | reader.readAsDataURL(file);
132 | return deferred.promise;
133 | };
134 | return {
135 | readAsDataUrl: readAsDataURL
136 | };
137 | }]);
138 |
139 | services.service('fileUpload', ['$http', function($http) {
140 | this.uploadToUrl = function(url, data) {
141 | var fd = new FormData();
142 | angular.forEach(data, function(val, key) {
143 | fd.append(key, val);
144 | });
145 | var args = {
146 | method: 'POST',
147 | url: url,
148 | data: fd,
149 | headers: { 'Content-Type': undefined },
150 | transformRequest: angular.identity
151 | };
152 | return $http(args);
153 | };
154 | }]);
155 |
--------------------------------------------------------------------------------
/public/javascripts/vendor/angular-1.1.5/angular-resource.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | AngularJS v1.1.5
3 | (c) 2010-2012 Google, Inc. http://angularjs.org
4 | License: MIT
5 | */
6 | (function(B,f,w){'use strict';f.module("ngResource",["ng"]).factory("$resource",["$http","$parse",function(x,y){function u(b,d){this.template=b;this.defaults=d||{};this.urlParams={}}function v(b,d,e){function j(c,b){var p={},b=m({},d,b);l(b,function(a,b){k(a)&&(a=a());var g;a&&a.charAt&&a.charAt(0)=="@"?(g=a.substr(1),g=y(g)(c)):g=a;p[b]=g});return p}function c(c){t(c||{},this)}var n=new u(b),e=m({},z,e);l(e,function(b,d){b.method=f.uppercase(b.method);var p=b.method=="POST"||b.method=="PUT"||b.method==
7 | "PATCH";c[d]=function(a,d,g,A){function f(){h.$resolved=!0}var i={},e,o=q,r=null;switch(arguments.length){case 4:r=A,o=g;case 3:case 2:if(k(d)){if(k(a)){o=a;r=d;break}o=d;r=g}else{i=a;e=d;o=g;break}case 1:k(a)?o=a:p?e=a:i=a;break;case 0:break;default:throw"Expected between 0-4 arguments [params, data, success, error], got "+arguments.length+" arguments.";}var h=this instanceof c?this:b.isArray?[]:new c(e),s={};l(b,function(a,b){b!="params"&&b!="isArray"&&(s[b]=t(a))});s.data=e;n.setUrlParams(s,m({},
8 | j(e,b.params||{}),i),b.url);i=x(s);h.$resolved=!1;i.then(f,f);h.$then=i.then(function(a){var d=a.data,g=h.$then,e=h.$resolved;if(d)b.isArray?(h.length=0,l(d,function(a){h.push(new c(a))})):(t(d,h),h.$then=g,h.$resolved=e);(o||q)(h,a.headers);a.resource=h;return a},r).then;return h};c.prototype["$"+d]=function(a,b,g){var e=j(this),f=q,i;switch(arguments.length){case 3:e=a;f=b;i=g;break;case 2:case 1:k(a)?(f=a,i=b):(e=a,f=b||q);case 0:break;default:throw"Expected between 1-3 arguments [params, success, error], got "+
9 | arguments.length+" arguments.";}c[d].call(this,e,p?this:w,f,i)}});c.bind=function(c){return v(b,m({},d,c),e)};return c}var z={get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}},q=f.noop,l=f.forEach,m=f.extend,t=f.copy,k=f.isFunction;u.prototype={setUrlParams:function(b,d,e){var j=this,c=e||j.template,n,k,m=j.urlParams={};l(c.split(/\W/),function(b){b&&RegExp("(^|[^\\\\]):"+b+"(\\W|$)").test(c)&&(m[b]=!0)});c=c.replace(/\\:/g,
10 | ":");d=d||{};l(j.urlParams,function(b,a){n=d.hasOwnProperty(a)?d[a]:j.defaults[a];f.isDefined(n)&&n!==null?(k=encodeURIComponent(n).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"%20").replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+"),c=c.replace(RegExp(":"+a+"(\\W|$)","g"),k+"$1")):c=c.replace(RegExp("(/?):"+a+"(\\W|$)","g"),function(b,a,c){return c.charAt(0)=="/"?c:a+c})});c=c.replace(/\/+$/,"");c=c.replace(/\/\.(?=\w+($|\?))/,".");
11 | b.url=c.replace(/\/\\\./,"/.");l(d,function(c,a){if(!j.urlParams[a])b.params=b.params||{},b.params[a]=c})}};return v}])})(window,window.angular);
12 |
--------------------------------------------------------------------------------
/public/javascripts/vendor/angular-1.5.0/angular-resource.min.js:
--------------------------------------------------------------------------------
1 | /*
2 | AngularJS v1.5.0
3 | (c) 2010-2016 Google, Inc. http://angularjs.org
4 | License: MIT
5 | */
6 | (function(Q,d,G){'use strict';function H(t,g){g=g||{};d.forEach(g,function(d,q){delete g[q]});for(var q in t)!t.hasOwnProperty(q)||"$"===q.charAt(0)&&"$"===q.charAt(1)||(g[q]=t[q]);return g}var z=d.$$minErr("$resource"),N=/^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/;d.module("ngResource",["ng"]).provider("$resource",function(){var t=/^https?:\/\/[^\/]*/,g=this;this.defaults={stripTrailingSlashes:!0,actions:{get:{method:"GET"},save:{method:"POST"},query:{method:"GET",isArray:!0},remove:{method:"DELETE"},"delete":{method:"DELETE"}}};
7 | this.$get=["$http","$log","$q","$timeout",function(q,M,I,J){function A(d,h){return encodeURIComponent(d).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,h?"%20":"+")}function B(d,h){this.template=d;this.defaults=v({},g.defaults,h);this.urlParams={}}function K(e,h,n,k){function c(a,b){var c={};b=v({},h,b);u(b,function(b,h){x(b)&&(b=b());var f;if(b&&b.charAt&&"@"==b.charAt(0)){f=a;var l=b.substr(1);if(null==l||""===l||"hasOwnProperty"===l||!N.test("."+
8 | l))throw z("badmember",l);for(var l=l.split("."),m=0,k=l.length;m";q=("hidden" in t);e=t.childNodes.length==1||(function(){(m.createElement)("a");var v=m.createDocumentFragment();return(typeof v.cloneNode=="undefined"||typeof v.createDocumentFragment=="undefined"||typeof v.createElement=="undefined")}())}catch(u){q=true;e=true}}());function f(t,v){var w=t.createElement("p"),u=t.getElementsByTagName("head")[0]||t.documentElement;w.innerHTML="x";return u.insertBefore(w.lastChild,u.firstChild)}function l(){var t=j.elements;return typeof t=="string"?t.split(" "):t}function p(t){var u=o[t[i]];if(!u){u={};a++;t[i]=a;o[a]=u}return u}function n(w,t,v){if(!t){t=m}if(e){return t.createElement(w)}if(!v){v=p(t)}var u;if(v.cache[w]){u=v.cache[w].cloneNode()}else{if(c.test(w)){u=(v.cache[w]=v.createElem(w)).cloneNode()}else{u=v.createElem(w)}}return u.canHaveChildren&&!h.test(w)?v.frag.appendChild(u):u}function r(v,x){if(!v){v=m}if(e){return v.createDocumentFragment()}x=x||p(v);var y=x.frag.cloneNode(),w=0,u=l(),t=u.length;for(;wr;r++)j[n[r]]=!!(n[r]in E);return j.list&&(j.list=!(!t.createElement("datalist")||!e.HTMLDataListElement)),j}("autocomplete autofocus list placeholder max min multiple pattern required step".split(" ")),p.inputtypes=function(e){for(var r,o,a,i=0,c=e.length;c>i;i++)E.setAttribute("type",o=e[i]),r="text"!==E.type,r&&(E.value=x,E.style.cssText="position:absolute;visibility:hidden;",/^range$/.test(o)&&E.style.WebkitAppearance!==n?(g.appendChild(E),a=t.defaultView,r=a.getComputedStyle&&"textfield"!==a.getComputedStyle(E,null).WebkitAppearance&&0!==E.offsetHeight,g.removeChild(E)):/^(search|tel)$/.test(o)||(r=/^(url|email)$/.test(o)?E.checkValidity&&E.checkValidity()===!1:E.value!=x)),P[e[i]]=!!r;return P}("search tel url email datetime date month week time datetime-local number range color".split(" "))}var d,f,m="2.8.3",p={},h=!0,g=t.documentElement,v="modernizr",y=t.createElement(v),b=y.style,E=t.createElement("input"),x=":)",w={}.toString,S=" -webkit- -moz- -o- -ms- ".split(" "),C="Webkit Moz O ms",k=C.split(" "),T=C.toLowerCase().split(" "),N={svg:"http://www.w3.org/2000/svg"},M={},P={},j={},$=[],D=$.slice,F=function(e,n,r,o){var a,i,c,s,u=t.createElement("div"),l=t.body,d=l||t.createElement("body");if(parseInt(r,10))for(;r--;)c=t.createElement("div"),c.id=o?o[r]:v+(r+1),u.appendChild(c);return a=["",'"].join(""),u.id=v,(l?u:d).innerHTML+=a,d.appendChild(u),l||(d.style.background="",d.style.overflow="hidden",s=g.style.overflow,g.style.overflow="hidden",g.appendChild(d)),i=n(u,e),l?u.parentNode.removeChild(u):(d.parentNode.removeChild(d),g.style.overflow=s),!!i},z=function(t){var n=e.matchMedia||e.msMatchMedia;if(n)return n(t)&&n(t).matches||!1;var r;return F("@media "+t+" { #"+v+" { position: absolute; } }",function(t){r="absolute"==(e.getComputedStyle?getComputedStyle(t,null):t.currentStyle).position}),r},A=function(){function e(e,o){o=o||t.createElement(r[e]||"div"),e="on"+e;var i=e in o;return i||(o.setAttribute||(o=t.createElement("div")),o.setAttribute&&o.removeAttribute&&(o.setAttribute(e,""),i=a(o[e],"function"),a(o[e],"undefined")||(o[e]=n),o.removeAttribute(e))),o=null,i}var r={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return e}(),L={}.hasOwnProperty;f=a(L,"undefined")||a(L.call,"undefined")?function(e,t){return t in e&&a(e.constructor.prototype[t],"undefined")}:function(e,t){return L.call(e,t)},Function.prototype.bind||(Function.prototype.bind=function(e){var t=this;if("function"!=typeof t)throw new TypeError;var n=D.call(arguments,1),r=function(){if(this instanceof r){var o=function(){};o.prototype=t.prototype;var a=new o,i=t.apply(a,n.concat(D.call(arguments)));return Object(i)===i?i:a}return t.apply(e,n.concat(D.call(arguments)))};return r}),M.flexbox=function(){return u("flexWrap")},M.flexboxlegacy=function(){return u("boxDirection")},M.canvas=function(){var e=t.createElement("canvas");return!(!e.getContext||!e.getContext("2d"))},M.canvastext=function(){return!(!p.canvas||!a(t.createElement("canvas").getContext("2d").fillText,"function"))},M.webgl=function(){return!!e.WebGLRenderingContext},M.touch=function(){var n;return"ontouchstart"in e||e.DocumentTouch&&t instanceof DocumentTouch?n=!0:F(["@media (",S.join("touch-enabled),("),v,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(e){n=9===e.offsetTop}),n},M.geolocation=function(){return"geolocation"in navigator},M.postmessage=function(){return!!e.postMessage},M.websqldatabase=function(){return!!e.openDatabase},M.indexedDB=function(){return!!u("indexedDB",e)},M.hashchange=function(){return A("hashchange",e)&&(t.documentMode===n||t.documentMode>7)},M.history=function(){return!(!e.history||!history.pushState)},M.draganddrop=function(){var e=t.createElement("div");return"draggable"in e||"ondragstart"in e&&"ondrop"in e},M.websockets=function(){return"WebSocket"in e||"MozWebSocket"in e},M.rgba=function(){return r("background-color:rgba(150,255,150,.5)"),i(b.backgroundColor,"rgba")},M.hsla=function(){return r("background-color:hsla(120,40%,100%,.5)"),i(b.backgroundColor,"rgba")||i(b.backgroundColor,"hsla")},M.multiplebgs=function(){return r("background:url(https://),url(https://),red url(https://)"),/(url\s*\(.*?){3}/.test(b.background)},M.backgroundsize=function(){return u("backgroundSize")},M.borderimage=function(){return u("borderImage")},M.borderradius=function(){return u("borderRadius")},M.boxshadow=function(){return u("boxShadow")},M.textshadow=function(){return""===t.createElement("div").style.textShadow},M.opacity=function(){return o("opacity:.55"),/^0.55$/.test(b.opacity)},M.cssanimations=function(){return u("animationName")},M.csscolumns=function(){return u("columnCount")},M.cssgradients=function(){var e="background-image:",t="gradient(linear,left top,right bottom,from(#9f9),to(white));",n="linear-gradient(left top,#9f9, white);";return r((e+"-webkit- ".split(" ").join(t+e)+S.join(n+e)).slice(0,-e.length)),i(b.backgroundImage,"gradient")},M.cssreflections=function(){return u("boxReflect")},M.csstransforms=function(){return!!u("transform")},M.csstransforms3d=function(){var e=!!u("perspective");return e&&"webkitPerspective"in g.style&&F("@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}",function(t){e=9===t.offsetLeft&&3===t.offsetHeight}),e},M.csstransitions=function(){return u("transition")},M.fontface=function(){var e;return F('@font-face {font-family:"font";src:url("https://")}',function(n,r){var o=t.getElementById("smodernizr"),a=o.sheet||o.styleSheet,i=a?a.cssRules&&a.cssRules[0]?a.cssRules[0].cssText:a.cssText||"":"";e=/src/i.test(i)&&0===i.indexOf(r.split(" ")[0])}),e},M.generatedcontent=function(){var e;return F(["#",v,"{font:0/0 a}#",v,':after{content:"',x,'";visibility:hidden;font:3px/1 a}'].join(""),function(t){e=t.offsetHeight>=3}),e},M.video=function(){var e=t.createElement("video"),n=!1;try{(n=!!e.canPlayType)&&(n=new Boolean(n),n.ogg=e.canPlayType('video/ogg; codecs="theora"').replace(/^no$/,""),n.h264=e.canPlayType('video/mp4; codecs="avc1.42E01E"').replace(/^no$/,""),n.webm=e.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,""))}catch(r){}return n},M.audio=function(){var e=t.createElement("audio"),n=!1;try{(n=!!e.canPlayType)&&(n=new Boolean(n),n.ogg=e.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,""),n.mp3=e.canPlayType("audio/mpeg;").replace(/^no$/,""),n.wav=e.canPlayType('audio/wav; codecs="1"').replace(/^no$/,""),n.m4a=(e.canPlayType("audio/x-m4a;")||e.canPlayType("audio/aac;")).replace(/^no$/,""))}catch(r){}return n},M.localstorage=function(){try{return localStorage.setItem(v,v),localStorage.removeItem(v),!0}catch(e){return!1}},M.sessionstorage=function(){try{return sessionStorage.setItem(v,v),sessionStorage.removeItem(v),!0}catch(e){return!1}},M.webworkers=function(){return!!e.Worker},M.applicationcache=function(){return!!e.applicationCache},M.svg=function(){return!!t.createElementNS&&!!t.createElementNS(N.svg,"svg").createSVGRect},M.inlinesvg=function(){var e=t.createElement("div");return e.innerHTML="",(e.firstChild&&e.firstChild.namespaceURI)==N.svg},M.smil=function(){return!!t.createElementNS&&/SVGAnimate/.test(w.call(t.createElementNS(N.svg,"animate")))},M.svgclippaths=function(){return!!t.createElementNS&&/SVGClipPath/.test(w.call(t.createElementNS(N.svg,"clipPath")))};for(var H in M)f(M,H)&&(d=H.toLowerCase(),p[d]=M[H](),$.push((p[d]?"":"no-")+d));return p.input||l(),p.addTest=function(e,t){if("object"==typeof e)for(var r in e)f(e,r)&&p.addTest(r,e[r]);else{if(e=e.toLowerCase(),p[e]!==n)return p;t="function"==typeof t?t():t,"undefined"!=typeof h&&h&&(g.className+=" "+(t?"":"no-")+e),p[e]=t}return p},r(""),y=E=null,function(e,t){function n(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x",r.insertBefore(n.lastChild,r.firstChild)}function r(){var e=y.elements;return"string"==typeof e?e.split(" "):e}function o(e){var t=v[e[h]];return t||(t={},g++,e[h]=g,v[g]=t),t}function a(e,n,r){if(n||(n=t),l)return n.createElement(e);r||(r=o(n));var a;return a=r.cache[e]?r.cache[e].cloneNode():p.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||m.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function i(e,n){if(e||(e=t),l)return e.createDocumentFragment();n=n||o(e);for(var a=n.frag.cloneNode(),i=0,c=r(),s=c.length;s>i;i++)a.createElement(c[i]);return a}function c(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return y.shivMethods?a(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+r().join().replace(/[\w\-]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(y,t.frag)}function s(e){e||(e=t);var r=o(e);return!y.shivCSS||u||r.hasCSS||(r.hasCSS=!!n(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||c(e,r),e}var u,l,d="3.7.0",f=e.html5||{},m=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,h="_html5shiv",g=0,v={};!function(){try{var e=t.createElement("a");e.innerHTML="",u="hidden"in e,l=1==e.childNodes.length||function(){t.createElement("a");var e=t.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(n){u=!0,l=!0}}();var y={elements:f.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:d,shivCSS:f.shivCSS!==!1,supportsUnknownElements:l,shivMethods:f.shivMethods!==!1,type:"default",shivDocument:s,createElement:a,createDocumentFragment:i};e.html5=y,s(t)}(this,t),p._version=m,p._prefixes=S,p._domPrefixes=T,p._cssomPrefixes=k,p.mq=z,p.hasEvent=A,p.testProp=function(e){return c([e])},p.testAllProps=u,p.testStyles=F,p.prefixed=function(e,t,n){return t?u(e,t,n):u(e,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(h?" js "+$.join(" "):""),p}(this,this.document);
--------------------------------------------------------------------------------
/public/javascripts/vendor/nprogress.js:
--------------------------------------------------------------------------------
1 | /* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress
2 | * @license MIT */
3 |
4 | ;(function(root, factory) {
5 |
6 | if (typeof define === 'function' && define.amd) {
7 | define(factory);
8 | } else if (typeof exports === 'object') {
9 | module.exports = factory();
10 | } else {
11 | root.NProgress = factory();
12 | }
13 |
14 | })(this, function() {
15 | var NProgress = {};
16 |
17 | NProgress.version = '0.1.6';
18 |
19 | var Settings = NProgress.settings = {
20 | minimum: 0.08,
21 | easing: 'ease',
22 | positionUsing: '',
23 | speed: 200,
24 | trickle: true,
25 | trickleRate: 0.02,
26 | trickleSpeed: 800,
27 | showSpinner: true,
28 | barSelector: '[role="bar"]',
29 | spinnerSelector: '[role="spinner"]',
30 | parent: 'body',
31 | template: ''
32 | };
33 |
34 | /**
35 | * Updates configuration.
36 | *
37 | * NProgress.configure({
38 | * minimum: 0.1
39 | * });
40 | */
41 | NProgress.configure = function(options) {
42 | var key, value;
43 | for (key in options) {
44 | value = options[key];
45 | if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value;
46 | }
47 |
48 | return this;
49 | };
50 |
51 | /**
52 | * Last number.
53 | */
54 |
55 | NProgress.status = null;
56 |
57 | /**
58 | * Sets the progress bar status, where `n` is a number from `0.0` to `1.0`.
59 | *
60 | * NProgress.set(0.4);
61 | * NProgress.set(1.0);
62 | */
63 |
64 | NProgress.set = function(n) {
65 | var started = NProgress.isStarted();
66 |
67 | n = clamp(n, Settings.minimum, 1);
68 | NProgress.status = (n === 1 ? null : n);
69 |
70 | var progress = NProgress.render(!started),
71 | bar = progress.querySelector(Settings.barSelector),
72 | speed = Settings.speed,
73 | ease = Settings.easing;
74 |
75 | progress.offsetWidth; /* Repaint */
76 |
77 | queue(function(next) {
78 | // Set positionUsing if it hasn't already been set
79 | if (Settings.positionUsing === '') Settings.positionUsing = NProgress.getPositioningCSS();
80 |
81 | // Add transition
82 | css(bar, barPositionCSS(n, speed, ease));
83 |
84 | if (n === 1) {
85 | // Fade out
86 | css(progress, {
87 | transition: 'none',
88 | opacity: 1
89 | });
90 | progress.offsetWidth; /* Repaint */
91 |
92 | setTimeout(function() {
93 | css(progress, {
94 | transition: 'all ' + speed + 'ms linear',
95 | opacity: 0
96 | });
97 | setTimeout(function() {
98 | NProgress.remove();
99 | next();
100 | }, speed);
101 | }, speed);
102 | } else {
103 | setTimeout(next, speed);
104 | }
105 | });
106 |
107 | return this;
108 | };
109 |
110 | NProgress.isStarted = function() {
111 | return typeof NProgress.status === 'number';
112 | };
113 |
114 | /**
115 | * Shows the progress bar.
116 | * This is the same as setting the status to 0%, except that it doesn't go backwards.
117 | *
118 | * NProgress.start();
119 | *
120 | */
121 | NProgress.start = function() {
122 | if (!NProgress.status) NProgress.set(0);
123 |
124 | var work = function() {
125 | setTimeout(function() {
126 | if (!NProgress.status) return;
127 | NProgress.trickle();
128 | work();
129 | }, Settings.trickleSpeed);
130 | };
131 |
132 | if (Settings.trickle) work();
133 |
134 | return this;
135 | };
136 |
137 | /**
138 | * Hides the progress bar.
139 | * This is the *sort of* the same as setting the status to 100%, with the
140 | * difference being `done()` makes some placebo effect of some realistic motion.
141 | *
142 | * NProgress.done();
143 | *
144 | * If `true` is passed, it will show the progress bar even if its hidden.
145 | *
146 | * NProgress.done(true);
147 | */
148 |
149 | NProgress.done = function(force) {
150 | if (!force && !NProgress.status) return this;
151 |
152 | return NProgress.inc(0.3 + 0.5 * Math.random()).set(1);
153 | };
154 |
155 | /**
156 | * Increments by a random amount.
157 | */
158 |
159 | NProgress.inc = function(amount) {
160 | var n = NProgress.status;
161 |
162 | if (!n) {
163 | return NProgress.start();
164 | } else {
165 | if (typeof amount !== 'number') {
166 | amount = (1 - n) * clamp(Math.random() * n, 0.1, 0.95);
167 | }
168 |
169 | n = clamp(n + amount, 0, 0.994);
170 | return NProgress.set(n);
171 | }
172 | };
173 |
174 | NProgress.trickle = function() {
175 | return NProgress.inc(Math.random() * Settings.trickleRate);
176 | };
177 |
178 | /**
179 | * Waits for all supplied jQuery promises and
180 | * increases the progress as the promises resolve.
181 | *
182 | * @param $promise jQUery Promise
183 | */
184 | (function() {
185 | var initial = 0, current = 0;
186 |
187 | NProgress.promise = function($promise) {
188 | if (!$promise || $promise.state() == "resolved") {
189 | return this;
190 | }
191 |
192 | if (current == 0) {
193 | NProgress.start();
194 | }
195 |
196 | initial++;
197 | current++;
198 |
199 | $promise.always(function() {
200 | current--;
201 | if (current == 0) {
202 | initial = 0;
203 | NProgress.done();
204 | } else {
205 | NProgress.set((initial - current) / initial);
206 | }
207 | });
208 |
209 | return this;
210 | };
211 |
212 | })();
213 |
214 | /**
215 | * (Internal) renders the progress bar markup based on the `template`
216 | * setting.
217 | */
218 |
219 | NProgress.render = function(fromStart) {
220 | if (NProgress.isRendered()) return document.getElementById('nprogress');
221 |
222 | addClass(document.documentElement, 'nprogress-busy');
223 |
224 | var progress = document.createElement('div');
225 | progress.id = 'nprogress';
226 | progress.innerHTML = Settings.template;
227 |
228 | var bar = progress.querySelector(Settings.barSelector),
229 | perc = fromStart ? '-100' : toBarPerc(NProgress.status || 0),
230 | parent = document.querySelector(Settings.parent),
231 | spinner;
232 |
233 | css(bar, {
234 | transition: 'all 0 linear',
235 | transform: 'translate3d(' + perc + '%,0,0)'
236 | });
237 |
238 | if (!Settings.showSpinner) {
239 | spinner = progress.querySelector(Settings.spinnerSelector);
240 | spinner && removeElement(spinner);
241 | }
242 |
243 | if (parent != document.body) {
244 | addClass(parent, 'nprogress-custom-parent');
245 | }
246 |
247 | parent.appendChild(progress);
248 | return progress;
249 | };
250 |
251 | /**
252 | * Removes the element. Opposite of render().
253 | */
254 |
255 | NProgress.remove = function() {
256 | removeClass(document.documentElement, 'nprogress-busy');
257 | removeClass(document.querySelector(Settings.parent), 'nprogress-custom-parent')
258 | var progress = document.getElementById('nprogress');
259 | progress && removeElement(progress);
260 | };
261 |
262 | /**
263 | * Checks if the progress bar is rendered.
264 | */
265 |
266 | NProgress.isRendered = function() {
267 | return !!document.getElementById('nprogress');
268 | };
269 |
270 | /**
271 | * Determine which positioning CSS rule to use.
272 | */
273 |
274 | NProgress.getPositioningCSS = function() {
275 | // Sniff on document.body.style
276 | var bodyStyle = document.body.style;
277 |
278 | // Sniff prefixes
279 | var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' :
280 | ('MozTransform' in bodyStyle) ? 'Moz' :
281 | ('msTransform' in bodyStyle) ? 'ms' :
282 | ('OTransform' in bodyStyle) ? 'O' : '';
283 |
284 | if (vendorPrefix + 'Perspective' in bodyStyle) {
285 | // Modern browsers with 3D support, e.g. Webkit, IE10
286 | return 'translate3d';
287 | } else if (vendorPrefix + 'Transform' in bodyStyle) {
288 | // Browsers without 3D support, e.g. IE9
289 | return 'translate';
290 | } else {
291 | // Browsers without translate() support, e.g. IE7-8
292 | return 'margin';
293 | }
294 | };
295 |
296 | /**
297 | * Helpers
298 | */
299 |
300 | function clamp(n, min, max) {
301 | if (n < min) return min;
302 | if (n > max) return max;
303 | return n;
304 | }
305 |
306 | /**
307 | * (Internal) converts a percentage (`0..1`) to a bar translateX
308 | * percentage (`-100%..0%`).
309 | */
310 |
311 | function toBarPerc(n) {
312 | return (-1 + n) * 100;
313 | }
314 |
315 |
316 | /**
317 | * (Internal) returns the correct CSS for changing the bar's
318 | * position given an n percentage, and speed and ease from Settings
319 | */
320 |
321 | function barPositionCSS(n, speed, ease) {
322 | var barCSS;
323 |
324 | if (Settings.positionUsing === 'translate3d') {
325 | barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' };
326 | } else if (Settings.positionUsing === 'translate') {
327 | barCSS = { transform: 'translate('+toBarPerc(n)+'%,0)' };
328 | } else {
329 | barCSS = { 'margin-left': toBarPerc(n)+'%' };
330 | }
331 |
332 | barCSS.transition = 'all '+speed+'ms '+ease;
333 |
334 | return barCSS;
335 | }
336 |
337 | /**
338 | * (Internal) Queues a function to be executed.
339 | */
340 |
341 | var queue = (function() {
342 | var pending = [];
343 |
344 | function next() {
345 | var fn = pending.shift();
346 | if (fn) {
347 | fn(next);
348 | }
349 | }
350 |
351 | return function(fn) {
352 | pending.push(fn);
353 | if (pending.length == 1) next();
354 | };
355 | })();
356 |
357 | /**
358 | * (Internal) Applies css properties to an element, similar to the jQuery
359 | * css method.
360 | *
361 | * While this helper does assist with vendor prefixed property names, it
362 | * does not perform any manipulation of values prior to setting styles.
363 | */
364 |
365 | var css = (function() {
366 | var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ],
367 | cssProps = {};
368 |
369 | function camelCase(string) {
370 | return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) {
371 | return letter.toUpperCase();
372 | });
373 | }
374 |
375 | function getVendorProp(name) {
376 | var style = document.body.style;
377 | if (name in style) return name;
378 |
379 | var i = cssPrefixes.length,
380 | capName = name.charAt(0).toUpperCase() + name.slice(1),
381 | vendorName;
382 | while (i--) {
383 | vendorName = cssPrefixes[i] + capName;
384 | if (vendorName in style) return vendorName;
385 | }
386 |
387 | return name;
388 | }
389 |
390 | function getStyleProp(name) {
391 | name = camelCase(name);
392 | return cssProps[name] || (cssProps[name] = getVendorProp(name));
393 | }
394 |
395 | function applyCss(element, prop, value) {
396 | prop = getStyleProp(prop);
397 | element.style[prop] = value;
398 | }
399 |
400 | return function(element, properties) {
401 | var args = arguments,
402 | prop,
403 | value;
404 |
405 | if (args.length == 2) {
406 | for (prop in properties) {
407 | value = properties[prop];
408 | if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value);
409 | }
410 | } else {
411 | applyCss(element, args[1], args[2]);
412 | }
413 | }
414 | })();
415 |
416 | /**
417 | * (Internal) Determines if an element or space separated list of class names contains a class name.
418 | */
419 |
420 | function hasClass(element, name) {
421 | var list = typeof element == 'string' ? element : classList(element);
422 | return list.indexOf(' ' + name + ' ') >= 0;
423 | }
424 |
425 | /**
426 | * (Internal) Adds a class to an element.
427 | */
428 |
429 | function addClass(element, name) {
430 | var oldList = classList(element),
431 | newList = oldList + name;
432 |
433 | if (hasClass(oldList, name)) return;
434 |
435 | // Trim the opening space.
436 | element.className = newList.substring(1);
437 | }
438 |
439 | /**
440 | * (Internal) Removes a class from an element.
441 | */
442 |
443 | function removeClass(element, name) {
444 | var oldList = classList(element),
445 | newList;
446 |
447 | if (!hasClass(element, name)) return;
448 |
449 | // Replace the class name.
450 | newList = oldList.replace(' ' + name + ' ', ' ');
451 |
452 | // Trim the opening and closing spaces.
453 | element.className = newList.substring(1, newList.length - 1);
454 | }
455 |
456 | /**
457 | * (Internal) Gets a space separated list of the class names on the element.
458 | * The list is wrapped with a single space on each end to facilitate finding
459 | * matches within the list.
460 | */
461 |
462 | function classList(element) {
463 | return (' ' + (element.className || '') + ' ').replace(/\s+/gi, ' ');
464 | }
465 |
466 | /**
467 | * (Internal) Removes an element from the DOM.
468 | */
469 |
470 | function removeElement(element) {
471 | element && element.parentNode && element.parentNode.removeChild(element);
472 | }
473 |
474 | return NProgress;
475 | });
476 |
477 |
--------------------------------------------------------------------------------
/public/stylesheets/grids-responsive-min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | Pure v0.6.0
3 | Copyright 2014 Yahoo! Inc. All rights reserved.
4 | Licensed under the BSD License.
5 | https://github.com/yahoo/pure/blob/master/LICENSE.md
6 | */
7 | @media screen and (min-width:35.5em){.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-1-2,.pure-u-sm-1-3,.pure-u-sm-2-3,.pure-u-sm-1-4,.pure-u-sm-3-4,.pure-u-sm-1-5,.pure-u-sm-2-5,.pure-u-sm-3-5,.pure-u-sm-4-5,.pure-u-sm-5-5,.pure-u-sm-1-6,.pure-u-sm-5-6,.pure-u-sm-1-8,.pure-u-sm-3-8,.pure-u-sm-5-8,.pure-u-sm-7-8,.pure-u-sm-1-12,.pure-u-sm-5-12,.pure-u-sm-7-12,.pure-u-sm-11-12,.pure-u-sm-1-24,.pure-u-sm-2-24,.pure-u-sm-3-24,.pure-u-sm-4-24,.pure-u-sm-5-24,.pure-u-sm-6-24,.pure-u-sm-7-24,.pure-u-sm-8-24,.pure-u-sm-9-24,.pure-u-sm-10-24,.pure-u-sm-11-24,.pure-u-sm-12-24,.pure-u-sm-13-24,.pure-u-sm-14-24,.pure-u-sm-15-24,.pure-u-sm-16-24,.pure-u-sm-17-24,.pure-u-sm-18-24,.pure-u-sm-19-24,.pure-u-sm-20-24,.pure-u-sm-21-24,.pure-u-sm-22-24,.pure-u-sm-23-24,.pure-u-sm-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-sm-1-24{width:4.1667%;*width:4.1357%}.pure-u-sm-1-12,.pure-u-sm-2-24{width:8.3333%;*width:8.3023%}.pure-u-sm-1-8,.pure-u-sm-3-24{width:12.5%;*width:12.469%}.pure-u-sm-1-6,.pure-u-sm-4-24{width:16.6667%;*width:16.6357%}.pure-u-sm-1-5{width:20%;*width:19.969%}.pure-u-sm-5-24{width:20.8333%;*width:20.8023%}.pure-u-sm-1-4,.pure-u-sm-6-24{width:25%;*width:24.969%}.pure-u-sm-7-24{width:29.1667%;*width:29.1357%}.pure-u-sm-1-3,.pure-u-sm-8-24{width:33.3333%;*width:33.3023%}.pure-u-sm-3-8,.pure-u-sm-9-24{width:37.5%;*width:37.469%}.pure-u-sm-2-5{width:40%;*width:39.969%}.pure-u-sm-5-12,.pure-u-sm-10-24{width:41.6667%;*width:41.6357%}.pure-u-sm-11-24{width:45.8333%;*width:45.8023%}.pure-u-sm-1-2,.pure-u-sm-12-24{width:50%;*width:49.969%}.pure-u-sm-13-24{width:54.1667%;*width:54.1357%}.pure-u-sm-7-12,.pure-u-sm-14-24{width:58.3333%;*width:58.3023%}.pure-u-sm-3-5{width:60%;*width:59.969%}.pure-u-sm-5-8,.pure-u-sm-15-24{width:62.5%;*width:62.469%}.pure-u-sm-2-3,.pure-u-sm-16-24{width:66.6667%;*width:66.6357%}.pure-u-sm-17-24{width:70.8333%;*width:70.8023%}.pure-u-sm-3-4,.pure-u-sm-18-24{width:75%;*width:74.969%}.pure-u-sm-19-24{width:79.1667%;*width:79.1357%}.pure-u-sm-4-5{width:80%;*width:79.969%}.pure-u-sm-5-6,.pure-u-sm-20-24{width:83.3333%;*width:83.3023%}.pure-u-sm-7-8,.pure-u-sm-21-24{width:87.5%;*width:87.469%}.pure-u-sm-11-12,.pure-u-sm-22-24{width:91.6667%;*width:91.6357%}.pure-u-sm-23-24{width:95.8333%;*width:95.8023%}.pure-u-sm-1,.pure-u-sm-1-1,.pure-u-sm-5-5,.pure-u-sm-24-24{width:100%}}@media screen and (min-width:48em){.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-1-2,.pure-u-md-1-3,.pure-u-md-2-3,.pure-u-md-1-4,.pure-u-md-3-4,.pure-u-md-1-5,.pure-u-md-2-5,.pure-u-md-3-5,.pure-u-md-4-5,.pure-u-md-5-5,.pure-u-md-1-6,.pure-u-md-5-6,.pure-u-md-1-8,.pure-u-md-3-8,.pure-u-md-5-8,.pure-u-md-7-8,.pure-u-md-1-12,.pure-u-md-5-12,.pure-u-md-7-12,.pure-u-md-11-12,.pure-u-md-1-24,.pure-u-md-2-24,.pure-u-md-3-24,.pure-u-md-4-24,.pure-u-md-5-24,.pure-u-md-6-24,.pure-u-md-7-24,.pure-u-md-8-24,.pure-u-md-9-24,.pure-u-md-10-24,.pure-u-md-11-24,.pure-u-md-12-24,.pure-u-md-13-24,.pure-u-md-14-24,.pure-u-md-15-24,.pure-u-md-16-24,.pure-u-md-17-24,.pure-u-md-18-24,.pure-u-md-19-24,.pure-u-md-20-24,.pure-u-md-21-24,.pure-u-md-22-24,.pure-u-md-23-24,.pure-u-md-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-md-1-24{width:4.1667%;*width:4.1357%}.pure-u-md-1-12,.pure-u-md-2-24{width:8.3333%;*width:8.3023%}.pure-u-md-1-8,.pure-u-md-3-24{width:12.5%;*width:12.469%}.pure-u-md-1-6,.pure-u-md-4-24{width:16.6667%;*width:16.6357%}.pure-u-md-1-5{width:20%;*width:19.969%}.pure-u-md-5-24{width:20.8333%;*width:20.8023%}.pure-u-md-1-4,.pure-u-md-6-24{width:25%;*width:24.969%}.pure-u-md-7-24{width:29.1667%;*width:29.1357%}.pure-u-md-1-3,.pure-u-md-8-24{width:33.3333%;*width:33.3023%}.pure-u-md-3-8,.pure-u-md-9-24{width:37.5%;*width:37.469%}.pure-u-md-2-5{width:40%;*width:39.969%}.pure-u-md-5-12,.pure-u-md-10-24{width:41.6667%;*width:41.6357%}.pure-u-md-11-24{width:45.8333%;*width:45.8023%}.pure-u-md-1-2,.pure-u-md-12-24{width:50%;*width:49.969%}.pure-u-md-13-24{width:54.1667%;*width:54.1357%}.pure-u-md-7-12,.pure-u-md-14-24{width:58.3333%;*width:58.3023%}.pure-u-md-3-5{width:60%;*width:59.969%}.pure-u-md-5-8,.pure-u-md-15-24{width:62.5%;*width:62.469%}.pure-u-md-2-3,.pure-u-md-16-24{width:66.6667%;*width:66.6357%}.pure-u-md-17-24{width:70.8333%;*width:70.8023%}.pure-u-md-3-4,.pure-u-md-18-24{width:75%;*width:74.969%}.pure-u-md-19-24{width:79.1667%;*width:79.1357%}.pure-u-md-4-5{width:80%;*width:79.969%}.pure-u-md-5-6,.pure-u-md-20-24{width:83.3333%;*width:83.3023%}.pure-u-md-7-8,.pure-u-md-21-24{width:87.5%;*width:87.469%}.pure-u-md-11-12,.pure-u-md-22-24{width:91.6667%;*width:91.6357%}.pure-u-md-23-24{width:95.8333%;*width:95.8023%}.pure-u-md-1,.pure-u-md-1-1,.pure-u-md-5-5,.pure-u-md-24-24{width:100%}}@media screen and (min-width:64em){.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-1-2,.pure-u-lg-1-3,.pure-u-lg-2-3,.pure-u-lg-1-4,.pure-u-lg-3-4,.pure-u-lg-1-5,.pure-u-lg-2-5,.pure-u-lg-3-5,.pure-u-lg-4-5,.pure-u-lg-5-5,.pure-u-lg-1-6,.pure-u-lg-5-6,.pure-u-lg-1-8,.pure-u-lg-3-8,.pure-u-lg-5-8,.pure-u-lg-7-8,.pure-u-lg-1-12,.pure-u-lg-5-12,.pure-u-lg-7-12,.pure-u-lg-11-12,.pure-u-lg-1-24,.pure-u-lg-2-24,.pure-u-lg-3-24,.pure-u-lg-4-24,.pure-u-lg-5-24,.pure-u-lg-6-24,.pure-u-lg-7-24,.pure-u-lg-8-24,.pure-u-lg-9-24,.pure-u-lg-10-24,.pure-u-lg-11-24,.pure-u-lg-12-24,.pure-u-lg-13-24,.pure-u-lg-14-24,.pure-u-lg-15-24,.pure-u-lg-16-24,.pure-u-lg-17-24,.pure-u-lg-18-24,.pure-u-lg-19-24,.pure-u-lg-20-24,.pure-u-lg-21-24,.pure-u-lg-22-24,.pure-u-lg-23-24,.pure-u-lg-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-lg-1-24{width:4.1667%;*width:4.1357%}.pure-u-lg-1-12,.pure-u-lg-2-24{width:8.3333%;*width:8.3023%}.pure-u-lg-1-8,.pure-u-lg-3-24{width:12.5%;*width:12.469%}.pure-u-lg-1-6,.pure-u-lg-4-24{width:16.6667%;*width:16.6357%}.pure-u-lg-1-5{width:20%;*width:19.969%}.pure-u-lg-5-24{width:20.8333%;*width:20.8023%}.pure-u-lg-1-4,.pure-u-lg-6-24{width:25%;*width:24.969%}.pure-u-lg-7-24{width:29.1667%;*width:29.1357%}.pure-u-lg-1-3,.pure-u-lg-8-24{width:33.3333%;*width:33.3023%}.pure-u-lg-3-8,.pure-u-lg-9-24{width:37.5%;*width:37.469%}.pure-u-lg-2-5{width:40%;*width:39.969%}.pure-u-lg-5-12,.pure-u-lg-10-24{width:41.6667%;*width:41.6357%}.pure-u-lg-11-24{width:45.8333%;*width:45.8023%}.pure-u-lg-1-2,.pure-u-lg-12-24{width:50%;*width:49.969%}.pure-u-lg-13-24{width:54.1667%;*width:54.1357%}.pure-u-lg-7-12,.pure-u-lg-14-24{width:58.3333%;*width:58.3023%}.pure-u-lg-3-5{width:60%;*width:59.969%}.pure-u-lg-5-8,.pure-u-lg-15-24{width:62.5%;*width:62.469%}.pure-u-lg-2-3,.pure-u-lg-16-24{width:66.6667%;*width:66.6357%}.pure-u-lg-17-24{width:70.8333%;*width:70.8023%}.pure-u-lg-3-4,.pure-u-lg-18-24{width:75%;*width:74.969%}.pure-u-lg-19-24{width:79.1667%;*width:79.1357%}.pure-u-lg-4-5{width:80%;*width:79.969%}.pure-u-lg-5-6,.pure-u-lg-20-24{width:83.3333%;*width:83.3023%}.pure-u-lg-7-8,.pure-u-lg-21-24{width:87.5%;*width:87.469%}.pure-u-lg-11-12,.pure-u-lg-22-24{width:91.6667%;*width:91.6357%}.pure-u-lg-23-24{width:95.8333%;*width:95.8023%}.pure-u-lg-1,.pure-u-lg-1-1,.pure-u-lg-5-5,.pure-u-lg-24-24{width:100%}}@media screen and (min-width:80em){.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-1-2,.pure-u-xl-1-3,.pure-u-xl-2-3,.pure-u-xl-1-4,.pure-u-xl-3-4,.pure-u-xl-1-5,.pure-u-xl-2-5,.pure-u-xl-3-5,.pure-u-xl-4-5,.pure-u-xl-5-5,.pure-u-xl-1-6,.pure-u-xl-5-6,.pure-u-xl-1-8,.pure-u-xl-3-8,.pure-u-xl-5-8,.pure-u-xl-7-8,.pure-u-xl-1-12,.pure-u-xl-5-12,.pure-u-xl-7-12,.pure-u-xl-11-12,.pure-u-xl-1-24,.pure-u-xl-2-24,.pure-u-xl-3-24,.pure-u-xl-4-24,.pure-u-xl-5-24,.pure-u-xl-6-24,.pure-u-xl-7-24,.pure-u-xl-8-24,.pure-u-xl-9-24,.pure-u-xl-10-24,.pure-u-xl-11-24,.pure-u-xl-12-24,.pure-u-xl-13-24,.pure-u-xl-14-24,.pure-u-xl-15-24,.pure-u-xl-16-24,.pure-u-xl-17-24,.pure-u-xl-18-24,.pure-u-xl-19-24,.pure-u-xl-20-24,.pure-u-xl-21-24,.pure-u-xl-22-24,.pure-u-xl-23-24,.pure-u-xl-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-xl-1-24{width:4.1667%;*width:4.1357%}.pure-u-xl-1-12,.pure-u-xl-2-24{width:8.3333%;*width:8.3023%}.pure-u-xl-1-8,.pure-u-xl-3-24{width:12.5%;*width:12.469%}.pure-u-xl-1-6,.pure-u-xl-4-24{width:16.6667%;*width:16.6357%}.pure-u-xl-1-5{width:20%;*width:19.969%}.pure-u-xl-5-24{width:20.8333%;*width:20.8023%}.pure-u-xl-1-4,.pure-u-xl-6-24{width:25%;*width:24.969%}.pure-u-xl-7-24{width:29.1667%;*width:29.1357%}.pure-u-xl-1-3,.pure-u-xl-8-24{width:33.3333%;*width:33.3023%}.pure-u-xl-3-8,.pure-u-xl-9-24{width:37.5%;*width:37.469%}.pure-u-xl-2-5{width:40%;*width:39.969%}.pure-u-xl-5-12,.pure-u-xl-10-24{width:41.6667%;*width:41.6357%}.pure-u-xl-11-24{width:45.8333%;*width:45.8023%}.pure-u-xl-1-2,.pure-u-xl-12-24{width:50%;*width:49.969%}.pure-u-xl-13-24{width:54.1667%;*width:54.1357%}.pure-u-xl-7-12,.pure-u-xl-14-24{width:58.3333%;*width:58.3023%}.pure-u-xl-3-5{width:60%;*width:59.969%}.pure-u-xl-5-8,.pure-u-xl-15-24{width:62.5%;*width:62.469%}.pure-u-xl-2-3,.pure-u-xl-16-24{width:66.6667%;*width:66.6357%}.pure-u-xl-17-24{width:70.8333%;*width:70.8023%}.pure-u-xl-3-4,.pure-u-xl-18-24{width:75%;*width:74.969%}.pure-u-xl-19-24{width:79.1667%;*width:79.1357%}.pure-u-xl-4-5{width:80%;*width:79.969%}.pure-u-xl-5-6,.pure-u-xl-20-24{width:83.3333%;*width:83.3023%}.pure-u-xl-7-8,.pure-u-xl-21-24{width:87.5%;*width:87.469%}.pure-u-xl-11-12,.pure-u-xl-22-24{width:91.6667%;*width:91.6357%}.pure-u-xl-23-24{width:95.8333%;*width:95.8023%}.pure-u-xl-1,.pure-u-xl-1-1,.pure-u-xl-5-5,.pure-u-xl-24-24{width:100%}}
--------------------------------------------------------------------------------
/public/stylesheets/mean-blog.min.css:
--------------------------------------------------------------------------------
1 | body{color:#777}.pure-img-responsive{max-width:100%;height:auto}#layout,#menu,.menu-link{-webkit-transition:all .2s ease-out;-moz-transition:all .2s ease-out;-ms-transition:all .2s ease-out;-o-transition:all .2s ease-out;transition:all .2s ease-out}#layout{position:relative;padding-left:0}#layout.active #menu{left:150px;width:150px}#layout.active .menu-link{left:150px}#menu{margin-left:-150px;width:150px;position:fixed;top:0;left:0;bottom:0;z-index:1000;background:#252a3a;overflow-y:auto;-webkit-overflow-scrolling:touch}#menu a{color:#ccc;border:0;padding:.6em 0 .6em .6em}#menu .pure-menu,#menu .pure-menu ul{border:0;background:transparent}#menu .pure-menu ul,#menu .pure-menu .menu-item-divided{border-top:1px solid #333}#menu .pure-menu li a:hover,#menu .pure-menu li a:focus{background:rgba(0,0,0,.3)}#menu .pure-menu-selected{background:rgba(0,0,0,.5)}#menu .pure-menu-selected a{color:#fff}#menu .pure-menu-heading{margin:0;border-bottom:0;font-size:110%;color:#4b7197}.menu-link{position:fixed;display:block;top:0;left:0;background:#000;background:rgba(0,0,0,.7);font-size:10px;z-index:10;width:2em;height:auto;padding:2.1em 1.6em;box-sizing:content-box}.menu-link:hover,.menu-link:focus{background:#000}.menu-link span{position:relative;display:block}.menu-link span,.menu-link span:before,.menu-link span:after{background-color:#fff;width:100%;height:.2em}.menu-link span:before,.menu-link span:after{position:absolute;margin-top:-.6em;content:" "}.menu-link span:after{margin-top:.6em}@media (min-width:48em){#layout{padding-left:150px;left:0}#menu{left:150px}.menu-link{position:fixed;left:150px;display:none}#layout.active .menu-link{left:150px}}@media (max-width:48em){#layout.active{position:relative}}body{font:14px 'Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.4em;background:#eaeaea url(../images/bg.png);color:#4d4d4d;margin:0 auto;-webkit-font-smoothing:antialiased;-moz-font-smoothing:antialiased;-ms-font-smoothing:antialiased;-o-font-smoothing:antialiased;font-smoothing:antialiased}ol,ul{list-style:none}blockquote,q{quotes:none}:focus{outline:0}table{border-collapse:collapse;border-spacing:0}*,:after,:before{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.clearfix{*zoom:1}.clearfix:after{content:".";display:block;height:0;visibility:hidden;font-size:0;clear:both}a{color:#00B7FF;text-decoration:none}.bgcolor-1{background:#f0efee}.bgcolor-2{background:#f9f9f9}.bgcolor-3{background:#e8e8e8}.bgcolor-4{background:#2f3238;color:#fff}.bgcolor-5{background:#df6659;color:#521e18}.bgcolor-6{background:#2fa8ec}.bgcolor-7{background:#d0d6d6}.bgcolor-8{background:#3d4444;color:#fff}.bgcolor-9{background:#ef3f52;color:#fff}.bgcolor-10{background:#64448f;color:#fff}.bgcolor-11{background:#3755ad;color:#fff}.bgcolor-12{background:#3498DB;color:#fff}.error{color:red}.success{color:green}#content{position:relative;overflow:auto}.container{width:100%;max-width:1100px;margin:0 auto;margin-top:2em}.waterfall{-moz-column-count:3;-webkit-column-count:3;column-count:3;-moz-column-gap:1em;-webkit-column-gap:1em;column-gap:1em}.waterfall .pin{background:#fefefe;border:2px solid #fcfcfc;-moz-box-shadow:0 1px 2px rgba(34,25,25,.4);-webkit-box-shadow:0 1px 2px rgba(34,25,25,.4);box-shadow:0 1px 2px rgba(34,25,25,.4);margin:0 .125em 1em;padding:1em;width:100%;transition:opacity .4s ease-in-out;display:inline-block;-moz-page-break-inside:avoid;-webkit-column-break-inside:avoid;break-inside:avoid;cursor:pointer}.waterfall .pin img.pinimg{width:100%;height:auto;border-bottom:1px solid #ccc;padding-bottom:1em;margin-bottom:.5em}.waterfall .pin figcaption a{font-size:.9rem;color:#444;line-height:1.5}.waterfall small{font-size:1rem;float:right;text-transform:uppercase;color:#aaa}.waterfall small a{color:#666;text-decoration:none;transition:.4s color}.waterfall:hover .pin:not(:hover){opacity:.4}.waterfall:hover .pin:hover{border-left:6px solid #1b98f8}.addBlog{display:block;font-weight:700;border:2px dashed silver;text-align:center;font-size:5em;line-height:1.5em;width:100%;position:relative;background-color:transparent;text-decoration:none;color:silver}.page{background:#fefefe;-moz-box-shadow:0 1px 2px rgba(34,25,25,.4);-webkit-box-shadow:0 1px 2px rgba(34,25,25,.4);box-shadow:0 1px 2px rgba(34,25,25,.4);width:100%;-moz-page-break-inside:avoid;-webkit-column-break-inside:avoid;break-inside:avoid}.page .page-header{background-color:silver;color:#fff;height:3em}.page .page-content{padding:1em}.button-xsmall{font-size:70%}.button-small{font-size:85%}.button-large{font-size:110%}.button-xlarge{font-size:125%}.button-success,.button-error,.button-warning,.button-secondary{color:#fff;border-radius:4px;text-shadow:0 1px 1px rgba(0,0,0,.2)}.button-success{background:#1cb841}.button-error{background:#ca3c3c}.button-warning{background:#df7514}.button-secondary{background:#42b8dd}.label-light-red,.label-pink,.label-yellow,.label-blue,.label-green,.label-dark-green,.label-dark-blue,.label-light-gray,.label-purple{width:15px;height:15px;display:inline-block;margin-right:.5em;border-radius:3px}.label-pink{background:#F1A4B8}.label-yellow{background:#ffc94c}.label-blue{background:#41ccb4}.label-green{background:#40c365}.label-dark-green{background-color:#5aba59}.label-dark-blue{background-color:#4d85d1}.label-light-red{background-color:#df2d4f}.label-light-gray{background-color:#999}.label-purple{background-color:#8156a7}.label-count{color:#4b7197}#menu .nav-item{display:inline-block}#menu .nav-item a{background:transparent;border:2px solid #b0cadb;color:#fff;margin-top:1em;letter-spacing:.05em;text-transform:uppercase;font-size:85%;padding:.5em 1em}.waterfall .pin.blog-item{padding:.9em 1em;border-bottom:1px solid #ddd;border-left:6px solid transparent}.blog-avatar{border-radius:3px;margin-right:.5em}.blog-author,.blog-subject{margin:0}.blog-author{text-transform:uppercase;color:#999}.blog-desc{font-size:80%;margin:.4em 0}.text-left{text-align:left}.text-right{text-align:right}.blog-content-header,.blog-content-body,.blog-content-footer{padding:1em 2em}.blog-content-header{border-bottom:1px solid #ddd}.blog-content-title{margin:.5em 0 0;line-height:38px}.blog-content-subtitle{font-size:1em;margin:0;font-weight:400}.blog-content-subtitle span{color:#999}.blog-content-controls{margin-top:2.5em;text-align:right}.blog-content-controls .secondary-button{margin-bottom:.3em}.blog-avatar{width:40px;height:40px}a.blog-author{color:#1b98f8}.blog{overflow:auto}.blog .blog-summary{line-height:4em;font-weight:600;color:gray;background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-moz-linear-gradient(top,rgba(0,0,0,.05) 0,rgba(0,0,0,.1));background-image:-o-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));border-radius:.3em;padding:1em}.primary-button,.secondary-button{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;border-radius:20px}.primary-button{color:#fff;background:#1b98f8;margin:1em 0}.secondary-button{background:#fff;border:1px solid #ddd;color:#666;padding:.5em 2em;font-size:80%}#info{margin:5em auto 0;color:#a6a6a6;font-size:.9em;text-shadow:0 1px 0 rgba(255,255,255,.7);text-align:center}#info a{color:inherit}img.hover(:hover){box-shadow:0 1px 5px rgba(34,25,25,.8);-moz-box-shadow:0 1px 5px rgba(34,25,25,.8);-webkit-box-shadow:0 1px 5px rgba(34,25,25,.8);filter:progid:DXImageTransform.Microsoft.Shadow(color=#979797, direction=135, strength=3)}.animate-pop-up.ng-enter,.animate-pop-up-enter{-webkit-animation:pop-up-enter .8s ease-in forwards;animation:pop-up-enter .8s ease-in forwards}.animate-pop-up.ng-leave,.animate-pop-up-leave{-webkit-animation:pop-up-leave .8s ease-in forwards;animation:pop-up-leave .8s ease-in forwards}@-webkit-keyframes pop-up-enter{0%{-webkit-transform:scale(0.4);transform:scale(0.4);opacity:0}70%{-webkit-transform:scale(1.1);opacity:.8;-webkit-animation-timing-function:ease-out}100%{-webkit-transform:scale(1);opacity:1}}@keyframes pop-up-enter{0%{-webkit-transform:scale(0.4);transform:scale(0.4);opacity:0}70%{-webkit-transform:scale(1.1);transform:scale(1.1);opacity:.8;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@-webkit-keyframes pop-up-leave{0%{-webkit-transform:scale(1);opacity:1}70%{-webkit-transform:scale(1.1);opacity:.8;-webkit-animation-timing-function:ease-out}100%{-webkit-transform:scale(0.4);transform:scale(0.4);opacity:0}}@keyframes pop-up-leave{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}70%{-webkit-transform:scale(1.1);transform:scale(1.1);opacity:.8;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}100%{-webkit-transform:scale(0.4);transform:scale(0.4);opacity:0}}.slide-in{width:100%;height:100%}.slide-in.ng-enter,.slide-in.ng-leave{-webkit-transition:all 1s ease;transition:all 1s ease}.slide-in.ng-enter{transform:translateX(100%)}.slide-in.ng-enter-active{transform:none}.slide-in.ng-leave{transform:none}.slide-in.ng-leave-active{transform:translateX(-100%)}.slide-down{width:100%;height:100%}.slide-down.ng-enter,.slide-down.ng-leave{-webkit-transition:all 1s ease;transition:all 1s ease}.slide-down.ng-enter{transform:translateY(-100%)}.slide-down.ng-enter.ng-enter-active{transform:none}.slide-down.ng-leave{transform:none}.slide-down.ng-leave.ng-leave-active{transform:translateY(100%)}.slide-right{-webkit-transition:all 0 cubic-bezier(0.25,.46,.45,.94);-moz-transition:all 0 cubic-bezier(0.25,.46,.45,.94);-ms-transition:all 0 cubic-bezier(0.25,.46,.45,.94);-o-transition:all 0 cubic-bezier(0.25,.46,.45,.94);transition:all 0 cubic-bezier(0.25,.46,.45,.94);-webkit-transition-timing-function:cubic-bezier(0.25,.46,.45,.94);-moz-transition-timing-function:cubic-bezier(0.25,.46,.45,.94);-ms-transition-timing-function:cubic-bezier(0.25,.46,.45,.94);-o-transition-timing-function:cubic-bezier(0.25,.46,.45,.94);transition-timing-function:cubic-bezier(0.25,.46,.45,.94)}.slide-right.ng-enter{transform:translateX(60px);-ms-transform:translateX(60px);-webkit-transform:translateX(60px);transition-duration:250ms;-webkit-transition-duration:250ms;opacity:0}.slide-right.ng-enter-active{transform:translateX(0);-ms-transform:translateX(0);-webkit-transform:translateX(0);opacity:1}.slide-right.ng-leave{transform:translateX(0);-ms-transform:translateX(0);-webkit-transform:translateX(0);transition-duration:250ms;-webkit-transition-duration:250ms;opacity:1}.slide-right.ng-leave-active{transform:translateX(60px);-ms-transform:translateX(60px);-webkit-transform:translateX(60px);opacity:0}.slide-right.ng-hide-add{transform:translateX(0);-ms-transform:translateX(0);-webkit-transform:translateX(0);transition-duration:250ms;-webkit-transition-duration:250ms;opacity:1}.slide-right.ng-hide-add.ng-hide-add-active{transform:translateX(60px);-ms-transform:translateX(60px);-webkit-transform:translateX(60px);opacity:0}.slide-right.ng-hide-remove{transform:translateX(60px);-ms-transform:translateX(60px);-webkit-transform:translateX(60px);transition-duration:250ms;-webkit-transition-duration:250ms;display:block!important;opacity:0}.slide-right.ng-hide-remove.ng-hide-remove-active{transform:translateX(0);-ms-transform:translateX(0);-webkit-transform:translateX(0);opacity:1}.fade{opacity:1}.fade.ng-enter,.fade.ng-leave{-webkit-transition:all 1.2s ease;transition:all 1.2s ease}.fade.ng-enter{opacity:0}.fade.ng-enter.ng-enter-active{opacity:1}.fade.ng-leave{opacity:1}.fade.ng-leave.ng-leave-active{opacity:0}.fade.ng-hide-add,.fade.ng-hide-remove{-webkit-transition:all 1.2s ease;transition:all 1.2s ease}.fade.ng-hide-add{opacity:1}.fade.ng-hide-add.ng-hide-add-active{opacity:0}.fade.ng-hide-remove{opacity:0}.fade.ng-hide-remove.ng-hide-remove-active{opacity:1}.flip-in.ng-hide-add{transform:perspective(300px) rotateX(0deg);-ms-transform:perspective(300px) rotateX(0deg);-webkit-transform:perspective(300px) rotateX(0deg);transition-duration:1.2s;-webkit-transition-duration:1.2s;opacity:1}.flip-in.ng-hide-add.ng-hide-add-active{transform:perspective(300px) rotateX(135deg);-ms-transform:perspective(300px) rotateX(135deg);-webkit-transform:perspective(300px) rotateX(135deg);opacity:0}.flip-in.ng-hide-remove{transform:perspective(300px) rotateX(90deg);-ms-transform:perspective(300px) rotateX(90deg);-webkit-transform:perspective(300px) rotateX(90deg);transition-duration:1.2s;-webkit-transition-duration:1.2s;display:block!important;opacity:0}.flip-in.ng-hide-remove.ng-hide-remove-active{transform:perspective(300px) rotateX(0deg);-ms-transform:perspective(300px) rotateX(0deg);-webkit-transform:perspective(300px) rotateX(0deg);opacity:1}.rotate-in.ng-hide-add{transform:perspective(300px) rotateY(0deg);-ms-transform:perspective(300px) rotateY(0deg);-webkit-transform:perspective(300px) rotateY(0deg);transition-duration:1.2s;-webkit-transition-duration:1.2s;opacity:1}.rotate-in.ng-hide-add.ng-hide-add-active{transform:perspective(300px) rotateY(-40deg);-ms-transform:perspective(300px) rotateY(-40deg);-webkit-transform:perspective(300px) rotateY(-40deg);opacity:0}.rotate-in.ng-hide-remove{transform:perspective(300px) rotateY(40deg);-ms-transform:perspective(300px) rotateY(40deg);-webkit-transform:perspective(300px) rotateY(40deg);transition-duration:1.2s;-webkit-transition-duration:1.2s;display:block!important;opacity:0}.rotate-in.ng-hide-remove.ng-hide-remove-active{transform:perspective(300px) rotateY(0deg);-ms-transform:perspective(300px) rotateY(0deg);-webkit-transform:perspective(300px) rotateY(0deg);opacity:1}nav.slideshow a{position:fixed;z-index:500;color:#59656c;text-align:center;cursor:pointer;font-size:1.8em;padding:.3em}nav.slideshow a.nav-prev,nav.slideshow a.nav-next{}nav.slideshow a.nav-prev{top:20px;right:0}nav.slideshow a.nav-next{bottom:20px;right:0}nav.slideshow a.nav-close{top:0;right:0}nav.slideshow a.nav-edit{top:0;right:50px}nav.slideshow a.nav-login{top:0;left:0}.overlay{position:fixed;left:0;right:0;bottom:0;top:0;background:rgba(0,0,0,.8);z-index:1001;color:#fff}.overlay article{position:absolute;left:50%;top:50%;margin:-165px 0 0 -250px;max-width:500px;width:100%;height:330px}.overlay nav.slideshow a{position:absolute;color:#fff}.overlay input{color:#4d4d4d}.overlay input[type=submit]{color:#fff}.overlay.blog-preview-dialog{background:#eaeaea url(../images/bg.png);z-index:500;margin-left:150px;overflow:auto}.overlay.blog-preview-dialog nav.slideshow a{color:#000}.overlay section.container{color:#4d4d4d}.fileInputContainer{display:inline-block;font-weight:700;text-align:center;height:25px;width:25px;line-height:25px;position:relative;background-color:transparent;font-size:18px;color:#000}.fileInput{height:25px;width:25px;overflow:hidden;position:absolute;right:0;top:0;opacity:0;filter:alpha(opacity=0);cursor:pointer}.menu-icon{display:inline-block;font-weight:700;font-size:18px;color:#000;cursor:pointer;margin-left:18px;margin-right:18px}article.about-content,article.contact-content{line-height:2em;font-size:16px}.img-box{position:relative}.img-box nav .nav-item{position:absolute;color:#000;display:inline-block}.img-box nav .nav-item.nav-cancel{right:0;top:0;font-size:18px}.img-box nav .nav-item.nav-ok{left:50%;top:50%;font-size:40px;margin-left:-28px;margin-top:-20px;color:#fff}@media screen and (max-width:1250px){.container.blogCreatePage{margin-top:0}}@media screen and (max-width:60em){.waterfall{column-gap:0;-moz-column-count:2;-webkit-column-count:2;column-count:2}.slideshow figure{top:0;left:0;margin:0}.slideshow figure img{width:auto;margin:0 auto;max-width:100%}.slideshow nav span,.slideshow nav span.nav-close{font-size:1.8em;padding:.3em}}@media screen and (max-width:40em){.waterfall{column-gap:0;-moz-column-count:1;-webkit-column-count:1;column-count:1}}@media screen and (max-width:30em){.waterfall{column-gap:0;-moz-column-count:1;-webkit-column-count:1;column-count:1}.overlay article{left:0;top:0;margin:3em auto;height:auto}.pure-form .pure-input-2-3{width:100%}}@media (max-width:48em){.overlay.blog-preview-dialog{margin-left:auto}}
--------------------------------------------------------------------------------
/public/stylesheets/normalize.css:
--------------------------------------------------------------------------------
1 | article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block;}audio,canvas,video{display:inline-block;}audio:not([controls]){display:none;height:0;}[hidden]{display:none;}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;}body{margin:0;}a:focus{outline:thin dotted;}a:active,a:hover{outline:0;}h1{font-size:2em;margin:0.67em 0;}abbr[title]{border-bottom:1px dotted;}b,strong{font-weight:bold;}dfn{font-style:italic;}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0;}mark{background:#ff0;color:#000;}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em;}pre{white-space:pre-wrap;}q{quotes:"\201C" "\201D" "\2018" "\2019";}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sup{top:-0.5em;}sub{bottom:-0.25em;}img{border:0;}svg:not(:root){overflow:hidden;}figure{margin:0;}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em;}legend{border:0;padding:0;}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0;}button,input{line-height:normal;}button,select{text-transform:none;}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer;}button[disabled],html input[disabled]{cursor:default;}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0;}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}textarea{overflow:auto;vertical-align:top;}table{border-collapse:collapse;border-spacing:0;}
--------------------------------------------------------------------------------
/public/stylesheets/nprogress.css:
--------------------------------------------------------------------------------
1 | /* Make clicks pass-through */
2 | #nprogress {
3 | pointer-events: none;
4 | }
5 |
6 | #nprogress .bar {
7 | background: #29d;
8 |
9 | position: fixed;
10 | z-index: 1031;
11 | top: 0;
12 | left: 0;
13 |
14 | width: 100%;
15 | height: 2px;
16 | }
17 |
18 | /* Fancy blur effect */
19 | #nprogress .peg {
20 | display: block;
21 | position: absolute;
22 | right: 0px;
23 | width: 100px;
24 | height: 100%;
25 | box-shadow: 0 0 10px #29d, 0 0 5px #29d;
26 | opacity: 1.0;
27 |
28 | -webkit-transform: rotate(3deg) translate(0px, -4px);
29 | -ms-transform: rotate(3deg) translate(0px, -4px);
30 | transform: rotate(3deg) translate(0px, -4px);
31 | }
32 |
33 | /* Remove these to get rid of the spinner */
34 | #nprogress .spinner {
35 | display: block;
36 | position: fixed;
37 | z-index: 1031;
38 | top: 15px;
39 | right: 15px;
40 | }
41 |
42 | #nprogress .spinner-icon {
43 | width: 18px;
44 | height: 18px;
45 | box-sizing: border-box;
46 |
47 | border: solid 2px transparent;
48 | border-top-color: #29d;
49 | border-left-color: #29d;
50 | border-radius: 50%;
51 |
52 | -webkit-animation: nprogress-spinner 400ms linear infinite;
53 | animation: nprogress-spinner 400ms linear infinite;
54 | }
55 |
56 | .nprogress-custom-parent {
57 | overflow: hidden;
58 | position: relative;
59 | }
60 |
61 | .nprogress-custom-parent #nprogress .spinner,
62 | .nprogress-custom-parent #nprogress .bar {
63 | position: absolute;
64 | }
65 |
66 | @-webkit-keyframes nprogress-spinner {
67 | 0% { -webkit-transform: rotate(0deg); }
68 | 100% { -webkit-transform: rotate(360deg); }
69 | }
70 | @keyframes nprogress-spinner {
71 | 0% { transform: rotate(0deg); }
72 | 100% { transform: rotate(360deg); }
73 | }
74 |
75 |
--------------------------------------------------------------------------------
/public/stylesheets/side-menu.css:
--------------------------------------------------------------------------------
1 | body {
2 | color: #777;
3 | }
4 |
5 | .pure-img-responsive {
6 | max-width: 100%;
7 | height: auto;
8 | }
9 |
10 |
11 | /*
12 | Add transition to containers so they can push in and out.
13 | */
14 |
15 | #layout,
16 | #menu,
17 | .menu-link {
18 | -webkit-transition: all 0.2s ease-out;
19 | -moz-transition: all 0.2s ease-out;
20 | -ms-transition: all 0.2s ease-out;
21 | -o-transition: all 0.2s ease-out;
22 | transition: all 0.2s ease-out;
23 | }
24 |
25 |
26 | /*
27 | This is the parent `` that contains the menu and the content area.
28 | */
29 |
30 | #layout {
31 | position: relative;
32 | padding-left: 0;
33 | }
34 |
35 | #layout.active #menu {
36 | left: 150px;
37 | width: 150px;
38 | }
39 |
40 | #layout.active .menu-link {
41 | left: 150px;
42 | }
43 |
44 |
45 | /*
46 | The content `
` is where all your content goes.
47 | */
48 |
49 |
50 | /*
51 | The `#menu` `
` is the parent `
` that contains the `.pure-menu` that
52 | appears on the left side of the page.
53 | */
54 |
55 | #menu {
56 | margin-left: -150px;
57 | /* "#menu" width */
58 | width: 150px;
59 | position: fixed;
60 | top: 0;
61 | left: 0;
62 | bottom: 0;
63 | z-index: 1000;
64 | /* so the menu or its navicon stays above all content */
65 | background: rgb(37, 42, 58);
66 | /* background: rgb(61, 79, 93); */
67 | /* background: rgb(176, 202, 219); */
68 | overflow-y: auto;
69 | -webkit-overflow-scrolling: touch;
70 | }
71 |
72 |
73 | /*
74 | All anchors inside the menu should be styled like this.
75 | */
76 |
77 | #menu a {
78 | color: #ccc;
79 | border: none;
80 | padding: 0.6em 0 0.6em 0.6em;
81 | }
82 |
83 |
84 | /*
85 | Remove all background/borders, since we are applying them to #menu.
86 | */
87 |
88 | #menu .pure-menu,
89 | #menu .pure-menu ul {
90 | border: none;
91 | background: transparent;
92 | }
93 |
94 |
95 | /*
96 | Add that light border to separate items into groups.
97 | */
98 |
99 | #menu .pure-menu ul,
100 | #menu .pure-menu .menu-item-divided {
101 | border-top: 1px solid #333;
102 | }
103 |
104 |
105 | /*
106 | Change color of the anchor links on hover/focus.
107 | */
108 |
109 | #menu .pure-menu li a:hover,
110 | #menu .pure-menu li a:focus {
111 | background: rgba(0, 0, 0, 0.3);
112 | }
113 |
114 |
115 | /*
116 | This styles the selected menu item `
`.
117 | */
118 |
119 | #menu .pure-menu-selected {
120 | background: rgba(0, 0, 0, 0.5);
121 | }
122 |
123 |
124 | /*
125 | This styles a link within a selected menu item ``.
126 | */
127 |
128 | #menu .pure-menu-selected a {
129 | color: #fff;
130 | }
131 |
132 |
133 | /*
134 | This styles the menu heading.
135 | */
136 |
137 | #menu .pure-menu-heading {
138 | margin: 0;
139 | border-bottom: none;
140 | font-size: 110%;
141 | color: rgb(75, 113, 151);
142 | }
143 |
144 |
145 | /* -- Dynamic Button For Responsive Menu -------------------------------------*/
146 |
147 |
148 | /*
149 | The button to open/close the Menu is custom-made and not part of Pure. Here's
150 | how it works:
151 | */
152 |
153 |
154 | /*
155 | `.menu-link` represents the responsive menu toggle that shows/hides on
156 | small screens.
157 | */
158 |
159 | .menu-link {
160 | position: fixed;
161 | display: block;
162 | /* show this only on small screens */
163 | top: 0;
164 | left: 0;
165 | /* "#menu width" */
166 | background: #000;
167 | background: rgba(0, 0, 0, 0.7);
168 | font-size: 10px;
169 | /* change this value to increase/decrease button size */
170 | z-index: 10;
171 | width: 2em;
172 | height: auto;
173 | padding: 2.1em 1.6em;
174 | box-sizing: content-box;
175 | }
176 |
177 | .menu-link:hover,
178 | .menu-link:focus {
179 | background: #000;
180 | }
181 |
182 | .menu-link span {
183 | position: relative;
184 | display: block;
185 | }
186 |
187 | .menu-link span,
188 | .menu-link span:before,
189 | .menu-link span:after {
190 | background-color: #fff;
191 | width: 100%;
192 | height: 0.2em;
193 | }
194 |
195 | .menu-link span:before,
196 | .menu-link span:after {
197 | position: absolute;
198 | margin-top: -0.6em;
199 | content: " ";
200 | }
201 |
202 | .menu-link span:after {
203 | margin-top: 0.6em;
204 | }
205 |
206 |
207 | /* -- Responsive Styles (Media Queries) ------------------------------------- */
208 |
209 |
210 | /*
211 | Hides the menu at `48em`, but modify this based on your app's needs.
212 | */
213 |
214 | @media (min-width: 48em) {
215 | #layout {
216 | padding-left: 150px;
217 | /* left col width "#menu" */
218 | left: 0;
219 | }
220 | #menu {
221 | left: 150px;
222 | }
223 | .menu-link {
224 | position: fixed;
225 | left: 150px;
226 | display: none;
227 | }
228 | #layout.active .menu-link {
229 | left: 150px;
230 | }
231 | }
232 |
233 | @media (max-width: 48em) {
234 | /* Only apply this when the window is small. Otherwise, the following
235 | case results in extra padding on the left:
236 | * Make the window small.
237 | * Tap the menu to trigger the active state.
238 | * Make the window large again.
239 | */
240 | #layout.active {
241 | position: relative;
242 | /* left: 150px; */
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/public/uploads/images/d8187d9836e494faec7be0433ce9d74b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caihuabin/mean-blog/ced946ce6adbcc609dc4ea47e2aaed584e37a48d/public/uploads/images/d8187d9836e494faec7be0433ce9d74b.png
--------------------------------------------------------------------------------
/routes/api.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | var userModel = require('../models/user').userModel;
5 | var postModel = require('../models/post').postModel;
6 | var util = require('util');
7 |
8 | var mongodbModels = {
9 | 'user': userModel,
10 | 'post': postModel
11 | };
12 |
13 | function checkUnique(model, field, data, uniqueId, callback) {
14 | if (!callback) {
15 | callback = uniqueId;
16 | uniqueId = null;
17 | }
18 | if (mongodbModels[model]) {
19 | mongodbModels[model].findOne(data, function(err, result) {
20 | if (err) {
21 | return callback(new Error('invalid data'));
22 | } else if (!result) {
23 | return callback(null, null);
24 | } else {
25 | if (result._id == uniqueId) {
26 | return callback(null, null);
27 | }
28 | return callback(null, result);
29 | }
30 | });
31 | } else {
32 | return callback(new Error('invalid model'));
33 | }
34 | }
35 | router.post('/checkUnique', function(req, res, next) {
36 | var model = /*'user' || */ req.body.model;
37 | var field = /*'username' || */ req.body.field;
38 | var uniqueId = req.body.value._id || null;
39 | var data = {};
40 | data[field] = req.body.value[field];
41 | checkUnique(model, field, data, uniqueId, function(err, user) {
42 | if (err) {
43 | next(err);
44 | } else if (!user) {
45 | res.json({
46 | isUnique: true,
47 | status: 'success'
48 | });
49 | } else {
50 | res.json({
51 | isUnique: false,
52 | status: 'success'
53 | });
54 | }
55 | });
56 |
57 | });
58 |
59 | module.exports = router;
60 |
--------------------------------------------------------------------------------
/routes/auth.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | var userModel = require('../models/user').userModel;
5 | var hash = require('../utility/hash').hash;
6 | var restrict = require('../utility/restrict');
7 | var validator = require('../utility/validator').validator;
8 | var util = require('util');
9 | var config = require('../config');
10 |
11 | function authenticate(username, password, fn) {
12 | if (!module.parent) console.log('authenticating %s:%s', username, password);
13 | userModel.findOne({ "username": username }, function(err, user) {
14 | if (err) {
15 | return fn(err);
16 | }
17 | if (!user) return fn(new Error('cannot find user'));
18 | hash(password, config.salt, function(err, hash) {
19 | if (err) return fn(err);
20 | if (hash == user.password) {
21 | return fn(null, user);
22 | } else {
23 | return fn(new Error('invalid password'));
24 | }
25 |
26 | });
27 | });
28 | }
29 |
30 | router.get('/restricted', restrict.isAuthenticated, function(req, res) {
31 | res.send('Wahoo! restricted area, click to logout');
32 | });
33 | router.get('/register', function(req, res, next) {
34 | res.render('auth/register');
35 | });
36 | router.post('/register', function(req, res, next) {
37 | var rules = {
38 | username: ['required'],
39 | password: ['required'],
40 | password_confirmation: ['required', 'confirmed:password'],
41 | email: ['required', 'email']
42 | };
43 | var data = {
44 | username: req.body.username,
45 | password: req.body.password,
46 | password_confirmation: req.body.password_confirmation,
47 | email: req.body.email
48 | };
49 | validator(rules, data, function(err) {
50 | if (err) {
51 | next(err);
52 | } else {
53 | hash(data.password, config.salt, function(err, hash) {
54 | if (err) {
55 | next(err);
56 | } else {
57 | var entity = new userModel({
58 | username: data.username,
59 | email: data.email,
60 | password: hash,
61 | role: 'admin',
62 | avatar: '/images/avatar.png'
63 | });
64 | entity.save(function(err) {
65 | if (err) {
66 | next(err);
67 | } else {
68 | res.json({
69 | status: 'success',
70 | data: null
71 | });
72 | }
73 |
74 | });
75 | }
76 | });
77 | }
78 | });
79 |
80 | });
81 | router.get('/login', function(req, res, next) {
82 | res.render('auth/login');
83 | });
84 |
85 | router.post('/login', function(req, res, next) {
86 | var rules = {
87 | username: ['required'],
88 | password: ['required']
89 | };
90 | var data = {
91 | username: req.body.username,
92 | password: req.body.password
93 | };
94 | validator(rules, data, function(err) {
95 | if (err) {
96 | next(err);
97 | } else {
98 | authenticate(req.body.username, req.body.password, function(err, user) {
99 | if (user) {
100 | req.session.regenerate(function() {
101 | req.session.user = {
102 | _id: user._id,
103 | username: user.username,
104 | email: user.email,
105 | role: user.role,
106 | avatar: user.avatar,
107 | createdTime: user.createdTime
108 | };
109 | req.session.success = 'Authenticated as ' + user.username + ' click to logout. ' + ' You may now access /restricted.';
110 | res.json({
111 | status: 'success',
112 | data: { user: req.session.user }
113 | });
114 | });
115 | } else {
116 | req.session.error = 'Authentication failed, please check your username and password.';
117 | err.message = req.session.error;
118 | next(err);
119 | }
120 | });
121 | }
122 | });
123 |
124 | });
125 | router.post('/check', function(req, res, next) {
126 | if (req.session.user) {
127 | res.json({
128 | isCheck: true,
129 | user: req.session.user
130 | });
131 | } else {
132 | res.json({
133 | isCheck: false,
134 | user: null
135 | });
136 | }
137 | });
138 | router.get('/logout', function(req, res) {
139 | req.session.destroy(function() {
140 | /*res.json({
141 | status: 'success',
142 | data: null
143 | });*/
144 | res.redirect('/');
145 | });
146 | });
147 |
148 | module.exports = router;
149 |
--------------------------------------------------------------------------------
/routes/blog.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | var restrict = require('../utility/restrict');
5 |
6 | router.get('/index', function(req, res, next) {
7 | res.render('blog/index', {
8 | title: ' - 全部文章'
9 | });
10 | });
11 |
12 | router.get('/show', function(req, res, next) {
13 | res.render('blog/show', {
14 | title: ' - 文章内容'
15 | });
16 | });
17 |
18 | router.get('/create', restrict.isAuthenticated, function(req, res, next) {
19 | res.render('blog/create', {
20 | title: ' - 新的文章'
21 | });
22 | });
23 |
24 | router.get('/edit', restrict.isAuthenticated, function(req, res, next) {
25 | res.render('blog/edit', {
26 | title: ' - 编辑文章'
27 | });
28 | });
29 |
30 | module.exports = router;
31 |
--------------------------------------------------------------------------------
/routes/comments.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | var commentModel = require('../models/comment').commentModel;
5 | var postModel = require('../models/post').postModel;
6 |
7 | var validator = require('../utility/validator').validator;
8 | var restrict = require('../utility/restrict');
9 | var tool = require('../utility/tool');
10 |
11 | router.post('/', restrict.isAuthenticated, restrict.isAuthorized, function(req, res, next) {
12 | var rules = {
13 | content: ['required'],
14 | post: ['required'],
15 | user: ['required']
16 | };
17 | var params = {
18 | content: req.body.content,
19 | post: req.body.post,
20 | user: req.session.user || req.body.user
21 | };
22 | params = tool.deObject(params);
23 | validator(rules, params, function(err) {
24 | if (err) {
25 | next(err);
26 | } else {
27 | commentModel.create(params, function(err, comment) {
28 | if (err) next(err);
29 | else {
30 | var commentParams = {
31 | _id: comment._id,
32 | content: comment.content,
33 | user: comment.user,
34 | voteCount: comment.voteCount,
35 | createdTime: comment.createdTime
36 | };
37 | postModel.findByIdAndUpdate(comment.post, { '$inc': { 'commentCount': 1 }, $pushAll: { commentList: [commentParams] } }, { new: true }, function(err, post) {
38 | if (err) {
39 | next(err);
40 | } else {
41 | res.json({
42 | status: 'success',
43 | data: commentParams
44 | });
45 | }
46 | });
47 | }
48 | });
49 | }
50 | });
51 | });
52 |
53 | router.put('/vote/:id', restrict.isAuthenticated, function(req, res, next) {
54 | var params = {
55 | voteCount: req.body.voteCount,
56 | voteList: req.body.voteList
57 | };
58 | params = tool.deObject(params);
59 | commentModel.findByIdAndUpdate(req.params.id, params, function(err) {
60 | if (err) {
61 | next(err);
62 | } else {
63 | res.json({
64 | status: 'success',
65 | data: null
66 | });
67 | }
68 | });
69 | });
70 |
71 | module.exports = router;
72 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | /* GET home page. */
5 | router.get('/', function(req, res, next) {
6 | res.render('index', { title: 'MEAN', clientUser: req.session.user });
7 | });
8 | router.get('/about', function(req, res, next) {
9 | res.render('about/index');
10 | });
11 | router.get('/contact', function(req, res, next) {
12 | res.render('about/contact');
13 | });
14 | module.exports = router;
15 |
--------------------------------------------------------------------------------
/routes/posts.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 | var path = require('path');
4 | var postProxy = require('../proxy/post');
5 | var userModel = require('../models/user').userModel;
6 | var postModel = require('../models/post').postModel;
7 | var eventproxy = require('eventproxy');
8 | var redisClient = require('../utility/redisClient');
9 | var tool = require('../utility/tool');
10 | var upload = tool.upload;
11 | var validator = require('../utility/validator').validator;
12 | var restrict = require('../utility/restrict');
13 |
14 | upload.configure({
15 | uploadDir: path.join(__dirname, '../public/uploads/images/'),
16 | uploadUrl: '/uploads/images/'
17 | });
18 |
19 | router.get('/', function(req, res, next) {
20 | var ep = new eventproxy(),
21 | filter,
22 | params = {
23 | skip: req.query.skip,
24 | limit: req.query.limit,
25 | sortName: req.query.sortName,
26 | sortOrder: req.query.sortOrder,
27 | searchText: req.query.searchText,
28 | filterType: req.query.filterType
29 | };
30 | if (req.query.filter) {
31 | filter = JSON.parse(req.query.filter);
32 | params.cateId = filter.cateId;
33 | params.userId = filter.userId;
34 | }
35 | params = tool.deObject(params);
36 | ep.all('posts', 'count', function(posts, count) {
37 | var post,
38 | result = [];
39 | posts.forEach(function(item) {
40 | post = {
41 | _id: item._id,
42 | title: item.title,
43 | alias: item.alias,
44 | summary: item.summary,
45 | imgList: item.imgList,
46 | labels: item.labels,
47 | url: item.url,
48 | user: item.user,
49 | category: item.category,
50 | viewCount: item.viewCount,
51 | voteCount: item.voteCount,
52 | commentCount: item.commentCount,
53 | createdTime: item.createdTime,
54 | updatedTime: item.updatedTime,
55 | };
56 | result.push(post);
57 | });
58 | res.json(result);
59 | });
60 |
61 | postProxy.getPosts(params, function(err, posts) {
62 | if (err) {
63 | next(err);
64 | } else {
65 | ep.emit('posts', posts);
66 | }
67 | });
68 |
69 | postProxy.getPostsCount(params, function(err, count) {
70 | if (err) {
71 | next(err);
72 | } else {
73 | ep.emit('count', count);
74 | }
75 | });
76 | });
77 |
78 |
79 | //保存文章
80 | router.post('/', restrict.isAuthenticated, restrict.isAuthorized, function(req, res, next) {
81 | var rules = {
82 | title: ['required'],
83 | alias: ['required'],
84 | content: ['required']
85 | };
86 | var params = {
87 | title: req.body.title,
88 | alias: req.body.alias,
89 | summary: req.body.summary,
90 | source: req.body.source,
91 | content: req.body.content,
92 | imgList: req.body.imgList,
93 | labels: req.body.labels,
94 | url: req.body.url,
95 | user: req.session.user || req.body.user,
96 | category: req.body.category,
97 | isDraft: req.body.isDraft == true
98 | };
99 | params = tool.deObject(params);
100 | validator(rules, params, function(err) {
101 | if (err) {
102 | next(err);
103 | } else {
104 | postModel.create(params, function(err, post) {
105 | if (err) next(err);
106 | else {
107 | var postParams = {
108 | _id: post._id,
109 | alias: post.alias,
110 | title: post.title,
111 | createdTime: post.createdTime
112 | };
113 | userModel.findByIdAndUpdate(params.user._id, { $pushAll: { postList: [postParams] } }, { new: true }, function(err, user) {
114 | if (err) {
115 | next(err);
116 | } else if (!user) {
117 | next(new Error('user can not be found'));
118 | } else {
119 | res.json({
120 | status: 'success',
121 | data: post
122 | });
123 | }
124 | });
125 | }
126 |
127 | });
128 |
129 | }
130 | });
131 | });
132 |
133 | router.put('/:id', restrict.isAuthenticated, restrict.isAuthorized, function(req, res, next) {
134 | var params = {
135 | title: req.body.title,
136 | alias: req.body.alias,
137 | summary: req.body.summary,
138 | source: req.body.source,
139 | content: req.body.content,
140 | imgList: req.body.imgList,
141 | category: req.body.category,
142 | user: req.session.user || req.body.user,
143 | labels: req.body.labels,
144 | url: req.body.url,
145 | isDraft: req.body.isDraft == true,
146 | isActive: req.body.isActive == true,
147 | updatedTime: Date.now()
148 | };
149 | params = tool.deObject(params);
150 | postModel.findByIdAndUpdate(req.params.id, params, function(err) {
151 | if (err) {
152 | next(err);
153 | } else {
154 | userModel.findById(req.body.user._id, function(err, user) {
155 | if (err) {
156 | next(err);
157 | } else {
158 | var postList = user.postList;
159 | var len = postList.length;
160 | for (var i = 0; i < len; i++) {
161 | if (postList[i]['_id'] == params._id) {
162 | postList[i]['title'] = params.title;
163 | postList[i]['alias'] = params.alias;
164 | break;
165 | }
166 | }
167 | user.save(function(err) {
168 | if (err) {
169 | next(err);
170 | } else {
171 | res.json({
172 | status: 'success',
173 | data: null
174 | });
175 | }
176 | });
177 | }
178 | });
179 | }
180 | });
181 | });
182 | //
183 | router.get('/:id', function(req, res, next) {
184 | var id = req.params.id;
185 | if (!id) {
186 | res.redirect('/admin/articlemanage');
187 | }
188 |
189 | postModel.findByIdAndUpdate(id, { "$inc": { "viewCount": 1 } }, { new: true }, function(err, post) {
190 | if (err) {
191 | next(err);
192 | } else if (!post) {
193 | next(new Error('404'));
194 | } else {
195 | var result = {
196 | _id: post._id,
197 | title: post.title,
198 | alias: post.alias,
199 | summary: post.summary,
200 | source: post.source,
201 | content: post.content,
202 | imgList: post.imgList,
203 | labels: post.labels,
204 | url: post.url,
205 | user: post.user,
206 | category: post.category,
207 | viewCount: post.viewCount,
208 | voteCount: post.voteCount,
209 | commentCount: post.commentCount,
210 | voteList: post.voteList,
211 | commentList: post.commentList,
212 | isDraft: post.isDraft,
213 | isActive: post.isActive,
214 | createdTime: post.createdTime,
215 | updatedTime: post.updatedTime
216 | };
217 | res.json({
218 | status: 'success',
219 | data: result
220 | });
221 | }
222 | })
223 | });
224 |
225 | //删除文章
226 | router.delete('/:id', restrict.isAuthenticated, restrict.isAuthorized, function(req, res, next) {
227 | postModel.findByIdAndUpdate(req.params.id, { 'softDelete': true }, function(err) {
228 | if (err) {
229 | next(err);
230 | } else {
231 | res.json({
232 | status: 'success',
233 | data: null
234 | });
235 | }
236 | });
237 | });
238 | router.put('/vote/:id', restrict.isAuthenticated, function(req, res, next) {
239 | var params = {
240 | voteCount: req.body.voteCount,
241 | voteList: req.body.voteList
242 | };
243 | params = tool.deObject(params);
244 | postModel.findByIdAndUpdate(req.params.id, params, function(err) {
245 | if (err) {
246 | next(err);
247 | } else {
248 | res.json({
249 | status: 'success',
250 | data: null
251 | });
252 | }
253 | });
254 | });
255 | //还原文章
256 | router.post('/undo/:id', restrict.isAuthenticated, restrict.isAdmin, function(req, res, next) {
257 | postModel.findByIdAndUpdate(req.params.id, { 'softDelete': false }, function(err) {
258 | if (err) {
259 | next(err);
260 | } else {
261 | res.json({
262 | status: 'success',
263 | data: null
264 | });
265 | }
266 | });
267 | });
268 |
269 | router.post('/upload', restrict.isAuthenticated, function(req, res, next) {
270 | upload.fileHandler()(req, res, next);
271 | });
272 |
273 | module.exports = router;
274 |
--------------------------------------------------------------------------------
/routes/users.js:
--------------------------------------------------------------------------------
1 | var express = require('express');
2 | var router = express.Router();
3 |
4 | var userModel = require('../models/user').userModel;
5 | var postModel = require('../models/post').postModel;
6 | var restrict = require('../utility/restrict');
7 | var tool = require('../utility/tool');
8 |
9 | /* GET users listing. */
10 | router.get('/', function(req, res, next) {
11 | res.send('respond with a resource');
12 | });
13 |
14 | router.get('/index', function(req, res, next) {
15 | res.render('user/index', {
16 | title: ' - 全部用户'
17 | });
18 | });
19 |
20 | router.get('/show', function(req, res, next) {
21 | res.render('user/show', {
22 | title: ' - 用户主页'
23 | });
24 | });
25 |
26 | //
27 | router.get('/:id', function(req, res, next) {
28 | var id = req.params.id;
29 | if (!id) {
30 | next(new Error('404'));
31 | }
32 | userModel.findById(id, function(err, user) {
33 | if (err) {
34 | next(err);
35 | } else if (!user) {
36 | next(new Error('404'));
37 | } else {
38 | var result = {
39 | _id: user._id,
40 | username: user.username,
41 | email: user.email,
42 | avatar: user.avatar,
43 |
44 | voteCount: user.voteCount,
45 | commentCount: user.commentCount,
46 |
47 | voteList: user.voteList,
48 | commentList: user.commentList,
49 | postList: user.postList,
50 |
51 | createdTime: user.createdTime,
52 | updatedTime: user.updatedTime
53 | };
54 | res.json({
55 | status: 'success',
56 | data: result
57 | });
58 | }
59 | })
60 | });
61 |
62 | router.put('/:id', restrict.isAuthenticated, restrict.isAuthorized, function(req, res, next) {
63 | if (!req.params.id) {
64 | next(new Error('404'));
65 | }
66 | var params = {
67 | username: req.body.username,
68 | email: req.body.email,
69 | avatar: req.body.avatar,
70 | updatedTime: Date.now()
71 | };
72 | params = tool.deObject(params);
73 | //new:true to return the modified document rather than the original.
74 | userModel.findByIdAndUpdate(req.params.id, { $set: params }, { new: true }, function(err, user) {
75 | if (err) {
76 | next(err);
77 | } else if (!user) {
78 | next(new Error('user is invalided'));
79 | } else {
80 | var userParams = {
81 | 'user.username': user.username,
82 | 'user.email': user.email,
83 | 'user.avatar': user.avatar,
84 | };
85 | var ids = user.postList.map(function(post, index) {
86 | return post._id;
87 | });
88 |
89 | postModel.update({ _id: { $in: ids } }, { $set: userParams }, { multi: true }, function(err) {
90 | if (err) {
91 | next(err);
92 | } else {
93 | res.json({
94 | status: 'success',
95 | data: null
96 | });
97 | }
98 | });
99 | }
100 | });
101 | });
102 |
103 | module.exports = router;
104 |
--------------------------------------------------------------------------------
/utility/hash.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Module dependencies.
3 | */
4 |
5 | var crypto = require('crypto');
6 |
7 | /**
8 | * Bytesize.
9 | */
10 |
11 | var len = 128;
12 |
13 | /**
14 | * Iterations. ~300ms
15 | */
16 |
17 | var iterations = 12000;
18 |
19 | /**
20 | * Hashes a password with optional `salt`, otherwise
21 | * generate a salt for `pass` and invoke `fn(err, salt, hash)`.
22 | *
23 | * @param {String} password to hash
24 | * @param {String} optional salt
25 | * @param {Function} callback
26 | * @api public
27 | */
28 |
29 | exports.hash = function(pwd, salt, fn) {
30 | if (3 == arguments.length) {
31 | crypto.pbkdf2(pwd, salt, iterations, len, function(err, hash) {
32 | fn(err, hash.toString('base64'));
33 | });
34 | } else {
35 | fn = salt;
36 | crypto.randomBytes(len, function(err, salt) {
37 | if (err) return fn(err);
38 | salt = salt.toString('base64');
39 | crypto.pbkdf2(pwd, salt, iterations, len, function(err, hash) {
40 | if (err) return fn(err);
41 | fn(null, salt, hash.toString('base64'));
42 | });
43 | });
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/utility/redisClient.js:
--------------------------------------------------------------------------------
1 | var redis = require('redis');
2 | var config = require('../config');
3 | var client = redis.createClient(config.RedisPort, config.RedisHost, { auth_pass: config.RedisPass });
4 | client.on('error', function(err) {
5 | console.error('Redis连接错误: ' + err);
6 | process.exit(1);
7 | });
8 |
9 | /**
10 | * 设置缓存
11 | * @param key 缓存key
12 | * @param value 缓存value
13 | * @param expired 缓存的有效时长,单位秒
14 | * @param callback 回调函数
15 | */
16 | exports.setItem = function(key, value, expired, callback) {
17 | client.set(key, JSON.stringify(value), function(err) {
18 | if (err) {
19 | return callback(err);
20 | }
21 | if (expired) {
22 | client.expire(key, expired);
23 | }
24 | return callback(null);
25 | });
26 | };
27 |
28 | /**
29 | * 获取缓存
30 | * @param key 缓存key
31 | * @param callback 回调函数
32 | */
33 | exports.getItem = function(key, callback) {
34 | client.get(key, function(err, reply) {
35 | if (err) {
36 | return callback(err);
37 | }
38 | return callback(null, JSON.parse(reply));
39 | });
40 | };
41 |
42 | /**
43 | * 移除缓存
44 | * @param key 缓存key
45 | * @param callback 回调函数
46 | */
47 | exports.removeItem = function(key, callback) {
48 | client.del(key, function(err) {
49 | if (err) {
50 | return callback(err);
51 | }
52 | return callback(null);
53 | });
54 | };
55 |
56 | /**
57 | * 获取默认过期时间,单位秒
58 | */
59 | exports.defaultExpired = parseInt(config.CacheExpired);
60 |
--------------------------------------------------------------------------------
/utility/restrict.js:
--------------------------------------------------------------------------------
1 | exports.isAuthenticated = function(req, res, next) {
2 | if (req.session.user) {
3 | next();
4 | } else {
5 | var err = new Error('notAuthenticated');
6 | err.status = 401;
7 | next(err);
8 | }
9 | };
10 | exports.isAuthorized = function(req, res, next) {
11 | if (req.session.user.role === 'admin') {
12 | next();
13 | } else if (req.body.user && req.body.user._id == req.session.user._id) {
14 | next();
15 | } else {
16 | var err = new Error('notAuthorized');
17 | err.status = 403;
18 | next(err);
19 | }
20 | };
21 | exports.isAdmin = function(req, res, next) {
22 | if (req.session.user.role === 'admin') {
23 | next();
24 | } else {
25 | var err = new Error('notAuthorized');
26 | err.status = 403;
27 | next(err);
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/utility/tool.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var formidable = require('formidable');
3 | /**
4 | * 搜索JSON数组
5 | * @param jsonArray JSON数组
6 | * @param conditions 查询条件,如 {"name":"value"}
7 | * @returns {Object} 匹配的JSON对象
8 | */
9 | exports.jsonQuery = function(jsonArray, conditions) {
10 | var i = 0,
11 | len = jsonArray.length,
12 | json,
13 | condition,
14 | flag;
15 | for (; i < len; i++) {
16 | flag = true;
17 | json = jsonArray[i];
18 | for (condition in conditions) {
19 | if (json[condition] !== conditions[condition]) {
20 | flag = false;
21 | break;
22 | }
23 | }
24 | if (flag) {
25 | return json;
26 | }
27 | }
28 | };
29 |
30 | /**
31 | * 读取配置文件
32 | * @param filePath 文件路径
33 | * @param [key] 要读取的配置项key
34 | * @param callback 回调函数
35 | */
36 | exports.getConfig = function(filePath, key, callback) {
37 | if (typeof key === 'function') {
38 | callback = key;
39 | key = undefined;
40 | }
41 | fs.readFile(filePath, 'utf8', function(err, file) {
42 | if (err) {
43 | console.log('读取文件%s出错:' + err, filePath);
44 | return callback(err);
45 | }
46 | var data = JSON.parse(file);
47 | if (typeof key === 'string') {
48 | data = data[key];
49 | }
50 | return callback(null, data);
51 | });
52 | };
53 |
54 | /**
55 | * 写入配置文件
56 | * @param filePath 文件路径
57 | * @param setters 要写入的对象
58 | */
59 | exports.setConfig = function(filePath, setters) {
60 | fs.readFile(filePath, 'utf8', function(err, file) {
61 | var data = JSON.parse(file),
62 | key;
63 | for (key in setters) {
64 | data[key] = setters[key];
65 | }
66 | var newFile = JSON.stringify(data, null, 2);
67 | fs.writeFile(filePath, newFile, 'utf8');
68 | });
69 | };
70 |
71 | /**
72 | * 根据对象的属性和值拼装key
73 | * @param [prefix] key前缀
74 | * @param obj 待解析对象
75 | * @returns {string} 拼装的key,带前缀的形如:prefix_name_Tom_age_20,不带前缀的形如:name_Tom_age_20
76 | */
77 | exports.generateKey = function(prefix, obj) {
78 | if (typeof prefix === 'object') {
79 | obj = prefix;
80 | prefix = undefined;
81 | }
82 | var attr,
83 | value,
84 | key = '';
85 | for (attr in obj) {
86 | value = obj[attr];
87 | key += attr.toString().toLowerCase() + '_' + value.toString();
88 | }
89 | if (prefix) {
90 | //形如:prefix_name_Tom_age_20
91 | key = prefix + '_' + key;
92 | }
93 | return key;
94 | };
95 | /**
96 | * 删除对象里值为空的数据
97 | * @param obj 该对象
98 | * @param callback 回调
99 | */
100 | exports.deObject = function(obj, callback) {
101 | for (var key in obj) {
102 | if (obj[key] === undefined || obj[key] === null || obj[key] === '') {
103 | delete obj[key];
104 | }
105 | }
106 | return obj;
107 | };
108 | /**
109 | * 返回上传对象,该上传对象有参数configure,fileHandler
110 | */
111 | exports.upload = (function() {
112 | var uploadDir, uploadUrl;
113 | var configure = function(opts) {
114 | opts.uploadDir && (uploadDir = opts.uploadDir);
115 | opts.uploadUrl && (uploadUrl = opts.uploadUrl);
116 | };
117 | var fileHandler = function() {
118 | var result = [];
119 | var form = new formidable.IncomingForm();
120 | form.uploadDir = uploadDir;
121 | form.maxFieldsSize = 2 * 1024 * 1024;
122 | form.maxFields = 1000;
123 | form.keepExtensions = true;
124 | /*form.encoding = 'utf-8';
125 | form.hash = false;
126 | form.on('end', function() {
127 | });
128 | */
129 | return function(req, res, next) {
130 | form.parse(req, function(err, fields, files) {
131 | if (err) {
132 | next(err);
133 | }
134 | var len = uploadDir.length;
135 | for (var key in files) {
136 | result.push(uploadUrl + files[key].path.substring(len));
137 | }
138 | res.json({
139 | status: 'success',
140 | data: result
141 | });
142 | });
143 | }
144 | };
145 | return {
146 | configure: configure,
147 | fileHandler: fileHandler
148 | };
149 | })();
150 |
--------------------------------------------------------------------------------
/utility/validator.js:
--------------------------------------------------------------------------------
1 | var validators = {
2 | required: function(key) {
3 | return !!this[key];
4 | },
5 | max: function(key, size) {
6 | return this[key] && (this[key].length <= size);
7 | },
8 | min: function(key, size) {
9 | return this[key] && (this[key].length >= size);
10 | },
11 | confirmed: function(key, key_confirmation) {
12 | return this[key] && (this[key] === this[key_confirmation]);
13 | },
14 | email: function(key) {
15 | return this[key] && /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((.[a-zA-Z0-9_-]{2,3}){1,2})$/.test(this[key]);
16 | },
17 | numeric: function(key) {
18 | return this[key] && /^[0-9]*$/.test(this[key]);
19 | },
20 | alpha: function(key) {
21 | return this[key] && /^[a-zA-Z]*$/.test(this[key]);
22 | },
23 | alpha_num: function(key) {
24 | return this[key] && /^[a-zA-Z0-9]*$/.test(this[key]);
25 | }
26 | };
27 | /**
28 | * 依据规则验证数据
29 | * @param rules 规则
30 | * @param data 被验证的对象
31 | * @param callback 回调
32 | */
33 |
34 | exports.validator = function(rules, data, callback) {
35 | for (var key in rules) {
36 | var i = 0,
37 | rule = rules[key],
38 | len = rule.length;
39 | while (i < len) {
40 | var rule_split = rule[i].split(':');
41 | var fun = rule_split[0];
42 | var args = rule_split.slice(1);
43 | args.unshift(key);
44 | if (!validators[fun].apply(data, args)) {
45 | return callback(new Error('input wrong on: ' + key));
46 | }
47 | ++i;
48 | }
49 | }
50 | return callback(null);
51 | };
52 |
--------------------------------------------------------------------------------
/views/about/contact.jade:
--------------------------------------------------------------------------------
1 | .container
2 | section.blog-content
3 | header.blog-content-header
4 | h1.blog-content-title Contact Me
5 | section.blog-content-body
6 | article.contact-content
7 | p OK, I am a programmer, or code farmer now, I am going to be an engineer...
8 | p Email: 1317203071@qq.com or caihuabin@outlook.com
9 | p Github:
10 | a(href='https://github.com/icyse') https://github.com/icyse
--------------------------------------------------------------------------------
/views/about/index.jade:
--------------------------------------------------------------------------------
1 | .container
2 | section.blog-content
3 | header.blog-content-header
4 | h1.blog-content-title About MEAN-BLOG
5 | section.blog-content-body
6 | article.about-content
7 | p MEAN-BLOG is a blog project written in MEAN - MongoDB, ExpressJS, AngularJS, NodeJS, And Redis.
8 | p It is a Full-Stack JavaScript project. You can click this
9 | a(href='https://github.com/icyse/mean-blog') [https://github.com/icyse/mean-blog]
10 | | to find all the lastest source code. What I hope is that you can support my work. Such as starring the project, pulling requests and so on.
11 | p By the way, this project is base on my another project named MEAN, which is a full-stack Javascript starting skeleton using MEAN - MongoDB, Express, AngularJS, Node.js, and Redis. You can click this
12 | a(href='https://github.com/icyse/mean') [https://github.com/icyse/mean]
13 | | to get it.
--------------------------------------------------------------------------------
/views/auth/login.jade:
--------------------------------------------------------------------------------
1 | .container
2 | section.blog-content
3 | header.blog-content-header
4 | h1.blog-content-title Login
5 | section.blog-content-body
6 | form.pure-form.pure-form-aligned(method='post', name='loginForm', ng-submit='login(credentials)', novalidate)
7 | fieldset
8 | .pure-control-group
9 | label(for='username') Username
10 | input(type='text', name='username', ng-model='credentials.username', required)
11 | span(style='color:red', ng-show='loginForm.username.$dirty && loginForm.username.$invalid')
12 | span(ng-show='loginForm.username.$error.required') 用户名必填.
13 | .pure-control-group
14 | label(for='password') Password
15 | input(type='password', name='password', ng-model='credentials.password', ng-minlength='6', required)
16 | span(style='color:red', ng-show='loginForm.password.$dirty && loginForm.password.$invalid')
17 | span(ng-show='loginForm.password.$error.required') 密码必填.
18 | span(ng-show='loginForm.password.$error.minlength') 不少于6字符.
19 | .pure-controls
20 | input.pure-button.button-success(type='submit', value='Login', ng-disabled='loginForm.username.$invalid || loginForm.password.$invalid')
21 |
--------------------------------------------------------------------------------
/views/auth/register.jade:
--------------------------------------------------------------------------------
1 | .container
2 | section.blog-content
3 | header.blog-content-header
4 | h1.blog-content-title Register
5 | section.blog-content-body
6 | form.pure-form.pure-form-aligned(method='post', name='registerForm', ng-submit='register()', novalidate)
7 | fieldset
8 | .pure-control-group
9 | label(for='username') Username
10 | input(type='text', name='username', ng-model='user.username', ensure-unique='user.username', required)
11 | span(style='color:red', ng-show='registerForm.username.$dirty && registerForm.username.$invalid')
12 | span(ng-show='registerForm.username.$error.required') 用户名必填.
13 | span(ng-show='registerForm.username.$error.unique') 用户名已存在.
14 | .pure-control-group
15 | label(for='email') Email
16 | input(type='email', name='email', ng-model='user.email', required)
17 | span(style='color:red', ng-show='registerForm.email.$dirty && registerForm.email.$invalid')
18 | span(ng-show='registerForm.email.$error.required') 邮箱必填.
19 | span(ng-show='registerForm.email.$error.email') 不是有效邮箱.
20 | .pure-control-group
21 | label(for='password') Password
22 | input(type='password', name='password', ng-model='user.password', ng-minlength='6', required)
23 | span(style='color:red', ng-show='registerForm.password.$dirty && registerForm.password.$invalid')
24 | span(ng-show='registerForm.password.$error.required') 密码必填.
25 | span(ng-show='registerForm.password.$error.minlength') 不少于6字符.
26 | .pure-control-group
27 | label(for='password_confirmation') Comfirm Password
28 | input(type='password', name='password_confirmation', ng-model='user.password_confirmation', pw-check="password", required)
29 | span(style='color:red', ng-show='registerForm.password_confirmation.$dirty && registerForm.password_confirmation.$invalid')
30 | span(ng-show='registerForm.password_confirmation.$error.required') 验证密码必填.
31 | span(ng-show='registerForm.password_confirmation.$error.pwconfirm') 两次输入不匹配.
32 | .pure-controls
33 | input.pure-button.pure-button-primary(type='submit', value='Register', ng-disabled='registerForm.username.$invalid || registerForm.password.$invalid || registerForm.password_confirmation.$invalid || registerForm.email.$invalid')
34 |
--------------------------------------------------------------------------------
/views/blog/create.jade:
--------------------------------------------------------------------------------
1 | div.container.topPage
2 | section.page
3 | header.page-header.pure-g
4 | div.pure-u-1-2
5 | div.pure-u-1-2.text-right
6 | div.fileInputContainer(ng-controller='UploaderController')
7 | input#imagesUpload.fileInput(type='file', name='files[]', multiple, file-model='readAndUpload')
8 | span.icon-picture
9 | a(ng-click='blogDialog()')
10 | span.menu-icon.icon-eye
11 | section.page-content
12 | form.pure-form.pure-form-aligned(method='post', name='postForm', ng-submit='save()', novalidate)
13 | fieldset
14 | .pure-g
15 | .pure-u-1.pure-u-md-2-3
16 | .pure-control-group
17 | label(for='title') Title
18 | input.pure-input-2-3(type='text', name='title', placeholder='Title', ng-model='post.title', required)
19 | span(style='color:red', ng-show='postForm.title.$dirty && postForm.title.$invalid')
20 | span(ng-show='postForm.title.$error.required') 标题是必须的.
21 | .pure-control-group
22 | label(for='alias') Alias
23 | input.pure-input-2-3(type='text', name='alias', placeholder='Alias', ng-model='post.alias', ensure-unique='post.alias', required)
24 | span(style='color:red', ng-show='postForm.alias.$dirty && postForm.alias.$invalid')
25 | span(ng-show='postForm.alias.$error.required') 是必须的.
26 | span(ng-show='postForm.alias.$error.unique') 已存在.
27 | .pure-control-group
28 | label(for='content') Content
29 | textarea.pure-input-2-3(name='content', rows='23', placeholder='Content(markdown)', ng-model='post.content', required)
30 | span(style='color:red', ng-show='postForm.content.$dirty && postForm.content.$invalid')
31 | span(ng-show='postForm.content.$error.required') 是必须的.
32 | .pure-u-1.pure-u-md-1-3
33 | .pure-control-group
34 | label(for='summary') Summary
35 | input.pure-input-2-3(type='text', name='summary', placeholder='Summary', ng-model='post.summary')
36 | .pure-control-group
37 | label(for='source') Source
38 | input.pure-input-2-3(type='text', name='source', placeholder='Source', ng-model='post.source')
39 | .pure-control-group
40 | label(for='url') Url
41 | input.pure-input-2-3(type='text', name='url', placeholder='Url', ng-model='post.url')
42 | .pure-control-group
43 | label(for='category') Category
44 | input.pure-input-2-3(type='text', name='category', placeholder='Category', ng-model='post.category')
45 | .pure-control-group
46 | label(for='labels') Labels
47 | input.pure-input-2-3(type='text', name='labels', placeholder='Labels', ng-model='post.labels')
48 | .pure-controls
49 | label.pure-checkbox(for='isDraft')
50 | input(type='checkbox', ng-model='post.isDraft', show-message='{"status":"info", "message": "It will be saved as a draft, can be found in your personal home page"}')
51 | | isDraft
52 | input.pure-button.button-large(type='submit', value='Done', ng-disabled='postForm.title.$invalid || postForm.content.$invalid || postForm.alias.$invalid')
53 | div.pure-u-1(ng-controller='IngredientsCtrl')
54 | div.pure-u-1-5.img-box(ng-repeat='imageDataUrl in imageDataUrlList track by $index')
55 | img.pure-img-responsive(ng-src='{{ imageDataUrl }}')
56 | nav
57 | span.icon-ok-circled2.nav-item.nav-ok
58 | a.icon-cancel-circled2.nav-item.nav-cancel(href='javascript:;', ng-click='removeImageDataUrl($index)')
59 | div(blog-dialog)
--------------------------------------------------------------------------------
/views/blog/edit.jade:
--------------------------------------------------------------------------------
1 | div.container.topPage
2 | section.page
3 | header.page-header.pure-g
4 | div.pure-u-1-2
5 | div.pure-u-1-2.text-right
6 | a(ng-click='remove()')
7 | span.menu-icon.icon-block
8 | div.fileInputContainer(ng-controller='UploaderController')
9 | input#imagesUpload.fileInput(type='file', name='files[]', multiple, file-model='readAndUpload')
10 | span.icon-picture
11 | a(ng-click='blogDialog()')
12 | span.menu-icon.icon-eye
13 | section.page-content
14 | form.pure-form.pure-form-aligned(method='post', name='postForm', ng-submit='update()', novalidate)
15 | fieldset
16 | .pure-control-group
17 | label(for='title') Title
18 | input.pure-input-2-3(type='text', name='title', placeholder='Title', ng-model='post.title', required)
19 | span(style='color:red', ng-show='postForm.title.$dirty && postForm.title.$invalid')
20 | span(ng-show='postForm.title.$error.required') 标题是必须的.
21 | .pure-control-group
22 | label(for='alias') Alias
23 | input.pure-input-2-3(type='text', name='alias', placeholder='Alias', ng-model='post.alias', ensure-unique='post.alias', required)
24 | span(style='color:red', ng-show='postForm.alias.$dirty && postForm.alias.$invalid')
25 | span(ng-show='postForm.alias.$error.required') 是必须的.
26 | span(ng-show='postForm.alias.$error.unique') 已存在.
27 | .pure-control-group
28 | label(for='summary') Summary
29 | input.pure-input-2-3(type='text', name='summary', placeholder='Summary', ng-model='post.summary')
30 | .pure-control-group
31 | label(for='source') Source
32 | input.pure-input-2-3(type='text', name='source', placeholder='Source', ng-model='post.source')
33 | .pure-control-group
34 | label(for='url') Url
35 | input.pure-input-2-3(type='text', name='url', placeholder='Url', ng-model='post.url')
36 | .pure-control-group
37 | label(for='category') Category
38 | input.pure-input-2-3(type='text', name='category', placeholder='Category', ng-model='post.category')
39 | .pure-control-group
40 | label(for='labels') Labels
41 | input.pure-input-2-3(type='text', name='labels', placeholder='Labels', ng-model='post.labels')
42 | .pure-control-group
43 | label(for='content') Content
44 | textarea.pure-input-2-3(name='content', rows='23', placeholder='Content(markdown)', ng-model='post.content', required)
45 | span(style='color:red', ng-show='postForm.content.$dirty && postForm.content.$invalid')
46 | span(ng-show='postForm.content.$error.required') 是必须的.
47 | div.pure-u-1(ng-controller='IngredientsCtrl')
48 | div.pure-u-1-5.img-box(ng-repeat='imageDataUrl in imageDataUrlList track by $index')
49 | img.pure-img-responsive(ng-src='{{ imageDataUrl }}')
50 | nav
51 | span.icon-ok-circled2.nav-item.nav-ok
52 | a.icon-cancel-circled2.nav-item.nav-cancel(href='javascript:;', ng-click='removeImageDataUrl($index)')
53 | .pure-controls
54 | label.pure-checkbox(for='isDraft')
55 | input(type='checkbox', ng-model='post.isDraft', show-message='{"status":"info", "message": "It will be saved as a draft, can be found in your personal home page"}')
56 | | isDraft
57 | input.pure-button.button-large(type='submit', value='Done', ng-disabled='postForm.title.$invalid || postForm.content.$invalid || postForm.alias.$invalid')
58 | div(blog-dialog)
--------------------------------------------------------------------------------
/views/blog/index.jade:
--------------------------------------------------------------------------------
1 | div.container.relative(on-scroll)
2 | div.waterfall
3 | figure.pin.blog-item
4 | a.addBlog(ng-href='/#/blog/create') +
5 | figure.pin.blog-item.animate-pop-up(ng-repeat='post in posts')
6 | figcaption
7 | a(ng-href='/#/blog/show/{{ post._id }}').pure-g
8 | div.pure-u
9 | img.blog-avatar(alt='no image, can talk', height='64', width='64', ng-src='{{ post.user.avatar}}')
10 | div.pure-u-3-4
11 | h5.blog-author {{ post.user.username }}
12 | h4.blog-subject {{ post.title }}
13 | p.blog-desc {{ post.summary }}
14 | div.pure-u-1
15 | img.pinimg(ng-src='{{ post.imgList[0] }}')
16 | div.pure-u-1-2.text-left
17 | span.icon-eye {{ post.viewCount }}
18 | div.pure-u-1-2.text-right
19 | span.icon-thumbs-up-alt {{ post.voteCount }}
20 | nav.slideshow(scroll-to)
21 | a.nav-prev(ng-click='scrollTo(0)')
22 | span.icon-up-open
23 | a.nav-next(ng-click='scrollTo(-1)')
24 | span.icon-down-open
--------------------------------------------------------------------------------
/views/blog/show.jade:
--------------------------------------------------------------------------------
1 | .container
2 | section.blog-content
3 | header.blog-content-header.pure-g
4 | div.pure-u-2-3
5 | h2.blog-content-title {{ post.title }}
6 | p.blog-content-subtitle
7 | | By
8 | a.blog-author {{ post.user.username }}
9 | | at
10 | span {{ post.createdTime }}
11 | p.blog-content-source(ng-if='!!post.source')
12 | | From
13 | a(href='{{ post.url }}') {{ post.source }}
14 |
15 | div.blog-content-controls.pure-u-1-3
16 | button.secondary-button.pure-button(scroll-into='commentText', ng-click='scrollInto()') Reply {{ post.commentCount }}
17 | button.secondary-button.pure-button View {{ post.viewCount }}
18 | button.secondary-button.pure-button(ng-click='vote()', class='{{ !!currentUser && post.voteList.indexOf(currentUser._id)===-1 ? "" : "active" }}') Vote {{ post.voteCount }}
19 | section.blog-content-body
20 | div.blog
21 | p.blog-summary {{ post.summary }}
22 | div.markdown-content(mark-down, ng-bind-html='previewContent')
23 | footer.blog-content-footer
24 | header.blog-comment-header
25 | section.blog-comment-content
26 | div
27 | div.cell.animate-pop-up(ng-repeat='comment in post.commentList')
28 | div.comment-box
29 | div.avatar
30 | a(ng-href='/#/user/{{comment.user._id}}')
31 | img(ng-src='{{comment.user.avatar}}')
32 | div.detail
33 | div.comment-content.typo {{comment.content}}
34 | div.comment-info
35 | span.info-item
36 | i.icon-user
37 | a(ng-href='/#/user/{{comment.user._id}}') {{comment.user.username}}
38 | span.info-item
39 | i.icon-clock
40 | span {{comment.createdTime.substring(0,10)}}
41 | span.pull-right.action
42 | i.icon-comment-1
43 | span Reply
44 | div.cell
45 | form.pure-form.pure-form-stacked(method='post', name='commentForm', ng-controller='CommentCtrl', ng-submit='save()', novalidate)
46 | textarea#commentText.pure-input-1(name='content', rows='6', placeholder='Please sign in to submit the comment', ng-disabled='!currentUser', ng-model='comment.content', required)
47 | input.pure-button.pure-button-primary(type='submit', value='Reply', ng-disabled='commentForm.content.$invalid || !currentUser')
48 | nav.slideshow
49 | a.nav-close(ng-href='/#/blog')
50 | span.icon-cancel
51 | a.nav-edit(ng-href='/#/blog/edit/{{post._id}}')
52 | span.icon-edit
--------------------------------------------------------------------------------
/views/error.jade:
--------------------------------------------------------------------------------
1 | h1= message
2 | h2= error.status
3 | pre= error.stack
--------------------------------------------------------------------------------
/views/index.jade:
--------------------------------------------------------------------------------
1 | extends ./layout
2 | block content
3 | div#layout
4 | a#menuLink.menu-link(href='javascript:;', menu-link)
5 | span
6 | nav#menu
7 | div.pure-menu
8 | a.pure-menu-heading(ng-href='/#/blog') MEAN-BLOG
9 | ul.pure-menu-list
10 | li.pure-menu-item
11 | a.pure-menu-link(ng-href='/#/blog', class='{{ (currentRoutePath.indexOf("/blog") == 0) ? "pure-menu-selected" : "" }}')
12 | span.label-light-red
13 | | BLOG
14 | span.label-count ( {{ BlogCount ? BlogCount : '^.^' }} )
15 | li.pure-menu-item
16 | a.pure-menu-link(ng-href='/#/about', class='{{ (currentRoutePath.indexOf("/about") == 0) ? "pure-menu-selected" : "" }}')
17 | span.label-yellow
18 | | About
19 | li.pure-menu-item
20 | a.pure-menu-link(ng-href='/#/contact', class='{{ (currentRoutePath.indexOf("/contact") == 0) ? "pure-menu-selected" : "" }}')
21 | span.label-green
22 | | Contact
23 | li.pure-menu-item
24 | a.pure-menu-link(ng-href='/#/blog')
25 | span.label-blue
26 | | Nothing
27 | li.pure-menu-heading.menu-item-divided User
28 | li.pure-menu-item(ng-if='!!currentUser')
29 | a.pure-menu-link(ng-href='/#/user/{{currentUser._id}}')
30 | span.label-dark-blue
31 | | Welcome, {{ currentUser.username }}
32 | li.pure-menu-item(ng-if='!!currentUser')
33 | a.pure-menu-link(href='/auth/logout')
34 | span.icon-logout
35 | | Sign Out
36 | ul.nav-list(ng-if='!currentUser')
37 | li.nav-item
38 | a.pure-button(href='javascript:;', ng-click='authDialog("login")') Sign I n
39 | li.nav-item
40 | a.pure-button(href='javascript:;', ng-click='authDialog("register")') Sign Up
41 | div#main
42 | a(href='https://github.com/icyse/mean-blog')
43 | img(style='position:absolute;top:0;left:0;border:0;z-index:1;', src='/images/fork-me-on-github.png', alt='Fork me on GitHub')
44 | section(id='content', ng-view, class='fade')
45 | footer(id='info')
46 | p Originated by
47 | a(href='https://github.com/icyse') Cai
48 | p Modified by
49 | a(href='https://github.com/icyse') Cai
50 | | in Aprl 2016
--------------------------------------------------------------------------------
/views/layout.jade:
--------------------------------------------------------------------------------
1 | doctype html
2 | html(lang='zh-CN')
3 | head
4 | meta(http-equiv='Content-Type', content='text/html; charset=utf-8')
5 | meta(charset='utf-8')
6 | meta(name='viewport', content='width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no')
7 | meta(http-equiv='X-UA-Compatible', content='IE=edge,chrome=1')
8 | meta(name='keywords',content='#{keywords ? keywords : "blog,mean"}')
9 | meta(name='description',content='#{description ? description : "blog"}')
10 | meta(name='fragment', content='!')
11 | base(href='/')
12 | link(href='/stylesheets/pure-min.css', rel='stylesheet')
13 | link(href='/stylesheets/grids-responsive-min.css', rel='stylesheet')
14 | link(href='/fonts/fontello/css/fontello.css', rel='stylesheet')
15 | link(href='/stylesheets/side-menu.css', rel='stylesheet')
16 | link(href='/stylesheets/nprogress.css', rel='stylesheet')
17 | link(href='/stylesheets/style.css', rel='stylesheet')
18 |
21 | script
22 | | var clientUser = !{ JSON.stringify(clientUser) || 'null' };window.clientUser = clientUser;
23 | title= 'MEAN-BLOG'
24 | body(ng-app='mean', ng-controller='ApplicationController')
25 |
26 | block content
27 | div(current-message)
28 | div(auth-dialog)
29 | script(type='text/javascript', src='/javascripts/vendor/nprogress.js', charset='utf-8')
30 | script(type='text/javascript', src='/javascripts/vendor/markdown.min.js', charset='utf-8')
31 | script(type='text/javascript', src='/javascripts/vendor/angular-1.5.0/angular.min.js', charset='utf-8')
32 | script(type='text/javascript', src='/javascripts/vendor/angular-1.5.0/angular-resource.min.js', charset='utf-8')
33 | script(type='text/javascript', src='/javascripts/vendor/angular-1.5.0/angular-route.min.js', charset='utf-8')
34 | script(type='text/javascript', src='/javascripts/vendor/angular-1.5.0/angular-animate.min.js', charset='utf-8')
35 |
36 | script(type='text/javascript', src='/javascripts/configs/configs.js', charset='utf-8')
37 | script(type='text/javascript', src='/javascripts/services/services.js', charset='utf-8')
38 | script(type='text/javascript', src='/javascripts/directives/directives.js', charset='utf-8')
39 | script(type='text/javascript', src='/javascripts/controllers/controllers.js', charset='utf-8')
--------------------------------------------------------------------------------
/views/user/index.jade:
--------------------------------------------------------------------------------
1 | div.container.topPage
2 | section.page
3 | header.page-header.pure-g
4 | section.page-content
5 |
--------------------------------------------------------------------------------
/views/user/show.jade:
--------------------------------------------------------------------------------
1 | div.container.topPage.pure-g
2 | div.pure-u-1.pure-u-md-7-24
3 | section.page.info-page(ng-show='!editable')
4 | header.page-header
5 | div.pure-u-1-2
6 | label Information
7 | div.pure-u-1-2.text-right(ng-show='isOwner')
8 | span.menu-icon.icon-edit(ng-click='editable=true')
9 | section.page-content
10 | div.pure-u-1.user-avatar
11 | img(alt='no image, can talk', ng-src='{{ user.avatar}}')
12 | ul.pure-u-1-3.text-right
13 | li Username
14 | li Email
15 | li Join on
16 | ul.pure-u-2-3.text-left
17 | li {{user.username}}
18 | li {{user.email}}
19 | li {{ user.createdTime.substring(0, 10) }}
20 | section.page.info-page(ng-show='isOwner && editable')
21 | header.page-header
22 | div.pure-u-1-2
23 | label Information
24 | div.pure-u-1-2.text-right
25 | span.menu-icon.icon-cancel-circled(ng-click='editable=false')
26 | section.page-content
27 | div.pure-u-1.user-avatar(ng-controller='UploaderController')
28 | input#imagesUpload.fileInput(type='file', name='files[]', multiple, file-model='readAndUpload')
29 | img(alt='no image, can talk', ng-controller='IngredientsCtrl', ng-src='{{ imageDataUrlList[0]? imageDataUrlList[0] : user.avatar}}')
30 | div.pure-u-1
31 | form.pure-form.form-ul-aligned(method='post', name='userForm', ng-submit='update()', novalidate)
32 | ul.pure-u-1-3.text-right
33 | li Username
34 | li Email
35 | ul.pure-u-2-3.text-left
36 | li
37 | input(type='text', name='username', ng-model='user.username', required)
38 | li
39 | input(type='email', name='email', ng-model='user.email', required)
40 | div.text-center
41 | input.pure-button.button-success(type='submit', value='Update', ng-disabled='userForm.username.$invalid || userForm.email.$invalid')
42 | div.pure-u-1.pure-u-md-17-24
43 | section.page.acti-page
44 | header.page-header
45 | label Activities
46 | section.page-content
47 | ul
48 | li.cell(ng-repeat='post in user.postList')
49 | a.post-title(ng-href='/#/blog/show/{{ post._id }}') {{post.title}}
50 | span.post-time {{post.createdTime.substring(0,10)}}
--------------------------------------------------------------------------------