Move all code to old/ to prepare for Nuxt rewrite
This commit is contained in:
parent
6007f4caeb
commit
51ff27c569
33 changed files with 0 additions and 1 deletions
5
old/.dockerignore
Normal file
5
old/.dockerignore
Normal file
|
@ -0,0 +1,5 @@
|
|||
node_modules
|
||||
.next/
|
||||
next-env.d.ts
|
||||
#env*
|
||||
push-subscriptions.json
|
65
old/.forgejo/workflows/build.yaml
Normal file
65
old/.forgejo/workflows/build.yaml
Normal file
|
@ -0,0 +1,65 @@
|
|||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
env:
|
||||
REGISTRY: forgejo.sbox.hornwitser.no
|
||||
REGISTRY_IMAGE: forgejo.sbox.hornwitser.no/furnavia/schedule-demo
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: docker
|
||||
container:
|
||||
image: forgejo.sbox.hornwitser.no/furnavia/builder:latest
|
||||
steps:
|
||||
-
|
||||
name: Authenticate
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
registry: ${{ env.REGISTRY }}
|
||||
-
|
||||
name: Get commit info
|
||||
id: info
|
||||
shell: bash
|
||||
run: |
|
||||
tee -a ${GITHUB_OUTPUT} <<EOF
|
||||
DEPLOY_IMAGE=${{ env.REGISTRY_IMAGE }}:${{ github.ref_name }}
|
||||
DEPLOY_BRANCH=${{ github.ref_name }}
|
||||
EOF
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: ${{ env.REGISTRY_IMAGE }}:latest
|
||||
outputs:
|
||||
DEPLOY_IMAGE: ${{ steps.info.outputs.DEPLOY_IMAGE }}
|
||||
DEPLOY_BRANCH: ${{ steps.info.outputs.DEPLOY_BRANCH }}
|
||||
deploy:
|
||||
runs-on: docker
|
||||
needs: build
|
||||
container:
|
||||
image: forgejo.sbox.hornwitser.no/furnavia/builder:latest
|
||||
steps:
|
||||
-
|
||||
name: Check info
|
||||
run: |
|
||||
echo DEPLOY_IMAGE=${{ needs.build.outputs.DEPLOY_IMAGE }}
|
||||
echo DEPLOY_BRANCH=${{ needs.build.outputs.DEPLOY_BRANCH }}
|
||||
-
|
||||
name: Configure kubectl
|
||||
run: |
|
||||
mkdir -p ~/.kube
|
||||
cat > ~/.kube/config <<"EOF"
|
||||
${{ secrets.KUBECONFIG_CONTENT }}
|
||||
EOF
|
||||
kubectl config view
|
||||
kubectl config use-context flux-sandbox
|
||||
-
|
||||
name: Deploy
|
||||
run: |
|
||||
kubectl rollout restart \
|
||||
-n schedule-demo \
|
||||
deployment website
|
48
old/Dockerfile
Normal file
48
old/Dockerfile
Normal file
|
@ -0,0 +1,48 @@
|
|||
# syntax=docker.io/docker/dockerfile:1
|
||||
FROM node:22 AS base
|
||||
|
||||
# Install dependencies only when needed
|
||||
FROM base AS deps
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
RUN corepack enable pnpm && pnpm i --frozen-lockfile
|
||||
|
||||
# Rebuild the source code only when needed
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
RUN corepack enable pnpm && pnpm run build
|
||||
|
||||
# Production image, copy all the files and run next
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./public
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||
COPY --chown=nextjs:nodejs schedule.json .
|
||||
RUN echo '[]' > push-subscriptions.json && chown nextjs:nodejs push-subscriptions.json
|
||||
|
||||
USER nextjs
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
|
||||
# server.js is created by next build from the standalone output
|
||||
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
|
||||
ENV HOSTNAME="0.0.0.0"
|
||||
CMD ["node", "server.js"]
|
36
old/README.md
Normal file
36
old/README.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
129
old/app/api/events/actions.ts
Normal file
129
old/app/api/events/actions.ts
Normal file
|
@ -0,0 +1,129 @@
|
|||
"use server";
|
||||
import webPush from "web-push";
|
||||
import { Schedule } from "@/app/schedule/types";
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
import { broadcastUpdate } from "./streams";
|
||||
|
||||
webPush.setVapidDetails(
|
||||
"mailto:webmaster@hornwitser.no",
|
||||
process.env.VAPID_PUBLIC_KEY!,
|
||||
process.env.VAPID_PRIVATE_KEY!,
|
||||
)
|
||||
|
||||
async function sendPush(title: string, body: string) {
|
||||
const payload = JSON.stringify({ title, body });
|
||||
let subscriptions: PushSubscriptionJSON[];
|
||||
try {
|
||||
subscriptions = JSON.parse(await readFile("push-subscriptions.json", "utf-8"));
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
console.log(`Dropping "${payload}", no push subscribers`);
|
||||
return;
|
||||
}
|
||||
subscriptions = [];
|
||||
}
|
||||
console.log(`Sending "${payload}" to ${subscriptions.length} subscribers`);
|
||||
const removeIndexes = [];
|
||||
for (let index = 0; index < subscriptions.length; index += 1) {
|
||||
const subscription = subscriptions[index];
|
||||
try {
|
||||
await webPush.sendNotification(
|
||||
subscription as webPush.PushSubscription,
|
||||
payload,
|
||||
{
|
||||
TTL: 3600,
|
||||
urgency: "high",
|
||||
}
|
||||
)
|
||||
} catch (err: any) {
|
||||
console.error("Received error sending push notice:", err.message, err?.statusCode)
|
||||
console.error(err);
|
||||
if (err?.statusCode >= 400 && err?.statusCode < 500) {
|
||||
removeIndexes.push(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (removeIndexes.length) {
|
||||
console.log(`Removing indexes ${removeIndexes} from subscriptions`)
|
||||
removeIndexes.reverse();
|
||||
for (const index of removeIndexes) {
|
||||
subscriptions.splice(index, 1);
|
||||
}
|
||||
await writeFile(
|
||||
"push-subscriptions.json",
|
||||
JSON.stringify(subscriptions, undefined, "\t"),
|
||||
"utf-8"
|
||||
);
|
||||
}
|
||||
console.log("Push notices sent");
|
||||
}
|
||||
|
||||
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: start + "Z",
|
||||
end: end + "Z",
|
||||
locations: [location],
|
||||
}
|
||||
]
|
||||
});
|
||||
broadcastUpdate(schedule);
|
||||
await writeFile("schedule.json", JSON.stringify(schedule, null, "\t"), "utf-8");
|
||||
await sendPush("New event", `${name} will start at ${start}`);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
const timeChanged = schedule.events[index].slots[0].start !== start + "Z";
|
||||
schedule.events[index] = {
|
||||
name,
|
||||
id,
|
||||
description,
|
||||
slots: [
|
||||
{
|
||||
id: `${id}-1`,
|
||||
start: start + "Z",
|
||||
end: end + "Z",
|
||||
locations: [location],
|
||||
}
|
||||
]
|
||||
};
|
||||
broadcastUpdate(schedule);
|
||||
await writeFile("schedule.json", JSON.stringify(schedule, null, "\t"), "utf-8");
|
||||
if (timeChanged)
|
||||
await sendPush(`New time for ${name}`, `${name} will now start at ${start}`);
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
31
old/app/api/events/route.ts
Normal file
31
old/app/api/events/route.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { addStream, deleteStream } from "./streams";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const encoder = new TextEncoder();
|
||||
const source = request.headers.get("x-forwarded-for");
|
||||
console.log(`starting event stream for ${source}`)
|
||||
const stream = new TransformStream<string, Uint8Array>({
|
||||
transform(chunk, controller) {
|
||||
controller.enqueue(encoder.encode(chunk));
|
||||
},
|
||||
flush(controller) {
|
||||
console.log(`finished event stream for ${source}`);
|
||||
deleteStream(stream.writable);
|
||||
},
|
||||
// @ts-expect-error experimental API
|
||||
cancel(reason) {
|
||||
console.log(`cancelled event stream for ${source}`);
|
||||
deleteStream(stream.writable);
|
||||
}
|
||||
})
|
||||
addStream(stream.writable);
|
||||
return new Response(
|
||||
stream.readable,
|
||||
{
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Content-Type": "text/event-stream",
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
50
old/app/api/events/streams.ts
Normal file
50
old/app/api/events/streams.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { Schedule } from "@/app/schedule/types";
|
||||
|
||||
function sendMessage(
|
||||
stream: WritableStream<string>,
|
||||
message: string,
|
||||
) {
|
||||
const writer = stream.getWriter();
|
||||
writer.ready
|
||||
.then(() => writer.write(message))
|
||||
.catch(console.error)
|
||||
.finally(() => writer.releaseLock())
|
||||
;
|
||||
}
|
||||
|
||||
declare global {
|
||||
var streams: Set<WritableStream<string>>;
|
||||
}
|
||||
global.streams = global.streams ?? new Set<WritableStream<string>>();
|
||||
|
||||
let keepaliveInterval: ReturnType<typeof setInterval> | null = null
|
||||
export function addStream(stream: WritableStream<string>) {
|
||||
if (streams.size === 0) {
|
||||
console.log("Starting keepalive")
|
||||
keepaliveInterval = setInterval(sendKeepalive, 4000)
|
||||
}
|
||||
streams.add(stream);
|
||||
}
|
||||
export function deleteStream(stream: WritableStream<string>) {
|
||||
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");
|
||||
}
|
||||
}
|
29
old/app/api/subscribe/route.ts
Normal file
29
old/app/api/subscribe/route.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { readFile, writeFile } from "fs/promises";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body: { subscription: PushSubscriptionJSON } = await request.json();
|
||||
let subscriptions: PushSubscriptionJSON[];
|
||||
try {
|
||||
subscriptions = JSON.parse(await readFile("push-subscriptions.json", "utf-8"));
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
subscriptions = [];
|
||||
}
|
||||
const existingIndex = subscriptions.findIndex(sub => sub.endpoint === body.subscription.endpoint);
|
||||
if (existingIndex !== -1) {
|
||||
subscriptions[existingIndex] = body.subscription;
|
||||
} else {
|
||||
subscriptions.push(body.subscription);
|
||||
}
|
||||
await writeFile(
|
||||
"push-subscriptions.json",
|
||||
JSON.stringify(subscriptions, undefined, "\t"),
|
||||
"utf-8"
|
||||
);
|
||||
if (existingIndex !== -1) {
|
||||
return new Response(JSON.stringify({ message: "Existing subscription refreshed."}));
|
||||
}
|
||||
return new Response(JSON.stringify({ message: "New subscription registered."}));
|
||||
}
|
26
old/app/api/unsubscribe/route.ts
Normal file
26
old/app/api/unsubscribe/route.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { readFile, writeFile } from "fs/promises";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body: { subscription: PushSubscriptionJSON } = await request.json();
|
||||
let subscriptions: PushSubscriptionJSON[];
|
||||
try {
|
||||
subscriptions = JSON.parse(await readFile("push-subscriptions.json", "utf-8"));
|
||||
} catch (err: any) {
|
||||
if (err.code !== "ENOENT") {
|
||||
throw err;
|
||||
}
|
||||
subscriptions = [];
|
||||
}
|
||||
const existingIndex = subscriptions.findIndex(sub => sub.endpoint === body.subscription.endpoint);
|
||||
if (existingIndex !== -1) {
|
||||
subscriptions.splice(existingIndex, 1);
|
||||
} else {
|
||||
return new Response(JSON.stringify({ message: "No subscription registered."}));
|
||||
}
|
||||
await writeFile(
|
||||
"push-subscriptions.json",
|
||||
JSON.stringify(subscriptions, undefined, "\t"),
|
||||
"utf-8"
|
||||
);
|
||||
return new Response(JSON.stringify({ message: "Existing subscription removed."}));
|
||||
}
|
BIN
old/app/favicon.ico
Normal file
BIN
old/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
70
old/app/globals.css
Normal file
70
old/app/globals.css
Normal file
|
@ -0,0 +1,70 @@
|
|||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
padding-inline: 1rem;
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
padding-inline-start: 1.5rem;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
margin-block: 0.75em 0.25em;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Enable hover only on non-touch devices */
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
a:hover {
|
||||
color: color-mix(in oklab, var(--foreground), blue 50%);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label>* {
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
p + p {
|
||||
margin-block-start: 0.5rem;
|
||||
}
|
||||
|
||||
label + label {
|
||||
margin-block-start: 0.5rem;
|
||||
}
|
21
old/app/layout.tsx
Normal file
21
old/app/layout.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import type { Metadata } from "next";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
12
old/app/page.tsx
Normal file
12
old/app/page.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import Link from "next/link";
|
||||
|
||||
export default function Home() {
|
||||
return <main>
|
||||
<h1>Schedule demo</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<Link href="/schedule">Schedule demo</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</main>;
|
||||
}
|
39
old/app/schedule/context.tsx
Normal file
39
old/app/schedule/context.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
"use client";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
import { Schedule } from "./types";
|
||||
|
||||
const ScheduleContext = createContext<Schedule | null>(null);
|
||||
|
||||
interface ScheduleProviderProps {
|
||||
children: React.ReactElement;
|
||||
schedule: Schedule;
|
||||
}
|
||||
|
||||
export function ScheduleProvider(props: ScheduleProviderProps) {
|
||||
const [schedule, setSchedule] = useState(props.schedule);
|
||||
useEffect(() => {
|
||||
console.log("Opening event source")
|
||||
const source = new EventSource("/api/events");
|
||||
source.addEventListener("message", (message) => {
|
||||
console.log("Message", 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();
|
||||
}
|
||||
}, [])
|
||||
return (
|
||||
<ScheduleContext.Provider value={schedule}>
|
||||
{props.children}
|
||||
</ScheduleContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSchedule() {
|
||||
return useContext(ScheduleContext);
|
||||
}
|
35
old/app/schedule/page.tsx
Normal file
35
old/app/schedule/page.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import Timetable from "@/ui/timetable"
|
||||
import { Schedule } from "./types"
|
||||
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";
|
||||
import { PushNotification } from "@/ui/push-notification";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function page() {
|
||||
const schedule: Schedule = JSON.parse(await readFile("schedule.json", "utf-8"));
|
||||
return (
|
||||
<ScheduleProvider schedule={schedule}>
|
||||
<main>
|
||||
<h1>Schedule & Events</h1>
|
||||
<p>
|
||||
Study carefully, we only hold these events once a year.
|
||||
</p>
|
||||
<p>
|
||||
Get notified about updates
|
||||
</p>
|
||||
<PushNotification vapidPublicKey={process.env.VAPID_PUBLIC_KEY!} />
|
||||
<h2>Schedule</h2>
|
||||
<Timetable />
|
||||
<EventsEdit />
|
||||
<h2>Events</h2>
|
||||
<Events />
|
||||
<h2>Locations</h2>
|
||||
<Locations />
|
||||
</main>
|
||||
</ScheduleProvider>
|
||||
);
|
||||
}
|
26
old/app/schedule/types.ts
Normal file
26
old/app/schedule/types.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
export interface ScheduleEvent {
|
||||
name: string,
|
||||
id: string,
|
||||
host?: string,
|
||||
cancelled?: boolean,
|
||||
description?: string,
|
||||
slots: TimeSlot[],
|
||||
}
|
||||
|
||||
export interface ScheduleLocation {
|
||||
name: string,
|
||||
id: string,
|
||||
description?: string,
|
||||
}
|
||||
|
||||
export interface TimeSlot {
|
||||
id: string,
|
||||
start: string,
|
||||
end: string,
|
||||
locations: string[],
|
||||
}
|
||||
|
||||
export interface Schedule {
|
||||
locations: ScheduleLocation[],
|
||||
events: ScheduleEvent[],
|
||||
}
|
8
old/css.d.ts
vendored
Normal file
8
old/css.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import type * as CSS from 'csstype';
|
||||
|
||||
// typing for custom variables.
|
||||
declare module 'csstype' {
|
||||
interface Properties {
|
||||
"--minutes"?: number,
|
||||
}
|
||||
}
|
7
old/next.config.ts
Normal file
7
old/next.config.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
26
old/package.json
Normal file
26
old/package.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "schedule-demo",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "15.1.7",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"web-push": "^3.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/web-push": "^3.6.4",
|
||||
"csstype": "^3.1.3",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.12+sha1.a2f983fbf8f2531dc85db2a5d7f398063d51a6f3"
|
||||
}
|
677
old/pnpm-lock.yaml
generated
Normal file
677
old/pnpm-lock.yaml
generated
Normal file
|
@ -0,0 +1,677 @@
|
|||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
next:
|
||||
specifier: 15.1.7
|
||||
version: 15.1.7(react-dom@19.0.0)(react@19.0.0)
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.0.0
|
||||
react-dom:
|
||||
specifier: ^19.0.0
|
||||
version: 19.0.0(react@19.0.0)
|
||||
web-push:
|
||||
specifier: ^3.6.7
|
||||
version: 3.6.7
|
||||
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20
|
||||
version: 20.0.0
|
||||
'@types/react':
|
||||
specifier: ^19
|
||||
version: 19.0.0
|
||||
'@types/react-dom':
|
||||
specifier: ^19
|
||||
version: 19.0.0
|
||||
'@types/web-push':
|
||||
specifier: ^3.6.4
|
||||
version: 3.6.4
|
||||
csstype:
|
||||
specifier: ^3.1.3
|
||||
version: 3.1.3
|
||||
typescript:
|
||||
specifier: ^5
|
||||
version: 5.0.2
|
||||
|
||||
packages:
|
||||
|
||||
/@emnapi/runtime@1.3.1:
|
||||
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-darwin-arm64@0.33.5:
|
||||
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.0.4
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-darwin-x64@0.33.5:
|
||||
resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.0.4
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-libvips-darwin-arm64@1.0.4:
|
||||
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-libvips-darwin-x64@1.0.4:
|
||||
resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-libvips-linux-arm64@1.0.4:
|
||||
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-libvips-linux-arm@1.0.5:
|
||||
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-libvips-linux-s390x@1.0.4:
|
||||
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-libvips-linux-x64@1.0.4:
|
||||
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-libvips-linuxmusl-arm64@1.0.4:
|
||||
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-libvips-linuxmusl-x64@1.0.4:
|
||||
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-linux-arm64@0.33.5:
|
||||
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.0.4
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-linux-arm@0.33.5:
|
||||
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.0.5
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-linux-s390x@0.33.5:
|
||||
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.0.4
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-linux-x64@0.33.5:
|
||||
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.0.4
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-linuxmusl-arm64@0.33.5:
|
||||
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-linuxmusl-x64@0.33.5:
|
||||
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-wasm32@0.33.5:
|
||||
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.3.1
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-win32-ia32@0.33.5:
|
||||
resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@img/sharp-win32-x64@0.33.5:
|
||||
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/env@15.1.7:
|
||||
resolution: {integrity: sha512-d9jnRrkuOH7Mhi+LHav2XW91HOgTAWHxjMPkXMGBc9B2b7614P7kjt8tAplRvJpbSt4nbO1lugcT/kAaWzjlLQ==}
|
||||
dev: false
|
||||
|
||||
/@next/swc-darwin-arm64@15.1.7:
|
||||
resolution: {integrity: sha512-hPFwzPJDpA8FGj7IKV3Yf1web3oz2YsR8du4amKw8d+jAOHfYHYFpMkoF6vgSY4W6vB29RtZEklK9ayinGiCmQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-darwin-x64@15.1.7:
|
||||
resolution: {integrity: sha512-2qoas+fO3OQKkU0PBUfwTiw/EYpN+kdAx62cePRyY1LqKtP09Vp5UcUntfZYajop5fDFTjSxCHfZVRxzi+9FYQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-arm64-gnu@15.1.7:
|
||||
resolution: {integrity: sha512-sKLLwDX709mPdzxMnRIXLIT9zaX2w0GUlkLYQnKGoXeWUhcvpCrK+yevcwCJPdTdxZEUA0mOXGLdPsGkudGdnA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-arm64-musl@15.1.7:
|
||||
resolution: {integrity: sha512-zblK1OQbQWdC8fxdX4fpsHDw+VSpBPGEUX4PhSE9hkaWPrWoeIJn+baX53vbsbDRaDKd7bBNcXRovY1hEhFd7w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-x64-gnu@15.1.7:
|
||||
resolution: {integrity: sha512-GOzXutxuLvLHFDAPsMP2zDBMl1vfUHHpdNpFGhxu90jEzH6nNIgmtw/s1MDwpTOiM+MT5V8+I1hmVFeAUhkbgQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-linux-x64-musl@15.1.7:
|
||||
resolution: {integrity: sha512-WrZ7jBhR7ATW1z5iEQ0ZJfE2twCNSXbpCSaAunF3BKcVeHFADSI/AW1y5Xt3DzTqPF1FzQlwQTewqetAABhZRQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-arm64-msvc@15.1.7:
|
||||
resolution: {integrity: sha512-LDnj1f3OVbou1BqvvXVqouJZKcwq++mV2F+oFHptToZtScIEnhNRJAhJzqAtTE2dB31qDYL45xJwrc+bLeKM2Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@next/swc-win32-x64-msvc@15.1.7:
|
||||
resolution: {integrity: sha512-dC01f1quuf97viOfW05/K8XYv2iuBgAxJZl7mbCKEjMgdQl5JjAKJ0D2qMKZCgPWDeFbFT0Q0nYWwytEW0DWTQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@swc/counter@0.1.3:
|
||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
||||
dev: false
|
||||
|
||||
/@swc/helpers@0.5.15:
|
||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
dev: false
|
||||
|
||||
/@types/node@20.0.0:
|
||||
resolution: {integrity: sha512-cD2uPTDnQQCVpmRefonO98/PPijuOnnEy5oytWJFPY1N9aJCz2wJ5kSGWO+zJoed2cY2JxQh6yBuUq4vIn61hw==}
|
||||
dev: true
|
||||
|
||||
/@types/react-dom@19.0.0:
|
||||
resolution: {integrity: sha512-1KfiQKsH1o00p9m5ag12axHQSb3FOU9H20UTrujVSkNhuCrRHiQWFqgEnTNK5ZNfnzZv8UWrnXVqCmCF9fgY3w==}
|
||||
dependencies:
|
||||
'@types/react': 19.0.0
|
||||
dev: true
|
||||
|
||||
/@types/react@19.0.0:
|
||||
resolution: {integrity: sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==}
|
||||
dependencies:
|
||||
csstype: 3.1.3
|
||||
dev: true
|
||||
|
||||
/@types/web-push@3.6.4:
|
||||
resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==}
|
||||
dependencies:
|
||||
'@types/node': 20.0.0
|
||||
dev: true
|
||||
|
||||
/agent-base@7.1.3:
|
||||
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
/asn1.js@5.4.1:
|
||||
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
|
||||
dependencies:
|
||||
bn.js: 4.12.1
|
||||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
safer-buffer: 2.1.2
|
||||
dev: false
|
||||
|
||||
/bn.js@4.12.1:
|
||||
resolution: {integrity: sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==}
|
||||
dev: false
|
||||
|
||||
/buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
dev: false
|
||||
|
||||
/busboy@1.6.0:
|
||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||
engines: {node: '>=10.16.0'}
|
||||
dependencies:
|
||||
streamsearch: 1.1.0
|
||||
dev: false
|
||||
|
||||
/caniuse-lite@1.0.30001701:
|
||||
resolution: {integrity: sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==}
|
||||
dev: false
|
||||
|
||||
/client-only@0.0.1:
|
||||
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
|
||||
dev: false
|
||||
|
||||
/color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/color-string@1.9.1:
|
||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
simple-swizzle: 0.2.2
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/color@4.2.3:
|
||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||
engines: {node: '>=12.5.0'}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
color-string: 1.9.1
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
dev: true
|
||||
|
||||
/debug@4.4.0:
|
||||
resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
dev: false
|
||||
|
||||
/detect-libc@2.0.3:
|
||||
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
||||
engines: {node: '>=8'}
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/http_ece@1.2.0:
|
||||
resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==}
|
||||
engines: {node: '>=16'}
|
||||
dev: false
|
||||
|
||||
/https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
dependencies:
|
||||
agent-base: 7.1.3
|
||||
debug: 4.4.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
dev: false
|
||||
|
||||
/is-arrayish@0.3.2:
|
||||
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/jwa@2.0.0:
|
||||
resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==}
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/jws@4.0.0:
|
||||
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
|
||||
dependencies:
|
||||
jwa: 2.0.0
|
||||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/minimalistic-assert@1.0.1:
|
||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||
dev: false
|
||||
|
||||
/minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
dev: false
|
||||
|
||||
/ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
dev: false
|
||||
|
||||
/nanoid@3.3.8:
|
||||
resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/next@15.1.7(react-dom@19.0.0)(react@19.0.0):
|
||||
resolution: {integrity: sha512-GNeINPGS9c6OZKCvKypbL8GTsT5GhWPp4DM0fzkXJuXMilOO2EeFxuAY6JZbtk6XIl6Ws10ag3xRINDjSO5+wg==}
|
||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@opentelemetry/api': ^1.1.0
|
||||
'@playwright/test': ^1.41.2
|
||||
babel-plugin-react-compiler: '*'
|
||||
react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
|
||||
sass: ^1.3.0
|
||||
peerDependenciesMeta:
|
||||
'@opentelemetry/api':
|
||||
optional: true
|
||||
'@playwright/test':
|
||||
optional: true
|
||||
babel-plugin-react-compiler:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@next/env': 15.1.7
|
||||
'@swc/counter': 0.1.3
|
||||
'@swc/helpers': 0.5.15
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: 1.0.30001701
|
||||
postcss: 8.4.31
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
styled-jsx: 5.1.6(react@19.0.0)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 15.1.7
|
||||
'@next/swc-darwin-x64': 15.1.7
|
||||
'@next/swc-linux-arm64-gnu': 15.1.7
|
||||
'@next/swc-linux-arm64-musl': 15.1.7
|
||||
'@next/swc-linux-x64-gnu': 15.1.7
|
||||
'@next/swc-linux-x64-musl': 15.1.7
|
||||
'@next/swc-win32-arm64-msvc': 15.1.7
|
||||
'@next/swc-win32-x64-msvc': 15.1.7
|
||||
sharp: 0.33.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
dev: false
|
||||
|
||||
/picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
dev: false
|
||||
|
||||
/postcss@8.4.31:
|
||||
resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
dependencies:
|
||||
nanoid: 3.3.8
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
dev: false
|
||||
|
||||
/react-dom@19.0.0(react@19.0.0):
|
||||
resolution: {integrity: sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==}
|
||||
peerDependencies:
|
||||
react: ^19.0.0
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
scheduler: 0.25.0
|
||||
dev: false
|
||||
|
||||
/react@19.0.0:
|
||||
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
dev: false
|
||||
|
||||
/safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
dev: false
|
||||
|
||||
/scheduler@0.25.0:
|
||||
resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==}
|
||||
dev: false
|
||||
|
||||
/semver@7.7.1:
|
||||
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/sharp@0.33.5:
|
||||
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
color: 4.2.3
|
||||
detect-libc: 2.0.3
|
||||
semver: 7.7.1
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.33.5
|
||||
'@img/sharp-darwin-x64': 0.33.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.0.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.0.4
|
||||
'@img/sharp-libvips-linux-arm': 1.0.5
|
||||
'@img/sharp-libvips-linux-arm64': 1.0.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.0.4
|
||||
'@img/sharp-libvips-linux-x64': 1.0.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
|
||||
'@img/sharp-linux-arm': 0.33.5
|
||||
'@img/sharp-linux-arm64': 0.33.5
|
||||
'@img/sharp-linux-s390x': 0.33.5
|
||||
'@img/sharp-linux-x64': 0.33.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.33.5
|
||||
'@img/sharp-linuxmusl-x64': 0.33.5
|
||||
'@img/sharp-wasm32': 0.33.5
|
||||
'@img/sharp-win32-ia32': 0.33.5
|
||||
'@img/sharp-win32-x64': 0.33.5
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
requiresBuild: true
|
||||
dependencies:
|
||||
is-arrayish: 0.3.2
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/streamsearch@1.1.0:
|
||||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
dev: false
|
||||
|
||||
/styled-jsx@5.1.6(react@19.0.0):
|
||||
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': '*'
|
||||
babel-plugin-macros: '*'
|
||||
react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
babel-plugin-macros:
|
||||
optional: true
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.0.0
|
||||
dev: false
|
||||
|
||||
/tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
dev: false
|
||||
|
||||
/typescript@5.0.2:
|
||||
resolution: {integrity: sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==}
|
||||
engines: {node: '>=12.20'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/web-push@3.6.7:
|
||||
resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==}
|
||||
engines: {node: '>= 16'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
asn1.js: 5.4.1
|
||||
http_ece: 1.2.0
|
||||
https-proxy-agent: 7.0.6
|
||||
jws: 4.0.0
|
||||
minimist: 1.2.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
2
old/public/robots.txt
Normal file
2
old/public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
User-Agent: *
|
||||
Disallow: /
|
25
old/public/sw.js
Normal file
25
old/public/sw.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
self.addEventListener("push", function (event) {
|
||||
console.log(event);
|
||||
if (!event.data)
|
||||
return;
|
||||
|
||||
const payload = event.data.json();
|
||||
const { body, icon, image, badge, url, title } = payload;
|
||||
const notificationTitle = title ?? "No title";
|
||||
const notificationOptions = {
|
||||
body,
|
||||
icon,
|
||||
image,
|
||||
data: {
|
||||
url,
|
||||
},
|
||||
badge,
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(notificationTitle, notificationOptions)
|
||||
.then(() => {
|
||||
console.log("Web push delivered");
|
||||
})
|
||||
);
|
||||
});
|
90
old/schedule.json
Normal file
90
old/schedule.json
Normal file
|
@ -0,0 +1,90 @@
|
|||
{
|
||||
"locations": [
|
||||
{
|
||||
"name": "House",
|
||||
"id": "house",
|
||||
"description": "Blue building east of the camping"
|
||||
},
|
||||
{
|
||||
"name": "Common House",
|
||||
"id": "common-house",
|
||||
"description": "That big red building in the middle"
|
||||
},
|
||||
{
|
||||
"name": "Info Desk",
|
||||
"id": "info-desk",
|
||||
"description": "Found at the entrance"
|
||||
},
|
||||
{
|
||||
"name": "Camping Fireplace",
|
||||
"id": "camping-fireplace",
|
||||
"description": "Next to the big tree"
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "Arcade",
|
||||
"id": "arcade",
|
||||
"description": "Play retro games!",
|
||||
"slots": [
|
||||
{
|
||||
"id": "arcade-1",
|
||||
"start": "2025-07-18T10:00Z",
|
||||
"end": "2025-07-19T01:30Z",
|
||||
"locations": ["house"]
|
||||
},
|
||||
{
|
||||
"id": "arcade-2",
|
||||
"start": "2025-07-19T10:00Z",
|
||||
"end": "2025-07-20T01:00Z",
|
||||
"locations": ["house"]
|
||||
},
|
||||
{
|
||||
"id": "arcade-3",
|
||||
"start": "2025-07-20T10:00Z",
|
||||
"end": "2025-07-20T18:00Z",
|
||||
"locations": ["house"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Bonfire Stories",
|
||||
"description": "Share your stories as we sit cosily around the bonfire.",
|
||||
"id": "bonfire",
|
||||
"slots": [
|
||||
{
|
||||
"id": "bonfire-1",
|
||||
"start": "2025-07-19T20:00Z",
|
||||
"end": "2025-07-20T01:00Z",
|
||||
"locations": ["camping-fireplace"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Fursuit Games",
|
||||
"description": "Playful time for the suiters.",
|
||||
"id": "fursuit-games",
|
||||
"slots": [
|
||||
{
|
||||
"id": "fursuit-games-1",
|
||||
"start": "2025-07-19T19:00Z",
|
||||
"end": "2025-07-19T20:00Z",
|
||||
"locations": ["common-house"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Late Stragglers",
|
||||
"description": "Wait a minute, why are you still here?.",
|
||||
"id": "too-late",
|
||||
"slots": [
|
||||
{
|
||||
"id": "too-late-1",
|
||||
"start": "2025-07-22T20:00Z",
|
||||
"end": "2025-07-23T01:00Z",
|
||||
"locations": ["camping-fireplace"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
27
old/tsconfig.json
Normal file
27
old/tsconfig.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "css.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
93
old/ui/events-edit.tsx
Normal file
93
old/ui/events-edit.tsx
Normal file
|
@ -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 <details>
|
||||
<summary>Admin Edit</summary>
|
||||
<h3>Create Event</h3>
|
||||
<Form action={createEvent}>
|
||||
<label>
|
||||
Id:
|
||||
<input type="text" name="id" required />
|
||||
</label>
|
||||
<label>
|
||||
Name:
|
||||
<input type="text" name="name" required />
|
||||
</label>
|
||||
<label>
|
||||
Description:
|
||||
<textarea name="description" />
|
||||
</label>
|
||||
<label>
|
||||
Start:
|
||||
<input type="datetime-local" name="start" defaultValue="2025-07-20T18:00" />
|
||||
</label>
|
||||
<label>
|
||||
End:
|
||||
<input type="datetime-local" name="end" defaultValue="2025-07-20T20:00" />
|
||||
</label>
|
||||
<label>
|
||||
Location
|
||||
<select name="location">
|
||||
{schedule?.locations.map(location => <option key={location.id} value={location.id}>{location.name}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Create</button>
|
||||
</Form>
|
||||
<h3>Edit Event</h3>
|
||||
<Form action={modifyEvent}>
|
||||
<label>
|
||||
Event
|
||||
<select name="id" onChange={(event) => {
|
||||
const newEvent = schedule.events.find(e => e.id === event.target.value)!;
|
||||
const form = event.target.form!;
|
||||
for (const element of form.elements as any) {
|
||||
if (element.name === "name") {
|
||||
element.value = newEvent.name;
|
||||
} else if (element.name === "description") {
|
||||
element.value = newEvent.description;
|
||||
} else if (element.name === "start") {
|
||||
element.value = newEvent.slots[0].start.replace("Z", "");
|
||||
} else if (element.name === "end") {
|
||||
element.value = newEvent.slots[0].end.replace("Z", "");
|
||||
} else if (element.name === "location") {
|
||||
element.value = newEvent.slots[0].locations[0];
|
||||
}
|
||||
}
|
||||
}}>
|
||||
{schedule?.events.map(event => <option key={event.id} value={event.id}>{event.name}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Name:
|
||||
<input type="text" name="name" defaultValue={event.name} />
|
||||
</label>
|
||||
<label>
|
||||
Description:
|
||||
<textarea name="description" defaultValue={event.description} />
|
||||
</label>
|
||||
<label>
|
||||
Start:
|
||||
<input type="datetime-local" name="start" defaultValue={event.slots[0].start.replace("Z", "")} />
|
||||
</label>
|
||||
<label>
|
||||
End:
|
||||
<input type="datetime-local" name="end" defaultValue={event.slots[0].end.replace("Z", "")} />
|
||||
</label>
|
||||
<label>
|
||||
Location
|
||||
<select name="location">
|
||||
{schedule?.locations.map(location => <option key={location.id} value={location.id}>{location.name}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Edit</button>
|
||||
<button type="submit" formAction={deleteEvent}>Delete</button>
|
||||
</Form>
|
||||
</details>;
|
||||
}
|
11
old/ui/events.module.css
Normal file
11
old/ui/events.module.css
Normal file
|
@ -0,0 +1,11 @@
|
|||
.event {
|
||||
background: color-mix(in oklab, var(--background), grey 20%);
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.event h3 {
|
||||
margin: 0;
|
||||
}
|
||||
.event + .event {
|
||||
margin-block-start: 0.5rem;
|
||||
}
|
24
old/ui/events.tsx
Normal file
24
old/ui/events.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
"use client";
|
||||
import styles from "./events.module.css"
|
||||
import { useSchedule } from "@/app/schedule/context";
|
||||
import { ScheduleEvent } from "@/app/schedule/types";
|
||||
|
||||
function EventInfo(props: { event: ScheduleEvent }) {
|
||||
return <section className={styles.event}>
|
||||
<h3>{props.event.name}</h3>
|
||||
<p>{props.event.description ?? "No description provided"}</p>
|
||||
<h4>Timeslots</h4>
|
||||
<ul>
|
||||
{props.event.slots.map(slot => <li key={slot.id}>
|
||||
{slot.start} - {slot.end}
|
||||
</li>)}
|
||||
</ul>
|
||||
</section>
|
||||
}
|
||||
|
||||
export function Events() {
|
||||
const schedule = useSchedule();
|
||||
return <>
|
||||
{schedule!.events.map(event => <EventInfo event={event} key={event.id}/>)}
|
||||
</>;
|
||||
}
|
12
old/ui/locations.tsx
Normal file
12
old/ui/locations.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
"use client";
|
||||
import { useSchedule } from "@/app/schedule/context";
|
||||
|
||||
export function Locations() {
|
||||
const schedule = useSchedule();
|
||||
return <ul>
|
||||
{schedule!.locations.map(location => <li key={location.id}>
|
||||
<h3>{location.name}</h3>
|
||||
{location.description ?? "No description provided"}
|
||||
</li>)}
|
||||
</ul>;
|
||||
}
|
120
old/ui/push-notification.tsx
Normal file
120
old/ui/push-notification.tsx
Normal file
|
@ -0,0 +1,120 @@
|
|||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
function notificationUnsupported() {
|
||||
return (
|
||||
!("serviceWorker" in navigator)
|
||||
|| !("PushManager" in window)
|
||||
|| !("showNotification" in ServiceWorkerRegistration.prototype)
|
||||
)
|
||||
}
|
||||
|
||||
async function registerAndSubscribe(
|
||||
vapidPublicKey: string,
|
||||
onSubscribe: (subs: PushSubscription | null ) => void,
|
||||
) {
|
||||
try {
|
||||
await navigator.serviceWorker.register("/sw.js");
|
||||
await subscribe(vapidPublicKey, onSubscribe);
|
||||
} catch (err) {
|
||||
console.error("Failed to register service worker:", err);
|
||||
}
|
||||
}
|
||||
|
||||
async function subscribe(
|
||||
vapidPublicKey: string,
|
||||
onSubscribe: (subs: PushSubscription | null) => void
|
||||
) {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const subscription = await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: vapidPublicKey,
|
||||
});
|
||||
console.log("Got subscription object", subscription.toJSON());
|
||||
await submitSubscription(subscription);
|
||||
onSubscribe(subscription);
|
||||
} catch (err) {
|
||||
console.error("Failed to subscribe:" , err);
|
||||
}
|
||||
}
|
||||
|
||||
async function unsubscribe(
|
||||
subscription: PushSubscription,
|
||||
onUnsubscribed: () => void,
|
||||
) {
|
||||
const body = JSON.stringify({ subscription });
|
||||
await subscription.unsubscribe();
|
||||
const res = await fetch("/api/unsubscribe", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body,
|
||||
});
|
||||
const result = await res.json();
|
||||
console.log("/api/unsubscribe returned", result);
|
||||
onUnsubscribed();
|
||||
}
|
||||
|
||||
async function submitSubscription(subscription: PushSubscription) {
|
||||
const res = await fetch("/api/subscribe", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ subscription }),
|
||||
});
|
||||
const result = await res.json();
|
||||
console.log("/api/subscribe returned", result);
|
||||
}
|
||||
|
||||
async function getSubscription(
|
||||
onSubscribe: (subs: PushSubscription | null) => void,
|
||||
) {
|
||||
const registration = await navigator.serviceWorker.ready;
|
||||
const subscription = await registration.pushManager.getSubscription();
|
||||
if (subscription) {
|
||||
onSubscribe(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
export function PushNotification(props: { vapidPublicKey: string }) {
|
||||
const [unsupported, setUnsupported] = useState<boolean | undefined>(undefined);
|
||||
const [subscription, setSubscription] = useState<PushSubscription | null>(null);
|
||||
useEffect(() => {
|
||||
const isUnsupported = notificationUnsupported();
|
||||
setUnsupported(isUnsupported);
|
||||
if (isUnsupported) {
|
||||
return;
|
||||
}
|
||||
|
||||
getSubscription(setSubscription);
|
||||
}, [props.vapidPublicKey])
|
||||
|
||||
return <section>
|
||||
Notifications are: <b>{subscription ? "Enabled" : "Disabled"}</b>
|
||||
<br />
|
||||
<button
|
||||
disabled={unsupported}
|
||||
onClick={() => {
|
||||
if (!subscription)
|
||||
registerAndSubscribe(props.vapidPublicKey, setSubscription)
|
||||
else
|
||||
unsubscribe(subscription, () => setSubscription(null))
|
||||
}}
|
||||
>
|
||||
{unsupported === undefined && "Checking for support"}
|
||||
{unsupported === true && "Notifications are not supported :(."}
|
||||
{unsupported === false && subscription ? "Disable notifications" : "Enable notifications"}
|
||||
</button>
|
||||
<details>
|
||||
<summary>Debug</summary>
|
||||
<pre>
|
||||
<code>
|
||||
{JSON.stringify(subscription?.toJSON(), undefined, 4) ?? "No subscription set"}
|
||||
</code>
|
||||
</pre>
|
||||
</details>
|
||||
</section>;
|
||||
}
|
BIN
old/ui/timetable-terminology.png
Normal file
BIN
old/ui/timetable-terminology.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.1 KiB |
46
old/ui/timetable.module.css
Normal file
46
old/ui/timetable.module.css
Normal file
|
@ -0,0 +1,46 @@
|
|||
.timetable {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.timetable table {
|
||||
border-spacing: 0;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
font-size: 0.8rem;
|
||||
--row-header-width: 6rem;
|
||||
--cell-size: 3rem;
|
||||
}
|
||||
|
||||
.timetable col {
|
||||
width: calc(var(--cell-size) * var(--minutes, 60) / 60);
|
||||
}
|
||||
|
||||
.timetable col.header {
|
||||
width: var(--row-header-width);
|
||||
}
|
||||
|
||||
.timetable th:first-child {
|
||||
background-color: var(--background);
|
||||
position: sticky;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.timetable :is(td, th) {
|
||||
padding: 0.1rem;
|
||||
border-top: 1px solid var(--foreground);
|
||||
border-right: 1px solid var(--foreground);
|
||||
}
|
||||
.timetable tr th:first-child {
|
||||
border-left: 1px solid var(--foreground);
|
||||
}
|
||||
.timetable tbody tr:last-child :is(td, th) {
|
||||
border-bottom: 1px solid var(--foreground);
|
||||
}
|
||||
|
||||
.break {
|
||||
background-color: color-mix(in oklab, var(--background), rgb(50, 50, 255) 60%);
|
||||
}
|
||||
|
||||
.event {
|
||||
background-color: color-mix(in oklab, var(--background), rgb(255, 125, 50) 60%);
|
||||
}
|
401
old/ui/timetable.tsx
Normal file
401
old/ui/timetable.tsx
Normal file
|
@ -0,0 +1,401 @@
|
|||
"use client";
|
||||
import { ScheduleEvent, ScheduleLocation, TimeSlot } from "@/app/schedule/types";
|
||||
import styles from "./timetable.module.css";
|
||||
import { useSchedule } from "@/app/schedule/context";
|
||||
|
||||
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||
const oneHourMs = 60 * 60 * 1000;
|
||||
const oneMinMs = 60 * 1000;
|
||||
|
||||
// See timetable-terminology.png for an illustration of how these terms are related
|
||||
|
||||
/** Point in time where a time slots starts or ends. */
|
||||
type Edge = { type: "start" | "end", slot: TimeSlot };
|
||||
|
||||
/** Point in time where multiple edges meet. */
|
||||
type Junction = { ts: string, edges: Edge[] };
|
||||
|
||||
/** Span of time between two adjacent junctions */
|
||||
type Span = {
|
||||
start: Junction;
|
||||
end: Junction,
|
||||
locations: Map<string, Set<TimeSlot>>,
|
||||
};
|
||||
|
||||
/**
|
||||
Collection of adjacent spans containing TimeSlots that are close to each
|
||||
other in time. The start and end of the stretch is aligned to a whole hour
|
||||
and the endpoint spans are always empty and at least one hour.
|
||||
*/
|
||||
type Stretch = {
|
||||
start: string,
|
||||
end: string,
|
||||
spans: Span[];
|
||||
}
|
||||
|
||||
/** Returns a tuple consisting of a running index starting from 0, and the item of the iterable */
|
||||
function* enumerate<T>(iterable: Iterable<T>) {
|
||||
let index = 0;
|
||||
for (const item of iterable) {
|
||||
yield [index++, item] as [number, T];
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns adjacent pairs from iterable */
|
||||
function* pairs<T>(iterable: Iterable<T>) {
|
||||
let first;
|
||||
let second;
|
||||
for (const [index, item] of enumerate(iterable)) {
|
||||
[first, second] = [second, item];
|
||||
if (index >= 1) {
|
||||
yield [first, second] as [T, T];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Returns true if all sets are equal
|
||||
@param sets set to compare
|
||||
@returns true if all sets are the same size and have the same elements
|
||||
*/
|
||||
export function setEquals<T>(...sets: Set<T>[]) {
|
||||
if (sets.length < 2) {
|
||||
throw TypeError("At least two sets must be passed to setEquals");
|
||||
}
|
||||
const ref = sets[0];
|
||||
const rest = sets.slice(1);
|
||||
if (rest.some(set => set.size !== ref.size)) {
|
||||
return false;
|
||||
}
|
||||
for (const set of rest) {
|
||||
for (const el of set) {
|
||||
if (!ref.has(el)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function isoStringFromTs(ts: number) {
|
||||
return new Date(ts).toISOString().replace(":00.000Z", "Z");
|
||||
}
|
||||
|
||||
function* edgesFromEvents(events: Iterable<ScheduleEvent>): Generator<Edge> {
|
||||
for (const event of events) {
|
||||
for (const slot of event.slots) {
|
||||
if (slot.start > slot.end) {
|
||||
throw new Error(`Slot ${slot.id} ends before it starts.`);
|
||||
}
|
||||
yield { type: "start", slot }
|
||||
yield { type: "end", slot }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function junctionsFromEdges(edges: Iterable<Edge>) {
|
||||
const junctions = new Map<string, Junction>();
|
||||
for (const edge of edges) {
|
||||
const ts = edge.slot[edge.type];
|
||||
const junction = junctions.get(ts);
|
||||
if (junction) {
|
||||
junction.edges.push(edge);
|
||||
} else {
|
||||
junctions.set(ts, { ts, edges: [edge] });
|
||||
}
|
||||
}
|
||||
const keys = [...junctions.keys()].sort();
|
||||
return keys.map(key => junctions.get(key)!);
|
||||
}
|
||||
|
||||
function* spansFromJunctions(
|
||||
junctions: Iterable<Junction>, locations: ScheduleLocation[]
|
||||
): Generator<Span> {
|
||||
const activeLocations = new Map(
|
||||
locations.map(location => [location.id, new Set<TimeSlot>()])
|
||||
);
|
||||
for (const [start, end] of pairs(junctions)) {
|
||||
for (const edge of start.edges) {
|
||||
if (edge.type === "start") {
|
||||
for (const location of edge.slot.locations) {
|
||||
activeLocations.get(location)!.add(edge.slot)
|
||||
}
|
||||
}
|
||||
}
|
||||
yield {
|
||||
start,
|
||||
end,
|
||||
locations: new Map(
|
||||
[...activeLocations]
|
||||
.filter(([_, slots]) => slots.size)
|
||||
.map(([location, slots]) => [location, new Set(slots)])
|
||||
),
|
||||
}
|
||||
for (const edge of end.edges) {
|
||||
if (edge.type === "end") {
|
||||
for (const location of edge.slot.locations) {
|
||||
activeLocations.get(location)!.delete(edge.slot)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createStretch(spans: Span[]): Stretch {
|
||||
let startTs = Date.parse(spans[0].start.ts) - oneHourMs;
|
||||
let endTs = Date.parse(spans[spans.length - 1].end.ts) + oneHourMs;
|
||||
// Extend stretch to nearest whole hours
|
||||
startTs = Math.floor(startTs / oneHourMs) * oneHourMs;
|
||||
endTs = Math.ceil(endTs / oneHourMs) * oneHourMs;
|
||||
// Convert back to ISO date string
|
||||
let start = isoStringFromTs(startTs);
|
||||
let end = isoStringFromTs(endTs);
|
||||
return {
|
||||
spans: [
|
||||
{
|
||||
start: { ts: start, edges: [] },
|
||||
end: spans[0].start,
|
||||
locations: new Map(),
|
||||
},
|
||||
...spans,
|
||||
{
|
||||
start: spans[spans.length - 1].end,
|
||||
end: { ts: end, edges: [] },
|
||||
locations: new Map(),
|
||||
},
|
||||
],
|
||||
start,
|
||||
end,
|
||||
}
|
||||
}
|
||||
|
||||
function* stretchesFromSpans(spans: Iterable<Span>, minSeparation: number): Generator<Stretch> {
|
||||
let currentSpans: Span[] = [];
|
||||
for (const span of spans) {
|
||||
// Based on how spans are generated I can assume that an empty span
|
||||
// will only occur between two spans with timeslots in them.
|
||||
if (span.locations.size === 0
|
||||
&& Date.parse(span.end.ts) - Date.parse(span.start.ts) >= minSeparation
|
||||
) {
|
||||
yield createStretch(currentSpans);
|
||||
currentSpans = [];
|
||||
} else {
|
||||
currentSpans.push(span);
|
||||
}
|
||||
}
|
||||
if (currentSpans.length)
|
||||
yield createStretch(currentSpans);
|
||||
}
|
||||
|
||||
/** Cuts up a span by whole hours that crosses it */
|
||||
function* cutSpansByHours(span: Span): Generator<Span> {
|
||||
const startHour = Date.parse(span.start.ts) / oneHourMs;
|
||||
const endHour = Date.parse(span.end.ts) / oneHourMs;
|
||||
let currentStart = startHour;
|
||||
let currentEnd = Math.min(Math.floor(startHour + 1), endHour);
|
||||
if (currentEnd === endHour) {
|
||||
yield span;
|
||||
return;
|
||||
}
|
||||
|
||||
yield {
|
||||
start: span.start,
|
||||
end: { ts: isoStringFromTs(currentEnd * oneHourMs), edges: [] },
|
||||
locations: span.locations,
|
||||
}
|
||||
|
||||
currentStart = currentEnd;
|
||||
while (++currentEnd < endHour) {
|
||||
yield {
|
||||
start: { ts: isoStringFromTs(currentStart * oneHourMs), edges: [] },
|
||||
end: { ts: isoStringFromTs(currentEnd * oneHourMs), edges: [] },
|
||||
locations: span.locations,
|
||||
}
|
||||
currentStart += 1;
|
||||
}
|
||||
|
||||
yield {
|
||||
start: { ts: isoStringFromTs(currentStart * oneHourMs), edges: [] },
|
||||
end: span.end,
|
||||
locations: span.locations,
|
||||
}
|
||||
}
|
||||
|
||||
function tableElementsFromStretches(
|
||||
stretches: Iterable<Stretch>, locations: ScheduleLocation[]
|
||||
) {
|
||||
type Col = { minutes?: number };
|
||||
type DayHead = { span: number, content?: string }
|
||||
type HourHead = { span: number, content?: string }
|
||||
type LocationCell = { span: number, slots: Set<TimeSlot> }
|
||||
const columnGroups: { className?: string, cols: Col[] }[] = [];
|
||||
const dayHeaders: DayHead[] = [];
|
||||
const hourHeaders: HourHead[]= [];
|
||||
const locationRows = new Map<string, LocationCell[]>(locations.map(location => [location.id, []]));
|
||||
|
||||
function startColumnGroup(className?: string) {
|
||||
columnGroups.push({ className, cols: []})
|
||||
}
|
||||
function startDay(content?: string) {
|
||||
dayHeaders.push({ span: 0, content })
|
||||
}
|
||||
function startHour(content?: string) {
|
||||
hourHeaders.push({ span: 0, content })
|
||||
}
|
||||
function startLocation(id: string, slots = new Set<TimeSlot>()) {
|
||||
locationRows.get(id)!.push({ span: 0, slots });
|
||||
}
|
||||
function pushColumn(minutes?: number) {
|
||||
columnGroups[columnGroups.length - 1].cols.push({ minutes })
|
||||
dayHeaders[dayHeaders.length - 1].span += 1;
|
||||
hourHeaders[hourHeaders.length - 1].span += 1;
|
||||
for(const location of locations) {
|
||||
const row = locationRows.get(location.id)!;
|
||||
row[row.length - 1].span += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let first = true;
|
||||
for (const stretch of stretches) {
|
||||
if (first) {
|
||||
first = false;
|
||||
startColumnGroup();
|
||||
startDay(stretch.start.slice(0, 10));
|
||||
startHour(stretch.start.slice(11, 16));
|
||||
for(const location of locations) {
|
||||
startLocation(location.id);
|
||||
}
|
||||
} else {
|
||||
startColumnGroup(styles.break);
|
||||
const dayName = stretch.start.slice(0, 10)
|
||||
const sameDay = dayName === dayHeaders[dayHeaders.length - 1].content;
|
||||
if (!sameDay)
|
||||
startDay();
|
||||
startHour("break");
|
||||
for(const location of locations) {
|
||||
startLocation(location.id);
|
||||
}
|
||||
pushColumn();
|
||||
|
||||
startColumnGroup();
|
||||
if (!sameDay)
|
||||
startDay(dayName);
|
||||
startHour(stretch.start.slice(11, 16));
|
||||
for(const location of locations) {
|
||||
startLocation(location.id);
|
||||
}
|
||||
}
|
||||
|
||||
for (const span of stretch.spans) {
|
||||
for (const cutSpan of cutSpansByHours(span)) {
|
||||
const startTs = Date.parse(cutSpan.start.ts);
|
||||
const endTs = Date.parse(cutSpan.end.ts);
|
||||
const durationMs = endTs - startTs;
|
||||
|
||||
for (const location of locations) {
|
||||
const rows = locationRows.get(location.id)!;
|
||||
const row = rows[rows.length - 1];
|
||||
const slots = cutSpan.locations.get(location.id) ?? new Set();
|
||||
if (!setEquals(slots, row.slots)) {
|
||||
startLocation(location.id, slots);
|
||||
}
|
||||
}
|
||||
|
||||
pushColumn(durationMs / oneMinMs);
|
||||
if (endTs % oneDayMs === 0) {
|
||||
startDay(cutSpan.end.ts.slice(0, 10));
|
||||
}
|
||||
if (endTs % oneHourMs === 0) {
|
||||
startHour(cutSpan.end.ts.slice(11, 16));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
columnGroups,
|
||||
dayHeaders: dayHeaders.filter(day => day.span),
|
||||
hourHeaders: hourHeaders.filter(hour => hour.span),
|
||||
locationRows: new Map([...locationRows].map(([id, cells]) => [id, cells.filter(cell => cell.span)])),
|
||||
};
|
||||
}
|
||||
|
||||
export default function Timetable() {
|
||||
const { locations, events } = useSchedule()!;
|
||||
const junctions = junctionsFromEdges(edgesFromEvents(events));
|
||||
const stretches = [...stretchesFromSpans(spansFromJunctions(junctions, locations), oneHourMs * 5)];
|
||||
const {
|
||||
columnGroups,
|
||||
dayHeaders,
|
||||
hourHeaders,
|
||||
locationRows,
|
||||
} = tableElementsFromStretches(stretches, locations);
|
||||
const eventBySlotId = new Map(
|
||||
events.flatMap(
|
||||
event => event.slots.map(slot => [slot.id, event])
|
||||
)
|
||||
);
|
||||
|
||||
const debug = <details>
|
||||
<summary>Debug</summary>
|
||||
<p><b>Junctions</b></p>
|
||||
{junctions.map(j => <div key={j.ts}>
|
||||
{j.ts}: {j.edges.map(e => `${e.type} ${e.slot.id}`).join(", ")}
|
||||
</div>)}
|
||||
<p><b>Stretches</b></p>
|
||||
<ol>
|
||||
{stretches.map(st => <li key={st.start}>
|
||||
<p>Stretch from {st.start} to {st.end}.</p>
|
||||
<p>Spans:</p>
|
||||
<ul>
|
||||
{st.spans.map(s => <li key={s.start.ts}>
|
||||
{s.start.ts} - {s.end.ts}:
|
||||
<ul>
|
||||
{[...s.locations].map(([id, slots]) => <li key={id}>
|
||||
{id}: {[...slots].map(s => s.id).join(", ")}
|
||||
</li>)}
|
||||
</ul>
|
||||
</li>)}
|
||||
</ul>
|
||||
</li>)}
|
||||
</ol>
|
||||
</details>;
|
||||
|
||||
return <figure className={styles.timetable}>
|
||||
{debug}
|
||||
<table>
|
||||
<colgroup>
|
||||
<col className={styles.header} />
|
||||
</colgroup>
|
||||
{columnGroups.map((group, groupIndex) => <colgroup key={groupIndex} className={group.className}>
|
||||
{group.cols.map((col, index) => <col key={index} style={{ "--minutes": col.minutes}} />)}
|
||||
</colgroup>)}
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
{dayHeaders.map((day, index) => <th key={index} colSpan={day.span}>
|
||||
{day.content}
|
||||
</th>)}
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Location</th>
|
||||
{hourHeaders.map((hour, index) => <th key={index} colSpan={hour.span}>
|
||||
{hour.content}
|
||||
</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{locations.map(location => <tr key={location.id}>
|
||||
<th>{location.name}</th>
|
||||
{locationRows.get(location.id)!.map((row, index) => <td
|
||||
key={index}
|
||||
colSpan={row.span}
|
||||
className={row.slots.size ? styles.event : undefined}
|
||||
>
|
||||
{[...row.slots].map(slot => eventBySlotId.get(slot.id)!.name).join(", ")}
|
||||
</td>)}
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue