Initial commit
This commit is contained in:
commit
44c090b5fd
20 changed files with 663 additions and 0 deletions
3
.bowerrc
Normal file
3
.bowerrc
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"directory": "src/bower_modules"
|
||||||
|
}
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules/
|
||||||
|
bower_modules/
|
||||||
|
|
||||||
|
# Don't track build output
|
||||||
|
dist/
|
||||||
|
dev/
|
17
bower.json
Normal file
17
bower.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "laine",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"components-bootstrap": "~3.1.1",
|
||||||
|
"crossroads": "~0.12.0",
|
||||||
|
"hasher": "~1.2.0",
|
||||||
|
"requirejs": "~2.1.11",
|
||||||
|
"requirejs-text": "~2.0.10",
|
||||||
|
"knockout": "~3.2.0",
|
||||||
|
"knockout-projections": "~1.1.0-pre",
|
||||||
|
"lodash": "~2.4.1",
|
||||||
|
"moment": "~2.8.3",
|
||||||
|
"marked": "~0.3.2"
|
||||||
|
}
|
||||||
|
}
|
98
gulpfile.js
Normal file
98
gulpfile.js
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
// Node modules
|
||||||
|
var fs = require('fs'), vm = require('vm'), merge = require('deeply'), chalk = require('chalk'), es = require('event-stream');
|
||||||
|
|
||||||
|
// Gulp and plugins
|
||||||
|
var gulp = require('gulp'), rjs = require('gulp-requirejs-bundler'), concat = require('gulp-concat'), clean = require('gulp-clean'),
|
||||||
|
replace = require('gulp-replace'), uglify = require('gulp-uglify'), htmlreplace = require('gulp-html-replace');
|
||||||
|
|
||||||
|
// Gulp minify for smallinizing our CSS
|
||||||
|
var minify = require('gulp-minify-css');
|
||||||
|
|
||||||
|
// Gulp filesize for printing sizes before and after minification
|
||||||
|
var size = require('gulp-size');
|
||||||
|
|
||||||
|
// Config
|
||||||
|
var requireJsRuntimeConfig = vm.runInNewContext(fs.readFileSync('src/app/require.config.js') + '; require;');
|
||||||
|
requireJsOptimizerConfig = merge(requireJsRuntimeConfig, {
|
||||||
|
out: 'scripts.js',
|
||||||
|
baseUrl: './src',
|
||||||
|
name: 'app/startup',
|
||||||
|
paths: {
|
||||||
|
requireLib: 'bower_modules/requirejs/require',
|
||||||
|
lodashLib: 'bower_modules/lodash/dist/lodash',
|
||||||
|
momentLib: 'bower_modules/moment/moment'
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
'requireLib',
|
||||||
|
'lodashLib',
|
||||||
|
'momentLib',
|
||||||
|
'components/nav-bar/nav-bar',
|
||||||
|
'components/home-page/home',
|
||||||
|
'components/db/db',
|
||||||
|
'components/transfer/transfer',
|
||||||
|
'components/single-post/single-post',
|
||||||
|
'components/pagination/pagination'
|
||||||
|
],
|
||||||
|
insertRequire: ['app/startup'],
|
||||||
|
bundles: {
|
||||||
|
// If you want parts of the site to load on demand, remove them from the 'include' list
|
||||||
|
// above, and group them into bundles here.
|
||||||
|
// 'bundle-name': [ 'some/module', 'another/module' ],
|
||||||
|
// 'another-bundle-name': [ 'yet-another-module' ]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Discovers all AMD dependencies, concatenates together all required .js files, minifies dist files
|
||||||
|
gulp.task('js', function() {
|
||||||
|
return rjs(requireJsOptimizerConfig)
|
||||||
|
.pipe(size({title: 'Original JS'}))
|
||||||
|
.pipe(gulp.dest('./dev/'))
|
||||||
|
.pipe(uglify({ preserveComments: false }))
|
||||||
|
.pipe(size({title: 'Minified JS'}))
|
||||||
|
.pipe(gulp.dest('./dist/'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Concatenates CSS files, rewrites relative paths to Bootstrap fonts, copies Bootstrap fonts
|
||||||
|
gulp.task('css', function() {
|
||||||
|
var bowerCss = gulp.src('src/bower_modules/components-bootstrap/css/bootstrap.min.css')
|
||||||
|
.pipe(replace(/url\((')?\.\.\/fonts\//g, 'url($1fonts/')),
|
||||||
|
appCss = gulp.src('src/css/*.css');
|
||||||
|
|
||||||
|
return es.concat(bowerCss, appCss)
|
||||||
|
.pipe(concat('css.css'))
|
||||||
|
.pipe(size({title: 'Original CSS'}))
|
||||||
|
.pipe(gulp.dest('./dev/'))
|
||||||
|
.pipe(minify())
|
||||||
|
.pipe(size({title: 'Minified CSS'}))
|
||||||
|
.pipe(gulp.dest('./dist/'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copies fonts
|
||||||
|
gulp.task('fonts', function() {
|
||||||
|
return gulp.src('./src/bower_modules/components-bootstrap/fonts/*', { base: './src/bower_modules/components-bootstrap/' })
|
||||||
|
.pipe(gulp.dest('./dev/'))
|
||||||
|
.pipe(gulp.dest('./dist/'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copies index.html, replacing <script> and <link> tags to reference production URLs
|
||||||
|
gulp.task('html', function() {
|
||||||
|
return gulp.src('./src/index.html')
|
||||||
|
.pipe(htmlreplace({
|
||||||
|
'css': 'css.css',
|
||||||
|
'js': 'scripts.js'
|
||||||
|
}))
|
||||||
|
.pipe(gulp.dest('./dist/'))
|
||||||
|
.pipe(gulp.dest('./dev/'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Removes all files from ./dist/
|
||||||
|
gulp.task('clean', function() {
|
||||||
|
return gulp.src('./dist/**/*', { read: false })
|
||||||
|
.pipe(clean());
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('default', ['html', 'js', 'css', 'fonts'], function(callback) {
|
||||||
|
callback();
|
||||||
|
console.log('\nPlaced optimized files in ' + chalk.magenta('dist/')
|
||||||
|
+ ' and dev files in ' + chalk.magenta('dev/\n'));
|
||||||
|
});
|
18
package.json
Normal file
18
package.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "laine",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"chalk": "~0.4.0",
|
||||||
|
"deeply": "~0.1.0",
|
||||||
|
"event-stream": "~3.1.0",
|
||||||
|
"gulp": "^3.8.9",
|
||||||
|
"gulp-clean": "~0.2.4",
|
||||||
|
"gulp-concat": "~2.2.0",
|
||||||
|
"gulp-html-replace": "~1.0.0",
|
||||||
|
"gulp-replace": "~0.2.0",
|
||||||
|
"gulp-requirejs-bundler": "^0.1.1",
|
||||||
|
"gulp-uglify": "~0.2.1",
|
||||||
|
"gulp-size": "~1.1.0",
|
||||||
|
"gulp-minify-css": "~0.3.11"
|
||||||
|
}
|
||||||
|
}
|
18
src/app/require.config.js
Normal file
18
src/app/require.config.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// require.js looks for the following global when initializing
|
||||||
|
var require = {
|
||||||
|
baseUrl: ".",
|
||||||
|
paths: {
|
||||||
|
"bootstrap": "bower_modules/components-bootstrap/js/bootstrap.min",
|
||||||
|
"crossroads": "bower_modules/crossroads/dist/crossroads.min",
|
||||||
|
"hasher": "bower_modules/hasher/dist/js/hasher.min",
|
||||||
|
"jquery": "bower_modules/jquery/dist/jquery",
|
||||||
|
"knockout": "bower_modules/knockout/dist/knockout",
|
||||||
|
"knockout-projections": "bower_modules/knockout-projections/dist/knockout-projections",
|
||||||
|
"signals": "bower_modules/js-signals/dist/signals.min",
|
||||||
|
"text": "bower_modules/requirejs-text/text",
|
||||||
|
"marked": "bower_modules/marked/lib/marked"
|
||||||
|
},
|
||||||
|
shim: {
|
||||||
|
"bootstrap": { deps: ["jquery"] }
|
||||||
|
}
|
||||||
|
};
|
37
src/app/router.js
Normal file
37
src/app/router.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
define(["knockout", "crossroads", "hasher"], function(ko, crossroads, hasher) {
|
||||||
|
|
||||||
|
// This module configures crossroads.js, a routing library. If you prefer, you
|
||||||
|
// can use any other routing library (or none at all) as Knockout is designed to
|
||||||
|
// compose cleanly with external libraries.
|
||||||
|
//
|
||||||
|
// You *don't* have to follow the pattern established here (each route entry
|
||||||
|
// specifies a 'page', which is a Knockout component) - there's nothing built into
|
||||||
|
// Knockout that requires or even knows about this technique. It's just one of
|
||||||
|
// many possible ways of setting up client-side routes.
|
||||||
|
|
||||||
|
return new Router({
|
||||||
|
routes: [
|
||||||
|
{ url: '', params: { page: 'home-page' } }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
function Router(config) {
|
||||||
|
var currentRoute = this.currentRoute = ko.observable({});
|
||||||
|
|
||||||
|
ko.utils.arrayForEach(config.routes, function(route) {
|
||||||
|
crossroads.addRoute(route.url, function(requestParams) {
|
||||||
|
currentRoute(ko.utils.extend(requestParams, route.params));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
activateCrossroads();
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateCrossroads() {
|
||||||
|
function parseHash(newHash, oldHash) { crossroads.parse(newHash); }
|
||||||
|
crossroads.normalizeFn = crossroads.NORM_AS_OBJECT;
|
||||||
|
hasher.initialized.add(parseHash);
|
||||||
|
hasher.changed.add(parseHash);
|
||||||
|
hasher.init();
|
||||||
|
}
|
||||||
|
});
|
30
src/app/startup.js
Normal file
30
src/app/startup.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
define(['jquery', 'knockout', './router', 'marked', 'bootstrap', 'knockout-projections'],
|
||||||
|
function($, ko, router, marked) {
|
||||||
|
|
||||||
|
// Components can be packaged as AMD modules, such as the following:
|
||||||
|
ko.components.register('nav-bar', { require: 'components/nav-bar/nav-bar' });
|
||||||
|
ko.components.register('home-page', { require: 'components/home-page/home' });
|
||||||
|
|
||||||
|
// ... or for template-only components, you can just point to a .html file directly:
|
||||||
|
ko.components.register('about-page', {
|
||||||
|
template: { require: 'text!components/about-page/about.html' }
|
||||||
|
});
|
||||||
|
|
||||||
|
ko.components.register('db', { require: 'components/db/db' });
|
||||||
|
|
||||||
|
ko.components.register('transfer', { require: 'components/transfer/transfer' });
|
||||||
|
|
||||||
|
ko.components.register('single-post', { require: 'components/single-post/single-post' });
|
||||||
|
|
||||||
|
ko.components.register('pagination', { require: 'components/pagination/pagination' });
|
||||||
|
|
||||||
|
// [Scaffolded component registrations will be inserted here. To retain this feature, don't remove this comment.]
|
||||||
|
|
||||||
|
// Set Markdown parser options
|
||||||
|
marked.setOptions({
|
||||||
|
smartypants: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the application
|
||||||
|
ko.applyBindings({ route: router.currentRoute });
|
||||||
|
});
|
223
src/components/db/db.js
Normal file
223
src/components/db/db.js
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
define(['knockout', '../transfer/transfer', 'moment'], function(ko, transfer, moment) {
|
||||||
|
|
||||||
|
function DB(params) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// List of all posts
|
||||||
|
self.posts = ko.observableArray();
|
||||||
|
// List of all pages
|
||||||
|
self.pages = ko.observableArray();
|
||||||
|
|
||||||
|
// Below are a few pure computeds for convenience, they should be autobuilt
|
||||||
|
// when the posts / pages are loaded
|
||||||
|
|
||||||
|
// All posts in a dict (slug as key) for faster retrieval of single post
|
||||||
|
self.postDict = ko.pureComputed(function() {
|
||||||
|
var ret = {};
|
||||||
|
_.forEach(self.posts(), function(post) {
|
||||||
|
ret[post().slug()] = post;
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dict of pages (slug as key)
|
||||||
|
self.pageDict = ko.pureComputed(function() {
|
||||||
|
var ret = {};
|
||||||
|
_.forEach(self.pages(), function(page) {
|
||||||
|
ret[page().slug()] = page;
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dict of tags (tag name as key) and the posts they have
|
||||||
|
self.tags = ko.pureComputed(function() {
|
||||||
|
var ret = {};
|
||||||
|
_.forEach(self.posts(), function(post) {
|
||||||
|
_.forEach(post().tags(), function(tag) {
|
||||||
|
if (!(tag in ret)) {
|
||||||
|
ret[tag] = ko.observableArray(post);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ret[tag].push(post);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dict of years (year number as key) and the posts for those years
|
||||||
|
self.years = ko.pureComputed(function() {
|
||||||
|
var ret = {};
|
||||||
|
_.forEach(self.posts(), function(post) {
|
||||||
|
var key = post().date().format('YYYY');
|
||||||
|
|
||||||
|
if (!(key in ret)) {
|
||||||
|
ret[key] = ko.observableArray(post);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ret[key].push(post);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Dict of months ('YYYY-MM' as key) and the posts for those months
|
||||||
|
self.months = ko.pureComputed(function() {
|
||||||
|
var ret = {};
|
||||||
|
_.forEach(self.posts(), function(post) {
|
||||||
|
var key = post().date().format('YYYY-MM');
|
||||||
|
|
||||||
|
if (!(key in ret)) {
|
||||||
|
ret[key] = ko.observableArray(post);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ret[key].push(post);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a line of tags separated by commas and return the tags in a list.
|
||||||
|
*/
|
||||||
|
self.parseTagLine = function(tagStr) {
|
||||||
|
var tags = tagStr.split(',');
|
||||||
|
var retTags = [];
|
||||||
|
_.forEach(tags, function(tag) {
|
||||||
|
retTags.push(tag.trim());
|
||||||
|
});
|
||||||
|
return retTags;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse data of a single post from a regex match, returning and observable
|
||||||
|
*/
|
||||||
|
self.parsePostData = function(matched) {
|
||||||
|
var tags = {};
|
||||||
|
|
||||||
|
if (!_.isUndefined(matched[3])) {
|
||||||
|
tags = self.parseTagLine(matched[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var date = moment(matched[1], 'YYYY-MM-DD');
|
||||||
|
var slug = matched[2];
|
||||||
|
|
||||||
|
var post = ko.observable({
|
||||||
|
slug: ko.observable(slug),
|
||||||
|
date: ko.observable(date),
|
||||||
|
tags: ko.observable(tags),
|
||||||
|
|
||||||
|
// True if this posts title and content have been fetched from the
|
||||||
|
// individual post file
|
||||||
|
synced: ko.observable(false),
|
||||||
|
|
||||||
|
// These will be parsed later from the individual post file
|
||||||
|
title: ko.observable(null),
|
||||||
|
content: ko.observable(null),
|
||||||
|
isSplit: ko.observable(null),
|
||||||
|
shortContent: ko.observable(null)
|
||||||
|
});
|
||||||
|
return post;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse data of a single page from a regex match, returning and observable
|
||||||
|
*/
|
||||||
|
self.parsePageData = function(matched) {
|
||||||
|
var slug = matched[1];
|
||||||
|
|
||||||
|
var page = ko.observable({
|
||||||
|
slug: slug,
|
||||||
|
linkText: matched[2],
|
||||||
|
|
||||||
|
synced: false,
|
||||||
|
title: null,
|
||||||
|
isSplit: false
|
||||||
|
});
|
||||||
|
return page;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the index file, putting its contents into the given
|
||||||
|
* observableArrays
|
||||||
|
*/
|
||||||
|
self.parseIndex = function(posts, pages, indexStr) {
|
||||||
|
var post_data_regex = /^(\d{4}-\d{2}-\d{2}) ([a-z0-9\-]+)(\s([^,]+?,?)*)?$/;
|
||||||
|
var page_data_regex = /^([a-z0-9\-]+) (.*)$/;
|
||||||
|
|
||||||
|
var lines = indexStr.split('\n');
|
||||||
|
var line_no = 1;
|
||||||
|
|
||||||
|
_.forEach(lines, function(line) {
|
||||||
|
var post_match = post_data_regex.exec(line);
|
||||||
|
var page_match = page_data_regex.exec(line);
|
||||||
|
|
||||||
|
if (post_match !== null) {
|
||||||
|
posts.push(self.parsePostData(post_match));
|
||||||
|
}
|
||||||
|
else if (page_match !== null) {
|
||||||
|
pages.push(self.parsePageData(page_match));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('Ignoring unmatched line ' + line_no + ': "' + line + '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
++line_no;
|
||||||
|
});
|
||||||
|
|
||||||
|
// After loading all posts, sort them descending by date. Pages will be
|
||||||
|
// left as-is
|
||||||
|
self.posts.sort(function(left, right) {
|
||||||
|
return left().date().isSame(right().date())? 0
|
||||||
|
: (left().date().isBefore(right().date())? 1 : -1);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse data from a data file, updating the given observable
|
||||||
|
*/
|
||||||
|
self.parseData = function(obj, dataStr) {
|
||||||
|
var lines = dataStr.split('\n');
|
||||||
|
obj().title(lines[0]);
|
||||||
|
obj().synced(true);
|
||||||
|
obj().content(lines.slice(2).join('\n'));
|
||||||
|
|
||||||
|
var parts = obj().content().split('<!--SPLIT-->');
|
||||||
|
obj().shortContent(parts[0]);
|
||||||
|
obj().isSplit(parts.length > 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse index data and store it in the system.
|
||||||
|
*/
|
||||||
|
self.loadIndex = function() {
|
||||||
|
transfer.loadIndex().success(function(data) {
|
||||||
|
self.parseIndex(self.posts, self.pages, data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a post with the given slug and update its data in the index.
|
||||||
|
*/
|
||||||
|
self.loadPost = function(slug) {
|
||||||
|
transfer.loadPost(slug).success(function(data) {
|
||||||
|
var post = self.postDict()[slug];
|
||||||
|
self.parseData(post, data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a page with the given slug and update its dat a in the index.
|
||||||
|
*/
|
||||||
|
self.loadPage = function(slug) {
|
||||||
|
transfer.loadPage(slug).success(function(data) {
|
||||||
|
var page = self.pageDict()[slug];
|
||||||
|
self.parseData(page, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This runs when the component is torn down. Put here any logic necessary to clean up,
|
||||||
|
// for example cancelling setTimeouts or disposing Knockout subscriptions/computeds.
|
||||||
|
DB.prototype.dispose = function() {};
|
||||||
|
|
||||||
|
return new DB();
|
||||||
|
});
|
3
src/components/home-page/home.html
Normal file
3
src/components/home-page/home.html
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<h2>Page: <!-- ko text: page --><!-- /ko --></h2>
|
||||||
|
|
||||||
|
<pagination params="posts: posts, page: page"></pagination>
|
13
src/components/home-page/home.js
Normal file
13
src/components/home-page/home.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
define(["knockout", "text!./home.html", "../db/db"], function(ko, homeTemplate, DB) {
|
||||||
|
|
||||||
|
function HomeViewModel(route) {
|
||||||
|
var self = this;
|
||||||
|
self.posts = DB.posts;
|
||||||
|
self.page = ko.observable(1);
|
||||||
|
|
||||||
|
DB.loadIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { viewModel: HomeViewModel, template: homeTemplate };
|
||||||
|
|
||||||
|
});
|
27
src/components/nav-bar/nav-bar.html
Normal file
27
src/components/nav-bar/nav-bar.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<!--
|
||||||
|
The navigation UI that is docked to the top of the window. Most of this markup simply
|
||||||
|
follows Bootstrap conventions. The only Knockout-specific parts are the data-bind
|
||||||
|
attributes on the <li> elements.
|
||||||
|
-->
|
||||||
|
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-header">
|
||||||
|
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||||
|
<span class="sr-only">Toggle navigation</span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
<span class="icon-bar"></span>
|
||||||
|
</button>
|
||||||
|
<a class="navbar-brand" href="#">Laine</a>
|
||||||
|
</div>
|
||||||
|
<div class="collapse navbar-collapse">
|
||||||
|
|
||||||
|
<ul class="nav navbar-nav">
|
||||||
|
<li data-bind="css: { active: route().page === 'home-page' }">
|
||||||
|
<a href="#">Home</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
13
src/components/nav-bar/nav-bar.js
Normal file
13
src/components/nav-bar/nav-bar.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
define(['knockout', 'text!./nav-bar.html'], function(ko, template) {
|
||||||
|
|
||||||
|
function NavBarViewModel(params) {
|
||||||
|
|
||||||
|
// This viewmodel doesn't do anything except pass through the 'route' parameter to the view.
|
||||||
|
// You could remove this viewmodel entirely, and define 'nav-bar' as a template-only component.
|
||||||
|
// But in most apps, you'll want some viewmodel logic to determine what navigation options appear.
|
||||||
|
|
||||||
|
this.route = params.route;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { viewModel: NavBarViewModel, template: template };
|
||||||
|
});
|
21
src/components/pagination/pagination.html
Normal file
21
src/components/pagination/pagination.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<div class="post-list" data-bind="foreach: { data: postsForPage(), as: 'post' }">
|
||||||
|
<single-post params="post: post, short: true"></single-post>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="pagination" data-bind="if: pageCount() > 1">
|
||||||
|
<li data-bind="css: { disabled: page() === 1 }">
|
||||||
|
<a data-bind="click: goBack">
|
||||||
|
«
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- ko foreach: _.range(1, pageCount() + 1) -->
|
||||||
|
<li data-bind="css: { active: $data === $parent.page() } ">
|
||||||
|
<a data-bind="click: $parent.goTo, text: $data"></a>
|
||||||
|
</li>
|
||||||
|
<!-- /ko -->
|
||||||
|
<li data-bind="css: { disabled: page() === pageCount() }">
|
||||||
|
<a data-bind="click: goForward">
|
||||||
|
»
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
45
src/components/pagination/pagination.js
Normal file
45
src/components/pagination/pagination.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
define(['knockout', 'text!./pagination.html'], function(ko, templateMarkup) {
|
||||||
|
|
||||||
|
function Pagination(params) {
|
||||||
|
var self = this;
|
||||||
|
self.posts = params.posts;
|
||||||
|
self.page = params.page;
|
||||||
|
|
||||||
|
self.postsPerPage = 3;
|
||||||
|
|
||||||
|
self.pageCount = ko.pureComputed(function() {
|
||||||
|
return Math.ceil(params.posts().length / self.postsPerPage);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.goTo = function(page) {
|
||||||
|
if (page < 1) {
|
||||||
|
page = 1;
|
||||||
|
}
|
||||||
|
else if (page > self.pageCount()) {
|
||||||
|
page = self.pageCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.page(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.goBack = function() {
|
||||||
|
self.goTo(self.page() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.goForward = function() {
|
||||||
|
self.goTo(self.page() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.postsForPage = ko.pureComputed(function() {
|
||||||
|
var start = (self.page() - 1) * self.postsPerPage;
|
||||||
|
return self.posts.slice(start, start + self.postsPerPage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This runs when the component is torn down. Put here any logic necessary to clean up,
|
||||||
|
// for example cancelling setTimeouts or disposing Knockout subscriptions/computeds.
|
||||||
|
Pagination.prototype.dispose = function() {};
|
||||||
|
|
||||||
|
return { viewModel: Pagination, template: templateMarkup };
|
||||||
|
|
||||||
|
});
|
9
src/components/single-post/single-post.html
Normal file
9
src/components/single-post/single-post.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<div class="single-post">
|
||||||
|
<h2 data-bind="text: post.title()"></h2>
|
||||||
|
|
||||||
|
<p class="post-tags" data-bind="foreach: post.tags()">
|
||||||
|
<span data-bind="text: $data"></span><!-- ko if: ($index() < post.tags().length - 1) -->,<!-- /ko -->
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="post-content" data-bind="html: content() !== null? marked(content()) : ''"></div>
|
||||||
|
</div>
|
32
src/components/single-post/single-post.js
Normal file
32
src/components/single-post/single-post.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
define(['knockout', 'text!./single-post.html', '../db/db', 'marked'],
|
||||||
|
function(ko, templateMarkup, DB, marked) {
|
||||||
|
|
||||||
|
function SinglePost(params) {
|
||||||
|
var self = this;
|
||||||
|
self.post = params.post;
|
||||||
|
self.marked = marked;
|
||||||
|
|
||||||
|
// Is this post shown as short (in pagination) or not?
|
||||||
|
self.short = params.short;
|
||||||
|
|
||||||
|
self.content = self.post.content;
|
||||||
|
if (self.short) {
|
||||||
|
self.content = self.post.shortContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this post is not fully fetched into the index yet, do that now
|
||||||
|
if (!self.post.synced()) {
|
||||||
|
DB.loadPost(self.post.slug());
|
||||||
|
|
||||||
|
// Show loading indicator before loading is done
|
||||||
|
self.post.title("Loading post contents…");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This runs when the component is torn down. Put here any logic necessary to clean up,
|
||||||
|
// for example cancelling setTimeouts or disposing Knockout subscriptions/computeds.
|
||||||
|
SinglePost.prototype.dispose = function() {};
|
||||||
|
|
||||||
|
return { viewModel: SinglePost, template: templateMarkup };
|
||||||
|
|
||||||
|
});
|
22
src/components/transfer/transfer.js
Normal file
22
src/components/transfer/transfer.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
define(['knockout'], function(ko) {
|
||||||
|
|
||||||
|
function Transfer(params) {
|
||||||
|
this.loadIndex = function() {
|
||||||
|
return $.get('index');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.loadPost = function(slug) {
|
||||||
|
return $.get('posts/' + slug + '.md');
|
||||||
|
};
|
||||||
|
|
||||||
|
this.loadPage = function(slug) {
|
||||||
|
return $.get('pages/' + slug + '.md')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// This runs when the component is torn down. Put here any logic necessary to clean up,
|
||||||
|
// for example cancelling setTimeouts or disposing Knockout subscriptions/computeds.
|
||||||
|
Transfer.prototype.dispose = function() {};
|
||||||
|
|
||||||
|
return new Transfer();
|
||||||
|
});
|
3
src/css/styles.css
Normal file
3
src/css/styles.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#page {
|
||||||
|
margin-top: 80px;
|
||||||
|
}
|
24
src/index.html
Normal file
24
src/index.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>Laine</title>
|
||||||
|
<!-- build:css -->
|
||||||
|
<link href="bower_modules/components-bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="css/styles.css" rel="stylesheet">
|
||||||
|
<!-- endbuild -->
|
||||||
|
<!-- build:js -->
|
||||||
|
<script src="app/require.config.js"></script>
|
||||||
|
<script data-main="app/startup" src="bower_modules/requirejs/require.js"></script>
|
||||||
|
<!-- endbuild -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav-bar params="route: route"></nav-bar>
|
||||||
|
<div id="page" class="container" data-bind="component: { name: route().page, params: route }">
|
||||||
|
<div class="loading">
|
||||||
|
<h1>Loading…</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in a new issue