Compare commits
24 Commits
7ea7536367
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
35765b90e6
|
|||
|
fa7e68ca10
|
|||
|
f507dce94a
|
|||
|
1ed1901311
|
|||
|
|
63d03f2226 | ||
|
e27b4fb0f4
|
|||
|
8fa1d41639
|
|||
|
f13305d116
|
|||
| ff59ac3747 | |||
|
09035e3acd
|
|||
|
9be8702e6a
|
|||
|
ffe875260d
|
|||
|
64b82eddff
|
|||
|
e3432ce7bd
|
|||
|
6cdf2e0933
|
|||
|
b0a9d56216
|
|||
|
3ffc715929
|
|||
|
9589a496c0
|
|||
|
bdebe768b2
|
|||
| ab4d4a1a91 | |||
|
e1220ff5ac
|
|||
|
3bde299ea5
|
|||
|
5f5988e270
|
|||
|
7ec678ae7d
|
@@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||||
"style": "default",
|
|
||||||
"tailwind": {
|
"tailwind": {
|
||||||
"config": "tailwind.config.js",
|
|
||||||
"css": "src\\styles\\app.css",
|
"css": "src\\styles\\app.css",
|
||||||
"baseColor": "slate"
|
"baseColor": "slate"
|
||||||
},
|
},
|
||||||
@@ -10,8 +8,9 @@
|
|||||||
"components": "$lib/components",
|
"components": "$lib/components",
|
||||||
"utils": "$lib/components/utils",
|
"utils": "$lib/components/utils",
|
||||||
"ui": "$lib/components/ui",
|
"ui": "$lib/components/ui",
|
||||||
"hooks": "$lib/hooks"
|
"hooks": "$lib/hooks",
|
||||||
|
"lib": "$lib"
|
||||||
},
|
},
|
||||||
"typescript": true,
|
"typescript": true,
|
||||||
"registry": "https://next.shadcn-svelte.com/registry"
|
"registry": "https://tw3.shadcn-svelte.com/registry/default"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!--
|
<!--
|
||||||
- This file is a part of the SteamWar software.
|
- This file is a part of the SteamWar software.
|
||||||
-
|
-
|
||||||
- Copyright (C) 2023 SteamWar.de-Serverteam
|
- Copyright (C) 2026 SteamWar.de-Serverteam
|
||||||
-
|
-
|
||||||
- This program is free software: you can redistribute it and/or modify
|
- This program is free software: you can redistribute it and/or modify
|
||||||
- it under the terms of the GNU Affero General Public License as published by
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
|||||||
@@ -18,83 +18,103 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Input, Label, Select} from "flowbite-svelte";
|
import { Input, Label, Select } from "flowbite-svelte";
|
||||||
import TypeAheadSearch from "./TypeAheadSearch.svelte";
|
import TypeAheadSearch from "./TypeAheadSearch.svelte";
|
||||||
import {gamemodes, groups, maps, players} from "@stores/stores.ts";
|
import { gamemodes, groups, maps } from "@stores/stores.ts";
|
||||||
import type {Team} from "@type/team.ts";
|
import type { Team } from "@type/team.ts";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
teams?: Team[];
|
teams?: Team[];
|
||||||
blueTeam: string;
|
blueTeam: string;
|
||||||
redTeam: string;
|
redTeam: string;
|
||||||
start?: string;
|
start?: string;
|
||||||
gamemode?: string;
|
gamemode?: string;
|
||||||
map?: string;
|
map?: string;
|
||||||
spectatePort?: string | null;
|
spectatePort?: string | null;
|
||||||
group?: string | null;
|
group?: string | null;
|
||||||
groupSearch?: string;
|
groupSearch?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
teams = [],
|
teams = [],
|
||||||
blueTeam = $bindable(),
|
blueTeam = $bindable(),
|
||||||
redTeam = $bindable(),
|
redTeam = $bindable(),
|
||||||
start = $bindable(""),
|
start = $bindable(""),
|
||||||
gamemode = $bindable(""),
|
gamemode = $bindable(""),
|
||||||
map = $bindable(""),
|
map = $bindable(""),
|
||||||
spectatePort = $bindable(null),
|
spectatePort = $bindable(null),
|
||||||
group = $bindable(""),
|
group = $bindable(""),
|
||||||
groupSearch = $bindable("")
|
groupSearch = $bindable(""),
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let selectableTeams = $derived(teams.map(team => {
|
let selectableTeams = $derived(
|
||||||
return {
|
teams
|
||||||
name: team.name,
|
.map((team) => {
|
||||||
value: team.id.toString()
|
return {
|
||||||
};
|
name: team.name,
|
||||||
}).sort((a, b) => a.name.localeCompare(b.name)));
|
value: team.id.toString(),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
);
|
||||||
|
|
||||||
let selectableGamemodes = $derived($gamemodes.map(gamemode => {
|
let selectableGamemodes = $derived(
|
||||||
return {
|
$gamemodes
|
||||||
name: gamemode,
|
.map((gamemode) => {
|
||||||
value: gamemode
|
return {
|
||||||
};
|
name: gamemode,
|
||||||
}).sort((a, b) => a.name.localeCompare(b.name)));
|
value: gamemode,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
);
|
||||||
let customGamemode = $derived(!selectableGamemodes.some((e) => e.name === gamemode) && gamemode !== "");
|
let customGamemode = $derived(!selectableGamemodes.some((e) => e.name === gamemode) && gamemode !== "");
|
||||||
let selectableCustomGamemode = $derived([
|
let selectableCustomGamemode = $derived([
|
||||||
...selectableGamemodes, {
|
...selectableGamemodes,
|
||||||
|
{
|
||||||
name: gamemode + " (custom)",
|
name: gamemode + " (custom)",
|
||||||
value: gamemode
|
value: gamemode,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let mapsStore = $derived(maps(gamemode));
|
let mapsStore = $derived(maps(gamemode));
|
||||||
let selectableMaps = $derived($mapsStore.map(map => {
|
let selectableMaps = $derived(
|
||||||
return {
|
$mapsStore
|
||||||
name: map,
|
.map((map) => {
|
||||||
value: map
|
return {
|
||||||
};
|
name: map,
|
||||||
}).sort((a, b) => a.name.localeCompare(b.name)));
|
value: map,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
);
|
||||||
let customMap = $derived(!selectableMaps.some((e) => e.name === map) && map !== "");
|
let customMap = $derived(!selectableMaps.some((e) => e.name === map) && map !== "");
|
||||||
let selectableCustomMaps = $derived([
|
let selectableCustomMaps = $derived([
|
||||||
...selectableMaps, {
|
...selectableMaps,
|
||||||
|
{
|
||||||
name: map + " (custom)",
|
name: map + " (custom)",
|
||||||
value: map
|
value: map,
|
||||||
}
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let selectableGroups = $derived([{
|
let selectableGroups = $derived([
|
||||||
name: "None",
|
{
|
||||||
value: ""
|
name: "None",
|
||||||
}, {
|
value: "",
|
||||||
value: groupSearch,
|
},
|
||||||
name: `Create: '${groupSearch}'`
|
{
|
||||||
}, ...$groups.map(group => {
|
value: groupSearch,
|
||||||
return {
|
name: `Create: '${groupSearch}'`,
|
||||||
name: group,
|
},
|
||||||
value: group
|
...$groups
|
||||||
};
|
.map((group) => {
|
||||||
}).sort((a, b) => a.name.localeCompare(b.name))]);
|
return {
|
||||||
|
name: group,
|
||||||
|
value: group,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||||
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="m-2">
|
<div class="m-2">
|
||||||
@@ -107,32 +127,29 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<Label for="fight-start">Start</Label>
|
<Label for="fight-start">Start</Label>
|
||||||
<Input id="fight-start" bind:value={start} >
|
<Input id="fight-start" bind:value={start}>
|
||||||
{#snippet children({ props })}
|
{#snippet children({ props })}
|
||||||
<input type="datetime-local" {...props} bind:value={start}/>
|
<input type="datetime-local" {...props} bind:value={start} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Input>
|
</Input>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2">
|
<div class="m-2">
|
||||||
<Label for="fight-gamemode">Gamemode</Label>
|
<Label for="fight-gamemode">Gamemode</Label>
|
||||||
<Select items={customGamemode ? selectableCustomGamemode : selectableGamemodes} bind:value={gamemode}
|
<Select items={customGamemode ? selectableCustomGamemode : selectableGamemodes} bind:value={gamemode} id="fight-gamemode"></Select>
|
||||||
id="fight-gamemode"></Select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2">
|
<div class="m-2">
|
||||||
<Label for="fight-maps">Map</Label>
|
<Label for="fight-maps">Map</Label>
|
||||||
<Select items={customMap ? selectableCustomMaps : selectableMaps} bind:value={map} id="fight-maps"
|
<Select items={customMap ? selectableCustomMaps : selectableMaps} bind:value={map} id="fight-maps" disabled={customGamemode} class={customGamemode ? "cursor-not-allowed" : ""}></Select>
|
||||||
disabled={customGamemode} class={customGamemode ? "cursor-not-allowed" : ""}></Select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2">
|
<div class="m-2">
|
||||||
<Label for="fight-port">Spectate Port</Label>
|
<Label for="fight-port">Spectate Port</Label>
|
||||||
<Input id="fight-port" bind:value={spectatePort} >
|
<Input id="fight-port" bind:value={spectatePort}>
|
||||||
{#snippet children({ props })}
|
{#snippet children({ props })}
|
||||||
<input type="number" inputmode="numeric" {...props} bind:value={spectatePort}/>
|
<input type="number" inputmode="numeric" {...props} bind:value={spectatePort} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Input>
|
</Input>
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2">
|
<div class="m-2">
|
||||||
<Label for="fight-kampf">Group</Label>
|
<Label for="fight-kampf">Group</Label>
|
||||||
<TypeAheadSearch items={selectableGroups} bind:selected={group} bind:searchValue={groupSearch}
|
<TypeAheadSearch items={selectableGroups} bind:selected={group} bind:searchValue={groupSearch} all></TypeAheadSearch>
|
||||||
all></TypeAheadSearch>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,19 +18,18 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Navbar, NavBrand, Spinner, TabItem, Tabs} from "flowbite-svelte";
|
import { Navbar, NavBrand, Spinner, TabItem, Tabs } from "flowbite-svelte";
|
||||||
import EventEdit from "./event/EventEdit.svelte";
|
import EventEdit from "./event/EventEdit.svelte";
|
||||||
import {ArrowLeftOutline} from "flowbite-svelte-icons";
|
import { ArrowLeftOutline } from "flowbite-svelte-icons";
|
||||||
import FightList from "./event/FightList.svelte";
|
|
||||||
import TeamList from "./event/TeamList.svelte";
|
import TeamList from "./event/TeamList.svelte";
|
||||||
import {eventRepo} from "@repo/event.ts";
|
import { eventRepo } from "@repo/event.ts";
|
||||||
import RefereesList from "@components/admin/pages/event/RefereesList.svelte";
|
import RefereesList from "@components/admin/pages/event/RefereesList.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: { id: number };
|
params: { id: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
let { params }: Props = $props();
|
let { params }: Props = $props();
|
||||||
|
|
||||||
let id = params.id;
|
let id = params.id;
|
||||||
let event = $eventRepo.getEvent(id.toString());
|
let event = $eventRepo.getEvent(id.toString());
|
||||||
@@ -38,44 +37,43 @@
|
|||||||
|
|
||||||
{#await event}
|
{#await event}
|
||||||
<div class="h-screen w-screen grid place-items-center">
|
<div class="h-screen w-screen grid place-items-center">
|
||||||
<Spinner size={16}/>
|
<Spinner size={16} />
|
||||||
</div>
|
</div>
|
||||||
{:then data}
|
{:then data}
|
||||||
<Navbar >
|
<Navbar>
|
||||||
{#snippet children({ hidden, toggle })}
|
{#snippet children({ hidden, toggle })}
|
||||||
<NavBrand href="#">
|
<NavBrand href="#">
|
||||||
<ArrowLeftOutline></ArrowLeftOutline>
|
<ArrowLeftOutline></ArrowLeftOutline>
|
||||||
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white">
|
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white">
|
||||||
{data.event.name}
|
{data.event.name}
|
||||||
</span>
|
</span>
|
||||||
</NavBrand>
|
</NavBrand>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
<Tabs style="pill" class="mx-4 flex shadow-lg border-b-2 border-gray-700 pb-2" contentClass="">
|
<Tabs style="pill" class="mx-4 flex shadow-lg border-b-2 border-gray-700 pb-2" contentClass="">
|
||||||
<TabItem open>
|
<TabItem open>
|
||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<span >Event</span>
|
<span>Event</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<EventEdit {data}/>
|
<EventEdit {data} />
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem>
|
<TabItem>
|
||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<span >Teams</span>
|
<span>Teams</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<TeamList {data}/>
|
<TeamList {data} />
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem>
|
<TabItem>
|
||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<span >Schiedsrichter</span>
|
<span>Schiedsrichter</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<RefereesList {data}/>
|
<RefereesList {data} />
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem>
|
<TabItem>
|
||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<span >Kämpfe</span>
|
<span>Kämpfe</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<FightList {data}/>
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
{:catch error}
|
{:catch error}
|
||||||
|
|||||||
@@ -18,21 +18,37 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { run, preventDefault } from 'svelte/legacy';
|
import { run, preventDefault } from "svelte/legacy";
|
||||||
|
|
||||||
import {Button, Card, Checkbox, Input, Label, Navbar, NavBrand, Radio, Spinner} from "flowbite-svelte";
|
import { Button, Card, Checkbox, Input, Label, Navbar, NavBrand, Radio, Spinner } from "flowbite-svelte";
|
||||||
import {ArrowLeftOutline} from "flowbite-svelte-icons";
|
import { ArrowLeftOutline } from "flowbite-svelte-icons";
|
||||||
import {players} from "@stores/stores.ts";
|
import { capitalize } from "../util.ts";
|
||||||
import {capitalize} from "../util.ts";
|
import { permsRepo } from "@repo/perms.ts";
|
||||||
import {permsRepo} from "@repo/perms.ts";
|
import { me } from "@stores/me.ts";
|
||||||
import {me} from "@stores/me.ts";
|
|
||||||
import SWButton from "@components/styled/SWButton.svelte";
|
import SWButton from "@components/styled/SWButton.svelte";
|
||||||
import SWModal from "@components/styled/SWModal.svelte";
|
import SWModal from "@components/styled/SWModal.svelte";
|
||||||
import {userRepo} from "@repo/user.ts";
|
import { userRepo } from "@repo/user.ts";
|
||||||
|
import { dataRepo } from "@repo/data.ts";
|
||||||
|
import type { Player } from "@type/data";
|
||||||
|
|
||||||
let search = $state("");
|
let search = $state("");
|
||||||
|
let playersList: Player[] = $state([]);
|
||||||
|
let debounceTimer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
function fetchPlayers(searchTerm: string) {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(async () => {
|
||||||
|
const res = await $dataRepo.queryPlayers(searchTerm || undefined, undefined, undefined, 100, 0, undefined, undefined);
|
||||||
|
playersList = res.players;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
fetchPlayers(search);
|
||||||
|
});
|
||||||
|
|
||||||
let selectedPlayer: string | null = $state(null);
|
let selectedPlayer: string | null = $state(null);
|
||||||
|
let selectedPlayerName: string = $state("");
|
||||||
let playerPerms = $state(loadPlayer(selectedPlayer));
|
let playerPerms = $state(loadPlayer(selectedPlayer));
|
||||||
|
|
||||||
let prefixEdit = $state("PREFIX_NONE");
|
let prefixEdit = $state("PREFIX_NONE");
|
||||||
@@ -46,7 +62,7 @@
|
|||||||
if (!id) {
|
if (!id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return $permsRepo.getPerms(id).then(value => {
|
return $permsRepo.getPerms(id).then((value) => {
|
||||||
activePerms = value.perms;
|
activePerms = value.perms;
|
||||||
prefixEdit = value.prefix.name;
|
prefixEdit = value.prefix.name;
|
||||||
return value;
|
return value;
|
||||||
@@ -56,7 +72,7 @@
|
|||||||
function togglePerm(perm: string) {
|
function togglePerm(perm: string) {
|
||||||
return () => {
|
return () => {
|
||||||
if (activePerms.includes(perm)) {
|
if (activePerms.includes(perm)) {
|
||||||
activePerms = activePerms.filter(value => value !== perm);
|
activePerms = activePerms.filter((value) => value !== perm);
|
||||||
} else {
|
} else {
|
||||||
activePerms = [...activePerms, perm];
|
activePerms = [...activePerms, perm];
|
||||||
}
|
}
|
||||||
@@ -64,7 +80,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save() {
|
||||||
playerPerms!.then(async perms => {
|
playerPerms!.then(async (perms) => {
|
||||||
if (perms.prefix.name != prefixEdit) {
|
if (perms.prefix.name != prefixEdit) {
|
||||||
await $permsRepo.setPrefix(selectedPlayer!, prefixEdit);
|
await $permsRepo.setPrefix(selectedPlayer!, prefixEdit);
|
||||||
}
|
}
|
||||||
@@ -99,24 +115,20 @@
|
|||||||
resetPasswordRepeat = "";
|
resetPasswordRepeat = "";
|
||||||
resetPasswordModal = false;
|
resetPasswordModal = false;
|
||||||
}
|
}
|
||||||
let lowerCaseSearch = $derived(search.toLowerCase());
|
|
||||||
let filteredPlayers = $derived($players.filter(value => value.name.toLowerCase().includes(lowerCaseSearch)));
|
|
||||||
let player = $derived($players.find(value => value.uuid === selectedPlayer));
|
|
||||||
run(() => {
|
run(() => {
|
||||||
playerPerms = loadPlayer(selectedPlayer);
|
playerPerms = loadPlayer(selectedPlayer);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-screen overflow-hidden">
|
<div class="flex flex-col h-screen overflow-hidden">
|
||||||
<Navbar >
|
<Navbar>
|
||||||
{#snippet children({ hidden, toggle })}
|
{#snippet children({ hidden, toggle })}
|
||||||
<NavBrand href="#">
|
<NavBrand href="#">
|
||||||
<ArrowLeftOutline></ArrowLeftOutline>
|
<ArrowLeftOutline></ArrowLeftOutline>
|
||||||
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white">
|
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white"> Permissions </span>
|
||||||
Permissions
|
</NavBrand>
|
||||||
</span>
|
{/snippet}
|
||||||
</NavBrand>
|
|
||||||
{/snippet}
|
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
|
||||||
<div class="p-4 flex-1 overflow-hidden">
|
<div class="p-4 flex-1 overflow-hidden">
|
||||||
@@ -124,14 +136,19 @@
|
|||||||
<Card class="h-full flex flex-col overflow-hidden !max-w-full">
|
<Card class="h-full flex flex-col overflow-hidden !max-w-full">
|
||||||
<div class="border-b border-b-gray-600 pb-2">
|
<div class="border-b border-b-gray-600 pb-2">
|
||||||
<Label for="user_search" class="mb-2">Search Users...</Label>
|
<Label for="user_search" class="mb-2">Search Users...</Label>
|
||||||
<Input type="text" id="user_search" placeholder="Name..." bind:value={search}/>
|
<Input type="text" id="user_search" placeholder="Name..." bind:value={search} />
|
||||||
</div>
|
</div>
|
||||||
{#if filteredPlayers.length < 100}
|
{#if playersList.length < 100}
|
||||||
<ul class="flex-1 overflow-scroll">
|
<ul class="flex-1 overflow-scroll">
|
||||||
{#each filteredPlayers as player (player.uuid)}
|
{#each playersList as player (player.uuid)}
|
||||||
<li class="p-4 transition-colors hover:bg-gray-700 cursor-pointer"
|
<li
|
||||||
|
class="p-4 transition-colors hover:bg-gray-700 cursor-pointer"
|
||||||
class:text-orange-500={player.uuid === selectedPlayer}
|
class:text-orange-500={player.uuid === selectedPlayer}
|
||||||
onclick={preventDefault(() => selectedPlayer = player.uuid)}>
|
onclick={preventDefault(() => {
|
||||||
|
selectedPlayer = player.uuid;
|
||||||
|
selectedPlayerName = player.name;
|
||||||
|
})}
|
||||||
|
>
|
||||||
{player.name}
|
{player.name}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -140,7 +157,7 @@
|
|||||||
</Card>
|
</Card>
|
||||||
<Card class="!max-w-full" style="grid-column: 2/4">
|
<Card class="!max-w-full" style="grid-column: 2/4">
|
||||||
{#if selectedPlayer}
|
{#if selectedPlayer}
|
||||||
<h1 class="text-3xl">{player.name}</h1>
|
<h1 class="text-3xl">{selectedPlayerName}</h1>
|
||||||
{#await permsFuture}
|
{#await permsFuture}
|
||||||
<Spinner></Spinner>
|
<Spinner></Spinner>
|
||||||
{:then perms}
|
{:then perms}
|
||||||
@@ -149,39 +166,27 @@
|
|||||||
{:then player}
|
{:then player}
|
||||||
<h1>Prefix</h1>
|
<h1>Prefix</h1>
|
||||||
{#each Object.entries(perms.prefixes) as [key, prefix]}
|
{#each Object.entries(perms.prefixes) as [key, prefix]}
|
||||||
<Radio name="prefix" bind:group={prefixEdit}
|
<Radio name="prefix" bind:group={prefixEdit} value={prefix.name}>{capitalize(prefix.name.substring(7).toLowerCase())}</Radio>
|
||||||
value={prefix.name}>{capitalize(prefix.name.substring(7).toLowerCase())}</Radio>
|
|
||||||
{/each}
|
{/each}
|
||||||
<h1>Permissions</h1>
|
<h1>Permissions</h1>
|
||||||
{#each perms.perms as perm}
|
{#each perms.perms as perm}
|
||||||
<Checkbox checked={activePerms.includes(perm)}
|
<Checkbox checked={activePerms.includes(perm)} onclick={togglePerm(perm)}>{capitalize(perm.toLowerCase())}</Checkbox>
|
||||||
onclick={togglePerm(perm)}>{capitalize(perm.toLowerCase())}</Checkbox>
|
|
||||||
{/each}
|
{/each}
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<Button disabled={prefixEdit === (player?.prefix.name ?? "") && activePerms === (player?.perms ?? [])}
|
<Button disabled={prefixEdit === (player?.prefix.name ?? "") && activePerms === (player?.perms ?? [])} onclick={save}>Save</Button>
|
||||||
onclick={save}>Save
|
|
||||||
</Button>
|
|
||||||
{#if $me != null && $me.perms.includes("ADMINISTRATION")}
|
{#if $me != null && $me.perms.includes("ADMINISTRATION")}
|
||||||
<Button onclick={() => resetPasswordModal = true}>
|
<Button onclick={() => (resetPasswordModal = true)}>Reset Password</Button>
|
||||||
Reset Password
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SWModal bind:open={resetPasswordModal} title="Reset Password">
|
<SWModal bind:open={resetPasswordModal} title="Reset Password">
|
||||||
<Label for="new_password">New Password</Label>
|
<Label for="new_password">New Password</Label>
|
||||||
<Input type="password" id="new_password" placeholder="New Password" bind:value={resetPassword}/>
|
<Input type="password" id="new_password" placeholder="New Password" bind:value={resetPassword} />
|
||||||
<Label for="repeat_password">Repeat Password</Label>
|
<Label for="repeat_password">Repeat Password</Label>
|
||||||
<Input type="password" id="repeat_password" placeholder="Repeat Password" bind:value={resetPasswordRepeat}/>
|
<Input type="password" id="repeat_password" placeholder="Repeat Password" bind:value={resetPasswordRepeat} />
|
||||||
|
|
||||||
{#snippet footer()}
|
{#snippet footer()}
|
||||||
|
<Button class="ml-auto mr-4" onclick={resetResetPassword}>Cancel</Button>
|
||||||
<Button class="ml-auto mr-4" onclick={resetResetPassword}>
|
<Button disabled={resetPassword === "" || resetPassword !== resetPasswordRepeat} onclick={resetPW}>Reset Password</Button>
|
||||||
Cancel
|
{/snippet}
|
||||||
</Button>
|
|
||||||
<Button disabled={resetPassword === "" || resetPassword !== resetPasswordRepeat} onclick={resetPW}>
|
|
||||||
Reset Password
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/snippet}
|
|
||||||
</SWModal>
|
</SWModal>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,312 +0,0 @@
|
|||||||
<!--
|
|
||||||
- This file is a part of the SteamWar software.
|
|
||||||
-
|
|
||||||
- Copyright (C) 2023 SteamWar.de-Serverteam
|
|
||||||
-
|
|
||||||
- This program is free software: you can redistribute it and/or modify
|
|
||||||
- it under the terms of the GNU Affero General Public License as published by
|
|
||||||
- the Free Software Foundation, either version 3 of the License, or
|
|
||||||
- (at your option) any later version.
|
|
||||||
-
|
|
||||||
- This program is distributed in the hope that it will be useful,
|
|
||||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
- GNU Affero General Public License for more details.
|
|
||||||
-
|
|
||||||
- You should have received a copy of the GNU Affero General Public License
|
|
||||||
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import type {EventFight, ExtendedEvent} from "@type/event.ts";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Checkbox, Input, Label,
|
|
||||||
Modal,
|
|
||||||
SpeedDial,
|
|
||||||
SpeedDialButton,
|
|
||||||
Toolbar,
|
|
||||||
ToolbarButton,
|
|
||||||
ToolbarGroup,
|
|
||||||
Tooltip
|
|
||||||
} from "flowbite-svelte";
|
|
||||||
import {
|
|
||||||
ArrowsRepeatOutline, CalendarWeekOutline,
|
|
||||||
PlusOutline, ProfileCardOutline, TrashBinOutline, UsersGroupOutline,
|
|
||||||
} from "flowbite-svelte-icons";
|
|
||||||
import FightCard from "./FightCard.svelte";
|
|
||||||
import CreateFightModal from "./modals/CreateFightModal.svelte";
|
|
||||||
import {groups, players} from "@stores/stores.ts";
|
|
||||||
import TypeAheadSearch from "../../components/TypeAheadSearch.svelte";
|
|
||||||
import {fightRepo, type UpdateFight} from "@repo/fight.ts";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
import duration from "dayjs/plugin/duration";
|
|
||||||
|
|
||||||
dayjs.extend(duration);
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: ExtendedEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { data = $bindable() }: Props = $props();
|
|
||||||
|
|
||||||
let createOpen = $state(false);
|
|
||||||
let fights = $state(data.fights);
|
|
||||||
let selectedFights: Set<EventFight> = $state(new Set());
|
|
||||||
|
|
||||||
let groupsMap = $derived(new Set(fights.map(fight => fight.group)));
|
|
||||||
let groupedFights = $derived(Array.from(groupsMap).map(group => {
|
|
||||||
return {
|
|
||||||
group: group,
|
|
||||||
fights: fights.filter(fight => fight.group === group)
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
|
|
||||||
function cycleSelect() {
|
|
||||||
if (selectedFights.size === fights.length) {
|
|
||||||
selectedFights = new Set();
|
|
||||||
} else if (selectedFights.size === 0) {
|
|
||||||
selectedFights = new Set(fights.filter(fight => fight.start > Date.now()));
|
|
||||||
|
|
||||||
if (selectedFights.size === 0) {
|
|
||||||
selectedFights = new Set(fights);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selectedFights = new Set(fights);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cycleGroup(groupFights: EventFight[]) {
|
|
||||||
if (groupFights.every(gf => selectedFights.has(gf))) {
|
|
||||||
groupFights.forEach(fight => selectedFights.delete(fight));
|
|
||||||
} else {
|
|
||||||
groupFights.forEach(fight => selectedFights.add(fight));
|
|
||||||
}
|
|
||||||
selectedFights = new Set(selectedFights);
|
|
||||||
}
|
|
||||||
|
|
||||||
let deleteOpen = $state(false);
|
|
||||||
|
|
||||||
async function deleteFights() {
|
|
||||||
for (const fight of selectedFights) {
|
|
||||||
await $fightRepo.deleteFight(fight.id);
|
|
||||||
}
|
|
||||||
fights = await $fightRepo.listFights(data.event.id);
|
|
||||||
selectedFights = new Set();
|
|
||||||
deleteOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let spectatePortOpen = $state(false);
|
|
||||||
let selectPlayers = $derived($players.map(player => {
|
|
||||||
return {
|
|
||||||
name: player.name,
|
|
||||||
value: player.uuid
|
|
||||||
};
|
|
||||||
}).sort((a, b) => a.name.localeCompare(b.name)));
|
|
||||||
let spectatePort = $state("");
|
|
||||||
|
|
||||||
async function updateSpectatePort() {
|
|
||||||
for (const fight of selectedFights) {
|
|
||||||
let f: UpdateFight = {
|
|
||||||
blueTeam: null,
|
|
||||||
group: null,
|
|
||||||
spectatePort: Number.parseInt(spectatePort),
|
|
||||||
map: null,
|
|
||||||
redTeam: null,
|
|
||||||
spielmodus: null,
|
|
||||||
start: null
|
|
||||||
};
|
|
||||||
await $fightRepo.updateFight(fight.id, f);
|
|
||||||
}
|
|
||||||
fights = await $fightRepo.listFights(data.event.id);
|
|
||||||
selectedFights = new Set();
|
|
||||||
spectatePort = "";
|
|
||||||
spectatePortOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let groupChangeOpen = $state(false);
|
|
||||||
let group = $state("");
|
|
||||||
let groupSearch = $state("");
|
|
||||||
|
|
||||||
let selectableGroups = $derived([{
|
|
||||||
name: "Keine",
|
|
||||||
value: ""
|
|
||||||
}, {
|
|
||||||
value: groupSearch,
|
|
||||||
name: `Erstelle: '${groupSearch}'`
|
|
||||||
}, ...$groups.map(group => {
|
|
||||||
return {
|
|
||||||
name: group,
|
|
||||||
value: group
|
|
||||||
};
|
|
||||||
}).sort((a, b) => a.name.localeCompare(b.name))]);
|
|
||||||
|
|
||||||
async function updateGroup() {
|
|
||||||
for (const fight of selectedFights) {
|
|
||||||
let f: UpdateFight = {
|
|
||||||
blueTeam: null,
|
|
||||||
group: group,
|
|
||||||
spectatePort: null,
|
|
||||||
map: null,
|
|
||||||
redTeam: null,
|
|
||||||
spielmodus: null,
|
|
||||||
start: null
|
|
||||||
};
|
|
||||||
await $fightRepo.updateFight(fight.id, f);
|
|
||||||
}
|
|
||||||
fights = await $fightRepo.listFights(data.event.id);
|
|
||||||
selectedFights = new Set();
|
|
||||||
group = "";
|
|
||||||
groupSearch = "";
|
|
||||||
groupChangeOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let minTime = $derived(dayjs(Math.min(...fights.map(fight => fight.start))).utc(true));
|
|
||||||
let changeTimeOpen = $state(false);
|
|
||||||
let changedTime = $state(fights.length != 0 ? dayjs(Math.min(...fights.map(fight => fight.start)))?.utc(true)?.toISOString()?.slice(0, -1) : undefined);
|
|
||||||
|
|
||||||
let deltaTime = $derived(dayjs.duration(dayjs(changedTime).utc(true).diff(minTime)));
|
|
||||||
|
|
||||||
async function updateStartTime() {
|
|
||||||
for (const fight of selectedFights) {
|
|
||||||
let f: UpdateFight = {
|
|
||||||
blueTeam: null,
|
|
||||||
group: null,
|
|
||||||
spectatePort: null,
|
|
||||||
map: null,
|
|
||||||
redTeam: null,
|
|
||||||
spielmodus: null,
|
|
||||||
start: dayjs(fight.start).add(deltaTime.asMilliseconds(), "millisecond")
|
|
||||||
};
|
|
||||||
await $fightRepo.updateFight(fight.id, f);
|
|
||||||
}
|
|
||||||
fights = await $fightRepo.listFights(data.event.id);
|
|
||||||
changedTime = minTime.toISOString().slice(0, -1);
|
|
||||||
selectedFights = new Set();
|
|
||||||
changeTimeOpen = false;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>{data.event.name} - Fights</title>
|
|
||||||
</svelte:head>
|
|
||||||
|
|
||||||
<div class="pb-28">
|
|
||||||
<Toolbar class="mx-4 mt-2 w-fit">
|
|
||||||
<ToolbarGroup>
|
|
||||||
<Checkbox class="ml-2" checked={selectedFights.size === fights.length} onclick={cycleSelect}/>
|
|
||||||
<Tooltip>Select Upcoming</Tooltip>
|
|
||||||
</ToolbarGroup>
|
|
||||||
<ToolbarGroup>
|
|
||||||
<ToolbarButton onclick={() => selectedFights.size > 0 ? changeTimeOpen = true : changeTimeOpen = false}>
|
|
||||||
<CalendarWeekOutline/>
|
|
||||||
</ToolbarButton>
|
|
||||||
<Tooltip>Reschedule Fights</Tooltip>
|
|
||||||
<ToolbarButton onclick={() => selectedFights.size > 0 ? spectatePortOpen = true : spectatePortOpen = false}
|
|
||||||
disabled={changedTime === undefined}>
|
|
||||||
<ProfileCardOutline/>
|
|
||||||
</ToolbarButton>
|
|
||||||
<Tooltip>Change Spectate Port</Tooltip>
|
|
||||||
<ToolbarButton onclick={() => selectedFights.size > 0 ? groupChangeOpen = true : groupChangeOpen = false}>
|
|
||||||
<UsersGroupOutline/>
|
|
||||||
</ToolbarButton>
|
|
||||||
<Tooltip>Change Group</Tooltip>
|
|
||||||
</ToolbarGroup>
|
|
||||||
<ToolbarGroup>
|
|
||||||
<ToolbarButton color="red"
|
|
||||||
onclick={() => selectedFights.size > 0 ? deleteOpen = true : deleteOpen = false}>
|
|
||||||
<TrashBinOutline/>
|
|
||||||
</ToolbarButton>
|
|
||||||
<Tooltip>Delete</Tooltip>
|
|
||||||
</ToolbarGroup>
|
|
||||||
</Toolbar>
|
|
||||||
{#each groupedFights as group}
|
|
||||||
<div class="flex mt-4">
|
|
||||||
<Checkbox class="ml-2 text-center" checked={group.fights.every(gf => selectedFights.has(gf))}
|
|
||||||
onclick={() => cycleGroup(group.fights)}/>
|
|
||||||
<h1 class="ml-4 text-2xl">{group.group ?? "Ungrouped"}</h1>
|
|
||||||
</div>
|
|
||||||
{#each group.fights.sort((a, b) => a.start - b.start) as fight, i (fight.id)}
|
|
||||||
{@const isSelected = selectedFights.has(fight)}
|
|
||||||
<FightCard {fight} {i} {data} selected={isSelected}
|
|
||||||
select={() => {
|
|
||||||
if (selectedFights.has(fight)) {
|
|
||||||
selectedFights.delete(fight);
|
|
||||||
} else {
|
|
||||||
selectedFights.add(fight);
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedFights = new Set(selectedFights);
|
|
||||||
}} update={async () => fights = await $fightRepo.listFights(data.event.id)}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CreateFightModal {data} bind:open={createOpen}
|
|
||||||
on:create={async () => data.fights = await $fightRepo.listFights(data.event.id)}></CreateFightModal>
|
|
||||||
|
|
||||||
<Modal bind:open={deleteOpen} title="Delete {selectedFights.size} Fights" autoclose size="sm">
|
|
||||||
<p>Are you sure you want to delete {selectedFights.size} fights?</p>
|
|
||||||
{#snippet footer()}
|
|
||||||
|
|
||||||
<Button color="red" class="ml-auto" onclick={deleteFights}>Delete</Button>
|
|
||||||
<Button onclick={() => deleteOpen = false} color="alternative">Cancel</Button>
|
|
||||||
|
|
||||||
{/snippet}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:open={spectatePortOpen} title="Change Kampfleiter" size="sm">
|
|
||||||
<div class="m-2">
|
|
||||||
<Label for="fight-kampf">Kampfleiter</Label>
|
|
||||||
<TypeAheadSearch items={selectPlayers} bind:selected={spectatePort}></TypeAheadSearch>
|
|
||||||
</div>
|
|
||||||
{#snippet footer()}
|
|
||||||
|
|
||||||
<Button class="ml-auto" onclick={updateSpectatePort}>Change</Button>
|
|
||||||
<Button onclick={() => spectatePortOpen = false} color="alternative">Cancel</Button>
|
|
||||||
|
|
||||||
{/snippet}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:open={groupChangeOpen} title="Change Group" size="sm">
|
|
||||||
<div class="m-2">
|
|
||||||
<Label for="fight-kampf">Group</Label>
|
|
||||||
<TypeAheadSearch items={selectableGroups} bind:selected={group} bind:searchValue={groupSearch}
|
|
||||||
all></TypeAheadSearch>
|
|
||||||
</div>
|
|
||||||
{#snippet footer()}
|
|
||||||
|
|
||||||
<Button class="ml-auto" onclick={updateGroup}>Change</Button>
|
|
||||||
<Button onclick={() => groupChangeOpen = false} color="alternative">Cancel</Button>
|
|
||||||
|
|
||||||
{/snippet}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<Modal bind:open={changeTimeOpen} title="Change Start Time" size="sm">
|
|
||||||
<div class="m-2">
|
|
||||||
<Label for="fight-start">New Start Time:</Label>
|
|
||||||
<Input id="fight-start" bind:value={changedTime} >
|
|
||||||
{#snippet children({ props })}
|
|
||||||
<input type="datetime-local" {...props} bind:value={changedTime}/>
|
|
||||||
{/snippet}
|
|
||||||
</Input>
|
|
||||||
</div>
|
|
||||||
<p>{deltaTime.asMilliseconds() < 0 ? '' : '+'}{("0" + deltaTime.hours()).slice(-2)}
|
|
||||||
:{("0" + deltaTime.minutes()).slice(-2)}</p>
|
|
||||||
{#snippet footer()}
|
|
||||||
|
|
||||||
<Button class="ml-auto" onclick={updateStartTime}>Update</Button>
|
|
||||||
<Button onclick={() => changeTimeOpen = false} color="alternative">Cancel</Button>
|
|
||||||
|
|
||||||
{/snippet}
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<SpeedDial>
|
|
||||||
<SpeedDialButton name="Add" onclick={() => createOpen = true}>
|
|
||||||
<PlusOutline/>
|
|
||||||
</SpeedDialButton>
|
|
||||||
<SpeedDialButton name="Generate" href="#/event/{data.event.id}/generate">
|
|
||||||
<ArrowsRepeatOutline/>
|
|
||||||
</SpeedDialButton>
|
|
||||||
</SpeedDial>
|
|
||||||
@@ -18,20 +18,19 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {ExtendedEvent} from "@type/event.ts";
|
import type { ExtendedEvent } from "@type/event.ts";
|
||||||
import {Button} from "flowbite-svelte";
|
import { Button } from "flowbite-svelte";
|
||||||
import {PlusOutline} from "flowbite-svelte-icons";
|
import { PlusOutline } from "flowbite-svelte-icons";
|
||||||
import SWModal from "@components/styled/SWModal.svelte";
|
import SWModal from "@components/styled/SWModal.svelte";
|
||||||
import SWButton from "@components/styled/SWButton.svelte";
|
import SWButton from "@components/styled/SWButton.svelte";
|
||||||
import TypeAheadSearch from "@components/admin/components/TypeAheadSearch.svelte";
|
import PlayerSelector from "@components/ui/PlayerSelector.svelte";
|
||||||
import {players} from "@stores/stores.ts";
|
import { eventRepo } from "@repo/event.ts";
|
||||||
import {eventRepo} from "@repo/event.ts";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: ExtendedEvent;
|
data: ExtendedEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
|
||||||
let searchValue = $state("");
|
let searchValue = $state("");
|
||||||
let selectedPlayer: string | null = $state(null);
|
let selectedPlayer: string | null = $state(null);
|
||||||
@@ -42,17 +41,19 @@
|
|||||||
|
|
||||||
async function addReferee() {
|
async function addReferee() {
|
||||||
if (selectedPlayer) {
|
if (selectedPlayer) {
|
||||||
referees = (await $eventRepo.updateEvent(data.event.id.toString(), {
|
referees = (
|
||||||
deadline: null,
|
await $eventRepo.updateEvent(data.event.id.toString(), {
|
||||||
end: null,
|
deadline: null,
|
||||||
maxTeamMembers: null,
|
end: null,
|
||||||
name: null,
|
maxTeamMembers: null,
|
||||||
publicSchemsOnly: null,
|
name: null,
|
||||||
removeReferee: null,
|
publicSchemsOnly: null,
|
||||||
schemType: null,
|
removeReferee: null,
|
||||||
start: null,
|
schemType: null,
|
||||||
addReferee: [selectedPlayer]
|
start: null,
|
||||||
})).referees;
|
addReferee: [selectedPlayer],
|
||||||
|
})
|
||||||
|
).referees;
|
||||||
}
|
}
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
@@ -60,18 +61,20 @@
|
|||||||
|
|
||||||
function removeReferee(id: string) {
|
function removeReferee(id: string) {
|
||||||
return async () => {
|
return async () => {
|
||||||
referees = (await $eventRepo.updateEvent(data.event.id.toString(), {
|
referees = (
|
||||||
deadline: null,
|
await $eventRepo.updateEvent(data.event.id.toString(), {
|
||||||
end: null,
|
deadline: null,
|
||||||
maxTeamMembers: null,
|
end: null,
|
||||||
name: null,
|
maxTeamMembers: null,
|
||||||
publicSchemsOnly: null,
|
name: null,
|
||||||
addReferee: null,
|
publicSchemsOnly: null,
|
||||||
schemType: null,
|
addReferee: null,
|
||||||
start: null,
|
schemType: null,
|
||||||
removeReferee: [id],
|
start: null,
|
||||||
})).referees;
|
removeReferee: [id],
|
||||||
}
|
})
|
||||||
|
).referees;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
@@ -84,9 +87,7 @@
|
|||||||
{#each referees as referee}
|
{#each referees as referee}
|
||||||
<li class="flex flex-grow justify-between">
|
<li class="flex flex-grow justify-between">
|
||||||
{referee.name}
|
{referee.name}
|
||||||
<SWButton onclick={removeReferee(referee.uuid)}>
|
<SWButton onclick={removeReferee(referee.uuid)}>Entfernen</SWButton>
|
||||||
Entfernen
|
|
||||||
</SWButton>
|
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
@@ -95,23 +96,22 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Button class="fixed bottom-6 right-6 !p-4 z-10 shadow-lg" onclick={() => showAdd = true}>
|
<Button class="fixed bottom-6 right-6 !p-4 z-10 shadow-lg" onclick={() => (showAdd = true)}>
|
||||||
<PlusOutline/>
|
<PlusOutline />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<SWModal title="Schiedsrichter hinzufügen" bind:open={showAdd}>
|
<SWModal title="Schiedsrichter hinzufügen" bind:open={showAdd}>
|
||||||
<div class="flex flex-grow justify-center h-80">
|
<div class="flex flex-grow justify-center h-80">
|
||||||
<div>
|
<div>
|
||||||
<TypeAheadSearch bind:searchValue bind:selected={selectedPlayer}
|
<PlayerSelector bind:value={selectedPlayer} placeholder="Search player..." />
|
||||||
items={$players.map(v => ({ name: v.name, value: v.uuid }))}/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#snippet footer()}
|
{#snippet footer()}
|
||||||
<div class="flex flex-grow justify-end">
|
<div class="flex flex-grow justify-end">
|
||||||
<SWButton onclick={reset} type="gray">Abbrechen</SWButton>
|
<SWButton onclick={reset} type="gray">Abbrechen</SWButton>
|
||||||
<SWButton onclick={addReferee}>Hinzufügen</SWButton>
|
<SWButton onclick={addReferee}>Hinzufügen</SWButton>
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</SWModal>
|
</SWModal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -55,35 +55,36 @@
|
|||||||
<!-- Apply horizontal offset only to the mid bridge and second segment fan-out; also shift vertical line to keep continuity -->
|
<!-- Apply horizontal offset only to the mid bridge and second segment fan-out; also shift vertical line to keep continuity -->
|
||||||
{@const midLeft = fromLeft + horizontalDistance / 2 + connection.offset}
|
{@const midLeft = fromLeft + horizontalDistance / 2 + connection.offset}
|
||||||
{@const firstSegmentWidth = horizontalDistance / 2}
|
{@const firstSegmentWidth = horizontalDistance / 2}
|
||||||
{@const secondSegmentWidth = horizontalDistance / 2}
|
|
||||||
|
|
||||||
<div
|
{#if firstSegmentWidth > 0}
|
||||||
class="horizontal-line"
|
<div
|
||||||
style="
|
class="horizontal-line"
|
||||||
|
style="
|
||||||
background-color: {connection.color};
|
background-color: {connection.color};
|
||||||
left: {fromLeft}px;
|
left: {fromLeft}px;
|
||||||
top: {fromTop + connection.offset / 4}px;
|
top: {fromTop + connection.offset / 4}px;
|
||||||
width: {firstSegmentWidth + connection.offset + 2}px;
|
width: {firstSegmentWidth + connection.offset + 2}px;
|
||||||
"
|
"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="vertical-line"
|
class="vertical-line"
|
||||||
style="
|
style="
|
||||||
background-color: {connection.color};
|
background-color: {connection.color};
|
||||||
left: {midLeft}px;
|
left: {midLeft}px;
|
||||||
top: {Math.min(fromTop + connection.offset / 4, toTop + connection.offset / 4)}px;
|
top: {Math.min(fromTop + connection.offset / 4, toTop + connection.offset / 4)}px;
|
||||||
height: {Math.abs(toTop + connection.offset / 4 - (fromTop + connection.offset / 4))}px;
|
height: {Math.abs(toTop + connection.offset / 4 - (fromTop + connection.offset / 4))}px;
|
||||||
"
|
"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="horizontal-line"
|
class="horizontal-line"
|
||||||
style="
|
style="
|
||||||
background-color: {connection.color};
|
background-color: {connection.color};
|
||||||
left: {midLeft}px;
|
left: {midLeft}px;
|
||||||
top: {toTop + connection.offset / 4}px;
|
top: {toTop + connection.offset / 4}px;
|
||||||
width: {secondSegmentWidth - connection.offset}px;
|
width: {firstSegmentWidth - connection.offset}px;
|
||||||
"
|
"
|
||||||
></div>
|
></div>
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{/key}
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ExtendedEvent, EventFight, ResponseGroups, ResponseRelation } from "@type/event.ts";
|
import type {
|
||||||
|
ExtendedEvent,
|
||||||
|
EventFight,
|
||||||
|
ResponseGroups,
|
||||||
|
ResponseRelation,
|
||||||
|
} from "@type/event.ts";
|
||||||
import type { DoubleEleminationViewConfig } from "./types";
|
import type { DoubleEleminationViewConfig } from "./types";
|
||||||
import EventCard from "./EventCard.svelte";
|
import EventCard from "./EventCard.svelte";
|
||||||
import EventFightChip from "./EventFightChip.svelte";
|
import EventFightChip from "./EventFightChip.svelte";
|
||||||
import { onMount, onDestroy, tick } from "svelte";
|
import { onMount, onDestroy, tick } from "svelte";
|
||||||
import { fightConnector } from "./connections.svelte.ts";
|
import { fightConnector } from "./connections.svelte.ts";
|
||||||
|
|
||||||
const { event, config }: { event: ExtendedEvent; config: DoubleEleminationViewConfig } = $props();
|
const {
|
||||||
|
event,
|
||||||
|
config,
|
||||||
|
}: { event: ExtendedEvent; config: DoubleEleminationViewConfig } = $props();
|
||||||
|
|
||||||
const defaultGroup: ResponseGroups = {
|
const defaultGroup: ResponseGroups = {
|
||||||
id: -1,
|
id: -1,
|
||||||
@@ -18,7 +26,9 @@
|
|||||||
points: null,
|
points: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function indexRelations(ev: ExtendedEvent): Map<number, ResponseRelation[]> {
|
function indexRelations(
|
||||||
|
ev: ExtendedEvent,
|
||||||
|
): Map<number, ResponseRelation[]> {
|
||||||
const map = new Map<number, ResponseRelation[]>();
|
const map = new Map<number, ResponseRelation[]>();
|
||||||
for (const rel of ev.relations) {
|
for (const rel of ev.relations) {
|
||||||
const list = map.get(rel.fight) ?? [];
|
const list = map.get(rel.fight) ?? [];
|
||||||
@@ -29,7 +39,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const relationsByFight = indexRelations(event);
|
const relationsByFight = indexRelations(event);
|
||||||
const fightMap = new Map<number, EventFight>(event.fights.map((f) => [f.id, f]));
|
const fightMap = new Map<number, EventFight>(
|
||||||
|
event.fights.map((f) => [f.id, f]),
|
||||||
|
);
|
||||||
|
|
||||||
function collectBracket(startFinalId: number): EventFight[][] {
|
function collectBracket(startFinalId: number): EventFight[][] {
|
||||||
const finalFight = fightMap.get(startFinalId);
|
const finalFight = fightMap.get(startFinalId);
|
||||||
@@ -45,10 +57,15 @@
|
|||||||
const rels = relationsByFight.get(fight.id) ?? [];
|
const rels = relationsByFight.get(fight.id) ?? [];
|
||||||
for (const rel of rels) {
|
for (const rel of rels) {
|
||||||
if (rel.type === "FIGHT" && rel.fromFight) {
|
if (rel.type === "FIGHT" && rel.fromFight) {
|
||||||
const src = fightMap.get(rel.fromFight.id) ?? rel.fromFight;
|
const src =
|
||||||
|
fightMap.get(rel.fromFight.id) ?? rel.fromFight;
|
||||||
if (!src) continue;
|
if (!src) continue;
|
||||||
// Only traverse within the same bracket (group) to avoid cross-bracket pollution
|
// Only traverse within the same bracket (group) to avoid cross-bracket pollution
|
||||||
if (bracketGroupId !== null && src.group?.id !== bracketGroupId) continue;
|
if (
|
||||||
|
bracketGroupId !== null &&
|
||||||
|
src.group?.id !== bracketGroupId
|
||||||
|
)
|
||||||
|
continue;
|
||||||
if (!visited.has(src.id)) {
|
if (!visited.has(src.id)) {
|
||||||
visited.add(src.id);
|
visited.add(src.id);
|
||||||
next.push(src);
|
next.push(src);
|
||||||
@@ -97,8 +114,12 @@
|
|||||||
for (const rel of event.relations) {
|
for (const rel of event.relations) {
|
||||||
if (rel.type !== "FIGHT" || !rel.fromFight) continue;
|
if (rel.type !== "FIGHT" || !rel.fromFight) continue;
|
||||||
const fromId = rel.fromFight.id;
|
const fromId = rel.fromFight.id;
|
||||||
const fromEl = document.getElementById(`fight-${fromId}`) as HTMLElement | null;
|
const fromEl = document.getElementById(
|
||||||
const toEl = document.getElementById(`fight-${rel.fight}-team-${rel.team.toLowerCase()}`) as HTMLElement | null;
|
`fight-${fromId}`,
|
||||||
|
) as HTMLElement | null;
|
||||||
|
const toEl = document.getElementById(
|
||||||
|
`fight-${rel.fight}-team-${rel.team.toLowerCase()}`,
|
||||||
|
) as HTMLElement | null;
|
||||||
if (!fromEl || !toEl) continue;
|
if (!fromEl || !toEl) continue;
|
||||||
// Use team-signed offsets so BLUE goes left (negative), RED goes right (positive)
|
// Use team-signed offsets so BLUE goes left (negative), RED goes right (positive)
|
||||||
const key = `${fromId}:${rel.team}`;
|
const key = `${fromId}:${rel.team}`;
|
||||||
@@ -118,12 +139,18 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !grandFinal}
|
{#if !grandFinal}
|
||||||
<p class="text-gray-400 italic">Konfiguration unvollständig (Grand Final fehlt).</p>
|
<p class="text-gray-400 italic">
|
||||||
|
Konfiguration unvollständig (Grand Final fehlt).
|
||||||
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#key winnersStages.length + ":" + losersStages.length}
|
{#key winnersStages.length + ":" + losersStages.length}
|
||||||
<!-- Build a grid where rows: winners (stages), losers (stages), with losers offset by one stage/column -->
|
<!-- Build a grid where rows: winners (stages), losers (stages), with losers offset by one stage/column -->
|
||||||
{@const totalColumns = Math.max(winnersStages.length, losersStages.length + 1) + 1}
|
{@const totalColumns =
|
||||||
<div class="grid gap-x-16 gap-y-6 items-start" style={`grid-template-columns: repeat(${totalColumns}, max-content);`}>
|
Math.max(winnersStages.length, losersStages.length + 1) + 1}
|
||||||
|
<div
|
||||||
|
class="grid gap-x-16 gap-y-6 items-start"
|
||||||
|
style={`grid-template-columns: repeat(${totalColumns}, max-content);`}
|
||||||
|
>
|
||||||
<!-- Winners heading spans all columns -->
|
<!-- Winners heading spans all columns -->
|
||||||
<h2 class="font-bold text-center">Winners Bracket</h2>
|
<h2 class="font-bold text-center">Winners Bracket</h2>
|
||||||
|
|
||||||
@@ -132,30 +159,50 @@
|
|||||||
<div style={`grid-row: 2; grid-column: ${i + 1};`}>
|
<div style={`grid-row: 2; grid-column: ${i + 1};`}>
|
||||||
<EventCard title={stageName(stage.length, true)}>
|
<EventCard title={stageName(stage.length, true)}>
|
||||||
{#each stage as fight}
|
{#each stage as fight}
|
||||||
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
|
<EventFightChip
|
||||||
|
{event}
|
||||||
|
{fight}
|
||||||
|
group={fight.group ?? defaultGroup}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</EventCard>
|
</EventCard>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<!-- Place Grand Final at the far right, aligned with winners row -->
|
<!-- Place Grand Final at the far right, aligned with winners row -->
|
||||||
<div style={`grid-row: 2; grid-column: ${totalColumns};`} class="self-center">
|
<div
|
||||||
|
style={`grid-row: 2; grid-column: ${totalColumns};`}
|
||||||
|
class="self-center"
|
||||||
|
>
|
||||||
<EventCard title="Grand Final">
|
<EventCard title="Grand Final">
|
||||||
{#if grandFinal}
|
{#if grandFinal}
|
||||||
<EventFightChip fight={grandFinal} group={grandFinal.group ?? defaultGroup} />
|
<EventFightChip
|
||||||
|
{event}
|
||||||
|
fight={grandFinal}
|
||||||
|
group={grandFinal.group ?? defaultGroup}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</EventCard>
|
</EventCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Losers heading spans all columns -->
|
<!-- Losers heading spans all columns -->
|
||||||
<h2 class="font-bold text-center" style="grid-row: 3; grid-column: 1 / {totalColumns};">Losers Bracket</h2>
|
<h2
|
||||||
|
class="font-bold text-center"
|
||||||
|
style="grid-row: 3; grid-column: 1 / {totalColumns};"
|
||||||
|
>
|
||||||
|
Losers Bracket
|
||||||
|
</h2>
|
||||||
|
|
||||||
<!-- Losers stages in row 4, offset by one column to the right -->
|
<!-- Losers stages in row 4, offset by one column to the right -->
|
||||||
{#each losersStages as stage, j}
|
{#each losersStages as stage, j}
|
||||||
<div style={`grid-row: 4; grid-column: ${j + 2};`} class="mt-2">
|
<div style={`grid-row: 4; grid-column: ${j + 2};`} class="mt-2">
|
||||||
<EventCard title={stageName(stage.length, false)}>
|
<EventCard title={stageName(stage.length, false)}>
|
||||||
{#each stage as fight}
|
{#each stage as fight}
|
||||||
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
|
<EventFightChip
|
||||||
|
{event}
|
||||||
|
{fight}
|
||||||
|
group={fight.group ?? defaultGroup}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</EventCard>
|
</EventCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ExtendedEvent, EventFight, ResponseGroups, ResponseRelation } from "@type/event.ts";
|
import type {
|
||||||
|
ExtendedEvent,
|
||||||
|
EventFight,
|
||||||
|
ResponseGroups,
|
||||||
|
ResponseRelation,
|
||||||
|
} from "@type/event.ts";
|
||||||
import type { EleminationViewConfig } from "./types";
|
import type { EleminationViewConfig } from "./types";
|
||||||
import EventCard from "./EventCard.svelte";
|
import EventCard from "./EventCard.svelte";
|
||||||
import EventFightChip from "./EventFightChip.svelte";
|
import EventFightChip from "./EventFightChip.svelte";
|
||||||
import { onMount, onDestroy, tick } from "svelte";
|
import { onMount, onDestroy, tick } from "svelte";
|
||||||
import { FightConnector, fightConnector } from "./connections.svelte.ts";
|
import { FightConnector, fightConnector } from "./connections.svelte.ts";
|
||||||
|
|
||||||
const { event, config }: { event: ExtendedEvent; config: EleminationViewConfig } = $props();
|
const {
|
||||||
|
event,
|
||||||
|
config,
|
||||||
|
}: { event: ExtendedEvent; config: EleminationViewConfig } = $props();
|
||||||
|
|
||||||
const defaultGroup: ResponseGroups = {
|
const defaultGroup: ResponseGroups = {
|
||||||
id: -1,
|
id: -1,
|
||||||
@@ -18,8 +26,13 @@
|
|||||||
points: null,
|
points: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildStages(ev: ExtendedEvent, finalFightId: number): EventFight[][] {
|
function buildStages(
|
||||||
const fightMap = new Map<number, EventFight>(ev.fights.map((f) => [f.id, f]));
|
ev: ExtendedEvent,
|
||||||
|
finalFightId: number,
|
||||||
|
): EventFight[][] {
|
||||||
|
const fightMap = new Map<number, EventFight>(
|
||||||
|
ev.fights.map((f) => [f.id, f]),
|
||||||
|
);
|
||||||
const relationsByFight = new Map<number, ResponseRelation[]>();
|
const relationsByFight = new Map<number, ResponseRelation[]>();
|
||||||
for (const rel of ev.relations) {
|
for (const rel of ev.relations) {
|
||||||
const list = relationsByFight.get(rel.fight) ?? [];
|
const list = relationsByFight.get(rel.fight) ?? [];
|
||||||
@@ -41,7 +54,8 @@
|
|||||||
const rels = relationsByFight.get(fight.id) ?? [];
|
const rels = relationsByFight.get(fight.id) ?? [];
|
||||||
for (const rel of rels) {
|
for (const rel of rels) {
|
||||||
if (rel.type === "FIGHT" && rel.fromFight) {
|
if (rel.type === "FIGHT" && rel.fromFight) {
|
||||||
const src = fightMap.get(rel.fromFight.id) ?? rel.fromFight;
|
const src =
|
||||||
|
fightMap.get(rel.fromFight.id) ?? rel.fromFight;
|
||||||
if (src && !visited.has(src.id)) {
|
if (src && !visited.has(src.id)) {
|
||||||
visited.add(src.id);
|
visited.add(src.id);
|
||||||
nextLayer.push(src);
|
nextLayer.push(src);
|
||||||
@@ -89,8 +103,12 @@
|
|||||||
|
|
||||||
for (const rel of event.relations) {
|
for (const rel of event.relations) {
|
||||||
if (rel.type !== "FIGHT" || !rel.fromFight) continue;
|
if (rel.type !== "FIGHT" || !rel.fromFight) continue;
|
||||||
const fromEl = document.getElementById(`fight-${rel.fromFight.id}`) as HTMLElement | null;
|
const fromEl = document.getElementById(
|
||||||
const toEl = document.getElementById(`fight-${rel.fight}-team-${rel.team.toLowerCase()}`) as HTMLElement | null;
|
`fight-${rel.fromFight.id}`,
|
||||||
|
) as HTMLElement | null;
|
||||||
|
const toEl = document.getElementById(
|
||||||
|
`fight-${rel.fight}-team-${rel.team.toLowerCase()}`,
|
||||||
|
) as HTMLElement | null;
|
||||||
if (fromEl && toEl) {
|
if (fromEl && toEl) {
|
||||||
connector.addConnection(fromEl, toEl, "#9ca3af");
|
connector.addConnection(fromEl, toEl, "#9ca3af");
|
||||||
}
|
}
|
||||||
@@ -111,7 +129,11 @@
|
|||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
<EventCard title={stageName(index, stage)}>
|
<EventCard title={stageName(index, stage)}>
|
||||||
{#each stage as fight}
|
{#each stage as fight}
|
||||||
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
|
<EventFightChip
|
||||||
|
{event}
|
||||||
|
{fight}
|
||||||
|
group={fight.group ?? defaultGroup}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</EventCard>
|
</EventCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { EventFight, ResponseGroups } from "@components/types/event";
|
import type { EventFight, ExtendedEvent, ResponseGroups } from "@components/types/event";
|
||||||
import EventCardOutline from "./EventCardOutline.svelte";
|
import EventCardOutline from "./EventCardOutline.svelte";
|
||||||
import EventTeamChip from "./EventTeamChip.svelte";
|
import EventTeamChip from "./EventTeamChip.svelte";
|
||||||
import { fightConnector } from "./connections.svelte.ts";
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
fight,
|
fight,
|
||||||
group,
|
group,
|
||||||
|
event,
|
||||||
|
disabled = false,
|
||||||
}: {
|
}: {
|
||||||
fight: EventFight;
|
fight: EventFight;
|
||||||
group: ResponseGroups;
|
group: ResponseGroups;
|
||||||
|
event: ExtendedEvent;
|
||||||
|
disabled?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
function getScore(group: ResponseGroups, fight: EventFight, blueTeam: boolean): string {
|
function getScore(group: ResponseGroups, fight: EventFight, blueTeam: boolean): string {
|
||||||
@@ -29,14 +32,36 @@
|
|||||||
<EventTeamChip
|
<EventTeamChip
|
||||||
team={{
|
team={{
|
||||||
id: -1,
|
id: -1,
|
||||||
kuerzel: new Date(fight.start).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
|
kuerzel: new Date(fight.start).toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}),
|
||||||
name: new Date(fight.start).toLocaleDateString([]),
|
name: new Date(fight.start).toLocaleDateString([]),
|
||||||
color: "-1",
|
color: "-1",
|
||||||
}}
|
}}
|
||||||
time={true}
|
time={true}
|
||||||
|
{event}
|
||||||
/>
|
/>
|
||||||
<div id={"fight-" + fight.id}>
|
<div id={"fight-" + fight.id}>
|
||||||
<EventTeamChip team={fight.blueTeam} score={getScore(group, fight, true)} showWinner={true} isWinner={fight.ergebnis === 1} noWinner={fight.ergebnis === 0} id="fight-{fight.id}-team-blue" />
|
<EventTeamChip
|
||||||
<EventTeamChip team={fight.redTeam} score={getScore(group, fight, false)} showWinner={true} isWinner={fight.ergebnis === 2} noWinner={fight.ergebnis === 0} id="fight-{fight.id}-team-red" />
|
{event}
|
||||||
|
{disabled}
|
||||||
|
team={fight.blueTeam}
|
||||||
|
score={getScore(group, fight, true)}
|
||||||
|
showWinner={true}
|
||||||
|
isWinner={fight.ergebnis === 1}
|
||||||
|
noWinner={fight.ergebnis === 0}
|
||||||
|
id="fight-{fight.id}-team-blue"
|
||||||
|
/>
|
||||||
|
<EventTeamChip
|
||||||
|
{event}
|
||||||
|
{disabled}
|
||||||
|
team={fight.redTeam}
|
||||||
|
score={getScore(group, fight, false)}
|
||||||
|
showWinner={true}
|
||||||
|
isWinner={fight.ergebnis === 2}
|
||||||
|
noWinner={fight.ergebnis === 0}
|
||||||
|
id="fight-{fight.id}-team-red"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</EventCardOutline>
|
</EventCardOutline>
|
||||||
|
|||||||
@@ -1,48 +1,70 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Team } from "@type/team.ts";
|
import type { Team } from "@type/team.ts";
|
||||||
import { fightConnector } from "./connections.svelte";
|
|
||||||
import { teamHoverService } from "./team-hover.svelte";
|
import { teamHoverService } from "./team-hover.svelte";
|
||||||
|
import { Sheet, SheetContent, SheetTrigger } from "@components/ui/sheet";
|
||||||
|
import TeamInfo from "./TeamInfo.svelte";
|
||||||
|
import type { ExtendedEvent } from "@components/types/event";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
team,
|
team,
|
||||||
|
event,
|
||||||
score = "",
|
score = "",
|
||||||
time = false,
|
time = false,
|
||||||
showWinner = false,
|
showWinner = false,
|
||||||
isWinner = false,
|
isWinner = false,
|
||||||
noWinner = false,
|
noWinner = false,
|
||||||
id,
|
id,
|
||||||
|
disabled = false,
|
||||||
}: {
|
}: {
|
||||||
team: Team;
|
team: Team;
|
||||||
|
event: ExtendedEvent;
|
||||||
score?: string;
|
score?: string;
|
||||||
time?: boolean;
|
time?: boolean;
|
||||||
showWinner?: boolean;
|
showWinner?: boolean;
|
||||||
isWinner?: boolean;
|
isWinner?: boolean;
|
||||||
noWinner?: boolean;
|
noWinner?: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
disabled?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let hoverService = $teamHoverService;
|
let hoverService = $teamHoverService;
|
||||||
|
|
||||||
|
type StringAnyRecord = Record<string, any>;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
{#if !disabled}
|
||||||
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
|
<Sheet>
|
||||||
? 'bg-gray-800'
|
<SheetTrigger>
|
||||||
: ''} {showWinner ? 'border-l-4' : ''} {showWinner && isWinner ? 'border-l-yellow-500' : 'border-l-gray-950'}"
|
{#snippet child({ props })}
|
||||||
onmouseenter={() => team.id === -1 || hoverService.setHover(team.id)}
|
{@render teamButton({ props })}
|
||||||
onmouseleave={() => team.id === -1 || hoverService.clearHover()}
|
{/snippet}
|
||||||
{id}
|
</SheetTrigger>
|
||||||
>
|
<SheetContent>
|
||||||
<div class="flex">
|
<TeamInfo {team} {event} />
|
||||||
<div class="w-12 {time ? 'font-bold' : ''}">{team.kuerzel}</div>
|
</SheetContent>
|
||||||
<span class={time ? "font-mono" : "font-bold"}>{team.name}</span>
|
</Sheet>
|
||||||
</div>
|
{:else}
|
||||||
<div class="{showWinner && isWinner && 'font-bold'} {isWinner ? 'text-yellow-400' : ''} {noWinner ? 'font-bold' : ''}">
|
{@render teamButton({ props: {} })}
|
||||||
{score}
|
{/if}
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<style>
|
{#snippet teamButton({ props }: { props: StringAnyRecord })}
|
||||||
.team-chip:not(:last-child) {
|
<button
|
||||||
@apply border-b border-b-gray-700;
|
{...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
|
||||||
</style>
|
? -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>
|
||||||
|
<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>
|
||||||
|
{/snippet}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import EventCardOutline from "./EventCardOutline.svelte";
|
import EventCardOutline from "./EventCardOutline.svelte";
|
||||||
import EventTeamChip from "./EventTeamChip.svelte";
|
import EventTeamChip from "./EventTeamChip.svelte";
|
||||||
import EventFightChip from "./EventFightChip.svelte";
|
import EventFightChip from "./EventFightChip.svelte";
|
||||||
|
import { teamHoverService } from "./team-hover.svelte";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
event,
|
event,
|
||||||
@@ -39,12 +40,30 @@
|
|||||||
if (currentRound.length) rounds.push(currentRound);
|
if (currentRound.length) rounds.push(currentRound);
|
||||||
return rounds;
|
return rounds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chunkIntoRows<T>(items: T[], rowCount: number): T[][] {
|
||||||
|
if (!items || items.length === 0) return [];
|
||||||
|
|
||||||
|
const rows = Math.max(1, Math.floor(rowCount || 1));
|
||||||
|
const perRow = Math.ceil(items.length / rows);
|
||||||
|
|
||||||
|
const chunked: T[][] = [];
|
||||||
|
for (let i = 0; i < rows; i++) {
|
||||||
|
const slice = items.slice(i * perRow, (i + 1) * perRow);
|
||||||
|
if (slice.length) chunked.push(slice);
|
||||||
|
}
|
||||||
|
return chunked;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hover = $teamHoverService;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each config.groups as groupId}
|
{#each config.groups as groupId}
|
||||||
{@const group = event.groups.find((g) => g.id === groupId)!!}
|
{@const group = event.groups.find((g) => g.id === groupId)!!}
|
||||||
{@const fights = event.fights.filter((f) => f.group?.id === groupId)}
|
{@const fights = event.fights.filter((f) => f.group?.id === groupId)}
|
||||||
{@const rounds = detectRounds(fights)}
|
{@const rounds = detectRounds(fights)}
|
||||||
|
{@const roundRows = config.roundRows ?? 1}
|
||||||
|
{@const roundRowsChunked = chunkIntoRows(rounds, roundRows)}
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div>
|
<div>
|
||||||
<EventCard title={group.name}>
|
<EventCard title={group.name}>
|
||||||
@@ -52,19 +71,27 @@
|
|||||||
{#each Object.entries(group.points ?? {}).sort((v1, v2) => v2[1] - v1[1]) as points}
|
{#each Object.entries(group.points ?? {}).sort((v1, v2) => v2[1] - v1[1]) as points}
|
||||||
{@const [teamId, point] = points}
|
{@const [teamId, point] = points}
|
||||||
{@const team = event.teams.find((t) => t.id.toString() === teamId)!!}
|
{@const team = event.teams.find((t) => t.id.toString() === teamId)!!}
|
||||||
<EventTeamChip {team} score={point.toString()} />
|
<EventTeamChip {team} {event} score={point.toString()} />
|
||||||
{/each}
|
{/each}
|
||||||
</EventCardOutline>
|
</EventCardOutline>
|
||||||
</EventCard>
|
</EventCard>
|
||||||
</div>
|
</div>
|
||||||
{#each rounds as round, index}
|
<div class="flex flex-col">
|
||||||
<div>
|
{#each roundRowsChunked as row}
|
||||||
<EventCard title="Runde {index + 1}">
|
<div class="flex">
|
||||||
{#each round as fight}
|
{#each row as round, index (round)}
|
||||||
<EventFightChip {fight} {group} />
|
{@const roundIndex = rounds.indexOf(round)}
|
||||||
|
{@const teams = Array.from(new Set(round.flatMap((f) => [f.redTeam, f.blueTeam])))}
|
||||||
|
<div class="{hover.currentHover && !teams.some((t) => t?.id === hover.currentHover) ? 'opacity-30' : ''} transition-opacity">
|
||||||
|
<EventCard title="Runde {roundIndex + 1}">
|
||||||
|
{#each round as fight}
|
||||||
|
<EventFightChip {event} {fight} {group} />
|
||||||
|
{/each}
|
||||||
|
</EventCard>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</EventCard>
|
</div>
|
||||||
</div>
|
{/each}
|
||||||
{/each}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
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">
|
<div class="py-2 border-t border-t-gray-600">
|
||||||
<h1 class="text-2xl font-bold mb-4">Angemeldete Teams</h1>
|
<h1 class="text-2xl font-bold mb-4">Angemeldete Teams</h1>
|
||||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2">
|
<div
|
||||||
|
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2"
|
||||||
|
>
|
||||||
{#each teams as team}
|
{#each teams as team}
|
||||||
<div class="bg-neutral-800 p-2 rounded-md border border-neutral-700 border-l-4 flex flex-row items-center gap-2" style="border-left-color: {colorMap[team.color] || '#FFFFFF'}">
|
<button
|
||||||
<span class="text-sm font-mono text-neutral-400 shrink-0 w-8 text-center">{team.kuerzel}</span>
|
class="bg-neutral-800 p-2 rounded-md border border-neutral-700 border-l-4 flex flex-row items-center gap-2 cursor-pointer hover:bg-neutral-700 transition-colors w-full text-left"
|
||||||
|
style="border-left-color: {colorMap[team.color] || '#FFFFFF'}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-sm font-mono text-neutral-400 shrink-0 w-8 text-center"
|
||||||
|
>{team.kuerzel}</span
|
||||||
|
>
|
||||||
<span class="font-bold truncate" title={team.name}>
|
<span class="font-bold truncate" title={team.name}>
|
||||||
{team.name}
|
{team.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
{#if teams.length === 0}
|
{#if teams.length === 0}
|
||||||
<p class="col-span-full text-center text-neutral-400">Keine Teams angemeldet.</p>
|
<p class="col-span-full text-center text-neutral-400">
|
||||||
|
Keine Teams angemeldet.
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,14 +5,16 @@ class TeamHoverService {
|
|||||||
public currentHover = $state<number | undefined>(undefined);
|
public currentHover = $state<number | undefined>(undefined);
|
||||||
private fightConnector = get(fightConnector);
|
private fightConnector = get(fightConnector);
|
||||||
|
|
||||||
|
public disableConnections = $state(false);
|
||||||
|
|
||||||
setHover(teamId: number): void {
|
setHover(teamId: number): void {
|
||||||
this.currentHover = teamId;
|
this.currentHover = teamId;
|
||||||
this.fightConnector.addTeamConnection(teamId);
|
if (!this.disableConnections) this.fightConnector.addTeamConnection(teamId);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearHover(): void {
|
clearHover(): void {
|
||||||
this.currentHover = undefined;
|
this.currentHover = undefined;
|
||||||
this.fightConnector.clearConnections();
|
if (!this.disableConnections) this.fightConnector.clearConnections();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { z } from "astro:content";
|
|||||||
export const GroupViewSchema = z.object({
|
export const GroupViewSchema = z.object({
|
||||||
type: z.literal("GROUP"),
|
type: z.literal("GROUP"),
|
||||||
groups: z.array(z.number()),
|
groups: z.array(z.number()),
|
||||||
|
roundRows: z.number().int().positive().optional().default(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type GroupViewConfig = z.infer<typeof GroupViewSchema>;
|
export type GroupViewConfig = z.infer<typeof GroupViewSchema>;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
import Event from "@components/moderator/pages/event/Event.svelte";
|
import Event from "@components/moderator/pages/event/Event.svelte";
|
||||||
import Pages from "@components/moderator/pages/pages/Pages.svelte";
|
import Pages from "@components/moderator/pages/pages/Pages.svelte";
|
||||||
import Generator from "@components/moderator/pages/generators/Generator.svelte";
|
import Generator from "@components/moderator/pages/generators/Generator.svelte";
|
||||||
|
import AuditLog from "@components/moderator/pages/logs/AuditLog.svelte";
|
||||||
import { Tooltip } from "bits-ui";
|
import { Tooltip } from "bits-ui";
|
||||||
|
|
||||||
const routes: RouteDefinition = {
|
const routes: RouteDefinition = {
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
"/event/:id": Event,
|
"/event/:id": Event,
|
||||||
"/event/:id/generate": Generator,
|
"/event/:id/generate": Generator,
|
||||||
"/pages": Pages,
|
"/pages": Pages,
|
||||||
|
"/logs": AuditLog,
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -27,4 +27,5 @@
|
|||||||
<a href="#/players" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/players"}> Players </a>
|
<a href="#/players" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/players"}> Players </a>
|
||||||
<a href="#/pages" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/pages"}> Pages </a>
|
<a href="#/pages" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/pages"}> Pages </a>
|
||||||
<a href="#/schematics" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/schematics"}> Schematics </a>
|
<a href="#/schematics" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/schematics"}> Schematics </a>
|
||||||
|
<a href="#/logs" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/logs"}> Logs </a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -19,12 +19,10 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Table, TableBody, TableCell, TableCaption, TableHead, TableHeader, TableRow } from "@components/ui/table";
|
import { Table, TableBody, TableCell, TableCaption, TableHead, TableHeader, TableRow } from "@components/ui/table";
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command/index.js";
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover/index.js";
|
|
||||||
import { Button } from "@components/ui/button/index.js";
|
import { Button } from "@components/ui/button/index.js";
|
||||||
import type { ExtendedEvent } from "@type/event.ts";
|
import type { ExtendedEvent } from "@type/event.ts";
|
||||||
import { eventRepo } from "@repo/event";
|
import { eventRepo } from "@repo/event";
|
||||||
import { players } from "@stores/stores";
|
import PlayerSelector from "@components/ui/PlayerSelector.svelte";
|
||||||
|
|
||||||
const { event }: { event: ExtendedEvent } = $props();
|
const { event }: { event: ExtendedEvent } = $props();
|
||||||
|
|
||||||
@@ -39,8 +37,6 @@
|
|||||||
await $eventRepo.deleteReferees(event.event.id.toString(), [value]);
|
await $eventRepo.deleteReferees(event.event.id.toString(), [value]);
|
||||||
referees = await $eventRepo.listReferees(event.event.id.toString());
|
referees = await $eventRepo.listReferees(event.event.id.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
let playerSearch = $state("");
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Table>
|
<Table>
|
||||||
@@ -60,27 +56,7 @@
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
{/each}
|
{/each}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
<Popover>
|
<TableCaption>
|
||||||
<TableCaption>
|
<PlayerSelector placeholder="Hinzufügen" onSelect={(player) => addReferee(player.uuid)} />
|
||||||
<PopoverTrigger>
|
</TableCaption>
|
||||||
<Button>Hinzufügen</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
</TableCaption>
|
|
||||||
<PopoverContent class="p-0">
|
|
||||||
<Command shouldFilter={false}>
|
|
||||||
<CommandInput bind:value={playerSearch} placeholder="Search players..." />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>No Players found :(</CommandEmpty>
|
|
||||||
<CommandGroup heading="Players">
|
|
||||||
{#each $players
|
|
||||||
.filter((v) => v.name.toLowerCase().includes(playerSearch.toLowerCase()))
|
|
||||||
.filter((v, i) => i < 50)
|
|
||||||
.filter((v) => !referees.some((k) => k.uuid === v.uuid)) as player (player.uuid)}
|
|
||||||
<CommandItem value={player.name} onSelect={() => addReferee(player.uuid)} keywords={[player.uuid]}>{player.name}</CommandItem>
|
|
||||||
{/each}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
191
src/components/moderator/pages/logs/AuditLog.svelte
Normal file
191
src/components/moderator/pages/logs/AuditLog.svelte
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createSvelteTable, FlexRender } from "@components/ui/data-table";
|
||||||
|
import { columns } from "./columns";
|
||||||
|
import { getCoreRowModel, getPaginationRowModel, type PaginationState } from "@tanstack/table-core";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@components/ui/table";
|
||||||
|
import { auditLog } from "@components/repo/auditlog";
|
||||||
|
import { now, ZonedDateTime } from "@internationalized/date";
|
||||||
|
import { AuditLogEntrySchema, type AuditLogEntry } from "@components/types/auditlog";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||||
|
import { Input } from "@components/ui/input";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||||
|
import { Check } from "lucide-svelte";
|
||||||
|
import { cn } from "@components/utils";
|
||||||
|
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||||
|
import PlayerSelector from "@components/ui/PlayerSelector.svelte";
|
||||||
|
|
||||||
|
let debounceTimer: NodeJS.Timeout;
|
||||||
|
const debounce = <T,>(value: T, func: (value: T) => void) => {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
func(value);
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
let actionText = $state("");
|
||||||
|
let serverText = $state("");
|
||||||
|
let fullText = $state("");
|
||||||
|
let actors = $state<number[]>([]);
|
||||||
|
let actionTypes = $state<string[]>([]);
|
||||||
|
let timeGreater = $state<ZonedDateTime>(now("Europe/Berlin").subtract({ months: 1 }));
|
||||||
|
let timeLess = $state<ZonedDateTime>(now("Europe/Berlin"));
|
||||||
|
let serverOwner = $state<number[]>([]);
|
||||||
|
let velocity = $state(false);
|
||||||
|
let sorting = $state("DESC");
|
||||||
|
|
||||||
|
let pagination = $state<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 25,
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = $state<AuditLogEntry[]>([]);
|
||||||
|
let rows = $state(0);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
$auditLog
|
||||||
|
.get(
|
||||||
|
actionText || undefined,
|
||||||
|
serverText || undefined,
|
||||||
|
fullText || undefined,
|
||||||
|
actors.length > 0 ? actors : undefined,
|
||||||
|
actionTypes.length > 0 ? actionTypes : undefined,
|
||||||
|
timeGreater ? timeGreater.toDate().getTime() : undefined,
|
||||||
|
timeLess ? timeLess.toDate().getTime() : undefined,
|
||||||
|
serverOwner.length > 0 ? serverOwner : undefined,
|
||||||
|
velocity,
|
||||||
|
pagination.pageIndex,
|
||||||
|
pagination.pageSize,
|
||||||
|
sorting || undefined
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
data = res.entries;
|
||||||
|
rows = res.rows;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = createSvelteTable({
|
||||||
|
get data() {
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
get pagination() {
|
||||||
|
return pagination;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onPaginationChange: (updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
pagination = updater(pagination);
|
||||||
|
} else {
|
||||||
|
pagination = updater;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
manualPagination: true,
|
||||||
|
get rowCount() {
|
||||||
|
return rows;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let playerSearch = $state("");
|
||||||
|
let ownerSearch = $state("");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="rounded border mb-4 p-2 flex lg:flex-row flex-col">
|
||||||
|
<Input
|
||||||
|
class="w-48 mr-2"
|
||||||
|
placeholder="Suchen..."
|
||||||
|
value={fullText}
|
||||||
|
onchange={(e) =>
|
||||||
|
debounce(e.currentTarget.value, (v) => {
|
||||||
|
fullText = v;
|
||||||
|
})}
|
||||||
|
oninput={(e) =>
|
||||||
|
debounce(e.currentTarget.value, (v) => {
|
||||||
|
fullText = v;
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Select type="multiple" value={actionTypes} onValueChange={(e) => (actionTypes = e)}>
|
||||||
|
<SelectTrigger class="w-48 mr-2" placeholder="Aktionstypen auswählen...">Aktionstypen ({actionTypes.length})</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{#each ["JOIN", "LEAVE", "COMMAND", "SENSITIVE_COMMAND", "CHAT", "GUI_OPEN", "GUI_CLOSE", "GUI_CLICK"] as option}
|
||||||
|
<SelectItem value={option}>{option}</SelectItem>
|
||||||
|
{/each}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<div class="mr-2">
|
||||||
|
<PlayerSelector bind:value={actors} multiple placeholder="Spieler Filter" />
|
||||||
|
</div>
|
||||||
|
<div class="mr-2">
|
||||||
|
<PlayerSelector bind:value={serverOwner} multiple placeholder="Server Owner" />
|
||||||
|
</div>
|
||||||
|
<div class="mr-2">
|
||||||
|
<DateTimePicker bind:value={timeGreater} />
|
||||||
|
</div>
|
||||||
|
<div class="mr-2">
|
||||||
|
<DateTimePicker bind:value={timeLess} />
|
||||||
|
</div>
|
||||||
|
<Select type="single" value={sorting} onValueChange={(e) => (sorting = e)}>
|
||||||
|
<SelectTrigger class="w-48 mr-2">{sorting === "ASC" ? "Aufsteigend" : "Absteigend"}</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ASC">Aufsteigend</SelectItem>
|
||||||
|
<SelectItem value="DESC">Absteigend</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="rounded border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||||
|
<TableRow>
|
||||||
|
{#each headerGroup.headers as header (header.id)}
|
||||||
|
<TableHead colspan={header.colSpan}>
|
||||||
|
{#if !header.isPlaceholder}
|
||||||
|
<FlexRender content={header.column.columnDef.header} context={header.getContext()} />
|
||||||
|
{/if}
|
||||||
|
</TableHead>
|
||||||
|
{/each}
|
||||||
|
</TableRow>
|
||||||
|
{/each}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{#each table.getRowModel().rows as row (row.id)}
|
||||||
|
<TableRow>
|
||||||
|
{#each row.getVisibleCells() as cell (cell.id)}
|
||||||
|
<TableCell class="p-2 align-top">
|
||||||
|
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
|
||||||
|
</TableCell>
|
||||||
|
{/each}
|
||||||
|
</TableRow>
|
||||||
|
{:else}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colspan={columns.length} class="h-24 text-center">Keine Einträge gefunden.</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{/each}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end space-x-2 py-4">
|
||||||
|
<div>
|
||||||
|
<Select type="single" value={pagination.pageSize.toString()} onValueChange={(e) => (pagination = { pageSize: +e, pageIndex: 0 })}>
|
||||||
|
<SelectTrigger>{pagination.pageSize}</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="5">5</SelectItem>
|
||||||
|
<SelectItem value="10">10</SelectItem>
|
||||||
|
<SelectItem value="25">25</SelectItem>
|
||||||
|
<SelectItem value="50">50</SelectItem>
|
||||||
|
<SelectItem value="100">100</SelectItem>
|
||||||
|
<SelectItem value="200">200</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" onclick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>Previous</Button>
|
||||||
|
<Button variant="outline" size="sm" onclick={() => table.nextPage()} disabled={!table.getCanNextPage()}>Next</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
35
src/components/moderator/pages/logs/columns.ts
Normal file
35
src/components/moderator/pages/logs/columns.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import type { AuditLogEntry } from "@components/types/auditlog";
|
||||||
|
import type { ColumnDef } from "@tanstack/table-core";
|
||||||
|
|
||||||
|
export const columns: ColumnDef<AuditLogEntry>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "time",
|
||||||
|
header: "Time",
|
||||||
|
cell: (info) => new Date(info.getValue<number>()).toLocaleString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "server",
|
||||||
|
header: "Server",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "serverOwner",
|
||||||
|
header: "Server Owner",
|
||||||
|
cell: (info) => info.getValue<string | null>() || "N/A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "actor",
|
||||||
|
header: "Spieler",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "actionType",
|
||||||
|
header: "Action Type",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "actionText",
|
||||||
|
header: "Action Text",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -18,24 +18,27 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {permissions, players} from "@stores/stores.ts";
|
import { permissions } from "@stores/stores.ts";
|
||||||
import {Select, SelectContent, SelectItem} from "@components/ui/select";
|
import { Select, SelectContent, SelectItem } from "@components/ui/select";
|
||||||
import {SelectTrigger} from "@components/ui/select/index.js";
|
import { SelectTrigger } from "@components/ui/select/index.js";
|
||||||
import {permsRepo} from "@repo/perms.ts";
|
import { permsRepo } from "@repo/perms.ts";
|
||||||
|
|
||||||
const {
|
const { perms, uuid }: { perms: string[]; uuid: string } = $props();
|
||||||
perms, uuid
|
|
||||||
}: { perms: string[], uuid: string } = $props();
|
|
||||||
|
|
||||||
let value = $state(perms);
|
let value = $state(perms);
|
||||||
let prevValue = $state(perms);
|
let prevValue = $state(perms);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
value = perms;
|
||||||
|
prevValue = perms;
|
||||||
|
});
|
||||||
|
|
||||||
function onChange(change: string[]) {
|
function onChange(change: string[]) {
|
||||||
$permissions.perms.forEach(perm => {
|
$permissions.perms.forEach((perm) => {
|
||||||
if (prevValue.includes(perm) && !change.includes(perm)) {
|
if (prevValue.includes(perm) && !change.includes(perm)) {
|
||||||
$permsRepo.removePerm(uuid, perm)
|
$permsRepo.removePerm(uuid, perm);
|
||||||
} else if (!prevValue.includes(perm) && change.includes(perm)) {
|
} else if (!prevValue.includes(perm) && change.includes(perm)) {
|
||||||
$permsRepo.addPerm(uuid, perm)
|
$permsRepo.addPerm(uuid, perm);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -17,16 +17,132 @@
|
|||||||
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import Table from "@components/moderator/pages/players/Table.svelte";
|
import { createSvelteTable, FlexRender } from "@components/ui/data-table";
|
||||||
|
import { columns } from "./columns";
|
||||||
|
import { getCoreRowModel, getPaginationRowModel, type PaginationState } from "@tanstack/table-core";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@components/ui/table";
|
||||||
|
import { dataRepo } from "@repo/data";
|
||||||
|
import type { Player } from "@type/data";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||||
|
import { Input } from "@components/ui/input";
|
||||||
|
|
||||||
import {dataRepo} from "@repo/data";
|
let debounceTimer: NodeJS.Timeout;
|
||||||
|
const debounce = <T,>(value: T, func: (value: T) => void) => {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
func(value);
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
let playersFuture = $state($dataRepo.getPlayers())
|
let search = $state("");
|
||||||
|
|
||||||
|
let pagination = $state<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 25,
|
||||||
|
});
|
||||||
|
|
||||||
|
let data = $state<Player[]>([]);
|
||||||
|
let rows = $state(0);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
$dataRepo.queryPlayers(search || undefined, undefined, undefined, pagination.pageSize, pagination.pageIndex, true, false).then((res) => {
|
||||||
|
data = res.entries;
|
||||||
|
rows = res.rows;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = createSvelteTable({
|
||||||
|
get data() {
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
get pagination() {
|
||||||
|
return pagination;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onPaginationChange: (updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
pagination = updater(pagination);
|
||||||
|
} else {
|
||||||
|
pagination = updater;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
manualPagination: true,
|
||||||
|
get rowCount() {
|
||||||
|
return rows;
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await playersFuture}
|
<div class="p-4">
|
||||||
<p>Loading...</p>
|
<div class="rounded border mb-4 p-2 flex lg:flex-row flex-col">
|
||||||
{:then players}
|
<Input
|
||||||
<Table data={players} />
|
class="w-48 mr-2"
|
||||||
{/await}
|
placeholder="Search players..."
|
||||||
|
value={search}
|
||||||
|
onchange={(e) =>
|
||||||
|
debounce(e.currentTarget.value, (v) => {
|
||||||
|
search = v;
|
||||||
|
})}
|
||||||
|
oninput={(e) =>
|
||||||
|
debounce(e.currentTarget.value, (v) => {
|
||||||
|
search = v;
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rounded border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||||
|
<TableRow>
|
||||||
|
{#each headerGroup.headers as header (header.id)}
|
||||||
|
<TableHead colspan={header.colSpan}>
|
||||||
|
{#if !header.isPlaceholder}
|
||||||
|
<FlexRender content={header.column.columnDef.header} context={header.getContext()} />
|
||||||
|
{/if}
|
||||||
|
</TableHead>
|
||||||
|
{/each}
|
||||||
|
</TableRow>
|
||||||
|
{/each}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{#each table.getRowModel().rows as row (row.id)}
|
||||||
|
<TableRow>
|
||||||
|
{#each row.getVisibleCells() as cell (cell.id)}
|
||||||
|
<TableCell class="p-2 align-top">
|
||||||
|
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
|
||||||
|
</TableCell>
|
||||||
|
{/each}
|
||||||
|
</TableRow>
|
||||||
|
{:else}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colspan={columns.length} class="h-24 text-center">No players found.</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{/each}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end space-x-2 py-4">
|
||||||
|
<div>
|
||||||
|
<Select type="single" value={pagination.pageSize.toString()} onValueChange={(e) => (pagination = { pageSize: +e, pageIndex: 0 })}>
|
||||||
|
<SelectTrigger>{pagination.pageSize}</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="5">5</SelectItem>
|
||||||
|
<SelectItem value="10">10</SelectItem>
|
||||||
|
<SelectItem value="25">25</SelectItem>
|
||||||
|
<SelectItem value="50">50</SelectItem>
|
||||||
|
<SelectItem value="100">100</SelectItem>
|
||||||
|
<SelectItem value="200">200</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" onclick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>Previous</Button>
|
||||||
|
<Button variant="outline" size="sm" onclick={() => table.nextPage()} disabled={!table.getCanNextPage()}>Next</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -18,16 +18,18 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Select, SelectContent, SelectItem, SelectTrigger} from "@components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||||
import {permissions} from "@stores/stores.ts";
|
import { permissions } from "@stores/stores.ts";
|
||||||
import {permsRepo} from "@repo/perms.ts";
|
import { permsRepo } from "@repo/perms.ts";
|
||||||
|
|
||||||
const {
|
const { prefix, uuid }: { prefix: string; uuid: string } = $props();
|
||||||
prefix, uuid
|
|
||||||
}: { prefix: string, uuid: string } = $props();
|
|
||||||
|
|
||||||
let value = $state(prefix);
|
let value = $state(prefix);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
value = prefix;
|
||||||
|
});
|
||||||
|
|
||||||
function onChange(change: string) {
|
function onChange(change: string) {
|
||||||
$permsRepo.setPrefix(uuid, change);
|
$permsRepo.setPrefix(uuid, change);
|
||||||
|
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
<!--
|
|
||||||
- This file is a part of the SteamWar software.
|
|
||||||
-
|
|
||||||
- Copyright (C) 2025 SteamWar.de-Serverteam
|
|
||||||
-
|
|
||||||
- This program is free software: you can redistribute it and/or modify
|
|
||||||
- it under the terms of the GNU Affero General Public License as published by
|
|
||||||
- the Free Software Foundation, either version 3 of the License, or
|
|
||||||
- (at your option) any later version.
|
|
||||||
-
|
|
||||||
- This program is distributed in the hope that it will be useful,
|
|
||||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
- GNU Affero General Public License for more details.
|
|
||||||
-
|
|
||||||
- You should have received a copy of the GNU Affero General Public License
|
|
||||||
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import {
|
|
||||||
type ColumnFiltersState,
|
|
||||||
getCoreRowModel, getFilteredRowModel,
|
|
||||||
getPaginationRowModel, getSortedRowModel,
|
|
||||||
type PaginationState,
|
|
||||||
type SortingState,
|
|
||||||
} from "@tanstack/table-core";
|
|
||||||
import {
|
|
||||||
createSvelteTable,
|
|
||||||
FlexRender,
|
|
||||||
} from "@components/ui/data-table/index";
|
|
||||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table";
|
|
||||||
import {Button} from "@components/ui/button";
|
|
||||||
import {Input} from "@components/ui/input";
|
|
||||||
import {Select} from "@components/ui/select";
|
|
||||||
import {SelectContent, SelectItem, SelectTrigger} from "@components/ui/select/index.js";
|
|
||||||
import type {Player} from "@type/data";
|
|
||||||
import { columns } from "./columns";
|
|
||||||
|
|
||||||
let { data }: { data: Player[] } = $props();
|
|
||||||
|
|
||||||
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 25 });
|
|
||||||
let sorting = $state<SortingState>([]);
|
|
||||||
let columnFilters = $state<ColumnFiltersState>([]);
|
|
||||||
|
|
||||||
const table = createSvelteTable({
|
|
||||||
get data() {
|
|
||||||
return data;
|
|
||||||
},
|
|
||||||
state: {
|
|
||||||
get pagination() {
|
|
||||||
return pagination;
|
|
||||||
},
|
|
||||||
get sorting() {
|
|
||||||
return sorting;
|
|
||||||
},
|
|
||||||
get columnFilters() {
|
|
||||||
return columnFilters;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onPaginationChange: (updater) => {
|
|
||||||
if (typeof updater === "function") {
|
|
||||||
pagination = updater(pagination);
|
|
||||||
} else {
|
|
||||||
pagination = updater;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSortingChange: (updater) => {
|
|
||||||
if (typeof updater === "function") {
|
|
||||||
sorting = updater(sorting);
|
|
||||||
} else {
|
|
||||||
sorting = updater;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onColumnFiltersChange: (updater) => {
|
|
||||||
if (typeof updater === "function") {
|
|
||||||
columnFilters = updater(columnFilters);
|
|
||||||
} else {
|
|
||||||
columnFilters = updater;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
columns,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="rounded-md border m-4">
|
|
||||||
<div class="flex items-center p-4 border-b">
|
|
||||||
<Input
|
|
||||||
placeholder="Filter Players..."
|
|
||||||
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
|
|
||||||
onchange={(e) => {
|
|
||||||
table.getColumn("name")?.setFilterValue(e.currentTarget.value);
|
|
||||||
}}
|
|
||||||
oninput={(e) => {
|
|
||||||
table.getColumn("name")?.setFilterValue(e.currentTarget.value);
|
|
||||||
}}
|
|
||||||
class="max-w-sm"
|
|
||||||
/>
|
|
||||||
<div class="flex items-center px-4">
|
|
||||||
<Select type="single" value={pagination.pageSize.toString()} onValueChange={(e) => pagination = { pageSize: +e, pageIndex: 0 }}>
|
|
||||||
<SelectTrigger>{pagination.pageSize}</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="5">5</SelectItem>
|
|
||||||
<SelectItem value="10">10</SelectItem>
|
|
||||||
<SelectItem value="25">25</SelectItem>
|
|
||||||
<SelectItem value="50">50</SelectItem>
|
|
||||||
<SelectItem value="100">100</SelectItem>
|
|
||||||
<SelectItem value="200">200</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Table>
|
|
||||||
<TableHeader>
|
|
||||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
|
||||||
<TableRow>
|
|
||||||
{#each headerGroup.headers as header (header.id)}
|
|
||||||
<TableHead>
|
|
||||||
{#if !header.isPlaceholder}
|
|
||||||
<FlexRender
|
|
||||||
content={header.column.columnDef.header}
|
|
||||||
context={header.getContext()}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</TableHead>
|
|
||||||
{/each}
|
|
||||||
</TableRow>
|
|
||||||
{/each}
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{#each table.getRowModel().rows as row (row.id)}
|
|
||||||
<TableRow data-state={row.getIsSelected() && "selected"}>
|
|
||||||
{#each row.getVisibleCells() as cell (cell.id)}
|
|
||||||
<TableCell>
|
|
||||||
<FlexRender
|
|
||||||
content={cell.column.columnDef.cell}
|
|
||||||
context={cell.getContext()}
|
|
||||||
/>
|
|
||||||
</TableCell>
|
|
||||||
{/each}
|
|
||||||
</TableRow>
|
|
||||||
{:else}
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colspan={columns.length} class="h-24 text-center">
|
|
||||||
No results.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
{/each}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
<div class="flex items-center justify-end space-x-2 p-4 border-t">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onclick={() => table.previousPage()}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
Previous
|
|
||||||
</Button>
|
|
||||||
<span>{pagination.pageIndex + 1}/{table.getPageCount()}</span>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onclick={() => table.nextPage()}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {ColumnDef} from "@tanstack/table-core";
|
import type { ColumnDef } from "@tanstack/table-core";
|
||||||
import type {Player} from "@type/data.ts";
|
import type { Player } from "@type/data.ts";
|
||||||
import { renderComponent } from "@components/ui/data-table";
|
import { renderComponent } from "@components/ui/data-table";
|
||||||
import PermissionsDropdown from "@components/moderator/pages/players/PermissionsDropdown.svelte";
|
import PermissionsDropdown from "@components/moderator/pages/players/PermissionsDropdown.svelte";
|
||||||
import PrefixDropdown from "@components/moderator/pages/players/PrefixDropdown.svelte";
|
import PrefixDropdown from "@components/moderator/pages/players/PrefixDropdown.svelte";
|
||||||
@@ -36,25 +36,20 @@ export const columns: ColumnDef<Player[]> = [
|
|||||||
accessorKey: "prefix",
|
accessorKey: "prefix",
|
||||||
header: "Prefix",
|
header: "Prefix",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return renderComponent(
|
return renderComponent(PrefixDropdown, {
|
||||||
PrefixDropdown, {
|
prefix: row.getValue("prefix"),
|
||||||
prefix: row.getValue("prefix"),
|
uuid: row.getValue("uuid"),
|
||||||
uuid: row.getValue("uuid"),
|
});
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "perms",
|
accessorKey: "perms",
|
||||||
header: "Permissions",
|
header: "Permissions",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
return renderComponent(
|
return renderComponent(PermissionsDropdown, {
|
||||||
PermissionsDropdown,
|
perms: row.getValue("perms"),
|
||||||
{
|
uuid: row.getValue("uuid"),
|
||||||
perms: row.getValue("perms"),
|
});
|
||||||
uuid: row.getValue("uuid"),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
40
src/components/repo/auditlog.ts
Normal file
40
src/components/repo/auditlog.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { derived } from "svelte/store";
|
||||||
|
import { fetchWithToken, tokenStore } from "./repo";
|
||||||
|
import { PagedAutidLogSchema } from "@components/types/auditlog";
|
||||||
|
|
||||||
|
export class AuditLogRepo {
|
||||||
|
async get(
|
||||||
|
actionText: string | undefined,
|
||||||
|
serverText: string | undefined,
|
||||||
|
fullText: string | undefined,
|
||||||
|
actor: number[] | undefined,
|
||||||
|
actionType: string[] | undefined,
|
||||||
|
timeFrom: number | undefined,
|
||||||
|
timeTo: number | undefined,
|
||||||
|
serverOwner: number[] | undefined,
|
||||||
|
velocity: boolean | undefined,
|
||||||
|
page: number,
|
||||||
|
pageSize: number,
|
||||||
|
sorting: string | undefined
|
||||||
|
) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (actionText) params.append("actionText", actionText);
|
||||||
|
if (serverText) params.append("serverText", serverText);
|
||||||
|
if (fullText) params.append("fullText", fullText);
|
||||||
|
if (actor) actor.forEach((a) => params.append("actor", a.toString()));
|
||||||
|
if (actionType) actionType.forEach((a) => params.append("actionType", a));
|
||||||
|
if (timeFrom) params.append("timeGreater", timeFrom.toString());
|
||||||
|
if (timeTo) params.append("timeLess", timeTo.toString());
|
||||||
|
if (serverOwner) serverOwner.forEach((s) => params.append("serverOwner", s.toString()));
|
||||||
|
if (velocity !== undefined) params.append("velocity", velocity.toString());
|
||||||
|
params.append("page", page.toString());
|
||||||
|
params.append("limit", pageSize.toString());
|
||||||
|
if (sorting) params.append("sorting", sorting);
|
||||||
|
|
||||||
|
return await fetchWithToken("", `/auditlog?${params.toString()}`)
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then((data) => PagedAutidLogSchema.parse(data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auditLog = derived(tokenStore, ($token) => new AuditLogRepo());
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is a part of the SteamWar software.
|
* This file is a part of the SteamWar software.
|
||||||
*
|
*
|
||||||
* Copyright (C) 2025 SteamWar.de-Serverteam
|
* Copyright (C) 2026 SteamWar.de-Serverteam
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { readable, writable } from "svelte/store";
|
import { readable, writable } from "svelte/store";
|
||||||
import { ResponseUserSchema } from "@components/types/data";
|
import { PlayerSchema } from "@components/types/data";
|
||||||
|
|
||||||
export class AuthV2Repo {
|
export class AuthV2Repo {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -42,7 +42,7 @@ export class AuthV2Repo {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then((value) => value.json())
|
.then((value) => value.json())
|
||||||
.then((value) => ResponseUserSchema.parse(value));
|
.then((value) => PlayerSchema.parse(value));
|
||||||
|
|
||||||
loggedIn.set(true);
|
loggedIn.set(true);
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ export class AuthV2Repo {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((value) => value.json())
|
.then((value) => value.json())
|
||||||
.then((value) => ResponseUserSchema.parse(value));
|
.then((value) => PlayerSchema.parse(value));
|
||||||
loggedIn.set(true);
|
loggedIn.set(true);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -89,6 +89,6 @@ export class AuthV2Repo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loggedIn = writable(false);
|
export const loggedIn = writable<boolean | undefined>(undefined);
|
||||||
|
|
||||||
export const authV2Repo = readable(new AuthV2Repo());
|
export const authV2Repo = readable(new AuthV2Repo());
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Player, Server } from "@type/data.ts";
|
import type { Player, PlayerList, Server } from "@type/data.ts";
|
||||||
import { PlayerSchema, ServerSchema } from "@type/data.ts";
|
import { PlayerListSchema, PlayerSchema, ServerSchema } from "@type/data.ts";
|
||||||
import { fetchWithToken, tokenStore } from "./repo.ts";
|
import { fetchWithToken, tokenStore } from "./repo.ts";
|
||||||
import { derived, get } from "svelte/store";
|
import { derived, get } from "svelte/store";
|
||||||
import { TeamSchema, type Team } from "@components/types/team.ts";
|
import { TeamSchema, type Team } from "@components/types/team.ts";
|
||||||
@@ -38,10 +38,28 @@ export class DataRepo {
|
|||||||
.then(PlayerSchema.parse);
|
.then(PlayerSchema.parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPlayers(): Promise<Player[]> {
|
public async queryPlayers(
|
||||||
return await fetchWithToken(get(tokenStore), "/data/admin/users")
|
name: string | undefined,
|
||||||
|
uuid: string | undefined,
|
||||||
|
team: number[] | undefined,
|
||||||
|
limit: number | undefined,
|
||||||
|
page: number | undefined,
|
||||||
|
includePerms: boolean | undefined,
|
||||||
|
includeId: boolean | undefined
|
||||||
|
): Promise<PlayerList> {
|
||||||
|
let query = new URLSearchParams();
|
||||||
|
|
||||||
|
if (name) query.append("name", name);
|
||||||
|
if (uuid) query.append("uuid", uuid);
|
||||||
|
if (team) team.forEach((t) => query.append("team", t.toString()));
|
||||||
|
if (limit) query.append("limit", limit.toString());
|
||||||
|
if (page) query.append("page", page.toString());
|
||||||
|
if (includePerms !== undefined) query.append("includePerms", includePerms.toString());
|
||||||
|
if (includeId !== undefined) query.append("includeId", includeId.toString());
|
||||||
|
|
||||||
|
return await fetchWithToken(this.token, "/data/admin/users?" + query.toString())
|
||||||
.then((value) => value.json())
|
.then((value) => value.json())
|
||||||
.then(PlayerSchema.array().parse);
|
.then(PlayerListSchema.parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getTeams(): Promise<Team[]> {
|
public async getTeams(): Promise<Team[]> {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import type { CreateEventGroup, UpdateEventGroup, CreateEventRelation, UpdateEve
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { Dayjs } from "dayjs";
|
import type { Dayjs } from "dayjs";
|
||||||
import { derived } from "svelte/store";
|
import { derived } from "svelte/store";
|
||||||
import { ResponseUserSchema } from "@components/types/data";
|
import { PlayerSchema, type Player } from "@components/types/data";
|
||||||
|
|
||||||
export interface CreateEvent {
|
export interface CreateEvent {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -247,10 +247,10 @@ export class EventRepo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Referees
|
// Referees
|
||||||
public async listReferees(eventId: string): Promise<ResponseUser[]> {
|
public async listReferees(eventId: string): Promise<Player[]> {
|
||||||
return await fetchWithToken(this.token, `/events/${eventId}/referees`)
|
return await fetchWithToken(this.token, `/events/${eventId}/referees`)
|
||||||
.then((value) => value.json())
|
.then((value) => value.json())
|
||||||
.then((value) => z.array(ResponseUserSchema).parse(value));
|
.then((value) => z.array(PlayerSchema).parse(value));
|
||||||
}
|
}
|
||||||
public async updateReferees(eventId: string, refereeUuids: string[]): Promise<boolean> {
|
public async updateReferees(eventId: string, refereeUuids: string[]): Promise<boolean> {
|
||||||
const res = await fetchWithToken(this.token, `/events/${eventId}/referees`, {
|
const res = await fetchWithToken(this.token, `/events/${eventId}/referees`, {
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ import { permsRepo } from "@repo/perms.ts";
|
|||||||
|
|
||||||
export const schemTypes = cached<SchematicType[]>([], () => fetchWithToken(get(tokenStore), "/data/admin/schematicTypes").then((res) => res.json()));
|
export const schemTypes = cached<SchematicType[]>([], () => fetchWithToken(get(tokenStore), "/data/admin/schematicTypes").then((res) => res.json()));
|
||||||
|
|
||||||
export const players = cached<Player[]>([], async () => {
|
|
||||||
return get(dataRepo).getPlayers();
|
|
||||||
});
|
|
||||||
|
|
||||||
export const teams = cached<Team[]>([], async () => {
|
export const teams = cached<Team[]>([], async () => {
|
||||||
return get(dataRepo).getTeams();
|
return get(dataRepo).getTeams();
|
||||||
});
|
});
|
||||||
|
|||||||
19
src/components/types/auditlog.ts
Normal file
19
src/components/types/auditlog.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const AuditLogEntrySchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
time: z.number(),
|
||||||
|
server: z.string(),
|
||||||
|
serverOwner: z.string().nullable(),
|
||||||
|
actor: z.string(),
|
||||||
|
actionType: z.enum(["JOIN", "LEAVE", "COMMAND", "SENSITIVE_COMMAND", "CHAT", "GUI_OPEN", "GUI_CLOSE", "GUI_CLICK"]),
|
||||||
|
actionText: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PagedAutidLogSchema = z.object({
|
||||||
|
entries: z.array(AuditLogEntrySchema),
|
||||||
|
rows: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AuditLogEntry = z.infer<typeof AuditLogEntrySchema>;
|
||||||
|
export type PagedAuditLog = z.infer<typeof PagedAutidLogSchema>;
|
||||||
@@ -29,12 +29,20 @@ export type SchematicType = z.infer<typeof SchematicTypeSchema>;
|
|||||||
export const PlayerSchema = z.object({
|
export const PlayerSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
uuid: z.string(),
|
uuid: z.string(),
|
||||||
prefix: z.string(),
|
prefix: z.string().nullable(),
|
||||||
perms: z.array(z.string()),
|
perms: z.array(z.string()).nullable(),
|
||||||
|
id: z.number().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Player = z.infer<typeof PlayerSchema>;
|
export type Player = z.infer<typeof PlayerSchema>;
|
||||||
|
|
||||||
|
export const PlayerListSchema = z.object({
|
||||||
|
entries: z.array(PlayerSchema),
|
||||||
|
rows: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type PlayerList = z.infer<typeof PlayerListSchema>;
|
||||||
|
|
||||||
export const ServerSchema = z.object({
|
export const ServerSchema = z.object({
|
||||||
description: z.any(),
|
description: z.any(),
|
||||||
players: z.object({
|
players: z.object({
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TeamSchema } from "./team.js";
|
import { TeamSchema } from "./team.js";
|
||||||
import { PlayerSchema, ResponseUserSchema } from "./data.js";
|
import { PlayerSchema } from "./data.js";
|
||||||
|
|
||||||
export const ResponseGroupsSchema = z.object({
|
export const ResponseGroupsSchema = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
@@ -93,7 +93,7 @@ export const ExtendedEventSchema = z.object({
|
|||||||
teams: z.array(TeamSchema),
|
teams: z.array(TeamSchema),
|
||||||
groups: z.array(ResponseGroupsSchema),
|
groups: z.array(ResponseGroupsSchema),
|
||||||
fights: z.array(EventFightSchema),
|
fights: z.array(EventFightSchema),
|
||||||
referees: z.array(ResponseUserSchema),
|
referees: z.array(PlayerSchema),
|
||||||
relations: z.array(ResponseRelationSchema),
|
relations: z.array(ResponseRelationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
122
src/components/ui/PlayerSelector.svelte
Normal file
122
src/components/ui/PlayerSelector.svelte
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { Check, ChevronsUpDown } from "lucide-svelte";
|
||||||
|
import { cn } from "@components/utils";
|
||||||
|
import { dataRepo } from "@repo/data";
|
||||||
|
import type { Player } from "@type/data";
|
||||||
|
|
||||||
|
let {
|
||||||
|
value = $bindable(null),
|
||||||
|
multiple = false,
|
||||||
|
placeholder = "Select player...",
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
value?: number | number[] | null;
|
||||||
|
multiple?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
onSelect?: (player: Player) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let search = $state("");
|
||||||
|
let players: Player[] = $state([]);
|
||||||
|
let loading = $state(false);
|
||||||
|
|
||||||
|
let debounceTimer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
function fetchPlayers(searchTerm: string) {
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(async () => {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const res = await $dataRepo.queryPlayers(searchTerm || undefined, undefined, undefined, 50, 0, false, true);
|
||||||
|
players = res.entries;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
fetchPlayers(search);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSelect(player: Player) {
|
||||||
|
if (onSelect) {
|
||||||
|
onSelect(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (multiple) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.includes(player.id!)) {
|
||||||
|
value = value.filter((v) => v !== player.id);
|
||||||
|
} else {
|
||||||
|
value = [...value, player.id!];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = [player.id!];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (value === player.id) {
|
||||||
|
value = null; // Deselect
|
||||||
|
} else {
|
||||||
|
value = player.id;
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelected(id: number) {
|
||||||
|
if (multiple) {
|
||||||
|
return Array.isArray(value) && value.includes(id);
|
||||||
|
}
|
||||||
|
return value === id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let triggerLabel = $derived.by(() => {
|
||||||
|
if (multiple) {
|
||||||
|
if (Array.isArray(value) && value.length > 0) {
|
||||||
|
return `${placeholder} (${value.length})`;
|
||||||
|
}
|
||||||
|
return placeholder;
|
||||||
|
} else {
|
||||||
|
// We might need to fetch the selected player's name if it's not in the current list
|
||||||
|
// For now, let's just show the placeholder or "Selected"
|
||||||
|
// Ideally we would have a way to resolve the name from the UUID if it's not in `players`
|
||||||
|
// But `players` only contains search results.
|
||||||
|
// If we want to show the name, we might need to fetch it or pass it in.
|
||||||
|
// Given the context of AuditLog, it shows "Spieler Filter (count)".
|
||||||
|
// Given RefereesList, it's a button "Hinzufügen".
|
||||||
|
return placeholder;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Popover bind:open>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="outline" class={cn("justify-between", Array.isArray(value) && !value?.length && "text-muted-foreground")} {...props} role="combobox" aria-expanded={open}>
|
||||||
|
{triggerLabel}
|
||||||
|
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="p-0">
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput bind:value={search} placeholder="Search players..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No players found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{#each players as player (player.uuid)}
|
||||||
|
<CommandItem value={player.id?.toString()} onSelect={() => handleSelect(player)}>
|
||||||
|
<Check class={cn("mr-2 size-4", isSelected(player.id!) ? "opacity-100" : "opacity-0")} />
|
||||||
|
{player.name}
|
||||||
|
</CommandItem>
|
||||||
|
{/each}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
@@ -1,7 +1,4 @@
|
|||||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||||
import { type VariantProps, tv } from "tailwind-variants";
|
|
||||||
|
|
||||||
import Portal from "./sheet-portal.svelte";
|
|
||||||
import Overlay from "./sheet-overlay.svelte";
|
import Overlay from "./sheet-overlay.svelte";
|
||||||
import Content from "./sheet-content.svelte";
|
import Content from "./sheet-content.svelte";
|
||||||
import Header from "./sheet-header.svelte";
|
import Header from "./sheet-header.svelte";
|
||||||
@@ -12,6 +9,7 @@ import Description from "./sheet-description.svelte";
|
|||||||
const Root = SheetPrimitive.Root;
|
const Root = SheetPrimitive.Root;
|
||||||
const Close = SheetPrimitive.Close;
|
const Close = SheetPrimitive.Close;
|
||||||
const Trigger = SheetPrimitive.Trigger;
|
const Trigger = SheetPrimitive.Trigger;
|
||||||
|
const Portal = SheetPrimitive.Portal;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
@@ -36,71 +34,3 @@ export {
|
|||||||
Title as SheetTitle,
|
Title as SheetTitle,
|
||||||
Description as SheetDescription,
|
Description as SheetDescription,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sheetVariants = tv({
|
|
||||||
base: "bg-background fixed z-50 gap-4 p-6 shadow-lg",
|
|
||||||
variants: {
|
|
||||||
side: {
|
|
||||||
top: "inset-x-0 top-0 border-b",
|
|
||||||
bottom: "inset-x-0 bottom-0 border-t",
|
|
||||||
left: "inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
|
||||||
right: "inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultVariants: {
|
|
||||||
side: "right",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const sheetTransitions = {
|
|
||||||
top: {
|
|
||||||
in: {
|
|
||||||
y: "-100%",
|
|
||||||
duration: 500,
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
out: {
|
|
||||||
y: "-100%",
|
|
||||||
duration: 300,
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
bottom: {
|
|
||||||
in: {
|
|
||||||
y: "100%",
|
|
||||||
duration: 500,
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
out: {
|
|
||||||
y: "100%",
|
|
||||||
duration: 300,
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
left: {
|
|
||||||
in: {
|
|
||||||
x: "-100%",
|
|
||||||
duration: 500,
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
out: {
|
|
||||||
x: "-100%",
|
|
||||||
duration: 300,
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
right: {
|
|
||||||
in: {
|
|
||||||
x: "100%",
|
|
||||||
duration: 500,
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
out: {
|
|
||||||
x: "100%",
|
|
||||||
duration: 300,
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Side = VariantProps<typeof sheetVariants>["side"];
|
|
||||||
|
|||||||
@@ -1,47 +1,53 @@
|
|||||||
<script lang="ts">
|
<script lang="ts" module>
|
||||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
import { tv, type VariantProps } from "tailwind-variants";
|
||||||
import X from "lucide-svelte/icons/x";
|
export const sheetVariants = tv({
|
||||||
import { fly } from "svelte/transition";
|
base: "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 gap-4 p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||||
import {
|
variants: {
|
||||||
SheetOverlay,
|
side: {
|
||||||
SheetPortal,
|
top: "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b",
|
||||||
type Side,
|
bottom: "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t",
|
||||||
sheetTransitions,
|
left: "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||||
sheetVariants,
|
right: "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||||
} from "./index.js";
|
},
|
||||||
import { cn } from "$lib/components/utils.js";
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
type $$Props = SheetPrimitive.ContentProps & {
|
export type Side = VariantProps<typeof sheetVariants>["side"];
|
||||||
side?: Side;
|
|
||||||
};
|
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
|
||||||
export let side: $$Props["side"] = "right";
|
|
||||||
export { className as class };
|
|
||||||
export let inTransition: $$Props["inTransition"] = fly;
|
|
||||||
export let inTransitionConfig: $$Props["inTransitionConfig"] =
|
|
||||||
sheetTransitions[side ?? "right"].in;
|
|
||||||
export let outTransition: $$Props["outTransition"] = fly;
|
|
||||||
export let outTransitionConfig: $$Props["outTransitionConfig"] =
|
|
||||||
sheetTransitions[side ?? "right"].out;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SheetPortal>
|
<script lang="ts">
|
||||||
|
import { Dialog as SheetPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||||
|
import X from "@lucide/svelte/icons/x";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import SheetOverlay from "./sheet-overlay.svelte";
|
||||||
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
side = "right",
|
||||||
|
portalProps,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
|
||||||
|
portalProps?: SheetPrimitive.PortalProps;
|
||||||
|
side?: Side;
|
||||||
|
children: Snippet;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SheetPrimitive.Portal {...portalProps}>
|
||||||
<SheetOverlay />
|
<SheetOverlay />
|
||||||
<SheetPrimitive.Content
|
<SheetPrimitive.Content bind:ref class={cn(sheetVariants({ side }), className)} {...restProps}>
|
||||||
{inTransition}
|
{@render children?.()}
|
||||||
{inTransitionConfig}
|
|
||||||
{outTransition}
|
|
||||||
{outTransitionConfig}
|
|
||||||
class={cn(sheetVariants({ side }), className)}
|
|
||||||
{...$$restProps}
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
<SheetPrimitive.Close
|
<SheetPrimitive.Close
|
||||||
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
|
class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"
|
||||||
>
|
>
|
||||||
<X class="h-4 w-4" />
|
<X class="size-4" />
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
</SheetPrimitive.Close>
|
</SheetPrimitive.Close>
|
||||||
</SheetPrimitive.Content>
|
</SheetPrimitive.Content>
|
||||||
</SheetPortal>
|
</SheetPrimitive.Portal>
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = SheetPrimitive.DescriptionProps;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
let className: $$Props["class"] = undefined;
|
class: className,
|
||||||
export { className as class };
|
...restProps
|
||||||
|
}: SheetPrimitive.DescriptionProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SheetPrimitive.Description class={cn("text-muted-foreground text-sm", className)} {...$$restProps}>
|
<SheetPrimitive.Description
|
||||||
<slot />
|
bind:ref
|
||||||
</SheetPrimitive.Description>
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
let className: $$Props["class"] = undefined;
|
class: className,
|
||||||
export { className as class };
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
bind:this={ref}
|
||||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
let className: $$Props["class"] = undefined;
|
class: className,
|
||||||
export { className as class };
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...$$restProps}>
|
<div
|
||||||
<slot />
|
bind:this={ref}
|
||||||
|
class={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = SheetPrimitive.OverlayProps;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
...restProps
|
||||||
|
}: SheetPrimitive.OverlayProps = $props();
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
|
||||||
export let transition: $$Props["transition"] = fade;
|
|
||||||
export let transitionConfig: $$Props["transitionConfig"] = {
|
|
||||||
duration: 150,
|
|
||||||
};
|
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SheetPrimitive.Overlay
|
<SheetPrimitive.Overlay
|
||||||
{transition}
|
bind:ref
|
||||||
{transitionConfig}
|
class={cn(
|
||||||
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ", className)}
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||||
{...$$restProps}
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 { Dialog as SheetPrimitive } from "bits-ui";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = SheetPrimitive.TitleProps;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
let className: $$Props["class"] = undefined;
|
class: className,
|
||||||
export { className as class };
|
...restProps
|
||||||
|
}: SheetPrimitive.TitleProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SheetPrimitive.Title
|
<SheetPrimitive.Title
|
||||||
|
bind:ref
|
||||||
class={cn("text-foreground text-lg font-semibold", className)}
|
class={cn("text-foreground text-lg font-semibold", className)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
>
|
/>
|
||||||
<slot />
|
|
||||||
</SheetPrimitive.Title>
|
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
eventId: 76
|
eventId: 76
|
||||||
mode: microwargear
|
mode: microwargear
|
||||||
verwantwortlicher: SteamWar
|
verwantwortlicher: SteamWar
|
||||||
|
viewConfig:
|
||||||
|
groups:
|
||||||
|
name: Gruppenphase
|
||||||
|
view:
|
||||||
|
type: "GROUP"
|
||||||
|
groups: [13]
|
||||||
|
roundRows: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
**Ahoi, liebe Community,**
|
**Ahoi, liebe Community,**
|
||||||
@@ -10,22 +17,23 @@ es ist wieder Zeit, das Jahr neigt sich dem Ende und damit ist es wieder Zeit f
|
|||||||
|
|
||||||
## Übersicht
|
## Übersicht
|
||||||
|
|
||||||
- **Datum:** 01.01.2026 Start gegen 15 Uhr
|
- **Datum:** 01.01.2026 Start gegen 15 Uhr
|
||||||
- **Spielmodus:** MicroWarGear (Eigener Schematic Typ)
|
- **Spielmodus:** MicroWarGear (Eigener Schematic Typ)
|
||||||
- **Teamgröße**: 3 Personen
|
- **Teamgröße**: 3 Personen
|
||||||
- **Anmeldeschluss:** 28. Dezember 2025 (23:59 Uhr)
|
- **Anmeldeschluss:** 28. Dezember 2025 (23:59 Uhr)
|
||||||
- **Einsendeschluss:** 28. Dezember 2025 (23:59 Uhr)
|
- **Einsendeschluss:** 30. Dezember 2025 (23:59 Uhr)
|
||||||
- **Hotfix-Schluss:** 31. Dezember
|
- **Hotfix-Schluss:** 31. Dezember
|
||||||
|
|
||||||
## Sonderregeln
|
## Sonderregeln
|
||||||
|
|
||||||
- Maße: **13x13x13**
|
- Maße: **13x13x13**
|
||||||
- Freiluftbrücken erlaubt
|
- Freiluftbrücken erlaubt
|
||||||
- Version 1.21
|
- Version 1.21
|
||||||
- Jedes Team darf nur eine schematic einsenden.
|
- Jedes Team darf nur eine schematic einsenden.
|
||||||
- Alle Eventschematics werden nach dem Event zu MiniWarGears
|
- Alle Eventschematics werden nach dem Event zu MiniWarGears
|
||||||
|
|
||||||
## Weitere Hinweise
|
## Weitere Hinweise
|
||||||
|
|
||||||
- Techhider wird aktiv sein
|
- Techhider wird aktiv sein
|
||||||
- Kampfleiter darf zum Schuss auffordern
|
- Kampfleiter darf zum Schuss auffordern
|
||||||
- Auto Tech KO wird deaktiviert
|
- Auto Tech KO wird deaktiviert
|
||||||
@@ -41,7 +49,7 @@ Jedes Team wird ein konkreten Ansprechpartner für das Event stellen. In der Reg
|
|||||||
Schemnamen müssen mit dem Team Kürzel enden. Sollte eine andere Person als der Ansprechpartner die Schem einsenden, ist dies vom Ansprechpartner mit dem Eventleiter abzuklären.
|
Schemnamen müssen mit dem Team Kürzel enden. Sollte eine andere Person als der Ansprechpartner die Schem einsenden, ist dies vom Ansprechpartner mit dem Eventleiter abzuklären.
|
||||||
Der Kampfplan wird drei Tage vor dem Event erstellt um Organisatorische Fehler zu beheben und Nachfragen zu beantworten. Am Eventtag wird der Kampfplan nicht mehr geändert.
|
Der Kampfplan wird drei Tage vor dem Event erstellt um Organisatorische Fehler zu beheben und Nachfragen zu beantworten. Am Eventtag wird der Kampfplan nicht mehr geändert.
|
||||||
|
|
||||||
Eine Formelle Abmeldung nach Anmeldeschluss ist nicht möglich. Sollte ein Team trotz Anmeldung nicht Teilnehmen aber dies (*Vor beginn des Events*) dem Eventleiter mitteilen, werden die betroffene Kämpfe automatisch als Verloren Gewertet bzw. bei frühzeitiger Abmeldung, wird der Kampfplan nachträglich geändert. (In diesem Fall, je früher eine Absage erfolgt, umso besser)
|
Eine Formelle Abmeldung nach Anmeldeschluss ist nicht möglich. Sollte ein Team trotz Anmeldung nicht Teilnehmen aber dies (_Vor beginn des Events_) dem Eventleiter mitteilen, werden die betroffene Kämpfe automatisch als Verloren Gewertet bzw. bei frühzeitiger Abmeldung, wird der Kampfplan nachträglich geändert. (In diesem Fall, je früher eine Absage erfolgt, umso besser)
|
||||||
Sollte ein Team ohne jede Abmeldung einfach nicht antreten, werden alle Kämpfe automatisch als Verloren gewertet und zusätzlich wird das Team mit einer Halb- Jährigen Eventsperre versehen.
|
Sollte ein Team ohne jede Abmeldung einfach nicht antreten, werden alle Kämpfe automatisch als Verloren gewertet und zusätzlich wird das Team mit einer Halb- Jährigen Eventsperre versehen.
|
||||||
|
|
||||||
**Wir wünschen Euch viel Spaß beim Event und eine schöne Vorweihnachtliche Zeit!**
|
**Wir wünschen Euch viel Spaß beim Event und eine schöne Vorweihnachtliche Zeit!**
|
||||||
@@ -22,12 +22,12 @@ lange ist es her seit dem letzten WarGear-Event. Nun ist es so weit: Am **29. un
|
|||||||
|
|
||||||
## Übersicht
|
## Übersicht
|
||||||
|
|
||||||
- **Datum:** 29.11.: Gruppenphase, 30.11.: KO-Phase
|
- **Datum:** 29.11.: Gruppenphase, 30.11.: KO-Phase
|
||||||
- **Spielmodus:** Standard **und** Pro WarGear
|
- **Spielmodus:** Standard **und** Pro WarGear
|
||||||
- **Teamgröße**: 6
|
- **Teamgröße**: 6
|
||||||
- **Anmeldeschluss:** 22. November
|
- **Anmeldeschluss:** 22. November
|
||||||
- **Einsendeschluss:** 24. November
|
- **Einsendeschluss:** 24. November
|
||||||
- **Hotfix-Schluss:** 27. November
|
- **Hotfix-Schluss:** 27. November
|
||||||
|
|
||||||
Bei der SFA muss sich an eines der Regelwerke gehalten werden. Standard- und Pro-WarGear treten gleichwertig gegeneinander an.
|
Bei der SFA muss sich an eines der Regelwerke gehalten werden. Standard- und Pro-WarGear treten gleichwertig gegeneinander an.
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ mode: warship
|
|||||||
|
|
||||||
### §3 Kanone
|
### §3 Kanone
|
||||||
|
|
||||||
1. Eine Kanone ist eine Vorrichtung zum Beschleunigen von maximal 2 Projektilen.
|
1. Eine Kanone ist eine Vorrichtung zum Beschleunigen von maximal 2 Projektilen. Dies gilt zu jedem Zeitpunkt des Kampfes.
|
||||||
2. Eine Kanone muss manuell beladen werden und darf maximal alle 2s schießen.
|
2. Eine Kanone muss manuell beladen werden und darf maximal alle 2s schießen.
|
||||||
3. Kanonen dürfen nicht gezielt Projektile anderer Kanonen beeinflussen.
|
3. Kanonen dürfen nicht gezielt Projektile anderer Kanonen beeinflussen.
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
---
|
---
|
||||||
import DashboardComponent from "@components/DashboardComponent.svelte";
|
import DashboardComponent from "@components/DashboardComponent.svelte";
|
||||||
import PageLayout from "@layouts/PageLayout.astro";
|
import PageLayout from "@layouts/PageLayout.astro";
|
||||||
import {t} from "astro-i18n";
|
import { t } from "astro-i18n";
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout title={t("dashboard.page")}>
|
<PageLayout title={t("dashboard.page")}>
|
||||||
<script>
|
<script>
|
||||||
import {l} from "../util/util";
|
import { l } from "../util/util";
|
||||||
import {navigate} from "astro:transitions/client";
|
import { navigate } from "astro:transitions/client";
|
||||||
import {get} from "svelte/store";
|
import { loggedIn } from "../components/repo/authv2";
|
||||||
import {loggedIn} from "../components/repo/authv2";
|
|
||||||
document.addEventListener("astro:page-load", () => {
|
document.addEventListener("astro:page-load", () => {
|
||||||
if (window.location.href.endsWith("/dashboard") || window.location.href.endsWith("/dashboard/")) {
|
loggedIn.subscribe((value) => {
|
||||||
if (!get(loggedIn)) {
|
if (window.location.href.endsWith("/dashboard") || window.location.href.endsWith("/dashboard/")) {
|
||||||
navigate(l("/login"), {});
|
if (value === false) {
|
||||||
|
navigate(l("/dashboard"), { history: "replace" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<DashboardComponent client:only="svelte" />
|
<DashboardComponent client:only="svelte" />
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ import TeamList from "@components/event/TeamList.svelte";
|
|||||||
export const getStaticPaths = createGetStaticPaths(async () => {
|
export const getStaticPaths = createGetStaticPaths(async () => {
|
||||||
const events = await Promise.all(
|
const events = await Promise.all(
|
||||||
(await getCollection("events")).map(async (event) => ({
|
(await getCollection("events")).map(async (event) => ({
|
||||||
event: (await fetch(import.meta.env.PUBLIC_API_SERVER + "/events/" + event.data.eventId).then((value) => value.json())) as ExtendedEvent,
|
event: (await fetch(
|
||||||
|
import.meta.env.PUBLIC_API_SERVER +
|
||||||
|
"/events/" +
|
||||||
|
event.data.eventId,
|
||||||
|
).then((value) => value.json())) as ExtendedEvent,
|
||||||
page: event,
|
page: event,
|
||||||
}))
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
return events.map((event) => ({
|
return events.map((event) => ({
|
||||||
@@ -25,7 +29,10 @@ export const getStaticPaths = createGetStaticPaths(async () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
const { event, page } = Astro.props as { event: ExtendedEvent; page: CollectionEntry<"events"> };
|
const { event, page } = Astro.props as {
|
||||||
|
event: ExtendedEvent;
|
||||||
|
page: CollectionEntry<"events">;
|
||||||
|
};
|
||||||
|
|
||||||
const { Content } = await page.render();
|
const { Content } = await page.render();
|
||||||
---
|
---
|
||||||
@@ -35,19 +42,26 @@ const { Content } = await page.render();
|
|||||||
<h1 class="text-2xl font-bold">{event.event.name}</h1>
|
<h1 class="text-2xl font-bold">{event.event.name}</h1>
|
||||||
<h2 class="text-md text-gray-300 mb-4">
|
<h2 class="text-md text-gray-300 mb-4">
|
||||||
{
|
{
|
||||||
new Date(event.event.start).toLocaleDateString(astroI18n.locale, {
|
new Date(event.event.start).toLocaleDateString(
|
||||||
year: "numeric",
|
astroI18n.locale,
|
||||||
month: "numeric",
|
{
|
||||||
day: "numeric",
|
year: "numeric",
|
||||||
})
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
new Date(event.event.start).toDateString() !== new Date(event.event.end).toDateString()
|
new Date(event.event.start).toDateString() !==
|
||||||
? ` - ${new Date(event.event.end).toLocaleDateString(astroI18n.locale, {
|
new Date(event.event.end).toDateString()
|
||||||
year: "numeric",
|
? ` - ${new Date(event.event.end).toLocaleDateString(
|
||||||
month: "numeric",
|
astroI18n.locale,
|
||||||
day: "numeric",
|
{
|
||||||
})}`
|
year: "numeric",
|
||||||
|
month: "numeric",
|
||||||
|
day: "numeric",
|
||||||
|
},
|
||||||
|
)}`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -60,7 +74,11 @@ const { Content } = await page.render();
|
|||||||
page.data.viewConfig && (
|
page.data.viewConfig && (
|
||||||
<div class="py-2 border-t border-t-gray-600">
|
<div class="py-2 border-t border-t-gray-600">
|
||||||
<h1 class="text-2xl font-bold mb-4">Kampfplan</h1>
|
<h1 class="text-2xl font-bold mb-4">Kampfplan</h1>
|
||||||
<EventFights viewConfig={page.data.viewConfig} event={event} client:load />
|
<EventFights
|
||||||
|
viewConfig={page.data.viewConfig}
|
||||||
|
event={event}
|
||||||
|
client:load
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import BackgroundImage from "../components/BackgroundImage.astro";
|
|||||||
import { l } from "../util/util";
|
import { l } from "../util/util";
|
||||||
import { navigate } from "astro:transitions/client";
|
import { navigate } from "astro:transitions/client";
|
||||||
import { loggedIn } from "../components/repo/authv2";
|
import { loggedIn } from "../components/repo/authv2";
|
||||||
import { get } from "svelte/store";
|
|
||||||
|
|
||||||
document.addEventListener("astro:page-load", () => {
|
document.addEventListener("astro:page-load", () => {
|
||||||
if (window.location.href.endsWith("/login") || window.location.href.endsWith("/login/")) {
|
loggedIn.subscribe((value) => {
|
||||||
if (get(loggedIn)) {
|
if (window.location.href.endsWith("/login") || window.location.href.endsWith("/login/")) {
|
||||||
navigate(l("/dashboard"), { history: "replace" });
|
if (value) {
|
||||||
|
navigate(l("/dashboard"), { history: "replace" });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<div class="h-screen w-screen fixed -z-10">
|
<div class="h-screen w-screen fixed -z-10">
|
||||||
|
|||||||
Reference in New Issue
Block a user