commit 4ecb1a9185b4986d9206f67cf9e4e36130ae83ef Author: Andrew Davidson Date: Sun Nov 15 03:07:13 2015 +0000 Initial commit, post creation and build is just barely working. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6787bcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +config.js + +logs +*.log +npm-debug.log* + +pids +*.pid + +node_modules + +preload.sql diff --git a/app.js b/app.js new file mode 100644 index 0000000..1f03ae4 --- /dev/null +++ b/app.js @@ -0,0 +1,312 @@ +var express = require('express'); +var flash = require('express-flash'); +var path = require('path'); +var passport = require('passport'); +var Strategy = require('passport-local').Strategy; +var async = require('async'); + +var helper = require('./helper.js'); +var db = require('./db.js'); +var build = require('./build.js'); + +var app = express(); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'jade'); + +// Make HTML pretty while we're setting up our jade templates +// but it doesn't need to be pretty in production. +if (app.get('env') === 'development') { + app.locals.pretty = true; +}; + + +// uncomment after placing your favicon in /public +//app.use(require('serve-favicon')(path.join(__dirname, 'public', +// 'favicon.ico'))); +app.use(flash()); +app.use(require('morgan')('dev')); +app.use(require('cookie-parser')()); +app.use(require('body-parser').urlencoded({ extended: true })); +app.use(require('express-session')({ secret: 'amdasdfasdfamd', + resave: true, saveUninitialized: true })); +app.use(express.static(path.join(__dirname, 'public'))); + +// Get config variables +var config = require('./config'); + +// Setup authentication via twitter. +passport.use(new Strategy( + function(username, password, done) { + db.getUser(username, password, function(user) { + if (!user) { return done(null, false); } + return done(null, user); + }); + } +)); + +passport.serializeUser(function(user, done) { + done(null, user.id); +}); + +passport.deserializeUser(function(id, done) { + db.getUserById(id, function (user) { + if (!user) { return done(false); } + done(null, user); + }); +}); + +app.use(passport.initialize()); +app.use(passport.session()); + +app.get('/login', function(req, res) { + res.render('admin-login', {user: req.user}); +}); + +app.post('/login', + passport.authenticate('local', { successReturnToOrRedirect: '/admin', failureRedirect: '/login' }) +); + +app.get('/logout', function(req, res) { + req.logout(); + res.redirect('/'); +}); + +app.get('/admin/post/list/:start?', + require('connect-ensure-login').ensureLoggedIn(), + function(req, res, next) { + var count = 25; + if (req.params.start) { + var start = req.params.start; + } else { + var start = 0; + } + db.listPosts(count, start, function(posts){ + for (post in posts) { + var date = new Date(posts[post].postDate); + posts[post].dateString = date.getFullYear() + '-' + + ("0" + (date.getMonth()+1)).slice(-2) + '-' + + ("0" + date.getDate()).slice(-2); + } + res.render('admin-post-list', {posts, user: req.user}); + }); +}); + +app.get('/admin/photo/new', + require('connect-ensure-login').ensureLoggedIn(), + function(req, res, next) { + res.render('admin-photo-new', { user: req.user }); +}); + +app.get('/admin/post/new', + require('connect-ensure-login').ensureLoggedIn(), + function(req, res, next) { + db.listCategories(function(rows){ + res.render('admin-post-new', { categories: rows, user: req.user }); + }); +}); + +app.post('/admin/post/new', + require('connect-ensure-login').ensureLoggedIn(), + function(req, res, next) { + var title = req.body.title; + if (req.body.slug != "") { + slug = req.body.slug; + } else { + slug = helper.makeSlug(title); + } + var markdown = req.body.markdown; + var postDate = helper.dateToEpoch(new Date(req.body.postDate)); + var updatedDate = postDate; + var createDate = helper.dateToEpoch(new Date()); + var categoryName = req.body.category; + + db.createPost(title, slug, markdown, postDate, updatedDate, createDate, function(err, row) { + db.setPostCategory(row.id, categoryName, function(category) { + req.flash('successNotice', 'Post created.'); + res.redirect('/admin/post/edit/'+row.id); + }); + }); +}); + +app.get('/admin/post/edit/:id', + require('connect-ensure-login').ensureLoggedIn(), + function(req, res, next) { + var id = req.params.id; + async.parallel({ + categories: function(callback) { + db.listCategories(function(categories) { + callback(null, categories); + }) + }, + postCategory: function(callback) { + db.getPostCategory(id, function(category) { + callback(null, category); + }) + }, + postTags: function(callback) { + db.getPostTagsAsString(id, function(tagString) { + callback(null, tagString); + }) + }, + post: function(callback) { + db.getPostById(id, function(post) { + callback(null, post); + }) + } + }, + function(err, results) { + res.render('admin-post-edit', { + successNotice: req.flash('successNotice'), + failureNotice: req.flash('failureNotice'), + categories: results.categories, + postCategory: results.postCategory, + post: results.post, + postTags: results.postTags, + formattedDate: helper.epochToDateString(results.post.postDate), + user: req.user + }); + }); +}); + +app.post('/admin/post/edit/:id', + require('connect-ensure-login').ensureLoggedIn(), + function(req, res, next) { + var id = req.params.id; + var title = req.body.title; + var slug = helper.makeSlug(title); + var markdown = req.body.markdown; + var postDate = helper.dateToEpoch(new Date(req.body.postDate)); + var categoryName = req.body.category; + var tags = helper.parseTags(req.body.tags); + db.tagPost(id, tags); + db.updatePost(id, title, slug, markdown, postDate, function(err, row) { + db.setPostCategory(id, categoryName, function(err) { + req.flash('successNotice', 'Post updated.'); + res.redirect('/admin/post/edit/'+id); + }); + }); +}); + +app.get('/admin/post/rebuild/:id?', + require('connect-ensure-login').ensureLoggedIn(), + function(req, res, next) { + if (req.params.id) { + build.buildPost(req.params.id, function(err) { + if (!err) { + req.flash('successNotice', 'Post rebuilt successfully.'); + res.redirect('/admin/post/edit/'+req.params.id); + } + else { + req.flash('failureNotice', 'Post rebuild failed, check logs.'); + res.redirect('/admin/post/edit/'+req.params.id); + } + }); + } + else { + res.redirect('/admin/post/list'); + } + +}); + +app.get('/admin', + require('connect-ensure-login').ensureLoggedIn(), + function(req, res, next) { + async.parallel({ + categories: function(callback) { + db.countCategories(function(count) { + callback(null, count); + }); + }, + posts: function(callback) { + db.countPosts(function(count) { + callback(null, count); + }); + }, + galleries: function(callback) { + db.countGalleries(function(count) { + callback(null, count); + }); + }, + photos: function(callback) { + db.countPhotos(function(count) { + callback(null, count); + }); + }, + tags: function(callback) { + db.countTags(function(count) { + callback(null, count); + }); + }, + rebuildDate: function(callback) { + db.getLastRebuildDate(function(date) { + var dateString = helper.epochToDateString(date.date).slice(0,-14); + callback(null, dateString); + }); + }, + uploadDate: function(callback) { + db.getLastUploadDate(function(date) { + var dateString = helper.epochToDateString(date.date).slice(0,-14); + callback(null, dateString); + }); + } + }, + function(err, results) { + res.render('admin-dashboard', { + categories: results.categories, + posts: results.posts, + galleries: results.galleries, + photos: results.photos, + tags: results.tags, + rebuildDate: results.rebuildDate, + uploadDate: results.uploadDate, + user: req.user}); + } + ); +}); + +app.get('/', function(req, res, next) { + res.render('admin-index', { title: 'AMDavidson.com', user: req.user }); +}); + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + var err = new Error('Not Found'); + err.status = 404; + next(err); +}); + +// error handlers + +// development error handler +// will print stacktrace +if (app.get('env') === 'development') { + app.use(function(err, req, res, next) { + res.status(err.status || 500); + res.render('admin-error', { + message: err.message, + error: err, + user: req.user + }); + }); +} + +// production error handler +// no stacktraces leaked to user +app.use(function(err, req, res, next) { + res.status(err.status || 500); + res.render('error', { + message: err.message, + user: req.user, + error: {} + }); +}); + + +app.set('port', process.env.PORT || 3000) +var server = require('http').createServer(app) + +server.listen(app.get('port'), function() { + console.log("Server listening on port " + app.get('port')); +}); diff --git a/build.js b/build.js new file mode 100644 index 0000000..ae40001 --- /dev/null +++ b/build.js @@ -0,0 +1,113 @@ +var fs = require('fs'); +var mkdirp = require('mkdirp'); +var jade = require('jade'); +var markdown = require( "markdown" ).markdown; +var async = require('async'); + +var helper = require('./helper.js'); +var db = require('./db.js'); + +// Build all archive pages associated with a particular post ID +// This currently calls the functions to make the yearly and monthly archives. +var buildPostArchives = function (id, cb) { + db.getPostById(id, function(post) { + console.log('Building dependencies for Post: '+id); + var date = new Date(post.postDate); + var year = date.getFullYear(); + console.log('Building archive for year: '+year); + var month = date.getMonth()+1; + async.parallel({ + monthArchiveErr: function(callback) { + buildPostMonthArchive(year, function(err) { + callback(null, err); + }); + }, + yearArchiveErr: function(callback) { + buildPostYearArchive(month, function(err) { + callback(null, err); + }); + } + }, function(err, results) { + if (!yearArchiveErr && !monthArchiveErr) { + cb(null); + } + else if (yearArchiveErr) { + cb(yearArchiveErr); + } + else if (monthArchiveErr) { + cb(monthArchiveErr); + } + else { + cb(null); + } + }); + + }); +} +exports.buildPostArchives = buildPostArchives; + + +// Function that builds the yearly archive pages. +// TODO: make this not a skeleton. +// TODO: create pagination. +var buildPostYearArchive = function (year, cb) { + console.log('Building archive for year: '+year); + cb(null); +} +exports.buildPostYearArchive = buildPostYearArchive; + + +// Function that builds the monthly archive pages. +// TODO: Make this not a skeleton. +// TODO: create pagination. +var buildPostMonthArchive = function (month, cb) { + console.log('Building archive for month: '+month); + cb(null); +} +exports.buildPostMonthArchive = buildPostMonthArchive; + + +// Function to build a single post page. +// TODO: (re)build images as required. +var buildPost = function (id, cb) { + db.getPostById(id, function(post) { + console.log('Fetching post id: '+id); + var title = post.title; + var content = markdown.toHTML(post.markdown); + var slug = post.slug; + var postDate = helper.epochToShortDateString(post.postDate); + var url = '/blog/'+postDate.slice(0,4)+'/'+postDate.slice(5,7)+'/'+slug+'/'; + var filepath = 'build'+url; + var filename = filepath+'index.html'; + var options = { + pretty: false, + title: title, + content: content, + slug: slug, + url: url, + postDate: postDate + }; + + console.log('Rendering post: '+title); + var jadeOut = jade.renderFile('views/render-post.jade', options); + + console.log('Creating directory: '+filepath); + mkdirp(filepath, function(err) { + if (err) { + console.log(err); + } + else { + console.log('Writing to file: '+filename); + + //console.log(jadeOut); + + fs.writeFile(filename, jadeOut, 'utf-8', function(err) { + if (err) console.log(err); + cb(err); + }); + } + }); + }); +} + +exports.buildPost = buildPost; diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..f2c4ee5 --- /dev/null +++ b/build/.gitignore @@ -0,0 +1 @@ +*.* diff --git a/data/logo.png b/data/logo.png new file mode 100644 index 0000000..44c7315 Binary files /dev/null and b/data/logo.png differ diff --git a/db.js b/db.js new file mode 100644 index 0000000..04c1e4c --- /dev/null +++ b/db.js @@ -0,0 +1,353 @@ +var sqlite = require('sqlite3').verbose(); +var db = new sqlite.Database('./app.db'); +var helper = require('./helper.js'); +var async = require('async'); + +// Function to get a user record by the username and password +// Returns SQL row for that user +exports.getUser = function(username, password, cb) { + db.get('SELECT salt FROM users WHERE username = ?', username, function(err, row) { + var hash = helper.hashPassword(password, row.salt); + db.get('SELECT username, id, displayName, createDate \ + FROM users WHERE username = ? AND password = ?', + username, hash, function(err, row) { + cb(row); + }); + }); +} + +// Function to get a user record by id, does not validate user password +// Returns SQL row for that user +exports.getUserById = function(id, cb) { + db.get('SELECT username, id, displayName, createDate \ + FROM users WHERE id = ?', id, function(err, row) { + cb(row); + }); +} + +// Function to get the latest rebuild date +// Returns a epoch time in seconds +exports.getLastRebuildDate = function(cb) { + db.get('SELECT MAX(lastBuildDate) as date FROM ( \ + SELECT lastBuildDate FROM posts \ + UNION \ + SELECT lastBuildDate FROM photos \ + UNION \ + SELECT lastBuildDate FROM galleries);', + function(err, row) { + cb(row); + }); +} + +// Function to get the latest upload date +// Returns a epoch time in seconds +exports.getLastUploadDate = function(cb) { + db.get('SELECT MAX(lastUpload) as date FROM ( \ + SELECT lastUpload FROM posts \ + UNION \ + SELECT lastUpload FROM photos \ + UNION \ + SELECT lastUpload FROM galleries);', + function(err, row) { + cb(row); + }); +} + +// Function to get a count of current posts +// Returns count of posts +exports.countPosts = function(cb) { + db.get('SELECT count(id) as count FROM posts;', function(err, count) { + cb(count.count); + }); +} + +// Function to get a count of current galleries +// Returns count of galleries +exports.countGalleries = function(cb) { + db.get('SELECT count(id) as count FROM galleries;', function(err, count) { + cb(count.count); + }); +} + +// Function to get a count of current categories +// Returns count of categories +exports.countCategories = function(cb) { + db.get('SELECT count(id) as count FROM categories;', function(err, count) { + cb(count.count); + }); +} + +// Function to get a count of current photos +// Returns count of tags +exports.countPhotos = function(cb) { + db.get('SELECT count(id) as count FROM photos;', function(err, count) { + cb(count.count); + }); +} + +// Function to get a count of current tags +// Returns count of tags +exports.countTags = function(cb) { + db.get('SELECT count(id) as count FROM tags;', function(err, count) { + cb(count.count); + }); +} + +// Function to get all categories including their ID and descriptions +// Returns array of rows of categories +exports.listCategories = function(cb) { + db.all('SELECT * FROM CATEGORIES', function(err, rows) { + cb(rows); + }); +} + +// Function to get a list of posts of a certain count and starting at an offset +// Returns a list of post objects +exports.listPosts = function(count, start, cb) { + db.all('SELECT * FROM posts ORDER BY postDate DESC, title ASC \ + LIMIT '+count+' OFFSET '+start+';', + function(err, posts) { + console.log(err); + cb(posts); + }); +} + +// NON-EXPORTED function to create a category if it does not exist +// Returns a row of category information +var getOrCreateCategory = function(name, cb) { + db.get('SELECT * FROM categories WHERE name = "'+name+'";', function(err, row) { + if (!err) { + cb(row); + } else { + epoch = helper.dateToEpoch(new Date()); + db.run('INSERT INTO categories (\ + name, slug, description, createDate, updatedDate, \ + lastBuildDate) VALUES (\ + name = "'+name+'", \ + slug = "'+helper.makeSlug('name')+'", \ + createDate = '+epoch+');', + function(err, row) { + db.get('SELECT * FROM categories WHERE name = "'+name+'";', + function(err, row){ + cb(row); + }) + }) + } + }) +} + +// Function to set a category for a post given a post id and a category name +// Returns error code +exports.setPostCategory = function(postId, categoryName, cb) { + getOrCreateCategory(categoryName, function(category) { + db.get('SELECT categoryId FROM categoryPosts \ + WHERE postId = '+postId+';', + function(err, row) { + if (row) { + if (row.categoryId == category.id) { + cb(err); + } else { + db.run('UPDATE categoryPosts SET \ + categoryId = '+category.id+'\ + WHERE postId = '+postId+';', + function(err, row) { + cb(err); + }); + } + } else { + db.run('INSERT INTO categoryPosts (postId, categoryId) \ + VALUES ('+postId+', '+category.id+');', + function(err, row) { + cb(err); + }); + } + + }) + + }); + +} + +// Function to create a new post +// Returns the post id +exports.createPost = function(title, slug, markdown, postDate, updatedDate, + createDate, cb) { + db.run('INSERT INTO posts (\ + title, \ + slug, \ + markdown, \ + postDate, \ + createDate, \ + lastBuildDate, \ + lastUpload) \ + VALUES (\ + "'+title+'", \ + "'+slug+'", \ + "'+markdown+'", \ + "'+postDate+'", \ + "'+createDate+'", \ + (strftime("%s","1900-01-01 00:00")*1000), \ + (strftime("%s","1900-01-01 00:00")*1000));', + function(err, row) { + db.get('SELECT * FROM posts WHERE title = "'+title+'" AND createDate = '+createDate+';', function(err, row){ + cb(err, row); + }) + }); +} + +// Function to update an existing post record +// Returns the post object +exports.updatePost = function(id, title, slug, markdown, postDate, cb) { + console.log("updatePost called."); + db.run('UPDATE posts SET \ + title = "'+title+'", \ + slug = "'+slug+'", \ + markdown = "'+markdown+'", \ + postDate = '+helper.dateToEpoch(postDate)+', \ + updatedDate = '+helper.dateToEpoch(new Date())+' \ + WHERE id = '+id+';', + function(err) { + console.log('updatePost UPDATE result: '+err); + db.get('SELECT * FROM posts WHERE id = '+id+';', function(err, row) { + console.log('updatePost SELECT result: ' + err); + cb(row); + }); + }); +} + +// Function to get a post record by the id of the post +// Returns the post record +exports.getPostById = function(id, cb) { + db.get('SELECT * FROM posts WHERE id = ?', id, function(err, row) { + cb(row); + }); +} + +// Function to get a category record given a post id +// Returns the first category record associated with the post. +exports.getPostCategory = function(id, cb) { + db.get('SELECT * FROM categories WHERE id = (\ + SELECT categoryId FROM categoryPosts WHERE postId = '+id+');', + function(err, row) { + cb(row); + }); +} + +// Function to get record of a tag, inserting it if required. +// Inputs are a tag name and a callback. +// Returns the created tag record. +var getOrCreateTag = function(tag, cb) { + var slug = helper.makeSlug(tag); + db.get('SELECT * from tags WHERE slug = "'+slug+'" LIMIT 1;', function(err, row) { + if (err) { + console.log(err); + } + if (row) { + console.log(slug+' tag exists'); + console.log(row); + cb(row); + } + else { + db.run('INSERT INTO tags (name, slug, createDate) \ + VALUES ("'+tag+'", "'+slug+'", '+ helper.dateToEpoch(new Date()) +');', + function(err, id) { + if (!err) { + console.log(slug+' tag created'); + db.get('SELECT * FROM tags WHERE slug = "'+slug+'" LIMIT 1;', + function (err, row) { + if (!err) { + console.log(row); + cb(row); + } + else { + console.log(err); + } + }); + }; + }); + } + }) +} + +exports.getOrCreateTag = getOrCreateTag; + + +// Function to tag a post with a list of tag names +// Inputs: Post ID, List of Tag names +// Does not return. +exports.tagPost = function (postId, tags) { + console.log('Deleting old tags'); + db.run('DELETE FROM postTags WHERE postId = '+postId+';', + function(err) { + console.log('Old tags deleted'); + for (var i = 0, size = tags.length; i < size; i++) { + getOrCreateTag(tags[i], function(row) { + db.run('INSERT INTO postTags (postId, tagId) \ + VALUES ('+postId+', '+row.id+');', + function(err) { + if (err) { + console.log(err); + } + else { + console.log('Post '+postId+' tagged as '+row.name); + } + }); + }); + + } + }); +} + + +// Function to get tag ids associated with a particular post id +// Inputs: Post ID +// Returns: Records associated with tags of that post id +var getPostTags = function (postId, cb) { + db.all('SELECT tagId FROM postTags WHERE postId = '+postId+';', + function(err, rows) { + if (err) { + console.log(err); + } + else { + var tagList = []; + for (row in rows) { + tagList.push(rows[row].tagId); + } + tagList = tagList.join(', '); + console.log('Tag ids for '+postId+': '+tagList); + db.all('SELECT * FROM tags WHERE id IN ('+tagList+');', + function(err, rows) { + if (err) { + console.log(err); + } + else { + cb(rows); + } + }) + } + }) +} + +exports.getPostTags = getPostTags; + + +// Function that returns all the post tags as a space separated string. +// Inputs: Post ID, callback function. +// Returns: callback of string with all tags separated by spaces. +var getPostTagsAsString = function(postId, cb) { + getPostTags(postId, function(tags) { + var str = false; + for (tag in tags) { + if (!str) { + str = tags[tag].name; + } + else { + str = str + ' ' + tags[tag].name; + } + } + console.log("Tags for "+postId+": "+str); + cb(str); + }) +} + +exports.getPostTagsAsString = getPostTagsAsString; diff --git a/helper.js b/helper.js new file mode 100644 index 0000000..ee4bd9f --- /dev/null +++ b/helper.js @@ -0,0 +1,64 @@ +// Helper functions +var crypto = require('crypto'); + +// sha256 hex-hashes password with supplied salt. +// Returns hex-hash string +exports.hashPassword = function(password, salt) { + var hash = crypto.createHash('sha256'); + hash.update(password); + hash.update(salt); + return hash.digest('hex'); +} + +// Convert date() to epoch +exports.dateToEpoch = function(date) { + return Math.floor(date); +}; + +// Convert epoch (in seconds) to date string +exports.epochToDateString = function(epoch) { + var date = new Date(epoch); + return date.toLocaleString(); +}; + +// Convert epoch to an ISO formatted date. +exports.epochToShortDateString = function(epoch) { + var date = new Date(epoch); + return date.toISOString().slice(0,10); +} + +// Build a slug from a title string +// Returns the slug as a string +exports.makeSlug = function(str) { + str = str.replace(/^\s+|\s+$/g, ''); // trim + str = str.toLowerCase(); + str = str.trim(); + + // remove accents, swap ñ for n, etc + var from = "àáäâèéëêìíïîòóöôùúüûñç·/_,:;&"; + var to = "aaaaeeeeiiiioooouuuunc-------"; + for (var i=0, l=from.length ; i li > a { + padding-right: 20px; + padding-left: 20px; +} +.nav-sidebar > form > button { + margin-left: 20px; +} + +.nav-sidebar > .active > a, +.nav-sidebar > .active > a:hover, +.nav-sidebar > .active > a:focus { + color: #fff; + background-color: #428bca; +} diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..7c3a4cb --- /dev/null +++ b/schema.sql @@ -0,0 +1,123 @@ +-- SQL Schema for Crunch + +-- Note all dates must be in epoch seconds to allow for sqlite comparison + +-- User table + +DROP TABLE IF EXISTS "users"; + +CREATE TABLE "users" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "username" TEXT NOT NULL UNIQUE, + "password" TEXT NOT NULL, -- sha256 hash of plaintext password + salt + "salt" TEXT NOT NULL, -- salt that is appended to the password + "email" TEXT UNIQUE, + "displayName" TEXT, + "createDate" TEXT +); + +-- Content Tables + +DROP TABLE IF EXISTS "posts"; + +CREATE TABLE "posts" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "title" TEXT, -- The title of the post + "slug" TEXT NOT NULL, -- A slug form of the title or id + "markdown" TEXT NOT NULL, -- The post content in markdown format + "postDate" INTEGER, -- The date that the post should go live and be sorted by + "updatedDate" INTEGER, -- The date that the post was last updated + "createDate" INTEGER NOT NULL, -- The date the post was created + "lastBuildDate" INTEGER, -- The date this post was last built + "lastUpload" INTEGER -- The date the post was last uploaded +); + +DROP TABLE IF EXISTS "galleries"; + +CREATE TABLE "galleries" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "title" TEXT, -- The title of the gallery + "slug" TEXT NOT NULL UNIQUE, -- A slug form of the title or id + "description" TEXT NOT NULL, -- The gallery description in markdown format + "postDate" INTEGER, -- The date that the gallery should go live and be sorted by + "updatedDate" INTEGER, -- The date that the gallery was last updated + "createDate" INTEGER NOT NULL, -- The date the gallery was created + "lastBuildDate" INTEGER, -- The date this gallery was last built + "lastUpload" INTEGER -- The date the gallery was last uploaded +); + +DROP TABLE IF EXISTS "photos"; + +CREATE TABLE "photos" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "path" TEXT NOT NULL, -- The path of the photo location on disk + "title" TEXT, -- The title of the photo + "slug" TEXT NOT NULL UNIQUE, -- A slug form of the title or id + "description" TEXT NOT NULL, -- The photo description in markdown format + "photoDate" INTEGER, -- The date that the photo should go live and be sorted by + "updatedDate" INTEGER, -- The date that the photo was last updated + "createDate" INTEGER NOT NULL, -- The date the photo was created + "lastBuildDate" INTEGER, -- The date this photo was last built + "lastUpload" INTEGER -- The date the photo was last uploaded +); + +DROP TABLE IF EXISTS "galleryPhotos"; + +CREATE TABLE "galleryPhotos" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "galleryId" INTEGER NOT NULL, -- the id of the gallery + "photoId" INTEGER NOT NULL, -- the id of the photo + FOREIGN KEY(galleryId) REFERENCES galleries(id), + FOREIGN KEY(photoId) REFERENCES photos(id) +); + +DROP TABLE IF EXISTS "categories"; + +CREATE TABLE "categories" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, -- The name of this category + "slug" TEXT NOT NULL UNIQUE, -- A slug form of the name or id + "description" TEXT, -- A brief description of the category in markdown + "createDate" INTEGER NOT NULL -- The date the category was created +); + +INSERT INTO "categories" VALUES (1, "Uncategorized", "uncategorized", "Uncategorized posts", (strftime('%s','now')*1000)); + +DROP TABLE IF EXISTS "categoryPosts"; + +CREATE TABLE "categoryPosts" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "categoryId" INTEGER NOT NULL, -- the id of the category + "postId" INTEGER NOT NULL, -- the id of the post + FOREIGN KEY(categoryId) REFERENCES categories(id), + FOREIGN KEY(postId) REFERENCES posts(id) +); + +DROP TABLE IF EXISTS "tags"; + +CREATE TABLE "tags" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, -- The name of this tag + "slug" TEXT NOT NULL UNIQUE, -- A slug form of the name or id + "createDate" INTEGER NOT NULL -- The date the tag was created +); + +DROP TABLE IF EXISTS "postTags"; + +CREATE TABLE "postTags" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "postId" INTEGER NOT NULL, -- the id of the post + "tagId" INTEGER NOT NULL, -- the id of the tag + FOREIGN KEY(postId) REFERENCES posts(id), + FOREIGN KEY(tagId) REFERENCES tags(id) +); + +DROP TABLE IF EXISTS "photoTags"; + +CREATE TABLE "photoTags" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "photoId" INTEGER NOT NULL, -- the id of the photo + "tagId" INTEGER NOT NULL, -- the id of the tag + FOREIGN KEY(photoId) REFERENCES photos(id), + FOREIGN KEY(tagId) REFERENCES tags(id) +); diff --git a/views/admin-dashboard.jade b/views/admin-dashboard.jade new file mode 100644 index 0000000..27a29ab --- /dev/null +++ b/views/admin-dashboard.jade @@ -0,0 +1,75 @@ +extends admin-layout + +block content + div(class="row") + include ./admin-sidebar.jade + + div(class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main") + h1(class="page-header") Crunch Dashboard + + include ./admin-messages.jade + + div(class="row") + div(class="col-md-6") + div(class="panel panel-success") + div(class="panel-heading") + h3(class="panel-title") Last Upload (UTC) + + div(class="panel-body") + h3 #{uploadDate} + + div(class="col-md-6") + div(class="panel panel-danger") + div(class="panel-heading") + h3(class="panel-title") Last Rebuild (UTC) + + div(class="panel-body") + h3 #{rebuildDate} + + div(class="row") + div(class="col-md-4") + div(class="panel panel-info") + div(class="panel-heading") + h3(class="panel-title") Number of Posts + div(class="panel-body") + h3: a(href="/admin/post/list") #{posts} + form(action="/admin/post/new", method="get") + button(class="btn btn-sm btn-primary") New Post + + + div(class="col-md-4") + div(class="panel panel-info") + div(class="panel-heading") + h3(class="panel-title") Number of Galleries + + div(class="panel-body") + h3: a(href="/admin/gallery/list") #{galleries} + form(action="/admin/gallery/new", method="get") + button(class="btn btn-sm btn-primary") New Gallery + + div(class="col-md-4") + div(class="panel panel-info") + div(class="panel-heading") + h3(class="panel-title") Number of Photos + + div(class="panel-body") + h3: a(href="/admin/photo/list") #{photos} + form(action="/admin/photo/new", method="get") + button(class="btn btn-sm btn-primary") New Photo + + div(class="row") + div(class="col-md-6") + div(class="panel panel-info") + div(class="panel-heading") + h3(class="panel-title") Number of Tags + + div(class="panel-body") + h3 #{tags} + + div(class="col-md-6") + div(class="panel panel-info") + div(class="panel-heading") + h3(class="panel-title") Number of Categories + + div(class="panel-body") + h3: a(href="/admin/category/list") #{categories} diff --git a/views/admin-error.jade b/views/admin-error.jade new file mode 100644 index 0000000..d5ee170 --- /dev/null +++ b/views/admin-error.jade @@ -0,0 +1,6 @@ +extends admin-layout + +block content + h1= message + h2= error.status + pre #{error.stack} diff --git a/views/admin-index.jade b/views/admin-index.jade new file mode 100644 index 0000000..253e799 --- /dev/null +++ b/views/admin-index.jade @@ -0,0 +1,10 @@ +extends admin-layout + +block content + div(class="page-header row col-sm-10") + h1 Crunch + div(class="row col-sm-10") + include ./admin-messages.jade + + div + p If you don't know why you're here, please head #[a(href='http://youtube.com') somewhere else] diff --git a/views/admin-layout.jade b/views/admin-layout.jade new file mode 100644 index 0000000..ddb99b6 --- /dev/null +++ b/views/admin-layout.jade @@ -0,0 +1,20 @@ +doctype html +html + head + title= title + meta(charset="utf-8") + meta(http-equiv="X-UA-Compatible", content="IE=edge") + meta(name="viewport", content="width=device-width, initial-scale=1") + //-link(href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css", rel="stylesheet" integrity="sha256-MfvZlkHCEqatNoGiOXveE8FIwMzZg4W85qfrfIFBfYc= sha512-dTfge/zgoMYpP7QbHy4gWMEGsbsdZeCXz7irItjcC3sPUFtf0kuFbDz/ixG7ArTxmDjLXDmezHubeNikyKGVyQ==" crossorigin="anonymous") + link(href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.5/yeti/bootstrap.min.css", rel="stylesheet" integrity="sha256-gJ9rCvTS5xodBImuaUYf1WfbdDKq54HCPz9wk8spvGs= sha512-weqt+X3kGDDAW9V32W7bWc6aSNCMGNQsdOpfJJz/qD/Yhp+kNeR+YyvvWojJ+afETB31L0C4eO0pcygxfTgjgw==" crossorigin="anonymous") + link(rel='stylesheet', href='/stylesheets/style.css') + block head-addition + body(role="document") + include ./admin-navbar.jade + div(class="container-fluid", role="main") + block content + + script(src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js") + script(src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js" integrity="sha256-Sk3nkD6mLTMOF0EOpNtsIry+s1CsaqQC1rVLTAy+0yc= sha512-K1qjQ+NcF2TYO/eI3M6v8EiNYZfA95pQumfvcVrTHtwQVDG+aHRqLi/ETn2uB+1JqwYqVG3LIvdm9lj6imS/pQ==" crossorigin="anonymous") + + block footer diff --git a/views/admin-login.jade b/views/admin-login.jade new file mode 100644 index 0000000..f43d05a --- /dev/null +++ b/views/admin-login.jade @@ -0,0 +1,14 @@ +extends admin-layout + +block content + div(class="page-header row col-sm-6 col-sm-offset-3") + h1 Login + div(class="row col-sm-6 col-sm-offset-3") + form(action="/login", method="post") + div(class="form-group form-horizontal") + label(class="control-label", for="username") Username: + input(class="form-control", type="text", name="username") + div(class="form-group") + label(class="control-label", for="password") Password: + input(class="form-control", type="password", name="password") + button(class="btn btn-success", type="submit") Log In diff --git a/views/admin-messages.jade b/views/admin-messages.jade new file mode 100644 index 0000000..8335390 --- /dev/null +++ b/views/admin-messages.jade @@ -0,0 +1,23 @@ +if (failureNotice) + if (failureNotice.length > 0) + div(class="alert alert-danger alert-dismissible" role="alert") + button(type="button" class="close" data-dismiss="alert" aria-label="Close") + span(aria-hidden="true") × + strong Failure! + | #{failureNotice} + +if (warningNotice) + if (warningNotice.length > 0) + div(class="alert alert-warning alert-dismissible" role="alert") + button(type="button" class="close" data-dismiss="alert" aria-label="Close") + span(aria-hidden="true") × + strong Warning! + | #{warningNotice} + +if (successNotice) + if (successNotice.length > 0) + div(class="alert alert-success alert-dismissible" role="alert") + button(type="button" class="close" data-dismiss="alert" aria-label="Close") + span(aria-hidden="true") × + strong Success! + | #{successNotice} diff --git a/views/admin-navbar.jade b/views/admin-navbar.jade new file mode 100644 index 0000000..582d414 --- /dev/null +++ b/views/admin-navbar.jade @@ -0,0 +1,34 @@ +nav(class="navbar navbar-inverse navbar-fixed-top") + div(class="container-fluid") + div(class="navbar-header") + button(type="button", class="navbar-toggle collapsed", data-toggle="collapse", data-target="#navbar", aria-expanded="false", aria-controls="navbar") + span(class="sr-only") Toggle Navbar + span(class="icon-bar") + span(class="icon-bar") + span(class="icon-bar") + a(class="navbar-brand", href="#") Crunch + div(id="navbar", class="collapse navbar-collapse") + ul(class="nav navbar-nav") + if(!user) + li + a(href="/") + span(class="glyphicon glyphicon-home") Home + li + a(href="/login") + span(class="glyphicon glyphicon-log-in") Login + if(user) + li + a(href="/admin") + span(class="glyphicon glyphicon-home") Dashboard + li + a(href="/admin/post/new") + span(class="glyphicon glyphicon-file") Post + li + a(href="/admin/photo/new") + span(class="glyphicon glyphicon-picture") Photo + li + a(href="/admin/gallery/new") + span(class="glyphicon glyphicon-th-large") Gallery + li + a(href="/logout") + span(class="glyphicon glyphicon-log-out") Logout diff --git a/views/admin-post-edit.jade b/views/admin-post-edit.jade new file mode 100644 index 0000000..8bce4c6 --- /dev/null +++ b/views/admin-post-edit.jade @@ -0,0 +1,39 @@ +extends admin-layout + +block content + div(class="row") + include ./admin-sidebar.jade + + div(class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main") + h1(class="page-header") Edit Post + + include ./admin-messages.jade + + form(method="post", action="/admin/post/edit/#{post.id}") + div(class="row page-header") + div(class="input-group input-group-lg col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1") + input(class="form-control", type="text", name="title", placeholder="Enter title", value="#{post.title}") + div(class="input-group col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1") + input(class="form-control", type="text", name="slug", placeholder="Enter slug (optional)", value="#{post.slug}") + div(class="row page-header") + div(class="input-group col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1") + div(class="input-group-btn") + button(type="button", class="btn btn-primary dropdown-toggle", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false") Category + ul(class="dropdown-menu") + each row in categories + li: a(href="#", onClick="document.getElementById('category').value = '#{row.name}'") #{row.name} + input(type="text", class="form-control", aria-label="Post category", name="category", id="category" value="#{postCategory.name}") + div(class="input-group col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1") + input(type="text", class="form-control", placeholder="Publish Date", name="postDate", value="#{formattedDate}") + div(class="input-group col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1") + input(type="text", class="form-control", placeholder="Tags", name="tags" value="#{postTags}") + div(class="row page-header") + div(class="input-group col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1") + textarea(class="form-control", rows="12", name="markdown") + | #{post.markdown} + div(class="row") + div(class="input-group col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1") + div(class="pull-left") + button(type="submit", class="btn btn-lg btn-success", name="submit") Update Post + div(class="pull-right") + button(type="submit", class="btn btn-lg btn-danger", name="submit") Delete Post diff --git a/views/admin-post-list.jade b/views/admin-post-list.jade new file mode 100644 index 0000000..606d872 --- /dev/null +++ b/views/admin-post-list.jade @@ -0,0 +1,27 @@ +extends admin-layout + +block content + div(class="row") + include ./admin-sidebar.jade + + div(class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main") + h1(class="page-header") Posts + + include ./admin-messages.jade + + table(class="table table-striped") + each post in posts + tr + td #{post.id} + td #{post.title} + td #{post.dateString} + td + a(href="/admin/post/edit/#{post.id}") Edit Post + | - + a(href="/admin/post/delete/#{post.id}") Delete Post + | - + a(href="/admin/post/rebuild/#{post.id}") Force Rebuild + | - + a(href="/admin/post/publish/#{post.id}") Force Publish + + diff --git a/views/admin-post-new.jade b/views/admin-post-new.jade new file mode 100644 index 0000000..fdc00e0 --- /dev/null +++ b/views/admin-post-new.jade @@ -0,0 +1,36 @@ +extends admin-layout + +block content + div(class="row") + include ./admin-sidebar.jade + + div(class="col-sm-10 col-sm-offset-2 col-md-10 col-md-offset-2 main") + h1(class="page-header") New Post + + include ./admin-messages.jade + + form(method="post", action="/admin/post/new") + div(class="row page-header") + div(class="input-group input-group-lg col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1") + input(class="form-control", type="text", name="title", placeholder="Enter title") + div(class="input-group col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1") + input(class="form-control", type="text", name="slug", placeholder="Enter slug (optional)") + div(class="row page-header") + div(class="input-group col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1") + div(class="input-group-btn") + button(type="button", class="btn btn-primary dropdown-toggle", data-toggle="dropdown", aria-haspopup="true", aria-expanded="false") Category + ul(class="dropdown-menu") + each row in categories + li: a(href="#", onClick="document.getElementById('category').value = '#{row.name}'") #{row.name} + input(type="text", class="form-control", aria-label="Post category", name="category", id="category", value="Uncategorized") + div(class="input-group col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1") + input(type="text", class="form-control", placeholder="Publish Date", name="postDate") + div(class="input-group col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1") + input(type="text", class="form-control", placeholder="Tags", name="tags") + div(class="row page-header") + div(class="input-group col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1") + textarea(class="form-control", rows="12", name="markdown", placeholder="Markdown formatted content") + div(class="row") + div(class="input-group col-xs-10 col-xs-offset-1 col-md-10 col-md-offset-1 centern") + div(class="pull-left") + button(type="submit", class="btn btn-lg btn-success", name="submit") Create Post diff --git a/views/admin-sidebar.jade b/views/admin-sidebar.jade new file mode 100644 index 0000000..ee47512 --- /dev/null +++ b/views/admin-sidebar.jade @@ -0,0 +1,25 @@ +div(class="col-sm-3 col-md-2 sidebar") + ul(class="nav nav-sidebar") + li: a(href="/admin/") Dashboard + + ul(class="nav nav-sidebar") + li: a(href="/admin/post/new") New Post + li: a(href="/admin/post/list") Edit Post + + ul(class="nav nav-sidebar") + li: a(href="/admin/category/new") New Category + li: a(href="/admin/category/list") Edit Category + + ul(class="nav nav-sidebar") + li: a(href="/admin/gallery/new") New Gallery + li: a(href="/admin/gallery/list") Edit Gallery + + ul(class="nav nav-sidebar") + form(action="/admin/rebuild/", method="get") + button(class="btn btn-lg btn-success") + span(class="glyphicon glyphicon-repeat") Build + + ul(class="nav nav-sidebar") + form(action="/admin/publish/", method="get") + button(class="btn btn-lg btn-warning") + span(class="glyphicon glyphicon-upload") Upload diff --git a/views/render-index.jade b/views/render-index.jade new file mode 100644 index 0000000..11b2400 --- /dev/null +++ b/views/render-index.jade @@ -0,0 +1,7 @@ +extends render-layout + +block content + each post in posts + h3: a(href="#{url}") #{title} + p #{postDate} + div !{content} diff --git a/views/render-layout.jade b/views/render-layout.jade new file mode 100644 index 0000000..32bc023 --- /dev/null +++ b/views/render-layout.jade @@ -0,0 +1,13 @@ +doctype html +html + head + title #{title} + meta(charset="utf-8") + meta(http-equiv="X-UA-Compatible", content="IE=edge") + meta(name="viewport", content="width=device-width, initial-scale=1") + link(href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css", rel="stylesheet" integrity="sha256-MfvZlkHCEqatNoGiOXveE8FIwMzZg4W85qfrfIFBfYc= sha512-dTfge/zgoMYpP7QbHy4gWMEGsbsdZeCXz7irItjcC3sPUFtf0kuFbDz/ixG7ArTxmDjLXDmezHubeNikyKGVyQ==" crossorigin="anonymous") + link(rel='stylesheet', href='/stylesheets/style.css') + block head-addition + body(role="document") + div(class="container-fluid", role="main") + block content diff --git a/views/render-post.jade b/views/render-post.jade new file mode 100644 index 0000000..ae775bf --- /dev/null +++ b/views/render-post.jade @@ -0,0 +1,6 @@ +extends render-layout + +block content + h3: a(href="#{url}") #{title} + p #{postDate} + div !{content}