- Comment composer - Comment renderer - Comment create service - Display list and display engine enhancements - Added emoji picker for comments - Admin for site settings - added main-sidebar layoutmaster
parent
c8e37db1c6
commit
d534b7950e
@ -0,0 +1,56 @@
|
||||
// admin/settings.js
|
||||
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
// License: Apache-2.0
|
||||
|
||||
'use strict';
|
||||
|
||||
const DTP_COMPONENT_NAME = 'admin:settings';
|
||||
const express = require('express');
|
||||
|
||||
const { SiteController } = require('../../../lib/site-lib');
|
||||
|
||||
class SettingsController extends SiteController {
|
||||
|
||||
constructor (dtp) {
|
||||
super(dtp, DTP_COMPONENT_NAME);
|
||||
}
|
||||
|
||||
async start ( ) {
|
||||
const router = express.Router();
|
||||
router.use(async (req, res, next) => {
|
||||
res.locals.currentView = 'admin';
|
||||
res.locals.adminView = 'settings';
|
||||
return next();
|
||||
});
|
||||
|
||||
router.post('/', this.postUpdateSettings.bind(this));
|
||||
|
||||
router.get('/', this.getSettingsView.bind(this));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
async postUpdateSettings (req, res, next) {
|
||||
const { cache: cacheService } = this.dtp.services;
|
||||
try {
|
||||
const settingsKey = `settings:${this.dtp.config.site.domainKey}:site`;
|
||||
await cacheService.setObject(settingsKey, req.body);
|
||||
res.redirect('/admin/settings');
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getSettingsView (req, res, next) {
|
||||
try {
|
||||
res.render('admin/settings/editor');
|
||||
} catch (error) {
|
||||
return next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async (dtp) => {
|
||||
let controller = new SettingsController(dtp);
|
||||
return controller;
|
||||
};
|
@ -0,0 +1,173 @@
|
||||
// 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,15 @@
|
||||
extends ../layouts/main
|
||||
block content
|
||||
|
||||
form(method="POST", action="/admin/settings").uk-form
|
||||
.uk-margin
|
||||
label(for="name").uk-form-label Site name
|
||||
input(id="name", name="name", type="text", maxlength="200", placeholder="Enter site name", value= site.name).uk-input
|
||||
.uk-margin
|
||||
label(for="description").uk-form-label Site description
|
||||
input(id="description", name="description", type="text", maxlength="500", placeholder="Enter site description", value= site.description).uk-input
|
||||
.uk-margin
|
||||
label(for="company").uk-form-label Company name
|
||||
input(id="company", name="company", type="text", maxlength="200", placeholder="Enter company name", value= site.company).uk-input
|
||||
|
||||
button(type="submit").uk-button.dtp-button-primary Save Settings
|
@ -0,0 +1,3 @@
|
||||
include ../../components/library
|
||||
include comment
|
||||
+renderComment(comment)
|
@ -0,0 +1,47 @@
|
||||
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')
|
@ -0,0 +1,8 @@
|
||||
mixin renderSectionTitle (title, barButton)
|
||||
.dtp-border-bottom
|
||||
div(uk-grid).uk-grid-small.uk-flex-middle
|
||||
.uk-width-expand
|
||||
h3.uk-heading-bullet.uk-margin-small= title
|
||||
if barButton
|
||||
.uk-width-auto
|
||||
a(href= barButton.url, target= "_blank", title= barButton.title).uk-button.uk-button-link.uk-button-small= barButton.label
|
@ -0,0 +1,13 @@
|
||||
extends main
|
||||
|
||||
block content-container
|
||||
section.uk-section.uk-section-default
|
||||
.uk-container
|
||||
div(uk-grid)#dtp-content-grid
|
||||
div(class="uk-width-1-1 uk-width-2-3@m")
|
||||
block content
|
||||
div(class="uk-width-1-1 uk-width-1-3@m")
|
||||
+renderPageSidebar()
|
||||
|
||||
block page-footer
|
||||
include ../components/page-footer
|
@ -1,22 +1,15 @@
|
||||
extends ../layouts/main
|
||||
extends ../layouts/main-sidebar
|
||||
block content
|
||||
|
||||
include ../components/page-sidebar
|
||||
|
||||
section.uk-section.uk-section-default.uk-section-small
|
||||
.uk-container
|
||||
article(dtp-page-id= page._id)
|
||||
.uk-margin
|
||||
div(uk-grid)
|
||||
.uk-width-2-3
|
||||
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
|
||||
|
||||
.uk-width-1-3
|
||||
+renderPageSidebar()
|
||||
.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,54 +1,79 @@
|
||||
extends ../layouts/main
|
||||
extends ../layouts/main-sidebar
|
||||
block content
|
||||
|
||||
include ../components/page-sidebar
|
||||
include ../comment/components/comment
|
||||
|
||||
section.uk-section.uk-section-default.uk-section-small
|
||||
.uk-container
|
||||
article(dtp-post-id= post._id)
|
||||
.uk-margin
|
||||
div(uk-grid)
|
||||
.uk-width-2-3
|
||||
article(dtp-post-id= post._id)
|
||||
.uk-margin
|
||||
div(uk-grid)
|
||||
.uk-width-expand
|
||||
h1.article-title= post.title
|
||||
if user && user.flags.isAdmin
|
||||
.uk-width-auto
|
||||
a(href=`/admin/post/${post._id}`).uk-button.dtp-button-text EDIT
|
||||
.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()}
|
||||
.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
|
||||
.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 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
|
||||
.dtp-border-bottom
|
||||
h3.uk-heading-bullet Comments
|
||||
|
||||
.uk-margin
|
||||
form(method="POST", action=`/post/${post._id}/comment`, onsubmit="return dtp.app.submitForm(event);").uk-form
|
||||
.uk-margin
|
||||
textarea(id="content", name="content", rows="4", placeholder="Enter comment").uk-textarea
|
||||
.uk-text-small
|
||||
div(uk-grid).uk-flex-between
|
||||
.uk-width-auto You are commenting as: #{user.username}
|
||||
.uk-width-auto 0 of 3000
|
||||
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-width-1-3
|
||||
+renderPageSidebar()
|
||||
.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.
|
Loading…
Reference in new issue