126 Commits

Author SHA1 Message Date
Chaoscaot 1b391b193e Implement code changes to enhance functionality and improve performance
SteamWarCI Build failed
2025-04-14 18:18:40 +02:00
Chaoscaot c05c032e3f Fix Merge
SteamWarCI Build failed
2025-04-14 18:15:05 +02:00
Chaoscaot da6f741806 Trigger Rebuild
SteamWarCI Build successful
2025-04-14 17:55:39 +02:00
Chaoscaot 6b54791331 Merge pull request 'Merge branch sw-arcade' (#9) from sw-arcade into master
SteamWarCI Build failed
Reviewed-on: #9
Reviewed-by: YoyoNow <yoyonow@noreply.localhost>
2025-04-14 17:54:22 +02:00
Chaoscaot 36685bffd1 Fix wording in SteamWar Arcade event announcement for clarity
SteamWarCI Build successful
2025-04-14 17:53:46 +02:00
Chaoscaot caf9ea6cf1 Add SteamWar Arcade event image and update markdown file
SteamWarCI Build successful
2025-04-14 17:48:14 +02:00
Chaoscaot d505265910 Update sw-arcade.md with event details and correct creation date
SteamWarCI Build successful
2025-04-14 17:36:20 +02:00
Lixfel 78e1a7b726 Update wgs25-kampfplan.md
SteamWarCI Build successful
2025-04-12 21:29:10 +02:00
Chaoscaot cd485e8dda Update sw-arcade.md
SteamWarCI Build successful
2025-04-06 23:07:10 +02:00
Chaoscaot 182c402c7e Update sw-arcade.md
SteamWarCI Build failed
2025-04-06 23:05:35 +02:00
Chaoscaot 098f5b9270 Create page announcements/de/sw-arcade.md
SteamWarCI Build failed
2025-04-06 23:04:24 +02:00
Lixfel cf0c66c910 Update wgs25-kampfplan.md
SteamWarCI Build successful
2025-04-06 14:12:00 +02:00
Chaoscaot c8156ea47e Set fixed height for chart and disable aspect ratio maintenance
SteamWarCI Build successful
2025-04-05 15:21:36 +02:00
Chaoscaot 20a47ca6b6 Add favicon.svg to the public directory
SteamWarCI Build successful
2025-04-05 15:06:10 +02:00
Chaoscaot 2d601b9c4d Update schematic stats label and remove permission check
SteamWarCI Build successful
2025-04-02 09:58:11 +02:00
Chaoscaot 48586f1a50 Add error handling and improve file upload UX
SteamWarCI Build successful
2025-04-02 09:52:37 +02:00
Chaoscaot 7153cacbab Merge remote-tracking branch 'origin/master'
SteamWarCI Build successful
# Conflicts:
#	src/content/modes/missilewars.json
2025-04-02 09:40:05 +02:00
Chaoscaot 73cee211f2 Refactor referee management into standalone component 2025-04-02 09:39:58 +02:00
Chaoscaot 83074df7ef Refactor referee management into standalone component
SteamWarCI Build failed
2025-04-02 09:21:35 +02:00
Chaoscaot d1c926c093 Refactor referee management into standalone component
SteamWarCI Build failed
2025-04-02 09:20:36 +02:00
YoyoNow f8a16acfeb Remove shop
SteamWarCI Build successful
2025-04-02 09:00:37 +02:00
YoyoNow 9ca63cd286 Add shop.md
SteamWarCI Build successful
2025-03-31 22:30:26 +02:00
YoyoNow a2456c8b46 Merge remote-tracking branch 'origin/master'
SteamWarCI Build successful
2025-03-31 22:28:54 +02:00
YoyoNow 0952035091 Add shop.md 2025-03-31 22:28:51 +02:00
Lixfel 9c8c02f679 Update wgs25-kampfplan.md
SteamWarCI Build successful
2025-03-31 19:43:24 +02:00
Lixfel 3b5fdc57c0 Update wgs25-kampfplan.md
SteamWarCI Build successful
2025-03-25 06:40:14 +01:00
Lixfel 733c63946f Update wgs25-kampfplan.md
SteamWarCI Build successful
2025-03-25 06:32:35 +01:00
Chaoscaot fd846250ab Merge pull request 'Addapted script side for new example hotkey script' (#8) from add-example-script-to-downloads into master
SteamWarCI Build successful
Reviewed-on: #8
Reviewed-by: Chaoscaot <max@chaoscaot.de>
2025-03-22 10:36:18 +01:00
D4rkr34lm 17460772e9 Addapted script side for new example hotkey script
SteamWarCI Build successful
2025-03-21 18:54:26 +01:00
Lixfel 9a20860072 Update wgs25-kampfplan.md
SteamWarCI Build successful
2025-03-18 06:48:53 +01:00
YoyoNow 8f51723a3b Fix Graf Spee name
SteamWarCI Build successful
2025-03-14 13:49:36 +01:00
YoyoNow 8ad2f283aa Fixup things
SteamWarCI Build successful
2025-03-13 16:30:12 +01:00
YoyoNow 39f1af8b73 Add MissileWars.md
SteamWarCI Build successful
2025-03-13 16:27:48 +01:00
YoyoNow 266c4cb4ea Add MissileWars ranking to Navbar.svelte
SteamWarCI Build successful
2025-03-13 16:24:45 +01:00
YoyoNow f3df3c0000 Add ranked to Micro WarGear
SteamWarCI Build successful
2025-03-13 16:05:26 +01:00
Lixfel cb78fc598b Update wgs25-kampfplan.md
SteamWarCI Build successful
2025-03-09 16:52:42 +01:00
Chaoscaot ba7ecc1a8e Merge remote-tracking branch 'origin/master'
SteamWarCI Build successful
2025-03-04 23:33:17 +01:00
Chaoscaot 6ea92f9383 Tracking and adding LFS artifacts 2025-03-04 23:33:00 +01:00
Lixfel 998770bf59 Update wgs25-kampfplan.md
SteamWarCI Build successful
2025-03-04 07:24:58 +01:00
Chaoscaot a231032555 Add and standardize MissileWars translations and links
SteamWarCI Build successful
2025-03-02 16:21:43 +01:00
Chaoscaot 3aa3731bcb Add MissileWars mode config and leaderboard link
SteamWarCI Build successful
2025-03-02 16:18:37 +01:00
Chaoscaot 5e80c95bfd Add download link and update source URL in teamserver.json
SteamWarCI Build successful
2025-03-02 16:12:47 +01:00
Lixfel 09dc28b6da Update wgs25-kampfplan.md
SteamWarCI Build successful
2025-03-02 15:07:14 +01:00
Lixfel fd7cf716ca Update wgs25-kampfplan.md
SteamWarCI Build successful
2025-03-01 22:08:23 +01:00
Chaoscaot 73bd6a5e96 Fix Website
SteamWarCI Build successful
2025-03-01 20:07:09 +01:00
Chaoscaot 9c02cc1f4d Fix Website
SteamWarCI Build successful
2025-03-01 20:04:41 +01:00
Chaoscaot de8457fe45 Fix Website
SteamWarCI Build successful
2025-03-01 20:03:04 +01:00
Chaoscaot 4fbe01f987 New Dashboard 2025-03-01 20:01:04 +01:00
Chaoscaot 86d90e3fd2 New Dashboard 2025-03-01 20:00:46 +01:00
Chaoscaot bccd5eb5a0 Enhance request handling with token refresh and retries
SteamWarCI Build successful
2025-03-01 11:55:01 +01:00
Chaoscaot 53afe70b27 Refactor token refresh logic to streamline error handling.
SteamWarCI Build successful
2025-03-01 11:34:09 +01:00
Chaoscaot 4bbdaa06a9 Refactor auth handling to improve token refresh logic
SteamWarCI Build successful
2025-03-01 11:30:30 +01:00
Chaoscaot f03867b9a7 Add retry mechanism and limit for token requests
SteamWarCI Build successful
2025-03-01 11:27:06 +01:00
Chaoscaot 23e10eef0f Fix group filtering logic in FightTable.svelte
SteamWarCI Build successful
2025-03-01 11:15:39 +01:00
Lixfel 4c72f4f26b FIx MW3 creation date
SteamWarCI Build successful
2025-03-01 10:52:44 +01:00
Lixfel 624ba7f296 Merge pull request 'Merge branch wgs25-kampfplan' (#7) from wgs25-kampfplan into master
SteamWarCI Build successful
Reviewed-on: #7
2025-03-01 10:51:19 +01:00
Lixfel d7d20e4347 Update wgs25-kampfplan.md
SteamWarCI Build successful
2025-03-01 10:50:17 +01:00
Lixfel 43bd8f4a7c Update wgs25-kampfplan.md
SteamWarCI Build failed
2025-03-01 10:49:03 +01:00
Lixfel 18e8627b54 Update wgs25-kampfplan.md
SteamWarCI Build failed
2025-03-01 10:42:19 +01:00
Lixfel 0efc46c7e2 Create page announcements/de/wgs25-kampfplan.md
SteamWarCI Build failed
2025-03-01 09:53:50 +01:00
Chaoscaot 62fff0c0b2 Merge pull request 'Refactor authentication and implement password reset.' (#3) from develop/authv2 into master
SteamWarCI Build successful
Reviewed-on: #3
Reviewed-by: Lixfel <lixfel@noreply.localhost>
2025-02-25 22:39:40 +01:00
TheBreadBeard 86b479fb28 Update missilewars-iii-eventplan.md
SteamWarCI Build successful
2025-02-23 18:34:53 +01:00
Chaoscaot 489402292d Update adventskalender-schems.md
SteamWarCI Build successful
2025-02-23 18:31:10 +01:00
Chaoscaot b53ce04a75 Remove reset password functionality
SteamWarCI Build successful
2025-02-23 17:23:45 +01:00
Chaoscaot 069a9973a4 Add Gitea link and icon to navbar layout
SteamWarCI Build successful
2025-02-23 15:20:50 +01:00
Chaoscaot c3410de1d7 Refactor event handling to use Promises for better efficiency.
SteamWarCI Build successful
2025-02-23 12:25:56 +01:00
Chaoscaot a23c514102 Revert "Refactor event mounts and update script management."
This reverts commit bf8110af6c.
2025-02-23 12:20:34 +01:00
Chaoscaot bf8110af6c Refactor event mounts and update script management.
SteamWarCI Build successful
2025-02-23 12:18:58 +01:00
Chaoscaot 349f71af1c Add event listener for "astro:before-swap" in slug page
SteamWarCI Build successful
2025-02-23 12:14:11 +01:00
Chaoscaot dda37127ca Use type import and update page load event handling.
SteamWarCI Build successful
2025-02-23 09:59:37 +01:00
Chaoscaot 6d210eb0ff Merge pull request 'Merge branch missilewars-iii-eventplan' (#4) from missilewars-iii-eventplan into master
SteamWarCI Build successful
Reviewed-on: #4
2025-02-23 09:49:04 +01:00
Chaoscaot cfede8f299 Update missilewars-iii-eventplan.md
SteamWarCI Build successful
2025-02-23 09:47:30 +01:00
TheBreadBeard 597153ed39 Update missilewars-iii-eventplan.md
SteamWarCI Build successful
2025-02-23 07:43:22 +01:00
TheBreadBeard 697e903a26 Create page announcements/de/missilewars-iii-eventplan.md
SteamWarCI Build failed
2025-02-23 07:21:02 +01:00
Chaoscaot 1433784369 Update auth API endpoints to remove "/v2" prefix
SteamWarCI Build successful
2025-02-20 22:15:02 +01:00
Chaoscaot 2c63a33bda Refine token validation and update user stats endpoint.
SteamWarCI Build successful
Extend access token validation to include a 10-second buffer to prevent potential expiry issues. Modify the user stats API call to use the base `/stats/user` endpoint for improved consistency.
2025-02-18 00:09:06 +01:00
Chaoscaot 87265e5ccc Add "Repeat Password" label to i18n and form components
SteamWarCI Build successful
2025-02-17 18:32:54 +01:00
Chaoscaot 75f1a6528b Refactor authentication and implement password reset.
SteamWarCI Build successful
2025-02-17 18:29:17 +01:00
TheBreadBeard 23f35a35c4 Update missilewars-iii-ankündigung.md
SteamWarCI Build successful
2025-01-29 21:58:50 +01:00
TheBreadBeard 973f469c7b Update missilewars-iii-ankündigung.md
SteamWarCI Build successful
2025-01-29 21:58:04 +01:00
TheBreadBeard 107caafc26 Update missilewars-iii-ankündigung.md
SteamWarCI Build successful
2025-01-29 21:57:15 +01:00
TheBreadBeard 7f26845802 Update missilewars-iii-ankündigung.md
SteamWarCI Build successful
2025-01-29 21:54:28 +01:00
TheBreadBeard 37b2e82e05 Update missilewars-iii-ankündigung.md
SteamWarCI Build successful
2025-01-29 21:53:28 +01:00
TheBreadBeard 7e2ba9dbce Update missilewars-iii-ankündigung.md
SteamWarCI Build successful
2025-01-29 21:50:59 +01:00
TheBreadBeard 69426da5be Update missilewars-iii-ankündigung.md
SteamWarCI Build successful
2025-01-29 21:50:11 +01:00
TheBreadBeard fd2ad65ad4 Update missilewars-iii-ankündigung.md
SteamWarCI Build successful
2025-01-29 21:46:58 +01:00
TheBreadBeard a728651cca Update missilewars-iii-ankündigung.md
SteamWarCI Build successful
2025-01-29 21:46:04 +01:00
TheBreadBeard b9e73ed7d0 Update missilewars-iii-ankündigung.md
SteamWarCI Build successful
2025-01-29 21:38:36 +01:00
YoyoNow f1d55b3c99 Merge pull request 'Merge branch missilewars-iii-ankündigung' (#2) from missilewars-iii-ankündigung into master
SteamWarCI Build successful
Reviewed-on: #2
Reviewed-by: YoyoNow <yoyonow@noreply.localhost>
2025-01-29 20:27:43 +01:00
TheBreadBeard 9b49a0f81c Update missilewars-iii-ankündigung.md
SteamWarCI Build successful
2025-01-29 19:46:02 +01:00
TheBreadBeard 11144043c1 Update missilewars-iii-ankündigung.md
SteamWarCI Build successful
2025-01-29 19:39:12 +01:00
TheBreadBeard e8f866ce8a Update missilewars-iii-ankündigung.md
SteamWarCI Build failed
2025-01-29 19:28:01 +01:00
TheBreadBeard e57a90feaf Create page announcements/de/missilewars-iii-ankündigung.md
SteamWarCI Build failed
2025-01-29 19:19:43 +01:00
TheBreadBeard 3aa8fea1fd Update verhaltensrichtlinien.md
SteamWarCI Build successful
2025-01-29 18:59:19 +01:00
TheBreadBeard 95b327951c Update verhaltensrichtlienien.md
SteamWarCI Build successful
2025-01-29 18:51:55 +01:00
TheBreadBeard 86b99b4e76 Update miniwargear.md
SteamWarCI Build successful
2025-01-29 18:36:59 +01:00
Chaoscaot 14b31be465 Fix i18n MircoWG
SteamWarCI Build successful
2025-01-26 10:35:23 +01:00
Chaoscaot 341e629aaf Merge pull request 'Merge branch MicroRW' (#1) from MicroRW into master
SteamWarCI Build successful
Reviewed-on: #1
2025-01-26 10:33:54 +01:00
Friderik 694ded4c61 Update microwargear.md
SteamWarCI Build successful
2025-01-26 10:32:11 +01:00
Chaoscaot a75b5b7c09 Fix Branch Creation
SteamWarCI Build successful
2025-01-26 10:24:20 +01:00
Chaoscaot 9146f65455 Fix Padding
SteamWarCI Build successful
2025-01-21 14:57:05 +01:00
Chaoscaot 254807efa6 Rebuild
SteamWarCI Build successful
2025-01-20 23:06:52 +01:00
Chaoscaot 36931aabb1 Fixes and Upgrade to Astro 5
SteamWarCI Build failed
2025-01-20 23:04:34 +01:00
Chaoscaot 628599f019 Fix Login Page and add Jahresplan
SteamWarCI Build successful
2025-01-20 19:21:21 +01:00
Chaoscaot 0a6c61bd88 Fix Upload
SteamWarCI Build successful
2025-01-20 17:42:08 +01:00
Chaoscaot 8bbad8b3cc Fix CI
SteamWarCI Build successful
2025-01-20 15:50:17 +01:00
Chaoscaot 5af6176889 Fix CI
SteamWarCI Build failed
2025-01-20 15:49:59 +01:00
Chaoscaot 9250dd5088 Fix CI
SteamWarCI Build failed
2025-01-20 15:49:36 +01:00
Chaoscaot 276e19409d Fix CI
SteamWarCI Build failed
2025-01-20 15:48:49 +01:00
Chaoscaot 11fa9fa126 Fix CI
SteamWarCI Build failed
2025-01-20 15:46:46 +01:00
Chaoscaot 17ec6023a9 Fix CI
SteamWarCI Build failed
2025-01-20 15:45:43 +01:00
Chaoscaot 3c7c899868 Fix CI
SteamWarCI Build failed
2025-01-20 15:41:59 +01:00
Chaoscaot 9cb161e470 Fix CI
SteamWarCI Build failed
2025-01-20 15:38:58 +01:00
Chaoscaot 7fc7c2a6eb Fix CI
SteamWarCI Build failed
2025-01-20 15:37:28 +01:00
Chaoscaot 2fce94d46b Fix CI
SteamWarCI Build failed
2025-01-20 15:35:21 +01:00
Chaoscaot 6356c9911a Fix CI
SteamWarCI Build failed
2025-01-20 15:34:38 +01:00
Chaoscaot 2402896fd5 Fix CI
SteamWarCI Build failed
2025-01-20 15:33:12 +01:00
Chaoscaot 2940304492 Fix CI
SteamWarCI Build failed
2025-01-20 15:32:20 +01:00
Chaoscaot 4c0a237b27 Merge remote-tracking branch 'origin/master'
SteamWarCI Build failed
2025-01-20 15:26:55 +01:00
Chaoscaot 77b8b41afb Fix CodeMirror 2025-01-20 15:19:18 +01:00
Chaoscaot 163d049829 astro.config.mjs aktualisiert
SteamWarCI Build failed
2025-01-20 15:06:44 +01:00
Chaoscaot a321b12680 Merge remote-tracking branch 'origin/master'
SteamWarCI Build failed
2025-01-20 15:05:42 +01:00
Chaoscaot feba5a5b4a Fix CodeMirror 2025-01-20 15:05:33 +01:00
Chaoscaot faaf5f1852 steamwarci.yml aktualisiert
SteamWarCI Build failed
2025-01-20 15:03:34 +01:00
Chaoscaot 18997e1384 tailwind.config.cjs aktualisiert
SteamWarCI Build failed
2025-01-20 15:02:11 +01:00
Chaoscaot fdc7bb93dd Add some stuff 2025-01-19 17:58:26 +01:00
334 changed files with 11020 additions and 3229 deletions
+2 -4
View File
@@ -5,10 +5,8 @@ import configureI18n from "./astro-i18n.adapter";
import sitemap from "@astrojs/sitemap";
import robotsTxt from "astro-robots-txt";
import {resolve} from "node:url";
import path from "node:path";
import mdx from "@astrojs/mdx";
import pagefind from "astro-pagefind";
// https://astro.build/config
export default defineConfig({
@@ -21,9 +19,8 @@ export default defineConfig({
integrations: [
svelte(),
tailwind({
configFile: "./tailwind.config.cjs",
configFile: "./tailwind.config.js",
}),
pagefind(),
configureI18n(),
sitemap({
i18n: {
@@ -69,6 +66,7 @@ export default defineConfig({
"@layouts": path.resolve("./src/layouts"),
"@repo": path.resolve("./src/components/repo"),
"@stores": path.resolve("./src/components/stores"),
"$lib": path.resolve("./src"),
},
},
},
+17
View File
@@ -0,0 +1,17 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.js",
"css": "src\\styles\\app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/components/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks"
},
"typescript": true,
"registry": "https://next.shadcn-svelte.com/registry"
}
+22 -9
View File
@@ -14,40 +14,54 @@
"i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types",
"clean:dist": "rm -rf dist",
"clean:node_modules": "rm -rf node_modules",
"ci": "pnpm run clean:dist && pnpm install && pnpm run i18n:sync && pnpm run build"
"ci": "pnpm install && pnpm run i18n:sync && pnpm run build"
},
"devDependencies": {
"@astrojs/svelte": "^6.0.2",
"@astrojs/tailwind": "^5.1.2",
"@astrojs/svelte": "^7.0.4",
"@astrojs/tailwind": "^5.1.5",
"@astropub/icons": "^0.2.0",
"@internationalized/date": "^3.7.0",
"@types/color": "^4.2.0",
"@types/node": "^22.9.3",
"@types/three": "^0.170.0",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"autoprefixer": "^10.4.20",
"bits-ui": "1.3.4",
"clsx": "^2.1.1",
"cmdk-sv": "^0.0.18",
"cssnano": "^7.0.6",
"embla-carousel-svelte": "^8.5.2",
"esbuild": "^0.24.0",
"eslint": "^9.15.0",
"eslint-plugin-astro": "^1.3.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-svelte": "^2.46.0",
"formsnap": "1.0.1",
"lucide-svelte": "^0.476.0",
"mode-watcher": "^0.5.1",
"paneforge": "^0.0.6",
"postcss-nesting": "^13.0.1",
"sass": "^1.81.0",
"svelte": "^5.16.0",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.5.5",
"tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.15",
"three": "^0.170.0",
"typescript": "^5.7.2"
"typescript": "^5.7.2",
"vaul-svelte": "^0.3.2",
"zod": "^3.23.8"
},
"dependencies": {
"@astrojs/mdx": "^3.1.9",
"@astrojs/mdx": "^4.0.7",
"@astrojs/sitemap": "^3.2.1",
"@codemirror/commands": "^6.8.0",
"@codemirror/lang-json": "^6.0.1",
"@ddietr/codemirror-themes": "^1.4.4",
"astro": "^4.16.14",
"@tanstack/table-core": "^8.21.2",
"astro": "^5.1.8",
"astro-i18n": "^2.2.4",
"astro-pagefind": "^1.6.0",
"astro-robots-txt": "^1.0.0",
"astro-seo": "^0.8.4",
"chart.js": "^4.4.6",
@@ -63,7 +77,6 @@
"sharp": "^0.33.5",
"svelte-awesome": "^3.3.5",
"svelte-codemirror-editor": "^1.4.1",
"svelte-spa-router": "^4.0.1",
"zod": "^3.23.8"
"svelte-spa-router": "^4.0.1"
}
}
+3106 -2648
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 406 KiB

+21
View File
@@ -0,0 +1,21 @@
{
"name": "MyWebSite",
"short_name": "MySite",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+1 -4
View File
@@ -20,6 +20,7 @@
<script lang="ts">
import {t} from "astro-i18n";
import {statsRepo} from "@repo/stats.ts";
import "@styles/table.css"
interface Props {
@@ -64,7 +65,3 @@
<p>{error.message}</p>
{/await}
<style>
@import "../styles/table.css";
</style>
+3 -1
View File
@@ -79,6 +79,8 @@
})
},
options: {
maintainAspectRatio: false,
scales: {
x: {
type: "time",
@@ -105,5 +107,5 @@
</script>
<div>
<canvas bind:this={canvas}></canvas>
<canvas height="500" bind:this={canvas}></canvas>
</div>
+2 -2
View File
@@ -19,7 +19,7 @@
-->
<script lang="ts">
import {window} from "./util.ts";
import {window} from "./utils.ts";
import {astroI18n, t} from "astro-i18n";
import type {EventFight, ExtendedEvent} from "@type/event";
import "@styles/table.css";
@@ -55,7 +55,7 @@
</tr>
</thead>
<tbody>
{#each window(event.fights.filter(f => f.group === group), rows) as fights}
{#each window(event.fights.filter(f => group === undefined ? true : f.group === group), rows) as fights}
<tr>
{#each fights as fight (fight.id)}
<td>{Intl.DateTimeFormat(astroI18n.locale, {
+1 -1
View File
@@ -19,7 +19,7 @@
-->
<script lang="ts">
import {window} from "./util.ts";
import {window} from "./utils.ts";
import {t} from "astro-i18n";
import type {ExtendedEvent} from "@type/event.ts";
import "@styles/table.css"
+5 -9
View File
@@ -31,8 +31,7 @@
let error: string = $state("");
async function login() {
let {tokenStore} = await import("./repo/repo.ts");
let {authRepo} = await import("./repo/auth.ts");
let {authV2Repo} = await import("./repo/authv2.ts");
if (username === "" || pw === "") {
pw = "";
error = t("login.error");
@@ -40,15 +39,14 @@
}
try {
let auth = await get(authRepo).login(username, pw);
if (auth == undefined) {
let auth = await get(authV2Repo).login(username, pw);
if (!auth) {
pw = "";
error = t("login.error");
return;
}
tokenStore.set(auth);
navigate(l("/dashboard"));
await navigate(l("/dashboard"));
} catch (e: any) {
pw = "";
error = t("login.error");
@@ -75,9 +73,7 @@
<style lang="postcss">
input {
@apply border-2 rounded-md p-2 shadow-2xl w-80
dark:bg-neutral-800
focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:border-transparent;
@apply border-2 rounded-md p-2 shadow-2xl w-80 dark:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:border-transparent text-black;
}
label {
+89 -128
View File
@@ -18,133 +18,94 @@
-->
<script lang="ts">
import "../styles/button.css";
import { CaretDownOutline, SearchOutline } from "flowbite-svelte-icons";
import { t } from "astro-i18n";
import { l } from "../util/util";
import { onMount } from "svelte";
import { loggedIn } from "@repo/authv2.ts";
import "../styles/button.css";
import {CaretDownOutline, SearchOutline} from "flowbite-svelte-icons";
import {t} from "astro-i18n";
import {l} from "../util/util";
import {onMount} from "svelte";
import {loggedIn} from "@repo/authv2.ts";
interface Props {
logo?: import("svelte").Snippet;
logo?: import('svelte').Snippet;
}
let { logo }: Props = $props();
let navbar = $state<HTMLDivElement>();
let searchOpen = $state(false);
let navbar = $state<HTMLDivElement>();
let searchOpen = $state(false);
let accountBtn = $state<HTMLAnchorElement>();
let accountBtn = $state<HTMLAnchorElement>();
$effect(() => {
if ($loggedIn) {
accountBtn!.href = l("/dashboard");
} else {
accountBtn!.href = l("/login");
$effect(() => {
if ($loggedIn) {
accountBtn!.href = l("/dashboard");
} else {
accountBtn!.href = l("/login");
}
})
onMount(() => {
handleScroll();
})
function handleScroll() {
if (window.scrollY > 0) {
navbar!.classList.add("before:scale-y-100");
} else {
navbar!.classList.remove("before:scale-y-100");
}
}
});
onMount(() => {
handleScroll();
});
function handleScroll() {
if (window.scrollY > 0) {
navbar!.classList.add("before:scale-y-100");
} else {
navbar!.classList.remove("before:scale-y-100");
}
}
</script>
<svelte:window onscroll={handleScroll} />
<svelte:window onscroll={handleScroll}/>
<nav
data-pagefind-ignore
class="fixed top-0 left-0 right-0 sm:px-4 py-1 transition-colors z-10 flex justify-center before:backdrop-blur before:shadow-2xl before:absolute before:top-0 before:left-0 before:bottom-0 before:right-0 before:-z-10 before:scale-y-0 before:transition-transform before:origin-top"
bind:this={navbar}
>
<div
class="flex flex-row items-center justify-evenly md:justify-between match"
>
<a class="flex items-center" href={l("/")}>
{@render logo?.()}
<span
class="text-2xl uppercase font-bold dark:text-white hidden md:inline-block"
>
{t("navbar.title")}
<span
class="before:scale-y-100"
style="display: none"
aria-hidden="true"
></span>
</span>
</a>
<div class="flex justify-center flex-wrap">
<div class="btn-dropdown">
<button class="btn btn-gray">
<a href={l("/")}>
<span class="btn__text">{t("navbar.links.home.title")}</span>
</a>
<CaretDownOutline class="ml-2 mt-auto" />
</button>
<div>
<a class="btn btn-gray" href={l("/announcements")}
>{t("navbar.links.home.announcements")}</a
>
<a class="btn btn-gray" href={l("/downloads")}
>{t("navbar.links.home.downloads")}</a
>
<a class="btn btn-gray" href={l("/tutorials")}
>{t("navbar.links.home.tutorials")}</a
>
<a class="btn btn-gray" href={l("/faq")}
>{t("navbar.links.home.faq")}</a
>
<a class="btn btn-gray" href={l("/code-of-conduct")}
>{t("navbar.links.rules.coc")}</a
>
</div>
</div>
<div class="btn-dropdown">
<button class="btn btn-gray">
<a rel="prefetch" href={l("/rules")}>
<span class="btn__text">{t("navbar.links.rules.title")}</span>
</a>
<CaretDownOutline class="ml-2 mt-auto" />
</button>
<div>
<a href={l("/rules/wargear")} class="btn btn-gray"
>{t("navbar.links.rules.wg")}</a
>
<a href={l("/rules/miniwargear")} class="btn btn-gray"
>{t("navbar.links.rules.mwg")}</a
>
<a href={l("/rules/warship")} class="btn btn-gray"
>{t("navbar.links.rules.ws")}</a
>
<a href={l("/rules/airship")} class="btn btn-gray"
>{t("navbar.links.rules.as")}</a
>
<a href={l("/rules/quickgear")} class="btn btn-gray"
>{t("navbar.links.rules.qg")}</a
>
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.rotating")}</h2>
<a href={l("/rules/megawargear")} class="btn btn-gray"
>{t("navbar.links.rules.megawg")}</a
>
<a href={l("/rules/microwargear")} class="btn btn-gray"
>{t("navbar.links.rules.micro")}</a
>
<a href={l("/rules/streetfight")} class="btn btn-gray"
>{t("navbar.links.rules.sf")}</a
>
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.ranked")}</h2>
<a href={l("/rangliste/missilewars")} class="btn btn-gray"
>{t("navbar.links.ranked.mw")}</a
>
</div>
</div>
<!-- TODO: Add help center
<nav data-pagefind-ignore class="fixed top-0 left-0 right-0 sm:px-4 py-1 transition-colors z-10 flex justify-center before:backdrop-blur before:shadow-2xl before:absolute before:top-0 before:left-0 before:bottom-0 before:right-0 before:-z-10 before:scale-y-0 before:transition-transform before:origin-top" bind:this={navbar}>
<div class="flex flex-row items-center justify-evenly md:justify-between match">
<a class="flex items-center" href={l("/")}>
{@render logo?.()}
<span class="text-2xl uppercase font-bold dark:text-white hidden md:inline-block">
{t("navbar.title")}
<span class="before:scale-y-100" style="display: none" aria-hidden="true"></span>
</span>
</a>
<div class="flex justify-center flex-wrap">
<div class="btn-dropdown">
<button class="btn btn-gray">
<a href={l("/")}>
<span class="btn__text">{t("navbar.links.home.title")}</span>
</a>
<CaretDownOutline class="ml-2 mt-auto"/>
</button>
<div>
<a class="btn btn-gray" href={l("/announcements")}>{t("navbar.links.home.announcements")}</a>
<a class="btn btn-gray" href={l("/downloads")}>{t("navbar.links.home.downloads")}</a>
<a class="btn btn-gray" href={l("/faq")}>{t("navbar.links.home.faq")}</a>
<a class="btn btn-gray" href={l("/code-of-conduct")}>{t("navbar.links.rules.coc")}</a>
</div>
</div>
<div class="btn-dropdown">
<button class="btn btn-gray">
<a rel="prefetch" href={l("/rules")}>
<span class="btn__text">{t("navbar.links.rules.title")}</span>
</a>
<CaretDownOutline class="ml-2 mt-auto"/>
</button>
<div>
<a href={l("/rules/wargear")} class="btn btn-gray">{t("navbar.links.rules.wg")}</a>
<a href={l("/rules/miniwargear")} class="btn btn-gray">{t("navbar.links.rules.mwg")}</a>
<a href={l("/rules/warship")} class="btn btn-gray">{t("navbar.links.rules.ws")}</a>
<a href={l("/rules/airship")} class="btn btn-gray">{t("navbar.links.rules.as")}</a>
<a href={l("/rules/quickgear")} class="btn btn-gray">{t("navbar.links.rules.qg")}</a>
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.rotating")}</h2>
<a href={l("/rules/megawargear")}
class="btn btn-gray">{t("navbar.links.rules.megawg")}</a>
<a href={l("/rules/microwargear")}
class="btn btn-gray">{t("navbar.links.rules.micro")}</a>
<a href={l("/rules/streetfight")} class="btn btn-gray">{t("navbar.links.rules.sf")}</a>
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.ranked")}</h2>
<a href={l("/rangliste/missilewars")} class="btn btn-gray">{t("navbar.links.ranked.mw")}</a>
</div>
</div>
<!-- TODO: Add help center
<div class="btn-dropdown my-1">
<div class="btn btn-gray" tabindex="1">
<a rel="prefetch">
@@ -158,26 +119,26 @@
</div>
</div>
-->
<a class="btn" href={l("/login")} bind:this={accountBtn}>
<span class="btn__text">{t("navbar.links.account")}</span>
</a>
<!--
<a class="btn" href={l("/login")} bind:this={accountBtn}>
<span class="btn__text">{t("navbar.links.account")}</span>
</a>
<!--
<button class="btn my-1" onclick={() => searchOpen = true}>
<SearchOutline ariaLabel="Site Search" class="inline-block h-6"/>
</button>
-->
</div>
</div>
</div>
</nav>
{#if searchOpen}
{#await import("./SearchComponent.svelte") then c}
<c.default bind:open={searchOpen} />
{/await}
{#await import("./SearchComponent.svelte") then c}
<c.default bind:open={searchOpen} />
{/await}
{/if}
<style lang="scss">
.match {
width: min(100vw, 70em);
}
</style>
.match {
width: min(100vw, 70em);
}
</style>
+1 -1
View File
@@ -1,5 +1,5 @@
---
import {CollectionEntry} from "astro:content";
import type {CollectionEntry} from "astro:content";
import {l} from "../util/util";
import {astroI18n} from "astro-i18n";
import {Image} from "astro:assets";
+10 -8
View File
@@ -22,38 +22,40 @@
import wrap from "svelte-spa-router/wrap";
import Router, {replace} from "svelte-spa-router";
import {get} from "svelte/store";
import {tokenStore} from "@repo/repo";
import {loggedIn} from "@repo/authv2.ts";
const routes: RouteDefinition = {
"/": wrap({asyncComponent: () => import("./pages/Home.svelte"), conditions: detail => get(tokenStore) != ""}),
"/": wrap({asyncComponent: () => import("./pages/Home.svelte"), conditions: detail => get(loggedIn)}),
"/perms": wrap({
asyncComponent: () => import("./pages/Perms.svelte"),
conditions: detail => get(tokenStore) != ""
conditions: detail => get(loggedIn)
}),
"/login": wrap({
asyncComponent: () => import("./pages/Login.svelte"),
conditions: detail => get(tokenStore) == ""
conditions: detail => !get(loggedIn)
}),
"/event/:id": wrap({
asyncComponent: () => import("./pages/Event.svelte"),
conditions: detail => get(tokenStore) != ""
conditions: detail => get(loggedIn)
}),
"/event/:id/generate": wrap({
asyncComponent: () => import("./pages/Generate.svelte"),
conditions: detail => get(tokenStore) != ""
conditions: detail => get(loggedIn)
}),
"/edit": wrap({
asyncComponent: () => import("./pages/Edit.svelte"),
conditions: detail => get(tokenStore) != ""
conditions: detail => get(loggedIn)
}),
"/display/:event": wrap({
asyncComponent: () => import("./pages/Display.svelte"),
conditions: detail => get(tokenStore) != ""
conditions: detail => get(loggedIn)
}),
"*": wrap({asyncComponent: () => import("./pages/NotFound.svelte")})
};
function conditionsFailed(event: ConditionsFailedEvent) {
console.log(event)
if (event.detail.location === "/login") {
replace("/");
} else {
+2 -2
View File
@@ -168,11 +168,11 @@
</div>
<div>
{#if selectedBranch !== "master"}
<Button onclick={createFile} color="alternative" disabled={!selectedPath}>Create File
<Button onclick={() => createFile()} color="alternative" disabled={!selectedPath}>Create File
</Button>
<Button onclick={() => deleteBranch(false)} color="none">Delete Branch</Button>
{:else}
<Button onclick={createBranch}>Create Branch</Button>
<Button onclick={() => createBranch()}>Create Branch</Button>
{/if}
</div>
</div>
+1
View File
@@ -38,6 +38,7 @@
</NavBrand>
<NavHamburger onclick={toggle}/>
<NavUl {hidden}>
<NavLi href="/admin/new">New UI</NavLi>
<NavLi href="#/edit">Edit Pages</NavLi>
<NavLi href="#/perms">Permissions</NavLi>
</NavUl>
@@ -34,7 +34,7 @@
dirty?: boolean;
}
let { pageId, branch, dirty = $bindable(false) }: Props = $props();
let { pageId, branch = $bindable(), dirty = $bindable(false) }: Props = $props();
let dispatcher = createEventDispatcher();
@@ -97,7 +97,7 @@
{#if page?.name.endsWith("md") || page?.name.endsWith("mdx")}
<MDEMarkdownEditor bind:value={pageContent} bind:dirty/>
{:else}
<CodeMirror bind:value={pageContent} lang={json()} theme={materialDark} on:change={() => dirty = true}/>
<CodeMirror bind:value={pageContent} lang={json()} theme={materialDark} onchange={() => dirty = true}/>
{/if}
</div>
{:catch error}
+2 -5
View File
@@ -31,7 +31,7 @@
let request = getRequest();
function getRequest() {
return $statsRepo.getUserStats(user.id)
return $statsRepo.getUserStats(user.uuid)
}
</script>
@@ -43,8 +43,5 @@
maximumFractionDigits: 2
}).format(data.playtime)})}h</p>
<p>{t("dashboard.stats.fights", {fights: data.fights})}</p>
{#if user.perms.includes("CHECK")}
<p>{t("dashboard.stats.checked", {checked: data.acceptedSchematics})}</p>
{/if}
{:catch error}
<p>{t("dashboard.stats.checked", {checked: data.acceptedSchematics})}</p>
{/await}
+35 -19
View File
@@ -21,19 +21,21 @@
import {createEventDispatcher} from "svelte";
import {schemRepo} from "@repo/schem.ts";
import SWModal from "@components/styled/SWModal.svelte";
import {t} from "astro-i18n"
import {t} from "astro-i18n";
const dispatch = createEventDispatcher();
interface Props {
open?: boolean;
}
interface Props {
open?: boolean;
}
let { open = $bindable(false) }: Props = $props();
let {open = $bindable(false)}: Props = $props();
async function upload() {
async function upload(e: Event) {
e.stopPropagation();
if (uploadFile == null) {
return
error = "dashboard.schematic.errors.noFile";
return;
}
let file = uploadFile[0];
@@ -42,32 +44,46 @@
let type = name.split(".").pop();
if (type !== "schem" && type !== "schematic") {
return
error = "dashboard.schematic.errors.invalidEnding";
return;
}
let content = await file.arrayBuffer();
// @ts-ignore
let b64 = btoa(String.fromCharCode.apply(null, new Uint8Array(content)));
await $schemRepo.uploadSchematic(name, b64);
try {
await $schemRepo.uploadSchematic(name, b64);
open = false;
uploadFile = null;
dispatch("reset")
open = false;
value = "";
dispatch("reset");
} catch (e) {
error = "dashboard.schematic.errors.upload";
}
}
function reset(e: Event) {
e.stopPropagation();
open = false
value = "";
}
let uploadFile: FileList | null = $state(null);
let value = $state("");
let error = $state(null)
</script>
<SWModal title={t("dashboard.schematic.title")} bind:open>
<form>
<input type="file" bind:files={uploadFile} />
<label for="schem-upload">{t("dashboard.schematic.title")}</label>
<input type="file" id="schem-upload" bind:files={uploadFile} class="overflow-ellipsis" bind:value accept=".schem, .schematic"/>
{#if error !== null}
<p class="text-red-400">{t(error)}</p>
{/if}
</form>
{#snippet footer()}
<button class="btn !ml-auto" onclick={upload}>{t("dashboard.schematic.upload")}</button>
<button class="btn btn-gray" onclick={() => open = false}>{t("dashboard.schematic.cancel")}</button>
{/snippet}
<button class="btn" onclick={upload}>{t("dashboard.schematic.upload")}</button>
<button class="btn btn-gray" onclick={reset}>{t("dashboard.schematic.cancel")}</button>
{/snippet}
</SWModal>
+5 -6
View File
@@ -22,9 +22,9 @@
import type {Player} from "@type/data.ts";
import {l} from "@utils/util.ts";
import Statistics from "./Statistics.svelte";
import {authRepo} from "@repo/auth.ts";
import {tokenStore} from "@repo/repo.ts";
import {authV2Repo} from "@repo/authv2.ts";
import Card from "@components/Card.svelte";
import {navigate} from "astro:transitions/client";
interface Props {
user: Player;
@@ -33,9 +33,8 @@
let { user }: Props = $props();
async function logout() {
await $authRepo.logout()
tokenStore.set("")
window.location.href = l("/login")
await $authV2Repo.logout();
await navigate(l("/login"));
}
</script>
@@ -56,7 +55,7 @@
</div>
<div>
<h1 class="text-4xl font-bold">{t("dashboard.title", {name: user.name})}</h1>
<p>{t("dashboard.rank", {rank: t("home.prefix." + user.prefix)})}</p>
<p>{t("dashboard.rank", {rank: t("home.prefix." + (user.prefix || "User"))})}</p>
<Statistics {user} />
</div>
</div>
+56
View File
@@ -0,0 +1,56 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import type {RouteDefinition} from "svelte-spa-router";
import Router from "svelte-spa-router";
import NavLinks from "@components/moderator/layout/NavLinks.svelte";
import {Switch} from "@components/ui/switch";
import {Label} from "@components/ui/label";
import {navigate} from "astro:transitions/client";
import Players from "@components/moderator/pages/players/Players.svelte";
import Events from "@components/moderator/pages/events/Events.svelte";
import Dashboard from "@components/moderator/pages/dashboard/Dashboard.svelte";
import Event from "@components/moderator/pages/event/Event.svelte";
const routes: RouteDefinition = {
"/": Dashboard,
"/events": Events,
"/players": Players,
"/event/:id": Event
};
</script>
<div class="flex flex-col bg-background min-w-full min-h-screen">
<div class="border-b">
<div class="flex h-16 items-center px-4">
<a href="/" class="text-sm font-bold transition-colors text-primary">
SteamWar
</a>
<NavLinks />
<div class="ml-auto flex items-center space-x-4">
<Switch id="new-ui-switch" checked={true} on:click={() => navigate("/admin")} />
<Label for="new-ui-switch">New UI!</Label>
</div>
</div>
</div>
<main class="flex flex-col">
<Router {routes} />
</main>
</div>
@@ -0,0 +1,42 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import type {ShortEvent} from "@type/event.ts";
import {Card, CardContent, CardHeader, CardTitle} from "@components/ui/card";
let { event }: { event: ShortEvent } = $props();
let sameDate = $derived(new Intl.DateTimeFormat().format(event.start) === new Intl.DateTimeFormat().format(event.end));
</script>
<Card>
<CardHeader>
<CardTitle>{event.name}</CardTitle>
</CardHeader>
<CardContent>
{#if !sameDate}
<p>Startet: {new Intl.DateTimeFormat().format(event.start)}</p>
<p>Endet: {new Intl.DateTimeFormat().format(event.end)}</p>
{:else}
<p>Am: {new Intl.DateTimeFormat().format(event.start)}</p>
<p>&nbsp;</p>
{/if}
</CardContent>
</Card>
@@ -0,0 +1,40 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {location} from "svelte-spa-router";
</script>
<nav class="flex items-center space-x-4 lg:space-x-6 mx-6">
<a href="#/" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/"}>
Dashboard
</a>
<a href="#/events" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/events"}>
Events
</a>
<a href="#/players" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/players"}>
Players
</a>
<a href="#/pages" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/pages"}>
Pages
</a>
<a href="#/schematics" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/schematics"}>
Schematics
</a>
</nav>
@@ -0,0 +1,22 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<div class="p-4">
<h1 class="font-bold text-xl">SteamWar Dashboard</h1>
</div>
@@ -0,0 +1,38 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {eventRepo} from "@repo/event.ts";
import EventView from "@components/moderator/pages/event/EventView.svelte";
interface Props {
params: { id: number };
}
let { params }: Props = $props();
let id = params.id;
let event = $eventRepo.getEvent(id.toString());
</script>
{#await event}
<p>Loading...</p>
{:then data}
<EventView event={data} />
{/await}
@@ -0,0 +1,128 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {Input} from "@components/ui/input";
import {Label} from "@components/ui/label";
import {Popover, PopoverContent, PopoverTrigger} from "@components/ui/popover";
import type {SWEvent} from "@type/event.ts"
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
import {fromAbsolute} from "@internationalized/date";
import {Button} from "@components/ui/button";
import {ChevronsUpDown} from "lucide-svelte";
import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList} from "@components/ui/command";
import {schemTypes} from "@stores/stores.ts";
import Check from "lucide-svelte/icons/check";
import {cn} from "@components/utils.ts";
import {Switch} from "@components/ui/switch";
import {eventRepo} from "@repo/event.ts";
const { event }: { event: SWEvent } = $props();
let rootEvent: SWEvent = $state(event)
let eventName = $state(rootEvent.name);
let eventDeadline = $state(fromAbsolute(rootEvent.deadline, "Europe/Berlin"));
let eventStart = $state(fromAbsolute(rootEvent.start, "Europe/Berlin"));
let eventEnd = $state(fromAbsolute(rootEvent.end, "Europe/Berlin"));
let eventTeamSize = $state(rootEvent.maxTeamMembers);
let eventSchematicType = $state(rootEvent.schemType);
let eventPublicsOnly = $state(rootEvent.publicSchemsOnly);
let dirty = $derived(eventName !== rootEvent.name ||
eventDeadline.toDate().getTime() !== rootEvent.deadline ||
eventStart.toDate().getTime() !== rootEvent.start ||
eventEnd.toDate().getTime() !== rootEvent.end ||
eventTeamSize !== rootEvent.maxTeamMembers ||
eventSchematicType !== rootEvent.schemType ||
eventPublicsOnly !== rootEvent.publicSchemsOnly);
async function updateEvent() {
rootEvent = await $eventRepo.updateEvent(event.id.toString(), {
name: eventName,
deadline: eventDeadline.toDate().getTime(),
start: eventStart.toDate().getTime(),
end: eventEnd.toDate().getTime(),
maxTeamMembers: eventTeamSize,
schemType: eventSchematicType,
publicSchemsOnly: eventPublicsOnly,
})
}
</script>
<div class="flex flex-col gap-2">
<Label for="event-name">Name</Label>
<Input id="event-name" bind:value={eventName} />
<Label>Deadline</Label>
<DateTimePicker bind:value={eventDeadline} />
<Label>Start</Label>
<DateTimePicker bind:value={eventStart} />
<Label>End</Label>
<DateTimePicker bind:value={eventEnd} />
<Label for="event-size">Teamsize</Label>
<Input id="event-size" bind:value={eventTeamSize} type="number" />
<Label>Schematic Type</Label>
<Popover>
<PopoverTrigger>
{#snippet child({ props })}
<Button
variant="outline"
class="justify-between"
{...props}
role="combobox"
>
{$schemTypes.find(value => value.db === eventSchematicType)?.name || eventSchematicType || "Select a schematic type..."}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search schematic types..." />
<CommandList>
<CommandEmpty>No schematic type found.</CommandEmpty>
<CommandGroup>
{#each $schemTypes as type}
<CommandItem
value={type.db}
onSelect={() => {
eventSchematicType = type.db;
}}
>
<Check
class={cn(
"mr-2 size-4",
eventSchematicType !== type.db && "text-transparent"
)}
/>
{type.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Label for="event-publics">Publics Schematics Only</Label>
<Switch id="event-publics" bind:checked={eventPublicsOnly} />
<div class="flex flex-row justify-end border-t pt-2 gap-4">
<Button variant="destructive">Delete</Button>
<Button disabled={!dirty} onclick={updateEvent}>Update</Button>
</div>
</div>
@@ -0,0 +1,107 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import type {ExtendedEvent} from "@type/event";
import {createSvelteTable, FlexRender} from "@components/ui/data-table";
import {
type ColumnFiltersState,
getCoreRowModel, getFilteredRowModel,
getPaginationRowModel, getSortedRowModel,
type SortingState,
} from "@tanstack/table-core";
import { columns } from "./columns"
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table";
let { data }: { data: ExtendedEvent } = $props();
let sorting = $state<SortingState>([]);
let columnFilters = $state<ColumnFiltersState>([]);
const table = createSvelteTable({
get data() {
return data.fights;
},
state: {
get sorting() {
return sorting;
},
get columnFilters() {
return columnFilters;
},
},
onSortingChange: (updater) => {
if (typeof updater === "function") {
sorting = updater(sorting);
} else {
sorting = updater;
}
},
onColumnFiltersChange: (updater) => {
if (typeof updater === "function") {
columnFilters = updater(columnFilters);
} else {
columnFilters = updater;
}
},
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
});
</script>
<Table>
<TableHeader>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<TableRow>
{#each headerGroup.headers as header (header.id)}
<TableHead>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</TableHead>
{/each}
</TableRow>
{/each}
</TableHeader>
<TableBody>
{#each table.getRowModel().rows as row (row.id)}
<TableRow data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<TableCell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</TableCell>
{/each}
</TableRow>
{:else}
<TableRow>
<TableCell colspan={columns.length} class="h-24 text-center">
No results.
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
@@ -0,0 +1,47 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import type {ExtendedEvent} from "@type/event.ts";
import EventEdit from "@components/moderator/pages/event/EventEdit.svelte";
import EventFightList from "@components/moderator/pages/event/EventFightList.svelte";
import RefereesList from "@components/moderator/pages/event/RefereesList.svelte";
const {
event
}: { event: ExtendedEvent } = $props();
</script>
<div class="flex flex-col m-4 p-4 rounded-md border gap-4">
<div class="flex flex-col md:flex-row">
<div class="md:w-1/3">
<h1 class="text-2xl font-bold mb-4">{event.event.name}</h1>
<EventEdit event={event.event} />
</div>
<div class="md:ml-4 md:pl-4 md:border-l md:w-1/3">
<h2>Teams</h2>
</div>
<div class="md:ml-4 md:pl-4 md:border-l md:w-1/3">
<h2>Referees</h2>
<RefereesList event={event} />
</div>
</div>
<EventFightList data={event} />
</div>
@@ -0,0 +1,92 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table/index.js";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@components/ui/command/index.js";
import {Popover, PopoverContent, PopoverTrigger} from "@components/ui/popover/index.js";
import {Button} from "@components/ui/button/index.js";
import type {ExtendedEvent} from "@type/event.ts";
import { eventRepo } from "@repo/event";
import { players } from "@stores/stores"
const {
event
}: { event: ExtendedEvent } = $props();
let referees = $state(event.event.referees)
async function addReferee(value: string) {
referees = (await $eventRepo.updateEvent(event.event.id.toString(), {
addReferee: [value]
})).referees;
}
async function removeReferee(value: string) {
referees = (await $eventRepo.updateEvent(event.event.id.toString(), {
removeReferee: [value]
})).referees;
}
</script>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#each referees as referee (referee.uuid)}
<TableRow>
<TableCell>{referee.name}</TableCell>
<TableCell>
<Button onclick={() => removeReferee(referee.uuid)}>Remove</Button>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
<Popover>
<PopoverTrigger>
<Button>
Add
</Button>
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search players..." />
<CommandList>
<CommandEmpty>No Players found :(</CommandEmpty>
<CommandGroup heading="Players">
{#each $players.filter(v => v.perms.length > 0).filter(v => !referees.some(k => k.uuid === v.uuid)) as player (player.uuid)}
<CommandItem value={player.uuid} onSelect={() => addReferee(player.uuid)}>{player.name}</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
@@ -1,7 +1,7 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2023 SteamWar.de-Serverteam
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -17,17 +17,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export function window<T>(arr: T[], len: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < arr.length; i += len) {
result.push(arr.slice(i, i + len));
}
return result;
}
import type {ColumnDef} from "@tanstack/table-core";
import type {EventFight} from "@type/event.ts";
export function stopPropagation(a: any) {
return (e: Event) => {
e.stopPropagation();
a(e);
};
}
export const columns: ColumnDef<EventFight> = [
{
accessorFn: (r) => r.blueTeam.name,
header: "Team Blue",
},
{
accessorFn: (r) => r.redTeam.name,
header: "Team Red",
},
];
@@ -0,0 +1,51 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import { eventRepo } from "@repo/event.ts";
import EventCard from "@components/moderator/components/EventCard.svelte";
let eventsFuture = $state($eventRepo.listEvents());
let millis = Date.now();
</script>
<div class="p-4">
{#await eventsFuture}
<p>Loading...</p>
{:then events}
<h1 class="mt-5 scroll-m-20 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0">Upcoming</h1>
<div class="grid gap-4 p-4 border-b" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))">
{#each events.filter((e) => e.start > millis) as event (event.id)}
<a href="#/event/{event.id}">
<EventCard {event} />
</a>
{/each}
</div>
<h1 class="mt-5 scroll-m-20 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0">Past</h1>
<div class="grid gap-4 p-4" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))">
{#each events.filter((e) => e.start < millis).reverse() as event (event.id)}
<a href="#/event/{event.id}">
<EventCard {event} />
</a>
{/each}
</div>
{:catch e}
{/await}
</div>
@@ -0,0 +1,56 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {permissions, players} from "@stores/stores.ts";
import {Select, SelectContent, SelectItem} from "@components/ui/select";
import {SelectTrigger} from "@components/ui/select/index.js";
import {permsRepo} from "@repo/perms.ts";
const {
perms, uuid
}: { perms: string[], uuid: string } = $props();
let value = $state(perms);
let prevValue = $state(perms);
function onChange(change: string[]) {
$permissions.perms.forEach(perm => {
if (prevValue.includes(perm) && !change.includes(perm)) {
$permsRepo.removePerm(uuid, perm)
} else if (!prevValue.includes(perm) && change.includes(perm)) {
$permsRepo.addPerm(uuid, perm)
}
});
prevValue = change;
value = change;
}
</script>
<Select type="multiple" bind:value onValueChange={onChange}>
<SelectTrigger>
{value.length} Permissions
</SelectTrigger>
<SelectContent>
{#each $permissions.perms as permission (permission)}
<SelectItem value={permission}>{permission}</SelectItem>
{/each}
</SelectContent>
</Select>
@@ -0,0 +1,32 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script>
import Table from "@components/moderator/pages/players/Table.svelte";
import {dataRepo} from "@repo/data";
let playersFuture = $state($dataRepo.getPlayers())
</script>
{#await playersFuture}
<p>Loading...</p>
{:then players}
<Table data={players} />
{/await}
@@ -0,0 +1,47 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {Select, SelectContent, SelectItem, SelectTrigger} from "@components/ui/select";
import {permissions} from "@stores/stores.ts";
import {permsRepo} from "@repo/perms.ts";
const {
prefix, uuid
}: { prefix: string, uuid: string } = $props();
let value = $state(prefix);
function onChange(change: string) {
$permsRepo.setPrefix(uuid, change);
value = $permissions.prefixes[change].chatPrefix;
}
</script>
<Select type="single" bind:value onValueChange={onChange}>
<SelectTrigger>
{value === "" ? "None" : value}
</SelectTrigger>
<SelectContent>
{#each Object.entries($permissions.prefixes) as prefix (prefix[1].name)}
<SelectItem value={prefix[0]}>{prefix[1].chatPrefix === "" ? "None" : prefix[1].chatPrefix}</SelectItem>
{/each}
</SelectContent>
</Select>
@@ -0,0 +1,174 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {
type ColumnFiltersState,
getCoreRowModel, getFilteredRowModel,
getPaginationRowModel, getSortedRowModel,
type PaginationState,
type SortingState,
} from "@tanstack/table-core";
import {
createSvelteTable,
FlexRender,
} from "@components/ui/data-table/index";
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table";
import {Button} from "@components/ui/button";
import {Input} from "@components/ui/input";
import {Select} from "@components/ui/select";
import {SelectContent, SelectItem, SelectTrigger} from "@components/ui/select/index.js";
import type {Player} from "@type/data";
import { columns } from "./columns";
let { data }: { data: Player[] } = $props();
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 25 });
let sorting = $state<SortingState>([]);
let columnFilters = $state<ColumnFiltersState>([]);
const table = createSvelteTable({
get data() {
return data;
},
state: {
get pagination() {
return pagination;
},
get sorting() {
return sorting;
},
get columnFilters() {
return columnFilters;
},
},
onPaginationChange: (updater) => {
if (typeof updater === "function") {
pagination = updater(pagination);
} else {
pagination = updater;
}
},
onSortingChange: (updater) => {
if (typeof updater === "function") {
sorting = updater(sorting);
} else {
sorting = updater;
}
},
onColumnFiltersChange: (updater) => {
if (typeof updater === "function") {
columnFilters = updater(columnFilters);
} else {
columnFilters = updater;
}
},
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
});
</script>
<div class="rounded-md border m-4">
<div class="flex items-center p-4 border-b">
<Input
placeholder="Filter Players..."
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
onchange={(e) => {
table.getColumn("name")?.setFilterValue(e.currentTarget.value);
}}
oninput={(e) => {
table.getColumn("name")?.setFilterValue(e.currentTarget.value);
}}
class="max-w-sm"
/>
<div class="flex items-center px-4">
<Select type="single" value={pagination.pageSize.toString()} onValueChange={(e) => pagination = { pageSize: +e, pageIndex: 0 }}>
<SelectTrigger>{pagination.pageSize}</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="25">25</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
<SelectItem value="200">200</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Table>
<TableHeader>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<TableRow>
{#each headerGroup.headers as header (header.id)}
<TableHead>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</TableHead>
{/each}
</TableRow>
{/each}
</TableHeader>
<TableBody>
{#each table.getRowModel().rows as row (row.id)}
<TableRow data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<TableCell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</TableCell>
{/each}
</TableRow>
{:else}
<TableRow>
<TableCell colspan={columns.length} class="h-24 text-center">
No results.
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
<div class="flex items-center justify-end space-x-2 p-4 border-t">
<Button
variant="outline"
size="sm"
onclick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<span>{pagination.pageIndex + 1}/{table.getPageCount()}</span>
<Button
variant="outline"
size="sm"
onclick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
@@ -0,0 +1,60 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type {ColumnDef} from "@tanstack/table-core";
import type {Player} from "@type/data.ts";
import { renderComponent } from "@components/ui/data-table";
import PermissionsDropdown from "@components/moderator/pages/players/PermissionsDropdown.svelte";
import PrefixDropdown from "@components/moderator/pages/players/PrefixDropdown.svelte";
export const columns: ColumnDef<Player[]> = [
{
accessorKey: "uuid",
header: "UUID",
},
{
accessorKey: "name",
header: "Name",
},
{
accessorKey: "prefix",
header: "Prefix",
cell: ({ row }) => {
return renderComponent(
PrefixDropdown, {
prefix: row.getValue("prefix"),
uuid: row.getValue("uuid"),
},
);
},
},
{
accessorKey: "perms",
header: "Permissions",
cell: ({ row }) => {
return renderComponent(
PermissionsDropdown,
{
perms: row.getValue("perms"),
uuid: row.getValue("uuid"),
},
);
},
},
];
+1 -1
View File
@@ -22,7 +22,7 @@
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import {onDestroy, onMount} from "svelte";
import { CollectionEntry } from "astro:content";
import type { CollectionEntry } from "astro:content";
interface Props {
pub: CollectionEntry<"publics">;
-44
View File
@@ -1,44 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2023 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {fetchWithToken, tokenStore} from "./repo.ts";
import {derived} from "svelte/store";
export class AuthRepo {
constructor(private token: string) {
}
public async login(username: string, password: string): Promise<string> {
return await fetchWithToken(this.token, "/auth/login", {
body: JSON.stringify({
username,
password,
}),
method: "POST",
}).then(value => value.json()).then(value => value.token);
}
public async logout(): Promise<void> {
await fetchWithToken(this.token, "/auth/tokens/logout", {
method: "POST",
});
}
}
export const authRepo = derived(tokenStore, ($token) => new AuthRepo($token));
+184
View File
@@ -0,0 +1,184 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {readable, writable} from "svelte/store";
import dayjs, {type Dayjs} from "dayjs";
import {type AuthToken, AuthTokenSchema} from "@type/auth.ts";
export class AuthV2Repo {
private accessToken: string | undefined;
private accessTokenExpires: Dayjs | undefined;
private refreshToken: string | undefined;
private refreshTokenExpires: Dayjs | undefined;
constructor() {
if (typeof localStorage === "undefined") {
return;
}
this.accessToken = localStorage.getItem("sw-access-token") ?? undefined;
if (this.accessToken) {
this.accessTokenExpires = dayjs(localStorage.getItem("sw-access-token-expires") ?? "");
}
this.refreshToken = localStorage.getItem("sw-refresh-token") ?? undefined;
if (this.refreshToken) {
loggedIn.set(true);
this.refreshTokenExpires = dayjs(localStorage.getItem("sw-refresh-token-expires") ?? "");
}
}
async login(name: string, password: string) {
if (this.accessToken !== undefined || this.refreshToken !== undefined) {
throw new Error("Already logged in");
}
try {
const login = await this.request("/auth", {
method: "POST",
body: JSON.stringify({
name,
password,
keepLoggedIn: true,
}),
}).then(value => value.json()).then(value => AuthTokenSchema.parse(value));
this.setLoginState(login);
return true;
} catch (e) {
return false;
}
}
async logout() {
if (this.accessToken === undefined) {
return;
}
await this.request("/auth", {
method: "DELETE",
});
this.resetAccessToken();
this.resetRefreshToken();
}
private setLoginState(tokens: AuthToken) {
this.setAccessToken(tokens.accessToken.token, dayjs(tokens.accessToken.expires));
this.setRefreshToken(tokens.refreshToken.token, dayjs(tokens.refreshToken.expires));
loggedIn.set(true);
}
private setAccessToken(token: string, expires: Dayjs) {
this.accessToken = token;
this.accessTokenExpires = expires;
localStorage.setItem("sw-access-token", token);
localStorage.setItem("sw-access-token-expires", expires.toString());
}
private resetAccessToken() {
if (this.accessToken === undefined) {
return;
}
this.accessToken = undefined;
this.accessTokenExpires = undefined;
localStorage.removeItem("sw-access-token");
localStorage.removeItem("sw-access-token-expires");
}
private setRefreshToken(token: string, expires: Dayjs) {
this.refreshToken = token;
this.refreshTokenExpires = expires;
localStorage.setItem("sw-refresh-token", token);
localStorage.setItem("sw-refresh-token-expires", expires.toString());
}
private resetRefreshToken() {
if (this.refreshToken === undefined) {
return;
}
this.refreshToken = undefined;
this.refreshTokenExpires = undefined;
localStorage.removeItem("sw-refresh-token");
localStorage.removeItem("sw-refresh-token-expires");
loggedIn.set(false);
}
private async refresh() {
if (this.refreshToken === undefined || this.refreshTokenExpires === undefined || this.refreshTokenExpires.isBefore(dayjs().add(10, "seconds"))) {
this.resetRefreshToken();
this.resetAccessToken();
return;
}
const response = await this.requestWithToken(this.refreshToken!, "/auth", {
method: "PUT",
}).then(value => {
if (value.status === 401) {
this.resetRefreshToken();
this.resetAccessToken();
return undefined;
}
return value.json();
}).then(value => AuthTokenSchema.parse(value));
this.setLoginState(response);
}
async request(url: string, params: RequestInit = {}, retryCount: number = 0) {
if (this.accessToken !== undefined && this.accessTokenExpires !== undefined && this.accessTokenExpires.isBefore(dayjs().add(10, "seconds"))) {
await this.refresh();
}
return this.requestWithToken(this.accessToken ?? "", url, params, retryCount);
}
private async requestWithToken(token: string, url: string, params: RequestInit = {}, retryCount: number = 0): Promise<Response> {
if (retryCount >= 3) {
throw new Error("Too many retries");
}
return fetch(`${import.meta.env.PUBLIC_API_SERVER}${url}`, {...params,
headers: {
...(token !== "" ? {"Authorization": "Bearer " + (token)} : {}),
"Content-Type": "application/json", ...params.headers,
},
})
.then(async value => {
if (value.status === 401 && url !== "/auth") {
try {
await this.refresh();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_e) { /* empty */ }
return this.request(url, params, retryCount + 1);
}
return value;
});
}
}
export const loggedIn = writable(false);
export const authV2Repo = readable(new AuthV2Repo());
+5 -1
View File
@@ -20,7 +20,7 @@
import type {Player, Server} from "@type/data.ts";
import {PlayerSchema, ServerSchema} from "@type/data.ts";
import {fetchWithToken, tokenStore} from "./repo.ts";
import {derived} from "svelte/store";
import {derived, get} from "svelte/store";
export class DataRepo {
constructor(private token: string) {
@@ -33,6 +33,10 @@ export class DataRepo {
public async getMe(): Promise<Player> {
return await fetchWithToken(this.token, "/data/me").then(value => value.json()).then(PlayerSchema.parse);
}
public async getPlayers(): Promise<Player[]> {
return await fetchWithToken(get(tokenStore), "/data/admin/users").then(value => value.json()).then(PlayerSchema.array().parse);
}
}
export const dataRepo = derived(tokenStore, ($token) => new DataRepo($token));
+9 -9
View File
@@ -31,15 +31,15 @@ export interface CreateEvent {
}
export interface UpdateEvent {
name: string | null;
start: Dayjs | null;
end: Dayjs | null;
deadline: Dayjs | null;
maxTeamMembers: number | null;
schemType: string | null;
publicSchemsOnly: boolean | null;
addReferee: string[] | null;
removeReferee: string[] | null;
name?: string | null;
start?: Dayjs | number | null;
end?: Dayjs | number | null;
deadline?: Dayjs | number | null;
maxTeamMembers?: number | null;
schemType?: string | null;
publicSchemsOnly?: boolean | null;
addReferee?: string[] | null;
removeReferee?: string[] | null;
}
export class EventRepo {
+4 -26
View File
@@ -17,31 +17,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {writable} from "svelte/store";
import {get, writable} from "svelte/store";
import {authV2Repo} from "@repo/authv2.ts";
export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) =>
fetch(`${import.meta.env.PUBLIC_API_SERVER}${url}`, {...params,
headers: {
...(token !== "" ? {"Authorization": "Bearer " + (token)} : {}),
"Content-Type": "application/json", ...params.headers,
},
})
.then(value => {
if (value.status === 401) {
tokenStore.set("");
}
return value;
});
export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) => get(authV2Repo).request(url, params);
export function getLocalStorage() {
if (typeof localStorage === "undefined") {
return {
getItem: () => "",
setItem: () => {},
};
}
return localStorage;
}
export const tokenStore = writable((getLocalStorage().getItem("sw-session") ?? ""));
tokenStore.subscribe((value) => getLocalStorage().setItem("sw-session", value));
export const tokenStore = writable("");
+2 -1
View File
@@ -20,6 +20,7 @@
import {fetchWithToken, tokenStore} from "./repo.ts";
import {type Schematic, SchematicSchema} from "@type/schem.ts";
import {derived} from "svelte/store";
import {ResponseErrorSchema} from "@type/data.ts";
export class SchematicRepo {
constructor(private token: string) {
@@ -40,7 +41,7 @@ export class SchematicRepo {
name,
content,
}),
});
}).then(value => value.json()).then(SchematicSchema.or(ResponseErrorSchema).parse);
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ export class StatsRepo {
}
public async getUserStats(id: string): Promise<UserStats> {
return await fetchWithToken(this.token, `/stats/user/${id}`).then(value => value.json()).then(UserStatsSchema.parse);
return await fetchWithToken(this.token, `/stats/user`).then(value => value.json()).then(UserStatsSchema.parse);
}
}
+8
View File
@@ -27,6 +27,7 @@ import {z} from "zod";
import {fetchWithToken, tokenStore} from "@repo/repo.ts";
import {pageRepo} from "@repo/page.ts";
import {dataRepo} from "@repo/data.ts";
import {permsRepo} from "@repo/perms.ts";
export const schemTypes = cached<SchematicType[]>([], () =>
fetchWithToken(get(tokenStore), "/data/admin/schematicTypes")
@@ -37,6 +38,13 @@ export const players = cached<Player[]>([], async () => {
return z.array(PlayerSchema).parse(await res.json());
});
export const permissions = cached({
perms: [],
prefixes: {},
}, async () => {
return get(permsRepo).listPerms();
});
export const gamemodes = cached<string[]>([], async () => {
const res = await fetchWithToken(get(tokenStore), "/data/admin/gamemodes");
return z.array(z.string()).parse(await res.json());
+7 -5
View File
@@ -19,7 +19,7 @@
<script lang="ts">
import {onMount} from "svelte";
import {stopPropagation} from "@components/util.ts";
import {stopPropagation} from "@components/utils.ts";
interface Props {
title: string;
@@ -68,16 +68,18 @@
})
</script>
<dialog bind:this={dialog} onclose={close} onclick={(e) => dialog.close()} aria-hidden="true" class="max-h-full max-w-md w-full rounded-lg shadow-lg dark:bg-neutral-800 dark:text-neutral-100">
<div onclick={stopPropagation(onclick)} aria-hidden="true">
<dialog bind:this={dialog} onclose={close} onclick={(e) => dialog.close()} aria-hidden="true" class="max-h-full min-w-md w-fit rounded-lg shadow-lg dark:bg-neutral-800 dark:text-neutral-100">
<div onclick={stopPropagation(onclick)} aria-hidden="true" class="w-fit">
<div class="p-6 border-b border-neutral-200 dark:border-neutral-700">
<h1 class="text-4xl font-bold">{title}</h1>
</div>
<div class="p-6 main border-b border-neutral-200 dark:border-neutral-700">
{@render children?.()}
</div>
<div class="flex mx-4 my-2 p-6" onclick={() => dialog.close()} aria-hidden="true">
{@render footer?.()}
<div class="mx-4 my-2 p-6">
<div class="ml-auto flex justify-end" onclick={() => dialog.close()} aria-hidden="true">
{@render footer?.()}
</div>
</div>
</div>
</dialog>
+34
View File
@@ -0,0 +1,34 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {z} from "zod";
export const TokenSchema = z.object({
token: z.string(),
expires: z.string(),
});
export type Token = z.infer<typeof TokenSchema>;
export const AuthTokenSchema = z.object({
accessToken: TokenSchema,
refreshToken: TokenSchema,
});
export type AuthToken = z.infer<typeof AuthTokenSchema>;
+7
View File
@@ -50,3 +50,10 @@ export const ServerSchema = z.object({
});
export type Server = z.infer<typeof ServerSchema>;
export const ResponseErrorSchema = z.object({
error: z.string(),
code: z.string(),
});
export type ResponseError = z.infer<typeof ResponseErrorSchema>;
@@ -0,0 +1,25 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { slide } from "svelte/transition";
import { cn } from "$lib/components/utils.js";
type $$Props = AccordionPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = slide;
export let transitionConfig: $$Props["transitionConfig"] = {
duration: 200,
};
export { className as class };
</script>
<AccordionPrimitive.Content
class={cn("overflow-hidden text-sm transition-all", className)}
{transition}
{transitionConfig}
{...$$restProps}
>
<div class="pb-4 pt-0">
<slot />
</div>
</AccordionPrimitive.Content>
@@ -0,0 +1,14 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
type $$Props = AccordionPrimitive.ItemProps;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<AccordionPrimitive.Item {value} class={cn("border-b", className)} {...$$restProps}>
<slot />
</AccordionPrimitive.Item>
@@ -0,0 +1,26 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import ChevronDown from "lucide-svelte/icons/chevron-down";
import { cn } from "$lib/components/utils.js";
type $$Props = AccordionPrimitive.TriggerProps;
type $$Events = AccordionPrimitive.TriggerEvents;
let className: $$Props["class"] = undefined;
export let level: AccordionPrimitive.HeaderProps["level"] = 3;
export { className as class };
</script>
<AccordionPrimitive.Header {level} class="flex">
<AccordionPrimitive.Trigger
class={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...$$restProps}
on:click
>
<slot />
<ChevronDown class="h-4 w-4 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
+17
View File
@@ -0,0 +1,17 @@
import { Accordion as AccordionPrimitive } from "bits-ui";
import Content from "./accordion-content.svelte";
import Item from "./accordion-item.svelte";
import Trigger from "./accordion-trigger.svelte";
const Root = AccordionPrimitive.Root;
export {
Root,
Content,
Item,
Trigger,
//
Root as Accordion,
Content as AccordionContent,
Item as AccordionItem,
Trigger as AccordionTrigger,
};
@@ -0,0 +1,21 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.ActionProps;
type $$Events = AlertDialogPrimitive.ActionEvents;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AlertDialogPrimitive.Action
class={cn(buttonVariants(), className)}
{...$$restProps}
on:click
on:keydown
let:builder
>
<slot {builder} />
</AlertDialogPrimitive.Action>
@@ -0,0 +1,21 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.CancelProps;
type $$Events = AlertDialogPrimitive.CancelEvents;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AlertDialogPrimitive.Cancel
class={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...$$restProps}
on:click
on:keydown
let:builder
>
<slot {builder} />
</AlertDialogPrimitive.Cancel>
@@ -0,0 +1,28 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import * as AlertDialog from "./index.js";
import { cn, flyAndScale } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.ContentProps;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AlertDialog.Portal>
<AlertDialog.Overlay />
<AlertDialogPrimitive.Content
{transition}
{transitionConfig}
class={cn(
"bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full",
className
)}
{...$$restProps}
>
<slot />
</AlertDialogPrimitive.Content>
</AlertDialog.Portal>
@@ -0,0 +1,16 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.DescriptionProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AlertDialogPrimitive.Description
class={cn("text-muted-foreground text-sm", className)}
{...$$restProps}
>
<slot />
</AlertDialogPrimitive.Description>
@@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...$$restProps}
>
<slot />
</div>
@@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...$$restProps}>
<slot />
</div>
@@ -0,0 +1,21 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { fade } from "svelte/transition";
import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.OverlayProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = fade;
export let transitionConfig: $$Props["transitionConfig"] = {
duration: 150,
};
export { className as class };
</script>
<AlertDialogPrimitive.Overlay
{transition}
{transitionConfig}
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ", className)}
{...$$restProps}
/>
@@ -0,0 +1,9 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
type $$Props = AlertDialogPrimitive.PortalProps;
</script>
<AlertDialogPrimitive.Portal {...$$restProps}>
<slot />
</AlertDialogPrimitive.Portal>
@@ -0,0 +1,14 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.TitleProps;
let className: $$Props["class"] = undefined;
export let level: $$Props["level"] = "h3";
export { className as class };
</script>
<AlertDialogPrimitive.Title class={cn("text-lg font-semibold", className)} {level} {...$$restProps}>
<slot />
</AlertDialogPrimitive.Title>
+40
View File
@@ -0,0 +1,40 @@
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import Title from "./alert-dialog-title.svelte";
import Action from "./alert-dialog-action.svelte";
import Cancel from "./alert-dialog-cancel.svelte";
import Portal from "./alert-dialog-portal.svelte";
import Footer from "./alert-dialog-footer.svelte";
import Header from "./alert-dialog-header.svelte";
import Overlay from "./alert-dialog-overlay.svelte";
import Content from "./alert-dialog-content.svelte";
import Description from "./alert-dialog-description.svelte";
const Root = AlertDialogPrimitive.Root;
const Trigger = AlertDialogPrimitive.Trigger;
export {
Root,
Title,
Action,
Cancel,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
//
Root as AlertDialog,
Title as AlertDialogTitle,
Action as AlertDialogAction,
Cancel as AlertDialogCancel,
Portal as AlertDialogPortal,
Footer as AlertDialogFooter,
Header as AlertDialogHeader,
Trigger as AlertDialogTrigger,
Overlay as AlertDialogOverlay,
Content as AlertDialogContent,
Description as AlertDialogDescription,
};
@@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("text-sm [&_p]:leading-relaxed", className)} {...$$restProps}>
<slot />
</div>
@@ -0,0 +1,21 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { HeadingLevel } from "./index.js";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
level?: HeadingLevel;
};
let className: $$Props["class"] = undefined;
export let level: $$Props["level"] = "h5";
export { className as class };
</script>
<svelte:element
this={level}
class={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</svelte:element>
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { type Variant, alertVariants } from "./index.js";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement> & {
variant?: Variant;
};
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export { className as class };
</script>
<div class={cn(alertVariants({ variant }), className)} {...$$restProps} role="alert">
<slot />
</div>
+33
View File
@@ -0,0 +1,33 @@
import { type VariantProps, tv } from "tailwind-variants";
import Root from "./alert.svelte";
import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte";
export const alertVariants = tv({
base: "[&>svg]:text-foreground relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4",
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
});
export type Variant = VariantProps<typeof alertVariants>["variant"];
export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
export {
Root,
Description,
Title,
//
Root as Alert,
Description as AlertDescription,
Title as AlertTitle,
};
@@ -0,0 +1,11 @@
<script lang="ts">
import { AspectRatio as AspectRatioPrimitive } from "bits-ui";
type $$Props = AspectRatioPrimitive.Props;
export let ratio: $$Props["ratio"] = 4 / 3;
</script>
<AspectRatioPrimitive.Root {ratio} {...$$restProps}>
<slot />
</AspectRatioPrimitive.Root>
+3
View File
@@ -0,0 +1,3 @@
import Root from "./aspect-ratio.svelte";
export { Root, Root as AspectRatio };
@@ -0,0 +1,16 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
type $$Props = AvatarPrimitive.FallbackProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Fallback
class={cn("bg-muted flex h-full w-full items-center justify-center rounded-full", className)}
{...$$restProps}
>
<slot />
</AvatarPrimitive.Fallback>
@@ -0,0 +1,18 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
type $$Props = AvatarPrimitive.ImageProps;
let className: $$Props["class"] = undefined;
export let src: $$Props["src"] = undefined;
export let alt: $$Props["alt"] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Image
{src}
{alt}
class={cn("aspect-square h-full w-full", className)}
{...$$restProps}
/>
+18
View File
@@ -0,0 +1,18 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
type $$Props = AvatarPrimitive.Props;
let className: $$Props["class"] = undefined;
export let delayMs: $$Props["delayMs"] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Root
{delayMs}
class={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...$$restProps}
>
<slot />
</AvatarPrimitive.Root>
+13
View File
@@ -0,0 +1,13 @@
import Root from "./avatar.svelte";
import Image from "./avatar-image.svelte";
import Fallback from "./avatar-fallback.svelte";
export {
Root,
Image,
Fallback,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback,
};
+18
View File
@@ -0,0 +1,18 @@
<script lang="ts">
import { type Variant, badgeVariants } from "./index.js";
import { cn } from "$lib/components/utils.js";
let className: string | undefined | null = undefined;
export let href: string | undefined = undefined;
export let variant: Variant = "default";
export { className as class };
</script>
<svelte:element
this={href ? "a" : "span"}
{href}
class={cn(badgeVariants({ variant, className }))}
{...$$restProps}
>
<slot />
</svelte:element>
+21
View File
@@ -0,0 +1,21 @@
import { type VariantProps, tv } from "tailwind-variants";
export { default as Badge } from "./badge.svelte";
export const badgeVariants = tv({
base: "focus:ring-ring inline-flex select-none items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
});
export type Variant = VariantProps<typeof badgeVariants>["variant"];
@@ -0,0 +1,24 @@
<script lang="ts">
import Ellipsis from "lucide-svelte/icons/ellipsis";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="presentation"
aria-hidden="true"
class={cn("flex h-9 w-9 items-center justify-center", className)}
{...$$restProps}
>
<Ellipsis class="h-4 w-4" />
<span class="sr-only">More</span>
</span>
@@ -0,0 +1,16 @@
<script lang="ts">
import type { HTMLLiAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<li bind:this={el} class={cn("inline-flex items-center gap-1.5", className)}>
<slot />
</li>
@@ -0,0 +1,31 @@
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAnchorAttributes & {
el?: HTMLAnchorElement;
asChild?: boolean;
};
export let href: $$Props["href"] = undefined;
export let el: $$Props["el"] = undefined;
export let asChild: $$Props["asChild"] = false;
let className: $$Props["class"] = undefined;
export { className as class };
let attrs: Record<string, unknown>;
$: attrs = {
class: cn("hover:text-foreground transition-colors", className),
href,
...$$restProps,
};
</script>
{#if asChild}
<slot {attrs} />
{:else}
<a bind:this={el} {...attrs} {href}>
<slot {attrs} />
</a>
{/if}
@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLOlAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLOlAttributes & {
el?: HTMLOListElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<ol
bind:this={el}
class={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5",
className
)}
{...$$restProps}
>
<slot />
</ol>
@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props["el"] = undefined;
export let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="link"
aria-disabled="true"
aria-current="page"
class={cn("text-foreground font-normal", className)}
{...$$restProps}
>
<slot />
</span>
@@ -0,0 +1,25 @@
<script lang="ts">
import type { HTMLLiAttributes } from "svelte/elements";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<li
role="presentation"
aria-hidden="true"
class={cn("[&>svg]:size-3.5", className)}
bind:this={el}
{...$$restProps}
>
<slot>
<ChevronRight />
</slot>
</li>
@@ -0,0 +1,15 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLElement> & {
el?: HTMLElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<nav class={className} bind:this={el} aria-label="breadcrumb" {...$$restProps}>
<slot />
</nav>
+25
View File
@@ -0,0 +1,25 @@
import Root from "./breadcrumb.svelte";
import Ellipsis from "./breadcrumb-ellipsis.svelte";
import Item from "./breadcrumb-item.svelte";
import Separator from "./breadcrumb-separator.svelte";
import Link from "./breadcrumb-link.svelte";
import List from "./breadcrumb-list.svelte";
import Page from "./breadcrumb-page.svelte";
export {
Root,
Ellipsis,
Item,
Separator,
Link,
List,
Page,
//
Root as Breadcrumb,
Ellipsis as BreadcrumbEllipsis,
Item as BreadcrumbItem,
Separator as BreadcrumbSeparator,
Link as BreadcrumbLink,
List as BreadcrumbList,
Page as BreadcrumbPage,
};
+74
View File
@@ -0,0 +1,74 @@
<script lang="ts" module>
import type { WithElementRef } from "bits-ui";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
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",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
import { cn } from "$lib/components/utils.js";
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{href}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>
{@render children?.()}
</button>
{/if}
+17
View File
@@ -0,0 +1,17 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};
@@ -0,0 +1,19 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.CellProps = $props();
</script>
<CalendarPrimitive.Cell
bind:ref
class={cn(
"[&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50 relative size-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md",
className
)}
{...restProps}
/>
@@ -0,0 +1,30 @@
<script lang="ts">
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/components/utils.js";
import { Calendar as CalendarPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.DayProps = $props();
</script>
<CalendarPrimitive.Day
bind:ref
class={cn(
buttonVariants({ variant: "ghost" }),
"size-9 p-0 font-normal",
"[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground",
// Selected
"data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground data-[selected]:opacity-100",
// Disabled
"data-[disabled]:text-muted-foreground data-[disabled]:opacity-50",
// Unavailable
"data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through",
// Outside months
"data-[outside-month]:text-muted-foreground [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground data-[outside-month]:pointer-events-none data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:opacity-30",
className
)}
{...restProps}
/>
@@ -0,0 +1,12 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridBodyProps = $props();
</script>
<CalendarPrimitive.GridBody bind:ref class={cn(className)} {...restProps} />
@@ -0,0 +1,12 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridHeadProps = $props();
</script>
<CalendarPrimitive.GridHead bind:ref class={cn(className)} {...restProps} />
@@ -0,0 +1,12 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridRowProps = $props();
</script>
<CalendarPrimitive.GridRow bind:ref class={cn("flex", className)} {...restProps} />
@@ -0,0 +1,16 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridProps = $props();
</script>
<CalendarPrimitive.Grid
bind:ref
class={cn("w-full border-collapse space-y-1", className)}
{...restProps}
/>
@@ -0,0 +1,16 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeadCellProps = $props();
</script>
<CalendarPrimitive.HeadCell
bind:ref
class={cn("text-muted-foreground w-9 rounded-md text-[0.8rem] font-normal", className)}
{...restProps}
/>
@@ -0,0 +1,16 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeaderProps = $props();
</script>
<CalendarPrimitive.Header
bind:ref
class={cn("relative flex w-full items-center justify-between pt-1", className)}
{...restProps}
/>
@@ -0,0 +1,12 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeadingProps = $props();
</script>
<CalendarPrimitive.Heading bind:ref class={cn("text-sm font-medium", className)} {...restProps} />
@@ -0,0 +1,20 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("mt-4 flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0", className)}
{...restProps}
>
{@render children?.()}
</div>
@@ -0,0 +1,28 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: CalendarPrimitive.PrevButtonProps = $props();
</script>
{#snippet Fallback()}
<ChevronRight class="size-4" />
{/snippet}
<CalendarPrimitive.NextButton
bind:ref
class={cn(
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
className
)}
children={children || Fallback}
{...restProps}
/>

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