26 changed files with 534 additions and 31 deletions
@ -0,0 +1,109 @@ |
|||
// admin/announcement.js
|
|||
// Copyright (C) 2022 DTP Technologies, LLC
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
const express = require('express'); |
|||
|
|||
const { SiteController } = require('../../../lib/site-lib'); |
|||
|
|||
class AnnouncementAdminController extends SiteController { |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, module.exports); |
|||
} |
|||
|
|||
async start ( ) { |
|||
const router = express.Router(); |
|||
router.use(async (req, res, next) => { |
|||
res.locals.currentView = 'admin'; |
|||
res.locals.adminView = 'announcement'; |
|||
return next(); |
|||
}); |
|||
|
|||
router.param('announcementId', this.populateAnnouncementId.bind(this)); |
|||
|
|||
router.post('/:announcementId', this.postUpdateAnnouncement.bind(this)); |
|||
router.post('/', this.postCreateAnnouncement.bind(this)); |
|||
|
|||
router.get('/create', this.getAnnouncementEditor.bind(this)); |
|||
router.get('/:announcementId', this.getAnnouncementEditor.bind(this)); |
|||
|
|||
router.get('/', this.getHomeView.bind(this)); |
|||
|
|||
router.delete('/:announcementId', this.deleteAnnouncement.bind(this)); |
|||
|
|||
return router; |
|||
} |
|||
|
|||
async populateAnnouncementId (req, res, next, announcementId) { |
|||
const { announcement: announcementService } = this.dtp.services; |
|||
try { |
|||
res.locals.announcement = await announcementService.getById(announcementId); |
|||
return next(); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
} |
|||
|
|||
async postUpdateAnnouncement (req, res, next) { |
|||
const { announcement: announcementService } = this.dtp.services; |
|||
try { |
|||
await announcementService.update(res.locals.announcement, req.body); |
|||
res.redirect('/admin/announcement'); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
} |
|||
|
|||
async postCreateAnnouncement (req, res, next) { |
|||
const { announcement: announcementService } = this.dtp.services; |
|||
try { |
|||
await announcementService.create(req.body); |
|||
res.redirect('/admin/announcement'); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
} |
|||
|
|||
async getAnnouncementEditor (req, res) { |
|||
res.render('admin/announcement/editor'); |
|||
} |
|||
|
|||
async getHomeView (req, res, next) { |
|||
const { announcement: announcementService } = this.dtp.services; |
|||
try { |
|||
res.locals.pagination = this.getPaginationParameters(req, 20); |
|||
res.locals.announcements = await announcementService.getAnnouncements(res.locals.pagination); |
|||
res.render('admin/announcement/index'); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
} |
|||
|
|||
async deleteAnnouncement (req, res) { |
|||
const { announcement: announcementService } = this.dtp.services; |
|||
try { |
|||
const displayList = this.createDisplayList('delete-announcement'); |
|||
await announcementService.remove(res.locals.announcement); |
|||
displayList.reloadView(); |
|||
res.status(200).json({ success: true, displayList }); |
|||
} catch (error) { |
|||
this.log.error('failed to delete announcement', { error }); |
|||
res.status(error.statusCode || 500).json({ |
|||
success: false, |
|||
message: error.message, |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
name: 'announcement', |
|||
slug: 'announcement', |
|||
create: async (dtp) => { |
|||
let controller = new AnnouncementAdminController(dtp); |
|||
return controller; |
|||
}, |
|||
}; |
@ -0,0 +1,66 @@ |
|||
// announcement.js
|
|||
// Copyright (C) 2022 DTP Technologies, LLC
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
const express = require('express'); |
|||
|
|||
const { SiteController, SiteError } = require('../../lib/site-lib'); |
|||
|
|||
class AnnouncementController extends SiteController { |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, module.exports); |
|||
} |
|||
|
|||
async start ( ) { |
|||
const router = express.Router(); |
|||
this.dtp.app.use('/announcement', router); |
|||
|
|||
router.param('announcementId', this.populateAnnouncementId.bind(this)); |
|||
|
|||
router.get('/:announcementId', this.getAnnouncementView.bind(this)); |
|||
router.get('/', this.getHome.bind(this)); |
|||
|
|||
return router; |
|||
} |
|||
|
|||
async populateAnnouncementId (req, res, next, announcementId) { |
|||
const { announcement: announcementService } = this.dtp.services; |
|||
try { |
|||
res.locals.announcement = await announcementService.getById(announcementId); |
|||
if (!res.locals.announcement) { |
|||
this.log.error('announcement not found', { announcementId }); |
|||
return next(new SiteError(404, 'Announcement not found')); |
|||
} |
|||
return next(); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
} |
|||
|
|||
async getAnnouncementView (req, res) { |
|||
res.render('announcement/view'); |
|||
} |
|||
|
|||
async getHome (req, res, next) { |
|||
const { announcement: announcementService } = this.dtp.services; |
|||
try { |
|||
res.locals.pagination = this.getPaginationParameters(req, 10); |
|||
res.locals.announcements = await announcementService.getAnnouncements(res.locals.pagination); |
|||
res.render('announcement/index'); |
|||
} catch (error) { |
|||
return next(error); |
|||
} |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
slug: 'announcement', |
|||
name: 'announcement', |
|||
create: async (dtp) => { |
|||
let controller = new AnnouncementController(dtp); |
|||
return controller; |
|||
}, |
|||
}; |
@ -0,0 +1,23 @@ |
|||
// announcement.js
|
|||
// Copyright (C) 2022 DTP Technologies, LLC
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
const mongoose = require('mongoose'); |
|||
|
|||
const Schema = mongoose.Schema; |
|||
|
|||
const AnnouncementSchema = new Schema({ |
|||
created: { type: Date, default: Date.now, required: true, index: -1, expires: '21d' }, |
|||
title: { |
|||
icon: { |
|||
class: { type: String, default: 'fa-bullhorn', required: true }, |
|||
color: { type: String, default: '#ffffff', required: true }, |
|||
}, |
|||
content: { type: String, required: true }, |
|||
}, |
|||
content: { type: String, required: true }, |
|||
}); |
|||
|
|||
module.exports = mongoose.model('Announcement', AnnouncementSchema); |
@ -0,0 +1,119 @@ |
|||
// announcement.js
|
|||
// Copyright (C) 2022 DTP Technologies, LLC
|
|||
// All Rights Reserved
|
|||
|
|||
'use strict'; |
|||
|
|||
const mongoose = require('mongoose'); |
|||
const Announcement = mongoose.model('Announcement'); |
|||
|
|||
const { SiteService } = require('../../lib/site-lib'); |
|||
|
|||
class AnnouncementService extends SiteService { |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, module.exports); |
|||
} |
|||
|
|||
async create (announcementDefinition) { |
|||
const NOW = new Date(); |
|||
const { coreNode: coreNodeService } = this.dtp.services; |
|||
|
|||
const announcement = new Announcement(); |
|||
announcement.created = NOW; |
|||
announcement.title = { |
|||
icon: { |
|||
class: announcementDefinition.titleIconClass, |
|||
color: announcementDefinition.titleIconColor, |
|||
}, |
|||
content: announcementDefinition.titleContent, |
|||
}; |
|||
announcement.content = announcementDefinition.content.trim(); |
|||
|
|||
await announcement.save(); |
|||
|
|||
/* |
|||
* Broadcast the Announcement to your DTP Constellation. |
|||
*/ |
|||
const announcementHref = coreNodeService.getLocalUrl(`/announcement/${announcement._id}`); |
|||
await coreNodeService.sendKaleidoscopeEvent({ |
|||
action: 'announcement', |
|||
label: announcement.title.content, |
|||
content: announcement.content, |
|||
href: announcementHref, |
|||
}); |
|||
|
|||
return announcement.toObject(); |
|||
} |
|||
|
|||
async update (announcement, announcementDefinition) { |
|||
await Announcement.updateOne( |
|||
{ _id: announcement._id }, |
|||
{ |
|||
$set: { |
|||
title: { |
|||
icon: { |
|||
class: announcementDefinition.titleIconClass, |
|||
color: announcementDefinition.titleIconColor, |
|||
}, |
|||
content: announcementDefinition.titleContent, |
|||
}, |
|||
content: announcementDefinition.content.trim(), |
|||
}, |
|||
}, |
|||
); |
|||
} |
|||
|
|||
async getLatest (user) { |
|||
const { user: userService } = this.dtp.services; |
|||
let announcements = [ ]; |
|||
|
|||
if (user) { |
|||
const search = { }; |
|||
if (user.lastAnnouncement) { |
|||
search.created = { $gt: user.lastAnnouncement }; |
|||
} |
|||
announcements = await Announcement |
|||
.find(search) |
|||
.sort({ created: -1 }) |
|||
.limit(1) |
|||
.lean(); |
|||
if (announcements && (announcements.length > 0)) { |
|||
await userService.setLastAnnouncement(user, announcements[0]); |
|||
} |
|||
} else { |
|||
announcements = await Announcement |
|||
.find() |
|||
.sort({ created: -1 }) |
|||
.limit(1) |
|||
.lean(); |
|||
} |
|||
|
|||
return announcements; |
|||
} |
|||
|
|||
async getById (announcementId) { |
|||
const announcement = await Announcement.findById(announcementId); |
|||
return announcement; |
|||
} |
|||
|
|||
async getAnnouncements (pagination) { |
|||
const announcements = await Announcement |
|||
.find() |
|||
.sort({ created: -1 }) |
|||
.skip(pagination.skip) |
|||
.limit(pagination.cpp) |
|||
.lean(); |
|||
return announcements; |
|||
} |
|||
|
|||
async remove (announcement) { |
|||
await Announcement.deleteOne({ _id: announcement._id }); |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
slug: 'announcement', |
|||
name: 'announcement', |
|||
create: (dtp) => { return new AnnouncementService(dtp); }, |
|||
}; |
@ -0,0 +1,50 @@ |
|||
extends ../layouts/main |
|||
block content |
|||
|
|||
- var formActionUrl = announcement ? `/admin/announcement/${announcement._id}` : '/admin/announcement'; |
|||
|
|||
form(method="POST", action= formActionUrl).uk-form |
|||
.uk-card.uk-card-default.uk-card-small |
|||
.uk-card-header |
|||
h1 Announcement Editor |
|||
|
|||
.uk-card-body |
|||
div(uk-grid) |
|||
.uk-width-1-2 |
|||
.uk-margin |
|||
label(for="icon-class").uk-form-label Icon Class |
|||
input( |
|||
id="icon-class", |
|||
name="titleIconClass", |
|||
type="text", |
|||
placeholder="Enter FontAwesome icon class", |
|||
value= announcement ? announcement.title.icon.class : 'fa-bullhorn', |
|||
).uk-input |
|||
.uk-text-small |
|||
a(href="https://fontawesome.com/v5/search", target="_blank") Search icons |
|||
|
|||
.uk-width-1-2 |
|||
.uk-margin |
|||
label(for="icon-color").uk-form-label Icon Color |
|||
input( |
|||
id="icon-color", |
|||
name="titleIconColor", |
|||
type="color", |
|||
value= announcement ? announcement.title.icon.color : '#ff0013', |
|||
).uk-input |
|||
|
|||
.uk-margin |
|||
label(for="title").uk-form-label Title |
|||
input( |
|||
id="title", |
|||
name="titleContent", |
|||
type="text", |
|||
placeholder="Enter announcement title", value= announcement ? announcement.title.content : undefined, |
|||
).uk-input |
|||
|
|||
.uk-margin |
|||
label(for="content").uk-form-label Announcement Body |
|||
textarea(id="content", name="content", rows="4", placeholder="Enter announcement").uk-textarea= announcement ? announcement.content : undefined |
|||
|
|||
.uk-card-footer |
|||
button(type="submit").uk-button.dtp-button-primary= announcement ? 'Update Announcement' : 'Create Announcement' |
@ -0,0 +1,28 @@ |
|||
extends ../layouts/main |
|||
block content |
|||
|
|||
div(uk-grid).uk-grid-small |
|||
.uk-width-expand |
|||
h1 Announcements |
|||
.uk-width-auto |
|||
a(href="/admin/announcement/create").uk-button.dtp-button-primary |
|||
span |
|||
i.fas.fa-plus |
|||
span.uk-margin-small-left Create |
|||
|
|||
if Array.isArray(announcements) && (announcements.length > 0) |
|||
ul.uk-list.uk-list-divider |
|||
each announcement in announcements |
|||
li |
|||
div(uk-grid).uk-grid-small.uk-flex-middle |
|||
.uk-width-expand |
|||
a(href=`/admin/announcement/${announcement._id}`) |
|||
span |
|||
i(class=`fas ${announcement.title.icon.class}`) |
|||
span.uk-margin-small-left= announcement.title.content |
|||
.uk-width-auto |
|||
button(type="button", data-announcement-id= announcement._id, onclick="return dtp.adminApp.deleteAnnouncement(event);").uk-button.dtp-button-danger |
|||
span |
|||
i.fas.fa-trash |
|||
else |
|||
div There are no announcements. |
@ -0,0 +1,12 @@ |
|||
mixin renderAnnouncement (announcement) |
|||
.uk-card.uk-card-default.uk-card-small |
|||
.uk-card-header |
|||
h1.uk-card-title |
|||
span |
|||
i(class=`fas ${announcement.title.icon.class}`, style=`color: ${announcement.title.icon.color}`) |
|||
span.uk-margin-small-left= announcement.title.content |
|||
.uk-card-body!= marked.parse(announcement.content, { renderer: marked.Renderer() }) |
|||
.uk-card-footer |
|||
.uk-text-small.uk-text-muted.uk-flex.uk-flex-between |
|||
div= moment(announcement.created).format('MMM DD, YYYY') |
|||
div= moment(announcement.created).format('hh:mm a') |
@ -0,0 +1,16 @@ |
|||
extends ../layouts/main |
|||
block content |
|||
|
|||
include components/announcement |
|||
|
|||
section.uk-section.uk-section-default.uk-section-small |
|||
.uk-container |
|||
h1 #{site.name} Announcements |
|||
|
|||
if Array.isArray(announcements) && (announcements.length > 0) |
|||
ul.uk-list.uk-list-divider |
|||
each announcement in announcements |
|||
li |
|||
+renderAnnouncement(announcement) |
|||
else |
|||
div There are no announcements. |
@ -0,0 +1,8 @@ |
|||
.content-block { |
|||
padding: @global-gutter; |
|||
background-color: @content-background-color; |
|||
|
|||
:last-child { |
|||
margin-bottom: 0; |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
section.uk-section { |
|||
|
|||
&.uk-section-default { |
|||
background-color: @page-background-color; |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
.sidebar-widget { |
|||
display: block; |
|||
padding: @global-gutter; |
|||
background-color: @content-background-color; |
|||
border: solid 1px @content-border-color; |
|||
border-radius: 6px; |
|||
overflow: hidden; |
|||
} |
Loading…
Reference in new issue