Compare commits
160 Commits
develop/da
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 446e4bb839 | |||
|
7f41765acb
|
|||
| 0d810f9a7e | |||
| 5d384bc336 | |||
| f95cf6cbfa | |||
|
972b8da9e6
|
|||
| cb41356351 | |||
| 276dc56627 | |||
| 0edec9cdf0 | |||
| 4703fde5a3 | |||
| 954a8cc318 | |||
| 1229edbf51 | |||
| 00bce50a49 | |||
| 5a44f2160c | |||
| 9b65d5d730 | |||
| 8397aace8d | |||
| c2b0bcc54e | |||
| 5c48f0cb85 | |||
| d30cceaad0 | |||
| 41be843be4 | |||
| 3768788f32 | |||
| 7e6f953e44 | |||
| cad3a795a7 | |||
| 48e8165417 | |||
| b11534490d | |||
| c0f4a852b5 | |||
| 54d49cca5b | |||
| 831ea3af11 | |||
| b6a0692c50 | |||
| 01394953d4 | |||
| c515b19e74 | |||
| 98199cc9a0 | |||
| 3f61564067 | |||
| 7b0f18f65d | |||
| 4ac5d2d2b2 | |||
| 8fd3e04116 | |||
| 3180ad1263 | |||
| f689415b98 | |||
| 894d0f8a05 | |||
| 16d377e3e4 | |||
| 1b2a05c204 | |||
| 04969e79c3 | |||
| a949237334 | |||
| 01a59d6de4 | |||
| 3daeb8b62d | |||
| aa72de70ef | |||
| 324025dd57 | |||
| 41b847b3e4 | |||
| a3b4a6d0c2 | |||
| 5f12a0cc7a | |||
| 7166575806 | |||
| 0055e9fb9c | |||
| fc5a209638 | |||
| c7cdc19102 | |||
| c6bbe8c9c8 | |||
| 1cec1b917e | |||
| 13805c7f3f | |||
| da668c574a | |||
| 2aab86573a | |||
| 5d7eb3b8fb | |||
| 6933af1554 | |||
| e607ea1343 | |||
| b0ae4e978e | |||
| 8fe273f3e0 | |||
| 1b48cbe1f4 | |||
| 7276552ed1 | |||
| a2ef92aaad | |||
| 8b85cd0729 | |||
| 2d024cf64b | |||
| 13d76d0a97 | |||
| e65fadb65c | |||
| 6b4693b7f1 | |||
| 92282006fe | |||
| 5457632598 | |||
| bed134f8e0 | |||
| 353a415990 | |||
| 3c6d0f8528 | |||
| 887235dc86 | |||
| a99a066f0d | |||
| e5e3c15b07 | |||
| fb74689c39 | |||
| 18b1f97a84 | |||
| 53b81db2c4 | |||
| 2314b4c5b5 | |||
| 6a81936f77 | |||
| a128de3213 | |||
| 6df661f885 | |||
| a32d84ed86 | |||
| e60cebc9a3 | |||
| 3576d5e034 | |||
| d5c7d8fc27 | |||
| ce895e9297 | |||
| 7c83ad0937 | |||
| 5e0a9d89b3 | |||
| 2a8b98ce5b | |||
| 427818d6bf | |||
| 8424c14ca9 | |||
| 602a7e1453 | |||
| 9f31c5ff0c | |||
| 8a41b98c58 | |||
| 9fc5c500f5 | |||
| bc879d7cad | |||
| 96f0019dc1 | |||
| 7418b608ab | |||
| 3802b9bc26 | |||
| 03effd2fd2 | |||
| a4669a897b | |||
| bd1c4f7f45 | |||
| eac0d5592d | |||
| bd9aea8f35 | |||
| 6e715cee07 | |||
| 4147a1d243 | |||
| 46dba2a6f9 | |||
| 3d8ad3a129 | |||
| 7d50a4db12 | |||
| df389b3acf | |||
| 4ecb5fa024 | |||
| 27f0b962c1 | |||
| e37583329c | |||
| 20b7a32b1b | |||
| dd7d701c48 | |||
| 3173b537bc | |||
| 5e2e4e2281 | |||
| da3699167b | |||
| 10ff84d410 | |||
| 7d75453be5 | |||
| 86bfaf4683 | |||
| f9212649ad | |||
| 4972ebf9bb | |||
| d5a2fc20e8 | |||
| 27c5698ac8 | |||
| fa5f25f37e | |||
| 260b7b24c4 | |||
| 4aea0c7fea | |||
| 314ff3e7c3 | |||
| 0205108d2d | |||
| 2bf3beb044 | |||
| b440456687 | |||
| 5277c9a3fc | |||
| 2f2c1be958 | |||
| 41c7df0d68 | |||
| cedf641039 | |||
| d9bdc636e3 | |||
| c8d05cb268 | |||
| cb2564c9ce | |||
| 80caf8fe6d | |||
| c4f8824115 | |||
| 1da279bb24 | |||
| fd3d621fd5 | |||
| 7d67ad0950 | |||
| 6377799e1b | |||
| b3598e1ee1 | |||
| b9db5be858 | |||
| 3e54934806 | |||
| 98638f94fc | |||
| 4da8fe50c0 | |||
| 7757978668 | |||
| 9eea0b2b3f | |||
| 063638d016 | |||
| f5a778d9b4 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,3 +26,4 @@ pnpm-debug.log*
|
|||||||
/src/env.d.ts
|
/src/env.d.ts
|
||||||
/src/pages/en/
|
/src/pages/en/
|
||||||
/.idea
|
/.idea
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import {defineConfig, sharpImageService} from "astro/config";
|
import { defineConfig, sharpImageService } from "astro/config";
|
||||||
import svelte from "@astrojs/svelte";
|
import svelte from "@astrojs/svelte";
|
||||||
import tailwind from "@astrojs/tailwind";
|
import tailwind from "@astrojs/tailwind";
|
||||||
import configureI18n from "./astro-i18n.adapter";
|
import configureI18n from "./astro-i18n.adapter";
|
||||||
@ -8,6 +8,8 @@ import robotsTxt from "astro-robots-txt";
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import mdx from "@astrojs/mdx";
|
import mdx from "@astrojs/mdx";
|
||||||
|
|
||||||
|
import starlight from "@astrojs/starlight";
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: "static",
|
output: "static",
|
||||||
@ -18,14 +20,40 @@ export default defineConfig({
|
|||||||
site: "https://steamwar.de",
|
site: "https://steamwar.de",
|
||||||
integrations: [
|
integrations: [
|
||||||
svelte(),
|
svelte(),
|
||||||
|
starlight({
|
||||||
|
disable404Route: true,
|
||||||
|
title: "SteamWar Docs",
|
||||||
|
defaultLocale: "de",
|
||||||
|
logo: {
|
||||||
|
src: "./src/images/logo.png",
|
||||||
|
},
|
||||||
|
social: [
|
||||||
|
{ icon: "discord", label: "Discord", href: "https://steamwar.de/discord" },
|
||||||
|
{ icon: "document", label: "Gitea", href: "https://git.steamwar.de" },
|
||||||
|
],
|
||||||
|
sidebar: [
|
||||||
|
{ label: "Startseite", slug: "docs" },
|
||||||
|
{ label: "Bau", badge: "WIP", items: ["docs/bausystem", { label: "Script System", items: ["docs/bausystem/script"] }] },
|
||||||
|
{ label: "Kampfsystem", badge: "WIP", items: ["docs/fightsystem"] },
|
||||||
|
{ label: "Minigames", badge: "WIP", items: ["docs/minigames"] },
|
||||||
|
{ label: "Schematicsystem", badge: "WIP", items: ["docs/schematicsystem"] },
|
||||||
|
{ label: "API", badge: "WIP", items: ["docs/api"] },
|
||||||
|
],
|
||||||
|
editLink: {
|
||||||
|
baseUrl: "https://git.steamwar.de/SteamWar/Website/src/branch/master/",
|
||||||
|
},
|
||||||
|
}),
|
||||||
tailwind({
|
tailwind({
|
||||||
configFile: "./tailwind.config.js",
|
configFile: "./tailwind.config.js",
|
||||||
|
applyBaseStyles: false,
|
||||||
}),
|
}),
|
||||||
configureI18n(),
|
configureI18n(),
|
||||||
sitemap({
|
sitemap({
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: "en", locales: {
|
defaultLocale: "en",
|
||||||
en: "en-US", de: "de-DE",
|
locales: {
|
||||||
|
en: "en-US",
|
||||||
|
de: "de-DE",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@ -49,7 +77,7 @@ export default defineConfig({
|
|||||||
{ userAgent: "omgili", disallow: "/" },
|
{ userAgent: "omgili", disallow: "/" },
|
||||||
{ userAgent: "OmigliBot", disallow: "/" },
|
{ userAgent: "OmigliBot", disallow: "/" },
|
||||||
{ userAgent: "PerplexityBot", disallow: "/" },
|
{ userAgent: "PerplexityBot", disallow: "/" },
|
||||||
{ userAgent: "Timpibot", disallow: "/" }
|
{ userAgent: "Timpibot", disallow: "/" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
mdx(),
|
mdx(),
|
||||||
@ -66,8 +94,8 @@ export default defineConfig({
|
|||||||
"@layouts": path.resolve("./src/layouts"),
|
"@layouts": path.resolve("./src/layouts"),
|
||||||
"@repo": path.resolve("./src/components/repo"),
|
"@repo": path.resolve("./src/components/repo"),
|
||||||
"@stores": path.resolve("./src/components/stores"),
|
"@stores": path.resolve("./src/components/stores"),
|
||||||
"$lib": path.resolve("./src"),
|
$lib: path.resolve("./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
173
package.json
173
package.json
@ -1,82 +1,95 @@
|
|||||||
{
|
{
|
||||||
"name": "steamwar-website",
|
"name": "steamwar-website",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"start": "astro dev",
|
"start": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"i18n:extract": "astro-i18n extract",
|
"i18n:extract": "astro-i18n extract",
|
||||||
"i18n:generate:pages": "astro-i18n generate:pages --purge",
|
"i18n:generate:pages": "astro-i18n generate:pages --purge",
|
||||||
"i18n:generate:types": "astro-i18n generate:types",
|
"i18n:generate:types": "astro-i18n generate:types",
|
||||||
"i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types",
|
"i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types",
|
||||||
"clean:dist": "rm -rf dist",
|
"clean:dist": "rm -rf dist",
|
||||||
"clean:node_modules": "rm -rf node_modules",
|
"clean:node_modules": "rm -rf node_modules",
|
||||||
"ci": "pnpm install && pnpm run i18n:sync && pnpm run build"
|
"ci": "pnpm install && pnpm run i18n:sync && pnpm run build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/svelte": "^7.0.4",
|
"@astrojs/svelte": "^7.1.0",
|
||||||
"@astrojs/tailwind": "^5.1.5",
|
"@astrojs/tailwind": "^5.1.5",
|
||||||
"@astropub/icons": "^0.2.0",
|
"@astropub/icons": "^0.2.0",
|
||||||
"@internationalized/date": "^3.7.0",
|
"@internationalized/date": "^3.8.1",
|
||||||
"@types/color": "^4.2.0",
|
"@lucide/svelte": "^0.488.0",
|
||||||
"@types/node": "^22.9.3",
|
"@types/color": "^4.2.0",
|
||||||
"@types/three": "^0.170.0",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
"@types/node": "^22.15.23",
|
||||||
"@typescript-eslint/parser": "^8.15.0",
|
"@types/three": "^0.170.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
||||||
"bits-ui": "1.3.4",
|
"@typescript-eslint/parser": "^8.33.0",
|
||||||
"clsx": "^2.1.1",
|
"autoprefixer": "^10.4.21",
|
||||||
"cmdk-sv": "^0.0.18",
|
"bits-ui": "1.3.4",
|
||||||
"cssnano": "^7.0.6",
|
"clsx": "^2.1.1",
|
||||||
"embla-carousel-svelte": "^8.5.2",
|
"cmdk-sv": "^0.0.18",
|
||||||
"esbuild": "^0.24.0",
|
"cssnano": "^7.0.7",
|
||||||
"eslint": "^9.15.0",
|
"embla-carousel-svelte": "^8.6.0",
|
||||||
"eslint-plugin-astro": "^1.3.1",
|
"esbuild": "^0.24.2",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint": "^9.27.0",
|
||||||
"eslint-plugin-svelte": "^2.46.0",
|
"eslint-plugin-astro": "^1.3.1",
|
||||||
"formsnap": "1.0.1",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"lucide-svelte": "^0.476.0",
|
"eslint-plugin-svelte": "^2.46.1",
|
||||||
"mode-watcher": "^0.5.1",
|
"formsnap": "1.0.1",
|
||||||
"paneforge": "^0.0.6",
|
"lucide-svelte": "^0.476.0",
|
||||||
"postcss-nesting": "^13.0.1",
|
"mode-watcher": "^0.5.1",
|
||||||
"sass": "^1.81.0",
|
"paneforge": "^0.0.6",
|
||||||
"svelte": "^5.16.0",
|
"postcss-nesting": "^13.0.1",
|
||||||
"svelte-sonner": "^0.3.28",
|
"sass": "^1.89.0",
|
||||||
"tailwind-merge": "^2.5.5",
|
"svelte": "^5.33.4",
|
||||||
"tailwind-variants": "^0.3.1",
|
"svelte-sonner": "^0.3.28",
|
||||||
"tailwindcss": "^3.4.15",
|
"tailwind-merge": "^2.6.0",
|
||||||
"three": "^0.170.0",
|
"tailwind-variants": "^0.3.1",
|
||||||
"typescript": "^5.7.2",
|
"tailwindcss": "^3.4.17",
|
||||||
"vaul-svelte": "^0.3.2",
|
"three": "^0.170.0",
|
||||||
"zod": "^3.23.8"
|
"typescript": "^5.8.3",
|
||||||
},
|
"vaul-svelte": "^0.3.2",
|
||||||
"dependencies": {
|
"zod": "^3.25.31"
|
||||||
"@astrojs/mdx": "^4.0.7",
|
},
|
||||||
"@astrojs/sitemap": "^3.2.1",
|
"dependencies": {
|
||||||
"@codemirror/commands": "^6.8.0",
|
"@astrojs/mdx": "^4.3.0",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@astrojs/sitemap": "^3.4.0",
|
||||||
"@ddietr/codemirror-themes": "^1.4.4",
|
"@astrojs/starlight": "^0.34.4",
|
||||||
"@tanstack/table-core": "^8.21.2",
|
"@astrojs/starlight-tailwind": "^4.0.1",
|
||||||
"astro": "^5.1.8",
|
"@codemirror/commands": "^6.8.1",
|
||||||
"astro-i18n": "^2.2.4",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"astro-robots-txt": "^1.0.0",
|
"@codemirror/view": "^6.36.8",
|
||||||
"astro-seo": "^0.8.4",
|
"@ddietr/codemirror-themes": "^1.5.1",
|
||||||
"chart.js": "^4.4.6",
|
"@tanstack/table-core": "^8.21.3",
|
||||||
"chartjs-adapter-dayjs-4": "^1.0.4",
|
"astro": "5.7.14",
|
||||||
"chartjs-adapter-moment": "^1.0.1",
|
"astro-i18n": "^2.2.4",
|
||||||
"color": "^4.2.3",
|
"astro-robots-txt": "^1.0.0",
|
||||||
"dayjs": "^1.11.13",
|
"astro-seo": "^0.8.4",
|
||||||
"easymde": "^2.18.0",
|
"chart.js": "^4.4.9",
|
||||||
"flowbite": "^2.5.2",
|
"chartjs-adapter-dayjs-4": "^1.0.4",
|
||||||
"flowbite-svelte": "^0.47.3",
|
"chartjs-adapter-moment": "^1.0.1",
|
||||||
"flowbite-svelte-icons": "^2.0.2",
|
"codemirror": "^6.0.1",
|
||||||
"qs": "^6.13.1",
|
"color": "^4.2.3",
|
||||||
"sharp": "^0.33.5",
|
"dayjs": "^1.11.13",
|
||||||
"svelte-awesome": "^3.3.5",
|
"easymde": "^2.20.0",
|
||||||
"svelte-codemirror-editor": "^1.4.1",
|
"flowbite": "^2.5.2",
|
||||||
"svelte-spa-router": "^4.0.1"
|
"flowbite-svelte": "^0.47.4",
|
||||||
}
|
"flowbite-svelte-icons": "^2.2.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"sharp": "^0.33.5",
|
||||||
|
"svelte-awesome": "^3.3.5",
|
||||||
|
"svelte-spa-router": "^4.0.1"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@parcel/watcher",
|
||||||
|
"esbuild",
|
||||||
|
"sharp"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9276
pnpm-lock.yaml
generated
9276
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -18,13 +18,13 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {twMerge} from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import {onMount} from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
let cardElement: HTMLDivElement = $state();
|
let cardElement: HTMLDivElement = $state();
|
||||||
|
|
||||||
function rotateElement(event: MouseEvent) {
|
function rotateElement(event: MouseEvent) {
|
||||||
if(!hoverEffect) return;
|
if (!hoverEffect) return;
|
||||||
|
|
||||||
const x = event.clientX;
|
const x = event.clientX;
|
||||||
const y = event.clientY;
|
const y = event.clientY;
|
||||||
@ -36,23 +36,23 @@
|
|||||||
const rotateX = (centerY - y) / 20;
|
const rotateX = (centerY - y) / 20;
|
||||||
const rotateY = -(centerX - x) / 20;
|
const rotateY = -(centerX - x) / 20;
|
||||||
|
|
||||||
cardElement.style.setProperty('--rotate-x', `${rotateX}deg`);
|
cardElement.style.setProperty("--rotate-x", `${rotateX}deg`);
|
||||||
cardElement.style.setProperty('--rotate-y', `${rotateY}deg`);
|
cardElement.style.setProperty("--rotate-y", `${rotateY}deg`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetElement() {
|
function resetElement() {
|
||||||
cardElement.style.setProperty('--rotate-x', "0");
|
cardElement.style.setProperty("--rotate-x", "0");
|
||||||
cardElement.style.setProperty('--rotate-y', "0");
|
cardElement.style.setProperty("--rotate-y", "0");
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
hoverEffect?: boolean;
|
hoverEffect?: boolean;
|
||||||
extraClasses?: string;
|
extraClasses?: string;
|
||||||
children?: import('svelte').Snippet;
|
children?: import("svelte").Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { hoverEffect = true, extraClasses = '', children }: Props = $props();
|
let { hoverEffect = true, extraClasses = "", children }: Props = $props();
|
||||||
let classes = $derived(twMerge("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", extraClasses))
|
let classes = $derived(twMerge("w-72 border-2 border-gray-100 flex flex-col items-center p-8 m-4 rounded-xl shadow-lg bg-zinc-900 dark:border-gray-800 dark:text-gray-100", extraClasses));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={classes} bind:this={cardElement} onmousemove={rotateElement} onmouseleave={resetElement} class:hoverEffect>
|
<div class={classes} bind:this={cardElement} onmousemove={rotateElement} onmouseleave={resetElement} class:hoverEffect>
|
||||||
@ -61,20 +61,20 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
div {
|
div {
|
||||||
transform: perspective(1000px) rotateX(var(--rotate-x, 0)) rotateY(var(--rotate-y, 0)) !important;
|
transform: perspective(1000px) rotateX(var(--rotate-x, 0)) rotateY(var(--rotate-y, 0)) !important;
|
||||||
|
|
||||||
transition: scale 300ms cubic-bezier(.2, 3, .67, .6);
|
transition: scale 300ms cubic-bezier(0.2, 3, 0.67, 0.6);
|
||||||
|
|
||||||
:global(h1) {
|
:global(h1) {
|
||||||
@apply text-xl font-bold mt-4;
|
@apply text-xl font-bold mt-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(svg) {
|
:global(svg) {
|
||||||
@apply transition-transform duration-300 ease-in-out hover:scale-110 hover:drop-shadow-2xl
|
@apply transition-transform duration-300 ease-in-out hover:scale-110 hover:drop-shadow-2xl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hoverEffect:hover {
|
.hoverEffect:hover {
|
||||||
scale: 105%;
|
scale: 105%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -19,8 +19,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FightStatsChart from "./FightStatsChart.svelte";
|
import FightStatsChart from "./FightStatsChart.svelte";
|
||||||
import {t} from "astro-i18n";
|
import { t } from "astro-i18n";
|
||||||
import {statsRepo} from "@repo/stats.ts";
|
import { statsRepo } from "@repo/stats.ts";
|
||||||
|
|
||||||
let request = getStats();
|
let request = getStats();
|
||||||
|
|
||||||
@ -35,4 +35,4 @@
|
|||||||
<FightStatsChart data={stats} />
|
<FightStatsChart data={stats} />
|
||||||
{:catch error}
|
{:catch error}
|
||||||
<p>error: {error}</p>
|
<p>error: {error}</p>
|
||||||
{/await}
|
{/await}
|
||||||
|
|||||||
@ -19,25 +19,27 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {window} from "./utils.ts";
|
import { window } from "./utils.ts";
|
||||||
import {astroI18n, t} from "astro-i18n";
|
import { astroI18n, t } from "astro-i18n";
|
||||||
import type {EventFight, ExtendedEvent} from "@type/event";
|
import type { EventFight, ExtendedEvent } from "@type/event";
|
||||||
import "@styles/table.css";
|
import "@styles/table.css";
|
||||||
|
|
||||||
export let event: ExtendedEvent;
|
export let event: ExtendedEvent;
|
||||||
export let group: string;
|
export let group: number;
|
||||||
export let rows: number = 1;
|
export let rows: number = 1;
|
||||||
|
|
||||||
function getWinner(fight: EventFight) {
|
function getWinner(fight: EventFight) {
|
||||||
|
if (!fight.hasFinished) {
|
||||||
|
return t("announcements.table.notPlayed");
|
||||||
|
}
|
||||||
|
|
||||||
switch (fight.ergebnis) {
|
switch (fight.ergebnis) {
|
||||||
case 1:
|
case 1:
|
||||||
return fight.blueTeam.kuerzel;
|
return fight.blueTeam.kuerzel;
|
||||||
case 2:
|
case 2:
|
||||||
return fight.redTeam.kuerzel;
|
return fight.redTeam.kuerzel;
|
||||||
case 3:
|
|
||||||
return t("announcements.table.draw");
|
|
||||||
default:
|
default:
|
||||||
return t("announcements.table.notPlayed");
|
return t("announcements.table.draw");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -55,13 +57,15 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each window(event.fights.filter(f => group === undefined ? true : f.group === group), rows) as fights}
|
{#each window( event.fights.filter((f) => (group === undefined ? true : f.group?.id === group)), rows ) as fights}
|
||||||
<tr>
|
<tr>
|
||||||
{#each fights as fight (fight.id)}
|
{#each fights as fight (fight.id)}
|
||||||
<td>{Intl.DateTimeFormat(astroI18n.locale, {
|
<td
|
||||||
hour: "numeric",
|
>{Intl.DateTimeFormat(astroI18n.locale, {
|
||||||
minute: "numeric",
|
hour: "numeric",
|
||||||
}).format(new Date(fight.start))}</td>
|
minute: "numeric",
|
||||||
|
}).format(new Date(fight.start))}</td
|
||||||
|
>
|
||||||
<td class:font-bold={fight.ergebnis === 1} class:italic={fight.ergebnis === 3}>{fight.blueTeam.kuerzel}</td>
|
<td class:font-bold={fight.ergebnis === 1} class:italic={fight.ergebnis === 3}>{fight.blueTeam.kuerzel}</td>
|
||||||
<td class:font-bold={fight.ergebnis === 2} class:italic={fight.ergebnis === 3}>{fight.redTeam.kuerzel}</td>
|
<td class:font-bold={fight.ergebnis === 2} class:italic={fight.ergebnis === 3}>{fight.redTeam.kuerzel}</td>
|
||||||
<td>{getWinner(fight)}</td>
|
<td>{getWinner(fight)}</td>
|
||||||
@ -70,4 +74,4 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -19,33 +19,29 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {window} from "./utils.ts";
|
import { window } from "./utils.ts";
|
||||||
import {t} from "astro-i18n";
|
import { t } from "astro-i18n";
|
||||||
import type {ExtendedEvent} from "@type/event.ts";
|
import type { ExtendedEvent } from "@type/event.ts";
|
||||||
import "@styles/table.css"
|
import "@styles/table.css";
|
||||||
|
|
||||||
export let event: ExtendedEvent;
|
let {
|
||||||
export let group: string;
|
event,
|
||||||
export let rows: number = 1;
|
group,
|
||||||
|
rows = 1,
|
||||||
|
}: {
|
||||||
|
event: ExtendedEvent;
|
||||||
|
group: number;
|
||||||
|
rows?: number;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
$: teamPoints = event.teams.map(team => {
|
let teamPoints = $derived(
|
||||||
const fights = event.fights.filter(fight => fight.blueTeam.id === team.id || fight.redTeam.id === team.id);
|
Object.entries(event.groups.find((g) => g.id === group)?.points ?? {})
|
||||||
const points = fights.reduce((acc, fight) => {
|
.map(([teamId, points]) => ({
|
||||||
if (fight.ergebnis === 1 && fight.blueTeam.id === team.id) {
|
team: event.teams.find((t) => t.id === Number(teamId))!!,
|
||||||
return acc + 3;
|
points: points,
|
||||||
} else if (fight.ergebnis === 2 && fight.redTeam.id === team.id) {
|
}))
|
||||||
return acc + 3;
|
.sort((a, b) => b.points - a.points)
|
||||||
} else if (fight.ergebnis === 3) {
|
);
|
||||||
return acc + 1;
|
|
||||||
} else {
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
return {
|
|
||||||
team,
|
|
||||||
points,
|
|
||||||
};
|
|
||||||
}).sort((a, b) => b.points - a.points);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-3 bg-gray-200 dark:bg-neutral-800 rounded-2xl w-3/4 mx-auto">
|
<div class="p-3 bg-gray-200 dark:bg-neutral-800 rounded-2xl w-3/4 mx-auto">
|
||||||
|
|||||||
@ -19,18 +19,18 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "../styles/button.css";
|
import "../styles/button.css";
|
||||||
import {CaretDownOutline, SearchOutline} from "flowbite-svelte-icons";
|
import { CaretDownOutline, SearchOutline } from "flowbite-svelte-icons";
|
||||||
import {t} from "astro-i18n";
|
import { t } from "astro-i18n";
|
||||||
import {l} from "../util/util";
|
import { l } from "../util/util";
|
||||||
import {onMount} from "svelte";
|
import { onMount } from "svelte";
|
||||||
import {loggedIn} from "@repo/authv2.ts";
|
import { loggedIn } from "@repo/authv2.ts";
|
||||||
interface Props {
|
interface Props {
|
||||||
logo?: import('svelte').Snippet;
|
logo?: import("svelte").Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { logo }: Props = $props();
|
let { logo }: Props = $props();
|
||||||
|
|
||||||
let navbar = $state<HTMLDivElement>();
|
let navbar = $state<HTMLElement>();
|
||||||
let searchOpen = $state(false);
|
let searchOpen = $state(false);
|
||||||
|
|
||||||
let accountBtn = $state<HTMLAnchorElement>();
|
let accountBtn = $state<HTMLAnchorElement>();
|
||||||
@ -41,11 +41,11 @@
|
|||||||
} else {
|
} else {
|
||||||
accountBtn!.href = l("/login");
|
accountBtn!.href = l("/login");
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
handleScroll();
|
handleScroll();
|
||||||
})
|
});
|
||||||
|
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
if (window.scrollY > 0) {
|
if (window.scrollY > 0) {
|
||||||
@ -56,13 +56,17 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onscroll={handleScroll}/>
|
<svelte:window onscroll={handleScroll} />
|
||||||
|
|
||||||
<nav data-pagefind-ignore class="fixed top-0 left-0 right-0 sm:px-4 py-1 transition-colors z-10 flex justify-center before:backdrop-blur before:shadow-2xl before:absolute before:top-0 before:left-0 before:bottom-0 before:right-0 before:-z-10 before:scale-y-0 before:transition-transform before:origin-top" bind:this={navbar}>
|
<nav
|
||||||
|
data-pagefind-ignore
|
||||||
|
class="z-20 fixed top-0 left-0 right-0 sm:px-4 py-1 transition-colors flex justify-center before:backdrop-blur before:shadow-2xl before:absolute before:top-0 before:left-0 before:bottom-0 before:right-0 before:-z-10 before:scale-y-0 before:transition-transform before:origin-top"
|
||||||
|
bind:this={navbar}
|
||||||
|
>
|
||||||
<div class="flex flex-row items-center justify-evenly md:justify-between match">
|
<div class="flex flex-row items-center justify-evenly md:justify-between match">
|
||||||
<a class="flex items-center" href={l("/")}>
|
<a class="flex items-center" href={l("/")}>
|
||||||
{@render logo?.()}
|
{@render logo?.()}
|
||||||
<span class="text-2xl uppercase font-bold dark:text-white hidden md:inline-block">
|
<span class="text-2xl uppercase font-bold text-white hidden md:inline-block">
|
||||||
{t("navbar.title")}
|
{t("navbar.title")}
|
||||||
<span class="before:scale-y-100" style="display: none" aria-hidden="true"></span>
|
<span class="before:scale-y-100" style="display: none" aria-hidden="true"></span>
|
||||||
</span>
|
</span>
|
||||||
@ -73,7 +77,7 @@
|
|||||||
<a href={l("/")}>
|
<a href={l("/")}>
|
||||||
<span class="btn__text">{t("navbar.links.home.title")}</span>
|
<span class="btn__text">{t("navbar.links.home.title")}</span>
|
||||||
</a>
|
</a>
|
||||||
<CaretDownOutline class="ml-2 mt-auto"/>
|
<CaretDownOutline class="ml-2 mt-auto" />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<a class="btn btn-gray" href={l("/announcements")}>{t("navbar.links.home.announcements")}</a>
|
<a class="btn btn-gray" href={l("/announcements")}>{t("navbar.links.home.announcements")}</a>
|
||||||
@ -87,7 +91,7 @@
|
|||||||
<a rel="prefetch" href={l("/rules")}>
|
<a rel="prefetch" href={l("/rules")}>
|
||||||
<span class="btn__text">{t("navbar.links.rules.title")}</span>
|
<span class="btn__text">{t("navbar.links.rules.title")}</span>
|
||||||
</a>
|
</a>
|
||||||
<CaretDownOutline class="ml-2 mt-auto"/>
|
<CaretDownOutline class="ml-2 mt-auto" />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<a href={l("/rules/wargear")} class="btn btn-gray">{t("navbar.links.rules.wg")}</a>
|
<a href={l("/rules/wargear")} class="btn btn-gray">{t("navbar.links.rules.wg")}</a>
|
||||||
@ -96,10 +100,8 @@
|
|||||||
<a href={l("/rules/airship")} class="btn btn-gray">{t("navbar.links.rules.as")}</a>
|
<a href={l("/rules/airship")} class="btn btn-gray">{t("navbar.links.rules.as")}</a>
|
||||||
<a href={l("/rules/quickgear")} class="btn btn-gray">{t("navbar.links.rules.qg")}</a>
|
<a href={l("/rules/quickgear")} class="btn btn-gray">{t("navbar.links.rules.qg")}</a>
|
||||||
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.rotating")}</h2>
|
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.rotating")}</h2>
|
||||||
<a href={l("/rules/megawargear")}
|
<a href={l("/rules/megawargear")} class="btn btn-gray">{t("navbar.links.rules.megawg")}</a>
|
||||||
class="btn btn-gray">{t("navbar.links.rules.megawg")}</a>
|
<a href={l("/rules/microwargear")} class="btn btn-gray">{t("navbar.links.rules.micro")}</a>
|
||||||
<a href={l("/rules/microwargear")}
|
|
||||||
class="btn btn-gray">{t("navbar.links.rules.micro")}</a>
|
|
||||||
<a href={l("/rules/streetfight")} class="btn btn-gray">{t("navbar.links.rules.sf")}</a>
|
<a href={l("/rules/streetfight")} class="btn btn-gray">{t("navbar.links.rules.sf")}</a>
|
||||||
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.ranked")}</h2>
|
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.ranked")}</h2>
|
||||||
<a href={l("/rangliste/missilewars")} class="btn btn-gray">{t("navbar.links.ranked.mw")}</a>
|
<a href={l("/rangliste/missilewars")} class="btn btn-gray">{t("navbar.links.ranked.mw")}</a>
|
||||||
@ -141,4 +143,4 @@
|
|||||||
.match {
|
.match {
|
||||||
width: min(100vw, 70em);
|
width: min(100vw, 70em);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,50 +1,60 @@
|
|||||||
---
|
---
|
||||||
import type {CollectionEntry} from "astro:content";
|
import type { CollectionEntry } from "astro:content";
|
||||||
import {l} from "../util/util";
|
import { l } from "../util/util";
|
||||||
import {astroI18n} from "astro-i18n";
|
import { astroI18n } from "astro-i18n";
|
||||||
import {Image} from "astro:assets";
|
import { Image } from "astro:assets";
|
||||||
import TagComponent from "./TagComponent.astro";
|
import TagComponent from "./TagComponent.astro";
|
||||||
import P from "./P.astro";
|
import P from "./P.astro";
|
||||||
import Card from "@components/Card.svelte";
|
import Card from "@components/Card.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
post: CollectionEntry<"announcements">
|
post: CollectionEntry<"announcements">;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { post, slim }: {
|
const {
|
||||||
post: CollectionEntry<"announcements">,
|
post,
|
||||||
slim: boolean,
|
slim,
|
||||||
|
}: {
|
||||||
|
post: CollectionEntry<"announcements">;
|
||||||
|
slim: boolean;
|
||||||
} = Astro.props as Props;
|
} = Astro.props as Props;
|
||||||
|
|
||||||
const postUrl = l(`/announcements/${post.slug.split("/").slice(1).join("/")}`);
|
const postUrl = l(`/announcements/${post.slug.split("/").slice(1).join("/")}`);
|
||||||
---
|
---
|
||||||
|
|
||||||
<Card extraClasses={`w-full items-start mx-0 ${slim ? "m-0 p-1" : ""}`} hoverEffect={false}>
|
<Card extraClasses={`w-full items-start mx-0 ${slim ? "m-0 p-1 backdrop-blur-xl bg-transparent" : ""}`} hoverEffect={false}>
|
||||||
<div class={`flex flex-row ${slim ? "":"p-4"}`}>
|
<div class={`flex flex-row ${slim ? "" : "p-4"}`}>
|
||||||
{post.data.image != null
|
{
|
||||||
? (
|
post.data.image != null ? (
|
||||||
<a href={postUrl}>
|
<a href={postUrl}>
|
||||||
<div class="flex-shrink-0 pr-2">
|
<div class="flex-shrink-0 pr-2">
|
||||||
<Image transition:name={post.data.title + "-image"} src={post.data.image} alt="Post Image" class="rounded-2xl shadow-2xl object-cover h-32 w-32 max-w-none transition-transform hover:scale-105" />
|
<Image
|
||||||
|
transition:name={post.data.title + "-image"}
|
||||||
|
src={post.data.image}
|
||||||
|
alt="Post Image"
|
||||||
|
class="rounded-2xl shadow-2xl object-cover h-32 w-32 max-w-none transition-transform hover:scale-105"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
)
|
) : null
|
||||||
: null}
|
}
|
||||||
<div>
|
<div>
|
||||||
<a href={postUrl} class="flex flex-col items-start">
|
<a href={postUrl} class="flex flex-col items-start">
|
||||||
<h2 class="text-2xl font-bold" transition:name={post.data.title + "-title"}>{post.data.title}</h2>
|
<h2 class="text-2xl font-bold" transition:name={post.data.title + "-title"}>{post.data.title}</h2>
|
||||||
<P class="text-gray-500">{Intl.DateTimeFormat(astroI18n.locale, {
|
<P class="text-gray-500"
|
||||||
day: "numeric",
|
>{
|
||||||
month: "long",
|
Intl.DateTimeFormat(astroI18n.locale, {
|
||||||
year: "numeric",
|
day: "numeric",
|
||||||
}).format(post.data.created)}</P>
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
}).format(post.data.created)
|
||||||
|
}</P
|
||||||
|
>
|
||||||
<P>{post.data.description}</P>
|
<P>{post.data.description}</P>
|
||||||
</a>
|
</a>
|
||||||
<div class="mt-1" transition:name={post.data.title + "-tags"}>
|
<div class="mt-1" transition:name={post.data.title + "-tags"}>
|
||||||
{post.data.tags.map((tag) => (
|
{post.data.tags.map((tag) => <TagComponent tag={tag} />)}
|
||||||
<TagComponent tag={tag} />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -18,23 +18,22 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Spinner, Toolbar, ToolbarButton, ToolbarGroup} from "flowbite-svelte";
|
import { Spinner, Toolbar, ToolbarButton, ToolbarGroup } from "flowbite-svelte";
|
||||||
import {json} from "@codemirror/lang-json";
|
import { json } from "@codemirror/lang-json";
|
||||||
import CodeMirror from "svelte-codemirror-editor";
|
import { base64ToBytes } from "../../util.ts";
|
||||||
import {base64ToBytes} from "../../util.ts";
|
import type { Page } from "@type/page.ts";
|
||||||
import type {Page} from "@type/page.ts";
|
import { materialDark } from "@ddietr/codemirror-themes/material-dark";
|
||||||
import {materialDark} from "@ddietr/codemirror-themes/material-dark";
|
import { createEventDispatcher } from "svelte";
|
||||||
import {createEventDispatcher} from "svelte";
|
|
||||||
import MDEMarkdownEditor from "./MDEMarkdownEditor.svelte";
|
import MDEMarkdownEditor from "./MDEMarkdownEditor.svelte";
|
||||||
import {pageRepo} from "@repo/page.ts";
|
import { pageRepo } from "@repo/page.ts";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
pageId: number;
|
pageId: number;
|
||||||
branch: string;
|
branch: string;
|
||||||
dirty?: boolean;
|
dirty?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { pageId, branch = $bindable(), dirty = $bindable(false) }: Props = $props();
|
let { pageId, branch = $bindable(), dirty = $bindable(false) }: Props = $props();
|
||||||
|
|
||||||
let dispatcher = createEventDispatcher();
|
let dispatcher = createEventDispatcher();
|
||||||
|
|
||||||
@ -71,35 +70,32 @@
|
|||||||
}
|
}
|
||||||
let pageFuture = $derived($pageRepo.getPage(pageId, branch).then(getPage));
|
let pageFuture = $derived($pageRepo.getPage(pageId, branch).then(getPage));
|
||||||
</script>
|
</script>
|
||||||
<svelte:window onbeforeunload={() => {
|
|
||||||
if (dirty) {
|
<svelte:window
|
||||||
return "You have unsaved changes. Are you sure you want to leave?";
|
onbeforeunload={() => {
|
||||||
}
|
if (dirty) {
|
||||||
}}/>
|
return "You have unsaved changes. Are you sure you want to leave?";
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{#await pageFuture}
|
{#await pageFuture}
|
||||||
<Spinner/>
|
<Spinner />
|
||||||
{:then p}
|
{:then p}
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<Toolbar class="!bg-gray-900">
|
<Toolbar class="!bg-gray-900">
|
||||||
{#snippet end()}
|
{#snippet end()}
|
||||||
<ToolbarGroup >
|
<ToolbarGroup>
|
||||||
<ToolbarButton onclick={deletePage}>
|
<ToolbarButton onclick={deletePage}>Delete</ToolbarButton>
|
||||||
Delete
|
<ToolbarButton color="primary" onclick={savePage}>Save</ToolbarButton>
|
||||||
</ToolbarButton>
|
</ToolbarGroup>
|
||||||
<ToolbarButton color="primary" onclick={savePage}>
|
{/snippet}
|
||||||
Save
|
|
||||||
</ToolbarButton>
|
|
||||||
</ToolbarGroup>
|
|
||||||
{/snippet}
|
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</div>
|
</div>
|
||||||
{#if page?.name.endsWith("md") || page?.name.endsWith("mdx")}
|
{#if page?.name.endsWith("md") || page?.name.endsWith("mdx")}
|
||||||
<MDEMarkdownEditor bind:value={pageContent} bind:dirty/>
|
<MDEMarkdownEditor bind:value={pageContent} bind:dirty />
|
||||||
{:else}
|
{:else}{/if}
|
||||||
<CodeMirror bind:value={pageContent} lang={json()} theme={materialDark} onchange={() => dirty = true}/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{:catch error}
|
{:catch error}
|
||||||
<p>{error.message}</p>
|
<p>{error.message}</p>
|
||||||
{/await}
|
{/await}
|
||||||
|
|||||||
@ -18,12 +18,14 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {createEventDispatcher} from "svelte";
|
import { Card } from "@components/ui/card";
|
||||||
interface Props {
|
|
||||||
children?: import('svelte').Snippet;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { children }: Props = $props();
|
interface Props {
|
||||||
|
children?: import("svelte").Snippet;
|
||||||
|
ondrop: (event: DragEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { children, ondrop }: Props = $props();
|
||||||
|
|
||||||
let dragover = $state(false);
|
let dragover = $state(false);
|
||||||
|
|
||||||
@ -32,19 +34,16 @@
|
|||||||
dragover = true;
|
dragover = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
function handleDrop(ev: DragEvent) {
|
function handleDrop(ev: DragEvent) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
dragover = false;
|
dragover = false;
|
||||||
dispatch("drop", ev);
|
ondrop(ev);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-56 bg-gray-800 p-4 rounded" class:border={dragover} class:m-px={!dragover} ondrop={handleDrop}
|
<Card class="w-56 p-4 rounded m-px {dragover ? 'border-white' : ''}" ondrop={handleDrop} ondragover={handleDragOver} ondragleave={() => (dragover = false)} role="none">
|
||||||
ondragover={handleDragOver} ondragleave={() => dragover = false} role="none">
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</Card>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
|
|||||||
@ -18,28 +18,28 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createBubbler } from 'svelte/legacy';
|
import type { Team } from "@type/team.ts";
|
||||||
|
import { brightness, colorFromTeam, lighten } from "../../util";
|
||||||
|
|
||||||
const bubble = createBubbler();
|
interface Props {
|
||||||
import type {Team} from "@type/team.ts";
|
team: Team;
|
||||||
import {brightness, colorFromTeam, lighten} from "../../util";
|
ondragstart: (event: DragEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
let { team, ondragstart }: Props = $props();
|
||||||
team: Team;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { team }: Props = $props();
|
|
||||||
|
|
||||||
let hover = $state(false);
|
let hover = $state(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="rounded w-fit p-2 border-gray-600 border cursor-grab select-none m-1 flex place-items-center"
|
<div
|
||||||
style:background-color={hover ? lighten(colorFromTeam(team)) : colorFromTeam(team)}
|
class="rounded w-fit p-2 border-gray-600 border cursor-grab select-none m-1 flex place-items-center"
|
||||||
class:text-black={brightness(colorFromTeam(team))} draggable="true"
|
style:background-color={hover ? lighten(colorFromTeam(team)) : colorFromTeam(team)}
|
||||||
ondragstart={bubble('dragstart')}
|
class:text-black={brightness(colorFromTeam(team))}
|
||||||
onmouseenter={() => hover = true}
|
draggable="true"
|
||||||
onmouseleave={() => hover = false}
|
{ondragstart}
|
||||||
role="figure">
|
onmouseenter={() => (hover = true)}
|
||||||
|
onmouseleave={() => (hover = false)}
|
||||||
|
role="figure"
|
||||||
|
>
|
||||||
<span>{team.name}</span>
|
<span>{team.name}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -18,19 +18,19 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {t} from "astro-i18n";
|
import { t } from "astro-i18n";
|
||||||
import type {Player} from "@type/data.ts";
|
import type { Player } from "@type/data.ts";
|
||||||
import {l} from "@utils/util.ts";
|
import { l } from "@utils/util.ts";
|
||||||
import Statistics from "./Statistics.svelte";
|
import Statistics from "./Statistics.svelte";
|
||||||
import {authV2Repo} from "@repo/authv2.ts";
|
import { authV2Repo } from "@repo/authv2.ts";
|
||||||
import Card from "@components/Card.svelte";
|
import Card from "@components/Card.svelte";
|
||||||
import {navigate} from "astro:transitions/client";
|
import { navigate } from "astro:transitions/client";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: Player;
|
user: Player;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { user }: Props = $props();
|
let { user }: Props = $props();
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await $authV2Repo.logout();
|
await $authV2Repo.logout();
|
||||||
@ -43,19 +43,25 @@
|
|||||||
<Card>
|
<Card>
|
||||||
<figure>
|
<figure>
|
||||||
<figcaption class="text-center mb-4 text-2xl">{user.name}</figcaption>
|
<figcaption class="text-center mb-4 text-2xl">{user.name}</figcaption>
|
||||||
<img src={`${import.meta.env.PUBLIC_API_SERVER}/data/skin/${user.uuid}`} class="transition duration-300 ease-in-out hover:scale-110 hover:drop-shadow-2xl" alt={user.name + "s bust"} width="150" height="150" />
|
<img
|
||||||
|
src={`${import.meta.env.PUBLIC_API_SERVER}/data/skin/${user.uuid}`}
|
||||||
|
class="transition duration-300 ease-in-out hover:scale-110 hover:drop-shadow-2xl"
|
||||||
|
alt={user.name + "s bust"}
|
||||||
|
width="150"
|
||||||
|
height="150"
|
||||||
|
/>
|
||||||
</figure>
|
</figure>
|
||||||
</Card>
|
</Card>
|
||||||
<div class="flex flex-wrap">
|
<div class="flex flex-wrap">
|
||||||
<button class="btn mt-2" onclick={logout}>{t("dashboard.buttons.logout")}</button>
|
<button class="btn mt-2" onclick={logout}>{t("dashboard.buttons.logout")}</button>
|
||||||
{#if user.perms.includes("MODERATION")}
|
{#if user.perms.includes("MODERATION")}
|
||||||
<a class="btn w-fit mt-2" href="/admin" data-astro-reload>{t("dashboard.buttons.admin")}</a>
|
<a class="btn w-fit mt-2" href="/admin/new" data-astro-reload>{t("dashboard.buttons.admin")}</a>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-4xl font-bold">{t("dashboard.title", {name: user.name})}</h1>
|
<h1 class="text-4xl font-bold">{t("dashboard.title", { name: user.name })}</h1>
|
||||||
<p>{t("dashboard.rank", {rank: t("home.prefix." + (user.prefix || "User"))})}</p>
|
<p>{t("dashboard.rank", { rank: t("home.prefix." + (user.prefix || "User")) })}</p>
|
||||||
<Statistics {user} />
|
<Statistics {user} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -18,39 +18,36 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {RouteDefinition} from "svelte-spa-router";
|
import type { RouteDefinition } from "svelte-spa-router";
|
||||||
import Router from "svelte-spa-router";
|
import Router from "svelte-spa-router";
|
||||||
import NavLinks from "@components/moderator/layout/NavLinks.svelte";
|
import NavLinks from "@components/moderator/layout/NavLinks.svelte";
|
||||||
import {Switch} from "@components/ui/switch";
|
|
||||||
import {Label} from "@components/ui/label";
|
|
||||||
import {navigate} from "astro:transitions/client";
|
|
||||||
import Players from "@components/moderator/pages/players/Players.svelte";
|
import Players from "@components/moderator/pages/players/Players.svelte";
|
||||||
import Events from "@components/moderator/pages/events/Events.svelte";
|
import Events from "@components/moderator/pages/events/Events.svelte";
|
||||||
import Dashboard from "@components/moderator/pages/dashboard/Dashboard.svelte";
|
import Dashboard from "@components/moderator/pages/dashboard/Dashboard.svelte";
|
||||||
import Event from "@components/moderator/pages/event/Event.svelte";
|
import Event from "@components/moderator/pages/event/Event.svelte";
|
||||||
|
import Pages from "@components/moderator/pages/pages/Pages.svelte";
|
||||||
|
import Generator from "@components/moderator/pages/generators/Generator.svelte";
|
||||||
|
import { Tooltip } from "bits-ui";
|
||||||
|
|
||||||
const routes: RouteDefinition = {
|
const routes: RouteDefinition = {
|
||||||
"/": Dashboard,
|
"/": Dashboard,
|
||||||
"/events": Events,
|
"/events": Events,
|
||||||
"/players": Players,
|
"/players": Players,
|
||||||
"/event/:id": Event
|
"/event/:id": Event,
|
||||||
|
"/event/:id/generate": Generator,
|
||||||
|
"/pages": Pages,
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col bg-background min-w-full min-h-screen">
|
<div class="flex flex-col bg-background min-w-full min-h-screen">
|
||||||
<div class="border-b">
|
<div class="border-b">
|
||||||
<div class="flex h-16 items-center px-4">
|
<div class="flex h-16 items-center px-4">
|
||||||
<a href="/" class="text-sm font-bold transition-colors text-primary">
|
<a href="/" class="text-sm font-bold transition-colors text-primary"> SteamWar </a>
|
||||||
SteamWar
|
|
||||||
</a>
|
|
||||||
<NavLinks />
|
<NavLinks />
|
||||||
<div class="ml-auto flex items-center space-x-4">
|
|
||||||
<Switch id="new-ui-switch" checked={true} on:click={() => navigate("/admin")} />
|
|
||||||
<Label for="new-ui-switch">New UI!</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<main class="flex flex-col">
|
|
||||||
|
<Tooltip.Provider>
|
||||||
<Router {routes} />
|
<Router {routes} />
|
||||||
</main>
|
</Tooltip.Provider>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
170
src/components/moderator/components/FightEdit.svelte
Normal file
170
src/components/moderator/components/FightEdit.svelte
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import GroupSelector from "./GroupSelector.svelte";
|
||||||
|
|
||||||
|
import type { EventFight, EventFightEdit, ResponseGroups, ResponseRelation, SWEvent } from "@type/event";
|
||||||
|
import { fromAbsolute } from "@internationalized/date";
|
||||||
|
import { Label } from "@components/ui/label";
|
||||||
|
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
|
import { gamemodes, maps } from "@components/stores/stores";
|
||||||
|
import { CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, Command } from "@components/ui/command";
|
||||||
|
import { ChevronsUpDown, Check } from "lucide-svelte";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { cn } from "@components/utils";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import { Input } from "@components/ui/input";
|
||||||
|
import TeamSelector from "./TeamSelector.svelte";
|
||||||
|
import type { EventModel } from "../pages/event/eventmodel.svelte";
|
||||||
|
|
||||||
|
let {
|
||||||
|
fight,
|
||||||
|
actions,
|
||||||
|
onSave,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
fight: EventFight | null;
|
||||||
|
actions: Snippet<[boolean, () => void]>;
|
||||||
|
onSave: (fight: EventFightEdit) => void;
|
||||||
|
data: EventModel;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let fightModus = $state(fight?.spielmodus);
|
||||||
|
let fightMap = $state(fight?.map);
|
||||||
|
let fightBlueTeam = $state(fight?.blueTeam);
|
||||||
|
let fightRedTeam = $state(fight?.redTeam);
|
||||||
|
let fightStart = $state(fight?.start ? fromAbsolute(fight.start, "Europe/Berlin") : fromAbsolute(data.event.start, "Europe/Berlin"));
|
||||||
|
let fightErgebnis = $state(fight?.ergebnis ?? 0);
|
||||||
|
let fightSpectatePort = $state(fight?.spectatePort?.toString() ?? null);
|
||||||
|
let fightGroup = $state(fight?.group?.id ?? null);
|
||||||
|
|
||||||
|
let mapsStore = $derived(maps(fightModus ?? "null"));
|
||||||
|
let gamemodeSelectOpen = $state(false);
|
||||||
|
let mapSelectOpen = $state(false);
|
||||||
|
|
||||||
|
let dirty = $derived(
|
||||||
|
fightModus !== fight?.spielmodus ||
|
||||||
|
fightMap !== fight?.map ||
|
||||||
|
fightBlueTeam?.id !== fight?.blueTeam?.id ||
|
||||||
|
fightRedTeam?.id !== fight?.redTeam?.id ||
|
||||||
|
fightStart.toDate().getTime() !== fight?.start ||
|
||||||
|
fightErgebnis !== fight?.ergebnis ||
|
||||||
|
fightSpectatePort !== (fight?.spectatePort?.toString() ?? null) ||
|
||||||
|
fightGroup !== (fight?.group?.id ?? null)
|
||||||
|
);
|
||||||
|
|
||||||
|
let loading = $state(false);
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
await onSave({
|
||||||
|
spielmodus: fightModus!,
|
||||||
|
map: fightMap!,
|
||||||
|
blueTeam: fightBlueTeam!,
|
||||||
|
redTeam: fightRedTeam!,
|
||||||
|
start: fightStart?.toDate().getTime(),
|
||||||
|
ergebnis: fightErgebnis,
|
||||||
|
spectatePort: fightSpectatePort ? +fightSpectatePort : null,
|
||||||
|
group: fightGroup,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Label for="fight-modus">Modus</Label>
|
||||||
|
<Popover bind:open={gamemodeSelectOpen}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="outline" class="justify-between" {...props} role="combobox">
|
||||||
|
{$gamemodes.find((value) => value === fightModus) || fightModus || "Select a modus type..."}
|
||||||
|
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search Fight Modus..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No fight modus found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{#each $gamemodes as modus}
|
||||||
|
<CommandItem
|
||||||
|
value={modus}
|
||||||
|
onSelect={() => {
|
||||||
|
fightModus = modus;
|
||||||
|
gamemodeSelectOpen = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check class={cn("mr-2 size-4", modus !== fightModus && "text-transparent")} />
|
||||||
|
{modus}
|
||||||
|
</CommandItem>
|
||||||
|
{/each}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<Label for="fight-map">Map</Label>
|
||||||
|
<Popover bind:open={mapSelectOpen}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="outline" class="justify-between" {...props} role="combobox">
|
||||||
|
{$mapsStore.find((value) => value === fightMap) || fightMap || "Select a map..."}
|
||||||
|
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search Maps..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No map found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{#each $mapsStore as map}
|
||||||
|
<CommandItem
|
||||||
|
value={map}
|
||||||
|
onSelect={() => {
|
||||||
|
fightMap = map;
|
||||||
|
mapSelectOpen = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check class={cn("mr-2 size-4", map !== fightMap && "text-transparent")} />
|
||||||
|
{map}
|
||||||
|
</CommandItem>
|
||||||
|
{/each}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<Label>Blue Team</Label>
|
||||||
|
<TeamSelector bind:selectedTeam={fightBlueTeam} {data} fightId={fight?.id} team="BLUE" />
|
||||||
|
<Label>Red Team</Label>
|
||||||
|
<TeamSelector bind:selectedTeam={fightRedTeam} {data} fightId={fight?.id} team="RED" />
|
||||||
|
<Label>Start</Label>
|
||||||
|
<DateTimePicker bind:value={fightStart} />
|
||||||
|
{#if fight !== null}
|
||||||
|
<Label for="fight-ergebnis">Ergebnis</Label>
|
||||||
|
<Select type="single" value={fightErgebnis?.toString()} onValueChange={(v) => (fightErgebnis = +v)}>
|
||||||
|
<SelectTrigger>
|
||||||
|
{fightErgebnis === 0 ? "Unentschieden" : (fightErgebnis === 1 ? fightBlueTeam?.name : fightRedTeam?.name) + " gewinnt"}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={"0"}>Unentschieden</SelectItem>
|
||||||
|
<SelectItem value={"1"}>{fightBlueTeam?.name ?? "Team Blau"} gewinnt</SelectItem>
|
||||||
|
<SelectItem value={"2"}>{fightRedTeam?.name ?? "Team Blau"} gewinnt</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Label for="fight-group">Gruppe</Label>
|
||||||
|
<GroupSelector event={data.event} bind:value={fightGroup} bind:groups={data.groups}></GroupSelector>
|
||||||
|
<Label for="spectate-port">Spectate Port</Label>
|
||||||
|
<Input id="spectate-port" bind:value={fightSpectatePort} type="number" placeholder="2001" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{@render actions(dirty && !loading, submit)}
|
||||||
78
src/components/moderator/components/GroupEdit.svelte
Normal file
78
src/components/moderator/components/GroupEdit.svelte
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
import type { ResponseGroups, GroupUpdateEdit } from "@type/event";
|
||||||
|
import { Label } from "@components/ui/label";
|
||||||
|
import { Input } from "@components/ui/input";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||||
|
|
||||||
|
const {
|
||||||
|
group,
|
||||||
|
actions,
|
||||||
|
onSave,
|
||||||
|
}: {
|
||||||
|
group: ResponseGroups | null;
|
||||||
|
actions: Snippet<[boolean, () => void]>;
|
||||||
|
onSave: (groupData: GroupUpdateEdit) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let groupName = $state(group?.name ?? "");
|
||||||
|
let groupType = $state(group?.type ?? "GROUP_STAGE");
|
||||||
|
let pointsPerWin = $state(group?.pointsPerWin ?? 3);
|
||||||
|
let pointsPerLoss = $state(group?.pointsPerLoss ?? 0);
|
||||||
|
let pointsPerDraw = $state(group?.pointsPerDraw ?? 1);
|
||||||
|
|
||||||
|
let canSave = $derived(groupName.length > 0 && (groupType === "GROUP_STAGE" || groupType === "ELIMINATION_STAGE") && pointsPerWin !== null && pointsPerLoss !== null && pointsPerDraw !== null);
|
||||||
|
|
||||||
|
let dirty = $derived(
|
||||||
|
groupName !== (group ? group.name : "") ||
|
||||||
|
groupType !== (group ? group.type : "GROUP_STAGE") ||
|
||||||
|
pointsPerWin !== (group ? group.pointsPerWin : 3) ||
|
||||||
|
pointsPerLoss !== (group ? group.pointsPerLoss : 0) ||
|
||||||
|
pointsPerDraw !== (group ? group.pointsPerDraw : 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
onSave({
|
||||||
|
name: groupName,
|
||||||
|
type: groupType,
|
||||||
|
pointsPerWin: pointsPerWin,
|
||||||
|
pointsPerLoss: pointsPerLoss,
|
||||||
|
pointsPerDraw: pointsPerDraw,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Label for="group-name">Name</Label>
|
||||||
|
<Input id="group-name" bind:value={groupName} placeholder="z.B. Gruppenphase A" />
|
||||||
|
|
||||||
|
<Label for="group-type">Typ</Label>
|
||||||
|
<Select
|
||||||
|
value={groupType}
|
||||||
|
type="single"
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (v) groupType = v as "GROUP_STAGE" | "ELIMINATION_STAGE";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="group-type" placeholder="Wähle einen Gruppentyp">
|
||||||
|
{groupType === "GROUP_STAGE" ? "Gruppenphase" : "Eliminierungsphase"}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="GROUP_STAGE">Gruppenphase</SelectItem>
|
||||||
|
<SelectItem value="ELIMINATION_STAGE">Eliminierungsphase</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{#if groupType === "GROUP_STAGE" && group !== null}
|
||||||
|
<Label for="points-win">Punkte pro Sieg</Label>
|
||||||
|
<Input id="points-win" type="number" bind:value={pointsPerWin} placeholder="3" />
|
||||||
|
|
||||||
|
<Label for="points-loss">Punkte pro Niederlage</Label>
|
||||||
|
<Input id="points-loss" type="number" bind:value={pointsPerLoss} placeholder="0" />
|
||||||
|
|
||||||
|
<Label for="points-draw">Punkte pro Unentschieden</Label>
|
||||||
|
<Input id="points-draw" type="number" bind:value={pointsPerDraw} placeholder="1" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{@render actions(group === null ? canSave : dirty, submit)}
|
||||||
103
src/components/moderator/components/GroupSelector.svelte
Normal file
103
src/components/moderator/components/GroupSelector.svelte
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { GroupUpdateEdit, ResponseGroups, SWEvent } from "@type/event";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
|
import { CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, Command } from "@components/ui/command";
|
||||||
|
import { ChevronsUpDownIcon, PlusIcon, CheckIcon, MinusIcon } from "lucide-svelte";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { cn } from "@components/utils";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@components/ui/dialog";
|
||||||
|
import GroupEdit from "./GroupEdit.svelte";
|
||||||
|
import { eventRepo } from "@components/repo/event";
|
||||||
|
|
||||||
|
let {
|
||||||
|
event,
|
||||||
|
groups = $bindable(),
|
||||||
|
value = $bindable(),
|
||||||
|
}: {
|
||||||
|
event: SWEvent;
|
||||||
|
groups: ResponseGroups[];
|
||||||
|
value: number | null;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let selectedGroup = $derived(groups.find((group) => group.id === value));
|
||||||
|
|
||||||
|
let createOpen = $state(false);
|
||||||
|
let groupSelectOpen = $state(false);
|
||||||
|
|
||||||
|
async function handleGroupSave(group: GroupUpdateEdit) {
|
||||||
|
let g = await $eventRepo.createGroup(event.id, group);
|
||||||
|
groups.push(g);
|
||||||
|
value = g.id;
|
||||||
|
createOpen = false;
|
||||||
|
groupSelectOpen = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog bind:open={createOpen}>
|
||||||
|
<Popover bind:open={groupSelectOpen}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button id="fight-group" variant="outline" class="justify-between" {...props} role="combobox">
|
||||||
|
{selectedGroup?.name || "Keine Gruppe"}
|
||||||
|
<ChevronsUpDownIcon class="ml-2 size-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Gruppe suchen..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem value={"new"} onSelect={() => (createOpen = true)}>
|
||||||
|
<PlusIcon class={"mr-2 size-4"} />
|
||||||
|
Neue Gruppe
|
||||||
|
</CommandItem>
|
||||||
|
|
||||||
|
<CommandGroup heading="Gruppen">
|
||||||
|
<CommandItem
|
||||||
|
value={"none"}
|
||||||
|
onSelect={() => {
|
||||||
|
value = null;
|
||||||
|
groupSelectOpen = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if value === null}
|
||||||
|
<CheckIcon class={"mr-2 size-4"} />
|
||||||
|
{:else}
|
||||||
|
<MinusIcon class={"mr-2 size-4"} />
|
||||||
|
{/if}
|
||||||
|
Keine Gruppe
|
||||||
|
</CommandItem>
|
||||||
|
|
||||||
|
{#each groups as group}
|
||||||
|
<CommandItem
|
||||||
|
value={group.id.toString()}
|
||||||
|
onSelect={() => {
|
||||||
|
value = group.id;
|
||||||
|
groupSelectOpen = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon class={cn("mr-2 size-4", value !== group.id && "text-transparent")} />
|
||||||
|
{group.name}
|
||||||
|
</CommandItem>
|
||||||
|
{/each}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Neue Gruppe erstellen</DialogTitle>
|
||||||
|
<DialogDescription>Hier kannst du eine neue Gruppe erstellen</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<GroupEdit group={null} onSave={handleGroupSave}>
|
||||||
|
{#snippet actions(dirty, submit)}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button disabled={!dirty} onclick={submit}>Speichern</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
{/snippet}
|
||||||
|
</GroupEdit>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
253
src/components/moderator/components/TeamSelector.svelte
Normal file
253
src/components/moderator/components/TeamSelector.svelte
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ResponseRelation } from "@components/types/event";
|
||||||
|
import type { Team } from "@components/types/team";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/ui/tabs";
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@components/ui/tooltip";
|
||||||
|
import { cn } from "@components/utils";
|
||||||
|
import { Check, ChevronsUpDown, GitPullRequestArrow, Plus } from "lucide-svelte";
|
||||||
|
import type { EventModel } from "../pages/event/eventmodel.svelte";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||||
|
import { Label } from "@components/ui/label";
|
||||||
|
import { eventRepo } from "@components/repo/event";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selectedTeam: Team | undefined;
|
||||||
|
open?: boolean;
|
||||||
|
team: "BLUE" | "RED";
|
||||||
|
data: EventModel;
|
||||||
|
fightId?: number;
|
||||||
|
onSelect?: (team: Team) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { selectedTeam = $bindable(), data, team, open = $bindable(false), fightId, onSelect }: Props = $props();
|
||||||
|
|
||||||
|
const currentRelation = $derived(data.relations.find((r) => r.fight === fightId && r.team === team));
|
||||||
|
|
||||||
|
let fromType = $state<"FIGHT" | "GROUP">(currentRelation?.type ?? "FIGHT");
|
||||||
|
|
||||||
|
let fromFight = $state<string | undefined>(currentRelation?.fromFight?.id?.toString());
|
||||||
|
|
||||||
|
let fromFightData = $derived(data.fights.find((f) => f.id.toString() === fromFight));
|
||||||
|
|
||||||
|
let fromGroup = $state<string | undefined>(currentRelation?.fromGroup?.id?.toString());
|
||||||
|
|
||||||
|
let fromGroupData = $derived(data.groups.find((g) => g.id.toString() === fromGroup));
|
||||||
|
|
||||||
|
let fromPlace = $state<string | undefined>(currentRelation?.fromPlace?.toString());
|
||||||
|
|
||||||
|
let relationOpen = $state(false);
|
||||||
|
|
||||||
|
async function saveRelation() {
|
||||||
|
relationOpen = false;
|
||||||
|
if (currentRelation === undefined) {
|
||||||
|
await $eventRepo.createRelation(data.event.id, {
|
||||||
|
fightId: fightId!,
|
||||||
|
team,
|
||||||
|
fromType,
|
||||||
|
fromId: fromType === "FIGHT" ? parseInt(fromFight!) : parseInt(fromGroup!),
|
||||||
|
fromPlace: parseInt(fromPlace!),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await $eventRepo.updateRelation(data.event.id, currentRelation.id, {
|
||||||
|
from: {
|
||||||
|
fromType,
|
||||||
|
fromId: fromType === "FIGHT" ? parseInt(fromFight!) : parseInt(fromGroup!),
|
||||||
|
fromPlace: parseInt(fromPlace!),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
data.relations = await $eventRepo.listRelations(data.event.id);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearRelation() {
|
||||||
|
relationOpen = false;
|
||||||
|
if (currentRelation !== undefined) {
|
||||||
|
await $eventRepo.deleteRelation(data.event.id, currentRelation.id);
|
||||||
|
data.relations = await $eventRepo.listRelations(data.event.id);
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
fromType = currentRelation?.type ?? "FIGHT";
|
||||||
|
fromFight = currentRelation?.fromFight?.id.toString();
|
||||||
|
fromGroup = currentRelation?.fromGroup?.id.toString();
|
||||||
|
fromPlace = currentRelation?.fromPlace.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
let canSave = $derived(
|
||||||
|
(fromType !== currentRelation?.type ||
|
||||||
|
fromFight !== (currentRelation?.fromFight?.id.toString() ?? "") ||
|
||||||
|
fromGroup !== (currentRelation?.fromGroup?.id.toString() ?? "") ||
|
||||||
|
fromPlace !== (currentRelation?.fromPlace.toString() ?? "")) &&
|
||||||
|
((fromType === "FIGHT" && fromFight !== "" && fromPlace !== "") || (fromType === "GROUP" && fromGroup !== "" && fromPlace !== ""))
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Popover bind:open>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="outline" class="justify-between flex-1" {...props} role="combobox">
|
||||||
|
{#if selectedTeam?.id === -1}
|
||||||
|
???
|
||||||
|
{:else if selectedTeam?.id === 0}
|
||||||
|
PUB
|
||||||
|
{:else}
|
||||||
|
{data.teams.find((v) => v.id === selectedTeam?.id)?.name || selectedTeam?.name || "Select a team..."}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if currentRelation !== undefined}
|
||||||
|
({#if currentRelation.type === "FIGHT"}
|
||||||
|
{currentRelation.fromPlace === 0 ? "Gewinner" : "Verlierer"} von {currentRelation.fromFight?.blueTeam.name} vs {currentRelation.fromFight?.redTeam.name} ({new Date(
|
||||||
|
currentRelation.fromFight?.start ?? 0
|
||||||
|
).toLocaleTimeString("de-DE", {
|
||||||
|
timeStyle: "short",
|
||||||
|
})})
|
||||||
|
{:else}
|
||||||
|
{currentRelation.fromPlace + 1}. Platz von {currentRelation.fromGroup?.name}
|
||||||
|
{/if})
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search Teams..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No team found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
<CommandItem
|
||||||
|
value={"-1"}
|
||||||
|
onSelect={() => {
|
||||||
|
selectedTeam = {
|
||||||
|
id: -1,
|
||||||
|
name: "?",
|
||||||
|
color: "7",
|
||||||
|
kuerzel: "?",
|
||||||
|
};
|
||||||
|
onSelect?.(selectedTeam);
|
||||||
|
open = false;
|
||||||
|
}}
|
||||||
|
keywords={["?"]}>???</CommandItem
|
||||||
|
>
|
||||||
|
<CommandItem
|
||||||
|
value={"0"}
|
||||||
|
onSelect={() => {
|
||||||
|
selectedTeam = {
|
||||||
|
id: 0,
|
||||||
|
name: "Public",
|
||||||
|
color: "7",
|
||||||
|
kuerzel: "PUB",
|
||||||
|
};
|
||||||
|
onSelect?.(selectedTeam);
|
||||||
|
open = false;
|
||||||
|
}}
|
||||||
|
keywords={["PUB", "Public"]}>PUB</CommandItem
|
||||||
|
>
|
||||||
|
</CommandGroup>
|
||||||
|
<CommandGroup heading="Teams">
|
||||||
|
{#each data.teams as team}
|
||||||
|
<CommandItem
|
||||||
|
value={team.name}
|
||||||
|
onSelect={() => {
|
||||||
|
selectedTeam = team;
|
||||||
|
onSelect?.(selectedTeam);
|
||||||
|
open = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check class={cn("mr-2 size-4", team.id !== selectedTeam?.id && "text-transparent")} />
|
||||||
|
{team.name}
|
||||||
|
</CommandItem>
|
||||||
|
{/each}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<Popover bind:open={relationOpen}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button {...props} size="icon" variant={currentRelation !== undefined ? "default" : "outline"} disabled={fightId === undefined}>
|
||||||
|
<GitPullRequestArrow />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Kampfverbindung</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
{/snippet}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<Tabs bind:value={fromType}>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="FIGHT">Kampf</TabsTrigger>
|
||||||
|
<TabsTrigger value="GROUP">Gruppe</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="FIGHT">
|
||||||
|
<Label>Kampf</Label>
|
||||||
|
<Select bind:value={fromFight} type="single" disabled={data.fights.length === 0}>
|
||||||
|
<SelectTrigger>
|
||||||
|
{fromFightData
|
||||||
|
? `${new Date(fromFightData.start).toLocaleString("de-DE", { timeStyle: "short" })}: ${fromFightData.blueTeam.kuerzel} vs. ${fromFightData.redTeam.kuerzel}`
|
||||||
|
: "Kampf auswählen..."}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{#each data.fights.filter((v) => v.id !== fightId) as fight (fight.id)}
|
||||||
|
<SelectItem value={fight.id.toString()}
|
||||||
|
>{new Date(fight.start).toLocaleString("de-DE", {
|
||||||
|
timeStyle: "short",
|
||||||
|
})}: {fight.blueTeam.kuerzel} vs. {fight.redTeam.kuerzel}</SelectItem
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Label>Team</Label>
|
||||||
|
<Select bind:value={fromPlace} type="single" disabled={data.fights.length === 0}>
|
||||||
|
<SelectTrigger>
|
||||||
|
{fromPlace ? (fromPlace === "0" ? "Gewinner" : "Verlierer") : "Platz auswählen..."}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value={"0"}>Gewinner</SelectItem>
|
||||||
|
<SelectItem value={"1"}>Verlierer</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="GROUP">
|
||||||
|
<Label>Gruppe</Label>
|
||||||
|
<Select bind:value={fromGroup} type="single" disabled={data.groups.length === 0}>
|
||||||
|
<SelectTrigger>
|
||||||
|
{fromGroupData ? fromGroupData.name : "Kampf auswählen..."}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{#each data.groups as group (group.id)}
|
||||||
|
<SelectItem value={group.id.toString()}>{group.name}</SelectItem>
|
||||||
|
{/each}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Label>Platz</Label>
|
||||||
|
<Select bind:value={fromPlace} type="single" disabled={data.fights.length === 0}>
|
||||||
|
<SelectTrigger>
|
||||||
|
{fromPlace ? `${parseInt(fromPlace) + 1}. Platz` : "Platz auswählen..."}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{#each Array(32) as _, i}
|
||||||
|
<SelectItem value={i.toString()}>{i + 1}. Platz</SelectItem>
|
||||||
|
{/each}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
<div class="flex justify-end gap-2 mt-2">
|
||||||
|
<Button onclick={clearRelation} variant="destructive">Löschen</Button>
|
||||||
|
<Button onclick={saveRelation} disabled={!canSave}>Übernehmen</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
@ -18,23 +18,13 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {location} from "svelte-spa-router";
|
import { location } from "svelte-spa-router";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="flex items-center space-x-4 lg:space-x-6 mx-6">
|
<nav class="flex items-center space-x-4 lg:space-x-6 mx-6">
|
||||||
<a href="#/" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/"}>
|
<a href="#/" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/"}> Dashboard </a>
|
||||||
Dashboard
|
<a href="#/events" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={!$location.startsWith("/event")}> Events </a>
|
||||||
</a>
|
<a href="#/players" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/players"}> Players </a>
|
||||||
<a href="#/events" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/events"}>
|
<a href="#/pages" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/pages"}> Pages </a>
|
||||||
Events
|
<a href="#/schematics" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/schematics"}> Schematics </a>
|
||||||
</a>
|
</nav>
|
||||||
<a href="#/players" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/players"}>
|
|
||||||
Players
|
|
||||||
</a>
|
|
||||||
<a href="#/pages" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/pages"}>
|
|
||||||
Pages
|
|
||||||
</a>
|
|
||||||
<a href="#/schematics" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/schematics"}>
|
|
||||||
Schematics
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
|
|||||||
@ -18,8 +18,11 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {eventRepo} from "@repo/event.ts";
|
import { eventRepo } from "@repo/event.ts";
|
||||||
import EventView from "@components/moderator/pages/event/EventView.svelte";
|
import EventView from "@components/moderator/pages/event/EventView.svelte";
|
||||||
|
import type { ExtendedEvent } from "@components/types/event";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { EventModel } from "./eventmodel.svelte";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
params: { id: number };
|
params: { id: number };
|
||||||
@ -28,11 +31,21 @@
|
|||||||
let { params }: Props = $props();
|
let { params }: Props = $props();
|
||||||
|
|
||||||
let id = params.id;
|
let id = params.id;
|
||||||
let event = $eventRepo.getEvent(id.toString());
|
let data: EventModel | undefined = $state(undefined);
|
||||||
|
let loaded = $state(false);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
data = new EventModel(await $eventRepo.getEvent(id.toString()));
|
||||||
|
loaded = true;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await event}
|
{#if loaded}
|
||||||
|
<EventView bind:event={data!!} {refresh} />
|
||||||
|
{:else}
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
{:then data}
|
{/if}
|
||||||
<EventView event={data} />
|
|
||||||
{/await}
|
|
||||||
|
|||||||
@ -18,24 +18,35 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Input} from "@components/ui/input";
|
import { Input } from "@components/ui/input";
|
||||||
import {Label} from "@components/ui/label";
|
import { Label } from "@components/ui/label";
|
||||||
import {Popover, PopoverContent, PopoverTrigger} from "@components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
import type {SWEvent} from "@type/event.ts"
|
import type { SWEvent } from "@type/event.ts";
|
||||||
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||||
import {fromAbsolute} from "@internationalized/date";
|
import { fromAbsolute } from "@internationalized/date";
|
||||||
import {Button} from "@components/ui/button";
|
import { Button, buttonVariants } from "@components/ui/button";
|
||||||
import {ChevronsUpDown} from "lucide-svelte";
|
import { ChevronsUpDown } from "lucide-svelte";
|
||||||
import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList} from "@components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||||
import {schemTypes} from "@stores/stores.ts";
|
import { schemTypes } from "@stores/stores.ts";
|
||||||
import Check from "lucide-svelte/icons/check";
|
import Check from "lucide-svelte/icons/check";
|
||||||
import {cn} from "@components/utils.ts";
|
import { cn } from "@components/utils.ts";
|
||||||
import {Switch} from "@components/ui/switch";
|
import { Switch } from "@components/ui/switch";
|
||||||
import {eventRepo} from "@repo/event.ts";
|
import { eventRepo } from "@repo/event.ts";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@components/ui/alert-dialog";
|
||||||
|
|
||||||
const { event }: { event: SWEvent } = $props();
|
const { event }: { event: SWEvent } = $props();
|
||||||
|
|
||||||
let rootEvent: SWEvent = $state(event)
|
let rootEvent: SWEvent = $state(event);
|
||||||
|
|
||||||
let eventName = $state(rootEvent.name);
|
let eventName = $state(rootEvent.name);
|
||||||
let eventDeadline = $state(fromAbsolute(rootEvent.deadline, "Europe/Berlin"));
|
let eventDeadline = $state(fromAbsolute(rootEvent.deadline, "Europe/Berlin"));
|
||||||
@ -45,13 +56,15 @@
|
|||||||
let eventSchematicType = $state(rootEvent.schemType);
|
let eventSchematicType = $state(rootEvent.schemType);
|
||||||
let eventPublicsOnly = $state(rootEvent.publicSchemsOnly);
|
let eventPublicsOnly = $state(rootEvent.publicSchemsOnly);
|
||||||
|
|
||||||
let dirty = $derived(eventName !== rootEvent.name ||
|
let dirty = $derived(
|
||||||
eventDeadline.toDate().getTime() !== rootEvent.deadline ||
|
eventName !== rootEvent.name ||
|
||||||
eventStart.toDate().getTime() !== rootEvent.start ||
|
eventDeadline.toDate().getTime() !== rootEvent.deadline ||
|
||||||
eventEnd.toDate().getTime() !== rootEvent.end ||
|
eventStart.toDate().getTime() !== rootEvent.start ||
|
||||||
eventTeamSize !== rootEvent.maxTeamMembers ||
|
eventEnd.toDate().getTime() !== rootEvent.end ||
|
||||||
eventSchematicType !== rootEvent.schemType ||
|
eventTeamSize !== rootEvent.maxTeamMembers ||
|
||||||
eventPublicsOnly !== rootEvent.publicSchemsOnly);
|
eventSchematicType !== rootEvent.schemType ||
|
||||||
|
eventPublicsOnly !== rootEvent.publicSchemsOnly
|
||||||
|
);
|
||||||
|
|
||||||
async function updateEvent() {
|
async function updateEvent() {
|
||||||
rootEvent = await $eventRepo.updateEvent(event.id.toString(), {
|
rootEvent = await $eventRepo.updateEvent(event.id.toString(), {
|
||||||
@ -62,7 +75,7 @@
|
|||||||
maxTeamMembers: eventTeamSize,
|
maxTeamMembers: eventTeamSize,
|
||||||
schemType: eventSchematicType,
|
schemType: eventSchematicType,
|
||||||
publicSchemsOnly: eventPublicsOnly,
|
publicSchemsOnly: eventPublicsOnly,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -81,13 +94,8 @@
|
|||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<Button
|
<Button variant="outline" class="justify-between" {...props} role="combobox">
|
||||||
variant="outline"
|
{$schemTypes.find((value) => value.db === eventSchematicType)?.name || eventSchematicType || "Select a schematic type..."}
|
||||||
class="justify-between"
|
|
||||||
{...props}
|
|
||||||
role="combobox"
|
|
||||||
>
|
|
||||||
{$schemTypes.find(value => value.db === eventSchematicType)?.name || eventSchematicType || "Select a schematic type..."}
|
|
||||||
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@ -98,19 +106,23 @@
|
|||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No schematic type found.</CommandEmpty>
|
<CommandEmpty>No schematic type found.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
|
<CommandItem
|
||||||
|
value={"null"}
|
||||||
|
onSelect={() => {
|
||||||
|
eventSchematicType = null;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check class={cn("mr-2 size-4", eventSchematicType !== null && "text-transparent")} />
|
||||||
|
Keinen
|
||||||
|
</CommandItem>
|
||||||
{#each $schemTypes as type}
|
{#each $schemTypes as type}
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={type.db}
|
value={type.db}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
eventSchematicType = type.db;
|
eventSchematicType = type.db;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Check
|
<Check class={cn("mr-2 size-4", eventSchematicType !== type.db && "text-transparent")} />
|
||||||
class={cn(
|
|
||||||
"mr-2 size-4",
|
|
||||||
eventSchematicType !== type.db && "text-transparent"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{type.name}
|
{type.name}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
{/each}
|
{/each}
|
||||||
@ -122,7 +134,19 @@
|
|||||||
<Label for="event-publics">Publics Schematics Only</Label>
|
<Label for="event-publics">Publics Schematics Only</Label>
|
||||||
<Switch id="event-publics" bind:checked={eventPublicsOnly} />
|
<Switch id="event-publics" bind:checked={eventPublicsOnly} />
|
||||||
<div class="flex flex-row justify-end border-t pt-2 gap-4">
|
<div class="flex flex-row justify-end border-t pt-2 gap-4">
|
||||||
<Button variant="destructive">Delete</Button>
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger class={buttonVariants({ variant: "destructive" })}>Delete</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction disabled>Delete</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
<Button disabled={!dirty} onclick={updateEvent}>Update</Button>
|
<Button disabled={!dirty} onclick={updateEvent}>Update</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -18,26 +18,41 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {ExtendedEvent} from "@type/event";
|
import FightEditRow from "./FightEditRow.svelte";
|
||||||
import {createSvelteTable, FlexRender} from "@components/ui/data-table";
|
|
||||||
import {
|
|
||||||
type ColumnFiltersState,
|
|
||||||
getCoreRowModel, getFilteredRowModel,
|
|
||||||
getPaginationRowModel, getSortedRowModel,
|
|
||||||
type SortingState,
|
|
||||||
} from "@tanstack/table-core";
|
|
||||||
import { columns } from "./columns"
|
|
||||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table";
|
|
||||||
|
|
||||||
let { data }: { data: ExtendedEvent } = $props();
|
import type { EventFightEdit } from "@type/event";
|
||||||
|
import { createSvelteTable, FlexRender } from "@components/ui/data-table";
|
||||||
|
import { type ColumnFiltersState, getCoreRowModel, getFilteredRowModel, getGroupedRowModel, getSortedRowModel, type RowSelectionState, type SortingState } from "@tanstack/table-core";
|
||||||
|
import { columns } from "./columns";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@components/ui/table";
|
||||||
|
import { Checkbox } from "@components/ui/checkbox";
|
||||||
|
import { Menubar, MenubarContent, MenubarItem, MenubarGroup, MenubarGroupHeading, MenubarMenu, MenubarTrigger, MenubarSub, MenubarSubTrigger, MenubarSubContent } from "@components/ui/menubar";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@components/ui/dialog";
|
||||||
|
import FightEdit from "@components/moderator/components/FightEdit.svelte";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { eventRepo } from "@components/repo/event";
|
||||||
|
import GroupEditDialog from "./GroupEditDialog.svelte";
|
||||||
|
import GroupResultsDialog from "./GroupResultsDialog.svelte";
|
||||||
|
import type { ResponseGroups } from "@type/event";
|
||||||
|
import { EditIcon, GroupIcon, LinkIcon } from "lucide-svelte";
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@components/ui/dropdown-menu";
|
||||||
|
import GroupSelector from "@components/moderator/components/GroupSelector.svelte";
|
||||||
|
import { fightRepo } from "@components/repo/fight";
|
||||||
|
import type { EventModel } from "./eventmodel.svelte";
|
||||||
|
|
||||||
|
let { data = $bindable(), refresh }: { data: EventModel; refresh: () => void } = $props();
|
||||||
|
|
||||||
let sorting = $state<SortingState>([]);
|
let sorting = $state<SortingState>([]);
|
||||||
let columnFilters = $state<ColumnFiltersState>([]);
|
let columnFilters = $state<ColumnFiltersState>([]);
|
||||||
|
let selection = $state<RowSelectionState>({});
|
||||||
|
|
||||||
const table = createSvelteTable({
|
const table = createSvelteTable({
|
||||||
get data() {
|
get data() {
|
||||||
return data.fights;
|
return data.fights;
|
||||||
},
|
},
|
||||||
|
initialState: {
|
||||||
|
columnOrder: ["auswahl", "begegnung", "group"],
|
||||||
|
},
|
||||||
state: {
|
state: {
|
||||||
get sorting() {
|
get sorting() {
|
||||||
return sorting;
|
return sorting;
|
||||||
@ -45,6 +60,12 @@
|
|||||||
get columnFilters() {
|
get columnFilters() {
|
||||||
return columnFilters;
|
return columnFilters;
|
||||||
},
|
},
|
||||||
|
get grouping() {
|
||||||
|
return ["group"];
|
||||||
|
},
|
||||||
|
get rowSelection() {
|
||||||
|
return selection;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
onSortingChange: (updater) => {
|
onSortingChange: (updater) => {
|
||||||
if (typeof updater === "function") {
|
if (typeof updater === "function") {
|
||||||
@ -60,13 +81,153 @@
|
|||||||
columnFilters = updater;
|
columnFilters = updater;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onRowSelectionChange: (updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
selection = updater(selection);
|
||||||
|
} else {
|
||||||
|
selection = updater;
|
||||||
|
}
|
||||||
|
},
|
||||||
columns,
|
columns,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
getSortedRowModel: getSortedRowModel(),
|
getSortedRowModel: getSortedRowModel(),
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getGroupedRowModel: getGroupedRowModel(),
|
||||||
|
groupedColumnMode: "remove",
|
||||||
|
getRowId: (row) => row.id.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let createOpen = $state(false);
|
||||||
|
let editGroupOpen = $state(false);
|
||||||
|
let selectedGroup: ResponseGroups | null = $state(null);
|
||||||
|
let groupResultsOpen = $state(false);
|
||||||
|
let selectedGroupForResults: ResponseGroups | null = $state(null);
|
||||||
|
|
||||||
|
let groupChangeOpen = $state(false);
|
||||||
|
let groupChangeSelected: number | null = $state(null);
|
||||||
|
|
||||||
|
async function handleSave(fight: EventFightEdit) {
|
||||||
|
await $eventRepo.createFight(data.event.id.toString(), {
|
||||||
|
...fight,
|
||||||
|
blueTeam: fight.blueTeam.id,
|
||||||
|
redTeam: fight.redTeam.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
createOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openGroupEditDialog(group: ResponseGroups) {
|
||||||
|
selectedGroup = group;
|
||||||
|
editGroupOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openGroupResultsDialog(group: ResponseGroups) {
|
||||||
|
selectedGroupForResults = group;
|
||||||
|
groupResultsOpen = true;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Dialog bind:open={createOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Fight Erstellen</DialogTitle>
|
||||||
|
<DialogDescription>Hier kannst du einen neuen Fight erstellen</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<FightEdit fight={null} {data} onSave={handleSave}>
|
||||||
|
{#snippet actions(dirty, submit)}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button disabled={!dirty} onclick={submit}>Speichern</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
{/snippet}
|
||||||
|
</FightEdit>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{#if selectedGroup}
|
||||||
|
<GroupEditDialog bind:open={editGroupOpen} group={selectedGroup} event={data.event} bind:groups={data.groups} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if selectedGroupForResults}
|
||||||
|
<GroupResultsDialog bind:open={groupResultsOpen} group={selectedGroupForResults} teams={data.teams} fights={data.fights} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Dialog bind:open={groupChangeOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Gruppe Ändern</DialogTitle>
|
||||||
|
<DialogDescription>Hier kannst du die Gruppe der ausgewählten Kämpfe ändern</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<GroupSelector event={data.event} bind:groups={data.groups} bind:value={groupChangeSelected} />
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
onclick={async () => {
|
||||||
|
groupChangeOpen = false;
|
||||||
|
let group = data.groups.find((g) => g.id === groupChangeSelected);
|
||||||
|
if (group) {
|
||||||
|
let selectedGroups = table.getSelectedRowModel().rows.map((row) => row.original);
|
||||||
|
for (const g of selectedGroups) {
|
||||||
|
await $fightRepo.updateFight(data.event.id, g.id, {
|
||||||
|
group: group.id,
|
||||||
|
spielmodus: null,
|
||||||
|
map: null,
|
||||||
|
blueTeam: null,
|
||||||
|
redTeam: null,
|
||||||
|
start: null,
|
||||||
|
spectatePort: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}}>Speichern</Button
|
||||||
|
>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Menubar>
|
||||||
|
<MenubarMenu>
|
||||||
|
<MenubarTrigger>Mehrfach Bearbeiten</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
<MenubarItem onclick={() => (groupChangeOpen = true)}>Gruppe Ändern</MenubarItem>
|
||||||
|
<MenubarItem disabled>Startzeit Verschieben</MenubarItem>
|
||||||
|
<MenubarItem disabled>Spectate Port Ändern</MenubarItem>
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
<MenubarMenu>
|
||||||
|
<MenubarTrigger>Erstellen</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
<MenubarItem onclick={() => (createOpen = true)}>Fight Erstellen</MenubarItem>
|
||||||
|
<MenubarGroup>
|
||||||
|
<MenubarGroupHeading>Generatoren</MenubarGroupHeading>
|
||||||
|
<a href="#/event/{data.event.id}/generate">
|
||||||
|
<MenubarItem>Gruppenphase</MenubarItem>
|
||||||
|
</a>
|
||||||
|
</MenubarGroup>
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
<MenubarMenu>
|
||||||
|
<MenubarTrigger disabled={!data.groups.length}>Gruppen</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
{#each data.groups as group (group.id)}
|
||||||
|
<MenubarSub>
|
||||||
|
<MenubarSubTrigger>
|
||||||
|
{group.name}
|
||||||
|
</MenubarSubTrigger>
|
||||||
|
<MenubarSubContent>
|
||||||
|
<MenubarItem onclick={() => openGroupEditDialog(group)}>Bearbeiten</MenubarItem>
|
||||||
|
<MenubarItem onclick={() => openGroupResultsDialog(group)}>Gruppen Ergebnisse</MenubarItem>
|
||||||
|
</MenubarSubContent>
|
||||||
|
</MenubarSub>
|
||||||
|
{/each}
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
</Menubar>
|
||||||
|
<Button variant="outline" class="ml-4" onclick={refresh}>Neu laden</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||||
@ -74,34 +235,81 @@
|
|||||||
{#each headerGroup.headers as header (header.id)}
|
{#each headerGroup.headers as header (header.id)}
|
||||||
<TableHead>
|
<TableHead>
|
||||||
{#if !header.isPlaceholder}
|
{#if !header.isPlaceholder}
|
||||||
<FlexRender
|
<FlexRender content={header.column.columnDef.header} context={header.getContext()} />
|
||||||
content={header.column.columnDef.header}
|
|
||||||
context={header.getContext()}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
</TableHead>
|
</TableHead>
|
||||||
{/each}
|
{/each}
|
||||||
|
<TableHead></TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{/each}
|
{/each}
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{#each table.getRowModel().rows as row (row.id)}
|
{#each table.getRowModel().rows as groupRow (groupRow.id)}
|
||||||
<TableRow data-state={row.getIsSelected() && "selected"}>
|
{#if groupRow.getIsGrouped()}
|
||||||
{#each row.getVisibleCells() as cell (cell.id)}
|
{@const group = data.groups.find((g) => g.id == groupRow.getValue("group"))}
|
||||||
<TableCell>
|
<TableRow class="font-bold">
|
||||||
<FlexRender
|
<TableCell colspan={columns.length - 1}>
|
||||||
content={cell.column.columnDef.cell}
|
<Checkbox
|
||||||
context={cell.getContext()}
|
checked={groupRow.getIsSelected()}
|
||||||
|
indeterminate={groupRow.getIsSomeSelected() && !groupRow.getIsSelected()}
|
||||||
|
onCheckedChange={() => groupRow.toggleSelected()}
|
||||||
|
class="mr-4"
|
||||||
/>
|
/>
|
||||||
|
{group?.name ?? "Keine Gruppe"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell class="text-right">
|
||||||
|
{#if group}
|
||||||
|
<Button variant="ghost" size="icon" onclick={() => openGroupEditDialog(group!)}>
|
||||||
|
<EditIcon />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" onclick={() => openGroupResultsDialog(group!)}>
|
||||||
|
<GroupIcon />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<LinkIcon />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onclick={() => navigator.clipboard.writeText(`<group-table data-event="${data.event.id}"${group ? ` data-group="${group?.id}"` : ""}> </group-table>`)}
|
||||||
|
>Punkte Tabelle</DropdownMenuItem
|
||||||
|
>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onclick={() => navigator.clipboard.writeText(`<fight-table data-event="${data.event.id}"${group ? ` data-group="${group?.id}"` : ""}> </group-table>`)}
|
||||||
|
>Kampf Tabelle</DropdownMenuItem
|
||||||
|
>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{#each groupRow.subRows as row (row.id)}
|
||||||
|
<TableRow data-state={row.getIsSelected() && "selected"}>
|
||||||
|
{#each row.getVisibleCells() as cell (cell.id)}
|
||||||
|
<TableCell>
|
||||||
|
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
|
||||||
|
</TableCell>
|
||||||
|
{/each}
|
||||||
|
<TableCell class="text-right">
|
||||||
|
<FightEditRow fight={row.original} {data} onupdate={(update) => (data._fights = data._fights.map((v) => (v.id === update.id ? update : v)))} {refresh}></FightEditRow>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
{/each}
|
{/each}
|
||||||
</TableRow>
|
{:else}
|
||||||
|
<TableRow data-state={groupRow.getIsSelected() && "selected"}>
|
||||||
|
{#each groupRow.getVisibleCells() as cell (cell.id)}
|
||||||
|
<TableCell>
|
||||||
|
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
|
||||||
|
</TableCell>
|
||||||
|
{/each}
|
||||||
|
</TableRow>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colspan={columns.length} class="h-24 text-center">
|
<TableCell colspan={columns.length} class="h-24 text-center">No results.</TableCell>
|
||||||
No results.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{/each}
|
{/each}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@ -18,14 +18,13 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {ExtendedEvent} from "@type/event.ts";
|
|
||||||
import EventEdit from "@components/moderator/pages/event/EventEdit.svelte";
|
import EventEdit from "@components/moderator/pages/event/EventEdit.svelte";
|
||||||
import EventFightList from "@components/moderator/pages/event/EventFightList.svelte";
|
import EventFightList from "@components/moderator/pages/event/EventFightList.svelte";
|
||||||
import RefereesList from "@components/moderator/pages/event/RefereesList.svelte";
|
import RefereesList from "@components/moderator/pages/event/RefereesList.svelte";
|
||||||
|
import TeamTable from "@components/moderator/pages/event/TeamTable.svelte";
|
||||||
|
import type { EventModel } from "./eventmodel.svelte";
|
||||||
|
|
||||||
const {
|
let { event = $bindable(), refresh }: { event: EventModel; refresh: () => void } = $props();
|
||||||
event
|
|
||||||
}: { event: ExtendedEvent } = $props();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col m-4 p-4 rounded-md border gap-4">
|
<div class="flex flex-col m-4 p-4 rounded-md border gap-4">
|
||||||
@ -35,13 +34,13 @@
|
|||||||
<EventEdit event={event.event} />
|
<EventEdit event={event.event} />
|
||||||
</div>
|
</div>
|
||||||
<div class="md:ml-4 md:pl-4 md:border-l md:w-1/3">
|
<div class="md:ml-4 md:pl-4 md:border-l md:w-1/3">
|
||||||
<h2>Teams</h2>
|
<h2 class="text-xl font-bold mb-4">Teams</h2>
|
||||||
|
<TeamTable bind:event />
|
||||||
</div>
|
</div>
|
||||||
<div class="md:ml-4 md:pl-4 md:border-l md:w-1/3">
|
<div class="md:ml-4 md:pl-4 md:border-l md:w-1/3">
|
||||||
<h2>Referees</h2>
|
<h2 class="text-xl font-bold mb-4">Referees</h2>
|
||||||
<RefereesList event={event} />
|
<RefereesList {event} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EventFightList data={event} />
|
<EventFightList bind:data={event} {refresh} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
84
src/components/moderator/pages/event/FightEditRow.svelte
Normal file
84
src/components/moderator/pages/event/FightEditRow.svelte
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { EventFight, EventFightEdit, ResponseGroups, ResponseRelation, SWEvent } from "@type/event";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { EditIcon, CopyIcon } from "lucide-svelte";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@components/ui/dialog";
|
||||||
|
import FightEdit from "@components/moderator/components/FightEdit.svelte";
|
||||||
|
import type { Team } from "@components/types/team";
|
||||||
|
import { fightRepo } from "@components/repo/fight";
|
||||||
|
import { eventRepo } from "@components/repo/event";
|
||||||
|
import type { EventModel } from "./eventmodel.svelte";
|
||||||
|
|
||||||
|
let { fight, onupdate, refresh, data }: { fight: EventFight; onupdate: (update: EventFight) => void; refresh: () => void; data: EventModel } = $props();
|
||||||
|
|
||||||
|
let editOpen = $state(false);
|
||||||
|
let duplicateOpen = $state(false);
|
||||||
|
|
||||||
|
async function handleSave(fightData: EventFightEdit) {
|
||||||
|
let f = await $fightRepo.updateFight(data.event.id, fight.id, {
|
||||||
|
...fightData,
|
||||||
|
blueTeam: fightData.blueTeam.id,
|
||||||
|
redTeam: fightData.redTeam.id,
|
||||||
|
group: fightData.group ?? -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
onupdate(f);
|
||||||
|
|
||||||
|
editOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlyCopy(fightData: EventFightEdit) {
|
||||||
|
await $eventRepo.createFight(data.event.id.toString(), {
|
||||||
|
...fightData,
|
||||||
|
blueTeam: fightData.blueTeam.id,
|
||||||
|
redTeam: fightData.redTeam.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
refresh();
|
||||||
|
|
||||||
|
duplicateOpen = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Dialog bind:open={editOpen}>
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<EditIcon />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Fight bearbeiten</DialogTitle>
|
||||||
|
<DialogDescription>Hier kannst du die Daten des Kampfes bearbeiten.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<FightEdit {fight} {data} onSave={handleSave}>
|
||||||
|
{#snippet actions(dirty, submit)}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button disabled={!dirty} onclick={submit}>Speichern</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
{/snippet}
|
||||||
|
</FightEdit>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
<Dialog bind:open={duplicateOpen}>
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<CopyIcon />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Fight duplizieren</DialogTitle>
|
||||||
|
<DialogDescription>Hier kannst du die Daten des duplizierten Fights ändern</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<FightEdit {fight} {data} onSave={handlyCopy}>
|
||||||
|
{#snippet actions(dirty, submit)}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button onclick={submit}>Speichern</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
{/snippet}
|
||||||
|
</FightEdit>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
45
src/components/moderator/pages/event/GroupEditDialog.svelte
Normal file
45
src/components/moderator/pages/event/GroupEditDialog.svelte
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { GroupUpdateEdit, ResponseGroups, SWEvent } from "@type/event";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@components/ui/dialog";
|
||||||
|
import GroupEdit from "@components/moderator/components/GroupEdit.svelte";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { eventRepo } from "@repo/event";
|
||||||
|
|
||||||
|
let { group, groups = $bindable(), open = $bindable(), event }: { group: ResponseGroups; groups: ResponseGroups[]; open?: boolean; event: SWEvent } = $props();
|
||||||
|
|
||||||
|
async function handleSave(groupData: GroupUpdateEdit) {
|
||||||
|
if (!group) return;
|
||||||
|
const updatedGroup = await $eventRepo.updateGroup(event.id.toString(), group.id.toString(), groupData);
|
||||||
|
groups = groups.map((g) => (g.id === updatedGroup.id ? updatedGroup : g));
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
if (!group) return;
|
||||||
|
await $eventRepo.deleteGroup(event.id.toString(), group.id.toString());
|
||||||
|
groups = groups.filter((g) => g.id !== group.id);
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if group}
|
||||||
|
<Dialog bind:open>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Gruppe Bearbeiten: {group.name}</DialogTitle>
|
||||||
|
<DialogDescription>Hier kannst du die Gruppendetails bearbeiten.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<GroupEdit {group} onSave={handleSave}>
|
||||||
|
{#snippet actions(dirty, submit)}
|
||||||
|
<DialogFooter class="flex justify-between">
|
||||||
|
<Button variant="destructive" onclick={handleDelete}>Löschen</Button>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button variant="outline" onclick={() => (open = false)}>Abbrechen</Button>
|
||||||
|
<Button disabled={!dirty} onclick={submit}>Speichern</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
{/snippet}
|
||||||
|
</GroupEdit>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
{/if}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { EventFight, ExtendedEvent, ResponseGroups, ResponseTeam } from "@type/event";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@components/ui/dialog";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@components/ui/table";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import type { Team } from "@components/types/team";
|
||||||
|
|
||||||
|
let { open = $bindable(), group, teams, fights }: { open?: boolean; group: ResponseGroups; teams: Team[]; fights: EventFight[] } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog bind:open>
|
||||||
|
<DialogContent class="sm:max-w-[600px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Ergebnisse: {group?.name}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Punkte: Sieg: {group?.pointsPerWin}, Unentschieden: {group?.pointsPerDraw}, Niederlage: {group?.pointsPerLoss}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
{#if group.points !== null}
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Team</TableHead>
|
||||||
|
<TableHead class="text-right">Spiele</TableHead>
|
||||||
|
<TableHead class="text-right">Punkte</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{#each Object.entries(group.points).toSorted((a, b) => b[1] - a[1]) as [teamIdString, points] (teamIdString)}
|
||||||
|
{@const teamId = Number(teamIdString)}
|
||||||
|
{@const team = teams.find((t) => t.id === teamId) as ResponseTeam | undefined}
|
||||||
|
{@const playedGames = fights.filter((f) => f.hasFinished && f.group?.id === group.id && (f.blueTeam.id === teamId || f.redTeam.id === teamId)).length}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>{team?.name ?? "?"} ({team?.kuerzel ?? "?"})</TableCell>
|
||||||
|
<TableCell class="text-right">{playedGames}</TableCell>
|
||||||
|
<TableCell class="text-right font-bold">{points}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{/each}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
{:else}
|
||||||
|
<p class="text-center py-4">Noch keine Ergebnisse für diese Gruppe vorhanden oder keine Spiele zugeordnet.</p>
|
||||||
|
{/if}
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onclick={() => (open = false)}>Schließen</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
@ -18,38 +18,29 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table/index.js";
|
import { Table, TableBody, TableCell, TableCaption, TableHead, TableHeader, TableRow } from "@components/ui/table";
|
||||||
import {
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command/index.js";
|
||||||
Command,
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover/index.js";
|
||||||
CommandEmpty,
|
import { Button } from "@components/ui/button/index.js";
|
||||||
CommandGroup,
|
import type { ExtendedEvent } from "@type/event.ts";
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList,
|
|
||||||
} from "@components/ui/command/index.js";
|
|
||||||
import {Popover, PopoverContent, PopoverTrigger} from "@components/ui/popover/index.js";
|
|
||||||
import {Button} from "@components/ui/button/index.js";
|
|
||||||
import type {ExtendedEvent} from "@type/event.ts";
|
|
||||||
import { eventRepo } from "@repo/event";
|
import { eventRepo } from "@repo/event";
|
||||||
import { players } from "@stores/stores"
|
import { players } from "@stores/stores";
|
||||||
|
|
||||||
const {
|
const { event }: { event: ExtendedEvent } = $props();
|
||||||
event
|
|
||||||
}: { event: ExtendedEvent } = $props();
|
|
||||||
|
|
||||||
let referees = $state(event.event.referees)
|
let referees = $state(event.referees);
|
||||||
|
|
||||||
async function addReferee(value: string) {
|
async function addReferee(value: string) {
|
||||||
referees = (await $eventRepo.updateEvent(event.event.id.toString(), {
|
await $eventRepo.updateReferees(event.event.id.toString(), [value]);
|
||||||
addReferee: [value]
|
referees = await $eventRepo.listReferees(event.event.id.toString());
|
||||||
})).referees;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeReferee(value: string) {
|
async function removeReferee(value: string) {
|
||||||
referees = (await $eventRepo.updateEvent(event.event.id.toString(), {
|
await $eventRepo.deleteReferees(event.event.id.toString(), [value]);
|
||||||
removeReferee: [value]
|
referees = await $eventRepo.listReferees(event.event.id.toString());
|
||||||
})).referees;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let playerSearch = $state("");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Table>
|
<Table>
|
||||||
@ -64,29 +55,32 @@
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>{referee.name}</TableCell>
|
<TableCell>{referee.name}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button onclick={() => removeReferee(referee.uuid)}>Remove</Button>
|
<Button onclick={() => removeReferee(referee.uuid)} variant="outline" size="sm">{referee.name} entfernen</Button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
{/each}
|
{/each}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
|
<Popover>
|
||||||
|
<TableCaption>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button>Hinzufügen</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</TableCaption>
|
||||||
|
<PopoverContent class="p-0">
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput bind:value={playerSearch} placeholder="Search players..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No Players found :(</CommandEmpty>
|
||||||
|
<CommandGroup heading="Players">
|
||||||
|
{#each $players
|
||||||
|
.filter((v) => v.name.toLowerCase().includes(playerSearch.toLowerCase()))
|
||||||
|
.filter((v, i) => i < 50)
|
||||||
|
.filter((v) => !referees.some((k) => k.uuid === v.uuid)) as player (player.uuid)}
|
||||||
|
<CommandItem value={player.name} onSelect={() => addReferee(player.uuid)} keywords={[player.uuid]}>{player.name}</CommandItem>
|
||||||
|
{/each}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</Table>
|
</Table>
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<Button>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent class="p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder="Search players..." />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>No Players found :(</CommandEmpty>
|
|
||||||
<CommandGroup heading="Players">
|
|
||||||
{#each $players.filter(v => v.perms.length > 0).filter(v => !referees.some(k => k.uuid === v.uuid)) as player (player.uuid)}
|
|
||||||
<CommandItem value={player.uuid} onSelect={() => addReferee(player.uuid)}>{player.name}</CommandItem>
|
|
||||||
{/each}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
91
src/components/moderator/pages/event/TeamTable.svelte
Normal file
91
src/components/moderator/pages/event/TeamTable.svelte
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell, TableCaption } from "@components/ui/table";
|
||||||
|
import { eventRepo } from "@repo/event";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||||
|
import { teams } from "@components/stores/stores";
|
||||||
|
import type { EventModel } from "./eventmodel.svelte";
|
||||||
|
|
||||||
|
let { event = $bindable() }: { event: EventModel } = $props();
|
||||||
|
|
||||||
|
async function addTeam(value: number) {
|
||||||
|
await $eventRepo.updateTeams(event.event.id.toString(), [value]);
|
||||||
|
event.teams = await $eventRepo.listTeams(event.event.id.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeTeam(value: number) {
|
||||||
|
await $eventRepo.deleteTeams(event.event.id.toString(), [value]);
|
||||||
|
event.teams = await $eventRepo.listTeams(event.event.id.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
let teamSearch = $state("");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Team</TableHead>
|
||||||
|
<TableHead>Name</TableHead>
|
||||||
|
<TableHead>Action</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{#each event.teams as t (t.id)}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>{t.kuerzel}</TableCell>
|
||||||
|
<TableCell>{t.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button onclick={() => removeTeam(t.id)} variant="outline" size="sm">{t.name} abmelden</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{/each}
|
||||||
|
{#if event.teams.length === 0}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell class="text-center col-span-3">No teams available</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{/if}
|
||||||
|
</TableBody>
|
||||||
|
<Popover>
|
||||||
|
<TableCaption>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button>Team Anmelden</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</TableCaption>
|
||||||
|
<PopoverContent class="p-0">
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput bind:value={teamSearch} placeholder="Search teams..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No teams found :(</CommandEmpty>
|
||||||
|
<CommandGroup heading="Teams">
|
||||||
|
{#each $teams
|
||||||
|
.filter((v) => v.name.includes(teamSearch))
|
||||||
|
.filter((v) => !event.teams.some((k) => k.id === v.id))
|
||||||
|
.filter((v, i) => i < 50) as t (t.id)}
|
||||||
|
<CommandItem value={t.id.toString()} onSelect={() => addTeam(t.id)} keywords={[t.name, t.kuerzel]}>{t.name}</CommandItem>
|
||||||
|
{/each}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</Table>
|
||||||
@ -17,16 +17,88 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {ColumnDef} from "@tanstack/table-core";
|
import { Checkbox } from "@components/ui/checkbox";
|
||||||
import type {EventFight} from "@type/event.ts";
|
import { renderComponent } from "@components/ui/data-table";
|
||||||
|
import type { ColumnDef } from "@tanstack/table-core";
|
||||||
|
import type { EventFightModel } from "./eventmodel.svelte";
|
||||||
|
|
||||||
export const columns: ColumnDef<EventFight> = [
|
export const columns: ColumnDef<EventFightModel>[] = [
|
||||||
{
|
{
|
||||||
accessorFn: (r) => r.blueTeam.name,
|
id: "auswahl",
|
||||||
header: "Team Blue",
|
header: ({ table }) => {
|
||||||
|
return renderComponent(Checkbox, {
|
||||||
|
checked: table.getIsAllRowsSelected(),
|
||||||
|
indeterminate: table.getIsSomeRowsSelected(),
|
||||||
|
onCheckedChange: () => {
|
||||||
|
if (!table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected()) {
|
||||||
|
const now = new Date();
|
||||||
|
const rows = table.getRowModel().rows.filter((row) => new Date(row.original.start) > now);
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
rows.forEach((row) => {
|
||||||
|
row.toggleSelected();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
table.toggleAllRowsSelected(true);
|
||||||
|
}
|
||||||
|
} else if (table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected()) {
|
||||||
|
table.toggleAllRowsSelected(true);
|
||||||
|
} else {
|
||||||
|
table.toggleAllRowsSelected(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return renderComponent(Checkbox, {
|
||||||
|
checked: row.getIsSelected(),
|
||||||
|
onCheckedChange: row.getToggleSelectedHandler(),
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorFn: (r) => r.redTeam.name,
|
accessorFn: (r) => r.blueTeam.nameWithRelation + " vs " + r.redTeam.nameWithRelation,
|
||||||
header: "Team Red",
|
id: "begegnung",
|
||||||
|
header: "Begegnung",
|
||||||
},
|
},
|
||||||
];
|
{
|
||||||
|
header: "Gruppe",
|
||||||
|
accessorKey: "group.id",
|
||||||
|
id: "group",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Datum",
|
||||||
|
accessorKey: "start",
|
||||||
|
id: "start",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return new Date(row.getValue("start")).toLocaleString("de-DE", {
|
||||||
|
dateStyle: "short",
|
||||||
|
timeStyle: "medium",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Spielmodus",
|
||||||
|
accessorKey: "spielmodus",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Map",
|
||||||
|
accessorKey: "map",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Ergebnis",
|
||||||
|
accessorKey: "ergebnis",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const fight = row.original;
|
||||||
|
if (!fight.hasFinished) {
|
||||||
|
return "Noch nicht gespielt";
|
||||||
|
} else if (fight.ergebnis === 1) {
|
||||||
|
return fight.blueTeam.name + " hat gewonnen";
|
||||||
|
} else if (fight.ergebnis === 2) {
|
||||||
|
return fight.redTeam.name + " hat gewonnen";
|
||||||
|
} else {
|
||||||
|
return "Unentschieden";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
68
src/components/moderator/pages/event/eventmodel.svelte.ts
Normal file
68
src/components/moderator/pages/event/eventmodel.svelte.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import type { ResponseUser } from "@components/repo/event";
|
||||||
|
import type { EventFight, ExtendedEvent, ResponseGroups, ResponseRelation, SWEvent } from "@components/types/event";
|
||||||
|
import type { Team } from "@components/types/team";
|
||||||
|
import { derived } from "svelte/store";
|
||||||
|
|
||||||
|
export class EventModel {
|
||||||
|
public event: SWEvent = $state({} as SWEvent);
|
||||||
|
public teams: Array<Team> = $state([]);
|
||||||
|
public groups: Array<ResponseGroups> = $state([]);
|
||||||
|
public _fights: Array<EventFight> = $state([]);
|
||||||
|
public referees: Array<ResponseUser> = $state([]);
|
||||||
|
public relations: Array<ResponseRelation> = $state([]);
|
||||||
|
|
||||||
|
public fights = $derived(this.remapFights(this._fights, this.relations));
|
||||||
|
|
||||||
|
constructor(data: ExtendedEvent) {
|
||||||
|
this.event = data.event;
|
||||||
|
this.relations = data.relations;
|
||||||
|
this.teams = data.teams;
|
||||||
|
this.groups = data.groups;
|
||||||
|
this._fights = data.fights;
|
||||||
|
this.referees = data.referees;
|
||||||
|
}
|
||||||
|
|
||||||
|
private remapFights(v: Array<EventFight>, rels: Array<ResponseRelation>) {
|
||||||
|
return v.map((fight) => {
|
||||||
|
let f = JSON.parse(JSON.stringify(fight)) as EventFight;
|
||||||
|
|
||||||
|
let blueTeamRelation = "";
|
||||||
|
let redTeamRelation = "";
|
||||||
|
|
||||||
|
let relations = rels.filter((relation) => relation.fight === f.id);
|
||||||
|
|
||||||
|
relations.forEach((relation) => {
|
||||||
|
let str = "";
|
||||||
|
if (relation.type === "FIGHT") {
|
||||||
|
str = `${relation.fromPlace === 0 ? "Gewinner" : "Verlierer"} von ${relation.fromFight?.blueTeam.name} vs ${relation.fromFight?.redTeam.name} (${new Date(
|
||||||
|
relation.fromFight?.start ?? 0
|
||||||
|
).toLocaleTimeString("de-DE", {
|
||||||
|
timeStyle: "short",
|
||||||
|
})})`;
|
||||||
|
} else {
|
||||||
|
str = `${relation.fromPlace + 1}. Platz von ${relation.fromGroup?.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relation.team === "BLUE") {
|
||||||
|
blueTeamRelation = str;
|
||||||
|
} else {
|
||||||
|
redTeamRelation = str;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...f,
|
||||||
|
blueTeam: {
|
||||||
|
...f.blueTeam,
|
||||||
|
nameWithRelation: blueTeamRelation ? `${f.blueTeam.name} (${blueTeamRelation})` : f.blueTeam.name,
|
||||||
|
},
|
||||||
|
redTeam: {
|
||||||
|
...f.redTeam,
|
||||||
|
nameWithRelation: redTeamRelation ? `${f.redTeam.name} (${redTeamRelation})` : f.redTeam.name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EventFightModel = (typeof EventModel.prototype.fights)[number];
|
||||||
@ -20,12 +20,136 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { eventRepo } from "@repo/event.ts";
|
import { eventRepo } from "@repo/event.ts";
|
||||||
import EventCard from "@components/moderator/components/EventCard.svelte";
|
import EventCard from "@components/moderator/components/EventCard.svelte";
|
||||||
|
import { Button } from "@components/ui/button/index.js";
|
||||||
|
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@components/ui/dialog/index.js";
|
||||||
|
import { Input } from "@components/ui/input/index.js";
|
||||||
|
import { Label } from "@components/ui/label/index.js";
|
||||||
|
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||||
|
import { PlusIcon } from "lucide-svelte";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { fromAbsolute, now, ZonedDateTime } from "@internationalized/date";
|
||||||
|
|
||||||
let eventsFuture = $state($eventRepo.listEvents());
|
let eventsFuture = $state($eventRepo.listEvents());
|
||||||
let millis = Date.now();
|
let millis = Date.now();
|
||||||
|
|
||||||
|
let createOpen = $state(false);
|
||||||
|
let newEventName = $state("");
|
||||||
|
let newEventStart: ZonedDateTime = $state(now("Europe/Berlin"));
|
||||||
|
let newEventEnd: ZonedDateTime = $state(
|
||||||
|
now("Europe/Berlin").add({
|
||||||
|
days: 1,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
let isSubmitting = $state(false);
|
||||||
|
let errorMsg = $state("");
|
||||||
|
|
||||||
|
function resetFormFields() {
|
||||||
|
newEventName = "";
|
||||||
|
newEventStart = now("Europe/Berlin");
|
||||||
|
newEventEnd = now("Europe/Berlin").add({
|
||||||
|
days: 1,
|
||||||
|
});
|
||||||
|
errorMsg = "";
|
||||||
|
isSubmitting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (createOpen) {
|
||||||
|
resetFormFields();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const canSubmit = $derived(
|
||||||
|
newEventName.trim() !== "" &&
|
||||||
|
newEventStart &&
|
||||||
|
newEventEnd &&
|
||||||
|
dayjs(newEventStart.toDate()).isValid() &&
|
||||||
|
dayjs(newEventEnd.toDate()).isValid() &&
|
||||||
|
newEventStart.toDate() < newEventEnd.toDate() &&
|
||||||
|
!isSubmitting
|
||||||
|
);
|
||||||
|
|
||||||
|
async function submitCreateEvent() {
|
||||||
|
if (!canSubmit) return;
|
||||||
|
|
||||||
|
isSubmitting = true;
|
||||||
|
errorMsg = "";
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
name: newEventName.trim(),
|
||||||
|
start: dayjs(newEventStart.toDate()),
|
||||||
|
end: dayjs(newEventEnd.toDate()),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $eventRepo.createEvent(payload);
|
||||||
|
eventsFuture = $eventRepo.listEvents(); // Refresh the list
|
||||||
|
createOpen = false;
|
||||||
|
} catch (e: any) {
|
||||||
|
errorMsg = e.message || "Failed to create event. Please try again.";
|
||||||
|
console.error("Failed to create event:", e);
|
||||||
|
} finally {
|
||||||
|
isSubmitting = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-4">
|
<div class="p-4 min-h-screen">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<h1 class="text-3xl font-semibold">Events</h1>
|
||||||
|
<Dialog bind:open={createOpen}>
|
||||||
|
<DialogTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="outline" {...props}>
|
||||||
|
<PlusIcon class="mr-2" />
|
||||||
|
Create Event
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent class="sm:max-w-[425px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create New Event</DialogTitle>
|
||||||
|
<DialogDescription>Fill in the details for the new event. Click create when you're done.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div class="grid gap-4 py-4">
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label for="eventName" class="text-right">Name</Label>
|
||||||
|
<Input id="eventName" bind:value={newEventName} class="col-span-3" placeholder="Event Name" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label for="eventStart" class="text-right">Start</Label>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<DateTimePicker bind:value={newEventStart} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
|
<Label for="eventEnd" class="text-right">End</Label>
|
||||||
|
<div class="col-span-3">
|
||||||
|
<DateTimePicker bind:value={newEventEnd} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if errorMsg}
|
||||||
|
<p class="col-span-4 text-sm text-red-600 dark:text-red-500 text-center">{errorMsg}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="outline" {...props}>Cancel</Button>
|
||||||
|
{/snippet}
|
||||||
|
</DialogClose>
|
||||||
|
<Button onclick={submitCreateEvent} disabled={!canSubmit}>
|
||||||
|
{#if isSubmitting}
|
||||||
|
Creating...
|
||||||
|
{:else}
|
||||||
|
Create Event
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#await eventsFuture}
|
{#await eventsFuture}
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
{:then events}
|
{:then events}
|
||||||
@ -45,7 +169,5 @@
|
|||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:catch e}
|
|
||||||
|
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { ExtendedEvent } from "@components/types/event";
|
||||||
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@components/ui/tabs";
|
||||||
|
import GroupPhaseGenerator from "./gens/group/GroupPhaseGenerator.svelte";
|
||||||
|
let {
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: ExtendedEvent;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="m-4">
|
||||||
|
<Tabs value="group">
|
||||||
|
<TabsList class="mb-4">
|
||||||
|
<TabsTrigger value="group">Gruppenphase</TabsTrigger>
|
||||||
|
<TabsTrigger value="ko">K.O. Phase</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="group">
|
||||||
|
<GroupPhaseGenerator {data} />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
22
src/components/moderator/pages/generators/Generator.svelte
Normal file
22
src/components/moderator/pages/generators/Generator.svelte
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { eventRepo } from "@components/repo/event";
|
||||||
|
import FightsGenerator from "./FightsGenerator.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
params: { id: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
let { params }: Props = $props();
|
||||||
|
|
||||||
|
let id = params.id;
|
||||||
|
|
||||||
|
let future = $eventRepo.getEvent(id.toString());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await future}
|
||||||
|
<p>Loading...</p>
|
||||||
|
{:then event}
|
||||||
|
<FightsGenerator data={event} />
|
||||||
|
{:catch error}
|
||||||
|
<p class="text-red-500">Error loading event: {error.message}</p>
|
||||||
|
{/await}
|
||||||
@ -0,0 +1,306 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import DragAcceptor from "@components/admin/pages/generate/DragAcceptor.svelte";
|
||||||
|
import TeamChip from "@components/admin/pages/generate/TeamChip.svelte";
|
||||||
|
import { eventRepo } from "@components/repo/event";
|
||||||
|
import { fightRepo } from "@components/repo/fight";
|
||||||
|
import { gamemodes, maps } from "@components/stores/stores";
|
||||||
|
import type { ExtendedEvent } from "@components/types/event";
|
||||||
|
import type { Team } from "@components/types/team";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { Card } from "@components/ui/card";
|
||||||
|
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||||
|
import { Dialog } from "@components/ui/dialog";
|
||||||
|
import { Input } from "@components/ui/input";
|
||||||
|
import { Label } from "@components/ui/label";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||||
|
import { Slider } from "@components/ui/slider";
|
||||||
|
import { fromAbsolute, fromDate, parseDateTime, parseDuration } from "@internationalized/date";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { Plus } from "lucide-svelte";
|
||||||
|
import { replace } from "svelte-spa-router";
|
||||||
|
let {
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: ExtendedEvent;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let teams = $derived(new Map<number, Team>(data.teams.map((team) => [team.id, team])));
|
||||||
|
|
||||||
|
let groups: number[][] = $state([]);
|
||||||
|
let teamsNotInGroup = $derived(data.teams.filter((team) => !groups.flat().includes(team.id)));
|
||||||
|
|
||||||
|
function dragToNewGroup(event: DragEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
let teamId = parseInt(event.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 = $state(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: DragEvent, groupIndex: number) {
|
||||||
|
ev.preventDefault();
|
||||||
|
let teamId = parseInt(ev.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 = $state(fromAbsolute(data.event.start, "Europe/Berlin"));
|
||||||
|
let gamemode = $state("");
|
||||||
|
let map = $state("");
|
||||||
|
|
||||||
|
let selectableGamemodes = $derived(
|
||||||
|
$gamemodes
|
||||||
|
.map((gamemode) => {
|
||||||
|
return {
|
||||||
|
name: gamemode,
|
||||||
|
value: gamemode,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
);
|
||||||
|
|
||||||
|
let mapsStore = $derived(maps(gamemode));
|
||||||
|
let selectableMaps = $derived(
|
||||||
|
$mapsStore
|
||||||
|
.map((map) => {
|
||||||
|
return {
|
||||||
|
name: map,
|
||||||
|
value: map,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
);
|
||||||
|
|
||||||
|
let roundTime = $state(30);
|
||||||
|
let startDelay = $state(30);
|
||||||
|
|
||||||
|
let showAutoGrouping = $state(false);
|
||||||
|
let groupCount = $state(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[][]): number[][][][] {
|
||||||
|
const groupFights: number[][][][] = [];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let groupsFights = $derived(generateGroups(groups));
|
||||||
|
|
||||||
|
let generateDisabled = $derived(groupsFights.length > 0 && groupsFights.every((value) => value.every((value1) => value1.length > 0)) && gamemode !== "" && map !== "");
|
||||||
|
|
||||||
|
async function generateFights() {
|
||||||
|
groupsFights.forEach((group, i) => {
|
||||||
|
$eventRepo
|
||||||
|
.createGroup(data.event.id, {
|
||||||
|
name: "Gruppe " + (i + 1),
|
||||||
|
type: "GROUP_STAGE",
|
||||||
|
})
|
||||||
|
.then((v) => {
|
||||||
|
group.forEach((round, j) => {
|
||||||
|
round.forEach(async (fight, k) => {
|
||||||
|
const blueTeam = teams.get(fight[0])!;
|
||||||
|
const redTeam = teams.get(fight[1])!;
|
||||||
|
|
||||||
|
let karte = map;
|
||||||
|
|
||||||
|
if (karte === "%random%") {
|
||||||
|
karte = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
await $fightRepo.createFight(data.event.id, {
|
||||||
|
blueTeam: blueTeam.id,
|
||||||
|
redTeam: redTeam.id,
|
||||||
|
group: v.id,
|
||||||
|
map: karte,
|
||||||
|
spectatePort: null,
|
||||||
|
spielmodus: gamemode,
|
||||||
|
start: dayjs(
|
||||||
|
startTime
|
||||||
|
.copy()
|
||||||
|
.add({
|
||||||
|
minutes: roundTime * j,
|
||||||
|
})
|
||||||
|
.add({
|
||||||
|
seconds: startDelay * (k + i * round.length),
|
||||||
|
})
|
||||||
|
.toDate()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await replace("#/event/" + data.event.id);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<Card
|
||||||
|
id="reseter"
|
||||||
|
class="flex w-fit p-2 border border-gray-700 rounded h-20 pt-6 relative {resetDragOver ? 'border-white' : ''}"
|
||||||
|
ondragover={resetDragOverEvent}
|
||||||
|
ondragleave={() => (resetDragOver = false)}
|
||||||
|
ondrop={dropReset}
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
{#each teamsNotInGroup as team (team.id)}
|
||||||
|
<TeamChip {team} ondragstart={(ev) => teamDragStart(ev, team)} />
|
||||||
|
{/each}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div class="flex items-center mr-4">
|
||||||
|
<Button onclick={() => (showAutoGrouping = true)}>Automatische Gruppen</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex mt-4 gap-4 border-b border-gray-700 pb-4">
|
||||||
|
{#each groups as group, i (i)}
|
||||||
|
<DragAcceptor ondrop={(ev) => dropGroup(ev, i)}>
|
||||||
|
<h1>Gruppe {i + 1} ({group.length})</h1>
|
||||||
|
{#each group as teamId (teamId)}
|
||||||
|
<TeamChip team={teams.get(teamId)!} ondragstart={(ev) => teamDragStart(ev, teams.get(teamId)!)} />
|
||||||
|
{/each}
|
||||||
|
</DragAcceptor>
|
||||||
|
{/each}
|
||||||
|
<DragAcceptor ondrop={dragToNewGroup}>
|
||||||
|
<h1>Neue Gruppe</h1>
|
||||||
|
</DragAcceptor>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-b mt-4 border-gray-700 pb-4">
|
||||||
|
<Label for="event-end">Startzeit</Label>
|
||||||
|
<DateTimePicker bind:value={startTime} />
|
||||||
|
<div class="mt-2">
|
||||||
|
<Label for="event-roundtime">Rundenzeit: {roundTime}m</Label>
|
||||||
|
<Slider id="event-roundtime" type="single" bind:value={roundTime} step={1} min={5} max={60} />
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<Label for="event-member">Startverzögerung: {startDelay}</Label>
|
||||||
|
<Slider id="event-member" type="single" bind:value={startDelay} step={1} min={0} max={30} />
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<Label for="fight-gamemode">Spielmodus</Label>
|
||||||
|
<Select type="single" bind:value={gamemode}>
|
||||||
|
<SelectTrigger id="fight-gamemode">{gamemode}</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{#each selectableGamemodes as gamemodeOption}
|
||||||
|
<SelectItem value={gamemodeOption.value}>{gamemodeOption.name}</SelectItem>
|
||||||
|
{/each}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<Label for="fight-maps">Map</Label>
|
||||||
|
<Select type="single" bind:value={map}>
|
||||||
|
<SelectTrigger id="fight-maps">{map}</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="%random%">Zufällige Map</SelectItem>
|
||||||
|
{#each selectableMaps as mapOption}
|
||||||
|
<SelectItem value={mapOption.value}>{mapOption.name}</SelectItem>
|
||||||
|
{/each}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center mx-2">
|
||||||
|
{#each groupsFights as fightsGroup, i}
|
||||||
|
<div>
|
||||||
|
<h1 class="text-4xl">Gruppe: {i + 1}</h1>
|
||||||
|
{#each fightsGroup as fightsRound, j}
|
||||||
|
<div class="border-b border-gray-700">
|
||||||
|
<h1 class="text-2xl">Runde: {j + 1}</h1>
|
||||||
|
{#each fightsRound as fightTeams, k}
|
||||||
|
<div class="text-left p-4">
|
||||||
|
<span class="p-2 border border-gray-700 rounded"
|
||||||
|
>{new Intl.DateTimeFormat("de-DE", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
year: "2-digit",
|
||||||
|
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
second: "2-digit",
|
||||||
|
}).format(
|
||||||
|
startTime
|
||||||
|
.copy()
|
||||||
|
.add({
|
||||||
|
minutes: roundTime * j,
|
||||||
|
seconds: startDelay * (k + i * fightsRound.length),
|
||||||
|
})
|
||||||
|
.toDate()
|
||||||
|
)}</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" disabled={!generateDisabled} onclick={generateFights}>
|
||||||
|
<Plus />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
:global(#reseter::before) {
|
||||||
|
content: "Reset";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(#reseter) {
|
||||||
|
min-width: 14rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
116
src/components/moderator/pages/pages/EditorWithTabs.svelte
Normal file
116
src/components/moderator/pages/pages/EditorWithTabs.svelte
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Separator } from "@components/ui/separator";
|
||||||
|
import { manager, OpenEditPage } from "./page.svelte";
|
||||||
|
import { File, X } from "lucide-svelte";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { EditorView } from "@codemirror/view";
|
||||||
|
import { basicSetup } from "codemirror";
|
||||||
|
import EasyMDE from "easymde";
|
||||||
|
import "easymde/dist/easymde.min.css";
|
||||||
|
import { json } from "@codemirror/lang-json";
|
||||||
|
import { materialDark } from "@ddietr/codemirror-themes/theme/material-dark";
|
||||||
|
import FrontmatterEditor from "./FrontmatterEditor.svelte";
|
||||||
|
import { slide } from "svelte/transition";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
|
||||||
|
let codemirrorParent: HTMLElement | undefined = $state();
|
||||||
|
let easyMdeParent: HTMLElement | undefined = $state();
|
||||||
|
let easyMdeWrapper: HTMLElement | undefined = $state();
|
||||||
|
|
||||||
|
let easyMde: EasyMDE | null = $state(null);
|
||||||
|
let view: EditorView | null = $state(null);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
switch (manager.selectedPage?.fileType) {
|
||||||
|
case "md":
|
||||||
|
case "mdx":
|
||||||
|
easyMdeWrapper?.classList.remove("hidden");
|
||||||
|
codemirrorParent?.classList.add("hidden");
|
||||||
|
break;
|
||||||
|
case "json":
|
||||||
|
easyMdeWrapper?.classList.add("hidden");
|
||||||
|
codemirrorParent?.classList.remove("hidden");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
easyMdeWrapper?.classList.add("hidden");
|
||||||
|
codemirrorParent?.classList.add("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function updatePage(page: OpenEditPage | undefined) {
|
||||||
|
if (page) {
|
||||||
|
easyMde?.value(page.content || "");
|
||||||
|
view?.dispatch({
|
||||||
|
changes: { from: 0, to: view.state.doc.length, insert: page.content || "" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => updatePage(manager.selectedPage));
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
view = new EditorView({
|
||||||
|
doc: manager.selectedPage?.content || "",
|
||||||
|
parent: codemirrorParent,
|
||||||
|
extensions: [basicSetup, json(), materialDark],
|
||||||
|
});
|
||||||
|
easyMde = new EasyMDE({
|
||||||
|
element: easyMdeParent,
|
||||||
|
spellChecker: false,
|
||||||
|
initialValue: manager.selectedPage?.content || "",
|
||||||
|
});
|
||||||
|
|
||||||
|
easyMde.codemirror.on("change", () => {
|
||||||
|
if (manager.selectedPage?.content !== easyMde?.value()) {
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.selectedPage!.content = easyMde?.value() || "";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col h-full w-full">
|
||||||
|
<div class="h-8 flex">
|
||||||
|
{#each manager.pages as tab, index}
|
||||||
|
{@const isActive = manager.openPageIndex === index}
|
||||||
|
<button
|
||||||
|
class="flex pl-4 border-r group items-center hover:bg-neutral-800 transition-colors cursor-pointer h-full {isActive
|
||||||
|
? 'text-primary bg-neutral-900'
|
||||||
|
: 'text-muted-foreground'} {tab.dirty ? 'italic' : ''}"
|
||||||
|
onclick={() => (manager.openPageIndex = index)}
|
||||||
|
>
|
||||||
|
<File class="h-4 w-4 mr-2" />
|
||||||
|
{tab.pageTitle}
|
||||||
|
<span
|
||||||
|
class="mx-4 hover:bg-neutral-700 transition-all rounded {isActive ? '' : 'opacity-0'} group-hover:opacity-100 cursor-pointer"
|
||||||
|
onclick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
manager.closePage(index);
|
||||||
|
}}><X /></span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div class="flex-1 flex flex-col">
|
||||||
|
{#if manager.selectedPage}
|
||||||
|
<div class="flex items-center justify-end p-2">
|
||||||
|
<Button disabled={!(manager.selectedPage?.dirty ?? false)} onclick={() => manager.selectedPage?.save()}>Speichern</Button>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
{#if manager.selectedPage.path.startsWith("src/content/announcements/")}
|
||||||
|
<div class="border-b flex-1" transition:slide>
|
||||||
|
<FrontmatterEditor />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex-1">
|
||||||
|
<div bind:this={codemirrorParent} class="hidden h-full"></div>
|
||||||
|
<div bind:this={easyMdeWrapper} class="hidden h-full">
|
||||||
|
<textarea bind:this={easyMdeParent}></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
122
src/components/moderator/pages/pages/FrontmatterEditor.svelte
Normal file
122
src/components/moderator/pages/pages/FrontmatterEditor.svelte
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { X } from "lucide-svelte";
|
||||||
|
import { manager } from "./page.svelte";
|
||||||
|
import { slide } from "svelte/transition";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<details class="group">
|
||||||
|
<summary class="flex items-center justify-between p-3 cursor-pointer hover:bg-neutral-800">
|
||||||
|
<span class="font-medium">Frontmatter</span>
|
||||||
|
<svg class="w-4 h-4 transition-transform group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</summary>
|
||||||
|
<div class="p-3 border-t bg-neutral-900">
|
||||||
|
{#each Object.entries(manager.selectedPage?.frontmatter || {}) as [key, value]}
|
||||||
|
<div class="flex flex-col gap-2 mb-3 p-2 border rounded bg-neutral-800">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={key}
|
||||||
|
onchange={(e) => {
|
||||||
|
const newKey = (e.target as HTMLInputElement).value;
|
||||||
|
if (newKey !== key) {
|
||||||
|
manager.selectedPage!.frontmatter[newKey] = manager.selectedPage!.frontmatter[key];
|
||||||
|
delete manager.selectedPage?.frontmatter[key];
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
class="px-2 py-1 border rounded text-sm flex-shrink-0 w-32 bg-neutral-900"
|
||||||
|
placeholder="Key"
|
||||||
|
/>
|
||||||
|
<span>:</span>
|
||||||
|
{#if Array.isArray(value)}
|
||||||
|
<span class="text-xs text-muted-foreground">Array ({value.length} items)</span>
|
||||||
|
{:else if value instanceof Date || key === "created"}
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={value instanceof Date ? value.toISOString().split("T")[0] : typeof value === "string" ? value : ""}
|
||||||
|
onchange={(e) => {
|
||||||
|
const dateValue = (e.target as HTMLInputElement).value;
|
||||||
|
manager.selectedPage!.frontmatter[key] = dateValue ? new Date(dateValue) : "";
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}}
|
||||||
|
class="px-2 py-1 border rounded text-sm flex-1 bg-neutral-900"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={manager.selectedPage!.frontmatter[key]}
|
||||||
|
onchange={() => (manager.selectedPage!.dirty = true)}
|
||||||
|
class="px-2 py-1 border rounded text-sm flex-1 bg-neutral-900"
|
||||||
|
placeholder="Value"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
delete manager.selectedPage!.frontmatter[key];
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}}
|
||||||
|
class="text-red-500 hover:text-red-700 p-1"
|
||||||
|
>
|
||||||
|
<X class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#if Array.isArray(value)}
|
||||||
|
<div class="ml-4 space-y-1">
|
||||||
|
{#each value as item, index}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-muted-foreground w-6">[{index}]</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={manager.selectedPage!.frontmatter[key][index]}
|
||||||
|
onchange={() => (manager.selectedPage!.dirty = true)}
|
||||||
|
class="px-2 py-1 border rounded text-sm flex-1 bg-neutral-900"
|
||||||
|
placeholder="Array item"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
manager.selectedPage!.frontmatter[key].splice(index, 1);
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}}
|
||||||
|
class="text-red-500 hover:text-red-700 p-1"
|
||||||
|
>
|
||||||
|
<X class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
manager.selectedPage!.frontmatter[key].push("");
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}}
|
||||||
|
class="text-xs text-blue-500 hover:text-blue-700 ml-8"
|
||||||
|
>
|
||||||
|
+ Add item
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
manager.selectedPage!.frontmatter[`new_key_${Object.keys(manager.selectedPage!.frontmatter).length}`] = "";
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}}
|
||||||
|
class="text-sm text-blue-500 hover:text-blue-700"
|
||||||
|
>
|
||||||
|
+ Add field
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
manager.selectedPage!.frontmatter[`new_array_${Object.keys(manager.selectedPage!.frontmatter).length}`] = [];
|
||||||
|
manager.selectedPage!.dirty = true;
|
||||||
|
}}
|
||||||
|
class="text-sm text-green-500 hover:text-green-700"
|
||||||
|
>
|
||||||
|
+ Add array
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
155
src/components/moderator/pages/pages/Pages.svelte
Normal file
155
src/components/moderator/pages/pages/Pages.svelte
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ResizablePane, ResizablePaneGroup } from "@components/ui/resizable";
|
||||||
|
import { Separator } from "@components/ui/separator";
|
||||||
|
import { manager } from "./page.svelte";
|
||||||
|
import ResizableHandle from "@components/ui/resizable/resizable-handle.svelte";
|
||||||
|
import PagesList from "./PagesList.svelte";
|
||||||
|
import EditorWithTabs from "./EditorWithTabs.svelte";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { Check, ChevronsUpDown, RefreshCw, FileImage, Plus } from "lucide-svelte";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||||
|
import { cn } from "@components/utils";
|
||||||
|
import { pageRepo } from "@components/repo/page";
|
||||||
|
|
||||||
|
let branchSelectOpen = $state(false);
|
||||||
|
let imageSelectOpen = $state(false);
|
||||||
|
|
||||||
|
let fileInput: HTMLInputElement | undefined = $state();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex-grow flex flex-col">
|
||||||
|
<ResizablePaneGroup direction="horizontal" class="flex-grow">
|
||||||
|
<ResizablePane defaultSize={20}>
|
||||||
|
<div class="overflow-y-scroll">
|
||||||
|
<div class="flex p-2 gap-2">
|
||||||
|
<Popover bind:open={branchSelectOpen}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="outline" class="justify-between flex-1" {...props} role="combobox">
|
||||||
|
{manager.branch}
|
||||||
|
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search Branches..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No Branches Found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{#each manager.branches as branch}
|
||||||
|
<CommandItem
|
||||||
|
value={branch}
|
||||||
|
onSelect={() => {
|
||||||
|
if (manager.anyUnsavedChanges()) {
|
||||||
|
if (!confirm("You have unsaved changes. Are you sure you want to switch branches?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.branch = branch;
|
||||||
|
manager.pages = [];
|
||||||
|
branchSelectOpen = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check class={cn("mr-2 size-4", branch !== manager.branch && "text-transparent")} />
|
||||||
|
{branch}
|
||||||
|
</CommandItem>
|
||||||
|
{/each}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<Button size="icon" variant="outline" onclick={() => manager.reloadImages()}>
|
||||||
|
<RefreshCw />
|
||||||
|
</Button>
|
||||||
|
<Popover bind:open={imageSelectOpen}>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button size="icon" variant="outline" {...props}>
|
||||||
|
<FileImage />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent side="right" class="w-[1000px] h-screen overflow-y-auto">
|
||||||
|
{#await manager.imagesLoad}
|
||||||
|
<p>Loading images...</p>
|
||||||
|
{:then images}
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="p-2">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
bind:this={fileInput}
|
||||||
|
onchange={async (e) => {
|
||||||
|
const file = e.target?.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (event) => {
|
||||||
|
const base64 = event.target?.result?.toString().split(",")[1];
|
||||||
|
if (base64) {
|
||||||
|
await $pageRepo.createImage(file.name, base64, manager.branch);
|
||||||
|
manager.reloadImages();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button onclick={() => fileInput?.click()} class="w-full">
|
||||||
|
<Plus class="mr-2 size-4" />
|
||||||
|
Upload Image
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-4 gap-2 p-2">
|
||||||
|
{#each images as image}
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
const backs = (manager.selectedPage?.path?.match(/\//g)?.length || 1) - 1;
|
||||||
|
|
||||||
|
const path = [...Array(backs).fill(".."), image.path.replace("src/", "")].join("/");
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(path);
|
||||||
|
imageSelectOpen = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={image.downloadUrl} alt={image.name} class="w-full h-auto object-cover" />
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
onclick={async () => {
|
||||||
|
const branchName = prompt("Enter branch name:");
|
||||||
|
if (branchName) {
|
||||||
|
await $pageRepo.createBranch(branchName);
|
||||||
|
manager.reloadBranches();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
{#await manager.pagesLoad}
|
||||||
|
<p>Loading pages...</p>
|
||||||
|
{:then pages}
|
||||||
|
{#each Object.values(pages.dirs) as page}
|
||||||
|
<PagesList {page} path={page.name + "/"} />
|
||||||
|
{/each}
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
</ResizablePane>
|
||||||
|
<ResizableHandle />
|
||||||
|
<ResizablePane defaultSize={80}>
|
||||||
|
<EditorWithTabs />
|
||||||
|
</ResizablePane>
|
||||||
|
</ResizablePaneGroup>
|
||||||
|
</div>
|
||||||
116
src/components/moderator/pages/pages/PagesList.svelte
Normal file
116
src/components/moderator/pages/pages/PagesList.svelte
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ChevronDown, ChevronRight, Folder, FolderPlus, FileJson, FileText, File, FilePlus } from "lucide-svelte";
|
||||||
|
import type { DirTree } from "./page.svelte";
|
||||||
|
import PagesList from "./PagesList.svelte";
|
||||||
|
import { slide } from "svelte/transition";
|
||||||
|
import Button from "@components/ui/button/button.svelte";
|
||||||
|
import { manager } from "./page.svelte";
|
||||||
|
|
||||||
|
const { page, depth = 0, path }: { page: DirTree; depth?: number; path: string } = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let newPage = $state(false);
|
||||||
|
let newPageName = $state("");
|
||||||
|
|
||||||
|
let newPageInput: HTMLInputElement | undefined = $state();
|
||||||
|
|
||||||
|
function startNewPageCreate(e: Event) {
|
||||||
|
e.stopPropagation();
|
||||||
|
newPage = true;
|
||||||
|
newPageName = "";
|
||||||
|
open = true;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
newPageInput?.focus();
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewPage(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (newPageName.trim() === "") {
|
||||||
|
alert("Page name cannot be empty");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newPageName.match(/^[a-zA-Z0-9_\-\.]+$/)) {
|
||||||
|
alert("Invalid page name. Only alphanumeric characters, underscores, dashes, and dots are allowed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newPageName.endsWith(".json") && !newPageName.endsWith(".md") && !newPageName.endsWith(".mdx")) {
|
||||||
|
newPageName += ".md";
|
||||||
|
}
|
||||||
|
|
||||||
|
manager
|
||||||
|
.createPage(path + newPageName, newPageName)
|
||||||
|
.then(() => {
|
||||||
|
newPage = false;
|
||||||
|
newPageName = "";
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
alert("Error creating page: " + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button class={`group flex flex-row justify-between h-full w-full hover:bg-neutral-700 pl-${4 * depth}`} onclick={() => (open = !open)}>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
{#if open}
|
||||||
|
<ChevronDown class="w-6 h-6" />
|
||||||
|
{:else}
|
||||||
|
<ChevronRight class="w-6 h-6" />
|
||||||
|
{/if}
|
||||||
|
<Folder class="mr-2 w-4 h-4" />
|
||||||
|
{page.name}/
|
||||||
|
</div>
|
||||||
|
<div class="flex-row items-center hidden group-hover:flex">
|
||||||
|
<Button variant="ghost" size="sm" class="p-0 m-0 h-6 w-6" onclick={startNewPageCreate}>
|
||||||
|
<FilePlus class="w-3 h-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<div transition:slide={{ duration: 200, axis: "y" }}>
|
||||||
|
<div>
|
||||||
|
{#if newPage}
|
||||||
|
<button class={`flex flex-row items-center h-full py-1 w-full hover:bg-neutral-700 pl-${4 * (depth + 1)}`}>
|
||||||
|
{#if newPageName.endsWith(".json")}
|
||||||
|
<FileJson class="mr-2 w-4 h-4" />
|
||||||
|
{:else if newPageName.endsWith(".md") || newPageName.endsWith(".mdx")}
|
||||||
|
<FileText class="mr-2 w-4 h-4" />
|
||||||
|
{:else}
|
||||||
|
<File class="mr-2 w-4 h-4" />
|
||||||
|
{/if}
|
||||||
|
<form onsubmit={createNewPage}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={newPageName}
|
||||||
|
bind:this={newPageInput}
|
||||||
|
onblur={() => (newPage = false)}
|
||||||
|
placeholder="New page name"
|
||||||
|
class="flex-grow bg-transparent border-none outline-none text-white"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#each Object.values(page.dirs) as subPage (subPage.name)}
|
||||||
|
<PagesList page={subPage} depth={depth + 1} path={path + subPage.name + "/"} />
|
||||||
|
{/each}
|
||||||
|
{#each Object.values(page.files) as file (file.id)}
|
||||||
|
<button class={`flex flex-row items-center h-full py-1 w-full hover:bg-neutral-700 pl-${4 * (depth + 1)}`} onclick={() => manager.openPage(file.id)}>
|
||||||
|
{#if file.name.endsWith(".json")}
|
||||||
|
<FileJson class="mr-2 w-4 h-4" />
|
||||||
|
{:else if file.name.endsWith(".md") || file.name.endsWith(".mdx")}
|
||||||
|
<FileText class="mr-2 w-4 h-4" />
|
||||||
|
{:else}
|
||||||
|
<File class="mr-2 w-4 h-4" />
|
||||||
|
{/if}
|
||||||
|
{file.name}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
226
src/components/moderator/pages/pages/page.svelte.ts
Normal file
226
src/components/moderator/pages/pages/page.svelte.ts
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import { base64ToBytes } from "@components/admin/util";
|
||||||
|
import { pageRepo } from "@components/repo/page";
|
||||||
|
import type { ListPage, PageList } from "@components/types/page";
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
|
export class OpenEditPage {
|
||||||
|
public content: string = "";
|
||||||
|
public frontmatter: { [key: string]: string | string[] | Date } = $state({});
|
||||||
|
public dirty: boolean = $state(false);
|
||||||
|
|
||||||
|
public readonly fileType: string;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private manager: PageManager,
|
||||||
|
public readonly pageId: number,
|
||||||
|
public readonly pageTitle: string,
|
||||||
|
public readonly sha: string,
|
||||||
|
public readonly originalContent: string,
|
||||||
|
public readonly path: string
|
||||||
|
) {
|
||||||
|
this.fileType = this.path.split(".").pop() || "md";
|
||||||
|
|
||||||
|
this.content = this.removeFrontmatter(originalContent);
|
||||||
|
this.frontmatter = this.parseFrontmatter(originalContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async save(): Promise<void> {
|
||||||
|
if (!this.dirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentToSave = "";
|
||||||
|
if (this.frontmatter) {
|
||||||
|
contentToSave += "---\n";
|
||||||
|
contentToSave += yaml.dump(this.frontmatter);
|
||||||
|
contentToSave += "---\n\n";
|
||||||
|
}
|
||||||
|
contentToSave += this.content;
|
||||||
|
|
||||||
|
await get(pageRepo).updatePage(this.pageId, contentToSave, this.sha, prompt("Was hast du geändert?", `Updated ${this.pageTitle}`) ?? `Updated ${this.pageTitle}`, this.manager.branch);
|
||||||
|
this.dirty = false;
|
||||||
|
this.manager.reloadImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
public focus(): boolean {
|
||||||
|
let index = this.manager.pages.indexOf(this);
|
||||||
|
|
||||||
|
if (index === this.manager.openPageIndex) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.manager.openPageIndex = this.manager.pages.indexOf(this);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseFrontmatter(content: string): { [key: string]: string | string[] | Date } {
|
||||||
|
const lines = content.split("\n");
|
||||||
|
let inFrontmatter = false;
|
||||||
|
const frontmatterLines: string[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim() === "---") {
|
||||||
|
if (inFrontmatter) {
|
||||||
|
break; // End of frontmatter
|
||||||
|
}
|
||||||
|
inFrontmatter = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (inFrontmatter) {
|
||||||
|
frontmatterLines.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frontmatterLines.length === 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// You'll need to install js-yaml: npm install js-yaml @types/js-yaml
|
||||||
|
return (yaml.load(frontmatterLines.join("\n")) || {}) as { [key: string]: string | string[] | Date };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to parse YAML frontmatter:", error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeFrontmatter(content: string): string {
|
||||||
|
const lines = content.split("\n");
|
||||||
|
let inFrontmatter = false;
|
||||||
|
const result: string[] = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim() === "---") {
|
||||||
|
inFrontmatter = !inFrontmatter;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!inFrontmatter) {
|
||||||
|
result.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.join("\n").trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DirTree {
|
||||||
|
name: string;
|
||||||
|
dirs: { [key: string]: DirTree };
|
||||||
|
files: { [key: string]: ListPage };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PageManager {
|
||||||
|
public reloadImages() {
|
||||||
|
this.updater = this.updater + 1;
|
||||||
|
}
|
||||||
|
public branch: string = $state("master");
|
||||||
|
public pages: OpenEditPage[] = $state([]);
|
||||||
|
public branches: string[] = $state([]);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.reloadBranches();
|
||||||
|
}
|
||||||
|
|
||||||
|
public reloadBranches() {
|
||||||
|
get(pageRepo)
|
||||||
|
.getBranches()
|
||||||
|
.then((branches) => {
|
||||||
|
this.branches = branches;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updater = $state(0);
|
||||||
|
|
||||||
|
public openPageIndex: number = $state(-1);
|
||||||
|
public pagesLoad = $derived(get(pageRepo).listPages(this.branch).then(this.convertToTree).then(this._t(this.updater)));
|
||||||
|
public imagesLoad = $derived(get(pageRepo).listImages(this.branch).then(this._t(this.updater)));
|
||||||
|
|
||||||
|
private _t<T>(n: number): (v: T) => T {
|
||||||
|
return (v: T) => v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectedPage = $derived(this.openPageIndex >= 0 ? this.pages[this.openPageIndex] : undefined);
|
||||||
|
|
||||||
|
private convertToTree(pages: PageList): DirTree {
|
||||||
|
const tree: DirTree = { dirs: {}, files: {}, name: "/" };
|
||||||
|
|
||||||
|
pages.forEach((page) => {
|
||||||
|
const pathParts = page.path.split("/").filter((part) => part !== "");
|
||||||
|
let current = tree;
|
||||||
|
|
||||||
|
// Navigate/create directory structure
|
||||||
|
for (let i = 0; i < pathParts.length - 1; i++) {
|
||||||
|
const dir = pathParts[i];
|
||||||
|
if (!current.dirs[dir]) {
|
||||||
|
current.dirs[dir] = { dirs: {}, files: {}, name: dir };
|
||||||
|
}
|
||||||
|
current = current.dirs[dir];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add file to the final directory
|
||||||
|
const fileName = pathParts[pathParts.length - 1];
|
||||||
|
current.files[fileName] = page;
|
||||||
|
});
|
||||||
|
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async openPage(pageId: number) {
|
||||||
|
const existingPage = this.existingPage(pageId);
|
||||||
|
if (existingPage) {
|
||||||
|
existingPage.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let r = await get(pageRepo).getPage(pageId, this.branch);
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPage = new OpenEditPage(this, pageId, r.name, r.sha, new TextDecoder().decode(base64ToBytes(r.content)), r.path);
|
||||||
|
this.pages.push(newPage);
|
||||||
|
newPage.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public existingPage(pageId: number): OpenEditPage | undefined {
|
||||||
|
return this.pages.find((page) => page.pageId === pageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public closePage(index: number) {
|
||||||
|
if (index < 0 || index >= this.pages.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = this.pages[index];
|
||||||
|
if (page.dirty) {
|
||||||
|
if (!confirm(`The page "${page.pageTitle}" has unsaved changes. Are you sure you want to close it?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pages.splice(index, 1);
|
||||||
|
if (this.openPageIndex >= index) {
|
||||||
|
this.openPageIndex = Math.max(0, this.openPageIndex - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.openPageIndex < 0 && this.pages.length > 0) {
|
||||||
|
this.openPageIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pages.length === 0) {
|
||||||
|
this.openPageIndex = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createPage(path: string, newPageName: string): Promise<void> {
|
||||||
|
await get(pageRepo).createFile(path, this.branch, newPageName, newPageName);
|
||||||
|
this.branch = this.branch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public anyUnsavedChanges() {
|
||||||
|
return this.pages.some((page) => page.dirty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const manager = $state(new PageManager());
|
||||||
@ -17,26 +17,38 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {Player, Server} from "@type/data.ts";
|
import type { Player, Server } from "@type/data.ts";
|
||||||
import {PlayerSchema, ServerSchema} from "@type/data.ts";
|
import { PlayerSchema, ServerSchema } from "@type/data.ts";
|
||||||
import {fetchWithToken, tokenStore} from "./repo.ts";
|
import { fetchWithToken, tokenStore } from "./repo.ts";
|
||||||
import {derived, get} from "svelte/store";
|
import { derived, get } from "svelte/store";
|
||||||
|
import { TeamSchema, type Team } from "@components/types/team.ts";
|
||||||
|
|
||||||
export class DataRepo {
|
export class DataRepo {
|
||||||
constructor(private token: string) {
|
constructor(private token: string) {}
|
||||||
}
|
|
||||||
|
|
||||||
public async getServer(): Promise<Server> {
|
public async getServer(): Promise<Server> {
|
||||||
return await fetchWithToken(this.token, "/data/server").then(value => value.json()).then(ServerSchema.parse);
|
return await fetchWithToken(this.token, "/data/server")
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then(ServerSchema.parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMe(): Promise<Player> {
|
public async getMe(): Promise<Player> {
|
||||||
return await fetchWithToken(this.token, "/data/me").then(value => value.json()).then(PlayerSchema.parse);
|
return await fetchWithToken(this.token, "/data/me")
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then(PlayerSchema.parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPlayers(): Promise<Player[]> {
|
public async getPlayers(): Promise<Player[]> {
|
||||||
return await fetchWithToken(get(tokenStore), "/data/admin/users").then(value => value.json()).then(PlayerSchema.array().parse);
|
return await fetchWithToken(get(tokenStore), "/data/admin/users")
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then(PlayerSchema.array().parse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getTeams(): Promise<Team[]> {
|
||||||
|
return await fetchWithToken(get(tokenStore), "/data/admin/teams")
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then(TeamSchema.array().parse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dataRepo = derived(tokenStore, ($token) => new DataRepo($token));
|
export const dataRepo = derived(tokenStore, ($token) => new DataRepo($token));
|
||||||
|
|||||||
@ -17,12 +17,26 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {ExtendedEvent, ShortEvent, SWEvent} from "@type/event";
|
import type { ExtendedEvent, ShortEvent, SWEvent, EventFight, ResponseGroups, ResponseRelation, ResponseTeam } from "@type/event";
|
||||||
import {fetchWithToken, tokenStore} from "./repo";
|
import { fetchWithToken, tokenStore } from "./repo";
|
||||||
import {ExtendedEventSchema, ShortEventSchema, SWEventSchema} from "@type/event.js";
|
import {
|
||||||
import {z} from "zod";
|
ExtendedEventSchema,
|
||||||
import type {Dayjs} from "dayjs";
|
ShortEventSchema,
|
||||||
import {derived} from "svelte/store";
|
SWEventSchema,
|
||||||
|
EventFightSchema,
|
||||||
|
ResponseGroupsSchema,
|
||||||
|
ResponseRelationSchema,
|
||||||
|
ResponseTeamSchema,
|
||||||
|
CreateEventGroupSchema,
|
||||||
|
UpdateEventGroupSchema,
|
||||||
|
CreateEventRelationSchema,
|
||||||
|
UpdateEventRelationSchema,
|
||||||
|
} from "@type/event.js";
|
||||||
|
import type { CreateEventGroup, UpdateEventGroup, CreateEventRelation, UpdateEventRelation } from "@type/event.js";
|
||||||
|
import { z } from "zod";
|
||||||
|
import type { Dayjs } from "dayjs";
|
||||||
|
import { derived } from "svelte/store";
|
||||||
|
import { ResponseUserSchema } from "@components/types/data";
|
||||||
|
|
||||||
export interface CreateEvent {
|
export interface CreateEvent {
|
||||||
name: string;
|
name: string;
|
||||||
@ -42,19 +56,25 @@ export interface UpdateEvent {
|
|||||||
removeReferee?: string[] | null;
|
removeReferee?: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResponseUser {
|
||||||
|
name: string;
|
||||||
|
uuid: string;
|
||||||
|
prefix: string;
|
||||||
|
perms: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export class EventRepo {
|
export class EventRepo {
|
||||||
constructor(private token: string) {
|
constructor(private token: string) {}
|
||||||
}
|
|
||||||
|
|
||||||
public async listEvents(): Promise<ShortEvent[]> {
|
public async listEvents(): Promise<ShortEvent[]> {
|
||||||
return await fetchWithToken(this.token, "/events")
|
return await fetchWithToken(this.token, "/events")
|
||||||
.then(value => value.json())
|
.then((value) => value.json())
|
||||||
.then(value => z.array(ShortEventSchema).parse(value));
|
.then((value) => z.array(ShortEventSchema).parse(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getEvent(id: string): Promise<ExtendedEvent> {
|
public async getEvent(id: string): Promise<ExtendedEvent> {
|
||||||
return await fetchWithToken(this.token, `/events/${id}`)
|
return await fetchWithToken(this.token, `/events/${id}`)
|
||||||
.then(value => value.json())
|
.then((value) => value.json())
|
||||||
.then(ExtendedEventSchema.parse);
|
.then(ExtendedEventSchema.parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +86,8 @@ export class EventRepo {
|
|||||||
start: +event.start,
|
start: +event.start,
|
||||||
end: +event.end,
|
end: +event.end,
|
||||||
}),
|
}),
|
||||||
}).then(value => value.json())
|
})
|
||||||
|
.then((value) => value.json())
|
||||||
.then(SWEventSchema.parse);
|
.then(SWEventSchema.parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +108,8 @@ export class EventRepo {
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
}).then(value => value.json())
|
})
|
||||||
|
.then((value) => value.json())
|
||||||
.then(SWEventSchema.parse);
|
.then(SWEventSchema.parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +120,154 @@ export class EventRepo {
|
|||||||
|
|
||||||
return res.ok;
|
return res.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fights
|
||||||
|
public async listFights(eventId: string): Promise<EventFight[]> {
|
||||||
|
return await fetchWithToken(this.token, `/events/${eventId}/fights`)
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then((value) => z.array(EventFightSchema).parse(value));
|
||||||
|
}
|
||||||
|
public async createFight(eventId: string, fight: any): Promise<EventFight> {
|
||||||
|
delete fight.ergebnis;
|
||||||
|
return await fetchWithToken(this.token, `/events/${eventId}/fights`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(fight),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
})
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then(EventFightSchema.parse);
|
||||||
|
}
|
||||||
|
public async deleteFight(eventId: string, fightId: string): Promise<boolean> {
|
||||||
|
const res = await fetchWithToken(this.token, `/events/${eventId}/fights/${fightId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
return res.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Groups
|
||||||
|
public async listGroups(eventId: string): Promise<ResponseGroups[]> {
|
||||||
|
return await fetchWithToken(this.token, `/events/${eventId}/groups`)
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then((value) => z.array(ResponseGroupsSchema).parse(value));
|
||||||
|
}
|
||||||
|
public async createGroup(eventId: number, group: CreateEventGroup): Promise<ResponseGroups> {
|
||||||
|
CreateEventGroupSchema.parse(group);
|
||||||
|
return await fetchWithToken(this.token, `/events/${eventId}/groups`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: group.name,
|
||||||
|
type: group.type,
|
||||||
|
}),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
})
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then(ResponseGroupsSchema.parse);
|
||||||
|
}
|
||||||
|
public async getGroup(eventId: string, groupId: string): Promise<ResponseGroups> {
|
||||||
|
return await fetchWithToken(this.token, `/events/${eventId}/groups/${groupId}`)
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then(ResponseGroupsSchema.parse);
|
||||||
|
}
|
||||||
|
public async updateGroup(eventId: string, groupId: string, group: UpdateEventGroup): Promise<ResponseGroups> {
|
||||||
|
UpdateEventGroupSchema.parse(group);
|
||||||
|
return await fetchWithToken(this.token, `/events/${eventId}/groups/${groupId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(group),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
})
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then(ResponseGroupsSchema.parse);
|
||||||
|
}
|
||||||
|
public async deleteGroup(eventId: string, groupId: string): Promise<boolean> {
|
||||||
|
const res = await fetchWithToken(this.token, `/events/${eventId}/groups/${groupId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
return res.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
public async listRelations(eventId: number): Promise<ResponseRelation[]> {
|
||||||
|
return await fetchWithToken(this.token, `/events/${eventId}/relations`)
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then((value) => z.array(ResponseRelationSchema).parse(value));
|
||||||
|
}
|
||||||
|
public async createRelation(eventId: number, relation: CreateEventRelation): Promise<ResponseRelation> {
|
||||||
|
CreateEventRelationSchema.parse(relation);
|
||||||
|
return await fetchWithToken(this.token, `/events/${eventId}/relations`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(relation),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
})
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then(ResponseRelationSchema.parse);
|
||||||
|
}
|
||||||
|
public async getRelation(eventId: string, relationId: string): Promise<ResponseRelation> {
|
||||||
|
return await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`)
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then(ResponseRelationSchema.parse);
|
||||||
|
}
|
||||||
|
public async updateRelation(eventId: number, relationId: number, relation: UpdateEventRelation): Promise<ResponseRelation> {
|
||||||
|
UpdateEventRelationSchema.parse(relation);
|
||||||
|
return await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(relation),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
})
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then(ResponseRelationSchema.parse);
|
||||||
|
}
|
||||||
|
public async deleteRelation(eventId: number, relationId: number): Promise<boolean> {
|
||||||
|
const res = await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
return res.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teams
|
||||||
|
public async listTeams(eventId: string): Promise<ResponseTeam[]> {
|
||||||
|
return await fetchWithToken(this.token, `/events/${eventId}/teams`)
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then((value) => z.array(ResponseTeamSchema).parse(value));
|
||||||
|
}
|
||||||
|
public async updateTeams(eventId: string, teams: number[]): Promise<boolean> {
|
||||||
|
const res = await fetchWithToken(this.token, `/events/${eventId}/teams`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(teams),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
return res.ok;
|
||||||
|
}
|
||||||
|
public async deleteTeams(eventId: string, teams: number[]): Promise<boolean> {
|
||||||
|
const res = await fetchWithToken(this.token, `/events/${eventId}/teams`, {
|
||||||
|
method: "DELETE",
|
||||||
|
body: JSON.stringify(teams),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
return res.ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Referees
|
||||||
|
public async listReferees(eventId: string): Promise<ResponseUser[]> {
|
||||||
|
return await fetchWithToken(this.token, `/events/${eventId}/referees`)
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then((value) => z.array(ResponseUserSchema).parse(value));
|
||||||
|
}
|
||||||
|
public async updateReferees(eventId: string, refereeUuids: string[]): Promise<boolean> {
|
||||||
|
const res = await fetchWithToken(this.token, `/events/${eventId}/referees`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify(refereeUuids),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
return res.status === 204;
|
||||||
|
}
|
||||||
|
public async deleteReferees(eventId: string, refereeUuids: string[]): Promise<boolean> {
|
||||||
|
const res = await fetchWithToken(this.token, `/events/${eventId}/referees`, {
|
||||||
|
method: "DELETE",
|
||||||
|
body: JSON.stringify(refereeUuids),
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
return res.status === 204;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const eventRepo = derived(tokenStore, ($token) => new EventRepo($token));
|
export const eventRepo = derived(tokenStore, ($token) => new EventRepo($token));
|
||||||
|
|||||||
@ -17,12 +17,12 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {EventFight} from "@type/event.js";
|
import type { EventFight } from "@type/event.js";
|
||||||
import {fetchWithToken, tokenStore} from "./repo";
|
import { fetchWithToken, tokenStore } from "./repo";
|
||||||
import {z} from "zod";
|
import { z } from "zod";
|
||||||
import {EventFightSchema} from "@type/event.js";
|
import { EventFightSchema } from "@type/event.js";
|
||||||
import type {Dayjs} from "dayjs";
|
import type { Dayjs } from "dayjs";
|
||||||
import {derived} from "svelte/store";
|
import { derived } from "svelte/store";
|
||||||
|
|
||||||
export interface CreateFight {
|
export interface CreateFight {
|
||||||
spielmodus: string;
|
spielmodus: string;
|
||||||
@ -31,7 +31,7 @@ export interface CreateFight {
|
|||||||
redTeam: number;
|
redTeam: number;
|
||||||
start: Dayjs;
|
start: Dayjs;
|
||||||
spectatePort: number | null;
|
spectatePort: number | null;
|
||||||
group: string | null;
|
group: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateFight {
|
export interface UpdateFight {
|
||||||
@ -39,26 +39,24 @@ export interface UpdateFight {
|
|||||||
map: string | null;
|
map: string | null;
|
||||||
blueTeam: number | null;
|
blueTeam: number | null;
|
||||||
redTeam: number | null;
|
redTeam: number | null;
|
||||||
start: Dayjs | null;
|
start: number | null;
|
||||||
spectatePort: number | null;
|
spectatePort: number | null;
|
||||||
group: string | null;
|
group: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FightRepo {
|
export class FightRepo {
|
||||||
constructor(private token: string) {
|
constructor(private token: string) {}
|
||||||
}
|
|
||||||
|
|
||||||
public async listFights(eventId: number): Promise<EventFight[]> {
|
public async listFights(eventId: number): Promise<EventFight[]> {
|
||||||
return await fetchWithToken(this.token, `/events/${eventId}/fights`)
|
return await fetchWithToken(this.token, `/events/${eventId}/fights`)
|
||||||
.then(value => value.json())
|
.then((value) => value.json())
|
||||||
.then(value => z.array(EventFightSchema).parse(value));
|
.then((value) => z.array(EventFightSchema).parse(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createFight(eventId: number, fight: CreateFight): Promise<EventFight> {
|
public async createFight(eventId: number, fight: CreateFight): Promise<EventFight> {
|
||||||
return await fetchWithToken(this.token, "/fights", {
|
return await fetchWithToken(this.token, `/events/${eventId}/fights`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
event: eventId,
|
|
||||||
spielmodus: fight.spielmodus,
|
spielmodus: fight.spielmodus,
|
||||||
map: fight.map,
|
map: fight.map,
|
||||||
blueTeam: fight.blueTeam,
|
blueTeam: fight.blueTeam,
|
||||||
@ -67,28 +65,25 @@ export class FightRepo {
|
|||||||
spectatePort: fight.spectatePort,
|
spectatePort: fight.spectatePort,
|
||||||
group: fight.group,
|
group: fight.group,
|
||||||
}),
|
}),
|
||||||
}).then(value => value.json())
|
})
|
||||||
|
.then((value) => value.json())
|
||||||
.then(EventFightSchema.parse);
|
.then(EventFightSchema.parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateFight(fightId: number, fight: UpdateFight): Promise<EventFight> {
|
public async updateFight(eventId: number, fightId: number, fight: UpdateFight): Promise<EventFight> {
|
||||||
return await fetchWithToken(this.token, `/fights/${fightId}`, {
|
return await fetchWithToken(this.token, `/events/${eventId}/fights/${fightId}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
spielmodus: fight.spielmodus,
|
...fight,
|
||||||
map: fight.map,
|
|
||||||
blueTeam: fight.blueTeam,
|
|
||||||
redTeam: fight.redTeam,
|
|
||||||
start: fight.start?.valueOf(),
|
start: fight.start?.valueOf(),
|
||||||
spectatePort: fight.spectatePort,
|
|
||||||
group: fight.group,
|
|
||||||
}),
|
}),
|
||||||
}).then(value => value.json())
|
})
|
||||||
|
.then((value) => value.json())
|
||||||
.then(EventFightSchema.parse);
|
.then(EventFightSchema.parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteFight(fightId: number): Promise<void> {
|
public async deleteFight(eventId: number, fightId: number): Promise<void> {
|
||||||
const res = await fetchWithToken(this.token, `/fights/${fightId}`, {
|
const res = await fetchWithToken(this.token, `/events/${eventId}/fights/${fightId}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -17,27 +17,26 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {Page, PageList} from "@type/page.ts";
|
import type { Page, PageList } from "@type/page.ts";
|
||||||
import {fetchWithToken, tokenStore} from "./repo.ts";
|
import { fetchWithToken, tokenStore } from "./repo.ts";
|
||||||
import {PageListSchema, PageSchema} from "@type/page.ts";
|
import { PageListSchema, PageSchema } from "@type/page.ts";
|
||||||
import {bytesToBase64} from "../admin/util.ts";
|
import { bytesToBase64 } from "../admin/util.ts";
|
||||||
import {z} from "zod";
|
import { z } from "zod";
|
||||||
import {derived} from "svelte/store";
|
import { derived } from "svelte/store";
|
||||||
|
|
||||||
export class PageRepo {
|
export class PageRepo {
|
||||||
constructor(private token: string) {
|
constructor(private token: string) {}
|
||||||
}
|
|
||||||
|
|
||||||
public async listPages(branch: string = "master"): Promise<PageList> {
|
public async listPages(branch: string = "master"): Promise<PageList> {
|
||||||
return await fetchWithToken(this.token, `/page?branch=${branch}`)
|
return await fetchWithToken(this.token, `/page?branch=${branch}`)
|
||||||
.then(value => value.json())
|
.then((value) => value.json())
|
||||||
.then(PageListSchema.parse)
|
.then(PageListSchema.parse)
|
||||||
.then(value => value.map(value1 => ({...value1, path: value1.path.replace("src/content/", "")})));
|
.then((value) => value.map((value1) => ({ ...value1, path: value1.path.replace("src/content/", "") })));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPage(id: number, branch: string = "master"): Promise<Page> {
|
public async getPage(id: number, branch: string = "master"): Promise<Page> {
|
||||||
return await fetchWithToken(this.token, `/page/${id}?branch=${branch}`)
|
return await fetchWithToken(this.token, `/page/${id}?branch=${branch}`)
|
||||||
.then(value => value.json())
|
.then((value) => value.json())
|
||||||
.then(PageSchema.parse);
|
.then(PageSchema.parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,42 +45,57 @@ export class PageRepo {
|
|||||||
method: "PUT",
|
method: "PUT",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
content: bytesToBase64(new TextEncoder().encode(content)),
|
content: bytesToBase64(new TextEncoder().encode(content)),
|
||||||
sha, message,
|
sha,
|
||||||
|
message,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getBranches(): Promise<string[]> {
|
public async getBranches(): Promise<string[]> {
|
||||||
return await fetchWithToken(this.token, "/page/branch")
|
return await fetchWithToken(this.token, "/page/branch")
|
||||||
.then(value => value.json())
|
.then((value) => value.json())
|
||||||
.then(value => z.array(z.string()).parse(value));
|
.then((value) => z.array(z.string()).parse(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createBranch(branch: string): Promise<void> {
|
public async createBranch(branch: string): Promise<void> {
|
||||||
await fetchWithToken(this.token, "/page/branch", {method: "POST", body: JSON.stringify({branch})});
|
await fetchWithToken(this.token, "/page/branch", { method: "POST", body: JSON.stringify({ branch }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteBranch(branch: string): Promise<void> {
|
public async deleteBranch(branch: string): Promise<void> {
|
||||||
await fetchWithToken(this.token, "/page/branch", {method: "DELETE", body: JSON.stringify({branch})});
|
await fetchWithToken(this.token, "/page/branch", { method: "DELETE", body: JSON.stringify({ branch }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createFile(path: string, branch: string = "master", slug: string | null = null, title: string | null = null): Promise<void> {
|
public async createFile(path: string, branch: string = "master", slug: string | null = null, title: string | null = null): Promise<void> {
|
||||||
await fetchWithToken(this.token, `/page?branch=${branch}`, {method: "POST", body: JSON.stringify({path, slug, title})});
|
await fetchWithToken(this.token, `/page?branch=${branch}`, { method: "POST", body: JSON.stringify({ path, slug, title }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async merge(branch: string, message: string): Promise<void> {
|
public async merge(branch: string, message: string): Promise<void> {
|
||||||
await fetchWithToken(this.token, "/page/branch/merge", {
|
await fetchWithToken(this.token, "/page/branch/merge", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({branch, message}),
|
body: JSON.stringify({ branch, message }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deletePage(id: number, message: string, sha: string, branch: string = "master"): Promise<void> {
|
public async deletePage(id: number, message: string, sha: string, branch: string = "master"): Promise<void> {
|
||||||
await fetchWithToken(this.token, `/page/${id}?branch=${branch}`, {
|
await fetchWithToken(this.token, `/page/${id}?branch=${branch}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
body: JSON.stringify({message, sha}),
|
body: JSON.stringify({ message, sha }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async listImages(branch: string = "master"): Promise<PageList> {
|
||||||
|
return await fetchWithToken(this.token, `/page/images?branch=${branch}`)
|
||||||
|
.then((value) => value.json())
|
||||||
|
.then(PageListSchema.parse)
|
||||||
|
.then((value) => value.map((value1) => ({ ...value1, path: value1.path.replace("src/content/", "") })));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createImage(name: string, data: string, branch: string = "master"): Promise<void> {
|
||||||
|
await fetchWithToken(this.token, `/page/images?branch=${branch}`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ name, data }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const pageRepo = derived(tokenStore, ($token) => new PageRepo($token));
|
export const pageRepo = derived(tokenStore, ($token) => new PageRepo($token));
|
||||||
|
|||||||
@ -17,20 +17,31 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {readonly, writable} from "svelte/store";
|
import { readonly, writable } from "svelte/store";
|
||||||
|
|
||||||
import type {Readable, Subscriber, Unsubscriber} from "svelte/store";
|
import type { Readable, Subscriber, Unsubscriber } from "svelte/store";
|
||||||
|
|
||||||
export interface Cached<T> extends Readable<T> {
|
export interface Cached<T> extends Readable<T> {
|
||||||
reload: () => void;
|
reload: () => void;
|
||||||
|
future: Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cached<T>(normal: T, init: () => Promise<T>): Cached<T> {
|
export function cached<T>(normal: T, init: () => Promise<T>): Cached<T> {
|
||||||
const store = writable<T>(normal);
|
const store = writable<T>(normal);
|
||||||
|
const future = new Promise<T>((resolve) => {
|
||||||
|
let f = false;
|
||||||
|
store.subscribe((value) => {
|
||||||
|
if (f) {
|
||||||
|
resolve(value);
|
||||||
|
} else {
|
||||||
|
f = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
let first = true;
|
let first = true;
|
||||||
|
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
init().then(data => {
|
init().then((data) => {
|
||||||
store.set(data);
|
store.set(data);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -45,6 +56,7 @@ export function cached<T>(normal: T, init: () => Promise<T>): Cached<T> {
|
|||||||
return store.subscribe(run, invalidate);
|
return store.subscribe(run, invalidate);
|
||||||
},
|
},
|
||||||
reload,
|
reload,
|
||||||
|
future,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +70,7 @@ export function cachedFamily<T, K>(normal: K, init: (arg0: T) => Promise<K>): (a
|
|||||||
let first = true;
|
let first = true;
|
||||||
|
|
||||||
const reload = () => {
|
const reload = () => {
|
||||||
init(arg).then(data => {
|
init(arg).then((data) => {
|
||||||
store.set(data);
|
store.set(data);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,41 +17,45 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {Player, SchematicType} from "@type/data";
|
import type { Player, SchematicType } from "@type/data";
|
||||||
import {PlayerSchema} from "@type/data.ts";
|
import { PlayerSchema } from "@type/data.ts";
|
||||||
import {cached, cachedFamily} from "./cached";
|
import { cached, cachedFamily } from "./cached";
|
||||||
import type {Team} from "@type/team.ts";
|
import type { Team } from "@type/team.ts";
|
||||||
import {TeamSchema} from "@type/team";
|
import { TeamSchema } from "@type/team";
|
||||||
import {derived, get, writable} from "svelte/store";
|
import { derived, get, writable } from "svelte/store";
|
||||||
import {z} from "zod";
|
import { z } from "zod";
|
||||||
import {fetchWithToken, tokenStore} from "@repo/repo.ts";
|
import { fetchWithToken, tokenStore } from "@repo/repo.ts";
|
||||||
import {pageRepo} from "@repo/page.ts";
|
import { pageRepo } from "@repo/page.ts";
|
||||||
import {dataRepo} from "@repo/data.ts";
|
import { dataRepo } from "@repo/data.ts";
|
||||||
import {permsRepo} from "@repo/perms.ts";
|
import { permsRepo } from "@repo/perms.ts";
|
||||||
|
|
||||||
export const schemTypes = cached<SchematicType[]>([], () =>
|
export const schemTypes = cached<SchematicType[]>([], () => fetchWithToken(get(tokenStore), "/data/admin/schematicTypes").then((res) => res.json()));
|
||||||
fetchWithToken(get(tokenStore), "/data/admin/schematicTypes")
|
|
||||||
.then(res => res.json()));
|
|
||||||
|
|
||||||
export const players = cached<Player[]>([], async () => {
|
export const players = cached<Player[]>([], async () => {
|
||||||
const res = await fetchWithToken(get(tokenStore), "/data/admin/users");
|
return get(dataRepo).getPlayers();
|
||||||
return z.array(PlayerSchema).parse(await res.json());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const permissions = cached({
|
export const teams = cached<Team[]>([], async () => {
|
||||||
perms: [],
|
return get(dataRepo).getTeams();
|
||||||
prefixes: {},
|
|
||||||
}, async () => {
|
|
||||||
return get(permsRepo).listPerms();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const permissions = cached(
|
||||||
|
{
|
||||||
|
perms: [],
|
||||||
|
prefixes: {},
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
return get(permsRepo).listPerms();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const gamemodes = cached<string[]>([], async () => {
|
export const gamemodes = cached<string[]>([], async () => {
|
||||||
const res = await fetchWithToken(get(tokenStore), "/data/admin/gamemodes");
|
const res = await fetchWithToken(get(tokenStore), "/data/admin/gamemodes");
|
||||||
return z.array(z.string()).parse(await res.json());
|
return z.array(z.string()).parse(await res.json());
|
||||||
});
|
});
|
||||||
|
|
||||||
export const maps = cachedFamily<string, string[]>([], async (gamemode) => {
|
export const maps = cachedFamily<string, string[]>([], async (gamemode) => {
|
||||||
if (get(gamemodes).every(value => value !== gamemode)) return [];
|
if ((await gamemodes.future).every((value) => value !== gamemode)) return [];
|
||||||
|
|
||||||
const res = await fetchWithToken(get(tokenStore), `/data/admin/gamemodes/${gamemode}/maps`);
|
const res = await fetchWithToken(get(tokenStore), `/data/admin/gamemodes/${gamemode}/maps`);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@ -66,17 +70,12 @@ export const groups = cached<string[]>([], async () => {
|
|||||||
return z.array(z.string()).parse(await res.json());
|
return z.array(z.string()).parse(await res.json());
|
||||||
});
|
});
|
||||||
|
|
||||||
export const teams = cached<Team[]>([], async () => {
|
|
||||||
const res = await fetchWithToken(get(tokenStore), "/team");
|
|
||||||
return z.array(TeamSchema).parse(await res.json());
|
|
||||||
});
|
|
||||||
|
|
||||||
export const branches = cached<string[]>([], async () => {
|
export const branches = cached<string[]>([], async () => {
|
||||||
const res = await get(pageRepo).getBranches();
|
const res = await get(pageRepo).getBranches();
|
||||||
return z.array(z.string()).parse(res);
|
return z.array(z.string()).parse(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const server = derived(dataRepo, $dataRepo => $dataRepo.getServer());
|
export const server = derived(dataRepo, ($dataRepo) => $dataRepo.getServer());
|
||||||
|
|
||||||
export const isWide = writable(typeof window !== "undefined" && window.innerWidth >= 640);
|
export const isWide = writable(typeof window !== "undefined" && window.innerWidth >= 640);
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {z} from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const SchematicTypeSchema = z.object({
|
export const SchematicTypeSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
@ -57,3 +57,12 @@ export const ResponseErrorSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type ResponseError = z.infer<typeof ResponseErrorSchema>;
|
export type ResponseError = z.infer<typeof ResponseErrorSchema>;
|
||||||
|
|
||||||
|
export const ResponseUserSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
uuid: z.string(),
|
||||||
|
prefix: z.string(),
|
||||||
|
perms: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ResponseUser = z.infer<typeof ResponseUserSchema>;
|
||||||
|
|||||||
@ -17,9 +17,58 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {z} from "zod";
|
import { z } from "zod";
|
||||||
import {TeamSchema} from "./team.js";
|
import { TeamSchema } from "./team.js";
|
||||||
import {PlayerSchema} from "./data.js";
|
import { PlayerSchema, ResponseUserSchema } from "./data.js";
|
||||||
|
|
||||||
|
export const ResponseGroupsSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
pointsPerWin: z.number(),
|
||||||
|
pointsPerLoss: z.number(),
|
||||||
|
pointsPerDraw: z.number(),
|
||||||
|
type: z.enum(["GROUP_STAGE", "ELIMINATION_STAGE"]),
|
||||||
|
points: z.record(z.string(), z.number()).nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const EventFightSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
spielmodus: z.string(),
|
||||||
|
map: z.string(),
|
||||||
|
blueTeam: TeamSchema,
|
||||||
|
redTeam: TeamSchema,
|
||||||
|
start: z.number(),
|
||||||
|
ergebnis: z.number(),
|
||||||
|
spectatePort: z.number().nullable(),
|
||||||
|
group: ResponseGroupsSchema.nullable(),
|
||||||
|
hasFinished: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type EventFight = z.infer<typeof EventFightSchema>;
|
||||||
|
|
||||||
|
export const EventFightEditSchema = EventFightSchema.omit({
|
||||||
|
id: true,
|
||||||
|
group: true,
|
||||||
|
hasFinished: true,
|
||||||
|
}).extend({
|
||||||
|
group: z.number().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type EventFightEdit = z.infer<typeof EventFightEditSchema>;
|
||||||
|
|
||||||
|
export type ResponseGroups = z.infer<typeof ResponseGroupsSchema>;
|
||||||
|
|
||||||
|
export const ResponseRelationSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
fight: z.number(),
|
||||||
|
team: z.enum(["RED", "BLUE"]),
|
||||||
|
type: z.enum(["FIGHT", "GROUP"]),
|
||||||
|
fromFight: EventFightSchema.optional(),
|
||||||
|
fromGroup: ResponseGroupsSchema.optional(),
|
||||||
|
fromPlace: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ResponseRelation = z.infer<typeof ResponseRelationSchema>;
|
||||||
|
|
||||||
export const ShortEventSchema = z.object({
|
export const ShortEventSchema = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
@ -35,29 +84,69 @@ export const SWEventSchema = ShortEventSchema.extend({
|
|||||||
maxTeamMembers: z.number(),
|
maxTeamMembers: z.number(),
|
||||||
schemType: z.string().nullable(),
|
schemType: z.string().nullable(),
|
||||||
publicSchemsOnly: z.boolean(),
|
publicSchemsOnly: z.boolean(),
|
||||||
referees: z.array(PlayerSchema),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SWEvent = z.infer<typeof SWEventSchema>;
|
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,
|
|
||||||
start: z.number(),
|
|
||||||
ergebnis: z.number(),
|
|
||||||
spectatePort: z.number().nullable(),
|
|
||||||
group: z.string().nullable(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type EventFight = z.infer<typeof EventFightSchema>;
|
|
||||||
|
|
||||||
export const ExtendedEventSchema = z.object({
|
export const ExtendedEventSchema = z.object({
|
||||||
event: SWEventSchema,
|
event: SWEventSchema,
|
||||||
teams: z.array(TeamSchema),
|
teams: z.array(TeamSchema),
|
||||||
|
groups: z.array(ResponseGroupsSchema),
|
||||||
fights: z.array(EventFightSchema),
|
fights: z.array(EventFightSchema),
|
||||||
|
referees: z.array(ResponseUserSchema),
|
||||||
|
relations: z.array(ResponseRelationSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ExtendedEvent = z.infer<typeof ExtendedEventSchema>;
|
export type ExtendedEvent = z.infer<typeof ExtendedEventSchema>;
|
||||||
|
|
||||||
|
export const ResponseTeamSchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
kuerzel: z.string(),
|
||||||
|
color: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ResponseTeam = z.infer<typeof ResponseTeamSchema>;
|
||||||
|
|
||||||
|
export const CreateEventGroupSchema = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
type: z.enum(["GROUP_STAGE", "ELIMINATION_STAGE"]),
|
||||||
|
});
|
||||||
|
export type CreateEventGroup = z.infer<typeof CreateEventGroupSchema>;
|
||||||
|
|
||||||
|
export const UpdateEventGroupSchema = z.object({
|
||||||
|
name: z.string().nullable().optional(),
|
||||||
|
type: z.enum(["GROUP_STAGE", "ELIMINATION_STAGE"]).nullable().optional(),
|
||||||
|
pointsPerWin: z.number().nullable().optional(),
|
||||||
|
pointsPerLoss: z.number().nullable().optional(),
|
||||||
|
pointsPerDraw: z.number().nullable().optional(),
|
||||||
|
});
|
||||||
|
export type UpdateEventGroup = z.infer<typeof UpdateEventGroupSchema>;
|
||||||
|
|
||||||
|
export const GroupEditSchema = ResponseGroupsSchema.omit({
|
||||||
|
id: true,
|
||||||
|
points: true,
|
||||||
|
});
|
||||||
|
export type GroupUpdateEdit = z.infer<typeof GroupEditSchema>;
|
||||||
|
|
||||||
|
export const CreateEventRelationSchema = z.object({
|
||||||
|
fightId: z.number(),
|
||||||
|
team: z.enum(["RED", "BLUE"]),
|
||||||
|
fromType: z.enum(["FIGHT", "GROUP"]),
|
||||||
|
fromId: z.number(),
|
||||||
|
fromPlace: z.number(),
|
||||||
|
});
|
||||||
|
export type CreateEventRelation = z.infer<typeof CreateEventRelationSchema>;
|
||||||
|
|
||||||
|
export const UpdateFromRelationSchema = z.object({
|
||||||
|
fromType: z.enum(["FIGHT", "GROUP"]),
|
||||||
|
fromId: z.number(),
|
||||||
|
fromPlace: z.number(),
|
||||||
|
});
|
||||||
|
export type UpdateFromRelation = z.infer<typeof UpdateFromRelationSchema>;
|
||||||
|
|
||||||
|
export const UpdateEventRelationSchema = z.object({
|
||||||
|
team: z.enum(["RED", "BLUE"]).nullable().optional(),
|
||||||
|
from: UpdateFromRelationSchema.nullable().optional(),
|
||||||
|
});
|
||||||
|
export type UpdateEventRelation = z.infer<typeof UpdateEventRelationSchema>;
|
||||||
|
|||||||
@ -17,11 +17,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {z} from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const PrefixSchema = z.object({
|
export const PrefixSchema = z.object({
|
||||||
name: z.string().startsWith("PREFIX_"),
|
name: z.string().startsWith("PREFIX_"),
|
||||||
colorCode: z.string().length(2).startsWith("§"),
|
colorCode: z.string().startsWith("§"),
|
||||||
chatPrefix: z.string(),
|
chatPrefix: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -17,12 +17,12 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {z} from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const TeamSchema = z.object({
|
export const TeamSchema = z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
kuerzel: z.string().min(1).max(4),
|
kuerzel: z.string().min(1).max(16),
|
||||||
color: z.string().max(1),
|
color: z.string().max(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,19 +3,11 @@
|
|||||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = AlertDialogPrimitive.ActionProps;
|
let {
|
||||||
type $$Events = AlertDialogPrimitive.ActionEvents;
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
let className: $$Props["class"] = undefined;
|
...restProps
|
||||||
export { className as class };
|
}: AlertDialogPrimitive.ActionProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AlertDialogPrimitive.Action
|
<AlertDialogPrimitive.Action bind:ref class={cn(buttonVariants(), className)} {...restProps} />
|
||||||
class={cn(buttonVariants(), className)}
|
|
||||||
{...$$restProps}
|
|
||||||
on:click
|
|
||||||
on:keydown
|
|
||||||
let:builder
|
|
||||||
>
|
|
||||||
<slot {builder} />
|
|
||||||
</AlertDialogPrimitive.Action>
|
|
||||||
|
|||||||
@ -3,19 +3,15 @@
|
|||||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = AlertDialogPrimitive.CancelProps;
|
let {
|
||||||
type $$Events = AlertDialogPrimitive.CancelEvents;
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
let className: $$Props["class"] = undefined;
|
...restProps
|
||||||
export { className as class };
|
}: AlertDialogPrimitive.CancelProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AlertDialogPrimitive.Cancel
|
<AlertDialogPrimitive.Cancel
|
||||||
|
bind:ref
|
||||||
class={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
class={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
on:click
|
/>
|
||||||
on:keydown
|
|
||||||
let:builder
|
|
||||||
>
|
|
||||||
<slot {builder} />
|
|
||||||
</AlertDialogPrimitive.Cancel>
|
|
||||||
|
|||||||
@ -1,28 +1,26 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
import { AlertDialog as AlertDialogPrimitive, type WithoutChild } from "bits-ui";
|
||||||
import * as AlertDialog from "./index.js";
|
import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
|
||||||
import { cn, flyAndScale } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = AlertDialogPrimitive.ContentProps;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
export let transition: $$Props["transition"] = flyAndScale;
|
class: className,
|
||||||
export let transitionConfig: $$Props["transitionConfig"] = undefined;
|
portalProps,
|
||||||
|
...restProps
|
||||||
let className: $$Props["class"] = undefined;
|
}: WithoutChild<AlertDialogPrimitive.ContentProps> & {
|
||||||
export { className as class };
|
portalProps?: AlertDialogPrimitive.PortalProps;
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AlertDialog.Portal>
|
<AlertDialogPrimitive.Portal {...portalProps}>
|
||||||
<AlertDialog.Overlay />
|
<AlertDialogOverlay />
|
||||||
<AlertDialogPrimitive.Content
|
<AlertDialogPrimitive.Content
|
||||||
{transition}
|
bind:ref
|
||||||
{transitionConfig}
|
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
>
|
/>
|
||||||
<slot />
|
</AlertDialogPrimitive.Portal>
|
||||||
</AlertDialogPrimitive.Content>
|
|
||||||
</AlertDialog.Portal>
|
|
||||||
|
|||||||
@ -2,15 +2,15 @@
|
|||||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = AlertDialogPrimitive.DescriptionProps;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
let className: $$Props["class"] = undefined;
|
class: className,
|
||||||
export { className as class };
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.DescriptionProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AlertDialogPrimitive.Description
|
<AlertDialogPrimitive.Description
|
||||||
|
bind:ref
|
||||||
class={cn("text-muted-foreground text-sm", className)}
|
class={cn("text-muted-foreground text-sm", className)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
>
|
/>
|
||||||
<slot />
|
|
||||||
</AlertDialogPrimitive.Description>
|
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
let className: $$Props["class"] = undefined;
|
class: className,
|
||||||
export { className as class };
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
bind:this={ref}
|
||||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,13 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLDivElement>;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
let className: $$Props["class"] = undefined;
|
class: className,
|
||||||
export { className as class };
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...$$restProps}>
|
<div
|
||||||
<slot />
|
bind:this={ref}
|
||||||
|
class={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,21 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
import { fade } from "svelte/transition";
|
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = AlertDialogPrimitive.OverlayProps;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
let className: $$Props["class"] = undefined;
|
class: className,
|
||||||
export let transition: $$Props["transition"] = fade;
|
...restProps
|
||||||
export let transitionConfig: $$Props["transitionConfig"] = {
|
}: AlertDialogPrimitive.OverlayProps = $props();
|
||||||
duration: 150,
|
|
||||||
};
|
|
||||||
export { className as class };
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AlertDialogPrimitive.Overlay
|
<AlertDialogPrimitive.Overlay
|
||||||
{transition}
|
bind:ref
|
||||||
{transitionConfig}
|
class={cn(
|
||||||
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ", className)}
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||||
{...$$restProps}
|
className
|
||||||
|
)}
|
||||||
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -2,13 +2,17 @@
|
|||||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = AlertDialogPrimitive.TitleProps;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
let className: $$Props["class"] = undefined;
|
class: className,
|
||||||
export let level: $$Props["level"] = "h3";
|
level = 3,
|
||||||
export { className as class };
|
...restProps
|
||||||
|
}: AlertDialogPrimitive.TitleProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AlertDialogPrimitive.Title class={cn("text-lg font-semibold", className)} {level} {...$$restProps}>
|
<AlertDialogPrimitive.Title
|
||||||
<slot />
|
bind:ref
|
||||||
</AlertDialogPrimitive.Title>
|
class={cn("text-lg font-semibold", className)}
|
||||||
|
{level}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
import Title from "./alert-dialog-title.svelte";
|
import Title from "./alert-dialog-title.svelte";
|
||||||
import Action from "./alert-dialog-action.svelte";
|
import Action from "./alert-dialog-action.svelte";
|
||||||
import Cancel from "./alert-dialog-cancel.svelte";
|
import Cancel from "./alert-dialog-cancel.svelte";
|
||||||
import Portal from "./alert-dialog-portal.svelte";
|
|
||||||
import Footer from "./alert-dialog-footer.svelte";
|
import Footer from "./alert-dialog-footer.svelte";
|
||||||
import Header from "./alert-dialog-header.svelte";
|
import Header from "./alert-dialog-header.svelte";
|
||||||
import Overlay from "./alert-dialog-overlay.svelte";
|
import Overlay from "./alert-dialog-overlay.svelte";
|
||||||
@ -12,6 +10,7 @@ import Description from "./alert-dialog-description.svelte";
|
|||||||
|
|
||||||
const Root = AlertDialogPrimitive.Root;
|
const Root = AlertDialogPrimitive.Root;
|
||||||
const Trigger = AlertDialogPrimitive.Trigger;
|
const Trigger = AlertDialogPrimitive.Trigger;
|
||||||
|
const Portal = AlertDialogPrimitive.Portal;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
|
|||||||
@ -1,35 +1,35 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Checkbox as CheckboxPrimitive } from "bits-ui";
|
import { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||||
import Check from "lucide-svelte/icons/check";
|
import Check from "@lucide/svelte/icons/check";
|
||||||
import Minus from "lucide-svelte/icons/minus";
|
import Minus from "@lucide/svelte/icons/minus";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = CheckboxPrimitive.Props;
|
let {
|
||||||
type $$Events = CheckboxPrimitive.Events;
|
ref = $bindable(null),
|
||||||
|
checked = $bindable(false),
|
||||||
let className: $$Props["class"] = undefined;
|
indeterminate = $bindable(false),
|
||||||
export let checked: $$Props["checked"] = false;
|
class: className,
|
||||||
export { className as class };
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
|
bind:ref
|
||||||
class={cn(
|
class={cn(
|
||||||
"border-primary ring-offset-background focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground peer box-content h-4 w-4 shrink-0 rounded-sm border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50",
|
"border-primary ring-offset-background focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground peer box-content size-4 shrink-0 rounded-sm border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
bind:checked
|
bind:checked
|
||||||
{...$$restProps}
|
bind:indeterminate
|
||||||
on:click
|
{...restProps}
|
||||||
>
|
>
|
||||||
<CheckboxPrimitive.Indicator
|
{#snippet children({ checked, indeterminate })}
|
||||||
class={cn("flex h-4 w-4 items-center justify-center text-current")}
|
<div class="flex size-4 items-center justify-center text-current">
|
||||||
let:isChecked
|
{#if indeterminate}
|
||||||
let:isIndeterminate
|
<Minus class="size-3.5" />
|
||||||
>
|
{:else}
|
||||||
{#if isChecked}
|
<Check class={cn("size-3.5", !checked && "text-transparent")} />
|
||||||
<Check class="h-3.5 w-3.5" />
|
{/if}
|
||||||
{:else if isIndeterminate}
|
</div>
|
||||||
<Minus class="h-3.5 w-3.5" />
|
{/snippet}
|
||||||
{/if}
|
|
||||||
</CheckboxPrimitive.Indicator>
|
|
||||||
</CheckboxPrimitive.Root>
|
</CheckboxPrimitive.Root>
|
||||||
|
|||||||
@ -24,14 +24,15 @@
|
|||||||
import { ScrollArea } from "$lib/components/ui/scroll-area";
|
import { ScrollArea } from "$lib/components/ui/scroll-area";
|
||||||
import { CalendarIcon } from "lucide-svelte";
|
import { CalendarIcon } from "lucide-svelte";
|
||||||
import { cn } from "@components/utils";
|
import { cn } from "@components/utils";
|
||||||
import type {ZonedDateTime} from "@internationalized/date";
|
import { fromDate, type ZonedDateTime } from "@internationalized/date";
|
||||||
|
import Input from "../input/input.svelte";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
value = $bindable(),
|
value = $bindable(),
|
||||||
onChange
|
onChange,
|
||||||
}: {
|
}: {
|
||||||
value: ZonedDateTime
|
value: ZonedDateTime;
|
||||||
onChange?: ((date: ZonedDateTime | undefined) => void) | undefined
|
onChange?: ((date: ZonedDateTime | undefined) => void) | undefined;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let isOpen = $state(false);
|
let isOpen = $state(false);
|
||||||
@ -63,13 +64,7 @@
|
|||||||
|
|
||||||
<Popover bind:open={isOpen}>
|
<Popover bind:open={isOpen}>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button
|
<Button variant="outline" class={cn("w-full justify-start text-left font-normal", !value && "text-muted-foreground")}>
|
||||||
variant="outline"
|
|
||||||
class={cn(
|
|
||||||
"w-full justify-start text-left font-normal",
|
|
||||||
!value && "text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CalendarIcon class="mr-2 h-4 w-4" />
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||||
{#if value}
|
{#if value}
|
||||||
{new Intl.DateTimeFormat("de-DE", {
|
{new Intl.DateTimeFormat("de-DE", {
|
||||||
@ -86,23 +81,14 @@
|
|||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverContent class="w-auto p-0">
|
<PopoverContent class="w-auto p-0">
|
||||||
|
<Input type="datetime-local" value={value.toString().slice(0, 16)} onchange={(e) => handleDateSelect(fromDate(e.target.valueAsDate, "Europe/Berlin"))} />
|
||||||
<div class="sm:flex">
|
<div class="sm:flex">
|
||||||
<Calendar
|
<Calendar mode="single" bind:value onValueChange={(date) => handleDateSelect(date)} initialFocus />
|
||||||
mode="single"
|
|
||||||
bind:value
|
|
||||||
onValueChange={(date) => handleDateSelect(date)}
|
|
||||||
initialFocus
|
|
||||||
/>
|
|
||||||
<div class="flex flex-col sm:flex-row sm:h-[300px] divide-y sm:divide-y-0 sm:divide-x">
|
<div class="flex flex-col sm:flex-row sm:h-[300px] divide-y sm:divide-y-0 sm:divide-x">
|
||||||
<ScrollArea class="w-64 sm:w-auto">
|
<ScrollArea class="w-64 sm:w-auto">
|
||||||
<div class="flex sm:flex-col p-2">
|
<div class="flex sm:flex-col p-2">
|
||||||
{#each [...hours].reverse() as hour}
|
{#each [...hours].reverse() as hour}
|
||||||
<Button
|
<Button size="icon" variant={value && value.hour === hour ? "default" : "ghost"} class="sm:w-full shrink-0 aspect-square" onclick={() => handleTimeChange("hour", hour)}>
|
||||||
size="icon"
|
|
||||||
variant={value && value.hour === hour ? "default" : "ghost"}
|
|
||||||
class="sm:w-full shrink-0 aspect-square"
|
|
||||||
onclick={() => handleTimeChange("hour", hour)}
|
|
||||||
>
|
|
||||||
{hour}
|
{hour}
|
||||||
</Button>
|
</Button>
|
||||||
{/each}
|
{/each}
|
||||||
@ -113,12 +99,12 @@
|
|||||||
<div class="flex sm:flex-col p-2">
|
<div class="flex sm:flex-col p-2">
|
||||||
{#each Array.from({ length: 60 }, (_, i) => i) as minute}
|
{#each Array.from({ length: 60 }, (_, i) => i) as minute}
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant={value && value.minute === minute ? "default" : "ghost"}
|
variant={value && value.minute === minute ? "default" : "ghost"}
|
||||||
class="sm:w-full shrink-0 aspect-square"
|
class="sm:w-full shrink-0 aspect-square"
|
||||||
onclick={() => handleTimeChange("minute", minute)}
|
onclick={() => handleTimeChange("minute", minute)}
|
||||||
>
|
>
|
||||||
{minute.toString().padStart(2, '0')}
|
{minute.toString().padStart(2, "0")}
|
||||||
</Button>
|
</Button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,29 +1,6 @@
|
|||||||
import Root from "./input.svelte";
|
import Root from "./input.svelte";
|
||||||
|
|
||||||
export type FormInputEvent<T extends Event = Event> = T & {
|
|
||||||
currentTarget: EventTarget & HTMLInputElement;
|
|
||||||
};
|
|
||||||
export type InputEvents = {
|
|
||||||
blur: FormInputEvent<FocusEvent>;
|
|
||||||
change: FormInputEvent<Event>;
|
|
||||||
click: FormInputEvent<MouseEvent>;
|
|
||||||
focus: FormInputEvent<FocusEvent>;
|
|
||||||
focusin: FormInputEvent<FocusEvent>;
|
|
||||||
focusout: FormInputEvent<FocusEvent>;
|
|
||||||
keydown: FormInputEvent<KeyboardEvent>;
|
|
||||||
keypress: FormInputEvent<KeyboardEvent>;
|
|
||||||
keyup: FormInputEvent<KeyboardEvent>;
|
|
||||||
mouseover: FormInputEvent<MouseEvent>;
|
|
||||||
mouseenter: FormInputEvent<MouseEvent>;
|
|
||||||
mouseleave: FormInputEvent<MouseEvent>;
|
|
||||||
mousemove: FormInputEvent<MouseEvent>;
|
|
||||||
paste: FormInputEvent<ClipboardEvent>;
|
|
||||||
input: FormInputEvent<InputEvent>;
|
|
||||||
wheel: FormInputEvent<WheelEvent>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Root,
|
Root,
|
||||||
//
|
//
|
||||||
Root as Input,
|
Root as Input,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,42 +1,39 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLInputAttributes } from "svelte/elements";
|
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||||
import type { InputEvents } from "./index.js";
|
import { cn } from "@components/utils";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { type WithElementRef } from "bits-ui";
|
||||||
|
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||||
type $$Props = HTMLInputAttributes;
|
type Props = WithElementRef<Omit<HTMLInputAttributes, "type"> & ({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })>;
|
||||||
type $$Events = InputEvents;
|
let { ref = $bindable(null), value = $bindable(), type, files = $bindable(), class: className, ...restProps }: Props = $props();
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
|
||||||
export let value: $$Props["value"] = undefined;
|
|
||||||
export { className as class };
|
|
||||||
|
|
||||||
// Workaround for https://github.com/sveltejs/svelte/issues/9305
|
|
||||||
// Fixed in Svelte 5, but not backported to 4.x.
|
|
||||||
export let readonly: $$Props["readonly"] = undefined;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<input
|
{#if type === "file"}
|
||||||
class={cn(
|
<input
|
||||||
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
bind:this={ref}
|
||||||
className
|
data-slot="input"
|
||||||
)}
|
class={cn(
|
||||||
bind:value
|
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
{readonly}
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
on:blur
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
on:change
|
className
|
||||||
on:click
|
)}
|
||||||
on:focus
|
type="file"
|
||||||
on:focusin
|
bind:files
|
||||||
on:focusout
|
bind:value
|
||||||
on:keydown
|
{...restProps}
|
||||||
on:keypress
|
/>
|
||||||
on:keyup
|
{:else}
|
||||||
on:mouseover
|
<input
|
||||||
on:mouseenter
|
bind:this={ref}
|
||||||
on:mouseleave
|
data-slot="input"
|
||||||
on:mousemove
|
class={cn(
|
||||||
on:paste
|
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
on:input
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
on:wheel|passive
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
{...$$restProps}
|
className
|
||||||
/>
|
)}
|
||||||
|
{type}
|
||||||
|
bind:value
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||||
|
|
||||||
import Root from "./menubar.svelte";
|
import Root from "./menubar.svelte";
|
||||||
import CheckboxItem from "./menubar-checkbox-item.svelte";
|
import CheckboxItem from "./menubar-checkbox-item.svelte";
|
||||||
import Content from "./menubar-content.svelte";
|
import Content from "./menubar-content.svelte";
|
||||||
import Item from "./menubar-item.svelte";
|
import Item from "./menubar-item.svelte";
|
||||||
import Label from "./menubar-label.svelte";
|
import GroupHeading from "./menubar-group-heading.svelte";
|
||||||
import RadioItem from "./menubar-radio-item.svelte";
|
import RadioItem from "./menubar-radio-item.svelte";
|
||||||
import Separator from "./menubar-separator.svelte";
|
import Separator from "./menubar-separator.svelte";
|
||||||
import Shortcut from "./menubar-shortcut.svelte";
|
import Shortcut from "./menubar-shortcut.svelte";
|
||||||
@ -22,7 +21,7 @@ export {
|
|||||||
CheckboxItem,
|
CheckboxItem,
|
||||||
Content,
|
Content,
|
||||||
Item,
|
Item,
|
||||||
Label,
|
GroupHeading,
|
||||||
RadioItem,
|
RadioItem,
|
||||||
Separator,
|
Separator,
|
||||||
Shortcut,
|
Shortcut,
|
||||||
@ -38,7 +37,7 @@ export {
|
|||||||
CheckboxItem as MenubarCheckboxItem,
|
CheckboxItem as MenubarCheckboxItem,
|
||||||
Content as MenubarContent,
|
Content as MenubarContent,
|
||||||
Item as MenubarItem,
|
Item as MenubarItem,
|
||||||
Label as MenubarLabel,
|
GroupHeading as MenubarGroupHeading,
|
||||||
RadioItem as MenubarRadioItem,
|
RadioItem as MenubarRadioItem,
|
||||||
Separator as MenubarSeparator,
|
Separator as MenubarSeparator,
|
||||||
Shortcut as MenubarShortcut,
|
Shortcut as MenubarShortcut,
|
||||||
|
|||||||
@ -1,35 +1,40 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
import { Menubar as MenubarPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||||
import Check from "lucide-svelte/icons/check";
|
import Check from "@lucide/svelte/icons/check";
|
||||||
|
import Minus from "@lucide/svelte/icons/minus";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
type $$Props = MenubarPrimitive.CheckboxItemProps;
|
let {
|
||||||
type $$Events = MenubarPrimitive.CheckboxItemEvents;
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
let className: $$Props["class"] = undefined;
|
checked = $bindable(false),
|
||||||
export let checked: $$Props["checked"] = false;
|
indeterminate = $bindable(false),
|
||||||
export { className as class };
|
children: childrenProp,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<MenubarPrimitive.CheckboxItemProps> & {
|
||||||
|
children?: Snippet;
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MenubarPrimitive.CheckboxItem
|
<MenubarPrimitive.CheckboxItem
|
||||||
|
bind:ref
|
||||||
bind:checked
|
bind:checked
|
||||||
|
bind:indeterminate
|
||||||
class={cn(
|
class={cn(
|
||||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
on:click
|
{...restProps}
|
||||||
on:keydown
|
|
||||||
on:focusin
|
|
||||||
on:focusout
|
|
||||||
on:pointerleave
|
|
||||||
on:pointermove
|
|
||||||
on:pointerdown
|
|
||||||
{...$$restProps}
|
|
||||||
>
|
>
|
||||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
{#snippet children({ checked, indeterminate })}
|
||||||
<MenubarPrimitive.CheckboxIndicator>
|
<span class="absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<Check class="h-4 w-4" />
|
{#if indeterminate}
|
||||||
</MenubarPrimitive.CheckboxIndicator>
|
<Minus class="size-4" />
|
||||||
</span>
|
{:else}
|
||||||
<slot />
|
<Check class={cn("size-4", !checked && "text-transparent")} />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{@render childrenProp?.()}
|
||||||
|
{/snippet}
|
||||||
</MenubarPrimitive.CheckboxItem>
|
</MenubarPrimitive.CheckboxItem>
|
||||||
|
|||||||
@ -1,33 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||||
import { cn, flyAndScale } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = MenubarPrimitive.ContentProps;
|
let {
|
||||||
type $$Events = MenubarPrimitive.ContentEvents;
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
let className: $$Props["class"] = undefined;
|
sideOffset = 8,
|
||||||
export let sideOffset: $$Props["sideOffset"] = 8;
|
alignOffset = -4,
|
||||||
export let alignOffset: $$Props["alignOffset"] = -4;
|
align = "start",
|
||||||
export let align: $$Props["align"] = "start";
|
side = "bottom",
|
||||||
export let side: $$Props["side"] = "bottom";
|
portalProps,
|
||||||
export let transition: $$Props["transition"] = flyAndScale;
|
...restProps
|
||||||
export let transitionConfig: $$Props["transitionConfig"] = undefined;
|
}: MenubarPrimitive.ContentProps & {
|
||||||
export { className as class };
|
portalProps?: MenubarPrimitive.PortalProps;
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MenubarPrimitive.Content
|
<MenubarPrimitive.Portal {...portalProps}>
|
||||||
{transition}
|
<MenubarPrimitive.Content
|
||||||
{transitionConfig}
|
bind:ref
|
||||||
{sideOffset}
|
{sideOffset}
|
||||||
{align}
|
{align}
|
||||||
{alignOffset}
|
{alignOffset}
|
||||||
{side}
|
{side}
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-popover text-popover-foreground z-50 min-w-[12rem] rounded-md border p-1 shadow-md focus:outline-none",
|
"bg-popover text-popover-foreground z-50 min-w-[12rem] rounded-md border p-1 shadow-md focus:outline-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
on:keydown
|
/>
|
||||||
>
|
</MenubarPrimitive.Portal>
|
||||||
<slot />
|
|
||||||
</MenubarPrimitive.Content>
|
|
||||||
|
|||||||
19
src/components/ui/menubar/menubar-group-heading.svelte
Normal file
19
src/components/ui/menubar/menubar-group-heading.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||||
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset = undefined,
|
||||||
|
...restProps
|
||||||
|
}: MenubarPrimitive.GroupHeadingProps & {
|
||||||
|
inset?: boolean;
|
||||||
|
} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<MenubarPrimitive.GroupHeading
|
||||||
|
bind:ref
|
||||||
|
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
@ -2,30 +2,22 @@
|
|||||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = MenubarPrimitive.ItemProps & {
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset = undefined,
|
||||||
|
...restProps
|
||||||
|
}: MenubarPrimitive.ItemProps & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
};
|
} = $props();
|
||||||
type $$Events = MenubarPrimitive.ItemEvents;
|
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
|
||||||
export let inset: $$Props["inset"] = undefined;
|
|
||||||
export { className as class };
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MenubarPrimitive.Item
|
<MenubarPrimitive.Item
|
||||||
|
bind:ref
|
||||||
class={cn(
|
class={cn(
|
||||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
inset && "pl-8",
|
inset && "pl-8",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
on:click
|
/>
|
||||||
on:keydown
|
|
||||||
on:focusin
|
|
||||||
on:focusout
|
|
||||||
on:pointerleave
|
|
||||||
on:pointermove
|
|
||||||
on:pointerdown
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</MenubarPrimitive.Item>
|
|
||||||
|
|||||||
@ -1,35 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
import { Menubar as MenubarPrimitive, type WithoutChild } from "bits-ui";
|
||||||
import Circle from "lucide-svelte/icons/circle";
|
import Circle from "@lucide/svelte/icons/circle";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = MenubarPrimitive.RadioItemProps;
|
let {
|
||||||
type $$Events = MenubarPrimitive.RadioItemEvents;
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
let className: $$Props["class"] = undefined;
|
children: childrenProp,
|
||||||
export let value: $$Props["value"];
|
...restProps
|
||||||
export { className as class };
|
}: WithoutChild<MenubarPrimitive.RadioItemProps> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MenubarPrimitive.RadioItem
|
<MenubarPrimitive.RadioItem
|
||||||
{value}
|
bind:ref
|
||||||
class={cn(
|
class={cn(
|
||||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
on:click
|
|
||||||
on:keydown
|
|
||||||
on:focusin
|
|
||||||
on:focusout
|
|
||||||
on:pointerleave
|
|
||||||
on:pointermove
|
|
||||||
on:pointerdown
|
|
||||||
>
|
>
|
||||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
{#snippet children({ checked })}
|
||||||
<MenubarPrimitive.RadioIndicator>
|
<span class="absolute left-2 flex size-3.5 items-center justify-center">
|
||||||
<Circle class="h-2 w-2 fill-current" />
|
{#if checked}
|
||||||
</MenubarPrimitive.RadioIndicator>
|
<Circle class="size-2 fill-current" />
|
||||||
</span>
|
{/if}
|
||||||
<slot />
|
</span>
|
||||||
|
{@render childrenProp?.({ checked })}
|
||||||
|
{/snippet}
|
||||||
</MenubarPrimitive.RadioItem>
|
</MenubarPrimitive.RadioItem>
|
||||||
|
|||||||
@ -2,10 +2,15 @@
|
|||||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = MenubarPrimitive.SeparatorProps;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
let className: $$Props["class"] = undefined;
|
class: className,
|
||||||
export { className as class };
|
...restProps
|
||||||
|
}: MenubarPrimitive.SeparatorProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MenubarPrimitive.Separator class={cn("bg-muted -mx-1 my-1 h-px", className)} {...$$restProps} />
|
<MenubarPrimitive.Separator
|
||||||
|
bind:ref
|
||||||
|
class={cn("bg-muted -mx-1 my-1 h-px", className)}
|
||||||
|
{...restProps}
|
||||||
|
/>
|
||||||
|
|||||||
@ -1,16 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
|
import type { WithElementRef } from "bits-ui";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = HTMLAttributes<HTMLSpanElement>;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
let className: $$Props["class"] = undefined;
|
class: className,
|
||||||
export { className as class };
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
|
bind:this={ref}
|
||||||
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<slot />
|
{@render children?.()}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@ -1,27 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||||
import { cn, flyAndScale } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = MenubarPrimitive.SubContentProps;
|
let {
|
||||||
type $$Events = MenubarPrimitive.SubContentEvents;
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
let className: $$Props["class"] = undefined;
|
...restProps
|
||||||
export let transition: $$Props["transition"] = flyAndScale;
|
}: MenubarPrimitive.SubContentProps = $props();
|
||||||
export let transitionConfig: $$Props["transitionConfig"] = { x: -10, y: 0 };
|
|
||||||
export { className as class };
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MenubarPrimitive.SubContent
|
<MenubarPrimitive.SubContent
|
||||||
{transition}
|
bind:ref
|
||||||
{transitionConfig}
|
|
||||||
class={cn(
|
class={cn(
|
||||||
"bg-popover text-popover-foreground z-50 min-w-max rounded-md border p-1 focus:outline-none",
|
"bg-popover text-popover-foreground z-50 min-w-max rounded-md border p-1 focus:outline-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
on:focusout
|
/>
|
||||||
on:pointermove
|
|
||||||
on:keydown
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</MenubarPrimitive.SubContent>
|
|
||||||
|
|||||||
@ -1,32 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
import { Menubar as MenubarPrimitive, type WithoutChild } from "bits-ui";
|
||||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = MenubarPrimitive.SubTriggerProps & {
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
|
inset = undefined,
|
||||||
|
children,
|
||||||
|
...restProps
|
||||||
|
}: WithoutChild<MenubarPrimitive.SubTriggerProps> & {
|
||||||
inset?: boolean;
|
inset?: boolean;
|
||||||
};
|
} = $props();
|
||||||
type $$Events = MenubarPrimitive.SubTriggerEvents;
|
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
|
||||||
export let inset: $$Props["inset"] = undefined;
|
|
||||||
export { className as class };
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MenubarPrimitive.SubTrigger
|
<MenubarPrimitive.SubTrigger
|
||||||
|
bind:ref
|
||||||
class={cn(
|
class={cn(
|
||||||
"data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
inset && "pl-8",
|
inset && "pl-8",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
on:click
|
{...restProps}
|
||||||
{...$$restProps}
|
|
||||||
on:keydown
|
|
||||||
on:focusin
|
|
||||||
on:focusout
|
|
||||||
on:pointerleave
|
|
||||||
on:pointermove
|
|
||||||
>
|
>
|
||||||
<slot />
|
{@render children?.()}
|
||||||
<ChevronRight class="ml-auto h-4 w-4" />
|
<ChevronRight class="ml-auto size-4" />
|
||||||
</MenubarPrimitive.SubTrigger>
|
</MenubarPrimitive.SubTrigger>
|
||||||
|
|||||||
@ -2,22 +2,18 @@
|
|||||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = MenubarPrimitive.TriggerProps;
|
let {
|
||||||
type $$Events = MenubarPrimitive.TriggerEvents;
|
ref = $bindable(null),
|
||||||
|
class: className,
|
||||||
let className: $$Props["class"] = undefined;
|
...restProps
|
||||||
export { className as class };
|
}: MenubarPrimitive.TriggerProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MenubarPrimitive.Trigger
|
<MenubarPrimitive.Trigger
|
||||||
|
bind:ref
|
||||||
class={cn(
|
class={cn(
|
||||||
"data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none",
|
"data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
on:click
|
{...restProps}
|
||||||
on:keydown
|
/>
|
||||||
on:pointerenter
|
|
||||||
{...$$restProps}
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</MenubarPrimitive.Trigger>
|
|
||||||
|
|||||||
@ -2,15 +2,15 @@
|
|||||||
import { Menubar as MenubarPrimitive } from "bits-ui";
|
import { Menubar as MenubarPrimitive } from "bits-ui";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = MenubarPrimitive.Props;
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
let className: $$Props["class"] = undefined;
|
class: className,
|
||||||
export { className as class };
|
...restProps
|
||||||
|
}: MenubarPrimitive.RootProps = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MenubarPrimitive.Root
|
<MenubarPrimitive.Root
|
||||||
|
bind:ref
|
||||||
class={cn("bg-background flex h-10 items-center space-x-1 rounded-md border p-1", className)}
|
class={cn("bg-background flex h-10 items-center space-x-1 rounded-md border p-1", className)}
|
||||||
{...$$restProps}
|
{...restProps}
|
||||||
>
|
/>
|
||||||
<slot />
|
|
||||||
</MenubarPrimitive.Root>
|
|
||||||
|
|||||||
45
src/content/announcements/de/2025-halloween.md
Normal file
45
src/content/announcements/de/2025-halloween.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
title: WarShip Halloween Event 2025
|
||||||
|
key: 2025-halloween
|
||||||
|
description: Das WarShip Halloween Event 2025 für die Community
|
||||||
|
created: 2025-10-27T00:00:00.000Z
|
||||||
|
tags:
|
||||||
|
- event
|
||||||
|
- warship
|
||||||
|
---
|
||||||
|
|
||||||
|
Ahoi Community,
|
||||||
|
das diesjährige Halloween-Event nähert sich, die Tage werden langsam kürzer und die Nächte länger. Es geht auf dem Herbst zu und erinnert daran, dass das Jahr wieder halb vorbei ist. Dieses Mal im Spielmodus Warship. Das im Format 6 gegen 6 ausgetragen wird. Neben dem eigentlichen Turnier wird das Außendesign bewertet. Die Bewertung des Außendedigns wird zu 70% Das SW Builderteam übernehmen und 30% die Userbewertung. Die prozentuale Bewertung soll dazu dienen, dass große Teams Ihr eigenes Design nicht hoch puschen können.
|
||||||
|
|
||||||
|
Das Event findet am 08.11.2025 in der Version 1.21 mit dem aktuellen Regelwerk statt.
|
||||||
|
|
||||||
|
~~Anmelde + Einsendeschluss 03.11.2025~~
|
||||||
|
|
||||||
|
**Neue Fristen**:
|
||||||
|
|
||||||
|
Einsendeschluss: 06.11.2025 23:59 Uhr
|
||||||
|
Hotfixschluss: 07.11.2025 23:59 Uhr
|
||||||
|
Der Anmeldeschluss bleibt der 03.11.2025
|
||||||
|
|
||||||
|
zusätzlich wird es mit einem Designcontest begleitet.
|
||||||
|
|
||||||
|
Design Regel: Halloween
|
||||||
|
Arena: Lucifus
|
||||||
|
|
||||||
|
Design Bewertung
|
||||||
|
|
||||||
|
- Userbewertung (30%) wird über den Discord Community Server von SW organisiert. (Bilder vom Außendesign werden gepostet und per Abstimmung ausgelost)
|
||||||
|
- Builderbewertung (70%) läuft nach folgende Kriterien ab.
|
||||||
|
- Form des WS
|
||||||
|
- Farbgestaltung
|
||||||
|
- Muster
|
||||||
|
- Thematisierung: Thema Halloween / Grusel
|
||||||
|
|
||||||
|
Es wird also 3 Sieges- Plätze geben welch wie Folgt ermittelt wird.
|
||||||
|
|
||||||
|
- Gesamtsieger: Höchste Fight Platzierung und Design Platzierung im Durchschnitt
|
||||||
|
- Event- Sieger : Höchste Fight Platzierung
|
||||||
|
- Designsieger: Bestes Design
|
||||||
|
|
||||||
|
Das Warshipdesign vom Gesamtsieger wird bis zum nächsten Halloween in der Lobby ausgestellt. Wir freuen uns auf zahlreiche Anmeldungen und sind gespannt, welche Designs uns erwarten!
|
||||||
|
Das Serverteam
|
||||||
39
src/content/announcements/de/SC-Eventplan.md
Normal file
39
src/content/announcements/de/SC-Eventplan.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
title: SpaceCraft Eventplan
|
||||||
|
description: Der Kampfplan für das SpaceCraft Event am 29.6.
|
||||||
|
key: SC-Eventplan
|
||||||
|
created: 2025-06-28T00:00:00.000Z
|
||||||
|
tags:
|
||||||
|
- SpaceCraft
|
||||||
|
- Event
|
||||||
|
image: ../../../images/SpaceCraftWinners3.png
|
||||||
|
---
|
||||||
|
|
||||||
|
### Infos:
|
||||||
|
Eventleitung: TheBreadBeard
|
||||||
|
|
||||||
|
Sollten fights zu schnell vorbei sein, ist eine vorverschiebung der folgenden fights möglich. In diesem Fall wird eine Pause von 10 Minuten garantiert.
|
||||||
|
|
||||||
|
|
||||||
|
## Gruppen
|
||||||
|
|
||||||
|
<group-table data-event="72" data-group="2"> </group-table>
|
||||||
|
|
||||||
|
<group-table data-event="72" data-group="3"> </group-table>
|
||||||
|
|
||||||
|
## Tabelle
|
||||||
|
|
||||||
|

|
||||||
|
## Fights
|
||||||
|
|
||||||
|
### Gruppenphase
|
||||||
|
|
||||||
|
**Gruppe 1**
|
||||||
|
<fight-table data-event="72" data-group="2"> </group-table>
|
||||||
|
|
||||||
|
**Gruppe 2**
|
||||||
|
<fight-table data-event="72" data-group="3"> </group-table>
|
||||||
|
|
||||||
|
### KO-Phase
|
||||||
|
|
||||||
|
<fight-table data-event="72" data-group="4"> </group-table>
|
||||||
25
src/content/announcements/de/mwgl.md
Normal file
25
src/content/announcements/de/mwgl.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: MiniWarGearLiga Ankündigung
|
||||||
|
description: Die MiniWargear-Liga 2025 findet am 27./28. September statt.
|
||||||
|
key: mwgl
|
||||||
|
created: 2025-07-28T00:00:00.000Z
|
||||||
|
tags:
|
||||||
|
- event
|
||||||
|
- miniwargear
|
||||||
|
image: ../../../images/generated-image(8).png
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ahoi Liebe Community,**
|
||||||
|
|
||||||
|
Es ist wieder so weit – die MiniWargear-Liga findet auch dieses Jahr wieder statt.
|
||||||
|
|
||||||
|
Infos zum Event:
|
||||||
|
|
||||||
|
* Die MWGL findet am Wochenende des **27./28.09.2025** um **16 Uhr** statt.
|
||||||
|
* Einsendeschluss ist der **22.09.2025** um 23:59 Uhr – bis dahin muss ein MWG gemäß Regelwerk vorhanden sein.
|
||||||
|
* Hotfixes (geringe Änderungen wie z.B. das Fixen von Läufen usw.) dürfen bis zum **25.09.2025** um 23:59 Uhr nachgereicht werden.
|
||||||
|
* Max. **4 Kämpfer** pro Team
|
||||||
|
* Normales SW-MWG-Regelwerk mit automatischen Kanonen
|
||||||
|
* Es wird einen eigenen Schem-Typ geben.
|
||||||
|
* Der Schem-Name muss mit dem Teamkürzel enden.
|
||||||
|
* Gefightet wird mit getrenntem Spectate- und Fightserver (wie bei der WGS)
|
||||||
98
src/content/announcements/de/mwgl_eventplan.md
Normal file
98
src/content/announcements/de/mwgl_eventplan.md
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
---
|
||||||
|
title: MiniWarGearLiga Eventplan
|
||||||
|
description: Der Eventplan für die MiniWargear-Liga 2025
|
||||||
|
key: mwgl
|
||||||
|
created: 2025-09-27T00:00:00.000Z
|
||||||
|
tags:
|
||||||
|
- event
|
||||||
|
- miniwargear
|
||||||
|
image: ../../../images/generated-image(8).png
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spieltag 27.09.2025
|
||||||
|
|
||||||
|
### Gruppenphase 1
|
||||||
|
|
||||||
|
##### Gruppen
|
||||||
|
|
||||||
|
| Gruppe 1 | Gruppe 2 | Gruppe 3 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| BF | Borg | MLT! |
|
||||||
|
| KT | EXO | Salo |
|
||||||
|
| ED | PL | FK |
|
||||||
|
|
||||||
|
In nachfolgenden Tabellen wird `W` für `Winner`/`Gewinner` stehen und `L` für `Loser`/`Verlierer`.
|
||||||
|
|
||||||
|
| 27.09.2025 | Arena 1 | Ergebnis | Arena 2 | Ergebnis | Arena 3 | Ergebnis |
|
||||||
|
|------------|----------|:--------:|-------------|:--------:|--------------|:--------:|
|
||||||
|
| 16:00 | BF vs KT | BF | Borg vs EXO | Borg | MLT! vs Salo | MLT! |
|
||||||
|
| 16:30 | BF vs ED | ED | Borg vs PL | Borg | MLT! vs FK | FK |
|
||||||
|
| 17:00 | KT vs BF | BF | EXO vs PL | PL | Salo vs MLT! | Salo |
|
||||||
|
| 17:30 | ED vs BF | BF | Borg vs PL | Borg | FK vs Salo | Salo |
|
||||||
|
|
||||||
|
### Gruppenphase 2
|
||||||
|
|
||||||
|
##### Gruppen
|
||||||
|
|
||||||
|
| Gruppe 1 | Gruppe 2 | Gruppe 3 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| BF | ED | MLT! |
|
||||||
|
| Borg | PL | EXO |
|
||||||
|
| Salo | FK | KT |
|
||||||
|
|
||||||
|
In nachfolgender Tabelle steht `Gr.` für `Gruppe` und das Wort `Platz` wird weggelassen.
|
||||||
|
|
||||||
|
| 27.09.2025 | Arena 1 | Ergebnis | Arena 2 | Ergebnis | Arena 3 | Ergebnis |
|
||||||
|
|------------|--------------|:--------:|----------|:--------:|-------------|:--------:|
|
||||||
|
| 18:00 | BF vs Borg | Borg | ED vs PL | PL | EXO vs KT | EXO |
|
||||||
|
| 18:30 | Borg vs Salo | Borg | PL vs FK | FK | EXO vs MLT! | MLT! |
|
||||||
|
| 19:00 | BF vs Salo | Salo | ED vs PL | PL | KT vs EXO | EXO |
|
||||||
|
| 19:30 | Borg vs Salo | Salo | FK vs PL | FK | MLT! vs EXO | EXO |
|
||||||
|
|
||||||
|
KT wird disqualifiziert und tritt morgen nicht mehr an. Das Team belegt somit den 9. und damit letzten Platz.
|
||||||
|
|
||||||
|
## Spieltag 28.09.2025
|
||||||
|
|
||||||
|
### Leiter
|
||||||
|
|
||||||
|
Die fights werden auf 5 Minuten an den vorherigen vorgezogen.
|
||||||
|
|
||||||
|
| 28.09.2025 | | Ergebnis |
|
||||||
|
|------------|-------------|:--------:|
|
||||||
|
| 16:00 | MLT! vs EXO | EXO |
|
||||||
|
| 16:15 | EXO vs ED | ED |
|
||||||
|
| 16:25 | ED vs PL | PL |
|
||||||
|
| 16:40 | PL vs FK | FK |
|
||||||
|
| 16:55 | FK vs BF | FK |
|
||||||
|
|
||||||
|
### Spiel um Platz 3
|
||||||
|
|
||||||
|
| 28.09.2025 | | Ergebnis |
|
||||||
|
|------------|------------|:--------:|
|
||||||
|
| 17:15 | FK vs Borg | Borg |
|
||||||
|
| 17:25 | FK vs Borg | Borg |
|
||||||
|
| Entfällt | FK vs Borg | / |
|
||||||
|
|
||||||
|
### Spiel um Platz 2 und 1
|
||||||
|
|
||||||
|
| 28.09.2025 | | Ergebnis |
|
||||||
|
|------------|--------------|:--------:|
|
||||||
|
| 17:45 | Borg vs Salo | Salo |
|
||||||
|
| 18:00 | Borg vs Salo | Borg |
|
||||||
|
| 18:10 | Borg vs Salo | Borg |
|
||||||
|
| 18:20 | Borg vs Salo | Borg |
|
||||||
|
| entfällt | Borg vs Salo | / |
|
||||||
|
|
||||||
|
## Endplatzierung
|
||||||
|
|
||||||
|
| Platz | Team |
|
||||||
|
|-------|------|
|
||||||
|
| 1. | Borg |
|
||||||
|
| 2. | Salo |
|
||||||
|
| 3. | FK |
|
||||||
|
| 4. | BF |
|
||||||
|
| 5. | PL |
|
||||||
|
| 6. | ED |
|
||||||
|
| 7. | EXO |
|
||||||
|
| 8. | MLT! |
|
||||||
|
| 9. | KT |
|
||||||
@ -0,0 +1,164 @@
|
|||||||
|
---
|
||||||
|
title: Spaceship Event Ankündigung und Regelwerk
|
||||||
|
description: Alle Infos, Regeln und Termine zum ersten Spaceship-Event am 19.06.2025 – jetzt mit neuer TNT-Physik und ohne Gravitation!
|
||||||
|
tags:
|
||||||
|
- spaceship
|
||||||
|
- event
|
||||||
|
created: 2025-05-25
|
||||||
|
key: spaceship-event-ankuendigung-und-regelwerk
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ahoi Community**
|
||||||
|
|
||||||
|
Am 29.06. findet des erste Spaceship Event statt. Dieser Modus soll Airship ersetzen und hat die Besonderheit, dass TNT (und Sand) keine Gravitation hat. Gezündetes, ungebremstes TNT fliegt durch den typischen Sprung bei der Zündung zum Beispiel mit gleichbleibender Geschwindigkeit schräg nach oben.
|
||||||
|
|
||||||
|
Die neue Physik nutzt ihr mit ***/nogravity*** auf dem Bau
|
||||||
|
|
||||||
|
### Alle Infos zum Event:
|
||||||
|
|
||||||
|
- Die Technik und das Design der Publics werden nochmal seperat Public gestellt. So könnt ihr, wenn ihr keine oder weniger eigene Technik bauen wollt, einfacher die Public technik verwenden.
|
||||||
|
- Das Event findet am 29.06.2025 um 16 Uhr statt
|
||||||
|
- Es gibt kein Entern
|
||||||
|
- Kits können wie bei WG und MWG selbst erstellt werden
|
||||||
|
- es wird Redstone TKO verwendet
|
||||||
|
- Das Event findet in der 1.20 statt
|
||||||
|
|
||||||
|
- **Einsendeschluss** am 24.06.2025 23:59 Uhr
|
||||||
|
- Bis zu diesem Zeitpunkt muss eine **dem Modus entsprechende Kampfmaschine** vorhanden sein
|
||||||
|
- Bis zum 27.06.2025 23:59 Uhr **Hotfix-Phase**
|
||||||
|
- In dieser Phase dürfen lediglich Hotfixes eingesendet werden. Das bedeutet: Die Kampfmaschine darf **keine signifikanten Änderungen** erfahren. Es dürfen z. B. keine ganzen Kanonen ausgetauscht oder die Aufstellung verändert werden.
|
||||||
|
- Die Teams ernennen jeweils einen Repräsentanten der das jeweilige Team vertritt und **gut erreichbar** ist
|
||||||
|
- Es dürfen Maximal 2 Spieler pro Team an einem fight teilnehmen
|
||||||
|
|
||||||
|
### RW Änderungen:
|
||||||
|
|
||||||
|
- Es dürfen keine Blöcke über die Grenze des Designbereiches bewegt werden
|
||||||
|
- Das hat zur Folge: Kein slime und honey Limit
|
||||||
|
- Wasser darf in Kanonen verwendet werden
|
||||||
|
- Dadurch könnt ihr TNT einfacher komprimieren. Wasser muss nicht mehr den Zweck erfüllen, die Kanone vor eigenem Schaden zu bewahren
|
||||||
|
- 32 Projektile pro Seite
|
||||||
|
- Unterpunkt: Crossbows
|
||||||
|
- Es gibt keinen Techhider
|
||||||
|
# Regelwerk für das Event:
|
||||||
|
|
||||||
|
## Definitionen
|
||||||
|
|
||||||
|
Ein AirShip ist eine beidseitig bewaffnete Struktur in Minecraft 1.20 und sieht flugfähig aus.
|
||||||
|
|
||||||
|
## Maße
|
||||||
|
|
||||||
|
|
||||||
|
**Technikbereich**:
|
||||||
|
- 70 lang
|
||||||
|
- 35 breit
|
||||||
|
- 24 hoch
|
||||||
|
|
||||||
|
**Designbereich**:
|
||||||
|
- 130 lang
|
||||||
|
- 51 breit
|
||||||
|
- 32 hoch
|
||||||
|
|
||||||
|
Technik, welche für die Funktion der Kanonen nicht relevant ist, darf sich in maßen auch im Design-Bereich befinden.
|
||||||
|
|
||||||
|
Entfernung von Mitte zu Mitte des Gegners: 100 Block
|
||||||
|
|
||||||
|
Es dürfen keine Blöcke über die Grenze des Designbereiches bewegt werden
|
||||||
|
|
||||||
|
Panzerung darf nur im Technikbereich verbaut werden.
|
||||||
|
|
||||||
|
## Projektile
|
||||||
|
|
||||||
|
Ein Projektil ist ein gezündetes TNT, welches in die gegnerische Hälfte wechselt. Eine Kanone ist eine Vorrichtung zum Beschleunigen von Projektilen. Es ist verboten, mehrere Kanonen als eine Einzige auszugeben.
|
||||||
|
|
||||||
|
Jeder Seite stehen bis zu 32 Projektile zur Verfügung. Sie müssen auf der dem Gegner zugewandten Seite oder, wenn beidseitig, in der Mitte gezündet werden
|
||||||
|
|
||||||
|
## Crossbows
|
||||||
|
|
||||||
|
Kanonen, welche, ohne nachgeladen werden zu müssen, mehrere Schüsse abgeben können, Müssen:
|
||||||
|
- Jeden Schuss das TNT an den selben Punkten zünden.
|
||||||
|
- Für jedes Projektil das die Kanone für einen Schuss maximal zünden könnte, mindestens 1,5 Redstone Tick zwischen den Schüssen warten.
|
||||||
|
|
||||||
|
##### Beispiele
|
||||||
|
|
||||||
|
| Projektile | Ticks |
|
||||||
|
|-------|------------|
|
||||||
|
| 1 | 1,5 |
|
||||||
|
| 2 | 3 |
|
||||||
|
| 3 | 4,5 |
|
||||||
|
| 4 | 6 |
|
||||||
|
| ... | ... |
|
||||||
|
| 8 | 12 |
|
||||||
|
| 12 | 18 |
|
||||||
|
| 16 | 24 |
|
||||||
|
| ... | ... |
|
||||||
|
| 32 | 48 |
|
||||||
|
|
||||||
|
## Siegesbedingung
|
||||||
|
|
||||||
|
- Ein AirShip wird zu 60% (nach prozentualer Redstonekomponenten) zerstört.
|
||||||
|
- Alle Kämpfer eines AirShips sind tot.
|
||||||
|
- Nach 10 Minuten wird ein Unentschieden eingeleitet.
|
||||||
|
- Der Kampfleiter entscheidet.
|
||||||
|
|
||||||
|
## Blöcke
|
||||||
|
|
||||||
|
Es dürfen maximal je 32 TNT und 32 Werfer verbaut werden. Verbaute Blöcke dürfen einen TNT-Widerstand von maximal 6 haben. Ausgenommen davon ist Wasser, welches ausschließlich in Kanonen verbaut werden darf.
|
||||||
|
|
||||||
|
Inventar-Blöcke dürfen nur mit Blumen, Honigflaschen und Pferderüstungen gefüllt sein. Zusätzlich dürfen sich in Kisten und Fässern TNT sowie in Werfern 2·64 Feuerbälle, 2·64 reguläre Pfeile oder 1 Eimer zum Aufheben von powdered Snow befinden.
|
||||||
|
|
||||||
|
Zusätzlich verboten: Monster Spawner, Eis, Nether Portal, Silberfischsteine, Leuchtfeuer, unsichtbare Blöcke (Ausnahme: structure_void) Das Duplizieren von Blöcken und Entities ist verboten.
|
||||||
|
Blöcke aus dem eigenen AS dürfen nicht in großer Menge entfernt werden.
|
||||||
|
Bug-Using ist nicht erwünscht.
|
||||||
|
|
||||||
|
# Versteckte Blöcke (Ersetzt durch Stein)
|
||||||
|
|
||||||
|
- WATER
|
||||||
|
- NOTE_BLOCK
|
||||||
|
- POWERED_RAIL
|
||||||
|
- DETECTOR_RAIL
|
||||||
|
- PISTON
|
||||||
|
- PISTON_HEAD
|
||||||
|
- STICKY_PISTON
|
||||||
|
- TNT
|
||||||
|
- CHEST
|
||||||
|
- TRAPPED_CHEST
|
||||||
|
- REDSTONE_WIRE
|
||||||
|
- STONE_PRESSURE_PLATE
|
||||||
|
- IRON_DOOR
|
||||||
|
- OAK_PRESSURE_PLATE
|
||||||
|
- SPRUCE_PRESSURE_PLATE
|
||||||
|
- BIRCH_PRESSURE_PLATE
|
||||||
|
- JUNGLE_PRESSURE_PLATE
|
||||||
|
- ACACIA_PRESSURE_PLATE
|
||||||
|
- DARK_OAK_PRESSURE_PLATE
|
||||||
|
- REDSTONE_TORCH
|
||||||
|
- REDSTONE_WALL_TORCH
|
||||||
|
- REPEATER
|
||||||
|
- BREWING_STAND
|
||||||
|
- TRIPWIRE_HOOK
|
||||||
|
- TRIPWIRE
|
||||||
|
- HEAVY_WEIGHTED_PRESSURE_PLATE
|
||||||
|
- LIGHT_WEIGHTED_PRESSURE_PLATE
|
||||||
|
- COMPARATOR
|
||||||
|
- REDSTONE_BLOCK
|
||||||
|
- HOPPER
|
||||||
|
- ACTIVATOR_RAIL
|
||||||
|
- DROPPER
|
||||||
|
- SLIME_BLOCK
|
||||||
|
- OBSERVER
|
||||||
|
- HONEY_BLOCK
|
||||||
|
- LEVER
|
||||||
|
|
||||||
|
# Zusatzinhalte folgender Blöcke (z.B. Text, Inhalt von Inventaren) werden mit versteckt:
|
||||||
|
|
||||||
|
- SIGN
|
||||||
|
- DISPENSER
|
||||||
|
- CHEST
|
||||||
|
- TRAPPED_CHEST
|
||||||
|
- FURNACE
|
||||||
|
- BREWING_STAND
|
||||||
|
- HOPPER
|
||||||
|
- DROPPER
|
||||||
|
- SHULKER_BOX
|
||||||
|
- JUKEBOX
|
||||||
|
- COMPARATOR
|
||||||
37
src/content/announcements/de/sw-arcade-fightplan.md
Normal file
37
src/content/announcements/de/sw-arcade-fightplan.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: SteamWar Arcade Eventplan
|
||||||
|
key: sw-arcade-eventplan
|
||||||
|
description: Der Eventplan für SteamWar Arcade
|
||||||
|
created: 2025-05-14T00:00:00.000Z
|
||||||
|
tags:
|
||||||
|
- event
|
||||||
|
- towerrun
|
||||||
|
- misslewars
|
||||||
|
- tntleague
|
||||||
|
- arcade
|
||||||
|
image: ../../../images/sw-arcade-image.png
|
||||||
|
---
|
||||||
|
|
||||||
|
### Infos:
|
||||||
|
|
||||||
|
Eventleitung: Chaoscaot
|
||||||
|
|
||||||
|
Fights werden nach Möglichkeit mit einer Pause von 10 Minuten vorverschoben.
|
||||||
|
|
||||||
|
# Kämpfe
|
||||||
|
|
||||||
|
## Punkte aus der Gruppenphase
|
||||||
|
|
||||||
|
<group-table data-event="70"> </group-table>
|
||||||
|
|
||||||
|
### Fights
|
||||||
|
|
||||||
|
<fight-table data-event="70" data-group="1"> </fight-table>
|
||||||
|
|
||||||
|
## Auswahl des Spielmodis
|
||||||
|
|
||||||
|
Die Teams können Ingame über den Befehl `/event vote` für einen Spielmodus abstimmen. Diese Abstimmung wird für die nächsten Fights gespeichert.
|
||||||
|
|
||||||
|
Wenn die Teams sich einig sind im Spielmodus, wird dieser gespielt. Wenn nicht, dann wird der von keinem Team gewählte Modus gespielt.
|
||||||
|
|
||||||
|
Sollte ein Team nicht abstimmen, wird zufällig eine Stimme abgegeben.
|
||||||
43
src/content/announcements/de/wargear-event.md
Normal file
43
src/content/announcements/de/wargear-event.md
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
title: WarGear Event
|
||||||
|
description: "Das erste WarGear Event in der 1.21 auf SteamWar!"
|
||||||
|
key: steamwar-wg-event-21
|
||||||
|
created: 2025-08-12
|
||||||
|
tags:
|
||||||
|
- event
|
||||||
|
- wargear
|
||||||
|
image: ../../../images/generated-image(11).png
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ahoi, liebe Community,**
|
||||||
|
|
||||||
|
lange ist es her seit dem letzten WarGear-Event. Nun ist es so weit: Am **29. und 30. November** findet ein neues WarGear-Event **mit** SFAs statt.
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
- **Datum:** 29.11.: Gruppenphase, 30.11.: KO-Phase
|
||||||
|
- **Spielmodus:** Standard **und** Pro WarGear
|
||||||
|
- **Teamgröße**: 6
|
||||||
|
- **Anmeldeschluss:** 22. November
|
||||||
|
- **Einsendeschluss:** 24. November
|
||||||
|
- **Hotfix-Schluss:** 27. November
|
||||||
|
|
||||||
|
Bei der SFA muss sich an eines der Regelwerke gehalten werden. Standard- und Pro-WarGear treten gleichwertig gegeneinander an.
|
||||||
|
|
||||||
|
## Sonderregeln
|
||||||
|
|
||||||
|
**Version:** 1.21.6 (aktuellste Bau-Version)
|
||||||
|
|
||||||
|
Es wird einen eigenen Schematic-Typen geben.
|
||||||
|
|
||||||
|
### Windcharges
|
||||||
|
|
||||||
|
Werden beim Überfliegen der Mittellinie entfernt.
|
||||||
|
|
||||||
|
### Cobwebs & Powder Snow
|
||||||
|
|
||||||
|
Dürfen uneingeschränkt benutzt werden, jedoch nicht als Panzerung. Die Bewertung liegt im Ermessen des Prüfers.
|
||||||
|
|
||||||
|
**Verantwortlicher:** Chaoscaot
|
||||||
|
|
||||||
|
**Frohes Bauen!**
|
||||||
@ -17,7 +17,9 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {defineCollection, reference, z} from "astro:content";
|
import { defineCollection, reference, z } from "astro:content";
|
||||||
|
import { docsLoader } from "@astrojs/starlight/loaders";
|
||||||
|
import { docsSchema } from "@astrojs/starlight/schema";
|
||||||
|
|
||||||
export const pagesSchema = z.object({
|
export const pagesSchema = z.object({
|
||||||
title: z.string().min(1).max(80),
|
title: z.string().min(1).max(80),
|
||||||
@ -55,8 +57,7 @@ export const downloads = defineCollection({
|
|||||||
schema: z.object({
|
schema: z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
url: z.string().url()
|
url: z.string().url().or(z.record(z.string(), z.string())),
|
||||||
.or(z.record(z.string(), z.string())),
|
|
||||||
sourceUrl: z.string().url().optional(),
|
sourceUrl: z.string().url().optional(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -71,45 +72,50 @@ export const rules = defineCollection({
|
|||||||
|
|
||||||
export const announcements = defineCollection({
|
export const announcements = defineCollection({
|
||||||
type: "content",
|
type: "content",
|
||||||
schema: ({image}) => z.object({
|
schema: ({ image }) =>
|
||||||
title: z.string(),
|
z.object({
|
||||||
description: z.string(),
|
title: z.string(),
|
||||||
author: z.string().optional(),
|
description: z.string(),
|
||||||
image: image().optional(),
|
author: z.string().optional(),
|
||||||
tags: z.array(z.string()),
|
image: image().optional(),
|
||||||
created: z.date(),
|
tags: z.array(z.string()),
|
||||||
key: z.string(),
|
created: z.date(),
|
||||||
}),
|
key: z.string(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const publics = defineCollection({
|
export const publics = defineCollection({
|
||||||
type: "data",
|
type: "data",
|
||||||
schema: ({image}) => z.object({
|
schema: ({ image }) =>
|
||||||
"name": z.string(),
|
z.object({
|
||||||
"description": z.string(),
|
name: z.string(),
|
||||||
"id": z.number().positive(),
|
description: z.string(),
|
||||||
"creator": z.string().array().optional(),
|
id: z.number().positive(),
|
||||||
"showcase": z.string().url().optional(),
|
creator: z.string().array().optional(),
|
||||||
"camera": z.object({
|
showcase: z.string().url().optional(),
|
||||||
"fov": z.number().optional(),
|
camera: z
|
||||||
"near": z.number().optional(),
|
.object({
|
||||||
"far": z.number().optional(),
|
fov: z.number().optional(),
|
||||||
"distance": z.number().optional(),
|
near: z.number().optional(),
|
||||||
}).optional(),
|
far: z.number().optional(),
|
||||||
"image": image(),
|
distance: z.number().optional(),
|
||||||
"alt": image().optional(),
|
})
|
||||||
"xray": image().optional(),
|
.optional(),
|
||||||
"gamemode": reference("modes"),
|
image: image(),
|
||||||
"3d": z.boolean().optional().default(true),
|
alt: image().optional(),
|
||||||
}),
|
xray: image().optional(),
|
||||||
|
gamemode: reference("modes"),
|
||||||
|
"3d": z.boolean().optional().default(true),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const collections = {
|
export const collections = {
|
||||||
"pages": pages,
|
pages: pages,
|
||||||
"help": help,
|
help: help,
|
||||||
"modes": modes,
|
modes: modes,
|
||||||
"rules": rules,
|
rules: rules,
|
||||||
"downloads": downloads,
|
downloads: downloads,
|
||||||
"announcements": announcements,
|
announcements: announcements,
|
||||||
"publics": publics,
|
publics: publics,
|
||||||
|
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||||
};
|
};
|
||||||
|
|||||||
5
src/content/docs/docs/api/index.md
Normal file
5
src/content/docs/docs/api/index.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Überblick
|
||||||
|
---
|
||||||
|
|
||||||
|
WIP
|
||||||
5
src/content/docs/docs/bausystem/index.md
Normal file
5
src/content/docs/docs/bausystem/index.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Überblick
|
||||||
|
---
|
||||||
|
|
||||||
|
WIP
|
||||||
5
src/content/docs/docs/bausystem/script/index.md
Normal file
5
src/content/docs/docs/bausystem/script/index.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Script Überblick
|
||||||
|
---
|
||||||
|
|
||||||
|
WIP
|
||||||
5
src/content/docs/docs/fightsystem/index.md
Normal file
5
src/content/docs/docs/fightsystem/index.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Überblick
|
||||||
|
---
|
||||||
|
|
||||||
|
WIP
|
||||||
29
src/content/docs/docs/index.mdx
Normal file
29
src/content/docs/docs/index.mdx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
title: Startseite
|
||||||
|
desciption: Startseite der SteamWar Dokumentation
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Tabs, TabItem } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
Herzlich Willkommen in der SteamWar Dokumentation!
|
||||||
|
|
||||||
|
## SteamWar beitreten
|
||||||
|
SteamWar ist ein Minecraft Java Server.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem label="Java Edition">
|
||||||
|
- IP: `steamwar.de`
|
||||||
|
- Empfohlene Version: `1.21.6`
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Bedrock Edition">
|
||||||
|
- IP: `steamwar.de`
|
||||||
|
- Port: `19132`
|
||||||
|
- Version: `Aktuellste`
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
## Open-Source
|
||||||
|
|
||||||
|
Die SteamWar Software ist Open-Source und auf der internen [Gitea](https://git.steamwar.de) verfügbar. Jeglicher Code ist unter der [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html) lizenziert. Das bedeutet, dass du den Code frei nutzen, modifizieren und verteilen kannst, solange du den Code ebenfalls unter der AGPL lizenziert verfügbar machst.
|
||||||
|
|
||||||
|
Bugs und Feature Requests können im [Gitea Issue Tracker](https://git.steamwar.de/SteamWar/SteamWar/issues) erstellt werden. Contributions sind immer willkommen! Bitte erstelle einen Pull Request im [SteamWar Repository](https://git.steamwar.de/SteamWar/SteamWar).
|
||||||
5
src/content/docs/docs/minigames/index.md
Normal file
5
src/content/docs/docs/minigames/index.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Überblick
|
||||||
|
---
|
||||||
|
|
||||||
|
WIP
|
||||||
5
src/content/docs/docs/schematicsystem/index.md
Normal file
5
src/content/docs/docs/schematicsystem/index.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Überblick
|
||||||
|
---
|
||||||
|
|
||||||
|
WIP
|
||||||
@ -2,7 +2,9 @@
|
|||||||
"name": "AdvancedScripts",
|
"name": "AdvancedScripts",
|
||||||
"description": "Ein Fabric-Mod, der für den BauServer von SteamWar Hotkeys für das ScriptSystem hinzufügt. Hierzu werden die einzelnen Zeichen an den Server gesendet und vom Server verarbeitet.",
|
"description": "Ein Fabric-Mod, der für den BauServer von SteamWar Hotkeys für das ScriptSystem hinzufügt. Hierzu werden die einzelnen Zeichen an den Server gesendet und vom Server verarbeitet.",
|
||||||
"url": {
|
"url": {
|
||||||
|
"1.21.6": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.2.3/AdvancedScripts-2.2.3.jar",
|
||||||
"1.21.4": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.2.0/AdvancedScripts-2.2.0.jar",
|
"1.21.4": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.2.0/AdvancedScripts-2.2.0.jar",
|
||||||
|
"1.21.3": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.2.1/AdvancedScripts-2.2.1.jar",
|
||||||
"1.20.6": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.1.0/AdvancedScripts-2.1.0.jar",
|
"1.20.6": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.1.0/AdvancedScripts-2.1.0.jar",
|
||||||
"1.19.3": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.0.0/AdvancedScripts-2.0.0.jar",
|
"1.19.3": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.0.0/AdvancedScripts-2.0.0.jar",
|
||||||
"Hotkey script": "https://git.steamwar.de/SteamWar/SteamWar/src/branch/main/BauSystem/hotkeys.lua"
|
"Hotkey script": "https://git.steamwar.de/SteamWar/SteamWar/src/branch/main/BauSystem/hotkeys.lua"
|
||||||
|
|||||||
5
src/content/modes/spacecraft.json
Normal file
5
src/content/modes/spacecraft.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"translationKey": "sc",
|
||||||
|
"main": false,
|
||||||
|
"ranked": false
|
||||||
|
}
|
||||||
@ -77,13 +77,19 @@ Folgende Richtlinien befassen sich mit dem Verhalten um jegliche Regelbrüche
|
|||||||
# Handel
|
# Handel
|
||||||
|
|
||||||
1. Jeglicher Handel, welcher mit einer reellen Vermögensveränderung verbunden ist, wird im Zusammenhang mit SteamWar nicht gestattet.
|
1. Jeglicher Handel, welcher mit einer reellen Vermögensveränderung verbunden ist, wird im Zusammenhang mit SteamWar nicht gestattet.
|
||||||
1. Bauten, die auf oder mithilfe von SteamWar.de entstanden sind, dürfen nicht verkauft werden. Das bezieht sich auch auf Handel außerhalb von SteamWar.
|
2. Bauten, die auf oder mithilfe von SteamWar.de entstanden sind, dürfen nicht verkauft werden. Das bezieht sich auch auf Handel außerhalb von SteamWar.
|
||||||
|
|
||||||
# Teamserver Integration
|
# Teamserver Integration
|
||||||
|
|
||||||
1. Die Verhaltensrichtlinien gelten auch für Team-Server, die über die Team-Server-Integration mit dem SteamWar-Netzwerk verbunden sind, sofern mehr Spieler als die des verbundenen Teams und wenige weitere ausgewählte Personen auf dem Team-Server zugelassen sind. Ausgenommen hiervon sind Paragraph 4.3.1, da Vorfälle dieser Art dem Serverbetreiber gemeldet werden müssen. Es sei denn es geht um SteamWar, sowie Paragraph 3.1.
|
1. Die Verhaltensrichtlinien gelten auch für Team-Server, die über die Team-Server-Integration mit dem SteamWar-Netzwerk verbunden sind, sofern mehr Spieler als die des verbundenen Teams und wenige weitere ausgewählte Personen auf dem Team-Server zugelassen sind. Ausgenommen hiervon sind:
|
||||||
|
- Paragraf 4.3, da Vorfälle dieser Art dem Serverbetreiber gemeldet werden müssen. Es sei denn es geht um SteamWar
|
||||||
|
- Paragraf 3.1.
|
||||||
2. Das Hinzufügen von Regeln, welche unsere Verhaltensrichtlinien erweitern ist erlaubt, solange diese nicht unsere Verhaltensrichtlinien verletzen.
|
2. Das Hinzufügen von Regeln, welche unsere Verhaltensrichtlinien erweitern ist erlaubt, solange diese nicht unsere Verhaltensrichtlinien verletzen.
|
||||||
3. Diese Regel greift nur, solange entsprechende Spieler auch über SteamWar.de auf den Team-Server gelangt sind
|
3. Diese Regel greift nur, solange entsprechende Spieler auch über SteamWar.de auf den Team-Server gelangt sind
|
||||||
|
4. SteamWar ist für den Inhalt auf einem Team-Server nicht verantwortlich.
|
||||||
|
5. Bei jeglichem rechtlichen Verstoß gegen die Eula von Mojang oder Gesetzen auf dem Team-Server steht SteamWar nicht in der Verantwortung. Diese Verantwortung trägt einzig der/die Verantwortliche(n) des Team-Servers.
|
||||||
|
6. Wir behalten uns vor jeden integrierten Team-Server von SteamWar aus nicht mehr erreichbar zu machen und die Leader aus dem Team zu bestrafen.
|
||||||
|
|
||||||
# Events
|
# Events
|
||||||
|
|
||||||
1. Kampfmaschinen, die von SteamWar in limitierter Form verteilt wurden, dürfen nicht weiterverbreitet werden.
|
1. Kampfmaschinen, die von SteamWar in limitierter Form verteilt wurden, dürfen nicht weiterverbreitet werden.
|
||||||
@ -96,7 +102,7 @@ Mögliche strafen sind:
|
|||||||
* No Schem Receiving: Es ist anderen unmöglich, dich auf ihre Schematics hinzuzufügen oder diese auf deinem Bau zu verwenden. Außerdem kannst du keine Schematics von anderen Spielern kopieren oder Schematics hochladen.
|
* No Schem Receiving: Es ist anderen unmöglich, dich auf ihre Schematics hinzuzufügen oder diese auf deinem Bau zu verwenden. Außerdem kannst du keine Schematics von anderen Spielern kopieren oder Schematics hochladen.
|
||||||
* No Schem Sharing: Du kannst niemandem mehr erlauben, deine Schematics zu verwenden. Zudem kann niemand auf deinem Bau eine Schematic erstellen. Du kannst auch keine Schematics herunterladen.
|
* No Schem Sharing: Du kannst niemandem mehr erlauben, deine Schematics zu verwenden. Zudem kann niemand auf deinem Bau eine Schematic erstellen. Du kannst auch keine Schematics herunterladen.
|
||||||
* No Schem Submitting: Du kannst keine Schematics mehr einreichen.
|
* No Schem Submitting: Du kannst keine Schematics mehr einreichen.
|
||||||
* No Teamserver: Du kannst deinen Teamserver nicht mehr mit SteamWar verknüpfen.
|
* No Teamserver: Du kannst deinen Team-Server nicht mehr mit SteamWar verknüpfen.
|
||||||
* No Event: Dein Team kann nicht mehr an Events teilnehmen.
|
* No Event: Dein Team kann nicht mehr an Events teilnehmen.
|
||||||
* Note: Es wird eine Notiz zu deinem Account hinzugefügt.
|
* Note: Es wird eine Notiz zu deinem Account hinzugefügt.
|
||||||
* Ban: Du kannst nicht mehr auf dem Server spielen.
|
* Ban: Du kannst nicht mehr auf dem Server spielen.
|
||||||
@ -107,4 +113,4 @@ Alle genannten Strafen können entweder temporär oder permanent sein.
|
|||||||
Das Serverteam behält sich das Recht vor, diese Richtlinien jederzeit zu ändern oder zu ergänzen
|
Das Serverteam behält sich das Recht vor, diese Richtlinien jederzeit zu ändern oder zu ergänzen
|
||||||
Unwissenheit schützt nicht vor Strafe!
|
Unwissenheit schützt nicht vor Strafe!
|
||||||
|
|
||||||
Letztes update: `04.05.2024`
|
Letztes update: `31.05.2025`
|
||||||
@ -39,7 +39,7 @@ Die Brücke muss folgende Voraussetzungen erfüllen:
|
|||||||
|
|
||||||
- Min. 25 m² (1 Block = 1 Meter)
|
- Min. 25 m² (1 Block = 1 Meter)
|
||||||
- Ein Sichtfenster auf das gegnerische MicroWarGear (Ausnahme bei Freiluftbrücken mit direkter Sicht auf den Gegner, diese benötigen kein extra Sichtfenster)
|
- Ein Sichtfenster auf das gegnerische MicroWarGear (Ausnahme bei Freiluftbrücken mit direkter Sicht auf den Gegner, diese benötigen kein extra Sichtfenster)
|
||||||
- Die Ansteuerung für min. 4 vom Gegner aus sichtbare Frontscheinwerfer.
|
- Die Ansteuerung für min. 2 vom Gegner aus sichtbare Frontscheinwerfer.
|
||||||
- Diese müssen ihren Zustand beibehalten, bis der/ die Mechanismen erneut manuell betätigt wird/ werden
|
- Diese müssen ihren Zustand beibehalten, bis der/ die Mechanismen erneut manuell betätigt wird/ werden
|
||||||
- Die einzige Ansteuerung für ggf. vorhandene Werfer, die mit Pfeilen oder Feuerbällen den Gegner beschießen
|
- Die einzige Ansteuerung für ggf. vorhandene Werfer, die mit Pfeilen oder Feuerbällen den Gegner beschießen
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,7 @@ Manuelle Kanonen dürfen vor dem Kampfgeschehen kein TNT beinhalten. Diese werde
|
|||||||
|
|
||||||
### Automatische Kanonen
|
### Automatische Kanonen
|
||||||
|
|
||||||
Automatische Kanonen sind Kanonen, welche vor dem Kampfgeschehen TNT beinhalten und ohne nachgeladen zu werden mehrere Schüsse abgeben können. Zu beachten ist, dass die Projektile aller Schüsse immer von dem/den exakt gleichen Punkt-/en aus abgeschossen werden müssen. Vor Fightbeginn dürfen automatische Kanonen vollständig leergeschossen werden. Automatische Kanonen müssen von der Kommandozentrale aus aktivierbar sein. Dies kann auch durch den Verbau des Autostarters innerhalb der Kommandozentrale erfolgen. Des weiteren muss eine Möglichkeit innerhalb der Kommandozentrale gegeben sein die automatische Kanone vollständig vor Fightbeginn leerschießen zu können.
|
Automatische Kanonen sind Kanonen, welche vor dem Kampfgeschehen TNT beinhalten und ohne nachgeladen zu werden mehrere Schüsse abgeben können. Zu beachten ist, dass die Projektile aller Schüsse immer von dem/den exakt gleichen Punkt-/en aus gezündet und abgeschossen werden müssen. Außerdem müssen alle Treibladungen am dem/den exakt gleichen Punkt-/en gezündet werden. Vor Fightbeginn dürfen automatische Kanonen vollständig leergeschossen werden. Automatische Kanonen müssen von der Kommandozentrale aus aktivierbar sein. Dies kann auch durch den Verbau des Autostarters innerhalb der Kommandozentrale erfolgen. Des weiteren muss eine Möglichkeit innerhalb der Kommandozentrale gegeben sein die automatische Kanone vollständig vor Fightbeginn leerschießen zu können.
|
||||||
|
|
||||||
Das wiederverwenden des Abschusswinkels einer Automatischen Kanone zählt immer als zweite Kanone. Auch das Nachladen der Automatischen Kanone zählt als zweite Kanone.
|
Das wiederverwenden des Abschusswinkels einer Automatischen Kanone zählt immer als zweite Kanone. Auch das Nachladen der Automatischen Kanone zählt als zweite Kanone.
|
||||||
|
|
||||||
|
|||||||
@ -7,73 +7,115 @@ mode: warship
|
|||||||
|
|
||||||
## Definitionen
|
## Definitionen
|
||||||
|
|
||||||
### WarShip
|
### §1 WarShip
|
||||||
|
|
||||||
Ein WarShip ist eine bewaffnete, schwimmende Struktur in Minecraft mit der optischen Erscheinung eines Schiffes. Der Schwimmkörper muss dabei einen Großteil des WarShips ausmachen. Ein WarShip kann optional ein Design aufweisen, das andere im Wasser schwimmende/befindliche Dinge oder Tiere repräsentiert, sofern das gewählte Design gänzlich implementiert wird. Jedes WarShip muss beidseitig gleich bewaffnet sein.
|
1. Ein WarShip ist eine bewaffnete, schwimmende Struktur mit der optischen Erscheinung eines Schiffes. Der Schwimmkörper muss dabei einen Großteil des WarShips ausmachen.
|
||||||
|
2. Ein WarShip kann optional ein Design aufweisen, das andere im Wasser schwimmende/befindliche Dinge oder Tiere repräsentiert, sofern das gewählte Design gänzlich implementiert wird.
|
||||||
|
3. Jedes WarShip muss beidseitig gleich bewaffnet sein.
|
||||||
|
|
||||||
### Projektil
|
### §2 Projektil
|
||||||
|
|
||||||
Ein Projektil ist ein TNT, welches im gezündeten Zustand in die gegnerische Hälfte wechselt. Projektile müssen auf der dem Gegner zugewandten Schiffsseite gezündet werden. Unter Wasser gezündete Projektile müssen innerhalb des Technikbereiches, oberhalb der Wasserlinie gezündete Projektile innerhalb des Ausfahrbereichs gezündet werden.
|
1. Ein Projektil ist ein TNT, welches im gezündeten Zustand in die gegnerische Hälfte wechselt.
|
||||||
|
2. Projektile müssen auf der dem Gegner zugewandten Schiffsseite gezündet werden.
|
||||||
|
3. Die Zündung eines Projektils muss innerhalb des Technikbereiches stattfinden, über der Wasserlinie darf es auch innerhalb des Ausfahrbereichs gezündet werden.
|
||||||
|
|
||||||
### Kanone
|
### §3 Kanone
|
||||||
|
|
||||||
Eine Kanone ist eine Vorrichtung zum Beschleunigen von Projektilen. Eine Kanone darf maximal 2 Projektile verschießen. Eine Kanone muss manuell beladen werden. Eine Kanone darf maximal alle 2s schießen. Es dürfen maximal 32 Kanonen pro Seite verbaut werden. Kanonen sind der einzige Ort, an dem Wasser verbaut werden darf, wenn es keinen anderen Zweck hat, als diese vor selbst verursachten Schaden zu bewahren und TNT zu transportieren.
|
1. Eine Kanone ist eine Vorrichtung zum Beschleunigen von maximal 2 Projektilen.
|
||||||
|
2. Eine Kanone muss manuell beladen werden und darf maximal alle 2s schießen.
|
||||||
|
3. Kanonen dürfen nicht gezielt Projektile anderer Kanonen beeinflussen.
|
||||||
|
|
||||||
## Maße
|
### §4 Raketen und Flugmaschinen
|
||||||
|
|
||||||
- Länge: 230 Block
|
1. Flugmaschinen sind automatisch bewegliche Blockkonstruktion, welche die Ausfahrmaße des WarShips verlassen.
|
||||||
- Breite: 35 Block (+ 4 Block Design pro Seite)
|
2. Raketen sind mit TNT bestückte Flugmaschinen
|
||||||
- Höhe: 30 Block + 20 Block Design
|
3. Ein Raketenmagazin enthält mehrere Raketen und schickt sie auf nahezu gleicher Flugbahn zum Gegner.
|
||||||
- Tiefe: Bis zu 8 Block unter dem Meeresspiegel
|
4. Raketen und Flugmaschinen dürfen sich während des Fluges nicht in mehrere flugfähige Einheiten aufteilen.
|
||||||
|
|
||||||
Bei jedem WarShip müssen sich mindestens 10% der absoluten Blöcke (45.000 Blöcke) über der Wasserlinie befinden.
|
### §5 Brücke
|
||||||
|
|
||||||
Im Designbereich dürfen sich keine kampfrelevanten (Kanonen, Schleim/Honigfahrzeuge, Schilde) Techniken befinden. Eine Durchpanzerung des Designbereiches ist nicht zulässig. Der Designbereich ist ausschließlich für einzelne überstehende Designobjekte wie beispielsweise Kanonenrohre, Segel, Banner oder Bullaugen da.
|
1. Eine Brücke ist der optisch zentrale Kontroll- und Steuerbereich eines Schiffes.
|
||||||
|
2. Die Brücke muss folgende Anforderung erfüllen:
|
||||||
|
|
||||||
## Blöcke
|
- Mindestens 50 begehbare Blöcke.
|
||||||
|
- Mindesthöhe von 2 Blöcken im gesamten Brückenraum
|
||||||
|
- Steuerung von mindestens zwei zum Gegner gerichteten Scheinwerfern
|
||||||
|
- Optische Brückeneinrichtung (z. B. Instrumente, Anzeigen)
|
||||||
|
|
||||||
Es dürfen nur Blöcke mit einem TNT-Widerstand von maximal 6 verbaut werden. Vor und im Kampf dürfen sich in allen Blöcken mit Inventar nur Blumen, Honigflaschen und Pferderüstungen befinden, in Kisten und Fässer darf auch TNT sein. Es dürfen maximal 32 Werfer pro Seite verbaut werden. In Werfern dürfen sich 2×64 Feuerbälle oder 2×64 Pfeile (ohne Effekte) befinden.
|
### §6 Bereiche
|
||||||
|
|
||||||
Das Verbauen von unsichtbaren Mauern oder anderen unsichtbaren Blöcken mit Hitbox ist verboten.
|
1. Der Technikbereich ist der einzige Bereich, der im nicht ausgefahrenen Zustand, kampfrelevante Technik enthalten darf.
|
||||||
Ebenso verboten sind: Monsterspawner, Eis, Netherportalblock, Alle Silberfischsteine.
|
2. Der Designbereich ist ausschließlich für einzelne überstehende Designobjekte wie beispielsweise Kanonenrohre, Segel, Banner oder Bullaugen da.
|
||||||
|
3. Eine Durchpanzerung des Designbereiches ist nicht zulässig.
|
||||||
|
4. Ausfahrbereich wird in §8 Abs. 2 definiert.
|
||||||
|
|
||||||
Das Missbrauchen von unverschiebbaren Blöcken als Panzerung ist verboten.
|
## Parameter
|
||||||
Wasser darf nicht zum Schutz des eigenen WarShips missbraucht werden.
|
|
||||||
|
|
||||||
## Design
|
### §7 Ausstattung
|
||||||
|
|
||||||
Größere Hohlräume im Rumpf zum Ausweichen feindlicher Schüsse sind nicht gestattet, auch nicht während des Kampfes. Jedes WarShip braucht eine Flagge.
|
1. Es dürfen maximal,
|
||||||
|
|
||||||
Ein WarShip benötigt einen fortbewegungsfähigen Rumpf mit entsprechendem Antrieb (z.B. Segel, Schiffsschrauben).
|
- 6 Flugmaschinen, (Flugmaschinen die dazu dienen, TNT von einigen Raketen zu zünden oder andere eigene Flugmaschinen zu stoppen, werden nicht gezählt)
|
||||||
Der Rumpf muss mindestens einen Block tief unter Wasser sowie mindestens 5 Block über dem Meeresspiegel sein (dies gilt auch während des Kampfes relativ zur Wasseroberfläche). Der Rumpf darf max. 16 Block hoch sein.
|
- 8 Raketen (separat von den Flugmaschinen gezählt), wobei ein Raketenmagazin als 2 Raketen zählt,
|
||||||
|
- 28 Kanonen,
|
||||||
|
- 32 Werfer,
|
||||||
|
pro Seite verbaut werden.
|
||||||
|
|
||||||
Jedes WarShip benötigt eine Brücke, welche die folgenden Kriterien erfüllt:
|
2. Es dürften maximal 1000 der vorverbauten Slime-, Honig- & TNT-Blöcke den Ausfahrbereich, als Bestandteil von Flugmaschinen, verlassen.
|
||||||
|
3. Jedes WarShip benötigt eine Brücke.
|
||||||
|
4. Jedes WarShip braucht eine Flagge.
|
||||||
|
|
||||||
- Min. 50 begehbare Blöcke
|
### §8 Maße
|
||||||
- Min. 2 Block hoch im gesamten Brückenraum
|
|
||||||
- Ansteuerung von min. zwei zum Gegner gewandten Scheinwerfer
|
|
||||||
- Optische Brückeneinrichtung
|
|
||||||
|
|
||||||
## Anti-Lag-Regeln
|
1. Die Maße eines Schiffs dürfen folgende Werte nicht überschreiten:
|
||||||
|
|
||||||
Clocks müssen sich mit Ende ihres Einsatzzweckes selbst abschalten.
|
- Länge: 175 Blöcke
|
||||||
Sämtliche Redstonetechnik zum Schutz des eigenen WarShips muss ihre Aktivität vor dem Verteilen der Kits eingestellt haben.
|
- Breite: 31 Blöcke + 4 Blöcke Designbereich pro Seite
|
||||||
Raketen und Flugmaschinen
|
- Höhe über dem Meeresspiegel: 30 Blöcke + 20 Blöcke Designbereich
|
||||||
|
- Tiefe unter dem Meeresspiegel: 8 Blöcke
|
||||||
|
|
||||||
Ein WarShip darf sich maximal 12 Block vom Technikbereich an weit ausfahren, davon ausgenommen sind Raketen und Flugmaschinen. Raketen und Flugmaschinen dürfen sich im Flug nicht in mehrere Schleim/Honigfahrzeuge aufteilen.
|
2. Ein WarShip darf sich maximal 12 Blöcke vom Technikbereich ausfahren. Ausgenommen davon sind Flugmaschinen. Dieser Bereich wird als Ausfahrbereich deklariert.
|
||||||
|
3. Bei jedem WarShip müssen sich mindestens 10% der absoluten Blöcke (30.000 Blöcke) über der Wasserlinie befinden.
|
||||||
|
|
||||||
Flugmaschinen sind Schleim/Honigfahrzeuge, welche die Ausfahrmaße des WarShips verlassen und kein TNT zum Gegner transportieren. Flugmaschinen, welche TNT von Raketen zünden oder Flugmaschinen und/oder Raketen stoppen werden bei der Bestimmung der Anzahl nicht gezählt. Es dürfen maximal 8 Flugmaschinen pro Seite verbaut werden.
|
### §9 Blöcke
|
||||||
|
|
||||||
Eine Rakete ist ein Schleim/Honigfahrzeug, das TNT zum Gegner transportiert. Es dürfen maximal 12 Raketen pro Seite verbaut werden. Ein Raketenmagazin ist in der Lage, mehrere Raketen auf der nahezu gleichen Flugbahn zum Gegner zu schicken. Ein Raketenmagazin wird wie 2 Raketen gewertet.
|
1. Es dürfen nur Blöcke mit einem TNT-Widerstand von maximal 6 verbaut werden.
|
||||||
|
2. Inventarblöcke dürfen nur Honigflaschen, Pferderüstungen und Blumen enthalten.
|
||||||
|
1. In Kisten und Fässern darf sich auch TNT befinden.
|
||||||
|
1. Werfer dürfen maximal 2 Stacks (2 × 64) Feuerbälle und Pfeile (ohne Effekte) enthalten. (Eimer innerhalb von Werfern der in Powder Snow schaut)
|
||||||
|
3. Es ist verboten, Monsterspawner, Eis, Netherportalblock, alle Silberfischsteine und unsichtbare Blöcke mit Hitboxen zu verbauen.
|
||||||
|
4. Der Missbrauch von nicht verschiebbaren Blöcken als Panzerung ist nicht gestattet.
|
||||||
|
5. Wasser darf nur in Kanonen zum Schutz vor Selbstbeschädigung oder um TNT zu transporieren verbaut werden.
|
||||||
|
1. Wasser, Spinnennetze oder Pulverschnee darf nicht zum Schutz des eigenen WarShips missbraucht werden.
|
||||||
|
|
||||||
Es dürften maximal 1000 der vorverbauten Slime- + Honig- + TNT-Blöcke das WarShip in Flugmaschinen und Raketen verlassen.
|
### §10 Design
|
||||||
|
|
||||||
## Kampfablauf
|
1. Ein WarShip benötigt einen fortbewegungsfähigen Rumpf mit entsprechendem Antrieb (z.B. Segel, Schiffsschrauben).
|
||||||
|
2. Der Rumpf muss mindestens 1 Block unter Wasser und zwischen 5 und maximal 16 Blöcken über dem Meeresspiegel liegen (auch während des Kampfes, relativ zur Wasseroberfläche).
|
||||||
|
3. Größere Hohlräume im Rumpf zum Ausweichen feindlicher Schüsse sind nicht gestattet, auch nicht während des Kampfes.
|
||||||
|
|
||||||
60 Sekunden vor Kampfbeginn können Flugmaschinen und Raketen das eigene WarShip verlassen. Mit Kampfbeginn dürfen Blöcke abgebaut und platziert werden; TNT-Schaden wird aktiviert. 10 Minuten nach Kampfbeginn wird das Entern des feindlichen WarShips erlaubt. Spieler mit einem Kit, welches TNT beladen kann und nicht dem Kapitän zugeordnet ist, dürfen erst 15 Minuten nach Kampfbeginn entern.
|
### §11 Anti-Lag-Regeln
|
||||||
|
|
||||||
Der Kampf endet, wenn:
|
1. Clocks müssen sich mit Ende ihres Einsatzzweckes selbst abschalten.
|
||||||
|
2. Sämtliche Redstone Technik zum Schutz des eigenen WarShips muss ihre Aktivität vor dem Verteilen der Kits eingestellt haben.
|
||||||
|
|
||||||
- Der Kampf länger als 20 Minuten dauert
|
### §12 Kampfablauf
|
||||||
- Der Anführer eines Teams tot ist
|
|
||||||
- Ein WarShip zu 7% beschädigt wurde
|
1. 60 Sekunden vor Kampfbeginn können Flugmaschinen und Raketen das eigene WarShip verlassen.
|
||||||
|
2. Mit Kampfbeginn dürfen Blöcke abgebaut und platziert werden. Der TNT-Schaden wird aktiviert.
|
||||||
|
3. 10 Minuten nach Kampfbeginn wird das Entern des feindlichen WarShips erlaubt. Spieler mit einem Kit, welches TNT beladen kann und nicht dem Kapitän zugeordnet ist, dürfen erst 15 Minuten nach Kampfbeginn entern.
|
||||||
|
4. Der Kampf endet,
|
||||||
|
|
||||||
|
- wenn er länger als 20 Minuten dauert.
|
||||||
|
- wenn der Kapitän eines Teams tot ist.
|
||||||
|
- wenn ein WarShip zu 7% beschädigt wurde.
|
||||||
|
|
||||||
|
### §13 Siegeskriterium
|
||||||
|
|
||||||
|
1. Folgende drei Szenarien führen zum Sieg:
|
||||||
|
|
||||||
|
- Das erfolgreiche eliminieren des gegnerischen Kapitäns.
|
||||||
|
- Eine Beschädigung des gegnerischen WarShips von mindestens 7%.
|
||||||
|
- Das nach Ablauf der Zeit das weniger beschädigte Schiff (prozentual).
|
||||||
|
|
||||||
|
2. Sollte keines dieser Szenarien eintreffen, endet der Kampf in einem Unentschieden.
|
||||||
|
|||||||
@ -209,6 +209,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
|
"page": "SteamWar - Login",
|
||||||
"title": "Login",
|
"title": "Login",
|
||||||
"placeholder": {
|
"placeholder": {
|
||||||
"username": "Nutzername...",
|
"username": "Nutzername...",
|
||||||
@ -219,7 +220,7 @@
|
|||||||
"password": "Passwort",
|
"password": "Passwort",
|
||||||
"repeat": "Passwort Wiederholen"
|
"repeat": "Passwort Wiederholen"
|
||||||
},
|
},
|
||||||
"setPassword": "Wie setzte ich mein Passwort?",
|
"setPassword": "Wie setze ich mein Passwort?",
|
||||||
"submit": "Login",
|
"submit": "Login",
|
||||||
"error": "Falscher Nutzername oder falsches Passwort"
|
"error": "Falscher Nutzername oder falsches Passwort"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
src/images/SpaceCraftWinners3.png
Normal file
BIN
src/images/SpaceCraftWinners3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
BIN
src/images/generated-image(11).png
Normal file
BIN
src/images/generated-image(11).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/images/generated-image(8).png
Normal file
BIN
src/images/generated-image(8).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user