began setup wizard

This commit is contained in:
AJ ONeal 2015-01-11 04:04:30 -07:00
parent bac3b49985
commit 31cd11ad60
20 changed files with 1321 additions and 684 deletions

19
BUGS.md Normal file
View File

@ -0,0 +1,19 @@
BUGS
====
* index page /index/index.html
* rss feed missing
Usability
=========
* compile dev vs prod
* new posts
Feautres
========
* permalink url maker
* tags
* categories
* output to os.tmpdir (i.e. /tmp)

View File

@ -1,9 +1,11 @@
Desirae
=====
In development.
A blog platform built for Developers, but with normal people in mind.
Blog Platform. A Ruhoh knock-off written in JavaScript for the Browser
Desirae runs entirely in the browser, but needs a little help from Node.js for saving and retrieving files.
She can also be run from entirely headless from node.js.
Key Features
------------
@ -18,6 +20,51 @@ Node (optional) - if you'd prefer to go headless, you can.
The server is *very* minimal and could easily be implemented in any language (such as ruby or python).
Install and Usage
=================
If you're on OS X or Linux, it's as easy as pie to install and use Desirae.
```bash
git clone git@github.com:DearDesi/desirae.git
pushd desirae
# Downloads and installs node.js and a few other tools Desirae needs
bash setup.sh ./blog
```
After the initial installation you can launch Dear Desi, the Web-based configuration and build tool like so:
```
deardesi ./blog 65080
```
Or, if you prefer, you can build with `desirae` from the command line:
```
desirae build ./blog
desirae build-dev ./blog
```
Create a new Post
-----------------
```
desirae post "My First Post"
```
Configuration
=============
There are a few configuration files:
* `site.yml` is stuff that might be unique to your site, such as (title, url, adwords id, etc)
* `authors/<<your-handle.yml>>` contains information about you (name, handle, facebook, etc)
* `desirae.yml` contains directives that describe *how* the blog should be compiled - more technical stuff.
If any of these files change, the entire site needs to be retemplated.
Widgets
=======

6
app.js
View File

@ -4,8 +4,12 @@
angular.module('myApp', [
'ngRoute',
'myApp.about',
'myApp.authors',
'myApp.site',
'myApp.build',
'myApp.version'
'myApp.create',
'myApp.version',
'myApp.services'
]).
config(['$routeProvider', function ($routeProvider) {
$routeProvider.otherwise({redirectTo: '/about'});

View File

@ -41,7 +41,8 @@
"angular": "~1.3.8",
"angular-route": "~1.3.8",
"html5-boilerplate": "~4.3.0",
"bootstrap": "~3.3.1"
"bootstrap": "~3.3.1",
"md5": "~0.1.3"
},
"resolutions": {
"bluebird": "~2.6.2"

View File

@ -1,9 +1,62 @@
angular.module('myApp.services', []).
factory('MyService', function($http) {
var MyService = {};
$http.get('resources/data.json').success(function(response) {
MyService.data = response;
});
return MyService;
factory('Desirae', ['$q', function($q) {
var Desi = window.Desi || require('./deardesi').Desi
, desi = {}
, fsapi = window.fsapi
;
return {
reset: function () {
desi = {};
}
, meta: function () {
var d = $q.defer()
;
if (desi.meta) {
d.resolve(desi);
return d.promise;
}
Desi.init(desi).then(function () {
d.resolve(desi);
});
return d.promise;
}
, build: function (env) {
var d = $q.defer()
;
if (desi.built) {
d.resolve(desi);
return d.promise;
}
Desi.build(desi, env).then(function () {
d.resolve(desi);
});
return d.promise;
}
, write: function (env) {
var d = $q.defer()
;
if (desi.written) {
d.resolve(desi);
return d.promise;
}
Desi.write(desi, env).then(function () {
d.resolve(desi);
});
return d.promise;
}
, putFiles: function (files) {
return $q.when(fsapi.putFiles(files));
}
};
}]
);

View File

@ -1,8 +1,6 @@
;(function (exports) {
'use strict';
//require('require-yaml');
var PromiseA = exports.Promise || require('bluebird').Promise
, path = exports.path || require('path')
, Mustache = exports.Mustache || require('mustache')
@ -44,12 +42,14 @@
return str;
}
/*
function toLocaleDate(d) {
return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
+ ' '
+ (d.getHours() % 12) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds())
;
}
*/
function fromLocaleDate(str) {
// handles ISO and ISO-ish dates
@ -204,16 +204,144 @@
}
}
function runDesi(desi, env) {
var cache = desi.cache
//, config = desi.config
, cacheByPath = {}
, cacheBySha1 = {}
, dfiles
, dthemes
, droot
console.log('');
console.log('');
console.info('getting config, data, caches...');
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function Desi() {
}
// read config and such
Desi.init = function (desi) {
// config.yml, data.yml, site.yml, authors
return PromiseA.all([fsapi.getBlogdir(), fsapi.getAllConfigFiles()]).then(function (plop) {
var blogdir = plop[0]
, arr = plop[1]
;
console.info('loaded config, data, caches, partials');
console.log({
config: arr.config
, data: arr.data
, site: arr.site
});
desi.blogdir = blogdir;
desi.originals = {};
desi.copies = {};
Object.keys(arr).forEach(function (key) {
desi.originals[key] = arr[key];
desi.copies[key] = clone(arr[key]);
desi[key] = clone(arr[key]);
});
desi.config.rootdir = desi.config.rootdir || '_root';
var collectionnames = Object.keys(desi.config.collections)
, themenames = Object.keys(desi.config.themes)
.filter(function (k) { return 'default' !== k; })
//.map(function (n) { return path.join(n, 'layouts'); })
, assetnames = Object.keys(desi.config.assets)
;
// TODO make document configurability
return PromiseA.all([
fsapi.getMeta(
themenames
, { dotfiles: false
, extensions: ['md', 'markdown', 'htm', 'html', 'jade', 'css', 'js', 'yml']
}
)
, fsapi.getMeta(
[desi.config.rootdir]
, { dotfiles: false
, extensions: ['md', 'markdown', 'htm', 'html', 'jade']
}
)
, fsapi.getMeta(
collectionnames
, { dotfiles: false
, extensions: ['md', 'markdown', 'htm', 'html', 'jade']
}
)
, fsapi.getMeta(
assetnames
, { dotfiles: false
//, extensions: ['md', 'markdown', 'htm', 'html', 'jade', 'css', 'js', 'yml']
}
)
, fsapi.getCache()
]);
}).then(function (things) {
console.info('loaded theme meta, root meta, collection meta');
console.log({
theme: things[0]
, root: things[1]
, collection: things[2]
, asset: things[3]
, cache: things[4]
});
function noErrors(map) {
Object.keys(map).forEach(function (path) {
map[path] = map[path].filter(function (m) {
if (!m.error && m.size) {
return true;
}
if (!m.size) {
console.warn("Ignoring 0 byte file " + (m.path || m.name));
return false;
}
console.warn("Couldn't get stats for " + (m.path || m.name));
console.warn(m.error);
});
});
return map;
}
var themes = noErrors(things[0])
, root = noErrors(things[1])[desi.config.rootdir]
, collections = noErrors(things[2])
, assets = noErrors(things[3])
, cache = noErrors(things[4])
;
if (!themes[Object.keys(themes)[0]].length) {
console.error('Missing THEMES!');
throw new Error('It seems that your themes directory is missing');
}
if (!root.length) {
console.error('Missing ROOT!');
}
if (!collections[Object.keys(collections)[0]].length) {
console.error('Missing Collections!');
}
console.info('last update: ' + (cache && cache.lastUpdate && new Date(cache.lastUpdate) || 'never'));
desi.cache = cache;
desi.meta = {
themes: themes
, collections: collections
, root: root
, assets: assets
};
desi.assets = [];
return desi;
});
};
Desi.setEnv = function (desi, env) {
desi.urls = desi.config.urls = {};
desi.env = {};
if (-1 === ['development', 'staging'].indexOf(env) || !desi.config[env]) {
@ -225,6 +353,23 @@
desi.config.compiled_path = desi.config[env].compiled_path;
desi.urls[env + '_url'] = desi.config[env].url;
return PromiseA.resolve(desi);
};
Desi.getDirtyFiles = function (desi, env) {
var cache = desi.cache
//, config = desi.config
, cacheByPath = {}
, cacheBySha1 = {}
, dfiles
, dthemes
, droot
;
if (!desi.env) {
Desi.setEnv(desi, env);
}
cache.sources = cache.sources || [];
cache.sources.forEach(function (source) {
cacheByPath[source.path] = source;
@ -265,6 +410,7 @@
console.warn(o.error);
}
// TODO also retrieve from cache?
desi.content = {
root: arr[0].filter(noErrors)
, collections: arr[1].filter(noErrors)
@ -273,130 +419,9 @@
return desi;
});
}
console.log('');
console.log('');
console.info('getting config, data, caches...');
function Desi() {
}
Desi.init = function (desi) {
return PromiseA.all([fsapi.getConfig(), fsapi.getData(), fsapi.getCache(), fsapi.getPartials()]).then(function (arr) {
var config = arr[0]
, data = arr[1]
, cache = arr[2]
, partials = arr[3]
, collectionnames = Object.keys(config.collections)
, themenames = Object.keys(config.themes)
.filter(function (k) { return 'default' !== k; })
//.map(function (n) { return path.join(n, 'layouts'); })
, assetnames = Object.keys(config.assets)
;
console.info('loaded config, data, caches, partials');
console.log({
config: arr[0]
, data: arr[1]
, cache: arr[2]
, partials: arr[3]
});
console.info('last update: ' + (cache.lastUpdate && new Date(cache.lastUpdate) || 'never'));
// TODO make document configurability
config.rootdir = config.rootdir || '_root';
return PromiseA.all([
fsapi.getMeta(
themenames
, { dotfiles: false
, extensions: ['md', 'markdown', 'htm', 'html', 'jade', 'css', 'js', 'yml']
}
)
, fsapi.getMeta(
[config.rootdir]
, { dotfiles: false
, extensions: ['md', 'markdown', 'htm', 'html', 'jade']
}
)
, fsapi.getMeta(
collectionnames
, { dotfiles: false
, extensions: ['md', 'markdown', 'htm', 'html', 'jade']
}
)
, fsapi.getMeta(
assetnames
, { dotfiles: false
//, extensions: ['md', 'markdown', 'htm', 'html', 'jade', 'css', 'js', 'yml']
}
)
]).then(function (things) {
console.info('loaded theme meta, root meta, collection meta');
console.log({
theme: things[0]
, root: things[1]
, collection: things[2]
, asset: things[3]
});
function noErrors(map) {
Object.keys(map).forEach(function (path) {
map[path] = map[path].filter(function (m) {
if (!m.error && m.size) {
return true;
}
if (!m.size) {
console.warn("Ignoring 0 byte file " + (m.path || m.name));
return false;
}
console.warn("Couldn't get stats for " + (m.path || m.name));
console.warn(m.error);
});
});
return map;
}
var themes = noErrors(things[0])
, root = noErrors(things[1])[config.rootdir]
, collections = noErrors(things[2])
, assets = noErrors(things[3])
;
if (!themes[Object.keys(themes)[0]].length) {
console.error('Missing THEMES!');
throw new Error('It seems that your themes directory is missing');
}
if (!root.length) {
console.error('Missing ROOT!');
}
if (!collections[Object.keys(collections)[0]].length) {
console.error('Missing Collections!');
}
return {
config: config
, data: data
, cache: cache
, meta: {
themes: themes
, collections: collections
, root: root
, assets: assets
}
, partials: partials
};
});
});
};
Desi.runDesi = runDesi;
Desi.otherStuff = function (desi) {
Desi.copyAssets = function(desi) {
var files = {}
;
@ -427,11 +452,16 @@
}
return desi;
}) || PromiseA.resolve(desi)).then(runDesi).then(function (desi) {
}) || PromiseA.resolve(desi));
};
Desi.parseFrontmatter = function (desi) {
return readFrontmatter(desi.content.root.concat(desi.content.themes.concat(desi.content.collections))).then(function () {
return desi;
});
}).then(function (desi) {
};
Desi.getNav = function (desi) {
// TODO add missing metadata and resave file
desi.navigation = [];
@ -455,6 +485,10 @@
};
});
return PromiseA.resolve(desi);
};
Desi.normalizeYml = function (desi) {
desi.content.root.forEach(function (page) {
page.yml = page.yml || {};
// TODO make default layout configurable
@ -560,6 +594,17 @@
}
}
desi.content.root.forEach(normalizeFrontmatter);
// TODO process tags and categories and such
desi.content.collections.forEach(normalizeFrontmatter);
desi.content.root.forEach(normalizeContentEntity);
desi.content.collections.forEach(normalizeContentEntity);
return PromiseA.resolve(desi);
};
Desi.collate = function (desi/*, collectionname*/) {
function byDate(a, b) {
if (a.year > b.year) {
return -1;
@ -640,20 +685,16 @@
return { years: yearsArr };
}
desi.content.root.forEach(normalizeFrontmatter);
// TODO process tags and categories and such
desi.content.collections.forEach(normalizeFrontmatter);
desi.content.root.forEach(normalizeContentEntity);
desi.content.collections.forEach(normalizeContentEntity);
desi.content.collections.sort(byDate);
desi.collated = collate(desi.content.collections);
console.info('desi.collated');
console.info(desi.collated);
return desi;
}).then(function (desi) {
return PromiseA.resolve(desi);
};
Desi.build = function (desi) {
var compiled = []
;
@ -661,7 +702,6 @@
function compileScriptEntity(entity, i, arr) {
}
*/
desi.assets = [];
function compileThemeEntity(entity, i, arr) {
console.log("compiling " + (i + 1) + "/" + arr.length + " " + (entity.path || entity.name));
// TODO less / sass / etc
@ -798,6 +838,7 @@
});
}
function doStuff() {
console.info('[first] compiling theme assets');
desi.content.themes.filter(function (f) { return !/\blayouts\b/.test(f.path); }).forEach(compileThemeEntity);
@ -807,8 +848,17 @@
desi.content.collections.forEach(compileContentEntity);
desi.compiled = compiled;
return desi;
}).then(function (desi) {
return PromiseA.resolve(desi);
}
if (!desi.partials) {
return fsapi.getAllPartials().then(doStuff);
} else {
return doStuff();
}
};
Desi.save = function (desi) {
var compiled = desi.compiled.slice(0)
, batches = []
, now
@ -855,7 +905,6 @@
+ ((Date.now() - now) / 1000).toFixed(3) + 's'
);
});
});
};
exports.Desi = Desi.Desi = Desi;

View File

@ -38,8 +38,10 @@
<div id="navbar-main" ng-class="!navCollapsed &amp;&amp; 'in'" class="navbar-collapse collapse">
<ul style="padding-top: 9px;" class="nav navbar-nav">
<li><a href="#/build">Configure</a></li>
<li><a href="#/post">Create Post</a></li>
<li><a href="#/authors">Authors</a></li>
<li><a href="#/site">Site</a></li>
<li><a href="#/build">Build</a></li>
<li><a href="#/create">Create Post</a></li>
</ul>
</div>
</div>
@ -102,9 +104,14 @@
<!-- UX Using Angular, but not getting fancy -->
<script src="./bower_components/angular/angular.js"></script>
<script src="./bower_components/angular-route/angular-route.js"></script>
<script src="./bower_components/md5/build/md5.min.js"></script>
<script src="./app.js"></script>
<script src="./views/build/build.js"></script>
<script src="./views/about/about.js"></script>
<script src="./views/authors/authors.js"></script>
<script src="./views/site/site.js"></script>
<script src="./views/build/build.js"></script>
<script src="./views/create/create.js"></script>
<script src="components/desirae/desirae.js"></script>
<script src="components/version/version.js"></script>
<script src="components/version/version-directive.js"></script>
<script src="components/version/interpolate-filter.js"></script>

View File

@ -193,6 +193,8 @@
var extensions = ''
, dotfiles = ''
, contents = ''
, sha1sum = ''
;
if (Array.isArray(opts.extensions)) {
@ -201,10 +203,17 @@
if (opts.dotfiles) {
dotfiles = '&dotfiles=true';
}
if (opts.contents) {
contents = '&contents=true';
}
if (false === opts.sha1sum) {
sha1sum = '&sha1sum=false';
}
return request.post('/api/fs/walk?_method=GET' + dotfiles + extensions, {
return request.post('/api/fs/walk?_method=GET' + dotfiles + extensions + contents + sha1sum, {
dirs: collections
}).then(function (resp) {
console.log(collections);
return JSON.parse(resp);
});
};
@ -223,6 +232,102 @@
});
};
fsapi.getConfigs = function (confs) {
var opts = { extensions: ['yml', 'yaml', 'json'], dotfiles: false, contents: true, sha1sum: true }
;
return fsapi.getMeta(confs, opts).then(function (collections) {
var obj = {}
;
Object.keys(collections).forEach(function (key) {
var files = collections[key]
, keyname = key.replace(/\.(json|ya?ml|\/)$/i, '')
;
obj[keyname] = obj[keyname] || {};
files.forEach(function (file) {
var filename = file.name.replace(/\.(json|ya?ml)$/i, '')
, data = {}
;
if (/\.(ya?ml)$/i.test(file.name)) {
console.log();
try {
data = exports.YAML.parse(file.contents) || {};
if ("undefined" === obj[keyname][filename]) {
data = {};
}
} catch(e) {
console.error("Could not parse yaml for " + filename);
console.error(file);
console.error(e);
}
}
else if (/\.(json)$/i.test(file.name)) {
try {
data = JSON.parse(file.contents) || {};
} catch(e) {
console.error("Could not parse json for " + filename);
console.error(file);
console.error(e);
}
} else {
console.error("Not sure what to do with this one...");
console.error(file);
}
obj[keyname][filename] = data;
/*
if (!obj[keyname][filename]) {
obj[keyname][filename] = {};
}
Object.keys(data).forEach(function (key) {
obj[keyname][filename][key] = data[key];
});
*/
});
});
return obj;
});
};
fsapi.getAllPartials = function () {
return fsapi.getConfigs(['partials', 'partials.yml']).then(function (results) {
var partials = {}
;
Object.keys(results.partials).forEach(function (key) {
var partial = partials[key];
Object.keys(partial).forEach(function (prop) {
if (partials[prop]) {
console.warn('partial \'' + prop + '\' overwritten by ' + key);
}
partials[prop] = partial[prop];
});
});
return partials;
});
};
fsapi.getBlogdir = function () {
return request.get('/api/fs').then(function (resp) {
return JSON.parse(resp);
});
};
fsapi.getAllConfigFiles = function () {
return fsapi.getConfigs(['config.yml', 'site.yml', 'authors']).then(function (results) {
var authors = results.authors
, config = results.config.config
, site = results.site.site
;
return { config: config, authors: authors, site: site };
});
};
fsapi.getData = function () {
return request.get('/data.yml').then(function (resp) {
return exports.YAML.parse(resp);
@ -232,8 +337,10 @@
fsapi.getCache = function () {
return request.get('/cache.json').then(function (resp) {
return JSON.parse(resp);
}).catch(function () {
}).catch(function (/*e*/) {
return {};
}).then(function (obj) {
return obj;
});
};

View File

@ -10,6 +10,7 @@ var PromiseA = require('bluebird').Promise
, sha1sum = function (str) { return require('secret-utils').hashsum('sha1', str); }
, mkdirp = PromiseA.promisify(require('mkdirp'))
, fsExtra = PromiseA.promisifyAll(require('fs.extra'))
//, tmpdir = require('os').tmpdir()
;
function strip(prefix, pathname) {
@ -32,7 +33,7 @@ function walkDir(parent, sub, opts) {
return false;
}
if ('.' === name[0] && !opts.dotfiles) {
if (!opts.dotfiles && ('.' === name[0])) {
return false;
}

View File

@ -15,9 +15,11 @@ var connect = require('connect')
, getfs = require('./lib/fsapi').getfs
, putfs = require('./lib/fsapi').putfs
, config = require('./config.yml')
, blogdir = process.argv[2] || 'blog'
, port = process.argv[3] || '65080'
, path = require('path')
, blogdir = path.resolve(config.blogdir || __dirname)
//, config = require(path.join('./', blogdir, 'config.yml'))
;
@ -41,6 +43,13 @@ app
return;
}
if (!dirnames.every(function (dirname) {
return 'string' === typeof dirname;
})) {
res.json({ error: "malformed request: " + JSON.stringify(dirnames) });
return;
}
/*
if (req.query.excludes) {
opts.excludes = req.query.excludes.split(',');
@ -54,6 +63,12 @@ app
if ('true' === req.query.dotfiles) {
opts.dotfiles = true;
}
if ('false' === req.query.sha1sum) {
opts.sha1sum = false;
}
if ('true' === req.query.contents) {
opts.contents = true;
}
// TODO opts.contents?
walk.walkDirs(blogdir, dirnames, opts).then(function (stats) {
@ -125,18 +140,29 @@ app
});
})
.use('/api/fs', function (req, res, next) {
next();
.use('/api/fs', function (req, res) {
var pathname = path.resolve(blogdir)
;
res.json({
path: pathname
, name: path.basename(pathname)
, relativePath: path.dirname(pathname)
//, cwd: path.resolve()
//, patharg: blogdir
});
return;
})
.use('/api/fs/static', serveStatic('.'))
.use('/api/fs/static', serveStatic(blogdir))
.use(serveStatic('.'))
.use(serveStatic(blogdir))
.use(serveStatic('./'))
.use('/compiled_dev', serveStatic(path.join(blogdir, '/compiled_dev')))
// TODO
//.use(serveStatic(tmpdir))
;
module.exports = app;
require('http').createServer().on('request', app).listen(process.argv[2] || 65080, function () {
console.log('listening ' + (process.argv[2] || 65080));
require('http').createServer().on('request', app).listen(port, function () {
console.log('listening ' + port);
});

36
swatches.yml Normal file
View File

@ -0,0 +1,36 @@
bootstrap_cdn: //maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css
bootswatches:
- Cerulean
- Cosmo
- Cyborg
- Darkly
- Flatly
- Journal
- Lumen
- Paper
- Readable
- Sandstone
- Simplex
- Slate
- Spacelab
- Superhero
- United
- Yeti
bootswatches_2:
- Amelia
- Cerulean
- Cosmo
- Cyborg
- Flatly
- Journal
- Readable
- Simplex
- Slate
- Spacelab
- Superhero
- United
bootswatch_cdn: //maxcdn.bootstrapcdn.com/bootswatch/3.3.1/{{name}}/bootstrap.min.css
fontawesome_cdn: //maxcdn.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css
bootswatch_2_download: http://bootswatch.com/2/{{bootswatch}}/bootstrap.min.css
bootstrap_2_cdn://maxcdn.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css

View File

@ -1,30 +1,32 @@
<div ui-view="content" autocroll="false">
<div style="margin-bottom: 0;" class="jumbotron">
<div class="row">
<div class="container">
<div class="row">
<div class="col-xs-12">
<p>Dear Desi, ...</p><br/>
</div>
<div class="page-header">
<h1>Welcome to Dear Desi! <small ng-bind="'(v%VERSION%)' | interpolate"></small></h1>
</div>
<div style="text-align: center;" class="col-md-6"><img ng-src="http://dropsha.re/files/fly6+.8/desi-parker-2.jpg" style="border: 5px solid white; width: 260px; height: 260px;" class="img-circle"/>
<h1>Desirae</h1>
<h3>The in-browser static blog generator
<small ng-bind="'(v%VERSION%)' | interpolate"></small>
</h3>
</div>
<div style="text-align: left;" class="col-md-6">
<div>
<legend>
<h2><span>Features</span></h2>
</legend>
<div class="jumbotron">
<div class="row">
<div class="col-lg-12">
<div class="col-lg-7">
<p>Setup your new blog in just 5 minutes.</p>
</div>
<div class="col-lg-5">
</div>
</div>
<div class="row">
<div class="col-lg-7">
<br/>
<iframe width="560" height="315" src="//www.youtube.com/embed/YZzhIIJmlE0" frameborder="0" allowfullscreen></iframe>
<br/>
</div>
<div class="col-lg-5">
<h2>Dear Desi is...</h2>
<br/>
<ul>
<li>Builds in the Browser
<ul>
<li>The in-browser static blog generator</li>
<li>Write content in Markdown, Jade, or HTML</li>
<li>Mustache Templates</li>
</ul>
@ -34,12 +36,12 @@
<li>Dual Licensed Apache2 and MIT</li>
<li>No Ruby version Hell - it'll still work in 6 months! :-D</li>
</ul>
<h3>What are you waiting for?</h3>
<br/>
<a class="btn btn-primary btn-lg pull-right" href="/#authors">Get Started</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

233
views/authors/authors.html Normal file
View File

@ -0,0 +1,233 @@
<div class="container">
<div class="row">
<div class="page-header">
<div class="col-md-5 col-sm-6 col-xs-8">
<h1>Primary Author</h1>
<div class="row" ng-if="Authors.authors">
<div class="col-md-offset-1 col-md-8">
<div class="form-group">
<select
ng-options="author as author.filename for (handle, author) in Authors.authors"
class="form-control"
ng-model="Authors.selectedAuthor"
ng-change="Authors.selectAuthor()"
></select>
</div>
</div>
<br/>
</div>
</div>
<div class="col-md-5 col-sm-6 col-xs-4">
<br/>
<img style="height:75px;" ng-src="{{Authors.headshot}}" />
</div>
</div>
</div>
<form class="form-horizontal" name="newAuthors" ng-submit="Authors.upsert(Authors.selectedAuthor)">
<div class="row">
<div class="col-sm-8">
<small><span ng-bind="Authors.blogdir"
></span>/authors/<span ng-bind="Authors.selectedAuthor.handle"
></span><span ng-if="Authors.selectedAuthor.handle"
>.yml</span></small>
</div>
<div class="col-sm-4">
<button class="btn btn-success pull-right" type="submit" ng-disabled="Authors.dirty || !Authors.selectedAuthor.handle">Save &amp; Continue</button>
</div>
</div>
<div class="row">
<br/>
</div>
<div class="row">
<div class="col-lg-12">
<div class="well bs-component">
<fieldset>
<legend>Profile Basics</legend>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="inputAuthorName" class="col-lg-4 control-label">Name*</label>
<div class="col-lg-8">
<input ng-model="Authors.selectedAuthor.name"
required="required"
type="text"
class="form-control"
id="inputAuthorName"
name="inputAuthorName"
placeholder="i.e. John Doe">
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="inputAuthorNickname" class="col-lg-4 control-label">Handle*</label>
<div class="col-lg-8">
<input ng-model="Authors.selectedAuthor.handle"
required="required"
type="text"
class="form-control"
id="inputAuthorNickname"
name="inputAuthorNickname"
placeholder="i.e. johndoe">
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="inputAuthorEmail" class="col-lg-4 control-label">Email*</label>
<div class="col-lg-8">
<input ng-model="Authors.selectedAuthor.email" ng-change="Authors.updateHeadshotUrl()"
required="required"
type="email"
class="form-control"
id="inputAuthorEmail"
name="inputAuthorEmail"
placeholder="i.e. john.doe@gmail.com">
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="inputAuthorWebsite" class="col-lg-4 control-label">Website</label>
<div class="col-lg-8">
<input ng-model="Authors.selectedAuthor.website"
type="url"
class="form-control"
id="inputAuthorWebsite"
name="inputAuthorWebsite"
placeholder="i.e. http://johndoe.name"
>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="form-group">
<label for="inputAuthorBio" class="col-lg-2 control-label">Bio
<small>(<span ng-bind="Authors.selectedAuthor.bio.length || 0"></span>/140)</small></label>
<div class="col-lg-10">
<textarea ng-model="Authors.selectedAuthor.bio"
class="form-control" id="inputAuthorBio" placeholder="i.e. Brogrammatic Ninja-throwing Rockstar Badassian Wizard JavaScript Superstar. 3+ years experience as a jalapeno poppers brony. YOLO."></textarea>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="form-group">
<label for="inputAuthorHeadshot" class="col-lg-2 control-label">Headshot</label>
<div class="col-lg-10">
<input ng-model="Authors.selectedAuthor.headshot" ng-change="Authors.updateHeadshotUrl()"
type="text" class="form-control" id="inputAuthorHeadshot" placeholder="i.e. https://i.imgur.com/qqpxDmJ.jpg">
</div>
</div>
</div>
</div>
</fieldset>
</div>
</div>
<div class="col-lg-12">
<div class="well bs-component">
<fieldset>
<legend>Social</legend>
<div class="form-group">
<label for="inputAuthorTwitter" class="col-lg-2 control-label">Twitter</label>
<div class="col-lg-10">
<input ng-model="Authors.selectedAuthor.twitter"
type="text" class="form-control" id="inputAuthorTwitter" placeholder="i.e. @johndoe">
</div>
</div>
<div class="form-group">
<label for="inputAuthorFacebook" class="col-lg-2 control-label">Facebook URL</label>
<div class="col-lg-10">
<input ng-model="Authors.selectedAuthor.facebook"
type="text" class="form-control" id="inputAuthorFacebook" placeholder="i.e. facebook.com/johndoe">
</div>
</div>
<div class="form-group">
<label for="inputAuthorGooglePlus" class="col-lg-2 control-label">Google+ URL</label>
<div class="col-lg-10">
<input ng-model="Authors.selectedAuthor.googleplus"
type="text" class="form-control" id="inputAuthorGooglePlus" placeholder="i.e. plus.google.com/+johndoe">
</div>
</div>
</fieldset>
</div>
</div>
<div class="col-lg-12">
<div class="well bs-component">
<fieldset>
<legend>Developers</legend>
<div class="form-group">
<label for="inputAuthorGithub" class="col-lg-2 control-label">Github</label>
<div class="col-lg-10">
<input ng-model="Authors.selectedAuthor.github"
type="text" class="form-control" id="inputAuthorGithub" placeholder="i.e. johndoe">
</div>
</div>
<div class="form-group">
<label for="inputAuthorStackOverflow" class="col-lg-2 control-label">StackOverflow</label>
<div class="col-lg-10">
<input ng-model="Authors.selectedAuthor.stackoverflow"
type="text" class="form-control" id="inputAuthorStackOverflow" placeholder="i.e. http://stackoverflow.com/users/151312/johndoe">
</div>
</div>
</fieldset>
</div>
</div>
<div class="col-lg-12">
<div class="well bs-component">
<fieldset>
<legend>Feeds</legend>
<div class="form-group">
<label for="inputAuthorFeedburner" class="col-lg-2 control-label">Feedburner</label>
<div class="col-lg-10">
<input ng-model="Authors.selectedAuthor.feedburner"
type="text" class="form-control" id="inputAuthorFeedburner" placeholder="i.e. johndoe">
</div>
</div>
</fieldset>
</div>
</div>
<button class="btn btn-primary pull-right" type="submit">Save &amp; Continue</button>
</div>
</form>
</div>
<!--
Instagram
Etsy
<div class="form-group">
<label for="inputAuthorPinterest" class="col-lg-2 control-label">Pinterest</label>
<div class="col-lg-10">
<input ng-model="Authors.selectedAuthor.pinterest"
type="text" class="form-control" id="inputAuthorPinterest" placeholder="i.e. @johndoe">
</div>
</div>
-->

119
views/authors/authors.js Normal file
View File

@ -0,0 +1,119 @@
'use strict';
angular.module('myApp.authors', ['ngRoute'])
.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/authors', {
templateUrl: 'views/authors/authors.html',
controller: 'AuthorsCtrl as Authors'
});
}])
.controller('AuthorsCtrl'
, ['$scope', '$timeout', '$location', 'Desirae'
, function($scope, $timeout, $location, Desirae) {
var scope = this
;
scope.newAuthor = function () {
console.log('new author');
scope.new = { filename: 'new' };
scope.selectAuthor(scope.new);
};
scope.selectAuthor = function (author) {
// TODO watch any change
scope.selectedAuthor = author || scope.selectedAuthor;
scope.updateHeadshotUrlNow();
};
scope.upsert = function () {
var author = scope.selectedAuthor
, files = []
, filename = author.filename
;
delete author.filename;
if ('new' !== filename && filename !== author.handle) {
files.push({ path: 'authors/' + filename + '.yml', contents: '', delete: true });
}
files.push({ path: 'authors/' + author.handle + '.yml', contents: window.jsyaml.dump(author) });
console.log(files);
Desirae.putFiles(files).then(function (results) {
console.log('updated author', results);
$location.path('/site');
}).catch(function (e) {
author.filename = filename;
console.error(e);
window.alert("Error Nation! :/");
throw e;
});
};
scope.updateHeadshotUrlNow = function () {
var gravatar = 'http://www.gravatar.com/avatar/' + window.md5((scope.selectedAuthor.email||'foo').toLowerCase()) + '?d=identicon'
;
if (scope.selectedAuthor.headshot) {
scope.headshot = scope.selectedAuthor.headshot;
}
else if (scope.selectedAuthor.email) {
scope.headshot = gravatar;
}
else {
scope.headshot = 'http://www.gravatar.com/avatar/' + window.md5((scope.selectedAuthor.email||'foo').toLowerCase()) + '?d=mm';
}
};
scope.updateHeadshotUrl = function () {
$timeout.cancel(scope.hslock);
scope.hslock = $timeout(function () {
scope.updateHeadshotUrlNow();
}, 300);
};
function init() {
scope.newAuthor();
console.log('desi loading');
Desirae.meta().then(function (desi) {
var filename
;
scope.blogdir = desi.blogdir.path.replace(/^\/(Users|home)\/[^\/]+\//, '~/');
desi.authors = desi.authors || {};
desi.authors.new = scope.new;
scope.authors = desi.authors;
Object.keys(desi.authors).forEach(function (filename) {
if ('new' === filename) {
return;
}
desi.authors[filename].filename = filename;
desi.authors[filename].handle = desi.authors[filename].handle || filename;
});
filename = Object.keys(desi.authors)[0];
scope.selectedAuthor = desi.authors[filename];
scope.updateHeadshotUrlNow();
}).catch(function (e) {
window.alert("An Error Occured. Most errors that occur in the init phase are parse errors in the config files or permissions errors on files or directories, but check the error console for details.");
console.error(e);
throw e;
});
}
init();
/*
$scope.$watch(angular.bind(this, function () { return this.selectedAuthor; }), function (newValue, oldValue) {
//$scope.$watch('Authors.selecteAuthor', function (newValue, oldValue)
console.log(newValue, oldValue);
if(newValue !== oldValue) {
scope.dirty = true;
}
}, true);
*/
}]);

0
views/build/build.html Normal file
View File

View File

@ -10,20 +10,4 @@ angular.module('myApp.build', ['ngRoute'])
}])
.controller('BuildCtrl', [function() {
var Desi = window.Desi || require('./deardesi').Desi
, scope = this
, desi = {}
;
Desi.init(desi).then(function () {
scope.run = function () {
return Desi.runDesi(desi).then(function () { Desi.otherStuff(); })
.catch(function (e) {
console.error('A great and uncatchable error has befallen the land. Read ye here for das detalles..');
console.error(e.message);
console.error(e);
throw e;
});
};
});
}]);

View File

@ -1,29 +0,0 @@
'use strict';
angular.module('myApp.configure', ['ngRoute'])
.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/configure', {
templateUrl: 'views/configure/configure.html',
controller: 'ConfigureCtrl as Configure'
});
}])
.controller('ConfigureCtrl', [function() {
var Desi = window.Desi || require('./deardesi').Desi
, scope = this
, desi = {}
;
Desi.init(desi).then(function () {
scope.run = function () {
return Desi.runDesi(desi).then(function () { Desi.otherStuff(); })
.catch(function (e) {
console.error('A great and uncatchable error has befallen the land. Read ye here for das detalles..');
console.error(e.message);
console.error(e);
throw e;
});
};
});
}]);

View File

@ -2,6 +2,7 @@
<div class="row">
<div class="page-header">
<h1>Blog Configuration</h1>
<h3><span ng-bind="Site.blogdir"></span></h3>
</div>
</div>
@ -15,42 +16,14 @@
<div class="form-group">
<label for="inputBlogTitle" class="col-lg-2 control-label">Title</label>
<div class="col-lg-10">
<input type="text" class="form-control" id="inputBlogTitle" placeholder="My Awesome Blog">
<input type="text" class="form-control" id="inputBlogTitle" placeholder="My Awesome Blog" ng-model="Site.title">
</div>
</div>
<div class="form-group">
<label for="inputBlogTagline" class="col-lg-2 control-label">Tagline</label>
<div class="col-lg-10">
<input type="text" class="form-control" id="inputBlogTagline" placeholder="For try-hard ethical master cleanse, 3 wolf moon Tumblr, disrupt lo-fi, narwhals and kale chips. YOLO.">
</div>
</div>
<div class="form-group">
<label for="inputBlogRoot" class="col-lg-2 control-label">Blog Root</label>
<div class="col-lg-10">
<input type="text" class="form-control" id="inputBlogRoot" disabled placeholder="./blog">
</div>
</div>
<div class="form-group">
<label for="inputBlogTheme" class="col-lg-2 control-label">Default Theme</label>
<div class="col-lg-10">
<select class="form-control" id="inputBlogTheme"
ng-options="item as item for item in ['twitter', 'sunburst']"
ng-model="themes.default"></select>
</div>
</div>
<div class="form-group">
<label for="inputBlogNav" class="col-lg-2 control-label">Navigation</label>
<div class="col-lg-10">
<div ng-repeat="nav in ['index', 'portfolio', 'archive']" class="checkbox">
<label>
<input type="checkbox" ng-model="nav.selected"> <span ng-bind="nav"></span>
</label>
</div>
<!--input type="text" class="form-control" id="inputBlogNav" disabled placeholder=""-->
<input type="text" class="form-control" id="inputBlogTagline" placeholder="For try-hard ethical master cleanse, 3 wolf moon Tumblr, disrupt lo-fi, narwhals and kale chips. YOLO." ng-model="Site.tagline">
</div>
</div>
@ -60,20 +33,24 @@
</div>
<div class="row">
<div class="col-lg-6">
<div class="col-lg-12">
<div class="well bs-component">
<fieldset>
<legend>Production</legend>
<div class="form-group">
<label for="inputProdHost" class="col-lg-3 control-label">Host</label>
<label for="inputProdHost" class="col-lg-3 control-label">Base URL</label>
<div class="col-lg-9">
<input type="text" class="form-control" id="inputProdHost" placeholder="http://dear.desi">
<input ng-model="Site.base_url"
placeholder="i.e. https://example.com in https://example.com/myblog"
type="text" class="form-control" id="inputProdHost">
</div>
</div>
<div class="form-group">
<label for="inputProdBase" class="col-lg-3 control-label">Base Path</label>
<div class="col-lg-9">
<input type="text" class="form-control" id="inputProdBase" placeholder="/blog">
<input ng-model="Site.base_path"
placeholder="i.e. /blog in https://example.com/blog"
type="text" class="form-control" id="inputProdBase">
</div>
</div>
<div class="form-group">
@ -86,31 +63,6 @@
</div>
</div>
<div class="col-lg-6">
<div class="well bs-component">
<fieldset>
<legend>Development</legend>
<div class="form-group">
<label for="inputDevHost" class="col-lg-3 control-label">Host</label>
<div class="col-lg-9">
<input type="text" class="form-control" id="inputDevHost" disabled placeholder="http://local.dear.desi:8080">
</div>
</div>
<div class="form-group">
<label for="inputDevBase" class="col-lg-3 control-label">Base Path</label>
<div class="col-lg-9">
<input type="text" class="form-control" id="inputDevBase" disabled placeholder="/compiled_dev">
</div>
</div>
<div class="form-group">
<label for="inputDevOutput" class="col-lg-3 control-label">Output Path</label>
<div class="col-lg-9">
<input type="text" class="form-control" id="inputDevOutput" disabled placeholder="./compiled_dev">
</div>
</div>
</fieldset>
</div>
</div>
</div>

26
views/site/site.js Normal file
View File

@ -0,0 +1,26 @@
'use strict';
angular.module('myApp.site', ['ngRoute'])
.config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/site', {
templateUrl: 'views/site/site.html',
controller: 'SiteCtrl as Site'
});
}])
.controller('SiteCtrl', ['$scope', 'Desirae', function($scope, Desirae) {
var scope = this
;
console.log('desi loading');
Desirae.meta().then(function (desi) {
console.log('desi loaded');
console.log(desi);
scope.blogdir = desi.blogdir.path.replace(/^\/(Users|home)\/[^\/]+\//, '~/');
}).catch(function (e) {
window.alert("An Error Occured. Most errors that occur in the init phase are parse errors in the config files or permissions errors on files or directories, but check the error console for details.");
console.error(e);
throw e;
});
}]);