2 Commits

Author SHA1 Message Date
D4rkr34lm 4d100fcafc Started implementing tutorials
SteamWarCI Build failed
2025-04-23 10:39:19 +02:00
D4rkr34lm 82f5ab48b8 Add tracer tutorial 2025-01-16 21:47:03 +01:00
502 changed files with 10355 additions and 24423 deletions
+4
View File
@@ -37,6 +37,10 @@
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
-88
View File
@@ -1,88 +0,0 @@
name: SteamWar CI
on:
push:
branches:
- master
pull_request:
workflow_dispatch:
env:
PUBLIC_API_SERVER: https://api.steamwar.de
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 8.14.0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Write production environment
run: echo "PUBLIC_API_SERVER=${PUBLIC_API_SERVER}" > .env
- name: Compile i18n files
run: pnpm run i18n:compile
- name: Build website
run: pnpm run build
- name: Upload website artifact
uses: actions/upload-artifact@v3
with:
name: steamwar-website
path: dist/
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
steps:
- name: Download website artifact
uses: actions/download-artifact@v3
with:
name: steamwar-website
path: dist
- name: Upload website with scp
shell: bash
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_PATH: /var/www/html
run: |
set -euo pipefail
: "${DEPLOY_HOST:?Missing DEPLOY_HOST secret}"
: "${DEPLOY_USER:?Missing DEPLOY_USER secret}"
: "${DEPLOY_SSH_KEY:?Missing DEPLOY_SSH_KEY secret}"
port="${DEPLOY_PORT:-22}"
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -p "$port" "$DEPLOY_HOST" >> ~/.ssh/known_hosts
ssh -i ~/.ssh/deploy_key -p "$port" "${DEPLOY_USER}@${DEPLOY_HOST}" "mkdir -p '$DEPLOY_PATH' && find '$DEPLOY_PATH' -mindepth 1 -maxdepth 1 -exec rm -rf {} +"
scp -i ~/.ssh/deploy_key -P "$port" -r dist/* "${DEPLOY_USER}@${DEPLOY_HOST}:$DEPLOY_PATH/"
+2 -1
View File
@@ -19,9 +19,10 @@ pnpm-debug.log*
# macOS-specific files
.DS_Store
/.astro-i18n/
/bun.lockb
/src/pages/de/
/src/pages/en/
/steamwar-website.zip
/src/env.d.ts
/src/pages/en/
/.idea
+3 -3
View File
@@ -7,7 +7,7 @@
- [Tailwind CSS](https://tailwindcss.com/)
- [Day.js](https://day.js.org/)
- [Chart.js](https://www.chartjs.org/)
- [Paraglide JS](https://inlang.com/m/gerre34r/library-inlang-paraglideJs)
- [astro-i18n](https://github.com/Alexandre-Fernandez/astro-i18n)
- [Flowbite Svelte](https://flowbite-svelte.com/)
## Building
@@ -28,8 +28,8 @@ pnpm run dev
pnpm run build
```
### i18n Compile
### i18n-sync
```bash
pnpm run i18n:compile
pnpm run i18n:sync
```
+37
View File
@@ -0,0 +1,37 @@
import type { AstroIntegration } from "astro";
import { mkdir, access, constants, copyFile, rename } from "node:fs/promises";
const locales = ["en"];
export default function configureI18n(): AstroIntegration {
return {
name: "astro-i18n-renamer",
hooks: {
"astro:build:done": async ({pages, dir, logger, routes}) => {
for (const page of pages) {
const [locale, ...rest] = page.pathname.split("/");
if (locales.includes(locale)) {
const path = rest.join("/");
const oldPath = `${dir.pathname}${page.pathname}`;
const newPath = `${dir.pathname}${path}`;
try {
await access(cutPrefix(newPath), constants.R_OK | constants.W_OK);
} catch (e) {
await mkdir(cutPrefix(newPath), {recursive: true});
}
await copyFile(`${cutPrefix(oldPath)}index.html`, `${cutPrefix(newPath)}index.html.${locale}`);
logger.info(`Copied ${oldPath}index.html to ${newPath}index.html.${locale}`);
} else {
const oldPath = cutPrefix(`${dir.pathname}${page.pathname}`);
await rename(`${oldPath}index.html`, `${oldPath}index.html.de`);
}
}
},
},
};
}
function cutPrefix(path: string): string {
return process.platform === "win32" ? path.substring(1) : path;
}
+27
View File
@@ -0,0 +1,27 @@
import { defineAstroI18nConfig } from "astro-i18n";
export default defineAstroI18nConfig({
primaryLocale: "de",
secondaryLocales: ["en"],
fallbackLocale: "de",
trailingSlash: "never",
run: "client+server",
showPrimaryLocale: false,
translationLoadingRules: [],
translationDirectory: {},
translations: {},
routes: {
en: {
"jetzt-spielen": "join",
impressum: "imprint",
verhaltensrichtlinien: "code-of-conduct",
regeln: "rules",
rangliste: "ranked",
"haeufige-fragen": "faq",
statistiken: "stats",
ankuendigungen: "announcements",
datenschutzerklaerung: "privacy-policy",
"passwort-setzen": "set-password",
},
},
});
+12 -103
View File
@@ -1,79 +1,18 @@
import { defineConfig, fontProviders, sharpImageService } from "astro/config";
import {defineConfig, sharpImageService} from "astro/config";
import svelte from "@astrojs/svelte";
import tailwindcss from "@tailwindcss/vite";
import tailwind from "@astrojs/tailwind";
import configureI18n from "./astro-i18n.adapter";
import sitemap from "@astrojs/sitemap";
import { paraglideVitePlugin } from "@inlang/paraglide-js";
import robotsTxt from "astro-robots-txt";
import {resolve} from "node:url";
import path from "node:path";
import mdx from "@astrojs/mdx";
import starlight from "@astrojs/starlight";
import pagefind from "astro-pagefind";
// https://astro.build/config
export default defineConfig({
fonts: [{
provider: fontProviders.google(),
name: "Roboto",
cssVariable: "--font-roboto",
}, {
provider: fontProviders.local(),
name: "Barlow Condensed",
cssVariable: "--font-barlow-condensed",
options: {
variants: [
{
src: ["./src/assets/fonts/barlow-condensed/barlow-condensed-regular.woff2"],
weight: "400",
style: "normal",
}, {
src: ["./src/assets/fonts/barlow-condensed/barlow-condensed-medium.woff2"],
weight: "500",
style: "normal"
}, {
src: ["./src/assets/fonts/barlow-condensed/barlow-condensed-bold.woff2"],
weight: "700",
style: "normal"
}, {
src: ["./src/assets/fonts/barlow-condensed/barlow-condensed-italic.woff2"],
weight: "400",
style: "italic"
}, {
src: ["./src/assets/fonts/barlow-condensed/barlow-condensed-medium-italic.woff2"],
weight: "500",
style: "italic"
}, {
src: ["./src/assets/fonts/barlow-condensed/barlow-condensed-bold-italic.woff2"],
weight: "700",
style: "italic"
}, {
src: ["./src/assets/fonts/barlow-condensed/barlow-condensed-black.woff2"],
weight: "900",
style: "normal"
}, {
src: ["./src/assets/fonts/barlow-condensed/barlow-condensed-black-italic.woff2"],
weight: "900",
style: "italic"
}, {
src: ["./src/assets/fonts/barlow-condensed/barlow-condensed-light.woff2"],
weight: "300",
style: "normal"
}, {
src: ["./src/assets/fonts/barlow-condensed/barlow-condensed-light-italic.woff2"],
weight: "300",
style: "italic"
}
]
}
}],
output: "static",
i18n: {
defaultLocale: "de",
locales: ["de", "en"],
routing: {
prefixDefaultLocale: false,
},
},
image: {
service: sharpImageService(),
},
@@ -81,35 +20,15 @@ export default defineConfig({
site: "https://steamwar.de",
integrations: [
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({
configFile: "./tailwind.config.cjs",
}),
pagefind(),
configureI18n(),
sitemap({
i18n: {
defaultLocale: "de",
locales: {
en: "en-US",
de: "de-DE",
defaultLocale: "en", locales: {
en: "en-US", de: "de-DE",
},
},
}),
@@ -133,21 +52,12 @@ export default defineConfig({
{ userAgent: "omgili", disallow: "/" },
{ userAgent: "OmigliBot", disallow: "/" },
{ userAgent: "PerplexityBot", disallow: "/" },
{ userAgent: "Timpibot", disallow: "/" },
{ userAgent: "Timpibot", disallow: "/" }
],
}),
mdx(),
],
vite: {
plugins: [
paraglideVitePlugin({
project: "./project.inlang",
outdir: "./src/paraglide",
strategy: ["url", "globalVariable", "baseLocale"],
emitTsDeclarations: true,
}),
tailwindcss(),
],
resolve: {
alias: {
"@components": path.resolve("./src/components"),
@@ -159,7 +69,6 @@ export default defineConfig({
"@layouts": path.resolve("./src/layouts"),
"@repo": path.resolve("./src/components/repo"),
"@stores": path.resolve("./src/components/stores"),
$lib: path.resolve("./src"),
},
},
},
-16
View File
@@ -1,16 +0,0 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"tailwind": {
"css": "src\\styles\\app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/components/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks",
"lib": "$lib"
},
"typescript": true,
"registry": "https://tw3.shadcn-svelte.com/registry/default"
}
+50 -69
View File
@@ -3,86 +3,67 @@
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "pnpm run i18n:generate:pages && astro dev",
"start": "pnpm run dev",
"build": "pnpm run i18n:generate:pages && astro build",
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"i18n:generate:pages": "node scripts/generate-en-pages.mjs",
"i18n:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide --strategy url globalVariable baseLocale --emit-ts-declarations && pnpm run i18n:generate:pages",
"i18n:extract": "astro-i18n extract",
"i18n:generate:pages": "astro-i18n generate:pages --purge",
"i18n:generate:types": "astro-i18n generate:types",
"i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types",
"clean:dist": "rm -rf dist",
"clean:node_modules": "rm -rf node_modules",
"ci": "pnpm install && pnpm run build"
"ci": "pnpm run clean:dist && pnpm install && pnpm run i18n:sync && pnpm run build"
},
"devDependencies": {
"@astrojs/svelte": "^8.1.1",
"@internationalized/date": "^3.12.1",
"@lucide/svelte": "^1.16.0",
"@tailwindcss/vite": "^4.3.0",
"@types/color": "^4.2.1",
"@types/js-yaml": "^4.0.9",
"@types/node": "^25.9.0",
"@types/three": "^0.184.1",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"autoprefixer": "^10.5.0",
"bits-ui": "2.18.1",
"clsx": "^2.1.1",
"cssnano": "^8.0.1",
"embla-carousel-svelte": "^8.6.0",
"esbuild": "^0.28.0",
"eslint": "^10.4.0",
"eslint-plugin-astro": "^1.7.0",
"eslint-plugin-svelte": "^3.17.1",
"formsnap": "2.0.1",
"mode-watcher": "^1.1.0",
"paneforge": "^1.0.2",
"postcss-nesting": "^14.0.0",
"sass": "^1.99.0",
"svelte": "^5.55.8",
"svelte-sonner": "^1.1.1",
"tailwind-merge": "^3.6.0",
"tailwind-variants": "^3.2.2",
"tailwindcss": "^4.3.0",
"three": "^0.184.0",
"typescript": "^6.0.3",
"zod": "^4.4.3"
"@astrojs/svelte": "^6.0.2",
"@astrojs/tailwind": "^5.1.2",
"@astropub/icons": "^0.2.0",
"@types/color": "^4.2.0",
"@types/node": "^22.9.3",
"@types/three": "^0.170.0",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"autoprefixer": "^10.4.20",
"cssnano": "^7.0.6",
"esbuild": "^0.24.0",
"eslint": "^9.15.0",
"eslint-plugin-astro": "^1.3.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-svelte": "^2.46.0",
"postcss-nesting": "^13.0.1",
"sass": "^1.81.0",
"svelte": "^5.16.0",
"tailwind-merge": "^2.5.5",
"tailwindcss": "^3.4.15",
"three": "^0.170.0",
"typescript": "^5.7.2"
},
"dependencies": {
"@astrojs/mdx": "^5.0.6",
"@astrojs/sitemap": "^3.7.2",
"@astrojs/starlight": "^0.39.2",
"@astrojs/starlight-tailwind": "^5.0.0",
"@codemirror/commands": "^6.10.3",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/view": "^6.43.0",
"@ddietr/codemirror-themes": "^1.5.2",
"@inlang/paraglide-js": "^2.18.0",
"@tanstack/table-core": "^8.21.3",
"astro": "6.3.5",
"@astrojs/mdx": "^3.1.9",
"@astrojs/sitemap": "^3.2.1",
"@codemirror/lang-json": "^6.0.1",
"@ddietr/codemirror-themes": "^1.4.4",
"astro": "^4.16.14",
"astro-i18n": "^2.2.4",
"astro-pagefind": "^1.6.0",
"astro-robots-txt": "^1.0.0",
"astro-seo": "^1.1.0",
"chart.js": "^4.5.1",
"astro-seo": "^0.8.4",
"chart.js": "^4.4.6",
"chartjs-adapter-dayjs-4": "^1.0.4",
"chartjs-adapter-moment": "^1.0.1",
"codemirror": "^6.0.2",
"color": "^5.0.3",
"dayjs": "^1.11.20",
"easymde": "^2.21.0",
"flowbite": "^4.0.2",
"flowbite-svelte": "^1.33.1",
"flowbite-svelte-icons": "^3.1.0",
"js-yaml": "^4.1.1",
"qs": "^6.15.2",
"sharp": "^0.34.5",
"color": "^4.2.3",
"dayjs": "^1.11.13",
"easymde": "^2.18.0",
"flowbite": "^2.5.2",
"flowbite-svelte": "^0.47.3",
"flowbite-svelte-icons": "^2.0.2",
"qs": "^6.13.1",
"sharp": "^0.33.5",
"svelte-awesome": "^3.3.5",
"svelte-spa-router": "^5.1.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"esbuild",
"sharp"
]
"svelte-codemirror-editor": "^1.4.1",
"svelte-spa-router": "^4.0.1",
"zod": "^3.23.8"
}
}
+7160 -5911
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,6 +4,6 @@ module.exports = {
plugins: [
require('autoprefixer'),
require('cssnano'),
require("postcss-nesting"),
require("tailwindcss/nesting"),
]
};
-11
View File
@@ -1,11 +0,0 @@
{
"$schema": "https://inlang.com/schema/project-settings",
"baseLocale": "de",
"locales": ["de", "en"],
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@latest/dist/index.js"
],
"plugin.inlang.messageFormat": {
"pathPattern": "./src/i18n/common/{locale}.json"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 406 KiB

@@ -0,0 +1,145 @@
/* barlow-condensed-thin */
@font-face {
font-family: Barlow Condensed;
font-style: normal;
font-weight: 100;
src: local("Barlow Condensed Thin"), local("BarlowCondensed-Thin"), url(barlow-condensed-thin.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-thin-italic */
@font-face {
font-family: Barlow Condensed;
font-style: italic;
font-weight: 100;
src: local("Barlow Condensed Thin Italic"), local("BarlowCondensed-ThinItalic"), url(barlow-condensed-thin-italic.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-extralight */
@font-face {
font-family: Barlow Condensed;
font-style: normal;
font-weight: 400;
src: local("Barlow Condensed ExtraLight"), local("BarlowCondensed-ExtraLight"), url(barlow-condensed-extralight.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-extralight-italic */
@font-face {
font-family: Barlow Condensed;
font-style: italic;
font-weight: 400;
src: local("Barlow Condensed ExtraLight Italic"), local("BarlowCondensed-ExtraLightItalic"), url(barlow-condensed-extralight-italic.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-light */
@font-face {
font-family: Barlow Condensed;
font-style: normal;
font-weight: 300;
src: local("Barlow Condensed Light"), local("BarlowCondensed-Light"), url(barlow-condensed-light.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-light-italic */
@font-face {
font-family: Barlow Condensed;
font-style: italic;
font-weight: 300;
src: local("Barlow Condensed Light Italic"), local("BarlowCondensed-LightItalic"), url(barlow-condensed-light-italic.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-regular */
@font-face {
font-family: Barlow Condensed;
font-style: normal;
font-weight: 400;
src: local("Barlow Condensed Regular"), local("BarlowCondensed-Regular"), url(barlow-condensed-regular.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-italic */
@font-face {
font-family: Barlow Condensed;
font-style: italic;
font-weight: 400;
src: local("Barlow Condensed Italic"), local("BarlowCondensed-Italic"), url(barlow-condensed-italic.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-medium */
@font-face {
font-family: Barlow Condensed;
font-style: normal;
font-weight: 500;
src: local("Barlow Condensed Medium"), local("BarlowCondensed-Medium"), url(barlow-condensed-medium.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-medium-italic */
@font-face {
font-family: Barlow Condensed;
font-style: italic;
font-weight: 500;
src: local("Barlow Condensed Medium Italic"), local("BarlowCondensed-MediumItalic"), url(barlow-condensed-medium-italic.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-semibold */
@font-face {
font-family: Barlow Condensed;
font-style: normal;
font-weight: 400;
src: local("Barlow Condensed SemiBold"), local("BarlowCondensed-SemiBold"), url(barlow-condensed-semibold.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-semibold-italic */
@font-face {
font-family: Barlow Condensed;
font-style: italic;
font-weight: 400;
src: local("Barlow Condensed SemiBold Italic"), local("BarlowCondensed-SemiBoldItalic"), url(barlow-condensed-semibold-italic.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-bold */
@font-face {
font-family: Barlow Condensed;
font-style: normal;
font-weight: 700;
src: local("Barlow Condensed Bold"), local("BarlowCondensed-Bold"), url(barlow-condensed-bold.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-bold-italic */
@font-face {
font-family: Barlow Condensed;
font-style: italic;
font-weight: 700;
src: local("Barlow Condensed Bold Italic"), local("BarlowCondensed-BoldItalic"), url(barlow-condensed-bold-italic.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-extrabold */
@font-face {
font-family: Barlow Condensed;
font-style: normal;
font-weight: 400;
src: local("Barlow Condensed ExtraBold"), local("BarlowCondensed-ExtraBold"), url(barlow-condensed-extrabold.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-extrabold-italic */
@font-face {
font-family: Barlow Condensed;
font-style: italic;
font-weight: 400;
src: local("Barlow Condensed ExtraBold Italic"), local("BarlowCondensed-ExtraBoldItalic"), url(barlow-condensed-extrabold-italic.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-black */
@font-face {
font-family: Barlow Condensed;
font-style: normal;
font-weight: 900;
src: local("Barlow Condensed Black"), local("BarlowCondensed-Black"), url(barlow-condensed-black.woff2) format("woff2");
font-display: swap;
}
/* barlow-condensed-black-italic */
@font-face {
font-family: Barlow Condensed;
font-style: italic;
font-weight: 900;
src: local("Barlow Condensed Black Italic"), local("BarlowCondensed-BlackItalic"), url(barlow-condensed-black-italic.woff2) format("woff2");
font-display: swap;
}
+108
View File
@@ -0,0 +1,108 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2024 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/>.
*/
/*
This file was generated by font-facer from the font files in the dir:
.
*/
@font-face {
font-family: "Roboto";
font-weight: 100;
font-style: normal;
src: url('./Roboto-Thin.ttf');
}
@font-face {
font-family: "Roboto";
font-weight: 100;
font-style: italic;
src: url('./Roboto-ThinItalic.ttf');
}
@font-face {
font-family: "Roboto";
font-weight: 300;
font-style: normal;
src: url('./Roboto-Light.ttf');
}
@font-face {
font-family: "Roboto";
font-weight: 300;
font-style: italic;
src: url('./Roboto-LightItalic.ttf');
}
@font-face {
font-family: "Roboto";
font-weight: 400;
font-style: normal;
src: url('./Roboto-Regular.ttf');
}
@font-face {
font-family: "Roboto";
font-weight: 400;
font-style: italic;
src: url('./Roboto-Italic.ttf');
}
@font-face {
font-family: "Roboto";
font-weight: 500;
font-style: normal;
src: url('./Roboto-Medium.ttf');
}
@font-face {
font-family: "Roboto";
font-weight: 500;
font-style: italic;
src: url('./Roboto-MediumItalic.ttf');
}
@font-face {
font-family: "Roboto";
font-weight: 700;
font-style: normal;
src: url('./Roboto-Bold.ttf');
}
@font-face {
font-family: "Roboto";
font-weight: 700;
font-style: italic;
src: url('./Roboto-BoldItalic.ttf');
}
@font-face {
font-family: "Roboto";
font-weight: 900;
font-style: normal;
src: url('./Roboto-Black.ttf');
}
@font-face {
font-family: "Roboto";
font-weight: 900;
font-style: italic;
src: url('./Roboto-BlackItalic.ttf');
}
-21
View File
@@ -1,21 +0,0 @@
{
"name": "MyWebSite",
"short_name": "MySite",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

-111
View File
@@ -1,111 +0,0 @@
import { mkdir, rm, writeFile, readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const pagesDir = path.join(root, "src", "pages");
const locale = "en";
const outputDir = path.join(pagesDir, locale);
const routeTranslations = new Map([
["jetzt-spielen", "join"],
["impressum", "imprint"],
["verhaltensrichtlinien", "code-of-conduct"],
["regeln", "rules"],
["rangliste", "ranked"],
["haeufige-fragen", "faq"],
["statistiken", "stats"],
["ankuendigungen", "announcements"],
["datenschutzerklaerung", "privacy-policy"],
["passwort-setzen", "set-password"],
]);
const pages = await collectAstroPages(pagesDir);
await rm(outputDir, { recursive: true, force: true });
for (const page of pages) {
const relativePage = path.relative(pagesDir, page);
const segments = relativePage.split(path.sep);
if (segments[0] === locale || segments[0] === "de") {
continue;
}
const outputSegments = translateSegments(segments);
const outFile = path.join(outputDir, ...outputSegments);
const source = await readFile(page, "utf8");
const hasGetStaticPaths = /\bexport\s+(?:const|async\s+function|function)\s+getStaticPaths\b/.test(source);
await mkdir(path.dirname(outFile), { recursive: true });
await writeFile(outFile, createProxyPage(page, outFile, hasGetStaticPaths));
}
function translateSegments(segments) {
const translated = [...segments];
if (translated[0] !== "index.astro" && !translated[0].startsWith("[")) {
translated[0] = routeTranslations.get(translated[0]) ?? translated[0];
}
const file = translated.pop();
const name = file.slice(0, -".astro".length);
if (name !== "index") {
translated.push(name);
}
translated.push("index.astro");
return translated;
}
function createProxyPage(sourceFile, outFile, hasGetStaticPaths) {
const outDir = path.dirname(outFile);
const importPath = normalizeImportPath(path.relative(outDir, sourceFile));
const lines = [
"---",
`import Page from "${importPath}";`,
];
if (hasGetStaticPaths) {
lines.push(
`import { getStaticPaths as proxyGetStaticPaths } from "${importPath}";`,
`export const getStaticPaths = (props) => proxyGetStaticPaths({ ...props, astroI18n: { locale: "${locale}" } });`,
);
}
lines.push(
"const { props } = Astro;",
"---",
"",
"<Page {...props} />",
"",
);
return lines.join("\n");
}
function normalizeImportPath(importPath) {
const normalized = importPath.split(path.sep).join("/");
return normalized.startsWith(".") ? normalized : `./${normalized}`;
}
async function collectAstroPages(dir) {
const entries = await import("node:fs/promises").then(({ readdir }) => readdir(dir, { withFileTypes: true }));
const files = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...await collectAstroPages(fullPath));
continue;
}
if (entry.isFile() && entry.name.endsWith(".astro")) {
files.push(fullPath);
}
}
return files;
}
+3 -9
View File
@@ -3,13 +3,7 @@ import { Image } from "astro:assets";
import localBau from "@images/90.png";
---
<Image
src={localBau}
alt="Bau"
widths={[240, 540, 720, 1080, 1920, localBau.width]}
<Image src={localBau} alt="Bau" widths={[240, 540, 720, 1080, 1920, localBau.width]}
sizes={`(max-width: 240px) 240px, (max-width: 540px) 540px, (max-width: 720px) 720px, (max-width: 1080px) 1080px, (max-width: 1920px) 1920px, ${localBau.width}px`}
class="w-full h-full object-cover"
quality={100}
draggable="false"
loading="eager"
/>
class="w-full h-full object-cover rounded-b-2xl shadow-2xl" quality={100}
draggable="false" loading="eager"/>
+10 -20
View File
@@ -36,23 +36,23 @@
const rotateX = (centerY - y) / 20;
const rotateY = -(centerX - x) / 20;
cardElement.style.setProperty("--rotate-x", `${rotateX}deg`);
cardElement.style.setProperty("--rotate-y", `${rotateY}deg`);
cardElement.style.setProperty('--rotate-x', `${rotateX}deg`);
cardElement.style.setProperty('--rotate-y', `${rotateY}deg`);
}
function resetElement() {
cardElement.style.setProperty("--rotate-x", "0");
cardElement.style.setProperty("--rotate-y", "0");
cardElement.style.setProperty('--rotate-x', "0");
cardElement.style.setProperty('--rotate-y', "0");
}
interface Props {
hoverEffect?: boolean;
extraClasses?: string;
children?: import("svelte").Snippet;
children?: import('svelte').Snippet;
}
let { hoverEffect = true, extraClasses = "", children }: Props = $props();
let classes = $derived(twMerge("flex flex-col items-center p-8 m-4 bg-[#0c0c0c] border border-[rgba(255,255,255,0.06)] text-gray-100", extraClasses));
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))
</script>
<div class={classes} bind:this={cardElement} onmousemove={rotateElement} onmouseleave={resetElement} class:hoverEffect>
@@ -63,24 +63,14 @@
div {
transform: perspective(1000px) rotateX(var(--rotate-x, 0)) rotateY(var(--rotate-y, 0)) !important;
transition: scale 300ms cubic-bezier(0.2, 3, 0.67, 0.6);
transition: scale 300ms cubic-bezier(.2, 3, .67, .6);
:global(h1) {
margin-top: 1rem;
font-size: 1.25rem;
line-height: 1.75rem;
font-weight: 700;
font-family: "Barlow Condensed", sans-serif;
letter-spacing: 0.06em;
@apply text-xl font-bold mt-4;
}
:global(svg) {
color: #f59e0b;
transition: transform 300ms ease-in-out;
}
:global(svg:hover) {
transform: scale(1.1);
@apply transition-transform duration-300 ease-in-out hover:scale-110 hover:drop-shadow-2xl
}
}
+1 -1
View File
@@ -18,7 +18,7 @@
-->
<script lang="ts">
import {t} from "$lib/i18n"
import {t} from "astro-i18n"
import UserInfo from "./dashboard/UserInfo.svelte";
import {dataRepo} from "@repo/data.ts";
import UploadModal from "@components/dashboard/UploadModal.svelte";
+5 -2
View File
@@ -18,9 +18,8 @@
-->
<script lang="ts">
import {t} from "$lib/i18n";
import {t} from "astro-i18n";
import {statsRepo} from "@repo/stats.ts";
import "@styles/table.css"
interface Props {
@@ -65,3 +64,7 @@
<p>{error.message}</p>
{/await}
<style>
@import "../styles/table.css";
</style>
+2 -2
View File
@@ -1,7 +1,7 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2026 SteamWar.de-Serverteam
- Copyright (C) 2023 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
@@ -19,7 +19,7 @@
<script lang="ts">
import FightStatsChart from "./FightStatsChart.svelte";
import { t } from "$lib/i18n";
import {t} from "astro-i18n";
import {statsRepo} from "@repo/stats.ts";
let request = getStats();
+1 -3
View File
@@ -79,8 +79,6 @@
})
},
options: {
maintainAspectRatio: false,
scales: {
x: {
type: "time",
@@ -107,5 +105,5 @@
</script>
<div>
<canvas height="500" bind:this={canvas}></canvas>
<canvas bind:this={canvas}></canvas>
</div>
+10 -14
View File
@@ -19,32 +19,30 @@
-->
<script lang="ts">
import { window } from "./utils.ts";
import { astroI18n, t } from "$lib/i18n";
import {window} from "./util.ts";
import {astroI18n, t} from "astro-i18n";
import type {EventFight, ExtendedEvent} from "@type/event";
import "@styles/table.css";
export let event: ExtendedEvent;
export let group: number;
export let group: string;
export let rows: number = 1;
function getWinner(fight: EventFight) {
if (!fight.hasFinished) {
return t("announcements.table.notPlayed");
}
switch (fight.ergebnis) {
case 1:
return fight.blueTeam.kuerzel;
case 2:
return fight.redTeam.kuerzel;
default:
case 3:
return t("announcements.table.draw");
default:
return t("announcements.table.notPlayed");
}
}
</script>
<div class="p-3 bg-[#0c0c0c] border border-[rgba(255,255,255,0.06)] w-3/4 mx-auto">
<div class="p-3 bg-gray-200 dark:bg-neutral-800 rounded-2xl w-3/4 mx-auto">
<table>
<thead>
<tr class="font-bold border-b">
@@ -57,15 +55,13 @@
</tr>
</thead>
<tbody>
{#each window( event.fights.filter((f) => (group === undefined ? true : f.group?.id === group)), rows, ) as fights}
{#each window(event.fights.filter(f => f.group === group), rows) as fights}
<tr>
{#each fights as fight (fight.id)}
<td
>{Intl.DateTimeFormat(astroI18n.locale, {
<td>{Intl.DateTimeFormat(astroI18n.locale, {
hour: "numeric",
minute: "numeric",
}).format(new Date(fight.start))}</td
>
}).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 === 2} class:italic={fight.ergebnis === 3}>{fight.redTeam.kuerzel}</td>
<td>{getWinner(fight)}</td>
+25 -21
View File
@@ -19,32 +19,36 @@
-->
<script lang="ts">
import { window } from "./utils.ts";
import { t } from "$lib/i18n";
import {window} from "./util.ts";
import {t} from "astro-i18n";
import type {ExtendedEvent} from "@type/event.ts";
import "@styles/table.css";
import "@styles/table.css"
let {
event,
group,
rows = 1,
}: {
event: ExtendedEvent;
group: number;
rows?: number;
} = $props();
export let event: ExtendedEvent;
export let group: string;
export let rows: number = 1;
let teamPoints = $derived(
Object.entries(event.groups.find((g) => g.id === group)?.points ?? {})
.map(([teamId, points]) => ({
team: event.teams.find((t) => t.id === Number(teamId))!!,
points: points,
}))
.sort((a, b) => b.points - a.points),
);
$: teamPoints = event.teams.map(team => {
const fights = event.fights.filter(fight => fight.blueTeam.id === team.id || fight.redTeam.id === team.id);
const points = fights.reduce((acc, fight) => {
if (fight.ergebnis === 1 && fight.blueTeam.id === team.id) {
return acc + 3;
} else if (fight.ergebnis === 2 && fight.redTeam.id === team.id) {
return acc + 3;
} else if (fight.ergebnis === 3) {
return acc + 1;
} else {
return acc;
}
}, 0);
return {
team,
points,
};
}).sort((a, b) => b.points - a.points);
</script>
<div class="p-3 bg-[#0c0c0c] border border-[rgba(255,255,255,0.06)] w-3/4 mx-auto">
<div class="p-3 bg-gray-200 dark:bg-neutral-800 rounded-2xl w-3/4 mx-auto">
<table class="w-full">
<thead>
<tr class="font-bold border-b">
+4 -4
View File
@@ -1,8 +1,8 @@
---
import { t } from "$lib/i18n";
import {t} from "astro-i18n";
---
<div class="border-l-2 border-amber-500 bg-amber-500/5 p-4" role="alert">
<p class="font-bold text-amber-400" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.1em; text-transform: uppercase;">{t("warning.title")}</p>
<p class="text-gray-400 text-sm">{t("warning.text")}</p>
<div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4" role="alert">
<p class="font-bold">{t("warning.title")}</p>
<p>{t("warning.text")}</p>
</div>
+19 -85
View File
@@ -1,7 +1,7 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2026 SteamWar.de-Serverteam
- Copyright (C) 2023 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
@@ -18,20 +18,21 @@
-->
<script lang="ts">
import { preventDefault } from "svelte/legacy";
import { preventDefault } from 'svelte/legacy';
import {l} from "@utils/util.ts";
import { t } from "$lib/i18n";
import {t} from "astro-i18n";
import {get} from "svelte/store";
import {navigate} from "astro:transitions/client";
import { onMount } from "svelte";
import { authV2Repo } from "./repo/authv2.ts";
let username: string = $state("");
let pw: string = $state("");
let error: string = $state("");
async function login() {
let { authV2Repo } = await import("./repo/authv2.ts");
let {tokenStore} = await import("./repo/repo.ts");
let {authRepo} = await import("./repo/auth.ts");
if (username === "" || pw === "") {
pw = "";
error = t("login.error");
@@ -39,43 +40,24 @@
}
try {
let auth = await get(authV2Repo).login(username, pw);
if (!auth) {
let auth = await get(authRepo).login(username, pw);
if (auth == undefined) {
pw = "";
error = t("login.error");
return;
}
await navigate(l("/dashboard"));
tokenStore.set(auth);
navigate(l("/dashboard"));
} catch (e: any) {
pw = "";
error = t("login.error");
}
}
onMount(() => {
if (window.location.hash.includes("access_token")) {
const params = new URLSearchParams(window.location.hash.substring(1));
const accessToken = params.get("access_token");
if (accessToken) {
(async () => {
let auth = await $authV2Repo.loginDiscord(accessToken);
if (!auth) {
pw = "";
error = t("login.error");
return;
}
navigate(l("/dashboard"));
})();
}
}
});
</script>
<form class="sw-login-form" onsubmit={preventDefault(login)}>
<h1 class="text-4xl text-white text-center" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.08em;">{t("login.title")}</h1>
<form class="bg-gray-100 dark:bg-neutral-900 p-12 rounded-2xl shadow-2xl border-2 border-gray-600 flex flex-col" onsubmit={preventDefault(login)}>
<h1 class="text-4xl text-white text-center">{t("login.title")}</h1>
<div class="ml-2 flex flex-col">
<label for="username">{t("login.label.username")}</label>
<input type="text" id="username" name="username" placeholder={t("login.placeholder.username")} bind:value={username} />
@@ -83,70 +65,22 @@
<input type="password" id="password" name="password" placeholder={t("login.placeholder.password")} bind:value={pw} />
</div>
<p class="mt-2">
<a class="sw-link" href={l("/set-password")}>{t("login.setPassword")}</a>
</p>
<a class="text-neutral-500 hover:underline" href={l("/set-password")}>{t("login.setPassword")}</a></p>
{#if error}
<p class="mt-2 text-red-500">{error}</p>
{/if}
<button class="btn mt-4 justify-center w-full" type="submit" onclick={preventDefault(login)}>{t("login.submit")}</button>
<a
class="btn mt-4 justify-center w-full"
href="https://discord.com/oauth2/authorize?client_id=869606970099904562&response_type=token&redirect_uri=https%3A%2F%2Fsteamwar.de%2Flogin&scope=identify"
>
{t("login.discord")}
</a>
<button class="btn mt-4 !mx-0 justify-center" type="submit" onclick={preventDefault(login)}>{t("login.submit")}</button>
</form>
<style lang="postcss">
.sw-login-form {
background: rgba(12, 12, 12, 0.95);
border: 1px solid rgba(255, 255, 255, 0.06);
border-top: 2px solid #f59e0b;
backdrop-filter: blur(24px);
padding: 3rem;
display: flex;
flex-direction: column;
}
input {
width: 20rem;
padding: 0.6rem 0.8rem;
margin-top: 0.25rem;
margin-bottom: 0.5rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
color: #f5f5f5;
font-size: 0.85rem;
outline: none;
transition: border-color 0.2s ease;
}
input:focus {
border-color: rgba(245, 158, 11, 0.5);
@apply border-2 rounded-md p-2 shadow-2xl w-80
dark:bg-neutral-800
focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:border-transparent;
}
label {
font-family: "Barlow Condensed", sans-serif;
font-size: 0.7rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: rgba(163, 163, 163, 0.7);
margin-top: 0.5rem;
}
.sw-link {
color: rgba(163, 163, 163, 0.5);
text-decoration: none;
font-size: 0.85rem;
border-bottom: 1px solid transparent;
transition:
color 0.2s,
border-color 0.2s;
}
.sw-link:hover {
color: #f59e0b;
border-bottom-color: #f59e0b;
@apply text-neutral-300;
}
</style>
+70 -70
View File
@@ -19,24 +19,22 @@
<script lang="ts">
import "../styles/button.css";
import { CaretDownOutline, GlobeOutline } from "flowbite-svelte-icons";
import { t, l } from "$lib/i18n";
import { CaretDownOutline, SearchOutline } from "flowbite-svelte-icons";
import { t } from "astro-i18n";
import { l } from "../util/util";
import { onMount } from "svelte";
import { loggedIn } from "@repo/authv2.ts";
import { astroI18n } from "$lib/i18n";
interface Props {
logo?: import("svelte").Snippet;
}
let { logo }: Props = $props();
let navbar = $state<HTMLElement>();
let navbar = $state<HTMLDivElement>();
let searchOpen = $state(false);
let accountBtn = $state<HTMLAnchorElement>();
let currentPage = $state(astroI18n.route);
$effect(() => {
if ($loggedIn) {
accountBtn!.href = l("/dashboard");
@@ -47,34 +45,41 @@
onMount(() => {
handleScroll();
document.addEventListener("astro:page-load", () => {
astroI18n.route = location.pathname;
currentPage = astroI18n.route;
});
});
function handleScroll() {
if (window.scrollY > 0) {
navbar!.classList.add("sw-nav-scrolled");
navbar!.classList.add("before:scale-y-100");
} else {
navbar!.classList.remove("sw-nav-scrolled");
navbar!.classList.remove("before:scale-y-100");
}
}
</script>
<svelte:window onscroll={handleScroll} />
<nav data-pagefind-ignore class="sw-nav z-20 fixed top-0 left-0 right-0 sm:px-4 py-1 transition-colors flex justify-center" bind:this={navbar}>
<div class="flex flex-row items-center justify-evenly md:justify-between match">
<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}
>
<div
class="flex flex-row items-center justify-evenly md:justify-between match"
>
<a class="flex items-center" href={l("/")}>
{@render logo?.()}
<span class="sw-nav-title hidden md:inline-block">
<span
class="text-2xl uppercase font-bold dark:text-white hidden md:inline-block"
>
{t("navbar.title")}
<span class="scrolled-trigger" style="display: none" aria-hidden="true"></span>
<span
class="before:scale-y-100"
style="display: none"
aria-hidden="true"
></span>
</span>
</a>
<div class="flex justify-center flex-wrap gap-2">
<div class="flex justify-center flex-wrap">
<div class="btn-dropdown">
<button class="btn btn-gray">
<a href={l("/")}>
@@ -83,11 +88,21 @@
<CaretDownOutline class="ml-2 mt-auto" />
</button>
<div>
<a class="btn btn-gray" href={l("/announcements")}>{t("navbar.links.home.announcements")}</a>
<a class="btn btn-gray" href={l("/events")}>{t("navbar.links.home.events")}</a>
<a class="btn btn-gray" href={l("/downloads")}>{t("navbar.links.home.downloads")}</a>
<a class="btn btn-gray" href={l("/faq")}>{t("navbar.links.home.faq")}</a>
<a class="btn btn-gray" href={l("/code-of-conduct")}>{t("navbar.links.rules.coc")}</a>
<a class="btn btn-gray" href={l("/announcements")}
>{t("navbar.links.home.announcements")}</a
>
<a class="btn btn-gray" href={l("/downloads")}
>{t("navbar.links.home.downloads")}</a
>
<a class="btn btn-gray" href={l("/tutorials")}
>{t("navbar.links.home.tutorials")}</a
>
<a class="btn btn-gray" href={l("/faq")}
>{t("navbar.links.home.faq")}</a
>
<a class="btn btn-gray" href={l("/code-of-conduct")}
>{t("navbar.links.rules.coc")}</a
>
</div>
</div>
<div class="btn-dropdown">
@@ -98,15 +113,35 @@
<CaretDownOutline class="ml-2 mt-auto" />
</button>
<div>
<a href={l("/rules/wargear")} class="btn btn-gray">{t("navbar.links.rules.wg")}</a>
<a href={l("/rules/miniwargear")} class="btn btn-gray">{t("navbar.links.rules.mwg")}</a>
<a href={l("/rules/warship")} class="btn btn-gray">{t("navbar.links.rules.ws")}</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/wargear")} class="btn btn-gray"
>{t("navbar.links.rules.wg")}</a
>
<a href={l("/rules/miniwargear")} class="btn btn-gray"
>{t("navbar.links.rules.mwg")}</a
>
<a href={l("/rules/warship")} class="btn btn-gray"
>{t("navbar.links.rules.ws")}</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
>
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.rotating")}</h2>
<a href={l("/rules/megawargear")} 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/streetfight")} class="btn btn-gray">{t("navbar.links.rules.sf")}</a>
<a href={l("/rules/megawargear")} 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/streetfight")} class="btn btn-gray"
>{t("navbar.links.rules.sf")}</a
>
<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
>
</div>
</div>
<!-- TODO: Add help center
@@ -126,21 +161,11 @@
<a class="btn" href={l("/login")} bind:this={accountBtn}>
<span class="btn__text">{t("navbar.links.account")}</span>
</a>
<div class="btn-dropdown">
<button class="btn btn-gray">
<GlobeOutline />
<!--
<button class="btn my-1" onclick={() => searchOpen = true}>
<SearchOutline ariaLabel="Site Search" class="inline-block h-6"/>
</button>
<div>
<a
data-astro-reload
href={l(currentPage, {}, { targetLocale: typeof navigator !== "undefined" ? navigator.language.split("-")[0] : "de" })}
onclick={() => cookieStore.delete("MANUAL_LANGUAGE")}
class="btn btn-gray">Auto</a
>
<a data-astro-reload href={l(currentPage, {}, { targetLocale: "de" })} onclick={() => cookieStore.set("MANUAL_LANGUAGE", "TRUE")} class="btn btn-gray">Deutsch</a>
<a data-astro-reload href={l(currentPage, {}, { targetLocale: "en" })} onclick={() => cookieStore.set("MANUAL_LANGUAGE", "TRUE")} class="btn btn-gray">English</a>
</div>
</div>
-->
</div>
</div>
</nav>
@@ -155,29 +180,4 @@
.match {
width: min(100vw, 70em);
}
:global(.sw-nav) {
backdrop-filter: none;
background: transparent;
transition:
background 0.3s ease,
backdrop-filter 0.3s ease,
border-color 0.3s ease;
border-bottom: 1px solid transparent;
}
:global(.sw-nav-scrolled) {
background: rgba(8, 8, 8, 0.85) !important;
backdrop-filter: blur(16px) !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.04) !important;
}
.sw-nav-title {
font-family: "Barlow Condensed", sans-serif;
font-size: 1.3rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #f5f5f5;
}
</style>
+19 -24
View File
@@ -1,54 +1,49 @@
---
import type { CollectionEntry } from "astro:content";
import {CollectionEntry} from "astro:content";
import {l} from "../util/util";
import { astroI18n, stripLocaleFromPath } from "$lib/i18n";
import {astroI18n} from "astro-i18n";
import {Image} from "astro:assets";
import TagComponent from "./TagComponent.astro";
import P from "./P.astro";
import Card from "@components/Card.svelte";
interface Props {
post: CollectionEntry<"announcements">;
post: CollectionEntry<"announcements">
}
const {
post,
slim,
}: {
post: CollectionEntry<"announcements">;
slim: boolean;
const { post, slim }: {
post: CollectionEntry<"announcements">,
slim: boolean,
} = Astro.props as Props;
const postUrl = l(`/announcements/${stripLocaleFromPath(post.id)}`);
const postUrl = l(`/announcements/${post.slug.split("/").slice(1).join("/")}`);
---
<Card extraClasses={`w-full items-start mx-0 ${slim ? "m-0 p-2 backdrop-blur-xl bg-transparent border-0" : "border-t-2 border-t-amber-500/30"}`} hoverEffect={false}>
<Card extraClasses={`w-full items-start mx-0 ${slim ? "m-0 p-1" : ""}`} hoverEffect={false}>
<div class={`flex flex-row ${slim ? "":"p-4"}`}>
{
post.data.image != null ? (
{post.data.image != null
? (
<a href={postUrl}>
<div class="flex-shrink-0 pr-2">
<Image transition:name={post.data.title + "-image"} src={post.data.image} alt="Post Image" class="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>
</a>
) : null
}
)
: null}
<div>
<a href={postUrl} class="flex flex-col items-start">
<h2 class="text-2xl font-bold" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.04em;" transition:name={post.data.title + "-title"}>{post.data.title}</h2>
<P class="text-gray-500 text-sm"
>{
Intl.DateTimeFormat(astroI18n.locale, {
<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, {
day: "numeric",
month: "long",
year: "numeric",
}).format(post.data.created)
}</P
>
}).format(post.data.created)}</P>
<P>{post.data.description}</P>
</a>
<div class="mt-1" transition:name={post.data.title + "-tags"}>
{post.data.tags.map((tag) => <TagComponent tag={tag} />)}
{post.data.tags.map((tag) => (
<TagComponent tag={tag} />
))}
</div>
</div>
</div>
+12 -25
View File
@@ -18,6 +18,7 @@
-->
<script lang="ts">
import {slide, fade} from "svelte/transition";
import {onMount} from "svelte";
import {importPagefind, type Pagefind, type PagefindDocument} from "@type/pagefind.js";
@@ -37,7 +38,7 @@
if (e.target instanceof HTMLInputElement) {
let search: {results: any[]} = await pagefind.debouncedSearch(e.target.value);
results = await Promise.all(search.results.slice(0, 10).map((value) => value.data()));
results = await Promise.all(search.results.slice(0, 10).map(value => value.data()))
}
}
@@ -48,13 +49,14 @@
let { open = $bindable(false) }: Props = $props();
</script>
<button transition:fade class="fixed top-0 left-0 w-screen h-screen bg-black/60 backdrop-blur-sm z-20 cursor-default" onclick={() => (open = false)}> </button>
<div transition:slide style="width: min(100%, 75em);" class="fixed top-0 left-1/2 -translate-x-1/2 h-2/3 z-30 p-4 text-white flex flex-col sw-search-panel">
<input placeholder="Search..." onkeypress={search} />
<button transition:fade class="fixed top-0 left-0 w-screen h-screen backdrop-blur z-20 cursor-default" onclick={() => open = false}>
</button>
<div transition:slide style="width: min(100%, 75em);" class="fixed top-0 left-1/2 -translate-x-1/2 h-2/3 dark:bg-zinc-900 rounded-b-2xl shadow-2xl z-30 p-4 text-white flex flex-col">
<input placeholder="Search..." onkeypress={search}>
<div class="overflow-y-scroll flex-1 w-full mt-2">
<div class="overflow-y-scroll flex-1 w-full mt-2 rounded-2xl">
{#each results as result}
<Card extraClasses="w-full m-0 my-2 border-t-2 border-t-amber-500/30" hoverEffect={false}>
<Card extraClasses="w-full m-0 my-2" hoverEffect={false}>
<a class="grid grid-cols-3" href={l(result.url)}>
<h1>{result.meta.title}</h1>
{#each result.sub_results.slice(0, 2) as sub_result}
@@ -67,28 +69,13 @@
</div>
<style lang="postcss">
.sw-search-panel {
background: rgba(8, 8, 8, 0.95);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
backdrop-filter: blur(24px);
}
input {
width: 100%;
padding: 0.7rem 1rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
color: #f5f5f5;
font-size: 0.9rem;
outline: none;
transition: border-color 0.2s ease;
}
input:focus {
border-color: rgba(245, 158, 11, 0.5);
@apply border-2 rounded-md p-2 shadow-2xl w-full
dark:bg-neutral-800
focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:border-transparent;
}
label {
color: rgba(163, 163, 163, 0.7);
@apply text-neutral-300;
}
</style>
+1 -1
View File
@@ -18,7 +18,7 @@
-->
<script lang="ts">
import { t } from "$lib/i18n"
import { t } from "astro-i18n"
import {server} from "./stores/stores.ts";
function generateVersionString(version: string): string {
+9 -18
View File
@@ -1,4 +1,5 @@
---
import {l} from "../util/util";
import {capitalize} from "./admin/util";
@@ -10,22 +11,12 @@ interface Props {
const {tag, noLink} = Astro.props;
---
{
noLink ? (
<span
class="inline-block bg-transparent border border-amber-500/30 px-3 py-0.5 text-xs font-semibold text-amber-400 mr-2 uppercase tracking-wider"
style="font-family: 'Barlow Condensed', sans-serif;"
>
{capitalize(tag)}
</span>
) : (
<a href={l(`/announcements/tags/${tag}`)}>
<span
class="inline-block bg-transparent border border-amber-500/30 px-3 py-0.5 text-xs font-semibold text-amber-400 mr-2 uppercase tracking-wider hover:border-amber-400 hover:text-amber-300 transition-colors"
style="font-family: 'Barlow Condensed', sans-serif;"
>
{capitalize(tag)}
</span>
</a>
{noLink
? (
<span class="inline-block bg-gray-800 rounded-full px-3 py-1 text-sm font-semibold text-white mr-2 shadow-2xl">{capitalize(tag)}</span>
)
}
: (
<a href={l(`/announcements/tags/${tag}`)}>
<span class="inline-block bg-gray-800 rounded-full px-3 py-1 text-sm font-semibold text-white mr-2 shadow-2xl">{capitalize(tag)}</span>
</a>
)}
+8 -10
View File
@@ -22,40 +22,38 @@
import wrap from "svelte-spa-router/wrap";
import Router, {replace} from "svelte-spa-router";
import {get} from "svelte/store";
import {loggedIn} from "@repo/authv2.ts";
import {tokenStore} from "@repo/repo";
const routes: RouteDefinition = {
"/": wrap({asyncComponent: () => import("./pages/Home.svelte"), conditions: detail => get(loggedIn)}),
"/": wrap({asyncComponent: () => import("./pages/Home.svelte"), conditions: detail => get(tokenStore) != ""}),
"/perms": wrap({
asyncComponent: () => import("./pages/Perms.svelte"),
conditions: detail => get(loggedIn)
conditions: detail => get(tokenStore) != ""
}),
"/login": wrap({
asyncComponent: () => import("./pages/Login.svelte"),
conditions: detail => !get(loggedIn)
conditions: detail => get(tokenStore) == ""
}),
"/event/:id": wrap({
asyncComponent: () => import("./pages/Event.svelte"),
conditions: detail => get(loggedIn)
conditions: detail => get(tokenStore) != ""
}),
"/event/:id/generate": wrap({
asyncComponent: () => import("./pages/Generate.svelte"),
conditions: detail => get(loggedIn)
conditions: detail => get(tokenStore) != ""
}),
"/edit": wrap({
asyncComponent: () => import("./pages/Edit.svelte"),
conditions: detail => get(loggedIn)
conditions: detail => get(tokenStore) != ""
}),
"/display/:event": wrap({
asyncComponent: () => import("./pages/Display.svelte"),
conditions: detail => get(loggedIn)
conditions: detail => get(tokenStore) != ""
}),
"*": wrap({asyncComponent: () => import("./pages/NotFound.svelte")})
};
function conditionsFailed(event: ConditionsFailedEvent) {
console.log(event)
if (event.detail.location === "/login") {
replace("/");
} else {
@@ -20,7 +20,7 @@
<script lang="ts">
import {Input, Label, Select} from "flowbite-svelte";
import TypeAheadSearch from "./TypeAheadSearch.svelte";
import { gamemodes, groups, maps } from "@stores/stores.ts";
import {gamemodes, groups, maps, players} from "@stores/stores.ts";
import type {Team} from "@type/team.ts";
interface Props {
@@ -44,77 +44,57 @@
map = $bindable(""),
spectatePort = $bindable(null),
group = $bindable(""),
groupSearch = $bindable(""),
groupSearch = $bindable("")
}: Props = $props();
let selectableTeams = $derived(
teams
.map((team) => {
let selectableTeams = $derived(teams.map(team => {
return {
name: team.name,
value: team.id.toString(),
value: team.id.toString()
};
})
.sort((a, b) => a.name.localeCompare(b.name))
);
}).sort((a, b) => a.name.localeCompare(b.name)));
let selectableGamemodes = $derived(
$gamemodes
.map((gamemode) => {
let selectableGamemodes = $derived($gamemodes.map(gamemode => {
return {
name: gamemode,
value: gamemode,
value: gamemode
};
})
.sort((a, b) => a.name.localeCompare(b.name))
);
}).sort((a, b) => a.name.localeCompare(b.name)));
let customGamemode = $derived(!selectableGamemodes.some((e) => e.name === gamemode) && gamemode !== "");
let selectableCustomGamemode = $derived([
...selectableGamemodes,
{
...selectableGamemodes, {
name: gamemode + " (custom)",
value: gamemode,
},
value: gamemode
}
]);
let mapsStore = $derived(maps(gamemode));
let selectableMaps = $derived(
$mapsStore
.map((map) => {
let selectableMaps = $derived($mapsStore.map(map => {
return {
name: map,
value: map,
value: map
};
})
.sort((a, b) => a.name.localeCompare(b.name))
);
}).sort((a, b) => a.name.localeCompare(b.name)));
let customMap = $derived(!selectableMaps.some((e) => e.name === map) && map !== "");
let selectableCustomMaps = $derived([
...selectableMaps,
{
...selectableMaps, {
name: map + " (custom)",
value: map,
},
value: map
}
]);
let selectableGroups = $derived([
{
let selectableGroups = $derived([{
name: "None",
value: "",
},
{
value: ""
}, {
value: groupSearch,
name: `Create: '${groupSearch}'`,
},
...$groups
.map((group) => {
name: `Create: '${groupSearch}'`
}, ...$groups.map(group => {
return {
name: group,
value: group,
value: group
};
})
.sort((a, b) => a.name.localeCompare(b.name)),
]);
}).sort((a, b) => a.name.localeCompare(b.name))]);
</script>
<div class="m-2">
@@ -135,11 +115,13 @@
</div>
<div class="m-2">
<Label for="fight-gamemode">Gamemode</Label>
<Select items={customGamemode ? selectableCustomGamemode : selectableGamemodes} bind:value={gamemode} id="fight-gamemode"></Select>
<Select items={customGamemode ? selectableCustomGamemode : selectableGamemodes} bind:value={gamemode}
id="fight-gamemode"></Select>
</div>
<div class="m-2">
<Label for="fight-maps">Map</Label>
<Select items={customMap ? selectableCustomMaps : selectableMaps} bind:value={map} id="fight-maps" disabled={customGamemode} class={customGamemode ? "cursor-not-allowed" : ""}></Select>
<Select items={customMap ? selectableCustomMaps : selectableMaps} bind:value={map} id="fight-maps"
disabled={customGamemode} class={customGamemode ? "cursor-not-allowed" : ""}></Select>
</div>
<div class="m-2">
<Label for="fight-port">Spectate Port</Label>
@@ -151,5 +133,6 @@
</div>
<div class="m-2">
<Label for="fight-kampf">Group</Label>
<TypeAheadSearch items={selectableGroups} bind:selected={group} bind:searchValue={groupSearch} all></TypeAheadSearch>
<TypeAheadSearch items={selectableGroups} bind:selected={group} bind:searchValue={groupSearch}
all></TypeAheadSearch>
</div>
+2 -2
View File
@@ -168,11 +168,11 @@
</div>
<div>
{#if selectedBranch !== "master"}
<Button onclick={() => createFile()} color="alternative" disabled={!selectedPath}>Create File
<Button onclick={createFile} color="alternative" disabled={!selectedPath}>Create File
</Button>
<Button onclick={() => deleteBranch(false)} color="none">Delete Branch</Button>
{:else}
<Button onclick={() => createBranch()}>Create Branch</Button>
<Button onclick={createBranch}>Create Branch</Button>
{/if}
</div>
</div>
+2
View File
@@ -21,6 +21,7 @@
import {Navbar, NavBrand, Spinner, TabItem, Tabs} from "flowbite-svelte";
import EventEdit from "./event/EventEdit.svelte";
import {ArrowLeftOutline} from "flowbite-svelte-icons";
import FightList from "./event/FightList.svelte";
import TeamList from "./event/TeamList.svelte";
import {eventRepo} from "@repo/event.ts";
import RefereesList from "@components/admin/pages/event/RefereesList.svelte";
@@ -74,6 +75,7 @@
{#snippet title()}
<span >Kämpfe</span>
{/snippet}
<FightList {data}/>
</TabItem>
</Tabs>
{:catch error}
-1
View File
@@ -38,7 +38,6 @@
</NavBrand>
<NavHamburger onclick={toggle}/>
<NavUl {hidden}>
<NavLi href="/admin/new">New UI</NavLi>
<NavLi href="#/edit">Edit Pages</NavLi>
<NavLi href="#/perms">Permissions</NavLi>
</NavUl>
+34 -39
View File
@@ -18,37 +18,21 @@
-->
<script lang="ts">
import { run, preventDefault } from "svelte/legacy";
import { run, preventDefault } from 'svelte/legacy';
import {Button, Card, Checkbox, Input, Label, Navbar, NavBrand, Radio, Spinner} from "flowbite-svelte";
import {ArrowLeftOutline} from "flowbite-svelte-icons";
import {players} from "@stores/stores.ts";
import {capitalize} from "../util.ts";
import {permsRepo} from "@repo/perms.ts";
import {me} from "@stores/me.ts";
import SWButton from "@components/styled/SWButton.svelte";
import SWModal from "@components/styled/SWModal.svelte";
import {userRepo} from "@repo/user.ts";
import { dataRepo } from "@repo/data.ts";
import type { Player } from "@type/data";
let search = $state("");
let playersList: Player[] = $state([]);
let debounceTimer: NodeJS.Timeout;
function fetchPlayers(searchTerm: string) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
const res = await $dataRepo.queryPlayers(searchTerm || undefined, undefined, undefined, 100, 0, undefined, undefined);
playersList = res.players;
}, 300);
}
$effect(() => {
fetchPlayers(search);
});
let selectedPlayer: string | null = $state(null);
let selectedPlayerName: string = $state("");
let playerPerms = $state(loadPlayer(selectedPlayer));
let prefixEdit = $state("PREFIX_NONE");
@@ -62,7 +46,7 @@
if (!id) {
return;
}
return $permsRepo.getPerms(id).then((value) => {
return $permsRepo.getPerms(id).then(value => {
activePerms = value.perms;
prefixEdit = value.prefix.name;
return value;
@@ -72,7 +56,7 @@
function togglePerm(perm: string) {
return () => {
if (activePerms.includes(perm)) {
activePerms = activePerms.filter((value) => value !== perm);
activePerms = activePerms.filter(value => value !== perm);
} else {
activePerms = [...activePerms, perm];
}
@@ -80,7 +64,7 @@
}
function save() {
playerPerms!.then(async (perms) => {
playerPerms!.then(async perms => {
if (perms.prefix.name != prefixEdit) {
await $permsRepo.setPrefix(selectedPlayer!, prefixEdit);
}
@@ -115,7 +99,9 @@
resetPasswordRepeat = "";
resetPasswordModal = false;
}
let lowerCaseSearch = $derived(search.toLowerCase());
let filteredPlayers = $derived($players.filter(value => value.name.toLowerCase().includes(lowerCaseSearch)));
let player = $derived($players.find(value => value.uuid === selectedPlayer));
run(() => {
playerPerms = loadPlayer(selectedPlayer);
});
@@ -126,7 +112,9 @@
{#snippet children({ hidden, toggle })}
<NavBrand href="#">
<ArrowLeftOutline></ArrowLeftOutline>
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white"> Permissions </span>
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white">
Permissions
</span>
</NavBrand>
{/snippet}
</Navbar>
@@ -138,17 +126,12 @@
<Label for="user_search" class="mb-2">Search Users...</Label>
<Input type="text" id="user_search" placeholder="Name..." bind:value={search}/>
</div>
{#if playersList.length < 100}
{#if filteredPlayers.length < 100}
<ul class="flex-1 overflow-scroll">
{#each playersList as player (player.uuid)}
<li
class="p-4 transition-colors hover:bg-gray-700 cursor-pointer"
{#each filteredPlayers as player (player.uuid)}
<li class="p-4 transition-colors hover:bg-gray-700 cursor-pointer"
class:text-orange-500={player.uuid === selectedPlayer}
onclick={preventDefault(() => {
selectedPlayer = player.uuid;
selectedPlayerName = player.name;
})}
>
onclick={preventDefault(() => selectedPlayer = player.uuid)}>
{player.name}
</li>
{/each}
@@ -157,7 +140,7 @@
</Card>
<Card class="!max-w-full" style="grid-column: 2/4">
{#if selectedPlayer}
<h1 class="text-3xl">{selectedPlayerName}</h1>
<h1 class="text-3xl">{player.name}</h1>
{#await permsFuture}
<Spinner></Spinner>
{:then perms}
@@ -166,16 +149,22 @@
{:then player}
<h1>Prefix</h1>
{#each Object.entries(perms.prefixes) as [key, prefix]}
<Radio name="prefix" bind:group={prefixEdit} value={prefix.name}>{capitalize(prefix.name.substring(7).toLowerCase())}</Radio>
<Radio name="prefix" bind:group={prefixEdit}
value={prefix.name}>{capitalize(prefix.name.substring(7).toLowerCase())}</Radio>
{/each}
<h1>Permissions</h1>
{#each perms.perms as perm}
<Checkbox checked={activePerms.includes(perm)} onclick={togglePerm(perm)}>{capitalize(perm.toLowerCase())}</Checkbox>
<Checkbox checked={activePerms.includes(perm)}
onclick={togglePerm(perm)}>{capitalize(perm.toLowerCase())}</Checkbox>
{/each}
<div class="mt-4">
<Button disabled={prefixEdit === (player?.prefix.name ?? "") && activePerms === (player?.perms ?? [])} onclick={save}>Save</Button>
<Button disabled={prefixEdit === (player?.prefix.name ?? "") && activePerms === (player?.perms ?? [])}
onclick={save}>Save
</Button>
{#if $me != null && $me.perms.includes("ADMINISTRATION")}
<Button onclick={() => (resetPasswordModal = true)}>Reset Password</Button>
<Button onclick={() => resetPasswordModal = true}>
Reset Password
</Button>
<SWModal bind:open={resetPasswordModal} title="Reset Password">
<Label for="new_password">New Password</Label>
@@ -184,8 +173,14 @@
<Input type="password" id="repeat_password" placeholder="Repeat Password" bind:value={resetPasswordRepeat}/>
{#snippet footer()}
<Button class="ml-auto mr-4" onclick={resetResetPassword}>Cancel</Button>
<Button disabled={resetPassword === "" || resetPassword !== resetPasswordRepeat} onclick={resetPW}>Reset Password</Button>
<Button class="ml-auto mr-4" onclick={resetResetPassword}>
Cancel
</Button>
<Button disabled={resetPassword === "" || resetPassword !== resetPasswordRepeat} onclick={resetPW}>
Reset Password
</Button>
{/snippet}
</SWModal>
{/if}
+13 -9
View File
@@ -20,6 +20,7 @@
<script lang="ts">
import {Spinner, Toolbar, ToolbarButton, ToolbarGroup} from "flowbite-svelte";
import {json} from "@codemirror/lang-json";
import CodeMirror from "svelte-codemirror-editor";
import {base64ToBytes} from "../../util.ts";
import type {Page} from "@type/page.ts";
import {materialDark} from "@ddietr/codemirror-themes/material-dark";
@@ -33,7 +34,7 @@
dirty?: boolean;
}
let { pageId, branch = $bindable(), dirty = $bindable(false) }: Props = $props();
let { pageId, branch, dirty = $bindable(false) }: Props = $props();
let dispatcher = createEventDispatcher();
@@ -70,14 +71,11 @@
}
let pageFuture = $derived($pageRepo.getPage(pageId, branch).then(getPage));
</script>
<svelte:window
onbeforeunload={() => {
<svelte:window onbeforeunload={() => {
if (dirty) {
return "You have unsaved changes. Are you sure you want to leave?";
}
}}
/>
}}/>
{#await pageFuture}
<Spinner/>
{:then p}
@@ -86,15 +84,21 @@
<Toolbar class="!bg-gray-900">
{#snippet end()}
<ToolbarGroup >
<ToolbarButton onclick={deletePage}>Delete</ToolbarButton>
<ToolbarButton color="primary" onclick={savePage}>Save</ToolbarButton>
<ToolbarButton onclick={deletePage}>
Delete
</ToolbarButton>
<ToolbarButton color="primary" onclick={savePage}>
Save
</ToolbarButton>
</ToolbarGroup>
{/snippet}
</Toolbar>
</div>
{#if page?.name.endsWith("md") || page?.name.endsWith("mdx")}
<MDEMarkdownEditor bind:value={pageContent} bind:dirty/>
{:else}{/if}
{:else}
<CodeMirror bind:value={pageContent} lang={json()} theme={materialDark} on:change={() => dirty = true}/>
{/if}
</div>
{:catch error}
<p>{error.message}</p>
@@ -0,0 +1,312 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2023 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import type {EventFight, ExtendedEvent} from "@type/event.ts";
import {
Button,
Checkbox, Input, Label,
Modal,
SpeedDial,
SpeedDialButton,
Toolbar,
ToolbarButton,
ToolbarGroup,
Tooltip
} from "flowbite-svelte";
import {
ArrowsRepeatOutline, CalendarWeekOutline,
PlusOutline, ProfileCardOutline, TrashBinOutline, UsersGroupOutline,
} from "flowbite-svelte-icons";
import FightCard from "./FightCard.svelte";
import CreateFightModal from "./modals/CreateFightModal.svelte";
import {groups, players} from "@stores/stores.ts";
import TypeAheadSearch from "../../components/TypeAheadSearch.svelte";
import {fightRepo, type UpdateFight} from "@repo/fight.ts";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
dayjs.extend(duration);
interface Props {
data: ExtendedEvent;
}
let { data = $bindable() }: Props = $props();
let createOpen = $state(false);
let fights = $state(data.fights);
let selectedFights: Set<EventFight> = $state(new Set());
let groupsMap = $derived(new Set(fights.map(fight => fight.group)));
let groupedFights = $derived(Array.from(groupsMap).map(group => {
return {
group: group,
fights: fights.filter(fight => fight.group === group)
};
}));
function cycleSelect() {
if (selectedFights.size === fights.length) {
selectedFights = new Set();
} else if (selectedFights.size === 0) {
selectedFights = new Set(fights.filter(fight => fight.start > Date.now()));
if (selectedFights.size === 0) {
selectedFights = new Set(fights);
}
} else {
selectedFights = new Set(fights);
}
}
function cycleGroup(groupFights: EventFight[]) {
if (groupFights.every(gf => selectedFights.has(gf))) {
groupFights.forEach(fight => selectedFights.delete(fight));
} else {
groupFights.forEach(fight => selectedFights.add(fight));
}
selectedFights = new Set(selectedFights);
}
let deleteOpen = $state(false);
async function deleteFights() {
for (const fight of selectedFights) {
await $fightRepo.deleteFight(fight.id);
}
fights = await $fightRepo.listFights(data.event.id);
selectedFights = new Set();
deleteOpen = false;
}
let spectatePortOpen = $state(false);
let selectPlayers = $derived($players.map(player => {
return {
name: player.name,
value: player.uuid
};
}).sort((a, b) => a.name.localeCompare(b.name)));
let spectatePort = $state("");
async function updateSpectatePort() {
for (const fight of selectedFights) {
let f: UpdateFight = {
blueTeam: null,
group: null,
spectatePort: Number.parseInt(spectatePort),
map: null,
redTeam: null,
spielmodus: null,
start: null
};
await $fightRepo.updateFight(fight.id, f);
}
fights = await $fightRepo.listFights(data.event.id);
selectedFights = new Set();
spectatePort = "";
spectatePortOpen = false;
}
let groupChangeOpen = $state(false);
let group = $state("");
let groupSearch = $state("");
let selectableGroups = $derived([{
name: "Keine",
value: ""
}, {
value: groupSearch,
name: `Erstelle: '${groupSearch}'`
}, ...$groups.map(group => {
return {
name: group,
value: group
};
}).sort((a, b) => a.name.localeCompare(b.name))]);
async function updateGroup() {
for (const fight of selectedFights) {
let f: UpdateFight = {
blueTeam: null,
group: group,
spectatePort: null,
map: null,
redTeam: null,
spielmodus: null,
start: null
};
await $fightRepo.updateFight(fight.id, f);
}
fights = await $fightRepo.listFights(data.event.id);
selectedFights = new Set();
group = "";
groupSearch = "";
groupChangeOpen = false;
}
let minTime = $derived(dayjs(Math.min(...fights.map(fight => fight.start))).utc(true));
let changeTimeOpen = $state(false);
let changedTime = $state(fights.length != 0 ? dayjs(Math.min(...fights.map(fight => fight.start)))?.utc(true)?.toISOString()?.slice(0, -1) : undefined);
let deltaTime = $derived(dayjs.duration(dayjs(changedTime).utc(true).diff(minTime)));
async function updateStartTime() {
for (const fight of selectedFights) {
let f: UpdateFight = {
blueTeam: null,
group: null,
spectatePort: null,
map: null,
redTeam: null,
spielmodus: null,
start: dayjs(fight.start).add(deltaTime.asMilliseconds(), "millisecond")
};
await $fightRepo.updateFight(fight.id, f);
}
fights = await $fightRepo.listFights(data.event.id);
changedTime = minTime.toISOString().slice(0, -1);
selectedFights = new Set();
changeTimeOpen = false;
}
</script>
<svelte:head>
<title>{data.event.name} - Fights</title>
</svelte:head>
<div class="pb-28">
<Toolbar class="mx-4 mt-2 w-fit">
<ToolbarGroup>
<Checkbox class="ml-2" checked={selectedFights.size === fights.length} onclick={cycleSelect}/>
<Tooltip>Select Upcoming</Tooltip>
</ToolbarGroup>
<ToolbarGroup>
<ToolbarButton onclick={() => selectedFights.size > 0 ? changeTimeOpen = true : changeTimeOpen = false}>
<CalendarWeekOutline/>
</ToolbarButton>
<Tooltip>Reschedule Fights</Tooltip>
<ToolbarButton onclick={() => selectedFights.size > 0 ? spectatePortOpen = true : spectatePortOpen = false}
disabled={changedTime === undefined}>
<ProfileCardOutline/>
</ToolbarButton>
<Tooltip>Change Spectate Port</Tooltip>
<ToolbarButton onclick={() => selectedFights.size > 0 ? groupChangeOpen = true : groupChangeOpen = false}>
<UsersGroupOutline/>
</ToolbarButton>
<Tooltip>Change Group</Tooltip>
</ToolbarGroup>
<ToolbarGroup>
<ToolbarButton color="red"
onclick={() => selectedFights.size > 0 ? deleteOpen = true : deleteOpen = false}>
<TrashBinOutline/>
</ToolbarButton>
<Tooltip>Delete</Tooltip>
</ToolbarGroup>
</Toolbar>
{#each groupedFights as group}
<div class="flex mt-4">
<Checkbox class="ml-2 text-center" checked={group.fights.every(gf => selectedFights.has(gf))}
onclick={() => cycleGroup(group.fights)}/>
<h1 class="ml-4 text-2xl">{group.group ?? "Ungrouped"}</h1>
</div>
{#each group.fights.sort((a, b) => a.start - b.start) as fight, i (fight.id)}
{@const isSelected = selectedFights.has(fight)}
<FightCard {fight} {i} {data} selected={isSelected}
select={() => {
if (selectedFights.has(fight)) {
selectedFights.delete(fight);
} else {
selectedFights.add(fight);
}
selectedFights = new Set(selectedFights);
}} update={async () => fights = await $fightRepo.listFights(data.event.id)}
/>
{/each}
{/each}
</div>
<CreateFightModal {data} bind:open={createOpen}
on:create={async () => data.fights = await $fightRepo.listFights(data.event.id)}></CreateFightModal>
<Modal bind:open={deleteOpen} title="Delete {selectedFights.size} Fights" autoclose size="sm">
<p>Are you sure you want to delete {selectedFights.size} fights?</p>
{#snippet footer()}
<Button color="red" class="ml-auto" onclick={deleteFights}>Delete</Button>
<Button onclick={() => deleteOpen = false} color="alternative">Cancel</Button>
{/snippet}
</Modal>
<Modal bind:open={spectatePortOpen} title="Change Kampfleiter" size="sm">
<div class="m-2">
<Label for="fight-kampf">Kampfleiter</Label>
<TypeAheadSearch items={selectPlayers} bind:selected={spectatePort}></TypeAheadSearch>
</div>
{#snippet footer()}
<Button class="ml-auto" onclick={updateSpectatePort}>Change</Button>
<Button onclick={() => spectatePortOpen = false} color="alternative">Cancel</Button>
{/snippet}
</Modal>
<Modal bind:open={groupChangeOpen} title="Change Group" size="sm">
<div class="m-2">
<Label for="fight-kampf">Group</Label>
<TypeAheadSearch items={selectableGroups} bind:selected={group} bind:searchValue={groupSearch}
all></TypeAheadSearch>
</div>
{#snippet footer()}
<Button class="ml-auto" onclick={updateGroup}>Change</Button>
<Button onclick={() => groupChangeOpen = false} color="alternative">Cancel</Button>
{/snippet}
</Modal>
<Modal bind:open={changeTimeOpen} title="Change Start Time" size="sm">
<div class="m-2">
<Label for="fight-start">New Start Time:</Label>
<Input id="fight-start" bind:value={changedTime} >
{#snippet children({ props })}
<input type="datetime-local" {...props} bind:value={changedTime}/>
{/snippet}
</Input>
</div>
<p>{deltaTime.asMilliseconds() < 0 ? '' : '+'}{("0" + deltaTime.hours()).slice(-2)}
:{("0" + deltaTime.minutes()).slice(-2)}</p>
{#snippet footer()}
<Button class="ml-auto" onclick={updateStartTime}>Update</Button>
<Button onclick={() => changeTimeOpen = false} color="alternative">Cancel</Button>
{/snippet}
</Modal>
<SpeedDial>
<SpeedDialButton name="Add" onclick={() => createOpen = true}>
<PlusOutline/>
</SpeedDialButton>
<SpeedDialButton name="Generate" href="#/event/{data.event.id}/generate">
<ArrowsRepeatOutline/>
</SpeedDialButton>
</SpeedDial>
@@ -23,7 +23,8 @@
import {PlusOutline} from "flowbite-svelte-icons";
import SWModal from "@components/styled/SWModal.svelte";
import SWButton from "@components/styled/SWButton.svelte";
import PlayerSelector from "@components/ui/PlayerSelector.svelte";
import TypeAheadSearch from "@components/admin/components/TypeAheadSearch.svelte";
import {players} from "@stores/stores.ts";
import {eventRepo} from "@repo/event.ts";
interface Props {
@@ -41,8 +42,7 @@
async function addReferee() {
if (selectedPlayer) {
referees = (
await $eventRepo.updateEvent(data.event.id.toString(), {
referees = (await $eventRepo.updateEvent(data.event.id.toString(), {
deadline: null,
end: null,
maxTeamMembers: null,
@@ -51,9 +51,8 @@
removeReferee: null,
schemType: null,
start: null,
addReferee: [selectedPlayer],
})
).referees;
addReferee: [selectedPlayer]
})).referees;
}
reset();
@@ -61,8 +60,7 @@
function removeReferee(id: string) {
return async () => {
referees = (
await $eventRepo.updateEvent(data.event.id.toString(), {
referees = (await $eventRepo.updateEvent(data.event.id.toString(), {
deadline: null,
end: null,
maxTeamMembers: null,
@@ -72,9 +70,8 @@
schemType: null,
start: null,
removeReferee: [id],
})
).referees;
};
})).referees;
}
}
function reset() {
@@ -87,7 +84,9 @@
{#each referees as referee}
<li class="flex flex-grow justify-between">
{referee.name}
<SWButton onclick={removeReferee(referee.uuid)}>Entfernen</SWButton>
<SWButton onclick={removeReferee(referee.uuid)}>
Entfernen
</SWButton>
</li>
{/each}
@@ -96,14 +95,15 @@
{/if}
</ul>
<Button class="fixed bottom-6 right-6 !p-4 z-10 shadow-lg" onclick={() => (showAdd = true)}>
<Button class="fixed bottom-6 right-6 !p-4 z-10 shadow-lg" onclick={() => showAdd = true}>
<PlusOutline/>
</Button>
<SWModal title="Schiedsrichter hinzufügen" bind:open={showAdd}>
<div class="flex flex-grow justify-center h-80">
<div>
<PlayerSelector bind:value={selectedPlayer} placeholder="Search player..." />
<TypeAheadSearch bind:searchValue bind:selected={selectedPlayer}
items={$players.map(v => ({ name: v.name, value: v.uuid }))}/>
</div>
</div>
{#snippet footer()}
@@ -116,6 +116,6 @@
<style>
li {
padding-block: 0.5rem;
@apply py-2;
}
</style>
@@ -18,14 +18,12 @@
-->
<script lang="ts">
import { Card } from "@components/ui/card";
import {createEventDispatcher} from "svelte";
interface Props {
children?: import("svelte").Snippet;
ondrop: (event: DragEvent) => void;
children?: import('svelte').Snippet;
}
let { children, ondrop }: Props = $props();
let { children }: Props = $props();
let dragover = $state(false);
@@ -34,16 +32,19 @@
dragover = true;
}
const dispatch = createEventDispatcher();
function handleDrop(ev: DragEvent) {
ev.preventDefault();
dragover = false;
ondrop(ev);
dispatch("drop", ev);
}
</script>
<Card class="w-56 p-4 rounded m-px {dragover ? 'border-white' : ''}" ondrop={handleDrop} ondragover={handleDragOver} ondragleave={() => (dragover = false)} role="none">
<div class="w-56 bg-gray-800 p-4 rounded" class:border={dragover} class:m-px={!dragover} ondrop={handleDrop}
ondragover={handleDragOver} ondragleave={() => dragover = false} role="none">
{@render children?.()}
</Card>
</div>
<style>
div {
@@ -18,28 +18,28 @@
-->
<script lang="ts">
import { createBubbler } from 'svelte/legacy';
const bubble = createBubbler();
import type {Team} from "@type/team.ts";
import {brightness, colorFromTeam, lighten} from "../../util";
interface Props {
team: Team;
ondragstart: (event: DragEvent) => void;
}
let { team, ondragstart }: Props = $props();
let { team }: Props = $props();
let hover = $state(false);
</script>
<div
class="rounded w-fit p-2 border-gray-600 border cursor-grab select-none m-1 flex place-items-center"
<div class="rounded w-fit p-2 border-gray-600 border cursor-grab select-none m-1 flex place-items-center"
style:background-color={hover ? lighten(colorFromTeam(team)) : colorFromTeam(team)}
class:text-black={brightness(colorFromTeam(team))}
draggable="true"
{ondragstart}
onmouseenter={() => (hover = true)}
onmouseleave={() => (hover = false)}
role="figure"
>
class:text-black={brightness(colorFromTeam(team))} draggable="true"
ondragstart={bubble('dragstart')}
onmouseenter={() => hover = true}
onmouseleave={() => hover = false}
role="figure">
<span>{team.name}</span>
</div>
@@ -18,7 +18,7 @@
-->
<script lang="ts">
import {astroI18n, t} from "$lib/i18n";
import {astroI18n, t} from "astro-i18n";
import {CheckSolid, XCircleOutline} from "flowbite-svelte-icons";
import type {SchematicInfo} from "@type/schem.ts";
import {createEventDispatcher} from "svelte";
+5 -22
View File
@@ -20,7 +20,7 @@
<script lang="ts">
import { preventDefault } from 'svelte/legacy';
import {t} from "$lib/i18n";
import {t} from "astro-i18n";
import {
ChevronDoubleRightOutline,
FolderOutline,
@@ -143,32 +143,15 @@
<style lang="postcss">
table {
width: 100%;
@apply w-full;
}
tr {
cursor: pointer;
border-bottom: 1px solid #e5e7eb;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-duration: 150ms;
}
tr:hover {
background-color: #d1d5db;
}
:global(.dark) tr:hover {
background-color: #1f2937;
@apply transition-colors cursor-pointer border-b
dark:hover:bg-gray-800 hover:bg-gray-300;
}
th {
padding-block: 1rem;
text-align: left;
}
@media (min-width: 768px) {
th {
padding-inline: 0.5rem;
}
@apply text-left py-4 md:px-2;
}
</style>
@@ -21,7 +21,7 @@
import { createBubbler, preventDefault } from 'svelte/legacy';
const bubble = createBubbler();
import {astroI18n, t} from "$lib/i18n";
import {astroI18n, t} from "astro-i18n";
import {CheckSolid, FileOutline, FolderOutline, XCircleOutline} from "flowbite-svelte-icons";
import type {Schematic} from "@type/schem.ts";
import type {Player} from "@type/data.ts";
@@ -82,32 +82,11 @@
<style lang="scss">
tr {
cursor: pointer;
border-bottom: 1px solid #d1d5db;
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
transition-duration: 150ms;
}
tr:hover {
background-color: #d1d5db;
}
:global(.dark) tr {
border-bottom-color: #404040;
}
:global(.dark) tr:hover {
background-color: #1f2937;
@apply transition-colors cursor-pointer border-b border-gray-300
dark:hover:bg-gray-800 hover:bg-gray-300 dark:border-neutral-700;
}
th {
padding-block: 1rem;
text-align: left;
}
@media (min-width: 768px) {
th {
padding-inline: 0.5rem;
}
@apply text-left py-4 md:px-2;
}
</style>
+5 -2
View File
@@ -19,7 +19,7 @@
<script lang="ts">
import type {Player} from "@type/data.ts";
import {astroI18n, t} from "$lib/i18n"
import {astroI18n, t} from "astro-i18n"
import {statsRepo} from "@repo/stats.ts";
interface Props {
@@ -31,7 +31,7 @@
let request = getRequest();
function getRequest() {
return $statsRepo.getUserStats(user.uuid)
return $statsRepo.getUserStats(user.id)
}
</script>
@@ -43,5 +43,8 @@
maximumFractionDigits: 2
}).format(data.playtime)})}h</p>
<p>{t("dashboard.stats.fights", {fights: data.fights})}</p>
{#if user.perms.includes("CHECK")}
<p>{t("dashboard.stats.checked", {checked: data.acceptedSchematics})}</p>
{/if}
{:catch error}
{/await}
+12 -28
View File
@@ -21,7 +21,7 @@
import {createEventDispatcher} from "svelte";
import {schemRepo} from "@repo/schem.ts";
import SWModal from "@components/styled/SWModal.svelte";
import {t} from "$lib/i18n";
import {t} from "astro-i18n"
const dispatch = createEventDispatcher();
@@ -31,11 +31,9 @@
let { open = $bindable(false) }: Props = $props();
async function upload(e: Event) {
e.stopPropagation();
async function upload() {
if (uploadFile == null) {
error = "dashboard.schematic.errors.noFile";
return;
return
}
let file = uploadFile[0];
@@ -44,46 +42,32 @@
let type = name.split(".").pop();
if (type !== "schem" && type !== "schematic") {
error = "dashboard.schematic.errors.invalidEnding";
return;
return
}
let content = await file.arrayBuffer();
// @ts-ignore
let b64 = btoa(String.fromCharCode.apply(null, new Uint8Array(content)));
try {
await $schemRepo.uploadSchematic(name, b64);
open = false;
value = "";
dispatch("reset");
} catch (e) {
error = "dashboard.schematic.errors.upload";
}
}
function reset(e: Event) {
e.stopPropagation();
open = false
value = "";
uploadFile = null;
dispatch("reset")
}
let uploadFile: FileList | null = $state(null);
let value = $state("");
let error = $state(null)
</script>
<SWModal title={t("dashboard.schematic.title")} bind:open>
<form>
<label for="schem-upload">{t("dashboard.schematic.title")}</label>
<input type="file" id="schem-upload" bind:files={uploadFile} class="overflow-ellipsis" bind:value accept=".schem, .schematic"/>
{#if error !== null}
<p class="text-red-400">{t(error)}</p>
{/if}
<input type="file" bind:files={uploadFile} />
</form>
{#snippet footer()}
<button class="btn" onclick={upload}>{t("dashboard.schematic.upload")}</button>
<button class="btn btn-gray" onclick={reset}>{t("dashboard.schematic.cancel")}</button>
<button class="btn !ml-auto" onclick={upload}>{t("dashboard.schematic.upload")}</button>
<button class="btn btn-gray" onclick={() => open = false}>{t("dashboard.schematic.cancel")}</button>
{/snippet}
</SWModal>
+10 -15
View File
@@ -18,13 +18,13 @@
-->
<script lang="ts">
import { t } from "$lib/i18n";
import {t} from "astro-i18n";
import type {Player} from "@type/data.ts";
import {l} from "@utils/util.ts";
import Statistics from "./Statistics.svelte";
import { authV2Repo } from "@repo/authv2.ts";
import {authRepo} from "@repo/auth.ts";
import {tokenStore} from "@repo/repo.ts";
import Card from "@components/Card.svelte";
import { navigate } from "astro:transitions/client";
interface Props {
user: Player;
@@ -33,8 +33,9 @@
let { user }: Props = $props();
async function logout() {
await $authV2Repo.logout();
await navigate(l("/login"));
await $authRepo.logout()
tokenStore.set("")
window.location.href = l("/login")
}
</script>
@@ -43,25 +44,19 @@
<Card>
<figure>
<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>
</Card>
<div class="flex flex-wrap gap-2">
<div class="flex flex-wrap">
<button class="btn mt-2" onclick={logout}>{t("dashboard.buttons.logout")}</button>
{#if user.perms.includes("MODERATION")}
<a class="btn w-fit mt-2" href="/admin/new" data-astro-reload>{t("dashboard.buttons.admin")}</a>
<a class="btn w-fit mt-2" href="/admin" data-astro-reload>{t("dashboard.buttons.admin")}</a>
{/if}
</div>
</div>
<div>
<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)})}</p>
<Statistics {user} />
</div>
</div>
-95
View File
@@ -1,95 +0,0 @@
<script lang="ts">
import dayjs from "dayjs";
import "dayjs/locale/de";
import type { ExtendedEvent } from "../types/event";
import { Button } from "../ui/button";
import { ChevronLeft, ChevronRight } from "@lucide/svelte";
import * as Card from "../ui/card";
import EventCard from "./EventCard.svelte";
import SWButton from "@components/styled/SWButton.svelte";
const {
events,
}: {
events: { slug: string; data: { event: ExtendedEvent } }[];
} = $props();
let currentYear = $state(dayjs().year());
// Group events by month
let eventsByMonth = $derived.by(() => {
const grouped = new Map<string, typeof events>();
events.forEach((event) => {
const eventDate = dayjs(event.data.event.event.start).locale("de");
if (eventDate.year() === currentYear) {
const monthKey = eventDate.format("YYYY-MM");
if (!grouped.has(monthKey)) {
grouped.set(monthKey, []);
}
grouped.get(monthKey)!.push(event);
}
});
return grouped;
});
// Generate all 12 months for the current year
let months = $derived.by(() => {
return Array.from({ length: 12 }, (_, i) => {
const monthDate = dayjs().locale("de").year(currentYear).month(i);
const monthKey = monthDate.format("YYYY-MM");
return {
date: monthDate,
key: monthKey,
name: monthDate.format("MMMM"),
events: eventsByMonth.get(monthKey) || [],
};
});
});
function prevYear() {
currentYear = currentYear - 1;
}
function nextYear() {
currentYear = currentYear + 1;
}
</script>
<div>
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-white">
{currentYear}
</h2>
<div class="flex gap-2">
<SWButton onclick={prevYear} type="gray">
<ChevronLeft size={20} />
</SWButton>
<SWButton onclick={nextYear} type="gray">
<ChevronRight size={20} />
</SWButton>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{#each months as month}
<EventCard title={month.name} unsized={true}>
{#if month.events.length > 0}
{#each month.events as event}
<a href={`/events/${event.slug}/`} class="block p-2 bg-slate-800 hover:bg-slate-700 rounded border border-slate-600 transition-colors group">
<div class="text-sm font-semibold text-white group-hover:text-blue-400 transition-colors">
{event.data.event.event.name}
</div>
<div class="text-xs text-gray-400 mt-1">
{dayjs(event.data.event.event.start).format("MMM D, YYYY • HH:mm")}
</div>
</a>
{/each}
{:else}
<p class="text-gray-500 text-sm italic">Keine Events für diesen Monat</p>
{/if}
</EventCard>
{/each}
</div>
</div>
@@ -1,109 +0,0 @@
<script lang="ts">
import { fightConnector } from "./connections.svelte";
import { onMount, onDestroy } from "svelte";
let root: HTMLElement | null = null;
let refresh = $state(0);
function handleScroll() {
refresh++;
}
function getScrollableParent(el: HTMLElement | null): HTMLElement | null {
let node: HTMLElement | null = el?.parentElement ?? null;
while (node) {
const style = getComputedStyle(node);
const canScrollX = (style.overflowX === "auto" || style.overflowX === "scroll") && node.scrollWidth > node.clientWidth;
const canScrollY = (style.overflowY === "auto" || style.overflowY === "scroll") && node.scrollHeight > node.clientHeight;
if (canScrollX || canScrollY) return node;
node = node.parentElement;
}
return null;
}
let cleanup: (() => void) | null = null;
onMount(() => {
const scrollParent = getScrollableParent(root);
const target: EventTarget | null = scrollParent ?? window;
target?.addEventListener("scroll", handleScroll, { passive: true } as AddEventListenerOptions);
window.addEventListener("resize", handleScroll, { passive: true });
cleanup = () => {
target?.removeEventListener?.("scroll", handleScroll as EventListener);
window.removeEventListener("resize", handleScroll as EventListener);
};
});
onDestroy(() => {
cleanup?.();
cleanup = null;
});
</script>
<div bind:this={root} class="connection-renderer-root">
{#key refresh}
{#each $fightConnector.showedConnections as connection}
{@const fromLeft = connection.fromElement.offsetLeft + connection.fromElement.offsetWidth}
{@const toLeft = connection.toElement.offsetLeft}
{@const fromTop = connection.fromElement.offsetTop + connection.fromElement.offsetHeight / 2}
{@const toTop = connection.toElement.offsetTop + connection.toElement.offsetHeight / 2}
{@const horizontalDistance = toLeft - fromLeft}
{@const verticalDistance = toTop - fromTop}
<!-- Apply horizontal offset only to the mid bridge and second segment fan-out; also shift vertical line to keep continuity -->
{@const midLeft = fromLeft + horizontalDistance / 2 + connection.offset}
{@const firstSegmentWidth = horizontalDistance / 2}
{#if firstSegmentWidth > 0}
<div
class="horizontal-line"
style="
background-color: {connection.color};
left: {fromLeft}px;
top: {fromTop + connection.offset / 4}px;
width: {firstSegmentWidth + connection.offset + 2}px;
"
></div>
<div
class="vertical-line"
style="
background-color: {connection.color};
left: {midLeft}px;
top: {Math.min(fromTop + connection.offset / 4, toTop + connection.offset / 4)}px;
height: {Math.abs(toTop + connection.offset / 4 - (fromTop + connection.offset / 4))}px;
"
></div>
<div
class="horizontal-line"
style="
background-color: {connection.color};
left: {midLeft}px;
top: {toTop + connection.offset / 4}px;
width: {firstSegmentWidth - connection.offset}px;
"
></div>
{/if}
{/each}
{/key}
</div>
<style>
.connection-renderer-root {
position: static;
pointer-events: none;
}
.vertical-line {
position: absolute;
width: 2px;
z-index: -10;
pointer-events: none;
}
.horizontal-line {
position: absolute;
height: 2px;
z-index: -10;
pointer-events: none;
}
</style>
@@ -1,212 +0,0 @@
<script lang="ts">
import type {
ExtendedEvent,
EventFight,
ResponseGroups,
ResponseRelation,
} from "@type/event.ts";
import type { DoubleEleminationViewConfig } from "./types";
import EventCard from "./EventCard.svelte";
import EventFightChip from "./EventFightChip.svelte";
import { onMount, onDestroy, tick } from "svelte";
import { fightConnector } from "./connections.svelte.ts";
const {
event,
config,
}: { event: ExtendedEvent; config: DoubleEleminationViewConfig } = $props();
const defaultGroup: ResponseGroups = {
id: -1,
name: "Double Elimination",
pointsPerWin: 0,
pointsPerLoss: 0,
pointsPerDraw: 0,
type: "ELIMINATION_STAGE",
points: null,
};
function indexRelations(
ev: ExtendedEvent,
): Map<number, ResponseRelation[]> {
const map = new Map<number, ResponseRelation[]>();
for (const rel of ev.relations) {
const list = map.get(rel.fight) ?? [];
list.push(rel);
map.set(rel.fight, list);
}
return map;
}
const relationsByFight = indexRelations(event);
const fightMap = new Map<number, EventFight>(
event.fights.map((f) => [f.id, f]),
);
function collectBracket(startFinalId: number): EventFight[][] {
const finalFight = fightMap.get(startFinalId);
if (!finalFight) return [];
const bracketGroupId = finalFight.group?.id ?? null;
const stages: EventFight[][] = [];
let layer: EventFight[] = [finalFight];
const visited = new Set<number>([finalFight.id]);
while (layer.length) {
stages.push(layer);
const next: EventFight[] = [];
for (const fight of layer) {
const rels = relationsByFight.get(fight.id) ?? [];
for (const rel of rels) {
if (rel.type === "FIGHT" && rel.fromFight) {
const src =
fightMap.get(rel.fromFight.id) ?? rel.fromFight;
if (!src) continue;
// Only traverse within the same bracket (group) to avoid cross-bracket pollution
if (
bracketGroupId !== null &&
src.group?.id !== bracketGroupId
)
continue;
if (!visited.has(src.id)) {
visited.add(src.id);
next.push(src);
}
}
}
}
layer = next;
}
stages.reverse();
return stages;
}
const winnersStages = $derived(collectBracket(config.winnersFinalFight));
const losersStages = $derived(collectBracket(config.losersFinalFight));
const grandFinal = fightMap.get(config.grandFinalFight);
function stageName(count: number, isWinners: boolean): string {
switch (count) {
case 1:
return isWinners ? "Finale (W)" : "Finale (L)";
case 2:
return isWinners ? "Halbfinale (W)" : "Halbfinale (L)";
case 4:
return isWinners ? "Viertelfinale (W)" : "Viertelfinale (L)";
case 8:
return isWinners ? "Achtelfinale (W)" : "Achtelfinale (L)";
default:
return `Runde (${count}) ${isWinners ? "W" : "L"}`;
}
}
let connector: any;
const unsubscribe = fightConnector.subscribe((v) => (connector = v));
onDestroy(() => {
connector.clearAllConnections();
unsubscribe();
});
function buildConnections() {
if (!connector) return;
connector.clearAllConnections();
// Track offsets per source fight and team to stagger multiple outgoing lines for visual clarity
const fightTeamOffsetMap = new Map<string, number>();
const step = 8; // px separation between parallel lines
for (const rel of event.relations) {
if (rel.type !== "FIGHT" || !rel.fromFight) continue;
const fromId = rel.fromFight.id;
const fromEl = document.getElementById(
`fight-${fromId}`,
) as HTMLElement | null;
const toEl = document.getElementById(
`fight-${rel.fight}-team-${rel.team.toLowerCase()}`,
) as HTMLElement | null;
if (!fromEl || !toEl) continue;
// Use team-signed offsets so BLUE goes left (negative), RED goes right (positive)
const key = `${fromId}:${rel.team}`;
const index = fightTeamOffsetMap.get(key) ?? 0;
const sign = rel.team === "BLUE" ? -1 : 1;
const offset = sign * (index + 1) * step;
const color = rel.fromPlace === 0 ? "#60a5fa" : "#f87171";
connector.addConnection(fromEl, toEl, color, offset);
fightTeamOffsetMap.set(key, index + 1);
}
}
onMount(async () => {
await tick();
buildConnections();
});
</script>
{#if !grandFinal}
<p class="text-gray-400 italic">
Konfiguration unvollständig (Grand Final fehlt).
</p>
{:else}
{#key winnersStages.length + ":" + losersStages.length}
<!-- Build a grid where rows: winners (stages), losers (stages), with losers offset by one stage/column -->
{@const totalColumns =
Math.max(winnersStages.length, losersStages.length + 1) + 1}
<div
class="grid gap-x-16 gap-y-6 items-start"
style={`grid-template-columns: repeat(${totalColumns}, max-content);`}
>
<!-- Winners heading spans all columns -->
<h2 class="font-bold text-center">Winners Bracket</h2>
<!-- Winners stages in row 2 -->
{#each winnersStages as stage, i}
<div style={`grid-row: 2; grid-column: ${i + 1};`}>
<EventCard title={stageName(stage.length, true)}>
{#each stage as fight}
<EventFightChip
{event}
{fight}
group={fight.group ?? defaultGroup}
/>
{/each}
</EventCard>
</div>
{/each}
<!-- Place Grand Final at the far right, aligned with winners row -->
<div
style={`grid-row: 2; grid-column: ${totalColumns};`}
class="self-center"
>
<EventCard title="Grand Final">
{#if grandFinal}
<EventFightChip
{event}
fight={grandFinal}
group={grandFinal.group ?? defaultGroup}
/>
{/if}
</EventCard>
</div>
<!-- Losers heading spans all columns -->
<h2
class="font-bold text-center"
style="grid-row: 3; grid-column: 1 / {totalColumns};"
>
Losers Bracket
</h2>
<!-- Losers stages in row 4, offset by one column to the right -->
{#each losersStages as stage, j}
<div style={`grid-row: 4; grid-column: ${j + 2};`} class="mt-2">
<EventCard title={stageName(stage.length, false)}>
{#each stage as fight}
<EventFightChip
{event}
{fight}
group={fight.group ?? defaultGroup}
/>
{/each}
</EventCard>
</div>
{/each}
</div>
{/key}
{/if}
@@ -1,142 +0,0 @@
<script lang="ts">
import type {
ExtendedEvent,
EventFight,
ResponseGroups,
ResponseRelation,
} from "@type/event.ts";
import type { EleminationViewConfig } from "./types";
import EventCard from "./EventCard.svelte";
import EventFightChip from "./EventFightChip.svelte";
import { onMount, onDestroy, tick } from "svelte";
import { FightConnector, fightConnector } from "./connections.svelte.ts";
const {
event,
config,
}: { event: ExtendedEvent; config: EleminationViewConfig } = $props();
const defaultGroup: ResponseGroups = {
id: -1,
name: "Elimination",
pointsPerWin: 0,
pointsPerLoss: 0,
pointsPerDraw: 0,
type: "ELIMINATION_STAGE",
points: null,
};
function buildStages(
ev: ExtendedEvent,
finalFightId: number,
): EventFight[][] {
const fightMap = new Map<number, EventFight>(
ev.fights.map((f) => [f.id, f]),
);
const relationsByFight = new Map<number, ResponseRelation[]>();
for (const rel of ev.relations) {
const list = relationsByFight.get(rel.fight) ?? [];
list.push(rel);
relationsByFight.set(rel.fight, list);
}
const finalFight = fightMap.get(finalFightId);
if (!finalFight) return [];
const stages: EventFight[][] = [];
let currentLayer: EventFight[] = [finalFight];
const visited = new Set<number>([finalFight.id]);
while (currentLayer.length) {
stages.push(currentLayer);
const nextLayer: EventFight[] = [];
for (const fight of currentLayer) {
const rels = relationsByFight.get(fight.id) ?? [];
for (const rel of rels) {
if (rel.type === "FIGHT" && rel.fromFight) {
const src =
fightMap.get(rel.fromFight.id) ?? rel.fromFight;
if (src && !visited.has(src.id)) {
visited.add(src.id);
nextLayer.push(src);
}
}
}
}
currentLayer = nextLayer;
}
stages.reverse();
return stages;
}
function stageName(index: number, fights: EventFight[]): string {
const count = fights.length;
switch (count) {
case 1:
return `Finale`;
case 2:
return "Halbfinale";
case 4:
return "Viertelfinale";
case 8:
return "Achtelfinale";
case 16:
return "Sechzehntelfinale";
default:
return `Runde ${index + 1}`;
}
}
const stages = $derived(buildStages(event, config.finalFight));
const connector = $fightConnector;
onDestroy(() => {
connector.clearAllConnections();
});
function buildConnections() {
if (!connector) return;
connector.clearConnections();
for (const rel of event.relations) {
if (rel.type !== "FIGHT" || !rel.fromFight) continue;
const fromEl = document.getElementById(
`fight-${rel.fromFight.id}`,
) as HTMLElement | null;
const toEl = document.getElementById(
`fight-${rel.fight}-team-${rel.team.toLowerCase()}`,
) as HTMLElement | null;
if (fromEl && toEl) {
connector.addConnection(fromEl, toEl, "#9ca3af");
}
}
}
onMount(async () => {
await tick();
buildConnections();
});
</script>
{#if stages.length === 0}
<p class="text-gray-400 italic">Keine Eliminationsdaten gefunden.</p>
{:else}
<div class="flex gap-12">
{#each stages as stage, index}
<div class="flex flex-col justify-center">
<EventCard title={stageName(index, stage)}>
{#each stage as fight}
<EventFightChip
{event}
{fight}
group={fight.group ?? defaultGroup}
/>
{/each}
</EventCard>
</div>
{/each}
</div>
{/if}
-22
View File
@@ -1,22 +0,0 @@
<script lang="ts">
import type { Snippet } from "svelte";
const {
title,
children,
unsized = false,
}: {
title: string;
children: Snippet;
unsized?: boolean;
} = $props();
</script>
<div class="flex flex-col gap-1 {unsized ? '' : 'w-72 m-4'}">
<div class="bg-amber-500 text-black font-bold px-2 uppercase text-xs tracking-wider" style="font-family: 'Barlow Condensed', sans-serif;">
{title}
</div>
<div class="border border-gray-600 rounded p-2 flex flex-col gap-2 bg-slate-900">
{@render children()}
</div>
</div>
@@ -1,13 +0,0 @@
<script lang="ts">
import type { Snippet } from "svelte";
const {
children,
}: {
children: Snippet;
} = $props();
</script>
<div class="bg-neutral-900 border border-gray-700 rounded-lg overflow-hidden">
{@render children()}
</div>
@@ -1,67 +0,0 @@
<script lang="ts">
import type { EventFight, ExtendedEvent, ResponseGroups } from "@components/types/event";
import EventCardOutline from "./EventCardOutline.svelte";
import EventTeamChip from "./EventTeamChip.svelte";
let {
fight,
group,
event,
disabled = false,
}: {
fight: EventFight;
group: ResponseGroups;
event: ExtendedEvent;
disabled?: boolean;
} = $props();
function getScore(group: ResponseGroups, fight: EventFight, blueTeam: boolean): string {
if (!fight.hasFinished) return "-";
if (fight.ergebnis === 1) {
return blueTeam ? group.pointsPerWin.toString() : group.pointsPerLoss.toString();
} else if (fight.ergebnis === 2) {
return blueTeam ? group.pointsPerLoss.toString() : group.pointsPerWin.toString();
} else {
return group.pointsPerDraw.toString();
}
}
</script>
<EventCardOutline>
<EventTeamChip
team={{
id: -1,
kuerzel: new Date(fight.start).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
}),
name: new Date(fight.start).toLocaleDateString([]),
color: "-1",
}}
time={true}
{event}
/>
<div id={"fight-" + fight.id}>
<EventTeamChip
{event}
{disabled}
team={fight.blueTeam}
score={getScore(group, fight, true)}
showWinner={true}
isWinner={fight.ergebnis === 1}
noWinner={fight.ergebnis === 0}
id="fight-{fight.id}-team-blue"
/>
<EventTeamChip
{event}
{disabled}
team={fight.redTeam}
score={getScore(group, fight, false)}
showWinner={true}
isWinner={fight.ergebnis === 2}
noWinner={fight.ergebnis === 0}
id="fight-{fight.id}-team-red"
/>
</div>
</EventCardOutline>
-50
View File
@@ -1,50 +0,0 @@
<script lang="ts">
import type { ExtendedEvent } from "@type/event.ts";
import type { EventViewConfig } from "./types";
import { onMount } from "svelte";
import { eventRepo } from "@components/repo/event";
import GroupDisplay from "./GroupDisplay.svelte";
import ConnectionRenderer from "./ConnectionRenderer.svelte";
import EleminationDisplay from "./EleminationDisplay.svelte";
import DoubleEleminationDisplay from "./DoubleEleminationDisplay.svelte";
const { event, viewConfig }: { event: ExtendedEvent; viewConfig: EventViewConfig } = $props();
let loadedEvent = $state<ExtendedEvent>(event);
onMount(() => {
loadEvent();
});
async function loadEvent() {
loadedEvent = await $eventRepo.getEvent(event.event.id.toString());
}
let selectedView = $state<string>(Object.keys(viewConfig)[0]);
</script>
<div class="flex gap-4 overflow-x-auto mb-4">
{#each Object.entries(viewConfig) as [name, view]}
<button
class="mb-8 border-gray-700 border rounded-lg p-4 w-60 hover:bg-gray-700 hover:shadow-lg transition-shadow hover:border-gray-500"
class:bg-gray-800={selectedView === name}
onclick={() => (selectedView = name)}
>
<h1 class="text-left">{view.name}</h1>
</button>
{/each}
</div>
{#if selectedView}
{@const view = viewConfig[selectedView]}
<div class="overflow-x-scroll relative">
<ConnectionRenderer />
{#if view.view.type === "GROUP"}
<GroupDisplay event={loadedEvent} config={view.view} />
{:else if view.view.type === "ELEMINATION"}
<EleminationDisplay event={loadedEvent} config={view.view} />
{:else if view.view.type === "DOUBLE_ELEMINATION"}
<DoubleEleminationDisplay event={loadedEvent} config={view.view} />
{/if}
</div>
{/if}
-122
View File
@@ -1,122 +0,0 @@
<script lang="ts">
import type { ExtendedEvent } from "../types/event";
import dayjs from "dayjs";
import * as Card from "../ui/card";
const { events }: { events: { slug: string; data: { event: ExtendedEvent } }[] } = $props();
// Categorize events into current, upcoming and past.
const now = dayjs();
const sorted = [...events].sort((a, b) => a.data.event.event.start - b.data.event.event.start);
const currentEvents = sorted
.filter((e) => {
const start = dayjs(e.data.event.event.start);
const end = dayjs(e.data.event.event.end);
return start.isBefore(now) && end.isAfter(now);
})
.sort((a, b) => a.data.event.event.end - b.data.event.event.end);
const currentEvent = currentEvents[0];
const upcomingEvents = sorted.filter((e) => dayjs(e.data.event.event.start).isAfter(now));
const pastEvents = sorted.filter((e) => dayjs(e.data.event.event.end).isBefore(now)).sort((a, b) => b.data.event.event.end - a.data.event.event.end);
</script>
{#if currentEvent}
<div class="mb-8">
<h2 class="text-xl font-semibold text-white mb-4">Aktuelles Event</h2>
<div class="grid grid-cols-1">
<a href={`/events/${currentEvent.slug}/`} class="group block h-full">
<Card.Root class="h-full overflow-hidden border-slate-700 bg-slate-800 transition-all hover:-translate-y-1 hover:shadow-xl">
<div class="h-32 bg-gradient-to-br from-blue-600 to-purple-700 relative">
<div class="absolute bottom-0 left-0 p-4 bg-gradient-to-t from-slate-900 to-transparent w-full">
<div class="inline-block bg-slate-900/80 backdrop-blur text-white text-xs font-bold px-2 py-1 rounded mb-1 border border-slate-600">
{dayjs(currentEvent.data.event.event.start).format("DD.MM.YYYY")}
</div>
</div>
</div>
<Card.Header>
<Card.Title class="text-white group-hover:text-blue-400 transition-colors">
{currentEvent.data.event.event.name}
</Card.Title>
</Card.Header>
<Card.Content>
<p class="text-gray-400 text-sm line-clamp-2">
Läuft seit {dayjs(currentEvent.data.event.event.start).format("HH:mm")}
</p>
<div class="mt-4 flex items-center text-sm text-blue-400 font-medium">
Details <span class="ml-1 transition-transform group-hover:translate-x-1"></span>
</div>
</Card.Content>
</Card.Root>
</a>
</div>
</div>
{/if}
{#if upcomingEvents.length}
<div class="mb-8">
<h2 class="text-xl font-semibold text-white mb-4">Bevorstehende Events</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each upcomingEvents as event}
<a href={`/events/${event.slug}/`} class="group block h-full">
<Card.Root class="h-full overflow-hidden border-slate-700 bg-slate-800 transition-all hover:-translate-y-1 hover:shadow-xl">
<div class="h-32 bg-gradient-to-br from-blue-600 to-purple-700 relative">
<div class="absolute bottom-0 left-0 p-4 bg-gradient-to-t from-slate-900 to-transparent w-full">
<div class="inline-block bg-slate-900/80 backdrop-blur text-white text-xs font-bold px-2 py-1 rounded mb-1 border border-slate-600">
{dayjs(event.data.event.event.start).format("DD.MM.YYYY")}
</div>
</div>
</div>
<Card.Header>
<Card.Title class="text-white group-hover:text-blue-400 transition-colors">
{event.data.event.event.name}
</Card.Title>
</Card.Header>
<Card.Content>
<p class="text-gray-400 text-sm line-clamp-2">
Startet um {dayjs(event.data.event.event.start).format("HH:mm")}
</p>
<div class="mt-4 flex items-center text-sm text-blue-400 font-medium">
Details <span class="ml-1 transition-transform group-hover:translate-x-1"></span>
</div>
</Card.Content>
</Card.Root>
</a>
{/each}
</div>
</div>
{/if}
{#if pastEvents.length}
<div class="mb-4">
<h2 class="text-xl font-semibold text-white mb-4">Vergangene Events</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 opacity-70">
{#each pastEvents as event}
<a href={`/events/${event.slug}/`} class="group block h-full">
<Card.Root class="h-full overflow-hidden border-slate-700 bg-slate-800 transition-all hover:-translate-y-1 hover:shadow-xl">
<div class="h-32 bg-gradient-to-br from-blue-600 to-purple-700 relative">
<div class="absolute bottom-0 left-0 p-4 bg-gradient-to-t from-slate-900 to-transparent w-full">
<div class="inline-block bg-slate-900/80 backdrop-blur text-white text-xs font-bold px-2 py-1 rounded mb-1 border border-slate-600">
{dayjs(event.data.event.event.start).format("DD.MM.YYYY")}
</div>
</div>
</div>
<Card.Header>
<Card.Title class="text-white group-hover:text-blue-400 transition-colors">
{event.data.event.event.name}
</Card.Title>
</Card.Header>
<Card.Content>
<p class="text-gray-400 text-sm line-clamp-2">
Stattgefunden um {dayjs(event.data.event.event.start).format("HH:mm")}
</p>
<div class="mt-4 flex items-center text-sm text-blue-400 font-medium">
Details <span class="ml-1 transition-transform group-hover:translate-x-1"></span>
</div>
</Card.Content>
</Card.Root>
</a>
{/each}
</div>
</div>
{/if}
-21
View File
@@ -1,21 +0,0 @@
<script lang="ts">
import type { ExtendedEvent } from "../types/event";
import { Button } from "../ui/button";
import { Calendar } from "@lucide/svelte";
import { List } from "@lucide/svelte";
import EventList from "./EventList.svelte";
import CalendarView from "./Calendar.svelte";
const { events }: { events: { slug: string; data: { event: ExtendedEvent } }[] } = $props();
let viewMode = $state<"list" | "calendar">("list");
</script>
<div class="flex flex-col gap-6">
<div class="flex justify-between items-center">
<h1 class="text-3xl font-bold text-white">Events</h1>
</div>
<CalendarView {events} />
<EventList {events} />
</div>
-70
View File
@@ -1,70 +0,0 @@
<script lang="ts">
import type { Team } from "@type/team.ts";
import { teamHoverService } from "./team-hover.svelte";
import { Sheet, SheetContent, SheetTrigger } from "@components/ui/sheet";
import TeamInfo from "./TeamInfo.svelte";
import type { ExtendedEvent } from "@components/types/event";
const {
team,
event,
score = "",
time = false,
showWinner = false,
isWinner = false,
noWinner = false,
id,
disabled = false,
}: {
team: Team;
event: ExtendedEvent;
score?: string;
time?: boolean;
showWinner?: boolean;
isWinner?: boolean;
noWinner?: boolean;
id?: string;
disabled?: boolean;
} = $props();
let hoverService = $teamHoverService;
type StringAnyRecord = Record<string, any>;
</script>
{#if !disabled}
<Sheet>
<SheetTrigger>
{#snippet child({ props })}
{@render teamButton({ props })}
{/snippet}
</SheetTrigger>
<SheetContent>
<TeamInfo {team} {event} />
</SheetContent>
</Sheet>
{:else}
{@render teamButton({ props: {} })}
{/if}
{#snippet teamButton({ props }: { props: StringAnyRecord })}
<button
{...props}
class="flex justify-between px-2 w-full team-chip text-left border-b border-b-gray-700 last:border-b-0 {time ? 'py-1 hover:bg-gray-800' : 'py-3 cursor-pointer'} team-{disabled
? -1
: team.id} {hoverService.currentHover === team.id ? 'bg-gray-800' : ''} {showWinner ? 'border-l-4' : ''} {showWinner && isWinner ? 'border-l-yellow-500' : 'border-l-gray-950'}"
onmouseenter={() => team.id === -1 || hoverService.setHover(team.id)}
onmouseleave={() => team.id === -1 || hoverService.clearHover()}
{id}
>
<div class="flex">
<div class="w-12 {time ? 'font-bold' : ''}">
{team.kuerzel}
</div>
<span class={time ? "font-mono" : "font-bold"}>{team.name}</span>
</div>
<div class="{showWinner && isWinner && 'font-bold'} {isWinner ? 'text-yellow-400' : ''} {noWinner ? 'font-bold' : ''}">
{score}
</div>
</button>
{/snippet}
-96
View File
@@ -1,96 +0,0 @@
<script lang="ts">
import type { EventFight, ExtendedEvent, ResponseGroups } from "@type/event.ts";
import type { GroupViewConfig } from "./types";
import EventCard from "./EventCard.svelte";
import EventCardOutline from "./EventCardOutline.svelte";
import EventTeamChip from "./EventTeamChip.svelte";
import EventFightChip from "./EventFightChip.svelte";
import { teamHoverService } from "./team-hover.svelte";
const {
event,
config,
}: {
event: ExtendedEvent;
config: GroupViewConfig;
} = $props();
function detectRounds(fights: EventFight[], groupingTimeMinutes: number): EventFight[][] {
if (!fights || fights.length === 0) return [];
const groupingTimeMs = Math.max(1, Math.floor(groupingTimeMinutes || 10)) * 60 * 1000;
const sorted = [...fights].sort((a, b) => a.start - b.start);
const rounds: EventFight[][] = [];
let currentRound: EventFight[] = [];
let roundStart = sorted[0].start;
for (const fight of sorted) {
if (fight.start - roundStart <= groupingTimeMs) {
currentRound.push(fight);
} else {
if (currentRound.length) rounds.push(currentRound);
currentRound = [fight];
roundStart = fight.start;
}
}
if (currentRound.length) rounds.push(currentRound);
return rounds;
}
function chunkIntoRows<T>(items: T[], rowCount: number): T[][] {
if (!items || items.length === 0) return [];
const rows = Math.max(1, Math.floor(rowCount || 1));
const perRow = Math.ceil(items.length / rows);
const chunked: T[][] = [];
for (let i = 0; i < rows; i++) {
const slice = items.slice(i * perRow, (i + 1) * perRow);
if (slice.length) chunked.push(slice);
}
return chunked;
}
const hover = $teamHoverService;
</script>
{#each config.groups as groupId}
{@const group = event.groups.find((g) => g.id === groupId)!!}
{@const fights = event.fights.filter((f) => f.group?.id === groupId)}
{@const rounds = detectRounds(fights, config.roundGroupingTimeMinutes ?? 10)}
{@const roundRows = config.roundRows ?? 1}
{@const roundPrefix = config.roundPrefix ?? "Runde"}
{@const roundRowsChunked = chunkIntoRows(rounds, roundRows)}
<div class="flex">
<div>
<EventCard title={group.name}>
<EventCardOutline>
{#each Object.entries(group.points ?? {}).sort((v1, v2) => v2[1] - v1[1]) as points}
{@const [teamId, point] = points}
{@const team = event.teams.find((t) => t.id.toString() === teamId)!!}
<EventTeamChip {team} {event} score={point.toString()} />
{/each}
</EventCardOutline>
</EventCard>
</div>
<div class="flex flex-col">
{#each roundRowsChunked as row}
<div class="flex">
{#each row as round, index (round)}
{@const roundIndex = rounds.indexOf(round)}
{@const teams = Array.from(new Set(round.flatMap((f) => [f.redTeam, f.blueTeam])))}
<div class="{hover.currentHover && !teams.some((t) => t?.id === hover.currentHover) ? 'opacity-30' : ''} transition-opacity">
<EventCard title={`${roundPrefix} ${roundIndex + 1}`}>
{#each round as fight}
<EventFightChip {event} {fight} {group} />
{/each}
</EventCard>
</div>
{/each}
</div>
{/each}
</div>
</div>
{/each}
-79
View File
@@ -1,79 +0,0 @@
<script lang="ts">
import { dataRepo } from "@components/repo/data";
import type { ExtendedEvent, ResponseTeam } from "@components/types/event";
import EventFightChip from "./EventFightChip.svelte";
import SheetHeader from "@components/ui/sheet/sheet-header.svelte";
import { SheetDescription, SheetTitle } from "@components/ui/sheet";
const { event, team }: { event: ExtendedEvent; team: ResponseTeam } = $props();
let members = $derived.by(() => {
return fetchMembers(team.id);
});
let recentFights = $derived.by(() => {
return event.fights
.filter((f) => f.hasFinished && (f.blueTeam.id === team.id || f.redTeam.id === team.id))
.sort((a, b) => b.start - a.start)
.slice(0, 5);
});
async function fetchMembers(teamId: number) {
return await $dataRepo.queryPlayers(undefined, undefined, [teamId], 50, 0, false, false);
}
</script>
<SheetHeader>
<SheetTitle
>{team.name}
<span class="text-sm text-gray-400">{team.kuerzel}</span></SheetTitle
>
<SheetDescription>Statistiken des Teams</SheetDescription>
</SheetHeader>
<div class="mt-8 space-y-8">
<section>
<h3 class="text-lg font-semibold mb-4 border-b border-slate-800 pb-2 text-blue-400">Teammitglieder</h3>
{#await members}
<p class="text-slate-500 italic text-sm">Lade Mitglieder...</p>
{:then member}
<div class="grid grid-cols-2 gap-2">
{#each member.entries as p (p.uuid)}
<div class="bg-slate-800/50 p-2 rounded border border-slate-700 flex items-center gap-2">
<div class="w-6 h-6 rounded-full bg-slate-700 flex items-center justify-center text-[10px]">
{p.name.charAt(0)}
</div>
<span class="truncate text-sm">{p.name}</span>
</div>
{/each}
</div>
{/await}
</section>
<section>
<h3 class="text-lg font-semibold mb-4 border-b border-slate-800 pb-2 text-green-400">Letzte 5 Kämpfe</h3>
{#if recentFights.length > 0}
<div class="space-y-3">
{#each recentFights as fight}
<div class="scale-90 origin-left">
<EventFightChip
{event}
disabled={true}
{fight}
group={fight.group ?? {
id: -1,
name: "Event",
pointsPerWin: 0,
pointsPerLoss: 0,
pointsPerDraw: 0,
type: "GROUP_STAGE",
points: null,
}}
/>
</div>
{/each}
</div>
{:else}
<p class="text-slate-500 italic text-sm">Keine beendeten Kämpfe in diesem Event.</p>
{/if}
</section>
</div>
-64
View File
@@ -1,64 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
import type { ExtendedEvent } from "@components/types/event";
import type { Team } from "@components/types/team";
import { eventRepo } from "@components/repo/event";
const {
event,
}: {
event: ExtendedEvent;
} = $props();
let teams: Team[] = $state(event.teams);
const colorMap: Record<string, string> = {
"0": "#000000",
"1": "#0000AA",
"2": "#00AA00",
"3": "#00AAAA",
"4": "#AA0000",
"5": "#AA00AA",
"6": "#FFAA00",
"7": "#AAAAAA",
"8": "#555555",
"9": "#5555FF",
a: "#55FF55",
b: "#55FFFF",
c: "#FF5555",
d: "#FF55FF",
e: "#FFFF55",
f: "#FFFFFF",
};
onMount(async () => {
teams = await $eventRepo.listTeams(event.event.id.toString());
});
</script>
<div class="py-2 border-t border-t-gray-600">
<h1 class="text-2xl font-bold mb-4">Angemeldete Teams</h1>
<div
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2"
>
{#each teams as team}
<button
class="bg-neutral-800 p-2 rounded-md border border-neutral-700 border-l-4 flex flex-row items-center gap-2 cursor-pointer hover:bg-neutral-700 transition-colors w-full text-left"
style="border-left-color: {colorMap[team.color] || '#FFFFFF'}"
>
<span
class="text-sm font-mono text-neutral-400 shrink-0 w-8 text-center"
>{team.kuerzel}</span
>
<span class="font-bold truncate" title={team.name}>
{team.name}
</span>
</button>
{/each}
{#if teams.length === 0}
<p class="col-span-full text-center text-neutral-400">
Keine Teams angemeldet.
</p>
{/if}
</div>
</div>
@@ -1,55 +0,0 @@
import { readonly, writable } from "svelte/store";
class FightConnection {
constructor(
public readonly fromElement: HTMLElement,
public readonly toElement: HTMLElement,
public readonly color: string = "white",
public readonly background: boolean,
public readonly offset: number = 0
) {}
}
export class FightConnector {
private connections: FightConnection[] = $state([]);
get allConnections(): FightConnection[] {
return this.connections;
}
get showedConnections(): FightConnection[] {
const showBackground = this.connections.some((conn) => !conn.background);
return showBackground ? this.connections.filter((conn) => !conn.background) : this.connections;
}
addTeamConnection(teamId: number): void {
const teamElements = document.getElementsByClassName(`team-${teamId}`);
const teamArray = Array.from(teamElements);
teamArray.sort((a, b) => {
const rectA = a.getBoundingClientRect();
const rectB = b.getBoundingClientRect();
return rectA.top - rectB.top || rectA.left - rectB.left;
});
for (let i = 1; i < teamElements.length; i++) {
const fromElement = teamElements[i - 1] as HTMLElement;
const toElement = teamElements[i] as HTMLElement;
this.connections.push(new FightConnection(fromElement, toElement, "white", false));
}
}
addConnection(fromElement: HTMLElement, toElement: HTMLElement, color: string = "white", offset: number = 0): void {
this.connections.push(new FightConnection(fromElement, toElement, color, true, offset));
}
clearConnections(): void {
this.connections = this.connections.filter((conn) => conn.background);
}
clearAllConnections(): void {
this.connections = [];
}
}
const fightConnectorInternal = writable(new FightConnector());
export const fightConnector = readonly(fightConnectorInternal);

Some files were not shown because too many files have changed in this diff Show More