Compare commits
19 Commits
ab4d4a1a91
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
35765b90e6
|
|||
|
fa7e68ca10
|
|||
|
f507dce94a
|
|||
|
1ed1901311
|
|||
|
|
63d03f2226 | ||
|
e27b4fb0f4
|
|||
|
8fa1d41639
|
|||
|
f13305d116
|
|||
| ff59ac3747 | |||
|
09035e3acd
|
|||
|
9be8702e6a
|
|||
|
ffe875260d
|
|||
|
64b82eddff
|
|||
|
e3432ce7bd
|
|||
|
6cdf2e0933
|
|||
|
b0a9d56216
|
|||
|
3ffc715929
|
|||
|
9589a496c0
|
|||
|
bdebe768b2
|
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src\\styles\\app.css",
|
||||
"baseColor": "slate"
|
||||
},
|
||||
@@ -10,8 +8,9 @@
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/components/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks"
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"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.
|
||||
-
|
||||
- Copyright (C) 2023 SteamWar.de-Serverteam
|
||||
- Copyright (C) 2026 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
|
||||
|
||||
@@ -18,19 +18,18 @@
|
||||
-->
|
||||
|
||||
<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 {ArrowLeftOutline} from "flowbite-svelte-icons";
|
||||
import FightList from "./event/FightList.svelte";
|
||||
import { ArrowLeftOutline } from "flowbite-svelte-icons";
|
||||
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";
|
||||
|
||||
interface Props {
|
||||
params: { id: number };
|
||||
}
|
||||
interface Props {
|
||||
params: { id: number };
|
||||
}
|
||||
|
||||
let { params }: Props = $props();
|
||||
let { params }: Props = $props();
|
||||
|
||||
let id = params.id;
|
||||
let event = $eventRepo.getEvent(id.toString());
|
||||
@@ -38,44 +37,43 @@
|
||||
|
||||
{#await event}
|
||||
<div class="h-screen w-screen grid place-items-center">
|
||||
<Spinner size={16}/>
|
||||
<Spinner size={16} />
|
||||
</div>
|
||||
{:then data}
|
||||
<Navbar >
|
||||
<Navbar>
|
||||
{#snippet children({ hidden, toggle })}
|
||||
<NavBrand href="#">
|
||||
<ArrowLeftOutline></ArrowLeftOutline>
|
||||
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white">
|
||||
{data.event.name}
|
||||
</span>
|
||||
</NavBrand>
|
||||
{/snippet}
|
||||
<NavBrand href="#">
|
||||
<ArrowLeftOutline></ArrowLeftOutline>
|
||||
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white">
|
||||
{data.event.name}
|
||||
</span>
|
||||
</NavBrand>
|
||||
{/snippet}
|
||||
</Navbar>
|
||||
|
||||
<Tabs style="pill" class="mx-4 flex shadow-lg border-b-2 border-gray-700 pb-2" contentClass="">
|
||||
<TabItem open>
|
||||
{#snippet title()}
|
||||
<span >Event</span>
|
||||
{/snippet}
|
||||
<EventEdit {data}/>
|
||||
<span>Event</span>
|
||||
{/snippet}
|
||||
<EventEdit {data} />
|
||||
</TabItem>
|
||||
<TabItem>
|
||||
{#snippet title()}
|
||||
<span >Teams</span>
|
||||
{/snippet}
|
||||
<TeamList {data}/>
|
||||
<span>Teams</span>
|
||||
{/snippet}
|
||||
<TeamList {data} />
|
||||
</TabItem>
|
||||
<TabItem>
|
||||
{#snippet title()}
|
||||
<span >Schiedsrichter</span>
|
||||
{/snippet}
|
||||
<RefereesList {data}/>
|
||||
<span>Schiedsrichter</span>
|
||||
{/snippet}
|
||||
<RefereesList {data} />
|
||||
</TabItem>
|
||||
<TabItem>
|
||||
{#snippet title()}
|
||||
<span >Kämpfe</span>
|
||||
{/snippet}
|
||||
<FightList {data}/>
|
||||
<span>Kämpfe</span>
|
||||
{/snippet}
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
{:catch error}
|
||||
|
||||
@@ -1,307 +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} from "@stores/stores.ts";
|
||||
import TypeAheadSearch from "../../components/TypeAheadSearch.svelte";
|
||||
import PlayerSelector from "@components/ui/PlayerSelector.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 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()}
|
||||
<Modal bind:open={spectatePortOpen} title="Change Kampfleiter" size="sm">
|
||||
<div class="m-2">
|
||||
<Label for="fight-kampf">Kampfleiter</Label>
|
||||
<PlayerSelector bind:value={spectatePort} placeholder="Search player..." />
|
||||
</div>
|
||||
{#snippet footer()}
|
||||
|
||||
<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>
|
||||
@@ -55,35 +55,36 @@
|
||||
<!-- 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 firstSegmentWidth = horizontalDistance / 2}
|
||||
{@const secondSegmentWidth = horizontalDistance / 2}
|
||||
|
||||
<div
|
||||
class="horizontal-line"
|
||||
style="
|
||||
{#if firstSegmentWidth > 0}
|
||||
<div
|
||||
class="horizontal-line"
|
||||
style="
|
||||
background-color: {connection.color};
|
||||
left: {fromLeft}px;
|
||||
top: {fromTop + connection.offset / 4}px;
|
||||
width: {firstSegmentWidth + connection.offset + 2}px;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
class="vertical-line"
|
||||
style="
|
||||
></div>
|
||||
<div
|
||||
class="vertical-line"
|
||||
style="
|
||||
background-color: {connection.color};
|
||||
left: {midLeft}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;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
class="horizontal-line"
|
||||
style="
|
||||
></div>
|
||||
<div
|
||||
class="horizontal-line"
|
||||
style="
|
||||
background-color: {connection.color};
|
||||
left: {midLeft}px;
|
||||
top: {toTop + connection.offset / 4}px;
|
||||
width: {secondSegmentWidth - connection.offset}px;
|
||||
width: {firstSegmentWidth - connection.offset}px;
|
||||
"
|
||||
></div>
|
||||
></div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<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 EventCard from "./EventCard.svelte";
|
||||
import EventFightChip from "./EventFightChip.svelte";
|
||||
import { onMount, onDestroy, tick } from "svelte";
|
||||
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 = {
|
||||
id: -1,
|
||||
@@ -18,7 +26,9 @@
|
||||
points: null,
|
||||
};
|
||||
|
||||
function indexRelations(ev: ExtendedEvent): Map<number, ResponseRelation[]> {
|
||||
function indexRelations(
|
||||
ev: ExtendedEvent,
|
||||
): Map<number, ResponseRelation[]> {
|
||||
const map = new Map<number, ResponseRelation[]>();
|
||||
for (const rel of ev.relations) {
|
||||
const list = map.get(rel.fight) ?? [];
|
||||
@@ -29,7 +39,9 @@
|
||||
}
|
||||
|
||||
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[][] {
|
||||
const finalFight = fightMap.get(startFinalId);
|
||||
@@ -45,10 +57,15 @@
|
||||
const rels = relationsByFight.get(fight.id) ?? [];
|
||||
for (const rel of rels) {
|
||||
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;
|
||||
// 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)) {
|
||||
visited.add(src.id);
|
||||
next.push(src);
|
||||
@@ -97,8 +114,12 @@
|
||||
for (const rel of event.relations) {
|
||||
if (rel.type !== "FIGHT" || !rel.fromFight) continue;
|
||||
const fromId = rel.fromFight.id;
|
||||
const fromEl = document.getElementById(`fight-${fromId}`) as HTMLElement | null;
|
||||
const toEl = document.getElementById(`fight-${rel.fight}-team-${rel.team.toLowerCase()}`) as HTMLElement | null;
|
||||
const fromEl = document.getElementById(
|
||||
`fight-${fromId}`,
|
||||
) as HTMLElement | null;
|
||||
const toEl = document.getElementById(
|
||||
`fight-${rel.fight}-team-${rel.team.toLowerCase()}`,
|
||||
) as HTMLElement | null;
|
||||
if (!fromEl || !toEl) continue;
|
||||
// Use team-signed offsets so BLUE goes left (negative), RED goes right (positive)
|
||||
const key = `${fromId}:${rel.team}`;
|
||||
@@ -118,12 +139,18 @@
|
||||
</script>
|
||||
|
||||
{#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}
|
||||
{#key winnersStages.length + ":" + losersStages.length}
|
||||
<!-- 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}
|
||||
<div class="grid gap-x-16 gap-y-6 items-start" style={`grid-template-columns: repeat(${totalColumns}, max-content);`}>
|
||||
{@const totalColumns =
|
||||
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 -->
|
||||
<h2 class="font-bold text-center">Winners Bracket</h2>
|
||||
|
||||
@@ -132,30 +159,50 @@
|
||||
<div style={`grid-row: 2; grid-column: ${i + 1};`}>
|
||||
<EventCard title={stageName(stage.length, true)}>
|
||||
{#each stage as fight}
|
||||
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
|
||||
<EventFightChip
|
||||
{event}
|
||||
{fight}
|
||||
group={fight.group ?? defaultGroup}
|
||||
/>
|
||||
{/each}
|
||||
</EventCard>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- 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">
|
||||
{#if grandFinal}
|
||||
<EventFightChip fight={grandFinal} group={grandFinal.group ?? defaultGroup} />
|
||||
<EventFightChip
|
||||
{event}
|
||||
fight={grandFinal}
|
||||
group={grandFinal.group ?? defaultGroup}
|
||||
/>
|
||||
{/if}
|
||||
</EventCard>
|
||||
</div>
|
||||
|
||||
<!-- 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 -->
|
||||
{#each losersStages as stage, j}
|
||||
<div style={`grid-row: 4; grid-column: ${j + 2};`} class="mt-2">
|
||||
<EventCard title={stageName(stage.length, false)}>
|
||||
{#each stage as fight}
|
||||
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
|
||||
<EventFightChip
|
||||
{event}
|
||||
{fight}
|
||||
group={fight.group ?? defaultGroup}
|
||||
/>
|
||||
{/each}
|
||||
</EventCard>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<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 EventCard from "./EventCard.svelte";
|
||||
import EventFightChip from "./EventFightChip.svelte";
|
||||
import { onMount, onDestroy, tick } from "svelte";
|
||||
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 = {
|
||||
id: -1,
|
||||
@@ -18,8 +26,13 @@
|
||||
points: null,
|
||||
};
|
||||
|
||||
function buildStages(ev: ExtendedEvent, finalFightId: number): EventFight[][] {
|
||||
const fightMap = new Map<number, EventFight>(ev.fights.map((f) => [f.id, f]));
|
||||
function buildStages(
|
||||
ev: ExtendedEvent,
|
||||
finalFightId: number,
|
||||
): EventFight[][] {
|
||||
const fightMap = new Map<number, EventFight>(
|
||||
ev.fights.map((f) => [f.id, f]),
|
||||
);
|
||||
const relationsByFight = new Map<number, ResponseRelation[]>();
|
||||
for (const rel of ev.relations) {
|
||||
const list = relationsByFight.get(rel.fight) ?? [];
|
||||
@@ -41,7 +54,8 @@
|
||||
const rels = relationsByFight.get(fight.id) ?? [];
|
||||
for (const rel of rels) {
|
||||
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)) {
|
||||
visited.add(src.id);
|
||||
nextLayer.push(src);
|
||||
@@ -89,8 +103,12 @@
|
||||
|
||||
for (const rel of event.relations) {
|
||||
if (rel.type !== "FIGHT" || !rel.fromFight) continue;
|
||||
const fromEl = document.getElementById(`fight-${rel.fromFight.id}`) as HTMLElement | null;
|
||||
const toEl = document.getElementById(`fight-${rel.fight}-team-${rel.team.toLowerCase()}`) as HTMLElement | null;
|
||||
const fromEl = document.getElementById(
|
||||
`fight-${rel.fromFight.id}`,
|
||||
) as HTMLElement | null;
|
||||
const toEl = document.getElementById(
|
||||
`fight-${rel.fight}-team-${rel.team.toLowerCase()}`,
|
||||
) as HTMLElement | null;
|
||||
if (fromEl && toEl) {
|
||||
connector.addConnection(fromEl, toEl, "#9ca3af");
|
||||
}
|
||||
@@ -111,7 +129,11 @@
|
||||
<div class="flex flex-col justify-center">
|
||||
<EventCard title={stageName(index, stage)}>
|
||||
{#each stage as fight}
|
||||
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
|
||||
<EventFightChip
|
||||
{event}
|
||||
{fight}
|
||||
group={fight.group ?? defaultGroup}
|
||||
/>
|
||||
{/each}
|
||||
</EventCard>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<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 EventTeamChip from "./EventTeamChip.svelte";
|
||||
import { fightConnector } from "./connections.svelte.ts";
|
||||
|
||||
let {
|
||||
fight,
|
||||
group,
|
||||
event,
|
||||
disabled = false,
|
||||
}: {
|
||||
fight: EventFight;
|
||||
group: ResponseGroups;
|
||||
event: ExtendedEvent;
|
||||
disabled?: boolean;
|
||||
} = $props();
|
||||
|
||||
function getScore(group: ResponseGroups, fight: EventFight, blueTeam: boolean): string {
|
||||
@@ -29,14 +32,36 @@
|
||||
<EventTeamChip
|
||||
team={{
|
||||
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([]),
|
||||
color: "-1",
|
||||
}}
|
||||
time={true}
|
||||
{event}
|
||||
/>
|
||||
<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 team={fight.redTeam} score={getScore(group, fight, false)} showWinner={true} isWinner={fight.ergebnis === 2} noWinner={fight.ergebnis === 0} id="fight-{fight.id}-team-red" />
|
||||
<EventTeamChip
|
||||
{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>
|
||||
</EventCardOutline>
|
||||
|
||||
@@ -1,48 +1,70 @@
|
||||
<script lang="ts">
|
||||
import type { Team } from "@type/team.ts";
|
||||
import { fightConnector } from "./connections.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 {
|
||||
team,
|
||||
event,
|
||||
score = "",
|
||||
time = false,
|
||||
showWinner = false,
|
||||
isWinner = false,
|
||||
noWinner = false,
|
||||
id,
|
||||
disabled = false,
|
||||
}: {
|
||||
team: Team;
|
||||
event: ExtendedEvent;
|
||||
score?: string;
|
||||
time?: boolean;
|
||||
showWinner?: boolean;
|
||||
isWinner?: boolean;
|
||||
noWinner?: boolean;
|
||||
id?: string;
|
||||
disabled?: boolean;
|
||||
} = $props();
|
||||
|
||||
let hoverService = $teamHoverService;
|
||||
|
||||
type StringAnyRecord = Record<string, any>;
|
||||
</script>
|
||||
|
||||
<button
|
||||
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
|
||||
? '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>
|
||||
{#if !disabled}
|
||||
<Sheet>
|
||||
<SheetTrigger>
|
||||
{#snippet child({ props })}
|
||||
{@render teamButton({ props })}
|
||||
{/snippet}
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<TeamInfo {team} {event} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
{:else}
|
||||
{@render teamButton({ props: {} })}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.team-chip:not(:last-child) {
|
||||
@apply border-b border-b-gray-700;
|
||||
}
|
||||
</style>
|
||||
{#snippet teamButton({ props }: { props: StringAnyRecord })}
|
||||
<button
|
||||
{...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
|
||||
? -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 EventTeamChip from "./EventTeamChip.svelte";
|
||||
import EventFightChip from "./EventFightChip.svelte";
|
||||
import { teamHoverService } from "./team-hover.svelte";
|
||||
|
||||
const {
|
||||
event,
|
||||
@@ -39,12 +40,30 @@
|
||||
if (currentRound.length) rounds.push(currentRound);
|
||||
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>
|
||||
|
||||
{#each config.groups as groupId}
|
||||
{@const group = event.groups.find((g) => g.id === groupId)!!}
|
||||
{@const fights = event.fights.filter((f) => f.group?.id === groupId)}
|
||||
{@const rounds = detectRounds(fights)}
|
||||
{@const roundRows = config.roundRows ?? 1}
|
||||
{@const roundRowsChunked = chunkIntoRows(rounds, roundRows)}
|
||||
<div class="flex">
|
||||
<div>
|
||||
<EventCard title={group.name}>
|
||||
@@ -52,19 +71,27 @@
|
||||
{#each Object.entries(group.points ?? {}).sort((v1, v2) => v2[1] - v1[1]) as points}
|
||||
{@const [teamId, point] = points}
|
||||
{@const team = event.teams.find((t) => t.id.toString() === teamId)!!}
|
||||
<EventTeamChip {team} score={point.toString()} />
|
||||
<EventTeamChip {team} {event} score={point.toString()} />
|
||||
{/each}
|
||||
</EventCardOutline>
|
||||
</EventCard>
|
||||
</div>
|
||||
{#each rounds as round, index}
|
||||
<div>
|
||||
<EventCard title="Runde {index + 1}">
|
||||
{#each round as fight}
|
||||
<EventFightChip {fight} {group} />
|
||||
<div class="flex flex-col">
|
||||
{#each roundRowsChunked as row}
|
||||
<div class="flex">
|
||||
{#each row as round, index (round)}
|
||||
{@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}
|
||||
</EventCard>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
79
src/components/event/TeamInfo.svelte
Normal file
79
src/components/event/TeamInfo.svelte
Normal file
@@ -0,0 +1,79 @@
|
||||
<script lang="ts">
|
||||
import { dataRepo } from "@components/repo/data";
|
||||
import type { ExtendedEvent, ResponseTeam } from "@components/types/event";
|
||||
import EventFightChip from "./EventFightChip.svelte";
|
||||
import SheetHeader from "@components/ui/sheet/sheet-header.svelte";
|
||||
import { SheetDescription, SheetTitle } from "@components/ui/sheet";
|
||||
|
||||
const { event, team }: { event: ExtendedEvent; team: ResponseTeam } = $props();
|
||||
|
||||
let members = $derived.by(() => {
|
||||
return fetchMembers(team.id);
|
||||
});
|
||||
let recentFights = $derived.by(() => {
|
||||
return event.fights
|
||||
.filter((f) => f.hasFinished && (f.blueTeam.id === team.id || f.redTeam.id === team.id))
|
||||
.sort((a, b) => b.start - a.start)
|
||||
.slice(0, 5);
|
||||
});
|
||||
|
||||
async function fetchMembers(teamId: number) {
|
||||
return await $dataRepo.queryPlayers(undefined, undefined, [teamId], 50, 0, false, false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<SheetHeader>
|
||||
<SheetTitle
|
||||
>{team.name}
|
||||
<span class="text-sm text-gray-400">{team.kuerzel}</span></SheetTitle
|
||||
>
|
||||
<SheetDescription>Statistiken des Teams</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div class="mt-8 space-y-8">
|
||||
<section>
|
||||
<h3 class="text-lg font-semibold mb-4 border-b border-slate-800 pb-2 text-blue-400">Teammitglieder</h3>
|
||||
{#await members}
|
||||
<p class="text-slate-500 italic text-sm">Lade Mitglieder...</p>
|
||||
{:then member}
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
{#each member.entries as p (p.uuid)}
|
||||
<div class="bg-slate-800/50 p-2 rounded border border-slate-700 flex items-center gap-2">
|
||||
<div class="w-6 h-6 rounded-full bg-slate-700 flex items-center justify-center text-[10px]">
|
||||
{p.name.charAt(0)}
|
||||
</div>
|
||||
<span class="truncate text-sm">{p.name}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/await}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3 class="text-lg font-semibold mb-4 border-b border-slate-800 pb-2 text-green-400">Letzte 5 Kämpfe</h3>
|
||||
{#if recentFights.length > 0}
|
||||
<div class="space-y-3">
|
||||
{#each recentFights as fight}
|
||||
<div class="scale-90 origin-left">
|
||||
<EventFightChip
|
||||
{event}
|
||||
disabled={true}
|
||||
{fight}
|
||||
group={fight.group ?? {
|
||||
id: -1,
|
||||
name: "Event",
|
||||
pointsPerWin: 0,
|
||||
pointsPerLoss: 0,
|
||||
pointsPerDraw: 0,
|
||||
type: "GROUP_STAGE",
|
||||
points: null,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-slate-500 italic text-sm">Keine beendeten Kämpfe in diesem Event.</p>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
@@ -38,17 +38,27 @@
|
||||
|
||||
<div class="py-2 border-t border-t-gray-600">
|
||||
<h1 class="text-2xl font-bold mb-4">Angemeldete Teams</h1>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2">
|
||||
<div
|
||||
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2"
|
||||
>
|
||||
{#each teams as team}
|
||||
<div class="bg-neutral-800 p-2 rounded-md border border-neutral-700 border-l-4 flex flex-row items-center gap-2" style="border-left-color: {colorMap[team.color] || '#FFFFFF'}">
|
||||
<span class="text-sm font-mono text-neutral-400 shrink-0 w-8 text-center">{team.kuerzel}</span>
|
||||
<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>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{#if teams.length === 0}
|
||||
<p class="col-span-full text-center text-neutral-400">Keine Teams angemeldet.</p>
|
||||
<p class="col-span-full text-center text-neutral-400">
|
||||
Keine Teams angemeldet.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,14 +5,16 @@ class TeamHoverService {
|
||||
public currentHover = $state<number | undefined>(undefined);
|
||||
private fightConnector = get(fightConnector);
|
||||
|
||||
public disableConnections = $state(false);
|
||||
|
||||
setHover(teamId: number): void {
|
||||
this.currentHover = teamId;
|
||||
this.fightConnector.addTeamConnection(teamId);
|
||||
if (!this.disableConnections) this.fightConnector.addTeamConnection(teamId);
|
||||
}
|
||||
|
||||
clearHover(): void {
|
||||
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({
|
||||
type: z.literal("GROUP"),
|
||||
groups: z.array(z.number()),
|
||||
roundRows: z.number().int().positive().optional().default(1),
|
||||
});
|
||||
|
||||
export type GroupViewConfig = z.infer<typeof GroupViewSchema>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* 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
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import { readable, writable } from "svelte/store";
|
||||
import { ResponseUserSchema } from "@components/types/data";
|
||||
import { PlayerSchema } from "@components/types/data";
|
||||
|
||||
export class AuthV2Repo {
|
||||
constructor() {
|
||||
@@ -42,7 +42,7 @@ export class AuthV2Repo {
|
||||
}),
|
||||
})
|
||||
.then((value) => value.json())
|
||||
.then((value) => ResponseUserSchema.parse(value));
|
||||
.then((value) => PlayerSchema.parse(value));
|
||||
|
||||
loggedIn.set(true);
|
||||
|
||||
@@ -62,7 +62,7 @@ export class AuthV2Repo {
|
||||
},
|
||||
})
|
||||
.then((value) => value.json())
|
||||
.then((value) => ResponseUserSchema.parse(value));
|
||||
.then((value) => PlayerSchema.parse(value));
|
||||
loggedIn.set(true);
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -89,6 +89,6 @@ export class AuthV2Repo {
|
||||
}
|
||||
}
|
||||
|
||||
export const loggedIn = writable(false);
|
||||
export const loggedIn = writable<boolean | undefined>(undefined);
|
||||
|
||||
export const authV2Repo = readable(new AuthV2Repo());
|
||||
|
||||
@@ -36,7 +36,7 @@ import type { CreateEventGroup, UpdateEventGroup, CreateEventRelation, UpdateEve
|
||||
import { z } from "zod";
|
||||
import type { Dayjs } from "dayjs";
|
||||
import { derived } from "svelte/store";
|
||||
import { ResponseUserSchema } from "@components/types/data";
|
||||
import { PlayerSchema, type Player } from "@components/types/data";
|
||||
|
||||
export interface CreateEvent {
|
||||
name: string;
|
||||
@@ -247,10 +247,10 @@ export class EventRepo {
|
||||
}
|
||||
|
||||
// Referees
|
||||
public async listReferees(eventId: string): Promise<ResponseUser[]> {
|
||||
public async listReferees(eventId: string): Promise<Player[]> {
|
||||
return await fetchWithToken(this.token, `/events/${eventId}/referees`)
|
||||
.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> {
|
||||
const res = await fetchWithToken(this.token, `/events/${eventId}/referees`, {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
import { z } from "zod";
|
||||
import { TeamSchema } from "./team.js";
|
||||
import { PlayerSchema, ResponseUserSchema } from "./data.js";
|
||||
import { PlayerSchema } from "./data.js";
|
||||
|
||||
export const ResponseGroupsSchema = z.object({
|
||||
id: z.number(),
|
||||
@@ -93,7 +93,7 @@ export const ExtendedEventSchema = z.object({
|
||||
teams: z.array(TeamSchema),
|
||||
groups: z.array(ResponseGroupsSchema),
|
||||
fights: z.array(EventFightSchema),
|
||||
referees: z.array(ResponseUserSchema),
|
||||
referees: z.array(PlayerSchema),
|
||||
relations: z.array(ResponseRelationSchema),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
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 Content from "./sheet-content.svelte";
|
||||
import Header from "./sheet-header.svelte";
|
||||
@@ -12,6 +9,7 @@ import Description from "./sheet-description.svelte";
|
||||
const Root = SheetPrimitive.Root;
|
||||
const Close = SheetPrimitive.Close;
|
||||
const Trigger = SheetPrimitive.Trigger;
|
||||
const Portal = SheetPrimitive.Portal;
|
||||
|
||||
export {
|
||||
Root,
|
||||
@@ -36,71 +34,3 @@ export {
|
||||
Title as SheetTitle,
|
||||
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">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import X from "lucide-svelte/icons/x";
|
||||
import { fly } from "svelte/transition";
|
||||
import {
|
||||
SheetOverlay,
|
||||
SheetPortal,
|
||||
type Side,
|
||||
sheetTransitions,
|
||||
sheetVariants,
|
||||
} from "./index.js";
|
||||
import { cn } from "$lib/components/utils.js";
|
||||
<script lang="ts" module>
|
||||
import { tv, type VariantProps } from "tailwind-variants";
|
||||
export const sheetVariants = tv({
|
||||
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",
|
||||
variants: {
|
||||
side: {
|
||||
top: "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 border-b",
|
||||
bottom: "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 border-t",
|
||||
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",
|
||||
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",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
});
|
||||
|
||||
type $$Props = SheetPrimitive.ContentProps & {
|
||||
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;
|
||||
export type Side = VariantProps<typeof sheetVariants>["side"];
|
||||
</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 />
|
||||
<SheetPrimitive.Content
|
||||
{inTransition}
|
||||
{inTransitionConfig}
|
||||
{outTransition}
|
||||
{outTransitionConfig}
|
||||
class={cn(sheetVariants({ side }), className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
<SheetPrimitive.Content bind:ref class={cn(sheetVariants({ side }), className)} {...restProps}>
|
||||
{@render children?.()}
|
||||
<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"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
<X class="size-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
</SheetPrimitive.Portal>
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/components/utils.js";
|
||||
|
||||
type $$Props = SheetPrimitive.DescriptionProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.DescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Description class={cn("text-muted-foreground text-sm", className)} {...$$restProps}>
|
||||
<slot />
|
||||
</SheetPrimitive.Description>
|
||||
<SheetPrimitive.Description
|
||||
bind:ref
|
||||
class={cn("text-muted-foreground text-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import { cn } from "$lib/components/utils.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
{...$$restProps}
|
||||
{...restProps}
|
||||
>
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLAttributes } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/components/utils.js";
|
||||
|
||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||
</script>
|
||||
|
||||
<div class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...$$restProps}>
|
||||
<slot />
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as SheetPrimitive } from "bits-ui";
|
||||
import { fade } from "svelte/transition";
|
||||
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 };
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Overlay
|
||||
{transition}
|
||||
{transitionConfig}
|
||||
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ", className)}
|
||||
{...$$restProps}
|
||||
bind:ref
|
||||
class={cn(
|
||||
"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",
|
||||
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 { cn } from "$lib/components/utils.js";
|
||||
|
||||
type $$Props = SheetPrimitive.TitleProps;
|
||||
|
||||
let className: $$Props["class"] = undefined;
|
||||
export { className as class };
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: SheetPrimitive.TitleProps = $props();
|
||||
</script>
|
||||
|
||||
<SheetPrimitive.Title
|
||||
bind:ref
|
||||
class={cn("text-foreground text-lg font-semibold", className)}
|
||||
{...$$restProps}
|
||||
>
|
||||
<slot />
|
||||
</SheetPrimitive.Title>
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
eventId: 76
|
||||
mode: microwargear
|
||||
verwantwortlicher: SteamWar
|
||||
viewConfig:
|
||||
groups:
|
||||
name: Gruppenphase
|
||||
view:
|
||||
type: "GROUP"
|
||||
groups: [13]
|
||||
roundRows: 2
|
||||
---
|
||||
|
||||
**Ahoi, liebe Community,**
|
||||
@@ -10,38 +17,39 @@ es ist wieder Zeit, das Jahr neigt sich dem Ende und damit ist es wieder Zeit f
|
||||
|
||||
## Ü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:** 28. Dezember 2025 (23:59 Uhr)
|
||||
- **Hotfix-Schluss:** 31. Dezember
|
||||
- **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
|
||||
- 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
|
||||
- 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.
|
||||
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.
|
||||
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!**
|
||||
**Wir wünschen Euch viel Spaß beim Event und eine schöne Vorweihnachtliche Zeit!**
|
||||
|
||||
@@ -22,12 +22,12 @@ lange ist es her seit dem letzten WarGear-Event. Nun ist es so weit: Am **29. un
|
||||
|
||||
## Übersicht
|
||||
|
||||
- **Datum:** 29.11.: Gruppenphase, 30.11.: KO-Phase
|
||||
- **Spielmodus:** Standard **und** Pro WarGear
|
||||
- **Teamgröße**: 6
|
||||
- **Anmeldeschluss:** 22. November
|
||||
- **Einsendeschluss:** 24. November
|
||||
- **Hotfix-Schluss:** 27. November
|
||||
- **Datum:** 29.11.: Gruppenphase, 30.11.: KO-Phase
|
||||
- **Spielmodus:** Standard **und** Pro WarGear
|
||||
- **Teamgröße**: 6
|
||||
- **Anmeldeschluss:** 22. November
|
||||
- **Einsendeschluss:** 24. November
|
||||
- **Hotfix-Schluss:** 27. November
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
3. Kanonen dürfen nicht gezielt Projektile anderer Kanonen beeinflussen.
|
||||
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
---
|
||||
import DashboardComponent from "@components/DashboardComponent.svelte";
|
||||
import PageLayout from "@layouts/PageLayout.astro";
|
||||
import {t} from "astro-i18n";
|
||||
import { t } from "astro-i18n";
|
||||
---
|
||||
|
||||
<PageLayout title={t("dashboard.page")}>
|
||||
<script>
|
||||
import {l} from "../util/util";
|
||||
import {navigate} from "astro:transitions/client";
|
||||
import {get} from "svelte/store";
|
||||
import {loggedIn} from "../components/repo/authv2";
|
||||
import { l } from "../util/util";
|
||||
import { navigate } from "astro:transitions/client";
|
||||
import { loggedIn } from "../components/repo/authv2";
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
if (window.location.href.endsWith("/dashboard") || window.location.href.endsWith("/dashboard/")) {
|
||||
if (!get(loggedIn)) {
|
||||
navigate(l("/login"), {});
|
||||
loggedIn.subscribe((value) => {
|
||||
if (window.location.href.endsWith("/dashboard") || window.location.href.endsWith("/dashboard/")) {
|
||||
if (value === false) {
|
||||
navigate(l("/dashboard"), { history: "replace" });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<DashboardComponent client:only="svelte" />
|
||||
</PageLayout>
|
||||
</PageLayout>
|
||||
|
||||
@@ -9,9 +9,13 @@ import TeamList from "@components/event/TeamList.svelte";
|
||||
export const getStaticPaths = createGetStaticPaths(async () => {
|
||||
const events = await Promise.all(
|
||||
(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,
|
||||
}))
|
||||
})),
|
||||
);
|
||||
|
||||
return events.map((event) => ({
|
||||
@@ -25,7 +29,10 @@ export const getStaticPaths = createGetStaticPaths(async () => {
|
||||
}));
|
||||
});
|
||||
|
||||
const { event, page } = Astro.props as { event: ExtendedEvent; page: CollectionEntry<"events"> };
|
||||
const { event, page } = Astro.props as {
|
||||
event: ExtendedEvent;
|
||||
page: CollectionEntry<"events">;
|
||||
};
|
||||
|
||||
const { Content } = await page.render();
|
||||
---
|
||||
@@ -35,19 +42,26 @@ const { Content } = await page.render();
|
||||
<h1 class="text-2xl font-bold">{event.event.name}</h1>
|
||||
<h2 class="text-md text-gray-300 mb-4">
|
||||
{
|
||||
new Date(event.event.start).toLocaleDateString(astroI18n.locale, {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
})
|
||||
new Date(event.event.start).toLocaleDateString(
|
||||
astroI18n.locale,
|
||||
{
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
},
|
||||
)
|
||||
}
|
||||
{
|
||||
new Date(event.event.start).toDateString() !== new Date(event.event.end).toDateString()
|
||||
? ` - ${new Date(event.event.end).toLocaleDateString(astroI18n.locale, {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
})}`
|
||||
new Date(event.event.start).toDateString() !==
|
||||
new Date(event.event.end).toDateString()
|
||||
? ` - ${new Date(event.event.end).toLocaleDateString(
|
||||
astroI18n.locale,
|
||||
{
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
},
|
||||
)}`
|
||||
: ""
|
||||
}
|
||||
</h2>
|
||||
@@ -60,7 +74,11 @@ const { Content } = await page.render();
|
||||
page.data.viewConfig && (
|
||||
<div class="py-2 border-t border-t-gray-600">
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,14 +10,14 @@ import BackgroundImage from "../components/BackgroundImage.astro";
|
||||
import { l } from "../util/util";
|
||||
import { navigate } from "astro:transitions/client";
|
||||
import { loggedIn } from "../components/repo/authv2";
|
||||
import { get } from "svelte/store";
|
||||
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
if (window.location.href.endsWith("/login") || window.location.href.endsWith("/login/")) {
|
||||
if (get(loggedIn)) {
|
||||
navigate(l("/dashboard"), { history: "replace" });
|
||||
loggedIn.subscribe((value) => {
|
||||
if (window.location.href.endsWith("/login") || window.location.href.endsWith("/login/")) {
|
||||
if (value) {
|
||||
navigate(l("/dashboard"), { history: "replace" });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<div class="h-screen w-screen fixed -z-10">
|
||||
|
||||
Reference in New Issue
Block a user