From c8e37db1c6c5997823a8b0e363c573d16639db93 Mon Sep 17 00:00:00 2001 From: rob Date: Thu, 9 Dec 2021 17:11:03 -0500 Subject: [PATCH] Pages, and TinyMCE put in dark mode --- .vscode/launch.json | 2 +- app/controllers/admin/page.js | 100 +++++++++++++++- app/controllers/page.js | 69 +++++++++++ app/models/page.js | 28 +++++ app/services/page.js | 161 ++++++++++++++++++++++++++ app/services/post.js | 10 ++ app/views/admin/newsletter/editor.pug | 7 ++ app/views/admin/page/editor.pug | 89 ++++++++++++++ app/views/admin/page/index.pug | 43 +++++++ app/views/admin/post/editor.pug | 13 ++- app/views/components/navbar.pug | 3 + app/views/index.pug | 43 ++++--- app/views/page/index.pug | 12 -- app/views/page/view.pug | 23 +++- app/views/post/view.pug | 2 +- config/limiter.js | 14 +++ lib/site-platform.js | 2 + 17 files changed, 582 insertions(+), 39 deletions(-) create mode 100644 app/controllers/page.js create mode 100644 app/models/page.js create mode 100644 app/services/page.js create mode 100644 app/views/admin/page/editor.pug create mode 100644 app/views/admin/page/index.pug delete mode 100644 app/views/page/index.pug diff --git a/.vscode/launch.json b/.vscode/launch.json index 87d2410..83fd18a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ { "type": "pwa-node", "request": "launch", - "name": "Launch Program", + "name": "dtp-sites", "skipFiles": [ "/**" ], diff --git a/app/controllers/admin/page.js b/app/controllers/admin/page.js index 21ff024..6ff84f9 100644 --- a/app/controllers/admin/page.js +++ b/app/controllers/admin/page.js @@ -7,7 +7,7 @@ const DTP_COMPONENT_NAME = 'admin:page'; const express = require('express'); -const { SiteController } = require('../../../lib/site-lib'); +const { SiteController, SiteError } = require('../../../lib/site-lib'); class PageController extends SiteController { @@ -25,17 +25,107 @@ class PageController extends SiteController { router.param('pageId', this.populatePageId.bind(this)); + router.post('/:pageId', this.pageUpdatePage.bind(this)); + router.post('/', this.pageCreatePage.bind(this)); + + router.get('/compose', this.getComposer.bind(this)); + router.get('/:pageId', this.getComposer.bind(this)); + router.get('/', this.getIndex.bind(this)); + router.delete('/:pageId', this.deletePage.bind(this)); + return router; } - async populatePageId (req, res, next/*, pageId*/) { - return next(); + async populatePageId (req, res, next, pageId) { + const { page: pageService } = this.dtp.services; + try { + res.locals.page = await pageService.getById(pageId); + if (!res.locals.page) { + throw new SiteError(404, 'Page not found'); + } + return next(); + } catch (error) { + this.log.error('failed to populate pageId', { pageId, error }); + return next(error); + } + } + + async pageUpdatePage (req, res, next) { + const { page: pageService } = this.dtp.services; + try { + await pageService.update(res.locals.page, req.body); + res.redirect('/admin/page'); + } catch (error) { + this.log.error('failed to update page', { newletterId: res.locals.page._id, error }); + return next(error); + } + } + + async pageCreatePage (req, res, next) { + const { page: pageService } = this.dtp.services; + try { + await pageService.create(req.user, req.body); + res.redirect('/admin/page'); + } catch (error) { + this.log.error('failed to create page', { error }); + return next(error); + } + } + + async getComposer (req, res, next) { + const { page: pageService } = this.dtp.services; + try { + let excludedPages; + if (res.locals.page) { + excludedPages = [res.locals.page._id]; + } + res.locals.availablePages = await pageService.getAvailablePages(excludedPages); + res.render('admin/page/editor'); + } catch (error) { + this.log.error('failed to serve page editor', { error }); + return next(error); + } } - async getIndex (req, res) { - res.render('admin/page/index'); + async getIndex (req, res, next) { + const { page: pageService } = this.dtp.services; + try { + res.locals.pagination = this.getPaginationParameters(req, 20); + res.locals.pages = await pageService.getPages(res.locals.pagination, ['draft', 'published', 'archived']); + res.render('admin/page/index'); + } catch (error) { + this.log.error('failed to fetch pages', { error }); + return next(error); + } + } + + async deletePage (req, res) { + const { page: pageService, displayEngine: displayEngineService } = this.dtp.services; + try { + const displayList = displayEngineService.createDisplayList('delete-page'); + + await pageService.deletePage(res.locals.page); + + displayList.removeElement(`li[data-page-id="${res.locals.page._id}"]`); + displayList.showNotification( + `Page "${res.locals.page.title}" deleted`, + 'success', + 'bottom-center', + 3000, + ); + res.status(200).json({ success: true, displayList }); + } catch (error) { + this.log.error('failed to delete page', { + pageId: res.local.page._id, + error, + }); + res.status(error.statusCode || 500).json({ + success: false, + message: error.message, + }); + } } } diff --git a/app/controllers/page.js b/app/controllers/page.js new file mode 100644 index 0000000..69d7aa1 --- /dev/null +++ b/app/controllers/page.js @@ -0,0 +1,69 @@ +// 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/models/page.js b/app/models/page.js new file mode 100644 index 0000000..c728337 --- /dev/null +++ b/app/models/page.js @@ -0,0 +1,28 @@ +// 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/services/page.js b/app/services/page.js new file mode 100644 index 0000000..981bfb5 --- /dev/null +++ b/app/services/page.js @@ -0,0 +1,161 @@ +// 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 index eb8fc09..69bad91 100644 --- a/app/services/post.js +++ b/app/services/post.js @@ -10,6 +10,7 @@ const slug = require('slug'); const { SiteService } = require('../../lib/site-lib'); const mongoose = require('mongoose'); +const ObjectId = mongoose.Types.ObjectId; const Post = mongoose.model('Post'); @@ -59,6 +60,15 @@ class PostService extends SiteService { 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()); } diff --git a/app/views/admin/newsletter/editor.pug b/app/views/admin/newsletter/editor.pug index 1269717..6a212e6 100644 --- a/app/views/admin/newsletter/editor.pug +++ b/app/views/admin/newsletter/editor.pug @@ -53,6 +53,13 @@ block viewjs 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]; diff --git a/app/views/admin/page/editor.pug b/app/views/admin/page/editor.pug new file mode 100644 index 0000000..d0f3f2c --- /dev/null +++ b/app/views/admin/page/editor.pug @@ -0,0 +1,89 @@ +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 new file mode 100644 index 0000000..84e1f50 --- /dev/null +++ b/app/views/admin/page/index.pug @@ -0,0 +1,43 @@ +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 index 216fa57..4d8e0ff 100644 --- a/app/views/admin/post/editor.pug +++ b/app/views/admin/post/editor.pug @@ -14,6 +14,15 @@ block content .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; + 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.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 @@ -34,7 +43,7 @@ block content | Enable comments .uk-width-auto label - input(id="is-featured", name="isFeatured", type="checkbox", checked= post ? post.flags.isFeatured : true).uk-checkbox + input(id="is-featured", name="isFeatured", type="checkbox", checked= post ? post.flags.isFeatured : false).uk-checkbox | Featured block viewjs @@ -70,6 +79,8 @@ block viewjs { title: 'Title Image', value: 'dtp-image-title' }, ], convert_urls: false, + skin: "oxide-dark", + content_css: "dark", }); window.dtp.app.editor = editors[0]; diff --git a/app/views/components/navbar.pug b/app/views/components/navbar.pug index b48d0af..55c93cc 100644 --- a/app/views/components/navbar.pug +++ b/app/views/components/navbar.pug @@ -12,6 +12,9 @@ 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 diff --git a/app/views/index.pug b/app/views/index.pug index af09314..2cb1817 100644 --- a/app/views/index.pug +++ b/app/views/index.pug @@ -3,6 +3,29 @@ block content include components/page-sidebar + mixin renderBlogPostListItem (post, postIndex = 1, postIndexModulus = 3) + a(href=`/post/${post.slug}`).uk-display-block.uk-link-reset + div(uk-grid).uk-grid-small + + div(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-flex-first': ((postIndex % postIndexModulus) !== 0), + 'uk-flex-last': ((postIndex % postIndexModulus) === 0), + }).uk-width-2-3 + 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")} + .uk-padding .uk-container //- Main Content Grid @@ -27,20 +50,12 @@ block content h3.uk-heading-bullet Blog Posts if Array.isArray(posts) && (posts.length > 0) - each post in posts - a(href=`/post/${post.slug}`).uk-display-block.uk-link-reset - div(uk-grid).uk-grid-small - .uk-width-1-3 - img(src="/img/default-poster.jpg").responsive - .uk-width-2-3 - 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")} + - 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! diff --git a/app/views/page/index.pug b/app/views/page/index.pug deleted file mode 100644 index cfeb936..0000000 --- a/app/views/page/index.pug +++ /dev/null @@ -1,12 +0,0 @@ -extends ../layouts/main -block content - - include ../components/page-header - - section.uk-section.uk-section-default.uk-section-xsmall - .uk-container - h1 Pages - ul.uk-list - each page of pages - li - h1= page.title \ No newline at end of file diff --git a/app/views/page/view.pug b/app/views/page/view.pug index fb11ee9..f552498 100644 --- a/app/views/page/view.pug +++ b/app/views/page/view.pug @@ -1,9 +1,22 @@ extends ../layouts/main block content - include ../components/page-header + include ../components/page-sidebar - section.uk-section.uk-section-default - .container - h1= page.title - article= page.content \ No newline at end of file + section.uk-section.uk-section-default.uk-section-small + .uk-container + div(uk-grid) + .uk-width-2-3 + article(dtp-page-id= page._id) + .uk-margin + div(uk-grid) + .uk-width-expand + h1.article-title= page.title + if user && user.flags.isAdmin + .uk-width-auto + a(href=`/admin/page/${page._id}`).uk-button.dtp-button-text EDIT + .uk-margin + != page.content + + .uk-width-1-3 + +renderPageSidebar() \ No newline at end of file diff --git a/app/views/post/view.pug b/app/views/post/view.pug index 058eac0..985b979 100644 --- a/app/views/post/view.pug +++ b/app/views/post/view.pug @@ -36,7 +36,7 @@ block content .uk-margin .uk-article-meta This post was updated on #{moment(post.updated).format('MMMM DD, YYYY, [at] hh:mm a')}. - if post.flags.enableComments + if user && post.flags.enableComments .dtp-border-bottom h3.uk-heading-bullet Comments diff --git a/config/limiter.js b/config/limiter.js index 6ea01d2..0639224 100644 --- a/config/limiter.js +++ b/config/limiter.js @@ -135,6 +135,20 @@ module.exports = { }, }, + /* + * PageController + */ + page: { + getView: { + total: 5, + expire: ONE_MINUTE, + message: 'You are reading pages too quickly', + }, + }, + + /* + * PostController + */ post: { getView: { total: 5, diff --git a/lib/site-platform.js b/lib/site-platform.js index 7f9cc6b..c464a2c 100644 --- a/lib/site-platform.js +++ b/lib/site-platform.js @@ -154,6 +154,7 @@ module.exports.startPlatform = async (dtp) => { }; module.exports.startWebServer = async (dtp) => { + const { page: pageService } = module.services; dtp.app = module.app = express(); @@ -296,6 +297,7 @@ module.exports.startWebServer = async (dtp) => { return next(error); } }); + module.app.use(pageService.menuMiddleware.bind(pageService)); /* * System Init