Initial Commit
Dieser Commit ist enthalten in:
Commit
8fac3b8165
24
.gitignore
vendored
Normale Datei
24
.gitignore
vendored
Normale Datei
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
3
.vscode/extensions.json
vendored
Normale Datei
3
.vscode/extensions.json
vendored
Normale Datei
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["svelte.svelte-vscode"]
|
||||
}
|
47
README.md
Normale Datei
47
README.md
Normale Datei
@ -0,0 +1,47 @@
|
||||
# Svelte + TS + Vite
|
||||
|
||||
This template should help get you started developing with Svelte and TypeScript in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
|
||||
|
||||
## Need an official Svelte framework?
|
||||
|
||||
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
|
||||
|
||||
## Technical considerations
|
||||
|
||||
**Why use this over SvelteKit?**
|
||||
|
||||
- It brings its own routing solution which might not be preferable for some users.
|
||||
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
|
||||
|
||||
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
|
||||
|
||||
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
|
||||
|
||||
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
|
||||
|
||||
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
|
||||
|
||||
**Why include `.vscode/extensions.json`?**
|
||||
|
||||
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
|
||||
|
||||
**Why enable `allowJs` in the TS template?**
|
||||
|
||||
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
|
||||
|
||||
**Why is HMR not preserving my local component state?**
|
||||
|
||||
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
|
||||
|
||||
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
|
||||
|
||||
```ts
|
||||
// store.ts
|
||||
// An extremely simple external store
|
||||
import { writable } from 'svelte/store'
|
||||
export default writable(0)
|
||||
```
|
25
index.html
Normale Datei
25
index.html
Normale Datei
@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="SteamWar Event Manager">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="SteamWar Multitool">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
|
||||
<title>SteamWar Eventplanner</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
const getPreferredScheme = () => localStorage.getItem("theme") ?? window?.matchMedia?.('(prefers-color-scheme:dark)')?.matches ? 'dark' : 'light';
|
||||
document.body.classList.add(getPreferredScheme());
|
||||
</script>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
38
package.json
Normale Datei
38
package.json
Normale Datei
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "event-planner",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.4.1",
|
||||
"@tsconfig/svelte": "^4.0.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"flowbite-svelte-icons": "^0.2.5",
|
||||
"postcss": "^8.4.24",
|
||||
"sass": "^1.62.0",
|
||||
"svelte": "^4.0.1",
|
||||
"svelte-check": "^3.4.3",
|
||||
"svelte-preprocess": "^5.0.3",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"tslib": "^2.5.0",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.3.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"color": "^4.2.3",
|
||||
"flowbite": "^1.7.0",
|
||||
"flowbite-svelte": "^0.39.2",
|
||||
"install": "^0.13.0",
|
||||
"moment": "^2.29.4",
|
||||
"svelte-awesome": "^3.2.0",
|
||||
"svelte-spa-router": "^3.3.0",
|
||||
"tailwind-merge": "^1.13.2"
|
||||
}
|
||||
}
|
1464
pnpm-lock.yaml
Normale Datei
1464
pnpm-lock.yaml
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
6
postcss.config.js
Normale Datei
6
postcss.config.js
Normale Datei
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
public/favicon.png
Normale Datei
BIN
public/favicon.png
Normale Datei
Binäre Datei nicht angezeigt.
Nachher Breite: | Höhe: | Größe: 1.4 KiB |
BIN
public/icons/Icon-192.png
Normale Datei
BIN
public/icons/Icon-192.png
Normale Datei
Binäre Datei nicht angezeigt.
Nachher Breite: | Höhe: | Größe: 2.7 KiB |
BIN
public/icons/Icon-512.png
Normale Datei
BIN
public/icons/Icon-512.png
Normale Datei
Binäre Datei nicht angezeigt.
Nachher Breite: | Höhe: | Größe: 342 KiB |
BIN
public/icons/Icon-maskable-192.png
Normale Datei
BIN
public/icons/Icon-maskable-192.png
Normale Datei
Binäre Datei nicht angezeigt.
Nachher Breite: | Höhe: | Größe: 2.8 KiB |
BIN
public/icons/Icon-maskable-512.png
Normale Datei
BIN
public/icons/Icon-maskable-512.png
Normale Datei
Binäre Datei nicht angezeigt.
Nachher Breite: | Höhe: | Größe: 278 KiB |
35
public/manifest.json
Normale Datei
35
public/manifest.json
Normale Datei
@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "SteamWar Multitool",
|
||||
"short_name": "SWMT",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A tool for SteamWar moderators",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icons/Icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/Icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
27
src/App.svelte
Normale Datei
27
src/App.svelte
Normale Datei
@ -0,0 +1,27 @@
|
||||
<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) != ""}),
|
||||
'/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>
|
3
src/app.scss
Normale Datei
3
src/app.scss
Normale Datei
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
13
src/components/ErrorModal.svelte
Normale Datei
13
src/components/ErrorModal.svelte
Normale Datei
@ -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}
|
103
src/components/FightEditPart.svelte
Normale Datei
103
src/components/FightEditPart.svelte
Normale Datei
@ -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 = "";
|
||||
export let redTeam = "";
|
||||
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
|
||||
}
|
||||
}).sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
$: selectableTeams = teams.map(team => {
|
||||
return {
|
||||
name: team.name,
|
||||
value: team.id
|
||||
}
|
||||
}).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>
|
31
src/components/TypeAheadSearch.svelte
Normale Datei
31
src/components/TypeAheadSearch.svelte
Normale Datei
@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import {Dropdown, DropdownItem, Search} from 'flowbite-svelte'
|
||||
|
||||
export let selected: string = ''
|
||||
export let items: {name: string, value: string}[] = []
|
||||
export let all = false
|
||||
|
||||
export let searchValue = items.find(item => item.value === selected)?.name || ''
|
||||
let focus = false
|
||||
|
||||
$: filteredItems = items.filter(item => item.name.toLowerCase().includes(searchValue.toLowerCase()))
|
||||
$: openDropdown = filteredItems.length > 0 && searchValue.length >= 3 && focus || all && focus
|
||||
|
||||
function selectItem(item) {
|
||||
selected = item.value
|
||||
searchValue = item.name
|
||||
focus = false
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<Search bind:value={searchValue} on:focus={() => focus = true} on:keydown={() => focus = true}/>
|
||||
{#if (openDropdown)}
|
||||
<Dropdown class="z-10" open>
|
||||
{#each filteredItems as item}
|
||||
<DropdownItem on:click={() => selectItem(item)}>
|
||||
{item.name}
|
||||
</DropdownItem>
|
||||
{/each}
|
||||
</Dropdown>
|
||||
{/if}
|
8
src/main.ts
Normale Datei
8
src/main.ts
Normale Datei
@ -0,0 +1,8 @@
|
||||
import "./app.scss";
|
||||
import App from "./App.svelte";
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById("app"),
|
||||
});
|
||||
|
||||
export default app;
|
47
src/pages/Event.svelte
Normale Datei
47
src/pages/Event.svelte
Normale Datei
@ -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}
|
42
src/pages/Generate.svelte
Normale Datei
42
src/pages/Generate.svelte
Normale Datei
@ -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}
|
72
src/pages/Home.svelte
Normale Datei
72
src/pages/Home.svelte
Normale Datei
@ -0,0 +1,72 @@
|
||||
<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 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 mx-4" 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) as event}
|
||||
<EventCard {event} />
|
||||
{/each}
|
||||
</div>
|
||||
{:catch error}
|
||||
<p>
|
||||
{error.message}
|
||||
</p>
|
||||
{/await}
|
||||
|
||||
<svelte:head>
|
||||
<title>SteamWar.de Multitool - Home</title>
|
||||
</svelte:head>
|
66
src/pages/Login.svelte
Normale Datei
66
src/pages/Login.svelte
Normale Datei
@ -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>
|
9
src/pages/NotFound.svelte
Normale Datei
9
src/pages/NotFound.svelte
Normale Datei
@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import {onMount} from "svelte";
|
||||
import {replace} from "svelte-spa-router";
|
||||
|
||||
onMount(() => {
|
||||
replace('/')
|
||||
});
|
||||
</script>
|
||||
|
130
src/pages/event/EventEdit.svelte
Normale Datei
130
src/pages/event/EventEdit.svelte
Normale Datei
@ -0,0 +1,130 @@
|
||||
<script lang="ts">
|
||||
import type {ExtendedEvent} from "../../types/event.js";
|
||||
import {Button, Heading, Input, Label, Modal, Range, Select, 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";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async function update() {
|
||||
let ev: UpdateEvent = {
|
||||
deadline: deadlineDate,
|
||||
end: endDate,
|
||||
maxTeamMembers: member,
|
||||
name: name,
|
||||
publicSchemsOnly: publicOnly,
|
||||
schemType: schemType,
|
||||
spectateSystem: spectateSystem,
|
||||
start: startDate
|
||||
};
|
||||
|
||||
try {
|
||||
event = await $eventRepo.updateEvent(event.id.toString(), ev);
|
||||
} 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>
|
95
src/pages/event/FightCard.svelte
Normale Datei
95
src/pages/event/FightCard.svelte
Normale Datei
@ -0,0 +1,95 @@
|
||||
<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} from "svelte";
|
||||
import {fightRepo} from "../../repo/repo.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>{fight.blueTeam.name}</span>
|
||||
vs.
|
||||
<span>{fight.redTeam.name}</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: ' + fight.blueTeam.name : 'Sieger: ' + fight.redTeam.name}</span>
|
||||
{:else}
|
||||
<span class="ml-2">Noch nicht gespielt</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}
|
267
src/pages/event/FightList.svelte
Normale Datei
267
src/pages/event/FightList.svelte
Normale Datei
@ -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>
|
21
src/pages/event/TeamList.svelte
Normale Datei
21
src/pages/event/TeamList.svelte
Normale Datei
@ -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>
|
88
src/pages/event/modals/CreateFightModal.svelte
Normale Datei
88
src/pages/event/modals/CreateFightModal.svelte
Normale Datei
@ -0,0 +1,88 @@
|
||||
<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.js";
|
||||
import ErrorModal from "../../../components/ErrorModal.svelte";
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import moment from "moment";
|
||||
import Number = types.Number;
|
||||
|
||||
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}/>
|
68
src/pages/event/modals/FightEditModal.svelte
Normale Datei
68
src/pages/event/modals/FightEditModal.svelte
Normale Datei
@ -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}/>
|
28
src/pages/generate/DragAcceptor.svelte
Normale Datei
28
src/pages/generate/DragAcceptor.svelte
Normale Datei
@ -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>
|
237
src/pages/generate/GroupGenerator.svelte
Normale Datei
237
src/pages/generate/GroupGenerator.svelte
Normale Datei
@ -0,0 +1,237 @@
|
||||
<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)
|
||||
$: console.log(groupsFights)
|
||||
|
||||
$: 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>
|
||||
|
||||
|
17
src/pages/generate/TeamChip.svelte
Normale Datei
17
src/pages/generate/TeamChip.svelte
Normale Datei
@ -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>
|
||||
|
71
src/pages/home/CreateEventModal.svelte
Normale Datei
71
src/pages/home/CreateEventModal.svelte
Normale Datei
@ -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}/>
|
21
src/pages/home/EventCard.svelte
Normale Datei
21
src/pages/home/EventCard.svelte
Normale Datei
@ -0,0 +1,21 @@
|
||||
<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>
|
||||
<p>Startet: {new Intl.DateTimeFormat().format(event.start)}</p>
|
||||
{#if !sameDate}
|
||||
<p>Endet: {new Intl.DateTimeFormat().format(event.end)}</p>
|
||||
{:else}
|
||||
<p> </p>
|
||||
{/if}
|
||||
</Card>
|
||||
</a>
|
94
src/repo/event.ts
Normale Datei
94
src/repo/event.ts
Normale Datei
@ -0,0 +1,94 @@
|
||||
import type {ExtendedEvent, ShortEvent, SWEvent} from "../types/event.js";
|
||||
import {fetchWithToken} from "./repo.js";
|
||||
import type {Moment} from "moment";
|
||||
|
||||
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 await res.json() as ShortEvent[];
|
||||
} 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 await res.json() as ExtendedEvent;
|
||||
} 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 await res.json() as SWEvent;
|
||||
} 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 await res.json() as SWEvent;
|
||||
} 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;
|
||||
}
|
||||
}
|
90
src/repo/fight.ts
Normale Datei
90
src/repo/fight.ts
Normale Datei
@ -0,0 +1,90 @@
|
||||
import type {EventFight} from "../types/event.js";
|
||||
import {fetchWithToken} from "./repo.js";
|
||||
import type {Moment} from "moment";
|
||||
|
||||
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 await res.json() as EventFight[];
|
||||
} 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 await res.json() as EventFight;
|
||||
} 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 await res.json() as EventFight;
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
13
src/repo/repo.ts
Normale Datei
13
src/repo/repo.ts
Normale Datei
@ -0,0 +1,13 @@
|
||||
import {derived, writable} from "svelte/store";
|
||||
import {EventRepo} from "./event.js";
|
||||
import {FightRepo} from "./fight.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))
|
63
src/stores/cached.ts
Normale Datei
63
src/stores/cached.ts
Normale Datei
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
41
src/stores/stores.ts
Normale Datei
41
src/stores/stores.ts
Normale Datei
@ -0,0 +1,41 @@
|
||||
import type {Player, SchematicType} from "../types/data.js";
|
||||
import {cached, cachedFamily} from "./cached.js";
|
||||
import type {Team} from "../types/team.js";
|
||||
import {get} from "svelte/store";
|
||||
import {tokenStore} from "../repo/repo.js";
|
||||
|
||||
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[]>([], () => {
|
||||
return fetch("https://steamwar.de/eventplanner-api/data/users", {headers: {"X-SW-Auth": get(tokenStore)}})
|
||||
.then(res => res.json())
|
||||
})
|
||||
|
||||
export const gamemodes = cached<string[]>([], () => {
|
||||
return fetch("https://steamwar.de/eventplanner-api/data/gamemodes", {headers: {"X-SW-Auth": get(tokenStore)}})
|
||||
.then(res => res.json())
|
||||
})
|
||||
|
||||
export const maps = cachedFamily<string, string[]>([], (gamemode) => {
|
||||
return fetch(`https://steamwar.de/eventplanner-api/data/gamemodes/${gamemode}/maps`, {headers: {"X-SW-Auth": get(tokenStore)}})
|
||||
.then(res => {
|
||||
if(!res.ok) {
|
||||
return [];
|
||||
} else {
|
||||
return res.json();
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
export const groups = cached<string[]>([], () => {
|
||||
return fetch("https://steamwar.de/eventplanner-api/data/groups", {headers: {"X-SW-Auth": get(tokenStore)}})
|
||||
.then(res => res.json())
|
||||
})
|
||||
|
||||
export const teams = cached<Team[]>([], () => {
|
||||
return fetch("https://steamwar.de/eventplanner-api/team", {headers: {"X-SW-Auth": get(tokenStore)}})
|
||||
.then(res => res.json())
|
||||
})
|
10
src/types/data.ts
Normale Datei
10
src/types/data.ts
Normale Datei
@ -0,0 +1,10 @@
|
||||
export interface SchematicType {
|
||||
name: string;
|
||||
db: string;
|
||||
}
|
||||
|
||||
export interface Player {
|
||||
id: number;
|
||||
name: string;
|
||||
uuid: string;
|
||||
}
|
35
src/types/event.ts
Normale Datei
35
src/types/event.ts
Normale Datei
@ -0,0 +1,35 @@
|
||||
import type {Team} from "./team.js";
|
||||
import type {Player} from "./data.js";
|
||||
|
||||
export interface ShortEvent {
|
||||
id: number;
|
||||
name: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface SWEvent extends ShortEvent {
|
||||
deadline: number;
|
||||
maxTeamMembers: number;
|
||||
schemType: string | null;
|
||||
publicSchemsOnly: boolean;
|
||||
spectateSystem: boolean;
|
||||
}
|
||||
|
||||
export interface EventFight {
|
||||
id: number;
|
||||
spielmodus: string;
|
||||
map: string;
|
||||
blueTeam: Team;
|
||||
redTeam: Team;
|
||||
kampfleiter: Player | null;
|
||||
start: number;
|
||||
ergebnis: number;
|
||||
group: string | null;
|
||||
}
|
||||
|
||||
export interface ExtendedEvent {
|
||||
event: SWEvent;
|
||||
teams: Team[];
|
||||
fights: EventFight[];
|
||||
}
|
8
src/types/team.ts
Normale Datei
8
src/types/team.ts
Normale Datei
@ -0,0 +1,8 @@
|
||||
import Color from "color";
|
||||
|
||||
export interface Team {
|
||||
id: number;
|
||||
name: string;
|
||||
kuerzel: string;
|
||||
color: string;
|
||||
}
|
49
src/util.ts
Normale Datei
49
src/util.ts
Normale Datei
@ -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()
|
||||
}
|
1
src/variables.scss
Normale Datei
1
src/variables.scss
Normale Datei
@ -0,0 +1 @@
|
||||
/* Variables and mixins declared here will be available in all other SCSS files */
|
2
src/vite-env.d.ts
vendored
Normale Datei
2
src/vite-env.d.ts
vendored
Normale Datei
@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
15
svelte.config.js
Normale Datei
15
svelte.config.js
Normale Datei
@ -0,0 +1,15 @@
|
||||
import preprocess from "svelte-preprocess";
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: [
|
||||
vitePreprocess(),
|
||||
preprocess({
|
||||
scss: {
|
||||
prependData: '@use "src/variables.scss" as *;',
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
20
tailwind.config.js
Normale Datei
20
tailwind.config.js
Normale Datei
@ -0,0 +1,20 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
'./src/**/*.{html,js,svelte,ts}',
|
||||
"./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'},
|
||||
}
|
||||
}
|
||||
},
|
||||
darkMode: 'class',
|
||||
plugins: [
|
||||
require('flowbite/plugin')
|
||||
],
|
||||
}
|
||||
|
21
tsconfig.json
Normale Datei
21
tsconfig.json
Normale Datei
@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "nodenext",
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
9
tsconfig.node.json
Normale Datei
9
tsconfig.node.json
Normale Datei
@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
15
vite.config.ts
Normale Datei
15
vite.config.ts
Normale Datei
@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "vite";
|
||||
import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '@use "src/variables.scss" as *;',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren