Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d100fcafc | |||
| 82f5ab48b8 |
@@ -37,6 +37,10 @@
|
|||||||
"error",
|
"error",
|
||||||
4
|
4
|
||||||
],
|
],
|
||||||
|
"linebreak-style": [
|
||||||
|
"error",
|
||||||
|
"unix"
|
||||||
|
],
|
||||||
"quotes": [
|
"quotes": [
|
||||||
"error",
|
"error",
|
||||||
"double"
|
"double"
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { defineAstroI18nConfig } from "astro-i18n";
|
||||||
|
|
||||||
|
export default defineAstroI18nConfig({
|
||||||
|
primaryLocale: "de",
|
||||||
|
secondaryLocales: ["en"],
|
||||||
|
fallbackLocale: "de",
|
||||||
|
trailingSlash: "never",
|
||||||
|
run: "client+server",
|
||||||
|
showPrimaryLocale: false,
|
||||||
|
translationLoadingRules: [],
|
||||||
|
translationDirectory: {},
|
||||||
|
translations: {},
|
||||||
|
routes: {
|
||||||
|
en: {
|
||||||
|
"jetzt-spielen": "join",
|
||||||
|
impressum: "imprint",
|
||||||
|
verhaltensrichtlinien: "code-of-conduct",
|
||||||
|
regeln: "rules",
|
||||||
|
rangliste: "ranked",
|
||||||
|
"haeufige-fragen": "faq",
|
||||||
|
statistiken: "stats",
|
||||||
|
ankuendigungen: "announcements",
|
||||||
|
datenschutzerklaerung: "privacy-policy",
|
||||||
|
"passwort-setzen": "set-password",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
+12
-103
@@ -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,7 +69,6 @@ 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"),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+7205
-5956
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -4,6 +4,6 @@ module.exports = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
require('autoprefixer'),
|
require('autoprefixer'),
|
||||||
require('cssnano'),
|
require('cssnano'),
|
||||||
require("postcss-nesting"),
|
require("tailwindcss/nesting"),
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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 |
@@ -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,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"
|
|
||||||
/>
|
|
||||||
|
|||||||
+23
-33
@@ -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>
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
+22
-88
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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>
|
</style>
|
||||||
+134
-134
@@ -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>
|
||||||
|
|||||||
@@ -1,54 +1,49 @@
|
|||||||
---
|
---
|
||||||
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>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,31 +71,34 @@
|
|||||||
}
|
}
|
||||||
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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
th {
|
|
||||||
padding-inline: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
th {
|
|
||||||
padding-inline: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -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}
|
||||||
@@ -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 +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>
|
||||||
@@ -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}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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}
|
|
||||||
@@ -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}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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}
|
|
||||||
@@ -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}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
Reference in New Issue
Block a user