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
+
+ Edit Event
+
+ ;
+}