Compare commits
45 Commits
2840fe80ef
...
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
|
|||
|
|
9aa62956a0 | ||
|
|
7ea7536367 | ||
| 2a2ee6701e | |||
| d1e889e2ff | |||
|
|
6beb488b0b | ||
|
|
f3b5be675a | ||
|
|
385d72b541 | ||
|
|
62a2a0fb3b | ||
|
5500f3b058
|
|||
|
b17cdb7d51
|
|||
|
a761ce371c
|
|||
|
ba88dd1ec3
|
|||
|
ddb19a85dc
|
|||
|
64adfe7c3b
|
|||
|
f503d59eeb
|
|||
|
a06e66012b
|
|||
|
d746e26a1c
|
|||
|
a9e1cb6025
|
|||
|
3daac95059
|
|||
|
1905aed535
|
|||
|
9c353a5eea
|
@@ -37,10 +37,6 @@
|
|||||||
"error",
|
"error",
|
||||||
4
|
4
|
||||||
],
|
],
|
||||||
"linebreak-style": [
|
|
||||||
"error",
|
|
||||||
"unix"
|
|
||||||
],
|
|
||||||
"quotes": [
|
"quotes": [
|
||||||
"error",
|
"error",
|
||||||
"double"
|
"double"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { preventDefault } from "svelte/legacy";
|
import { preventDefault } from "svelte/legacy";
|
||||||
|
|
||||||
import { l } from "@utils/util.ts";
|
import { l } from "@utils/util.ts";
|
||||||
import { t } from "astro-i18n";
|
import { t } from "astro-i18n";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
@@ -29,7 +28,6 @@
|
|||||||
|
|
||||||
let username: string = $state("");
|
let username: string = $state("");
|
||||||
let pw: string = $state("");
|
let pw: string = $state("");
|
||||||
|
|
||||||
let error: string = $state("");
|
let error: string = $state("");
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
@@ -61,14 +59,16 @@
|
|||||||
const accessToken = params.get("access_token");
|
const accessToken = params.get("access_token");
|
||||||
|
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
let auth = $authV2Repo.loginDiscord(accessToken);
|
(async () => {
|
||||||
if (!auth) {
|
let auth = await $authV2Repo.loginDiscord(accessToken);
|
||||||
pw = "";
|
if (!auth) {
|
||||||
error = t("login.error");
|
pw = "";
|
||||||
return;
|
error = t("login.error");
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
navigate(l("/dashboard"));
|
navigate(l("/dashboard"));
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -90,10 +90,7 @@
|
|||||||
<p class="mt-2 text-red-500">{error}</p>
|
<p class="mt-2 text-red-500">{error}</p>
|
||||||
{/if}
|
{/if}
|
||||||
<button class="btn mt-4 !mx-0 justify-center" type="submit" onclick={preventDefault(login)}>{t("login.submit")}</button>
|
<button class="btn mt-4 !mx-0 justify-center" type="submit" onclick={preventDefault(login)}>{t("login.submit")}</button>
|
||||||
<a
|
<a class="btn mt-4 !mx-0 justify-center" href="https://discord.com/oauth2/authorize?client_id=869606970099904562&response_type=token&redirect_uri=https%3A%2F%2Fsteamwar.de%2Flogin&scope=identify">
|
||||||
class="btn mt-4 !mx-0 justify-center"
|
|
||||||
href="https://discord.com/oauth2/authorize?client_id=869611389818400779&response_type=token&redirect_uri=http%3A%2F%2Flocalhost%3A4321%2Flogin&scope=identify"
|
|
||||||
>
|
|
||||||
{t("login.discord")}
|
{t("login.discord")}
|
||||||
</a>
|
</a>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
95
src/components/event/Calendar.svelte
Normal file
95
src/components/event/Calendar.svelte
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import "dayjs/locale/de";
|
||||||
|
import type { ExtendedEvent } from "../types/event";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-svelte";
|
||||||
|
import * as Card from "../ui/card";
|
||||||
|
import EventCard from "./EventCard.svelte";
|
||||||
|
import SWButton from "@components/styled/SWButton.svelte";
|
||||||
|
|
||||||
|
const {
|
||||||
|
events,
|
||||||
|
}: {
|
||||||
|
events: { slug: string; data: { event: ExtendedEvent } }[];
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let currentYear = $state(dayjs().year());
|
||||||
|
|
||||||
|
// Group events by month
|
||||||
|
let eventsByMonth = $derived.by(() => {
|
||||||
|
const grouped = new Map<string, typeof events>();
|
||||||
|
|
||||||
|
events.forEach((event) => {
|
||||||
|
const eventDate = dayjs(event.data.event.event.start).locale("de");
|
||||||
|
if (eventDate.year() === currentYear) {
|
||||||
|
const monthKey = eventDate.format("YYYY-MM");
|
||||||
|
if (!grouped.has(monthKey)) {
|
||||||
|
grouped.set(monthKey, []);
|
||||||
|
}
|
||||||
|
grouped.get(monthKey)!.push(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate all 12 months for the current year
|
||||||
|
let months = $derived.by(() => {
|
||||||
|
return Array.from({ length: 12 }, (_, i) => {
|
||||||
|
const monthDate = dayjs().locale("de").year(currentYear).month(i);
|
||||||
|
const monthKey = monthDate.format("YYYY-MM");
|
||||||
|
return {
|
||||||
|
date: monthDate,
|
||||||
|
key: monthKey,
|
||||||
|
name: monthDate.format("MMMM"),
|
||||||
|
events: eventsByMonth.get(monthKey) || [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function prevYear() {
|
||||||
|
currentYear = currentYear - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextYear() {
|
||||||
|
currentYear = currentYear + 1;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h2 class="text-2xl font-bold text-white">
|
||||||
|
{currentYear}
|
||||||
|
</h2>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<SWButton onclick={prevYear} type="gray">
|
||||||
|
<ChevronLeft size={20} />
|
||||||
|
</SWButton>
|
||||||
|
<SWButton onclick={nextYear} type="gray">
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</SWButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{#each months as month}
|
||||||
|
<EventCard title={month.name} unsized={true}>
|
||||||
|
{#if month.events.length > 0}
|
||||||
|
{#each month.events as event}
|
||||||
|
<a href={`/events/${event.slug}/`} class="block p-2 bg-slate-800 hover:bg-slate-700 rounded border border-slate-600 transition-colors group">
|
||||||
|
<div class="text-sm font-semibold text-white group-hover:text-blue-400 transition-colors">
|
||||||
|
{event.data.event.event.name}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-400 mt-1">
|
||||||
|
{dayjs(event.data.event.event.start).format("MMM D, YYYY • HH:mm")}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<p class="text-gray-500 text-sm italic">Keine Events für diesen Monat</p>
|
||||||
|
{/if}
|
||||||
|
</EventCard>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -4,13 +4,15 @@
|
|||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
children,
|
children,
|
||||||
|
unsized = false,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
children: Snippet;
|
children: Snippet;
|
||||||
|
unsized?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col w-72 m-4 gap-1">
|
<div class="flex flex-col gap-1 {unsized ? '' : 'w-72 m-4'}">
|
||||||
<div class="bg-gray-100 text-black font-bold px-2 rounded uppercase">
|
<div class="bg-gray-100 text-black font-bold px-2 rounded uppercase">
|
||||||
{title}
|
{title}
|
||||||
</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>
|
||||||
|
|||||||
122
src/components/event/EventList.svelte
Normal file
122
src/components/event/EventList.svelte
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ExtendedEvent } from "../types/event";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import * as Card from "../ui/card";
|
||||||
|
|
||||||
|
const { events }: { events: { slug: string; data: { event: ExtendedEvent } }[] } = $props();
|
||||||
|
// Categorize events into current, upcoming and past.
|
||||||
|
const now = dayjs();
|
||||||
|
const sorted = [...events].sort((a, b) => a.data.event.event.start - b.data.event.event.start);
|
||||||
|
|
||||||
|
const currentEvents = sorted
|
||||||
|
.filter((e) => {
|
||||||
|
const start = dayjs(e.data.event.event.start);
|
||||||
|
const end = dayjs(e.data.event.event.end);
|
||||||
|
return start.isBefore(now) && end.isAfter(now);
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.data.event.event.end - b.data.event.event.end);
|
||||||
|
|
||||||
|
const currentEvent = currentEvents[0];
|
||||||
|
const upcomingEvents = sorted.filter((e) => dayjs(e.data.event.event.start).isAfter(now));
|
||||||
|
const pastEvents = sorted.filter((e) => dayjs(e.data.event.event.end).isBefore(now)).sort((a, b) => b.data.event.event.end - a.data.event.event.end);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if currentEvent}
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-white mb-4">Aktuelles Event</h2>
|
||||||
|
<div class="grid grid-cols-1">
|
||||||
|
<a href={`/events/${currentEvent.slug}/`} class="group block h-full">
|
||||||
|
<Card.Root class="h-full overflow-hidden border-slate-700 bg-slate-800 transition-all hover:-translate-y-1 hover:shadow-xl">
|
||||||
|
<div class="h-32 bg-gradient-to-br from-blue-600 to-purple-700 relative">
|
||||||
|
<div class="absolute bottom-0 left-0 p-4 bg-gradient-to-t from-slate-900 to-transparent w-full">
|
||||||
|
<div class="inline-block bg-slate-900/80 backdrop-blur text-white text-xs font-bold px-2 py-1 rounded mb-1 border border-slate-600">
|
||||||
|
{dayjs(currentEvent.data.event.event.start).format("DD.MM.YYYY")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title class="text-white group-hover:text-blue-400 transition-colors">
|
||||||
|
{currentEvent.data.event.event.name}
|
||||||
|
</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<p class="text-gray-400 text-sm line-clamp-2">
|
||||||
|
Läuft seit {dayjs(currentEvent.data.event.event.start).format("HH:mm")}
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 flex items-center text-sm text-blue-400 font-medium">
|
||||||
|
Details <span class="ml-1 transition-transform group-hover:translate-x-1">→</span>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if upcomingEvents.length}
|
||||||
|
<div class="mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-white mb-4">Bevorstehende Events</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{#each upcomingEvents as event}
|
||||||
|
<a href={`/events/${event.slug}/`} class="group block h-full">
|
||||||
|
<Card.Root class="h-full overflow-hidden border-slate-700 bg-slate-800 transition-all hover:-translate-y-1 hover:shadow-xl">
|
||||||
|
<div class="h-32 bg-gradient-to-br from-blue-600 to-purple-700 relative">
|
||||||
|
<div class="absolute bottom-0 left-0 p-4 bg-gradient-to-t from-slate-900 to-transparent w-full">
|
||||||
|
<div class="inline-block bg-slate-900/80 backdrop-blur text-white text-xs font-bold px-2 py-1 rounded mb-1 border border-slate-600">
|
||||||
|
{dayjs(event.data.event.event.start).format("DD.MM.YYYY")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title class="text-white group-hover:text-blue-400 transition-colors">
|
||||||
|
{event.data.event.event.name}
|
||||||
|
</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<p class="text-gray-400 text-sm line-clamp-2">
|
||||||
|
Startet um {dayjs(event.data.event.event.start).format("HH:mm")}
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 flex items-center text-sm text-blue-400 font-medium">
|
||||||
|
Details <span class="ml-1 transition-transform group-hover:translate-x-1">→</span>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if pastEvents.length}
|
||||||
|
<div class="mb-4">
|
||||||
|
<h2 class="text-xl font-semibold text-white mb-4">Vergangene Events</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 opacity-70">
|
||||||
|
{#each pastEvents as event}
|
||||||
|
<a href={`/events/${event.slug}/`} class="group block h-full">
|
||||||
|
<Card.Root class="h-full overflow-hidden border-slate-700 bg-slate-800 transition-all hover:-translate-y-1 hover:shadow-xl">
|
||||||
|
<div class="h-32 bg-gradient-to-br from-blue-600 to-purple-700 relative">
|
||||||
|
<div class="absolute bottom-0 left-0 p-4 bg-gradient-to-t from-slate-900 to-transparent w-full">
|
||||||
|
<div class="inline-block bg-slate-900/80 backdrop-blur text-white text-xs font-bold px-2 py-1 rounded mb-1 border border-slate-600">
|
||||||
|
{dayjs(event.data.event.event.start).format("DD.MM.YYYY")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title class="text-white group-hover:text-blue-400 transition-colors">
|
||||||
|
{event.data.event.event.name}
|
||||||
|
</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<p class="text-gray-400 text-sm line-clamp-2">
|
||||||
|
Stattgefunden um {dayjs(event.data.event.event.start).format("HH:mm")}
|
||||||
|
</p>
|
||||||
|
<div class="mt-4 flex items-center text-sm text-blue-400 font-medium">
|
||||||
|
Details <span class="ml-1 transition-transform group-hover:translate-x-1">→</span>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
21
src/components/event/EventPage.svelte
Normal file
21
src/components/event/EventPage.svelte
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ExtendedEvent } from "../types/event";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Calendar } from "lucide-svelte";
|
||||||
|
import { List } from "lucide-svelte";
|
||||||
|
import EventList from "./EventList.svelte";
|
||||||
|
import CalendarView from "./Calendar.svelte";
|
||||||
|
|
||||||
|
const { events }: { events: { slug: string; data: { event: ExtendedEvent } }[] } = $props();
|
||||||
|
|
||||||
|
let viewMode = $state<"list" | "calendar">("list");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-6">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h1 class="text-3xl font-bold text-white">Events</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CalendarView {events} />
|
||||||
|
<EventList {events} />
|
||||||
|
</div>
|
||||||
@@ -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>
|
||||||
64
src/components/event/TeamList.svelte
Normal file
64
src/components/event/TeamList.svelte
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import type { ExtendedEvent } from "@components/types/event";
|
||||||
|
import type { Team } from "@components/types/team";
|
||||||
|
import { eventRepo } from "@components/repo/event";
|
||||||
|
|
||||||
|
const {
|
||||||
|
event,
|
||||||
|
}: {
|
||||||
|
event: ExtendedEvent;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let teams: Team[] = $state(event.teams);
|
||||||
|
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
"0": "#000000",
|
||||||
|
"1": "#0000AA",
|
||||||
|
"2": "#00AA00",
|
||||||
|
"3": "#00AAAA",
|
||||||
|
"4": "#AA0000",
|
||||||
|
"5": "#AA00AA",
|
||||||
|
"6": "#FFAA00",
|
||||||
|
"7": "#AAAAAA",
|
||||||
|
"8": "#555555",
|
||||||
|
"9": "#5555FF",
|
||||||
|
a: "#55FF55",
|
||||||
|
b: "#55FFFF",
|
||||||
|
c: "#FF5555",
|
||||||
|
d: "#FF55FF",
|
||||||
|
e: "#FFFF55",
|
||||||
|
f: "#FFFFFF",
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
teams = await $eventRepo.listTeams(event.event.id.toString());
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="py-2 border-t border-t-gray-600">
|
||||||
|
<h1 class="text-2xl font-bold mb-4">Angemeldete Teams</h1>
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2"
|
||||||
|
>
|
||||||
|
{#each teams as team}
|
||||||
|
<button
|
||||||
|
class="bg-neutral-800 p-2 rounded-md border border-neutral-700 border-l-4 flex flex-row items-center gap-2 cursor-pointer hover:bg-neutral-700 transition-colors w-full text-left"
|
||||||
|
style="border-left-color: {colorMap[team.color] || '#FFFFFF'}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="text-sm font-mono text-neutral-400 shrink-0 w-8 text-center"
|
||||||
|
>{team.kuerzel}</span
|
||||||
|
>
|
||||||
|
<span class="font-bold truncate" title={team.name}>
|
||||||
|
{team.name}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{#if teams.length === 0}
|
||||||
|
<p class="col-span-full text-center text-neutral-400">
|
||||||
|
Keine Teams angemeldet.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -5,14 +5,16 @@ class TeamHoverService {
|
|||||||
public currentHover = $state<number | undefined>(undefined);
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,4 +56,4 @@
|
|||||||
<SelectItem value={permission}>{permission}</SelectItem>
|
<SelectItem value={permission}>{permission}</SelectItem>
|
||||||
{/each}
|
{/each}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
@@ -44,4 +46,4 @@
|
|||||||
<SelectItem value={prefix[0]}>{prefix[1].chatPrefix === "" ? "None" : prefix[1].chatPrefix}</SelectItem>
|
<SelectItem value={prefix[0]}>{prefix[1].chatPrefix === "" ? "None" : prefix[1].chatPrefix}</SelectItem>
|
||||||
{/each}
|
{/each}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -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,13 +18,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
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() {
|
||||||
this.request("/data/me").then((value) => {
|
this.request("/data/me").then((value) => {
|
||||||
if (value.ok) {
|
if (value.ok) {
|
||||||
loggedIn.set(true);
|
loggedIn.set(true);
|
||||||
|
} else {
|
||||||
|
loggedIn.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -40,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);
|
||||||
|
|
||||||
@@ -55,9 +57,12 @@ export class AuthV2Repo {
|
|||||||
await this.request("/auth/discord", {
|
await this.request("/auth/discord", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: token,
|
body: token,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.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) {
|
||||||
@@ -84,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>
|
|
||||||
|
|||||||
55
src/content/events/neujahr2026.md
Normal file
55
src/content/events/neujahr2026.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
eventId: 76
|
||||||
|
mode: microwargear
|
||||||
|
verwantwortlicher: SteamWar
|
||||||
|
viewConfig:
|
||||||
|
groups:
|
||||||
|
name: Gruppenphase
|
||||||
|
view:
|
||||||
|
type: "GROUP"
|
||||||
|
groups: [13]
|
||||||
|
roundRows: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ahoi, liebe Community,**
|
||||||
|
|
||||||
|
es ist wieder Zeit, das Jahr neigt sich dem Ende und damit ist es wieder Zeit für das Neujahrsevent!
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
- **Datum:** 01.01.2026 Start gegen 15 Uhr
|
||||||
|
- **Spielmodus:** MicroWarGear (Eigener Schematic Typ)
|
||||||
|
- **Teamgröße**: 3 Personen
|
||||||
|
- **Anmeldeschluss:** 28. Dezember 2025 (23:59 Uhr)
|
||||||
|
- **Einsendeschluss:** 30. Dezember 2025 (23:59 Uhr)
|
||||||
|
- **Hotfix-Schluss:** 31. Dezember
|
||||||
|
|
||||||
|
## Sonderregeln
|
||||||
|
|
||||||
|
- Maße: **13x13x13**
|
||||||
|
- Freiluftbrücken erlaubt
|
||||||
|
- Version 1.21
|
||||||
|
- Jedes Team darf nur eine schematic einsenden.
|
||||||
|
- Alle Eventschematics werden nach dem Event zu MiniWarGears
|
||||||
|
|
||||||
|
## Weitere Hinweise
|
||||||
|
|
||||||
|
- Techhider wird aktiv sein
|
||||||
|
- Kampfleiter darf zum Schuss auffordern
|
||||||
|
- Auto Tech KO wird deaktiviert
|
||||||
|
- Es wird ein eigenen Schemtypen geben
|
||||||
|
- Turniersystem: All vs All
|
||||||
|
|
||||||
|
**Eventleiter:** AdmiralSeekrank
|
||||||
|
|
||||||
|
**Sonst noch wichtiges zu wissen**
|
||||||
|
|
||||||
|
Alle Absprachungen werden **nur** mit dem Eventleiter getroffen. Absprachungen mit anderen Personen gelten nicht! Fragen bezüglich des Events werden vom Eventleiter bearbeitet.
|
||||||
|
Jedes Team wird ein konkreten Ansprechpartner für das Event stellen. In der Regel ist dies der jeweilige Teamleader. Sollte eine andere Person dies übernehmen, ist diese Person dem Eventleiter mitzuteilen! Die Ansprechperson hat die Aufgabe, sich für Rückfragen bereitzustellen, Organisatorische Anliegen mit dem Eventleiter zu klären und die Schematic einzusenden.
|
||||||
|
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.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
**Wir wünschen Euch viel Spaß beim Event und eine schöne Vorweihnachtliche Zeit!**
|
||||||
@@ -3,6 +3,17 @@ eventId: 75
|
|||||||
mode: "wargear"
|
mode: "wargear"
|
||||||
verwantwortlicher: "Chaoscaot"
|
verwantwortlicher: "Chaoscaot"
|
||||||
image: ../../images/generated-image(11).png
|
image: ../../images/generated-image(11).png
|
||||||
|
viewConfig:
|
||||||
|
groups:
|
||||||
|
name: Gruppenphase
|
||||||
|
view:
|
||||||
|
type: "GROUP"
|
||||||
|
groups: [11]
|
||||||
|
elim:
|
||||||
|
name: Finale
|
||||||
|
view:
|
||||||
|
type: "ELEMINATION"
|
||||||
|
finalFight: 1613
|
||||||
---
|
---
|
||||||
|
|
||||||
**Ahoi, liebe Community,**
|
**Ahoi, liebe Community,**
|
||||||
@@ -11,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.
|
||||||
|
|
||||||
|
|||||||
11
src/content/rules/en/megawargear.md
Normal file
11
src/content/rules/en/megawargear.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
translationKey: megawg
|
||||||
|
---
|
||||||
|
|
||||||
|
# MegaWarGear Ruleset
|
||||||
|
|
||||||
|
For technical reasons MegaWarGear-Fights are held in version 1.12.2.
|
||||||
|
MegaWarGears provide the opportunity to build without limitations.
|
||||||
|
An elaborate design with defined shape is mandatory though (you may not just build some cube).
|
||||||
|
Besides the lack of limitations regarding dimensions and amounts, MegaWarGears are supposed to be similar to regular WarGears, in that endstone is the most resistant armoring block, dispensers should not contain TNT, etc.
|
||||||
|
Since this game-mode is not meant for serious competition, an approved MegaWarGear should be fine to release as a public.
|
||||||
135
src/content/rules/en/microwargear.md
Normal file
135
src/content/rules/en/microwargear.md
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
translationKey: microwg
|
||||||
|
---
|
||||||
|
|
||||||
|
# MicroWarGear Ruleset
|
||||||
|
|
||||||
|
MicroWargears are constructed in version 1.20.
|
||||||
|
|
||||||
|
## Dimensions
|
||||||
|
|
||||||
|
Max. 7 blocks deep
|
||||||
|
Max. 7 blocks wide
|
||||||
|
Max. 7 block high
|
||||||
|
|
||||||
|
A MicroWarGear may extend at most 7 blocks into every direction.
|
||||||
|
Shield related technology may be activated from any place within the MicroWarGear.
|
||||||
|
|
||||||
|
## Materials
|
||||||
|
|
||||||
|
Blocks used in MicroWarGear construction must not exceed a blast resistance of 9.
|
||||||
|
Inventory blocks must not contain items other than flowers, honey bottles and horse armor.
|
||||||
|
Chests, shulker boxes and barrels may contain TNT.
|
||||||
|
Dispensers may only individually contain either one stack of fire charges or one stack of basic arrows.
|
||||||
|
No more than 8 dispensers may be installed.
|
||||||
|
|
||||||
|
The following materials may not be used for construction: all saplings, minecraft:ice, nether portals, lava, waterlogged leaves and roots, TNT (pre-installed), any non-block entities.
|
||||||
|
Water may only be used within cannons and only to prevent damaging your gear.
|
||||||
|
|
||||||
|
## Cannons
|
||||||
|
|
||||||
|
Every MicroWarGear must have at least one functioning cannon.
|
||||||
|
A cannon is a continuous redstone contraption which is able to damage the opponent using primed TNT.
|
||||||
|
A TNT-cannon is the only place within a MicroWarGear where water may be placed and only if it does not leave the cannon or form water-shields.
|
||||||
|
Furthermore a cannon may not intentionally damage itself.
|
||||||
|
A cannon may at most shoot 8 projectiles at once.
|
||||||
|
Additionally, a single main-cannon can be installed, which may shoot up to 12 projectiles.
|
||||||
|
|
||||||
|
## Command Bridge
|
||||||
|
|
||||||
|
A MicroWarGear must contain a command bridge, either in the form of a clearly distinguishable room, open air command bridge or crawlspace command bridge.
|
||||||
|
The command bridge must be separated from the rest of the MicroWarGear by doors, fence gates, trapdoors or pistons.
|
||||||
|
|
||||||
|
A command bridge must adhere to the following conditions:
|
||||||
|
- At least 25 m² (1 block = 1 meter)
|
||||||
|
- A window through which the opponent is visible directly (not required for open air command bridges)
|
||||||
|
- Controls for at least 2 headlights that are visible from the opponent's position
|
||||||
|
- These controls must save their state until manually activated again
|
||||||
|
- The command bridge must be the only place where dispensers may be activated, which aim at the opponent
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
MicroWarGears must have a visual design.
|
||||||
|
The outermost layer of a MicroWarGear must have a blast resistance of at most 6.
|
||||||
|
It is expected that there is a continuous design structure across the entire front of the MicroWarGear.
|
||||||
|
At least 2 different kinds of blocks must be used in a design (not counting redstone components).
|
||||||
|
A "continuous design structure" means, that no substantial surface areas have too little or no depth to them.
|
||||||
|
Depth variation may also be achieved using walls or stairs.
|
||||||
|
|
||||||
|
## Bug-Using
|
||||||
|
|
||||||
|
The duplication of any blocks or entities is forbidden.
|
||||||
|
Excessive use of blocks that are replaced by the tech hider is also forbidden.
|
||||||
|
|
||||||
|
## Definitions
|
||||||
|
|
||||||
|
### Projectile
|
||||||
|
|
||||||
|
A projectile is any primed TNT entity, which leaves the extension limits of a MicroWarGear (7 blocks) towards the opponent.
|
||||||
|
|
||||||
|
### Propellant
|
||||||
|
|
||||||
|
A propellant is any primed TNT entity, which by exploding accellerates projectiles towards the opponent.
|
||||||
|
The propellant of any one cannon must only affect projectiles of that same cannon.
|
||||||
|
|
||||||
|
## Hidden Blocks (Replaced with Endstone)
|
||||||
|
|
||||||
|
- WATER
|
||||||
|
- NOTE_BLOCK
|
||||||
|
- POWERED_RAIL
|
||||||
|
- DETECTOR_RAIL
|
||||||
|
- PISTON
|
||||||
|
- PISTON_HEAD
|
||||||
|
- STICKY_PISTON
|
||||||
|
- TNT
|
||||||
|
- CHEST
|
||||||
|
- TRAPPED_CHEST
|
||||||
|
- REDSTONE_WIRE
|
||||||
|
- STONE_PRESSURE_PLATE
|
||||||
|
- IRON_DOOR
|
||||||
|
- OAK_PRESSURE_PLATE
|
||||||
|
- SPRUCE_PRESSURE_PLATE
|
||||||
|
- BIRCH_PRESSURE_PLATE
|
||||||
|
- JUNGLE_PRESSURE_PLATE
|
||||||
|
- ACACIA_PRESSURE_PLATE
|
||||||
|
- DARK_OAK_PRESSURE_PLATE
|
||||||
|
- REDSTONE_TORCH
|
||||||
|
- REDSTONE_WALL_TORCH
|
||||||
|
- REPEATER
|
||||||
|
- BREWING_STAND
|
||||||
|
- TRIPWIRE_HOOK
|
||||||
|
- TRIPWIRE
|
||||||
|
- HEAVY_WEIGHTED_PRESSURE_PLATE
|
||||||
|
- LIGHT_WEIGHTED_PRESSURE_PLATE
|
||||||
|
- COMPARATOR
|
||||||
|
- REDSTONE_BLOCK
|
||||||
|
- HOPPER
|
||||||
|
- ACTIVATOR_RAIL
|
||||||
|
- DROPPER
|
||||||
|
- SLIME_BLOCK
|
||||||
|
- OBSERVER
|
||||||
|
- HONEY_BLOCK
|
||||||
|
- LEVER
|
||||||
|
- SCULK_SENSOR
|
||||||
|
- POLISHED_BLACKSTONE_PRESSURE_PLATE
|
||||||
|
- MANGROVE_PRESSURE_PLATE
|
||||||
|
- CRIMSON_PRESSURE_PLATE
|
||||||
|
- WARPED_PRESSURE_PLATE
|
||||||
|
|
||||||
|
## The Contents of the Following Blocks Are Also Hidden
|
||||||
|
|
||||||
|
- SIGN
|
||||||
|
- DISPENSER
|
||||||
|
- CHEST
|
||||||
|
- TRAPPED_CHEST
|
||||||
|
- FURNACE
|
||||||
|
- BREWING_STAND
|
||||||
|
- HOPPER
|
||||||
|
- DROPPER
|
||||||
|
- SHULKER_BOX
|
||||||
|
- JUKEBOX
|
||||||
|
- COMPARATOR
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
Whether or not a MicroWarGear is rules compliant is up to the examiners.
|
||||||
158
src/content/rules/en/miniwargear.md
Normal file
158
src/content/rules/en/miniwargear.md
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
---
|
||||||
|
translationKey: mwg
|
||||||
|
mode: MiniWarGear
|
||||||
|
---
|
||||||
|
|
||||||
|
# MiniWarGear-Ruleset
|
||||||
|
|
||||||
|
MiniWarGears are constructed in version 1.20.
|
||||||
|
|
||||||
|
## Dimensions
|
||||||
|
|
||||||
|
- Max. 20 blocks deep (+ 1 block for design on each side) (22)
|
||||||
|
- Max. 35 blocks wide (+ 1 block for design on each side) (37)
|
||||||
|
- Max. 26 blocks high
|
||||||
|
|
||||||
|
A MiniWarGear may extend at most 7 blocks into every direction.
|
||||||
|
All shield related technology may only be activated from within the command bridge.
|
||||||
|
|
||||||
|
## Materials
|
||||||
|
|
||||||
|
Blocks used for the construction of a MiniWarGear must not exceed a blast resistance of 9.
|
||||||
|
A maximum of 120 TNT may be pre-installed.
|
||||||
|
Inventory blocks must not contain items other than flowers, honey bottles or horse armor.
|
||||||
|
Dispensers may only individually contain either one stack of fire charges or one stack of basic arrows.
|
||||||
|
No more than 16 dispensers may be installed.
|
||||||
|
|
||||||
|
The following materials may not be used for construction: all saplings, minecraft:ice, nether portals, lava, waterlogged leaves and roots, TNT (pre-installed), any non-block entities.
|
||||||
|
Water may only be used within cannons and only to prevent damaging your gear.
|
||||||
|
|
||||||
|
## Cannons
|
||||||
|
|
||||||
|
A cannon is a continuous redstone contraption which is able to damage the opponent using primed TNT.
|
||||||
|
A TNT-cannon is the only place within a MiniWarGear where water may be placed and only if it does not leave the cannon or form water-shields.
|
||||||
|
Furthermore a cannon may not intentionally damage itself.
|
||||||
|
A cannon may at most shoot 8 projectiles at once.
|
||||||
|
Additionally, a single main-cannon can be installed, which may shoot up to 12 projectiles.
|
||||||
|
The main-cannon must be a manual cannon.
|
||||||
|
|
||||||
|
A MiniWarGear may be equipped with up to 9 cannons.
|
||||||
|
It is forbidden to try to pass off multiple cannons as a single one.
|
||||||
|
It is also forbidden to try to pass off a single cannon as multiple.
|
||||||
|
Whether or not this is the case is up to the examiners and fight-judges.
|
||||||
|
|
||||||
|
Manual cannons are TNT-cannons, which require manual loading.
|
||||||
|
They may not be pre-loaded at the time of construction.
|
||||||
|
Furthermore manual cannons may fire up to three individual times after being loaded once.
|
||||||
|
All projectils of a manual cannon which is able to fire multiple times like this must be launched from the exact same launch-point.
|
||||||
|
That launch-point is defined by the first shot of a salve the cannon performs.
|
||||||
|
|
||||||
|
Automatic cannons are TNT-cannons which can fire at least 5 individual times, without being manually loaded.
|
||||||
|
They must be pre-loaded at the time of construction.
|
||||||
|
To qualify for being allowed to be pre-loaded the cannon must fire at least 5 times.
|
||||||
|
All projectils of an automatic cannon must be launched from the exact same launch-point.
|
||||||
|
The first 5 shots of an automatic cannon must have the exact same number of projectiles.
|
||||||
|
After the 6th shot the amount of projectiles may decrease, and must not increase.
|
||||||
|
Between individual shots of an automatic cannon must be at least 4 seconds of delay (40 redstone ticks, 80 game ticks).
|
||||||
|
A MiniWarGear may be equipped with up to two automatic cannons.
|
||||||
|
|
||||||
|
## Brücke
|
||||||
|
|
||||||
|
A MiniWarGear must feature a command bridge in the form of a clearly distinguishable room.
|
||||||
|
The command bridge must be separated from the rest of the MiniWarGear by doors, fence gates, trapdoors or pistons.
|
||||||
|
|
||||||
|
A command bridge must adhere to the following conditions:
|
||||||
|
|
||||||
|
- At least 25 m² (1 block = 1 meter)
|
||||||
|
- A periodic, acoustic (note block / bell) and optical damage sensor
|
||||||
|
- A window through which the opponent is visible directly
|
||||||
|
- Controls for at least 4 headlights that are visible from the opposing position
|
||||||
|
- Controls for automatic cannons (if any are installed)
|
||||||
|
- Controls for shield technology (if there is any)
|
||||||
|
- The command bridge is the only place where dispensers which aim at the opponent may be controlled
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
MiniWarGears must (besides at least one cannon) feature a visual design.
|
||||||
|
The outermost layer of a MiniWarGear must only have a blast resistance of at most 6.
|
||||||
|
It is expected that there is a continuous design structure across the entire front of a MiniWarGear.
|
||||||
|
At least 2 different kinds of blocks must be used in a design (not counting redstone components).
|
||||||
|
A "continuous design structure" means, that no substantial surface areas have little to no depth.
|
||||||
|
Depth variation may also in part be achieved using walls or stairs, but this should not be overused.
|
||||||
|
Whether or not this is the case is up to the examiner.
|
||||||
|
|
||||||
|
## Bug-Using
|
||||||
|
|
||||||
|
The creation of TNT in a MiniWarGear is forbidden.
|
||||||
|
|
||||||
|
Excessive use of blocks which are hidden by the tech-hider is also forbidden.
|
||||||
|
|
||||||
|
## Definitions
|
||||||
|
|
||||||
|
### Projectile
|
||||||
|
|
||||||
|
A Projectile is any primed TNT entity, which is accelerated by propellant.
|
||||||
|
Furthermore a projectile is any primed TNT entity, which leaves the extension limits of a MiniWarGear (7 blocks) towards the opponent.
|
||||||
|
|
||||||
|
### Propellant
|
||||||
|
|
||||||
|
A propellant is any primed TNT entity, which by exploding accellerates projectiles towards the opposing half of the arena.
|
||||||
|
The propellant of any one cannon must only affect projectiles of that same cannon.
|
||||||
|
|
||||||
|
## Hidden Blocks (Replaced with Endstone)
|
||||||
|
|
||||||
|
- WATER
|
||||||
|
- NOTE_BLOCK
|
||||||
|
- POWERED_RAIL
|
||||||
|
- DETECTOR_RAIL
|
||||||
|
- PISTON
|
||||||
|
- PISTON_HEAD
|
||||||
|
- STICKY_PISTON
|
||||||
|
- TNT
|
||||||
|
- CHEST
|
||||||
|
- TRAPPED_CHEST
|
||||||
|
- REDSTONE_WIRE
|
||||||
|
- STONE_PRESSURE_PLATE
|
||||||
|
- IRON_DOOR
|
||||||
|
- OAK_PRESSURE_PLATE
|
||||||
|
- SPRUCE_PRESSURE_PLATE
|
||||||
|
- BIRCH_PRESSURE_PLATE
|
||||||
|
- JUNGLE_PRESSURE_PLATE
|
||||||
|
- ACACIA_PRESSURE_PLATE
|
||||||
|
- DARK_OAK_PRESSURE_PLATE
|
||||||
|
- REDSTONE_TORCH
|
||||||
|
- REDSTONE_WALL_TORCH
|
||||||
|
- REPEATER
|
||||||
|
- BREWING_STAND
|
||||||
|
- TRIPWIRE_HOOK
|
||||||
|
- TRIPWIRE
|
||||||
|
- HEAVY_WEIGHTED_PRESSURE_PLATE
|
||||||
|
- LIGHT_WEIGHTED_PRESSURE_PLATE
|
||||||
|
- COMPARATOR
|
||||||
|
- REDSTONE_BLOCK
|
||||||
|
- HOPPER
|
||||||
|
- ACTIVATOR_RAIL
|
||||||
|
- DROPPER
|
||||||
|
- SLIME_BLOCK
|
||||||
|
- OBSERVER
|
||||||
|
- HONEY_BLOCK
|
||||||
|
- LEVER
|
||||||
|
- SCULK_SENSOR
|
||||||
|
- POLISHED_BLACKSTONE_PRESSURE_PLATE
|
||||||
|
- MANGROVE_PRESSURE_PLATE
|
||||||
|
- CRIMSON_PRESSURE_PLATE
|
||||||
|
- WARPED_PRESSURE_PLATE
|
||||||
|
|
||||||
|
## The Contents of the Following Blocks Are Also Hidden:
|
||||||
|
|
||||||
|
- SIGN
|
||||||
|
- DISPENSER
|
||||||
|
- CHEST
|
||||||
|
- TRAPPED_CHEST
|
||||||
|
- FURNACE
|
||||||
|
- BREWING_STAND
|
||||||
|
- HOPPER
|
||||||
|
- DROPPER
|
||||||
|
- SHULKER_BOX
|
||||||
|
- JUKEBOX
|
||||||
|
- COMPARATOR
|
||||||
31
src/content/rules/en/quickgear.md
Normal file
31
src/content/rules/en/quickgear.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
translationKey: qg
|
||||||
|
---
|
||||||
|
|
||||||
|
# QuickGear-Ruleset
|
||||||
|
|
||||||
|
QuickGears are constructed in version 1.20.
|
||||||
|
|
||||||
|
## Dimensions
|
||||||
|
|
||||||
|
Max. 20 blocks deep (+ 1 block for design on each side) (22)
|
||||||
|
Max. 35 blocks wide (+ 1 block for design on each side) (37)
|
||||||
|
Max. 26 blocks high
|
||||||
|
|
||||||
|
No block may leave a QuickGear.
|
||||||
|
|
||||||
|
## Materials
|
||||||
|
|
||||||
|
All blocks in a QuickGear must by destructible by TNT explosions (except for water).
|
||||||
|
There must not be any pre-installed TNT blocks in a QuickGear.
|
||||||
|
Blocks with inventories may only contain flowers, honey bottles and horse armor.
|
||||||
|
Dispensers may only individually contain one stack of fire charges or one stack of arrows (without effects).
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
A design is very welcome, but not required.
|
||||||
|
|
||||||
|
## Bug-Using
|
||||||
|
|
||||||
|
Primed TNT may only be created from TNT blocks that players have placed.
|
||||||
|
The duplication of TNT is prohibited.
|
||||||
@@ -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) 2025 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
|
||||||
@@ -22,4 +22,4 @@ import { useAstroI18n } from "astro-i18n";
|
|||||||
|
|
||||||
const astroI18n = useAstroI18n();
|
const astroI18n = useAstroI18n();
|
||||||
|
|
||||||
export const onRequest = sequence(astroI18n);
|
export const onRequest = sequence(astroI18n);
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
---
|
---
|
||||||
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" />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@@ -4,13 +4,18 @@ import PageLayout from "@layouts/PageLayout.astro";
|
|||||||
import { astroI18n, createGetStaticPaths } from "astro-i18n";
|
import { astroI18n, createGetStaticPaths } from "astro-i18n";
|
||||||
import { getCollection, type CollectionEntry } from "astro:content";
|
import { getCollection, type CollectionEntry } from "astro:content";
|
||||||
import EventFights from "@components/event/EventFights.svelte";
|
import EventFights from "@components/event/EventFights.svelte";
|
||||||
|
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) => ({
|
||||||
@@ -24,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();
|
||||||
---
|
---
|
||||||
@@ -34,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>
|
||||||
@@ -54,11 +69,16 @@ const { Content } = await page.render();
|
|||||||
<article>
|
<article>
|
||||||
<Content />
|
<Content />
|
||||||
</article>
|
</article>
|
||||||
|
<TeamList client:load event={event} />
|
||||||
{
|
{
|
||||||
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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,35 +2,23 @@
|
|||||||
import type { ExtendedEvent } from "@components/types/event";
|
import type { ExtendedEvent } from "@components/types/event";
|
||||||
import PageLayout from "@layouts/PageLayout.astro";
|
import PageLayout from "@layouts/PageLayout.astro";
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
|
import EventPage from "@components/event/EventPage.svelte";
|
||||||
|
|
||||||
const events = await Promise.all(
|
const events = await Promise.all(
|
||||||
(await getCollection("events")).map(async (event) => ({
|
(await getCollection("events")).map(async (event) => ({
|
||||||
...event,
|
...event,
|
||||||
data: {
|
data: {
|
||||||
...event.data,
|
...event.data,
|
||||||
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,
|
||||||
},
|
},
|
||||||
}))
|
})),
|
||||||
);
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
<PageLayout title="Events">
|
<PageLayout title="Events">
|
||||||
{
|
<EventPage client:load {events} />
|
||||||
events.map((event) => (
|
|
||||||
<article class="mb-8">
|
|
||||||
<h2 class="text-2xl font-bold mb-2">
|
|
||||||
<a href={`/events/${event.slug}/`} class="text-blue-600 hover:underline">
|
|
||||||
{event.data.event.event.name ?? "Hello, World!"}
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
<p class="text-gray-600 mb-1">
|
|
||||||
{new Date(event.data.event.event.start).toLocaleDateString(undefined, {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
|
|||||||
@@ -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