@ -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 |
|||
ul.uk-subnav.uk-flex-center |
|||
each socialIcon in socialIcons |
|||
li |
|||
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()] |
|||
span Copyright #{moment().format('YYYY')} |
|||
span |
|||
+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 |
|||
|
|||
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) |
|||
a(href=`/post/${post.slug}`).uk-display-block.uk-link-reset |
|||
div(uk-grid).uk-grid-small |
|||
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. |
|||
|
|||
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. |
|||
|
|||
div(class='uk-visible@m', class={ |
|||
'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={ |
|||
'uk-flex-first': ((postIndex % postIndexModulus) !== 0), |
|||
'uk-flex-last': ((postIndex % postIndexModulus) === 0), |
|||
}) |
|||
article.uk-article |
|||
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! |
|||
div(uk-grid) |
|||
.uk-width-1-2 |
|||
a(href="/welcome/signup").uk-button.dtp-button-primary.uk-display-block Get Started |
|||
.uk-width-1-2 |
|||
a(href="/welcome/login").uk-button.dtp-button-default.uk-display-block Login |
@ -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 |