diff --git a/README.md b/README.md index 62c3081d5f..2912f7a13d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # [Ghost v0.1.1](https://github.com/TryGhost/Ghost) [![Build Status](https://magnum.travis-ci.com/TryGhost/Ghost.png?token=hMRLUurj2P3wzBdscyQs&branch=master)](https://magnum.travis-ci.com/TryGhost/Ghost) -Ghost is a free, open, simple blogging platform that's available to anyone who wants to use it. Created and maintained by [John O'Nolan](http://twitter.com/JohnONolan) + [Hannah Wolfe](http://twitter.com/ErisDS) + an amazing group of [contributors](https://github.com/TryGhost/Ghost/pulse). +Ghost is a free, open, simple blogging platform that's available to anyone who wants to use it. Lovingly created and maintained by [John O'Nolan](http://twitter.com/JohnONolan) + [Hannah Wolfe](http://twitter.com/ErisDS) + an amazing group of [contributors](https://github.com/TryGhost/Ghost/contributors). Visit the project's home page at [http://tryghost.org](http://tryghost.org)! diff --git a/app.js b/app.js index f800a84777..c7c2719d2f 100755 --- a/app.js +++ b/app.js @@ -6,17 +6,23 @@ // Module dependencies. var express = require('express'), + when = require('when'), + _ = require('underscore'), + errors = require('./core/shared/errorHandling'), admin = require('./core/admin/controllers'), frontend = require('./core/frontend/controllers'), api = require('./core/shared/api'), flash = require('connect-flash'), Ghost = require('./core/ghost'), I18n = require('./core/lang/i18n'), + filters = require('./core/frontend/filters'), helpers = require('./core/frontend/helpers'), // ## Variables auth, authAPI, + ghostLocals, + loading = when.defer(), /** * Create new Ghost object @@ -24,6 +30,7 @@ */ ghost = new Ghost(); + ghost.app().configure('development', function () { ghost.app().use(express.favicon(__dirname + '/content/images/favicon.ico')); ghost.app().use(express.errorHandler({ dumpExceptions: true, showStack: true })); @@ -66,53 +73,76 @@ next(); }; - helpers.loadCoreHelpers(ghost); - - /** - * API routes.. - * @todo auth should be public auth not user auth + * Expose the standard locals that every external page should have available; + * path, navItems and ghostGlobals */ - ghost.app().get('/api/v0.1/posts', authAPI, api.requestHandler(api.posts.browse)); - ghost.app().post('/api/v0.1/posts', authAPI, api.requestHandler(api.posts.add)); - ghost.app().get('/api/v0.1/posts/:id', authAPI, api.requestHandler(api.posts.read)); - ghost.app().put('/api/v0.1/posts/:id', authAPI, api.requestHandler(api.posts.edit)); - ghost.app().del('/api/v0.1/posts/:id', authAPI, api.requestHandler(api.posts.destroy)); - ghost.app().get('/api/v0.1/settings', authAPI, api.requestHandler(api.settings.browse)); - ghost.app().get('/api/v0.1/settings/:key', authAPI, api.requestHandler(api.settings.read)); - ghost.app().put('/api/v0.1/settings', authAPI, api.requestHandler(api.settings.edit)); + ghostLocals = function(req, res, next) { + ghost.doFilter('ghostNavItems', {path: req.path, navItems: []}, function(navData) { + // Make sure we have a locals value. + res.locals = res.locals || {}; - /** - * Admin routes.. - * @todo put these somewhere in admin - */ + // Extend it with nav data and ghostGlobals + _.extend(res.locals, navData, { + ghostGlobals: ghost.globals() + }); - ghost.app().get(/^\/logout\/?$/, admin.logout); - ghost.app().get('/ghost/login/', admin.login); - ghost.app().get('/ghost/register/', admin.register); - ghost.app().post('/ghost/login/', admin.auth); - ghost.app().post('/ghost/register/', admin.doRegister); - ghost.app().get('/ghost/editor/:id', auth, admin.editor); - ghost.app().get('/ghost/editor', auth, admin.editor); - ghost.app().get('/ghost/blog', auth, admin.blog); - ghost.app().get('/ghost/settings', auth, admin.settings); - ghost.app().get('/ghost/debug', auth, admin.debug.index); - ghost.app().get('/ghost/debug/db/delete/', auth, admin.debug.dbdelete); - ghost.app().get('/ghost/debug/db/populate/', auth, admin.debug.dbpopulate); - ghost.app().get(/^\/(ghost$|(ghost-admin|admin|wp-admin|dashboard|login)\/?)/, auth, function (req, res) { - res.redirect('/ghost/'); - }); - ghost.app().get('/ghost/', auth, admin.index); + next(); + }); + }; - /** - * Frontend routes.. - * @todo dynamic routing, homepage generator, filters ETC ETC - */ - ghost.app().get('/:slug', frontend.single); - ghost.app().get('/', frontend.homepage); + // Expose the promise we will resolve after our pre-loading + ghost.loaded = loading.promise; + when.all([filters.loadCoreFilters(ghost), helpers.loadCoreHelpers(ghost)]).then(function () { - ghost.app().listen(3333, function () { - console.log("Express server listening on port " + 3333); - }); + /** + * API routes.. + * @todo auth should be public auth not user auth + */ + ghost.app().get('/api/v0.1/posts', authAPI, api.requestHandler(api.posts.browse)); + ghost.app().post('/api/v0.1/posts', authAPI, api.requestHandler(api.posts.add)); + ghost.app().get('/api/v0.1/posts/:id', authAPI, api.requestHandler(api.posts.read)); + ghost.app().put('/api/v0.1/posts/:id', authAPI, api.requestHandler(api.posts.edit)); + ghost.app().del('/api/v0.1/posts/:id', authAPI, api.requestHandler(api.posts.destroy)); + ghost.app().get('/api/v0.1/settings', authAPI, api.requestHandler(api.settings.browse)); + ghost.app().get('/api/v0.1/settings/:key', authAPI, api.requestHandler(api.settings.read)); + ghost.app().put('/api/v0.1/settings', authAPI, api.requestHandler(api.settings.edit)); + + /** + * Admin routes.. + * @todo put these somewhere in admin + */ + ghost.app().get(/^\/logout\/?$/, admin.logout); + ghost.app().get('/ghost/login/', admin.login); + ghost.app().get('/ghost/register/', admin.register); + ghost.app().post('/ghost/login/', admin.auth); + ghost.app().post('/ghost/register/', admin.doRegister); + ghost.app().get('/ghost/editor/:id', auth, admin.editor); + ghost.app().get('/ghost/editor', auth, admin.editor); + ghost.app().get('/ghost/content', auth, admin.content); + ghost.app().get('/ghost/settings', auth, admin.settings); + ghost.app().get('/ghost/debug', auth, admin.debug.index); + ghost.app().get('/ghost/debug/db/delete/', auth, admin.debug.dbdelete); + ghost.app().get('/ghost/debug/db/populate/', auth, admin.debug.dbpopulate); + ghost.app().get(/^\/(ghost$|(ghost-admin|admin|wp-admin|dashboard|login)\/?)/, auth, function (req, res) { + res.redirect('/ghost/'); + }); + ghost.app().get('/ghost/', auth, admin.index); + + /** + * Frontend routes.. + * @todo dynamic routing, homepage generator, filters ETC ETC + */ + ghost.app().get('/:slug', ghostLocals, frontend.single); + ghost.app().get('/', ghostLocals, frontend.homepage); + + ghost.app().listen(3333, function () { + console.log("Express server listening on port " + 3333); + + // Let everyone know we have finished loading + loading.resolve(); + }); + + }, errors.logAndThrowError); }()); \ No newline at end of file diff --git a/config.js b/config.js index 6d4551e57e..f8662d4ff2 100644 --- a/config.js +++ b/config.js @@ -68,7 +68,8 @@ connection: { filename: './core/shared/data/testdb.db' }, - debug: true + debug: false + // debug: true }, staging: {}, @@ -86,6 +87,17 @@ }; + /** + * @property {Array} nav + */ + config.nav = [{ + title: 'Home', + url: '/' + }, { + title: 'Admin', + url: '/ghost' + }]; + /** * @property {Object} exports */ diff --git a/content/plugins/exampleFilters.js b/content/plugins/exampleFilters.js new file mode 100644 index 0000000000..712525d583 --- /dev/null +++ b/content/plugins/exampleFilters.js @@ -0,0 +1,27 @@ +(function(){ + "use strict"; + + /** + * Because I didn't want to write over FancyFirstChar + */ + var ExampleFilter; + + var ExampleFilter = function(ghost){ + this.ghost = function() { + return ghost; + } + } + + ExampleFilter.prototype.init = function() { + + this.ghost().registerFilter('messWithAdmin', function(adminNavbar){ + console.log('adminnavbar settings run'); + delete adminNavbar.add; + return adminNavbar; + }); + + }; + + module.exports = ExampleFilter; + +}()); \ No newline at end of file diff --git a/content/plugins/fancyFirstChar.js b/content/plugins/fancyFirstChar.js index f133346be9..b3652f11f8 100644 --- a/content/plugins/fancyFirstChar.js +++ b/content/plugins/fancyFirstChar.js @@ -9,6 +9,7 @@ return ghost; }; }; + FancyFirstChar.prototype.init = function () { this.ghost().registerFilter('prePostsRender', function (posts) { var post, @@ -41,5 +42,7 @@ FancyFirstChar.prototype.activate = function () {}; FancyFirstChar.prototype.deactivate = function () {}; + + module.exports = FancyFirstChar; }()); diff --git a/core/admin/assets/img/dash/Facebook_Images@2x.png b/core/admin/assets/img/dash/Facebook_Images@2x.png new file mode 100644 index 0000000000..838e35c6f3 Binary files /dev/null and b/core/admin/assets/img/dash/Facebook_Images@2x.png differ diff --git a/core/admin/assets/js/editor.js b/core/admin/assets/js/editor.js index 8b101c5c99..cbcd359aad 100644 --- a/core/admin/assets/js/editor.js +++ b/core/admin/assets/js/editor.js @@ -149,10 +149,83 @@ } }); + // ## Shortcuts // Zen writing mode shortcut.add("Alt+Shift+Z", function () { $('body').toggleClass('zen'); }); + var MarkdownShortcuts = [ + { + 'key': 'Ctrl+B', + 'style': 'bold' + }, + { + 'key': 'Meta+B', + 'style': 'bold' + }, + { + 'key': 'Ctrl+I', + 'style': 'italic' + }, + { + 'key': 'Meta+I', + 'style': 'italic' + }, + { + 'key': 'Ctrl+Alt+U', + 'style': 'strike' + }, + { + 'key': 'Ctrl+Shift+K', + 'style': 'code' + }, + { + 'key': 'Alt+1', + 'style': 'h1' + }, + { + 'key': 'Alt+2', + 'style': 'h2' + }, + { + 'key': 'Alt+3', + 'style': 'h3' + }, + { + 'key': 'Alt+4', + 'style': 'h4' + }, + { + 'key': 'Alt+5', + 'style': 'h5' + }, + { + 'key': 'Alt+6', + 'style': 'h6' + }, + { + 'key': 'Ctrl+Shift+L', + 'style': 'link' + }, + { + 'key': 'Ctrl+Shift+I', + 'style': 'image' + }, + { + 'key': 'Ctrl+Q', + 'style': 'blockquote' + }, + { + 'key': 'Ctrl+Shift+1', + 'style': 'currentdate' + } + ]; + + $.each(MarkdownShortcuts, function (index, short) { + shortcut.add(short.key, function () { + return editor.addMarkdown({style: short.style}); + }); + }); }); }(jQuery, Showdown, CodeMirror, shortcut)); \ No newline at end of file diff --git a/core/admin/assets/js/markdown-actions.js b/core/admin/assets/js/markdown-actions.js new file mode 100644 index 0000000000..15f059d849 --- /dev/null +++ b/core/admin/assets/js/markdown-actions.js @@ -0,0 +1,53 @@ +/*global console, jQuery, CodeMirror*/ + +// # Surrounds given text with Markdown syntax +(function ($) { + "use strict"; + var Markdown = { + init : function (options, elem) { + var self = this; + self.elem = elem; + + self.style = (typeof options === 'string') ? options : options.style; + + self.options = $.extend({}, CodeMirror.prototype.addMarkdown.options, options); + + self.replace(); + }, + replace: function () { + var text = this.elem.getSelection(), md; + if (this.options.syntax[this.style]) { + md = this.options.syntax[this.style].replace('$1', text); + this.elem.replaceSelection(md); + } else { + console.log("Invalid style."); + } + + } + }; + + CodeMirror.prototype.addMarkdown = function (options) { + var markdown = Object.create(Markdown); + markdown.init(options, this); + }; + + CodeMirror.prototype.addMarkdown.options = { + style: null, + syntax: { + bold: "**$1**", + italic: "_$1_", + strike: "~~$1~~", + code: "`$1`", + h1: "\n# $1\n", + h2: "\n## $1\n", + h3: "\n### $1\n", + h4: "\n#### $1\n", + h5: "\n##### $1\n", + h6: "\n###### $1\n", + link: "[$1](http://)", + image: "!image[$1](http://)", + blockquote: "> $1", + currentDate: new Date().toLocaleString() + } + }; +}(jQuery)); \ No newline at end of file diff --git a/core/admin/assets/sass/layouts/dashboard.scss b/core/admin/assets/sass/layouts/dashboard.scss index 395141f5dc..84be5dbfb6 100644 --- a/core/admin/assets/sass/layouts/dashboard.scss +++ b/core/admin/assets/sass/layouts/dashboard.scss @@ -4,141 +4,381 @@ * */ -.widget { - width:341px; - height:300px; - background:#fff; - box-shadow: $shadow; - float:left; - margin:0 15px 15px 0; - display:none; +/* ========================================================================== + Widget Base + ========================================================================== */ + +%widget, .widget { + width: $widget-base-width; + height: $widget-base-height; + float:left; + position:relative; + margin:0 15px 15px 0; + display: none; + background-color:#fff; + box-shadow: $shadow; + + .widget-content { + @include box-sizing(border-box); + margin-bottom: 40px; + padding: 20px; + } + + .widget-footer, .widget-header { + @include box-sizing(border-box); + width: 100%; + height: 40px; + font-size: 1.2em; + color: #cecbc7; + border-top: 1px solid #EDECE4; + + .widget-title { + display: inline-block; + padding-top: 7px; + padding-left: 15px; + vertical-align: middle; + text-transform: uppercase; + } + } + + .widget-settings-toggle { + @include box-sizing(border-box); + display: block; + height: 39px; + width: 46px; + float: right; + padding: 7px 14px; + border-left: 1px solid #EDECE4; + cursor: pointer; + @include icon($i-settings, 1em); + } + + .widget-footer { + position: absolute; + bottom: 0; + } } -.none { - margin-right:0; +/* ========================================================================== + Widget Sizes + ========================================================================== */ + +.widget-1x2 { + height: $widget-base-height * 2; } -.time { - background-image: url(../img/dash/Time@2x.png); - background-size: 341px 300px; +.widget-2x2 { + width: $widget-base-width * 2; + height: $widget-base-height * 2; } -.image { - max-width: 100%; - width: 682px + 15px; - background-image: url(../img/dash/Image@2x.png); - background-size: 697px 300px; +.widget-2x1 { + width: $widget-base-width * 2; } -.stats { - max-width: 100%; - width: 682px + 15px; - height: 615px; - background-image: url(../img/dash/Stats@2x.png); - background-size: 697px 615px; + +/* ========================================================================== + Widget Variations + ========================================================================== */ + +%widget-number, .widget-number { + @extend %widget; + + .widget-content { + .info { + margin-top: 30px; + + .count { + display: block; + font-size: 5em; + line-height: 1em; + font-weight: 400; + color: #4a4a4a; + } + + .sub { + font-size: 2em; + color: #9b9b9b; + + mark { + background-color: transparent; + + &.up { + color: $green; + } + &.down { + color: $red; + } + } // mark + } // .sub + } // .info + } // .widget-content + + &.widget-2x2 { + .widget-content { + .info { + margin-top: 100px; + + .count { + font-size: 9em; + } + + .sub { + font-size: 2.5em; + } + } // .info + } // .widget-content + } // .widget-2x2 +} // %widget-number, .widget-number + +//For the settings panel +.widget-settings { + @extend %widget; + background-color: #2d3032; + + .widget-header { + height: 40px; + border-top: none; + border-bottom: 1px solid #4a4a4a; + color: #7E878B; + } + + .widget-content { + padding: 0; + } + + label { + width:100%; + height: 40px; + display: block; + border-bottom: 1px solid #4a4a4a; + font-size: 1.2em; + } + + .title { + @include box-sizing(border-box); + display: inline-block; + width: 100px; + height: 100%; + padding: 8px; + color: #E3EDF2; + text-transform: uppercase; + text-align: right; + } + + input[type="text"] { + @include box-sizing(border-box); + height: 100%; + padding: 8px; + color: #E3EDF2; + text-transform: none; + background: none; + border: none; + border-left: 1px solid #4a4a4a; + } + + .widget-footer, .widget-header { + border-color: #4a4a4a; + } + + .widget-settings-toggle { + border-color: #4a4a4a; + + &.close { + @include icon($i-x, 1em); + } + &.done { + background-color: #A0B95D; + color: #ffffff; + @include icon($i-check, 1em); + } + } +} // .widget-settings + +.none { + margin-right:0; } -.facebook { - background-image: url(../img/dash/Facebook@2x.png); - background-size: 341px 300px; +/* ========================================================================== + Individual Widgets + ========================================================================== */ + +.widget-time { + @extend %widget; + + .summary { + margin-bottom: 30px; + font-size: 1.4em; + color: #cecbc7; + + .day { + float: left; + } + .weather { + float: right; + // TODO: icon for weather + } + + } + + time { + margin-top: 30px; + + .clock { + display: block; + font-size: 5em; + line-height: 1em; + font-weight: 400; + color: #4a4a4a; + } + + .date { + font-size: 2em; + color: #9b9b9b; + } + } +} // .widget-time + +.widget-image { + @extend %widget; + + .widget-content { + height: 100%; + background-image: url(../img/dash/Image@2x.png); + background-size: 100% 100%; + } + + .widget-footer { + margin-top: -40px; + opacity: 0; + background: #ffffff; + @include transition(opacity 200ms linear); + } + + &:hover { + .widget-footer { + opacity: 1; + } + } + +} // .widget-image + +.widget-stats { + @extend %widget-number; } -.gplus { - background-image: url(../img/dash/GooglePlus@2x.png); - background-size: 341px 300px; +.widget-facebook { + @extend %widget-number; + + .info { + .faces { + display: block; + width: 100%; + height: 30px; + margin-top: 25px; + background-image: url("../img/dash/Facebook_Images@2x.png"); + background-size: 100% 100%; + } + } } -.twitter { - background-image: url(../img/dash/Twitter@2x.png); - background-size: 341px 300px; +.widget-gplus { + @extend %widget-number; } -.campaignmonitor { - background-image: url(../img/dash/CampaignMonitor@2x.png); - background-size: 341px 300px; +.widget-twitter { + @extend %widget; } -.posts { - background-image: url(../img/dash/PostsStats@2x.png); - background-size: 341px 300px; - position: relative; +.widget-campaignmonitor { + @extend %widget-number; } -.chart { - box-sizing: border-box; - width: 250px; - height: 250px; - margin: 25px auto 0 auto; - background: #242628; - border: #efefef 54px solid; - border-radius: 500px; -} - -#poststats { +.widget-posts { + @extend %widget; position: relative; - top:-54px; - left: -54px; + + .chart { + @include box-sizing(border-box); + width: 250px; + height: 250px; + position: relative; + z-index: 1; + margin: 0 auto; + background: #242628; + border: #efefef 54px solid; + border-radius: 250px; + + #poststats { + position: relative; + top:-54px; + left: -54px; + } + + .data { + position: absolute; + top: 5px; + color: $midgrey; + font-size: 13px; + list-style: none; + + .ready { + font-size: 18px; + vertical-align: -5%; + margin-right: 5px; + color: $green; + } + + .pending { + font-size: 18px; + vertical-align: -5%; + margin-right: 5px; + color: #f9e15d; + } + + .draft { + font-size: 18px; + vertical-align: -5%; + margin-right: 5px; + color: $red; + } + + } + + } } -.data { - position: absolute; - top: 87px; - color: $midgrey; - font-size: 13px; - list-style: none; -} - -.ready { - font-size: 18px; - vertical-align: -5%; - margin-right: 5px; - color: $green; -} - -.pending { - font-size: 18px; - vertical-align: -5%; - margin-right: 5px; - color: #f9e15d; -} - -.draft { - font-size: 18px; - vertical-align: -5%; - margin-right: 5px; - color: $red; -} /* -.dashboard-controls { +.dashboard-controls { @extend %box; padding:0 15px; - .text { + .text { padding:12px 0; } } -.controls-nav { +.controls-nav { float:left; margin-left:20px; - ul { + ul { border-left: $lightgrey 1px solid; - li { + li { margin: 0; border-right: 1px solid $lightgrey; - a { + a { padding: 12px 15px; color: $grey; - span { + span { color: $darkgrey; } - &:hover { + &:hover { color: $darkgrey; text-decoration: none; } @@ -147,18 +387,18 @@ } } -.widget-stats { - span { +.widget-stats { + span { display: block; font-size: 1.6em; line-height: 1.2em; color: $grey; margin-bottom: 15px; - strong { + strong { font-size: 1.2em; } } - span:first-child { + span:first-child { font-size: 5.4em; line-height: 1.4em; color: #000; @@ -166,16 +406,16 @@ } } -@media only screen and (min-width: 1200px) { - .span4 .vert2 { - .widget-stats { - span { +@media only screen and (min-width: 1200px) { + .span4 .vert2 { + .widget-stats { + span { font-size: 2.6em; - strong { + strong { font-size: 1.2em; } } - span:first-child { + span:first-child { font-size: 12.4em; } } @@ -183,30 +423,30 @@ } // Time & Date Box -.time-date { - .time { +.time-date { + .time { font-size: 7.4em; line-height: 0.7em; border-bottom: 1px solid $lightgrey; - span { + span { font-size: 0.2em; color: $grey; text-transform: uppercase; font-style: normal; } - @media only screen and (min-width: 1400px) { - span { + @media only screen and (min-width: 1400px) { + span { font-size: 0.3em; } } } - .date { + .date { font-size: 2.2em; line-height: 1em; font-weight: bold; color: #000; padding: 15px 0; - span { + span { font-size: 0.7em; font-weight: normal; display: block; @@ -217,23 +457,23 @@ } // Post Statuses Box -.post-statuses { - .status-levels { +.post-statuses { + .status-levels { width: 30%; - div { + div { text-indent: -9999px; } } - .status-text { + .status-text { width: 70%; text-transform: uppercase; font-size: 1.2em; color: $grey; - div { + div { background: none; padding: 15px 0; } - strong { + strong { font-size: 1.6em; width: 60px; padding-right: 5px; @@ -241,38 +481,38 @@ display: inline-block; } } - .scheduled { + .scheduled { background: $green; - strong { + strong { color: $green; } } - .pending { + .pending { background: #fcd039; - strong { + strong { color: #fcd039; } } - .draft { + .draft { background: $red; - strong { + strong { color: $red; } } } -.todays-traffic { - ul { - li { +.todays-traffic { + ul { + li { position: relative; padding: 10px; margin-bottom: 1px; - div { + div { position: relative; z-index: 99; } } - li:before { + li:before { content: ''; position: absolute; height: 34px; @@ -280,84 +520,84 @@ left: 0; z-index: 20; } - li:nth-child(1):before { + li:nth-child(1):before { background: $blue; width: 80%; } - li:nth-child(2):before { + li:nth-child(2):before { background: lighten($blue, 3%); width: 60%; } - li:nth-child(3):before { + li:nth-child(3):before { background: lighten($blue, 6%); width: 40%; } - li:nth-child(4):before { + li:nth-child(4):before { background: lighten($blue, 10%); width: 20%; } } } -.table { +.table { width: 100%; display: block; margin-bottom: 10px; - thead, tbody, tr { + thead, tbody, tr { display: block; } - @media only screen and (min-width: 400px) { - thead { + @media only screen and (min-width: 400px) { + thead { display: none; } } - tbody { - tr { + tbody { + tr { background: $lightbrown; margin-top: 5px; display: block; position: relative; - &:first-child { + &:first-child { margin-top: 0; } } - @media only screen and (min-width: 1200px) { - tr { + @media only screen and (min-width: 1200px) { + tr { padding: 0 10px 0 40px; margin-top: 15px; } } - td { + td { padding: 10px; text-align: right; color: $grey; - strong { + strong { color: #000; } - span { + span { display: none; } - @media only screen and (min-width: 500px) { - span { + @media only screen and (min-width: 500px) { + span { display: inline; } } - .callout { + .callout { color: $green; } - &:first-child { + &:first-child { text-align: left; } } } - .user-img { + .user-img { position: absolute; top: 0; left: 0; display: none; } - @media only screen and (min-width: 1200px) { - .user-img { + @media only screen and (min-width: 1200px) { + .user-img { display: block; } } diff --git a/core/admin/assets/sass/modules/mixins.scss b/core/admin/assets/sass/modules/mixins.scss index 57019e46a9..3d03a0a3a1 100644 --- a/core/admin/assets/sass/modules/mixins.scss +++ b/core/admin/assets/sass/modules/mixins.scss @@ -182,4 +182,11 @@ $green: #9FBB58; text-decoration: none; } } -} \ No newline at end of file +} + +/* ============================================================================= + Widgets + ============================================================================= */ + +$widget-base-height: 300px; +$widget-base-width: 340px; \ No newline at end of file diff --git a/core/admin/controllers/index.js b/core/admin/controllers/index.js index 95b8c52f90..c9b888f123 100755 --- a/core/admin/controllers/index.js +++ b/core/admin/controllers/index.js @@ -17,32 +17,36 @@ name: 'Dashboard', navClass: 'dashboard', key: 'admin.navbar.dashboard', - defaultString: 'dashboard', - path: '' + // defaultString: 'dashboard', + path: '/' }, - blog: { + content: { name: 'Content', navClass: 'content', - key: 'admin.navbar.blog', - defaultString: 'blog', - path: '/blog' + key: 'admin.navbar.content', + // defaultString: 'content', + path: '/content/' }, add: { name: 'New Post', navClass: 'editor', key: 'admin.navbar.editor', - defaultString: 'editor', - path: '/editor' + // defaultString: 'editor', + path: '/editor/' }, settings: { name: 'Settings', navClass: 'settings', key: 'admin.navbar.settings', - defaultString: 'settings', - path: '/settings' + // defaultString: 'settings', + path: '/settings/' } }; + ghost.doFilter('messWithAdmin', adminNavbar, function() { + console.log('the dofilter hook called in /core/admin/controllers/index.js'); + }); + // TODO - make this a util or helper function setSelected(list, name) { _.each(list, function (item, key) { @@ -64,9 +68,9 @@ console.log('user found: ', user); req.session.user = "ghostadmin"; res.redirect(req.query.redirect || '/ghost/'); - }, function (err) { + }, function (error) { // Do something here to signal the reason for an error - console.log(err.stack); + req.flash('error', error.message); res.redirect('/ghost/login/'); }); }, @@ -78,16 +82,19 @@ }); }, 'doRegister': function (req, res) { - // console.log(req.body); - if (req.body.email_address !== '' && req.body.password.length > 5) { + var email = req.body.email_address, + password = req.body.password; + + if (email !== '' && password.length > 5) { api.users.add({ - email_address: req.body.email_address, - password: req.body.password + email_address: email, + password: password }).then(function (user) { console.log('user added', user); res.redirect('/ghost/login/'); }, function (error) { - console.log('there was an error', error); + req.flash('error', error.message); + res.redirect('/ghost/register/'); }); } else { req.flash('error', "The password is too short. Have at least 6 characters in there"); @@ -111,7 +118,7 @@ .then(function (post) { res.render('editor', { bodyClass: 'editor', - adminNav: setSelected(adminNavbar, 'blog'), + adminNav: setSelected(adminNavbar, 'content'), title: post.get('title'), content: post.get('content') }); @@ -123,12 +130,12 @@ }); } }, - 'blog': function (req, res) { + 'content': function (req, res) { api.posts.browse() .then(function (posts) { - res.render('blog', { + res.render('content', { bodyClass: 'manage', - adminNav: setSelected(adminNavbar, 'blog'), + adminNav: setSelected(adminNavbar, 'content'), posts: posts.toJSON() }); }); diff --git a/core/admin/views/blog.hbs b/core/admin/views/content.hbs similarity index 100% rename from core/admin/views/blog.hbs rename to core/admin/views/content.hbs diff --git a/core/admin/views/dashboard.hbs b/core/admin/views/dashboard.hbs index 71cf077760..43a1e2f563 100644 --- a/core/admin/views/dashboard.hbs +++ b/core/admin/views/dashboard.hbs @@ -4,9 +4,9 @@ $(document).ready(function(){ //$('body').click(function(){ - $('.time').fadeIn(1000); - $('.image').delay(300).fadeIn(1000); - $('.posts').delay(600).fadeIn(900, function(){ + $('.widget-time').fadeIn(1000); + $('.widget-image').delay(300).fadeIn(1000); + $('.widget-posts').delay(600).fadeIn(900, function(){ var ctx = $("#poststats").get(0).getContext("2d"); var data = [ @@ -32,32 +32,133 @@ }); - $('.stats').delay(800).fadeIn(1000); - $('.facebook').delay(1000).fadeIn(1000); - $('.gplus').delay(1200).fadeIn(1000); - $('.twitter').delay(1300).fadeIn(1000); - $('.campaignmonitor').delay(1400).fadeIn(1000); + $('.widget-stats').delay(800).fadeIn(1000); + $('.widget-facebook').delay(1000).fadeIn(1000); + $('.widget-gplus').delay(1200).fadeIn(1000); + $('.widget-twitter').delay(1300).fadeIn(1000); + $('.widget-campaignmonitor').delay(1400).fadeIn(1000); //}); - }); {{/contentFor}} {{!< default}} -
-
-
-
- - -
+
+
+
+ Today + 12° +
+ +
+
+ Linz, Austria +
+
+
+ +
+
+ +
+
+ Ghost +
+
-
-
-
-
-
\ No newline at end of file + +
+
+
+ +
    +
  • 9 Ready
  • +
  • 4 Pending
  • +
  • 1 Draft
  • +
+
+
+
+ Upcoming Posts +
+
+
+ +
+
+
+ 293,042 + +35% in the last 30 days +
+
+
+ Google Analytics Unique Visitors +
+
+
+ +
+
+
+ 9,392 + -39 likes today + +
+
+
+ Facebook +
+
+
+ +
+
+
+ 4,103 + have you in circles +
+
+
+ Google Plus +
+
+
+ +
+
+ Twitter Settings +
+
+
+ + + + +
+
+
+
+
+
+
+
+ 3,502 + +35 subscribers this week +
+
+
+ Campaign Monitor +
+
+
\ No newline at end of file diff --git a/core/admin/views/editor.hbs b/core/admin/views/editor.hbs index 0b972c6b01..ba5efc8884 100644 --- a/core/admin/views/editor.hbs +++ b/core/admin/views/editor.hbs @@ -4,6 +4,7 @@ + {{/contentFor}} diff --git a/core/admin/views/partials/navbar.hbs b/core/admin/views/partials/navbar.hbs index c22b5043e6..a4de3e9239 100644 --- a/core/admin/views/partials/navbar.hbs +++ b/core/admin/views/partials/navbar.hbs @@ -1,3 +1,4 @@ + \ No newline at end of file + diff --git a/core/frontend/controllers/index.js b/core/frontend/controllers/index.js index 69a96044d2..7e2def4a92 100644 --- a/core/frontend/controllers/index.js +++ b/core/frontend/controllers/index.js @@ -16,14 +16,14 @@ 'homepage': function (req, res) { api.posts.browse().then(function (posts) { ghost.doFilter('prePostsRender', posts.toJSON(), function (posts) { - res.render('index', {posts: posts, ghostGlobals: ghost.globalConfig}); + res.render('index', {posts: posts, ghostGlobals: ghost.globalConfig, navItems: res.locals.navItems}); }); }); }, 'single': function (req, res) { api.posts.read({'slug': req.params.slug}).then(function (post) { ghost.doFilter('prePostsRender', post.toJSON(), function (post) { - res.render('single', {post: post, ghostGlobals: ghost.globalConfig}); + res.render('single', {post: post, ghostGlobals: ghost.globalConfig, navItems: res.locals.navItems}); }); }); } diff --git a/core/frontend/filters/index.js b/core/frontend/filters/index.js new file mode 100644 index 0000000000..9768e41d6f --- /dev/null +++ b/core/frontend/filters/index.js @@ -0,0 +1,30 @@ +(function () { + "use strict"; + + var _ = require('underscore'), + coreFilters; + + coreFilters = function (ghost) { + ghost.registerFilter('ghostNavItems', function (args) { + var selectedItem; + + // Set the nav items based on the config + args.navItems = ghost.config().nav; + + // Mark the current selected Item + selectedItem = _.find(args.navItems, function (item) { + // TODO: Better selection determination? + return item.url === args.path; + }); + + if (selectedItem) { + selectedItem.active = true; + } + + return args; + }); + }; + + module.exports.loadCoreFilters = coreFilters; + +}()); \ No newline at end of file diff --git a/core/frontend/helpers/ghostNav.js b/core/frontend/helpers/ghostNav.js new file mode 100644 index 0000000000..f1ae8bd9a7 --- /dev/null +++ b/core/frontend/helpers/ghostNav.js @@ -0,0 +1,53 @@ +(function () { + "use strict"; + + var fs = require('fs'), + path = require('path'), + _ = require('underscore'), + handlebars = require('express-hbs').handlebars, + nodefn = require('when/node/function'), + GhostNavHelper; + + GhostNavHelper = function (navTemplate) { + // Bind the context for our methods. + _.bindAll(this, 'compileTemplate', 'renderNavItems'); + + if (_.isFunction(navTemplate)) { + this.navTemplateFunc = navTemplate; + } else { + this.navTemplatePath = navTemplate; + } + }; + + GhostNavHelper.prototype.compileTemplate = function (templatePath) { + var self = this; + + // Allow people to overwrite the navTemplatePath + templatePath = templatePath || this.navTemplatePath; + + return nodefn.call(fs.readFile, templatePath).then(function(navTemplateContents) { + // TODO: Can handlebars compile async? + self.navTemplateFunc = handlebars.compile(navTemplateContents.toString()); + }); + }; + + GhostNavHelper.prototype.renderNavItems = function (navItems) { + var output; + + output = this.navTemplateFunc({links: navItems}); + + return output; + }; + + // A static helper method for registering with ghost + GhostNavHelper.registerWithGhost = function(ghost) { + var templatePath = path.join(ghost.paths().frontendViews, 'nav.hbs'), + ghostNavHelper = new GhostNavHelper(templatePath); + + return ghostNavHelper.compileTemplate().then(function() { + ghost.registerThemeHelper("ghostNav", ghostNavHelper.renderNavItems); + }); + }; + + module.exports = GhostNavHelper; +}()); \ No newline at end of file diff --git a/core/frontend/helpers/index.js b/core/frontend/helpers/index.js index b0565110f9..51c2e5e1da 100644 --- a/core/frontend/helpers/index.js +++ b/core/frontend/helpers/index.js @@ -3,10 +3,11 @@ var _ = require('underscore'), moment = require('moment'), + when = require('when'), + navHelper = require('./ghostNav'), coreHelpers; coreHelpers = function (ghost) { - /** * [ description] * @todo ghost core helpers + a way for themes to register them @@ -39,6 +40,10 @@ return output; }); + return when.all([ + // Just one async helper for now, but could be more in the future + navHelper.registerWithGhost(ghost) + ]); }; diff --git a/core/frontend/views/nav.hbs b/core/frontend/views/nav.hbs new file mode 100644 index 0000000000..72b01d6977 --- /dev/null +++ b/core/frontend/views/nav.hbs @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/core/ghost.js b/core/ghost.js index ebb9553d0c..1042a05130 100644 --- a/core/ghost.js +++ b/core/ghost.js @@ -13,10 +13,10 @@ _ = require('underscore'), Polyglot = require('node-polyglot'), models = require('./shared/models'), + ExampleFilter = require('../content/plugins/exampleFilters'), Ghost, - instance, - filterCallbacks = {}, + instance, statuses; // ## Article Statuses @@ -40,10 +40,12 @@ */ Ghost = function () { var app, + plugin, polyglot; if (!instance) { instance = this; + plugin = new ExampleFilter(instance).init(); app = express(); @@ -60,19 +62,32 @@ dataProvider: models, statuses: function () { return statuses; }, polyglot: function () { return polyglot; }, + plugin: function() { return plugin; }, paths: function () { return { - 'activeTheme': __dirname + '/../content/' + config.themeDir + '/' + config.activeTheme + '/', - 'adminViews': __dirname + '/admin/views/', - 'lang': __dirname + '/lang/' + 'activeTheme': __dirname + '/../content/' + config.themeDir + '/' + config.activeTheme + '/', + 'adminViews': __dirname + '/admin/views/', + 'frontendViews': __dirname + '/frontend/views/', + 'lang': __dirname + '/lang/' }; } }); } - return instance; }; + /** + * Holds the filters + * @type {Array} + */ + Ghost.prototype.filterCallbacks = []; + + /** + * Holds the filter hooks (that are built in to Ghost Core) + * @type {Array} + */ + Ghost.prototype.filters = []; + /** * @param {string} name * @param {Function} fn @@ -105,11 +120,11 @@ * @param {Function} fn */ Ghost.prototype.registerFilter = function (name, fn) { - if (!filterCallbacks.hasOwnProperty(name)) { - filterCallbacks[name] = []; + if (!this.filterCallbacks.hasOwnProperty(name)) { + this.filterCallbacks[name] = []; } console.log('registering filter for ', name); - filterCallbacks[name].push(fn); + this.filterCallbacks[name].push(fn); }; /** @@ -121,14 +136,15 @@ Ghost.prototype.doFilter = function (name, args, callback) { var fn; - if (filterCallbacks.hasOwnProperty(name)) { - for (fn in filterCallbacks[name]) { - if (filterCallbacks[name].hasOwnProperty(fn)) { + if (this.filterCallbacks.hasOwnProperty(name)) { + for (fn in this.filterCallbacks[name]) { + if (this.filterCallbacks[name].hasOwnProperty(fn)) { console.log('doing filter for ', name); - args = filterCallbacks[name][fn](args); + args = this.filterCallbacks[name][fn](args); } } } + callback(args); }; diff --git a/core/shared/data/fixtures/001.js b/core/shared/data/fixtures/001.js index f2ecbd92d5..849f92a6ae 100644 --- a/core/shared/data/fixtures/001.js +++ b/core/shared/data/fixtures/001.js @@ -50,15 +50,16 @@ module.exports = { users: [ { - "id": "1", - "username": "johnonolan", + "id": "1", + "username": "johnonolan", "first_name": "John", "last_name": "O'Nolan", + "password": "$2a$10$.pb3wOEhbEPvArvOBB.iyuKslBjC7lSXCUzp29civDTvCg3M1j0XO", "email_address": "john@onolan.org", "profile_picture": "logo.png", "cover_picture": "", - "bio": "Interactive designer, public speaker, startup advisor and writer. Living in Austria, attempting world domination via keyboard.", - "url": "john.onolan.org", + "bio": "Interactive designer, public speaker, startup advisor and writer. Living in Austria, attempting world domination via keyboard.", + "url": "john.onolan.org", "created_by": 1, "updated_by": 1 } diff --git a/core/shared/errorHandling.js b/core/shared/errorHandling.js index f61dcaafc0..af678453ae 100644 --- a/core/shared/errorHandling.js +++ b/core/shared/errorHandling.js @@ -10,7 +10,7 @@ errors = { throwError: function (err) { if (!err) { - return; + err = new Error("An error occurred"); } if (_.isString(err)) { diff --git a/core/test/ghost/errorHandling_spec.js b/core/test/ghost/errorHandling_spec.js index 816739e106..b4dc03b5f3 100644 --- a/core/test/ghost/errorHandling_spec.js +++ b/core/test/ghost/errorHandling_spec.js @@ -31,6 +31,14 @@ runThrowError.should['throw']("test2"); }); + it("throws error even if nothing passed", function () { + var runThrowError = function () { + errors.throwError(); + }; + + runThrowError.should['throw']("An error occurred"); + }); + it("logs errors", function () { var err = new Error("test1"), logStub = sinon.stub(console, "log"); diff --git a/core/test/ghost/frontend_helpers_ghostNav_spec.js b/core/test/ghost/frontend_helpers_ghostNav_spec.js new file mode 100644 index 0000000000..bbf7177ec2 --- /dev/null +++ b/core/test/ghost/frontend_helpers_ghostNav_spec.js @@ -0,0 +1,69 @@ +/*globals describe, beforeEach, it*/ +(function () { + "use strict"; + + var should = require('should'), + sinon = require('sinon'), + _ = require('underscore'), + path = require('path'), + GhostNavHelper = require('../../frontend/helpers/ghostNav'); + + describe('ghostNav Helper', function () { + var navTemplatePath = path.join(process.cwd(), 'core/frontend/views/nav.hbs'); + + should.exist(GhostNavHelper, "GhostNavHelper exists"); + + it('can compile the nav template', function (done) { + var helper = new GhostNavHelper(navTemplatePath); + + helper.compileTemplate().then(function () { + should.exist(helper.navTemplateFunc); + _.isFunction(helper.navTemplateFunc).should.equal(true); + + done(); + }, done); + }); + + it('can render nav items', function () { + var helper = new GhostNavHelper(function (data) { return "rendered " + data.links.length; }), + templateSpy = sinon.spy(helper, 'navTemplateFunc'), + fakeNavItems = [{ + title: 'test1', + url: '/test1' + }, { + title: 'test2', + url: '/test2' + }], + rendered; + + rendered = helper.renderNavItems(fakeNavItems); + + // Returns a string returned from navTemplateFunc + should.exist(rendered); + rendered.should.equal("rendered 2"); + + templateSpy.calledWith({ links: fakeNavItems }).should.equal(true); + }); + + it('can register with ghost', function (done) { + var fakeGhost = { + paths: function () { + return { + frontendViews: path.join(process.cwd(), 'core/frontend/views/') + }; + }, + + registerThemeHelper: function () { + return; + } + }, + registerStub = sinon.stub(fakeGhost, 'registerThemeHelper'); + + GhostNavHelper.registerWithGhost(fakeGhost).then(function () { + registerStub.called.should.equal(true); + + done(); + }, done); + }); + }); +}()); \ No newline at end of file diff --git a/package.json b/package.json index 0cf04c5962..54a2841d17 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "start": "node app", - "test": "grunt validate" + "test": "grunt validate --verbose" }, "dependencies": { "express": "3.1.x", @@ -23,6 +23,7 @@ }, "devDependencies": { "grunt": "~0.4.1", + "grunt-cli": "0.1.9", "grunt-jslint": "0.2.x", "should": "~1.2.2", "grunt-mocha-test": "~0.4.0",