Browse Source

libertylinks starting

master
Rob Colbert 2 years ago
parent
commit
9889a74309
  1. 4
      .env.default
  2. 44
      app/controllers/home.js
  3. 69
      app/controllers/page.js
  4. 129
      app/controllers/post.js
  5. 25
      app/models/category.js
  6. 44
      app/models/comment.js
  7. 16
      app/models/lib/resource-stats.js
  8. 21
      app/models/link.js
  9. 28
      app/models/page.js
  10. 33
      app/models/post.js
  11. 173
      app/services/comment.js
  12. 80
      app/services/link.js
  13. 161
      app/services/page.js
  14. 138
      app/services/post.js
  15. 11
      app/services/user.js
  16. 17
      app/views/admin/category/editor.pug
  17. 21
      app/views/admin/category/index.pug
  18. 89
      app/views/admin/page/editor.pug
  19. 43
      app/views/admin/page/index.pug
  20. 89
      app/views/admin/post/editor.pug
  21. 50
      app/views/admin/post/index.pug
  22. 10
      app/views/article/components/article.pug
  23. 8
      app/views/article/view.pug
  24. 6
      app/views/category/components/list-item.pug
  25. 17
      app/views/category/home.pug
  26. 32
      app/views/category/view.pug
  27. 3
      app/views/comment/components/comment-standalone.pug
  28. 47
      app/views/comment/components/comment.pug
  29. 5
      app/views/components/navbar.pug
  30. 15
      app/views/components/page-footer.pug
  31. 7
      app/views/index-logged-in.pug
  32. 63
      app/views/index.pug
  33. 7
      app/views/layouts/main.pug
  34. 23
      app/views/layouts/public-profile.pug
  35. 15
      app/views/page/view.pug
  36. 79
      app/views/post/view.pug
  37. 11
      app/views/profile/home.pug
  38. 5
      app/views/welcome/index.pug
  39. 9
      app/views/welcome/signup.pug
  40. BIN
      client/img/icon/icon-114x114.png
  41. BIN
      client/img/icon/icon-120x120.png
  42. BIN
      client/img/icon/icon-144x144.png
  43. BIN
      client/img/icon/icon-150x150.png
  44. BIN
      client/img/icon/icon-152x152.png
  45. BIN
      client/img/icon/icon-16x16.png
  46. BIN
      client/img/icon/icon-180x180.png
  47. BIN
      client/img/icon/icon-192x192.png
  48. BIN
      client/img/icon/icon-256x256.png
  49. BIN
      client/img/icon/icon-310x310.png
  50. BIN
      client/img/icon/icon-32x32.png
  51. BIN
      client/img/icon/icon-36x36.png
  52. BIN
      client/img/icon/icon-384x384.png
  53. BIN
      client/img/icon/icon-48x48.png
  54. BIN
      client/img/icon/icon-512x512.png
  55. BIN
      client/img/icon/icon-57x57.png
  56. BIN
      client/img/icon/icon-60x60.png
  57. BIN
      client/img/icon/icon-70x70.png
  58. BIN
      client/img/icon/icon-72x72.png
  59. BIN
      client/img/icon/icon-76x76.png
  60. BIN
      client/img/icon/icon-96x96.png
  61. BIN
      client/img/icon/justjoeradio.com.png
  62. BIN
      client/img/icon/libertylinks.io.png
  63. 4
      client/less/site/main.less
  64. 48
      config/limiter.js
  65. 10
      lib/site-platform.js

4
.env.default

@ -61,8 +61,8 @@ DTP_LOG_MONGODB=enabled
DTP_LOG_FILE=enabled
DTP_LOG_FILE_PATH=/tmp/dtp-sites/logs
DTP_LOG_FILE_NAME_APP=justjoeradio-app.log
DTP_LOG_FILE_NAME_HTTP=justjoeradio-access.log
DTP_LOG_FILE_NAME_APP=libertylinks-app.log
DTP_LOG_FILE_NAME_HTTP=libertylinks-access.log
DTP_LOG_DEBUG=enabled
DTP_LOG_INFO=enabled

44
app/controllers/home.js

@ -23,24 +23,60 @@ class HomeController extends SiteController {
const router = express.Router();
dtp.app.use('/', router);
router.use(this.dtp.services.gabTV.channelMiddleware('mrjoeprich'));
router.param('username', this.populateUsername.bind(this));
router.use(async (req, res, next) => {
res.locals.currentView = 'home';
return next();
});
router.get('/:username',
limiterService.create(limiterService.config.home.getPublicProfile),
this.getPublicProfile.bind(this),
);
router.get('/',
limiterService.create(limiterService.config.home.getHome),
this.getHome.bind(this),
);
}
async populateUsername (req, res, next, username) {
const { user: userService } = this.dtp.services;
try {
res.locals.userProfile = await userService.getPublicProfile(username);
return next();
} catch (error) {
this.log.error('failed to populate username', { username, error });
return next(error);
}
}
async getPublicProfile (req, res, next) {
const { link: linkService } = this.dtp.services;
try {
this.log.debug('profile request', { url: req.url });
if (!res.locals.userProfile) {
return next();
}
res.locals.currentView = 'public-profile';
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.links = await linkService.getForUser(res.locals.userProfile, res.locals.pagination);
res.render('profile/home');
} catch (error) {
this.log.error('failed to display landing page', { error });
return next(error);
}
}
async getHome (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('index');
if (req.user) {
res.render('index-logged-in');
} else {
res.render('index');
}
} catch (error) {
return next(error);
}

69
app/controllers/page.js

@ -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;
};

129
app/controllers/post.js

@ -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;
};

25
app/models/category.js

@ -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);

44
app/models/comment.js

@ -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);

16
app/models/lib/resource-stats.js

@ -9,27 +9,15 @@ const mongoose = require('mongoose');
const Schema = mongoose.Schema;
module.exports.ResourceStats = new Schema({
totalViewCount: { type: Number, default: 0, required: true },
totalVisitCount: { type: Number, default: 0, required: true },
upvoteCount: { type: Number, default: 0, required: true },
downvoteCount: { type: Number, default: 0, required: true },
commentCount: { type: Number, default: 0, required: true },
});
module.exports.ResourceStatsDefaults = {
totalViewCount: 0,
totalVisitCount: 0,
upvoteCount: 0,
downvoteCount: 0,
commentCount: 0,
};
module.exports.CommentStats = new Schema({
upvoteCount: { type: Number, default: 0, required: true },
downvoteCount: { type: Number, default: 0, required: true },
replyCount: { type: Number, default: 0, required: true },
});
module.exports.CommentStatsDefaults = {
upvoteCount: 0,
downvoteCount: 0,
replyCount: 0,
};

21
app/models/link.js

@ -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);

28
app/models/page.js

@ -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);

33
app/models/post.js

@ -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);

173
app/services/comment.js

@ -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); },
};

80
app/services/link.js

@ -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); },
};

161
app/services/page.js

@ -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); },
};

138
app/services/post.js

@ -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); },
};

11
app/services/user.js

@ -284,6 +284,17 @@ class UserService {
return user;
}
async getPublicProfile (username) {
if (!username || (typeof username !== 'string') || (username.length === 0)) {
throw new SiteError(406, 'Invalid username');
}
username = username.trim().toLowerCase();
const user = await User
.findOne({ username_lc: username })
.select('_id created username username_lc displayName picture');
return user;
}
async setUserSettings (user, settings) {
const {
crypto: cryptoService,

17
app/views/admin/category/editor.pug

@ -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'

21
app/views/admin/category/index.pug

@ -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.

89
app/views/admin/page/editor.pug

@ -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];
});

43
app/views/admin/page/index.pug

@ -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.

89
app/views/admin/post/editor.pug

@ -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];
});

50
app/views/admin/post/index.pug

@ -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.

10
app/views/article/components/article.pug

@ -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

8
app/views/article/view.pug

@ -1,8 +0,0 @@
extends ../layouts/main
block content
include components/article
section.uk-section.uk-section-default
.uk-container
+renderArticle(article)

6
app/views/category/components/list-item.pug

@ -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

17
app/views/category/home.pug

@ -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.

32
app/views/category/view.pug

@ -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)

3
app/views/comment/components/comment-standalone.pug

@ -1,3 +0,0 @@
include ../../components/library
include comment
+renderComment(comment)

47
app/views/comment/components/comment.pug

@ -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')

5
app/views/components/navbar.pug

@ -12,9 +12,6 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top
a(href="/", class="uk-visible@xl").uk-navbar-item.uk-logo
span= site.name
each menuItem in mainMenu
a(href= menuItem.url).uk-navbar-item= menuItem.label
//- Center menu (visible only on tablet and mobile)
div(class="uk-hidden@m").uk-navbar-center
a(href="/").uk-navbar-item
@ -60,7 +57,7 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top
li.uk-nav-divider
li
a(href=`/user/${user._id}`)
a(href=`/${user.username}`)
span.nav-item-icon
i.fas.fa-user
span Profile

15
app/views/components/page-footer.pug

@ -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 &copy; 2021 #[+renderSiteLink()]
span Copyright #{moment().format('YYYY')}
span
+renderSiteLink()

7
app/views/index-logged-in.pug

@ -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' })

63
app/views/index.pug

@ -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

7
app/views/layouts/main.pug

@ -51,7 +51,12 @@ html(lang='en')
}
}
body.dtp(class= 'dtp-dark', data-dtp-env= process.env.NODE_ENV, data-dtp-domain= site.domainKey, data-current-view= currentView)
body.dtp(
class= 'dtp-dark',
data-dtp-env= process.env.NODE_ENV,
data-dtp-domain= site.domainKey,
data-current-view= currentView,
)
include ../components/site-link

23
app/views/layouts/public-profile.pug

@ -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}

15
app/views/page/view.pug

@ -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

79
app/views/post/view.pug

@ -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.

11
app/views/profile/home.pug

@ -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

5
app/views/welcome/index.pug

@ -3,7 +3,10 @@ block content
section.uk-section.uk-section-secondary
.uk-container.uk-text-center
h1 Welcome to #{site.name}
.uk-width-auto.uk-margin-auto
img(src="/img/icon/icon-256x256.png")
h1= site.name
.uk-text-lead= site.description
.uk-margin-medium-top

9
app/views/welcome/signup.pug

@ -2,7 +2,7 @@ extends ../layouts/main
block content
form(method="POST", action="/user").uk-form
section.uk-section.uk-section-muted.uk-section
section.uk-section.uk-section-default.uk-section-xsmall
.uk-container.uk-container-small
p You are creating a new member account on #[+renderSiteLink()]. If you have an account, please #[a(href="/welcome/login") log in here]. An account is required to comment on posts and use other site features.
@ -35,12 +35,7 @@ block content
input(id="passwordv", name="passwordv", type="password", placeholder="Verify password").uk-input
.uk-text-small.uk-text-muted.uk-margin-small-top(class="uk-visible@m") Please enter your password again to prove you're not an idiot.
.uk-margin
label
input(type="checkbox", checked).uk-checkbox
| Join #{site.name}'s Newsletter
section.uk-section.uk-section-secondary.uk-section
section.uk-section.uk-section-secondary.uk-section-xsmall
.uk-container.uk-container-small
.uk-margin-large
.uk-text-center

BIN
client/img/icon/icon-114x114.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
client/img/icon/icon-120x120.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
client/img/icon/icon-144x144.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
client/img/icon/icon-150x150.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
client/img/icon/icon-152x152.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
client/img/icon/icon-16x16.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 581 B

After

Width:  |  Height:  |  Size: 616 B

BIN
client/img/icon/icon-180x180.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 10 KiB

BIN
client/img/icon/icon-192x192.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
client/img/icon/icon-256x256.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 16 KiB

BIN
client/img/icon/icon-310x310.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
client/img/icon/icon-32x32.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
client/img/icon/icon-36x36.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
client/img/icon/icon-384x384.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 30 KiB

BIN
client/img/icon/icon-48x48.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
client/img/icon/icon-512x512.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 49 KiB

BIN
client/img/icon/icon-57x57.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
client/img/icon/icon-60x60.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
client/img/icon/icon-70x70.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
client/img/icon/icon-72x72.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
client/img/icon/icon-76x76.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
client/img/icon/icon-96x96.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
client/img/icon/justjoeradio.com.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

BIN
client/img/icon/libertylinks.io.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

4
client/less/site/main.less

@ -6,6 +6,10 @@ html, body {
body {
padding-top: @site-navbar-height;
&[data-current-view="public-profile"] {
padding-top: 0;
}
}
.no-select {

48
config/limiter.js

@ -46,22 +46,6 @@ module.exports = {
},
},
/*
* CryptoExchangeController
*/
cryptoExchange: {
getRateGraph: {
total: 10,
expire: ONE_MINUTE,
message: 'You are loading exchange rate graphs too quickly',
},
getCurrentRates: {
total: 10,
expire: ONE_MINUTE,
message: 'You are loading cryptocurrency exchange rates too quickly',
},
},
/*
* DashboardController
*/
@ -82,6 +66,11 @@ module.exports = {
* HomeController
*/
home: {
getPublicProfile: {
total: 20,
expire: ONE_MINUTE,
message: 'You are feteching profiles too quickly',
},
getHome: {
total: 20,
expire: ONE_MINUTE,
@ -135,33 +124,6 @@ module.exports = {
},
},
/*
* PageController
*/
page: {
getView: {
total: 5,
expire: ONE_MINUTE,
message: 'You are reading pages too quickly',
},
},
/*
* PostController
*/
post: {
getView: {
total: 5,
expire: ONE_MINUTE,
message: 'You are reading posts too quickly',
},
getIndex: {
total: 60,
expire: ONE_MINUTE,
message: 'You are refreshing too quickly',
},
},
/*
* UserController
*/

10
lib/site-platform.js

@ -154,8 +154,6 @@ module.exports.startPlatform = async (dtp) => {
};
module.exports.startWebServer = async (dtp) => {
const { page: pageService } = module.services;
dtp.app = module.app = express();
module.app.set('views', path.join(dtp.config.root, 'app', 'views'));
@ -293,14 +291,18 @@ module.exports.startWebServer = async (dtp) => {
];
const settingsKey = `settings:${dtp.config.site.domainKey}:site`;
res.locals.site = (await cacheService.getObject(settingsKey)) || dtp.config.site;
res.locals.site = dtp.config.site;
const settings = await cacheService.getObject(settingsKey);
if (settings) {
res.locals.site = Object.assign(res.locals.site, settings);
}
return next();
} catch (error) {
module.log.error('failed to populate general request data', { error });
return next(error);
}
});
module.app.use(pageService.menuMiddleware.bind(pageService));
/*
* System Init

Loading…
Cancel
Save