├── 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+="![no image, can talk]("+a+' "by Cai")'}),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+="![no image, can talk]("+a+' "by Cai")'}),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)}} --------------------------------------------------------------------------------