diff --git a/.env.default b/.env.default index 1c40be4..db277dd 100644 --- a/.env.default +++ b/.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 diff --git a/app/controllers/home.js b/app/controllers/home.js index 9d821ff..e4d4ec8 100644 --- a/app/controllers/home.js +++ b/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); } diff --git a/app/controllers/page.js b/app/controllers/page.js deleted file mode 100644 index 69d7aa1..0000000 --- a/app/controllers/page.js +++ /dev/null @@ -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; -}; \ No newline at end of file diff --git a/app/controllers/post.js b/app/controllers/post.js deleted file mode 100644 index a538d33..0000000 --- a/app/controllers/post.js +++ /dev/null @@ -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; -}; \ No newline at end of file diff --git a/app/models/category.js b/app/models/category.js deleted file mode 100644 index 33cba63..0000000 --- a/app/models/category.js +++ /dev/null @@ -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); \ No newline at end of file diff --git a/app/models/comment.js b/app/models/comment.js deleted file mode 100644 index 608692b..0000000 --- a/app/models/comment.js +++ /dev/null @@ -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); \ No newline at end of file diff --git a/app/models/lib/resource-stats.js b/app/models/lib/resource-stats.js index 10ad788..f46f445 100644 --- a/app/models/lib/resource-stats.js +++ b/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, }; \ No newline at end of file diff --git a/app/models/link.js b/app/models/link.js new file mode 100644 index 0000000..cba0dd6 --- /dev/null +++ b/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); \ No newline at end of file diff --git a/app/models/page.js b/app/models/page.js deleted file mode 100644 index c728337..0000000 --- a/app/models/page.js +++ /dev/null @@ -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); \ No newline at end of file diff --git a/app/models/post.js b/app/models/post.js deleted file mode 100644 index 9d45be9..0000000 --- a/app/models/post.js +++ /dev/null @@ -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); \ No newline at end of file diff --git a/app/services/comment.js b/app/services/comment.js deleted file mode 100644 index 175782c..0000000 --- a/app/services/comment.js +++ /dev/null @@ -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); }, -}; diff --git a/app/services/link.js b/app/services/link.js new file mode 100644 index 0000000..2c2dc08 --- /dev/null +++ b/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); }, +}; \ No newline at end of file diff --git a/app/services/page.js b/app/services/page.js deleted file mode 100644 index 981bfb5..0000000 --- a/app/services/page.js +++ /dev/null @@ -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); }, -}; \ No newline at end of file diff --git a/app/services/post.js b/app/services/post.js deleted file mode 100644 index 69bad91..0000000 --- a/app/services/post.js +++ /dev/null @@ -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); }, -}; \ No newline at end of file diff --git a/app/services/user.js b/app/services/user.js index 9c703d9..8051f82 100644 --- a/app/services/user.js +++ b/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, diff --git a/app/views/admin/category/editor.pug b/app/views/admin/category/editor.pug deleted file mode 100644 index ad57d93..0000000 --- a/app/views/admin/category/editor.pug +++ /dev/null @@ -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' \ No newline at end of file diff --git a/app/views/admin/category/index.pug b/app/views/admin/category/index.pug deleted file mode 100644 index d8e68e4..0000000 --- a/app/views/admin/category/index.pug +++ /dev/null @@ -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. \ No newline at end of file diff --git a/app/views/admin/page/editor.pug b/app/views/admin/page/editor.pug deleted file mode 100644 index d0f3f2c..0000000 --- a/app/views/admin/page/editor.pug +++ /dev/null @@ -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]; - }); \ No newline at end of file diff --git a/app/views/admin/page/index.pug b/app/views/admin/page/index.pug deleted file mode 100644 index 84e1f50..0000000 --- a/app/views/admin/page/index.pug +++ /dev/null @@ -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. \ No newline at end of file diff --git a/app/views/admin/post/editor.pug b/app/views/admin/post/editor.pug deleted file mode 100644 index 50a413e..0000000 --- a/app/views/admin/post/editor.pug +++ /dev/null @@ -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]; - }); \ No newline at end of file diff --git a/app/views/admin/post/index.pug b/app/views/admin/post/index.pug deleted file mode 100644 index a680e8e..0000000 --- a/app/views/admin/post/index.pug +++ /dev/null @@ -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. \ No newline at end of file diff --git a/app/views/article/components/article.pug b/app/views/article/components/article.pug deleted file mode 100644 index b2b287a..0000000 --- a/app/views/article/components/article.pug +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/views/article/view.pug b/app/views/article/view.pug deleted file mode 100644 index 7b80f77..0000000 --- a/app/views/article/view.pug +++ /dev/null @@ -1,8 +0,0 @@ -extends ../layouts/main -block content - - include components/article - - section.uk-section.uk-section-default - .uk-container - +renderArticle(article) \ No newline at end of file diff --git a/app/views/category/components/list-item.pug b/app/views/category/components/list-item.pug deleted file mode 100644 index 41f5f55..0000000 --- a/app/views/category/components/list-item.pug +++ /dev/null @@ -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 \ No newline at end of file diff --git a/app/views/category/home.pug b/app/views/category/home.pug deleted file mode 100644 index da7cb09..0000000 --- a/app/views/category/home.pug +++ /dev/null @@ -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. \ No newline at end of file diff --git a/app/views/category/view.pug b/app/views/category/view.pug deleted file mode 100644 index 7cd7b4d..0000000 --- a/app/views/category/view.pug +++ /dev/null @@ -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) diff --git a/app/views/comment/components/comment-standalone.pug b/app/views/comment/components/comment-standalone.pug deleted file mode 100644 index a5a1f34..0000000 --- a/app/views/comment/components/comment-standalone.pug +++ /dev/null @@ -1,3 +0,0 @@ -include ../../components/library -include comment -+renderComment(comment) \ No newline at end of file diff --git a/app/views/comment/components/comment.pug b/app/views/comment/components/comment.pug deleted file mode 100644 index 4f6a8e3..0000000 --- a/app/views/comment/components/comment.pug +++ /dev/null @@ -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') \ No newline at end of file diff --git a/app/views/components/navbar.pug b/app/views/components/navbar.pug index 601345a..4c8273d 100644 --- a/app/views/components/navbar.pug +++ b/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 diff --git a/app/views/components/page-footer.pug b/app/views/components/page-footer.pug index 418b082..8b4a027 100644 --- a/app/views/components/page-footer.pug +++ b/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 © 2021 #[+renderSiteLink()] \ No newline at end of file + span Copyright #{moment().format('YYYY')} + span + +renderSiteLink() \ No newline at end of file diff --git a/app/views/index-logged-in.pug b/app/views/index-logged-in.pug new file mode 100644 index 0000000..835d787 --- /dev/null +++ b/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' }) diff --git a/app/views/index.pug b/app/views/index.pug index 5dc5f4a..9793164 100644 --- a/app/views/index.pug +++ b/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! \ No newline at end of file + 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 \ No newline at end of file diff --git a/app/views/layouts/main.pug b/app/views/layouts/main.pug index 4fef5b2..019778d 100644 --- a/app/views/layouts/main.pug +++ b/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 diff --git a/app/views/layouts/public-profile.pug b/app/views/layouts/public-profile.pug new file mode 100644 index 0000000..0e0cb8d --- /dev/null +++ b/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} \ No newline at end of file diff --git a/app/views/page/view.pug b/app/views/page/view.pug deleted file mode 100644 index f05f27b..0000000 --- a/app/views/page/view.pug +++ /dev/null @@ -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 diff --git a/app/views/post/view.pug b/app/views/post/view.pug deleted file mode 100644 index 027ac66..0000000 --- a/app/views/post/view.pug +++ /dev/null @@ -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. \ No newline at end of file diff --git a/app/views/profile/home.pug b/app/views/profile/home.pug new file mode 100644 index 0000000..c21af2d --- /dev/null +++ b/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 \ No newline at end of file diff --git a/app/views/welcome/index.pug b/app/views/welcome/index.pug index 4b7af06..f5dec8a 100644 --- a/app/views/welcome/index.pug +++ b/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 diff --git a/app/views/welcome/signup.pug b/app/views/welcome/signup.pug index 8bbb1c3..7f1dd62 100644 --- a/app/views/welcome/signup.pug +++ b/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 diff --git a/client/img/icon/icon-114x114.png b/client/img/icon/icon-114x114.png index cdff427..f7beb6d 100644 Binary files a/client/img/icon/icon-114x114.png and b/client/img/icon/icon-114x114.png differ diff --git a/client/img/icon/icon-120x120.png b/client/img/icon/icon-120x120.png index 8522d1f..0e7b238 100644 Binary files a/client/img/icon/icon-120x120.png and b/client/img/icon/icon-120x120.png differ diff --git a/client/img/icon/icon-144x144.png b/client/img/icon/icon-144x144.png index 637e04e..8342502 100644 Binary files a/client/img/icon/icon-144x144.png and b/client/img/icon/icon-144x144.png differ diff --git a/client/img/icon/icon-150x150.png b/client/img/icon/icon-150x150.png index 99635d2..d9ed163 100644 Binary files a/client/img/icon/icon-150x150.png and b/client/img/icon/icon-150x150.png differ diff --git a/client/img/icon/icon-152x152.png b/client/img/icon/icon-152x152.png index 5b80938..8cd116c 100644 Binary files a/client/img/icon/icon-152x152.png and b/client/img/icon/icon-152x152.png differ diff --git a/client/img/icon/icon-16x16.png b/client/img/icon/icon-16x16.png index 4cd8efc..f3852c4 100644 Binary files a/client/img/icon/icon-16x16.png and b/client/img/icon/icon-16x16.png differ diff --git a/client/img/icon/icon-180x180.png b/client/img/icon/icon-180x180.png index 7acf658..7969cd1 100644 Binary files a/client/img/icon/icon-180x180.png and b/client/img/icon/icon-180x180.png differ diff --git a/client/img/icon/icon-192x192.png b/client/img/icon/icon-192x192.png index 4ff9d8e..daf997a 100644 Binary files a/client/img/icon/icon-192x192.png and b/client/img/icon/icon-192x192.png differ diff --git a/client/img/icon/icon-256x256.png b/client/img/icon/icon-256x256.png index f8d2d7f..a8fce76 100644 Binary files a/client/img/icon/icon-256x256.png and b/client/img/icon/icon-256x256.png differ diff --git a/client/img/icon/icon-310x310.png b/client/img/icon/icon-310x310.png index b049ab6..4e038ea 100644 Binary files a/client/img/icon/icon-310x310.png and b/client/img/icon/icon-310x310.png differ diff --git a/client/img/icon/icon-32x32.png b/client/img/icon/icon-32x32.png index 0cddef8..15d0528 100644 Binary files a/client/img/icon/icon-32x32.png and b/client/img/icon/icon-32x32.png differ diff --git a/client/img/icon/icon-36x36.png b/client/img/icon/icon-36x36.png index ce2df49..bd78360 100644 Binary files a/client/img/icon/icon-36x36.png and b/client/img/icon/icon-36x36.png differ diff --git a/client/img/icon/icon-384x384.png b/client/img/icon/icon-384x384.png index b812a38..7cb8de7 100644 Binary files a/client/img/icon/icon-384x384.png and b/client/img/icon/icon-384x384.png differ diff --git a/client/img/icon/icon-48x48.png b/client/img/icon/icon-48x48.png index b1eda57..6f01610 100644 Binary files a/client/img/icon/icon-48x48.png and b/client/img/icon/icon-48x48.png differ diff --git a/client/img/icon/icon-512x512.png b/client/img/icon/icon-512x512.png index b61ab22..63ce2db 100644 Binary files a/client/img/icon/icon-512x512.png and b/client/img/icon/icon-512x512.png differ diff --git a/client/img/icon/icon-57x57.png b/client/img/icon/icon-57x57.png index 3df418c..41198db 100644 Binary files a/client/img/icon/icon-57x57.png and b/client/img/icon/icon-57x57.png differ diff --git a/client/img/icon/icon-60x60.png b/client/img/icon/icon-60x60.png index 8df806d..8587417 100644 Binary files a/client/img/icon/icon-60x60.png and b/client/img/icon/icon-60x60.png differ diff --git a/client/img/icon/icon-70x70.png b/client/img/icon/icon-70x70.png index 09aede8..e6bfb5d 100644 Binary files a/client/img/icon/icon-70x70.png and b/client/img/icon/icon-70x70.png differ diff --git a/client/img/icon/icon-72x72.png b/client/img/icon/icon-72x72.png index a8c29bf..783babc 100644 Binary files a/client/img/icon/icon-72x72.png and b/client/img/icon/icon-72x72.png differ diff --git a/client/img/icon/icon-76x76.png b/client/img/icon/icon-76x76.png index 33d87be..9b5cf8d 100644 Binary files a/client/img/icon/icon-76x76.png and b/client/img/icon/icon-76x76.png differ diff --git a/client/img/icon/icon-96x96.png b/client/img/icon/icon-96x96.png index 939a540..b8ff25c 100644 Binary files a/client/img/icon/icon-96x96.png and b/client/img/icon/icon-96x96.png differ diff --git a/client/img/icon/justjoeradio.com.png b/client/img/icon/justjoeradio.com.png deleted file mode 100644 index 3417174..0000000 Binary files a/client/img/icon/justjoeradio.com.png and /dev/null differ diff --git a/client/img/icon/libertylinks.io.png b/client/img/icon/libertylinks.io.png new file mode 100644 index 0000000..fb6bcdd Binary files /dev/null and b/client/img/icon/libertylinks.io.png differ diff --git a/client/less/site/main.less b/client/less/site/main.less index 5cc365a..30679a3 100644 --- a/client/less/site/main.less +++ b/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 { diff --git a/config/limiter.js b/config/limiter.js index 0639224..6222b8f 100644 --- a/config/limiter.js +++ b/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 */ diff --git a/lib/site-platform.js b/lib/site-platform.js index 8c496b0..b82877d 100644 --- a/lib/site-platform.js +++ b/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