feat: Add event collection and event page structure
All checks were successful
SteamWarCI Build successful
All checks were successful
SteamWarCI Build successful
- Introduced a new events collection in config.ts with schema validation. - Created a new event markdown file for the WarGear event. - Updated German translations to include new event-related strings. - Modified PageLayout to support a wide layout option. - Enhanced announcements page to improve tag filtering and post rendering. - Implemented dynamic event pages with detailed event information and fight plans. - Added an index page for events to list all upcoming events.
This commit is contained in:
@@ -194,6 +194,16 @@
|
||||
<MenubarItem onclick={() => (groupChangeOpen = true)}>Gruppe Ändern</MenubarItem>
|
||||
<MenubarItem disabled>Startzeit Verschieben</MenubarItem>
|
||||
<MenubarItem disabled>Spectate Port Ändern</MenubarItem>
|
||||
<MenubarItem
|
||||
onclick={async () => {
|
||||
let selectedGroups = table.getSelectedRowModel().rows.map((row) => row.original);
|
||||
for (const g of selectedGroups) {
|
||||
await $fightRepo.deleteFight(data.event.id, g.id);
|
||||
}
|
||||
|
||||
refresh();
|
||||
}}>Kämpfe Löschen</MenubarItem
|
||||
>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
<MenubarMenu>
|
||||
|
||||
@@ -38,6 +38,11 @@
|
||||
|
||||
duplicateOpen = false;
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
await $fightRepo.deleteFight(data.event.id, fight.id);
|
||||
refresh();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
@@ -55,6 +60,7 @@
|
||||
<FightEdit {fight} {data} onSave={handleSave}>
|
||||
{#snippet actions(dirty, submit)}
|
||||
<DialogFooter>
|
||||
<Button variant="destructive" onclick={handleDelete}>Löschen</Button>
|
||||
<Button disabled={!dirty} onclick={submit}>Speichern</Button>
|
||||
</DialogFooter>
|
||||
{/snippet}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import type { ExtendedEvent } from "@components/types/event";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@components/ui/tabs";
|
||||
import GroupPhaseGenerator from "./gens/group/GroupPhaseGenerator.svelte";
|
||||
import SingleEliminationGenerator from "./gens/elimination/SingleEliminationGenerator.svelte";
|
||||
import DoubleEliminationGenerator from "./gens/elimination/DoubleEliminationGenerator.svelte";
|
||||
let {
|
||||
data,
|
||||
}: {
|
||||
@@ -14,9 +16,16 @@
|
||||
<TabsList class="mb-4">
|
||||
<TabsTrigger value="group">Gruppenphase</TabsTrigger>
|
||||
<TabsTrigger value="ko">K.O. Phase</TabsTrigger>
|
||||
<TabsTrigger value="double">Double Elimination</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="group">
|
||||
<GroupPhaseGenerator {data} />
|
||||
</TabsContent>
|
||||
<TabsContent value="ko">
|
||||
<SingleEliminationGenerator {data} />
|
||||
</TabsContent>
|
||||
<TabsContent value="double">
|
||||
<DoubleEliminationGenerator {data} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,515 @@
|
||||
<script lang="ts">
|
||||
import type { ExtendedEvent } from "@components/types/event";
|
||||
import type { Team } from "@components/types/team";
|
||||
import { eventRepo } from "@components/repo/event";
|
||||
import { fightRepo } from "@components/repo/fight";
|
||||
import { gamemodes, maps } from "@components/stores/stores";
|
||||
import { Button } from "@components/ui/button";
|
||||
import { Card } from "@components/ui/card";
|
||||
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||
import { Label } from "@components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||
import { Slider } from "@components/ui/slider";
|
||||
import { fromAbsolute } from "@internationalized/date";
|
||||
import dayjs from "dayjs";
|
||||
import { Plus, Shuffle } from "lucide-svelte";
|
||||
import { replace } from "svelte-spa-router";
|
||||
|
||||
let { data }: { data: ExtendedEvent } = $props();
|
||||
|
||||
// Seed model (reuse from single elimination)
|
||||
interface SeedTeamSlot {
|
||||
kind: "TEAM";
|
||||
id: number;
|
||||
}
|
||||
interface SeedGroupSlot {
|
||||
kind: "GROUP";
|
||||
groupId: number;
|
||||
place: number;
|
||||
}
|
||||
interface SeedFightSlot {
|
||||
kind: "FIGHT";
|
||||
fightId: number;
|
||||
place: 0 | 1;
|
||||
}
|
||||
type SeedSlot = SeedTeamSlot | SeedGroupSlot | SeedFightSlot;
|
||||
|
||||
let seedSlots = $state<SeedSlot[]>(data.teams.map((t) => ({ kind: "TEAM", id: t.id })));
|
||||
const teams = $derived(new Map<number, Team>(data.teams.map((t) => [t.id, t])));
|
||||
function shuffleTeams() {
|
||||
const teamIndices = seedSlots.map((v, i) => ({ v, i })).filter((x) => x.v.kind === "TEAM");
|
||||
const shuffledIds = teamIndices.map((x) => (x.v as SeedTeamSlot).id).sort(() => Math.random() - 0.5);
|
||||
let p = 0;
|
||||
seedSlots = seedSlots.map((slot) => (slot.kind === "TEAM" ? { kind: "TEAM", id: shuffledIds[p++] } : slot));
|
||||
}
|
||||
function moveSlot(index: number, dir: -1 | 1) {
|
||||
const ni = index + dir;
|
||||
if (ni < 0 || ni >= seedSlots.length) return;
|
||||
const copy = [...seedSlots];
|
||||
const [item] = copy.splice(index, 1);
|
||||
copy.splice(ni, 0, item);
|
||||
seedSlots = copy;
|
||||
}
|
||||
function removeSlot(index: number) {
|
||||
seedSlots = seedSlots.filter((_, i) => i !== index);
|
||||
}
|
||||
function addUnknown() {
|
||||
seedSlots = [...seedSlots, { kind: "TEAM", id: -1 }];
|
||||
}
|
||||
let selectedAddTeam = $state<number>(data.teams[0]?.id ?? 0);
|
||||
function addTeam() {
|
||||
if (selectedAddTeam !== undefined) seedSlots = [...seedSlots, { kind: "TEAM", id: selectedAddTeam }];
|
||||
}
|
||||
let selectedGroup = $state<number | null>(data.groups[0]?.id ?? null);
|
||||
let selectedGroupPlace = $state<number>(0);
|
||||
function addGroupPlace() {
|
||||
if (selectedGroup != null) seedSlots = [...seedSlots, { kind: "GROUP", groupId: selectedGroup, place: selectedGroupPlace }];
|
||||
}
|
||||
let selectedFight = $state<number | null>(data.fights[0]?.id ?? null);
|
||||
let selectedFightPlace = $state<0 | 1>(0);
|
||||
function addFightPlace() {
|
||||
if (selectedFight != null) seedSlots = [...seedSlots, { kind: "FIGHT", fightId: selectedFight, place: selectedFightPlace }];
|
||||
}
|
||||
|
||||
// Config
|
||||
let startTime = $state(fromAbsolute(data.event.start, "Europe/Berlin"));
|
||||
let roundTime = $state(30);
|
||||
let startDelay = $state(30);
|
||||
let gamemode = $state("");
|
||||
let map = $state("");
|
||||
let selectableGamemodes = $derived($gamemodes.map((g) => ({ name: g, value: g })).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
let mapsStore = $derived(maps(gamemode));
|
||||
let selectableMaps = $derived($mapsStore.map((m) => ({ name: m, value: m })).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
// Build winners bracket rounds (same as single elimination seeding)
|
||||
interface BracketFightPreview {
|
||||
blue: SeedSlot;
|
||||
red: SeedSlot;
|
||||
}
|
||||
type BracketRoundPreview = BracketFightPreview[];
|
||||
function buildWinnersRounds(order: SeedSlot[]): BracketRoundPreview[] {
|
||||
const n = order.length;
|
||||
if (n < 2) return [];
|
||||
if ((n & (n - 1)) !== 0) return []; // power of two required
|
||||
const rounds: BracketRoundPreview[] = [];
|
||||
let round: BracketRoundPreview = [];
|
||||
for (let i = 0; i < order.length; i += 2) round.push({ blue: order[i], red: order[i + 1] });
|
||||
rounds.push(round);
|
||||
let prevWinners = round.map((f) => f.blue);
|
||||
while (prevWinners.length > 1) {
|
||||
const next: BracketRoundPreview = [];
|
||||
for (let i = 0; i < prevWinners.length; i += 2) next.push({ blue: prevWinners[i], red: prevWinners[i + 1] });
|
||||
rounds.push(next);
|
||||
prevWinners = next.map((f) => f.blue);
|
||||
}
|
||||
return rounds;
|
||||
}
|
||||
let winnersRounds = $derived(buildWinnersRounds(seedSlots));
|
||||
|
||||
// Losers bracket structure: each losers round takes losers from previous winners round or previous losers round.
|
||||
// Simplified pairing: For each winners round except final, collect losers and pair them sequentially; then advance until one remains for losers final.
|
||||
function buildLosersTemplate(wRounds: BracketRoundPreview[]): BracketRoundPreview[] {
|
||||
const losersRounds: BracketRoundPreview[] = [];
|
||||
if (wRounds.length < 2) return losersRounds;
|
||||
// Round 1 losers (from winners round 1)
|
||||
const firstLosersPairs: BracketRoundPreview = [];
|
||||
wRounds[0].forEach((f) => firstLosersPairs.push({ blue: f.blue, red: f.red })); // placeholders (will label as losers)
|
||||
losersRounds.push(firstLosersPairs);
|
||||
// Subsequent losers rounds shrink similarly
|
||||
let remaining = firstLosersPairs.length; // number of fights that feed losers next stage
|
||||
while (remaining > 1) {
|
||||
const next: BracketRoundPreview = [];
|
||||
for (let i = 0; i < remaining; i += 2) next.push(firstLosersPairs[i]); // placeholder reuse
|
||||
losersRounds.push(next);
|
||||
remaining = next.length;
|
||||
}
|
||||
return losersRounds;
|
||||
}
|
||||
let losersRounds = $derived(buildLosersTemplate(winnersRounds));
|
||||
|
||||
let generateDisabled = $derived(gamemode !== "" && map !== "" && winnersRounds.length > 0 && seedSlots.length >= 4);
|
||||
|
||||
// Type helpers
|
||||
function slotLabel(slot: SeedSlot): string {
|
||||
if (slot.kind === "TEAM") return teams.get(slot.id)?.name ?? "???";
|
||||
if (slot.kind === "GROUP") {
|
||||
const gname = data.groups.find((g) => g.id === slot.groupId)?.name ?? "?";
|
||||
return `(Grp ${gname} Platz ${slot.place + 1})`;
|
||||
}
|
||||
if (slot.kind === "FIGHT") {
|
||||
const f = data.fights.find((x) => x.id === slot.fightId);
|
||||
const when = f ? new Date(f.start).toLocaleTimeString("de-DE", { timeStyle: "short" }) : "?";
|
||||
const vs = f ? `${f.blueTeam.kuerzel} vs. ${f.redTeam.kuerzel}` : "Kampf";
|
||||
return `${slot.place === 0 ? "Gewinner" : "Verlierer"} von ${vs} (${when})`;
|
||||
}
|
||||
return "???";
|
||||
}
|
||||
|
||||
async function generateDouble() {
|
||||
if (!generateDisabled) return;
|
||||
const eventId = data.event.id;
|
||||
// Create two groups: winners & losers + grand final group (optional combine winners)
|
||||
const winnersGroup = await $eventRepo.createGroup(eventId, { name: "Winners", type: "ELIMINATION_STAGE" });
|
||||
const losersGroup = await $eventRepo.createGroup(eventId, { name: "Losers", type: "ELIMINATION_STAGE" });
|
||||
const finalGroup = await $eventRepo.createGroup(eventId, { name: "Final", type: "ELIMINATION_STAGE" });
|
||||
|
||||
function fallbackTeamId(slot: SeedSlot): number {
|
||||
if (slot.kind === "GROUP" || slot.kind === "FIGHT") return -1;
|
||||
if (slot.kind === "TEAM") return slot.id;
|
||||
return data.teams[0].id;
|
||||
}
|
||||
|
||||
const winnersFightIdsByRound: number[][] = [];
|
||||
for (let r = 0; r < winnersRounds.length; r++) {
|
||||
const round = winnersRounds[r];
|
||||
const ids: number[] = [];
|
||||
for (let i = 0; i < round.length; i++) {
|
||||
let finalMap = map;
|
||||
if (finalMap === "%random%" && selectableMaps.length > 0) finalMap = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
|
||||
const f = await $fightRepo.createFight(eventId, {
|
||||
blueTeam: fallbackTeamId(round[i].blue),
|
||||
redTeam: fallbackTeamId(round[i].red),
|
||||
group: winnersGroup.id,
|
||||
map: finalMap,
|
||||
spectatePort: null,
|
||||
spielmodus: gamemode,
|
||||
start: dayjs(
|
||||
startTime
|
||||
.copy()
|
||||
.add({ minutes: roundTime * r })
|
||||
.add({ seconds: startDelay * i })
|
||||
.toDate()
|
||||
),
|
||||
});
|
||||
// Attach relations for GROUP/FIGHT seeds
|
||||
const pair = round[i];
|
||||
if (pair.blue.kind === "GROUP") await $eventRepo.createRelation(eventId, { fightId: f.id, team: "BLUE", fromType: "GROUP", fromId: pair.blue.groupId, fromPlace: pair.blue.place });
|
||||
if (pair.red.kind === "GROUP") await $eventRepo.createRelation(eventId, { fightId: f.id, team: "RED", fromType: "GROUP", fromId: pair.red.groupId, fromPlace: pair.red.place });
|
||||
if (pair.blue.kind === "FIGHT") await $eventRepo.createRelation(eventId, { fightId: f.id, team: "BLUE", fromType: "FIGHT", fromId: pair.blue.fightId, fromPlace: pair.blue.place });
|
||||
if (pair.red.kind === "FIGHT") await $eventRepo.createRelation(eventId, { fightId: f.id, team: "RED", fromType: "FIGHT", fromId: pair.red.fightId, fromPlace: pair.red.place });
|
||||
ids.push(f.id);
|
||||
}
|
||||
winnersFightIdsByRound.push(ids);
|
||||
}
|
||||
|
||||
// Progression in winners bracket
|
||||
for (let r = 1; r < winnersFightIdsByRound.length; r++) {
|
||||
const prev = winnersFightIdsByRound[r - 1];
|
||||
const curr = winnersFightIdsByRound[r];
|
||||
for (let i = 0; i < curr.length; i++) {
|
||||
const target = curr[i];
|
||||
const srcA = prev[i * 2];
|
||||
const srcB = prev[i * 2 + 1];
|
||||
await $eventRepo.createRelation(eventId, { fightId: target, team: "BLUE", fromType: "FIGHT", fromId: srcA, fromPlace: 0 });
|
||||
await $eventRepo.createRelation(eventId, { fightId: target, team: "RED", fromType: "FIGHT", fromId: srcB, fromPlace: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
// Losers bracket (canonical pattern):
|
||||
// L1: losers of WBR1 paired; then for r=2..(k-1):
|
||||
// Major: winners of previous L vs losers of WBRr
|
||||
// Minor: winners of that major paired (except after last WBR where we go to LB final vs WB final loser)
|
||||
const losersFightIdsByRound: number[][] = [];
|
||||
let losersRoundIndex = 0;
|
||||
const k = winnersFightIdsByRound.length; // number of winners rounds
|
||||
|
||||
// L1 from WBR1 losers
|
||||
{
|
||||
const wb1 = winnersFightIdsByRound[0];
|
||||
const ids: number[] = [];
|
||||
for (let i = 0; i < wb1.length; i += 2) {
|
||||
let finalMap = map;
|
||||
if (finalMap === "%random%" && selectableMaps.length > 0) finalMap = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
|
||||
const lf = await $fightRepo.createFight(eventId, {
|
||||
blueTeam: -1,
|
||||
redTeam: -1,
|
||||
group: losersGroup.id,
|
||||
map: finalMap,
|
||||
spectatePort: null,
|
||||
spielmodus: gamemode,
|
||||
start: dayjs(
|
||||
startTime
|
||||
.copy()
|
||||
.add({ minutes: roundTime * (k + losersRoundIndex) })
|
||||
.add({ seconds: startDelay * (i / 2) })
|
||||
.toDate()
|
||||
),
|
||||
});
|
||||
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "BLUE", fromType: "FIGHT", fromId: wb1[i], fromPlace: 1 });
|
||||
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "RED", fromType: "FIGHT", fromId: wb1[i + 1], fromPlace: 1 });
|
||||
ids.push(lf.id);
|
||||
}
|
||||
losersFightIdsByRound.push(ids);
|
||||
losersRoundIndex++;
|
||||
}
|
||||
|
||||
// For each subsequent winners round except the final
|
||||
for (let wr = 1; wr < k - 1; wr++) {
|
||||
const prevLBRound = losersFightIdsByRound[losersFightIdsByRound.length - 1];
|
||||
|
||||
// Major: winners of prevLBRound vs losers of current WBR (wr)
|
||||
{
|
||||
const ids: number[] = [];
|
||||
for (let j = 0; j < winnersFightIdsByRound[wr].length; j++) {
|
||||
let finalMap = map;
|
||||
if (finalMap === "%random%" && selectableMaps.length > 0) finalMap = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
|
||||
const lf = await $fightRepo.createFight(eventId, {
|
||||
blueTeam: -1,
|
||||
redTeam: -1,
|
||||
group: losersGroup.id,
|
||||
map: finalMap,
|
||||
spectatePort: null,
|
||||
spielmodus: gamemode,
|
||||
start: dayjs(
|
||||
startTime
|
||||
.copy()
|
||||
.add({ minutes: roundTime * (k + losersRoundIndex) })
|
||||
.add({ seconds: startDelay * j })
|
||||
.toDate()
|
||||
),
|
||||
});
|
||||
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "BLUE", fromType: "FIGHT", fromId: prevLBRound[j], fromPlace: 0 });
|
||||
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "RED", fromType: "FIGHT", fromId: winnersFightIdsByRound[wr][j], fromPlace: 1 });
|
||||
ids.push(lf.id);
|
||||
}
|
||||
losersFightIdsByRound.push(ids);
|
||||
losersRoundIndex++;
|
||||
}
|
||||
|
||||
// Minor: pair winners of last LBRound among themselves (if more than 1)
|
||||
{
|
||||
const last = losersFightIdsByRound[losersFightIdsByRound.length - 1];
|
||||
if (last.length > 1) {
|
||||
const ids: number[] = [];
|
||||
for (let j = 0; j < last.length; j += 2) {
|
||||
let finalMap = map;
|
||||
if (finalMap === "%random%" && selectableMaps.length > 0) finalMap = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
|
||||
const lf = await $fightRepo.createFight(eventId, {
|
||||
blueTeam: -1,
|
||||
redTeam: -1,
|
||||
group: losersGroup.id,
|
||||
map: finalMap,
|
||||
spectatePort: null,
|
||||
spielmodus: gamemode,
|
||||
start: dayjs(
|
||||
startTime
|
||||
.copy()
|
||||
.add({ minutes: roundTime * (k + losersRoundIndex) })
|
||||
.add({ seconds: startDelay * (j / 2) })
|
||||
.toDate()
|
||||
),
|
||||
});
|
||||
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "BLUE", fromType: "FIGHT", fromId: last[j], fromPlace: 0 });
|
||||
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "RED", fromType: "FIGHT", fromId: last[j + 1], fromPlace: 0 });
|
||||
ids.push(lf.id);
|
||||
}
|
||||
losersFightIdsByRound.push(ids);
|
||||
losersRoundIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final losers round: winners of last LBRound vs loser of Winners Final (last WBR)
|
||||
const winnersFinal = winnersFightIdsByRound[k - 1][0];
|
||||
const lastLBRound = losersFightIdsByRound[losersFightIdsByRound.length - 1];
|
||||
let losersFinal: number | undefined = undefined;
|
||||
if (lastLBRound && lastLBRound.length >= 1) {
|
||||
let finalMap2 = map;
|
||||
if (finalMap2 === "%random%" && selectableMaps.length > 0) finalMap2 = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
|
||||
const lf = await $fightRepo.createFight(eventId, {
|
||||
blueTeam: -1,
|
||||
redTeam: -1,
|
||||
group: losersGroup.id,
|
||||
map: finalMap2,
|
||||
spectatePort: null,
|
||||
spielmodus: gamemode,
|
||||
start: dayjs(
|
||||
startTime
|
||||
.copy()
|
||||
.add({ minutes: roundTime * (k + losersRoundIndex) })
|
||||
.toDate()
|
||||
),
|
||||
});
|
||||
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "BLUE", fromType: "FIGHT", fromId: lastLBRound[lastLBRound.length - 1], fromPlace: 0 });
|
||||
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "RED", fromType: "FIGHT", fromId: winnersFinal, fromPlace: 1 });
|
||||
losersFinal = lf.id;
|
||||
losersFightIdsByRound.push([lf.id]);
|
||||
losersRoundIndex++;
|
||||
}
|
||||
|
||||
// Grand Final
|
||||
let finalMap = map;
|
||||
if (finalMap === "%random%" && selectableMaps.length > 0) finalMap = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
|
||||
const grandFinal = await $fightRepo.createFight(eventId, {
|
||||
blueTeam: -1,
|
||||
redTeam: -1,
|
||||
group: finalGroup.id,
|
||||
map: finalMap,
|
||||
spectatePort: null,
|
||||
spielmodus: gamemode,
|
||||
start: dayjs(
|
||||
startTime
|
||||
.copy()
|
||||
.add({ minutes: roundTime * (k + losersRoundIndex) })
|
||||
.toDate()
|
||||
),
|
||||
});
|
||||
await $eventRepo.createRelation(eventId, { fightId: grandFinal.id, team: "BLUE", fromType: "FIGHT", fromId: winnersFinal, fromPlace: 0 });
|
||||
if (losersFinal !== undefined) await $eventRepo.createRelation(eventId, { fightId: grandFinal.id, team: "RED", fromType: "FIGHT", fromId: losersFinal, fromPlace: 0 });
|
||||
|
||||
await replace("#/event/" + eventId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card class="p-4 mb-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold">Double Elimination Bracket</h2>
|
||||
<div class="flex gap-2">
|
||||
<Button onclick={shuffleTeams} aria-label="Shuffle Teams"><Shuffle size={16} /> Shuffle</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#if seedSlots.length < 4}
|
||||
<p class="text-gray-400">Mindestens vier Seeds benötigt.</p>
|
||||
{:else if winnersRounds.length === 0}
|
||||
<p class="text-yellow-400">Seedanzahl muss eine Zweierpotenz sein. Aktuell: {seedSlots.length}</p>
|
||||
{/if}
|
||||
<div class="grid lg:grid-cols-3 gap-6">
|
||||
<div class="space-y-4">
|
||||
<Label>Seeds</Label>
|
||||
<ul class="mt-2 space-y-1">
|
||||
{#each seedSlots as slot, i (i)}
|
||||
<li class="flex items-center gap-2 text-sm">
|
||||
<span class="w-6 text-right">{i + 1}.</span>
|
||||
<span class="flex-1 truncate">{slotLabel(slot)}</span>
|
||||
<div class="flex gap-1">
|
||||
<Button size="sm" onclick={() => moveSlot(i, -1)} disabled={i === 0}>↑</Button>
|
||||
<Button size="sm" onclick={() => moveSlot(i, 1)} disabled={i === seedSlots.length - 1}>↓</Button>
|
||||
<Button size="sm" variant="destructive" onclick={() => removeSlot(i)}>✕</Button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<div class="space-y-2">
|
||||
<Label>Hinzufügen</Label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedAddTeam}>
|
||||
{#each data.teams as t}<option value={t.id}>{t.name}</option>{/each}
|
||||
</select>
|
||||
<Button size="sm" onclick={addTeam}>Team</Button>
|
||||
<Button size="sm" onclick={addUnknown}>???</Button>
|
||||
</div>
|
||||
{#if data.groups.length > 0}
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedGroup}>
|
||||
{#each data.groups as g}<option value={g.id}>{g.name}</option>{/each}
|
||||
</select>
|
||||
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedGroupPlace}>
|
||||
{#each Array(16) as _, idx}<option value={idx}>{idx + 1}. Platz</option>{/each}
|
||||
</select>
|
||||
<Button size="sm" onclick={addGroupPlace}>Gruppenplatz</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if data.fights.length > 0}
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedFight}>
|
||||
{#each data.fights as f}
|
||||
<option value={f.id}>{new Date(f.start).toLocaleTimeString("de-DE", { timeStyle: "short" })}: {f.blueTeam.kuerzel} vs. {f.redTeam.kuerzel}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedFightPlace}>
|
||||
<option value={0}>Gewinner</option>
|
||||
<option value={1}>Verlierer</option>
|
||||
</select>
|
||||
<Button size="sm" onclick={addFightPlace}>Kampfplatz</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Konfiguration</Label>
|
||||
<DateTimePicker bind:value={startTime} />
|
||||
<div class="mt-4">
|
||||
<Label>Rundenzeit: {roundTime}m</Label>
|
||||
<Slider type="single" bind:value={roundTime} step={5} min={5} max={60} />
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<Label>Startverzögerung: {startDelay}s</Label>
|
||||
<Slider type="single" bind:value={startDelay} step={5} min={0} max={60} />
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<Label>Spielmodus</Label>
|
||||
<Select type="single" bind:value={gamemode}>
|
||||
<SelectTrigger>{gamemode}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each selectableGamemodes as gm}<SelectItem value={gm.value}>{gm.name}</SelectItem>{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<Label>Map</Label>
|
||||
<Select type="single" bind:value={map}>
|
||||
<SelectTrigger>{map}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="%random%">Zufällige Map</SelectItem>
|
||||
{#each selectableMaps as mp}<SelectItem value={mp.value}>{mp.name}</SelectItem>{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<Label>Winners Bracket Vorschau</Label>
|
||||
{#if winnersRounds.length > 0}
|
||||
<div class="flex gap-6 overflow-x-auto mt-2">
|
||||
{#each winnersRounds as round, r}
|
||||
<div>
|
||||
<h3 class="font-semibold mb-2">W Runde {r + 1}</h3>
|
||||
<ul class="space-y-1">
|
||||
{#each round as fight, i}
|
||||
<li class="p-2 border border-gray-700 rounded text-xs">
|
||||
<span class="text-gray-400"
|
||||
>{new Intl.DateTimeFormat("de-DE", { hour: "2-digit", minute: "2-digit" }).format(
|
||||
startTime
|
||||
.copy()
|
||||
.add({ minutes: roundTime * r, seconds: startDelay * i })
|
||||
.toDate()
|
||||
)}</span
|
||||
>
|
||||
: {slotLabel(fight.blue)} vs. {slotLabel(fight.red)}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<Label>Losers Bracket (vereinfachte Vorschau)</Label>
|
||||
{#if losersRounds.length > 0}
|
||||
<div class="flex gap-6 overflow-x-auto mt-2">
|
||||
{#each losersRounds as round, r}
|
||||
<div>
|
||||
<h3 class="font-semibold mb-2">L Runde {r + 1}</h3>
|
||||
<ul class="space-y-1">
|
||||
{#each round as fight, i}
|
||||
<li class="p-2 border border-gray-700 rounded text-xs">
|
||||
Verlierer Paar {i + 1} (aus W Runde {r + 1})
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button class="!p-4 fixed bottom-4 right-4" disabled={!generateDisabled} onclick={generateDouble} aria-label="Double Bracket generieren">
|
||||
<Plus />
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<!-- minimal styles only -->
|
||||
@@ -0,0 +1,364 @@
|
||||
<script lang="ts">
|
||||
import type { ExtendedEvent } from "@components/types/event";
|
||||
import type { Team } from "@components/types/team";
|
||||
import { eventRepo } from "@components/repo/event";
|
||||
import { fightRepo } from "@components/repo/fight";
|
||||
import { gamemodes, maps } from "@components/stores/stores";
|
||||
import { Button } from "@components/ui/button";
|
||||
import { Card } from "@components/ui/card";
|
||||
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||
import { Label } from "@components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||
import { Slider } from "@components/ui/slider";
|
||||
import { fromAbsolute } from "@internationalized/date";
|
||||
import dayjs from "dayjs";
|
||||
import { Plus, Shuffle } from "lucide-svelte";
|
||||
import { replace } from "svelte-spa-router";
|
||||
|
||||
let { data }: { data: ExtendedEvent } = $props();
|
||||
|
||||
// --- Seeding model: support teams, group results, unknown placeholders ---
|
||||
interface SeedTeamSlot {
|
||||
kind: "TEAM";
|
||||
id: number;
|
||||
}
|
||||
interface SeedGroupSlot {
|
||||
kind: "GROUP";
|
||||
groupId: number;
|
||||
place: number;
|
||||
}
|
||||
interface SeedUnknownSlot {
|
||||
kind: "UNKNOWN";
|
||||
uid: number;
|
||||
}
|
||||
interface SeedFightSlot {
|
||||
kind: "FIGHT";
|
||||
fightId: number;
|
||||
place: 0 | 1;
|
||||
} // 0 winner, 1 loser
|
||||
type SeedSlot = SeedTeamSlot | SeedGroupSlot | SeedUnknownSlot | SeedFightSlot;
|
||||
|
||||
let seedSlots = $state<SeedSlot[]>(data.teams.map((t) => ({ kind: "TEAM", id: t.id })));
|
||||
const teams = $derived(new Map<number, Team>(data.teams.map((t) => [t.id, t])));
|
||||
let unknownCounter = 1;
|
||||
|
||||
function shuffleTeams() {
|
||||
const teamIndices = seedSlots.map((v, i) => ({ v, i })).filter((x) => x.v.kind === "TEAM");
|
||||
const shuffledIds = teamIndices.map((x) => (x.v as SeedTeamSlot).id).sort(() => Math.random() - 0.5);
|
||||
let p = 0;
|
||||
seedSlots = seedSlots.map((slot) => (slot.kind === "TEAM" ? { kind: "TEAM", id: shuffledIds[p++] } : slot));
|
||||
}
|
||||
|
||||
function moveSlot(index: number, dir: -1 | 1) {
|
||||
const newIndex = index + dir;
|
||||
if (newIndex < 0 || newIndex >= seedSlots.length) return;
|
||||
const copy = [...seedSlots];
|
||||
const [item] = copy.splice(index, 1);
|
||||
copy.splice(newIndex, 0, item);
|
||||
seedSlots = copy;
|
||||
}
|
||||
function removeSlot(index: number) {
|
||||
seedSlots = seedSlots.filter((_, i) => i !== index);
|
||||
}
|
||||
function addUnknown() {
|
||||
seedSlots = [...seedSlots, { kind: "UNKNOWN", uid: unknownCounter++ }];
|
||||
}
|
||||
let selectedAddTeam = $state<number>(data.teams[0]?.id ?? 0);
|
||||
function addTeam() {
|
||||
if (selectedAddTeam !== undefined) seedSlots = [...seedSlots, { kind: "TEAM", id: selectedAddTeam }];
|
||||
}
|
||||
let selectedGroup = $state<number | null>(data.groups[0]?.id ?? null);
|
||||
let selectedGroupPlace = $state<number>(0);
|
||||
function addGroupPlace() {
|
||||
if (selectedGroup != null) seedSlots = [...seedSlots, { kind: "GROUP", groupId: selectedGroup, place: selectedGroupPlace }];
|
||||
}
|
||||
|
||||
// Fight seed selection
|
||||
let selectedFight = $state<number | null>(data.fights[0]?.id ?? null);
|
||||
let selectedFightPlace = $state<0 | 1>(0);
|
||||
function addFightPlace() {
|
||||
if (selectedFight != null) seedSlots = [...seedSlots, { kind: "FIGHT", fightId: selectedFight, place: selectedFightPlace }];
|
||||
}
|
||||
|
||||
// Config inputs
|
||||
let startTime = $state(fromAbsolute(data.event.start, "Europe/Berlin"));
|
||||
let roundTime = $state(30); // minutes per round
|
||||
let startDelay = $state(30); // seconds between fights inside round
|
||||
let gamemode = $state("");
|
||||
let map = $state("");
|
||||
|
||||
// Gamemode / Map selection stores
|
||||
let selectableGamemodes = $derived($gamemodes.map((g) => ({ name: g, value: g })).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
let mapsStore = $derived(maps(gamemode));
|
||||
let selectableMaps = $derived($mapsStore.map((m) => ({ name: m, value: m })).sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
// Derived: bracket rounds preview
|
||||
interface BracketFightPreview {
|
||||
blue: SeedSlot;
|
||||
red: SeedSlot;
|
||||
}
|
||||
type BracketRoundPreview = BracketFightPreview[];
|
||||
|
||||
function buildBracketSeeds(order: SeedSlot[]): BracketRoundPreview[] {
|
||||
const n = order.length;
|
||||
if (n < 2) return [];
|
||||
// Require power of two for now; simplest implementation
|
||||
if ((n & (n - 1)) !== 0) return [];
|
||||
let rounds: BracketRoundPreview[] = [];
|
||||
let round: BracketRoundPreview = [];
|
||||
for (let i = 0; i < order.length; i += 2) round.push({ blue: order[i], red: order[i + 1] });
|
||||
rounds.push(round);
|
||||
// Higher rounds placeholders using first team from each prior pairing as seed representative
|
||||
let prevWinners = round.map((fight) => fight.blue);
|
||||
while (prevWinners.length > 1) {
|
||||
const nextRound: BracketRoundPreview = [];
|
||||
for (let i = 0; i < prevWinners.length; i += 2) {
|
||||
nextRound.push({ blue: prevWinners[i], red: prevWinners[i + 1] });
|
||||
}
|
||||
rounds.push(nextRound);
|
||||
prevWinners = nextRound.map((f) => f.blue);
|
||||
}
|
||||
return rounds;
|
||||
}
|
||||
|
||||
let bracketRounds = $derived(buildBracketSeeds(seedSlots));
|
||||
|
||||
let generateDisabled = $derived(gamemode !== "" && map !== "" && bracketRounds.length > 0 && seedSlots.length >= 2);
|
||||
|
||||
async function generateBracket() {
|
||||
if (!generateDisabled) return;
|
||||
const eventId = data.event.id;
|
||||
// create elimination group
|
||||
const group = await $eventRepo.createGroup(eventId, { name: "Elimination", type: "ELIMINATION_STAGE" });
|
||||
|
||||
// Create fights round by round & keep ids for relation wiring
|
||||
const fightIdsByRound: number[][] = [];
|
||||
function fallbackTeamId(slot: SeedSlot): number {
|
||||
// If this seed is a relation (GROUP or FIGHT), use -1 as requested
|
||||
if (slot.kind === "GROUP" || slot.kind === "FIGHT") return -1;
|
||||
if (slot.kind === "TEAM") return slot.id;
|
||||
// UNKNOWN stays as a concrete placeholder team or -1? Keep concrete team to avoid backend errors.
|
||||
return data.teams[0].id;
|
||||
}
|
||||
for (let r = 0; r < bracketRounds.length; r++) {
|
||||
const round = bracketRounds[r];
|
||||
const ids: number[] = [];
|
||||
for (let i = 0; i < round.length; i++) {
|
||||
const pair = round[i];
|
||||
let finalMap = map;
|
||||
if (finalMap === "%random%" && selectableMaps.length > 0) finalMap = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
|
||||
const fight = await $fightRepo.createFight(eventId, {
|
||||
blueTeam: fallbackTeamId(pair.blue),
|
||||
redTeam: fallbackTeamId(pair.red),
|
||||
group: group.id,
|
||||
map: finalMap,
|
||||
spectatePort: null,
|
||||
spielmodus: gamemode,
|
||||
start: dayjs(
|
||||
startTime
|
||||
.copy()
|
||||
.add({ minutes: roundTime * r })
|
||||
.add({ seconds: startDelay * i })
|
||||
.toDate()
|
||||
),
|
||||
});
|
||||
if (pair.blue.kind === "GROUP") await $eventRepo.createRelation(eventId, { fightId: fight.id, team: "BLUE", fromType: "GROUP", fromId: pair.blue.groupId, fromPlace: pair.blue.place });
|
||||
if (pair.red.kind === "GROUP") await $eventRepo.createRelation(eventId, { fightId: fight.id, team: "RED", fromType: "GROUP", fromId: pair.red.groupId, fromPlace: pair.red.place });
|
||||
if (pair.blue.kind === "FIGHT") await $eventRepo.createRelation(eventId, { fightId: fight.id, team: "BLUE", fromType: "FIGHT", fromId: pair.blue.fightId, fromPlace: pair.blue.place });
|
||||
if (pair.red.kind === "FIGHT") await $eventRepo.createRelation(eventId, { fightId: fight.id, team: "RED", fromType: "FIGHT", fromId: pair.red.fightId, fromPlace: pair.red.place });
|
||||
ids.push(fight.id);
|
||||
}
|
||||
fightIdsByRound.push(ids);
|
||||
}
|
||||
|
||||
// Wire relations: for each fight in rounds >0, reference winners of two source fights from previous round
|
||||
for (let r = 1; r < fightIdsByRound.length; r++) {
|
||||
const prev = fightIdsByRound[r - 1];
|
||||
const current = fightIdsByRound[r];
|
||||
for (let i = 0; i < current.length; i++) {
|
||||
const targetFightId = current[i];
|
||||
const srcA = prev[i * 2];
|
||||
const srcB = prev[i * 2 + 1];
|
||||
// Winner assumed place 1
|
||||
await $eventRepo.createRelation(eventId, {
|
||||
fightId: targetFightId,
|
||||
team: "BLUE",
|
||||
fromType: "FIGHT",
|
||||
fromId: srcA,
|
||||
fromPlace: 1,
|
||||
});
|
||||
await $eventRepo.createRelation(eventId, {
|
||||
fightId: targetFightId,
|
||||
team: "RED",
|
||||
fromType: "FIGHT",
|
||||
fromId: srcB,
|
||||
fromPlace: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect back to event view
|
||||
await replace("#/event/" + eventId);
|
||||
}
|
||||
|
||||
// Helpers for template rendering with TS type guards
|
||||
function isTeam(slot: SeedSlot): slot is SeedTeamSlot {
|
||||
return slot.kind === "TEAM";
|
||||
}
|
||||
function isGroup(slot: SeedSlot): slot is SeedGroupSlot {
|
||||
return slot.kind === "GROUP";
|
||||
}
|
||||
function slotLabel(slot: SeedSlot): string {
|
||||
if (isTeam(slot)) return teams.get(slot.id)?.name ?? "Team";
|
||||
if (isGroup(slot)) {
|
||||
const gname = data.groups.find((g) => g.id === slot.groupId)?.name ?? "?";
|
||||
return `(Grp ${gname} Platz ${slot.place + 1})`;
|
||||
}
|
||||
if (slot.kind === "FIGHT") {
|
||||
const f = data.fights.find((x) => x.id === slot.fightId);
|
||||
const when = f ? new Date(f.start).toLocaleTimeString("de-DE", { timeStyle: "short" }) : "?";
|
||||
const vs = f ? `${f.blueTeam.kuerzel} vs. ${f.redTeam.kuerzel}` : "Kampf";
|
||||
return `${slot.place === 0 ? "Gewinner" : "Verlierer"} von ${vs} (${when})`;
|
||||
}
|
||||
return "???";
|
||||
}
|
||||
</script>
|
||||
|
||||
<Card class="p-4 mb-4">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold">Single Elimination Bracket</h2>
|
||||
<div class="flex gap-2">
|
||||
<Button onclick={shuffleTeams} aria-label="Shuffle Teams"><Shuffle size={16} /> Shuffle</Button>
|
||||
</div>
|
||||
</div>
|
||||
{#if seedSlots.length < 2}
|
||||
<p class="text-gray-400">Mindestens zwei Seeds benötigt.</p>
|
||||
{:else if bracketRounds.length === 0}
|
||||
<p class="text-yellow-400">Anzahl der Seeds muss eine Zweierpotenz sein (2,4,8,16,...). Aktuell: {seedSlots.length}</p>
|
||||
{/if}
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<Label>Seeds / Reihenfolge</Label>
|
||||
<ul class="mt-2 space-y-1">
|
||||
{#each seedSlots as slot, i (i)}
|
||||
<li class="flex items-center gap-2 text-sm">
|
||||
<span class="w-6 text-right">{i + 1}.</span>
|
||||
<span class="flex-1 truncate">{slotLabel(slot)}</span>
|
||||
<div class="flex gap-1">
|
||||
<Button size="sm" onclick={() => moveSlot(i, -1)} disabled={i === 0}>↑</Button>
|
||||
<Button size="sm" onclick={() => moveSlot(i, 1)} disabled={i === seedSlots.length - 1}>↓</Button>
|
||||
<Button size="sm" variant="destructive" onclick={() => removeSlot(i)}>✕</Button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label>Seed hinzufügen</Label>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedAddTeam}>
|
||||
{#each data.teams as t}<option value={t.id}>{t.name}</option>{/each}
|
||||
</select>
|
||||
<Button size="sm" onclick={addTeam}>Team</Button>
|
||||
<Button size="sm" onclick={addUnknown}>???</Button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
{#if data.groups.length > 0}
|
||||
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedGroup}>
|
||||
{#each data.groups as g}<option value={g.id}>{g.name}</option>{/each}
|
||||
</select>
|
||||
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedGroupPlace}>
|
||||
{#each Array(16) as _, idx}<option value={idx}>{idx + 1}. Platz</option>{/each}
|
||||
</select>
|
||||
<Button size="sm" onclick={addGroupPlace}>Gruppenplatz</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
{#if data.fights.length > 0}
|
||||
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedFight}>
|
||||
{#each data.fights as f}
|
||||
<option value={f.id}>{new Date(f.start).toLocaleTimeString("de-DE", { timeStyle: "short" })}: {f.blueTeam.kuerzel} vs. {f.redTeam.kuerzel}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedFightPlace}>
|
||||
<option value={0}>Gewinner</option>
|
||||
<option value={1}>Verlierer</option>
|
||||
</select>
|
||||
<Button size="sm" onclick={addFightPlace}>Kampfplatz</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">Gruppen- oder Kampfplätze erzeugen Relationen beim Generieren. ??? bleibt Platzhalter.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Startzeit</Label>
|
||||
<DateTimePicker bind:value={startTime} />
|
||||
<div class="mt-4">
|
||||
<Label>Rundenzeit: {roundTime}m</Label>
|
||||
<Slider type="single" bind:value={roundTime} step={5} min={5} max={60} />
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<Label>Startverzögerung: {startDelay}s</Label>
|
||||
<Slider type="single" bind:value={startDelay} step={5} min={0} max={60} />
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<Label>Spielmodus</Label>
|
||||
<Select type="single" bind:value={gamemode}>
|
||||
<SelectTrigger>{gamemode}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each selectableGamemodes as gm}
|
||||
<SelectItem value={gm.value}>{gm.name}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<Label>Map</Label>
|
||||
<Select type="single" bind:value={map}>
|
||||
<SelectTrigger>{map}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="%random%">Zufällige Map</SelectItem>
|
||||
{#each selectableMaps as mp}
|
||||
<SelectItem value={mp.value}>{mp.name}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<Label>Vorschau</Label>
|
||||
{#if bracketRounds.length > 0}
|
||||
<div class="flex gap-8 overflow-x-auto mt-2">
|
||||
{#each bracketRounds as round, r}
|
||||
<div>
|
||||
<h3 class="font-semibold mb-2">Runde {r + 1}</h3>
|
||||
<ul class="space-y-1">
|
||||
{#each round as fight, i}
|
||||
<li class="p-2 border border-gray-700 rounded text-sm">
|
||||
<span class="text-gray-400"
|
||||
>{new Intl.DateTimeFormat("de-DE", { hour: "2-digit", minute: "2-digit" }).format(
|
||||
startTime
|
||||
.copy()
|
||||
.add({ minutes: roundTime * r, seconds: startDelay * i })
|
||||
.toDate()
|
||||
)}</span
|
||||
>
|
||||
: {slotLabel(fight.blue)} vs. {slotLabel(fight.red)}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Button class="!p-4 fixed bottom-4 right-4" disabled={!generateDisabled} onclick={generateBracket} aria-label="Bracket generieren">
|
||||
<Plus />
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<!-- no component-scoped styles needed -->
|
||||
Reference in New Issue
Block a user