Browse Source
- Multi-channel support for Venue - Venue channel editor and management for Admin - Venue UI components for channel card, channel list item, and channel grid - Home page fixes and updates - Image display and responsiveness for home page down to mobile - SiteLink model, service, etc., for adding links to your Site - Many navbar enhancements and fixes - RSS, ATOM, and JSON feed generatorspull/24/head
Rob Colbert
1 year ago
27 changed files with 526 additions and 171 deletions
@ -0,0 +1,92 @@ |
|||
// feed.js
|
|||
// Copyright (C) 2022 DTP Technologies, LLC
|
|||
// License: Apache-2.0
|
|||
|
|||
'use strict'; |
|||
|
|||
const express = require('express'); |
|||
|
|||
const { SiteController } = require('../../lib/site-lib'); |
|||
|
|||
class SiteFeedController extends SiteController { |
|||
|
|||
constructor (dtp) { |
|||
super(dtp, module.exports); |
|||
} |
|||
|
|||
async start ( ) { |
|||
const router = express.Router(); |
|||
this.dtp.app.use('/feed', router); |
|||
|
|||
router.use(async (req, res, next) => { |
|||
res.locals.currentView = 'feed'; |
|||
return next(); |
|||
}); |
|||
|
|||
router.get('/rss', this.getRssFeed.bind(this)); |
|||
router.get('/atom', this.getAtomFeed.bind(this)); |
|||
router.get('/json', this.getJsonFeed.bind(this)); |
|||
|
|||
router.get('/', this.getHome.bind(this)); |
|||
|
|||
return router; |
|||
} |
|||
|
|||
async getRssFeed (req, res) { |
|||
const { feed: feedService } = this.dtp.services; |
|||
try { |
|||
const feed = await feedService.getSiteFeed(); |
|||
const rss = feed.rss2(); |
|||
res.set('Content-Type', 'application/rss+xml'); |
|||
res.set('Content-Length', rss.length); |
|||
res.send(rss); |
|||
} catch (error) { |
|||
res.status(error.statusCode || 500).json({ |
|||
success: false, |
|||
message: error.message, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
async getAtomFeed (req, res) { |
|||
const { feed: feedService } = this.dtp.services; |
|||
try { |
|||
const feed = await feedService.getSiteFeed(); |
|||
const atom = feed.atom1(); |
|||
res.set('Content-Type', 'application/rss+xml'); |
|||
res.set('Content-Length', atom.length); |
|||
res.send(atom); |
|||
} catch (error) { |
|||
res.status(error.statusCode || 500).json({ |
|||
success: false, |
|||
message: error.message, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
async getJsonFeed (req, res) { |
|||
const { feed: feedService } = this.dtp.services; |
|||
try { |
|||
const feed = await feedService.getSiteFeed(); |
|||
const json = feed.json1(); |
|||
res.set('Content-Type', 'application/json'); |
|||
res.set('Content-Length', json.length); |
|||
res.send(json); |
|||
} catch (error) { |
|||
res.status(error.statusCode || 500).json({ |
|||
success: false, |
|||
message: error.message, |
|||
}); |
|||
} |
|||
} |
|||
|
|||
async getHome (req, res) { |
|||
res.render('feed/index'); |
|||
} |
|||
} |
|||
|
|||
module.exports = { |
|||
slug: 'feed', |
|||
name: 'feed', |
|||
create: async (dtp) => { return new SiteFeedController(dtp); }, |
|||
}; |
@ -1,37 +1,147 @@ |
|||
extends ../../layouts/main |
|||
block content |
|||
h1 VenueChannel Editor |
|||
|
|||
- var formAction = channel ? `/admin/venue/channel/${channel._id}` : '/admin/venue/channel'; |
|||
|
|||
form(method="POST", action= formAction).uk-form |
|||
form(method="POST", action= formAction, onsubmit="return dtp.channelEditor.submitForm(event);").uk-form |
|||
.uk-card.uk-card-secondary.uk-card-small |
|||
.uk-card-header |
|||
h1.uk-card-title= channel ? 'Update Channel' : 'Create Channel' |
|||
|
|||
p You are linking a Shing.tv stream channel to your Venue. Venue wants the Shing channel information, but wants the local user information for the owner of the channel. You are tying a remote Shing channel to a local user of #{site.name}. |
|||
|
|||
.uk-card-body |
|||
.uk-margin |
|||
label(for="slug").uk-form-label Channel URL |
|||
input(type="url", name="url", placeholder="Paste Shing.tv channel URL", value= channel ? `https://${dtp.services.venue.soapboxDomain}/channel/${channel.slug}` : undefined).uk-input |
|||
.uk-text-small.uk-text-muted #{site.name} integrates #{dtp.services.venue.soapboxDomain} and wants the channel URL from there. |
|||
|
|||
.uk-margin |
|||
label(for="owner").uk-form-label Owner |
|||
input(type="text", name="owner", placeholder=`Enter channel owner's local username (here on ${site.name})`, value= channel ? channel.owner.username : 'rob').uk-input |
|||
.uk-text-small.uk-text-muted Enter the user's username here on #{site.name}, not from Shing.tv. |
|||
|
|||
div(uk-grid) |
|||
div(class="uk-width-1-1 uk-width-2-3@m") |
|||
label(for="stream-key").uk-form-label Stream Key |
|||
input(id="stream-key", name="credentials.streamKey", type="text", placeholder="Paste Shing.tv stream key", value= channel ? channel.credentials.streamKey : undefined).uk-input |
|||
div(uk-grid).uk-grid-small.uk-flex-middle |
|||
.uk-width-expand |
|||
input( |
|||
id="stream-key", |
|||
name="credentials.streamKey", |
|||
type="text", |
|||
placeholder="Paste Shing.tv stream key", |
|||
data-key-value= channel ? channel.credentials.streamKey : undefined, |
|||
).uk-input |
|||
.uk-width-auto |
|||
button( |
|||
type="button", |
|||
onclick="return dtp.channelEditor.toggleKeyValue(event, 'stream-key');", |
|||
).uk-button.uk-button-text |
|||
span |
|||
i.button-icon.fas.fa-lock |
|||
span.button-label.uk-margin-small-left Show |
|||
|
|||
div(class="uk-width-1-1 uk-width-1-3@m") |
|||
label(for="widget-key").uk-form-label Widget Key |
|||
input(id="widget-key", name="credentials.widgetKey", type="text", placeholder="Paste Shing.tv widget key", value= channel ? channel.credentials.widgetKey : undefined).uk-input |
|||
div(uk-grid).uk-grid-small.uk-flex-middle |
|||
.uk-width-expand |
|||
input( |
|||
id="widget-key", |
|||
name="credentials.widgetKey", |
|||
type="text", |
|||
placeholder="Paste Shing.tv widget key", |
|||
data-key-value= channel ? channel.credentials.widgetKey : undefined, |
|||
).uk-input |
|||
.uk-width-auto |
|||
button( |
|||
type="button", |
|||
onclick="return dtp.channelEditor.toggleKeyValue(event, 'widget-key');", |
|||
).uk-button.uk-button-text |
|||
span |
|||
i.button-icon.fas.fa-lock |
|||
span.button-label.uk-margin-small-left Show |
|||
|
|||
.uk-card-footer.uk-flex.uk-flex-between |
|||
.uk-width-auto |
|||
div(uk-grid).uk-flex-middle |
|||
div(class="uk-width-1-1 uk-width-1-2@s") |
|||
label(for="owner").uk-form-label Owner |
|||
.uk-form-controls |
|||
input(type="text", name="owner", placeholder=`Enter channel owner's local username (here on ${site.name})`, value= channel ? channel.owner.username : 'rob').uk-input |
|||
|
|||
div(class="uk-width-1-1 uk-width-1-2@s") |
|||
label(for="sort-order").uk-form-label Sort order |
|||
.uk-form-controls |
|||
input(id="sort-order", name="sortOrder", type="number", min=0, step=1, placeholder="Enter channel sort order", value= channel ? channel.sortOrder : 0).uk-input |
|||
|
|||
div(uk-grid).uk-card-footer |
|||
.uk-width-expand |
|||
+renderBackButton({ includeLabel: true, label: 'Cancel' }) |
|||
|
|||
if channel |
|||
.uk-width-auto |
|||
button( |
|||
type="button", |
|||
data-channel={ _id: channel._id, name: channel.name }, |
|||
onclick="return dtp.adminApp.deleteVenueChannel(event);", |
|||
).uk-button.dtp-button-danger.uk-border-rounded |
|||
span |
|||
i.fas.fa-trash |
|||
span.uk-margin-small-left DELETE |
|||
|
|||
.uk-width-auto |
|||
button(type="submit").uk-button.dtp-button-primary.uk-border-rounded= channel ? 'Update Channel' : 'Create Channel' |
|||
button(type="submit").uk-button.dtp-button-primary.uk-border-rounded |
|||
span |
|||
i.fas.fa-save |
|||
span.uk-margin-small-left= channel ? 'Update' : 'Create' |
|||
|
|||
block viewjs |
|||
script. |
|||
(async ( ) => { |
|||
window.dtp = window.dtp || { }; |
|||
window.dtp.channelEditor = { }; |
|||
|
|||
window.dtp.channelEditor.toggleKeyValue = async (event, fieldId) => { |
|||
event.preventDefault(); |
|||
event.stopPropagation(); |
|||
|
|||
const button = event.currentTarget || event.target; |
|||
const target = document.getElementById(fieldId); |
|||
target.toggleAttribute('data-masked'); |
|||
|
|||
if (target.hasAttribute('data-masked')) { |
|||
target.value = target.getAttribute('data-masked-value'); |
|||
target.toggleAttribute('data-masked', true); |
|||
target.toggleAttribute('disabled', true); |
|||
button.querySelector('.button-label').textContent = 'SHOW'; |
|||
const icon = button.querySelector('.button-icon'); |
|||
icon.classList.remove('fa-lock-open'); |
|||
icon.classList.add('fa-lock'); |
|||
} else { |
|||
target.value = target.getAttribute('data-key-value'); |
|||
target.toggleAttribute('disabled', false); |
|||
button.querySelector('.button-label').textContent = 'HIDE'; |
|||
const icon = button.querySelector('.button-icon'); |
|||
icon.classList.remove('fa-lock'); |
|||
icon.classList.add('fa-lock-open'); |
|||
} |
|||
}; |
|||
|
|||
window.dtp.channelEditor.submitForm = async (event) => { |
|||
// simply restore the values back to their key values, and allow the |
|||
// form to submit per normal processing. |
|||
window.dtp.channelEditor.keyInputs.forEach((input) => { |
|||
if (input.hasAttribute('data-masked')) { |
|||
input.value = input.getAttribute('data-key-value'); |
|||
input.toggleAttribute('data-masked', false); |
|||
input.toggleAttribute('disabled', false); |
|||
} |
|||
input.style.visibility = 'hidden'; // since we are "showing" the keys |
|||
}); |
|||
}; |
|||
|
|||
window.dtp.channelEditor.keyInputs = document.querySelectorAll("input[data-key-value]"); |
|||
window.dtp.channelEditor.keyInputs.forEach((input) => { |
|||
const keyValue = input.getAttribute('data-key-value'); |
|||
const maskedValue = keyValue.split('').map((char) => '*').join(''); |
|||
input.setAttribute('data-masked-value', maskedValue); |
|||
input.toggleAttribute('data-masked', true); |
|||
input.toggleAttribute('disabled', true); |
|||
input.value = maskedValue; |
|||
}); |
|||
|
|||
})(); |
@ -1,5 +1,10 @@ |
|||
mixin renderButtonIcon (buttonClass, buttonLabel) |
|||
span |
|||
i(class=`fas ${buttonClass}`) |
|||
if buttonLabel |
|||
span(class="uk-visible@m").uk-margin-small-left= buttonLabel |
|||
|
|||
mixin renderButtonImage (imageUrl, buttonLabel) |
|||
img(src= imageUrl, alt= `Image icon for button ${buttonLabel || 'unnamed'}`) |
|||
if buttonLabel |
|||
span(class="uk-visible@m").uk-margin-small-left= buttonLabel |
@ -0,0 +1,11 @@ |
|||
extends ../layouts/main-sidebar |
|||
block content |
|||
|
|||
h1 #{site.name} Feeds |
|||
ul.uk-list |
|||
li |
|||
a(href='/feed/rss') RSS |
|||
li |
|||
a(href='/feed/atom') ATOM |
|||
li |
|||
a(href='/feed/json') JSON |
@ -1,21 +1,21 @@ |
|||
mixin renderVenueChannelCard (channel) |
|||
.uk-card.uk-card-default.uk-card-small.uk-card-hover.uk-margin |
|||
|
|||
if channel.lastStatus && channel.lastStatus.liveThumbnail |
|||
if channel.currentStatus && channel.currentStatus.liveThumbnail |
|||
.uk-card-media-top |
|||
a(href=`/venue/${channel.slug}`) |
|||
img( |
|||
src= channel.lastStatus.liveThumbnail.url, |
|||
onerror=`this.src = '${channel.lastStatus.thumbnailUrl}';`, |
|||
src= channel.currentStatus.liveThumbnail.url, |
|||
onerror=`this.src = '${channel.currentStatus.thumbnailUrl}';`, |
|||
title="Tune in now", |
|||
) |
|||
|
|||
if channel.lastStatus && channel.lastStatus.liveEpisode && channel.lastStatus.liveEpisode.title |
|||
if channel.currentStatus && channel.currentStatus.liveEpisode && channel.currentStatus.liveEpisode.title |
|||
.uk-card-body |
|||
.uk-text-bold.uk-text-truncate |
|||
a(href="/venue", uk-tooltip= `Watch "${channel.lastStatus.liveEpisode.title}" now!`)= channel.lastStatus.liveEpisode.title |
|||
a(href="/venue", uk-tooltip= `Watch "${channel.currentStatus.liveEpisode.title}" now!`)= channel.currentStatus.liveEpisode.title |
|||
.uk-text-small |
|||
div(uk-grid).uk-grid-small.uk-flex-between |
|||
.uk-width-auto |
|||
div Started: #{moment(channel.lastStatus.liveEpisode.created).fromNow()} |
|||
.uk-width-auto #[i.fas.fa-eye] #{formatCount(channel.lastStatus.liveEpisode.stats.currentViewerCount)} |
|||
div Started: #{moment(channel.currentStatus.liveEpisode.created).fromNow()} |
|||
.uk-width-auto #[i.fas.fa-eye] #{formatCount(channel.currentStatus.liveEpisode.stats.currentViewerCount)} |
@ -0,0 +1,30 @@ |
|||
include ./channel-card |
|||
include ./channel-list-item |
|||
|
|||
mixin renderVenueChannelGrid (channels) |
|||
- |
|||
var onlineChannels = channels.filter((channel) => channel.currentStatus && (channel.currentStatus.status === 'live')) |
|||
var offlineChannels = channels.filter((channel) => !channel.currentStatus || (channel.currentStatus.status !== 'live')) |
|||
|
|||
div(uk-grid) |
|||
div(class="uk-width-1-1 uk-width-2-3@s") |
|||
+renderSectionTitle('Live Channels') |
|||
.uk-margin-small |
|||
if Array.isArray(onlineChannels) && (onlineChannels.length > 0) |
|||
div(uk-grid).uk-grid-small |
|||
each channel of onlineChannels |
|||
div(class="uk-width-1-1 uk-width-1-2@s uk-width-1-3@l") |
|||
+renderVenueChannelCard(channel) |
|||
else |
|||
div There are no live channels. Please check back later! |
|||
|
|||
div(class="uk-width-1-1 uk-width-1-3@s") |
|||
+renderSectionTitle('Offline Channels') |
|||
.uk-margin-small |
|||
if Array.isArray(offlineChannels) && (offlineChannels.length > 0) |
|||
ul.uk-list.uk-list-divider |
|||
each channel of offlineChannels |
|||
li |
|||
+renderVenueChannelListItem(channel) |
|||
else |
|||
div There are no offline channels. |
Loading…
Reference in new issue