Compare commits
20 Commits
a16d663fa3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| befc69cefc | |||
| cdc2ae2432 | |||
| 69527cf2e9 | |||
| bca2d6e85e | |||
| 53886332f0 | |||
| 1011ab1245 | |||
|
167992769e
|
|||
|
413c84e293
|
|||
|
4c434f8511
|
|||
|
038c2768e6
|
|||
|
a9260b1ca0
|
|||
|
9717946784
|
|||
|
46ad3599e8
|
|||
| e27269ec8b | |||
| 388492cd21 | |||
|
b98f197d14
|
|||
| fc53376c73 | |||
| ae5d232c54 | |||
| 08e2e37737 | |||
|
3b7aafd56e
|
@@ -0,0 +1,88 @@
|
|||||||
|
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/"
|
||||||
+1
-3
@@ -19,11 +19,9 @@ 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
|
||||||
pnpm-lock.yaml
|
|
||||||
|
|||||||
@@ -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/)
|
||||||
- [astro-i18n](https://github.com/Alexandre-Fernandez/astro-i18n)
|
- [Paraglide JS](https://inlang.com/m/gerre34r/library-inlang-paraglideJs)
|
||||||
- [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-sync
|
### i18n Compile
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run i18n:sync
|
pnpm run i18n:compile
|
||||||
```
|
```
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import type { AstroIntegration } from "astro";
|
|
||||||
import { mkdir, access, constants, copyFile, rename } from "node:fs/promises";
|
|
||||||
|
|
||||||
const locales = [
|
|
||||||
"en",
|
|
||||||
"de",
|
|
||||||
];
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
+74
-8
@@ -1,8 +1,8 @@
|
|||||||
import { defineConfig, sharpImageService } from "astro/config";
|
import { defineConfig, fontProviders, sharpImageService } from "astro/config";
|
||||||
import svelte from "@astrojs/svelte";
|
import svelte from "@astrojs/svelte";
|
||||||
import tailwind from "@astrojs/tailwind";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
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 path from "node:path";
|
import path from "node:path";
|
||||||
@@ -12,7 +12,68 @@ 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(),
|
||||||
},
|
},
|
||||||
@@ -43,13 +104,9 @@ export default defineConfig({
|
|||||||
baseUrl: "https://git.steamwar.de/SteamWar/Website/src/branch/master/",
|
baseUrl: "https://git.steamwar.de/SteamWar/Website/src/branch/master/",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
tailwind({
|
|
||||||
configFile: "./tailwind.config.js",
|
|
||||||
applyBaseStyles: false,
|
|
||||||
}),
|
|
||||||
sitemap({
|
sitemap({
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: "en",
|
defaultLocale: "de",
|
||||||
locales: {
|
locales: {
|
||||||
en: "en-US",
|
en: "en-US",
|
||||||
de: "de-DE",
|
de: "de-DE",
|
||||||
@@ -82,6 +139,15 @@ export default defineConfig({
|
|||||||
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"),
|
||||||
|
|||||||
+58
-65
@@ -3,87 +3,80 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "pnpm run i18n:generate:pages && astro dev",
|
||||||
"start": "astro dev",
|
"start": "pnpm run dev",
|
||||||
"build": "astro build",
|
"build": "pnpm run i18n:generate:pages && astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"i18n:extract": "astro-i18n extract",
|
"i18n:generate:pages": "node scripts/generate-en-pages.mjs",
|
||||||
"i18n:generate:pages": "astro-i18n generate:pages --purge",
|
"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:types": "astro-i18n generate:types",
|
|
||||||
"i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types",
|
|
||||||
"clean:dist": "rm -rf dist",
|
"clean:dist": "rm -rf dist",
|
||||||
"clean:node_modules": "rm -rf node_modules",
|
"clean:node_modules": "rm -rf node_modules",
|
||||||
"ci": "pnpm install && pnpm run i18n:sync && pnpm run build"
|
"ci": "pnpm install && pnpm run build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/svelte": "^7.1.0",
|
"@astrojs/svelte": "^8.1.1",
|
||||||
"@astrojs/tailwind": "^5.1.5",
|
"@internationalized/date": "^3.12.1",
|
||||||
"@astropub/icons": "^0.2.0",
|
"@lucide/svelte": "^1.16.0",
|
||||||
"@internationalized/date": "^3.8.1",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
"@lucide/svelte": "^0.488.0",
|
"@types/color": "^4.2.1",
|
||||||
"@types/color": "^4.2.0",
|
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/node": "^22.15.23",
|
"@types/node": "^25.9.0",
|
||||||
"@types/three": "^0.170.0",
|
"@types/three": "^0.184.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.33.0",
|
"@typescript-eslint/eslint-plugin": "^8.59.4",
|
||||||
"@typescript-eslint/parser": "^8.33.0",
|
"@typescript-eslint/parser": "^8.59.4",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.5.0",
|
||||||
"bits-ui": "1.3.4",
|
"bits-ui": "2.18.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk-sv": "^0.0.18",
|
"cssnano": "^8.0.1",
|
||||||
"cssnano": "^7.0.7",
|
|
||||||
"embla-carousel-svelte": "^8.6.0",
|
"embla-carousel-svelte": "^8.6.0",
|
||||||
"esbuild": "^0.24.2",
|
"esbuild": "^0.28.0",
|
||||||
"eslint": "^9.27.0",
|
"eslint": "^10.4.0",
|
||||||
"eslint-plugin-astro": "^1.3.1",
|
"eslint-plugin-astro": "^1.7.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-svelte": "^3.17.1",
|
||||||
"eslint-plugin-svelte": "^2.46.1",
|
"formsnap": "2.0.1",
|
||||||
"formsnap": "1.0.1",
|
"mode-watcher": "^1.1.0",
|
||||||
"lucide-svelte": "^0.476.0",
|
"paneforge": "^1.0.2",
|
||||||
"mode-watcher": "^0.5.1",
|
"postcss-nesting": "^14.0.0",
|
||||||
"paneforge": "^0.0.6",
|
"sass": "^1.99.0",
|
||||||
"postcss-nesting": "^13.0.1",
|
"svelte": "^5.55.8",
|
||||||
"sass": "^1.89.0",
|
"svelte-sonner": "^1.1.1",
|
||||||
"svelte": "^5.33.4",
|
"tailwind-merge": "^3.6.0",
|
||||||
"svelte-sonner": "^0.3.28",
|
"tailwind-variants": "^3.2.2",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwindcss": "^4.3.0",
|
||||||
"tailwind-variants": "^0.3.1",
|
"three": "^0.184.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"typescript": "^6.0.3",
|
||||||
"three": "^0.170.0",
|
"zod": "^4.4.3"
|
||||||
"typescript": "^5.8.3",
|
|
||||||
"vaul-svelte": "^0.3.2",
|
|
||||||
"zod": "^3.25.31"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^4.3.0",
|
"@astrojs/mdx": "^5.0.6",
|
||||||
"@astrojs/sitemap": "^3.4.0",
|
"@astrojs/sitemap": "^3.7.2",
|
||||||
"@astrojs/starlight": "^0.34.4",
|
"@astrojs/starlight": "^0.39.2",
|
||||||
"@astrojs/starlight-tailwind": "^4.0.1",
|
"@astrojs/starlight-tailwind": "^5.0.0",
|
||||||
"@codemirror/commands": "^6.8.1",
|
"@codemirror/commands": "^6.10.3",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
"@codemirror/view": "^6.36.8",
|
"@codemirror/view": "^6.43.0",
|
||||||
"@ddietr/codemirror-themes": "^1.5.1",
|
"@ddietr/codemirror-themes": "^1.5.2",
|
||||||
|
"@inlang/paraglide-js": "^2.18.0",
|
||||||
"@tanstack/table-core": "^8.21.3",
|
"@tanstack/table-core": "^8.21.3",
|
||||||
"astro": "5.7.14",
|
"astro": "6.3.5",
|
||||||
"astro-i18n": "^2.2.4",
|
|
||||||
"astro-robots-txt": "^1.0.0",
|
"astro-robots-txt": "^1.0.0",
|
||||||
"astro-seo": "^0.8.4",
|
"astro-seo": "^1.1.0",
|
||||||
"chart.js": "^4.4.9",
|
"chart.js": "^4.5.1",
|
||||||
"chartjs-adapter-dayjs-4": "^1.0.4",
|
"chartjs-adapter-dayjs-4": "^1.0.4",
|
||||||
"chartjs-adapter-moment": "^1.0.1",
|
"chartjs-adapter-moment": "^1.0.1",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.2",
|
||||||
"color": "^4.2.3",
|
"color": "^5.0.3",
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.20",
|
||||||
"easymde": "^2.20.0",
|
"easymde": "^2.21.0",
|
||||||
"flowbite": "^2.5.2",
|
"flowbite": "^4.0.2",
|
||||||
"flowbite-svelte": "^0.47.4",
|
"flowbite-svelte": "^1.33.1",
|
||||||
"flowbite-svelte-icons": "^2.2.0",
|
"flowbite-svelte-icons": "^3.1.0",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.1",
|
||||||
"qs": "^6.14.0",
|
"qs": "^6.15.2",
|
||||||
"sharp": "^0.33.5",
|
"sharp": "^0.34.5",
|
||||||
"svelte-awesome": "^3.3.5",
|
"svelte-awesome": "^3.3.5",
|
||||||
"svelte-spa-router": "^4.0.1"
|
"svelte-spa-router": "^5.1.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
Generated
+7569
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("tailwindcss/nesting"),
|
require("postcss-nesting"),
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
/* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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');
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -66,14 +66,21 @@
|
|||||||
transition: scale 300ms cubic-bezier(0.2, 3, 0.67, 0.6);
|
transition: scale 300ms cubic-bezier(0.2, 3, 0.67, 0.6);
|
||||||
|
|
||||||
:global(h1) {
|
:global(h1) {
|
||||||
@apply text-xl font-bold mt-4;
|
margin-top: 1rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
font-family: "Barlow Condensed", sans-serif;
|
font-family: "Barlow Condensed", sans-serif;
|
||||||
letter-spacing: 0.06em;
|
letter-spacing: 0.06em;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(svg) {
|
:global(svg) {
|
||||||
color: #f59e0b;
|
color: #f59e0b;
|
||||||
@apply transition-transform duration-300 ease-in-out hover:scale-110;
|
transition: transform 300ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(svg:hover) {
|
||||||
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {t} from "astro-i18n"
|
import {t} from "$lib/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,7 +18,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {t} from "astro-i18n";
|
import {t} from "$lib/i18n";
|
||||||
import {statsRepo} from "@repo/stats.ts";
|
import {statsRepo} from "@repo/stats.ts";
|
||||||
import "@styles/table.css"
|
import "@styles/table.css"
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FightStatsChart from "./FightStatsChart.svelte";
|
import FightStatsChart from "./FightStatsChart.svelte";
|
||||||
import { t } from "astro-i18n";
|
import { t } from "$lib/i18n";
|
||||||
import { statsRepo } from "@repo/stats.ts";
|
import { statsRepo } from "@repo/stats.ts";
|
||||||
|
|
||||||
let request = getStats();
|
let request = getStats();
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { window } from "./utils.ts";
|
import { window } from "./utils.ts";
|
||||||
import { astroI18n, t } from "astro-i18n";
|
import { astroI18n, t } from "$lib/i18n";
|
||||||
import type { EventFight, ExtendedEvent } from "@type/event";
|
import type { EventFight, ExtendedEvent } from "@type/event";
|
||||||
import "@styles/table.css";
|
import "@styles/table.css";
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { window } from "./utils.ts";
|
import { window } from "./utils.ts";
|
||||||
import { t } from "astro-i18n";
|
import { t } from "$lib/i18n";
|
||||||
import type { ExtendedEvent } from "@type/event.ts";
|
import type { ExtendedEvent } from "@type/event.ts";
|
||||||
import "@styles/table.css";
|
import "@styles/table.css";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
import { t } from "astro-i18n";
|
import { t } from "$lib/i18n";
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="border-l-2 border-amber-500 bg-amber-500/5 p-4" role="alert">
|
<div class="border-l-2 border-amber-500 bg-amber-500/5 p-4" role="alert">
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { preventDefault } from "svelte/legacy";
|
import { preventDefault } from "svelte/legacy";
|
||||||
import { l } from "@utils/util.ts";
|
import { l } from "@utils/util.ts";
|
||||||
import { t } from "astro-i18n";
|
import { t } from "$lib/i18n";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import { navigate } from "astro:transitions/client";
|
import { navigate } from "astro:transitions/client";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "../styles/button.css";
|
import "../styles/button.css";
|
||||||
import { CaretDownOutline, GlobeOutline } from "flowbite-svelte-icons";
|
import { CaretDownOutline, GlobeOutline } from "flowbite-svelte-icons";
|
||||||
import { t, l } from "astro-i18n";
|
import { t, l } from "$lib/i18n";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { loggedIn } from "@repo/authv2.ts";
|
import { loggedIn } from "@repo/authv2.ts";
|
||||||
import { astroI18n } from "astro-i18n";
|
import { astroI18n } from "$lib/i18n";
|
||||||
interface Props {
|
interface Props {
|
||||||
logo?: import("svelte").Snippet;
|
logo?: import("svelte").Snippet;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
import type { CollectionEntry } from "astro:content";
|
import type { CollectionEntry } from "astro:content";
|
||||||
import { l } from "../util/util";
|
import { l } from "../util/util";
|
||||||
import { astroI18n } from "astro-i18n";
|
import { astroI18n, stripLocaleFromPath } from "$lib/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";
|
||||||
@@ -19,7 +19,7 @@ const {
|
|||||||
slim: boolean;
|
slim: boolean;
|
||||||
} = Astro.props as Props;
|
} = Astro.props as Props;
|
||||||
|
|
||||||
const postUrl = l(`/announcements/${post.slug.split("/").slice(1).join("/")}`);
|
const postUrl = l(`/announcements/${stripLocaleFromPath(post.id)}`);
|
||||||
---
|
---
|
||||||
|
|
||||||
<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-2 backdrop-blur-xl bg-transparent border-0" : "border-t-2 border-t-amber-500/30"}`} hoverEffect={false}>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t } from "astro-i18n"
|
import { t } from "$lib/i18n"
|
||||||
import {server} from "./stores/stores.ts";
|
import {server} from "./stores/stores.ts";
|
||||||
|
|
||||||
function generateVersionString(version: string): string {
|
function generateVersionString(version: string): string {
|
||||||
|
|||||||
@@ -116,6 +116,6 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
li {
|
li {
|
||||||
@apply py-2;
|
padding-block: 0.5rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {astroI18n, t} from "astro-i18n";
|
import {astroI18n, t} from "$lib/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 "astro-i18n";
|
import {t} from "$lib/i18n";
|
||||||
import {
|
import {
|
||||||
ChevronDoubleRightOutline,
|
ChevronDoubleRightOutline,
|
||||||
FolderOutline,
|
FolderOutline,
|
||||||
@@ -143,15 +143,32 @@
|
|||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
table {
|
table {
|
||||||
@apply w-full;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
@apply transition-colors cursor-pointer border-b
|
cursor: pointer;
|
||||||
dark:hover:bg-gray-800 hover:bg-gray-300;
|
border-bottom: 1px solid #e5e7eb;
|
||||||
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) tr:hover {
|
||||||
|
background-color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
@apply text-left py-4 md:px-2;
|
padding-block: 1rem;
|
||||||
|
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 "astro-i18n";
|
import {astroI18n, t} from "$lib/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,11 +82,32 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
tr {
|
tr {
|
||||||
@apply transition-colors cursor-pointer border-b border-gray-300
|
cursor: pointer;
|
||||||
dark:hover:bg-gray-800 hover:bg-gray-300 dark:border-neutral-700;
|
border-bottom: 1px solid #d1d5db;
|
||||||
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover {
|
||||||
|
background-color: #d1d5db;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) tr {
|
||||||
|
border-bottom-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) tr:hover {
|
||||||
|
background-color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
@apply text-left py-4 md:px-2;
|
padding-block: 1rem;
|
||||||
|
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 "astro-i18n"
|
import {astroI18n, t} from "$lib/i18n"
|
||||||
import {statsRepo} from "@repo/stats.ts";
|
import {statsRepo} from "@repo/stats.ts";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
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 "astro-i18n";
|
import {t} from "$lib/i18n";
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t } from "astro-i18n";
|
import { t } from "$lib/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";
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import "dayjs/locale/de";
|
import "dayjs/locale/de";
|
||||||
import type { ExtendedEvent } from "../types/event";
|
import type { ExtendedEvent } from "../types/event";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-svelte";
|
import { ChevronLeft, ChevronRight } from "@lucide/svelte";
|
||||||
import * as Card from "../ui/card";
|
import * as Card from "../ui/card";
|
||||||
import EventCard from "./EventCard.svelte";
|
import EventCard from "./EventCard.svelte";
|
||||||
import SWButton from "@components/styled/SWButton.svelte";
|
import SWButton from "@components/styled/SWButton.svelte";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { ExtendedEvent } from "../types/event";
|
import type { ExtendedEvent } from "../types/event";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Calendar } from "lucide-svelte";
|
import { Calendar } from "@lucide/svelte";
|
||||||
import { List } from "lucide-svelte";
|
import { List } from "@lucide/svelte";
|
||||||
import EventList from "./EventList.svelte";
|
import EventList from "./EventList.svelte";
|
||||||
import CalendarView from "./Calendar.svelte";
|
import CalendarView from "./Calendar.svelte";
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,10 @@
|
|||||||
config: GroupViewConfig;
|
config: GroupViewConfig;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
// Groups fights into rounds: a round starts at the first fight's start;
|
function detectRounds(fights: EventFight[], groupingTimeMinutes: number): EventFight[][] {
|
||||||
// all fights starting within 10 minutes (600_000 ms) of that are in the same round.
|
|
||||||
function detectRounds(fights: EventFight[]): EventFight[][] {
|
|
||||||
if (!fights || fights.length === 0) return [];
|
if (!fights || fights.length === 0) return [];
|
||||||
|
|
||||||
const TEN_MIN_MS = 10 * 60 * 1000;
|
const groupingTimeMs = Math.max(1, Math.floor(groupingTimeMinutes || 10)) * 60 * 1000;
|
||||||
const sorted = [...fights].sort((a, b) => a.start - b.start);
|
const sorted = [...fights].sort((a, b) => a.start - b.start);
|
||||||
|
|
||||||
const rounds: EventFight[][] = [];
|
const rounds: EventFight[][] = [];
|
||||||
@@ -28,7 +26,7 @@
|
|||||||
let roundStart = sorted[0].start;
|
let roundStart = sorted[0].start;
|
||||||
|
|
||||||
for (const fight of sorted) {
|
for (const fight of sorted) {
|
||||||
if (fight.start - roundStart <= TEN_MIN_MS) {
|
if (fight.start - roundStart <= groupingTimeMs) {
|
||||||
currentRound.push(fight);
|
currentRound.push(fight);
|
||||||
} else {
|
} else {
|
||||||
if (currentRound.length) rounds.push(currentRound);
|
if (currentRound.length) rounds.push(currentRound);
|
||||||
@@ -61,8 +59,9 @@
|
|||||||
{#each config.groups as groupId}
|
{#each config.groups as groupId}
|
||||||
{@const group = event.groups.find((g) => g.id === groupId)!!}
|
{@const group = event.groups.find((g) => g.id === groupId)!!}
|
||||||
{@const fights = event.fights.filter((f) => f.group?.id === groupId)}
|
{@const fights = event.fights.filter((f) => f.group?.id === groupId)}
|
||||||
{@const rounds = detectRounds(fights)}
|
{@const rounds = detectRounds(fights, config.roundGroupingTimeMinutes ?? 10)}
|
||||||
{@const roundRows = config.roundRows ?? 1}
|
{@const roundRows = config.roundRows ?? 1}
|
||||||
|
{@const roundPrefix = config.roundPrefix ?? "Runde"}
|
||||||
{@const roundRowsChunked = chunkIntoRows(rounds, roundRows)}
|
{@const roundRowsChunked = chunkIntoRows(rounds, roundRows)}
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div>
|
<div>
|
||||||
@@ -83,7 +82,7 @@
|
|||||||
{@const roundIndex = rounds.indexOf(round)}
|
{@const roundIndex = rounds.indexOf(round)}
|
||||||
{@const teams = Array.from(new Set(round.flatMap((f) => [f.redTeam, f.blueTeam])))}
|
{@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">
|
<div class="{hover.currentHover && !teams.some((t) => t?.id === hover.currentHover) ? 'opacity-30' : ''} transition-opacity">
|
||||||
<EventCard title="Runde {roundIndex + 1}">
|
<EventCard title={`${roundPrefix} ${roundIndex + 1}`}>
|
||||||
{#each round as fight}
|
{#each round as fight}
|
||||||
<EventFightChip {event} {fight} {group} />
|
<EventFightChip {event} {fight} {group} />
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ export const GroupViewSchema = z.object({
|
|||||||
type: z.literal("GROUP"),
|
type: z.literal("GROUP"),
|
||||||
groups: z.array(z.number()),
|
groups: z.array(z.number()),
|
||||||
roundRows: z.number().int().positive().optional().default(1),
|
roundRows: z.number().int().positive().optional().default(1),
|
||||||
|
roundGroupingTimeMinutes: z.number().int().positive().optional().default(10),
|
||||||
|
roundPrefix: z.enum(["Runde", "Tag"]).optional().default("Runde"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type GroupViewConfig = z.infer<typeof GroupViewSchema>;
|
export type GroupViewConfig = z.infer<typeof GroupViewSchema>;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
import { gamemodes, maps } from "@components/stores/stores";
|
import { gamemodes, maps } from "@components/stores/stores";
|
||||||
import { CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, Command } from "@components/ui/command";
|
import { CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, Command } from "@components/ui/command";
|
||||||
import { ChevronsUpDown, Check } from "lucide-svelte";
|
import { ChevronsUpDown, Check } from "@lucide/svelte";
|
||||||
import { Button } from "@components/ui/button";
|
import { Button } from "@components/ui/button";
|
||||||
import { cn } from "@components/utils";
|
import { cn } from "@components/utils";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import type { GroupUpdateEdit, ResponseGroups, SWEvent } from "@type/event";
|
import type { GroupUpdateEdit, ResponseGroups, SWEvent } from "@type/event";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
import { CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, Command } from "@components/ui/command";
|
import { CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, Command } from "@components/ui/command";
|
||||||
import { ChevronsUpDownIcon, PlusIcon, CheckIcon, MinusIcon } from "lucide-svelte";
|
import { ChevronsUpDownIcon, PlusIcon, CheckIcon, MinusIcon } from "@lucide/svelte";
|
||||||
import { Button } from "@components/ui/button";
|
import { Button } from "@components/ui/button";
|
||||||
import { cn } from "@components/utils";
|
import { cn } from "@components/utils";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@components/ui/dialog";
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/ui/tabs";
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@components/ui/tooltip";
|
||||||
import { cn } from "@components/utils";
|
import { cn } from "@components/utils";
|
||||||
import { Check, ChevronsUpDown, GitPullRequestArrow, Plus } from "lucide-svelte";
|
import { Check, ChevronsUpDown, GitPullRequestArrow, Plus } from "@lucide/svelte";
|
||||||
import type { EventModel } from "../pages/event/eventmodel.svelte";
|
import type { EventModel } from "../pages/event/eventmodel.svelte";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
|
||||||
import { Label } from "@components/ui/label";
|
import { Label } from "@components/ui/label";
|
||||||
|
|||||||
@@ -18,14 +18,14 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { location } from "svelte-spa-router";
|
import { router } from "svelte-spa-router";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="flex items-center space-x-4 lg:space-x-6 mx-6">
|
<nav class="flex items-center space-x-4 lg:space-x-6 mx-6">
|
||||||
<a href="#/" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/"}> Dashboard </a>
|
<a href="#/" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={router.location !== "/"}> Dashboard </a>
|
||||||
<a href="#/events" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={!$location.startsWith("/event")}> Events </a>
|
<a href="#/events" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={!router.location.startsWith("/event")}> Events </a>
|
||||||
<a href="#/players" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/players"}> Players </a>
|
<a href="#/players" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={router.location !== "/players"}> Players </a>
|
||||||
<a href="#/pages" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/pages"}> Pages </a>
|
<a href="#/pages" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={router.location !== "/pages"}> Pages </a>
|
||||||
<a href="#/schematics" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/schematics"}> Schematics </a>
|
<a href="#/schematics" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={router.location !== "/schematics"}> Schematics </a>
|
||||||
<a href="#/logs" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/logs"}> Logs </a>
|
<a href="#/logs" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={router.location !== "/logs"}> Logs </a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -25,10 +25,10 @@
|
|||||||
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||||
import { fromAbsolute } from "@internationalized/date";
|
import { fromAbsolute } from "@internationalized/date";
|
||||||
import { Button, buttonVariants } from "@components/ui/button";
|
import { Button, buttonVariants } from "@components/ui/button";
|
||||||
import { ChevronsUpDown } from "lucide-svelte";
|
import { ChevronsUpDown } from "@lucide/svelte";
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||||
import { schemTypes } from "@stores/stores.ts";
|
import { schemTypes } from "@stores/stores.ts";
|
||||||
import Check from "lucide-svelte/icons/check";
|
import Check from "@lucide/svelte/icons/check";
|
||||||
import { cn } from "@components/utils.ts";
|
import { cn } from "@components/utils.ts";
|
||||||
import { Switch } from "@components/ui/switch";
|
import { Switch } from "@components/ui/switch";
|
||||||
import { eventRepo } from "@repo/event.ts";
|
import { eventRepo } from "@repo/event.ts";
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
import GroupEditDialog from "./GroupEditDialog.svelte";
|
import GroupEditDialog from "./GroupEditDialog.svelte";
|
||||||
import GroupResultsDialog from "./GroupResultsDialog.svelte";
|
import GroupResultsDialog from "./GroupResultsDialog.svelte";
|
||||||
import type { ResponseGroups } from "@type/event";
|
import type { ResponseGroups } from "@type/event";
|
||||||
import { EditIcon, GroupIcon, LinkIcon } from "lucide-svelte";
|
import { EditIcon, GroupIcon, LinkIcon } from "@lucide/svelte";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@components/ui/dropdown-menu";
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@components/ui/dropdown-menu";
|
||||||
import GroupSelector from "@components/moderator/components/GroupSelector.svelte";
|
import GroupSelector from "@components/moderator/components/GroupSelector.svelte";
|
||||||
import { fightRepo } from "@components/repo/fight";
|
import { fightRepo } from "@components/repo/fight";
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { EventFight, EventFightEdit, ResponseGroups, ResponseRelation, SWEvent } from "@type/event";
|
import type { EventFight, EventFightEdit, ResponseGroups, ResponseRelation, SWEvent } from "@type/event";
|
||||||
import { Button } from "@components/ui/button";
|
import { Button } from "@components/ui/button";
|
||||||
import { EditIcon, CopyIcon } from "lucide-svelte";
|
import { EditIcon, CopyIcon } from "@lucide/svelte";
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@components/ui/dialog";
|
||||||
import FightEdit from "@components/moderator/components/FightEdit.svelte";
|
import FightEdit from "@components/moderator/components/FightEdit.svelte";
|
||||||
import type { Team } from "@components/types/team";
|
import type { Team } from "@components/types/team";
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
import { Input } from "@components/ui/input/index.js";
|
import { Input } from "@components/ui/input/index.js";
|
||||||
import { Label } from "@components/ui/label/index.js";
|
import { Label } from "@components/ui/label/index.js";
|
||||||
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||||
import { PlusIcon } from "lucide-svelte";
|
import { PlusIcon } from "@lucide/svelte";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { fromAbsolute, now, ZonedDateTime } from "@internationalized/date";
|
import { fromAbsolute, now, ZonedDateTime } from "@internationalized/date";
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
import { Slider } from "@components/ui/slider";
|
import { Slider } from "@components/ui/slider";
|
||||||
import { fromAbsolute } from "@internationalized/date";
|
import { fromAbsolute } from "@internationalized/date";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { Plus, Shuffle } from "lucide-svelte";
|
import { Plus, Shuffle } from "@lucide/svelte";
|
||||||
import { replace } from "svelte-spa-router";
|
import { replace } from "svelte-spa-router";
|
||||||
|
|
||||||
let { data }: { data: ExtendedEvent } = $props();
|
let { data }: { data: ExtendedEvent } = $props();
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
import { Slider } from "@components/ui/slider";
|
import { Slider } from "@components/ui/slider";
|
||||||
import { fromAbsolute } from "@internationalized/date";
|
import { fromAbsolute } from "@internationalized/date";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { Plus, Shuffle } from "lucide-svelte";
|
import { Plus, Shuffle } from "@lucide/svelte";
|
||||||
import { replace } from "svelte-spa-router";
|
import { replace } from "svelte-spa-router";
|
||||||
|
|
||||||
let { data }: { data: ExtendedEvent } = $props();
|
let { data }: { data: ExtendedEvent } = $props();
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
import { Slider } from "@components/ui/slider";
|
import { Slider } from "@components/ui/slider";
|
||||||
import { fromAbsolute, fromDate, parseDateTime, parseDuration } from "@internationalized/date";
|
import { fromAbsolute, fromDate, parseDateTime, parseDuration } from "@internationalized/date";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { Plus } from "lucide-svelte";
|
import { Plus } from "@lucide/svelte";
|
||||||
import { replace } from "svelte-spa-router";
|
import { replace } from "svelte-spa-router";
|
||||||
let {
|
let {
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
import { Input } from "@components/ui/input";
|
import { Input } from "@components/ui/input";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||||
import { Check } from "lucide-svelte";
|
import { Check } from "@lucide/svelte";
|
||||||
import { cn } from "@components/utils";
|
import { cn } from "@components/utils";
|
||||||
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||||
import PlayerSelector from "@components/ui/PlayerSelector.svelte";
|
import PlayerSelector from "@components/ui/PlayerSelector.svelte";
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Separator } from "@components/ui/separator";
|
|
||||||
import { manager, OpenEditPage } from "./page.svelte";
|
import { manager, OpenEditPage } from "./page.svelte";
|
||||||
import { File, X } from "lucide-svelte";
|
import { File, FileCode2, FileText, Save, X } from "@lucide/svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { EditorView } from "@codemirror/view";
|
import { EditorView } from "@codemirror/view";
|
||||||
import { basicSetup } from "codemirror";
|
import { basicSetup } from "codemirror";
|
||||||
@@ -61,56 +60,144 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
easyMde.codemirror.on("change", () => {
|
easyMde.codemirror.on("change", () => {
|
||||||
if (manager.selectedPage?.content !== easyMde?.value()) {
|
if (!manager.selectedPage) {
|
||||||
manager.selectedPage!.dirty = true;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.selectedPage!.content = easyMde?.value() || "";
|
if (manager.selectedPage.content !== easyMde?.value()) {
|
||||||
|
manager.selectedPage.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.selectedPage.content = easyMde?.value() || "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
view?.destroy();
|
||||||
|
easyMde?.toTextArea();
|
||||||
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col h-full w-full">
|
<div class="flex h-full min-h-0 w-full flex-col bg-neutral-950">
|
||||||
<div class="h-8 flex">
|
<div class="flex h-11 shrink-0 items-end overflow-x-auto border-b border-neutral-800 bg-neutral-950 px-2">
|
||||||
|
{#if manager.pages.length === 0}
|
||||||
|
<div class="flex h-full items-center px-2 text-sm text-muted-foreground">No file open</div>
|
||||||
|
{:else}
|
||||||
{#each manager.pages as tab, index}
|
{#each manager.pages as tab, index}
|
||||||
{@const isActive = manager.openPageIndex === index}
|
{@const isActive = manager.openPageIndex === index}
|
||||||
<button
|
<button
|
||||||
class="flex pl-4 border-r group items-center hover:bg-neutral-800 transition-colors cursor-pointer h-full {isActive
|
class="group flex h-9 max-w-64 items-center gap-2 rounded-t-md border border-b-0 px-3 text-sm transition-colors {isActive
|
||||||
? 'text-primary bg-neutral-900'
|
? 'border-neutral-800 bg-neutral-900 text-foreground'
|
||||||
: 'text-muted-foreground'} {tab.dirty ? 'italic' : ''}"
|
: 'border-transparent text-muted-foreground hover:bg-neutral-900/70'} {tab.dirty ? 'italic' : ''}"
|
||||||
onclick={() => (manager.openPageIndex = index)}
|
onclick={() => (manager.openPageIndex = index)}
|
||||||
>
|
>
|
||||||
<File class="h-4 w-4 mr-2" />
|
{#if tab.fileType === "json"}
|
||||||
{tab.pageTitle}
|
<FileCode2 class="size-4 shrink-0" />
|
||||||
|
{:else if tab.fileType === "md" || tab.fileType === "mdx"}
|
||||||
|
<FileText class="size-4 shrink-0" />
|
||||||
|
{:else}
|
||||||
|
<File class="size-4 shrink-0" />
|
||||||
|
{/if}
|
||||||
|
<span class="truncate">{tab.pageTitle}</span>
|
||||||
|
{#if tab.dirty}
|
||||||
|
<span class="size-1.5 shrink-0 rounded-full bg-primary"></span>
|
||||||
|
{/if}
|
||||||
<span
|
<span
|
||||||
class="mx-4 hover:bg-neutral-700 transition-all rounded {isActive ? '' : 'opacity-0'} group-hover:opacity-100 cursor-pointer"
|
class="ml-1 rounded p-0.5 opacity-0 transition-opacity hover:bg-neutral-800 group-hover:opacity-100 {isActive ? 'opacity-100' : ''}"
|
||||||
onclick={(e) => {
|
onclick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
manager.closePage(index);
|
manager.closePage(index);
|
||||||
}}><X /></span
|
}}><X class="size-4" /></span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
|
||||||
<div class="flex-1 flex flex-col">
|
<div class="flex min-h-0 flex-1 flex-col">
|
||||||
{#if manager.selectedPage}
|
{#if manager.selectedPage}
|
||||||
<div class="flex items-center justify-end p-2">
|
<header class="flex shrink-0 items-center justify-between gap-4 border-b border-neutral-800 bg-neutral-900/70 px-4 py-3">
|
||||||
<Button disabled={!(manager.selectedPage?.dirty ?? false)} onclick={() => manager.selectedPage?.save()}>Speichern</Button>
|
<div class="min-w-0">
|
||||||
</div>
|
<div class="flex items-center gap-2">
|
||||||
<div class="flex gap-2 items-center">
|
<h2 class="truncate text-base font-semibold">{manager.selectedPage.pageTitle}</h2>
|
||||||
{#if manager.selectedPage.path.startsWith("src/content/announcements/")}
|
{#if manager.selectedPage.dirty}
|
||||||
<div class="border-b flex-1" transition:slide>
|
<span class="rounded-full border border-primary/40 bg-primary/10 px-2 py-0.5 text-xs text-primary">Unsaved</span>
|
||||||
<FrontmatterEditor />
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<p class="mt-1 truncate text-xs text-muted-foreground">{manager.selectedPage.path}</p>
|
||||||
|
</div>
|
||||||
|
<Button disabled={!(manager.selectedPage?.dirty ?? false)} onclick={() => manager.selectedPage?.save()}>
|
||||||
|
<Save class="mr-2 size-4" />
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex-1">
|
|
||||||
|
<div class="grid min-h-0 flex-1 grid-cols-1 xl:grid-cols-[minmax(0,1fr)_26rem]">
|
||||||
|
<main class="min-h-0 overflow-hidden border-r border-neutral-800 bg-neutral-950">
|
||||||
|
<div class="flex h-full min-h-0 flex-col">
|
||||||
|
<div class="shrink-0 border-b border-neutral-800 px-4 py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground">Content</div>
|
||||||
|
<div class="relative min-h-0 flex-1 overflow-hidden">
|
||||||
<div bind:this={codemirrorParent} class="hidden h-full"></div>
|
<div bind:this={codemirrorParent} class="hidden h-full"></div>
|
||||||
<div bind:this={easyMdeWrapper} class="hidden h-full">
|
<div bind:this={easyMdeWrapper} class="hidden h-full">
|
||||||
<textarea bind:this={easyMdeParent}></textarea>
|
<textarea bind:this={easyMdeParent}></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if !manager.selectedPage}
|
||||||
|
<div class="absolute inset-0 flex items-center justify-center bg-neutral-950 p-8">
|
||||||
|
<div class="max-w-sm text-center">
|
||||||
|
<div class="mx-auto mb-4 flex size-12 items-center justify-center rounded-md border border-neutral-800 bg-neutral-900">
|
||||||
|
<FileText class="size-6 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-lg font-semibold">Select a page</h2>
|
||||||
|
<p class="mt-2 text-sm text-muted-foreground">Open a markdown, MDX, or JSON file from the repository tree to start editing.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{#if manager.selectedPage?.supportsFrontmatter() && manager.selectedPage.frontmatterSchema}
|
||||||
|
<aside class="min-h-0 overflow-y-auto bg-neutral-950" transition:slide>
|
||||||
|
<FrontmatterEditor />
|
||||||
|
</aside>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.EasyMDEContainer) {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
background: rgb(10 10 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.EasyMDEContainer .CodeMirror) {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
background: rgb(10 10 10);
|
||||||
|
color: rgb(245 245 245);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.EasyMDEContainer .editor-toolbar) {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid rgb(38 38 38);
|
||||||
|
background: rgb(23 23 23);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.EasyMDEContainer .editor-toolbar button) {
|
||||||
|
color: rgb(212 212 212) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.EasyMDEContainer .editor-toolbar button:hover),
|
||||||
|
:global(.EasyMDEContainer .editor-toolbar button.active) {
|
||||||
|
background: rgb(38 38 38);
|
||||||
|
border-color: rgb(64 64 64);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,122 +1,309 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { X } from "lucide-svelte";
|
import { Button } from "@components/ui/button";
|
||||||
|
import { Input } from "@components/ui/input";
|
||||||
|
import { Label } from "@components/ui/label";
|
||||||
|
import { Checkbox } from "@components/ui/checkbox";
|
||||||
|
import { Plus, X } from "@lucide/svelte";
|
||||||
|
import yaml from "js-yaml";
|
||||||
import { manager } from "./page.svelte";
|
import { manager } from "./page.svelte";
|
||||||
import { slide } from "svelte/transition";
|
import type { FrontmatterFieldSchema } from "../../../../content/frontmatter-editor-schemas";
|
||||||
|
import EventSelector from "./frontmatter/EventSelector.svelte";
|
||||||
|
import ImageFrontmatterSelector from "./frontmatter/ImageFrontmatterSelector.svelte";
|
||||||
|
import ViewConfigEditor from "./frontmatter/ViewConfigEditor.svelte";
|
||||||
|
|
||||||
|
function markDirty() {
|
||||||
|
if (manager.selectedPage) {
|
||||||
|
manager.selectedPage.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setField(key: string, value: unknown) {
|
||||||
|
if (!manager.selectedPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.selectedPage.frontmatter[key] = value;
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeField(key: string) {
|
||||||
|
if (!manager.selectedPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete manager.selectedPage.frontmatter[key];
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldValue(field: FrontmatterFieldSchema) {
|
||||||
|
const existingValue = manager.selectedPage?.frontmatter[field.key];
|
||||||
|
if (existingValue !== undefined) {
|
||||||
|
return existingValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.defaultValue !== undefined) {
|
||||||
|
return field.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (field.kind) {
|
||||||
|
case "boolean":
|
||||||
|
return false;
|
||||||
|
case "number":
|
||||||
|
return "";
|
||||||
|
case "string-array":
|
||||||
|
return [];
|
||||||
|
case "object":
|
||||||
|
return {};
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dateInputValue(value: unknown) {
|
||||||
|
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
||||||
|
return value.toISOString().split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value.split("T")[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function yamlValue(value: unknown) {
|
||||||
|
if (value === undefined || value === null || value === "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return yaml.dump(value).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setYamlField(key: string, value: string) {
|
||||||
|
if (!value.trim()) {
|
||||||
|
setField(key, {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setField(key, yaml.load(value));
|
||||||
|
} catch (error) {
|
||||||
|
alert(`Invalid YAML for ${key}: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayValue(key: string): string[] {
|
||||||
|
const value = manager.selectedPage?.frontmatter[key];
|
||||||
|
return Array.isArray(value) ? value.map((item) => String(item)) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setArrayItem(key: string, index: number, value: string) {
|
||||||
|
const values = arrayValue(key);
|
||||||
|
values[index] = value;
|
||||||
|
setField(key, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addArrayItem(key: string) {
|
||||||
|
setField(key, [...arrayValue(key), ""]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeArrayItem(key: string, index: number) {
|
||||||
|
setField(
|
||||||
|
key,
|
||||||
|
arrayValue(key).filter((_, itemIndex) => itemIndex !== index)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameCustomField(oldKey: string, newKey: string) {
|
||||||
|
if (!manager.selectedPage || !newKey || newKey === oldKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.hasOwn(manager.selectedPage.frontmatter, newKey)) {
|
||||||
|
alert(`A frontmatter field named "${newKey}" already exists.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.selectedPage.frontmatter[newKey] = manager.selectedPage.frontmatter[oldKey];
|
||||||
|
delete manager.selectedPage.frontmatter[oldKey];
|
||||||
|
markDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addCustomField() {
|
||||||
|
if (!manager.selectedPage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = 1;
|
||||||
|
let key = "customField";
|
||||||
|
while (Object.hasOwn(manager.selectedPage.frontmatter, key)) {
|
||||||
|
index += 1;
|
||||||
|
key = `customField${index}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
setField(key, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function schemaKeys(schema = manager.selectedPage?.frontmatterSchema) {
|
||||||
|
return new Set(schema?.fields.map((field) => field.key) ?? []);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<details class="group">
|
{#if manager.selectedPage?.frontmatterSchema}
|
||||||
<summary class="flex items-center justify-between p-3 cursor-pointer hover:bg-neutral-800">
|
{@const schema = manager.selectedPage.frontmatterSchema}
|
||||||
<span class="font-medium">Frontmatter</span>
|
{@const customEntries = Object.entries(manager.selectedPage.frontmatter).filter(([key]) => !schemaKeys(schema).has(key))}
|
||||||
<svg class="w-4 h-4 transition-transform group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
|
<details class="group" open>
|
||||||
|
<summary class="sticky top-0 z-10 flex cursor-pointer items-center justify-between border-b border-neutral-800 bg-neutral-950/95 px-4 py-3 backdrop-blur hover:bg-neutral-900">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold">{schema.label} Frontmatter</p>
|
||||||
|
<p class="text-xs text-muted-foreground">{schema.collection}</p>
|
||||||
|
</div>
|
||||||
|
<svg class="h-4 w-4 transition-transform group-open:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="p-3 border-t bg-neutral-900">
|
|
||||||
{#each Object.entries(manager.selectedPage?.frontmatter || {}) as [key, value]}
|
<div class="grid grid-cols-1 gap-4 bg-neutral-950 p-4">
|
||||||
<div class="flex flex-col gap-2 mb-3 p-2 border rounded bg-neutral-800">
|
{#each schema.fields as field (field.key)}
|
||||||
<div class="flex items-center gap-2">
|
{@const value = getFieldValue(field)}
|
||||||
<input
|
<div class="space-y-2">
|
||||||
type="text"
|
<div class="flex items-center justify-between gap-3">
|
||||||
value={key}
|
<Label for={`frontmatter-${field.key}`} class="text-sm">
|
||||||
onchange={(e) => {
|
{field.label}
|
||||||
const newKey = (e.target as HTMLInputElement).value;
|
{#if field.required}
|
||||||
if (newKey !== key) {
|
<span class="text-red-400">*</span>
|
||||||
manager.selectedPage!.frontmatter[newKey] = manager.selectedPage!.frontmatter[key];
|
{/if}
|
||||||
delete manager.selectedPage?.frontmatter[key];
|
</Label>
|
||||||
manager.selectedPage!.dirty = true;
|
{#if field.collection}
|
||||||
}
|
<span class="rounded border border-neutral-700 px-2 py-0.5 text-xs text-muted-foreground">{field.collection}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if field.key === "eventId"}
|
||||||
|
<EventSelector value={value as number | string | null | undefined} onSelect={(eventId) => setField(field.key, eventId)} />
|
||||||
|
{:else if field.key === "viewConfig"}
|
||||||
|
<ViewConfigEditor value={value} eventId={manager.selectedPage.frontmatter.eventId as number | string | null | undefined} onChange={(viewConfig) => setField(field.key, viewConfig)} />
|
||||||
|
{:else if field.kind === "image"}
|
||||||
|
<ImageFrontmatterSelector value={String(value ?? "")} onSelect={(path) => setField(field.key, path)} />
|
||||||
|
{:else if field.kind === "boolean"}
|
||||||
|
<label class="flex h-9 items-center gap-3 rounded-md border border-neutral-800 bg-neutral-900 px-3">
|
||||||
|
<Checkbox checked={Boolean(value)} onCheckedChange={(checked) => setField(field.key, checked === true)} />
|
||||||
|
<span class="text-sm">{Boolean(value) ? "True" : "False"}</span>
|
||||||
|
</label>
|
||||||
|
{:else if field.kind === "number"}
|
||||||
|
<Input
|
||||||
|
id={`frontmatter-${field.key}`}
|
||||||
|
type="number"
|
||||||
|
value={typeof value === "number" ? value : ""}
|
||||||
|
onchange={(event) => {
|
||||||
|
const nextValue = (event.currentTarget as HTMLInputElement).value;
|
||||||
|
setField(field.key, nextValue === "" ? "" : Number(nextValue));
|
||||||
}}
|
}}
|
||||||
class="px-2 py-1 border rounded text-sm flex-shrink-0 w-32 bg-neutral-900"
|
|
||||||
placeholder="Key"
|
|
||||||
/>
|
/>
|
||||||
<span>:</span>
|
{:else if field.kind === "date"}
|
||||||
{#if Array.isArray(value)}
|
<Input
|
||||||
<span class="text-xs text-muted-foreground">Array ({value.length} items)</span>
|
id={`frontmatter-${field.key}`}
|
||||||
{:else if value instanceof Date || key === "created"}
|
|
||||||
<input
|
|
||||||
type="date"
|
type="date"
|
||||||
value={value instanceof Date ? value.toISOString().split("T")[0] : typeof value === "string" ? value : ""}
|
value={dateInputValue(value)}
|
||||||
onchange={(e) => {
|
onchange={(event) => {
|
||||||
const dateValue = (e.target as HTMLInputElement).value;
|
const nextValue = (event.currentTarget as HTMLInputElement).value;
|
||||||
manager.selectedPage!.frontmatter[key] = dateValue ? new Date(dateValue) : "";
|
setField(field.key, nextValue ? new Date(`${nextValue}T00:00:00.000Z`) : "");
|
||||||
manager.selectedPage!.dirty = true;
|
|
||||||
}}
|
}}
|
||||||
class="px-2 py-1 border rounded text-sm flex-1 bg-neutral-900"
|
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else if field.kind === "text"}
|
||||||
<input
|
<textarea
|
||||||
type="text"
|
id={`frontmatter-${field.key}`}
|
||||||
bind:value={manager.selectedPage!.frontmatter[key]}
|
class="min-h-20 w-full rounded-md border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
onchange={() => (manager.selectedPage!.dirty = true)}
|
maxlength={field.maxLength}
|
||||||
class="px-2 py-1 border rounded text-sm flex-1 bg-neutral-900"
|
value={String(value ?? "")}
|
||||||
placeholder="Value"
|
oninput={(event) => setField(field.key, (event.currentTarget as HTMLTextAreaElement).value)}
|
||||||
/>
|
></textarea>
|
||||||
{/if}
|
{:else if field.kind === "string-array"}
|
||||||
<button
|
<div class="space-y-2 rounded-md border border-neutral-800 bg-neutral-900 p-2">
|
||||||
onclick={() => {
|
{#each arrayValue(field.key) as item, index}
|
||||||
delete manager.selectedPage!.frontmatter[key];
|
|
||||||
manager.selectedPage!.dirty = true;
|
|
||||||
}}
|
|
||||||
class="text-red-500 hover:text-red-700 p-1"
|
|
||||||
>
|
|
||||||
<X class="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{#if Array.isArray(value)}
|
|
||||||
<div class="ml-4 space-y-1">
|
|
||||||
{#each value as item, index}
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-xs text-muted-foreground w-6">[{index}]</span>
|
<Input value={item} oninput={(event) => setArrayItem(field.key, index, (event.currentTarget as HTMLInputElement).value)} />
|
||||||
<input
|
<Button type="button" variant="ghost" size="icon" onclick={() => removeArrayItem(field.key, index)}>
|
||||||
type="text"
|
<X class="size-4" />
|
||||||
bind:value={manager.selectedPage!.frontmatter[key][index]}
|
</Button>
|
||||||
onchange={() => (manager.selectedPage!.dirty = true)}
|
|
||||||
class="px-2 py-1 border rounded text-sm flex-1 bg-neutral-900"
|
|
||||||
placeholder="Array item"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onclick={() => {
|
|
||||||
manager.selectedPage!.frontmatter[key].splice(index, 1);
|
|
||||||
manager.selectedPage!.dirty = true;
|
|
||||||
}}
|
|
||||||
class="text-red-500 hover:text-red-700 p-1"
|
|
||||||
>
|
|
||||||
<X class="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<button
|
<Button type="button" variant="outline" size="sm" onclick={() => addArrayItem(field.key)}>
|
||||||
onclick={() => {
|
<Plus class="mr-2 size-4" />
|
||||||
manager.selectedPage!.frontmatter[key].push("");
|
Add item
|
||||||
manager.selectedPage!.dirty = true;
|
</Button>
|
||||||
}}
|
|
||||||
class="text-xs text-blue-500 hover:text-blue-700 ml-8"
|
|
||||||
>
|
|
||||||
+ Add item
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
{:else if field.kind === "object"}
|
||||||
|
<textarea
|
||||||
|
id={`frontmatter-${field.key}`}
|
||||||
|
class="min-h-28 w-full rounded-md border border-neutral-800 bg-neutral-900 px-3 py-2 font-mono text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
value={yamlValue(value)}
|
||||||
|
onchange={(event) => setYamlField(field.key, (event.currentTarget as HTMLTextAreaElement).value)}
|
||||||
|
></textarea>
|
||||||
|
{:else}
|
||||||
|
<Input
|
||||||
|
id={`frontmatter-${field.key}`}
|
||||||
|
type="text"
|
||||||
|
maxlength={field.maxLength}
|
||||||
|
placeholder={field.kind === "reference" ? `Reference to ${field.collection}` : field.kind === "image" ? "Image path" : ""}
|
||||||
|
value={String(value ?? "")}
|
||||||
|
oninput={(event) => setField(field.key, (event.currentTarget as HTMLInputElement).value)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if field.description}
|
||||||
|
<p class="text-xs text-muted-foreground">{field.description}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
<div class="flex gap-2">
|
|
||||||
<button
|
{#if customEntries.length > 0}
|
||||||
onclick={() => {
|
<div class="space-y-3 border-t border-neutral-800 pt-4">
|
||||||
manager.selectedPage!.frontmatter[`new_key_${Object.keys(manager.selectedPage!.frontmatter).length}`] = "";
|
<p class="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Custom fields</p>
|
||||||
manager.selectedPage!.dirty = true;
|
{#each customEntries as [key, value] (key)}
|
||||||
}}
|
<div class="rounded-md border border-neutral-800 bg-neutral-900 p-2">
|
||||||
class="text-sm text-blue-500 hover:text-blue-700"
|
<div class="grid gap-2">
|
||||||
>
|
<Input value={key} onchange={(event) => renameCustomField(key, (event.currentTarget as HTMLInputElement).value)} />
|
||||||
+ Add field
|
{#if Array.isArray(value)}
|
||||||
</button>
|
<div class="space-y-2">
|
||||||
<button
|
{#each arrayValue(key) as item, index}
|
||||||
onclick={() => {
|
<div class="flex items-center gap-2">
|
||||||
manager.selectedPage!.frontmatter[`new_array_${Object.keys(manager.selectedPage!.frontmatter).length}`] = [];
|
<Input value={item} oninput={(event) => setArrayItem(key, index, (event.currentTarget as HTMLInputElement).value)} />
|
||||||
manager.selectedPage!.dirty = true;
|
<Button type="button" variant="ghost" size="icon" onclick={() => removeArrayItem(key, index)}>
|
||||||
}}
|
<X class="size-4" />
|
||||||
class="text-sm text-green-500 hover:text-green-700"
|
</Button>
|
||||||
>
|
</div>
|
||||||
+ Add array
|
{/each}
|
||||||
</button>
|
<Button type="button" variant="outline" size="sm" onclick={() => addArrayItem(key)}>
|
||||||
|
<Plus class="mr-2 size-4" />
|
||||||
|
Add item
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{:else if typeof value === "object" && value !== null}
|
||||||
|
<textarea
|
||||||
|
class="min-h-24 w-full rounded-md border border-neutral-800 bg-neutral-950 px-3 py-2 font-mono text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
value={yamlValue(value)}
|
||||||
|
onchange={(event) => setYamlField(key, (event.currentTarget as HTMLTextAreaElement).value)}
|
||||||
|
></textarea>
|
||||||
|
{:else}
|
||||||
|
<Input value={String(value ?? "")} onchange={(event) => setField(key, (event.currentTarget as HTMLInputElement).value)} />
|
||||||
|
{/if}
|
||||||
|
<Button type="button" variant="ghost" size="icon" onclick={() => removeField(key)}>
|
||||||
|
<X class="size-4" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button type="button" variant="outline" size="sm" onclick={addCustomField}>
|
||||||
|
<Plus class="mr-2 size-4" />
|
||||||
|
Add custom field
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ResizablePane, ResizablePaneGroup } from "@components/ui/resizable";
|
import { ResizablePane, ResizablePaneGroup } from "@components/ui/resizable";
|
||||||
import { Separator } from "@components/ui/separator";
|
|
||||||
import { manager } from "./page.svelte";
|
import { manager } from "./page.svelte";
|
||||||
import ResizableHandle from "@components/ui/resizable/resizable-handle.svelte";
|
import ResizableHandle from "@components/ui/resizable/resizable-handle.svelte";
|
||||||
import PagesList from "./PagesList.svelte";
|
import PagesList from "./PagesList.svelte";
|
||||||
import EditorWithTabs from "./EditorWithTabs.svelte";
|
import EditorWithTabs from "./EditorWithTabs.svelte";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
import { Button } from "@components/ui/button";
|
import { Button } from "@components/ui/button";
|
||||||
import { Check, ChevronsUpDown, RefreshCw, FileImage, Plus } from "lucide-svelte";
|
import { Check, ChevronsUpDown, RefreshCw, FileImage, Plus, GitBranch, Upload } from "@lucide/svelte";
|
||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||||
import { cn } from "@components/utils";
|
import { cn } from "@components/utils";
|
||||||
import { pageRepo } from "@components/repo/page";
|
import { pageRepo } from "@components/repo/page";
|
||||||
@@ -18,16 +17,40 @@
|
|||||||
let fileInput: HTMLInputElement | undefined = $state();
|
let fileInput: HTMLInputElement | undefined = $state();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex-grow flex flex-col">
|
<div class="flex min-h-0 flex-grow flex-col bg-neutral-950">
|
||||||
<ResizablePaneGroup direction="horizontal" class="flex-grow">
|
<ResizablePaneGroup direction="horizontal" class="min-h-0 flex-grow">
|
||||||
<ResizablePane defaultSize={20}>
|
<ResizablePane defaultSize={24} minSize={18} maxSize={36}>
|
||||||
<div class="overflow-y-scroll">
|
<aside class="flex h-full min-h-0 flex-col border-r border-neutral-800 bg-neutral-950">
|
||||||
<div class="flex p-2 gap-2">
|
<div class="border-b border-neutral-800 p-3">
|
||||||
|
<div class="mb-3 flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-base font-semibold leading-none">Pages</h1>
|
||||||
|
<p class="mt-1 text-xs text-muted-foreground">Content repository</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="outline"
|
||||||
|
onclick={async () => {
|
||||||
|
const branchName = prompt("Enter branch name:");
|
||||||
|
if (branchName) {
|
||||||
|
await $pageRepo.createBranch(branchName);
|
||||||
|
manager.reloadBranches();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus class="size-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-[1fr_auto_auto] gap-2">
|
||||||
<Popover bind:open={branchSelectOpen}>
|
<Popover bind:open={branchSelectOpen}>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<Button variant="outline" class="justify-between flex-1" {...props} role="combobox">
|
<Button variant="outline" class="min-w-0 justify-between" {...props} role="combobox">
|
||||||
{manager.branch}
|
<span class="flex min-w-0 items-center gap-2">
|
||||||
|
<GitBranch class="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<span class="truncate">{manager.branch}</span>
|
||||||
|
</span>
|
||||||
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -62,23 +85,23 @@
|
|||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Button size="icon" variant="outline" onclick={() => manager.reloadImages()}>
|
<Button size="icon" variant="outline" onclick={() => manager.reloadImages()} title="Refresh images">
|
||||||
<RefreshCw />
|
<RefreshCw class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Popover bind:open={imageSelectOpen}>
|
<Popover bind:open={imageSelectOpen}>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
{#snippet child({ props })}
|
{#snippet child({ props })}
|
||||||
<Button size="icon" variant="outline" {...props}>
|
<Button size="icon" variant="outline" {...props} title="Images">
|
||||||
<FileImage />
|
<FileImage class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent side="right" class="w-[1000px] h-screen overflow-y-auto">
|
<PopoverContent side="right" class="h-screen w-[960px] max-w-[calc(100vw-2rem)] overflow-y-auto p-0">
|
||||||
{#await manager.imagesLoad}
|
{#await manager.imagesLoad}
|
||||||
<p>Loading images...</p>
|
<p class="p-4 text-sm text-muted-foreground">Loading images...</p>
|
||||||
{:then images}
|
{:then images}
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="p-2">
|
<div class="sticky top-0 z-10 border-b border-neutral-800 bg-neutral-950/95 p-3 backdrop-blur">
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
@@ -100,13 +123,14 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button onclick={() => fileInput?.click()} class="w-full">
|
<Button onclick={() => fileInput?.click()} class="w-full">
|
||||||
<Plus class="mr-2 size-4" />
|
<Upload class="mr-2 size-4" />
|
||||||
Upload Image
|
Upload Image
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-4 gap-2 p-2">
|
<div class="grid grid-cols-3 gap-3 p-3 xl:grid-cols-4">
|
||||||
{#each images as image}
|
{#each images as image}
|
||||||
<button
|
<button
|
||||||
|
class="overflow-hidden rounded-md border border-neutral-800 bg-neutral-900 text-left transition-colors hover:border-primary"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
const backs = (manager.selectedPage?.path?.match(/\//g)?.length || 1) - 1;
|
const backs = (manager.selectedPage?.path?.match(/\//g)?.length || 1) - 1;
|
||||||
|
|
||||||
@@ -116,7 +140,8 @@
|
|||||||
imageSelectOpen = false;
|
imageSelectOpen = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img src={image.downloadUrl} alt={image.name} class="w-full h-auto object-cover" />
|
<img src={image.downloadUrl} alt={image.name} class="aspect-video w-full object-cover" />
|
||||||
|
<div class="truncate px-2 py-1.5 text-xs text-muted-foreground">{image.name}</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -124,31 +149,22 @@
|
|||||||
{/await}
|
{/await}
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
<Button
|
|
||||||
size="icon"
|
|
||||||
onclick={async () => {
|
|
||||||
const branchName = prompt("Enter branch name:");
|
|
||||||
if (branchName) {
|
|
||||||
await $pageRepo.createBranch(branchName);
|
|
||||||
manager.reloadBranches();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
</div>
|
||||||
|
|
||||||
|
<div class="min-h-0 flex-1 overflow-y-auto py-2">
|
||||||
{#await manager.pagesLoad}
|
{#await manager.pagesLoad}
|
||||||
<p>Loading pages...</p>
|
<p class="px-4 py-3 text-sm text-muted-foreground">Loading pages...</p>
|
||||||
{:then pages}
|
{:then pages}
|
||||||
{#each Object.values(pages.dirs) as page}
|
{#each Object.values(pages.dirs) as page}
|
||||||
<PagesList {page} path={page.name + "/"} />
|
<PagesList {page} path={page.name + "/"} />
|
||||||
{/each}
|
{/each}
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
</aside>
|
||||||
</ResizablePane>
|
</ResizablePane>
|
||||||
<ResizableHandle />
|
<ResizableHandle />
|
||||||
<ResizablePane defaultSize={80}>
|
<ResizablePane defaultSize={76}>
|
||||||
<EditorWithTabs />
|
<EditorWithTabs />
|
||||||
</ResizablePane>
|
</ResizablePane>
|
||||||
</ResizablePaneGroup>
|
</ResizablePaneGroup>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ChevronDown, ChevronRight, Folder, FolderPlus, FileJson, FileText, File, FilePlus } from "lucide-svelte";
|
import { ChevronDown, ChevronRight, Folder, FileJson, FileText, File, FilePlus } from "@lucide/svelte";
|
||||||
import type { DirTree } from "./page.svelte";
|
import type { DirTree } from "./page.svelte";
|
||||||
import PagesList from "./PagesList.svelte";
|
import PagesList from "./PagesList.svelte";
|
||||||
import { slide } from "svelte/transition";
|
import { slide } from "svelte/transition";
|
||||||
@@ -55,19 +55,23 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button class={`group flex flex-row justify-between h-full w-full hover:bg-neutral-700 pl-${4 * depth}`} onclick={() => (open = !open)}>
|
<button
|
||||||
<div class="flex flex-row items-center">
|
class="group flex h-8 w-full items-center justify-between px-2 text-left text-sm text-neutral-200 transition-colors hover:bg-neutral-900"
|
||||||
|
style={`padding-left: ${0.75 + depth * 0.85}rem`}
|
||||||
|
onclick={() => (open = !open)}
|
||||||
|
>
|
||||||
|
<div class="flex min-w-0 flex-row items-center">
|
||||||
{#if open}
|
{#if open}
|
||||||
<ChevronDown class="w-6 h-6" />
|
<ChevronDown class="mr-1 size-4 shrink-0 text-muted-foreground" />
|
||||||
{:else}
|
{:else}
|
||||||
<ChevronRight class="w-6 h-6" />
|
<ChevronRight class="mr-1 size-4 shrink-0 text-muted-foreground" />
|
||||||
{/if}
|
{/if}
|
||||||
<Folder class="mr-2 w-4 h-4" />
|
<Folder class="mr-2 size-4 shrink-0 text-amber-400" />
|
||||||
{page.name}/
|
<span class="truncate">{page.name}/</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-row items-center hidden group-hover:flex">
|
<div class="hidden flex-row items-center group-hover:flex">
|
||||||
<Button variant="ghost" size="sm" class="p-0 m-0 h-6 w-6" onclick={startNewPageCreate}>
|
<Button variant="ghost" size="sm" class="m-0 size-6 p-0" onclick={startNewPageCreate} title="New page">
|
||||||
<FilePlus class="w-3 h-3" />
|
<FilePlus class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -76,39 +80,43 @@
|
|||||||
<div transition:slide={{ duration: 200, axis: "y" }}>
|
<div transition:slide={{ duration: 200, axis: "y" }}>
|
||||||
<div>
|
<div>
|
||||||
{#if newPage}
|
{#if newPage}
|
||||||
<button class={`flex flex-row items-center h-full py-1 w-full hover:bg-neutral-700 pl-${4 * (depth + 1)}`}>
|
<div class="flex h-8 w-full flex-row items-center bg-neutral-900 px-2 py-1" style={`padding-left: ${0.75 + (depth + 1) * 0.85}rem`}>
|
||||||
{#if newPageName.endsWith(".json")}
|
{#if newPageName.endsWith(".json")}
|
||||||
<FileJson class="mr-2 w-4 h-4" />
|
<FileJson class="mr-2 size-4 shrink-0 text-sky-400" />
|
||||||
{:else if newPageName.endsWith(".md") || newPageName.endsWith(".mdx")}
|
{:else if newPageName.endsWith(".md") || newPageName.endsWith(".mdx")}
|
||||||
<FileText class="mr-2 w-4 h-4" />
|
<FileText class="mr-2 size-4 shrink-0 text-emerald-400" />
|
||||||
{:else}
|
{:else}
|
||||||
<File class="mr-2 w-4 h-4" />
|
<File class="mr-2 size-4 shrink-0 text-muted-foreground" />
|
||||||
{/if}
|
{/if}
|
||||||
<form onsubmit={createNewPage}>
|
<form onsubmit={createNewPage} class="min-w-0 flex-1">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newPageName}
|
bind:value={newPageName}
|
||||||
bind:this={newPageInput}
|
bind:this={newPageInput}
|
||||||
onblur={() => (newPage = false)}
|
onblur={() => (newPage = false)}
|
||||||
placeholder="New page name"
|
placeholder="New page name"
|
||||||
class="flex-grow bg-transparent border-none outline-none text-white"
|
class="w-full rounded border border-neutral-700 bg-neutral-950 px-2 py-1 text-sm text-white outline-none focus:border-primary"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</button>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#each Object.values(page.dirs) as subPage (subPage.name)}
|
{#each Object.values(page.dirs) as subPage (subPage.name)}
|
||||||
<PagesList page={subPage} depth={depth + 1} path={path + subPage.name + "/"} />
|
<PagesList page={subPage} depth={depth + 1} path={path + subPage.name + "/"} />
|
||||||
{/each}
|
{/each}
|
||||||
{#each Object.values(page.files) as file (file.id)}
|
{#each Object.values(page.files) as file (file.id)}
|
||||||
<button class={`flex flex-row items-center h-full py-1 w-full hover:bg-neutral-700 pl-${4 * (depth + 1)}`} onclick={() => manager.openPage(file.id)}>
|
<button
|
||||||
|
class="flex h-8 w-full min-w-0 flex-row items-center px-2 py-1 text-left text-sm text-muted-foreground transition-colors hover:bg-neutral-900 hover:text-foreground"
|
||||||
|
style={`padding-left: ${0.75 + (depth + 1) * 0.85}rem`}
|
||||||
|
onclick={() => manager.openPage(file.id)}
|
||||||
|
>
|
||||||
{#if file.name.endsWith(".json")}
|
{#if file.name.endsWith(".json")}
|
||||||
<FileJson class="mr-2 w-4 h-4" />
|
<FileJson class="mr-2 size-4 shrink-0 text-sky-400" />
|
||||||
{:else if file.name.endsWith(".md") || file.name.endsWith(".mdx")}
|
{:else if file.name.endsWith(".md") || file.name.endsWith(".mdx")}
|
||||||
<FileText class="mr-2 w-4 h-4" />
|
<FileText class="mr-2 size-4 shrink-0 text-emerald-400" />
|
||||||
{:else}
|
{:else}
|
||||||
<File class="mr-2 w-4 h-4" />
|
<File class="mr-2 size-4 shrink-0" />
|
||||||
{/if}
|
{/if}
|
||||||
{file.name}
|
<span class="truncate">{file.name}</span>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||||
|
import { eventRepo } from "@repo/event";
|
||||||
|
import type { ShortEvent } from "@type/event";
|
||||||
|
import { CalendarDays, Check, ChevronsUpDown } from "@lucide/svelte";
|
||||||
|
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
value: number | string | null | undefined;
|
||||||
|
onSelect: (eventId: number) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let eventsFuture = $state($eventRepo.listEvents());
|
||||||
|
|
||||||
|
function selectedEvent(events: ShortEvent[]) {
|
||||||
|
const eventId = Number(value);
|
||||||
|
return events.find((event) => event.id === eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(timestamp: number) {
|
||||||
|
return new Date(timestamp).toLocaleDateString("de-DE", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortEvents(events: ShortEvent[]) {
|
||||||
|
return [...events].sort((a, b) => b.start - a.start);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Popover bind:open>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button variant="outline" class="w-full justify-between" {...props} role="combobox">
|
||||||
|
{#await eventsFuture}
|
||||||
|
Loading events...
|
||||||
|
{:then events}
|
||||||
|
{@const event = selectedEvent(events)}
|
||||||
|
{#if event}
|
||||||
|
<span class="truncate">{event.name} #{event.id}</span>
|
||||||
|
{:else}
|
||||||
|
<span class="text-muted-foreground">Select event</span>
|
||||||
|
{/if}
|
||||||
|
{:catch}
|
||||||
|
<span class="text-red-400">Could not load events</span>
|
||||||
|
{/await}
|
||||||
|
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-[36rem] max-w-[calc(100vw-2rem)] p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search events..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No events found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{#await eventsFuture}
|
||||||
|
<div class="p-3 text-sm text-muted-foreground">Loading events...</div>
|
||||||
|
{:then events}
|
||||||
|
{#each sortEvents(events) as event (event.id)}
|
||||||
|
<CommandItem
|
||||||
|
value={`${event.name} ${event.id}`}
|
||||||
|
onSelect={() => {
|
||||||
|
onSelect(event.id);
|
||||||
|
open = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check class="mr-2 size-4 {Number(value) === event.id ? '' : 'text-transparent'}" />
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="truncate text-sm">{event.name}</div>
|
||||||
|
<div class="flex items-center gap-1 text-xs text-muted-foreground">
|
||||||
|
<CalendarDays class="size-3" />
|
||||||
|
#{event.id} · {formatDate(event.start)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
{/each}
|
||||||
|
{:catch error}
|
||||||
|
<div class="p-3 text-sm text-red-400">{error instanceof Error ? error.message : "Failed to load events."}</div>
|
||||||
|
{/await}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { Input } from "@components/ui/input";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
|
import { Image, Search } from "@lucide/svelte";
|
||||||
|
import { manager } from "../page.svelte";
|
||||||
|
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
value: string | null | undefined;
|
||||||
|
onSelect: (path: string) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let open = $state(false);
|
||||||
|
let search = $state("");
|
||||||
|
|
||||||
|
function imagePath(path: string) {
|
||||||
|
const backs = (manager.selectedPage?.path?.match(/\//g)?.length || 1) - 1;
|
||||||
|
return [...Array(backs).fill(".."), path.replace("src/", "")].join("/");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Input value={value ?? ""} placeholder="Image path" oninput={(event) => onSelect((event.currentTarget as HTMLInputElement).value)} />
|
||||||
|
<Popover bind:open>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button type="button" variant="outline" size="icon" {...props}>
|
||||||
|
<Image class="size-4" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="h-[34rem] w-[56rem] max-w-[calc(100vw-2rem)] overflow-hidden p-0" side="bottom">
|
||||||
|
<div class="flex items-center gap-2 border-b border-neutral-800 p-3">
|
||||||
|
<Search class="size-4 text-muted-foreground" />
|
||||||
|
<Input bind:value={search} placeholder="Search images..." class="border-0 bg-transparent shadow-none focus-visible:ring-0" />
|
||||||
|
</div>
|
||||||
|
<div class="h-[30rem] overflow-y-auto p-3">
|
||||||
|
{#await manager.imagesLoad}
|
||||||
|
<p class="text-sm text-muted-foreground">Loading images...</p>
|
||||||
|
{:then images}
|
||||||
|
{@const filteredImages = images.filter((image) => image.name.toLowerCase().includes(search.toLowerCase()) || image.path.toLowerCase().includes(search.toLowerCase()))}
|
||||||
|
{#if filteredImages.length === 0}
|
||||||
|
<p class="text-sm text-muted-foreground">No images found.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||||
|
{#each filteredImages as image (image.id)}
|
||||||
|
{@const path = imagePath(image.path)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="group overflow-hidden rounded-md border border-neutral-800 bg-neutral-900 text-left transition-colors hover:border-primary"
|
||||||
|
onclick={() => {
|
||||||
|
onSelect(path);
|
||||||
|
open = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={image.downloadUrl} alt={image.name} class="aspect-video w-full bg-neutral-950 object-cover" />
|
||||||
|
<div class="space-y-1 p-2">
|
||||||
|
<div class="truncate text-xs font-medium">{image.name}</div>
|
||||||
|
<div class="truncate text-[11px] text-muted-foreground">{path}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:catch error}
|
||||||
|
<p class="text-sm text-red-400">{error instanceof Error ? error.message : "Failed to load images."}</p>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Button } from "@components/ui/button";
|
||||||
|
import { Input } from "@components/ui/input";
|
||||||
|
import { Label } from "@components/ui/label";
|
||||||
|
import { eventRepo } from "@repo/event";
|
||||||
|
import type { EventFight, ExtendedEvent, ResponseGroups } from "@type/event";
|
||||||
|
import { Plus, Trash2 } from "@lucide/svelte";
|
||||||
|
|
||||||
|
type StageType = "GROUP" | "ELEMINATION" | "DOUBLE_ELEMINATION";
|
||||||
|
type RoundPrefix = "Runde" | "Tag";
|
||||||
|
const roundGroupingTimeOptions = [
|
||||||
|
{ label: "10 Minutes", value: 10 },
|
||||||
|
{ label: "30 Minutes", value: 30 },
|
||||||
|
{ label: "1 Hour", value: 60 },
|
||||||
|
{ label: "2 Hours", value: 120 },
|
||||||
|
{ label: "12 Hours", value: 720 },
|
||||||
|
];
|
||||||
|
type StageConfig = {
|
||||||
|
name: string;
|
||||||
|
view:
|
||||||
|
| { type: "GROUP"; groups: number[]; roundRows?: number; roundGroupingTimeMinutes?: number; roundPrefix?: RoundPrefix }
|
||||||
|
| { type: "ELEMINATION"; finalFight: number }
|
||||||
|
| { type: "DOUBLE_ELEMINATION"; winnersFinalFight: number; losersFinalFight: number; grandFinalFight: number };
|
||||||
|
};
|
||||||
|
type ViewConfig = Record<string, StageConfig>;
|
||||||
|
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
eventId,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: unknown;
|
||||||
|
eventId: number | string | null | undefined;
|
||||||
|
onChange: (value: ViewConfig) => void;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
let eventFuture: Promise<ExtendedEvent> | undefined = $state();
|
||||||
|
|
||||||
|
const config = $derived(normalizeConfig(value));
|
||||||
|
const selectedEventId = $derived(Number(eventId));
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (Number.isFinite(selectedEventId) && selectedEventId > 0) {
|
||||||
|
eventFuture = $eventRepo.getEvent(selectedEventId.toString());
|
||||||
|
} else {
|
||||||
|
eventFuture = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function normalizeConfig(raw: unknown): ViewConfig {
|
||||||
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return raw as ViewConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneConfig(value: ViewConfig): ViewConfig {
|
||||||
|
return JSON.parse(JSON.stringify(value)) as ViewConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextConfig(mutator: (draft: ViewConfig) => void) {
|
||||||
|
const draft = cloneConfig(config);
|
||||||
|
mutator(draft);
|
||||||
|
onChange(draft);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStage() {
|
||||||
|
nextConfig((draft) => {
|
||||||
|
let index = Object.keys(draft).length + 1;
|
||||||
|
let key = `stage${index}`;
|
||||||
|
while (Object.hasOwn(draft, key)) {
|
||||||
|
index += 1;
|
||||||
|
key = `stage${index}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
draft[key] = {
|
||||||
|
name: "New stage",
|
||||||
|
view: { type: "GROUP", groups: [], roundRows: 1, roundGroupingTimeMinutes: 10, roundPrefix: "Runde" },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameStage(oldKey: string, newKey: string) {
|
||||||
|
const cleanKey = newKey.trim();
|
||||||
|
if (!cleanKey || cleanKey === oldKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextConfig((draft) => {
|
||||||
|
if (Object.hasOwn(draft, cleanKey)) {
|
||||||
|
alert(`A view config stage named "${cleanKey}" already exists.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
draft[cleanKey] = draft[oldKey];
|
||||||
|
delete draft[oldKey];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeStage(key: string) {
|
||||||
|
nextConfig((draft) => {
|
||||||
|
delete draft[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStageName(key: string, name: string) {
|
||||||
|
nextConfig((draft) => {
|
||||||
|
draft[key].name = name;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStageType(key: string, type: StageType, event?: ExtendedEvent) {
|
||||||
|
nextConfig((draft) => {
|
||||||
|
if (type === "GROUP") {
|
||||||
|
draft[key].view = { type, groups: [], roundRows: 1, roundGroupingTimeMinutes: 10, roundPrefix: "Runde" };
|
||||||
|
} else if (type === "ELEMINATION") {
|
||||||
|
draft[key].view = { type, finalFight: event?.fights[0]?.id ?? 0 };
|
||||||
|
} else {
|
||||||
|
draft[key].view = {
|
||||||
|
type,
|
||||||
|
winnersFinalFight: event?.fights[0]?.id ?? 0,
|
||||||
|
losersFinalFight: event?.fights[0]?.id ?? 0,
|
||||||
|
grandFinalFight: event?.fights[0]?.id ?? 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setGroupSelection(key: string, groupId: number, selected: boolean) {
|
||||||
|
nextConfig((draft) => {
|
||||||
|
const view = draft[key].view;
|
||||||
|
if (view.type !== "GROUP") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
view.groups = selected ? [...new Set([...view.groups, groupId])] : view.groups.filter((id) => id !== groupId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRoundRows(key: string, value: string) {
|
||||||
|
nextConfig((draft) => {
|
||||||
|
const view = draft[key].view;
|
||||||
|
if (view.type === "GROUP") {
|
||||||
|
view.roundRows = Math.max(1, Number(value) || 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRoundGroupingTimeMinutes(key: string, value: string) {
|
||||||
|
nextConfig((draft) => {
|
||||||
|
const view = draft[key].view;
|
||||||
|
if (view.type === "GROUP") {
|
||||||
|
view.roundGroupingTimeMinutes = Math.max(1, Math.floor(Number(value) || 10));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRoundPrefix(key: string, value: RoundPrefix) {
|
||||||
|
nextConfig((draft) => {
|
||||||
|
const view = draft[key].view;
|
||||||
|
if (view.type === "GROUP") {
|
||||||
|
view.roundPrefix = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFightParam(key: string, param: "finalFight" | "winnersFinalFight" | "losersFinalFight" | "grandFinalFight", fightId: string) {
|
||||||
|
nextConfig((draft) => {
|
||||||
|
const view = draft[key].view as Record<string, unknown>;
|
||||||
|
view[param] = Number(fightId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupLabel(group: ResponseGroups) {
|
||||||
|
return `${group.name} #${group.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fightLabel(fight: EventFight) {
|
||||||
|
const start = new Date(fight.start).toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
||||||
|
return `#${fight.id} ${fight.blueTeam.kuerzel} vs ${fight.redTeam.kuerzel} · ${start}`;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-3 rounded-md border border-neutral-800 bg-neutral-900 p-3">
|
||||||
|
{#if !selectedEventId}
|
||||||
|
<p class="text-sm text-muted-foreground">Select an eventId first to edit the view config with event groups and fights.</p>
|
||||||
|
{:else if eventFuture}
|
||||||
|
{#await eventFuture}
|
||||||
|
<p class="text-sm text-muted-foreground">Loading event context...</p>
|
||||||
|
{:then event}
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium">{event.event.name}</p>
|
||||||
|
<p class="text-xs text-muted-foreground">{event.groups.length} groups · {event.fights.length} fights</p>
|
||||||
|
</div>
|
||||||
|
<Button type="button" variant="outline" size="sm" onclick={addStage}>
|
||||||
|
<Plus class="mr-2 size-4" />
|
||||||
|
Add stage
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if Object.entries(config).length === 0}
|
||||||
|
<div class="rounded-md border border-dashed border-neutral-700 p-4 text-sm text-muted-foreground">No stages configured.</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each Object.entries(config) as [key, stage] (key)}
|
||||||
|
<div class="space-y-3 rounded-md border border-neutral-800 bg-neutral-950 p-3">
|
||||||
|
<div class="flex items-center justify-between gap-3 border-b border-neutral-800 pb-3">
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="truncate text-sm font-medium">{stage.name || key}</p>
|
||||||
|
<p class="text-xs text-muted-foreground">{stage.view.type}</p>
|
||||||
|
</div>
|
||||||
|
<Button type="button" variant="outline" size="sm" onclick={() => removeStage(key)}>
|
||||||
|
<Trash2 class="mr-2 size-4" />
|
||||||
|
Remove stage
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label>Key</Label>
|
||||||
|
<Input value={key} onchange={(event) => renameStage(key, (event.currentTarget as HTMLInputElement).value)} />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label>Name</Label>
|
||||||
|
<Input value={stage.name} oninput={(event) => setStageName(key, (event.currentTarget as HTMLInputElement).value)} />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label>Type</Label>
|
||||||
|
<select
|
||||||
|
class="h-9 w-full rounded-md border border-neutral-800 bg-neutral-900 px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
value={stage.view.type}
|
||||||
|
onchange={(changeEvent) => setStageType(key, (changeEvent.currentTarget as HTMLSelectElement).value as StageType, event)}
|
||||||
|
>
|
||||||
|
<option value="GROUP">Group</option>
|
||||||
|
<option value="ELEMINATION">Elimination</option>
|
||||||
|
<option value="DOUBLE_ELEMINATION">Double elimination</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if stage.view.type === "GROUP"}
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<div>
|
||||||
|
<Label>Groups</Label>
|
||||||
|
<div class="mt-2 grid gap-2">
|
||||||
|
{#each event.groups as group (group.id)}
|
||||||
|
<label class="flex items-center gap-2 rounded border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={stage.view.groups.includes(group.id)}
|
||||||
|
onchange={(changeEvent) => setGroupSelection(key, group.id, (changeEvent.currentTarget as HTMLInputElement).checked)}
|
||||||
|
/>
|
||||||
|
<span>{groupLabel(group)}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label>Round rows</Label>
|
||||||
|
<Input type="number" min="1" value={stage.view.roundRows ?? 1} onchange={(event) => setRoundRows(key, (event.currentTarget as HTMLInputElement).value)} />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label>Round grouping time (minutes)</Label>
|
||||||
|
<select
|
||||||
|
class="h-9 w-full rounded-md border border-neutral-800 bg-neutral-900 px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
value={(stage.view.roundGroupingTimeMinutes ?? 10).toString()}
|
||||||
|
onchange={(event) => setRoundGroupingTimeMinutes(key, (event.currentTarget as HTMLSelectElement).value)}
|
||||||
|
>
|
||||||
|
{#each roundGroupingTimeOptions as option}
|
||||||
|
<option value={option.value.toString()}>{option.label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label>Round prefix</Label>
|
||||||
|
<select
|
||||||
|
class="h-9 w-full rounded-md border border-neutral-800 bg-neutral-900 px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
value={stage.view.roundPrefix ?? "Runde"}
|
||||||
|
onchange={(event) => setRoundPrefix(key, (event.currentTarget as HTMLSelectElement).value as RoundPrefix)}
|
||||||
|
>
|
||||||
|
<option value="Runde">Runde</option>
|
||||||
|
<option value="Tag">Tag</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if stage.view.type === "ELEMINATION"}
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label>Final fight</Label>
|
||||||
|
<select
|
||||||
|
class="h-9 w-full rounded-md border border-neutral-800 bg-neutral-900 px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
value={stage.view.finalFight.toString()}
|
||||||
|
onchange={(event) => setFightParam(key, "finalFight", (event.currentTarget as HTMLSelectElement).value)}
|
||||||
|
>
|
||||||
|
{#each event.fights as fight (fight.id)}
|
||||||
|
<option value={fight.id.toString()}>{fightLabel(fight)}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label>Winners final</Label>
|
||||||
|
<select
|
||||||
|
class="h-9 w-full rounded-md border border-neutral-800 bg-neutral-900 px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
value={stage.view.winnersFinalFight.toString()}
|
||||||
|
onchange={(event) => setFightParam(key, "winnersFinalFight", (event.currentTarget as HTMLSelectElement).value)}
|
||||||
|
>
|
||||||
|
{#each event.fights as fight (fight.id)}
|
||||||
|
<option value={fight.id.toString()}>{fightLabel(fight)}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label>Losers final</Label>
|
||||||
|
<select
|
||||||
|
class="h-9 w-full rounded-md border border-neutral-800 bg-neutral-900 px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
value={stage.view.losersFinalFight.toString()}
|
||||||
|
onchange={(event) => setFightParam(key, "losersFinalFight", (event.currentTarget as HTMLSelectElement).value)}
|
||||||
|
>
|
||||||
|
{#each event.fights as fight (fight.id)}
|
||||||
|
<option value={fight.id.toString()}>{fightLabel(fight)}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<Label>Grand final</Label>
|
||||||
|
<select
|
||||||
|
class="h-9 w-full rounded-md border border-neutral-800 bg-neutral-900 px-3 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
value={stage.view.grandFinalFight.toString()}
|
||||||
|
onchange={(event) => setFightParam(key, "grandFinalFight", (event.currentTarget as HTMLSelectElement).value)}
|
||||||
|
>
|
||||||
|
{#each event.fights as fight (fight.id)}
|
||||||
|
<option value={fight.id.toString()}>{fightLabel(fight)}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{:catch error}
|
||||||
|
<p class="text-sm text-red-400">{error instanceof Error ? error.message : "Failed to load event context."}</p>
|
||||||
|
{/await}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -3,13 +3,18 @@ import { pageRepo } from "@components/repo/page";
|
|||||||
import type { ListPage, PageList } from "@components/types/page";
|
import type { ListPage, PageList } from "@components/types/page";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
|
import { getMarkdownFrontmatterSchema, type FrontmatterCollectionSchema } from "../../../../content/frontmatter-editor-schemas";
|
||||||
|
|
||||||
|
type FrontmatterValue = string | string[] | number | boolean | Date | Record<string, unknown> | unknown[] | null;
|
||||||
|
|
||||||
export class OpenEditPage {
|
export class OpenEditPage {
|
||||||
public content: string = "";
|
public content: string = "";
|
||||||
public frontmatter: { [key: string]: string | string[] | Date } = $state({});
|
public frontmatter: { [key: string]: FrontmatterValue } = $state({});
|
||||||
public dirty: boolean = $state(false);
|
public dirty: boolean = $state(false);
|
||||||
|
|
||||||
public readonly fileType: string;
|
public readonly fileType: string;
|
||||||
|
public readonly collection: string | undefined;
|
||||||
|
public readonly frontmatterSchema: FrontmatterCollectionSchema | undefined;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private manager: PageManager,
|
private manager: PageManager,
|
||||||
@@ -20,6 +25,8 @@ export class OpenEditPage {
|
|||||||
public readonly path: string
|
public readonly path: string
|
||||||
) {
|
) {
|
||||||
this.fileType = this.path.split(".").pop() || "md";
|
this.fileType = this.path.split(".").pop() || "md";
|
||||||
|
this.collection = this.resolveContentCollection();
|
||||||
|
this.frontmatterSchema = getMarkdownFrontmatterSchema(this.collection);
|
||||||
|
|
||||||
this.content = this.removeFrontmatter(originalContent);
|
this.content = this.removeFrontmatter(originalContent);
|
||||||
this.frontmatter = this.parseFrontmatter(originalContent);
|
this.frontmatter = this.parseFrontmatter(originalContent);
|
||||||
@@ -31,7 +38,7 @@ export class OpenEditPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let contentToSave = "";
|
let contentToSave = "";
|
||||||
if (this.frontmatter) {
|
if (this.hasFrontmatter()) {
|
||||||
contentToSave += "---\n";
|
contentToSave += "---\n";
|
||||||
contentToSave += yaml.dump(this.frontmatter);
|
contentToSave += yaml.dump(this.frontmatter);
|
||||||
contentToSave += "---\n\n";
|
contentToSave += "---\n\n";
|
||||||
@@ -54,31 +61,49 @@ export class OpenEditPage {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseFrontmatter(content: string): { [key: string]: string | string[] | Date } {
|
public supportsFrontmatter(): boolean {
|
||||||
|
return (this.fileType === "md" || this.fileType === "mdx") && !!this.collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private hasFrontmatter(): boolean {
|
||||||
|
return this.supportsFrontmatter() && Object.keys(this.frontmatter).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveContentCollection(): string | undefined {
|
||||||
|
const match = this.path.match(/^src\/content\/([^/]+)\//);
|
||||||
|
return match?.[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseFrontmatter(content: string): { [key: string]: FrontmatterValue } {
|
||||||
|
if (!this.supportsFrontmatter()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
const lines = content.split("\n");
|
const lines = content.split("\n");
|
||||||
let inFrontmatter = false;
|
let inFrontmatter = false;
|
||||||
const frontmatterLines: string[] = [];
|
const frontmatterLines: string[] = [];
|
||||||
|
|
||||||
for (const line of lines) {
|
if (lines[0]?.trim() !== "---") {
|
||||||
if (line.trim() === "---") {
|
return {};
|
||||||
if (inFrontmatter) {
|
|
||||||
break; // End of frontmatter
|
|
||||||
}
|
|
||||||
inFrontmatter = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (inFrontmatter) {
|
|
||||||
frontmatterLines.push(line);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (frontmatterLines.length === 0) {
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line.trim() === "---") {
|
||||||
|
inFrontmatter = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
inFrontmatter = true;
|
||||||
|
frontmatterLines.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inFrontmatter || frontmatterLines.length === 0) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// You'll need to install js-yaml: npm install js-yaml @types/js-yaml
|
return (yaml.load(frontmatterLines.join("\n")) || {}) as { [key: string]: FrontmatterValue };
|
||||||
return (yaml.load(frontmatterLines.join("\n")) || {}) as { [key: string]: string | string[] | Date };
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to parse YAML frontmatter:", error);
|
console.error("Failed to parse YAML frontmatter:", error);
|
||||||
return {};
|
return {};
|
||||||
@@ -86,21 +111,21 @@ export class OpenEditPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private removeFrontmatter(content: string): string {
|
private removeFrontmatter(content: string): string {
|
||||||
|
if (!this.supportsFrontmatter()) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
const lines = content.split("\n");
|
const lines = content.split("\n");
|
||||||
let inFrontmatter = false;
|
if (lines[0]?.trim() !== "---") {
|
||||||
const result: string[] = [];
|
return content;
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.trim() === "---") {
|
|
||||||
inFrontmatter = !inFrontmatter;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!inFrontmatter) {
|
|
||||||
result.push(line);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.join("\n").trim();
|
const endIndex = lines.slice(1).findIndex((line) => line.trim() === "---");
|
||||||
|
if (endIndex === -1) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.slice(endIndex + 2).join("\n").trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +161,7 @@ export class PageManager {
|
|||||||
public pagesLoad = $derived(get(pageRepo).listPages(this.branch).then(this.convertToTree).then(this._t(this.updater)));
|
public pagesLoad = $derived(get(pageRepo).listPages(this.branch).then(this.convertToTree).then(this._t(this.updater)));
|
||||||
public imagesLoad = $derived(get(pageRepo).listImages(this.branch).then(this._t(this.updater)));
|
public imagesLoad = $derived(get(pageRepo).listImages(this.branch).then(this._t(this.updater)));
|
||||||
|
|
||||||
private _t<T>(n: number): (v: T) => T {
|
private _t<T>(_n: number): (v: T) => T {
|
||||||
return (v: T) => v;
|
return (v: T) => v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
|
||||||
import { Button } from "@components/ui/button";
|
import { Button } from "@components/ui/button";
|
||||||
import { Check, ChevronsUpDown } from "lucide-svelte";
|
import { Check, ChevronsUpDown } from "@lucide/svelte";
|
||||||
import { cn } from "@components/utils";
|
import { cn } from "@components/utils";
|
||||||
import { dataRepo } from "@repo/data";
|
import { dataRepo } from "@repo/data";
|
||||||
import type { Player } from "@type/data";
|
import type { Player } from "@type/data";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Accordion as AccordionPrimitive } from "bits-ui";
|
import { Accordion as AccordionPrimitive } from "bits-ui";
|
||||||
import ChevronDown from "lucide-svelte/icons/chevron-down";
|
import ChevronDown from "@lucide/svelte/icons/chevron-down";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = AccordionPrimitive.TriggerProps;
|
type $$Props = AccordionPrimitive.TriggerProps;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Ellipsis from "lucide-svelte/icons/ellipsis";
|
import Ellipsis from "@lucide/svelte/icons/ellipsis";
|
||||||
import type { HTMLAttributes } from "svelte/elements";
|
import type { HTMLAttributes } from "svelte/elements";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { HTMLLiAttributes } from "svelte/elements";
|
import type { HTMLLiAttributes } from "svelte/elements";
|
||||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = HTMLLiAttributes & {
|
type $$Props = HTMLLiAttributes & {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { type VariantProps, tv } from "tailwind-variants";
|
import { type VariantProps, tv } from "tailwind-variants";
|
||||||
|
|
||||||
export const buttonVariants = tv({
|
export const buttonVariants = tv({
|
||||||
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
base: "ring-offset-background focus-visible:ring-ring inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
import ChevronRight from "lucide-svelte/icons/chevron-right";
|
import ChevronRight from "@lucide/svelte/icons/chevron-right";
|
||||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Calendar as CalendarPrimitive } from "bits-ui";
|
import { Calendar as CalendarPrimitive } from "bits-ui";
|
||||||
import ChevronLeft from "lucide-svelte/icons/chevron-left";
|
import ChevronLeft from "@lucide/svelte/icons/chevron-left";
|
||||||
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
import { buttonVariants } from "$lib/components/ui/button/index.js";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ArrowRight from "lucide-svelte/icons/arrow-right";
|
import ArrowRight from "@lucide/svelte/icons/arrow-right";
|
||||||
import type { VariantProps } from "tailwind-variants";
|
import type { VariantProps } from "tailwind-variants";
|
||||||
import { getEmblaContext } from "./context.js";
|
import { getEmblaContext } from "./context.js";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ArrowLeft from "lucide-svelte/icons/arrow-left";
|
import ArrowLeft from "@lucide/svelte/icons/arrow-left";
|
||||||
import type { VariantProps } from "tailwind-variants";
|
import type { VariantProps } from "tailwind-variants";
|
||||||
import { getEmblaContext } from "./context.js";
|
import { getEmblaContext } from "./context.js";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
bind:ref
|
bind:ref
|
||||||
class={cn(
|
class={cn(
|
||||||
"border-primary ring-offset-background focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground peer box-content size-4 shrink-0 rounded-sm border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50",
|
"border-primary ring-offset-background focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground peer box-content size-4 shrink-0 cursor-pointer rounded-sm border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
bind:checked
|
bind:checked
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Command as CommandPrimitive } from "bits-ui";
|
import { Command as CommandPrimitive } from "bits-ui";
|
||||||
import Search from "lucide-svelte/icons/search";
|
import Search from "@lucide/svelte/icons/search";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<CommandPrimitive.Item
|
<CommandPrimitive.Item
|
||||||
class={cn(
|
class={cn(
|
||||||
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
bind:ref
|
bind:ref
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<CommandPrimitive.LinkItem
|
<CommandPrimitive.LinkItem
|
||||||
class={cn(
|
class={cn(
|
||||||
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
bind:ref
|
bind:ref
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
import { ContextMenu as ContextMenuPrimitive } from "bits-ui";
|
||||||
import Check from "lucide-svelte/icons/check";
|
import Check from "@lucide/svelte/icons/check";
|
||||||
import { cn } from "$lib/components/utils.js";
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
type $$Props = ContextMenuPrimitive.CheckboxItemProps;
|
type $$Props = ContextMenuPrimitive.CheckboxItemProps;
|
||||||
type $$Events = ContextMenuPrimitive.CheckboxItemEvents;
|
|
||||||
|
|
||||||
let className: $$Props["class"] = undefined;
|
let className: $$Props["class"] = undefined;
|
||||||
export let checked: $$Props["checked"] = undefined;
|
export let checked: $$Props["checked"] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
@@ -14,7 +12,7 @@
|
|||||||
<ContextMenuPrimitive.CheckboxItem
|
<ContextMenuPrimitive.CheckboxItem
|
||||||
bind:checked
|
bind:checked
|
||||||
class={cn(
|
class={cn(
|
||||||
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
@@ -26,10 +24,12 @@
|
|||||||
on:pointerleave
|
on:pointerleave
|
||||||
on:pointermove
|
on:pointermove
|
||||||
>
|
>
|
||||||
|
{#snippet children({ checked })}
|
||||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<ContextMenuPrimitive.CheckboxIndicator>
|
{#if checked}
|
||||||
<Check class="h-4 w-4" />
|
<Check class="h-4 w-4" />
|
||||||
</ContextMenuPrimitive.CheckboxIndicator>
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
<slot />
|
<slot />
|
||||||
|
{/snippet}
|
||||||
</ContextMenuPrimitive.CheckboxItem>
|
</ContextMenuPrimitive.CheckboxItem>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user