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