feat: Add Team info Sidebar
Some checks failed
SteamWarCI Build failed

This commit is contained in:
2025-12-20 18:36:33 +01:00
parent ff59ac3747
commit f13305d116
19 changed files with 400 additions and 239 deletions

View File

@@ -1,8 +1,6 @@
{ {
"$schema": "https://shadcn-svelte.com/schema.json", "$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": { "tailwind": {
"config": "tailwind.config.js",
"css": "src\\styles\\app.css", "css": "src\\styles\\app.css",
"baseColor": "slate" "baseColor": "slate"
}, },
@@ -10,8 +8,9 @@
"components": "$lib/components", "components": "$lib/components",
"utils": "$lib/components/utils", "utils": "$lib/components/utils",
"ui": "$lib/components/ui", "ui": "$lib/components/ui",
"hooks": "$lib/hooks" "hooks": "$lib/hooks",
"lib": "$lib"
}, },
"typescript": true, "typescript": true,
"registry": "https://next.shadcn-svelte.com/registry" "registry": "https://tw3.shadcn-svelte.com/registry/default"
} }

View File

@@ -1,12 +1,20 @@
<script lang="ts"> <script lang="ts">
import type { ExtendedEvent, EventFight, ResponseGroups, ResponseRelation } from "@type/event.ts"; import type {
ExtendedEvent,
EventFight,
ResponseGroups,
ResponseRelation,
} from "@type/event.ts";
import type { DoubleEleminationViewConfig } from "./types"; import type { DoubleEleminationViewConfig } from "./types";
import EventCard from "./EventCard.svelte"; import EventCard from "./EventCard.svelte";
import EventFightChip from "./EventFightChip.svelte"; import EventFightChip from "./EventFightChip.svelte";
import { onMount, onDestroy, tick } from "svelte"; import { onMount, onDestroy, tick } from "svelte";
import { fightConnector } from "./connections.svelte.ts"; import { fightConnector } from "./connections.svelte.ts";
const { event, config }: { event: ExtendedEvent; config: DoubleEleminationViewConfig } = $props(); const {
event,
config,
}: { event: ExtendedEvent; config: DoubleEleminationViewConfig } = $props();
const defaultGroup: ResponseGroups = { const defaultGroup: ResponseGroups = {
id: -1, id: -1,
@@ -18,7 +26,9 @@
points: null, points: null,
}; };
function indexRelations(ev: ExtendedEvent): Map<number, ResponseRelation[]> { function indexRelations(
ev: ExtendedEvent,
): Map<number, ResponseRelation[]> {
const map = new Map<number, ResponseRelation[]>(); const map = new Map<number, ResponseRelation[]>();
for (const rel of ev.relations) { for (const rel of ev.relations) {
const list = map.get(rel.fight) ?? []; const list = map.get(rel.fight) ?? [];
@@ -29,7 +39,9 @@
} }
const relationsByFight = indexRelations(event); const relationsByFight = indexRelations(event);
const fightMap = new Map<number, EventFight>(event.fights.map((f) => [f.id, f])); const fightMap = new Map<number, EventFight>(
event.fights.map((f) => [f.id, f]),
);
function collectBracket(startFinalId: number): EventFight[][] { function collectBracket(startFinalId: number): EventFight[][] {
const finalFight = fightMap.get(startFinalId); const finalFight = fightMap.get(startFinalId);
@@ -45,10 +57,15 @@
const rels = relationsByFight.get(fight.id) ?? []; const rels = relationsByFight.get(fight.id) ?? [];
for (const rel of rels) { for (const rel of rels) {
if (rel.type === "FIGHT" && rel.fromFight) { if (rel.type === "FIGHT" && rel.fromFight) {
const src = fightMap.get(rel.fromFight.id) ?? rel.fromFight; const src =
fightMap.get(rel.fromFight.id) ?? rel.fromFight;
if (!src) continue; if (!src) continue;
// Only traverse within the same bracket (group) to avoid cross-bracket pollution // Only traverse within the same bracket (group) to avoid cross-bracket pollution
if (bracketGroupId !== null && src.group?.id !== bracketGroupId) continue; if (
bracketGroupId !== null &&
src.group?.id !== bracketGroupId
)
continue;
if (!visited.has(src.id)) { if (!visited.has(src.id)) {
visited.add(src.id); visited.add(src.id);
next.push(src); next.push(src);
@@ -97,8 +114,12 @@
for (const rel of event.relations) { for (const rel of event.relations) {
if (rel.type !== "FIGHT" || !rel.fromFight) continue; if (rel.type !== "FIGHT" || !rel.fromFight) continue;
const fromId = rel.fromFight.id; const fromId = rel.fromFight.id;
const fromEl = document.getElementById(`fight-${fromId}`) as HTMLElement | null; const fromEl = document.getElementById(
const toEl = document.getElementById(`fight-${rel.fight}-team-${rel.team.toLowerCase()}`) as HTMLElement | null; `fight-${fromId}`,
) as HTMLElement | null;
const toEl = document.getElementById(
`fight-${rel.fight}-team-${rel.team.toLowerCase()}`,
) as HTMLElement | null;
if (!fromEl || !toEl) continue; if (!fromEl || !toEl) continue;
// Use team-signed offsets so BLUE goes left (negative), RED goes right (positive) // Use team-signed offsets so BLUE goes left (negative), RED goes right (positive)
const key = `${fromId}:${rel.team}`; const key = `${fromId}:${rel.team}`;
@@ -118,12 +139,18 @@
</script> </script>
{#if !grandFinal} {#if !grandFinal}
<p class="text-gray-400 italic">Konfiguration unvollständig (Grand Final fehlt).</p> <p class="text-gray-400 italic">
Konfiguration unvollständig (Grand Final fehlt).
</p>
{:else} {:else}
{#key winnersStages.length + ":" + losersStages.length} {#key winnersStages.length + ":" + losersStages.length}
<!-- Build a grid where rows: winners (stages), losers (stages), with losers offset by one stage/column --> <!-- Build a grid where rows: winners (stages), losers (stages), with losers offset by one stage/column -->
{@const totalColumns = Math.max(winnersStages.length, losersStages.length + 1) + 1} {@const totalColumns =
<div class="grid gap-x-16 gap-y-6 items-start" style={`grid-template-columns: repeat(${totalColumns}, max-content);`}> Math.max(winnersStages.length, losersStages.length + 1) + 1}
<div
class="grid gap-x-16 gap-y-6 items-start"
style={`grid-template-columns: repeat(${totalColumns}, max-content);`}
>
<!-- Winners heading spans all columns --> <!-- Winners heading spans all columns -->
<h2 class="font-bold text-center">Winners Bracket</h2> <h2 class="font-bold text-center">Winners Bracket</h2>
@@ -132,30 +159,50 @@
<div style={`grid-row: 2; grid-column: ${i + 1};`}> <div style={`grid-row: 2; grid-column: ${i + 1};`}>
<EventCard title={stageName(stage.length, true)}> <EventCard title={stageName(stage.length, true)}>
{#each stage as fight} {#each stage as fight}
<EventFightChip {fight} group={fight.group ?? defaultGroup} /> <EventFightChip
{event}
{fight}
group={fight.group ?? defaultGroup}
/>
{/each} {/each}
</EventCard> </EventCard>
</div> </div>
{/each} {/each}
<!-- Place Grand Final at the far right, aligned with winners row --> <!-- Place Grand Final at the far right, aligned with winners row -->
<div style={`grid-row: 2; grid-column: ${totalColumns};`} class="self-center"> <div
style={`grid-row: 2; grid-column: ${totalColumns};`}
class="self-center"
>
<EventCard title="Grand Final"> <EventCard title="Grand Final">
{#if grandFinal} {#if grandFinal}
<EventFightChip fight={grandFinal} group={grandFinal.group ?? defaultGroup} /> <EventFightChip
{event}
fight={grandFinal}
group={grandFinal.group ?? defaultGroup}
/>
{/if} {/if}
</EventCard> </EventCard>
</div> </div>
<!-- Losers heading spans all columns --> <!-- Losers heading spans all columns -->
<h2 class="font-bold text-center" style="grid-row: 3; grid-column: 1 / {totalColumns};">Losers Bracket</h2> <h2
class="font-bold text-center"
style="grid-row: 3; grid-column: 1 / {totalColumns};"
>
Losers Bracket
</h2>
<!-- Losers stages in row 4, offset by one column to the right --> <!-- Losers stages in row 4, offset by one column to the right -->
{#each losersStages as stage, j} {#each losersStages as stage, j}
<div style={`grid-row: 4; grid-column: ${j + 2};`} class="mt-2"> <div style={`grid-row: 4; grid-column: ${j + 2};`} class="mt-2">
<EventCard title={stageName(stage.length, false)}> <EventCard title={stageName(stage.length, false)}>
{#each stage as fight} {#each stage as fight}
<EventFightChip {fight} group={fight.group ?? defaultGroup} /> <EventFightChip
{event}
{fight}
group={fight.group ?? defaultGroup}
/>
{/each} {/each}
</EventCard> </EventCard>
</div> </div>

View File

@@ -1,12 +1,20 @@
<script lang="ts"> <script lang="ts">
import type { ExtendedEvent, EventFight, ResponseGroups, ResponseRelation } from "@type/event.ts"; import type {
ExtendedEvent,
EventFight,
ResponseGroups,
ResponseRelation,
} from "@type/event.ts";
import type { EleminationViewConfig } from "./types"; import type { EleminationViewConfig } from "./types";
import EventCard from "./EventCard.svelte"; import EventCard from "./EventCard.svelte";
import EventFightChip from "./EventFightChip.svelte"; import EventFightChip from "./EventFightChip.svelte";
import { onMount, onDestroy, tick } from "svelte"; import { onMount, onDestroy, tick } from "svelte";
import { FightConnector, fightConnector } from "./connections.svelte.ts"; import { FightConnector, fightConnector } from "./connections.svelte.ts";
const { event, config }: { event: ExtendedEvent; config: EleminationViewConfig } = $props(); const {
event,
config,
}: { event: ExtendedEvent; config: EleminationViewConfig } = $props();
const defaultGroup: ResponseGroups = { const defaultGroup: ResponseGroups = {
id: -1, id: -1,
@@ -18,8 +26,13 @@
points: null, points: null,
}; };
function buildStages(ev: ExtendedEvent, finalFightId: number): EventFight[][] { function buildStages(
const fightMap = new Map<number, EventFight>(ev.fights.map((f) => [f.id, f])); ev: ExtendedEvent,
finalFightId: number,
): EventFight[][] {
const fightMap = new Map<number, EventFight>(
ev.fights.map((f) => [f.id, f]),
);
const relationsByFight = new Map<number, ResponseRelation[]>(); const relationsByFight = new Map<number, ResponseRelation[]>();
for (const rel of ev.relations) { for (const rel of ev.relations) {
const list = relationsByFight.get(rel.fight) ?? []; const list = relationsByFight.get(rel.fight) ?? [];
@@ -41,7 +54,8 @@
const rels = relationsByFight.get(fight.id) ?? []; const rels = relationsByFight.get(fight.id) ?? [];
for (const rel of rels) { for (const rel of rels) {
if (rel.type === "FIGHT" && rel.fromFight) { if (rel.type === "FIGHT" && rel.fromFight) {
const src = fightMap.get(rel.fromFight.id) ?? rel.fromFight; const src =
fightMap.get(rel.fromFight.id) ?? rel.fromFight;
if (src && !visited.has(src.id)) { if (src && !visited.has(src.id)) {
visited.add(src.id); visited.add(src.id);
nextLayer.push(src); nextLayer.push(src);
@@ -89,8 +103,12 @@
for (const rel of event.relations) { for (const rel of event.relations) {
if (rel.type !== "FIGHT" || !rel.fromFight) continue; if (rel.type !== "FIGHT" || !rel.fromFight) continue;
const fromEl = document.getElementById(`fight-${rel.fromFight.id}`) as HTMLElement | null; const fromEl = document.getElementById(
const toEl = document.getElementById(`fight-${rel.fight}-team-${rel.team.toLowerCase()}`) as HTMLElement | null; `fight-${rel.fromFight.id}`,
) as HTMLElement | null;
const toEl = document.getElementById(
`fight-${rel.fight}-team-${rel.team.toLowerCase()}`,
) as HTMLElement | null;
if (fromEl && toEl) { if (fromEl && toEl) {
connector.addConnection(fromEl, toEl, "#9ca3af"); connector.addConnection(fromEl, toEl, "#9ca3af");
} }
@@ -111,7 +129,11 @@
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
<EventCard title={stageName(index, stage)}> <EventCard title={stageName(index, stage)}>
{#each stage as fight} {#each stage as fight}
<EventFightChip {fight} group={fight.group ?? defaultGroup} /> <EventFightChip
{event}
{fight}
group={fight.group ?? defaultGroup}
/>
{/each} {/each}
</EventCard> </EventCard>
</div> </div>

View File

@@ -1,15 +1,18 @@
<script lang="ts"> <script lang="ts">
import type { EventFight, ResponseGroups } from "@components/types/event"; import type { EventFight, ExtendedEvent, ResponseGroups } from "@components/types/event";
import EventCardOutline from "./EventCardOutline.svelte"; import EventCardOutline from "./EventCardOutline.svelte";
import EventTeamChip from "./EventTeamChip.svelte"; import EventTeamChip from "./EventTeamChip.svelte";
import { fightConnector } from "./connections.svelte.ts";
let { let {
fight, fight,
group, group,
event,
disabled = false,
}: { }: {
fight: EventFight; fight: EventFight;
group: ResponseGroups; group: ResponseGroups;
event: ExtendedEvent;
disabled?: boolean;
} = $props(); } = $props();
function getScore(group: ResponseGroups, fight: EventFight, blueTeam: boolean): string { function getScore(group: ResponseGroups, fight: EventFight, blueTeam: boolean): string {
@@ -29,14 +32,36 @@
<EventTeamChip <EventTeamChip
team={{ team={{
id: -1, id: -1,
kuerzel: new Date(fight.start).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }), kuerzel: new Date(fight.start).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
}),
name: new Date(fight.start).toLocaleDateString([]), name: new Date(fight.start).toLocaleDateString([]),
color: "-1", color: "-1",
}} }}
time={true} time={true}
{event}
/> />
<div id={"fight-" + fight.id}> <div id={"fight-" + fight.id}>
<EventTeamChip team={fight.blueTeam} score={getScore(group, fight, true)} showWinner={true} isWinner={fight.ergebnis === 1} noWinner={fight.ergebnis === 0} id="fight-{fight.id}-team-blue" /> <EventTeamChip
<EventTeamChip team={fight.redTeam} score={getScore(group, fight, false)} showWinner={true} isWinner={fight.ergebnis === 2} noWinner={fight.ergebnis === 0} id="fight-{fight.id}-team-red" /> {event}
{disabled}
team={fight.blueTeam}
score={getScore(group, fight, true)}
showWinner={true}
isWinner={fight.ergebnis === 1}
noWinner={fight.ergebnis === 0}
id="fight-{fight.id}-team-blue"
/>
<EventTeamChip
{event}
{disabled}
team={fight.redTeam}
score={getScore(group, fight, false)}
showWinner={true}
isWinner={fight.ergebnis === 2}
noWinner={fight.ergebnis === 0}
id="fight-{fight.id}-team-red"
/>
</div> </div>
</EventCardOutline> </EventCardOutline>

View File

@@ -1,48 +1,70 @@
<script lang="ts"> <script lang="ts">
import type { Team } from "@type/team.ts"; import type { Team } from "@type/team.ts";
import { fightConnector } from "./connections.svelte";
import { teamHoverService } from "./team-hover.svelte"; import { teamHoverService } from "./team-hover.svelte";
import { Sheet, SheetContent, SheetTrigger } from "@components/ui/sheet";
import TeamInfo from "./TeamInfo.svelte";
import type { ExtendedEvent } from "@components/types/event";
const { const {
team, team,
event,
score = "", score = "",
time = false, time = false,
showWinner = false, showWinner = false,
isWinner = false, isWinner = false,
noWinner = false, noWinner = false,
id, id,
disabled = false,
}: { }: {
team: Team; team: Team;
event: ExtendedEvent;
score?: string; score?: string;
time?: boolean; time?: boolean;
showWinner?: boolean; showWinner?: boolean;
isWinner?: boolean; isWinner?: boolean;
noWinner?: boolean; noWinner?: boolean;
id?: string; id?: string;
disabled?: boolean;
} = $props(); } = $props();
let hoverService = $teamHoverService; let hoverService = $teamHoverService;
type StringAnyRecord = Record<string, any>;
</script> </script>
{#if !disabled}
<Sheet>
<SheetTrigger>
{#snippet child({ props })}
{@render teamButton({ props })}
{/snippet}
</SheetTrigger>
<SheetContent>
<TeamInfo {team} {event} />
</SheetContent>
</Sheet>
{:else}
{@render teamButton({ props: {} })}
{/if}
{#snippet teamButton({ props }: { props: StringAnyRecord })}
<button <button
class="flex justify-between px-2 w-full team-chip text-left {time ? 'py-1 hover:bg-gray-800' : 'py-3 cursor-pointer'} team-{team.id} {hoverService.currentHover === team.id {...props}
? 'bg-gray-800' class="flex justify-between px-2 w-full team-chip text-left border-b border-b-gray-700 last:border-b-0 {time ? 'py-1 hover:bg-gray-800' : 'py-3 cursor-pointer'} team-{disabled
: ''} {showWinner ? 'border-l-4' : ''} {showWinner && isWinner ? 'border-l-yellow-500' : 'border-l-gray-950'}" ? -1
: team.id} {hoverService.currentHover === team.id ? 'bg-gray-800' : ''} {showWinner ? 'border-l-4' : ''} {showWinner && isWinner ? 'border-l-yellow-500' : 'border-l-gray-950'}"
onmouseenter={() => team.id === -1 || hoverService.setHover(team.id)} onmouseenter={() => team.id === -1 || hoverService.setHover(team.id)}
onmouseleave={() => team.id === -1 || hoverService.clearHover()} onmouseleave={() => team.id === -1 || hoverService.clearHover()}
{id} {id}
> >
<div class="flex"> <div class="flex">
<div class="w-12 {time ? 'font-bold' : ''}">{team.kuerzel}</div> <div class="w-12 {time ? 'font-bold' : ''}">
{team.kuerzel}
</div>
<span class={time ? "font-mono" : "font-bold"}>{team.name}</span> <span class={time ? "font-mono" : "font-bold"}>{team.name}</span>
</div> </div>
<div class="{showWinner && isWinner && 'font-bold'} {isWinner ? 'text-yellow-400' : ''} {noWinner ? 'font-bold' : ''}"> <div class="{showWinner && isWinner && 'font-bold'} {isWinner ? 'text-yellow-400' : ''} {noWinner ? 'font-bold' : ''}">
{score} {score}
</div> </div>
</button> </button>
{/snippet}
<style>
.team-chip:not(:last-child) {
@apply border-b border-b-gray-700;
}
</style>

View File

@@ -52,7 +52,7 @@
{#each Object.entries(group.points ?? {}).sort((v1, v2) => v2[1] - v1[1]) as points} {#each Object.entries(group.points ?? {}).sort((v1, v2) => v2[1] - v1[1]) as points}
{@const [teamId, point] = points} {@const [teamId, point] = points}
{@const team = event.teams.find((t) => t.id.toString() === teamId)!!} {@const team = event.teams.find((t) => t.id.toString() === teamId)!!}
<EventTeamChip {team} score={point.toString()} /> <EventTeamChip {team} {event} score={point.toString()} />
{/each} {/each}
</EventCardOutline> </EventCardOutline>
</EventCard> </EventCard>
@@ -61,7 +61,7 @@
<div> <div>
<EventCard title="Runde {index + 1}"> <EventCard title="Runde {index + 1}">
{#each round as fight} {#each round as fight}
<EventFightChip {fight} {group} /> <EventFightChip {event} {fight} {group} />
{/each} {/each}
</EventCard> </EventCard>
</div> </div>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
import { dataRepo } from "@components/repo/data";
import type { ExtendedEvent, ResponseTeam } from "@components/types/event";
import EventFightChip from "./EventFightChip.svelte";
import SheetHeader from "@components/ui/sheet/sheet-header.svelte";
import { SheetDescription, SheetTitle } from "@components/ui/sheet";
const { event, team }: { event: ExtendedEvent; team: ResponseTeam } = $props();
let members = $derived.by(() => {
return fetchMembers(team.id);
});
let recentFights = $derived.by(() => {
return event.fights
.filter((f) => f.hasFinished && (f.blueTeam.id === team.id || f.redTeam.id === team.id))
.sort((a, b) => b.start - a.start)
.slice(0, 5);
});
async function fetchMembers(teamId: number) {
return await $dataRepo.queryPlayers(undefined, undefined, [teamId], 50, 0, false, false);
}
</script>
<SheetHeader>
<SheetTitle
>{team.name}
<span class="text-sm text-gray-400">{team.kuerzel}</span></SheetTitle
>
<SheetDescription>Statistiken des Teams</SheetDescription>
</SheetHeader>
<div class="mt-8 space-y-8">
<section>
<h3 class="text-lg font-semibold mb-4 border-b border-slate-800 pb-2 text-blue-400">Teammitglieder</h3>
{#await members}
<p class="text-slate-500 italic text-sm">Lade Mitglieder...</p>
{:then member}
<div class="grid grid-cols-2 gap-2">
{#each member.entries as p (p.uuid)}
<div class="bg-slate-800/50 p-2 rounded border border-slate-700 flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-slate-700 flex items-center justify-center text-[10px]">
{p.name.charAt(0)}
</div>
<span class="truncate text-sm">{p.name}</span>
</div>
{/each}
</div>
{/await}
</section>
<section>
<h3 class="text-lg font-semibold mb-4 border-b border-slate-800 pb-2 text-green-400">Letzte 5 Kämpfe</h3>
{#if recentFights.length > 0}
<div class="space-y-3">
{#each recentFights as fight}
<div class="scale-90 origin-left">
<EventFightChip
{event}
disabled={true}
{fight}
group={fight.group ?? {
id: -1,
name: "Event",
pointsPerWin: 0,
pointsPerLoss: 0,
pointsPerDraw: 0,
type: "GROUP_STAGE",
points: null,
}}
/>
</div>
{/each}
</div>
{:else}
<p class="text-slate-500 italic text-sm">Keine beendeten Kämpfe in diesem Event.</p>
{/if}
</section>
</div>

View File

@@ -38,17 +38,27 @@
<div class="py-2 border-t border-t-gray-600"> <div class="py-2 border-t border-t-gray-600">
<h1 class="text-2xl font-bold mb-4">Angemeldete Teams</h1> <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"> <div
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2"
>
{#each teams as team} {#each teams as 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="border-left-color: {colorMap[team.color] || '#FFFFFF'}"> <button
<span class="text-sm font-mono text-neutral-400 shrink-0 w-8 text-center">{team.kuerzel}</span> class="bg-neutral-800 p-2 rounded-md border border-neutral-700 border-l-4 flex flex-row items-center gap-2 cursor-pointer hover:bg-neutral-700 transition-colors w-full text-left"
style="border-left-color: {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}> <span class="font-bold truncate" title={team.name}>
{team.name} {team.name}
</span> </span>
</div> </button>
{/each} {/each}
{#if teams.length === 0} {#if teams.length === 0}
<p class="col-span-full text-center text-neutral-400">Keine Teams angemeldet.</p> <p class="col-span-full text-center text-neutral-400">
Keine Teams angemeldet.
</p>
{/if} {/if}
</div> </div>
</div> </div>

View File

@@ -5,14 +5,16 @@ class TeamHoverService {
public currentHover = $state<number | undefined>(undefined); public currentHover = $state<number | undefined>(undefined);
private fightConnector = get(fightConnector); private fightConnector = get(fightConnector);
public disableConnections = $state(false);
setHover(teamId: number): void { setHover(teamId: number): void {
this.currentHover = teamId; this.currentHover = teamId;
this.fightConnector.addTeamConnection(teamId); if (!this.disableConnections) this.fightConnector.addTeamConnection(teamId);
} }
clearHover(): void { clearHover(): void {
this.currentHover = undefined; this.currentHover = undefined;
this.fightConnector.clearConnections(); if (!this.disableConnections) this.fightConnector.clearConnections();
} }
} }

View File

@@ -93,7 +93,7 @@ export const ExtendedEventSchema = z.object({
teams: z.array(TeamSchema), teams: z.array(TeamSchema),
groups: z.array(ResponseGroupsSchema), groups: z.array(ResponseGroupsSchema),
fights: z.array(EventFightSchema), fights: z.array(EventFightSchema),
referees: z.array(ResponseUserSchema), referees: z.array(PlayerSchema),
relations: z.array(ResponseRelationSchema), relations: z.array(ResponseRelationSchema),
}); });

View File

@@ -1,7 +1,4 @@
import { Dialog as SheetPrimitive } from "bits-ui"; import { Dialog as SheetPrimitive } from "bits-ui";
import { type VariantProps, tv } from "tailwind-variants";
import Portal from "./sheet-portal.svelte";
import Overlay from "./sheet-overlay.svelte"; import Overlay from "./sheet-overlay.svelte";
import Content from "./sheet-content.svelte"; import Content from "./sheet-content.svelte";
import Header from "./sheet-header.svelte"; import Header from "./sheet-header.svelte";
@@ -12,6 +9,7 @@ import Description from "./sheet-description.svelte";
const Root = SheetPrimitive.Root; const Root = SheetPrimitive.Root;
const Close = SheetPrimitive.Close; const Close = SheetPrimitive.Close;
const Trigger = SheetPrimitive.Trigger; const Trigger = SheetPrimitive.Trigger;
const Portal = SheetPrimitive.Portal;
export { export {
Root, Root,
@@ -36,71 +34,3 @@ export {
Title as SheetTitle, Title as SheetTitle,
Description as SheetDescription, Description as SheetDescription,
}; };
export const sheetVariants = tv({
base: "bg-background fixed z-50 gap-4 p-6 shadow-lg",
variants: {
side: {
top: "inset-x-0 top-0 border-b",
bottom: "inset-x-0 bottom-0 border-t",
left: "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
right: "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
});
export const sheetTransitions = {
top: {
in: {
y: "-100%",
duration: 500,
opacity: 1,
},
out: {
y: "-100%",
duration: 300,
opacity: 1,
},
},
bottom: {
in: {
y: "100%",
duration: 500,
opacity: 1,
},
out: {
y: "100%",
duration: 300,
opacity: 1,
},
},
left: {
in: {
x: "-100%",
duration: 500,
opacity: 1,
},
out: {
x: "-100%",
duration: 300,
opacity: 1,
},
},
right: {
in: {
x: "100%",
duration: 500,
opacity: 1,
},
out: {
x: "100%",
duration: 300,
opacity: 1,
},
},
};
export type Side = VariantProps<typeof sheetVariants>["side"];

View File

@@ -1,47 +1,53 @@
<script lang="ts"> <script lang="ts" module>
import { Dialog as SheetPrimitive } from "bits-ui"; import { tv, type VariantProps } from "tailwind-variants";
import X from "lucide-svelte/icons/x"; export const sheetVariants = tv({
import { fly } from "svelte/transition"; base: "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 gap-4 p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
import { variants: {
SheetOverlay, side: {
SheetPortal, top: "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b",
type Side, bottom: "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t",
sheetTransitions, left: "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
sheetVariants, right: "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
} from "./index.js"; },
import { cn } from "$lib/components/utils.js"; },
defaultVariants: {
side: "right",
},
});
type $$Props = SheetPrimitive.ContentProps & { export type Side = VariantProps<typeof sheetVariants>["side"];
side?: Side;
};
let className: $$Props["class"] = undefined;
export let side: $$Props["side"] = "right";
export { className as class };
export let inTransition: $$Props["inTransition"] = fly;
export let inTransitionConfig: $$Props["inTransitionConfig"] =
sheetTransitions[side ?? "right"].in;
export let outTransition: $$Props["outTransition"] = fly;
export let outTransitionConfig: $$Props["outTransitionConfig"] =
sheetTransitions[side ?? "right"].out;
</script> </script>
<SheetPortal> <script lang="ts">
import { Dialog as SheetPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import X from "@lucide/svelte/icons/x";
import type { Snippet } from "svelte";
import SheetOverlay from "./sheet-overlay.svelte";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
side = "right",
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
portalProps?: SheetPrimitive.PortalProps;
side?: Side;
children: Snippet;
} = $props();
</script>
<SheetPrimitive.Portal {...portalProps}>
<SheetOverlay /> <SheetOverlay />
<SheetPrimitive.Content <SheetPrimitive.Content bind:ref class={cn(sheetVariants({ side }), className)} {...restProps}>
{inTransition} {@render children?.()}
{inTransitionConfig}
{outTransition}
{outTransitionConfig}
class={cn(sheetVariants({ side }), className)}
{...$$restProps}
>
<slot />
<SheetPrimitive.Close <SheetPrimitive.Close
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none" class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
> >
<X class="h-4 w-4" /> <X class="size-4" />
<span class="sr-only">Close</span> <span class="sr-only">Close</span>
</SheetPrimitive.Close> </SheetPrimitive.Close>
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPrimitive.Portal>

View File

@@ -2,12 +2,15 @@
import { Dialog as SheetPrimitive } from "bits-ui"; import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = SheetPrimitive.DescriptionProps; let {
ref = $bindable(null),
let className: $$Props["class"] = undefined; class: className,
export { className as class }; ...restProps
}: SheetPrimitive.DescriptionProps = $props();
</script> </script>
<SheetPrimitive.Description class={cn("text-muted-foreground text-sm", className)} {...$$restProps}> <SheetPrimitive.Description
<slot /> bind:ref
</SheetPrimitive.Description> class={cn("text-muted-foreground text-sm", className)}
{...restProps}
/>

View File

@@ -1,16 +1,20 @@
<script lang="ts"> <script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>; let {
ref = $bindable(null),
let className: $$Props["class"] = undefined; class: className,
export { className as class }; children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script> </script>
<div <div
bind:this={ref}
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...$$restProps} {...restProps}
> >
<slot /> {@render children?.()}
</div> </div>

View File

@@ -1,13 +1,20 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>; let {
ref = $bindable(null),
let className: $$Props["class"] = undefined; class: className,
export { className as class }; children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script> </script>
<div class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...$$restProps}> <div
<slot /> bind:this={ref}
class={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div> </div>

View File

@@ -1,21 +1,21 @@
<script lang="ts"> <script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui"; import { Dialog as SheetPrimitive } from "bits-ui";
import { fade } from "svelte/transition";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = SheetPrimitive.OverlayProps; let {
ref = $bindable(null),
class: className,
...restProps
}: SheetPrimitive.OverlayProps = $props();
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = fade;
export let transitionConfig: $$Props["transitionConfig"] = {
duration: 150,
};
export { className as class }; export { className as class };
</script> </script>
<SheetPrimitive.Overlay <SheetPrimitive.Overlay
{transition} bind:ref
{transitionConfig} class={cn(
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ", className)} "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
{...$$restProps} className
)}
{...restProps}
/> />

View File

@@ -1,13 +0,0 @@
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
type $$Props = SheetPrimitive.PortalProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<SheetPrimitive.Portal class={cn(className)} {...$$restProps}>
<slot />
</SheetPrimitive.Portal>

View File

@@ -2,15 +2,15 @@
import { Dialog as SheetPrimitive } from "bits-ui"; import { Dialog as SheetPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = SheetPrimitive.TitleProps; let {
ref = $bindable(null),
let className: $$Props["class"] = undefined; class: className,
export { className as class }; ...restProps
}: SheetPrimitive.TitleProps = $props();
</script> </script>
<SheetPrimitive.Title <SheetPrimitive.Title
bind:ref
class={cn("text-foreground text-lg font-semibold", className)} class={cn("text-foreground text-lg font-semibold", className)}
{...$$restProps} {...restProps}
> />
<slot />
</SheetPrimitive.Title>

View File

@@ -9,9 +9,13 @@ import TeamList from "@components/event/TeamList.svelte";
export const getStaticPaths = createGetStaticPaths(async () => { export const getStaticPaths = createGetStaticPaths(async () => {
const events = await Promise.all( const events = await Promise.all(
(await getCollection("events")).map(async (event) => ({ (await getCollection("events")).map(async (event) => ({
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,
page: event, page: event,
})) })),
); );
return events.map((event) => ({ return events.map((event) => ({
@@ -25,7 +29,10 @@ 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();
--- ---
@@ -35,19 +42,26 @@ const { Content } = await page.render();
<h1 class="text-2xl font-bold">{event.event.name}</h1> <h1 class="text-2xl font-bold">{event.event.name}</h1>
<h2 class="text-md text-gray-300 mb-4"> <h2 class="text-md text-gray-300 mb-4">
{ {
new Date(event.event.start).toLocaleDateString(astroI18n.locale, { new Date(event.event.start).toLocaleDateString(
astroI18n.locale,
{
year: "numeric", year: "numeric",
month: "numeric", month: "numeric",
day: "numeric", day: "numeric",
}) },
)
} }
{ {
new Date(event.event.start).toDateString() !== new Date(event.event.end).toDateString() new Date(event.event.start).toDateString() !==
? ` - ${new Date(event.event.end).toLocaleDateString(astroI18n.locale, { new Date(event.event.end).toDateString()
? ` - ${new Date(event.event.end).toLocaleDateString(
astroI18n.locale,
{
year: "numeric", year: "numeric",
month: "numeric", month: "numeric",
day: "numeric", day: "numeric",
})}` },
)}`
: "" : ""
} }
</h2> </h2>
@@ -60,7 +74,11 @@ const { Content } = await page.render();
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">
<h1 class="text-2xl font-bold mb-4">Kampfplan</h1> <h1 class="text-2xl font-bold mb-4">Kampfplan</h1>
<EventFights viewConfig={page.data.viewConfig} event={event} client:load /> <EventFights
viewConfig={page.data.viewConfig}
event={event}
client:load
/>
</div> </div>
) )
} }