From 27f8f3a8c565f71dc1023eee6fec6e0eb7e55e93 Mon Sep 17 00:00:00 2001 From: Rob Colbert Date: Mon, 3 Feb 2025 10:52:47 -0500 Subject: [PATCH] the start of the broadcast show service, controller, and route --- src/app/controllers/broadcast-show.ts | 108 ++++++++++++++++++++++++ src/app/controllers/lib/populators.ts | 28 ++++++ src/app/services/broadcast-show.ts | 30 +++++++ src/app/views/broadcast-show/editor.pug | 34 ++++++++ src/app/views/broadcast-show/home.pug | 13 +++ 5 files changed, 213 insertions(+) create mode 100644 src/app/controllers/broadcast-show.ts create mode 100644 src/app/views/broadcast-show/editor.pug create mode 100644 src/app/views/broadcast-show/home.pug diff --git a/src/app/controllers/broadcast-show.ts b/src/app/controllers/broadcast-show.ts new file mode 100644 index 0000000..e396d59 --- /dev/null +++ b/src/app/controllers/broadcast-show.ts @@ -0,0 +1,108 @@ +// app/controllers/broadcast-show.ts +// Copyright (C) 2025 DTP Technologies, LLC +// All Rights Reserved + +import { NextFunction, Request, Response } from "express"; + +import { WebController, WebServer } from "../../lib/dtplib.js"; + +import BroadcastShowService from "../services/broadcast-show.js"; +import { populateBroadcastShow } from "./lib/populators.js"; + +export default class BroadcastShowController extends WebController { + static get name(): string { + return "BroadcastShowController"; + } + static get slug(): string { + return "broadcastShow"; + } + + constructor(server: WebServer) { + super(server, BroadcastShowController); + } + + get route(): string { + return "/show"; + } + + async start(): Promise { + this.router.param("showId", populateBroadcastShow(this)); + + this.router.post("/:showId", this.postUpdate.bind(this)); + this.router.post("/", this.postCreate.bind(this)); + + this.router.get("/create", this.getEditor.bind(this)); + + this.router.get("/:showId/edit", this.getEditor.bind(this)); + this.router.get("/:showId", this.getShowView.bind(this)); + + this.router.get("/", this.getHome.bind(this)); + } + + async postCreate( + req: Request, + res: Response, + next: NextFunction + ): Promise { + const showService = this.getService("broadcastShow"); + try { + res.locals.show = await showService.create(req.body); + res.redirect(`/show/${res.locals.show._id}/edit`); + } catch (error) { + this.log.error("failed to process show create request", { error }); + return next(error); + } + } + + async postUpdate( + req: Request, + res: Response, + next: NextFunction + ): Promise { + const showService = this.getService("broadcastShow"); + try { + res.locals.show = await showService.update(res.locals.show, req.body); + res.redirect(`/show/${res.locals.show._id}/edit`); + } catch (error) { + this.log.error("failed to process show update request", { error }); + return next(error); + } + } + + async getEditor(_req: Request, res: Response): Promise { + res.render("broadcast-show/editor"); + } + + async getShowView( + _req: Request, + res: Response, + next: NextFunction + ): Promise { + const showService = this.getService("broadcastShow"); + try { + res.locals.recommendedShows = await showService.getRecommended( + 3, + res.locals.show + ); + res.render("broadcast-show/view"); + } catch (error) { + this.log.error("failed to present the Show Home view", { error }); + return next(error); + } + } + async getHome( + req: Request, + res: Response, + next: NextFunction + ): Promise { + const showService = this.getService("broadcastShow"); + try { + res.locals.pagination = this.getPaginationParameters(req, 12); + res.locals.showLibrary = await showService.getLive(res.locals.pagination); + res.render("broadcast-show/home"); + } catch (error) { + this.log.error("failed to present the Show Home view", { error }); + return next(error); + } + } +} diff --git a/src/app/controllers/lib/populators.ts b/src/app/controllers/lib/populators.ts index d307f61..7ca9012 100644 --- a/src/app/controllers/lib/populators.ts +++ b/src/app/controllers/lib/populators.ts @@ -11,6 +11,7 @@ import { WebController, WebError } from "../../../lib/dtplib.js"; import { ImageService } from "../../services/image.js"; import { UserService } from "../../services/user.js"; +import { BroadcastShowService } from "app/services/broadcast-show.js"; export function populateAccountByEmail( controller: WebController @@ -66,6 +67,33 @@ export function populateAccountById(controller: WebController): RequestHandler { }; } +export function populateBroadcastShow( + controller: WebController +): RequestHandler { + const showService = + controller.getService("broadcastShow"); + return async function ( + _req: Request, + res: Response, + next: NextFunction, + showId?: string + ): Promise { + assert(showId, "Show ID is required"); + try { + res.locals.show = await showService.getById( + Types.ObjectId.createFromHexString(showId) + ); + if (!res.locals.show) { + throw new WebError(404, "Broadcast show not found"); + } + return next(); + } catch (error) { + controller.log.error("failed to populate show ID", { showId, error }); + return next(error); + } + }; +} + export function populateImageId(controller: WebController): RequestHandler { const imageService = controller.platform.components.getService("image"); diff --git a/src/app/services/broadcast-show.ts b/src/app/services/broadcast-show.ts index a1626de..b13cf5a 100644 --- a/src/app/services/broadcast-show.ts +++ b/src/app/services/broadcast-show.ts @@ -119,6 +119,36 @@ export class BroadcastShowService extends DtpService { const totalShowCount = await BroadcastShow.estimatedDocumentCount(); return { shows, totalShowCount }; } + + /** + * For now just fetches a sample of the live list minus any shows specified + * for exclusion (such as the one(s) on the page right now). + * @param maxCount number The maximum number of shows to return. + * @param exclude Array Array of shows to exclude from the result set. + * @returns An array of recommended shows for display. + */ + async getRecommended( + maxCount: number, + exclude?: Array + ): Promise> { + const pipeline: Array = []; + + if (exclude) { + if (!Array.isArray(exclude)) { + exclude = [exclude]; + } + pipeline.push({ + $match: { $nin: exclude.map((show) => show._id) }, + }); + } + + pipeline.push({ + $sample: { size: maxCount }, + }); + + const shows = await BroadcastShow.aggregate(pipeline); + return await BroadcastShow.populate(shows, this.populateBroadcastShow); + } } export default BroadcastShowService; diff --git a/src/app/views/broadcast-show/editor.pug b/src/app/views/broadcast-show/editor.pug new file mode 100644 index 0000000..e88b6e0 --- /dev/null +++ b/src/app/views/broadcast-show/editor.pug @@ -0,0 +1,34 @@ +extends ../layouts/main-sidebar +block content + + - var actionUrl = show ? `/show/${show._id}` : "/show"; + + form(method="POST", action= actionUrl).uk-form + .uk-card.uk-card-default.uk-card-small + .uk-card-header + h1.uk-card-title #{show ? "Edit" : "Create New"} Broadcast Show + .uk-card-body + .uk-margin + label(for="title").uk-form-label Title + input(id="title", name="title", type="text", placeholder="Enter show title", required).uk-input + .uk-margin + label(for="description").uk-form-label Description + textarea( + id="description", + name="description", + rows="4", + placeholder="Enter show description", + required, + ).uk-textarea.uk-resize-vertical + .uk-card-footer + div(uk-grid).uk-grid-small + .uk-width-expand + +renderBackButton() + .uk-width-auto + button(type="submit").uk-button.uk-button-primary.uk-border-rounded + span + if show + i.fa-solid.fa-save + else + i.fa-solid.fa-plus + span.uk-margin-small-left #{show ? "Update" : "Create"} Show diff --git a/src/app/views/broadcast-show/home.pug b/src/app/views/broadcast-show/home.pug new file mode 100644 index 0000000..a7f35b3 --- /dev/null +++ b/src/app/views/broadcast-show/home.pug @@ -0,0 +1,13 @@ +extends ../layouts/main-sidebar +block content + + div(uk-grid).uk-grid-small.uk-flex-middle + .uk-width-expand + h1 Show Directory + .uk-width-auto + a(href="/show/create").uk-button.uk-button-default.uk-button-small.uk-border-rounded + span + i.fa-solid.fa-plus + span.uk-margin-small-left Create Show + + pre= JSON.stringify(showLibrary, null, 2) \ No newline at end of file