From 093a6816bc7bc85c8691d1817305b0e998b1494c Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Thu, 27 Feb 2025 18:39:04 +0100 Subject: [PATCH] Implement editing of the schedule Cobbled together minimum viable system for editing the schedule in a way that propogates updates to connected clients. --- app/api/events/actions.ts | 70 +++++++++++++++++++++++++++++ app/api/events/route.ts | 46 ++----------------- app/api/events/streams.ts | 50 +++++++++++++++++++++ app/globals.css | 12 +++++ app/schedule/context.tsx | 12 +++-- app/schedule/page.tsx | 2 + ui/events-edit.tsx | 93 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 236 insertions(+), 49 deletions(-) create mode 100644 app/api/events/actions.ts create mode 100644 app/api/events/streams.ts create mode 100644 ui/events-edit.tsx diff --git a/app/api/events/actions.ts b/app/api/events/actions.ts new file mode 100644 index 0000000..e95ba8a --- /dev/null +++ b/app/api/events/actions.ts @@ -0,0 +1,70 @@ +"use server"; +import { Schedule } from "@/app/schedule/types"; +import { readFile, writeFile } from "fs/promises"; +import { broadcastUpdate } from "./streams"; + +export async function createEvent(formData: FormData) { + const schedule: Schedule = JSON.parse(await readFile("schedule.json", "utf-8")); + const id = formData.get("id") as string; + const name = formData.get("name") as string; + const description = formData.get("description") as string; + const start = formData.get("start") as string; + const end = formData.get("end") as string; + const location = formData.get("location") as string; + schedule.events.push({ + name, + id, + description, + slots: [ + { + id: `${id}-1`, + start, + end, + locations: [location], + } + ] + }); + broadcastUpdate(schedule); + await writeFile("schedule.json", JSON.stringify(schedule, null, "\t"), "utf-8"); +} + +export async function modifyEvent(formData: FormData) { + const schedule: Schedule = JSON.parse(await readFile("schedule.json", "utf-8")); + const id = formData.get("id") as string; + const name = formData.get("name") as string; + const description = formData.get("description") as string; + const start = formData.get("start") as string; + const end = formData.get("end") as string; + const location = formData.get("location") as string; + const index = schedule.events.findIndex(event => event.id === id); + if (index === -1) { + throw Error("No such event"); + } + schedule.events[index] = { + name, + id, + description, + slots: [ + { + id: `${id}-1`, + start, + end, + locations: [location], + } + ] + }; + broadcastUpdate(schedule); + await writeFile("schedule.json", JSON.stringify(schedule, null, "\t"), "utf-8"); +} + +export async function deleteEvent(formData: FormData) { + const schedule: Schedule = JSON.parse(await readFile("schedule.json", "utf-8")); + const id = formData.get("id") as string; + const index = schedule.events.findIndex(event => event.id === id); + if (index === -1) { + throw Error("No such event"); + } + schedule.events.splice(index, 1); + broadcastUpdate(schedule); + await writeFile("schedule.json", JSON.stringify(schedule, null, "\t"), "utf-8"); +} diff --git a/app/api/events/route.ts b/app/api/events/route.ts index fed8ce0..73f1892 100644 --- a/app/api/events/route.ts +++ b/app/api/events/route.ts @@ -1,36 +1,4 @@ -import { Schedule } from "@/app/schedule/types"; - -function sendMessage( - stream: WritableStream, - message: string, -) { - const writer = stream.getWriter(); - writer.ready - .then(() => writer.write(message)) - .catch(console.error) - .finally(() => writer.releaseLock()) - ; -} - -let streams = new Set>(); -let lastBroadcastData: string | null = null; -let lastBroadcastId = 0; -export function broadcastUpdate(schedule: Schedule) { - const id = Date.now(); - const data = JSON.stringify(schedule); - lastBroadcastId = id; - lastBroadcastData = data; - const message = `id: ${id}\nevent: update\ndata: ${data}\n\n` - for (const stream of streams) { - sendMessage(stream, message); - } -} - -setInterval(() => { - for (const stream of streams) { - sendMessage(stream, "data: keepalive\n\n"); - } -}, 10e3) +import { addStream, deleteStream } from "./streams"; export async function GET(request: Request) { const encoder = new TextEncoder(); @@ -42,21 +10,15 @@ export async function GET(request: Request) { }, flush(controller) { console.log(`finished event stream for ${source}`); - streams.delete(stream.writable); + deleteStream(stream.writable); }, // @ts-expect-error experimental API cancel(reason) { console.log(`cancelled event stream for ${source}`); - streams.delete(stream.writable); + deleteStream(stream.writable); } }) - streams.add(stream.writable); - if (lastBroadcastId) { - sendMessage( - stream.writable, - `id: ${lastBroadcastData}\nevent: update\ndata: ${lastBroadcastData}\n\n` - ); - } + addStream(stream.writable); return new Response( stream.readable, { diff --git a/app/api/events/streams.ts b/app/api/events/streams.ts new file mode 100644 index 0000000..86ba572 --- /dev/null +++ b/app/api/events/streams.ts @@ -0,0 +1,50 @@ +import { Schedule } from "@/app/schedule/types"; + +function sendMessage( + stream: WritableStream, + message: string, +) { + const writer = stream.getWriter(); + writer.ready + .then(() => writer.write(message)) + .catch(console.error) + .finally(() => writer.releaseLock()) + ; +} + +declare global { + var streams: Set>; +} +global.streams = global.streams ?? new Set>(); + +let keepaliveInterval: ReturnType | null = null +export function addStream(stream: WritableStream) { + if (streams.size === 0) { + console.log("Starting keepalive") + keepaliveInterval = setInterval(sendKeepalive, 4000) + } + streams.add(stream); +} +export function deleteStream(stream: WritableStream) { + streams.delete(stream); + if (streams.size === 0) { + console.log("Ending keepalive") + clearInterval(keepaliveInterval!); + } +} + +export function broadcastUpdate(schedule: Schedule) { + const id = Date.now(); + const data = JSON.stringify(schedule); + const message = `id: ${id}\nevent: update\ndata: ${data}\n\n` + console.log(`broadcasting update from ${process.pid} to ${streams.size} clients`); + for (const stream of streams) { + sendMessage(stream, message); + } +} + +function sendKeepalive() { + for (const stream of streams) { + sendMessage(stream, "data: keepalive\n\n"); + } +} diff --git a/app/globals.css b/app/globals.css index f122b45..b6c5b22 100644 --- a/app/globals.css +++ b/app/globals.css @@ -52,3 +52,15 @@ a { border-color: transparent; } } + +label { + display: block; +} + +label>* { + margin-inline-start: 0.5rem; +} + +label + label { + margin-block-start: 0.5rem; +} diff --git a/app/schedule/context.tsx b/app/schedule/context.tsx index edba881..4717624 100644 --- a/app/schedule/context.tsx +++ b/app/schedule/context.tsx @@ -16,14 +16,12 @@ export function ScheduleProvider(props: ScheduleProviderProps) { const source = new EventSource("/api/events"); source.addEventListener("message", (message) => { console.log("Message", message.data); - setSchedule(old => { - const copy: Schedule = JSON.parse(JSON.stringify(old)); - const ts = copy.events[0].slots[0].start; - copy.events[0].slots[0].start = new Date(Date.parse(ts) + 36e5).toISOString(); - return copy; - }) }); - source.addEventListener("update", (message) => console.log("Update", message.data)); + source.addEventListener("update", (message) => { + const updatedSchedule: Schedule = JSON.parse(message.data); + console.log("Update", updatedSchedule); + setSchedule(updatedSchedule); + }); return () => { console.log("Closing event source") source.close(); diff --git a/app/schedule/page.tsx b/app/schedule/page.tsx index 8fa3726..405335b 100644 --- a/app/schedule/page.tsx +++ b/app/schedule/page.tsx @@ -4,6 +4,7 @@ import { readFile } from "fs/promises" import { ScheduleProvider } from "./context" import { Events } from "@/ui/events"; import { Locations } from "@/ui/locations"; +import { EventsEdit } from "@/ui/events-edit"; export default async function page() { const schedule: Schedule = JSON.parse(await readFile("schedule.json", "utf-8")); @@ -16,6 +17,7 @@ export default async function page() {

Schedule

+

Events

Locations

diff --git a/ui/events-edit.tsx b/ui/events-edit.tsx new file mode 100644 index 0000000..a9e6421 --- /dev/null +++ b/ui/events-edit.tsx @@ -0,0 +1,93 @@ +"use client"; +import { createEvent, deleteEvent, modifyEvent } from "@/app/api/events/actions"; +import { useSchedule } from "@/app/schedule/context"; +import Form from "next/form"; +import { useState } from "react"; + +export function EventsEdit() { + const schedule = useSchedule()!; + const event = schedule.events[0]; + + return
+ Admin Edit +

Create Event

+
+ + +