Browse Source

Newsroom Kiosk Chyron added

And progress on the weather widget, which is unfinished.
develop
Rob Colbert 4 months ago
parent
commit
c0f44ad99c
  1. 6
      README.md
  2. 19
      app/controllers/newsroom.js
  3. 28
      app/models/postal-code.js
  4. 8
      app/models/user.js
  5. 14
      app/services/venue.js
  6. 94
      app/services/weather.js
  7. 52
      app/views/newsroom/kiosk/chyron.pug
  8. 5
      app/views/newsroom/layouts/widget.pug
  9. BIN
      client/img/vote-gov.png
  10. 8
      client/less/site/main.less
  11. 12
      client/less/site/uikit-theme.dtp-light.less
  12. 106
      client/less/site/widget.less
  13. 1
      client/less/style.common.less
  14. 5
      config/limiter.js
  15. 21
      data/static/license-uszips.txt
  16. 33789
      data/static/uszips.csv

6
README.md

@ -132,4 +132,8 @@ Redis simply has many different documents to describe it's many different featur
## Software License
DTP Sites and the DTP Phoenix Engine and framework are licensed under the [Apache 2.0](https://spdx.org/licenses/Apache-2.0.html) open source software license. See [LICENSE](LICENSE) for more information.
DTP Sites and the DTP Phoenix Engine and framework are licensed under the [Apache 2.0](https://spdx.org/licenses/Apache-2.0.html) open source software license. See [LICENSE](LICENSE) for more information.
Uses postal code data provided by SimpleMaps: https://simplemaps.com/data/us-zips. The data is used to find geographic coordinates for a postal code. Those coordinates are then used to request weather data for that location.
Uses GeoIP-Lite data to geolocate HTTP requests by source IP address. The data is used to find geographic coordinates for an IP address to request weather data for that location. IP addresses and related information may be used for recordkeeping and to help balance traffic while also seeing (roughly) where it comes from in aggregate reports.

19
app/controllers/newsroom.js

@ -32,6 +32,12 @@ class NewsroomController extends SiteController {
router.param('feedId', this.populateFeedId.bind(this));
router.param('feedEntryId', this.populateFeedEntryId.bind(this));
router.get(
'/kiosk/chyron',
limiterService.createMiddleware(limiterConfig.getKioskChyron),
kioskMiddleware,
this.getKioskChyron.bind(this),
);
router.get(
'/kiosk/:feedId/article/:feedEntryId/image',
limiterService.createMiddleware(limiterConfig.getKioskFeedEntryImage),
@ -161,6 +167,19 @@ class NewsroomController extends SiteController {
}
}
async getKioskChyron (req, res, next) {
const { feed: feedService } = this.dtp.services;
try {
res.locals.viewMode = 'widget';
res.locals.pagination = this.getPaginationParameters(req, 25);
res.locals.entries = await feedService.getNewsfeed(res.locals.pagination);
res.render('newsroom/kiosk/chyron');
} catch (error) {
this.log.error('failed to serve Newsroom Kiosk Chyron view', { error });
return next(error);
}
}
async getKiosk (req, res, next) {
const { feed: feedService } = this.dtp.services;
try {

28
app/models/postal-code.js

@ -0,0 +1,28 @@
// user.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const PostalCodeSchema = new Schema({
zip: { type: Number, required: true, index: 1 },
lat: { type: Number, required: true },
lon: { type: Number, required: true },
city: { type: String },
state: {
abbreviation: { type: String },
name: { type: String },
},
county: { type: String, required: true },
timezone: { type: String, required: true },
population: { type: Number },
imprecise: { type: Boolean, default: false, required: true },
military: { type: Boolean, default: false, required: true },
});
module.exports = (conn) => {
return conn.model('PostalCode', PostalCodeSchema);
};

8
app/models/user.js

@ -20,6 +20,13 @@ const {
UserOptInSchema,
} = require('./lib/user-types');
const WidgetSettingsSchema = new Schema({
weather: {
lat: { type: Number },
lon: { type: Number },
},
});
const UserSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: -1 },
email: { type: String, required: true, lowercase: true, unique: true, select: false },
@ -41,6 +48,7 @@ const UserSchema = new Schema({
theme: { type: String, enum: DTP_THEME_LIST, default: 'dtp-light', required: true },
resourceStats: { type: ResourceStats, default: ResourceStatsDefaults, required: true },
lastAnnouncement: { type: Date },
widgetSettings: { type: WidgetSettingsSchema, select: false },
}, {
toObject: { virtuals: true },
});

14
app/services/venue.js

@ -54,15 +54,19 @@ class VenueService extends SiteService {
return next();
}
this.log.info('populating Venue channel data for route', { path: req.path });
this.log.debug('populating Venue channel data for route', { path: req.path });
await VenueChannel
.find()
.populate(this.populateVenueChannel)
.lean()
.cursor()
.eachAsync(async (channel) => {
channel.currentStatus = await this.updateChannelStatus(channel);
res.locals.venue.channels.push(channel);
try {
channel.currentStatus = await this.updateChannelStatus(channel);
res.locals.venue.channels.push(channel);
} catch (error) {
// fall through, no venue for you.
}
});
return next();
} catch (error) {
@ -213,7 +217,7 @@ class VenueService extends SiteService {
const { logan: loganService } = this.dtp.services;
try {
const requestUrl = `https://${this.soapboxDomain}/channel/${channel.slug}/status`;
this.log.info('fetching Shing channel status', { slug: channel.slug, requestUrl });
this.log.debug('fetching Shing channel status', { slug: channel.slug, requestUrl });
const response = await fetch(requestUrl, {
agent: this.httpsAgent,
@ -232,7 +236,7 @@ class VenueService extends SiteService {
}
return json.channel;
} catch (error) {
this.log.error('failed to update channel status', { error });
// this.log.error('failed to update channel status', { error });
loganService.sendEvent(module.exports, {
level: 'error',
event: 'updateChannelStatus',

94
app/services/weather.js

@ -0,0 +1,94 @@
// weather.js
// Copyright (C) 2022,2023 DTP Technologies, LLC
// License: Apache-2.0
//
// Postal code data provided by:
// https://simplemaps.com/data/us-zips
'use strict';
const mongoose = require('mongoose');
const User = mongoose.model('User');
const geoip = require('geoip-lite');
const { SiteService, SiteError } = require('../../lib/site-lib');
class WeatherService extends SiteService {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
this.userAgent = '(sites.digitaltelepresence.com, [email protected])';
}
/**
* Attempts to find an authenticated User in the request session, and then
* attempts to find widget configuration data for the weather widget. If
* found, a precise report is fetched based on the User's postal code and
* coordinates.
*
* If a User is not found in the session, or if the User has not provided a
* postal code in settings, attempt to resolve the request's source IP address
* using GeoIP-Lite. This will return a postal code and also geo coordinates,
* whcih are then used only to generate a report (not saved to the User as
* settings).
*
* @param {Request} req ExpressJS request object for which a report is to be created.
*/
async getReport (req) {
if (req.user) {
try {
const report = await this.getUserReport(req.user);
return report;
} catch (error) {
// fall through
}
}
if (req.ip) {
const geo = geoip.lookup(req.ip);
if (geo) {
visit.geoip = {
country: geo.country,
region: geo.region,
eu: geo.eu,
timezone: geo.timezone,
city: geo.city,
};
if (Array.isArray(geo.ll) && (geo.ll.length === 2)) {
visit.geoip.location = {
type: 'Point',
coordinates: geo.ll,
};
}
}
}
}
/**
* Fetch a weather forecast and report for the given user or source IP address.
* @param {User} user The User issuing the request (if any)
*/
async getUserReport (user) {
const { weather } = user.widgetSettings;
const response = await fetch(`https://api.weather.gov/points/${weather.lat},${weather.lon}`, {
headers: {
'User-Agent': this.userAgent,
},
});
}
async getLocationReport (location) {
}
}
module.exports = {
logId: 'svc:weather',
index: 'weather',
className: 'WeatherService',
create: (dtp) => { return new WeatherService(dtp); },
};

52
app/views/newsroom/kiosk/chyron.pug

@ -0,0 +1,52 @@
extends ../layouts/widget
block kiosk-content
include ../components/kiosk-carousel
mixin renderChyronCard (entry)
-
var feedLink = entry.feed.link.replace('http://', '').replace('https://', '');
if (feedLink.endsWith('/')) {
feedLink = feedLink.slice(0, -1);
}
.dtp-chyron-card
.uk-flex.uk-match-height
.uk-width-auto
img(src= `/newsroom/kiosk/${entry.feed._id}/article/${entry._id}/image`).feed-entry-image
.uk-width-expand
.feed-entry-content
h1.uk-text-truncate= entry.title
.feed-entry-description!= marked.parse(entry.description)
.feed-entry-meta
div(uk-grid)
.uk-width-auto= entry.feed.title
.uk-width-expand= moment(entry.published).format('MMMM D [at] h:mm a')
.uk-width-auto= feedLink
.dtp-chyron
each entry in entries.entries
+renderChyronCard(entry)
block viewjs
script.
(( ) => {
const cards = document.querySelectorAll('.dtp-chyron-card');
for (const card of cards) {
card.removeAttribute('active');
}
let activeCard, cardIdx = 0;
function advance ( ) {
if (++cardIdx >= cards.length) {
cardIdx = 0;
}
activeCard.removeAttribute('active');
activeCard = cards[cardIdx];
activeCard.toggleAttribute('active', true);
}
activeCard = cards[cardIdx];
activeCard.toggleAttribute('active', true);
setInterval(advance, 15000);
})();

5
app/views/newsroom/layouts/widget.pug

@ -0,0 +1,5 @@
extends ../../layouts/focused
block content
.dtp-widget
.dtp-widget-content
block kiosk-content

BIN
client/img/vote-gov.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

8
client/less/site/main.less

@ -2,6 +2,7 @@ html, body {
margin: 0;
padding: 0;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
}
body {
@ -49,6 +50,13 @@ body {
}
}
html, body {
&[data-view-mode="widget"] {
background: none;
}
}
.no-select {
user-select: none;
-moz-user-select: none;

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

@ -18,6 +18,13 @@
@scrollbar-border-color: @content-border-color;
@scrollbar-thumb-color: #ff001380;
//
// Post tags
//
@post-tag-background-color: #1a1a1a;
@post-tag-color: #e8e8e8;
//
// Component: Navbar
//
@ -32,7 +39,4 @@
@internal-form-datalist-image: "/uikit/images/backgrounds/form-datalist.svg";
@internal-form-radio-image: "/uikit/images/backgrounds/form-radio.svg";
@internal-form-checkbox-image: "/uikit/images/backgrounds/form-checkbox.svg";
@internal-form-checkbox-indeterminate-image: "/uikit/images/backgrounds/form-checkbox-indeterminate.svg";
@post-tag-background-color: #1a1a1a;
@post-tag-color: #e8e8e8;
@internal-form-checkbox-indeterminate-image: "/uikit/images/backgrounds/form-checkbox-indeterminate.svg";

106
client/less/site/widget.less

@ -0,0 +1,106 @@
@chyron-height: 240px;
@chyron-color: #e8e8e8;
@chyron-border-color: #e8e8e8;
.dtp-widget {
position: fixed;
top: 0; right: 0; bottom: 0; left: 0;
box-sizing: border-box;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
flex-grow: 1 1;
background: none;
.dtp-widget-content {
box-sizing: border-box;
position: relative;
flex-grow: 1 1;
width: 100%;
background: none;
color: @chyron-color;
.dtp-chyron {
box-sizing: border-box;
width: 100%;
.dtp-chyron-card {
box-sizing: border-box;
display: none;
border: solid 3px @chyron-border-color;
// border-top-left-radius: 16px;
// border-top-right-radius: 16px;
overflow: hidden;
opacity: 0;
transition: opacity 1s;
&[active] {
display: block;
opacity: 1;
}
img.feed-entry-image {
box-sizing: border-box;
height: @chyron-height;
width: @chyron-height * 1.78; // 16:9
object-fit: cover;
object-position: center center;
}
.feed-entry-content {
box-sizing: border-box;
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: 0 16px 8px 16px;
background: rgba(0, 0, 0, 0.9);
font-size: 28px;
line-height: 1.2em;
h1, h2, h3, h4, h5, h6 {
margin: 0;
margin-bottom: 8px;
padding: 8px 0;
flex-grow: 0;
line-height: 1.1em;
font-size: 38px;
color: inherit;
border-bottom: solid 2px;
border-color: #c0c0c0;
}
.feed-entry-description {
display: -webkit-box;
flex-grow: 1;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.feed-entry-meta {
flex-grow: 0;
font-size: 24px;
}
}
}
}
}
}

1
client/less/style.common.less

@ -21,5 +21,6 @@
@import "site/section.less";
@import "site/sidebar.less";
@import "site/site.less";
@import "site/widget.less";
@import "site/chat.less";

5
config/limiter.js

@ -303,6 +303,11 @@ module.exports = {
expire: ONE_MINUTE,
message: 'You are loading Newsroom kiosk feeds too quickly',
},
getKioskChyron: {
total: 15,
expire: ONE_MINUTE,
message: 'You are loading the Newsroom kiosk chyron too quickly',
},
getKiosk: {
total: 15,
expire: ONE_MINUTE,

21
data/static/license-uszips.txt

@ -0,0 +1,21 @@
This license is a legal document designed to protect your rights and the rights of the Pareto Software, LLC, the owner of Simplemaps.com. Please read it carefully. Purchasing or downloading a data product constitutes acceptance of this license.
Description of Product and Parties: This license is a contract between you (hereafter, the Customer) and Pareto Software, LLC (hereafter, the Provider) regarding the use and/or sale of an collection of geographic data (hereafter, the Database).
Ownership of Database: All rights to the Database are owned by the Provider. The Database is a cleaned and curated collection of geographic facts and the Provider retains all rights to the Database afforded by the law. Ownership of any intellectual property generated by the Provider while performing custom modifications to the Database for a Customer (with or without payment) is retained by the Provider.
License: Customers who purchase a license are allowed to use the database for projects that benefit their organization or that their organization oversees. Attribution is not required. The Customer is allowed to query the database to power privately or publicly facing applications. The Customer is allowed to make copies and backups of the data. The Customer may not publicly redistribute the Database without prior written permission. Customers can transfer their license to a third-party, at the sole discretion of the Provider, by submitting a request via email.
Free US Zip Code Database: The Provider offers a free version of the US Zip Code Database. This Database is offered free of charge conditional on a link back to https://simplemaps.com/data/us-zips. This backlink must come from a public webpage where the Customer is using the data. If the Customer uses the data internally, the backlink must be placed on the organization's website on a page that can be easily found though links on the root domain. The link must be clearly visible to the human eye. The backlink must be placed before the Customer uses the Database in production.
Free US Cities Database: The Provider offers a free version of the US Cities Database. This Database is offered free of charge conditional on a link back to https://simplemaps.com/data/us-cities. This backlink must come from a public webpage where the Customer is using the data. If the Customer uses the data internally, the backlink must be placed on the organization's website on a page that can be easily found though links on the root domain. The link must be clearly visible to the human eye. The backlink must be placed before the Customer uses the Database in production.
Basic World Cities Database: The Provider offers a Basic World Cities Database free of charge. This database is licensed under the MIT license as described at: https://opensource.org/licenses/MIT.
Comprehensive World Cities Database Density Data: The Comprehensive World Cities Database includes density estimates from The Center for International Earth Science Information Network - CIESIN - Columbia University. 2016. Gridded Population of the World, Version 4 (GPWv4): Population Count. Palisades, NY: NASA Socioeconomic Data and Applications Center (SEDAC). http://dx.doi.org/10.7927/H4X63JVC. Accessed June 2017. The density estimates are include under the Creative Commons Attribution 4.0 International License. The Provider places no additional restrictions on the use or distribution of the density data.
Guarantee: The Provider guarantees that for the period of thirty (30) days from the purchase of a License that the Customer shall, upon request, be refunded their actual purchase price within a reasonable period of time. The Customer acknowledges that receipt of a refund constitutes a termination of their License to use the Database. In the event of a Refund, the Customer promises to delete the Database immediately. Refunds after the period of thirty (30) days shall be at the sole discretion of the Provider.
LIMITATION OF LIABILITY: THE DATABASE IS SOLD "AS IS" AND "WITH ALL FAULTS". THE PROVIDER MAKES NO WARRANTY THAT IT IS FREE OF DEFECTS OR IS SUITABLE FOR ANY PARTICULAR PURPOSE. IN NO EVENT SHALL THE PROVIDER BE RESPONSIBLE FOR LOSS OR DAMAGES ARRISING FROM THE INSTALLATION OR USE OF THE DATABASE, INCLUDING BUT NOT LIMITED TO ANY INDIRECT, PUNITIVE, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES. THE CUSTOMER IS SOLELY RESPONSIBLE FOR ENSURING THAT THEIR USE OF THE DATABASE IS IN ACCORDANCE WITH THE LAW OF THEIR JURISDICTION.
PROHIBITION OF ILLEGAL USE: USE OF THE DATABASE WHICH IS CONTRARY TO THE LAW IS PROHIBITED, AND IMMEDIATELY TERMINATES THE CUSTOMER'S LICENSE TO USE THE DATABASE.

33789
data/static/uszips.csv

File diff suppressed because it is too large
Loading…
Cancel
Save