Pages, and TinyMCE put in dark mode

master
rob 3 years ago
parent cf1a95e340
commit c8e37db1c6

@ -7,7 +7,7 @@
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"name": "dtp-sites",
"skipFiles": [
"<node_internals>/**"
],

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save