parent
cf1a95e340
commit
c8e37db1c6
@ -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); },
|
||||||
|
};
|
@ -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.
|
@ -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
|
extends ../layouts/main
|
||||||
block content
|
block content
|
||||||
|
|
||||||
include ../components/page-header
|
include ../components/page-sidebar
|
||||||
|
|
||||||
section.uk-section.uk-section-default
|
section.uk-section.uk-section-default.uk-section-small
|
||||||
.container
|
.uk-container
|
||||||
h1= page.title
|
div(uk-grid)
|
||||||
article= page.content
|
.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()
|
Loading…
Reference in new issue