This commit is contained in:
95
src/components/event/Calendar.svelte
Normal file
95
src/components/event/Calendar.svelte
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import "dayjs/locale/de";
|
||||||
|
import type { ExtendedEvent } from "../types/event";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-svelte";
|
||||||
|
import * as Card from "../ui/card";
|
||||||
|
import EventCard from "./EventCard.svelte";
|
||||||
|
import SWButton from "@components/styled/SWButton.svelte";
|
||||||
|
|
||||||
|
const {
|
||||||
|
events,
|
||||||
|
}: {
|
||||||
|
events: { slug: string; data: { event: ExtendedEvent } }[];
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let currentYear = $state(dayjs().year());
|
||||||
|
|
||||||
|
// Group events by month
|
||||||
|
let eventsByMonth = $derived.by(() => {
|
||||||
|
const grouped = new Map<string, typeof events>();
|
||||||
|
|
||||||
|
events.forEach((event) => {
|
||||||
|
const eventDate = dayjs(event.data.event.event.start).locale("de");
|
||||||
|
if (eventDate.year() === currentYear) {
|
||||||
|
const monthKey = eventDate.format("YYYY-MM");
|
||||||
|
if (!grouped.has(monthKey)) {
|
||||||
|
grouped.set(monthKey, []);
|
||||||
|
}
|
||||||
|
grouped.get(monthKey)!.push(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate all 12 months for the current year
|
||||||
|
let months = $derived.by(() => {
|
||||||
|
return Array.from({ length: 12 }, (_, i) => {
|
||||||
|
const monthDate = dayjs().locale("de").year(currentYear).month(i);
|
||||||
|
const monthKey = monthDate.format("YYYY-MM");
|
||||||
|
return {
|
||||||
|
date: monthDate,
|
||||||
|
key: monthKey,
|
||||||
|
name: monthDate.format("MMMM"),
|
||||||
|
events: eventsByMonth.get(monthKey) || [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function prevYear() {
|
||||||
|
currentYear = currentYear - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextYear() {
|
||||||
|
currentYear = currentYear + 1;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-2xl font-bold text-white">
|
||||||
|
{currentYear}
|
||||||
|
</h2>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<SWButton onclick={prevYear} type="gray">
|
||||||
|
<ChevronLeft size={20} />
|
||||||
|
</SWButton>
|
||||||
|
<SWButton onclick={nextYear} type="gray">
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</SWButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{#each months as month}
|
||||||
|
<EventCard title={month.name} unsized={true}>
|
||||||
|
{#if month.events.length > 0}
|
||||||
|
{#each month.events as event}
|
||||||
|
<a href={`/events/${event.slug}/`} class="block p-2 bg-slate-800 hover:bg-slate-700 rounded border border-slate-600 transition-colors group">
|
||||||
|
<div class="text-sm font-semibold text-white group-hover:text-blue-400 transition-colors">
|
||||||
|
{event.data.event.event.name}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400 mt-1">
|
||||||
|
{dayjs(event.data.event.event.start).format("MMM D, YYYY • HH:mm")}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<p class="text-gray-500 text-sm italic">Keine Events für diesen Monat</p>
|
||||||
|
{/if}
|
||||||
|
</EventCard>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -4,13 +4,15 @@
|
|||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
|
unsized = false,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
|
unsized?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col w-72 m-4 gap-1">
|
<div class="flex flex-col gap-1 {unsized ? '' : 'w-72 m-4'}">
|
||||||
<div class="bg-gray-100 text-black font-bold px-2 rounded uppercase">
|
<div class="bg-gray-100 text-black font-bold px-2 rounded uppercase">
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
122
src/components/event/EventList.svelte
Normal file
122
src/components/event/EventList.svelte
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ExtendedEvent } from "../types/event";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import * as Card from "../ui/card";
|
||||||
|
|
||||||
|
const { events }: { events: { slug: string; data: { event: ExtendedEvent } }[] } = $props();
|
||||||
|
// Categorize events into current, upcoming and past.
|
||||||
|
const now = dayjs();
|
||||||
|
const sorted = [...events].sort((a, b) => a.data.event.event.start - b.data.event.event.start);
|
||||||
|
|
||||||
|
const currentEvents = sorted
|
||||||
|
.filter((e) => {
|
||||||
|
const start = dayjs(e.data.event.event.start);
|
||||||
|
const end = dayjs(e.data.event.event.end);
|
||||||
|
return start.isBefore(now) && end.isAfter(now);
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.data.event.event.end - b.data.event.event.end);
|
||||||
|
|
||||||
|
const currentEvent = currentEvents[0];
|
||||||
|
const upcomingEvents = sorted.filter((e) => dayjs(e.data.event.event.start).isAfter(now));
|
||||||
|
const pastEvents = sorted.filter((e) => dayjs(e.data.event.event.end).isBefore(now)).sort((a, b) => b.data.event.event.end - a.data.event.event.end);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if currentEvent}
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-white mb-4">Aktuelles Event</h2>
|
||||||
|
<div class="grid grid-cols-1">
|
||||||
|
<a href={`/events/${currentEvent.slug}/`} class="group block h-full">
|
||||||
|
<Card.Root class="h-full overflow-hidden border-slate-700 bg-slate-800 transition-all hover:-translate-y-1 hover:shadow-xl">
|
||||||
|
<div class="h-32 bg-gradient-to-br from-blue-600 to-purple-700 relative">
|
||||||
|
<div class="absolute bottom-0 left-0 p-4 bg-gradient-to-t from-slate-900 to-transparent w-full">
|
||||||
|
<div class="inline-block bg-slate-900/80 backdrop-blur text-white text-xs font-bold px-2 py-1 rounded mb-1 border border-slate-600">
|
||||||
|
{dayjs(currentEvent.data.event.event.start).format("DD.MM.YYYY")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title class="text-white group-hover:text-blue-400 transition-colors">
|
||||||
|
{currentEvent.data.event.event.name}
|
||||||
|
</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<p class="text-gray-400 text-sm line-clamp-2">
|
||||||
|
Läuft seit {dayjs(currentEvent.data.event.event.start).format("HH:mm")}
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 flex items-center text-sm text-blue-400 font-medium">
|
||||||
|
Details <span class="ml-1 transition-transform group-hover:translate-x-1">→</span>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if upcomingEvents.length}
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-white mb-4">Bevorstehende Events</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{#each upcomingEvents as event}
|
||||||
|
<a href={`/events/${event.slug}/`} class="group block h-full">
|
||||||
|
<Card.Root class="h-full overflow-hidden border-slate-700 bg-slate-800 transition-all hover:-translate-y-1 hover:shadow-xl">
|
||||||
|
<div class="h-32 bg-gradient-to-br from-blue-600 to-purple-700 relative">
|
||||||
|
<div class="absolute bottom-0 left-0 p-4 bg-gradient-to-t from-slate-900 to-transparent w-full">
|
||||||
|
<div class="inline-block bg-slate-900/80 backdrop-blur text-white text-xs font-bold px-2 py-1 rounded mb-1 border border-slate-600">
|
||||||
|
{dayjs(event.data.event.event.start).format("DD.MM.YYYY")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title class="text-white group-hover:text-blue-400 transition-colors">
|
||||||
|
{event.data.event.event.name}
|
||||||
|
</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<p class="text-gray-400 text-sm line-clamp-2">
|
||||||
|
Startet um {dayjs(event.data.event.event.start).format("HH:mm")}
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 flex items-center text-sm text-blue-400 font-medium">
|
||||||
|
Details <span class="ml-1 transition-transform group-hover:translate-x-1">→</span>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if pastEvents.length}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h2 class="text-xl font-semibold text-white mb-4">Vergangene Events</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 opacity-70">
|
||||||
|
{#each pastEvents as event}
|
||||||
|
<a href={`/events/${event.slug}/`} class="group block h-full">
|
||||||
|
<Card.Root class="h-full overflow-hidden border-slate-700 bg-slate-800 transition-all hover:-translate-y-1 hover:shadow-xl">
|
||||||
|
<div class="h-32 bg-gradient-to-br from-blue-600 to-purple-700 relative">
|
||||||
|
<div class="absolute bottom-0 left-0 p-4 bg-gradient-to-t from-slate-900 to-transparent w-full">
|
||||||
|
<div class="inline-block bg-slate-900/80 backdrop-blur text-white text-xs font-bold px-2 py-1 rounded mb-1 border border-slate-600">
|
||||||
|
{dayjs(event.data.event.event.start).format("DD.MM.YYYY")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title class="text-white group-hover:text-blue-400 transition-colors">
|
||||||
|
{event.data.event.event.name}
|
||||||
|
</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<p class="text-gray-400 text-sm line-clamp-2">
|
||||||
|
Stattgefunden um {dayjs(event.data.event.event.start).format("HH:mm")}
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 flex items-center text-sm text-blue-400 font-medium">
|
||||||
|
Details <span class="ml-1 transition-transform group-hover:translate-x-1">→</span>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
21
src/components/event/EventPage.svelte
Normal file
21
src/components/event/EventPage.svelte
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ExtendedEvent } from "../types/event";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Calendar } from "lucide-svelte";
|
||||||
|
import { List } from "lucide-svelte";
|
||||||
|
import EventList from "./EventList.svelte";
|
||||||
|
import CalendarView from "./Calendar.svelte";
|
||||||
|
|
||||||
|
const { events }: { events: { slug: string; data: { event: ExtendedEvent } }[] } = $props();
|
||||||
|
|
||||||
|
let viewMode = $state<"list" | "calendar">("list");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h1 class="text-3xl font-bold text-white">Events</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CalendarView {events} />
|
||||||
|
<EventList {events} />
|
||||||
|
</div>
|
||||||
@@ -27,6 +27,25 @@ export const getStaticPaths = createGetStaticPaths(async () => {
|
|||||||
const { event, page } = Astro.props as { event: ExtendedEvent; page: CollectionEntry<"events"> };
|
const { event, page } = Astro.props as { event: ExtendedEvent; page: CollectionEntry<"events"> };
|
||||||
|
|
||||||
const { Content } = await page.render();
|
const { Content } = await page.render();
|
||||||
|
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
"0": "#000000",
|
||||||
|
"1": "#0000AA",
|
||||||
|
"2": "#00AA00",
|
||||||
|
"3": "#00AAAA",
|
||||||
|
"4": "#AA0000",
|
||||||
|
"5": "#AA00AA",
|
||||||
|
"6": "#FFAA00",
|
||||||
|
"7": "#AAAAAA",
|
||||||
|
"8": "#555555",
|
||||||
|
"9": "#5555FF",
|
||||||
|
a: "#55FF55",
|
||||||
|
b: "#55FFFF",
|
||||||
|
c: "#FF5555",
|
||||||
|
d: "#FF55FF",
|
||||||
|
e: "#FFFF55",
|
||||||
|
f: "#FFFFFF",
|
||||||
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout title={event.event.name} wide={true}>
|
<PageLayout title={event.event.name} wide={true}>
|
||||||
@@ -54,6 +73,23 @@ const { Content } = await page.render();
|
|||||||
<article>
|
<article>
|
||||||
<Content />
|
<Content />
|
||||||
</article>
|
</article>
|
||||||
|
{
|
||||||
|
event.teams.length > 0 && (
|
||||||
|
<div class="py-2 border-t border-t-gray-600">
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Angemeldete Teams</h1>
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2">
|
||||||
|
{event.teams.map((team) => (
|
||||||
|
<div class="bg-neutral-800 p-2 rounded-md border border-neutral-700 border-l-4 flex flex-row items-center gap-2" style={{ borderLeftColor: colorMap[team.color] || "#FFFFFF" }}>
|
||||||
|
<span class="text-sm font-mono text-neutral-400 shrink-0 w-8 text-center">{team.kuerzel}</span>
|
||||||
|
<span class="font-bold truncate" title={team.name}>
|
||||||
|
{team.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
{
|
{
|
||||||
page.data.viewConfig && (
|
page.data.viewConfig && (
|
||||||
<div class="py-2 border-t border-t-gray-600">
|
<div class="py-2 border-t border-t-gray-600">
|
||||||
|
|||||||
@@ -2,35 +2,23 @@
|
|||||||
import type { ExtendedEvent } from "@components/types/event";
|
import type { ExtendedEvent } from "@components/types/event";
|
||||||
import PageLayout from "@layouts/PageLayout.astro";
|
import PageLayout from "@layouts/PageLayout.astro";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
|
import EventPage from "@components/event/EventPage.svelte";
|
||||||
|
|
||||||
const events = await Promise.all(
|
const events = await Promise.all(
|
||||||
(await getCollection("events")).map(async (event) => ({
|
(await getCollection("events")).map(async (event) => ({
|
||||||
...event,
|
...event,
|
||||||
data: {
|
data: {
|
||||||
...event.data,
|
...event.data,
|
||||||
event: (await fetch(import.meta.env.PUBLIC_API_SERVER + "/events/" + event.data.eventId).then((value) => value.json())) as ExtendedEvent,
|
event: (await fetch(
|
||||||
|
import.meta.env.PUBLIC_API_SERVER +
|
||||||
|
"/events/" +
|
||||||
|
event.data.eventId,
|
||||||
|
).then((value) => value.json())) as ExtendedEvent,
|
||||||
},
|
},
|
||||||
}))
|
})),
|
||||||
);
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout title="Events">
|
<PageLayout title="Events">
|
||||||
{
|
<EventPage client:load {events} />
|
||||||
events.map((event) => (
|
|
||||||
<article class="mb-8">
|
|
||||||
<h2 class="text-2xl font-bold mb-2">
|
|
||||||
<a href={`/events/${event.slug}/`} class="text-blue-600 hover:underline">
|
|
||||||
{event.data.event.event.name ?? "Hello, World!"}
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
<p class="text-gray-600 mb-1">
|
|
||||||
{new Date(event.data.event.event.start).toLocaleDateString(undefined, {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user