...
 
Commits (2)
.mongo-data
node_modules
\ No newline at end of file
.PHONY: mongo
mongo:
docker run --rm --name blog-mongo -v $(CURDIR)/.mongo-data:/data/db -p 27017:27017 mongo
\ No newline at end of file
const helpers = {
catchError: (res) => {
return (error) => {
res.locals.title = 'Error'
res.status(500).render('error', {
error
})
}
}
}
module.exports = helpers
\ No newline at end of file
const main = async () => {
const sprintf = require('sprintf-js').sprintf
const express = require('express')
const MongoClient = require('mongodb').MongoClient
// express plugins
const bodyParser = require('body-parser')
const handlebars = require('express-handlebars')
const flash = require('connect-flash')
const session = require('express-session')
// express app
const app = express()
// flash messages
app.use(session({
secret: process.env.COOKIE_SECRET || '73126D3D-A167-405B-B3E4-DE9126B9B0E9',
resave: false,
saveUninitialized: true,
}))
app.use(flash())
// handlebars
const hbs = handlebars({
defaultLayout: 'root',
extname: '.hbs',
helpers: {
excerpt: (text, opt) => {
const size = Math.max(opt.size || 120, 3)
if (text.length > (size - 3)) {
return sprintf('%s...', text.substr(0, size - 3))
} else {
return text
}
}
}
})
app.set('view options', { layout: 'root' })
app.engine('.hbs', hbs)
app.set('view engine', '.hbs')
// parse JSON input
app.use(bodyParser.urlencoded({ extended: true }))
// public assets
app.use(express.static('public'))
// mongodb
const mongo = await MongoClient.connect('mongodb://localhost:27017', {
useNewUrlParser: true
})
// middleware
app.use((req, res, next) => {
res.locals.errors = req.flash('errors')
next()
})
// routes
const router = require('./router')
router(app, mongo.db('blog'))
// listen
const host = process.env.HOST || 'localhost'
const port = process.env.PORT || '4000'
app.listen(port, host, () => {
console.log(sprintf('Blog running on %s:%s', host, port))
})
}
main()
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"scripts": {
"dev": "nodemon ./index.js"
},
"dependencies": {
"body-parser": "^1.18.3",
"connect-flash": "^0.1.1",
"cookie-parser": "^1.4.3",
"express": "^4.16.3",
"express-handlebars": "^3.0.0",
"express-session": "^1.15.6",
"mongodb": "^3.1.4",
"mustache": "^2.3.2",
"sprintf-js": "^1.1.1"
},
"devDependencies": {
"nodemon": "^1.18.4"
}
}
:root {
--c-darker: #1C1D21;
--c-dark: #31353D;
--c-primary: #445878;
--c-secondary: #92CDCF;
--c-faint: #EEEFF7;
--c-alt-white: #f7f8fa;
--c-danger: #ff6961;
}
* {
box-sizing: border-box;
}
html {
padding: 0;
margin: 0;
font-size: 14px;
}
body {
margin: 0;
background: var(--c-alt-white);
font-size: 1.5rem;
color: var(--c-dark);
font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, ubuntu, roboto, noto, segoe ui, arial, sans-serif;
line-height: 1.5;
}
a {
text-decoration: none;
}
a, a:visited {
color: var(--c-primary);
}
h1, h2, h3, h4, h5, h6 {
line-height: 1.2;
}
.nav-container {
background: white;
padding: 0.75rem 1.25rem;
border-bottom: 3px solid var(--c-secondary);
box-shadow: 0px 3px 10px 3px rgba(146, 205, 207, 0.2);
}
nav {
max-width: 1120px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
}
nav h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: var(--c-primary);
}
nav .nav-links {
display: flex;
align-items: center;
}
nav a {
font-size: 1.125rem;
padding: 0 0.5rem;
}
nav a:last-child {
padding-right: 0;
}
.container {
max-width: 960px;
margin: 0 auto;
padding: 2.5rem;
}
.ts-s {
font-size: 0.75rem;
}
.ts-m {
font-size: 1rem;
}
.ts-l {
font-size: 1.25rem;
}
.form {
max-width: 32rem;
}
.form header h2 {
margin-bottom: 0.25rem;
}
.form-action-buttons {
clear: both;
}
.form-action-buttons button {
float: right;
}
.form-input {
padding-top: 1rem;
}
.form-input input, .form-input textarea {
width: 100%;
display: block;
border: 1px solid var(--c-secondary);
background: white;
padding: 0.5rem;
}
.form-input textarea {
min-height: 16rem;
}
.form-input label {
font-size: 1rem;
text-transform: uppercase;
color: var(--c-primary);
}
.button {
background-color: var(--c-secondary);
color: white;
border-radius: 2px;
cursor: pointer;
padding: 0.375em 1.5em;
border: none;
transition: box-shadow 0.4s;
}
.button:hover, .button:focus {
box-shadow: inset 9999px 9999px rgba(0, 0, 0, 0.2);
transition: box-shadow 0.4s;
}
.new-post-cta a {
font-size: 1rem;
text-transform: uppercase;
color: var(--c-primary);
}
.errors {
font-size: 1.125rem;
padding-top: .5rem;
}
.errors + .form-input {
padding-top: 0;
}
.errors p {
margin-bottom: 0.5rem;
}
.errors ul {
margin-top: 0.25rem;
list-style: none;
padding-left: 1.875rem;
}
.errors li {
color: var(--c-danger);
}
.posts-list .post-item-container {
margin: 2rem 0;
border-radius: 0.25rem;
box-shadow: 0px 2px 2px 1px rgba(146, 205, 207, 0.18);
background: white;
}
.posts-list .post-item-container header {
background: var(--c-primary);
border-radius: 0.25rem 0.25rem 0 0;
}
.posts-list .post-item-container header h2 {
padding: 0.5rem 1rem;
font-size: 1.75rem;
font-weight: 400;
color: white;
margin: 0;
}
.posts-list .post-item-container .post-preview {
padding: 0 2rem 0.5rem;
}
.posts-list .post-item-container .post-preview p {
font-size: 1.25rem;
}
.posts-list .post-item-container .post-preview a {
font-size: 1rem;
}
.post-page header h1 {
font-size: 2.5rem;
margin-top: 1rem;
margin-bottom: 0;
}
.post-page header time {
font-size: 1rem;
color: var(--c-secondary);
text-transform: uppercase;
}
.post-page article {
font-size: 1.25rem;
}
router = (app, db) => {
// posts
require('./posts')(app, db)
}
module.exports = router
\ No newline at end of file
const sprintf = require('sprintf-js').sprintf
const catchError = require('../helpers.js').catchError
const ObjectId = require('mongodb').ObjectID
module.exports = (app, db) => {
app.get('/', (req, res) => {
res.locals.title = 'Home'
const col = db.collection('posts')
col.find().sort('_id', -1).toArray().then((posts) => {
res.render('posts/index', {
posts
})
}).catch(catchError(res))
})
app.get('/posts/new', (req, res) => {
res.render('posts/new', {
title: 'New Post'
})
})
app.get('/posts/:id', (req, res) => {
const id = req.params.id
const col = db.collection('posts')
col.findOne({
_id: new ObjectId(id)
}).then((post) => {
res.locals.title = post.title;
const timestamp = post._id.getTimestamp()
const dates = {
raw: timestamp,
human: timestamp.toISOString().split("T")[0]
}
res.render('posts/view', {
dates,
post
})
}).catch(catchError(res))
})
app.post('/posts', (req, res) => {
let errors = [], post = {}
const fields = ['title', 'body']
fields.forEach((field => {
const val = req.body[field]
if (!val || val.trim() == '') {
errors.push(sprintf("Field %s is required", field))
}
post[field] = val
}))
if (errors.length) {
req.flash('errors', errors)
res.redirect('back')
return
}
const col = db.collection('posts')
col.insertOne(post).then((obj) => {
let record = obj.ops[0]
res.redirect(sprintf('/posts/%s', record._id))
}).catch(catchError(res))
})
}
\ No newline at end of file
<h3>Oh no! An error has occured.</h3>
<p>
<pre>{{ error }}</pre>
</p>
\ No newline at end of file
Hi {{ name }}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{ title }} &larr; Blog</title>
<link rel="stylesheet" href="/resources/app.css">
</head>
<body>
{{> nav }}
<div class="container">
{{{ body }}}
</div>
</body>
</html>
\ No newline at end of file
<div class="errors">
{{#if errors.length}}
<p>Oh no! There were some errors:</p>
<ul>
{{#each errors}}
<li>{{ this }}</li>
{{/each}}
</ul>
{{/if}}
</div>
\ No newline at end of file
<div class="nav-container">
<nav>
<h1>Blog</h1>
<div class="nav-links">
<a href="/">Home</a>
<a href="/posts/new">New Post</a>
</div>
</nav>
</div>
\ No newline at end of file
<main>
<div class="new-post-cta">
<a href="/posts/new">+ Write a new post</a>
</div>
{{#each posts}}
<div>
<div class="posts-list">
<div class="post-item-container">
<header>
<h2>{{ title }}</h2>
</header>
<div class="post-preview">
<p>{{ excerpt body size=120 }}</p>
<div class="cf">
<a href="/posts/{{ _id }}">View Post&rarr;</a>
</div>
</div>
</div>
</div>
</div>
{{else}}
<p>
This blog doesn't have any posts yet.
</p>
{{/each}}
</main>
\ No newline at end of file
<main>
<form action="/posts" method="post" class="form">
<header>
<h2>New Post</h2>
</header>
{{> errors }}
<div class="form-input">
<label for="title">Title</label>
<input type="text" name="title" id="title" class="ts-l" required>
</div>
<div class="form-input">
<label for="body">Body</label>
<textarea name="body" id="body" class="ts-m" required></textarea>
</div>
<div class="form-input form-action-buttons">
<button type="submit" class="ts-l button">Post</button>
</div>
</form>
</main>
\ No newline at end of file
<main class="post-page">
<nav>
<a href="/">&larr; Back</a>
</nav>
<header>
<h1>
{{ post.title }}
</h1>
<time datetime="{{ dates.raw }}">Posted on {{ dates.human }}</time>
</header>
<article>
<p>
{{ post.body }}
</p>
</article>
</main>
\ No newline at end of file