Port application from Next.js to Nuxt
Nuxt is based on Vue.js and I find their building blocks to be much neater compared to the React based Next.js.
This commit is contained in:
parent
8c8b561f1a
commit
250ca9a1ac
45 changed files with 662 additions and 1358 deletions
9
app.vue
9
app.vue
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<NuxtRouteAnnouncer />
|
||||
<NuxtWelcome />
|
||||
</div>
|
||||
<NuxtPage />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import "~/assets/global.css";
|
||||
</script>
|
||||
|
|
34
components/EventCard.vue
Normal file
34
components/EventCard.vue
Normal file
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<section class="event">
|
||||
<h3>{{ event.name }}</h3>
|
||||
<p>{{ event.description ?? "No description provided" }}</p>
|
||||
<h4>Timeslots</h4>
|
||||
<ul>
|
||||
<li v-for="slot in event.slots" :key="slot.id">
|
||||
{{ slot.start }} - {{ slot.end }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ScheduleEvent } from '~/shared/types/schedule';
|
||||
|
||||
defineProps<{
|
||||
event: ScheduleEvent
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.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;
|
||||
}
|
||||
</style>
|
111
components/EventsEdit.vue
Normal file
111
components/EventsEdit.vue
Normal file
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<details>
|
||||
<summary>Admin Edit</summary>
|
||||
<h3>Create Event</h3>
|
||||
<form method="post" action="/api/create-event">
|
||||
<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" required />
|
||||
</label>
|
||||
<label>
|
||||
End:
|
||||
<input type="datetime-local" name="end" required/>
|
||||
</label>
|
||||
<label>
|
||||
Location
|
||||
<select name="location">
|
||||
<option
|
||||
v-for="location in schedule.locations"
|
||||
:key="location.id"
|
||||
:value="location.id"
|
||||
>{{ location.name }} </option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
<h3>Edit Event</h3>
|
||||
<form method="post" action="/api/modify-event">
|
||||
<label>
|
||||
Event
|
||||
<select name="id" @change="onChange" ref="eventSelect">
|
||||
<option
|
||||
v-for="event in schedule.events"
|
||||
:key="event.id"
|
||||
:value="event.id"
|
||||
>{{ event.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Name:
|
||||
<input type="text" name="name" required />
|
||||
</label>
|
||||
<label>
|
||||
Description:
|
||||
<textarea name="description" />
|
||||
</label>
|
||||
<label>
|
||||
Start:
|
||||
<input type="datetime-local" name="start" required />
|
||||
</label>
|
||||
<label>
|
||||
End:
|
||||
<input type="datetime-local" name="end" required />
|
||||
</label>
|
||||
<label>
|
||||
Location
|
||||
<select name="location">
|
||||
<option
|
||||
v-for="location in schedule.locations"
|
||||
:key="location.id"
|
||||
:value="location.id"
|
||||
>{{ location.name }} </option>
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit">Edit</button>
|
||||
<button type="submit" formaction="/api/delete-event">Delete</button>
|
||||
</form>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const schedule = useSchedule();
|
||||
const eventSelect = useTemplateRef("eventSelect");
|
||||
|
||||
function onChange(event: any) {
|
||||
const newEvent = schedule.value.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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onChange({ target: eventSelect.value });
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
|
@ -1,6 +1,23 @@
|
|||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
<template>
|
||||
<section>
|
||||
Notifications are: <b>{{ subscription ? "Enabled" : "Disabled" }}</b>
|
||||
<br />
|
||||
<button
|
||||
:disabled="unsupported"
|
||||
@click="onClick"
|
||||
>
|
||||
{{ unsupported === undefined ? "Checking for support" : null }}
|
||||
{{ unsupported === true ? "Notifications are not supported :(." : null }}
|
||||
{{ unsupported === false ? (subscription ? "Disable notifications" : "Enable notifications") : null }}
|
||||
</button>
|
||||
<details>
|
||||
<summary>Debug</summary>
|
||||
<pre><code>{{ JSON.stringify(subscription?.toJSON(), undefined, 4) ?? "No subscription set" }}</code></pre>
|
||||
</details>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
function notificationUnsupported() {
|
||||
return (
|
||||
!("serviceWorker" in navigator)
|
||||
|
@ -79,42 +96,22 @@ async function getSubscription(
|
|||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
const unsupported = ref<boolean | undefined>(undefined);
|
||||
const subscription = ref<PushSubscription | null>(null);
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
|
||||
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>;
|
||||
function onClick() {
|
||||
if (!subscription.value)
|
||||
registerAndSubscribe(runtimeConfig.public.vapidPublicKey, (subs) => { subscription.value = subs })
|
||||
else
|
||||
unsubscribe(subscription.value, () => { subscription.value = null })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
unsupported.value = notificationUnsupported()
|
||||
if (unsupported.value) {
|
||||
return;
|
||||
}
|
||||
getSubscription(subs => { subscription.value = subs });
|
||||
})
|
||||
</script>
|
|
@ -1,7 +1,69 @@
|
|||
"use client";
|
||||
import { ScheduleEvent, ScheduleLocation, TimeSlot } from "@/app/schedule/types";
|
||||
import styles from "./timetable.module.css";
|
||||
import { useSchedule } from "@/app/schedule/context";
|
||||
<template>
|
||||
<figure class="timetable">
|
||||
<details>
|
||||
<summary>Debug</summary>
|
||||
<p><b>Junctions</b></p>
|
||||
<div v-for="j in junctions" :key="j.ts">
|
||||
{{ j.ts }}: {{ j.edges.map(e => `${e.type} ${e.slot.id}`).join(", ") }}
|
||||
</div>
|
||||
<p><b>Stretches</b></p>
|
||||
<ol>
|
||||
<li v-for="st in stretches" :key="st.start">
|
||||
<p>Stretch from {{ st.start }} to {{ st.end }}.</p>
|
||||
<p>Spans:</p>
|
||||
<ul>
|
||||
<li v-for="s in st.spans" :key="s.start.ts">
|
||||
{{ s.start.ts }} - {{ s.end.ts }}:
|
||||
<ul>
|
||||
<li v-for="[id, slots] in s.locations" :key="id">
|
||||
{{ id }}: {{ [...slots].map(s => s.id).join(", ") }}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
</details>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col class="header" />
|
||||
</colgroup>
|
||||
<colgroup v-for="group, groupIndex in columnGroups" :key="groupIndex" :class="group.className">
|
||||
<col v-for="col, index in group.cols" :key="index" :style='{"--minutes": col.minutes}' />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th v-for="day, index in dayHeaders" :key="index" :colSpan="day.span">
|
||||
{{ day.content }}
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Location</th>
|
||||
<th v-for="hour, index in hourHeaders" :key="index" :colSpan="hour.span">
|
||||
{{ hour.content }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="location in schedule.locations" :key="location.id">
|
||||
<th>{{ location.name }}</th>
|
||||
<td
|
||||
v-for="row, index in locationRows.get(location.id)"
|
||||
:key="index"
|
||||
:colSpan="row.span"
|
||||
:class='{"event": row.slots.size }'
|
||||
>
|
||||
{{ [...row.slots].map(slot => eventBySlotId.get(slot.id)!.name).join(", ") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ScheduleEvent, ScheduleLocation, TimeSlot } from "~/shared/types/schedule";
|
||||
|
||||
const oneDayMs = 24 * 60 * 60 * 1000;
|
||||
const oneHourMs = 60 * 60 * 1000;
|
||||
|
@ -58,7 +120,7 @@ function* pairs<T>(iterable: Iterable<T>) {
|
|||
@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>[]) {
|
||||
function setEquals<T>(...sets: Set<T>[]) {
|
||||
if (sets.length < 2) {
|
||||
throw TypeError("At least two sets must be passed to setEquals");
|
||||
}
|
||||
|
@ -266,7 +328,7 @@ function tableElementsFromStretches(
|
|||
startLocation(location.id);
|
||||
}
|
||||
} else {
|
||||
startColumnGroup(styles.break);
|
||||
startColumnGroup("break");
|
||||
const dayName = stretch.start.slice(0, 10)
|
||||
const sameDay = dayName === dayHeaders[dayHeaders.length - 1].content;
|
||||
if (!sameDay)
|
||||
|
@ -320,82 +382,68 @@ function tableElementsFromStretches(
|
|||
};
|
||||
}
|
||||
|
||||
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 schedule = useSchedule();
|
||||
const junctions = computed(() => junctionsFromEdges(edgesFromEvents(schedule.value.events)));
|
||||
const stretches = computed(() => [
|
||||
...stretchesFromSpans(spansFromJunctions(junctions.value, schedule.value.locations), oneHourMs * 5)
|
||||
])
|
||||
const elements = computed(() => tableElementsFromStretches(stretches.value, schedule.value.locations));
|
||||
const columnGroups = computed(() => elements.value.columnGroups);
|
||||
const dayHeaders = computed(() => elements.value.dayHeaders);
|
||||
const hourHeaders = computed(() => elements.value.hourHeaders);
|
||||
const locationRows = computed(() => elements.value.locationRows);
|
||||
const eventBySlotId = computed(() => new Map(
|
||||
schedule.value.events.flatMap(
|
||||
event => event.slots.map(slot => [slot.id, event])
|
||||
)
|
||||
));
|
||||
</script>
|
||||
|
||||
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>
|
||||
<style scoped>
|
||||
.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%);
|
||||
}
|
||||
</style>
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.1 KiB |
3
composables/states.ts
Normal file
3
composables/states.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import type { Schedule } from "~/shared/types/schedule";
|
||||
|
||||
export const useSchedule = () => useState<Schedule>('schedule');
|
|
@ -4,7 +4,7 @@ import fs from "node:fs/promises";
|
|||
const vapidKeys = webPush.generateVAPIDKeys();
|
||||
|
||||
const envData = `\
|
||||
VAPID_PUBLIC_KEY=${vapidKeys.publicKey}
|
||||
VAPID_PRIVATE_KEY=${vapidKeys.privateKey}
|
||||
NUXT_PUBLIC_VAPID_PUBLIC_KEY=${vapidKeys.publicKey}
|
||||
NUXT_VAPID_PRIVATE_KEY=${vapidKeys.privateKey}
|
||||
`;
|
||||
await fs.writeFile(".env", envData, "utf-8");
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2024-11-01',
|
||||
devtools: { enabled: true }
|
||||
devtools: { enabled: true },
|
||||
runtimeConfig: {
|
||||
vapidPrivateKey: "",
|
||||
public: {
|
||||
vapidPublicKey: "",
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
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.
|
|
@ -1,129 +0,0 @@
|
|||
"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");
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
|
@ -1,21 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
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>;
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
"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);
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
8
old/css.d.ts
vendored
8
old/css.d.ts
vendored
|
@ -1,8 +0,0 @@
|
|||
import type * as CSS from 'csstype';
|
||||
|
||||
// typing for custom variables.
|
||||
declare module 'csstype' {
|
||||
interface Properties {
|
||||
"--minutes"?: number,
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
};
|
||||
|
||||
export default nextConfig;
|
|
@ -1,26 +0,0 @@
|
|||
{
|
||||
"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
677
old/pnpm-lock.yaml
generated
|
@ -1,677 +0,0 @@
|
|||
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
|
|
@ -1,2 +0,0 @@
|
|||
User-Agent: *
|
||||
Disallow: /
|
|
@ -1,27 +0,0 @@
|
|||
{
|
||||
"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"]
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
"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>;
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
.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;
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
"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}/>)}
|
||||
</>;
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
"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>;
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
.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%);
|
||||
}
|
10
package.json
10
package.json
|
@ -12,13 +12,17 @@
|
|||
"dependencies": {
|
||||
"nuxt": "^3.15.4",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest"
|
||||
"vue-router": "latest",
|
||||
"web-push": "^3.6.7"
|
||||
},
|
||||
"packageManager": "pnpm@10.5.2+sha512.da9dc28cd3ff40d0592188235ab25d3202add8a207afbedc682220e4a0029ffbff4562102b9e6e46b4e3f9e8bd53e6d05de48544b0c57d4b0179e22c76d1199b",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"esbuild"
|
||||
"@parcel/watcher",
|
||||
"esbuild"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/web-push": "^3.6.4"
|
||||
}
|
||||
}
|
||||
|
|
10
pages/index.vue
Normal file
10
pages/index.vue
Normal file
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<main>
|
||||
<h1>Schedule Demo</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<NuxtLink to="/schedule">Schedule demo</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</main>
|
||||
</template>
|
57
pages/schedule.vue
Normal file
57
pages/schedule.vue
Normal file
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<main>
|
||||
<h1>Schedule & Events</h1>
|
||||
<p>
|
||||
Study carefully, we only hold these events once a year.
|
||||
</p>
|
||||
<p>
|
||||
Get notified about updates
|
||||
</p>
|
||||
<PushNotification />
|
||||
<h2>Schedule</h2>
|
||||
<Timetable />
|
||||
<EventsEdit />
|
||||
<h2>Events</h2>
|
||||
<EventCard v-for="event in schedule.events" :event/>
|
||||
<h2>Locations</h2>
|
||||
<ul>
|
||||
<li v-for="location in schedule.locations" :key="location.id">
|
||||
<h3>{{ location.name }}</h3>
|
||||
{{ location.description ?? "No description provided" }}
|
||||
</li>
|
||||
</ul>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Schedule } from '~/shared/types/schedule';
|
||||
|
||||
const schedule = useSchedule();
|
||||
|
||||
await callOnce(async () => {
|
||||
schedule.value = await $fetch("/api/schedule")
|
||||
})
|
||||
|
||||
const source = ref<EventSource | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
console.log("Opening event source")
|
||||
source.value = new EventSource("/api/events");
|
||||
source.value.addEventListener("message", (message) => {
|
||||
console.log("Message", message.data);
|
||||
});
|
||||
source.value.addEventListener("update", (message) => {
|
||||
const updatedSchedule: Schedule = JSON.parse(message.data);
|
||||
console.log("Update", updatedSchedule);
|
||||
schedule.value = updatedSchedule;
|
||||
});
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (source.value) {
|
||||
console.log("Closing event source")
|
||||
source.value.close();
|
||||
source.value = null;
|
||||
}
|
||||
});
|
||||
</script>
|
94
pnpm-lock.yaml
generated
94
pnpm-lock.yaml
generated
|
@ -17,6 +17,13 @@ importers:
|
|||
vue-router:
|
||||
specifier: latest
|
||||
version: 4.5.0(vue@3.5.13(typescript@5.8.2))
|
||||
web-push:
|
||||
specifier: ^3.6.7
|
||||
version: 3.6.7
|
||||
devDependencies:
|
||||
'@types/web-push':
|
||||
specifier: ^3.6.4
|
||||
version: 3.6.4
|
||||
|
||||
packages:
|
||||
|
||||
|
@ -875,6 +882,9 @@ packages:
|
|||
'@types/resolve@1.20.2':
|
||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||
|
||||
'@types/web-push@3.6.4':
|
||||
resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==}
|
||||
|
||||
'@unhead/dom@1.11.20':
|
||||
resolution: {integrity: sha512-jgfGYdOH+xHJF/j8gudjsYu3oIjFyXhCWcgKaw3vQnT616gSqyqnGQGOItL+BQtQZACKNISwIfx5PuOtztMKLA==}
|
||||
|
||||
|
@ -1040,6 +1050,9 @@ packages:
|
|||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
asn1.js@5.4.1:
|
||||
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
|
||||
|
||||
ast-kit@1.4.0:
|
||||
resolution: {integrity: sha512-BlGeOw73FDsX7z0eZE/wuuafxYoek2yzNJ6l6A1nsb4+z/p87TOPbHaWuN53kFKNuUXiCQa2M+xLF71IqQmRSw==}
|
||||
engines: {node: '>=16.14.0'}
|
||||
|
@ -1083,6 +1096,9 @@ packages:
|
|||
birpc@0.2.19:
|
||||
resolution: {integrity: sha512-5WeXXAvTmitV1RqJFppT5QtUiz2p1mRSYU000Jkft5ZUCLJIk4uQriYNO50HknxKwM6jd8utNc66K1qGIwwWBQ==}
|
||||
|
||||
bn.js@4.12.1:
|
||||
resolution: {integrity: sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==}
|
||||
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
|
@ -1105,6 +1121,9 @@ packages:
|
|||
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
|
@ -1432,6 +1451,9 @@ packages:
|
|||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
|
@ -1677,6 +1699,10 @@ packages:
|
|||
resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==}
|
||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||
|
||||
http_ece@1.2.0:
|
||||
resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
@ -1853,6 +1879,12 @@ packages:
|
|||
jsonfile@6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
|
||||
jwa@2.0.0:
|
||||
resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==}
|
||||
|
||||
jws@4.0.0:
|
||||
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
|
||||
|
||||
kleur@3.0.3:
|
||||
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||
engines: {node: '>=6'}
|
||||
|
@ -1957,6 +1989,9 @@ packages:
|
|||
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
minimalistic-assert@1.0.1:
|
||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
|
@ -1968,6 +2003,9 @@ packages:
|
|||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
minipass@3.3.6:
|
||||
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -2524,6 +2562,9 @@ packages:
|
|||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
scule@1.3.0:
|
||||
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
|
||||
|
||||
|
@ -3042,6 +3083,11 @@ packages:
|
|||
typescript:
|
||||
optional: true
|
||||
|
||||
web-push@3.6.7:
|
||||
resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==}
|
||||
engines: {node: '>= 16'}
|
||||
hasBin: true
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
|
@ -3998,6 +4044,10 @@ snapshots:
|
|||
|
||||
'@types/resolve@1.20.2': {}
|
||||
|
||||
'@types/web-push@3.6.4':
|
||||
dependencies:
|
||||
'@types/node': 22.13.8
|
||||
|
||||
'@unhead/dom@1.11.20':
|
||||
dependencies:
|
||||
'@unhead/schema': 1.11.20
|
||||
|
@ -4240,6 +4290,13 @@ snapshots:
|
|||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
asn1.js@5.4.1:
|
||||
dependencies:
|
||||
bn.js: 4.12.1
|
||||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
ast-kit@1.4.0:
|
||||
dependencies:
|
||||
'@babel/parser': 7.26.9
|
||||
|
@ -4281,6 +4338,8 @@ snapshots:
|
|||
|
||||
birpc@0.2.19: {}
|
||||
|
||||
bn.js@4.12.1: {}
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
brace-expansion@1.1.11:
|
||||
|
@ -4305,6 +4364,8 @@ snapshots:
|
|||
|
||||
buffer-crc32@1.0.0: {}
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
buffer@6.0.3:
|
||||
|
@ -4618,6 +4679,10 @@ snapshots:
|
|||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
electron-to-chromium@1.5.109: {}
|
||||
|
@ -4912,6 +4977,8 @@ snapshots:
|
|||
|
||||
http-shutdown@1.2.2: {}
|
||||
|
||||
http_ece@1.2.0: {}
|
||||
|
||||
https-proxy-agent@7.0.6(supports-color@9.4.0):
|
||||
dependencies:
|
||||
agent-base: 7.1.3
|
||||
|
@ -5065,6 +5132,17 @@ snapshots:
|
|||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
jwa@2.0.0:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jws@4.0.0:
|
||||
dependencies:
|
||||
jwa: 2.0.0
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
kleur@3.0.3: {}
|
||||
|
||||
klona@2.0.6: {}
|
||||
|
@ -5167,6 +5245,8 @@ snapshots:
|
|||
|
||||
mimic-fn@4.0.0: {}
|
||||
|
||||
minimalistic-assert@1.0.1: {}
|
||||
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.11
|
||||
|
@ -5179,6 +5259,8 @@ snapshots:
|
|||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
minipass@3.3.6:
|
||||
dependencies:
|
||||
yallist: 4.0.0
|
||||
|
@ -5901,6 +5983,8 @@ snapshots:
|
|||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
scule@1.3.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
|
@ -6441,6 +6525,16 @@ snapshots:
|
|||
optionalDependencies:
|
||||
typescript: 5.8.2
|
||||
|
||||
web-push@3.6.7:
|
||||
dependencies:
|
||||
asn1.js: 5.4.1
|
||||
http_ece: 1.2.0
|
||||
https-proxy-agent: 7.0.6(supports-color@9.4.0)
|
||||
jws: 4.0.0
|
||||
minimist: 1.2.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
webpack-virtual-modules@0.6.2: {}
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
|
||||
User-Agent: *
|
||||
Disallow: /
|
||||
|
|
31
server/api/create-event.post.ts
Normal file
31
server/api/create-event.post.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { Schedule } from "~/shared/types/schedule";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { broadcastUpdate } from "~/server/streams";
|
||||
import { sendPush } from "~/server/web-push";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const formData = await readFormData(event);
|
||||
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}`);
|
||||
});
|
16
server/api/delete-event.post.ts
Normal file
16
server/api/delete-event.post.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Schedule } from "~/shared/types/schedule";
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
import { broadcastUpdate } from "~/server/streams";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const formData = await readFormData(event);
|
||||
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");
|
||||
});
|
|
@ -1,8 +1,8 @@
|
|||
import { addStream, deleteStream } from "./streams";
|
||||
import { addStream, deleteStream } from "~/server/streams";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
export default defineEventHandler(async (event) => {
|
||||
const encoder = new TextEncoder();
|
||||
const source = request.headers.get("x-forwarded-for");
|
||||
const source = event.headers.get("x-forwarded-for");
|
||||
console.log(`starting event stream for ${source}`)
|
||||
const stream = new TransformStream<string, Uint8Array>({
|
||||
transform(chunk, controller) {
|
||||
|
@ -28,4 +28,4 @@ export async function GET(request: Request) {
|
|||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
37
server/api/modify-event.post.ts
Normal file
37
server/api/modify-event.post.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { Schedule } from "~/shared/types/schedule";
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
import { broadcastUpdate } from "~/server/streams";
|
||||
import { sendPush } from "~/server/web-push";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const formData = await readFormData(event);
|
||||
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}`);
|
||||
});
|
6
server/api/schedule.ts
Normal file
6
server/api/schedule.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { readFile } from "node:fs/promises";
|
||||
import { Schedule } from "~/shared/types/schedule";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
return JSON.parse(await readFile("schedule.json", "utf-8")) as Schedule;
|
||||
})
|
|
@ -1,7 +1,7 @@
|
|||
import { readFile, writeFile } from "fs/promises";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body: { subscription: PushSubscriptionJSON } = await request.json();
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body: { subscription: PushSubscriptionJSON } = await readBody(event);
|
||||
let subscriptions: PushSubscriptionJSON[];
|
||||
try {
|
||||
subscriptions = JSON.parse(await readFile("push-subscriptions.json", "utf-8"));
|
||||
|
@ -23,7 +23,7 @@ export async function POST(request: Request) {
|
|||
"utf-8"
|
||||
);
|
||||
if (existingIndex !== -1) {
|
||||
return new Response(JSON.stringify({ message: "Existing subscription refreshed."}));
|
||||
return { message: "Existing subscription refreshed."};
|
||||
}
|
||||
return new Response(JSON.stringify({ message: "New subscription registered."}));
|
||||
}
|
||||
return { message: "New subscription registered."};
|
||||
})
|
|
@ -1,7 +1,7 @@
|
|||
import { readFile, writeFile } from "fs/promises";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body: { subscription: PushSubscriptionJSON } = await request.json();
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body: { subscription: PushSubscriptionJSON } = await readBody(event);
|
||||
let subscriptions: PushSubscriptionJSON[];
|
||||
try {
|
||||
subscriptions = JSON.parse(await readFile("push-subscriptions.json", "utf-8"));
|
||||
|
@ -15,12 +15,12 @@ export async function POST(request: Request) {
|
|||
if (existingIndex !== -1) {
|
||||
subscriptions.splice(existingIndex, 1);
|
||||
} else {
|
||||
return new Response(JSON.stringify({ message: "No subscription registered."}));
|
||||
return { 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."}));
|
||||
}
|
||||
return { message: "Existing subscription removed."};
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
import { Schedule } from "@/app/schedule/types";
|
||||
import { Schedule } from "~/shared/types/schedule"
|
||||
|
||||
function sendMessage(
|
||||
stream: WritableStream<string>,
|
||||
|
@ -12,10 +12,7 @@ function sendMessage(
|
|||
;
|
||||
}
|
||||
|
||||
declare global {
|
||||
var streams: Set<WritableStream<string>>;
|
||||
}
|
||||
global.streams = global.streams ?? new Set<WritableStream<string>>();
|
||||
const streams = new Set<WritableStream<string>>();
|
||||
|
||||
let keepaliveInterval: ReturnType<typeof setInterval> | null = null
|
||||
export function addStream(stream: WritableStream<string>) {
|
56
server/web-push.ts
Normal file
56
server/web-push.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import webPush from "web-push";
|
||||
|
||||
webPush.setVapidDetails(
|
||||
"mailto:webmaster@hornwitser.no",
|
||||
process.env.NUXT_PUBLIC_VAPID_PUBLIC_KEY!,
|
||||
process.env.NUXT_VAPID_PRIVATE_KEY!,
|
||||
)
|
||||
|
||||
export 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");
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue