diff options
-rwxr-xr-x | scripts/docker-test.sh | 2 | ||||
-rwxr-xr-x | src/app.ts | 5 | ||||
-rwxr-xr-x | src/routes.js | 294 | ||||
-rw-r--r-- | src/routes/frontend.ts | 220 | ||||
-rwxr-xr-x | views/optionsform.handlebars | 2 | ||||
-rwxr-xr-x | views/partials/neweventform.handlebars | 12 | ||||
-rwxr-xr-x | views/partials/sidebar.handlebars | 2 |
7 files changed, 229 insertions, 308 deletions
diff --git a/scripts/docker-test.sh b/scripts/docker-test.sh index 1cba254..f071740 100755 --- a/scripts/docker-test.sh +++ b/scripts/docker-test.sh @@ -15,6 +15,6 @@ trap cleanup 0 docker-compose up --build & while [[ "$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/)" -ne "200" ]]; do sleep 5; done -curl -v http://localhost:3000/new/event/public +curl -v http://localhost:3000/new cleanup
\ No newline at end of file @@ -1,7 +1,9 @@ import express from "express"; -import routes from "./routes.js"; import hbs from "express-handlebars"; +import routes from "./routes.js"; +import frontend from "./routes/frontend.js"; + const app = express(); // View engine // @@ -39,6 +41,7 @@ app.use(express.json({ type: "application/activity+json" })); // support json en app.use(express.urlencoded({ extended: true })); // Router // +app.use("/", frontend); app.use("/", routes); export default app; diff --git a/src/routes.js b/src/routes.js index 55436c9..be0dcde 100755 --- a/src/routes.js +++ b/src/routes.js @@ -51,47 +51,6 @@ const nanoid = customAlphabet( const router = express.Router(); -// Extra marked renderer (used to render plaintext event description for page metadata) -// Adapted from https://dustinpfister.github.io/2017/11/19/nodejs-marked/ -// ? to ? helper -function htmlEscapeToText(text) { - return text.replace(/\&\#[0-9]*;|&/g, function (escapeCode) { - if (escapeCode.match(/amp/)) { - return "&"; - } - return String.fromCharCode(escapeCode.match(/[0-9]+/)); - }); -} - -function render_plain() { - var render = new marked.Renderer(); - // render just the text of a link, strong, em - render.link = function (href, title, text) { - return text; - }; - render.strong = function (text) { - return text; - }; - render.em = function (text) { - return text; - }; - // render just the text of a paragraph - render.paragraph = function (text) { - return htmlEscapeToText(text) + "\r\n"; - }; - // render nothing for headings, images, and br - render.heading = function (text, level) { - return ""; - }; - render.image = function (href, title, text) { - return ""; - }; - render.br = function () { - return ""; - }; - return render; -} - let sendEmails = false; let nodemailerTransporter; if (config.general.mail_service) { @@ -224,46 +183,6 @@ schedule.scheduleJob("59 23 * * *", function (fireDate) { // old (they're not going to become active) }); -// FRONTEND ROUTES - -router.get("/", (req, res) => { - res.render("home", { - domain, - email: contactEmail, - siteName, - showKofi, - }); -}); - -router.get("/new", (req, res) => { - res.render("home"); -}); - -router.get("/new/event", (req, res) => { - res.render("newevent", { - domain: domain, - email: contactEmail, - siteName: siteName, - }); -}); -router.get("/new/event/public", (req, res) => { - let isPrivate = false; - let isPublic = true; - let isOrganisation = false; - let isUnknownType = false; - res.render("newevent", { - title: "New event", - isPrivate: isPrivate, - isPublic: isPublic, - isOrganisation: isOrganisation, - isUnknownType: isUnknownType, - eventType: "public", - domain: domain, - email: contactEmail, - siteName: siteName, - }); -}); - // return the JSON for the featured/pinned post for this event router.get("/:eventID/featured", (req, res) => { if (!isFederated) return res.sendStatus(404); @@ -390,216 +309,6 @@ router.get("/.well-known/webfinger", (req, res) => { } }); -router.get("/:eventID", (req, res) => { - Event.findOne({ - id: req.params.eventID, - }) - .lean() // Required, see: https://stackoverflow.com/questions/59690923/handlebars-access-has-been-denied-to-resolve-the-property-from-because-it-is - .populate("eventGroup") - .then((event) => { - if (event) { - const parsedLocation = event.location.replace(/\s+/g, "+"); - let displayDate; - if (moment.tz(event.end, event.timezone).isSame(event.start, "day")) { - // Happening during one day - displayDate = - moment - .tz(event.start, event.timezone) - .format( - 'dddd D MMMM YYYY [<span class="text-muted">from</span>] h:mm a' - ) + - moment - .tz(event.end, event.timezone) - .format( - ' [<span class="text-muted">to</span>] h:mm a [<span class="text-muted">](z)[</span>]' - ); - } else { - displayDate = - moment - .tz(event.start, event.timezone) - .format( - 'dddd D MMMM YYYY [<span class="text-muted">at</span>] h:mm a' - ) + - moment - .tz(event.end, event.timezone) - .format( - ' [<span class="text-muted">–</span>] dddd D MMMM YYYY [<span class="text-muted">at</span>] h:mm a [<span class="text-muted">](z)[</span>]' - ); - } - let eventStartISO = moment.tz(event.start, "Etc/UTC").toISOString(); - let eventEndISO = moment.tz(event.end, "Etc/UTC").toISOString(); - let parsedStart = moment - .tz(event.start, event.timezone) - .format("YYYYMMDD[T]HHmmss"); - let parsedEnd = moment - .tz(event.end, event.timezone) - .format("YYYYMMDD[T]HHmmss"); - let eventHasConcluded = false; - if ( - moment - .tz(event.end, event.timezone) - .isBefore(moment.tz(event.timezone)) - ) { - eventHasConcluded = true; - } - let eventHasBegun = false; - if ( - moment - .tz(event.start, event.timezone) - .isBefore(moment.tz(event.timezone)) - ) { - eventHasBegun = true; - } - let fromNow = moment.tz(event.start, event.timezone).fromNow(); - let parsedDescription = marked.parse(event.description); - let eventEditToken = event.editToken; - - let escapedName = event.name.replace(/\s+/g, "+"); - - let eventHasCoverImage = false; - if (event.image) { - eventHasCoverImage = true; - } else { - eventHasCoverImage = false; - } - let eventHasHost = false; - if (event.hostName) { - eventHasHost = true; - } else { - eventHasHost = false; - } - let firstLoad = false; - if (event.firstLoad === true) { - firstLoad = true; - Event.findOneAndUpdate( - { id: req.params.eventID }, - { firstLoad: false }, - function (err, raw) { - if (err) { - res.send(err); - } - } - ); - } - let editingEnabled = false; - if (Object.keys(req.query).length !== 0) { - if (!req.query.e) { - editingEnabled = false; - console.log("No edit token set"); - } else { - if (req.query.e === eventEditToken) { - editingEnabled = true; - } else { - editingEnabled = false; - } - } - } - let eventAttendees = event.attendees - .sort((a, b) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0)) - .map((el) => { - if (!el.id) { - el.id = el._id; - } - if (el.number > 1) { - el.name = `${el.name} (${el.number} people)`; - } - return el; - }) - .filter((obj, pos, arr) => { - return ( - obj.status === "attending" && - arr.map((mapObj) => mapObj.id).indexOf(obj.id) === pos - ); - }); - - let spotsRemaining, noMoreSpots; - let numberOfAttendees = eventAttendees.reduce((acc, attendee) => { - if (attendee.status === "attending") { - return acc + attendee.number || 1; - } - return acc; - }, 0); - if (event.maxAttendees) { - spotsRemaining = event.maxAttendees - numberOfAttendees; - if (spotsRemaining <= 0) { - noMoreSpots = true; - } - } - let metadata = { - title: event.name, - description: marked - .parse(event.description, { renderer: render_plain() }) - .split(" ") - .splice(0, 40) - .join(" ") - .trim(), - image: eventHasCoverImage - ? `https://${domain}/events/` + event.image - : null, - url: `https://${domain}/` + req.params.eventID, - }; - if ( - req.headers.accept && - (req.headers.accept.includes("application/activity+json") || - req.headers.accept.includes("application/json") || - req.headers.accept.includes("application/json+ld")) - ) { - res - .header("Content-Type", "application/activity+json") - .send(JSON.parse(event.activityPubActor)); - } else { - res.set("X-Robots-Tag", "noindex"); - res.render("event", { - domain: domain, - isFederated: isFederated, - email: contactEmail, - title: event.name, - escapedName: escapedName, - eventData: event, - eventAttendees: eventAttendees, - numberOfAttendees, - spotsRemaining: spotsRemaining, - noMoreSpots: noMoreSpots, - eventStartISO: eventStartISO, - eventEndISO: eventEndISO, - parsedLocation: parsedLocation, - parsedStart: parsedStart, - parsedEnd: parsedEnd, - displayDate: displayDate, - fromNow: fromNow, - timezone: event.timezone, - parsedDescription: parsedDescription, - editingEnabled: editingEnabled, - eventHasCoverImage: eventHasCoverImage, - eventHasHost: eventHasHost, - firstLoad: firstLoad, - eventHasConcluded: eventHasConcluded, - eventHasBegun: eventHasBegun, - metadata: metadata, - siteName: siteName, - }); - } - } else { - res.status(404); - res.render("404", { url: req.url }); - } - }) - .catch((err) => { - addToLog( - "displayEvent", - "error", - "Attempt to display event " + - req.params.eventID + - " failed with error: " + - err - ); - console.log(err); - res.status(404); - res.render("404", { url: req.url }); - return; - }); -}); - router.get("/:eventID/followers", (req, res) => { if (!isFederated) return res.sendStatus(404); const eventID = req.params.eventID; @@ -917,7 +626,7 @@ router.post("/newevent", async (req, res) => { const event = new Event({ id: eventID, - type: req.body.eventType, + type: "public", // This is for backwards compatibility name: req.body.eventName, location: req.body.eventLocation, start: startUTC, @@ -1082,6 +791,7 @@ router.post("/newevent", async (req, res) => { res.end(); }) .catch((err) => { + console.error(err); res.status(500).send("Database error, please try again :( - " + err); addToLog( "createEvent", diff --git a/src/routes/frontend.ts b/src/routes/frontend.ts new file mode 100644 index 0000000..9dea619 --- /dev/null +++ b/src/routes/frontend.ts @@ -0,0 +1,220 @@ +import { Router, Request, Response } from "express"; +import Event from "../models/Event.js"; +import moment from "moment-timezone"; +import { marked } from "marked"; +import { frontendConfig } from "../util/config.js"; +import { renderPlain } from "../util/markdown.js"; +import getConfig from "../lib/config.js"; +import { addToLog } from "../helpers.js"; + +const config = getConfig(); + +const router = Router(); +router.get("/", (_: Request, res: Response) => { + res.render("home", frontendConfig()); +}); + +router.get("/new", (_: Request, res: Response) => { + res.render("newevent", { + title: "New event", + ...frontendConfig(), + }); +}); + +router.get("/:eventID", async (req: Request, res: Response) => { + try { + const event = await Event.findOne({ + id: req.params.eventID, + }) + .lean() // Required, see: https://stackoverflow.com/questions/59690923/handlebars-access-has-been-denied-to-resolve-the-property-from-because-it-is + .populate("eventGroup"); + if (!event) { + res.status(404); + res.render("404", { url: req.url }); + return; + } + const parsedLocation = event.location.replace(/\s+/g, "+"); + let displayDate; + if (moment.tz(event.end, event.timezone).isSame(event.start, "day")) { + // Happening during one day + displayDate = + moment + .tz(event.start, event.timezone) + .format( + 'dddd D MMMM YYYY [<span class="text-muted">from</span>] h:mm a' + ) + + moment + .tz(event.end, event.timezone) + .format( + ' [<span class="text-muted">to</span>] h:mm a [<span class="text-muted">](z)[</span>]' + ); + } else { + displayDate = + moment + .tz(event.start, event.timezone) + .format( + 'dddd D MMMM YYYY [<span class="text-muted">at</span>] h:mm a' + ) + + moment + .tz(event.end, event.timezone) + .format( + ' [<span class="text-muted">–</span>] dddd D MMMM YYYY [<span class="text-muted">at</span>] h:mm a [<span class="text-muted">](z)[</span>]' + ); + } + let eventStartISO = moment.tz(event.start, "Etc/UTC").toISOString(); + let eventEndISO = moment.tz(event.end, "Etc/UTC").toISOString(); + let parsedStart = moment + .tz(event.start, event.timezone) + .format("YYYYMMDD[T]HHmmss"); + let parsedEnd = moment + .tz(event.end, event.timezone) + .format("YYYYMMDD[T]HHmmss"); + let eventHasConcluded = false; + if ( + moment.tz(event.end, event.timezone).isBefore(moment.tz(event.timezone)) + ) { + eventHasConcluded = true; + } + let eventHasBegun = false; + if ( + moment.tz(event.start, event.timezone).isBefore(moment.tz(event.timezone)) + ) { + eventHasBegun = true; + } + let fromNow = moment.tz(event.start, event.timezone).fromNow(); + let parsedDescription = marked.parse(event.description); + let eventEditToken = event.editToken; + + let escapedName = event.name.replace(/\s+/g, "+"); + + let eventHasCoverImage = false; + if (event.image) { + eventHasCoverImage = true; + } else { + eventHasCoverImage = false; + } + let eventHasHost = false; + if (event.hostName) { + eventHasHost = true; + } else { + eventHasHost = false; + } + let firstLoad = false; + if (event.firstLoad === true) { + firstLoad = true; + await Event.findOneAndUpdate( + { id: req.params.eventID }, + { firstLoad: false } + ); + } + let editingEnabled = false; + if (Object.keys(req.query).length !== 0) { + if (!req.query.e) { + editingEnabled = false; + console.log("No edit token set"); + } else { + if (req.query.e === eventEditToken) { + editingEnabled = true; + } else { + editingEnabled = false; + } + } + } + let eventAttendees = event.attendees + ?.sort((a, b) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0)) + .map((el) => { + if (!el.id) { + el.id = el._id; + } + if (el.number && el.number > 1) { + el.name = `${el.name} (${el.number} people)`; + } + return el; + }) + .filter((obj, pos, arr) => { + return ( + obj.status === "attending" && + arr.map((mapObj) => mapObj.id).indexOf(obj.id) === pos + ); + }); + + let spotsRemaining, noMoreSpots; + let numberOfAttendees = + eventAttendees?.reduce((acc, attendee) => { + if (attendee.status === "attending") { + return acc + (attendee.number || 1); + } + return acc; + }, 0) || 0; + if (event.maxAttendees) { + spotsRemaining = event.maxAttendees - numberOfAttendees; + if (spotsRemaining <= 0) { + noMoreSpots = true; + } + } + let metadata = { + title: event.name, + description: marked + .parse(event.description, { renderer: renderPlain() }) + .split(" ") + .splice(0, 40) + .join(" ") + .trim(), + image: eventHasCoverImage + ? `https://${config.general.domain}/events/` + event.image + : null, + url: `https://${config.general.domain}/` + req.params.eventID, + }; + if ( + req.headers.accept && + (req.headers.accept.includes("application/activity+json") || + req.headers.accept.includes("application/json") || + req.headers.accept.includes("application/json+ld")) + ) { + res + .header("Content-Type", "application/activity+json") + .send(JSON.parse(event.activityPubActor || "{}")); + } else { + res.set("X-Robots-Tag", "noindex"); + res.render("event", { + ...frontendConfig(), + title: event.name, + escapedName: escapedName, + eventData: event, + eventAttendees: eventAttendees, + numberOfAttendees, + spotsRemaining: spotsRemaining, + noMoreSpots: noMoreSpots, + eventStartISO: eventStartISO, + eventEndISO: eventEndISO, + parsedLocation: parsedLocation, + parsedStart: parsedStart, + parsedEnd: parsedEnd, + displayDate: displayDate, + fromNow: fromNow, + timezone: event.timezone, + parsedDescription: parsedDescription, + editingEnabled: editingEnabled, + eventHasCoverImage: eventHasCoverImage, + eventHasHost: eventHasHost, + firstLoad: firstLoad, + eventHasConcluded: eventHasConcluded, + eventHasBegun: eventHasBegun, + metadata: metadata, + }); + } + } catch (err) { + addToLog( + "displayEvent", + "error", + "Attempt to display event " + + req.params.eventID + + " failed with error: " + + err + ); + console.log(err); + res.status(404).render("404", { url: req.url }); + } +}); + +export default router; diff --git a/views/optionsform.handlebars b/views/optionsform.handlebars index a844d12..85ebd9f 100755 --- a/views/optionsform.handlebars +++ b/views/optionsform.handlebars @@ -10,7 +10,7 @@ <div class="form-check"> <input class="form-check-input" type="checkbox" id="guestlistCheckbox" name="guestlistCheckbox" {{#if data.guestlistCheckbox}}checked{{/if}}> <label class="form-check-label" for="guestlistCheckbox"> - {{#if isPrivate}}Privately display{{else}}Publicly display{{/if}} the list of attendees + Display the list of attendees </label> </div> <div class="form-check"> diff --git a/views/partials/neweventform.handlebars b/views/partials/neweventform.handlebars index d456d2e..3c7e060 100755 --- a/views/partials/neweventform.handlebars +++ b/views/partials/neweventform.handlebars @@ -1,6 +1,5 @@ <h4 class="mb-2">Create an event</h4> <form id="newEventForm" action="/newevent" method="post" enctype="multipart/form-data"> - <input type="text" hidden class="form-control" id="eventType" name="eventType" value="{{eventType}}"> <div class="form-group row"> <label for="eventName" class="col-sm-2 col-form-label">Event name</label> <div class="form-group col-sm-10"> @@ -54,17 +53,6 @@ <small class="form-text">Recommended dimensions (w x h): 920px by 300px.</small> </div> </div> - {{#unless isPublic}} - <div class="form-group row"> - <label for="eventPassword" class="col-sm-2 col-form-label">Event password</label> - <div class="form-group col-sm-10"> - <input type="password" class="form-control" id="eventPassword" name="eventPassword" placeholder="Don't forget it!" data-validation="required"> - </div> - <div class="form-group col-sm-10 offset-sm-2"> - <div class="" id="passwordStrengthBar"></div> - </div> - </div> - {{/unless}} <div class="form-group row"> <label for="hostName" class="col-sm-2 col-form-label">Host name</label> <div class="form-group col-sm-10"> diff --git a/views/partials/sidebar.handlebars b/views/partials/sidebar.handlebars index 1aa0f74..980e699 100755 --- a/views/partials/sidebar.handlebars +++ b/views/partials/sidebar.handlebars @@ -3,5 +3,5 @@ <p class="lead text-center mb-4">Nicer events</p> - <a class="btn btn-success mb-2 btn-block" href="/new/event/public"><i class="far fa-calendar-plus"></i> New event</a> + <a class="btn btn-success mb-2 btn-block" href="/new"><i class="far fa-calendar-plus"></i> New event</a> </div> |