174 lines
6.8 KiB
Svelte
174 lines
6.8 KiB
Svelte
<!--
|
|
- This file is a part of the SteamWar software.
|
|
-
|
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
|
-
|
|
- This program is free software: you can redistribute it and/or modify
|
|
- it under the terms of the GNU Affero General Public License as published by
|
|
- the Free Software Foundation, either version 3 of the License, or
|
|
- (at your option) any later version.
|
|
-
|
|
- This program is distributed in the hope that it will be useful,
|
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
- GNU Affero General Public License for more details.
|
|
-
|
|
- You should have received a copy of the GNU Affero General Public License
|
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
-->
|
|
|
|
<script lang="ts">
|
|
import { eventRepo } from "@repo/event.ts";
|
|
import EventCard from "@components/moderator/components/EventCard.svelte";
|
|
import { Button } from "@components/ui/button/index.js";
|
|
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@components/ui/dialog/index.js";
|
|
import { Input } from "@components/ui/input/index.js";
|
|
import { Label } from "@components/ui/label/index.js";
|
|
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
|
import { PlusIcon } from "lucide-svelte";
|
|
import dayjs from "dayjs";
|
|
import { fromAbsolute, now, ZonedDateTime } from "@internationalized/date";
|
|
|
|
let eventsFuture = $state($eventRepo.listEvents());
|
|
let millis = Date.now();
|
|
|
|
let createOpen = $state(false);
|
|
let newEventName = $state("");
|
|
let newEventStart: ZonedDateTime = $state(now("Europe/Berlin"));
|
|
let newEventEnd: ZonedDateTime = $state(
|
|
now("Europe/Berlin").add({
|
|
days: 1,
|
|
})
|
|
);
|
|
let isSubmitting = $state(false);
|
|
let errorMsg = $state("");
|
|
|
|
function resetFormFields() {
|
|
newEventName = "";
|
|
newEventStart = now("Europe/Berlin");
|
|
newEventEnd = now("Europe/Berlin").add({
|
|
days: 1,
|
|
});
|
|
errorMsg = "";
|
|
isSubmitting = false;
|
|
}
|
|
|
|
$effect(() => {
|
|
if (createOpen) {
|
|
resetFormFields();
|
|
}
|
|
});
|
|
|
|
const canSubmit = $derived(
|
|
newEventName.trim() !== "" &&
|
|
newEventStart &&
|
|
newEventEnd &&
|
|
dayjs(newEventStart.toDate()).isValid() &&
|
|
dayjs(newEventEnd.toDate()).isValid() &&
|
|
newEventStart.toDate() < newEventEnd.toDate() &&
|
|
!isSubmitting
|
|
);
|
|
|
|
async function submitCreateEvent() {
|
|
if (!canSubmit) return;
|
|
|
|
isSubmitting = true;
|
|
errorMsg = "";
|
|
|
|
const payload = {
|
|
name: newEventName.trim(),
|
|
start: dayjs(newEventStart.toDate()),
|
|
end: dayjs(newEventEnd.toDate()),
|
|
};
|
|
|
|
try {
|
|
await $eventRepo.createEvent(payload);
|
|
eventsFuture = $eventRepo.listEvents(); // Refresh the list
|
|
createOpen = false;
|
|
} catch (e: any) {
|
|
errorMsg = e.message || "Failed to create event. Please try again.";
|
|
console.error("Failed to create event:", e);
|
|
} finally {
|
|
isSubmitting = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="p-4 min-h-screen">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h1 class="text-3xl font-semibold">Events</h1>
|
|
<Dialog bind:open={createOpen}>
|
|
<DialogTrigger>
|
|
{#snippet child({ props })}
|
|
<Button variant="outline" {...props}>
|
|
<PlusIcon class="mr-2" />
|
|
Create Event
|
|
</Button>
|
|
{/snippet}
|
|
</DialogTrigger>
|
|
<DialogContent class="sm:max-w-[425px]">
|
|
<DialogHeader>
|
|
<DialogTitle>Create New Event</DialogTitle>
|
|
<DialogDescription>Fill in the details for the new event. Click create when you're done.</DialogDescription>
|
|
</DialogHeader>
|
|
<div class="grid gap-4 py-4">
|
|
<div class="grid grid-cols-4 items-center gap-4">
|
|
<Label for="eventName" class="text-right">Name</Label>
|
|
<Input id="eventName" bind:value={newEventName} class="col-span-3" placeholder="Event Name" />
|
|
</div>
|
|
<div class="grid grid-cols-4 items-center gap-4">
|
|
<Label for="eventStart" class="text-right">Start</Label>
|
|
<div class="col-span-3">
|
|
<DateTimePicker bind:value={newEventStart} />
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-cols-4 items-center gap-4">
|
|
<Label for="eventEnd" class="text-right">End</Label>
|
|
<div class="col-span-3">
|
|
<DateTimePicker bind:value={newEventEnd} />
|
|
</div>
|
|
</div>
|
|
{#if errorMsg}
|
|
<p class="col-span-4 text-sm text-red-600 dark:text-red-500 text-center">{errorMsg}</p>
|
|
{/if}
|
|
</div>
|
|
<DialogFooter>
|
|
<DialogClose>
|
|
{#snippet child({ props })}
|
|
<Button variant="outline" {...props}>Cancel</Button>
|
|
{/snippet}
|
|
</DialogClose>
|
|
<Button onclick={submitCreateEvent} disabled={!canSubmit}>
|
|
{#if isSubmitting}
|
|
Creating...
|
|
{:else}
|
|
Create Event
|
|
{/if}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
|
|
{#await eventsFuture}
|
|
<p>Loading...</p>
|
|
{:then events}
|
|
<h1 class="mt-5 scroll-m-20 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0">Upcoming</h1>
|
|
<div class="grid gap-4 p-4 border-b" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))">
|
|
{#each events.filter((e) => e.start > millis) as event (event.id)}
|
|
<a href="#/event/{event.id}">
|
|
<EventCard {event} />
|
|
</a>
|
|
{/each}
|
|
</div>
|
|
<h1 class="mt-5 scroll-m-20 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0">Past</h1>
|
|
<div class="grid gap-4 p-4" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))">
|
|
{#each events.filter((e) => e.start < millis).reverse() as event (event.id)}
|
|
<a href="#/event/{event.id}">
|
|
<EventCard {event} />
|
|
</a>
|
|
{/each}
|
|
</div>
|
|
{/await}
|
|
</div>
|