Refactor dependency graph to create layers

I was playing around with Madge and noticed that the previous dependency
graph looked almost *exactly* like a bowl of spaghetti I had last week.
After a few hours on Wikipedia I got interested in refactoring the code
so that each `require()` imported a module from one level deeper into
the tree.

I don't know if this is actually useful, but it's better than spaghetti.

In the future I think I should probably refactor the database
convenience functions out of the "models" since they really aren't
models but it's the best name I could come up with for what they are and
how they're used. This will probably go through some more evolution when
I rip out EJS and replace it with something much smaller.
This commit is contained in:
Christian Bundy 2019-06-26 22:25:13 -07:00
parent 07e56b8879
commit 400477d5c1
No known key found for this signature in database
GPG Key ID: EB541AAEF4366237
37 changed files with 524 additions and 442 deletions

View File

@ -1,54 +1,3 @@
#!/usr/bin/env node
const Koa = require('koa')
const path = require('path')
const router = require('koa-router')()
const views = require('koa-views')
const koaStatic = require('koa-static')
const mount = require('koa-mount')
const open = require('open')
const koaBody = require('koa-body')
const author = require('./routes/author')
const hashtag = require('./routes/hashtag')
const home = require('./routes/home')
const profile = require('./routes/profile')
const raw = require('./routes/raw')
const thread = require('./routes/thread')
const like = require('./routes/like')
const assets = new Koa()
assets.use(koaStatic(path.join(__dirname, 'public')))
const hljs = new Koa()
hljs.use(koaStatic(path.join(__dirname, 'node_modules', 'highlight.js', 'styles')))
const app = module.exports = new Koa()
app.use(mount('/static/public', assets))
app.use(mount('/static/hljs', hljs))
app.use(views(path.join(__dirname, 'views'), {
map: { html: 'ejs' }
}))
router
.get('/', home)
.get('/author/:id', author)
.get('/hashtag/:id', hashtag)
.get('/profile/', profile)
.get('/thread/:id', thread)
.get('/raw/:id', raw)
.post('/like/:id', koaBody(), like)
app.use(router.routes())
const opts = {
host: 'localhost',
port: 3000
}
const uri = `http://${opts.host}:${opts.port}/`
app.listen(opts)
console.log(`Listening on http://${uri}`)
open(uri)
require('./src/app')()

View File

@ -1,5 +0,0 @@
const ssbMd = require('ssb-markdown')
const toUrl = require('./to-url')
module.exports = (msg) =>
ssbMd.block(msg.value.content.text, { toUrl: toUrl(msg) })

View File

@ -1,5 +0,0 @@
const md = require('ssb-markdown')
const toUrl = require('./to-url')
module.exports = (input) =>
md.block(input, { toUrl: toUrl() })

View File

@ -1,92 +0,0 @@
const lodash = require('lodash')
const md = require('ssb-markdown')
const prettyMs = require('pretty-ms')
const pull = require('pull-stream')
const cooler = require('./cooler')
const toUrl = require('./to-url')
module.exports = (ssb) => async function (msg) {
lodash.set(msg, 'value.meta.md.block', () =>
md.block(msg.value.content.text, { toUrl: toUrl(msg) })
)
var filterQuery = {
$filter: {
dest: msg.key
}
}
const whoami = await cooler.get(ssb.whoami)
const backlinkStream = await cooler.read(ssb.backlinks.read, {
query: [ filterQuery ],
index: 'DTA', // use asserted timestamps
private: true,
meta: true
})
const rawVotes = await new Promise((resolve, reject) => {
pull(
backlinkStream,
pull.filter(ref =>
typeof ref.value.content !== 'string' &&
ref.value.content.type === 'vote' &&
ref.value.content.vote &&
typeof ref.value.content.vote.value === 'number' &&
ref.value.content.vote.value >= 0 &&
ref.value.content.vote.link === msg.key
),
pull.collect((err, msgs) => {
if (err) return reject(err)
resolve(msgs)
})
)
})
// { @key: 1, @key2: 0, @key3: 1 }
//
// only one vote per person!
const reducedVotes = rawVotes.reduce((acc, vote) => {
acc[vote.value.author] = vote.value.content.vote.value
return acc
}, {})
// gets *only* the people who voted 1
// [ @key, @key, @key ]
const voters = Object.entries(reducedVotes).filter(e => e[1] === 1).map(e => e[0])
const name = await cooler.get(
ssb.about.socialValue, { key: 'name',
dest: msg.value.author
}
)
const avatarMsg = await cooler.get(
ssb.about.socialValue, { key: 'image',
dest: msg.value.author
}
)
const avatarId = avatarMsg != null && typeof avatarMsg.link === 'string'
? avatarMsg.link
: avatarMsg
const avatarUrl = `http://localhost:8989/blobs/get/${avatarId}`
const ts = new Date(msg.value.timestamp)
lodash.set(msg, 'value.meta.timestamp.received.iso8601', ts.toISOString())
const ago = Date.now() - Number(ts)
lodash.set(msg, 'value.meta.timestamp.received.since', prettyMs(ago, { compact: true }))
lodash.set(msg, 'value.meta.author.name', name)
lodash.set(msg, 'value.meta.author.avatar', {
id: avatarId,
url: avatarUrl
})
lodash.set(msg, 'value.meta.votes', voters)
lodash.set(msg, 'value.meta.voted', voters.includes(whoami.id))
return msg
}

View File

@ -1,66 +0,0 @@
const ssbRef = require('ssb-ref')
const pull = require('pull-stream')
const cooler = require('../lib/cooler')
const renderMd = require('../lib/render-markdown')
const renderMsg = require('../lib/render-message')
module.exports = async function (ctx) {
if (ssbRef.isFeed(ctx.params.id) === false) {
throw new Error(`not a feed: ${ctx.params.id}`)
}
var ssb = await cooler.connect()
var rawDescription = await cooler.get(
ssb.about.socialValue,
{ key: 'description', dest: ctx.params.id }
)
const name = await cooler.get(
ssb.about.socialValue, { key: 'name',
dest: ctx.params.id
}
)
const avatarMsg = await cooler.get(
ssb.about.socialValue, { key: 'image',
dest: ctx.params.id
}
)
const avatarId = avatarMsg != null && typeof avatarMsg.link === 'string'
? avatarMsg.link
: avatarMsg
const avatarUrl = `http://localhost:8989/blobs/get/${avatarId}`
const description = renderMd(rawDescription)
var msgSource = await cooler.read(
ssb.createUserStream, {
id: ctx.params.id,
private: true,
reverse: true,
meta: true
}
)
const rawMsgs = await new Promise((resolve, reject) => {
pull(
msgSource,
pull.filter(msg =>
typeof msg.value.content !== 'string' &&
msg.value.content.type === 'post'
),
pull.take(32),
pull.collect((err, msgs) => {
if (err) return reject(err)
resolve(msgs)
})
)
})
const msgs = await Promise.all(rawMsgs.map(renderMsg(ssb)))
await ctx.render('author', { msgs, name, description, avatarUrl })
}

View File

@ -1,44 +0,0 @@
const pull = require('pull-stream')
const cooler = require('../lib/cooler')
const renderMsg = require('../lib/render-message')
module.exports = async function hashtag (ctx) {
var ssb = await cooler.connect()
var filterQuery = {
$filter: {
dest: '#' + ctx.params.id
}
}
const msgSource = await cooler.read(
ssb.backlinks.read,
{
query: [ filterQuery ],
index: 'DTA', // use asserted timestamps
reverse: true,
private: true,
meta: true
}
)
const rawMsgs = await new Promise((resolve, reject) => {
pull(
msgSource,
pull.filter(msg =>
typeof msg.value.content !== 'string' &&
msg.value.content.type === 'post'
),
pull.take(32),
pull.collect((err, msgs) => {
if (err) return reject(err)
resolve(msgs)
})
)
})
const msgs = await Promise.all(rawMsgs.map(renderMsg(ssb)))
await ctx.render('home', { msgs })
}

View File

@ -1,31 +0,0 @@
const pull = require('pull-stream')
const cooler = require('../lib/cooler')
const renderMsg = require('../lib/render-message')
module.exports = async function home (ctx) {
var ssb = await cooler.connect()
var msgSource = await cooler.read(
ssb.messagesByType, {
limit: 32,
private: true,
reverse: true,
type: 'post'
}
)
const rawMsgs = await new Promise((resolve, reject) => {
pull(
msgSource,
pull.collect((err, msgs) => {
if (err) return reject(err)
resolve(msgs)
})
)
})
const msgs = await Promise.all(rawMsgs.map(renderMsg(ssb)))
await ctx.render('home', { msgs })
}

View File

@ -1,17 +0,0 @@
const cooler = require('../lib/cooler')
module.exports = async function like (ctx) {
const ssb = await cooler.connect()
await cooler.get(ssb.publish, {
type: 'vote',
vote: {
link: ctx.params.id,
value: Number(ctx.request.body.voteValue)
}
})
const back = new URL(ctx.request.header.referer)
back.hash = encodeURIComponent(ctx.params.id)
ctx.redirect(back)
}

View File

@ -1,9 +0,0 @@
const cooler = require('../lib/cooler')
const author = require('./author')
module.exports = async function (ctx) {
const ssb = await cooler.connect()
const whoami = await cooler.get(ssb.whoami)
ctx.params.id = whoami.id
await author(ctx)
}

View File

@ -1,12 +0,0 @@
const cooler = require('../lib/cooler')
module.exports = async function thread (ctx) {
const ssb = await cooler.connect()
const rawMsg = await cooler.get(ssb.get, {
id: ctx.params.id,
meta: true,
private: true
})
ctx.body = rawMsg
}

View File

@ -1,91 +0,0 @@
const lodash = require('lodash')
const pull = require('pull-stream')
const cooler = require('../lib/cooler')
const renderMsg = require('../lib/render-message')
module.exports = async function thread (ctx) {
const ssb = await cooler.connect()
const rawMsg = await cooler.get(ssb.get, {
id: ctx.params.id,
meta: true,
private: true
})
const parents = []
const getParents = (msg) => new Promise(async (resolve, reject) => {
if (typeof msg.value.content === 'string') {
return resolve(parents)
}
if (typeof msg.value.content.fork === 'string') {
const fork = await cooler.get(ssb.get, {
id: msg.value.content.fork,
meta: true,
private: true
})
parents.push(fork)
resolve(getParents(fork))
} else if (typeof msg.value.content.root === 'string') {
const root = await cooler.get(ssb.get, {
id: msg.value.content.root,
meta: true,
private: true
})
parents.push(root)
resolve(getParents(root))
} else {
resolve(parents)
}
})
const ancestors = await getParents(rawMsg)
const root = rawMsg.key
var filterQuery = {
$filter: {
dest: root
}
}
const backlinkStream = await cooler.read(ssb.backlinks.read, {
query: [filterQuery],
index: 'DTA' // use asserted timestamps
})
const replies = await new Promise((resolve, reject) =>
pull(
backlinkStream,
pull.filter(msg => {
const isPost = lodash.get(msg, 'value.content.type') === 'post'
if (isPost === false) {
return false
}
const root = lodash.get(msg, 'value.content.root')
const fork = lodash.get(msg, 'value.content.fork')
if (root !== rawMsg.key && fork !== rawMsg.key) {
// mention
return false
}
return true
}
),
pull.collect((err, msgs) => {
if (err) return reject(err)
resolve(msgs)
})
)
)
const allMsgs = [...ancestors, rawMsg, ...replies]
const msgs = await Promise.all(allMsgs.map(renderMsg(ssb)))
await ctx.render('home', { msgs })
}

51
src/app.js Normal file
View File

@ -0,0 +1,51 @@
const Koa = require('koa')
const path = require('path')
const router = require('koa-router')()
const koaStatic = require('koa-static')
const mount = require('koa-mount')
const open = require('open')
const koaBody = require('koa-body')
const author = require('./routes/author')
const hashtag = require('./routes/hashtag')
const home = require('./routes/home')
const profile = require('./routes/profile')
const raw = require('./routes/raw')
const thread = require('./routes/thread')
const like = require('./routes/like')
module.exports = (options) => {
const assets = new Koa()
assets.use(koaStatic(path.join(__dirname, 'assets')))
const hljs = new Koa()
hljs.use(koaStatic(path.join(__dirname, '..', 'node_modules', 'highlight.js', 'styles')))
const app = module.exports = new Koa()
app.use(mount('/static/assets', assets))
app.use(mount('/static/hljs', hljs))
router
.get('/', home)
.get('/author/:id', author)
.get('/hashtag/:id', hashtag)
.get('/profile/', profile)
.get('/thread/:id', thread)
.get('/raw/:id', raw)
.post('/like/:id', koaBody(), like)
app.use(router.routes())
const defaultConfig = {
host: 'localhost',
port: 3000
}
const config = Object.assign({}, defaultConfig, options)
const uri = `http://${config.host}:${config.port}/`
app.listen(config)
console.log(`Listening on http://${uri}`)
open(uri)
}

28
src/routes/author.js Normal file
View File

@ -0,0 +1,28 @@
const ssbRef = require('ssb-ref')
const about = require('./models/about')
const post = require('./models/post')
const views = require('./views')
module.exports = async function (ctx) {
const feedId = ctx.params.id
if (ssbRef.isFeed(feedId) === false) {
throw new Error(`not a feed: ${ctx.params.id}`)
}
const description = await about.description(feedId)
const name = await about.name(feedId)
const image = await about.image(feedId)
const msgs = await post.fromFeed(feedId)
const avatarUrl = `http://localhost:8989/blobs/get/${image}`
ctx.body = await views('author', {
msgs,
name,
description,
avatarUrl
})
}

10
src/routes/hashtag.js Normal file
View File

@ -0,0 +1,10 @@
const post = require('./models/post')
const views = require('./views')
module.exports = async function hashtag (ctx) {
const hashtag = ctx.params.id
const msgs = await post.fromHashtag(hashtag)
ctx.body = await views('home', { msgs })
}

8
src/routes/home.js Normal file
View File

@ -0,0 +1,8 @@
const views = require('./views')
const post = require('./models/post')
module.exports = async function home (ctx) {
const msgs = await post.latest()
ctx.body = await views('home', { msgs })
}

12
src/routes/like.js Normal file
View File

@ -0,0 +1,12 @@
const vote = require('./models/vote')
module.exports = async function like (ctx) {
const msgId = ctx.params.id
const value = Number(ctx.request.body.voteValue)
const referer = new URL(ctx.request.header.referer)
await vote.publish(msgId, value)
referer.hash = encodeURIComponent(ctx.params.id)
ctx.redirect(referer)
}

View File

@ -0,0 +1,41 @@
const cooler = require('./lib/cooler')
const markdown = require('./lib/markdown')
const nullImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='
module.exports = {
name: async (feedId) => {
const ssb = await cooler.connect()
return cooler.get(
ssb.about.socialValue, {
key: 'name',
dest: feedId
})
},
image: async (feedId) => {
const ssb = await cooler.connect()
const raw = await cooler.get(
ssb.about.socialValue, {
key: 'image',
dest: feedId
}
)
if (raw == null || raw.link == null) {
return nullImage
} else if (typeof raw.link === 'string') {
return raw.link
} else {
return raw
}
},
description: async (feedId) => {
const ssb = await cooler.connect()
const raw = await cooler.get(
ssb.about.socialValue, {
key: 'description',
dest: feedId
})
return markdown(raw)
}
}

View File

@ -0,0 +1,8 @@
const defaultOptions = {
private: true,
reverse: true,
meta: true
}
module.exports = (customOptions) =>
Object.assign({}, customOptions, defaultOptions)

View File

@ -1,12 +1,10 @@
const ssbRef = require('ssb-ref')
const md = require('ssb-markdown')
const ssbMsgs = require('ssb-msgs')
const lodash = require('lodash')
const ssbRef = require('ssb-ref')
module.exports = (msg) => {
const toUrl = (mentions = []) => {
var mentionNames = {}
const mentions = lodash.get(msg, 'value.content.mentions', [])
ssbMsgs.links(mentions, 'feed').forEach(function (link) {
if (link.name && typeof link.name === 'string') {
var name = (link.name.charAt(0) === '@') ? link.name : '@' + link.name
@ -32,3 +30,8 @@ module.exports = (msg) => {
return ''
}
}
module.exports = (input, mentions) =>
md.block(input, {
toUrl: toUrl(mentions)
})

16
src/routes/models/meta.js Normal file
View File

@ -0,0 +1,16 @@
const cooler = require('./lib/cooler')
module.exports = {
whoami: async () => {
const ssb = await cooler.connect()
return cooler.get(ssb.whoami)
},
get: async (msgId) => {
const ssb = await cooler.connect()
return cooler.get(ssb.get, {
id: msgId,
meta: true,
private: true
})
}
}

256
src/routes/models/post.js Normal file
View File

@ -0,0 +1,256 @@
const lodash = require('lodash')
const pull = require('pull-stream')
const cooler = require('./lib/cooler')
const configure = require('./lib/configure')
const markdown = require('./lib/markdown')
const prettyMs = require('pretty-ms')
const transform = (ssb, messages) => Promise.all(messages.map(async (msg) => {
lodash.set(msg, 'value.meta.md.block', () =>
markdown(msg.value.content.text, msg.value.content.mentions)
)
var filterQuery = {
$filter: {
dest: msg.key
}
}
const whoami = await cooler.get(ssb.whoami)
const backlinkStream = await cooler.read(ssb.backlinks.read, {
query: [ filterQuery ],
index: 'DTA', // use asserted timestamps
private: true,
meta: true
})
const rawVotes = await new Promise((resolve, reject) => {
pull(
backlinkStream,
pull.filter(ref =>
typeof ref.value.content !== 'string' &&
ref.value.content.type === 'vote' &&
ref.value.content.vote &&
typeof ref.value.content.vote.value === 'number' &&
ref.value.content.vote.value >= 0 &&
ref.value.content.vote.link === msg.key
),
pull.collect((err, msgs) => {
if (err) return reject(err)
resolve(msgs)
})
)
})
// { @key: 1, @key2: 0, @key3: 1 }
//
// only one vote per person!
const reducedVotes = rawVotes.reduce((acc, vote) => {
acc[vote.value.author] = vote.value.content.vote.value
return acc
}, {})
// gets *only* the people who voted 1
// [ @key, @key, @key ]
const voters = Object.entries(reducedVotes).filter(e => e[1] === 1).map(e => e[0])
const name = await cooler.get(
ssb.about.socialValue, { key: 'name',
dest: msg.value.author
}
)
const avatarMsg = await cooler.get(
ssb.about.socialValue, { key: 'image',
dest: msg.value.author
}
)
const avatarId = avatarMsg != null && typeof avatarMsg.link === 'string'
? avatarMsg.link
: avatarMsg
const avatarUrl = `http://localhost:8989/blobs/get/${avatarId}`
const ts = new Date(msg.value.timestamp)
lodash.set(msg, 'value.meta.timestamp.received.iso8601', ts.toISOString())
const ago = Date.now() - Number(ts)
lodash.set(msg, 'value.meta.timestamp.received.since', prettyMs(ago, { compact: true }))
lodash.set(msg, 'value.meta.author.name', name)
lodash.set(msg, 'value.meta.author.avatar', {
id: avatarId,
url: avatarUrl
})
lodash.set(msg, 'value.meta.votes', voters)
lodash.set(msg, 'value.meta.voted', voters.includes(whoami.id))
return msg
}))
module.exports = {
fromFeed: async (feedId, customOptions = {}) => {
const ssb = await cooler.connect()
const options = configure({ id: feedId }, customOptions)
const source = await cooler.read(
ssb.createUserStream,
options
)
const messages = await new Promise((resolve, reject) => {
pull(
source,
pull.filter(msg =>
typeof msg.value.content !== 'string' &&
msg.value.content.type === 'post'
),
pull.take(32),
pull.collect((err, msgs) => {
if (err) return reject(err)
resolve(transform(ssb, msgs))
})
)
})
return messages
},
fromHashtag: async (hashtag, customOptions = {}) => {
const ssb = await cooler.connect()
const query = [ {
$filter: {
dest: '#' + hashtag
}
} ]
const options = configure({ query, index: 'DTA' }, customOptions)
const source = await cooler.read(
ssb.backlinks.read, options
)
const messages = await new Promise((resolve, reject) => {
pull(
source,
pull.filter(msg =>
typeof msg.value.content !== 'string' &&
msg.value.content.type === 'post'
),
pull.take(32),
pull.collect((err, msgs) => {
if (err) return reject(err)
resolve(transform(ssb, msgs))
})
)
})
return messages
},
latest: async (customOptions = {}) => {
const ssb = await cooler.connect()
const options = configure({
type: 'post',
limit: 32
}, customOptions)
const source = await cooler.read(
ssb.messagesByType,
options
)
const messages = await new Promise((resolve, reject) => {
pull(
source,
pull.collect((err, msgs) => {
if (err) return reject(err)
resolve(transform(ssb, msgs))
})
)
})
return messages
},
fromThread: async (msgId, customOptions) => {
const ssb = await cooler.connect()
const options = configure({ id: msgId }, customOptions)
const rawMsg = await cooler.get(ssb.get, options)
const parents = []
const getParents = (msg) => new Promise(async (resolve, reject) => {
if (typeof msg.value.content === 'string') {
return resolve(parents)
}
if (typeof msg.value.content.fork === 'string') {
const fork = await cooler.get(ssb.get, {
id: msg.value.content.fork,
meta: true,
private: true
})
parents.push(fork)
resolve(getParents(fork))
} else if (typeof msg.value.content.root === 'string') {
const root = await cooler.get(ssb.get, {
id: msg.value.content.root,
meta: true,
private: true
})
parents.push(root)
resolve(getParents(root))
} else {
resolve(parents)
}
})
const ancestors = await getParents(rawMsg)
const root = rawMsg.key
var filterQuery = {
$filter: {
dest: root
}
}
const backlinkStream = await cooler.read(ssb.backlinks.read, {
query: [filterQuery],
index: 'DTA' // use asserted timestamps
})
const replies = await new Promise((resolve, reject) =>
pull(
backlinkStream,
pull.filter(msg => {
const isPost = lodash.get(msg, 'value.content.type') === 'post'
if (isPost === false) {
return false
}
const root = lodash.get(msg, 'value.content.root')
const fork = lodash.get(msg, 'value.content.fork')
if (root !== rawMsg.key && fork !== rawMsg.key) {
// mention
return false
}
return true
}
),
pull.collect((err, msgs) => {
if (err) return reject(err)
resolve(msgs)
})
)
)
const allMessages = [...ancestors, rawMsg, ...replies]
return transform(ssb, allMessages)
}
}

14
src/routes/models/vote.js Normal file
View File

@ -0,0 +1,14 @@
const cooler = require('./lib/cooler')
module.exports = {
publish: async (messageId, value) => {
const ssb = await cooler.connect()
await cooler.get(ssb.publish, {
type: 'vote',
vote: {
link: messageId,
value: Number(value)
}
})
}
}

30
src/routes/profile.js Normal file
View File

@ -0,0 +1,30 @@
const ssbRef = require('ssb-ref')
const about = require('./models/about')
const post = require('./models/post')
const meta = require('./models/meta')
const views = require('./views')
module.exports = async function (ctx) {
const whoami = await meta.whoami()
const feedId = whoami.id
if (ssbRef.isFeed(feedId) === false) {
throw new Error(`not a feed: ${ctx.params.id}`)
}
const description = await about.description(feedId)
const name = await about.name(feedId)
const image = await about.image(feedId)
const msgs = await post.fromFeed(feedId)
const avatarUrl = `http://localhost:8989/blobs/get/${image}`
ctx.body = await views('author', {
msgs,
name,
description,
avatarUrl
})
}

8
src/routes/raw.js Normal file
View File

@ -0,0 +1,8 @@
const meta = require('./models/meta')
module.exports = async function thread (ctx) {
const msgId = ctx.params.id
const message = await meta.get(msgId)
ctx.body = message
}

9
src/routes/thread.js Normal file
View File

@ -0,0 +1,9 @@
const views = require('./views')
const post = require('./models/post')
module.exports = async function thread (ctx) {
const msgId = ctx.params.id
const msgs = await post.fromThread(msgId)
ctx.body = await views('home', { msgs })
}

11
src/routes/views/index.js Normal file
View File

@ -0,0 +1,11 @@
const ejs = require('ejs')
const path = require('path')
module.exports = (filename, data) => new Promise((resolve, reject) => {
const options = {}
const target = path.join(__dirname, 'templates', filename + '.html')
ejs.renderFile(target, data, options, (err, str) => {
if (err) return reject(err)
resolve(str)
})
})

View File

@ -1,4 +1,4 @@
<%- include('partials/header.html'); %>
<%- include('layout/header.html'); %>
<header class="profile">
<img class="avatar" src="<%= avatarUrl %>">
@ -14,4 +14,4 @@
<%- include('feed.html', { msgs }); %>
<%- include('partials/footer.html'); %>
<%- include('layout/footer.html'); %>

View File

@ -0,0 +1,5 @@
<%- include('layout/header.html'); %>
<%- include('feed.html', { msgs }); %>
<%- include('layout/footer.html'); %>

View File

@ -2,7 +2,7 @@
<html>
<head>
<title>&#x1F3DD;&#xFE0F; Oasis</title>
<link rel="stylesheet" href="/static/public/style.css">
<link rel="stylesheet" href="/static/assets/style.css">
<link rel="stylesheet" href="/static/hljs/github.css">
</head>
<body>

View File

@ -0,0 +1,5 @@
<%- include('layout/header.html'); %>
<%- include('message.html', { msg }); %>
<%- include('layout/footer.html'); %>

View File

@ -1,5 +0,0 @@
<%- include('partials/header.html'); %>
<%- include('feed.html', { msgs }); %>
<%- include('partials/footer.html'); %>

View File

@ -1,5 +0,0 @@
<%- include('partials/header.html'); %>
<%- include('message.html', { msg }); %>
<%- include('partials/footer.html'); %>