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:
@ -18,12 +18,14 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import { Card } from "@components/ui/card";
|
||||
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
children?: import("svelte").Snippet;
|
||||
ondrop: (event: DragEvent) => void;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
let { children, ondrop }: Props = $props();
|
||||
|
||||
let dragover = $state(false);
|
||||
|
||||
@ -32,19 +34,16 @@
|
||||
dragover = true;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function handleDrop(ev: DragEvent) {
|
||||
ev.preventDefault();
|
||||
dragover = false;
|
||||
dispatch("drop", ev);
|
||||
ondrop(ev);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-56 bg-gray-800 p-4 rounded" class:border={dragover} class:m-px={!dragover} ondrop={handleDrop}
|
||||
ondragover={handleDragOver} ondragleave={() => dragover = false} role="none">
|
||||
<Card class="w-56 p-4 rounded m-px {dragover ? 'border-white' : ''}" ondrop={handleDrop} ondragover={handleDragOver} ondragleave={() => (dragover = false)} role="none">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<style>
|
||||
div {
|
||||
|
||||
@ -18,28 +18,28 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { createBubbler } from 'svelte/legacy';
|
||||
|
||||
const bubble = createBubbler();
|
||||
import type {Team} from "@type/team.ts";
|
||||
import {brightness, colorFromTeam, lighten} from "../../util";
|
||||
import type { Team } from "@type/team.ts";
|
||||
import { brightness, colorFromTeam, lighten } from "../../util";
|
||||
|
||||
interface Props {
|
||||
team: Team;
|
||||
ondragstart: (event: DragEvent) => void;
|
||||
}
|
||||
|
||||
let { team }: Props = $props();
|
||||
let { team, ondragstart }: Props = $props();
|
||||
|
||||
let hover = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="rounded w-fit p-2 border-gray-600 border cursor-grab select-none m-1 flex place-items-center"
|
||||
<div
|
||||
class="rounded w-fit p-2 border-gray-600 border cursor-grab select-none m-1 flex place-items-center"
|
||||
style:background-color={hover ? lighten(colorFromTeam(team)) : colorFromTeam(team)}
|
||||
class:text-black={brightness(colorFromTeam(team))} draggable="true"
|
||||
ondragstart={bubble('dragstart')}
|
||||
onmouseenter={() => hover = true}
|
||||
onmouseleave={() => hover = false}
|
||||
role="figure">
|
||||
class:text-black={brightness(colorFromTeam(team))}
|
||||
draggable="true"
|
||||
{ondragstart}
|
||||
onmouseenter={() => (hover = true)}
|
||||
onmouseleave={() => (hover = false)}
|
||||
role="figure"
|
||||
>
|
||||
<span>{team.name}</span>
|
||||
</div>
|
||||
|
||||
|
||||
@ -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>
|
||||
@ -150,7 +150,7 @@ export class EventRepo {
|
||||
.then((value) => value.json())
|
||||
.then((value) => z.array(ResponseGroupsSchema).parse(value));
|
||||
}
|
||||
public async createGroup(eventId: string, group: CreateEventGroup): Promise<ResponseGroups> {
|
||||
public async createGroup(eventId: number, group: CreateEventGroup): Promise<ResponseGroups> {
|
||||
CreateEventGroupSchema.parse(group);
|
||||
return await fetchWithToken(this.token, `/events/${eventId}/groups`, {
|
||||
method: "POST",
|
||||
|
||||
@ -31,7 +31,7 @@ export interface CreateFight {
|
||||
redTeam: number;
|
||||
start: Dayjs;
|
||||
spectatePort: number | null;
|
||||
group: string | null;
|
||||
group: number | null;
|
||||
}
|
||||
|
||||
export interface UpdateFight {
|
||||
@ -57,7 +57,6 @@ export class FightRepo {
|
||||
return await fetchWithToken(this.token, `/events/${eventId}/fights`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
event: eventId,
|
||||
spielmodus: fight.spielmodus,
|
||||
map: fight.map,
|
||||
blueTeam: fight.blueTeam,
|
||||
|
||||
@ -81,7 +81,7 @@
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent class="w-auto p-0">
|
||||
<Input type="datetime-local" value={value.toDate().toISOString().slice(0, 16)} onchange={(e) => handleDateSelect(fromDate(e.target.valueAsDate, "Europe/Berlin"))} />
|
||||
<Input type="datetime-local" value={value.toString().slice(0, 16)} onchange={(e) => handleDateSelect(fromDate(e.target.valueAsDate, "Europe/Berlin"))} />
|
||||
<div class="sm:flex">
|
||||
<Calendar mode="single" bind:value onValueChange={(date) => handleDateSelect(date)} initialFocus />
|
||||
<div class="flex flex-col sm:flex-row sm:h-[300px] divide-y sm:divide-y-0 sm:divide-x">
|
||||
|
||||
@ -13,7 +13,7 @@ SteamWar ist ein Minecraft Java Server.
|
||||
<Tabs>
|
||||
<TabItem label="Java Edition">
|
||||
- IP: `steamwar.de`
|
||||
- Empholene Version: `1.21.5`
|
||||
- Empholene Version: `1.21.6`
|
||||
</TabItem>
|
||||
<TabItem label="Bedrock Edition">
|
||||
- IP: `steamwar.de`
|
||||
|
||||
Reference in New Issue
Block a user