Browse Source

brought announcements in from Soapbox/Venue

develop
Rob Colbert 2 years ago
parent
commit
065e7fbfdd
  1. 1
      app/controllers/admin.js
  2. 109
      app/controllers/admin/announcement.js
  3. 66
      app/controllers/announcement.js
  4. 11
      app/controllers/home.js
  5. 23
      app/models/announcement.js
  6. 2
      app/models/chat-message.js
  7. 2
      app/models/email-log.js
  8. 1
      app/models/user.js
  9. 119
      app/services/announcement.js
  10. 27
      app/services/core-node.js
  11. 15
      app/services/user.js
  12. 50
      app/views/admin/announcement/editor.pug
  13. 28
      app/views/admin/announcement/index.pug
  14. 6
      app/views/admin/components/menu.pug
  15. 12
      app/views/announcement/components/announcement.pug
  16. 16
      app/views/announcement/index.pug
  17. 11
      app/views/components/page-sidebar.pug
  18. 2
      app/views/components/section-title.pug
  19. 30
      app/views/index.pug
  20. 1
      app/views/layouts/main-sidebar.pug
  21. 8
      client/less/site/content.less
  22. 6
      client/less/site/section.less
  23. 8
      client/less/site/sidebar.less
  24. 4
      client/less/site/uikit-theme.dtp-dark.less
  25. 4
      client/less/site/uikit-theme.dtp-light.less
  26. 3
      client/less/style.common.less

1
app/controllers/admin.js

@ -42,6 +42,7 @@ class AdminController extends SiteController {
}),
);
router.use('/announcement', await this.loadChild(path.join(__dirname, 'admin', 'announcement')));
router.use('/content-report', await this.loadChild(path.join(__dirname, 'admin', 'content-report')));
router.use('/core-node', await this.loadChild(path.join(__dirname, 'admin', 'core-node')));
router.use('/core-user', await this.loadChild(path.join(__dirname, 'admin', 'core-user')));

109
app/controllers/admin/announcement.js

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

66
app/controllers/announcement.js

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

11
app/controllers/home.js

@ -62,8 +62,15 @@ class HomeController extends SiteController {
res.render('policy/view');
}
async getHome (req, res) {
res.render('index');
async getHome (req, res, next) {
const { announcement: announcementService } = this.dtp.services;
try {
res.locals.announcements = await announcementService.getLatest(req.user);
res.render('index');
} catch (error) {
this.log.error('failed to render home view', { error });
return next(error);
}
}
}

23
app/models/announcement.js

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

2
app/models/chat-message.js

@ -1,6 +1,6 @@
// chat-message.js
// Copyright (C) 2022 DTP Technologies, LLC
// License Apache-2.0
// License: Apache-2.0
'use strict';

2
app/models/email-log.js

@ -1,6 +1,6 @@
// email-log.js
// Copyright (C) 2022 DTP Technologies, LLC
// All Rights Reserved
// License: Apache-2.0
'use strict';

1
app/models/user.js

@ -39,6 +39,7 @@ const UserSchema = new Schema({
optIn: { type: UserOptInSchema, required: true, select: false },
theme: { type: String, enum: DTP_THEME_LIST, default: 'dtp-light', required: true },
stats: { type: ResourceStats, default: ResourceStatsDefaults, required: true },
lastAnnouncement: { type: Date },
});
module.exports = mongoose.model('User', UserSchema);

119
app/services/announcement.js

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

27
app/services/core-node.js

@ -276,24 +276,27 @@ class CoreNodeService extends SiteService {
return { connectedCount, pendingCount, potentialReach };
}
async sendKaleidoscopeEvent (emitter, event) {
async sendKaleidoscopeEvent (event) {
const CORE_SCHEME = this.getCoreRequestScheme();
const { pkg } = this.dtp;
const { site } = this.dtp.config;
let emitterUrl = emitter.coreUserId ? `/user/core/${emitter._id}` : `/user/${emitter._id}`;
event.source = Object.assign({
pkg: { name: pkg.name, version: pkg.version },
site,
emitter: {
emitterType: emitter.type,
emitterId: emitter._id.toString(),
displayName: emitter.displayName,
username: emitter.username,
href: `${CORE_SCHEME}://${site.domain}${emitterUrl}`,
},
}, event.source);
if (event.emitter) {
let emitterUrl = event.emitter.coreUserId ? `/user/core/${event.emitter._id}` : `/user/${event.emitter._id}`;
event.source.emitter = {
emitterType: event.emitter.type,
emitterId: event.emitter._id.toString(),
displayName: event.emitter.displayName,
username: event.emitter.username,
href: `${CORE_SCHEME}://${site.domain}${emitterUrl}`,
};
}
const request = {
tokenized: false,
method: 'POST',
@ -339,6 +342,12 @@ class CoreNodeService extends SiteService {
return `${coreScheme}://${core.address.host}:${core.address.port}${requestUrl}`;
}
getLocalUrl (url) {
const CORE_SCHEME = this.getCoreRequestScheme();
const { site } = this.dtp.config;
return `${CORE_SCHEME}://${site.domain}${url}`;
}
async sendRequest (core, request) {
try {
const req = new CoreNodeRequest();

15
app/services/user.js

@ -637,6 +637,21 @@ class UserService extends SiteService {
{ upsert: true },
);
}
/**
* Updates the `lastAnnouncement` field of a User to the `created` date of the
* specified announcement (for tracking last-seen announcements).
* @param {User} user The user being updated
* @param {Announcement} announcement The announcement being seen by the User
*/
async setLastAnnouncement (user, announcement) {
await User.updateOne(
{ _id: user._id },
{
$set: { lastAnnouncement: announcement.created },
},
);
}
}
module.exports = {

50
app/views/admin/announcement/editor.pug

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

28
app/views/admin/announcement/index.pug

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

6
app/views/admin/components/menu.pug

@ -15,6 +15,12 @@ ul(uk-nav).uk-nav-default
li.uk-nav-divider
li(class={ 'uk-active': (adminView === 'announcement') })
a(href="/admin/announcement")
span.nav-item-icon
i.fas.fa-bullhorn
span.uk-margin-small-left Announcements
li(class={ 'uk-active': (adminView === 'user') })
a(href="/admin/user")
span.nav-item-icon

12
app/views/announcement/components/announcement.pug

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

16
app/views/announcement/index.pug

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

11
app/views/components/page-sidebar.pug

@ -1,6 +1,13 @@
include ../announcement/components/announcement
mixin renderPageSidebar ( )
//- Gab TV 3 Most Recent Episodes
.uk-margin
if Array.isArray(announcements) && (announcements.length > 0)
ul.uk-list.uk-margin
each announcement in announcements
li
+renderAnnouncement(announcement)
.sidebar-widget.uk-margin
+renderSectionTitle('Widget', {
label: 'Sample URL',
title: 'Sample URL Title',

2
app/views/components/section-title.pug

@ -2,7 +2,7 @@ mixin renderSectionTitle (title, barButton)
.dtp-border-bottom
div(uk-grid).uk-grid-small.uk-flex-middle
.uk-width-expand
h3.uk-heading-bullet.uk-margin-small= title
h4.uk-heading-bullet.uk-margin-small= title
if barButton
.uk-width-auto
a(href= barButton.url, title= barButton.title).uk-button.uk-button-link.uk-button-small= barButton.label

30
app/views/index.pug

@ -1,22 +1,22 @@
extends layouts/main-sidebar
block content
p
img(src="/img/the-bobs.jpg", alt="The Bobs have questions").responsive
img(src="/img/the-bobs.jpg", alt="The Bobs have questions").responsive
h1 Sample DTP Web Application
p This application doesn't actually do anything. The Bobs would have questions.
.content-block
h1 Sample DTP Web Application
p This application doesn't actually do anything. The Bobs would have questions.
if user
h2 Current User
pre= JSON.stringify(user, null, 2)
if user
h2 Current User
pre= JSON.stringify(user, null, 2)
if session
h2 Session
pre= JSON.stringify(session, null, 2)
h2 Site Configuration
pre= JSON.stringify(config, null, 2)
if session
h2 Session
pre= JSON.stringify(session, null, 2)
h2 Package Information
pre= JSON.stringify(pkg, null, 2)
h2 Site Configuration
pre= JSON.stringify(config, null, 2)
h2 Package Information
pre= JSON.stringify(pkg, null, 2)

1
app/views/layouts/main-sidebar.pug

@ -6,6 +6,7 @@ block content-container
div(uk-grid)#dtp-content-grid
div(class="uk-width-1-1 uk-width-2-3@m")
block content
div(class="uk-width-1-1 uk-width-1-3@m")
block content-sidebar
+renderPageSidebar()

8
client/less/site/content.less

@ -0,0 +1,8 @@
.content-block {
padding: @global-gutter;
background-color: @content-background-color;
:last-child {
margin-bottom: 0;
}
}

6
client/less/site/section.less

@ -0,0 +1,6 @@
section.uk-section {
&.uk-section-default {
background-color: @page-background-color;
}
}

8
client/less/site/sidebar.less

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

4
client/less/site/uikit-theme.dtp-dark.less

@ -2,6 +2,10 @@
// Colors
//
@page-background-color: #000000;
@content-background-color: #2a2a2a;
@content-border-color: #4a4a4a;
@site-brand-color: #ff0013;
@button-label-color: #e8e8e8;
@button-label-hover: #2a2a2a;

4
client/less/site/uikit-theme.dtp-light.less

@ -2,6 +2,10 @@
// Colors
//
@page-background-color: #e8e8e8;
@content-background-color: #ffffff;
@content-border-color: #a8a8a8;
@site-brand-color: #ff0013;
@button-label-color: #2a2a2a;
@button-label-hover: #ffffff;

3
client/less/style.common.less

@ -9,10 +9,13 @@
@import "site/image.less";
@import "site/nav.less";
@import "site/content.less";
@import "site/core-node.less";
@import "site/dashboard.less";
@import "site/site.less";
@import "site/form.less";
@import "site/button.less";
@import "site/sidebar.less";
@import "site/section.less";
@import "site/chat.less";
Loading…
Cancel
Save