Dieser Commit ist enthalten in:
Chaoscaot 2023-10-01 10:04:04 +02:00
Ursprung 7728d9e177
Commit e0f2702eca
Signiert von: Chaoscaot
GPG-Schlüssel-ID: BDF8FADD7D5EDB7A
49 geänderte Dateien mit 2589 neuen und 68 gelöschten Zeilen

Datei anzeigen

@ -13,5 +13,8 @@ export default defineConfig({
},
integrations: [i18n(), svelte(), tailwind(), prefetch({
selector: "a"
})]
})],
vite: {
}
});

Datei anzeigen

@ -17,15 +17,28 @@
"@astrojs/svelte": "^4.0.2",
"@astrojs/tailwind": "^5.0.0",
"@astropub/icons": "^0.2.0",
"@types/crypto-js": "^4.1.2",
"@types/node": "^20.6.3",
"astro-i18n": "^1.8.1",
"cssnano": "^6.0.1",
"postcss-nesting": "^12.0.1",
"sass": "^1.68.0",
"svelte": "^4.0.0",
"tailwindcss": "^3.0.24"
"tailwind-merge": "^1.13.2",
"tailwindcss": "^3.3.2"
},
"dependencies": {
"astro": "^3.1.1",
"sharp": "^0.32.6"
"color": "^4.2.3",
"crypto-js": "^4.1.1",
"flowbite": "^1.7.0",
"flowbite-svelte": "^0.39.2",
"flowbite-svelte-icons": "^0.2.5",
"gitea-js": "^1.20.1",
"moment": "^2.29.4",
"sharp": "^0.32.6",
"svelte-awesome": "^3.2.0",
"svelte-spa-router": "^3.3.0",
"zod": "^3.21.4"
}
}

Datei anzeigen

@ -0,0 +1,28 @@
<script lang="ts">
import type {ConditionsFailedEvent, RouteDefinition} from "svelte-spa-router";
import wrap from "svelte-spa-router/wrap";
import Router, {replace} from "svelte-spa-router";
import {get} from "svelte/store";
import {tokenStore} from "./repo/repo.js";
const routes: RouteDefinition = {
'/': wrap({asyncComponent: () => import('./pages/Home.svelte'), conditions: detail => get(tokenStore) != ""}),
'/perms': wrap({asyncComponent: () => import('./pages/Perms.svelte'), conditions: detail => get(tokenStore) != ""}),
'/login': wrap({asyncComponent: () => import('./pages/Login.svelte'), conditions: detail => get(tokenStore) == ""}),
'/event/:id': wrap({asyncComponent: () => import('./pages/Event.svelte'), conditions: detail => get(tokenStore) != ""}),
'/event/:id/generate': wrap({asyncComponent: () => import('./pages/Generate.svelte'), conditions: detail => get(tokenStore) != ""}),
'*': wrap({asyncComponent: () => import('./pages/NotFound.svelte')})
}
function conditionsFailed(event: ConditionsFailedEvent) {
if(event.detail.location === "/login") {
replace("/")
} else {
replace("/login")
}
}
</script>
<main class="dark:bg-gray-900 min-w-full min-h-screen text-gray-900 dark:text-gray-300">
<Router {routes} on:conditionsFailed={conditionsFailed} />
</main>

Datei anzeigen

@ -0,0 +1,13 @@
<script lang="ts">
import {Button, Modal} from "flowbite-svelte";
export let open: boolean = false;
export let error: Error | undefined;
</script>
{#if (error instanceof Error)}
<Modal bind:open title={error.message}>
<p>{error.stack}</p>
<Button slot="footer" on:click={() => open = false}>Close</Button>
</Modal>
{/if}

Datei anzeigen

@ -0,0 +1,103 @@
<script lang="ts">
import {Input, Label, Select} from "flowbite-svelte";
import TypeAheadSearch from "./TypeAheadSearch.svelte";
import {gamemodes, groups, maps, players} from "../stores/stores.js";
import type {Team} from '../types/team.js';
export let teams: Team[] = [];
export let blueTeam: string;
export let redTeam: string;
export let start = "";
export let gamemode = "";
export let map = "";
export let kampfleiter = "";
export let group = "";
export let groupSearch = "";
$: selectPlayers = $players.map(player => {
return {
name: player.name,
value: player.id.toString()
}
}).sort((a, b) => a.name.localeCompare(b.name));
$: selectableTeams = teams.map(team => {
return {
name: team.name,
value: team.id.toString()
}
}).sort((a, b) => a.name.localeCompare(b.name));
$: selectableGamemodes = $gamemodes.map(gamemode => {
return {
name: gamemode,
value: gamemode
}
}).sort((a, b) => a.name.localeCompare(b.name));
$: customGamemode = !selectableGamemodes.some((e) => e.name === gamemode) && gamemode !== '';
$: selectableCustomGamemode = [
...selectableGamemodes, {
name: gamemode + ' (custom)',
value: gamemode
}
]
$: mapsStore = maps(gamemode);
$: selectableMaps = $mapsStore.map(map => {
return {
name: map,
value: map
}
}).sort((a, b) => a.name.localeCompare(b.name));
$: customMap = !selectableMaps.some((e) => e.name === map) && map !== ''
$: selectableCustomMaps = [
...selectableMaps, {
name: map + ' (custom)',
value: map
}
]
$: selectableGroups = [{
name: 'None',
value: ''
}, {
value: groupSearch,
name: `Create: '${groupSearch}'`
}, ...$groups.map(group => {
return {
name: group,
value: group
}
}).sort((a, b) => a.name.localeCompare(b.name))];
</script>
<div class="m-2">
<Label for="fight-blue">Blue Team</Label>
<Select items={selectableTeams} bind:value={blueTeam} id="fight-blue"></Select>
</div>
<div class="m-2">
<Label for="fight-red">Red Team</Label>
<Select items={selectableTeams} bind:value={redTeam} id="fight-red"></Select>
</div>
<div class="mt-4">
<Label for="fight-start">Start</Label>
<Input id="fight-start" bind:value={start} let:props>
<input type="datetime-local" {...props} bind:value={start}/>
</Input>
</div>
<div class="m-2">
<Label for="fight-gamemode">Gamemode</Label>
<Select items={customGamemode ? selectableCustomGamemode : selectableGamemodes} bind:value={gamemode} id="fight-gamemode"></Select>
</div>
<div class="m-2">
<Label for="fight-maps">Map</Label>
<Select items={customMap ? selectableCustomMaps : selectableMaps} bind:value={map} id="fight-maps" disabled={customGamemode} class={customGamemode ? "cursor-not-allowed" : ""}></Select>
</div>
<div class="m-2">
<Label for="fight-kampf">Kampfleiter</Label>
<TypeAheadSearch items={selectPlayers} bind:selected={kampfleiter}></TypeAheadSearch>
</div>
<div class="m-2">
<Label for="fight-kampf">Group</Label>
<TypeAheadSearch items={selectableGroups} bind:selected={group} bind:searchValue={groupSearch} all></TypeAheadSearch>
</div>

Datei anzeigen

@ -0,0 +1,30 @@
<script lang="ts">
import {Button, Dropdown, DropdownItem, Search} from 'flowbite-svelte'
export let selected: string = null
export let items: {name: string, value: string}[] = []
export let searchValue = items.find(item => item.value === selected)?.name || ''
let open = false
$: filteredItems = items.filter(item => item.name.toLowerCase().includes(searchValue.toLowerCase())).filter((value, index) => index < 5)
$: console.log(selected)
function selectItem(item: {name: string, value: string}) {
selected = item.value
searchValue = item.name
open = false
}
</script>
<Button color="alternative" on:click={() => open = true}>{selected === null ? 'Auswählen' : items.find(value => value.value === selected)?.name}</Button>
<Dropdown bind:open class="w-60">
<div class="overflow-y-auto p-3 text-sm w-60" slot="header">
<Search bind:value={searchValue} on:focus={() => open = true} on:keydown={() => open = true}/>
</div>
{#each filteredItems as item}
<button on:click={() => selectItem(item)} class="rounded p-2 hover:bg-gray-100 dark:hover:bg-gray-600 w-full cursor-pointer border-b border-b-gray-600">
{item.name}
</button>
{/each}
</Dropdown>

Datei anzeigen

@ -0,0 +1,47 @@
<script lang="ts">
import {Avatar, Navbar, NavBrand, Spinner, TabItem, Tabs} from "flowbite-svelte";
import EventEdit from "./event/EventEdit.svelte";
import {ArrowLeftSolid} from "flowbite-svelte-icons";
import FightList from "./event/FightList.svelte";
import {eventRepo} from "../repo/repo.js";
import TeamList from "./event/TeamList.svelte";
export let params: { id: number } = {};
let id = params.id;
let event = $eventRepo.getEvent(id.toString());
</script>
{#await event}
<div class="h-screen w-screen grid place-items-center">
<Spinner size={16}/>
</div>
{:then data}
<Navbar let:hidden let:toggle>
<NavBrand href="#">
<ArrowLeftSolid></ArrowLeftSolid>
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white">
{data.event.name}
</span>
</NavBrand>
</Navbar>
<Tabs style="pill" class="mx-4 flex shadow-lg border-b-2 border-gray-700 pb-2" contentClass="">
<TabItem open>
<span slot="title">Event</span>
<EventEdit {data} />
</TabItem>
<TabItem>
<span slot="title">Teams</span>
<TeamList {data}/>
</TabItem>
<TabItem>
<span slot="title">Fights</span>
<FightList {data}/>
</TabItem>
</Tabs>
{:catch error}
<p>
{error.message}
</p>
{/await}

Datei anzeigen

@ -0,0 +1,42 @@
<script lang="ts">
import {Avatar, Navbar, NavBrand, Spinner, TabItem, Tabs} from "flowbite-svelte";
import EventEdit from "./event/EventEdit.svelte";
import {ArrowLeftSolid} from "flowbite-svelte-icons";
import FightList from "./event/FightList.svelte";
import {eventRepo} from "../repo/repo.js";
import TeamList from "./event/TeamList.svelte";
import GroupGenerator from "./generate/GroupGenerator.svelte";
export let params: { id: number } = {};
let id = params.id;
let event = $eventRepo.getEvent(id.toString());
</script>
{#await event}
<div class="h-screen w-screen grid place-items-center">
<Spinner size={16}/>
</div>
{:then data}
<Navbar let:hidden let:toggle>
<NavBrand href="#/event/{id}">
<ArrowLeftSolid></ArrowLeftSolid>
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white">
{data.event.name} - Generate
</span>
</NavBrand>
</Navbar>
<Tabs style="pill" class="mx-4 flex shadow-lg border-b-2 border-gray-700 pb-2" contentClass="">
<TabItem title="Group" open>
<GroupGenerator {data}/>
</TabItem>
<TabItem title="KO">
<h1>WIP</h1>
</TabItem>
</Tabs>
{:catch error}
<p>
{error.message}
</p>
{/await}

Datei anzeigen

@ -0,0 +1,73 @@
<script lang="ts">
import {Button, Modal, Navbar, NavBrand, NavHamburger, NavLi, NavUl, Spinner} from "flowbite-svelte";
import {replace} from "svelte-spa-router";
import {PlusSolid} from "flowbite-svelte-icons";
import EventCard from "./home/EventCard.svelte";
import CreateEventModal from "./home/CreateEventModal.svelte";
import {eventRepo, tokenStore} from "../repo/repo.js";
let events = $eventRepo.listEvents()
let showAdd = false
let showLogoutModal = false
let millis = Date.now()
function logout() {
$tokenStore = ""
replace("/login")
}
</script>
<Navbar let:hidden let:toggle class="shadow-lg border-b">
<NavBrand href="#">
<span class="self-center whitespace-nowrap text-xl font-semibold dark:text-white">
Eventplanner
</span>
</NavBrand>
<NavHamburger on:click={toggle} />
<NavUl {hidden}>
<NavLi href="#/perms">Permissions</NavLi>
<NavLi on:click={() => showLogoutModal = true} class="cursor-pointer select-none">Logout</NavLi>
</NavUl>
</Navbar>
<CreateEventModal bind:open={showAdd} on:create={() => events = $eventRepo.listEvents()}/>
<Modal bind:open={showLogoutModal} title="Logout?" outsideclose size="sm">
<p>Do you really want to logout?</p>
<svelte:fragment slot="footer">
<Button on:click={() => showLogoutModal = false} color="alternative" class="ml-auto">Cancel</Button>
<Button on:click={logout} color="red">Logout</Button>
</svelte:fragment>
</Modal>
{#await events}
<div class="h-screen w-screen grid place-items-center">
<Spinner size={16}/>
</div>
{:then data}
<Button class="fixed bottom-6 right-6 !p-4 z-10 shadow-lg" on:click={() => showAdd = true}>
<PlusSolid/>
</Button>
<h1 class="text-3xl mt-4 ml-4">Upcoming</h1>
<div class="grid gap-4 p-4 border-b" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))">
{#each data.filter((e) => e.start > millis) as event}
<EventCard {event} />
{/each}
</div>
<h1 class="text-3xl mt-4 ml-4">Past</h1>
<div class="grid gap-4 p-4" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))">
{#each data.filter((e) => e.start < millis).reverse() as event}
<EventCard {event} />
{/each}
</div>
{:catch error}
<p>
{error.message}
</p>
{/await}
<svelte:head>
<title>SteamWar.de Multitool - Home</title>
</svelte:head>

Datei anzeigen

@ -0,0 +1,66 @@
<script lang="ts">
import {Button, Input, Label, Spinner, Toast} from "flowbite-svelte";
import {fly} from "svelte/transition";
import {replace} from "svelte-spa-router";
import {EyeOutline, EyeSlashOutline, EyeSolid} from "flowbite-svelte-icons";
import {tokenStore} from "../repo/repo.js";
let show = false;
let loading = false;
let value = "";
let error = false;
async function handleSubmit() {
loading = true;
let res = await fetch("https://steamwar.de/eventplanner-api/data", {headers: {"X-SW-Auth": value}})
loading = false;
if(res.ok) {
$tokenStore = value;
await replace("/");
} else {
error = true;
value = "";
setTimeout(() => {
error = false;
}, 5000)
}
}
</script>
<div class="h-screen w-screen grid place-items-center overflow-hidden">
<form on:submit|preventDefault={handleSubmit} class="grid">
<div class="grid gap-6 mb-6 md:grid-cols-1">
<div>
<Label for="token-xyz" class="mb-2">Token</Label>
<Input type={show?'text':'password'} id="token-xyz" placeholder="•••••••••" required size="lg" bind:value>
<button slot="left" on:click={() => (show = !show)} class="pointer-events-auto" type="button">
{#if show}
<EyeOutline />
{:else}
<EyeSlashOutline />
{/if}
</button>
</Input>
</div>
</div>
<Button type="submit">
{#if loading}
<Spinner size={4} class="mr-3" color="white"/> <span>Loading...</span>
{:else}
<span>Submit</span>
{/if}
</Button>
</form>
</div>
<Toast color="red" position="bottom-left" bind:open={error} transition={fly} params="{{x: -200}}">
<svelte:fragment slot="icon">
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg>
<span class="sr-only">Error icon</span>
</svelte:fragment>
Invalid Token.
</Toast>
<svelte:head>
<title>SteamWar.de Multitool - Login</title>
</svelte:head>

Datei anzeigen

@ -0,0 +1,9 @@
<script lang="ts">
import {onMount} from "svelte";
import {replace} from "svelte-spa-router";
onMount(() => {
replace('/')
});
</script>

Datei anzeigen

@ -0,0 +1,123 @@
<script lang="ts">
import {Button, Card, Checkbox, Input, Label, Navbar, NavBrand, Radio, Spinner} from "flowbite-svelte";
import {ArrowLeftSolid} from "flowbite-svelte-icons";
import {players} from "../stores/stores.ts";
import {permsRepo} from "../repo/repo.ts";
import {capitalize} from "../util.ts";
let search = "";
$: lowerCaseSearch = search.toLowerCase();
$: filteredPlayers = $players.filter(value => value.name.toLowerCase().includes(lowerCaseSearch));
let selectedPlayer = null;
$: player = $players.find(value => value.id === selectedPlayer);
let playerPerms = loadPlayer(selectedPlayer);
$: playerPerms = loadPlayer(selectedPlayer);
let prefixEdit = "PREFIX_NONE";
let activePerms = [];
function loadPlayer(id: number) {
if (!id) {
return;
}
return $permsRepo.getPerms(id).then(value => {
activePerms = value.perms;
prefixEdit = value.prefix.name;
return value;
})
}
function togglePerm(perm: string) {
return () => {
if (activePerms.includes(perm)) {
activePerms = activePerms.filter(value => value !== perm);
} else {
activePerms = [...activePerms, perm];
}
}
}
function save() {
playerPerms.then(async perms => {
if (perms.prefix.name != prefixEdit) {
await $permsRepo.setPrefix(selectedPlayer, prefixEdit);
}
for (let value of activePerms) {
if (!perms.perms.includes(value)) {
await $permsRepo.addPerm(selectedPlayer, value);
}
}
for (let value of perms.perms) {
if (!activePerms.includes(value)) {
await $permsRepo.removePerm(selectedPlayer, value);
}
}
playerPerms = loadPlayer(selectedPlayer);
})
}
let permsFuture = $permsRepo.listPerms();
</script>
<div class="flex flex-col h-screen overflow-hidden">
<Navbar let:hidden let:toggle>
<NavBrand href="#">
<ArrowLeftSolid></ArrowLeftSolid>
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white">
Permissions
</span>
</NavBrand>
</Navbar>
<div class="p-4 flex-1 overflow-hidden">
<div class="grid md:grid-cols-3 grid-cols-1 h-full gap-8">
<Card class="h-full flex flex-col overflow-hidden !max-w-full">
<div class="border-b border-b-gray-600 pb-2">
<Label for="user_search" class="mb-2">Search Users...</Label>
<Input type="text" id="user_search" placeholder="Name..." bind:value={search}/>
</div>
{#if filteredPlayers.length < 100}
<ul class="flex-1 overflow-scroll">
{#each filteredPlayers as player}
<li class="p-4 transition-colors hover:bg-gray-700 cursor-pointer" class:text-orange-500={player.id === selectedPlayer} on:click|preventDefault={() => selectedPlayer = player.id}>
{player.name}
</li>
{/each}
</ul>
{/if}
</Card>
<Card class="!max-w-full" style="grid-column: 2/4">
{#if selectedPlayer}
<h1 class="text-3xl">{player.name}</h1>
{#await permsFuture}
<Spinner></Spinner>
{:then perms}
{#await playerPerms}
<Spinner></Spinner>
{:then player}
<h1>Prefix</h1>
{#each Object.entries(perms.prefixes) as [key, prefix]}
<Radio name="prefix" bind:group={prefixEdit} value={prefix.name}>{capitalize(prefix.name.substring(7).toLowerCase())}</Radio>
{/each}
<h1>Permissions</h1>
{#each perms.perms as perm}
<Checkbox checked={activePerms.includes(perm)} on:click={togglePerm(perm)}>{capitalize(perm.toLowerCase())}</Checkbox>
{/each}
<div class="mt-4">
<Button disabled={prefixEdit === player.prefix.name && activePerms === player.perms} on:click={save}>Save</Button>
</div>
{:catch error}
<p>{error.toString()}</p>
{/await}
{:catch error}
<p>{error.toString()}</p>
{/await}
{/if}
</Card>
</div>
</div>
</div>

Datei anzeigen

@ -0,0 +1,140 @@
<script lang="ts">
import type {ExtendedEvent} from "../../types/event.js";
import {Button, Heading, Input, Label, Modal, Range, Select, Toast, Toggle} from "flowbite-svelte";
import {schemTypes} from "../../stores/stores.js";
import moment from "moment/moment.js";
import type {UpdateEvent} from "../../repo/event.js";
import {eventRepo} from "../../repo/repo.js";
import ErrorModal from "../../components/ErrorModal.svelte";
import {replace} from "svelte-spa-router";
import {CheckCircleOutline} from "flowbite-svelte-icons";
export let data: ExtendedEvent;
let event = data.event;
let name = event.name;
let deadline = moment(event.deadline).utc(true).toISOString().slice(0, -1);
let start = moment(event.start).utc(true).toISOString().slice(0, -1);
let end = moment(event.end).utc(true).toISOString().slice(0, -1);
let member = event.maxTeamMembers;
let schemType = event.schemType;
let publicOnly = event.publicSchemsOnly;
let spectateSystem = event.spectateSystem;
let errorOpen = false;
let error: Error = undefined;
let deleteOpen = false;
$: deadlineDate = moment(deadline);
$: startDate = moment(start);
$: endDate = moment(end);
$: selectTypes = [{
value: null,
name: "None"
}, ...$schemTypes.map((type) => {
return {
value: type.db,
name: type.name
}
})];
$: changed = name !== event.name ||
deadlineDate.diff(moment(event.deadline)) !== 0 ||
startDate.diff(moment(event.start)) !== 0 ||
endDate.diff(moment(event.end)) !== 0 ||
member !== event.maxTeamMembers ||
schemType != event.schemType ||
publicOnly !== event.publicSchemsOnly ||
spectateSystem !== event.spectateSystem;
async function del() {
try {
await $eventRepo.deleteEvent(event.id.toString());
await replace("/")
} catch (e) {
error = e;
errorOpen = true;
}
}
let successToast: boolean = false;
async function update() {
let ev: UpdateEvent = {
deadline: deadlineDate,
end: endDate,
maxTeamMembers: member,
name: name,
publicSchemsOnly: publicOnly,
schemType: schemType ?? 'null',
spectateSystem: spectateSystem,
start: startDate
};
try {
event = await $eventRepo.updateEvent(event.id.toString(), ev);
successToast = true;
setTimeout(() => successToast = false, 5000);
} catch (e) {
error = e;
errorOpen = true;
}
}
</script>
<svelte:head>
<title>{event.name} - Edit</title>
</svelte:head>
<form class="m-4">
<div class="mt-4">
<Label for="event-name">Name</Label>
<Input type="text" id="event-name" bind:value={name} class="w-80" size="lg"></Input>
</div>
<div class="mt-4">
<Label for="event-deadline">Deadline</Label>
<Input id="event-deadline" bind:value={name} class="w-80" let:props size="lg">
<input type="datetime-local" {...props} bind:value={deadline}/>
</Input>
</div>
<div class="mt-4">
<Label for="event-start">Start</Label>
<Input id="event-start" bind:value={name} class="w-80" let:props size="lg">
<input type="datetime-local" {...props} bind:value={start}/>
</Input>
</div>
<div class="mt-4">
<Label for="event-end">End</Label>
<Input id="event-end" bind:value={name} class="w-80" let:props size="lg">
<input type="datetime-local" {...props} bind:value={end}/>
</Input>
</div>
<div class="mt-4">
<Label for="event-member">Member: {member}</Label>
<Range id="event-member" bind:value={member} step="1" min="1" max="30"/>
</div>
<div class="mt-4">
<Label for="event-type">Schematic Type:</Label>
<Select bind:value={schemType} items={selectTypes}/>
</div>
<Toggle bind:checked={publicOnly} class="mt-4">Public Schematics Only</Toggle>
<Toggle bind:checked={spectateSystem} class="mt-4">Spectate System</Toggle>
<div class="flex mt-4">
<Button disabled={!changed} on:click={update}>Update</Button>
<Button class="ml-4" color="red" on:click={() => deleteOpen = true}>Delete</Button>
</div>
</form>
<ErrorModal bind:open={errorOpen} bind:error={error}/>
<Modal bind:open={deleteOpen} outsideclose title="Delete {event.name}?">
<p>Are you sure you want to delete {event.name}?</p>
<div class="flex justify-end">
<Button on:click={() => deleteOpen = false}>Cancel</Button>
<Button class="ml-4" color="red" on:click={del}>Delete</Button>
</div>
</Modal>
<Toast bind:open={successToast} position="bottom-left" color="green">
<CheckCircleOutline slot="icon"/>
Updated Successfully
</Toast>

Datei anzeigen

@ -0,0 +1,96 @@
<script lang="ts">
import {EditOutline, InboxSolid, TrashBinOutline} from "flowbite-svelte-icons";
import {Button, Checkbox, Modal, Toolbar, ToolbarButton} from "flowbite-svelte";
import type {EventFight, ExtendedEvent} from "../../types/event.js";
import FightEditModal from "./modals/FightEditModal.svelte";
import {createEventDispatcher, onMount} from "svelte";
import {fightRepo} from "../../repo/repo.js";
import {isWide} from "../../stores/stores.js";
export let fight: EventFight;
export let data: ExtendedEvent;
export let i: number;
export let selected: boolean = false;
export let hideEdit: boolean = false;
let deleteOpen = false;
let editOpen = false;
let dispatcher = createEventDispatcher();
function dispatchSelect() {
setTimeout(() => {
if (!deleteOpen && !editOpen) {
dispatcher('select');
}
}, 1);
}
async function deleteFight() {
await $fightRepo.deleteFight(fight.id);
dispatcher('update');
}
</script>
<div class="flex h-16 {i % 2 === 0 ? 'bg-gray-800' : ''} mx-4 mt-6 rounded border {selected ? 'border-orange-700' : 'border-gray-700'} p-2 hover:bg-gray-700 transition justify-between shadow-lg cursor-pointer"
on:click={dispatchSelect} on:keypress={dispatchSelect} role="checkbox" aria-checked={selected} tabindex="0"
>
<div class="flex">
<div class="flex flex-col">
<div>
<span>{$isWide ? fight.blueTeam.name : fight.blueTeam.kuerzel}</span>
vs.
<span>{$isWide ? fight.redTeam.name : fight.redTeam.kuerzel}</span>
</div>
{#if (fight.ergebnis === 3)}
<span class="ml-2">Unentschieden</span>
{:else if (fight.ergebnis !== 0)}
<span class="ml-2">{fight.ergebnis === 1 ? 'Sieger: ' + ($isWide ? fight.blueTeam.name : fight.blueTeam.kuerzel) : 'Sieger: ' + ($isWide ? fight.redTeam.name : fight.redTeam.kuerzel)}</span>
{:else}
<span class="ml-2">{$isWide ? 'Noch nicht gespielt' : 'kommend'}</span>
{/if}
</div>
</div>
<div class="flex">
<div class="mr-2 flex flex-col">
<span>
{new Intl.DateTimeFormat(Intl.Locale.name, {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
}).format(fight.start)}
</span>
<span>
{new Intl.DateTimeFormat(Intl.Locale.name, {
day: '2-digit',
month: '2-digit',
year: '2-digit'
}).format(fight.start)}
</span>
</div>
{#if !hideEdit}
<Toolbar embedded>
<ToolbarButton on:click={() => editOpen = true}>
<EditOutline/>
</ToolbarButton>
<ToolbarButton color="red" on:click={() => deleteOpen = true}>
<TrashBinOutline />
</ToolbarButton>
</Toolbar>
{/if}
</div>
</div>
<Modal title="Delete {fight.blueTeam.name} vs. {fight.redTeam.name}" bind:open={deleteOpen} autoclose outsideclose size="xs">
<div class="text-center">
<p class="mb-5">
Are you sure you want to delete this fight?
</p>
<Button color="red" on:click={deleteFight}>Delete Fight</Button>
<Button color="alternative">Cancel</Button>
</div>
</Modal>
{#if (editOpen)}
<FightEditModal {fight} bind:data bind:open={editOpen} on:update/>
{/if}

Datei anzeigen

@ -0,0 +1,267 @@
<script lang="ts">
import type {EventFight, ExtendedEvent} from "../../types/event.js";
import {
Button,
Checkbox, Input, Label,
Modal,
SpeedDial,
SpeedDialButton,
Toolbar,
ToolbarButton,
ToolbarGroup,
Tooltip
} from "flowbite-svelte";
import {
ArrowsRepeatSolid, CalendarWeekOutline,
PlusSolid, ProfileCardOutline, TrashBinOutline, UsersGroupOutline,
} from "flowbite-svelte-icons";
import FightCard from "./FightCard.svelte";
import CreateFightModal from "./modals/CreateFightModal.svelte";
import {fightRepo} from "../../repo/repo.js";
import {groups, players} from "../../stores/stores.js";
import TypeAheadSearch from "../../components/TypeAheadSearch.svelte";
import type {UpdateFight} from "../../repo/fight.js";
import moment from "moment";
export let data: ExtendedEvent;
let createOpen = false;
let fights = data.fights;
let selectedFights: Set<EventFight> = new Set();
$: groupsMap = new Set(fights.map(fight => fight.group));
$: groupedFights = 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 = selectedFights;
}
let deleteOpen = 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 kampfleiterOpen = false;
$: selectPlayers = $players.map(player => {
return {
name: player.name,
value: player.id.toString()
}
}).sort((a, b) => a.name.localeCompare(b.name));
let kampfleiter = "";
async function updateKampfleiter() {
for (const fight of selectedFights) {
let f: UpdateFight = {
blueTeam: null,
group: null,
kampfleiter: Number.parseInt(kampfleiter),
map: null,
redTeam: null,
spielmodus: null,
start: null
};
await $fightRepo.updateFight(fight.id, f);
}
fights = await $fightRepo.listFights(data.event.id);
selectedFights = new Set();
kampfleiter = "";
kampfleiterOpen = false;
}
let groupChangeOpen = false;
let group = "";
let groupSearch = "";
$: selectableGroups = [{
name: 'None',
value: ''
}, {
value: groupSearch,
name: `Create: '${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,
kampfleiter: 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;
}
$: minTime = moment(Math.min(...fights.map(fight => fight.start))).utc(true);
let changeTimeOpen = false;
let changedTime = moment(Math.min(...fights.map(fight => fight.start)))?.utc(true)?.toISOString()?.slice(0, -1);
$: deltaTime = moment.duration(moment(changedTime).utc(true).diff(minTime))
async function updateStartTime() {
for (const fight of selectedFights) {
let f: UpdateFight = {
blueTeam: null,
group: null,
kampfleiter: null,
map: null,
redTeam: null,
spielmodus: null,
start: moment(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} on:click={cycleSelect}/>
<Tooltip>Select Upcoming</Tooltip>
</ToolbarGroup>
<ToolbarGroup>
<ToolbarButton on:click={() => selectedFights.size > 0 ? changeTimeOpen = true : changeTimeOpen = false}>
<CalendarWeekOutline/>
</ToolbarButton>
<Tooltip>Reschedule Fights</Tooltip>
<ToolbarButton on:click={() => selectedFights.size > 0 ? kampfleiterOpen = true : kampfleiterOpen = false}>
<ProfileCardOutline/>
</ToolbarButton>
<Tooltip>Change Kampfleiter</Tooltip>
<ToolbarButton on:click={() => selectedFights.size > 0 ? groupChangeOpen = true : groupChangeOpen = false}>
<UsersGroupOutline/>
</ToolbarButton>
<Tooltip>Change Group</Tooltip>
</ToolbarGroup>
<ToolbarGroup>
<ToolbarButton color="red" on:click={() => 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))} on:click={() => 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}
<FightCard {fight} {i} {data} selected={selectedFights.has(fight)}
on:select={() => {
if (selectedFights.has(fight)) {
selectedFights.delete(fight);
} else {
selectedFights.add(fight);
}
selectedFights = selectedFights;
}}
on: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>
<svelte:fragment slot="footer">
<Button color="red" class="ml-auto" on:click={deleteFights}>Delete</Button>
<Button on:click={() => deleteOpen = false} color="alternative">Cancel</Button>
</svelte:fragment>
</Modal>
<Modal bind:open={kampfleiterOpen} title="Change Kampfleiter" size="sm">
<div class="m-2">
<Label for="fight-kampf">Kampfleiter</Label>
<TypeAheadSearch items={selectPlayers} bind:selected={kampfleiter}></TypeAheadSearch>
</div>
<svelte:fragment slot="footer">
<Button class="ml-auto" on:click={updateKampfleiter}>Change</Button>
<Button on:click={() => kampfleiterOpen = false} color="alternative">Cancel</Button>
</svelte:fragment>
</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>
<svelte:fragment slot="footer">
<Button class="ml-auto" on:click={updateGroup}>Change</Button>
<Button on:click={() => groupChangeOpen = false} color="alternative">Cancel</Button>
</svelte:fragment>
</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} let:props>
<input type="datetime-local" {...props} bind:value={changedTime}/>
</Input>
</div>
<p>{deltaTime.asMilliseconds() < 0 ? '' : '+'}{("0" + deltaTime.hours()).slice(-2)}:{("0" + deltaTime.minutes()).slice(-2)}</p>
<svelte:fragment slot="footer">
<Button class="ml-auto" on:click={updateStartTime}>Update</Button>
<Button on:click={() => changeTimeOpen = false} color="alternative">Cancel</Button>
</svelte:fragment>
</Modal>
<SpeedDial>
<SpeedDialButton name="Add" on:click={() => createOpen = true}>
<PlusSolid/>
</SpeedDialButton>
<SpeedDialButton name="Generate" href="#/event/{data.event.id}/generate">
<ArrowsRepeatSolid/>
</SpeedDialButton>
</SpeedDial>

Datei anzeigen

@ -0,0 +1,21 @@
<script lang="ts">
import {Avatar, Button, Modal} from "flowbite-svelte";
import type {ExtendedEvent} from "../../types/event.js";
export let data: ExtendedEvent;
</script>
<div class="m-4">
{#each data.teams as team}
<div class="flex flex-row my-2">
<Avatar size="lg">{team.kuerzel}</Avatar>
<div class="m-2">
<h1 class="text-2xl">{team.name}</h1>
<h2 class="text-lg text-gray-400">Fights: {data.fights.filter(value => value.blueTeam.id === team.id || value.redTeam.id === team.id).length}</h2>
</div>
</div>
{/each}
</div>
<svelte:head>
<title>{data.event.name} - Teams</title>
</svelte:head>

Datei anzeigen

@ -0,0 +1,87 @@
<script lang="ts">
import {Button, Modal} from "flowbite-svelte";
import type {ExtendedEvent} from "../../../types/event.js";
import FightEditPart from "../../../components/FightEditPart.svelte";
import {fightRepo} from "../../../repo/repo.js";
import type {CreateFight} from "../../../repo/fight.ts";
import ErrorModal from "../../../components/ErrorModal.svelte";
import {createEventDispatcher} from "svelte";
import moment from "moment";
let dispatch = createEventDispatcher();
export let open: boolean = false;
export let data: ExtendedEvent;
let blueTeam: string = "";
let redTeam: string = "";
let start: string = "";
let gamemode: string = "";
let map: string = "";
let kampfleiter: string | null = null;
let group: string | null = null;
let groupSearch = "";
let errorOpen = false;
let error: Error | null = null;
$: canCreate = blueTeam !== "" && redTeam !== "" && start !== "" && gamemode !== "" && map !== "";
async function create() {
try {
open = false;
let res = await $fightRepo.createFight(data.event.id, {
spielmodus: gamemode,
blueTeam: parseInt(blueTeam),
redTeam: parseInt(redTeam),
start: moment(start),
map,
kampfleiter: parseInt(kampfleiter),
group,
});
reset()
dispatch("create")
} catch (e) {
error = e;
errorOpen = true;
reset()
}
}
function reset() {
blueTeam = "";
redTeam = "";
start = "";
gamemode = "";
map = "";
kampfleiter = "";
group = null;
groupSearch = "";
}
</script>
<Modal bind:open outsideclose title="Create Fight" on:hide={reset}>
<div class="text-center">
<FightEditPart
bind:blueTeam
bind:redTeam
bind:start
bind:kampfleiter
bind:gamemode
bind:map
bind:group
bind:groupSearch
teams={data.teams}
/>
</div>
<svelte:fragment slot="footer">
<Button on:click={create} class="mr-auto" disabled={!canCreate}>Create</Button>
<Button color="light" on:click={() => open = false}>Cancel</Button>
</svelte:fragment>
</Modal>
<ErrorModal bind:open={errorOpen} bind:error={error} on:close={() => errorOpen = false}/>

Datei anzeigen

@ -0,0 +1,68 @@
<script lang="ts">
import {Button, Input, Label, Modal, Select} from "flowbite-svelte";
import moment from "moment";
import {gamemodes, groups, maps, players} from "../../../stores/stores.js";
import type {EventFight, ExtendedEvent} from "../../../types/event.js";
import TypeAheadSearch from "../../../components/TypeAheadSearch.svelte";
import FightEditPart from "../../../components/FightEditPart.svelte";
import type {UpdateFight} from "../../../repo/fight.js";
import {fightRepo} from "../../../repo/repo.js";
import ErrorModal from "../../../components/ErrorModal.svelte";
import {createEventDispatcher} from "svelte";
export let fight: EventFight;
export let data: ExtendedEvent;
export let open = false;
let redTeam = fight.redTeam.id.toString();
let blueTeam = fight.blueTeam.id.toString();
let start = moment(fight.start).utc(true).toISOString().slice(0, -1);
let kampfleiter = fight.kampfleiter.id.toString();
let gamemode = fight.spielmodus
let map = fight.map;
let group = fight.group;
let groupSearch = fight.group ?? "";
let errorOpen = false;
let error = undefined;
let dispatch = createEventDispatcher();
function save() {
const update: UpdateFight = {
blueTeam: parseInt(blueTeam), group: group === "" ? null : group, kampfleiter: parseInt(kampfleiter), map: map, redTeam: parseInt(redTeam), spielmodus: gamemode, start: moment(start)
}
$fightRepo.updateFight(fight.id, update)
.then(value => {
open = false;
fight = value;
dispatch("update", value);
})
.catch((e) => {
error = e.message;
errorOpen = true;
})
}
</script>
<Modal title="Edit {fight.blueTeam.name} vs. {fight.redTeam.name}" bind:open outsideclose size="xs">
<div class="text-center">
<FightEditPart
bind:blueTeam
bind:redTeam
bind:start
bind:kampfleiter
bind:gamemode
bind:map
bind:group
bind:groupSearch
teams={data.teams}
/>
</div>
<div class="flex">
<Button on:click={save}>Save</Button>
<Button color="light" class="ml-auto" on:click={() => open = false}>Cancel</Button>
</div>
</Modal>
<ErrorModal bind:open={errorOpen} bind:error={error} on:close={() => errorOpen = false}/>

Datei anzeigen

@ -0,0 +1,28 @@
<script lang="ts">
import {createEventDispatcher} from "svelte";
let dragover = false;
function handleDragOver(e) {
e.preventDefault();
dragover = true;
}
const dispatch = createEventDispatcher();
function handleDrop(ev: DragEvent) {
ev.preventDefault();
dragover = false;
dispatch('drop', ev)
}
</script>
<div class="w-56 bg-gray-800 p-4 rounded" class:border={dragover} class:m-px={!dragover} on:drop={handleDrop} on:dragover={handleDragOver} on:dragleave={() => dragover = false}>
<slot></slot>
</div>
<style>
div {
min-height: 14rem;
}
</style>

Datei anzeigen

@ -0,0 +1,236 @@
<script lang="ts">
import type {ExtendedEvent} from "../../types/event.js";
import TeamChip from "./TeamChip.svelte";
import type {Team} from "../../types/team.js";
import DragAcceptor from "./DragAcceptor.svelte";
import moment from "moment";
import {Button, Input, Label, Modal, Range, Select} from "flowbite-svelte";
import {gamemodes, maps} from "../../stores/stores.js";
import {PlusSolid} from "flowbite-svelte-icons";
import {fightRepo} from "../../repo/repo.js";
import {replace} from "svelte-spa-router";
export let data: ExtendedEvent;
$: teams = new Map<number, Team>(data.teams.map(team => [team.id, team]));
let groups: number[][] = [];
$: teamsNotInGroup = data.teams.filter(team => !groups.flat().includes(team.id));
function dragToNewGroup(event: CustomEvent<DragEvent>) {
event.detail.preventDefault();
let teamId = parseInt(event.detail.dataTransfer.getData("team"));
groups = [...groups.map(value => value.filter(value1 => value1 != teamId)), [teamId]].filter(value => value.length > 0);
}
function teamDragStart(ev: DragEvent, team: Team) {
ev.dataTransfer.setData("team", team.id.toString())
}
let resetDragOver = false;
function resetDragOverEvent(ev: DragEvent) {
resetDragOver = true;
ev.preventDefault()
}
function dropReset(ev: DragEvent) {
ev.preventDefault();
let teamId = parseInt(ev.dataTransfer.getData("team"));
groups = groups.map(group => group.filter(team => team !== teamId)).filter(group => group.length > 0);
resetDragOver = false;
}
function dropGroup(ev: CustomEvent<DragEvent>, groupIndex: number) {
ev.preventDefault();
let teamId = parseInt(ev.detail.dataTransfer.getData("team"));
groups = groups.map((group, i) => i === groupIndex ? [...group.filter(value => value != teamId), teamId] : group.filter(value => value != teamId)).filter(group => group.length > 0);
}
let startTime = moment(data.event.start).utc(true).toISOString().slice(0, -1)
$: startMoment = moment(startTime);
let gamemode = ''
let map = ''
$: selectableGamemodes = $gamemodes.map(gamemode => {
return {
name: gamemode,
value: gamemode
}
}).sort((a, b) => a.name.localeCompare(b.name));
$: mapsStore = maps(gamemode);
$: selectableMaps = $mapsStore.map(map => {
return {
name: map,
value: map
}
}).sort((a, b) => a.name.localeCompare(b.name));
let roundTime = 30;
let startDelay = 30;
let showAutoGrouping = false;
let groupCount = Math.floor(data.teams.length / 2);
function createGroups() {
let teams = data.teams.map(team => team.id).sort(() => Math.random() - 0.5);
groups = [];
for (let i = 0; i < groupCount; i++) {
groups.push([])
}
while (teams.length > 0) {
groups[teams.length % groupCount].push(teams.pop() as number)
}
showAutoGrouping = false;
groups = groups.filter(group => group.length > 0);
}
function generateGroups(groups): number[][][][] {
let groupFights = [];
groups.forEach((group) => {
let round = group.length + (group.length % 2) - 1;
let groupFight = [];
for (let i = 0; i < round; i++) {
let availableTeams = [...group];
if(group.length % 2 === 1) {
availableTeams = availableTeams.filter((team, index) => index !== i)
}
let roundFights = [];
while (availableTeams.length > 0) {
let team1 = availableTeams.pop() as number;
let team2 = availableTeams.at(i % availableTeams.length) as number;
availableTeams = availableTeams.filter(team => team !== team2);
let fight = [team1, team2];
fight.sort(() => Math.random() - 0.5);
roundFights.push(fight)
}
groupFight.push(roundFights)
}
groupFights.push(groupFight)
})
return groupFights;
}
$: groupsFights = generateGroups(groups)
$: generateDisabled = groupsFights.length > 0 && groupsFights.every(value => value.every(value1 => value1.length > 0)) && gamemode !== '' && map !== ''
async function generateFights() {
groupsFights.forEach((group, i) => {
group.forEach((round, j) => {
round.forEach(async (fight, k) => {
let blueTeam = teams.get(fight[0])
let redTeam = teams.get(fight[1])
await $fightRepo.createFight(data.event.id, {
blueTeam: blueTeam.id,
redTeam: redTeam.id,
group: "Gruppe " + (i + 1),
kampfleiter: 0,
map: map,
spielmodus: gamemode,
start: startMoment.clone().add(roundTime * j, "minutes").add(startDelay * (k + (i * round.length)), "seconds")
})
})
})
})
await replace("#/event/" + data.event.id)
}
</script>
<div class="flex justify-between">
<div id="reseter" class:border-white={resetDragOver} class="flex m-2 bg-gray-800 w-fit p-2 border border-gray-700 rounded ml-4 h-20 pt-6 relative" on:dragover={resetDragOverEvent} on:dragleave={() => resetDragOver = false} on:drop={dropReset} role="group">
{#each teamsNotInGroup as team}
<TeamChip {team} on:dragstart={ev => teamDragStart(ev, team)}/>
{/each}
</div>
<div class="flex items-center mr-4">
<Button on:click={() => showAutoGrouping = true}>Automatic Grouping</Button>
</div>
</div>
<div class="flex m-4 gap-4 border-b border-gray-700 pb-4">
{#each groups as group, i}
<DragAcceptor on:drop={ev => dropGroup(ev, i)}>
<h1>Group {i + 1} ({group.length})</h1>
{#each group as teamId}
<TeamChip team={teams.get(teamId)} on:dragstart={ev => teamDragStart(ev, teams.get(teamId))}/>
{/each}
</DragAcceptor>
{/each}
<DragAcceptor on:drop={dragToNewGroup}>
<h1>Create Group</h1>
</DragAcceptor>
</div>
<div class="m-4 border-b border-gray-700 pb-4">
<Label for="event-end">Start Time</Label>
<Input id="event-end" bind:value={startTime} class="w-80" let:props size="lg">
<input type="datetime-local" {...props} bind:value={startTime}/>
</Input>
<div class="mt-2">
<Label for="event-roundtime">Round time: {roundTime}m</Label>
<Range id="event-roundtime" bind:value={roundTime} step="1" min="5" max="60"/>
</div>
<div class="mt-2">
<Label for="event-member">Start delay: {startDelay}</Label>
<Range id="event-member" bind:value={startDelay} step="1" min="0" max="30"/>
</div>
<div class="mt-2">
<Label for="fight-gamemode">Gamemode</Label>
<Select items={selectableGamemodes} bind:value={gamemode} id="fight-gamemode"></Select>
</div>
<div class="mt-2">
<Label for="fight-maps">Map</Label>
<Select items={selectableMaps} bind:value={map} id="fight-maps"></Select>
</div>
</div>
<div class="text-center mx-2">
{#each groupsFights as fightsGroup, i}
<div>
<h1 class="text-4xl">Group: {i + 1}</h1>
{#each fightsGroup as fightsRound, j}
<div class="border-b border-gray-700">
<h1 class="text-2xl">Round: {j + 1}</h1>
{#each fightsRound as fightTeams, k}
<div class="text-left p-4">
<span class="bg-gray-800 p-2 border border-gray-700 rounded">{startMoment.clone().add(roundTime * j, "minutes").add(startDelay * (k + (i * fightsRound.length)), "seconds").format("DD.MM.yyyy HH:mm:ss")}</span>
{teams.get(fightTeams[0]).name} vs. {teams.get(fightTeams[1]).name}
</div>
{/each}
</div>
{/each}
</div>
{/each}
</div>
<Button class="!p-4 fixed bottom-4 right-4" pill disabled={!generateDisabled} on:click={generateFights}>
<PlusSolid/>
</Button>
<Modal bind:open={showAutoGrouping} outsideclose title="Auto Grouping" size="sm">
<Label for="event-member">Groups: {groupCount}</Label>
<Range id="event-member" bind:value={groupCount} step="1" min="1" max={Math.floor(data.teams.length / 2)}/>
<svelte:fragment slot="footer">
<Button class="ml-auto" on:click={createGroups}>Create</Button>
<Button color="alternative" on:click={() => showAutoGrouping = false}>Cancel</Button>
</svelte:fragment>
</Modal>
<style lang="scss">
#reseter::before {
content: 'Reset';
position: absolute;
top: 0;
color: gray;
}
#reseter {
min-width: 14rem;
}
</style>

Datei anzeigen

@ -0,0 +1,17 @@
<script lang="ts">
import type {Team} from "../../types/team.js";
import {brightness, colorFromTeam, lighten} from "../../util.js";
export let team: Team;
let hover = false;
</script>
<div class="rounded w-fit p-2 border-gray-600 border cursor-grab select-none m-1 flex place-items-center"
style:background-color={hover ? lighten(colorFromTeam(team)) : colorFromTeam(team)} class:text-black={brightness(colorFromTeam(team))} draggable="true"
on:dragstart
on:mouseenter={() => hover = true}
on:mouseleave={() => hover = false}>
<span>{team.name}</span>
</div>

Datei anzeigen

@ -0,0 +1,71 @@
<script lang="ts">
import {Button, Input, Label, Modal} from "flowbite-svelte";
import moment from "moment";
import {createEventDispatcher} from "svelte";
import ErrorModal from "../../components/ErrorModal.svelte";
import {eventRepo} from "../../repo/repo.js";
import type {SWEvent} from "../../types/event.js";
export let open = false;
const dispatch = createEventDispatcher();
let errorOpen = false;
let error = undefined;
let eventName = "";
let start = "";
$: startDate = moment(start)
let end = "";
$: endDate = moment(end)
$: canSubmit = eventName.length > 0 && startDate.isValid() && endDate.isValid() && startDate.isBefore(endDate)
async function createEvent() {
try {
await $eventRepo.createEvent({
name: eventName,
start: startDate,
end: endDate
})
dispatch("create");
open = false;
} catch (e) {
error = e;
errorOpen = true;
open = false;
}
}
function clear() {
eventName = "";
start = "";
end = "";
}
</script>
<Modal bind:open title="Create Event" outsideclose size="sm" on:hide={clear}>
<div class="flex flex-col place-items-center text-center">
<div class="w-2/3 m-2">
<Label for="event-create-name">Event Name</Label>
<Input id="event-create-name" bind:value={eventName} placeholder="Name..."></Input>
</div>
<div class="w-2/3 m-2">
<Label for="event-create-start">End</Label>
<Input id="event-create-start" bind:value={start} let:props>
<input type="datetime-local" {...props} bind:value={start}/>
</Input>
</div>
<div class="w-2/3 m-2">
<Label for="event-create-start">End</Label>
<Input id="event-create-start" bind:value={end} let:props>
<input type="datetime-local" {...props} bind:value={end}/>
</Input>
</div>
</div>
<svelte:fragment slot="footer">
<Button color="alternative" on:click={() => open = false} class="mr-auto">Cancel</Button>
<Button on:click={createEvent} disabled={!canSubmit}>Create</Button>
</svelte:fragment>
</Modal>
<ErrorModal bind:open={errorOpen} bind:error={error}/>

Datei anzeigen

@ -0,0 +1,22 @@
<script lang="ts">
import {Card} from "flowbite-svelte";
import {link} from 'svelte-spa-router'
import type {ShortEvent} from "../../types/event.js";
export let event: ShortEvent;
$: sameDate = new Intl.DateTimeFormat().format(event.start) === new Intl.DateTimeFormat().format(event.end);
</script>
<a href="/event/{event.id}" use:link>
<Card class="hover:scale-105 transition">
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">{event.name}</h5>
{#if !sameDate}
<p>Startet: {new Intl.DateTimeFormat().format(event.start)}</p>
<p>Endet: {new Intl.DateTimeFormat().format(event.end)}</p>
{:else}
<p>Am: {new Intl.DateTimeFormat().format(event.start)}</p>
<p>&nbsp;</p>
{/if}
</Card>
</a>

Datei anzeigen

@ -0,0 +1,96 @@
import type {ExtendedEvent, ShortEvent, SWEvent} from "../types/event.js";
import {fetchWithToken} from "./repo.js";
import type {Moment} from "moment";
import {ExtendedEventSchema, ShortEventSchema, SWEventSchema} from "../types/event.js";
import {z} from "zod";
export interface CreateEvent {
name: string
start: Moment
end: Moment
}
export interface UpdateEvent {
name: string
start: Moment
end: Moment
deadline: Moment
maxTeamMembers: number
schemType: string | null
publicSchemsOnly: boolean
spectateSystem: boolean
}
export class EventRepo {
constructor(private token: string) {}
public async listEvents(): Promise<ShortEvent[]> {
const res = await fetchWithToken(this.token, "/events");
if (res.ok) {
return z.array(ShortEventSchema).parse(await res.json());
} else {
throw new Error("Could not fetch events: " + res.statusText);
}
}
public async getEvent(id: string): Promise<ExtendedEvent> {
const res = await fetchWithToken(this.token, `/events/${id}`);
if (res.ok) {
return ExtendedEventSchema.parse(await res.json());
} else {
throw new Error("Could not fetch event: " + res.statusText);
}
}
public async createEvent(event: CreateEvent): Promise<SWEvent> {
const res = await fetchWithToken(this.token, "/events", {
method: "POST",
body: JSON.stringify({
name: event.name,
start: +event.start,
end: +event.end
}),
});
if (res.ok) {
return SWEventSchema.parse(await res.json());
} else {
throw new Error("Could not create event: " + res.statusText);
}
}
public async updateEvent(id: string, event: UpdateEvent): Promise<SWEvent> {
const res = await fetchWithToken(this.token, `/events/${id}`, {
method: "PUT",
body: JSON.stringify({
name: event.name,
start: +event.start,
end: +event.end,
deadline: +event.deadline,
maxTeamMembers: event.maxTeamMembers,
schemType: event.schemType,
publicSchemsOnly: event.publicSchemsOnly,
spectateSystem: event.spectateSystem
}),
headers: {
"Content-Type": "application/json"
}
});
if (res.ok) {
return SWEventSchema.parse(await res.json());
} else {
throw new Error("Could not update event: " + res.statusText);
}
}
public async deleteEvent(id: string): Promise<boolean> {
const res = await fetchWithToken(this.token, `/events/${id}`, {
method: "DELETE"
});
return res.ok;
}
}

Datei anzeigen

@ -0,0 +1,92 @@
import type {EventFight} from "../types/event.js";
import {fetchWithToken} from "./repo.js";
import type {Moment} from "moment";
import {z} from "zod";
import {EventFightSchema} from "../types/event.js";
export interface CreateFight {
spielmodus: string
map: string
blueTeam: number
redTeam: number
start: Moment
kampfleiter: number | null
group: string | null
}
export interface UpdateFight {
spielmodus: string | null
map: string | null
blueTeam: number | null
redTeam: number | null
start: Moment | null
kampfleiter: number | null
group: string | null
}
export class FightRepo {
constructor(private token: string) {}
public async listFights(eventId: number): Promise<EventFight[]> {
const res = await fetchWithToken(this.token, `/events/${eventId}/fights`);
if (res.ok) {
return z.array(EventFightSchema).parse(await res.json());
} else {
throw new Error("Could not fetch fights: " + res.statusText);
}
}
public async createFight(eventId: number, fight: CreateFight): Promise<EventFight> {
let res = await fetchWithToken(this.token, `/fights`, {
method: "POST",
body: JSON.stringify({
event: eventId,
spielmodus: fight.spielmodus,
map: fight.map,
blueTeam: fight.blueTeam,
redTeam: fight.redTeam,
start: +fight.start,
kampfleiter: fight.kampfleiter,
group: fight.group
})
})
if (res.ok) {
return EventFightSchema.parse(await res.json());
} else {
throw new Error("Could not create fight: " + res.statusText);
}
}
public async updateFight(fightId: number, fight: UpdateFight): Promise<EventFight> {
let res = await fetchWithToken(this.token, `/fights/${fightId}`, {
method: "PUT",
body: JSON.stringify({
spielmodus: fight.spielmodus,
map: fight.map,
blueTeam: fight.blueTeam,
redTeam: fight.redTeam,
start: fight.start?.valueOf(),
kampfleiter: fight.kampfleiter,
group: fight.group
})
})
if (res.ok) {
return EventFightSchema.parse(await res.json());
} else {
throw new Error("Could not update fight: " + res.statusText);
}
}
public async deleteFight(fightId: number): Promise<void> {
let res = await fetchWithToken(this.token, `/fights/${fightId}`, {
method: "DELETE"
})
if (!res.ok) {
throw new Error("Could not delete fight: " + res.statusText);
}
}
}

Datei anzeigen

@ -0,0 +1,57 @@
import type {Perms, UserPerms} from "../types/perms.js";
import {fetchWithToken} from "./repo.js";
import {PermsSchema, UserPermsSchema} from "../types/perms.js";
export class PermsRepo {
constructor(private token: string) {}
public async listPerms(): Promise<Perms> {
const res = await fetchWithToken(this.token, "/perms");
if (res.ok) {
return PermsSchema.parse(await res.json());
} else {
throw new Error("Could not fetch perms: " + res.statusText);
}
}
public async getPerms(userId: number): Promise<UserPerms> {
const res = await fetchWithToken(this.token, `/perms/user/${userId}`);
if (res.ok) {
return UserPermsSchema.parse(await res.json());
} else {
throw new Error("Could not fetch perms: " + res.statusText);
}
}
public async setPrefix(userId: number, prefix: string): Promise<void> {
const res = await fetchWithToken(this.token, `/perms/user/${userId}/prefix/${prefix}`, {
method: "PUT",
});
if (!res.ok) {
throw new Error("Could not set prefix: " + res.statusText);
}
}
public async addPerm(userId: number, perm: string): Promise<void> {
const res = await fetchWithToken(this.token, `/perms/user/${userId}/${perm}`, {
method: "PUT",
});
if (!res.ok) {
throw new Error("Could not add perm: " + res.statusText);
}
}
public async removePerm(userId: number, perm: string): Promise<void> {
const res = await fetchWithToken(this.token, `/perms/user/${userId}/${perm}`, {
method: "DELETE",
});
if (!res.ok) {
throw new Error("Could not remove perm: " + res.statusText);
}
}
}

Datei anzeigen

@ -0,0 +1,15 @@
import {derived, writable} from "svelte/store";
import {EventRepo} from "./event.js";
import {FightRepo} from "./fight.js";
import {PermsRepo} from "./perms.js";
export { EventRepo } from "./event.js"
export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) => fetch(`https://steamwar.de/eventplanner-api${url}`, {...params, headers: {"X-SW-Auth": token, "Content-Type": "application/json", ...params.headers}});
export const tokenStore = writable(localStorage.getItem("sw-api-token") ?? "")
tokenStore.subscribe((value) => localStorage.setItem("sw-api-token", value))
export const eventRepo = derived(tokenStore, ($token) => new EventRepo($token))
export const fightRepo = derived(tokenStore, ($token) => new FightRepo($token))
export const permsRepo = derived(tokenStore, ($token) => new PermsRepo($token))

Datei anzeigen

@ -0,0 +1,63 @@
import {readonly, writable} from "svelte/store";
import type {Readable, Subscriber, Unsubscriber} from "svelte/store";
export interface Cached<T> extends Readable<T>{
reload: () => void;
}
export function cached<T>(normal: T, init: () => Promise<T>): Cached<T> {
const store = writable<T>(normal);
let first = true;
const reload = () => {
init().then(data => {
store.set(data);
});
}
return {
...readonly(store),
subscribe: (run: Subscriber<T>, invalidate?: (value?: T) => void): Unsubscriber => {
if(first) {
first = false;
reload();
}
return store.subscribe(run, invalidate);
},
reload
};
}
export function cachedFamily<T, K>(normal: K, init: (T) => Promise<K>): (T) => Cached<K> {
const stores: Map<T, Cached<K>> = new Map();
return (arg: T) => {
if(stores.has(arg)) {
return stores.get(arg);
} else {
const store = writable<K>(normal);
let first = true;
const reload = () => {
init(arg).then(data => {
store.set(data);
});
}
const cachedStore = {
...readonly(store),
subscribe: (run: Subscriber<K>, invalidate?: (value?: K) => void): Unsubscriber => {
if(first) {
first = false;
reload();
}
return store.subscribe(run, invalidate);
},
reload
} as Cached<K>;
stores.set(arg, cachedStore);
return cachedStore;
}
}
}

Datei anzeigen

@ -0,0 +1,47 @@
import type {Player, SchematicType} from "../types/data.js";
import {PlayerSchema} from "../types/data.js";
import {cached, cachedFamily} from "./cached.js";
import type {Team} from "../types/team.js";
import {TeamSchema} from "../types/team.js";
import {get, writable} from "svelte/store";
import {permsRepo, tokenStore} from "../repo/repo.js";
import {z} from "zod";
export const schemTypes = cached<SchematicType[]>([], () => {
return fetch("https://steamwar.de/eventplanner-api/data/schematicTypes", {headers: {"X-SW-Auth": get(tokenStore)}})
.then(res => res.json())
})
export const players = cached<Player[]>([], async () => {
const res = await fetch("https://steamwar.de/eventplanner-api/data/users", {headers: {"X-SW-Auth": get(tokenStore)}});
return z.array(PlayerSchema).parse(await res.json());
})
export const gamemodes = cached<string[]>([], async () => {
const res = await fetch("https://steamwar.de/eventplanner-api/data/gamemodes", {headers: {"X-SW-Auth": get(tokenStore)}});
return z.array(z.string()).parse(await res.json());
})
export const maps = cachedFamily<string, string[]>([], async (gamemode) => {
if (get(gamemodes).every(value => value !== gamemode)) return [];
const res = await fetch(`https://steamwar.de/eventplanner-api/data/gamemodes/${gamemode}/maps`, {headers: {"X-SW-Auth": get(tokenStore)}});
if (!res.ok) {
return [];
} else {
return res.json();
}
})
export const groups = cached<string[]>([], async () => {
const res = await fetch("https://steamwar.de/eventplanner-api/data/groups", {headers: {"X-SW-Auth": get(tokenStore)}});
return z.array(z.string()).parse(await res.json());
})
export const teams = cached<Team[]>([], async () => {
const res = await fetch("https://steamwar.de/eventplanner-api/team", {headers: {"X-SW-Auth": get(tokenStore)}});
return z.array(TeamSchema).parse(await res.json());
})
export const isWide = writable(window.innerWidth >= 640);
window.addEventListener("resize", () => isWide.set(window.innerWidth >= 640));

Datei anzeigen

@ -0,0 +1,16 @@
import {z} from "zod";
export const SchematicTypeSchema = z.object({
name: z.string(),
db: z.string(),
})
export type SchematicType = z.infer<typeof SchematicTypeSchema>;
export const PlayerSchema = z.object({
id: z.number(),
name: z.string(),
uuid: z.string(),
})
export type Player = z.infer<typeof PlayerSchema>;

Datei anzeigen

@ -0,0 +1,46 @@
import type {Team} from "./team.js";
import type {Player} from "./data.js";
import {z} from "zod";
import {TeamSchema} from "./team.js";
import {PlayerSchema} from "./data.js";
export const ShortEventSchema = z.object({
id: z.number(),
name: z.string(),
start: z.number(),
end: z.number(),
})
export type ShortEvent = z.infer<typeof ShortEventSchema>;
export const SWEventSchema = ShortEventSchema.extend({
deadline: z.number(),
maxTeamMembers: z.number(),
schemType: z.string().nullable(),
publicSchemsOnly: z.boolean(),
spectateSystem: z.boolean(),
})
export type SWEvent = z.infer<typeof SWEventSchema>;
export const EventFightSchema = z.object({
id: z.number(),
spielmodus: z.string(),
map: z.string(),
blueTeam: TeamSchema,
redTeam: TeamSchema,
kampfleiter: PlayerSchema.nullable(),
start: z.number(),
ergebnis: z.number(),
group: z.string().nullable(),
})
export type EventFight = z.infer<typeof EventFightSchema>;
export const ExtendedEventSchema = z.object({
event: SWEventSchema,
teams: z.array(TeamSchema),
fights: z.array(EventFightSchema),
})
export type ExtendedEvent = z.infer<typeof ExtendedEventSchema>;

Datei anzeigen

@ -0,0 +1,23 @@
import {z} from "zod";
export const PrefixSchema = z.object({
name: z.string().startsWith("PREFIX_"),
colorCode: z.string().length(2).startsWith("§"),
chatPrefix: z.string()
})
export type Prefix = z.infer<typeof PrefixSchema>;
export const PermsSchema = z.object({
perms: z.array(z.string()),
prefixes: z.record(PrefixSchema),
})
export type Perms = z.infer<typeof PermsSchema>;
export const UserPermsSchema = z.object({
prefix: PrefixSchema,
perms: z.array(z.string()),
})
export type UserPerms = z.infer<typeof UserPermsSchema>;

Datei anzeigen

@ -0,0 +1,12 @@
import {z} from "zod";
export const TeamSchema = z.object({
id: z.number(),
name: z.string(),
kuerzel: z.string().min(1).max(4),
color: z.string().max(1),
})
export type Team = z.infer<typeof TeamSchema>;

49
src/components/admin/util.ts Normale Datei
Datei anzeigen

@ -0,0 +1,49 @@
import Color from "color";
import type {Team} from "./types/team.js";
export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
export function colorFromTeam(team: Team): string {
switch (team.color) {
case "1":
return "#0000AA";
case "2":
return "#00AA00";
case "3":
return "#00AAAA";
case "4":
return "#AA0000";
case "5":
return "#AA00AA";
case "6":
return "#FFAA00";
case "7":
return "#AAAAAA";
case "8":
return "#555555";
case "9":
return "#5555FF";
case "a":
return "#55FF55";
case "b":
return "#55FFFF";
case "c":
return "#FF5555";
case "d":
return "#FF55FF";
case "e":
return "#FFFF55";
case "f":
return "#FFFFFF";
default:
return "#000000";
}
}
export function lighten(color: string) {
return brightness(color) ? Color(color).lighten(0.2).hex() : Color(color).darken(0.2).hex()
}
export function brightness(color: string) {
return Color(color).isLight()
}

14
src/content/config.ts Normale Datei
Datei anzeigen

@ -0,0 +1,14 @@
// @ts-ignore
import { defineCollection, z } from 'astro:content';
export const pages = defineCollection({
type: "content",
schema: z.object({
title: z.string().min(1).max(80),
description: z.string().min(1).max(120),
})
})
export const collections = {
'pages': pages
}

144
src/content/pages/en/faq.md Normale Datei
Datei anzeigen

@ -0,0 +1,144 @@
---
title: FAQ
description: Frequently asked questions
slug: faq
---
# Server/ IP-Adresse
#### Was ist die IP? Wie kann ich auf den Server joinen?
Steamwar.de
#### Mit welcher Version kann ich auf den Server joinen?
SteamWar läuft auf der 1.19.4, du kannst aber mit den Versionen 1.12.x -1.19.4 joinen.
#### Kann ich auch mit der Bedrock-Version joinen?
Ja, über die IP: steamwar.de
# Allgemeines
#### Wo finde ich die Verhaltensrichtlinien/ Serverregeln
Über diese Website unter „Startseite“ oder indem du hier drauf klickst
# Minigames/ Fights
#### Wie kann ich Kämpfe starten?
Mit „/fight“ kannst du Kämpfe mit dem aktuellen WarGear Regelwerk und der WG Submodi starten. Du kannst aber auch die Lobby-Portale nutzen, indem du entweder durch die großen Portale am Spawn gehst, oder im Teleporter auf „Arenen“ drückst, und dann dort über die Portale eine Runde startest.
#### Wie kann ich Kämpfe mit alten Regelwerken starten?
Mit „/historic“ kannst du alle historischen Spielmodi spielen, bis zur Version 1.7.
#### Wie kann ich gegen einen bestimmten Spieler kämpfen?
Mit „/challenge“ kannst du den gewählten Spieler herausfordern. So können auch z. B. Private MissileWars Runden gestartet werden, in der jeder Spieler eingeladen werden muss, um teilnehmen zu können. Der Challenge-Befehl funktioniert nicht mit historischen Spielmodi.
#### Was sind rotierende Modi?
Für manche Modi gibt es nicht genug Nachfrage, um eine dauerhafte Aufnahme dieser in die regulär spielbaren Modi zu rechtfertigen. Diese Modi kann man daher alle paar Monate spielen und werden allgemein als rotierende Modi bezeichnet. Es steht immer ein rotierender Modus zur Verfügung, und dieser wechselt ungefähr jeden Monat. Zu den rotierenden Modi gehören z. B. MicroWarGear und MegaWarGear.
#### Wie kann ich anderen Kämpfen beitreten?
Entweder klickst du auf die entsprechende Nachricht im Chat, trittst einer Arena mit „/arena [arena]“ bei, oder folgst einem Spieler mit „/join [spieler]“
#### Nach welchen Regeln muss ich meine Kampfmaschinen bauen?
Die Regelwerke findest du hier auf der Website unter dem Menüpunkt „Spielmodi“
# Bauserver
#### Was ist ein Bauserver?
Ein Bauserver ist ein privater Server, auf welchem Redstoneschaltungen, Kanonen und Schematics für unsere Spielmodi im Kreativmodus mit einer großen Liste an Hilfsmitteln gebaut werden können.
#### Wie kann ich meinen Bauserver starten?
Entweder mit „/bau“, „/b“ oder „/build“. Solltest du auf einen Bau einer anderen Spielversion wollen, benutze „/b [version]“.
#### Wie lasse ich andere auf meinen Bau?
Entweder über „/b addmember [spieler]“ oder klicke auf die Nachricht im Chat, wenn sie versuchen deinem Bau per command beizutreten. Du wirst sie erkennen, wenn sie erscheint.
#### Wie trete ich anderen Baus bei?
Entweder über „/b tp [spieler]“ oder „/join [spieler]“, wenn du weißt, dass er sich auf seinem Bau befindet. Solltest du auf den Bau einer anderen Spielversion wollen, gebe „/b tp [spieler] [version]“ ein.
#### Wie gebe ich anderen Spielern Rechte?
Für Worldrechte (Regionsverwaltung) „/b toggleworld [spieler]“
Für Worldedit „/b togglewe [spieler]“
#### Wie kann ich die verschiedenen Funktionen meines Baus nutzen?
Am Anfang ist es am einfachsten, das Bau-GUI zu nutzen. Das geht entweder über „/gui“ oder indem du deine Taste zum Hand wechseln zweimal schnell hintereinander drückst (standardmäßig „F“)
#### Wie schreibe ich im lokalen Chat bzw. im Bau Chat?
„/bc [Nachricht]“, „/local [Nachricht]“ oder „+ [Nachricht]“
# Schematics
#### Wo kann ich meine Schematics hochladen?
Entweder über https://steamwar.de/startseite/schematicupload/
Oder schicke unserem Discord Bot (Steamwar Bot#9952) die Schematic per Privatnachricht. Dafür musst du deinen Minecraft und Discord Account bei uns verknüpft haben.
#### Wie kann ich meine Schematics herunterladen?
Über die ingame Download Funktion im Schem-GUI „//schem gui“, und „//schem download [schematic]“,
#### Wie kann ich meine Schematic auf die Arena bringen/ freigeben lassen?
„//schem changetype [schemname] [Typ]“ oder wenn du dich mit den Typen nicht auskennst „//schem changetype [schemname]“. Danach muss die Schematic von einem Supporter oder Moderator geprüft werden.
#### Wie erfahre ich den Grund für die Ablehnung meiner Schematic?
Wenn du zum Zeitpunkt der Ablehnung offline warst „//schem search [schemname]“
Wenn du zum Zeitpunkt der Ablehnung online bist, steht eine Nachricht im Chat.
Über eine Privatnachricht vom SteamWar Discord Bot. Verbinde dafür deinen Minecraft und Discord Account (Informationen dazu unter dem Punkt „Discord“)
#### Wie erstelle ich eine Schematic/ speichere ich mein WarGear, WarShip usw.?
Gehe auf die Region mit deinem Bauwerk. Gebe nun „/select build“ ein. Stelle dich jetzt mittig vor den Baubereich (normalerweise der gelbe Glasblock), und gebe „//copy“ ein. Jetzt kannst du dein Bauwerk mit „//schem save [schemname]“ speichern.
Gib „//wand“ ein, nimm das Item in die Hand und wähle mit Linksklick bspw. die untere linke Ecke des Bereichs aus, in welchem sich dein Bauwerk befindet, und wähle dann mit Rechtsklick die schräg gegenüberliegende Ecke aus. Alternativ kannst du die Ecken auch mit „//pos1“ und „//pos2“ auswählen. Dafür müssen dann die Füße deines Charakters in der jeweiligen Ecke bei Senden des Commands befinden. Jetzt sollte sich, wenn du das Item in der Hand hältst, dein Bauwerk innerhalb eines Rahmens aus Partikeln befinden. Sollte dies nicht der Fall sein, passe die Ecken an, bis sich dein Bauwerk in dem Rahmen befindet. Gehe nun an die Stelle, wo du dein Bauwerk kopieren möchtest, was üblicherweise mittig auf dem Boden vor diesem ist (der gelbe Glasblock im Boden). Gebe jetzt „//copy“ ein. Um das Ganze letztendlich zu speichern, gebe „//schem save [schemname]“ ein.
#### Wie sende ich ein Public ein?
Informiere ein entsprechendes Ratsmitglied (einsehbar auf unserem Discord Server) und füge alle Mitglieder des entsprechenden Rates auf die Schem hinzu. Danach wird der Antrag innerhalb des Rates diskutiert. Sollte der Rat dem Antrag zustimmen, muss ein Admin entscheiden, ob es Public wird. Wenn ja, wirst du aufgefordert alle wieder von der Schematic zu entfernen und es wird zur Public Schem geändert.
# Discord
#### Wo finde ich den Discord Server?
[steamwar.de/discord](https://steamwar.de/discord)
#### Wo finde ich den Social Media Discord Server?
[Discord](https://discord.gg/PvXFsRvZfB)
#### Wie verbinde ich meinen Discord und Minecraft Account?
Klicke in dem Discord Channel „#「👮」regel-infos“ auf den Button „Minecraft verknüpfen“. Folge dann den von dem SteamWar Bot angewiesenen Schritten.
# Support
#### Wie öffne ich ein Support Ticket?
* Auf unserem Discord-Server über den Channel „#「❓」support“. Dort kannst du Fragen stellen, Bugs und Spieler melden, sowie Features vorschlagen und Entbannungsanträge stellen.
#### Wie melde ich schnell einen Bug?
„/bug [beschreibung des bugs]“
# Events
#### Wie kann ich mich für ein Event anmelden?
„/team event [eventname]“ oder, wenn du deinen Minecraft Account mit deinem Discord Accout verknüpft hast, über den Discord Channel „#「📣」events“ auf unserem Discord-Server.
#### Kann ich mit zwei Accounts gleichzeitig teilnehmen?
Nein
#### Kann ich während eines Events noch Spieler in mein Team einladen?
Nein. Während eines laufenden Events sind Neuaufnahmen in ein Team nicht möglich.
# Forum
#### Wo finde ich das Forum?
Auf unserem Discord Server unter der Kategorie „Forum“
# Website
#### Wie melde ich mich auf der Website an?
Tritt unserem Minecraft Server bei, und gebe „/webpw [passwort]“ ein
# Teams
#### Wie trete ich einem Team bei?
Mit „/team join [Team]“. Mit „/team“ siehst du alle weiteren Commands.
# Sonstiges
#### Wo erhalte ich weitere Hilfe?
Mit „/tutorial“ kannst du dir Tutorial-Welten von Spielern ansehen.

Datei anzeigen

@ -0,0 +1,5 @@
---
title: Join Now!
description: How to join SteamWar.de
slug: join
---

1
src/env.d.ts vendored
Datei anzeigen

@ -1,3 +1,4 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
/// <reference path="../.astro-i18n/generated.d.ts" />

Datei anzeigen

@ -4,18 +4,19 @@ import {astroI18n} from "astro-i18n";
const { title, description } = Astro.props.frontmatter || Astro.props;
---
<html lang={astroI18n.langCode}>
<html lang={astroI18n.langCode} class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="description" content={description}/>
<link rel="stylesheet" href="/fonts/barlow-condensed/barlow-condensed.css" />
<title>{title}</title>
<slot name="head" />
</head>
<body>
<body class="dark:bg-zinc-800">
<slot />
</body>
</html>

Datei anzeigen

@ -11,26 +11,62 @@ const { title } = Astro.props;
<Basic title={title}>
<slot name="head" slot="head" />
<Fragment>
<div>
<nav-bar class="h-24 sm:h-12 fixed top-0 left-0 right-0 flex flex-col sm:flex-row items-center justify-evenly sm:justify-between px-4 transition-colors z-10 \
<div class="min-h-screen flex flex-col">
<nav-bar class="fixed top-0 left-0 right-0 px-4 transition-colors z-10 flex justify-center \
before:bg-transparent before:absolute before:top-0 before:left-0 before:bottom-0 before:right-0 before:bg-black before:-z-10 before:scale-y-0 before:transition-transform before:origin-top">
<div class="flex flex-col md:flex-row items-center justify-evenly md:justify-between match">
<a class="flex items-center" href={l("/")}>
<Image src={localLogo} alt={t("navbar.logo.alt")} width="44" height="44" quality="max" class="mr-2 p-1 bg-black rounded-full" />
<h1 class="text-2xl uppercase font-bold inline-block">
<h1 class="text-2xl uppercase font-bold inline-block dark:text-white">
{t("navbar.title")}
</h1>
</a>
<div class="flex items-center">
<div class="flex items-center flex-wrap">
<div class="btn-dropdown my-1">
<a class="btn btn-gray" href={l("/")}>
<span class="btn__text">Start</span>
<span class="btn__text">{t("navbar.links.home.title")}</span>
</a>
<div>
<a class="btn btn-gray" href={l("/about")}>{t("navbar.links.home.about")}</a>
<a class="btn btn-gray">{t("navbar.links.home.downloads")}</a>
<a class="btn btn-gray" href={l("/faq")}>{t("navbar.links.home.faq")}</a>
</div>
</div>
<a class="btn btn-gray my-1" href={l("/")}>
<span class="btn__text">{t("navbar.links.announcements")}</span>
</a>
<div class="btn-dropdown my-1">
<a class="btn btn-gray" rel="prefetch" href={l("/blog")}>
<span class="btn__text">Blog</span>
<span class="btn__text">{t("navbar.links.rules.title")}</span>
</a>
<a class="btn">
<div>
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.gamemode")}</h2>
<a class="btn btn-gray">{t("navbar.links.rules.wg")}</a>
<a class="btn btn-gray">{t("navbar.links.rules.mwg")}</a>
<a class="btn btn-gray">{t("navbar.links.rules.ws")}</a>
<a class="btn btn-gray">{t("navbar.links.rules.as")}</a>
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.rotating")}</h2>
<a class="btn btn-gray">{t("navbar.links.rules.megawg")}</a>
<a class="btn btn-gray">{t("navbar.links.rules.micro")}</a>
<a class="btn btn-gray">{t("navbar.links.rules.sf")}</a>
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.general")}</h2>
<a class="btn btn-gray">{t("navbar.links.rules.coc")}</a>
</div>
</div>
<div class="btn-dropdown my-1">
<a class="btn btn-gray" rel="prefetch">
<span class="btn__text">Help</span>
</a>
<div>
<a class="btn btn-gray">Helpcenter</a>
<a class="btn btn-gray">Docs</a>
</div>
</div>
<a class="btn my-1">
<span class="btn__text">Login</span>
</a>
</div>
</div>
</nav-bar>
<script>
class Navbar extends HTMLElement {
@ -54,14 +90,47 @@ const { title } = Astro.props;
customElements.define('nav-bar', Navbar);
</script>
<main>
<main class="flex-1">
<slot />
</main>
<footer class="bg-gray-900 w-screen h-80 mt-4 rounded-t-2xl flex flex-col dark:bg-neutral-900">
<div class="flex-1 flex justify-evenly mt-4 ">
<div class="footer-card">
<h1>Serverstatus</h1>
</div>
<div class="footer-card">
<h1>Links</h1>
<a href={l("/")}>Startseite</a>
<a href={l("/join")}>Join Now</a>
<a href={l("/")}>Announcements</a>
<a href={l("/")}>Gamemodes</a>
<a href={l("/")}>Stats</a>
<a href={l("/")}>Code of conduct</a>
<a href={l("/")}>Privacy Policy</a>
<a href={l("/")}>Imprint</a>
</div>
<div class="footer-card">
<h1>Social Media</h1>
<a>YouTube</a>
</div>
</div>
<span class="text-sm text-white text-center">© SteamWar.de</span>
</footer>
</div>
</Fragment>
</Basic>
<style>
.footer-card {
@apply w-36 text-gray-400 flex flex-col;
>h1 {
@apply text-xl font-bold text-gray-100;
}
>a {
@apply underline;
}
}
.scrolled {
@apply text-white;
@ -69,4 +138,8 @@ const { title } = Astro.props;
@apply scale-y-100;
}
}
.match {
width: clamp(75%, 25rem, 100vw);
}
</style>

Datei anzeigen

@ -1,17 +1,64 @@
{
"home": {
"title": "SteamWar.de",
"page": "SteamWar - Home",
"title": {
"first": "Steam",
"second": "War"
},
"subtitle": {
"1": "Players Online: ",
"2": "WarGears, AirShips, WarShips",
"3": "Version: 1.12 - 1.20"
},
"join": "Join Now"
"join": "Join Now",
"benefits": {
"historic": {
"title": "Historical Gamemodes",
"description": {
"1": "Play with your old WarGears and WarShips!",
"2": "You can play with your old tech from 1.7 WarGear to 1.12 WarShips"
}
},
"server": {
"title": "Own Build Server",
"description": "To garantuee the best performance, every player has his own build server, which runs independently from other servers."
},
"events": {
"title": "Regular Turnaments",
"description": {
"1": "Test your skills in regular turnaments!",
"2": "Our own event system is very flexible and allows us to create unique turnaments."
}
}
}
},
"navbar": {
"title": "SteamWar.de",
"title": "SteamWar",
"logo": {
"alt": "SteamWar.de Logo"
"alt": "SteamWar Logo"
},
"links": {
"home": {
"title": "Home",
"about": "About",
"downloads": "Downloads",
"faq": "FAQ"
},
"announcements": "Announcements",
"rules": {
"title": "Rules",
"gamemode": "Gamemodes",
"wg": "WarGear",
"mwg": "MiniWarGear",
"ws": "WarShip",
"as": "AirShip",
"rotating": "Rotating",
"megawg": "MegaWarGear",
"micro": "MicroWarGear",
"sf": "StreetFight",
"general": "General",
"coc": "Code of Conduct"
}
}
}
}

54
src/pages/[...slug].astro Normale Datei
Datei anzeigen

@ -0,0 +1,54 @@
---
import { getCollection } from 'astro:content'
import {astroI18n} from "astro-i18n";
import NavbarLayout from "../layouts/NavbarLayout.astro";
export async function getStaticPaths() {
let posts = await getCollection("pages");
return posts.filter(value => value.id.split("/")[0] === astroI18n.langCode).map((page) => ({
props: { page }, params: { slug: page.slug }
}) )
}
const { page } = Astro.props;
const { Content } = await page.render();
---
<NavbarLayout title={page.data.title}>
<article>
<h1 class="text-left">{page.data.title}</h1>
<p class="text-left border-b-2 border-neutral-800">{page.data.description}</p>
<Content />
</article>
</NavbarLayout>
<style is:global>
article {
width: clamp(75%, 25rem, 100vw);
@apply mx-auto bg-gray-100 px-4 py-8 rounded-b-md shadow-md pt-40 sm:pt-28 md:pt-14
dark:text-white dark:bg-neutral-900;
h1 {
@apply text-4xl font-bold mt-4 text-center;
}
h2 {
@apply text-3xl font-bold mt-4;
}
h3 {
@apply text-2xl font-bold mt-4;
}
h4 {
@apply text-xl font-bold mt-4;
}
a {
@apply text-blue-500 hover:text-blue-700;
}
pre.astro-code {
@apply w-fit p-4 rounded-md border-2 border-gray-600;
}
}
</style>

8
src/pages/about.astro Normale Datei
Datei anzeigen

@ -0,0 +1,8 @@
---
import NavbarLayout from "../layouts/NavbarLayout.astro";
---
<NavbarLayout title="About Us">
</NavbarLayout>

8
src/pages/admin/index.astro Normale Datei
Datei anzeigen

@ -0,0 +1,8 @@
---
import App from '../../components/admin/App.svelte'
import Basic from "../../layouts/Basic.astro";
---
<Basic>
<App client:only="svelte" />
</Basic>

Datei anzeigen

@ -4,14 +4,17 @@ import NavbarLayout from "../layouts/NavbarLayout.astro";
import { Image } from "astro:assets";
import localBau from "../images/2022-03-28_13.18.25.png";
import {l, t} from "astro-i18n";
import {CaretRight} from "@astropub/icons"
import {CaretRight, Archive, Rocket, Bell} from "@astropub/icons"
---
<NavbarLayout title="SteamWar.de - Home">
<NavbarLayout title={t("home.page")}>
<div class="w-screen h-screen relative mb-4">
<Image src={localBau} alt="Bau" width="1920" height="1080" class="w-screen object-cover rounded-b-2xl shadow-2xl" style="height: calc(100vh + 1rem)" draggable="false" />
<Image src={localBau} alt="Bau" width="1920" height="1080" class="w-screen object-cover rounded-b-2xl shadow-2xl dark:brightness-75" style="height: calc(100vh + 1rem)" draggable="false" />
<drop-in class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center">
<h1 class="text-2xl sm:text-8xl font-extrabold text-white -translate-y-16 opacity-0 barlow" style="transition: transform .7s ease-out, opacity .7s linear; text-shadow: 2px 2px 5px black">{t("home.title")}</h1>
<h1 class="text-2xl sm:text-8xl font-extrabold text-white -translate-y-16 opacity-0 barlow" style="transition: transform .7s ease-out, opacity .7s linear; text-shadow: 2px 2px 5px black;">
<span class="text-yellow-400">{t("home.title.first")}</span>
<span class="text-neutral-600">{t("home.title.second")}</span>
</h1>
<text-carousel class="h-20 w-full relative select-none">
<h2 class="-translate-y-16">{t("home.subtitle.1")}<player-count /></h2>
<h2>{t("home.subtitle.2")}</h2>
@ -22,12 +25,8 @@ import {CaretRight} from "@astropub/icons"
class TextCarousel extends HTMLElement {
current = 0;
constructor() {
super();
}
connectedCallback() {
this.children[this.current].classList.add("!opacity-100")
this._current.classList.add("!opacity-100")
for (let i = 0; i < this.children.length; i++) {
if (i !== this.current) {
@ -40,32 +39,28 @@ import {CaretRight} from "@astropub/icons"
}, 5000)
}
get _current() {
return this.children[this.current]
}
next() {
this.children[this.current].classList.remove("!opacity-100")
this.children[this.current].classList.add("translate-y-8")
this.children[this.current].classList.remove("!delay-500")
this._current.classList.remove("!opacity-100")
this._current.classList.add("translate-y-8")
this._current.classList.remove("!delay-500")
this.current = (this.current + 1) % this.children.length
this.children[this.current].classList.add("!opacity-100")
this.children[this.current].classList.remove("translate-y-8")
this.children[this.current].classList.add("!delay-500")
this._current.classList.add("!opacity-100")
this._current.classList.remove("translate-y-8")
this._current.classList.add("!delay-500")
}
}
class PlayerCount extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerText = String(Math.floor(Math.random() * 100))
}
}
class DropIn extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
for (let child of this.children) {
if(child.classList.contains("opacity-0")) {
@ -85,15 +80,37 @@ import {CaretRight} from "@astropub/icons"
</script>
</drop-in>
</div>
<h2>HELLO!</h2>
<section class="w-screen flex flex-col items-center justify-center">
<div class="py-10 flex flex-col lg:flex-row">
<div class="card">
<Archive heigth="64" width="64" />
<h1>{t("home.benefits.historic.title")}</h1>
<p>{t("home.benefits.historic.description.1")}</p>
<p>{t("home.benefits.historic.description.2")}</p>
</div>
<div class="card">
<Rocket heigth="64" width="64" />
<h1>{t("home.benefits.server.title")}</h1>
<p>{t("home.benefits.server.description")}</p>
</div>
<div class="card">
<Bell heigth="64" width="64" />
<h1>{t("home.benefits.events.title")}</h1>
<p>{t("home.benefits.events.description.1")}</p>
<p>{t("home.benefits.events.description.2")}</p>
</div>
</div>
<a class="btn px-8 flex" href={l("/about")}>Read More <CaretRight width="24" heigth="24" /></a>
</section>
<section>
</section>
</NavbarLayout>
<style>
@import url("/fonts/barlow-condensed/barlow-condensed.css");
text-carousel{
>* {
@apply absolute top-0 left-0 w-full text-xl sm:text-4xl underline text-white text-center opacity-0;
@apply absolute top-0 left-0 w-full text-xl sm:text-4xl italic text-white text-center opacity-0;
transition: transform .5s ease-out, opacity .5s linear;
text-shadow: 2px 2px 5px black;
}
@ -102,4 +119,15 @@ import {CaretRight} from "@astropub/icons"
.barlow {
font-family: Barlow Condensed;
}
.card {
@apply w-72 border-2 bg-zinc-50 border-gray-100 flex flex-col items-center p-8 m-4 rounded-xl shadow-lg
dark:bg-zinc-900 dark:border-gray-800 dark:text-gray-100;
>h1 {
@apply text-xl font-bold underline mt-4;
}
>p {
@apply mt-4;
}
}
</style>

Datei anzeigen

@ -1,7 +0,0 @@
---
import NavbarLayout from "../layouts/NavbarLayout.astro";---
<NavbarLayout title="Join">
</NavbarLayout>

Datei anzeigen

@ -1,6 +1,6 @@
.btn {
@apply bg-blue-800 text-white font-bold py-2 px-4 rounded cursor-pointer select-none mx-2;
@apply hover:bg-blue-700;
@apply bg-yellow-400 font-bold py-2 px-4 rounded cursor-pointer select-none mx-2 text-black;
@apply hover:bg-yellow-300 hover:text-black;
transition: all 0.5s cubic-bezier(.2,3,.67,.6),
background-color .1s ease-in-out,
outline-width .1s ease-in-out,
@ -12,12 +12,30 @@
}
}
.btn-dropdown {
@apply relative mx-2;
>:nth-child(1) {
@apply block !mx-0;
}
>:nth-child(2) {
@apply hidden absolute top-full left-1/2 -translate-x-1/2 bg-gray-800 list-none text-white rounded py-2 flex-col text-sm;
}
&:hover,&:focus-within {
>:nth-child(2) {
@apply flex;
}
}
}
.btn-gray {
@apply bg-gray-800 text-white font-bold py-2 px-4 rounded cursor-pointer select-none mx-2;
@apply bg-gray-800 text-white;
}
.btn-text {
@apply bg-transparent underline;
@apply bg-transparent underline text-white;
@apply hover:bg-transparent hover:outline hover:outline-1;
.btn__text {

Datei anzeigen

@ -2,10 +2,19 @@ const defaultTheme = require("tailwindcss/defaultTheme");
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
content: [
'./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}',
"./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}"
],
theme: {
extend: {
colors: {
// flowbite-svelte
primary: { 50: '#FFF5F2', 100: '#FFF1EE', 200: '#FFE4DE', 300: '#FFD5CC', 400: '#FFBCAD', 500: '#FE795D', 600: '#EF562F', 700: '#EB4F27', 800: '#CC4522', 900: '#A5371B'},
}
},
},
plugins: [],
plugins: [
require('flowbite/plugin')
],
}

Datei anzeigen

@ -1 +0,0 @@
Hello World