Refactor event and fight repositories to use numeric IDs for groups; update datetime picker input handling; add new generator components for event fights and group phases.
All checks were successful
SteamWarCI Build successful
All checks were successful
SteamWarCI Build successful
This commit is contained in:
@@ -29,12 +29,14 @@
|
||||
import Dashboard from "@components/moderator/pages/dashboard/Dashboard.svelte";
|
||||
import Event from "@components/moderator/pages/event/Event.svelte";
|
||||
import Pages from "@components/moderator/pages/pages/Pages.svelte";
|
||||
import Generator from "@components/moderator/pages/generators/Generator.svelte";
|
||||
|
||||
const routes: RouteDefinition = {
|
||||
"/": Dashboard,
|
||||
"/events": Events,
|
||||
"/players": Players,
|
||||
"/event/:id": Event,
|
||||
"/event/:id/generate": Generator,
|
||||
"/pages": Pages,
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
let groupSelectOpen = $state(false);
|
||||
|
||||
async function handleGroupSave(group: GroupUpdateEdit) {
|
||||
let g = await $eventRepo.createGroup(event.id.toString(), group);
|
||||
let g = await $eventRepo.createGroup(event.id, group);
|
||||
groups.push(g);
|
||||
value = g.id;
|
||||
createOpen = false;
|
||||
|
||||
@@ -202,8 +202,9 @@
|
||||
<MenubarItem onclick={() => (createOpen = true)}>Fight Erstellen</MenubarItem>
|
||||
<MenubarGroup>
|
||||
<MenubarGroupHeading>Generatoren</MenubarGroupHeading>
|
||||
<MenubarItem disabled>Gruppenphase</MenubarItem>
|
||||
<MenubarItem disabled>K.O. Phase</MenubarItem>
|
||||
<a href="#/event/{data.event.id}/generate">
|
||||
<MenubarItem>Gruppenphase</MenubarItem>
|
||||
</a>
|
||||
</MenubarGroup>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import type { ExtendedEvent } from "@components/types/event";
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@components/ui/tabs";
|
||||
import GroupPhaseGenerator from "./gens/group/GroupPhaseGenerator.svelte";
|
||||
let {
|
||||
data,
|
||||
}: {
|
||||
data: ExtendedEvent;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="m-4">
|
||||
<Tabs value="group">
|
||||
<TabsList class="mb-4">
|
||||
<TabsTrigger value="group">Gruppenphase</TabsTrigger>
|
||||
<TabsTrigger value="ko">K.O. Phase</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="group">
|
||||
<GroupPhaseGenerator {data} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
22
src/components/moderator/pages/generators/Generator.svelte
Normal file
22
src/components/moderator/pages/generators/Generator.svelte
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { eventRepo } from "@components/repo/event";
|
||||
import FightsGenerator from "./FightsGenerator.svelte";
|
||||
|
||||
interface Props {
|
||||
params: { id: number };
|
||||
}
|
||||
|
||||
let { params }: Props = $props();
|
||||
|
||||
let id = params.id;
|
||||
|
||||
let future = $eventRepo.getEvent(id.toString());
|
||||
</script>
|
||||
|
||||
{#await future}
|
||||
<p>Loading...</p>
|
||||
{:then event}
|
||||
<FightsGenerator data={event} />
|
||||
{:catch error}
|
||||
<p class="text-red-500">Error loading event: {error.message}</p>
|
||||
{/await}
|
||||
@@ -0,0 +1,306 @@
|
||||
<script lang="ts">
|
||||
import DragAcceptor from "@components/admin/pages/generate/DragAcceptor.svelte";
|
||||
import TeamChip from "@components/admin/pages/generate/TeamChip.svelte";
|
||||
import { eventRepo } from "@components/repo/event";
|
||||
import { fightRepo } from "@components/repo/fight";
|
||||
import { gamemodes, maps } from "@components/stores/stores";
|
||||
import type { ExtendedEvent } from "@components/types/event";
|
||||
import type { Team } from "@components/types/team";
|
||||
import { Button } from "@components/ui/button";
|
||||
import { Card } from "@components/ui/card";
|
||||
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||
import { Dialog } from "@components/ui/dialog";
|
||||
import { Input } from "@components/ui/input";
|
||||
import { Label } from "@components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||
import { Slider } from "@components/ui/slider";
|
||||
import { fromAbsolute, fromDate, parseDateTime, parseDuration } from "@internationalized/date";
|
||||
import dayjs from "dayjs";
|
||||
import { Plus } from "lucide-svelte";
|
||||
import { replace } from "svelte-spa-router";
|
||||
let {
|
||||
data,
|
||||
}: {
|
||||
data: ExtendedEvent;
|
||||
} = $props();
|
||||
|
||||
let teams = $derived(new Map<number, Team>(data.teams.map((team) => [team.id, team])));
|
||||
|
||||
let groups: number[][] = $state([]);
|
||||
let teamsNotInGroup = $derived(data.teams.filter((team) => !groups.flat().includes(team.id)));
|
||||
|
||||
function dragToNewGroup(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
let teamId = parseInt(event.dataTransfer!.getData("team"));
|
||||
groups = [...groups.map((value) => value.filter((value1) => value1 != teamId)), [teamId]].filter((value) => value.length > 0);
|
||||
}
|
||||
|
||||
function teamDragStart(ev: DragEvent, team: Team) {
|
||||
ev.dataTransfer!.setData("team", team.id.toString());
|
||||
}
|
||||
|
||||
let resetDragOver = $state(false);
|
||||
|
||||
function resetDragOverEvent(ev: DragEvent) {
|
||||
resetDragOver = true;
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
function dropReset(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
let teamId = parseInt(ev.dataTransfer!.getData("team"));
|
||||
groups = groups.map((group) => group.filter((team) => team !== teamId)).filter((group) => group.length > 0);
|
||||
resetDragOver = false;
|
||||
}
|
||||
|
||||
function dropGroup(ev: DragEvent, groupIndex: number) {
|
||||
ev.preventDefault();
|
||||
let teamId = parseInt(ev.dataTransfer!.getData("team"));
|
||||
groups = groups.map((group, i) => (i === groupIndex ? [...group.filter((value) => value != teamId), teamId] : group.filter((value) => value != teamId))).filter((group) => group.length > 0);
|
||||
}
|
||||
|
||||
let startTime = $state(fromAbsolute(data.event.start, "Europe/Berlin"));
|
||||
let gamemode = $state("");
|
||||
let map = $state("");
|
||||
|
||||
let selectableGamemodes = $derived(
|
||||
$gamemodes
|
||||
.map((gamemode) => {
|
||||
return {
|
||||
name: gamemode,
|
||||
value: gamemode,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
);
|
||||
|
||||
let mapsStore = $derived(maps(gamemode));
|
||||
let selectableMaps = $derived(
|
||||
$mapsStore
|
||||
.map((map) => {
|
||||
return {
|
||||
name: map,
|
||||
value: map,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
);
|
||||
|
||||
let roundTime = $state(30);
|
||||
let startDelay = $state(30);
|
||||
|
||||
let showAutoGrouping = $state(false);
|
||||
let groupCount = $state(Math.floor(data.teams.length / 2));
|
||||
|
||||
function createGroups() {
|
||||
let teams = data.teams.map((team) => team.id).sort(() => Math.random() - 0.5);
|
||||
groups = [];
|
||||
for (let i = 0; i < groupCount; i++) {
|
||||
groups.push([]);
|
||||
}
|
||||
while (teams.length > 0) {
|
||||
groups[teams.length % groupCount].push(teams.pop() as number);
|
||||
}
|
||||
showAutoGrouping = false;
|
||||
groups = groups.filter((group) => group.length > 0);
|
||||
}
|
||||
|
||||
function generateGroups(groups: number[][]): number[][][][] {
|
||||
const groupFights: number[][][][] = [];
|
||||
groups.forEach((group) => {
|
||||
let round = group.length + (group.length % 2) - 1;
|
||||
let groupFight = [];
|
||||
for (let i = 0; i < round; i++) {
|
||||
let availableTeams = [...group];
|
||||
if (group.length % 2 === 1) {
|
||||
availableTeams = availableTeams.filter((team, index) => index !== i);
|
||||
}
|
||||
let roundFights = [];
|
||||
while (availableTeams.length > 0) {
|
||||
let team1 = availableTeams.pop() as number;
|
||||
let team2 = availableTeams.at(i % availableTeams.length) as number;
|
||||
availableTeams = availableTeams.filter((team) => team !== team2);
|
||||
let fight = [team1, team2];
|
||||
fight.sort(() => Math.random() - 0.5);
|
||||
roundFights.push(fight);
|
||||
}
|
||||
groupFight.push(roundFights);
|
||||
}
|
||||
groupFights.push(groupFight);
|
||||
});
|
||||
return groupFights;
|
||||
}
|
||||
|
||||
let groupsFights = $derived(generateGroups(groups));
|
||||
|
||||
let generateDisabled = $derived(groupsFights.length > 0 && groupsFights.every((value) => value.every((value1) => value1.length > 0)) && gamemode !== "" && map !== "");
|
||||
|
||||
async function generateFights() {
|
||||
groupsFights.forEach((group, i) => {
|
||||
$eventRepo
|
||||
.createGroup(data.event.id, {
|
||||
name: "Gruppe " + (i + 1),
|
||||
type: "GROUP_STAGE",
|
||||
})
|
||||
.then((v) => {
|
||||
group.forEach((round, j) => {
|
||||
round.forEach(async (fight, k) => {
|
||||
const blueTeam = teams.get(fight[0])!;
|
||||
const redTeam = teams.get(fight[1])!;
|
||||
|
||||
let karte = map;
|
||||
|
||||
if (karte === "%random%") {
|
||||
karte = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
|
||||
}
|
||||
|
||||
await $fightRepo.createFight(data.event.id, {
|
||||
blueTeam: blueTeam.id,
|
||||
redTeam: redTeam.id,
|
||||
group: v.id,
|
||||
map: karte,
|
||||
spectatePort: null,
|
||||
spielmodus: gamemode,
|
||||
start: dayjs(
|
||||
startTime
|
||||
.copy()
|
||||
.add({
|
||||
minutes: roundTime * j,
|
||||
})
|
||||
.add({
|
||||
seconds: startDelay * (k + i * round.length),
|
||||
})
|
||||
.toDate()
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await replace("#/event/" + data.event.id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<Card
|
||||
id="reseter"
|
||||
class="flex w-fit p-2 border border-gray-700 rounded h-20 pt-6 relative {resetDragOver ? 'border-white' : ''}"
|
||||
ondragover={resetDragOverEvent}
|
||||
ondragleave={() => (resetDragOver = false)}
|
||||
ondrop={dropReset}
|
||||
role="group"
|
||||
>
|
||||
{#each teamsNotInGroup as team (team.id)}
|
||||
<TeamChip {team} ondragstart={(ev) => teamDragStart(ev, team)} />
|
||||
{/each}
|
||||
</Card>
|
||||
|
||||
<div class="flex items-center mr-4">
|
||||
<Button onclick={() => (showAutoGrouping = true)}>Automatische Gruppen</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-4 gap-4 border-b border-gray-700 pb-4">
|
||||
{#each groups as group, i (i)}
|
||||
<DragAcceptor ondrop={(ev) => dropGroup(ev, i)}>
|
||||
<h1>Gruppe {i + 1} ({group.length})</h1>
|
||||
{#each group as teamId (teamId)}
|
||||
<TeamChip team={teams.get(teamId)!} ondragstart={(ev) => teamDragStart(ev, teams.get(teamId)!)} />
|
||||
{/each}
|
||||
</DragAcceptor>
|
||||
{/each}
|
||||
<DragAcceptor ondrop={dragToNewGroup}>
|
||||
<h1>Neue Gruppe</h1>
|
||||
</DragAcceptor>
|
||||
</div>
|
||||
|
||||
<div class="border-b mt-4 border-gray-700 pb-4">
|
||||
<Label for="event-end">Startzeit</Label>
|
||||
<DateTimePicker bind:value={startTime} />
|
||||
<div class="mt-2">
|
||||
<Label for="event-roundtime">Rundenzeit: {roundTime}m</Label>
|
||||
<Slider id="event-roundtime" type="single" bind:value={roundTime} step={1} min={5} max={60} />
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<Label for="event-member">Startverzögerung: {startDelay}</Label>
|
||||
<Slider id="event-member" type="single" bind:value={startDelay} step={1} min={0} max={30} />
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<Label for="fight-gamemode">Spielmodus</Label>
|
||||
<Select type="single" bind:value={gamemode}>
|
||||
<SelectTrigger id="fight-gamemode">{gamemode}</SelectTrigger>
|
||||
<SelectContent>
|
||||
{#each selectableGamemodes as gamemodeOption}
|
||||
<SelectItem value={gamemodeOption.value}>{gamemodeOption.name}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<Label for="fight-maps">Map</Label>
|
||||
<Select type="single" bind:value={map}>
|
||||
<SelectTrigger id="fight-maps">{map}</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="%random%">Zufällige Map</SelectItem>
|
||||
{#each selectableMaps as mapOption}
|
||||
<SelectItem value={mapOption.value}>{mapOption.name}</SelectItem>
|
||||
{/each}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center mx-2">
|
||||
{#each groupsFights as fightsGroup, i}
|
||||
<div>
|
||||
<h1 class="text-4xl">Gruppe: {i + 1}</h1>
|
||||
{#each fightsGroup as fightsRound, j}
|
||||
<div class="border-b border-gray-700">
|
||||
<h1 class="text-2xl">Runde: {j + 1}</h1>
|
||||
{#each fightsRound as fightTeams, k}
|
||||
<div class="text-left p-4">
|
||||
<span class="p-2 border border-gray-700 rounded"
|
||||
>{new Intl.DateTimeFormat("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "2-digit",
|
||||
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}).format(
|
||||
startTime
|
||||
.copy()
|
||||
.add({
|
||||
minutes: roundTime * j,
|
||||
seconds: startDelay * (k + i * fightsRound.length),
|
||||
})
|
||||
.toDate()
|
||||
)}</span
|
||||
>
|
||||
{teams.get(fightTeams[0])!.name} vs. {teams.get(fightTeams[1])!.name}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<Button class="!p-4 fixed bottom-4 right-4" disabled={!generateDisabled} onclick={generateFights}>
|
||||
<Plus />
|
||||
</Button>
|
||||
|
||||
<style lang="scss">
|
||||
:global(#reseter::before) {
|
||||
content: "Reset";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:global(#reseter) {
|
||||
min-width: 14rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user