@@ -1,8 +1,6 @@
|
||||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src\\styles\\app.css",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
@@ -10,8 +8,9 @@
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/components/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks"
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://next.shadcn-svelte.com/registry"
|
||||
"registry": "https://tw3.shadcn-svelte.com/registry/default"
|
||||
}
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<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 EventCard from "./EventCard.svelte";
|
||||
import EventFightChip from "./EventFightChip.svelte";
|
||||
import { onMount, onDestroy, tick } from "svelte";
|
||||
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 = {
|
||||
id: -1,
|
||||
@@ -18,7 +26,9 @@
|
||||
points: null,
|
||||
};
|
||||
|
||||
function indexRelations(ev: ExtendedEvent): Map<number, ResponseRelation[]> {
|
||||
function indexRelations(
|
||||
ev: ExtendedEvent,
|
||||
): Map<number, ResponseRelation[]> {
|
||||
const map = new Map<number, ResponseRelation[]>();
|
||||
for (const rel of ev.relations) {
|
||||
const list = map.get(rel.fight) ?? [];
|
||||
@@ -29,7 +39,9 @@
|
||||
}
|
||||
|
||||
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[][] {
|
||||
const finalFight = fightMap.get(startFinalId);
|
||||
@@ -45,10 +57,15 @@
|
||||
const rels = relationsByFight.get(fight.id) ?? [];
|
||||
for (const rel of rels) {
|
||||
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;
|
||||
// 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)) {
|
||||
visited.add(src.id);
|
||||
next.push(src);
|
||||
@@ -97,8 +114,12 @@
|
||||
for (const rel of event.relations) {
|
||||
if (rel.type !== "FIGHT" || !rel.fromFight) continue;
|
||||
const fromId = rel.fromFight.id;
|
||||
const fromEl = document.getElementById(`fight-${fromId}`) as HTMLElement | null;
|
||||
const toEl = document.getElementById(`fight-${rel.fight}-team-${rel.team.toLowerCase()}`) as HTMLElement | null;
|
||||
const fromEl = document.getElementById(
|
||||
`fight-${fromId}`,
|
||||
) as HTMLElement | null;
|
||||
const toEl = document.getElementById(
|
||||
`fight-${rel.fight}-team-${rel.team.toLowerCase()}`,
|
||||
) as HTMLElement | null;
|
||||
if (!fromEl || !toEl) continue;
|
||||
// Use team-signed offsets so BLUE goes left (negative), RED goes right (positive)
|
||||
const key = `${fromId}:${rel.team}`;
|
||||
@@ -118,12 +139,18 @@
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
{#key winnersStages.length + ":" + losersStages.length}
|
||||
<!-- 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}
|
||||
<div class="grid gap-x-16 gap-y-6 items-start" style={`grid-template-columns: repeat(${totalColumns}, max-content);`}>
|
||||
{@const totalColumns =
|
||||
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 -->
|
||||
<h2 class="font-bold text-center">Winners Bracket</h2>
|
||||
|
||||
@@ -132,30 +159,50 @@
|
||||
<div style={`grid-row: 2; grid-column: ${i + 1};`}>
|
||||
<EventCard title={stageName(stage.length, true)}>
|
||||
{#each stage as fight}
|
||||
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
|
||||
<EventFightChip
|
||||
{event}
|
||||
{fight}
|
||||
group={fight.group ?? defaultGroup}
|
||||
/>
|
||||
{/each}
|
||||
</EventCard>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- 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">
|
||||
{#if grandFinal}
|
||||
<EventFightChip fight={grandFinal} group={grandFinal.group ?? defaultGroup} />
|
||||
<EventFightChip
|
||||
{event}
|
||||
fight={grandFinal}
|
||||
group={grandFinal.group ?? defaultGroup}
|
||||
/>
|
||||
{/if}
|
||||
</EventCard>
|
||||
</div>
|
||||
|
||||
<!-- 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 -->
|
||||
{#each losersStages as stage, j}
|
||||
<div style={`grid-row: 4; grid-column: ${j + 2};`} class="mt-2">
|
||||
<EventCard title={stageName(stage.length, false)}>
|
||||
{#each stage as fight}
|
||||
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
|
||||
<EventFightChip
|
||||
{event}
|
||||
{fight}
|
||||
group={fight.group ?? defaultGroup}
|
||||
/>
|
||||
{/each}
|
||||
</EventCard>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<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 EventCard from "./EventCard.svelte";
|
||||
import EventFightChip from "./EventFightChip.svelte";
|
||||
import { onMount, onDestroy, tick } from "svelte";
|
||||
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 = {
|
||||
id: -1,
|
||||
@@ -18,8 +26,13 @@
|
||||
points: null,
|
||||
};
|
||||
|
||||
function buildStages(ev: ExtendedEvent, finalFightId: number): EventFight[][] {
|
||||
const fightMap = new Map<number, EventFight>(ev.fights.map((f) => [f.id, f]));
|
||||
function buildStages(
|
||||
ev: ExtendedEvent,
|
||||
finalFightId: number,
|
||||
): EventFight[][] {
|
||||
const fightMap = new Map<number, EventFight>(
|
||||
ev.fights.map((f) => [f.id, f]),
|
||||
);
|
||||
const relationsByFight = new Map<number, ResponseRelation[]>();
|
||||
for (const rel of ev.relations) {
|
||||
const list = relationsByFight.get(rel.fight) ?? [];
|
||||
@@ -41,7 +54,8 @@
|
||||
const rels = relationsByFight.get(fight.id) ?? [];
|
||||
for (const rel of rels) {
|
||||
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)) {
|
||||
visited.add(src.id);
|
||||
nextLayer.push(src);
|
||||
@@ -89,8 +103,12 @@
|
||||
|
||||
for (const rel of event.relations) {
|
||||
if (rel.type !== "FIGHT" || !rel.fromFight) continue;
|
||||
const fromEl = document.getElementById(`fight-${rel.fromFight.id}`) as HTMLElement | null;
|
||||
const toEl = document.getElementById(`fight-${rel.fight}-team-${rel.team.toLowerCase()}`) as HTMLElement | null;
|
||||
const fromEl = document.getElementById(
|
||||
`fight-${rel.fromFight.id}`,
|
||||
) as HTMLElement | null;
|
||||
const toEl = document.getElementById(
|
||||
`fight-${rel.fight}-team-${rel.team.toLowerCase()}`,
|
||||
) as HTMLElement | null;
|
||||
if (fromEl && toEl) {
|
||||
connector.addConnection(fromEl, toEl, "#9ca3af");
|
||||
}
|
||||
@@ -111,7 +129,11 @@
|
||||
<div class="flex flex-col justify-center">
|
||||
<EventCard title={stageName(index, stage)}>
|
||||
{#each stage as fight}
|
||||
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
|
||||
<EventFightChip
|
||||
{event}
|
||||
{fight}
|
||||
group={fight.group ?? defaultGroup}
|
||||
/>
|
||||
{/each}
|
||||
</EventCard>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<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 EventTeamChip from "./EventTeamChip.svelte";
|
||||
import { fightConnector } from "./connections.svelte.ts";
|
||||
|
||||
let {
|
||||
fight,
|
||||
group,
|
||||
event,
|
||||
disabled = false,
|
||||
}: {
|
||||
fight: EventFight;
|
||||
group: ResponseGroups;
|
||||
event: ExtendedEvent;
|
||||
disabled?: boolean;
|
||||
} = $props();
|
||||
|
||||
function getScore(group: ResponseGroups, fight: EventFight, blueTeam: boolean): string {
|
||||
@@ -29,14 +32,36 @@
|
||||
<EventTeamChip
|
||||
team={{
|
||||
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([]),
|
||||
color: "-1",
|
||||
}}
|
||||
time={true}
|
||||
{event}
|
||||
/>
|
||||
<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 team={fight.redTeam} score={getScore(group, fight, false)} showWinner={true} isWinner={fight.ergebnis === 2} noWinner={fight.ergebnis === 0} id="fight-{fight.id}-team-red" />
|
||||
<EventTeamChip
|
||||
{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>
|
||||
</EventCardOutline>
|
||||
|
||||
@@ -1,48 +1,70 @@
|
||||
<script lang="ts">
|
||||
import type { Team } from "@type/team.ts";
|
||||
import { fightConnector } from "./connections.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 {
|
||||
team,
|
||||
event,
|
||||
score = "",
|
||||
time = false,
|
||||
showWinner = false,
|
||||
isWinner = false,
|
||||
noWinner = false,
|
||||
id,
|
||||
disabled = false,
|
||||
}: {
|
||||
team: Team;
|
||||
event: ExtendedEvent;
|
||||
score?: string;
|
||||
time?: boolean;
|
||||
showWinner?: boolean;
|
||||
isWinner?: boolean;
|
||||
noWinner?: boolean;
|
||||
id?: string;
|
||||
disabled?: boolean;
|
||||
} = $props();
|
||||
|
||||
let hoverService = $teamHoverService;
|
||||
|
||||
type StringAnyRecord = Record<string, any>;
|
||||
</script>
|
||||
|
||||
<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
|
||||
? 'bg-gray-800'
|
||||
: ''} {showWinner ? 'border-l-4' : ''} {showWinner && isWinner ? 'border-l-yellow-500' : 'border-l-gray-950'}"
|
||||
{#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
|
||||
{...props}
|
||||
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
|
||||
? -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)}
|
||||
onmouseleave={() => team.id === -1 || hoverService.clearHover()}
|
||||
{id}
|
||||
>
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
<div class="{showWinner && isWinner && 'font-bold'} {isWinner ? 'text-yellow-400' : ''} {noWinner ? 'font-bold' : ''}">
|
||||
{score}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.team-chip:not(:last-child) {
|
||||
@apply border-b border-b-gray-700;
|
||||
}
|
||||
</style>
|
||||
</button>
|
||||
{/snippet}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
{#each Object.entries(group.points ?? {}).sort((v1, v2) => v2[1] - v1[1]) as points}
|
||||
{@const [teamId, point] = points}
|
||||
{@const team = event.teams.find((t) => t.id.toString() === teamId)!!}
|
||||
<EventTeamChip {team} score={point.toString()} />
|
||||
<EventTeamChip {team} {event} score={point.toString()} />
|
||||
{/each}
|
||||
</EventCardOutline>
|
||||
</EventCard>
|
||||
@@ -61,7 +61,7 @@
|
||||
<div>
|
||||
<EventCard title="Runde {index + 1}">
|
||||
{#each round as fight}
|
||||
<EventFightChip {fight} {group} />
|
||||
<EventFightChip {event} {fight} {group} />
|
||||
{/each}
|
||||
</EventCard>
|
||||
</div>
|
||||
|
||||
79
src/components/event/TeamInfo.svelte
Normal file
79
src/components/event/TeamInfo.svelte
Normal 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>
|
||||
@@ -38,17 +38,27 @@
|
||||
|
||||
<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">
|
||||
<div
|
||||
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2"
|
||||
>
|
||||
{#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'}">
|
||||
<span class="text-sm font-mono text-neutral-400 shrink-0 w-8 text-center">{team.kuerzel}</span>
|
||||
<button
|
||||
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}>
|
||||
{team.name}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{#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}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,14 +5,16 @@ class TeamHoverService {
|
||||
public currentHover = $state<number | undefined>(undefined);
|
||||
private fightConnector = get(fightConnector);
|
||||
|
||||
public disableConnections = $state(false);
|
||||
|
||||
setHover(teamId: number): void {
|
||||
this.currentHover = teamId;
|
||||
this.fightConnector.addTeamConnection(teamId);
|
||||
if (!this.disableConnections) this.fightConnector.addTeamConnection(teamId);
|
||||
}
|
||||
|
||||
clearHover(): void {
|
||||
this.currentHover = undefined;
|
||||
this.fightConnector.clearConnections();
|
||||
if (!this.disableConnections) this.fightConnector.clearConnections();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export const ExtendedEventSchema = z.object({
|
||||
teams: z.array(TeamSchema),
|
||||
groups: z.array(ResponseGroupsSchema),
|
||||
fights: z.array(EventFightSchema),
|
||||
referees: z.array(ResponseUserSchema),
|
||||
referees: z.array(PlayerSchema),
|
||||
relations: z.array(ResponseRelationSchema),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
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 Content from "./sheet-content.svelte";
|
||||
import Header from "./sheet-header.svelte";
|
||||
@@ -12,6 +9,7 @@ import Description from "./sheet-description.svelte";
|
||||
const Root = SheetPrimitive.Root;
|
||||
const Close = SheetPrimitive.Close;
|
||||
const Trigger = SheetPrimitive.Trigger;
|
||||
const Portal = SheetPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
@@ -36,71 +34,3 @@ export {
|
||||
Title as SheetTitle,
|
||||
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"];
|
||||
|
||||
@@ -1,47 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import { fly } from "svelte/transition";
|
||||
import {
|
||||
SheetOverlay,
|
||||
SheetPortal,
|
||||
type Side,
|
||||
sheetTransitions,
|
||||
sheetVariants,
|
||||
} from "./index.js";
|
||||
import { cn } from "$lib/components/utils.js";
|
||||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
export const sheetVariants = tv({
|
||||
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",
|
||||
variants: {
|
||||
side: {
|
||||
top: "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b",
|
||||
bottom: "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t",
|
||||
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",
|
||||
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",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
});
|
||||
|
||||
type $$Props = SheetPrimitive.ContentProps & {
|
||||
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;
|
||||
export type Side = VariantProps<typeof sheetVariants>["side"];
|
||||
</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 />
|
||||
<SheetPrimitive.Content
|
||||
{inTransition}
|
||||
{inTransitionConfig}
|
||||
{outTransition}
|
||||
{outTransitionConfig}
|
||||
class={cn(sheetVariants({ side }), className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
<SheetPrimitive.Content bind:ref class={cn(sheetVariants({ side }), className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
<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"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
<X class="size-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
</SheetPrimitive.Portal>
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/components/utils.js";
|
||||
|
||||
type $$Props = SheetPrimitive.DescriptionProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Description class={cn("text-muted-foreground text-sm", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</SheetPrimitive.Description>
|
||||
<SheetPrimitive.Description
|
||||
bind:ref
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/components/utils.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...$$restProps}
|
||||
{...restProps}
|
||||
>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/components/utils.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...$$restProps}>
|
||||
<slot />
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { fade } from "svelte/transition";
|
||||
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 };
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Overlay
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ", className)}
|
||||
{...$$restProps}
|
||||
bind:ref
|
||||
class={cn(
|
||||
"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",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
@@ -2,15 +2,15 @@
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/components/utils.js";
|
||||
|
||||
type $$Props = SheetPrimitive.TitleProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Title
|
||||
bind:ref
|
||||
class={cn("text-foreground text-lg font-semibold", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</SheetPrimitive.Title>
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -9,9 +9,13 @@ import TeamList from "@components/event/TeamList.svelte";
|
||||
export const getStaticPaths = createGetStaticPaths(async () => {
|
||||
const events = await Promise.all(
|
||||
(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,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
|
||||
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();
|
||||
---
|
||||
@@ -35,19 +42,26 @@ const { Content } = await page.render();
|
||||
<h1 class="text-2xl font-bold">{event.event.name}</h1>
|
||||
<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",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
{
|
||||
new Date(event.event.start).toDateString() !== new Date(event.event.end).toDateString()
|
||||
? ` - ${new Date(event.event.end).toLocaleDateString(astroI18n.locale, {
|
||||
new Date(event.event.start).toDateString() !==
|
||||
new Date(event.event.end).toDateString()
|
||||
? ` - ${new Date(event.event.end).toLocaleDateString(
|
||||
astroI18n.locale,
|
||||
{
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
})}`
|
||||
},
|
||||
)}`
|
||||
: ""
|
||||
}
|
||||
</h2>
|
||||
@@ -60,7 +74,11 @@ const { Content } = await page.render();
|
||||
page.data.viewConfig && (
|
||||
<div class="py-2 border-t border-t-gray-600">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user