@ -1,69 +0,0 @@ |
|||||
// page.js
|
|
||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||
// License: Apache-2.0
|
|
||||
|
|
||||
'use strict'; |
|
||||
|
|
||||
const DTP_COMPONENT_NAME = 'page'; |
|
||||
|
|
||||
const express = require('express'); |
|
||||
|
|
||||
const { SiteController, SiteError } = require('../../lib/site-lib'); |
|
||||
|
|
||||
class PageController extends SiteController { |
|
||||
|
|
||||
constructor (dtp) { |
|
||||
super(dtp, DTP_COMPONENT_NAME); |
|
||||
} |
|
||||
|
|
||||
async start ( ) { |
|
||||
const { dtp } = this; |
|
||||
const { limiter: limiterService } = dtp.services; |
|
||||
|
|
||||
const router = express.Router(); |
|
||||
dtp.app.use('/page', router); |
|
||||
|
|
||||
router.use(this.dtp.services.gabTV.channelMiddleware('mrjoeprich')); |
|
||||
router.use(async (req, res, next) => { |
|
||||
res.locals.currentView = 'home'; |
|
||||
return next(); |
|
||||
}); |
|
||||
|
|
||||
router.param('pageSlug', this.populatePageSlug.bind(this)); |
|
||||
|
|
||||
router.get('/:pageSlug', |
|
||||
limiterService.create(limiterService.config.page.getView), |
|
||||
this.getView.bind(this), |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
async populatePageSlug (req, res, next, pageSlug) { |
|
||||
const { page: pageService } = this.dtp.services; |
|
||||
try { |
|
||||
res.locals.page = await pageService.getBySlug(pageSlug); |
|
||||
if (!res.locals.page) { |
|
||||
throw new SiteError(404, 'Page not found'); |
|
||||
} |
|
||||
return next(); |
|
||||
} catch (error) { |
|
||||
this.log.error('failed to populate pageSlug', { pageSlug, error }); |
|
||||
return next(error); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async getView (req, res, next) { |
|
||||
const { resource: resourceService } = this.dtp.services; |
|
||||
try { |
|
||||
await resourceService.recordView(req, 'Page', res.locals.page._id); |
|
||||
res.render('page/view'); |
|
||||
} catch (error) { |
|
||||
this.log.error('failed to service page view', { pageId: res.locals.page._id, error }); |
|
||||
return next(error); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
module.exports = async (dtp) => { |
|
||||
let controller = new PageController(dtp); |
|
||||
return controller; |
|
||||
}; |
|
@ -1,129 +0,0 @@ |
|||||
// post.js
|
|
||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||
// License: Apache-2.0
|
|
||||
|
|
||||
'use strict'; |
|
||||
|
|
||||
const DTP_COMPONENT_NAME = 'post'; |
|
||||
|
|
||||
const express = require('express'); |
|
||||
const multer = require('multer'); |
|
||||
|
|
||||
const { SiteController, SiteError } = require('../../lib/site-lib'); |
|
||||
|
|
||||
class PostController extends SiteController { |
|
||||
|
|
||||
constructor (dtp) { |
|
||||
super(dtp, DTP_COMPONENT_NAME); |
|
||||
} |
|
||||
|
|
||||
async start ( ) { |
|
||||
const { dtp } = this; |
|
||||
const { limiter: limiterService } = dtp.services; |
|
||||
|
|
||||
const upload = multer({ dest: `/tmp/dtp-sites/${this.dtp.config.site.domainKey}`}); |
|
||||
|
|
||||
const router = express.Router(); |
|
||||
dtp.app.use('/post', router); |
|
||||
|
|
||||
router.use(this.dtp.services.gabTV.channelMiddleware('mrjoeprich')); |
|
||||
router.use(async (req, res, next) => { |
|
||||
res.locals.currentView = 'home'; |
|
||||
return next(); |
|
||||
}); |
|
||||
|
|
||||
router.param('postSlug', this.populatePostSlug.bind(this)); |
|
||||
|
|
||||
router.post('/:postSlug/comment', upload.none(), this.postComment.bind(this)); |
|
||||
|
|
||||
router.get('/:postSlug', |
|
||||
limiterService.create(limiterService.config.post.getView), |
|
||||
this.getView.bind(this), |
|
||||
); |
|
||||
|
|
||||
router.get('/', |
|
||||
limiterService.create(limiterService.config.post.getIndex), |
|
||||
this.getIndex.bind(this), |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
async populatePostSlug (req, res, next, postSlug) { |
|
||||
const { post: postService } = this.dtp.services; |
|
||||
try { |
|
||||
res.locals.post = await postService.getBySlug(postSlug); |
|
||||
if (!res.locals.post) { |
|
||||
throw new SiteError(404, 'Post not found'); |
|
||||
} |
|
||||
return next(); |
|
||||
} catch (error) { |
|
||||
this.log.error('failed to populate postSlug', { postSlug, error }); |
|
||||
return next(error); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async postComment (req, res) { |
|
||||
const { |
|
||||
comment: commentService, |
|
||||
displayEngine: displayEngineService, |
|
||||
} = this.dtp.services; |
|
||||
try { |
|
||||
const displayList = displayEngineService.createDisplayList('add-recipient'); |
|
||||
|
|
||||
res.locals.comment = await commentService.create(req.user, 'Post', res.locals.post, req.body); |
|
||||
|
|
||||
displayList.setInputValue('textarea#content', ''); |
|
||||
displayList.setTextContent('#comment-character-count', '0'); |
|
||||
|
|
||||
let viewModel = Object.assign({ }, req.app.locals); |
|
||||
viewModel = Object.assign(viewModel, res.locals); |
|
||||
|
|
||||
const html = await commentService.templates.comment(viewModel); |
|
||||
displayList.addElement('ul#post-comment-list', 'afterBegin', html); |
|
||||
|
|
||||
displayList.showNotification( |
|
||||
'Comment created', |
|
||||
'success', |
|
||||
'bottom-center', |
|
||||
4000, |
|
||||
); |
|
||||
res.status(200).json({ success: true, displayList }); |
|
||||
} catch (error) { |
|
||||
res.status(error.statusCode || 500).json({ success: false, message: error.message }); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async getView (req, res, next) { |
|
||||
const { comment: commentService, resource: resourceService } = this.dtp.services; |
|
||||
try { |
|
||||
await resourceService.recordView(req, 'Post', res.locals.post._id); |
|
||||
|
|
||||
res.locals.pagination = this.getPaginationParameters(req, 20); |
|
||||
res.locals.comments = await commentService.getForResource( |
|
||||
res.locals.post, |
|
||||
['published', 'mod-warn'], |
|
||||
res.locals.pagination, |
|
||||
); |
|
||||
|
|
||||
res.render('post/view'); |
|
||||
} catch (error) { |
|
||||
this.log.error('failed to service post view', { postId: res.locals.post._id, error }); |
|
||||
return next(error); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async getIndex (req, res, next) { |
|
||||
const { post: postService } = this.dtp.services; |
|
||||
try { |
|
||||
res.locals.pagination = this.getPaginationParameters(req, 20); |
|
||||
res.locals.posts = await postService.getPosts(res.locals.pagination); |
|
||||
res.render('post/index'); |
|
||||
} catch (error) { |
|
||||
return next(error); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
module.exports = async (dtp) => { |
|
||||
let controller = new PostController(dtp); |
|
||||
return controller; |
|
||||
}; |
|
@ -1,25 +0,0 @@ |
|||||
// category.js
|
|
||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||
// License: Apache-2.0
|
|
||||
|
|
||||
'use strict'; |
|
||||
|
|
||||
const mongoose = require('mongoose'); |
|
||||
|
|
||||
const Schema = mongoose.Schema; |
|
||||
|
|
||||
const CategorySchema = new Schema({ |
|
||||
name: { type: String }, |
|
||||
slug: { type: String, lowercase: true, required: true, index: 1 }, |
|
||||
description: { type: String }, |
|
||||
images: { |
|
||||
header: { type: Schema.ObjectId }, |
|
||||
icon: { type: Schema.ObjectId }, |
|
||||
}, |
|
||||
stats: { |
|
||||
articleCount: { type: Number, default: 0, required: true }, |
|
||||
articleViewCount: { type: Number, default: 0, required: true }, |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
module.exports = mongoose.model('Category', CategorySchema); |
|
@ -1,44 +0,0 @@ |
|||||
// comment.js
|
|
||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||
// License: Apache-2.0
|
|
||||
|
|
||||
'use strict'; |
|
||||
|
|
||||
const path = require('path'); |
|
||||
|
|
||||
const mongoose = require('mongoose'); |
|
||||
const Schema = mongoose.Schema; |
|
||||
|
|
||||
const CommentHistorySchema = new Schema({ |
|
||||
created: { type: Date }, |
|
||||
content: { type: String, maxlength: 3000 }, |
|
||||
}); |
|
||||
|
|
||||
const { CommentStats, CommentStatsDefaults } = require(path.join(__dirname, 'lib', 'resource-stats.js')); |
|
||||
|
|
||||
const COMMENT_STATUS_LIST = ['published', 'removed', 'mod-warn', 'mod-removed']; |
|
||||
|
|
||||
const CommentSchema = new Schema({ |
|
||||
created: { type: Date, default: Date.now, required: true, index: 1 }, |
|
||||
resourceType: { type: String, enum: ['Post', 'Page', 'Newsletter'], required: true }, |
|
||||
resource: { type: Schema.ObjectId, required: true, index: 1, refPath: 'resourceType' }, |
|
||||
author: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, |
|
||||
replyTo: { type: Schema.ObjectId, index: 1, ref: 'Comment' }, |
|
||||
status: { type: String, enum: COMMENT_STATUS_LIST, default: 'published', required: true }, |
|
||||
content: { type: String, required: true, maxlength: 3000 }, |
|
||||
contentHistory: { type: [CommentHistorySchema], select: false }, |
|
||||
stats: { type: CommentStats, default: CommentStatsDefaults, required: true }, |
|
||||
}); |
|
||||
|
|
||||
/* |
|
||||
* An index to optimize finding replies to a specific comment |
|
||||
*/ |
|
||||
CommentSchema.index({ |
|
||||
resource: 1, |
|
||||
replyTo: 1, |
|
||||
}, { |
|
||||
partialFilterExpression: { $exists: { replyTo: 1 } }, |
|
||||
name: 'comment_replies', |
|
||||
}); |
|
||||
|
|
||||
module.exports = mongoose.model('Comment', CommentSchema); |
|
@ -0,0 +1,21 @@ |
|||||
|
// link.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
|
||||
|
const Schema = mongoose.Schema; |
||||
|
|
||||
|
const { ResourceStats, ResourceStatsDefaults } = require('./lib/resource-stats'); |
||||
|
|
||||
|
const LinkSchema = new Schema({ |
||||
|
created: { type: Date, required: true, default: Date.now, index: -1, expires: '7d' }, |
||||
|
user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, |
||||
|
label: { type: String, required: true, maxlength: 100 }, |
||||
|
href: { type: String, required: true, maxlength: 255 }, |
||||
|
stats: { type: ResourceStats, default: ResourceStatsDefaults, required: true }, |
||||
|
}); |
||||
|
|
||||
|
module.exports = mongoose.model('Link', LinkSchema); |
@ -1,28 +0,0 @@ |
|||||
// page.js
|
|
||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||
// License: Apache-2.0
|
|
||||
|
|
||||
'use strict'; |
|
||||
|
|
||||
const mongoose = require('mongoose'); |
|
||||
const Schema = mongoose.Schema; |
|
||||
|
|
||||
const PAGE_STATUS_LIST = ['draft','published','archived']; |
|
||||
|
|
||||
const PageSchema = new Schema({ |
|
||||
title: { type: String, required: true }, |
|
||||
slug: { type: String, required: true, lowercase: true, unique: true }, |
|
||||
image: { |
|
||||
header: { type: Schema.ObjectId, ref: 'Image' }, |
|
||||
icon: { type: Schema.ObjectId, ref: 'Image' }, |
|
||||
}, |
|
||||
content: { type: String, required: true, select: false }, |
|
||||
status: { type: String, enum: PAGE_STATUS_LIST, default: 'draft', index: true }, |
|
||||
menu: { |
|
||||
label: { type: String, required: true }, |
|
||||
order: { type: Number, default: 0, required: true }, |
|
||||
parent: { type: Schema.ObjectId, index: 1, ref: 'Page' }, |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
module.exports = mongoose.model('Page', PageSchema); |
|
@ -1,33 +0,0 @@ |
|||||
// post.js
|
|
||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||
// License: Apache-2.0
|
|
||||
|
|
||||
'use strict'; |
|
||||
|
|
||||
const path = require('path'); |
|
||||
const mongoose = require('mongoose'); |
|
||||
|
|
||||
const Schema = mongoose.Schema; |
|
||||
|
|
||||
const { ResourceStats, ResourceStatsDefaults } = require(path.join(__dirname, 'lib', 'resource-stats.js')); |
|
||||
|
|
||||
const POST_STATUS_LIST = ['draft','published','archived']; |
|
||||
|
|
||||
const PostSchema = new Schema({ |
|
||||
created: { type: Date, default: Date.now, required: true, index: -1 }, |
|
||||
updated: { type: Date }, |
|
||||
author: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, |
|
||||
image: { type: Schema.ObjectId, ref: 'Image' }, |
|
||||
title: { type: String, required: true }, |
|
||||
slug: { type: String, required: true, lowercase: true, unique: true }, |
|
||||
summary: { type: String, required: true }, |
|
||||
content: { type: String, required: true, select: false }, |
|
||||
status: { type: String, enum: POST_STATUS_LIST, default: 'draft', index: true }, |
|
||||
stats: { type: ResourceStats, default: ResourceStatsDefaults, required: true }, |
|
||||
flags: { |
|
||||
enableComments: { type: Boolean, default: true, index: true }, |
|
||||
isFeatured: { type: Boolean, default: false, index: true }, |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
module.exports = mongoose.model('Post', PostSchema); |
|
@ -1,173 +0,0 @@ |
|||||
// comment.js
|
|
||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||
// License: Apache-2.0
|
|
||||
|
|
||||
'use strict'; |
|
||||
|
|
||||
const path = require('path'); |
|
||||
|
|
||||
const mongoose = require('mongoose'); |
|
||||
|
|
||||
const Comment = mongoose.model('Comment'); // jshint ignore:line
|
|
||||
|
|
||||
const pug = require('pug'); |
|
||||
const striptags = require('striptags'); |
|
||||
|
|
||||
const { SiteService, SiteError } = require('../../lib/site-lib'); |
|
||||
|
|
||||
class CommentService extends SiteService { |
|
||||
|
|
||||
constructor (dtp) { |
|
||||
super(dtp, module.exports); |
|
||||
this.populateComment = [ |
|
||||
{ |
|
||||
path: 'author', |
|
||||
select: '', |
|
||||
}, |
|
||||
{ |
|
||||
path: 'replyTo', |
|
||||
}, |
|
||||
]; |
|
||||
} |
|
||||
|
|
||||
async start ( ) { |
|
||||
this.templates = { }; |
|
||||
this.templates.comment = pug.compileFile(path.join(this.dtp.config.root, 'app', 'views', 'comment', 'components', 'comment-standalone.pug')); |
|
||||
} |
|
||||
|
|
||||
async create (author, resourceType, resource, commentDefinition) { |
|
||||
const NOW = new Date(); |
|
||||
let comment = new Comment(); |
|
||||
|
|
||||
comment.created = NOW; |
|
||||
comment.resourceType = resourceType; |
|
||||
comment.resource = resource._id; |
|
||||
comment.author = author._id; |
|
||||
if (commentDefinition.replyTo) { |
|
||||
comment.replyTo = mongoose.Types.ObjectId(commentDefinition.replyTo); |
|
||||
} |
|
||||
if (commentDefinition.content) { |
|
||||
comment.content = striptags(commentDefinition.content.trim()); |
|
||||
} |
|
||||
|
|
||||
await comment.save(); |
|
||||
|
|
||||
const model = mongoose.model(resourceType); |
|
||||
await model.updateOne( |
|
||||
{ _id: resource._id }, |
|
||||
{ |
|
||||
$inc: { 'stats.commentCount': 1 }, |
|
||||
}, |
|
||||
); |
|
||||
|
|
||||
/* |
|
||||
* increment the reply count of every parent comment until you reach a |
|
||||
* comment with no parent. |
|
||||
*/ |
|
||||
|
|
||||
let replyTo = comment.replyTo; |
|
||||
while (replyTo) { |
|
||||
await Comment.updateOne( |
|
||||
{ _id: replyTo }, |
|
||||
{ |
|
||||
$inc: { 'stats.replyCount': 1 }, |
|
||||
}, |
|
||||
); |
|
||||
let parent = await Comment.findById(replyTo).select('replyTo').lean(); |
|
||||
if (parent.replyTo) { |
|
||||
replyTo = parent.replyTo; |
|
||||
} else { |
|
||||
replyTo = false; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
comment = comment.toObject(); |
|
||||
comment.author = author; |
|
||||
return comment; |
|
||||
} |
|
||||
|
|
||||
async update (comment, commentDefinition) { |
|
||||
const updateOp = { $set: { } }; |
|
||||
|
|
||||
if (!commentDefinition.content || (commentDefinition.content.length === 0)) { |
|
||||
throw new SiteError(406, 'The comment cannot be empty'); |
|
||||
} |
|
||||
updateOp.$set.content = striptags(commentDefinition.content.trim()); |
|
||||
updateOp.$push = { |
|
||||
contentHistory: { |
|
||||
created: new Date(), |
|
||||
content: comment.content, |
|
||||
}, |
|
||||
}; |
|
||||
this.log.info('updating comment content', { commentId: comment._id }); |
|
||||
await Comment.updateOne({ _id: comment._id }, updateOp); |
|
||||
} |
|
||||
|
|
||||
async setStatus (comment, status) { |
|
||||
await Comment.updateOne({ _id: comment._id }, { $set: { status } }); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* Pushes the current comment content to the contentHistory array, sets the |
|
||||
* content field to 'Content removed' and updates the comment status to the |
|
||||
* status provided. This preserves the comment content, but removes it from |
|
||||
* public view. |
|
||||
* @param {Document} comment |
|
||||
* @param {String} status |
|
||||
*/ |
|
||||
async remove (comment, status = 'removed') { |
|
||||
await Comment.updateOne( |
|
||||
{ _id: comment._id }, |
|
||||
{ |
|
||||
$set: { |
|
||||
status, |
|
||||
content: 'Comment removed', |
|
||||
}, |
|
||||
$push: { |
|
||||
contentHistory: { |
|
||||
created: new Date(), |
|
||||
content: comment.content, |
|
||||
}, |
|
||||
}, |
|
||||
}, |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
async getForResource (resource, statuses, pagination) { |
|
||||
const comments = await Comment |
|
||||
.find({ resource: resource._id, status: { $in: statuses } }) |
|
||||
.sort({ created: 1 }) |
|
||||
.skip(pagination.skip) |
|
||||
.limit(pagination.cpp) |
|
||||
.populate(this.populateComment) |
|
||||
.lean(); |
|
||||
return comments; |
|
||||
} |
|
||||
|
|
||||
async getContentHistory (comment, pagination) { |
|
||||
/* |
|
||||
* Extract a page from the contentHistory using $slice on the array |
|
||||
*/ |
|
||||
const fullComment = await Comment |
|
||||
.findOne( |
|
||||
{ _id: comment._id }, |
|
||||
{ |
|
||||
contentHistory: { |
|
||||
$sort: { created: 1 }, |
|
||||
$slice: [pagination.skip, pagination.cpp], |
|
||||
}, |
|
||||
} |
|
||||
) |
|
||||
.select('contentHistory').lean(); |
|
||||
if (!fullComment) { |
|
||||
throw new SiteError(404, 'Comment not found'); |
|
||||
} |
|
||||
return fullComment.contentHistory || [ ]; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
module.exports = { |
|
||||
slug: 'comment', |
|
||||
name: 'comment', |
|
||||
create: (dtp) => { return new CommentService(dtp); }, |
|
||||
}; |
|
@ -0,0 +1,80 @@ |
|||||
|
// minio.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
const Link = mongoose.model('Link'); |
||||
|
|
||||
|
const striptags = require('striptags'); |
||||
|
|
||||
|
const { SiteService } = require('../../lib/site-lib'); |
||||
|
|
||||
|
class LinkService extends SiteService { |
||||
|
|
||||
|
constructor (dtp) { |
||||
|
super(dtp, module.exports); |
||||
|
|
||||
|
this.populateLink = [ |
||||
|
{ |
||||
|
path: 'user', |
||||
|
select: '_id username username_lc displayName picture', |
||||
|
}, |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
async create (user, linkDefinition) { |
||||
|
const NOW = new Date(); |
||||
|
const link = new Link(); |
||||
|
|
||||
|
link.created = NOW; |
||||
|
link.user = user._id; |
||||
|
link.label = striptags(linkDefinition.label.trim()); |
||||
|
link.href = striptags(linkDefinition.href.trim()); |
||||
|
|
||||
|
await link.save(); |
||||
|
|
||||
|
return link.toObject(); |
||||
|
} |
||||
|
|
||||
|
async update (link, linkDefinition) { |
||||
|
const updateOp = { $set: { } }; |
||||
|
|
||||
|
if (linkDefinition.label) { |
||||
|
updateOp.$set.label = striptags(linkDefinition.label.trim()); |
||||
|
} |
||||
|
if (linkDefinition.href) { |
||||
|
updateOp.$set.href = striptags(linkDefinition.href.trim()); |
||||
|
} |
||||
|
|
||||
|
await Link.updateOne({ _id: link._id }, updateOp); |
||||
|
} |
||||
|
|
||||
|
async getById (linkId) { |
||||
|
const link = await Link |
||||
|
.findById(linkId) |
||||
|
.populate(this.populateLink) |
||||
|
.lean(); |
||||
|
return link; |
||||
|
} |
||||
|
|
||||
|
async getForUser (user, pagination) { |
||||
|
const links = await Link |
||||
|
.find({ user: user._id }) |
||||
|
.skip(pagination.skip) |
||||
|
.limit(pagination.cpp) |
||||
|
.lean(); |
||||
|
return links; |
||||
|
} |
||||
|
|
||||
|
async remove (link) { |
||||
|
await Link.deleteOne({ _id: link._id }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
slug: 'link', |
||||
|
name: 'link', |
||||
|
create: (dtp) => { return new LinkService(dtp); }, |
||||
|
}; |
@ -1,161 +0,0 @@ |
|||||
// page.js
|
|
||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||
// License: Apache-2.0
|
|
||||
|
|
||||
'use strict'; |
|
||||
|
|
||||
const striptags = require('striptags'); |
|
||||
const slug = require('slug'); |
|
||||
|
|
||||
const { SiteService } = require('../../lib/site-lib'); |
|
||||
|
|
||||
const mongoose = require('mongoose'); |
|
||||
const ObjectId = mongoose.Types.ObjectId; |
|
||||
|
|
||||
const Page = mongoose.model('Page'); |
|
||||
|
|
||||
class PageService extends SiteService { |
|
||||
|
|
||||
constructor (dtp) { |
|
||||
super(dtp, module.exports); |
|
||||
} |
|
||||
|
|
||||
async menuMiddleware (req, res, next) { |
|
||||
try { |
|
||||
const pages = await Page.find({ parent: { $exists: false } }).lean(); |
|
||||
res.locals.mainMenu = pages |
|
||||
.map((page) => { |
|
||||
return { |
|
||||
url: `/page/${page.slug}`, |
|
||||
label: page.menu.label, |
|
||||
order: page.menu.order, |
|
||||
}; |
|
||||
}) |
|
||||
.sort((a, b) => { |
|
||||
return a.order < b.order; |
|
||||
}); |
|
||||
return next(); |
|
||||
} catch (error) { |
|
||||
this.log.error('failed to build page menu', { error }); |
|
||||
return next(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
async create (author, pageDefinition) { |
|
||||
const page = new Page(); |
|
||||
page.title = striptags(pageDefinition.title.trim()); |
|
||||
page.slug = this.createPageSlug(page._id, page.title); |
|
||||
page.content = pageDefinition.content.trim(); |
|
||||
page.status = pageDefinition.status || 'draft'; |
|
||||
page.menu = { |
|
||||
label: pageDefinition.menuLabel || page.title.slice(0, 10), |
|
||||
order: parseInt(pageDefinition.menuOrder || '0', 10), |
|
||||
}; |
|
||||
if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) { |
|
||||
page.menu.parent = pageDefinition.parentPageId; |
|
||||
} |
|
||||
await page.save(); |
|
||||
|
|
||||
return page.toObject(); |
|
||||
} |
|
||||
|
|
||||
async update (page, pageDefinition) { |
|
||||
const NOW = new Date(); |
|
||||
const updateOp = { |
|
||||
$set: { |
|
||||
updated: NOW, |
|
||||
}, |
|
||||
}; |
|
||||
|
|
||||
if (pageDefinition.title) { |
|
||||
updateOp.$set.title = striptags(pageDefinition.title.trim()); |
|
||||
} |
|
||||
if (pageDefinition.slug) { |
|
||||
let pageSlug = striptags(slug(pageDefinition.slug.trim())).split('-'); |
|
||||
while (ObjectId.isValid(pageSlug[pageSlug.length - 1])) { |
|
||||
pageSlug.pop(); |
|
||||
} |
|
||||
pageSlug = pageSlug.splice(0, 4); |
|
||||
pageSlug.push(page._id.toString()); |
|
||||
updateOp.$set.slug = `${pageSlug.join('-')}`; |
|
||||
} |
|
||||
if (pageDefinition.summary) { |
|
||||
updateOp.$set.summary = striptags(pageDefinition.summary.trim()); |
|
||||
} |
|
||||
if (pageDefinition.content) { |
|
||||
updateOp.$set.content = pageDefinition.content.trim(); |
|
||||
} |
|
||||
if (pageDefinition.status) { |
|
||||
updateOp.$set.status = striptags(pageDefinition.status.trim()); |
|
||||
} |
|
||||
|
|
||||
updateOp.$set.menu = { |
|
||||
label: pageDefinition.menuLabel || updateOp.$set.title.slice(0, 10), |
|
||||
order: parseInt(pageDefinition.menuOrder || '0', 10), |
|
||||
}; |
|
||||
if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) { |
|
||||
updateOp.$set.menu.parent = pageDefinition.parentPageId; |
|
||||
} |
|
||||
|
|
||||
await Page.updateOne( |
|
||||
{ _id: page._id }, |
|
||||
updateOp, |
|
||||
{ upsert: true }, |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
async getPages (pagination, status = ['published']) { |
|
||||
if (!Array.isArray(status)) { |
|
||||
status = [status]; |
|
||||
} |
|
||||
const pages = await Page |
|
||||
.find({ status: { $in: status } }) |
|
||||
.sort({ created: -1 }) |
|
||||
.skip(pagination.skip) |
|
||||
.limit(pagination.cpp) |
|
||||
.lean(); |
|
||||
return pages; |
|
||||
} |
|
||||
|
|
||||
async getById (pageId) { |
|
||||
const page = await Page |
|
||||
.findById(pageId) |
|
||||
.select('+content') |
|
||||
.lean(); |
|
||||
return page; |
|
||||
} |
|
||||
|
|
||||
async getBySlug (pageSlug) { |
|
||||
const slugParts = pageSlug.split('-'); |
|
||||
const pageId = slugParts[slugParts.length - 1]; |
|
||||
return this.getById(pageId); |
|
||||
} |
|
||||
|
|
||||
async getAvailablePages (excludedPageIds) { |
|
||||
const search = { }; |
|
||||
if (excludedPageIds) { |
|
||||
search._id = { $nin: excludedPageIds }; |
|
||||
} |
|
||||
const pages = await Page.find(search).lean(); |
|
||||
return pages; |
|
||||
} |
|
||||
|
|
||||
async deletePage (page) { |
|
||||
this.log.info('deleting page', { pageId: page._id }); |
|
||||
await Page.deleteOne({ _id: page._id }); |
|
||||
} |
|
||||
|
|
||||
createPageSlug (pageId, pageTitle) { |
|
||||
if ((typeof pageTitle !== 'string') || (pageTitle.length < 1)) { |
|
||||
throw new Error('Invalid input for making a page slug'); |
|
||||
} |
|
||||
const pageSlug = slug(pageTitle.trim().toLowerCase()).split('-').slice(0, 4).join('-'); |
|
||||
return `${pageSlug}-${pageId}`; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
module.exports = { |
|
||||
slug: 'page', |
|
||||
name: 'page', |
|
||||
create: (dtp) => { return new PageService(dtp); }, |
|
||||
}; |
|
@ -1,138 +0,0 @@ |
|||||
// post.js
|
|
||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
||||
// License: Apache-2.0
|
|
||||
|
|
||||
'use strict'; |
|
||||
|
|
||||
const striptags = require('striptags'); |
|
||||
const slug = require('slug'); |
|
||||
|
|
||||
const { SiteService } = require('../../lib/site-lib'); |
|
||||
|
|
||||
const mongoose = require('mongoose'); |
|
||||
const ObjectId = mongoose.Types.ObjectId; |
|
||||
|
|
||||
const Post = mongoose.model('Post'); |
|
||||
|
|
||||
class PostService extends SiteService { |
|
||||
|
|
||||
constructor (dtp) { |
|
||||
super(dtp, module.exports); |
|
||||
|
|
||||
this.populatePost = [ |
|
||||
{ |
|
||||
path: 'author', |
|
||||
select: '_id username username_lc displayName picture', |
|
||||
}, |
|
||||
]; |
|
||||
} |
|
||||
|
|
||||
async create (author, postDefinition) { |
|
||||
const NOW = new Date(); |
|
||||
|
|
||||
const post = new Post(); |
|
||||
post.created = NOW; |
|
||||
post.author = author._id; |
|
||||
post.title = striptags(postDefinition.title.trim()); |
|
||||
post.slug = this.createPostSlug(post._id, post.title); |
|
||||
post.summary = striptags(postDefinition.summary.trim()); |
|
||||
post.content = postDefinition.content.trim(); |
|
||||
post.status = postDefinition.status || 'draft'; |
|
||||
post.flags = { |
|
||||
enableComments: postDefinition.enableComments === 'on', |
|
||||
isFeatured: postDefinition.isFeatured === 'on', |
|
||||
}; |
|
||||
|
|
||||
await post.save(); |
|
||||
|
|
||||
return post.toObject(); |
|
||||
} |
|
||||
|
|
||||
async update (post, postDefinition) { |
|
||||
const NOW = new Date(); |
|
||||
const updateOp = { |
|
||||
$set: { |
|
||||
updated: NOW, |
|
||||
}, |
|
||||
}; |
|
||||
|
|
||||
if (postDefinition.title) { |
|
||||
updateOp.$set.title = striptags(postDefinition.title.trim()); |
|
||||
updateOp.$set.slug = this.createPostSlug(post._id, updateOp.$set.title); |
|
||||
} |
|
||||
if (postDefinition.slug) { |
|
||||
let postSlug = striptags(slug(postDefinition.slug.trim())).split('-'); |
|
||||
while (ObjectId.isValid(postSlug[postSlug.length - 1])) { |
|
||||
postSlug.pop(); |
|
||||
} |
|
||||
postSlug = postSlug.splice(0, 4); |
|
||||
postSlug.push(post._id.toString()); |
|
||||
updateOp.$set.slug = `${postSlug.join('-')}`; |
|
||||
} |
|
||||
if (postDefinition.summary) { |
|
||||
updateOp.$set.summary = striptags(postDefinition.summary.trim()); |
|
||||
} |
|
||||
if (postDefinition.content) { |
|
||||
updateOp.$set.content = postDefinition.content.trim(); |
|
||||
} |
|
||||
if (postDefinition.status) { |
|
||||
updateOp.$set.status = striptags(postDefinition.status.trim()); |
|
||||
} |
|
||||
|
|
||||
updateOp.$set['flags.enableComments'] = postDefinition.enableComments === 'on'; |
|
||||
updateOp.$set['flags.isFeatured'] = postDefinition.isFeatured === 'on'; |
|
||||
|
|
||||
await Post.updateOne( |
|
||||
{ _id: post._id }, |
|
||||
updateOp, |
|
||||
{ upsert: true }, |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
async getPosts (pagination, status = ['published']) { |
|
||||
if (!Array.isArray(status)) { |
|
||||
status = [status]; |
|
||||
} |
|
||||
const posts = await Post |
|
||||
.find({ status: { $in: status } }) |
|
||||
.sort({ created: -1 }) |
|
||||
.skip(pagination.skip) |
|
||||
.limit(pagination.cpp) |
|
||||
.lean(); |
|
||||
return posts; |
|
||||
} |
|
||||
|
|
||||
async getById (postId) { |
|
||||
const post = await Post |
|
||||
.findById(postId) |
|
||||
.select('+content') |
|
||||
.populate(this.populatePost) |
|
||||
.lean(); |
|
||||
return post; |
|
||||
} |
|
||||
|
|
||||
async getBySlug (postSlug) { |
|
||||
const slugParts = postSlug.split('-'); |
|
||||
const postId = slugParts[slugParts.length - 1]; |
|
||||
return this.getById(postId); |
|
||||
} |
|
||||
|
|
||||
async deletePost (post) { |
|
||||
this.log.info('deleting post', { postId: post._id }); |
|
||||
await Post.deleteOne({ _id: post._id }); |
|
||||
} |
|
||||
|
|
||||
createPostSlug (postId, postTitle) { |
|
||||
if ((typeof postTitle !== 'string') || (postTitle.length < 1)) { |
|
||||
throw new Error('Invalid input for making a post slug'); |
|
||||
} |
|
||||
const postSlug = slug(postTitle.trim().toLowerCase()).split('-').slice(0, 4).join('-'); |
|
||||
return `${postSlug}-${postId}`; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
module.exports = { |
|
||||
slug: 'post', |
|
||||
name: 'post', |
|
||||
create: (dtp) => { return new PostService(dtp); }, |
|
||||
}; |
|
@ -1,17 +0,0 @@ |
|||||
extends ../layouts/main |
|
||||
block content |
|
||||
|
|
||||
- var formAction = category ? `/admin/category/${category._id}` : '/admin/category'; |
|
||||
|
|
||||
pre= JSON.stringify(category, null, 2) |
|
||||
|
|
||||
form(method="POST", action= formAction).uk-form |
|
||||
.uk-margin |
|
||||
label(for="name").uk-form-label Category Name |
|
||||
input(id="name", name="name", type="text", placeholder="Enter category name", value= category ? category.name : undefined).uk-input |
|
||||
|
|
||||
.uk-margin |
|
||||
label(for="description").uk-form-label Description |
|
||||
textarea(id="description", name="description", rows="3", placeholder="Enter category description").uk-textarea= category ? category.description : undefined |
|
||||
|
|
||||
button(type="submit").uk-button.uk-button-primary= category ? 'Update Category' : 'Create Category' |
|
@ -1,21 +0,0 @@ |
|||||
extends ../layouts/main |
|
||||
block content |
|
||||
|
|
||||
.uk-margin |
|
||||
div(uk-grid).uk-flex-middle |
|
||||
.uk-width-expand |
|
||||
h2 Category Manager |
|
||||
.uk-width-auto |
|
||||
a(href="/admin/category/create").uk-button.uk-button-primary |
|
||||
span |
|
||||
i.fas.fa-plus |
|
||||
span.uk-margin-small-left Add category |
|
||||
|
|
||||
.uk-margin |
|
||||
if Array.isArray(categories) && (categories.length > 0) |
|
||||
uk.uk-list |
|
||||
each category in categories |
|
||||
li |
|
||||
a(href=`/admin/category/${category._id}`)= category.name |
|
||||
else |
|
||||
h4 There are no categories. |
|
@ -1,89 +0,0 @@ |
|||||
extends ../layouts/main |
|
||||
block content |
|
||||
|
|
||||
- var actionUrl = page ? `/admin/page/${page._id}` : `/admin/page`; |
|
||||
|
|
||||
form(method="POST", action= actionUrl).uk-form |
|
||||
div(uk-grid).uk-grid-small |
|
||||
div(class="uk-width-1-1 uk-width-2-3@m") |
|
||||
.uk-margin |
|
||||
label(for="content").uk-form-label Page body |
|
||||
textarea(id="content", name="content", rows="4").uk-textarea= page ? page.content : undefined |
|
||||
|
|
||||
div(class="uk-width-1-1 uk-width-1-3@m") |
|
||||
.uk-margin |
|
||||
label(for="title").uk-form-label Page title |
|
||||
input(id="title", name="title", type="text", placeholder= "Enter page title", value= page ? page.title : undefined).uk-input |
|
||||
.uk-margin |
|
||||
label(for="slug").uk-form-label URL slug |
|
||||
- |
|
||||
var pageSlug; |
|
||||
pageSlug = page ? (page.slug || 'enter-slug-here').split('-') : ['enter', 'slug', 'here', '']; |
|
||||
pageSlug.pop(); |
|
||||
pageSlug = pageSlug.join('-'); |
|
||||
input(id="slug", name="slug", type="text", placeholder= "Enter page URL slug", value= page ? pageSlug : undefined).uk-input |
|
||||
.uk-text-small The slug is used in the link to the page https://#{site.domain}/page/#{pageSlug} |
|
||||
div(uk-grid) |
|
||||
.uk-width-auto |
|
||||
button(type="submit").uk-button.dtp-button-primary= page ? 'Update page' : 'Create page' |
|
||||
.uk-margin |
|
||||
label(for="status").uk-form-label Status |
|
||||
select(id="status", name="status").uk-select |
|
||||
option(value="draft", selected= page ? page.status === 'draft' : true) Draft |
|
||||
option(value="published", selected= page ? page.status === 'published' : false) Published |
|
||||
option(value="archived", selected= page ? page.status === 'archived' : false) Archived |
|
||||
|
|
||||
fieldset |
|
||||
legend Menu |
|
||||
.uk-margin |
|
||||
label(for="menu-label").uk-form-label Menu item label |
|
||||
input(id="menu-label", name="menuLabel", type="text", maxlength="80", placeholder="Enter label", value= page ? page.menu.label : undefined).uk-input |
|
||||
.uk-margin |
|
||||
label(for="menu-order").uk-form-label Menu item order |
|
||||
input(id="menu-order", name="menuOrder", type="number", min="0", value= page ? page.menu.order : 0).uk-input |
|
||||
if Array.isArray(availablePages) && (availablePages.length > 0) |
|
||||
.uk-margin |
|
||||
label(for="menu-parent").uk-form-label Parent page |
|
||||
select(id="menu-parent", name="parentPageId").uk-select |
|
||||
option(value= "none") --- Select parent page --- |
|
||||
each menuPage in availablePages |
|
||||
option(value= menuPage._id)= menuPage.title |
|
||||
block viewjs |
|
||||
script(src="/tinymce/tinymce.min.js") |
|
||||
script. |
|
||||
window.addEventListener('dtp-load', async ( ) => { |
|
||||
const toolbarItems = [ |
|
||||
'undo redo', |
|
||||
'formatselect visualblocks', |
|
||||
'bold italic backcolor', |
|
||||
'alignleft aligncenter alignright alignjustify', |
|
||||
'bullist numlist outdent indent removeformat', |
|
||||
'link image code', |
|
||||
'help' |
|
||||
]; |
|
||||
const pluginItems = [ |
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'print', |
|
||||
'preview', 'anchor', 'searchreplace', 'visualblocks', 'code', |
|
||||
'fullscreen', 'insertdatetime', 'media', 'table', 'paste', 'code', |
|
||||
'help', 'wordcount', |
|
||||
] |
|
||||
|
|
||||
const editors = await tinymce.init({ |
|
||||
selector: 'textarea#content', |
|
||||
height: 500, |
|
||||
menubar: false, |
|
||||
plugins: pluginItems.join(' '), |
|
||||
toolbar: toolbarItems.join('|'), |
|
||||
branding: false, |
|
||||
images_upload_url: '/image/tinymce', |
|
||||
image_class_list: [ |
|
||||
{ title: 'Body Image', value: 'dtp-image-body' }, |
|
||||
{ title: 'Title Image', value: 'dtp-image-title' }, |
|
||||
], |
|
||||
convert_urls: false, |
|
||||
skin: "oxide-dark", |
|
||||
content_css: "dark", |
|
||||
}); |
|
||||
|
|
||||
window.dtp.app.editor = editors[0]; |
|
||||
}); |
|
@ -1,43 +0,0 @@ |
|||||
extends ../layouts/main |
|
||||
block content |
|
||||
|
|
||||
.uk-margin |
|
||||
div(uk-grid) |
|
||||
.uk-width-expand |
|
||||
h1.uk-text-truncate Pages |
|
||||
.uk-width-auto |
|
||||
a(href="/admin/page/compose").uk-button.dtp-button-primary |
|
||||
+renderButtonIcon('fa-plus', 'New Page') |
|
||||
|
|
||||
.uk-margin |
|
||||
if (Array.isArray(pages) && (pages.length > 0)) |
|
||||
|
|
||||
ul.uk-list |
|
||||
|
|
||||
each page in pages |
|
||||
|
|
||||
li(data-page-id= page._id) |
|
||||
div(uk-grid).uk-grid-small.uk-flex-middle |
|
||||
.uk-width-expand |
|
||||
a(href=`/page/${page.slug}`).uk-display-block.uk-text-large.uk-text-truncate #{page.title} |
|
||||
|
|
||||
.uk-width-auto |
|
||||
div(uk-grid).uk-grid-small.uk-flex-middle |
|
||||
.uk-width-auto(class={ |
|
||||
'uk-text-info': (page.status === 'draft'), |
|
||||
'uk-text-success': (page.status === 'published'), |
|
||||
'uk-text-danger': (page.status === 'archived'), |
|
||||
})= page.status |
|
||||
.uk-width-auto |
|
||||
a(href=`/admin/page/${page._id}`).uk-button.dtp-button-primary |
|
||||
+renderButtonIcon('fa-pen', 'Edit') |
|
||||
.uk-width-auto |
|
||||
button( |
|
||||
type="button", |
|
||||
data-page-id= page._id, |
|
||||
data-page-title= page.title, |
|
||||
onclick="return dtp.adminApp.deletePage(event);", |
|
||||
).uk-button.dtp-button-danger |
|
||||
+renderButtonIcon('fa-trash', 'Delete') |
|
||||
else |
|
||||
div There are no pages at this time. |
|
@ -1,89 +0,0 @@ |
|||||
extends ../layouts/main |
|
||||
block content |
|
||||
|
|
||||
- var actionUrl = post ? `/admin/post/${post._id}` : `/admin/post`; |
|
||||
|
|
||||
form(method="POST", action= actionUrl).uk-form |
|
||||
div(uk-grid).uk-grid-small |
|
||||
div(class="uk-width-1-1 uk-width-2-3@m") |
|
||||
.uk-margin |
|
||||
label(for="content").uk-form-label Post body |
|
||||
textarea(id="content", name="content", rows="4").uk-textarea= post ? post.content : undefined |
|
||||
|
|
||||
div(class="uk-width-1-1 uk-width-1-3@m") |
|
||||
.uk-margin |
|
||||
label(for="title").uk-form-label Post title |
|
||||
input(id="title", name="title", type="text", placeholder= "Enter post title", value= post ? post.title : undefined).uk-input |
|
||||
.uk-margin |
|
||||
label(for="slug").uk-form-label URL slug |
|
||||
- |
|
||||
var postSlug; |
|
||||
if (post) { |
|
||||
postSlug = post.slug.split('-'); |
|
||||
postSlug.pop(); |
|
||||
postSlug = postSlug.join('-'); |
|
||||
} |
|
||||
input(id="slug", name="slug", type="text", placeholder= "Enter post URL slug", value= post ? postSlug : undefined).uk-input |
|
||||
.uk-text-small The slug is used in the link to the page https://#{site.domain}/post/#{post ? post.slug : 'your-slug-here'} |
|
||||
.uk-margin |
|
||||
label(for="summary").uk-form-label Post summary |
|
||||
textarea(id="summary", name="summary", rows="4", placeholder= "Enter post summary (text only, no HTML)").uk-textarea= post ? post.summary : undefined |
|
||||
div(uk-grid) |
|
||||
.uk-width-auto |
|
||||
button(type="submit").uk-button.dtp-button-primary= post ? 'Update post' : 'Create post' |
|
||||
.uk-margin |
|
||||
label(for="status").uk-form-label Status |
|
||||
select(id="status", name="status").uk-select |
|
||||
option(value="draft", selected= post ? post.status === 'draft' : true) Draft |
|
||||
option(value="published", selected= post ? post.status === 'published' : false) Published |
|
||||
option(value="archived", selected= post ? post.status === 'archived' : false) Archived |
|
||||
.uk-margin |
|
||||
div(uk-grid).uk-grid-small |
|
||||
.uk-width-auto |
|
||||
label |
|
||||
input(id="enable-comments", name="enableComments", type="checkbox", checked= post ? post.flags.enableComments : true).uk-checkbox |
|
||||
| Enable comments |
|
||||
.uk-width-auto |
|
||||
label |
|
||||
input(id="is-featured", name="isFeatured", type="checkbox", checked= post ? post.flags.isFeatured : false).uk-checkbox |
|
||||
| Featured |
|
||||
|
|
||||
block viewjs |
|
||||
script(src="/tinymce/tinymce.min.js") |
|
||||
script. |
|
||||
window.addEventListener('dtp-load', async ( ) => { |
|
||||
const toolbarItems = [ |
|
||||
'undo redo', |
|
||||
'formatselect visualblocks', |
|
||||
'bold italic backcolor', |
|
||||
'alignleft aligncenter alignright alignjustify', |
|
||||
'bullist numlist outdent indent removeformat', |
|
||||
'link image code', |
|
||||
'help' |
|
||||
]; |
|
||||
const pluginItems = [ |
|
||||
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'print', |
|
||||
'preview', 'anchor', 'searchreplace', 'visualblocks', 'code', |
|
||||
'fullscreen', 'insertdatetime', 'media', 'table', 'paste', 'code', |
|
||||
'help', 'wordcount', |
|
||||
] |
|
||||
|
|
||||
const editors = await tinymce.init({ |
|
||||
selector: 'textarea#content', |
|
||||
height: 500, |
|
||||
menubar: false, |
|
||||
plugins: pluginItems.join(' '), |
|
||||
toolbar: toolbarItems.join('|'), |
|
||||
branding: false, |
|
||||
images_upload_url: '/image/tinymce', |
|
||||
image_class_list: [ |
|
||||
{ title: 'Body Image', value: 'dtp-image-body' }, |
|
||||
{ title: 'Title Image', value: 'dtp-image-title' }, |
|
||||
], |
|
||||
convert_urls: false, |
|
||||
skin: "oxide-dark", |
|
||||
content_css: "dark", |
|
||||
}); |
|
||||
|
|
||||
window.dtp.app.editor = editors[0]; |
|
||||
}); |
|
@ -1,50 +0,0 @@ |
|||||
extends ../layouts/main |
|
||||
block content |
|
||||
|
|
||||
.uk-margin |
|
||||
div(uk-grid) |
|
||||
.uk-width-expand |
|
||||
h1.uk-text-truncate Posts |
|
||||
.uk-width-auto |
|
||||
a(href="/admin/post/compose").uk-button.dtp-button-primary |
|
||||
+renderButtonIcon('fa-plus', 'New Post') |
|
||||
|
|
||||
.uk-margin |
|
||||
if (Array.isArray(posts) && (posts.length > 0)) |
|
||||
|
|
||||
ul.uk-list |
|
||||
|
|
||||
each post in posts |
|
||||
|
|
||||
li(data-post-id= post._id) |
|
||||
div(uk-grid).uk-grid-small.uk-flex-middle |
|
||||
.uk-width-expand |
|
||||
a(href=`/post/${post.slug}`).uk-display-block.uk-text-large.uk-text-truncate #{post.title} |
|
||||
.uk-text-small |
|
||||
div(uk-grid).uk-grid-small |
|
||||
.uk-width-auto |
|
||||
span published: #{moment(post.created).format('MMM DD, YYYY [at] hh:mm:ss a')} |
|
||||
if post.updated |
|
||||
.uk-width-auto |
|
||||
span last update: #{moment(post.updated).format('MMM DD, YYYY [at] hh:mm:ss a')} |
|
||||
|
|
||||
.uk-width-auto |
|
||||
div(uk-grid).uk-grid-small.uk-flex-middle |
|
||||
.uk-width-auto(class={ |
|
||||
'uk-text-info': (post.status === 'draft'), |
|
||||
'uk-text-success': (post.status === 'published'), |
|
||||
'uk-text-danger': (post.status === 'archived'), |
|
||||
})= post.status |
|
||||
.uk-width-auto |
|
||||
a(href=`/admin/post/${post._id}`).uk-button.dtp-button-primary |
|
||||
+renderButtonIcon('fa-pen', 'Edit') |
|
||||
.uk-width-auto |
|
||||
button( |
|
||||
type="button", |
|
||||
data-post-id= post._id, |
|
||||
data-post-title= post.title, |
|
||||
onclick="return dtp.adminApp.deletePost(event);", |
|
||||
).uk-button.dtp-button-danger |
|
||||
+renderButtonIcon('fa-trash', 'Delete') |
|
||||
else |
|
||||
div There are no posts at this time. |
|
@ -1,10 +0,0 @@ |
|||||
mixin renderArticle (article) |
|
||||
article.uk-article |
|
||||
if article.image |
|
||||
img(src="/img/payment/payment-option.jpg").responsive |
|
||||
h1.uk-article-title= article.title |
|
||||
if article.meta |
|
||||
p.uk-article-meta= article.meta |
|
||||
if article.lead |
|
||||
p.-uk-text-lead= article.lead |
|
||||
div!= article.content |
|
@ -1,8 +0,0 @@ |
|||||
extends ../layouts/main |
|
||||
block content |
|
||||
|
|
||||
include components/article |
|
||||
|
|
||||
section.uk-section.uk-section-default |
|
||||
.uk-container |
|
||||
+renderArticle(article) |
|
@ -1,6 +0,0 @@ |
|||||
mixin renderCategoryListItem (category) |
|
||||
a(href=`/category/${category.slug}`).uk-display-block.uk-link-reset |
|
||||
img(src='/img/default-poster.jpg').uk-display-block.uk-margin-small.responsive.uk-border-rounded |
|
||||
.uk-link-reset.uk-text-bold= category.name |
|
||||
.uk-ling-reset.uk-text-muted #{numeral(category.stats.liveChannelCount).format("0,0")} live channels |
|
||||
.uk-ling-reset.uk-text-muted #{numeral(category.stats.currentViewerCount).format("0,0.0a")} viewers |
|
@ -1,17 +0,0 @@ |
|||||
extends ../layouts/main |
|
||||
block content |
|
||||
|
|
||||
include components/list-item |
|
||||
|
|
||||
section.uk-section.uk-section-default.uk-section-small |
|
||||
.uk-container.uk-container-expand |
|
||||
|
|
||||
if Array.isArray(categories) && (categories.length > 0) |
|
||||
div(uk-grid).uk-flex-center.uk-grid-small |
|
||||
each category in categories |
|
||||
.uk-width-auto |
|
||||
.uk-width-medium |
|
||||
.uk-margin |
|
||||
+renderCategoryListItem(category) |
|
||||
else |
|
||||
h4.uk-text-center There are no categories or the system is down for maintenance. |
|
@ -1,32 +0,0 @@ |
|||||
extends ../layouts/main |
|
||||
block content |
|
||||
|
|
||||
include ../channel/components/list-item |
|
||||
|
|
||||
section(style="font: Verdana;").uk-section.uk-section-muted.uk-section-small |
|
||||
.uk-container |
|
||||
div(uk-grid).uk-grid-small |
|
||||
.uk-width-auto |
|
||||
img(src="/img/default-poster.jpg").uk-width-small |
|
||||
.uk-width-expand |
|
||||
h1.uk-margin-remove.uk-padding-remove= category.name |
|
||||
div= category.description |
|
||||
div(uk-grid).uk-grid-small |
|
||||
.uk-width-auto #{category.stats.streamCount} live shows. |
|
||||
.uk-width-auto #{category.stats.viewerCount} total viewers. |
|
||||
|
|
||||
section.uk-section.uk-section-default |
|
||||
.uk-container |
|
||||
if Array.isArray(channels) && (channels.length > 0) |
|
||||
div(uk-grid).uk-flex-center.uk-grid-small |
|
||||
each channel in channels |
|
||||
div(class="uk-width-1-1 uk-width-1-2@s uk-width-1-3@m uk-width-1-4@l") |
|
||||
+renderChannelListItem(channel) |
|
||||
else |
|
||||
.uk-text-lead No channels in this category, check back later. |
|
||||
include ../components/back-button |
|
||||
|
|
||||
|
|
||||
//- pre= JSON.stringify(category, null, 2) |
|
||||
pre= JSON.stringify(category, null, 2) |
|
||||
pre= JSON.stringify(channels, null, 2) |
|
@ -1,3 +0,0 @@ |
|||||
include ../../components/library |
|
||||
include comment |
|
||||
+renderComment(comment) |
|
@ -1,47 +0,0 @@ |
|||||
mixin renderComment (comment) |
|
||||
.uk-card.uk-card-secondary.uk-card-small.uk-border-rounded |
|
||||
.uk-card-body |
|
||||
div(uk-grid).uk-grid-small |
|
||||
.uk-width-auto |
|
||||
img(src="/img/default-member.png").site-profile-picture.sb-small |
|
||||
.uk-width-expand |
|
||||
div(uk-grid).uk-grid-small.uk-flex-middle.uk-text-small |
|
||||
if comment.author.displayName |
|
||||
.uk-width-auto |
|
||||
span= comment.author.displayName |
|
||||
.uk-width-auto= comment.author.username |
|
||||
.uk-width-auto= moment(comment.created).fromNow() |
|
||||
div!= marked.parse(comment.content) |
|
||||
div(uk-grid).uk-grid-small.uk-text-small |
|
||||
.uk-width-auto |
|
||||
button( |
|
||||
type="button", |
|
||||
data-comment-id= comment._id, |
|
||||
onclick="return dtp.app.upvoteComment(event);", |
|
||||
title="Upvote this comment", |
|
||||
).uk-button.uk-button-link |
|
||||
+renderButtonIcon('fa-chevron-up', formatCount(comment.stats.upvoteCount)) |
|
||||
.uk-width-auto |
|
||||
button( |
|
||||
type="button", |
|
||||
data-comment-id= comment._id, |
|
||||
onclick="return dtp.app.downvoteComment(event);", |
|
||||
title="Downvote this comment", |
|
||||
).uk-button.uk-button-link |
|
||||
+renderButtonIcon('fa-chevron-down', formatCount(comment.stats.downvoteCount)) |
|
||||
.uk-width-auto |
|
||||
button( |
|
||||
type="button", |
|
||||
data-comment-id= comment._id, |
|
||||
onclick="return dtp.app.openReplies(event);", |
|
||||
title="Load replies to this comment", |
|
||||
).uk-button.uk-button-link |
|
||||
+renderButtonIcon('fa-comment', formatCount(comment.stats.replyCount)) |
|
||||
.uk-width-auto |
|
||||
button( |
|
||||
type="button", |
|
||||
data-comment-id= comment._id, |
|
||||
onclick="return dtp.app.openReplyComposer(event);", |
|
||||
title="Write a reply to this comment", |
|
||||
).uk-button.uk-button-link |
|
||||
+renderButtonIcon('fa-reply', 'reply') |
|
@ -1,12 +1,5 @@ |
|||||
section.uk-section.uk-section-muted.uk-section-small.dtp-site-footer |
section.uk-section.uk-section-default.uk-section-small.dtp-site-footer |
||||
.uk-container.uk-text-small.uk-text-center |
.uk-container.uk-text-small.uk-text-center |
||||
ul.uk-subnav.uk-flex-center |
span Copyright #{moment().format('YYYY')} |
||||
each socialIcon in socialIcons |
span |
||||
li |
+renderSiteLink() |
||||
a(href=socialIcon.url).dtp-social-link |
|
||||
span |
|
||||
i(class=`fab ${socialIcon.icon}`) |
|
||||
span.uk-margin-small-left= socialIcon.label |
|
||||
.uk-width-medium.uk-margin-auto |
|
||||
hr |
|
||||
div Copyright © 2021 #[+renderSiteLink()] |
|
@ -0,0 +1,7 @@ |
|||||
|
extends layouts/main |
||||
|
block content |
||||
|
|
||||
|
section.uk-section.uk-section-default |
||||
|
.uk-container.uk-container-expand |
||||
|
.uk-margin |
||||
|
+renderSectionTitle('My Links', { url: `/${user.username}`, label: 'My profile' }) |
@ -1,53 +1,18 @@ |
|||||
extends layouts/main-sidebar |
extends layouts/main |
||||
block content |
block content |
||||
|
|
||||
include components/page-sidebar |
section.uk-section.uk-section-default |
||||
|
.uk-container |
||||
|
.uk-margin.uk-text-center |
||||
|
h1.uk-margin-remove= site.name |
||||
|
.uk-text-lead= site.description |
||||
|
|
||||
mixin renderBlogPostListItem (post, postIndex = 1, postIndexModulus = 3) |
p #{site.name} provides a landing page people can use to display a collection of links. These can be used for people to help others find them on social media, ways to donate and support them, and more. |
||||
a(href=`/post/${post.slug}`).uk-display-block.uk-link-reset |
|
||||
div(uk-grid).uk-grid-small |
|
||||
|
|
||||
div(class='uk-visible@m', class={ |
p We do not allow the promotion of pornography or linking to it. This is otherwise a free-speech online service and will not ban people for their social or political views. |
||||
'uk-flex-first': ((postIndex % postIndexModulus) === 0), |
|
||||
'uk-flex-last': ((postIndex % postIndexModulus) !== 0), |
|
||||
}).uk-width-1-3 |
|
||||
img(src="/img/default-poster.jpg").responsive |
|
||||
|
|
||||
div(class='uk-width-1-1 uk-width-2-3@m', class={ |
div(uk-grid) |
||||
'uk-flex-first': ((postIndex % postIndexModulus) !== 0), |
.uk-width-1-2 |
||||
'uk-flex-last': ((postIndex % postIndexModulus) === 0), |
a(href="/welcome/signup").uk-button.dtp-button-primary.uk-display-block Get Started |
||||
}) |
.uk-width-1-2 |
||||
article.uk-article |
a(href="/welcome/login").uk-button.dtp-button-default.uk-display-block Login |
||||
h4(style="line-height: 1;").uk-article-title.uk-margin-remove= post.title |
|
||||
.uk-text-truncate= post.summary |
|
||||
.uk-article-meta |
|
||||
div(uk-grid).uk-grid-small |
|
||||
.uk-width-auto published: #{moment(post.created).format("MMM DD YYYY HH:MM a")} |
|
||||
if post.updated |
|
||||
.uk-width-auto updated: #{moment(post.updated).format("MMM DD YYYY HH:MM a")} |
|
||||
|
|
||||
+renderSectionTitle('Featured') |
|
||||
.uk-margin |
|
||||
div(style="position: relative; overflow: hidden; width: 100%; padding-top: 56.25%") |
|
||||
iframe( |
|
||||
src="https://tv.gab.com/channel/mrjoeprich/embed/what-is-just-joe-radio-61ad9b2165a83d20e95a465d", |
|
||||
width="960", |
|
||||
height="540", |
|
||||
style="position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%;", |
|
||||
) |
|
||||
|
|
||||
if featuredEmbed |
|
||||
div.dtp-featured-embed!= featuredEmbed |
|
||||
|
|
||||
//- Blog Posts |
|
||||
+renderSectionTitle('Blog Posts') |
|
||||
|
|
||||
if Array.isArray(posts) && (posts.length > 0) |
|
||||
- var postIndex = 1; |
|
||||
ul.uk-list.uk-list-divider.uk-list-small |
|
||||
each post in posts |
|
||||
li |
|
||||
+renderBlogPostListItem(post, postIndex, 2) |
|
||||
- postIndex += 1; |
|
||||
else |
|
||||
div There are no posts at this time. Please check back later! |
|
@ -0,0 +1,23 @@ |
|||||
|
extends main |
||||
|
|
||||
|
block content-container |
||||
|
section.uk-section.uk-section-default |
||||
|
.uk-container |
||||
|
div(uk-grid) |
||||
|
.uk-width-1-3 |
||||
|
.uk-margin |
||||
|
img(src="/img/default-member.png").site-profile-picture |
||||
|
|
||||
|
.uk-width-expand |
||||
|
block content |
||||
|
|
||||
|
block page-footer |
||||
|
section.uk-section.uk-section-default.uk-section-small.dtp-site-footer |
||||
|
.uk-container.uk-text-small |
||||
|
a(href="/", title=`Learn more about ${site.name}`).uk-display-block.uk-link-reset |
||||
|
div(uk-grid).uk-grid-small.uk-flex-center.uk-flex-bottom |
||||
|
.uk-width-auto |
||||
|
img(src="/img/icon/icon-48x48.png").uk-display-block |
||||
|
.uk-width-auto(style="line-height: 1em;") |
||||
|
.uk-text-small.uk-text-muted hosted by |
||||
|
div #{site.name} |
@ -1,15 +0,0 @@ |
|||||
extends ../layouts/main-sidebar |
|
||||
block content |
|
||||
|
|
||||
include ../components/page-sidebar |
|
||||
|
|
||||
article(dtp-page-id= page._id) |
|
||||
.uk-margin |
|
||||
div(uk-grid) |
|
||||
.uk-width-expand |
|
||||
h1.article-title= page.title |
|
||||
if user && user.flags.isAdmin |
|
||||
.uk-width-auto |
|
||||
a(href=`/admin/page/${page._id}`).uk-button.dtp-button-text EDIT |
|
||||
.uk-margin |
|
||||
!= page.content |
|
@ -1,79 +0,0 @@ |
|||||
extends ../layouts/main-sidebar |
|
||||
block content |
|
||||
|
|
||||
include ../comment/components/comment |
|
||||
|
|
||||
article(dtp-post-id= post._id) |
|
||||
.uk-margin |
|
||||
div(uk-grid) |
|
||||
.uk-width-expand |
|
||||
h1.article-title= post.title |
|
||||
.uk-text-lead= post.summary |
|
||||
.uk-margin |
|
||||
.uk-article-meta |
|
||||
div(uk-grid).uk-grid-small.uk-flex-top |
|
||||
.uk-width-expand |
|
||||
div published: #{moment(post.created).format('MMM DD, YYYY - hh:mm a').toUpperCase()} |
|
||||
if user && user.flags.isAdmin |
|
||||
.uk-width-auto |
|
||||
a(href=`/admin/post/${post._id}`) |
|
||||
+renderButtonIcon('fa-pen', 'edit') |
|
||||
.uk-width-auto |
|
||||
+renderButtonIcon('fa-eye', displayIntegerValue(post.stats.totalViewCount)) |
|
||||
.uk-width-auto |
|
||||
+renderButtonIcon('fa-chevron-up', displayIntegerValue(post.stats.upvoteCount)) |
|
||||
.uk-width-auto |
|
||||
+renderButtonIcon('fa-chevron-down', displayIntegerValue(post.stats.downvoteCount)) |
|
||||
.uk-width-auto |
|
||||
+renderButtonIcon('fa-comment', displayIntegerValue(post.stats.commentCount)) |
|
||||
.uk-margin |
|
||||
!= post.content |
|
||||
|
|
||||
if post.updated |
|
||||
.uk-margin |
|
||||
.uk-article-meta This post was updated on #{moment(post.updated).format('MMMM DD, YYYY, [at] hh:mm a')}. |
|
||||
|
|
||||
if user && post.flags.enableComments |
|
||||
+renderSectionTitle('Add a comment') |
|
||||
|
|
||||
.uk-margin |
|
||||
form(method="POST", action=`/post/${post._id}/comment`, onsubmit="return dtp.app.submitForm(event, 'create-comment');").uk-form |
|
||||
.uk-card.uk-card-secondary.uk-card-small |
|
||||
.uk-card-body |
|
||||
textarea( |
|
||||
id="content", |
|
||||
name="content", |
|
||||
rows="4", |
|
||||
maxlength="3000", |
|
||||
placeholder="Enter comment", |
|
||||
oninput="return dtp.app.onCommentInput(event);", |
|
||||
).uk-textarea.uk-resize-vertical |
|
||||
.uk-text-small |
|
||||
div(uk-grid).uk-flex-between |
|
||||
.uk-width-auto You are commenting as: #{user.username} |
|
||||
.uk-width-auto #[span#comment-character-count 0] of 3,000 |
|
||||
.uk-card-footer |
|
||||
div(uk-grid).uk-flex-between |
|
||||
.uk-width-expand |
|
||||
button( |
|
||||
type="button", |
|
||||
data-target-element="content", |
|
||||
title="Add an emoji", |
|
||||
onclick="return dtp.app.showEmojiPicker(event);", |
|
||||
).uk-button.dtp-button-default |
|
||||
span |
|
||||
i.far.fa-smile |
|
||||
.uk-width-auto |
|
||||
button(type="submit").uk-button.dtp-button-primary Post comment |
|
||||
|
|
||||
.uk-margin |
|
||||
+renderSectionTitle('Comments') |
|
||||
|
|
||||
.uk-margin |
|
||||
if Array.isArray(comments) && (comments.length > 0) |
|
||||
ul#post-comment-list.uk-list |
|
||||
each comment in comments |
|
||||
+renderComment(comment) |
|
||||
else |
|
||||
ul#post-comment-list.uk-list |
|
||||
div There are no comments at this time. Please check back later. |
|
@ -0,0 +1,11 @@ |
|||||
|
extends ../layouts/public-profile |
||||
|
block content |
||||
|
|
||||
|
.uk-margin |
||||
|
+renderSectionTitle(`${userProfile.displayName || userProfile.username}'s links`) |
||||
|
|
||||
|
.uk-margin |
||||
|
ul.uk-list |
||||
|
each link in links |
||||
|
li |
||||
|
a(href= link.href).uk-button.dtp-button-primary.uk-display-block.uk-border-rounded= link.label |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 8.2 KiB |
Before Width: | Height: | Size: 581 B After Width: | Height: | Size: 616 B |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 49 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 3.4 KiB |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 97 KiB |
After Width: | Height: | Size: 29 KiB |