126 Commits

Author SHA1 Message Date
1b391b193e Implement code changes to enhance functionality and improve performance
Some checks failed
SteamWarCI Build failed
2025-04-14 18:18:40 +02:00
c05c032e3f Fix Merge
Some checks failed
SteamWarCI Build failed
2025-04-14 18:15:05 +02:00
da6f741806 Trigger Rebuild
All checks were successful
SteamWarCI Build successful
2025-04-14 17:55:39 +02:00
6b54791331 Merge pull request 'Merge branch sw-arcade' (#9) from sw-arcade into master
Some checks failed
SteamWarCI Build failed
Reviewed-on: #9
Reviewed-by: YoyoNow <yoyonow@noreply.localhost>
2025-04-14 17:54:22 +02:00
36685bffd1 Fix wording in SteamWar Arcade event announcement for clarity
All checks were successful
SteamWarCI Build successful
2025-04-14 17:53:46 +02:00
caf9ea6cf1 Add SteamWar Arcade event image and update markdown file
All checks were successful
SteamWarCI Build successful
2025-04-14 17:48:14 +02:00
d505265910 Update sw-arcade.md with event details and correct creation date
All checks were successful
SteamWarCI Build successful
2025-04-14 17:36:20 +02:00
78e1a7b726 Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-04-12 21:29:10 +02:00
cd485e8dda Update sw-arcade.md
All checks were successful
SteamWarCI Build successful
2025-04-06 23:07:10 +02:00
182c402c7e Update sw-arcade.md
Some checks failed
SteamWarCI Build failed
2025-04-06 23:05:35 +02:00
098f5b9270 Create page announcements/de/sw-arcade.md
Some checks failed
SteamWarCI Build failed
2025-04-06 23:04:24 +02:00
cf0c66c910 Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-04-06 14:12:00 +02:00
c8156ea47e Set fixed height for chart and disable aspect ratio maintenance
All checks were successful
SteamWarCI Build successful
2025-04-05 15:21:36 +02:00
20a47ca6b6 Add favicon.svg to the public directory
All checks were successful
SteamWarCI Build successful
2025-04-05 15:06:10 +02:00
2d601b9c4d Update schematic stats label and remove permission check
All checks were successful
SteamWarCI Build successful
2025-04-02 09:58:11 +02:00
48586f1a50 Add error handling and improve file upload UX
All checks were successful
SteamWarCI Build successful
2025-04-02 09:52:37 +02:00
7153cacbab Merge remote-tracking branch 'origin/master'
All checks were successful
SteamWarCI Build successful
# Conflicts:
#	src/content/modes/missilewars.json
2025-04-02 09:40:05 +02:00
73cee211f2 Refactor referee management into standalone component 2025-04-02 09:39:58 +02:00
83074df7ef Refactor referee management into standalone component
Some checks failed
SteamWarCI Build failed
2025-04-02 09:21:35 +02:00
d1c926c093 Refactor referee management into standalone component
Some checks failed
SteamWarCI Build failed
2025-04-02 09:20:36 +02:00
f8a16acfeb Remove shop
All checks were successful
SteamWarCI Build successful
2025-04-02 09:00:37 +02:00
9ca63cd286 Add shop.md
All checks were successful
SteamWarCI Build successful
2025-03-31 22:30:26 +02:00
a2456c8b46 Merge remote-tracking branch 'origin/master'
All checks were successful
SteamWarCI Build successful
2025-03-31 22:28:54 +02:00
0952035091 Add shop.md 2025-03-31 22:28:51 +02:00
9c8c02f679 Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-03-31 19:43:24 +02:00
3b5fdc57c0 Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-03-25 06:40:14 +01:00
733c63946f Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-03-25 06:32:35 +01:00
fd846250ab Merge pull request 'Addapted script side for new example hotkey script' (#8) from add-example-script-to-downloads into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #8
Reviewed-by: Chaoscaot <max@chaoscaot.de>
2025-03-22 10:36:18 +01:00
17460772e9 Addapted script side for new example hotkey script
All checks were successful
SteamWarCI Build successful
2025-03-21 18:54:26 +01:00
9a20860072 Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-03-18 06:48:53 +01:00
8f51723a3b Fix Graf Spee name
All checks were successful
SteamWarCI Build successful
2025-03-14 13:49:36 +01:00
8ad2f283aa Fixup things
All checks were successful
SteamWarCI Build successful
2025-03-13 16:30:12 +01:00
39f1af8b73 Add MissileWars.md
All checks were successful
SteamWarCI Build successful
2025-03-13 16:27:48 +01:00
266c4cb4ea Add MissileWars ranking to Navbar.svelte
All checks were successful
SteamWarCI Build successful
2025-03-13 16:24:45 +01:00
f3df3c0000 Add ranked to Micro WarGear
All checks were successful
SteamWarCI Build successful
2025-03-13 16:05:26 +01:00
cb78fc598b Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-03-09 16:52:42 +01:00
ba7ecc1a8e Merge remote-tracking branch 'origin/master'
All checks were successful
SteamWarCI Build successful
2025-03-04 23:33:17 +01:00
6ea92f9383 Tracking and adding LFS artifacts 2025-03-04 23:33:00 +01:00
998770bf59 Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-03-04 07:24:58 +01:00
a231032555 Add and standardize MissileWars translations and links
All checks were successful
SteamWarCI Build successful
2025-03-02 16:21:43 +01:00
3aa3731bcb Add MissileWars mode config and leaderboard link
All checks were successful
SteamWarCI Build successful
2025-03-02 16:18:37 +01:00
5e80c95bfd Add download link and update source URL in teamserver.json
All checks were successful
SteamWarCI Build successful
2025-03-02 16:12:47 +01:00
09dc28b6da Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-03-02 15:07:14 +01:00
fd7cf716ca Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-03-01 22:08:23 +01:00
73bd6a5e96 Fix Website
All checks were successful
SteamWarCI Build successful
2025-03-01 20:07:09 +01:00
9c02cc1f4d Fix Website
All checks were successful
SteamWarCI Build successful
2025-03-01 20:04:41 +01:00
de8457fe45 Fix Website
All checks were successful
SteamWarCI Build successful
2025-03-01 20:03:04 +01:00
4fbe01f987 New Dashboard 2025-03-01 20:01:04 +01:00
86d90e3fd2 New Dashboard 2025-03-01 20:00:46 +01:00
bccd5eb5a0 Enhance request handling with token refresh and retries
All checks were successful
SteamWarCI Build successful
2025-03-01 11:55:01 +01:00
53afe70b27 Refactor token refresh logic to streamline error handling.
All checks were successful
SteamWarCI Build successful
2025-03-01 11:34:09 +01:00
4bbdaa06a9 Refactor auth handling to improve token refresh logic
All checks were successful
SteamWarCI Build successful
2025-03-01 11:30:30 +01:00
f03867b9a7 Add retry mechanism and limit for token requests
All checks were successful
SteamWarCI Build successful
2025-03-01 11:27:06 +01:00
23e10eef0f Fix group filtering logic in FightTable.svelte
All checks were successful
SteamWarCI Build successful
2025-03-01 11:15:39 +01:00
4c72f4f26b FIx MW3 creation date
All checks were successful
SteamWarCI Build successful
2025-03-01 10:52:44 +01:00
624ba7f296 Merge pull request 'Merge branch wgs25-kampfplan' (#7) from wgs25-kampfplan into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #7
2025-03-01 10:51:19 +01:00
d7d20e4347 Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-03-01 10:50:17 +01:00
43bd8f4a7c Update wgs25-kampfplan.md
Some checks failed
SteamWarCI Build failed
2025-03-01 10:49:03 +01:00
18e8627b54 Update wgs25-kampfplan.md
Some checks failed
SteamWarCI Build failed
2025-03-01 10:42:19 +01:00
0efc46c7e2 Create page announcements/de/wgs25-kampfplan.md
Some checks failed
SteamWarCI Build failed
2025-03-01 09:53:50 +01:00
62fff0c0b2 Merge pull request 'Refactor authentication and implement password reset.' (#3) from develop/authv2 into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #3
Reviewed-by: Lixfel <lixfel@noreply.localhost>
2025-02-25 22:39:40 +01:00
86b479fb28 Update missilewars-iii-eventplan.md
All checks were successful
SteamWarCI Build successful
2025-02-23 18:34:53 +01:00
489402292d Update adventskalender-schems.md
All checks were successful
SteamWarCI Build successful
2025-02-23 18:31:10 +01:00
b53ce04a75 Remove reset password functionality
All checks were successful
SteamWarCI Build successful
2025-02-23 17:23:45 +01:00
069a9973a4 Add Gitea link and icon to navbar layout
All checks were successful
SteamWarCI Build successful
2025-02-23 15:20:50 +01:00
c3410de1d7 Refactor event handling to use Promises for better efficiency.
All checks were successful
SteamWarCI Build successful
2025-02-23 12:25:56 +01:00
a23c514102 Revert "Refactor event mounts and update script management."
This reverts commit bf8110af6c.
2025-02-23 12:20:34 +01:00
bf8110af6c Refactor event mounts and update script management.
All checks were successful
SteamWarCI Build successful
2025-02-23 12:18:58 +01:00
349f71af1c Add event listener for "astro:before-swap" in slug page
All checks were successful
SteamWarCI Build successful
2025-02-23 12:14:11 +01:00
dda37127ca Use type import and update page load event handling.
All checks were successful
SteamWarCI Build successful
2025-02-23 09:59:37 +01:00
6d210eb0ff Merge pull request 'Merge branch missilewars-iii-eventplan' (#4) from missilewars-iii-eventplan into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #4
2025-02-23 09:49:04 +01:00
cfede8f299 Update missilewars-iii-eventplan.md
All checks were successful
SteamWarCI Build successful
2025-02-23 09:47:30 +01:00
597153ed39 Update missilewars-iii-eventplan.md
All checks were successful
SteamWarCI Build successful
2025-02-23 07:43:22 +01:00
697e903a26 Create page announcements/de/missilewars-iii-eventplan.md
Some checks failed
SteamWarCI Build failed
2025-02-23 07:21:02 +01:00
1433784369 Update auth API endpoints to remove "/v2" prefix
All checks were successful
SteamWarCI Build successful
2025-02-20 22:15:02 +01:00
2c63a33bda Refine token validation and update user stats endpoint.
All checks were successful
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
87265e5ccc Add "Repeat Password" label to i18n and form components
All checks were successful
SteamWarCI Build successful
2025-02-17 18:32:54 +01:00
75f1a6528b Refactor authentication and implement password reset.
All checks were successful
SteamWarCI Build successful
2025-02-17 18:29:17 +01:00
23f35a35c4 Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:58:50 +01:00
973f469c7b Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:58:04 +01:00
107caafc26 Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:57:15 +01:00
7f26845802 Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:54:28 +01:00
37b2e82e05 Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:53:28 +01:00
7e2ba9dbce Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:50:59 +01:00
69426da5be Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:50:11 +01:00
fd2ad65ad4 Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:46:58 +01:00
a728651cca Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:46:04 +01:00
b9e73ed7d0 Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:38:36 +01:00
f1d55b3c99 Merge pull request 'Merge branch missilewars-iii-ankündigung' (#2) from missilewars-iii-ankündigung into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #2
Reviewed-by: YoyoNow <yoyonow@noreply.localhost>
2025-01-29 20:27:43 +01:00
9b49a0f81c Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 19:46:02 +01:00
11144043c1 Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 19:39:12 +01:00
e8f866ce8a Update missilewars-iii-ankündigung.md
Some checks failed
SteamWarCI Build failed
2025-01-29 19:28:01 +01:00
e57a90feaf Create page announcements/de/missilewars-iii-ankündigung.md
Some checks failed
SteamWarCI Build failed
2025-01-29 19:19:43 +01:00
3aa8fea1fd Update verhaltensrichtlinien.md
All checks were successful
SteamWarCI Build successful
2025-01-29 18:59:19 +01:00
95b327951c Update verhaltensrichtlienien.md
All checks were successful
SteamWarCI Build successful
2025-01-29 18:51:55 +01:00
86b99b4e76 Update miniwargear.md
All checks were successful
SteamWarCI Build successful
2025-01-29 18:36:59 +01:00
14b31be465 Fix i18n MircoWG
All checks were successful
SteamWarCI Build successful
2025-01-26 10:35:23 +01:00
341e629aaf Merge pull request 'Merge branch MicroRW' (#1) from MicroRW into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #1
2025-01-26 10:33:54 +01:00
694ded4c61 Update microwargear.md
All checks were successful
SteamWarCI Build successful
2025-01-26 10:32:11 +01:00
a75b5b7c09 Fix Branch Creation
All checks were successful
SteamWarCI Build successful
2025-01-26 10:24:20 +01:00
9146f65455 Fix Padding
All checks were successful
SteamWarCI Build successful
2025-01-21 14:57:05 +01:00
254807efa6 Rebuild
All checks were successful
SteamWarCI Build successful
2025-01-20 23:06:52 +01:00
36931aabb1 Fixes and Upgrade to Astro 5
Some checks failed
SteamWarCI Build failed
2025-01-20 23:04:34 +01:00
628599f019 Fix Login Page and add Jahresplan
All checks were successful
SteamWarCI Build successful
2025-01-20 19:21:21 +01:00
0a6c61bd88 Fix Upload
All checks were successful
SteamWarCI Build successful
2025-01-20 17:42:08 +01:00
8bbad8b3cc Fix CI
All checks were successful
SteamWarCI Build successful
2025-01-20 15:50:17 +01:00
5af6176889 Fix CI
Some checks failed
SteamWarCI Build failed
2025-01-20 15:49:59 +01:00
9250dd5088 Fix CI
Some checks failed
SteamWarCI Build failed
2025-01-20 15:49:36 +01:00
276e19409d Fix CI
Some checks failed
SteamWarCI Build failed
2025-01-20 15:48:49 +01:00
11fa9fa126 Fix CI
Some checks failed
SteamWarCI Build failed
2025-01-20 15:46:46 +01:00
17ec6023a9 Fix CI
Some checks failed
SteamWarCI Build failed
2025-01-20 15:45:43 +01:00
3c7c899868 Fix CI
Some checks failed
SteamWarCI Build failed
2025-01-20 15:41:59 +01:00
9cb161e470 Fix CI
Some checks failed
SteamWarCI Build failed
2025-01-20 15:38:58 +01:00
7fc7c2a6eb Fix CI
Some checks failed
SteamWarCI Build failed
2025-01-20 15:37:28 +01:00
2fce94d46b Fix CI
Some checks failed
SteamWarCI Build failed
2025-01-20 15:35:21 +01:00
6356c9911a Fix CI
Some checks failed
SteamWarCI Build failed
2025-01-20 15:34:38 +01:00
2402896fd5 Fix CI
Some checks failed
SteamWarCI Build failed
2025-01-20 15:33:12 +01:00
2940304492 Fix CI
Some checks failed
SteamWarCI Build failed
2025-01-20 15:32:20 +01:00
4c0a237b27 Merge remote-tracking branch 'origin/master'
Some checks failed
SteamWarCI Build failed
2025-01-20 15:26:55 +01:00
77b8b41afb Fix CodeMirror 2025-01-20 15:19:18 +01:00
163d049829 astro.config.mjs aktualisiert
Some checks failed
SteamWarCI Build failed
2025-01-20 15:06:44 +01:00
a321b12680 Merge remote-tracking branch 'origin/master'
Some checks failed
SteamWarCI Build failed
2025-01-20 15:05:42 +01:00
feba5a5b4a Fix CodeMirror 2025-01-20 15:05:33 +01:00
faaf5f1852 steamwarci.yml aktualisiert
Some checks failed
SteamWarCI Build failed
2025-01-20 15:03:34 +01:00
18997e1384 tailwind.config.cjs aktualisiert
Some checks failed
SteamWarCI Build failed
2025-01-20 15:02:11 +01:00
fdc7bb93dd Add some stuff 2025-01-19 17:58:26 +01:00
334 changed files with 11020 additions and 3229 deletions

View File

@ -5,10 +5,8 @@ import configureI18n from "./astro-i18n.adapter";
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
import robotsTxt from "astro-robots-txt"; import robotsTxt from "astro-robots-txt";
import {resolve} from "node:url";
import path from "node:path"; import path from "node:path";
import mdx from "@astrojs/mdx"; import mdx from "@astrojs/mdx";
import pagefind from "astro-pagefind";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
@ -21,9 +19,8 @@ export default defineConfig({
integrations: [ integrations: [
svelte(), svelte(),
tailwind({ tailwind({
configFile: "./tailwind.config.cjs", configFile: "./tailwind.config.js",
}), }),
pagefind(),
configureI18n(), configureI18n(),
sitemap({ sitemap({
i18n: { i18n: {
@ -69,6 +66,7 @@ export default defineConfig({
"@layouts": path.resolve("./src/layouts"), "@layouts": path.resolve("./src/layouts"),
"@repo": path.resolve("./src/components/repo"), "@repo": path.resolve("./src/components/repo"),
"@stores": path.resolve("./src/components/stores"), "@stores": path.resolve("./src/components/stores"),
"$lib": path.resolve("./src"),
}, },
}, },
}, },

17
components.json Normal file
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"
}

View File

@ -14,40 +14,54 @@
"i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types", "i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types",
"clean:dist": "rm -rf dist", "clean:dist": "rm -rf dist",
"clean:node_modules": "rm -rf node_modules", "clean:node_modules": "rm -rf node_modules",
"ci": "pnpm run clean:dist && pnpm install && pnpm run i18n:sync && pnpm run build" "ci": "pnpm install && pnpm run i18n:sync && pnpm run build"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/svelte": "^6.0.2", "@astrojs/svelte": "^7.0.4",
"@astrojs/tailwind": "^5.1.2", "@astrojs/tailwind": "^5.1.5",
"@astropub/icons": "^0.2.0", "@astropub/icons": "^0.2.0",
"@internationalized/date": "^3.7.0",
"@types/color": "^4.2.0", "@types/color": "^4.2.0",
"@types/node": "^22.9.3", "@types/node": "^22.9.3",
"@types/three": "^0.170.0", "@types/three": "^0.170.0",
"@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0", "@typescript-eslint/parser": "^8.15.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bits-ui": "1.3.4",
"clsx": "^2.1.1",
"cmdk-sv": "^0.0.18",
"cssnano": "^7.0.6", "cssnano": "^7.0.6",
"embla-carousel-svelte": "^8.5.2",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"eslint": "^9.15.0", "eslint": "^9.15.0",
"eslint-plugin-astro": "^1.3.1", "eslint-plugin-astro": "^1.3.1",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-svelte": "^2.46.0", "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", "postcss-nesting": "^13.0.1",
"sass": "^1.81.0", "sass": "^1.81.0",
"svelte": "^5.16.0", "svelte": "^5.16.0",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.15",
"three": "^0.170.0", "three": "^0.170.0",
"typescript": "^5.7.2" "typescript": "^5.7.2",
"vaul-svelte": "^0.3.2",
"zod": "^3.23.8"
}, },
"dependencies": { "dependencies": {
"@astrojs/mdx": "^3.1.9", "@astrojs/mdx": "^4.0.7",
"@astrojs/sitemap": "^3.2.1", "@astrojs/sitemap": "^3.2.1",
"@codemirror/commands": "^6.8.0",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@ddietr/codemirror-themes": "^1.4.4", "@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-i18n": "^2.2.4",
"astro-pagefind": "^1.6.0",
"astro-robots-txt": "^1.0.0", "astro-robots-txt": "^1.0.0",
"astro-seo": "^0.8.4", "astro-seo": "^0.8.4",
"chart.js": "^4.4.6", "chart.js": "^4.4.6",
@ -63,7 +77,6 @@
"sharp": "^0.33.5", "sharp": "^0.33.5",
"svelte-awesome": "^3.3.5", "svelte-awesome": "^3.3.5",
"svelte-codemirror-editor": "^1.4.1", "svelte-codemirror-editor": "^1.4.1",
"svelte-spa-router": "^4.0.1", "svelte-spa-router": "^4.0.1"
"zod": "^3.23.8"
} }
} }

5754
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
public/favicon-96x96.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

3
public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 406 KiB

21
public/site.webmanifest Normal file
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

View File

@ -20,6 +20,7 @@
<script lang="ts"> <script lang="ts">
import {t} from "astro-i18n"; import {t} from "astro-i18n";
import {statsRepo} from "@repo/stats.ts"; import {statsRepo} from "@repo/stats.ts";
import "@styles/table.css"
interface Props { interface Props {
@ -64,7 +65,3 @@
<p>{error.message}</p> <p>{error.message}</p>
{/await} {/await}
<style>
@import "../styles/table.css";
</style>

View File

@ -79,6 +79,8 @@
}) })
}, },
options: { options: {
maintainAspectRatio: false,
scales: { scales: {
x: { x: {
type: "time", type: "time",
@ -105,5 +107,5 @@
</script> </script>
<div> <div>
<canvas bind:this={canvas}></canvas> <canvas height="500" bind:this={canvas}></canvas>
</div> </div>

View File

@ -19,7 +19,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import {window} from "./util.ts"; import {window} from "./utils.ts";
import {astroI18n, t} from "astro-i18n"; import {astroI18n, t} from "astro-i18n";
import type {EventFight, ExtendedEvent} from "@type/event"; import type {EventFight, ExtendedEvent} from "@type/event";
import "@styles/table.css"; import "@styles/table.css";
@ -55,7 +55,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <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> <tr>
{#each fights as fight (fight.id)} {#each fights as fight (fight.id)}
<td>{Intl.DateTimeFormat(astroI18n.locale, { <td>{Intl.DateTimeFormat(astroI18n.locale, {

View File

@ -19,7 +19,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import {window} from "./util.ts"; import {window} from "./utils.ts";
import {t} from "astro-i18n"; import {t} from "astro-i18n";
import type {ExtendedEvent} from "@type/event.ts"; import type {ExtendedEvent} from "@type/event.ts";
import "@styles/table.css" import "@styles/table.css"

View File

@ -31,8 +31,7 @@
let error: string = $state(""); let error: string = $state("");
async function login() { async function login() {
let {tokenStore} = await import("./repo/repo.ts"); let {authV2Repo} = await import("./repo/authv2.ts");
let {authRepo} = await import("./repo/auth.ts");
if (username === "" || pw === "") { if (username === "" || pw === "") {
pw = ""; pw = "";
error = t("login.error"); error = t("login.error");
@ -40,15 +39,14 @@
} }
try { try {
let auth = await get(authRepo).login(username, pw); let auth = await get(authV2Repo).login(username, pw);
if (auth == undefined) { if (!auth) {
pw = ""; pw = "";
error = t("login.error"); error = t("login.error");
return; return;
} }
tokenStore.set(auth); await navigate(l("/dashboard"));
navigate(l("/dashboard"));
} catch (e: any) { } catch (e: any) {
pw = ""; pw = "";
error = t("login.error"); error = t("login.error");
@ -75,9 +73,7 @@
<style lang="postcss"> <style lang="postcss">
input { input {
@apply border-2 rounded-md p-2 shadow-2xl w-80 @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;
dark:bg-neutral-800
focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:border-transparent;
} }
label { label {

View File

@ -19,13 +19,13 @@
<script lang="ts"> <script lang="ts">
import "../styles/button.css"; import "../styles/button.css";
import { CaretDownOutline, SearchOutline } from "flowbite-svelte-icons"; import {CaretDownOutline, SearchOutline} from "flowbite-svelte-icons";
import { t } from "astro-i18n"; import {t} from "astro-i18n";
import { l } from "../util/util"; import {l} from "../util/util";
import { onMount } from "svelte"; import {onMount} from "svelte";
import { loggedIn } from "@repo/authv2.ts"; import {loggedIn} from "@repo/authv2.ts";
interface Props { interface Props {
logo?: import("svelte").Snippet; logo?: import('svelte').Snippet;
} }
let { logo }: Props = $props(); let { logo }: Props = $props();
@ -41,11 +41,11 @@
} else { } else {
accountBtn!.href = l("/login"); accountBtn!.href = l("/login");
} }
}); })
onMount(() => { onMount(() => {
handleScroll(); handleScroll();
}); })
function handleScroll() { function handleScroll() {
if (window.scrollY > 0) { if (window.scrollY > 0) {
@ -56,27 +56,15 @@
} }
</script> </script>
<svelte:window onscroll={handleScroll} /> <svelte:window onscroll={handleScroll}/>
<nav <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}>
data-pagefind-ignore <div class="flex flex-row items-center justify-evenly md:justify-between match">
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("/")}> <a class="flex items-center" href={l("/")}>
{@render logo?.()} {@render logo?.()}
<span <span class="text-2xl uppercase font-bold dark:text-white hidden md:inline-block">
class="text-2xl uppercase font-bold dark:text-white hidden md:inline-block"
>
{t("navbar.title")} {t("navbar.title")}
<span <span class="before:scale-y-100" style="display: none" aria-hidden="true"></span>
class="before:scale-y-100"
style="display: none"
aria-hidden="true"
></span>
</span> </span>
</a> </a>
<div class="flex justify-center flex-wrap"> <div class="flex justify-center flex-wrap">
@ -85,24 +73,13 @@
<a href={l("/")}> <a href={l("/")}>
<span class="btn__text">{t("navbar.links.home.title")}</span> <span class="btn__text">{t("navbar.links.home.title")}</span>
</a> </a>
<CaretDownOutline class="ml-2 mt-auto" /> <CaretDownOutline class="ml-2 mt-auto"/>
</button> </button>
<div> <div>
<a class="btn btn-gray" href={l("/announcements")} <a class="btn btn-gray" href={l("/announcements")}>{t("navbar.links.home.announcements")}</a>
>{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("/downloads")} <a class="btn btn-gray" href={l("/code-of-conduct")}>{t("navbar.links.rules.coc")}</a>
>{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> </div>
<div class="btn-dropdown"> <div class="btn-dropdown">
@ -110,38 +87,22 @@
<a rel="prefetch" href={l("/rules")}> <a rel="prefetch" href={l("/rules")}>
<span class="btn__text">{t("navbar.links.rules.title")}</span> <span class="btn__text">{t("navbar.links.rules.title")}</span>
</a> </a>
<CaretDownOutline class="ml-2 mt-auto" /> <CaretDownOutline class="ml-2 mt-auto"/>
</button> </button>
<div> <div>
<a href={l("/rules/wargear")} class="btn btn-gray" <a href={l("/rules/wargear")} class="btn btn-gray">{t("navbar.links.rules.wg")}</a>
>{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/miniwargear")} class="btn btn-gray" <a href={l("/rules/airship")} class="btn btn-gray">{t("navbar.links.rules.as")}</a>
>{t("navbar.links.rules.mwg")}</a <a href={l("/rules/quickgear")} class="btn btn-gray">{t("navbar.links.rules.qg")}</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> <h2 class="px-2 text-gray-300">{t("navbar.links.rules.rotating")}</h2>
<a href={l("/rules/megawargear")} class="btn btn-gray" <a href={l("/rules/megawargear")}
>{t("navbar.links.rules.megawg")}</a class="btn btn-gray">{t("navbar.links.rules.megawg")}</a>
> <a href={l("/rules/microwargear")}
<a href={l("/rules/microwargear")} class="btn btn-gray" class="btn btn-gray">{t("navbar.links.rules.micro")}</a>
>{t("navbar.links.rules.micro")}</a <a href={l("/rules/streetfight")} class="btn btn-gray">{t("navbar.links.rules.sf")}</a>
>
<a href={l("/rules/streetfight")} class="btn btn-gray"
>{t("navbar.links.rules.sf")}</a
>
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.ranked")}</h2> <h2 class="px-2 text-gray-300">{t("navbar.links.rules.ranked")}</h2>
<a href={l("/rangliste/missilewars")} class="btn btn-gray" <a href={l("/rangliste/missilewars")} class="btn btn-gray">{t("navbar.links.ranked.mw")}</a>
>{t("navbar.links.ranked.mw")}</a
>
</div> </div>
</div> </div>
<!-- TODO: Add help center <!-- TODO: Add help center

View File

@ -1,5 +1,5 @@
--- ---
import {CollectionEntry} from "astro:content"; import type {CollectionEntry} from "astro:content";
import {l} from "../util/util"; import {l} from "../util/util";
import {astroI18n} from "astro-i18n"; import {astroI18n} from "astro-i18n";
import {Image} from "astro:assets"; import {Image} from "astro:assets";

View File

@ -22,38 +22,40 @@
import wrap from "svelte-spa-router/wrap"; import wrap from "svelte-spa-router/wrap";
import Router, {replace} from "svelte-spa-router"; import Router, {replace} from "svelte-spa-router";
import {get} from "svelte/store"; import {get} from "svelte/store";
import {tokenStore} from "@repo/repo"; import {loggedIn} from "@repo/authv2.ts";
const routes: RouteDefinition = { 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({ "/perms": wrap({
asyncComponent: () => import("./pages/Perms.svelte"), asyncComponent: () => import("./pages/Perms.svelte"),
conditions: detail => get(tokenStore) != "" conditions: detail => get(loggedIn)
}), }),
"/login": wrap({ "/login": wrap({
asyncComponent: () => import("./pages/Login.svelte"), asyncComponent: () => import("./pages/Login.svelte"),
conditions: detail => get(tokenStore) == "" conditions: detail => !get(loggedIn)
}), }),
"/event/:id": wrap({ "/event/:id": wrap({
asyncComponent: () => import("./pages/Event.svelte"), asyncComponent: () => import("./pages/Event.svelte"),
conditions: detail => get(tokenStore) != "" conditions: detail => get(loggedIn)
}), }),
"/event/:id/generate": wrap({ "/event/:id/generate": wrap({
asyncComponent: () => import("./pages/Generate.svelte"), asyncComponent: () => import("./pages/Generate.svelte"),
conditions: detail => get(tokenStore) != "" conditions: detail => get(loggedIn)
}), }),
"/edit": wrap({ "/edit": wrap({
asyncComponent: () => import("./pages/Edit.svelte"), asyncComponent: () => import("./pages/Edit.svelte"),
conditions: detail => get(tokenStore) != "" conditions: detail => get(loggedIn)
}), }),
"/display/:event": wrap({ "/display/:event": wrap({
asyncComponent: () => import("./pages/Display.svelte"), asyncComponent: () => import("./pages/Display.svelte"),
conditions: detail => get(tokenStore) != "" conditions: detail => get(loggedIn)
}), }),
"*": wrap({asyncComponent: () => import("./pages/NotFound.svelte")}) "*": wrap({asyncComponent: () => import("./pages/NotFound.svelte")})
}; };
function conditionsFailed(event: ConditionsFailedEvent) { function conditionsFailed(event: ConditionsFailedEvent) {
console.log(event)
if (event.detail.location === "/login") { if (event.detail.location === "/login") {
replace("/"); replace("/");
} else { } else {

View File

@ -168,11 +168,11 @@
</div> </div>
<div> <div>
{#if selectedBranch !== "master"} {#if selectedBranch !== "master"}
<Button onclick={createFile} color="alternative" disabled={!selectedPath}>Create File <Button onclick={() => createFile()} color="alternative" disabled={!selectedPath}>Create File
</Button> </Button>
<Button onclick={() => deleteBranch(false)} color="none">Delete Branch</Button> <Button onclick={() => deleteBranch(false)} color="none">Delete Branch</Button>
{:else} {:else}
<Button onclick={createBranch}>Create Branch</Button> <Button onclick={() => createBranch()}>Create Branch</Button>
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -38,6 +38,7 @@
</NavBrand> </NavBrand>
<NavHamburger onclick={toggle}/> <NavHamburger onclick={toggle}/>
<NavUl {hidden}> <NavUl {hidden}>
<NavLi href="/admin/new">New UI</NavLi>
<NavLi href="#/edit">Edit Pages</NavLi> <NavLi href="#/edit">Edit Pages</NavLi>
<NavLi href="#/perms">Permissions</NavLi> <NavLi href="#/perms">Permissions</NavLi>
</NavUl> </NavUl>

View File

@ -34,7 +34,7 @@
dirty?: boolean; dirty?: boolean;
} }
let { pageId, branch, dirty = $bindable(false) }: Props = $props(); let { pageId, branch = $bindable(), dirty = $bindable(false) }: Props = $props();
let dispatcher = createEventDispatcher(); let dispatcher = createEventDispatcher();
@ -97,7 +97,7 @@
{#if page?.name.endsWith("md") || page?.name.endsWith("mdx")} {#if page?.name.endsWith("md") || page?.name.endsWith("mdx")}
<MDEMarkdownEditor bind:value={pageContent} bind:dirty/> <MDEMarkdownEditor bind:value={pageContent} bind:dirty/>
{:else} {: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} {/if}
</div> </div>
{:catch error} {:catch error}

View File

@ -31,7 +31,7 @@
let request = getRequest(); let request = getRequest();
function getRequest() { function getRequest() {
return $statsRepo.getUserStats(user.id) return $statsRepo.getUserStats(user.uuid)
} }
</script> </script>
@ -43,8 +43,5 @@
maximumFractionDigits: 2 maximumFractionDigits: 2
}).format(data.playtime)})}h</p> }).format(data.playtime)})}h</p>
<p>{t("dashboard.stats.fights", {fights: data.fights})}</p> <p>{t("dashboard.stats.fights", {fights: data.fights})}</p>
{#if user.perms.includes("CHECK")}
<p>{t("dashboard.stats.checked", {checked: data.acceptedSchematics})}</p> <p>{t("dashboard.stats.checked", {checked: data.acceptedSchematics})}</p>
{/if}
{:catch error}
{/await} {/await}

View File

@ -21,7 +21,7 @@
import {createEventDispatcher} from "svelte"; import {createEventDispatcher} from "svelte";
import {schemRepo} from "@repo/schem.ts"; import {schemRepo} from "@repo/schem.ts";
import SWModal from "@components/styled/SWModal.svelte"; import SWModal from "@components/styled/SWModal.svelte";
import {t} from "astro-i18n" import {t} from "astro-i18n";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -29,11 +29,13 @@
open?: boolean; 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) { if (uploadFile == null) {
return error = "dashboard.schematic.errors.noFile";
return;
} }
let file = uploadFile[0]; let file = uploadFile[0];
@ -42,32 +44,46 @@
let type = name.split(".").pop(); let type = name.split(".").pop();
if (type !== "schem" && type !== "schematic") { if (type !== "schem" && type !== "schematic") {
return error = "dashboard.schematic.errors.invalidEnding";
return;
} }
let content = await file.arrayBuffer(); let content = await file.arrayBuffer();
// @ts-ignore
let b64 = btoa(String.fromCharCode.apply(null, new Uint8Array(content))); let b64 = btoa(String.fromCharCode.apply(null, new Uint8Array(content)));
try {
await $schemRepo.uploadSchematic(name, b64); await $schemRepo.uploadSchematic(name, b64);
open = false; open = false;
uploadFile = null; value = "";
dispatch("reset") dispatch("reset");
} catch (e) {
error = "dashboard.schematic.errors.upload";
}
}
function reset(e: Event) {
e.stopPropagation();
open = false
value = "";
} }
let uploadFile: FileList | null = $state(null); let uploadFile: FileList | null = $state(null);
let value = $state("");
let error = $state(null)
</script> </script>
<SWModal title={t("dashboard.schematic.title")} bind:open> <SWModal title={t("dashboard.schematic.title")} bind:open>
<form> <form>
<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> </form>
{#snippet footer()} {#snippet footer()}
<button class="btn" onclick={upload}>{t("dashboard.schematic.upload")}</button>
<button class="btn !ml-auto" onclick={upload}>{t("dashboard.schematic.upload")}</button> <button class="btn btn-gray" onclick={reset}>{t("dashboard.schematic.cancel")}</button>
<button class="btn btn-gray" onclick={() => open = false}>{t("dashboard.schematic.cancel")}</button>
{/snippet} {/snippet}
</SWModal> </SWModal>

View File

@ -22,9 +22,9 @@
import type {Player} from "@type/data.ts"; import type {Player} from "@type/data.ts";
import {l} from "@utils/util.ts"; import {l} from "@utils/util.ts";
import Statistics from "./Statistics.svelte"; import Statistics from "./Statistics.svelte";
import {authRepo} from "@repo/auth.ts"; import {authV2Repo} from "@repo/authv2.ts";
import {tokenStore} from "@repo/repo.ts";
import Card from "@components/Card.svelte"; import Card from "@components/Card.svelte";
import {navigate} from "astro:transitions/client";
interface Props { interface Props {
user: Player; user: Player;
@ -33,9 +33,8 @@
let { user }: Props = $props(); let { user }: Props = $props();
async function logout() { async function logout() {
await $authRepo.logout() await $authV2Repo.logout();
tokenStore.set("") await navigate(l("/login"));
window.location.href = l("/login")
} }
</script> </script>
@ -56,7 +55,7 @@
</div> </div>
<div> <div>
<h1 class="text-4xl font-bold">{t("dashboard.title", {name: user.name})}</h1> <h1 class="text-4xl font-bold">{t("dashboard.title", {name: user.name})}</h1>
<p>{t("dashboard.rank", {rank: t("home.prefix." + user.prefix)})}</p> <p>{t("dashboard.rank", {rank: t("home.prefix." + (user.prefix || "User"))})}</p>
<Statistics {user} /> <Statistics {user} />
</div> </div>
</div> </div>

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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,7 +1,7 @@
/* /*
* This file is a part of the SteamWar software. * This file is a part of the SteamWar software.
* *
* Copyright (C) 2023 SteamWar.de-Serverteam * Copyright (C) 2025 SteamWar.de-Serverteam
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by * it under the terms of the GNU Affero General Public License as published by
@ -17,17 +17,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export function window<T>(arr: T[], len: number): T[][] { import type {ColumnDef} from "@tanstack/table-core";
const result: T[][] = []; import type {EventFight} from "@type/event.ts";
for (let i = 0; i < arr.length; i += len) {
result.push(arr.slice(i, i + len));
}
return result;
}
export function stopPropagation(a: any) { export const columns: ColumnDef<EventFight> = [
return (e: Event) => { {
e.stopPropagation(); accessorFn: (r) => r.blueTeam.name,
a(e); header: "Team Blue",
}; },
} {
accessorFn: (r) => r.redTeam.name,
header: "Team Red",
},
];

View File

@ -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>

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 {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>

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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"),
},
);
},
},
];

View File

@ -22,7 +22,7 @@
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import {onDestroy, onMount} from "svelte"; import {onDestroy, onMount} from "svelte";
import { CollectionEntry } from "astro:content"; import type { CollectionEntry } from "astro:content";
interface Props { interface Props {
pub: CollectionEntry<"publics">; pub: CollectionEntry<"publics">;

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));

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());

View File

@ -20,7 +20,7 @@
import type {Player, Server} from "@type/data.ts"; import type {Player, Server} from "@type/data.ts";
import {PlayerSchema, ServerSchema} from "@type/data.ts"; import {PlayerSchema, ServerSchema} from "@type/data.ts";
import {fetchWithToken, tokenStore} from "./repo.ts"; import {fetchWithToken, tokenStore} from "./repo.ts";
import {derived} from "svelte/store"; import {derived, get} from "svelte/store";
export class DataRepo { export class DataRepo {
constructor(private token: string) { constructor(private token: string) {
@ -33,6 +33,10 @@ export class DataRepo {
public async getMe(): Promise<Player> { public async getMe(): Promise<Player> {
return await fetchWithToken(this.token, "/data/me").then(value => value.json()).then(PlayerSchema.parse); 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)); export const dataRepo = derived(tokenStore, ($token) => new DataRepo($token));

View File

@ -31,15 +31,15 @@ export interface CreateEvent {
} }
export interface UpdateEvent { export interface UpdateEvent {
name: string | null; name?: string | null;
start: Dayjs | null; start?: Dayjs | number | null;
end: Dayjs | null; end?: Dayjs | number | null;
deadline: Dayjs | null; deadline?: Dayjs | number | null;
maxTeamMembers: number | null; maxTeamMembers?: number | null;
schemType: string | null; schemType?: string | null;
publicSchemsOnly: boolean | null; publicSchemsOnly?: boolean | null;
addReferee: string[] | null; addReferee?: string[] | null;
removeReferee: string[] | null; removeReferee?: string[] | null;
} }
export class EventRepo { export class EventRepo {

View File

@ -17,31 +17,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * 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 = {}) => export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) => get(authV2Repo).request(url, params);
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 function getLocalStorage() { export const tokenStore = writable("");
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));

View File

@ -20,6 +20,7 @@
import {fetchWithToken, tokenStore} from "./repo.ts"; import {fetchWithToken, tokenStore} from "./repo.ts";
import {type Schematic, SchematicSchema} from "@type/schem.ts"; import {type Schematic, SchematicSchema} from "@type/schem.ts";
import {derived} from "svelte/store"; import {derived} from "svelte/store";
import {ResponseErrorSchema} from "@type/data.ts";
export class SchematicRepo { export class SchematicRepo {
constructor(private token: string) { constructor(private token: string) {
@ -40,7 +41,7 @@ export class SchematicRepo {
name, name,
content, content,
}), }),
}); }).then(value => value.json()).then(SchematicSchema.or(ResponseErrorSchema).parse);
} }
} }

View File

@ -36,7 +36,7 @@ export class StatsRepo {
} }
public async getUserStats(id: string): Promise<UserStats> { 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);
} }
} }

View File

@ -27,6 +27,7 @@ import {z} from "zod";
import {fetchWithToken, tokenStore} from "@repo/repo.ts"; import {fetchWithToken, tokenStore} from "@repo/repo.ts";
import {pageRepo} from "@repo/page.ts"; import {pageRepo} from "@repo/page.ts";
import {dataRepo} from "@repo/data.ts"; import {dataRepo} from "@repo/data.ts";
import {permsRepo} from "@repo/perms.ts";
export const schemTypes = cached<SchematicType[]>([], () => export const schemTypes = cached<SchematicType[]>([], () =>
fetchWithToken(get(tokenStore), "/data/admin/schematicTypes") fetchWithToken(get(tokenStore), "/data/admin/schematicTypes")
@ -37,6 +38,13 @@ export const players = cached<Player[]>([], async () => {
return z.array(PlayerSchema).parse(await res.json()); 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 () => { export const gamemodes = cached<string[]>([], async () => {
const res = await fetchWithToken(get(tokenStore), "/data/admin/gamemodes"); const res = await fetchWithToken(get(tokenStore), "/data/admin/gamemodes");
return z.array(z.string()).parse(await res.json()); return z.array(z.string()).parse(await res.json());

View File

@ -19,7 +19,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte"; import {onMount} from "svelte";
import {stopPropagation} from "@components/util.ts"; import {stopPropagation} from "@components/utils.ts";
interface Props { interface Props {
title: string; title: string;
@ -68,16 +68,18 @@
}) })
</script> </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"> <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"> <div onclick={stopPropagation(onclick)} aria-hidden="true" class="w-fit">
<div class="p-6 border-b border-neutral-200 dark:border-neutral-700"> <div class="p-6 border-b border-neutral-200 dark:border-neutral-700">
<h1 class="text-4xl font-bold">{title}</h1> <h1 class="text-4xl font-bold">{title}</h1>
</div> </div>
<div class="p-6 main border-b border-neutral-200 dark:border-neutral-700"> <div class="p-6 main border-b border-neutral-200 dark:border-neutral-700">
{@render children?.()} {@render children?.()}
</div> </div>
<div class="flex mx-4 my-2 p-6" onclick={() => dialog.close()} aria-hidden="true"> <div class="mx-4 my-2 p-6">
<div class="ml-auto flex justify-end" onclick={() => dialog.close()} aria-hidden="true">
{@render footer?.()} {@render footer?.()}
</div> </div>
</div> </div>
</div>
</dialog> </dialog>

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>;

View File

@ -50,3 +50,10 @@ export const ServerSchema = z.object({
}); });
export type Server = z.infer<typeof ServerSchema>; 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>;

View File

@ -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>

View File

@ -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>

View File

@ -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>

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,
};

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}
/>

View File

@ -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>

View File

@ -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>

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,
};

View File

@ -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>

View File

@ -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>

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>

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,
};

View File

@ -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>

View File

@ -0,0 +1,3 @@
import Root from "./aspect-ratio.svelte";
export { Root, Root as AspectRatio };

View File

@ -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>

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.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}
/>

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>

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,
};

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>

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"];

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

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,
};

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}

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,
};

View File

@ -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}
/>

View File

@ -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}
/>

View File

@ -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} />

View File

@ -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} />

View File

@ -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} />

View File

@ -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}
/>

View File

@ -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}
/>

View File

@ -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}
/>

View File

@ -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} />

View File

@ -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>

View File

@ -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