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", "error",
4 4
], ],
"linebreak-style": [
"error",
"unix"
],
"quotes": [ "quotes": [
"error", "error",
"double" "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 # macOS-specific files
.DS_Store .DS_Store
/.astro-i18n/
/bun.lockb /bun.lockb
/src/pages/de/ /src/pages/de/
/src/pages/en/
/steamwar-website.zip /steamwar-website.zip
/src/env.d.ts /src/env.d.ts
/src/pages/en/
/.idea /.idea
+4 -4
View File
@@ -7,7 +7,7 @@
- [Tailwind CSS](https://tailwindcss.com/) - [Tailwind CSS](https://tailwindcss.com/)
- [Day.js](https://day.js.org/) - [Day.js](https://day.js.org/)
- [Chart.js](https://www.chartjs.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/) - [Flowbite Svelte](https://flowbite-svelte.com/)
## Building ## Building
@@ -28,8 +28,8 @@ pnpm run dev
pnpm run build pnpm run build
``` ```
### i18n Compile ### i18n-sync
```bash ```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",
},
},
});
+13 -104
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 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 sitemap from "@astrojs/sitemap";
import { paraglideVitePlugin } from "@inlang/paraglide-js";
import robotsTxt from "astro-robots-txt"; import robotsTxt from "astro-robots-txt";
import {resolve} from "node:url";
import path from "node:path"; import path from "node:path";
import mdx from "@astrojs/mdx"; import mdx from "@astrojs/mdx";
import pagefind from "astro-pagefind";
import starlight from "@astrojs/starlight";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ 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", output: "static",
i18n: {
defaultLocale: "de",
locales: ["de", "en"],
routing: {
prefixDefaultLocale: false,
},
},
image: { image: {
service: sharpImageService(), service: sharpImageService(),
}, },
@@ -81,35 +20,15 @@ export default defineConfig({
site: "https://steamwar.de", site: "https://steamwar.de",
integrations: [ integrations: [
svelte(), svelte(),
starlight({ tailwind({
disable404Route: true, configFile: "./tailwind.config.cjs",
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/",
},
}), }),
pagefind(),
configureI18n(),
sitemap({ sitemap({
i18n: { i18n: {
defaultLocale: "de", defaultLocale: "en", locales: {
locales: { en: "en-US", de: "de-DE",
en: "en-US",
de: "de-DE",
}, },
}, },
}), }),
@@ -133,21 +52,12 @@ export default defineConfig({
{ userAgent: "omgili", disallow: "/" }, { userAgent: "omgili", disallow: "/" },
{ userAgent: "OmigliBot", disallow: "/" }, { userAgent: "OmigliBot", disallow: "/" },
{ userAgent: "PerplexityBot", disallow: "/" }, { userAgent: "PerplexityBot", disallow: "/" },
{ userAgent: "Timpibot", disallow: "/" }, { userAgent: "Timpibot", disallow: "/" }
], ],
}), }),
mdx(), mdx(),
], ],
vite: { vite: {
plugins: [
paraglideVitePlugin({
project: "./project.inlang",
outdir: "./src/paraglide",
strategy: ["url", "globalVariable", "baseLocale"],
emitTsDeclarations: true,
}),
tailwindcss(),
],
resolve: { resolve: {
alias: { alias: {
"@components": path.resolve("./src/components"), "@components": path.resolve("./src/components"),
@@ -159,8 +69,7 @@ export default defineConfig({
"@layouts": path.resolve("./src/layouts"), "@layouts": path.resolve("./src/layouts"),
"@repo": path.resolve("./src/components/repo"), "@repo": path.resolve("./src/components/repo"),
"@stores": path.resolve("./src/components/stores"), "@stores": path.resolve("./src/components/stores"),
$lib: path.resolve("./src"),
}, },
}, },
}, },
}); });
-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"
}
+67 -86
View File
@@ -1,88 +1,69 @@
{ {
"name": "steamwar-website", "name": "steamwar-website",
"type": "module", "type": "module",
"version": "0.0.1", "version": "0.0.1",
"scripts": { "scripts": {
"dev": "pnpm run i18n:generate:pages && astro dev", "dev": "astro dev",
"start": "pnpm run dev", "start": "astro dev",
"build": "pnpm run i18n:generate:pages && astro build", "build": "astro build",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro", "astro": "astro",
"i18n:generate:pages": "node scripts/generate-en-pages.mjs", "i18n:extract": "astro-i18n extract",
"i18n:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/paraglide --strategy url globalVariable baseLocale --emit-ts-declarations && pnpm run i18n:generate:pages", "i18n:generate:pages": "astro-i18n generate:pages --purge",
"clean:dist": "rm -rf dist", "i18n:generate:types": "astro-i18n generate:types",
"clean:node_modules": "rm -rf node_modules", "i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types",
"ci": "pnpm install && pnpm run build" "clean:dist": "rm -rf dist",
}, "clean:node_modules": "rm -rf node_modules",
"devDependencies": { "ci": "pnpm run clean:dist && pnpm install && pnpm run i18n:sync && pnpm run build"
"@astrojs/svelte": "^8.1.1", },
"@internationalized/date": "^3.12.1", "devDependencies": {
"@lucide/svelte": "^1.16.0", "@astrojs/svelte": "^6.0.2",
"@tailwindcss/vite": "^4.3.0", "@astrojs/tailwind": "^5.1.2",
"@types/color": "^4.2.1", "@astropub/icons": "^0.2.0",
"@types/js-yaml": "^4.0.9", "@types/color": "^4.2.0",
"@types/node": "^25.9.0", "@types/node": "^22.9.3",
"@types/three": "^0.184.1", "@types/three": "^0.170.0",
"@typescript-eslint/eslint-plugin": "^8.59.4", "@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.59.4", "@typescript-eslint/parser": "^8.15.0",
"autoprefixer": "^10.5.0", "autoprefixer": "^10.4.20",
"bits-ui": "2.18.1", "cssnano": "^7.0.6",
"clsx": "^2.1.1", "esbuild": "^0.24.0",
"cssnano": "^8.0.1", "eslint": "^9.15.0",
"embla-carousel-svelte": "^8.6.0", "eslint-plugin-astro": "^1.3.1",
"esbuild": "^0.28.0", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint": "^10.4.0", "eslint-plugin-svelte": "^2.46.0",
"eslint-plugin-astro": "^1.7.0", "postcss-nesting": "^13.0.1",
"eslint-plugin-svelte": "^3.17.1", "sass": "^1.81.0",
"formsnap": "2.0.1", "svelte": "^5.16.0",
"mode-watcher": "^1.1.0", "tailwind-merge": "^2.5.5",
"paneforge": "^1.0.2", "tailwindcss": "^3.4.15",
"postcss-nesting": "^14.0.0", "three": "^0.170.0",
"sass": "^1.99.0", "typescript": "^5.7.2"
"svelte": "^5.55.8", },
"svelte-sonner": "^1.1.1", "dependencies": {
"tailwind-merge": "^3.6.0", "@astrojs/mdx": "^3.1.9",
"tailwind-variants": "^3.2.2", "@astrojs/sitemap": "^3.2.1",
"tailwindcss": "^4.3.0", "@codemirror/lang-json": "^6.0.1",
"three": "^0.184.0", "@ddietr/codemirror-themes": "^1.4.4",
"typescript": "^6.0.3", "astro": "^4.16.14",
"zod": "^4.4.3" "astro-i18n": "^2.2.4",
}, "astro-pagefind": "^1.6.0",
"dependencies": { "astro-robots-txt": "^1.0.0",
"@astrojs/mdx": "^5.0.6", "astro-seo": "^0.8.4",
"@astrojs/sitemap": "^3.7.2", "chart.js": "^4.4.6",
"@astrojs/starlight": "^0.39.2", "chartjs-adapter-dayjs-4": "^1.0.4",
"@astrojs/starlight-tailwind": "^5.0.0", "chartjs-adapter-moment": "^1.0.1",
"@codemirror/commands": "^6.10.3", "color": "^4.2.3",
"@codemirror/lang-json": "^6.0.2", "dayjs": "^1.11.13",
"@codemirror/view": "^6.43.0", "easymde": "^2.18.0",
"@ddietr/codemirror-themes": "^1.5.2", "flowbite": "^2.5.2",
"@inlang/paraglide-js": "^2.18.0", "flowbite-svelte": "^0.47.3",
"@tanstack/table-core": "^8.21.3", "flowbite-svelte-icons": "^2.0.2",
"astro": "6.3.5", "qs": "^6.13.1",
"astro-robots-txt": "^1.0.0", "sharp": "^0.33.5",
"astro-seo": "^1.1.0", "svelte-awesome": "^3.3.5",
"chart.js": "^4.5.1", "svelte-codemirror-editor": "^1.4.1",
"chartjs-adapter-dayjs-4": "^1.0.4", "svelte-spa-router": "^4.0.1",
"chartjs-adapter-moment": "^1.0.1", "zod": "^3.23.8"
"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",
"svelte-awesome": "^3.3.5",
"svelte-spa-router": "^5.1.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",
"esbuild",
"sharp"
]
}
} }
+7205 -5956
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -4,6 +4,6 @@ module.exports = {
plugins: [ plugins: [
require('autoprefixer'), require('autoprefixer'),
require('cssnano'), 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"; import localBau from "@images/90.png";
--- ---
<Image <Image src={localBau} alt="Bau" widths={[240, 540, 720, 1080, 1920, localBau.width]}
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`} 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" class="w-full h-full object-cover rounded-b-2xl shadow-2xl" quality={100}
quality={100} draggable="false" loading="eager"/>
draggable="false"
loading="eager"
/>
+24 -34
View File
@@ -18,13 +18,13 @@
--> -->
<script lang="ts"> <script lang="ts">
import { twMerge } from "tailwind-merge"; import {twMerge} from "tailwind-merge";
import { onMount } from "svelte"; import {onMount} from "svelte";
let cardElement: HTMLDivElement = $state(); let cardElement: HTMLDivElement = $state();
function rotateElement(event: MouseEvent) { function rotateElement(event: MouseEvent) {
if (!hoverEffect) return; if(!hoverEffect) return;
const x = event.clientX; const x = event.clientX;
const y = event.clientY; const y = event.clientY;
@@ -36,23 +36,23 @@
const rotateX = (centerY - y) / 20; const rotateX = (centerY - y) / 20;
const rotateY = -(centerX - x) / 20; const rotateY = -(centerX - x) / 20;
cardElement.style.setProperty("--rotate-x", `${rotateX}deg`); cardElement.style.setProperty('--rotate-x', `${rotateX}deg`);
cardElement.style.setProperty("--rotate-y", `${rotateY}deg`); cardElement.style.setProperty('--rotate-y', `${rotateY}deg`);
} }
function resetElement() { function resetElement() {
cardElement.style.setProperty("--rotate-x", "0"); cardElement.style.setProperty('--rotate-x', "0");
cardElement.style.setProperty("--rotate-y", "0"); cardElement.style.setProperty('--rotate-y', "0");
} }
interface Props { interface Props {
hoverEffect?: boolean; hoverEffect?: boolean;
extraClasses?: string; extraClasses?: string;
children?: import("svelte").Snippet; children?: import('svelte').Snippet;
} }
let { hoverEffect = true, extraClasses = "", children }: Props = $props(); let { hoverEffect = true, extraClasses = '', children }: Props = $props();
let classes = $derived(twMerge("flex flex-col items-center p-8 m-4 bg-[#0c0c0c] border border-[rgba(255,255,255,0.06)] text-gray-100", extraClasses)); 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> </script>
<div class={classes} bind:this={cardElement} onmousemove={rotateElement} onmouseleave={resetElement} class:hoverEffect> <div class={classes} bind:this={cardElement} onmousemove={rotateElement} onmouseleave={resetElement} class:hoverEffect>
@@ -61,30 +61,20 @@
<style lang="scss"> <style lang="scss">
div { div {
transform: perspective(1000px) rotateX(var(--rotate-x, 0)) rotateY(var(--rotate-y, 0)) !important; transform: perspective(1000px) rotateX(var(--rotate-x, 0)) rotateY(var(--rotate-y, 0)) !important;
transition: scale 300ms cubic-bezier(0.2, 3, 0.67, 0.6); transition: scale 300ms cubic-bezier(.2, 3, .67, .6);
:global(h1) { :global(h1) {
margin-top: 1rem; @apply text-xl font-bold mt-4;
font-size: 1.25rem; }
line-height: 1.75rem;
font-weight: 700;
font-family: "Barlow Condensed", sans-serif;
letter-spacing: 0.06em;
}
:global(svg) { :global(svg) {
color: #f59e0b; @apply transition-transform duration-300 ease-in-out hover:scale-110 hover:drop-shadow-2xl
transition: transform 300ms ease-in-out; }
}
:global(svg:hover) {
transform: scale(1.1);
}
} }
.hoverEffect:hover { .hoverEffect:hover {
scale: 105%; scale: 105%;
} }
</style> </style>
+1 -1
View File
@@ -18,7 +18,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import {t} from "$lib/i18n" import {t} from "astro-i18n"
import UserInfo from "./dashboard/UserInfo.svelte"; import UserInfo from "./dashboard/UserInfo.svelte";
import {dataRepo} from "@repo/data.ts"; import {dataRepo} from "@repo/data.ts";
import UploadModal from "@components/dashboard/UploadModal.svelte"; import UploadModal from "@components/dashboard/UploadModal.svelte";
+5 -2
View File
@@ -18,9 +18,8 @@
--> -->
<script lang="ts"> <script lang="ts">
import {t} from "$lib/i18n"; import {t} from "astro-i18n";
import {statsRepo} from "@repo/stats.ts"; import {statsRepo} from "@repo/stats.ts";
import "@styles/table.css"
interface Props { interface Props {
@@ -65,3 +64,7 @@
<p>{error.message}</p> <p>{error.message}</p>
{/await} {/await}
<style>
@import "../styles/table.css";
</style>
+4 -4
View File
@@ -1,7 +1,7 @@
<!-- <!--
- This file is a part of the SteamWar software. - 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 - 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 - it under the terms of the GNU Affero General Public License as published by
@@ -19,8 +19,8 @@
<script lang="ts"> <script lang="ts">
import FightStatsChart from "./FightStatsChart.svelte"; import FightStatsChart from "./FightStatsChart.svelte";
import { t } from "$lib/i18n"; import {t} from "astro-i18n";
import { statsRepo } from "@repo/stats.ts"; import {statsRepo} from "@repo/stats.ts";
let request = getStats(); let request = getStats();
@@ -35,4 +35,4 @@
<FightStatsChart data={stats} /> <FightStatsChart data={stats} />
{:catch error} {:catch error}
<p>error: {error}</p> <p>error: {error}</p>
{/await} {/await}
+1 -3
View File
@@ -79,8 +79,6 @@
}) })
}, },
options: { options: {
maintainAspectRatio: false,
scales: { scales: {
x: { x: {
type: "time", type: "time",
@@ -107,5 +105,5 @@
</script> </script>
<div> <div>
<canvas height="500" bind:this={canvas}></canvas> <canvas bind:this={canvas}></canvas>
</div> </div>
+14 -18
View File
@@ -19,32 +19,30 @@
--> -->
<script lang="ts"> <script lang="ts">
import { window } from "./utils.ts"; import {window} from "./util.ts";
import { astroI18n, t } from "$lib/i18n"; import {astroI18n, t} from "astro-i18n";
import type { EventFight, ExtendedEvent } from "@type/event"; import type {EventFight, ExtendedEvent} from "@type/event";
import "@styles/table.css"; import "@styles/table.css";
export let event: ExtendedEvent; export let event: ExtendedEvent;
export let group: number; export let group: string;
export let rows: number = 1; export let rows: number = 1;
function getWinner(fight: EventFight) { function getWinner(fight: EventFight) {
if (!fight.hasFinished) {
return t("announcements.table.notPlayed");
}
switch (fight.ergebnis) { switch (fight.ergebnis) {
case 1: case 1:
return fight.blueTeam.kuerzel; return fight.blueTeam.kuerzel;
case 2: case 2:
return fight.redTeam.kuerzel; return fight.redTeam.kuerzel;
default: case 3:
return t("announcements.table.draw"); return t("announcements.table.draw");
default:
return t("announcements.table.notPlayed");
} }
} }
</script> </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> <table>
<thead> <thead>
<tr class="font-bold border-b"> <tr class="font-bold border-b">
@@ -57,15 +55,13 @@
</tr> </tr>
</thead> </thead>
<tbody> <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> <tr>
{#each fights as fight (fight.id)} {#each fights as fight (fight.id)}
<td <td>{Intl.DateTimeFormat(astroI18n.locale, {
>{Intl.DateTimeFormat(astroI18n.locale, { hour: "numeric",
hour: "numeric", minute: "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 === 1} class:italic={fight.ergebnis === 3}>{fight.blueTeam.kuerzel}</td>
<td class:font-bold={fight.ergebnis === 2} class:italic={fight.ergebnis === 3}>{fight.redTeam.kuerzel}</td> <td class:font-bold={fight.ergebnis === 2} class:italic={fight.ergebnis === 3}>{fight.redTeam.kuerzel}</td>
<td>{getWinner(fight)}</td> <td>{getWinner(fight)}</td>
@@ -74,4 +70,4 @@
{/each} {/each}
</tbody> </tbody>
</table> </table>
</div> </div>
+26 -22
View File
@@ -19,32 +19,36 @@
--> -->
<script lang="ts"> <script lang="ts">
import { window } from "./utils.ts"; import {window} from "./util.ts";
import { t } from "$lib/i18n"; import {t} from "astro-i18n";
import type { ExtendedEvent } from "@type/event.ts"; import type {ExtendedEvent} from "@type/event.ts";
import "@styles/table.css"; import "@styles/table.css"
let { export let event: ExtendedEvent;
event, export let group: string;
group, export let rows: number = 1;
rows = 1,
}: {
event: ExtendedEvent;
group: number;
rows?: number;
} = $props();
let teamPoints = $derived( $: teamPoints = event.teams.map(team => {
Object.entries(event.groups.find((g) => g.id === group)?.points ?? {}) const fights = event.fights.filter(fight => fight.blueTeam.id === team.id || fight.redTeam.id === team.id);
.map(([teamId, points]) => ({ const points = fights.reduce((acc, fight) => {
team: event.teams.find((t) => t.id === Number(teamId))!!, if (fight.ergebnis === 1 && fight.blueTeam.id === team.id) {
points: points, return acc + 3;
})) } else if (fight.ergebnis === 2 && fight.redTeam.id === team.id) {
.sort((a, b) => b.points - a.points), 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> </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"> <table class="w-full">
<thead> <thead>
<tr class="font-bold border-b"> <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"> <div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 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="font-bold">{t("warning.title")}</p>
<p class="text-gray-400 text-sm">{t("warning.text")}</p> <p>{t("warning.text")}</p>
</div> </div>
+23 -89
View File
@@ -1,7 +1,7 @@
<!-- <!--
- This file is a part of the SteamWar software. - 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 - 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 - it under the terms of the GNU Affero General Public License as published by
@@ -18,20 +18,21 @@
--> -->
<script lang="ts"> <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 {l} from "@utils/util.ts";
import { get } from "svelte/store"; import {t} from "astro-i18n";
import { navigate } from "astro:transitions/client"; import {get} from "svelte/store";
import { onMount } from "svelte"; import {navigate} from "astro:transitions/client";
import { authV2Repo } from "./repo/authv2.ts";
let username: string = $state(""); let username: string = $state("");
let pw: string = $state(""); let pw: string = $state("");
let error: string = $state(""); let error: string = $state("");
async function login() { 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 === "") { if (username === "" || pw === "") {
pw = ""; pw = "";
error = t("login.error"); error = t("login.error");
@@ -39,43 +40,24 @@
} }
try { try {
let auth = await get(authV2Repo).login(username, pw); let auth = await get(authRepo).login(username, pw);
if (!auth) { if (auth == undefined) {
pw = ""; pw = "";
error = t("login.error"); error = t("login.error");
return; return;
} }
await navigate(l("/dashboard")); tokenStore.set(auth);
navigate(l("/dashboard"));
} catch (e: any) { } catch (e: any) {
pw = ""; pw = "";
error = t("login.error"); 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> </script>
<form class="sw-login-form" onsubmit={preventDefault(login)}> <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" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.08em;">{t("login.title")}</h1> <h1 class="text-4xl text-white text-center">{t("login.title")}</h1>
<div class="ml-2 flex flex-col"> <div class="ml-2 flex flex-col">
<label for="username">{t("login.label.username")}</label> <label for="username">{t("login.label.username")}</label>
<input type="text" id="username" name="username" placeholder={t("login.placeholder.username")} bind:value={username} /> <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} /> <input type="password" id="password" name="password" placeholder={t("login.placeholder.password")} bind:value={pw} />
</div> </div>
<p class="mt-2"> <p class="mt-2">
<a class="sw-link" href={l("/set-password")}>{t("login.setPassword")}</a> <a class="text-neutral-500 hover:underline" href={l("/set-password")}>{t("login.setPassword")}</a></p>
</p>
{#if error} {#if error}
<p class="mt-2 text-red-500">{error}</p> <p class="mt-2 text-red-500">{error}</p>
{/if} {/if}
<button class="btn mt-4 justify-center w-full" type="submit" onclick={preventDefault(login)}>{t("login.submit")}</button> <button class="btn mt-4 !mx-0 justify-center" 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>
</form> </form>
<style lang="postcss"> <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 { input {
width: 20rem; @apply border-2 rounded-md p-2 shadow-2xl w-80
padding: 0.6rem 0.8rem; dark:bg-neutral-800
margin-top: 0.25rem; focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:border-transparent;
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);
} }
label { label {
font-family: "Barlow Condensed", sans-serif; @apply text-neutral-300;
font-size: 0.7rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: rgba(163, 163, 163, 0.7);
margin-top: 0.5rem;
} }
</style>
.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;
}
</style>
+134 -134
View File
@@ -18,98 +18,133 @@
--> -->
<script lang="ts"> <script lang="ts">
import "../styles/button.css"; import "../styles/button.css";
import { CaretDownOutline, GlobeOutline } from "flowbite-svelte-icons"; import { CaretDownOutline, SearchOutline } from "flowbite-svelte-icons";
import { t, l } from "$lib/i18n"; import { t } from "astro-i18n";
import { onMount } from "svelte"; import { l } from "../util/util";
import { loggedIn } from "@repo/authv2.ts"; import { onMount } from "svelte";
import { astroI18n } from "$lib/i18n"; import { loggedIn } from "@repo/authv2.ts";
interface Props { interface Props {
logo?: import("svelte").Snippet; logo?: import("svelte").Snippet;
}
let { logo }: Props = $props();
let navbar = $state<HTMLDivElement>();
let searchOpen = $state(false);
let accountBtn = $state<HTMLAnchorElement>();
$effect(() => {
if ($loggedIn) {
accountBtn!.href = l("/dashboard");
} else {
accountBtn!.href = l("/login");
} }
});
let { logo }: Props = $props(); onMount(() => {
handleScroll();
});
let navbar = $state<HTMLElement>(); function handleScroll() {
let searchOpen = $state(false); if (window.scrollY > 0) {
navbar!.classList.add("before:scale-y-100");
let accountBtn = $state<HTMLAnchorElement>(); } else {
navbar!.classList.remove("before:scale-y-100");
let currentPage = $state(astroI18n.route);
$effect(() => {
if ($loggedIn) {
accountBtn!.href = l("/dashboard");
} else {
accountBtn!.href = l("/login");
}
});
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");
} else {
navbar!.classList.remove("sw-nav-scrolled");
}
} }
}
</script> </script>
<svelte:window onscroll={handleScroll} /> <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}> <nav
<div class="flex flex-row items-center justify-evenly md:justify-between match"> data-pagefind-ignore
<a class="flex items-center" href={l("/")}> 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"
{@render logo?.()} bind:this={navbar}
<span class="sw-nav-title hidden md:inline-block"> >
{t("navbar.title")} <div
<span class="scrolled-trigger" style="display: none" aria-hidden="true"></span> class="flex flex-row items-center justify-evenly md:justify-between match"
</span> >
</a> <a class="flex items-center" href={l("/")}>
<div class="flex justify-center flex-wrap gap-2"> {@render logo?.()}
<div class="btn-dropdown"> <span
<button class="btn btn-gray"> class="text-2xl uppercase font-bold dark:text-white hidden md:inline-block"
<a href={l("/")}> >
<span class="btn__text">{t("navbar.links.home.title")}</span> {t("navbar.title")}
</a> <span
<CaretDownOutline class="ml-2 mt-auto" /> class="before:scale-y-100"
</button> style="display: none"
<div> aria-hidden="true"
<a class="btn btn-gray" href={l("/announcements")}>{t("navbar.links.home.announcements")}</a> ></span>
<a class="btn btn-gray" href={l("/events")}>{t("navbar.links.home.events")}</a> </span>
<a class="btn btn-gray" href={l("/downloads")}>{t("navbar.links.home.downloads")}</a> </a>
<a class="btn btn-gray" href={l("/faq")}>{t("navbar.links.home.faq")}</a> <div class="flex justify-center flex-wrap">
<a class="btn btn-gray" href={l("/code-of-conduct")}>{t("navbar.links.rules.coc")}</a> <div class="btn-dropdown">
</div> <button class="btn btn-gray">
</div> <a href={l("/")}>
<div class="btn-dropdown"> <span class="btn__text">{t("navbar.links.home.title")}</span>
<button class="btn btn-gray"> </a>
<a rel="prefetch" href={l("/rules")}> <CaretDownOutline class="ml-2 mt-auto" />
<span class="btn__text">{t("navbar.links.rules.title")}</span> </button>
</a> <div>
<CaretDownOutline class="ml-2 mt-auto" /> <a class="btn btn-gray" href={l("/announcements")}
</button> >{t("navbar.links.home.announcements")}</a
<div> >
<a href={l("/rules/wargear")} class="btn btn-gray">{t("navbar.links.rules.wg")}</a> <a class="btn btn-gray" href={l("/downloads")}
<a href={l("/rules/miniwargear")} class="btn btn-gray">{t("navbar.links.rules.mwg")}</a> >{t("navbar.links.home.downloads")}</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 class="btn btn-gray" href={l("/tutorials")}
<a href={l("/rules/quickgear")} class="btn btn-gray">{t("navbar.links.rules.qg")}</a> >{t("navbar.links.home.tutorials")}</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 class="btn btn-gray" href={l("/faq")}
<a href={l("/rules/microwargear")} class="btn btn-gray">{t("navbar.links.rules.micro")}</a> >{t("navbar.links.home.faq")}</a
<a href={l("/rules/streetfight")} class="btn btn-gray">{t("navbar.links.rules.sf")}</a> >
</div> <a class="btn btn-gray" href={l("/code-of-conduct")}
</div> >{t("navbar.links.rules.coc")}</a
<!-- TODO: Add help center >
</div>
</div>
<div class="btn-dropdown">
<button class="btn btn-gray">
<a rel="prefetch" href={l("/rules")}>
<span class="btn__text">{t("navbar.links.rules.title")}</span>
</a>
<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
>
<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
>
<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
<div class="btn-dropdown my-1"> <div class="btn-dropdown my-1">
<div class="btn btn-gray" tabindex="1"> <div class="btn btn-gray" tabindex="1">
<a rel="prefetch"> <a rel="prefetch">
@@ -123,61 +158,26 @@
</div> </div>
</div> </div>
--> -->
<a class="btn" href={l("/login")} bind:this={accountBtn}> <a class="btn" href={l("/login")} bind:this={accountBtn}>
<span class="btn__text">{t("navbar.links.account")}</span> <span class="btn__text">{t("navbar.links.account")}</span>
</a> </a>
<div class="btn-dropdown"> <!--
<button class="btn btn-gray"> <button class="btn my-1" onclick={() => searchOpen = true}>
<GlobeOutline /> <SearchOutline ariaLabel="Site Search" class="inline-block h-6"/>
</button> </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> </div>
</div>
</nav> </nav>
{#if searchOpen} {#if searchOpen}
{#await import("./SearchComponent.svelte") then c} {#await import("./SearchComponent.svelte") then c}
<c.default bind:open={searchOpen} /> <c.default bind:open={searchOpen} />
{/await} {/await}
{/if} {/if}
<style lang="scss"> <style lang="scss">
.match { .match {
width: min(100vw, 70em); 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> </style>
+26 -31
View File
@@ -1,55 +1,50 @@
--- ---
import type { CollectionEntry } from "astro:content"; import {CollectionEntry} from "astro:content";
import { l } from "../util/util"; import {l} from "../util/util";
import { astroI18n, stripLocaleFromPath } from "$lib/i18n"; import {astroI18n} from "astro-i18n";
import { Image } from "astro:assets"; import {Image} from "astro:assets";
import TagComponent from "./TagComponent.astro"; import TagComponent from "./TagComponent.astro";
import P from "./P.astro"; import P from "./P.astro";
import Card from "@components/Card.svelte"; import Card from "@components/Card.svelte";
interface Props { interface Props {
post: CollectionEntry<"announcements">; post: CollectionEntry<"announcements">
} }
const { const { post, slim }: {
post, post: CollectionEntry<"announcements">,
slim, slim: boolean,
}: {
post: CollectionEntry<"announcements">;
slim: boolean;
} = Astro.props as Props; } = 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"}`}> <div class={`flex flex-row ${slim ? "":"p-4"}`}>
{ {post.data.image != null
post.data.image != null ? ( ? (
<a href={postUrl}> <a href={postUrl}>
<div class="flex-shrink-0 pr-2"> <div class="flex-shrink-0 pr-2">
<Image transition:name={post.data.title + "-image"} src={post.data.image} alt="Post Image" class="object-cover h-32 w-32 max-w-none transition-transform hover:scale-105" /> <Image transition:name={post.data.title + "-image"} src={post.data.image} alt="Post Image" class="rounded-2xl shadow-2xl object-cover h-32 w-32 max-w-none transition-transform hover:scale-105" />
</div> </div>
</a> </a>
) : null )
} : null}
<div> <div>
<a href={postUrl} class="flex flex-col items-start"> <a href={postUrl} class="flex flex-col items-start">
<h2 class="text-2xl font-bold" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.04em;" transition:name={post.data.title + "-title"}>{post.data.title}</h2> <h2 class="text-2xl font-bold" transition:name={post.data.title + "-title"}>{post.data.title}</h2>
<P class="text-gray-500 text-sm" <P class="text-gray-500">{Intl.DateTimeFormat(astroI18n.locale, {
>{ day: "numeric",
Intl.DateTimeFormat(astroI18n.locale, { month: "long",
day: "numeric", year: "numeric",
month: "long", }).format(post.data.created)}</P>
year: "numeric",
}).format(post.data.created)
}</P
>
<P>{post.data.description}</P> <P>{post.data.description}</P>
</a> </a>
<div class="mt-1" transition:name={post.data.title + "-tags"}> <div class="mt-1" transition:name={post.data.title + "-tags"}>
{post.data.tags.map((tag) => <TagComponent tag={tag} />)} {post.data.tags.map((tag) => (
<TagComponent tag={tag} />
))}
</div> </div>
</div> </div>
</div> </div>
</Card> </Card>
+22 -35
View File
@@ -18,11 +18,12 @@
--> -->
<script lang="ts"> <script lang="ts">
import { slide, fade } from "svelte/transition";
import { onMount } from "svelte"; import {slide, fade} from "svelte/transition";
import { importPagefind, type Pagefind, type PagefindDocument } from "@type/pagefind.js"; import {onMount} from "svelte";
import {importPagefind, type Pagefind, type PagefindDocument} from "@type/pagefind.js";
import Card from "@components/Card.svelte"; import Card from "@components/Card.svelte";
import { l } from "@utils/util.ts"; import {l} from "@utils/util.ts";
let pagefind: Pagefind; let pagefind: Pagefind;
onMount(async () => { onMount(async () => {
@@ -35,26 +36,27 @@
async function search(e: KeyboardEvent) { async function search(e: KeyboardEvent) {
if (e.target instanceof HTMLInputElement) { if (e.target instanceof HTMLInputElement) {
let search: { results: any[] } = await pagefind.debouncedSearch(e.target.value); 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()))
} }
} }
interface Props { interface Props {
open?: boolean; open?: boolean;
} }
let { open = $bindable(false) }: Props = $props(); let { open = $bindable(false) }: Props = $props();
</script> </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> <button transition:fade class="fixed top-0 left-0 w-screen h-screen backdrop-blur z-20 cursor-default" onclick={() => open = false}>
<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"> </button>
<input placeholder="Search..." onkeypress={search} /> <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} {#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)}> <a class="grid grid-cols-3" href={l(result.url)}>
<h1>{result.meta.title}</h1> <h1>{result.meta.title}</h1>
{#each result.sub_results.slice(0, 2) as sub_result} {#each result.sub_results.slice(0, 2) as sub_result}
@@ -67,28 +69,13 @@
</div> </div>
<style lang="postcss"> <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 { input {
width: 100%; @apply border-2 rounded-md p-2 shadow-2xl w-full
padding: 0.7rem 1rem; dark:bg-neutral-800
background: rgba(255, 255, 255, 0.03); focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:border-transparent;
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);
} }
label { label {
color: rgba(163, 163, 163, 0.7); @apply text-neutral-300;
} }
</style> </style>
+1 -1
View File
@@ -18,7 +18,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import { t } from "$lib/i18n" import { t } from "astro-i18n"
import {server} from "./stores/stores.ts"; import {server} from "./stores/stores.ts";
function generateVersionString(version: string): string { function generateVersionString(version: string): string {
+12 -21
View File
@@ -1,31 +1,22 @@
--- ---
import { l } from "../util/util";
import { capitalize } from "./admin/util"; import {l} from "../util/util";
import {capitalize} from "./admin/util";
interface Props { interface Props {
tag: string; tag: string;
noLink?: boolean; noLink?: boolean;
} }
const { tag, noLink } = Astro.props; const {tag, noLink} = Astro.props;
--- ---
{ {noLink
noLink ? ( ? (
<span <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>
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>
) )
} : (
<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 wrap from "svelte-spa-router/wrap";
import Router, {replace} from "svelte-spa-router"; import Router, {replace} from "svelte-spa-router";
import {get} from "svelte/store"; import {get} from "svelte/store";
import {loggedIn} from "@repo/authv2.ts"; import {tokenStore} from "@repo/repo";
const routes: RouteDefinition = { 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({ "/perms": wrap({
asyncComponent: () => import("./pages/Perms.svelte"), asyncComponent: () => import("./pages/Perms.svelte"),
conditions: detail => get(loggedIn) conditions: detail => get(tokenStore) != ""
}), }),
"/login": wrap({ "/login": wrap({
asyncComponent: () => import("./pages/Login.svelte"), asyncComponent: () => import("./pages/Login.svelte"),
conditions: detail => !get(loggedIn) conditions: detail => get(tokenStore) == ""
}), }),
"/event/:id": wrap({ "/event/:id": wrap({
asyncComponent: () => import("./pages/Event.svelte"), asyncComponent: () => import("./pages/Event.svelte"),
conditions: detail => get(loggedIn) conditions: detail => get(tokenStore) != ""
}), }),
"/event/:id/generate": wrap({ "/event/:id/generate": wrap({
asyncComponent: () => import("./pages/Generate.svelte"), asyncComponent: () => import("./pages/Generate.svelte"),
conditions: detail => get(loggedIn) conditions: detail => get(tokenStore) != ""
}), }),
"/edit": wrap({ "/edit": wrap({
asyncComponent: () => import("./pages/Edit.svelte"), asyncComponent: () => import("./pages/Edit.svelte"),
conditions: detail => get(loggedIn) conditions: detail => get(tokenStore) != ""
}), }),
"/display/:event": wrap({ "/display/:event": wrap({
asyncComponent: () => import("./pages/Display.svelte"), asyncComponent: () => import("./pages/Display.svelte"),
conditions: detail => get(loggedIn) conditions: detail => get(tokenStore) != ""
}), }),
"*": wrap({asyncComponent: () => import("./pages/NotFound.svelte")}) "*": wrap({asyncComponent: () => import("./pages/NotFound.svelte")})
}; };
function conditionsFailed(event: ConditionsFailedEvent) { function conditionsFailed(event: ConditionsFailedEvent) {
console.log(event)
if (event.detail.location === "/login") { if (event.detail.location === "/login") {
replace("/"); replace("/");
} else { } else {
@@ -18,103 +18,83 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Input, Label, Select } from "flowbite-svelte"; import {Input, Label, Select} from "flowbite-svelte";
import TypeAheadSearch from "./TypeAheadSearch.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"; import type {Team} from "@type/team.ts";
interface Props { interface Props {
teams?: Team[]; teams?: Team[];
blueTeam: string; blueTeam: string;
redTeam: string; redTeam: string;
start?: string; start?: string;
gamemode?: string; gamemode?: string;
map?: string; map?: string;
spectatePort?: string | null; spectatePort?: string | null;
group?: string | null; group?: string | null;
groupSearch?: string; groupSearch?: string;
} }
let { let {
teams = [], teams = [],
blueTeam = $bindable(), blueTeam = $bindable(),
redTeam = $bindable(), redTeam = $bindable(),
start = $bindable(""), start = $bindable(""),
gamemode = $bindable(""), gamemode = $bindable(""),
map = $bindable(""), map = $bindable(""),
spectatePort = $bindable(null), spectatePort = $bindable(null),
group = $bindable(""), group = $bindable(""),
groupSearch = $bindable(""), groupSearch = $bindable("")
}: Props = $props(); }: Props = $props();
let selectableTeams = $derived( let selectableTeams = $derived(teams.map(team => {
teams return {
.map((team) => { name: team.name,
return { value: team.id.toString()
name: team.name, };
value: team.id.toString(), }).sort((a, b) => a.name.localeCompare(b.name)));
};
})
.sort((a, b) => a.name.localeCompare(b.name))
);
let selectableGamemodes = $derived( let selectableGamemodes = $derived($gamemodes.map(gamemode => {
$gamemodes return {
.map((gamemode) => { name: gamemode,
return { value: gamemode
name: 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 customGamemode = $derived(!selectableGamemodes.some((e) => e.name === gamemode) && gamemode !== "");
let selectableCustomGamemode = $derived([ let selectableCustomGamemode = $derived([
...selectableGamemodes, ...selectableGamemodes, {
{
name: gamemode + " (custom)", name: gamemode + " (custom)",
value: gamemode, value: gamemode
}, }
]); ]);
let mapsStore = $derived(maps(gamemode)); let mapsStore = $derived(maps(gamemode));
let selectableMaps = $derived( let selectableMaps = $derived($mapsStore.map(map => {
$mapsStore return {
.map((map) => { name: map,
return { value: map
name: 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 customMap = $derived(!selectableMaps.some((e) => e.name === map) && map !== "");
let selectableCustomMaps = $derived([ let selectableCustomMaps = $derived([
...selectableMaps, ...selectableMaps, {
{
name: map + " (custom)", name: map + " (custom)",
value: map, value: map
}, }
]); ]);
let selectableGroups = $derived([ let selectableGroups = $derived([{
{ name: "None",
name: "None", value: ""
value: "", }, {
}, value: groupSearch,
{ name: `Create: '${groupSearch}'`
value: groupSearch, }, ...$groups.map(group => {
name: `Create: '${groupSearch}'`, return {
}, name: group,
...$groups value: group
.map((group) => { };
return { }).sort((a, b) => a.name.localeCompare(b.name))]);
name: group,
value: group,
};
})
.sort((a, b) => a.name.localeCompare(b.name)),
]);
</script> </script>
<div class="m-2"> <div class="m-2">
@@ -127,29 +107,32 @@
</div> </div>
<div class="mt-4"> <div class="mt-4">
<Label for="fight-start">Start</Label> <Label for="fight-start">Start</Label>
<Input id="fight-start" bind:value={start}> <Input id="fight-start" bind:value={start} >
{#snippet children({ props })} {#snippet children({ props })}
<input type="datetime-local" {...props} bind:value={start} /> <input type="datetime-local" {...props} bind:value={start}/>
{/snippet} {/snippet}
</Input> </Input>
</div> </div>
<div class="m-2"> <div class="m-2">
<Label for="fight-gamemode">Gamemode</Label> <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>
<div class="m-2"> <div class="m-2">
<Label for="fight-maps">Map</Label> <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>
<div class="m-2"> <div class="m-2">
<Label for="fight-port">Spectate Port</Label> <Label for="fight-port">Spectate Port</Label>
<Input id="fight-port" bind:value={spectatePort}> <Input id="fight-port" bind:value={spectatePort} >
{#snippet children({ props })} {#snippet children({ props })}
<input type="number" inputmode="numeric" {...props} bind:value={spectatePort} /> <input type="number" inputmode="numeric" {...props} bind:value={spectatePort}/>
{/snippet} {/snippet}
</Input> </Input>
</div> </div>
<div class="m-2"> <div class="m-2">
<Label for="fight-kampf">Group</Label> <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> </div>
+2 -2
View File
@@ -168,11 +168,11 @@
</div> </div>
<div> <div>
{#if selectedBranch !== "master"} {#if selectedBranch !== "master"}
<Button onclick={() => createFile()} color="alternative" disabled={!selectedPath}>Create File <Button onclick={createFile} color="alternative" disabled={!selectedPath}>Create File
</Button> </Button>
<Button onclick={() => deleteBranch(false)} color="none">Delete Branch</Button> <Button onclick={() => deleteBranch(false)} color="none">Delete Branch</Button>
{:else} {:else}
<Button onclick={() => createBranch()}>Create Branch</Button> <Button onclick={createBranch}>Create Branch</Button>
{/if} {/if}
</div> </div>
</div> </div>
+29 -27
View File
@@ -18,18 +18,19 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Navbar, NavBrand, Spinner, TabItem, Tabs } from "flowbite-svelte"; import {Navbar, NavBrand, Spinner, TabItem, Tabs} from "flowbite-svelte";
import EventEdit from "./event/EventEdit.svelte"; import EventEdit from "./event/EventEdit.svelte";
import { ArrowLeftOutline } from "flowbite-svelte-icons"; import {ArrowLeftOutline} from "flowbite-svelte-icons";
import FightList from "./event/FightList.svelte";
import TeamList from "./event/TeamList.svelte"; import TeamList from "./event/TeamList.svelte";
import { eventRepo } from "@repo/event.ts"; import {eventRepo} from "@repo/event.ts";
import RefereesList from "@components/admin/pages/event/RefereesList.svelte"; import RefereesList from "@components/admin/pages/event/RefereesList.svelte";
interface Props { interface Props {
params: { id: number }; params: { id: number };
} }
let { params }: Props = $props(); let { params }: Props = $props();
let id = params.id; let id = params.id;
let event = $eventRepo.getEvent(id.toString()); let event = $eventRepo.getEvent(id.toString());
@@ -37,43 +38,44 @@
{#await event} {#await event}
<div class="h-screen w-screen grid place-items-center"> <div class="h-screen w-screen grid place-items-center">
<Spinner size={16} /> <Spinner size={16}/>
</div> </div>
{:then data} {:then data}
<Navbar> <Navbar >
{#snippet children({ hidden, toggle })} {#snippet children({ hidden, toggle })}
<NavBrand href="#"> <NavBrand href="#">
<ArrowLeftOutline></ArrowLeftOutline> <ArrowLeftOutline></ArrowLeftOutline>
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white"> <span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white">
{data.event.name} {data.event.name}
</span> </span>
</NavBrand> </NavBrand>
{/snippet} {/snippet}
</Navbar> </Navbar>
<Tabs style="pill" class="mx-4 flex shadow-lg border-b-2 border-gray-700 pb-2" contentClass=""> <Tabs style="pill" class="mx-4 flex shadow-lg border-b-2 border-gray-700 pb-2" contentClass="">
<TabItem open> <TabItem open>
{#snippet title()} {#snippet title()}
<span>Event</span> <span >Event</span>
{/snippet} {/snippet}
<EventEdit {data} /> <EventEdit {data}/>
</TabItem> </TabItem>
<TabItem> <TabItem>
{#snippet title()} {#snippet title()}
<span>Teams</span> <span >Teams</span>
{/snippet} {/snippet}
<TeamList {data} /> <TeamList {data}/>
</TabItem> </TabItem>
<TabItem> <TabItem>
{#snippet title()} {#snippet title()}
<span>Schiedsrichter</span> <span >Schiedsrichter</span>
{/snippet} {/snippet}
<RefereesList {data} /> <RefereesList {data}/>
</TabItem> </TabItem>
<TabItem> <TabItem>
{#snippet title()} {#snippet title()}
<span>Kämpfe</span> <span >Kämpfe</span>
{/snippet} {/snippet}
<FightList {data}/>
</TabItem> </TabItem>
</Tabs> </Tabs>
{:catch error} {:catch error}
-1
View File
@@ -38,7 +38,6 @@
</NavBrand> </NavBrand>
<NavHamburger onclick={toggle}/> <NavHamburger onclick={toggle}/>
<NavUl {hidden}> <NavUl {hidden}>
<NavLi href="/admin/new">New UI</NavLi>
<NavLi href="#/edit">Edit Pages</NavLi> <NavLi href="#/edit">Edit Pages</NavLi>
<NavLi href="#/perms">Permissions</NavLi> <NavLi href="#/perms">Permissions</NavLi>
</NavUl> </NavUl>
+51 -56
View File
@@ -18,37 +18,21 @@
--> -->
<script lang="ts"> <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 {Button, Card, Checkbox, Input, Label, Navbar, NavBrand, Radio, Spinner} from "flowbite-svelte";
import { ArrowLeftOutline } from "flowbite-svelte-icons"; import {ArrowLeftOutline} from "flowbite-svelte-icons";
import { capitalize } from "../util.ts"; import {players} from "@stores/stores.ts";
import { permsRepo } from "@repo/perms.ts"; import {capitalize} from "../util.ts";
import { me } from "@stores/me.ts"; import {permsRepo} from "@repo/perms.ts";
import {me} from "@stores/me.ts";
import SWButton from "@components/styled/SWButton.svelte"; import SWButton from "@components/styled/SWButton.svelte";
import SWModal from "@components/styled/SWModal.svelte"; import SWModal from "@components/styled/SWModal.svelte";
import { userRepo } from "@repo/user.ts"; import {userRepo} from "@repo/user.ts";
import { dataRepo } from "@repo/data.ts";
import type { Player } from "@type/data";
let search = $state(""); 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 selectedPlayer: string | null = $state(null);
let selectedPlayerName: string = $state("");
let playerPerms = $state(loadPlayer(selectedPlayer)); let playerPerms = $state(loadPlayer(selectedPlayer));
let prefixEdit = $state("PREFIX_NONE"); let prefixEdit = $state("PREFIX_NONE");
@@ -62,7 +46,7 @@
if (!id) { if (!id) {
return; return;
} }
return $permsRepo.getPerms(id).then((value) => { return $permsRepo.getPerms(id).then(value => {
activePerms = value.perms; activePerms = value.perms;
prefixEdit = value.prefix.name; prefixEdit = value.prefix.name;
return value; return value;
@@ -72,7 +56,7 @@
function togglePerm(perm: string) { function togglePerm(perm: string) {
return () => { return () => {
if (activePerms.includes(perm)) { if (activePerms.includes(perm)) {
activePerms = activePerms.filter((value) => value !== perm); activePerms = activePerms.filter(value => value !== perm);
} else { } else {
activePerms = [...activePerms, perm]; activePerms = [...activePerms, perm];
} }
@@ -80,7 +64,7 @@
} }
function save() { function save() {
playerPerms!.then(async (perms) => { playerPerms!.then(async perms => {
if (perms.prefix.name != prefixEdit) { if (perms.prefix.name != prefixEdit) {
await $permsRepo.setPrefix(selectedPlayer!, prefixEdit); await $permsRepo.setPrefix(selectedPlayer!, prefixEdit);
} }
@@ -115,20 +99,24 @@
resetPasswordRepeat = ""; resetPasswordRepeat = "";
resetPasswordModal = false; 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(() => { run(() => {
playerPerms = loadPlayer(selectedPlayer); playerPerms = loadPlayer(selectedPlayer);
}); });
</script> </script>
<div class="flex flex-col h-screen overflow-hidden"> <div class="flex flex-col h-screen overflow-hidden">
<Navbar> <Navbar >
{#snippet children({ hidden, toggle })} {#snippet children({ hidden, toggle })}
<NavBrand href="#"> <NavBrand href="#">
<ArrowLeftOutline></ArrowLeftOutline> <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">
</NavBrand> Permissions
{/snippet} </span>
</NavBrand>
{/snippet}
</Navbar> </Navbar>
<div class="p-4 flex-1 overflow-hidden"> <div class="p-4 flex-1 overflow-hidden">
@@ -136,19 +124,14 @@
<Card class="h-full flex flex-col overflow-hidden !max-w-full"> <Card class="h-full flex flex-col overflow-hidden !max-w-full">
<div class="border-b border-b-gray-600 pb-2"> <div class="border-b border-b-gray-600 pb-2">
<Label for="user_search" class="mb-2">Search Users...</Label> <Label for="user_search" class="mb-2">Search Users...</Label>
<Input type="text" id="user_search" placeholder="Name..." bind:value={search} /> <Input type="text" id="user_search" placeholder="Name..." bind:value={search}/>
</div> </div>
{#if playersList.length < 100} {#if filteredPlayers.length < 100}
<ul class="flex-1 overflow-scroll"> <ul class="flex-1 overflow-scroll">
{#each playersList as player (player.uuid)} {#each filteredPlayers as player (player.uuid)}
<li <li class="p-4 transition-colors hover:bg-gray-700 cursor-pointer"
class="p-4 transition-colors hover:bg-gray-700 cursor-pointer"
class:text-orange-500={player.uuid === selectedPlayer} class:text-orange-500={player.uuid === selectedPlayer}
onclick={preventDefault(() => { onclick={preventDefault(() => selectedPlayer = player.uuid)}>
selectedPlayer = player.uuid;
selectedPlayerName = player.name;
})}
>
{player.name} {player.name}
</li> </li>
{/each} {/each}
@@ -157,7 +140,7 @@
</Card> </Card>
<Card class="!max-w-full" style="grid-column: 2/4"> <Card class="!max-w-full" style="grid-column: 2/4">
{#if selectedPlayer} {#if selectedPlayer}
<h1 class="text-3xl">{selectedPlayerName}</h1> <h1 class="text-3xl">{player.name}</h1>
{#await permsFuture} {#await permsFuture}
<Spinner></Spinner> <Spinner></Spinner>
{:then perms} {:then perms}
@@ -166,27 +149,39 @@
{:then player} {:then player}
<h1>Prefix</h1> <h1>Prefix</h1>
{#each Object.entries(perms.prefixes) as [key, prefix]} {#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} {/each}
<h1>Permissions</h1> <h1>Permissions</h1>
{#each perms.perms as perm} {#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} {/each}
<div class="mt-4"> <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")} {#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"> <SWModal bind:open={resetPasswordModal} title="Reset Password">
<Label for="new_password">New Password</Label> <Label for="new_password">New Password</Label>
<Input type="password" id="new_password" placeholder="New Password" bind:value={resetPassword} /> <Input type="password" id="new_password" placeholder="New Password" bind:value={resetPassword}/>
<Label for="repeat_password">Repeat Password</Label> <Label for="repeat_password">Repeat Password</Label>
<Input type="password" id="repeat_password" placeholder="Repeat Password" bind:value={resetPasswordRepeat} /> <Input type="password" id="repeat_password" placeholder="Repeat Password" bind:value={resetPasswordRepeat}/>
{#snippet footer()} {#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}>
{/snippet} Cancel
</Button>
<Button disabled={resetPassword === "" || resetPassword !== resetPasswordRepeat} onclick={resetPW}>
Reset Password
</Button>
{/snippet}
</SWModal> </SWModal>
{/if} {/if}
</div> </div>
+34 -30
View File
@@ -18,22 +18,23 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Spinner, Toolbar, ToolbarButton, ToolbarGroup } from "flowbite-svelte"; import {Spinner, Toolbar, ToolbarButton, ToolbarGroup} from "flowbite-svelte";
import { json } from "@codemirror/lang-json"; import {json} from "@codemirror/lang-json";
import { base64ToBytes } from "../../util.ts"; import CodeMirror from "svelte-codemirror-editor";
import type { Page } from "@type/page.ts"; import {base64ToBytes} from "../../util.ts";
import { materialDark } from "@ddietr/codemirror-themes/material-dark"; import type {Page} from "@type/page.ts";
import { createEventDispatcher } from "svelte"; import {materialDark} from "@ddietr/codemirror-themes/material-dark";
import {createEventDispatcher} from "svelte";
import MDEMarkdownEditor from "./MDEMarkdownEditor.svelte"; import MDEMarkdownEditor from "./MDEMarkdownEditor.svelte";
import { pageRepo } from "@repo/page.ts"; import {pageRepo} from "@repo/page.ts";
interface Props { interface Props {
pageId: number; pageId: number;
branch: string; branch: string;
dirty?: boolean; dirty?: boolean;
} }
let { pageId, branch = $bindable(), dirty = $bindable(false) }: Props = $props(); let { pageId, branch, dirty = $bindable(false) }: Props = $props();
let dispatcher = createEventDispatcher(); let dispatcher = createEventDispatcher();
@@ -70,32 +71,35 @@
} }
let pageFuture = $derived($pageRepo.getPage(pageId, branch).then(getPage)); let pageFuture = $derived($pageRepo.getPage(pageId, branch).then(getPage));
</script> </script>
<svelte:window onbeforeunload={() => {
<svelte:window if (dirty) {
onbeforeunload={() => { return "You have unsaved changes. Are you sure you want to leave?";
if (dirty) { }
return "You have unsaved changes. Are you sure you want to leave?"; }}/>
}
}}
/>
{#await pageFuture} {#await pageFuture}
<Spinner /> <Spinner/>
{:then p} {:then p}
<div> <div>
<div> <div>
<Toolbar class="!bg-gray-900"> <Toolbar class="!bg-gray-900">
{#snippet end()} {#snippet end()}
<ToolbarGroup> <ToolbarGroup >
<ToolbarButton onclick={deletePage}>Delete</ToolbarButton> <ToolbarButton onclick={deletePage}>
<ToolbarButton color="primary" onclick={savePage}>Save</ToolbarButton> Delete
</ToolbarGroup> </ToolbarButton>
{/snippet} <ToolbarButton color="primary" onclick={savePage}>
Save
</ToolbarButton>
</ToolbarGroup>
{/snippet}
</Toolbar> </Toolbar>
</div> </div>
{#if page?.name.endsWith("md") || page?.name.endsWith("mdx")} {#if page?.name.endsWith("md") || page?.name.endsWith("mdx")}
<MDEMarkdownEditor bind:value={pageContent} bind:dirty /> <MDEMarkdownEditor bind:value={pageContent} bind:dirty/>
{:else}{/if} {:else}
<CodeMirror bind:value={pageContent} lang={json()} theme={materialDark} on:change={() => dirty = true}/>
{/if}
</div> </div>
{:catch error} {:catch error}
<p>{error.message}</p> <p>{error.message}</p>
{/await} {/await}
@@ -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>
@@ -18,19 +18,20 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { ExtendedEvent } from "@type/event.ts"; import type {ExtendedEvent} from "@type/event.ts";
import { Button } from "flowbite-svelte"; import {Button} from "flowbite-svelte";
import { PlusOutline } from "flowbite-svelte-icons"; import {PlusOutline} from "flowbite-svelte-icons";
import SWModal from "@components/styled/SWModal.svelte"; import SWModal from "@components/styled/SWModal.svelte";
import SWButton from "@components/styled/SWButton.svelte"; import SWButton from "@components/styled/SWButton.svelte";
import PlayerSelector from "@components/ui/PlayerSelector.svelte"; import TypeAheadSearch from "@components/admin/components/TypeAheadSearch.svelte";
import { eventRepo } from "@repo/event.ts"; import {players} from "@stores/stores.ts";
import {eventRepo} from "@repo/event.ts";
interface Props { interface Props {
data: ExtendedEvent; data: ExtendedEvent;
} }
let { data }: Props = $props(); let { data }: Props = $props();
let searchValue = $state(""); let searchValue = $state("");
let selectedPlayer: string | null = $state(null); let selectedPlayer: string | null = $state(null);
@@ -41,19 +42,17 @@
async function addReferee() { async function addReferee() {
if (selectedPlayer) { if (selectedPlayer) {
referees = ( referees = (await $eventRepo.updateEvent(data.event.id.toString(), {
await $eventRepo.updateEvent(data.event.id.toString(), { deadline: null,
deadline: null, end: null,
end: null, maxTeamMembers: null,
maxTeamMembers: null, name: null,
name: null, publicSchemsOnly: null,
publicSchemsOnly: null, removeReferee: null,
removeReferee: null, schemType: null,
schemType: null, start: null,
start: null, addReferee: [selectedPlayer]
addReferee: [selectedPlayer], })).referees;
})
).referees;
} }
reset(); reset();
@@ -61,20 +60,18 @@
function removeReferee(id: string) { function removeReferee(id: string) {
return async () => { return async () => {
referees = ( referees = (await $eventRepo.updateEvent(data.event.id.toString(), {
await $eventRepo.updateEvent(data.event.id.toString(), { deadline: null,
deadline: null, end: null,
end: null, maxTeamMembers: null,
maxTeamMembers: null, name: null,
name: null, publicSchemsOnly: null,
publicSchemsOnly: null, addReferee: null,
addReferee: null, schemType: null,
schemType: null, start: null,
start: null, removeReferee: [id],
removeReferee: [id], })).referees;
}) }
).referees;
};
} }
function reset() { function reset() {
@@ -87,7 +84,9 @@
{#each referees as referee} {#each referees as referee}
<li class="flex flex-grow justify-between"> <li class="flex flex-grow justify-between">
{referee.name} {referee.name}
<SWButton onclick={removeReferee(referee.uuid)}>Entfernen</SWButton> <SWButton onclick={removeReferee(referee.uuid)}>
Entfernen
</SWButton>
</li> </li>
{/each} {/each}
@@ -96,26 +95,27 @@
{/if} {/if}
</ul> </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 /> <PlusOutline/>
</Button> </Button>
<SWModal title="Schiedsrichter hinzufügen" bind:open={showAdd}> <SWModal title="Schiedsrichter hinzufügen" bind:open={showAdd}>
<div class="flex flex-grow justify-center h-80"> <div class="flex flex-grow justify-center h-80">
<div> <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>
</div> </div>
{#snippet footer()} {#snippet footer()}
<div class="flex flex-grow justify-end"> <div class="flex flex-grow justify-end">
<SWButton onclick={reset} type="gray">Abbrechen</SWButton> <SWButton onclick={reset} type="gray">Abbrechen</SWButton>
<SWButton onclick={addReferee}>Hinzufügen</SWButton> <SWButton onclick={addReferee}>Hinzufügen</SWButton>
</div> </div>
{/snippet} {/snippet}
</SWModal> </SWModal>
<style> <style>
li { li {
padding-block: 0.5rem; @apply py-2;
} }
</style> </style>
@@ -18,14 +18,12 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Card } from "@components/ui/card"; import {createEventDispatcher} from "svelte";
interface Props {
children?: import('svelte').Snippet;
}
interface Props { let { children }: Props = $props();
children?: import("svelte").Snippet;
ondrop: (event: DragEvent) => void;
}
let { children, ondrop }: Props = $props();
let dragover = $state(false); let dragover = $state(false);
@@ -34,16 +32,19 @@
dragover = true; dragover = true;
} }
const dispatch = createEventDispatcher();
function handleDrop(ev: DragEvent) { function handleDrop(ev: DragEvent) {
ev.preventDefault(); ev.preventDefault();
dragover = false; dragover = false;
ondrop(ev); dispatch("drop", ev);
} }
</script> </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?.()} {@render children?.()}
</Card> </div>
<style> <style>
div { div {
@@ -18,28 +18,28 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { Team } from "@type/team.ts"; import { createBubbler } from 'svelte/legacy';
import { brightness, colorFromTeam, lighten } from "../../util";
interface Props { const bubble = createBubbler();
team: Team; import type {Team} from "@type/team.ts";
ondragstart: (event: DragEvent) => void; import {brightness, colorFromTeam, lighten} from "../../util";
}
let { team, ondragstart }: Props = $props(); interface Props {
team: Team;
}
let { team }: Props = $props();
let hover = $state(false); let hover = $state(false);
</script> </script>
<div <div class="rounded w-fit p-2 border-gray-600 border cursor-grab select-none m-1 flex place-items-center"
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)}
style:background-color={hover ? lighten(colorFromTeam(team)) : colorFromTeam(team)} class:text-black={brightness(colorFromTeam(team))} draggable="true"
class:text-black={brightness(colorFromTeam(team))} ondragstart={bubble('dragstart')}
draggable="true" onmouseenter={() => hover = true}
{ondragstart} onmouseleave={() => hover = false}
onmouseenter={() => (hover = true)} role="figure">
onmouseleave={() => (hover = false)}
role="figure"
>
<span>{team.name}</span> <span>{team.name}</span>
</div> </div>
@@ -18,7 +18,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import {astroI18n, t} from "$lib/i18n"; import {astroI18n, t} from "astro-i18n";
import {CheckSolid, XCircleOutline} from "flowbite-svelte-icons"; import {CheckSolid, XCircleOutline} from "flowbite-svelte-icons";
import type {SchematicInfo} from "@type/schem.ts"; import type {SchematicInfo} from "@type/schem.ts";
import {createEventDispatcher} from "svelte"; import {createEventDispatcher} from "svelte";
+6 -23
View File
@@ -20,7 +20,7 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy'; import { preventDefault } from 'svelte/legacy';
import {t} from "$lib/i18n"; import {t} from "astro-i18n";
import { import {
ChevronDoubleRightOutline, ChevronDoubleRightOutline,
FolderOutline, FolderOutline,
@@ -143,32 +143,15 @@
<style lang="postcss"> <style lang="postcss">
table { table {
width: 100%; @apply w-full;
} }
tr { tr {
cursor: pointer; @apply transition-colors cursor-pointer border-b
border-bottom: 1px solid #e5e7eb; dark:hover:bg-gray-800 hover:bg-gray-300;
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;
} }
th { th {
padding-block: 1rem; @apply text-left py-4 md:px-2;
text-align: left;
} }
</style>
@media (min-width: 768px) {
th {
padding-inline: 0.5rem;
}
}
</style>
@@ -21,7 +21,7 @@
import { createBubbler, preventDefault } from 'svelte/legacy'; import { createBubbler, preventDefault } from 'svelte/legacy';
const bubble = createBubbler(); 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 {CheckSolid, FileOutline, FolderOutline, XCircleOutline} from "flowbite-svelte-icons";
import type {Schematic} from "@type/schem.ts"; import type {Schematic} from "@type/schem.ts";
import type {Player} from "@type/data.ts"; import type {Player} from "@type/data.ts";
@@ -82,32 +82,11 @@
<style lang="scss"> <style lang="scss">
tr { tr {
cursor: pointer; @apply transition-colors cursor-pointer border-b border-gray-300
border-bottom: 1px solid #d1d5db; dark:hover:bg-gray-800 hover:bg-gray-300 dark:border-neutral-700;
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;
} }
th { th {
padding-block: 1rem; @apply text-left py-4 md:px-2;
text-align: left;
} }
</style>
@media (min-width: 768px) {
th {
padding-inline: 0.5rem;
}
}
</style>
+6 -3
View File
@@ -19,7 +19,7 @@
<script lang="ts"> <script lang="ts">
import type {Player} from "@type/data.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"; import {statsRepo} from "@repo/stats.ts";
interface Props { interface Props {
@@ -31,7 +31,7 @@
let request = getRequest(); let request = getRequest();
function getRequest() { function getRequest() {
return $statsRepo.getUserStats(user.uuid) return $statsRepo.getUserStats(user.id)
} }
</script> </script>
@@ -43,5 +43,8 @@
maximumFractionDigits: 2 maximumFractionDigits: 2
}).format(data.playtime)})}h</p> }).format(data.playtime)})}h</p>
<p>{t("dashboard.stats.fights", {fights: data.fights})}</p> <p>{t("dashboard.stats.fights", {fights: data.fights})}</p>
<p>{t("dashboard.stats.checked", {checked: data.acceptedSchematics})}</p> {#if user.perms.includes("CHECK")}
<p>{t("dashboard.stats.checked", {checked: data.acceptedSchematics})}</p>
{/if}
{:catch error}
{/await} {/await}
+19 -35
View File
@@ -21,21 +21,19 @@
import {createEventDispatcher} from "svelte"; import {createEventDispatcher} from "svelte";
import {schemRepo} from "@repo/schem.ts"; import {schemRepo} from "@repo/schem.ts";
import SWModal from "@components/styled/SWModal.svelte"; import SWModal from "@components/styled/SWModal.svelte";
import {t} from "$lib/i18n"; import {t} from "astro-i18n"
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
interface Props { interface Props {
open?: boolean; open?: boolean;
} }
let {open = $bindable(false)}: Props = $props(); let { open = $bindable(false) }: Props = $props();
async function upload(e: Event) { async function upload() {
e.stopPropagation();
if (uploadFile == null) { if (uploadFile == null) {
error = "dashboard.schematic.errors.noFile"; return
return;
} }
let file = uploadFile[0]; let file = uploadFile[0];
@@ -44,46 +42,32 @@
let type = name.split(".").pop(); let type = name.split(".").pop();
if (type !== "schem" && type !== "schematic") { if (type !== "schem" && type !== "schematic") {
error = "dashboard.schematic.errors.invalidEnding"; return
return;
} }
let content = await file.arrayBuffer(); let content = await file.arrayBuffer();
// @ts-ignore
let b64 = btoa(String.fromCharCode.apply(null, new Uint8Array(content))); let b64 = btoa(String.fromCharCode.apply(null, new Uint8Array(content)));
try { await $schemRepo.uploadSchematic(name, b64);
await $schemRepo.uploadSchematic(name, b64);
open = false; open = false;
value = ""; uploadFile = null;
dispatch("reset"); dispatch("reset")
} catch (e) {
error = "dashboard.schematic.errors.upload";
}
}
function reset(e: Event) {
e.stopPropagation();
open = false
value = "";
} }
let uploadFile: FileList | null = $state(null); let uploadFile: FileList | null = $state(null);
let value = $state("");
let error = $state(null)
</script> </script>
<SWModal title={t("dashboard.schematic.title")} bind:open> <SWModal title={t("dashboard.schematic.title")} bind:open>
<form> <form>
<label for="schem-upload">{t("dashboard.schematic.title")}</label> <input type="file" bind:files={uploadFile} />
<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}
</form> </form>
{#snippet footer()} {#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>
{/snippet} <button class="btn btn-gray" onclick={() => open = false}>{t("dashboard.schematic.cancel")}</button>
{/snippet}
</SWModal> </SWModal>
+18 -23
View File
@@ -18,23 +18,24 @@
--> -->
<script lang="ts"> <script lang="ts">
import { t } from "$lib/i18n"; import {t} from "astro-i18n";
import type { Player } from "@type/data.ts"; import type {Player} from "@type/data.ts";
import { l } from "@utils/util.ts"; import {l} from "@utils/util.ts";
import Statistics from "./Statistics.svelte"; import Statistics from "./Statistics.svelte";
import { authV2Repo } from "@repo/authv2.ts"; import {authRepo} from "@repo/auth.ts";
import {tokenStore} from "@repo/repo.ts";
import Card from "@components/Card.svelte"; import Card from "@components/Card.svelte";
import { navigate } from "astro:transitions/client";
interface Props { interface Props {
user: Player; user: Player;
} }
let { user }: Props = $props(); let { user }: Props = $props();
async function logout() { async function logout() {
await $authV2Repo.logout(); await $authRepo.logout()
await navigate(l("/login")); tokenStore.set("")
window.location.href = l("/login")
} }
</script> </script>
@@ -43,25 +44,19 @@
<Card> <Card>
<figure> <figure>
<figcaption class="text-center mb-4 text-2xl">{user.name}</figcaption> <figcaption class="text-center mb-4 text-2xl">{user.name}</figcaption>
<img <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" />
src={`${import.meta.env.PUBLIC_API_SERVER}/data/skin/${user.uuid}`}
class="transition duration-300 ease-in-out hover:scale-110 hover:drop-shadow-2xl"
alt={user.name + "s bust"}
width="150"
height="150"
/>
</figure> </figure>
</Card> </Card>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap">
<button class="btn mt-2" onclick={logout}>{t("dashboard.buttons.logout")}</button> <button class="btn mt-2" onclick={logout}>{t("dashboard.buttons.logout")}</button>
{#if user.perms.includes("MODERATION")} {#if user.perms.includes("MODERATION")}
<a class="btn w-fit mt-2" href="/admin/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} {/if}
</div> </div>
</div> </div>
<div> <div>
<h1 class="text-4xl font-bold">{t("dashboard.title", { name: user.name })}</h1> <h1 class="text-4xl font-bold">{t("dashboard.title", {name: user.name})}</h1>
<p>{t("dashboard.rank", { rank: t("home.prefix." + (user.prefix || "User")) })}</p> <p>{t("dashboard.rank", {rank: t("home.prefix." + user.prefix)})}</p>
<Statistics {user} /> <Statistics {user} />
</div> </div>
</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