372 Commits

Author SHA1 Message Date
bd1c4f7f45 feat: Refactor event management components and introduce EventModel for better state handling
All checks were successful
SteamWarCI Build successful
2025-06-04 11:33:11 +02:00
df389b3acf Merge remote-tracking branch 'origin/master' into event-brackets
All checks were successful
SteamWarCI Build successful
2025-06-01 15:40:33 +02:00
4ecb5fa024 Merge branch 'master' of https://git.steamwar.de/SteamWar/Website
All checks were successful
SteamWarCI Build successful
2025-05-31 21:02:48 +02:00
27f0b962c1 feat: Enhance article styling with code and link formatting 2025-05-31 21:02:24 +02:00
e37583329c src/content/pages/de/verhaltensrichtlienien.md aktualisiert
All checks were successful
SteamWarCI Build successful
2025-05-31 20:55:31 +02:00
20b7a32b1b package.json aktualisiert
All checks were successful
SteamWarCI Build successful
2025-05-31 20:54:29 +02:00
dd7d701c48 Fix formatting
Some checks failed
SteamWarCI Build failed
2025-05-31 09:56:01 +02:00
3173b537bc Fix 'letztes update'
Some checks failed
SteamWarCI Build failed
2025-05-31 09:53:36 +02:00
5e2e4e2281 Fix some verhaltensrichtlienien.md not copied over from the old version.
Some checks failed
SteamWarCI Build failed
2025-05-31 09:50:52 +02:00
da3699167b feat: Add frontmatter editor and enhance page management with YAML support; update dependencies and improve UI interactions
Some checks failed
SteamWarCI Build failed
2025-05-29 12:35:58 +02:00
Chaoscaot
10ff84d410 Add Image left.png
Some checks failed
SteamWarCI Build failed
2025-05-29 00:43:22 +02:00
7d75453be5 Refactor FightTable and GroupTable components to use numeric group identifiers; enhance event handling in FightEdit and EventFightList; add new Pages management UI with editor tabs; improve event data handling and display logic; update event types to include hasFinished status; optimize announcement page rendering and structure.
Some checks failed
SteamWarCI Build failed
2025-05-28 12:30:05 +02:00
TheBreadBeard
86bfaf4683 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-05-26 10:56:46 +02:00
TheBreadBeard
f9212649ad Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-05-26 10:55:26 +02:00
TheBreadBeard
4972ebf9bb Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-05-25 19:19:47 +02:00
d5a2fc20e8 src/content/announcements/de/spacheship-event-ankündigung-und-regelwerk.md aktualisiert
All checks were successful
SteamWarCI Build successful
2025-05-25 17:31:44 +02:00
27c5698ac8 tsconfig.json aktualisiert
All checks were successful
SteamWarCI Build successful
2025-05-25 17:27:55 +02:00
fa5f25f37e Merge pull request 'Merge branch spacheship-event-ankündigung-und-regelwerk' (#15) from spacheship-event-ankündigung-und-regelwerk into master
Some checks failed
SteamWarCI Build failed
Reviewed-on: #15
2025-05-25 17:26:03 +02:00
260b7b24c4 src/content/announcements/de/spacheship-event-ankündigung-und-regelwerk.md aktualisiert
All checks were successful
SteamWarCI Build successful
2025-05-25 17:25:56 +02:00
TheBreadBeard
4aea0c7fea Update spacheship-event-ankündigung-und-regelwerk-2.md
Some checks failed
SteamWarCI Build failed
2025-05-25 17:20:45 +02:00
TheBreadBeard
314ff3e7c3 Create page announcements/de/spacheship-event-ankündigung-und-regelwerk.md
Some checks failed
SteamWarCI Build failed
2025-05-25 14:47:46 +02:00
0205108d2d Implement code changes to enhance functionality and improve performance
All checks were successful
SteamWarCI Build successful
2025-05-23 14:25:29 +02:00
2bf3beb044 feat: Implement group management features with dialogs for editing and displaying group results, enhance event creation with a form, and update team and referee management UI
All checks were successful
SteamWarCI Build successful
2025-05-23 14:23:33 +02:00
b440456687 Merge branch 'event-brackets' of https://git.steamwar.de/SteamWar/Website into event-brackets
All checks were successful
SteamWarCI Build successful
2025-05-22 19:42:17 +02:00
5277c9a3fc feat: Enhance event management with FightEdit and GroupEdit components, including improved data handling and new functionalities 2025-05-22 19:41:49 +02:00
2f2c1be958 Merge pull request 'Fix micro rw' (#13) from fix-micro-rw into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #13
Reviewed-by: Chaoscaot <max@chaoscaot.de>
2025-05-21 23:39:31 +02:00
D4rkr34lm
41c7df0d68 Fix micro rw
All checks were successful
SteamWarCI Build successful
2025-05-21 23:37:08 +02:00
Chaoscaot
cedf641039 Update sw-arcade-fightplan.md
All checks were successful
SteamWarCI Build successful
2025-05-18 13:41:00 +02:00
d9bdc636e3 Merge pull request 'Merge branch sw-arcade-fightplan' (#12) from sw-arcade-fightplan into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #12
2025-05-14 19:44:22 +02:00
Chaoscaot
c8d05cb268 Update sw-arcade-fightplan.md
All checks were successful
SteamWarCI Build successful
2025-05-14 19:43:02 +02:00
cb2564c9ce src/content/announcements/de/sw-arcade-fightplan.md aktualisiert
Some checks failed
SteamWarCI Build failed
2025-05-14 19:37:05 +02:00
Chaoscaot
80caf8fe6d Create page announcements/de/sw-arcade-fightplan
All checks were successful
SteamWarCI Build successful
2025-05-14 19:36:07 +02:00
c4f8824115 Merge branch 'master' into event-brackets
All checks were successful
SteamWarCI Build successful
2025-05-11 10:06:22 +02:00
1da279bb24 feat: Add FightEdit and GroupEdit components for enhanced event management
All checks were successful
SteamWarCI Build successful
2025-05-10 22:22:12 +02:00
Tim7077
fd3d621fd5 Update warship.md
All checks were successful
SteamWarCI Build successful
2025-05-10 21:44:18 +02:00
7d67ad0950 Refactor stores and types for improved data handling and schema definitions
All checks were successful
SteamWarCI Build successful
- Consolidated player fetching logic in stores.ts to utilize dataRepo.
- Introduced teams fetching logic in stores.ts.
- Updated permissions structure in stores.ts for better clarity.
- Enhanced data schemas in data.ts with new ResponseUser and ResponseTeam schemas.
- Expanded event-related schemas in event.ts to include groups, relations, and event creation/update structures.
- Improved code formatting for consistency and readability across files.
2025-05-08 21:47:36 +02:00
6377799e1b style: Improve code formatting and readability across multiple components
All checks were successful
SteamWarCI Build successful
2025-05-07 14:33:48 +02:00
b3598e1ee1 style: Improve code formatting and readability in FightStatistics component
All checks were successful
SteamWarCI Build successful
2025-05-06 13:42:49 +02:00
b9db5be858 style: Improve formatting and readability of WarShip rules
All checks were successful
SteamWarCI Build successful
2025-04-22 23:34:13 +02:00
3e54934806 feat: Enable autoDarkMode in Basic layout for admin new page
All checks were successful
SteamWarCI Build successful
2025-04-18 12:46:21 +02:00
98638f94fc feat: Add autoDarkMode support to Basic layout and update admin index
All checks were successful
SteamWarCI Build successful
2025-04-18 12:43:09 +02:00
4da8fe50c0 feat: Refactor EventEdit and EventFightList components for improved UI and functionality
All checks were successful
SteamWarCI Build successful
- Enhanced EventEdit component with AlertDialog for delete confirmation.
- Added Menubar component to EventFightList for batch editing options.
- Updated alert-dialog components to streamline props and improve reactivity.
- Refactored menubar components for better structure and usability.
- Improved accessibility and code readability across various components.
2025-04-16 12:55:10 +02:00
7757978668 refactor: clean up imports and improve player search functionality in RefereesList
All checks were successful
SteamWarCI Build successful
2025-04-16 00:17:10 +02:00
9eea0b2b3f feat: enhance EventFightList with grouping and selection features
All checks were successful
SteamWarCI Build successful
- Added grouping functionality to the EventFightList component, allowing fights to be grouped by their associated group.
- Implemented row selection with checkboxes for both individual fights and groups, enabling bulk selection.
- Updated columns definition to include a checkbox for selecting all rows and individual row selection checkboxes.
- Modified the checkbox component to support indeterminate state and improved styling.
- Enhanced date formatting for fight start times in the table.
2025-04-15 16:28:19 +02:00
063638d016 Add TeamTable component and improve EventView layout
All checks were successful
SteamWarCI Build successful
2025-04-14 23:31:19 +02:00
f5a778d9b4 Trigger Rebuild
All checks were successful
SteamWarCI Build successful
2025-04-14 18:21:26 +02:00
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
Lixfel
78e1a7b726 Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-04-12 21:29:10 +02:00
Chaoscaot
cd485e8dda Update sw-arcade.md
All checks were successful
SteamWarCI Build successful
2025-04-06 23:07:10 +02:00
Chaoscaot
182c402c7e Update sw-arcade.md
Some checks failed
SteamWarCI Build failed
2025-04-06 23:05:35 +02:00
Chaoscaot
098f5b9270 Create page announcements/de/sw-arcade.md
Some checks failed
SteamWarCI Build failed
2025-04-06 23:04:24 +02:00
Lixfel
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
Lixfel
9c8c02f679 Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-03-31 19:43:24 +02:00
Lixfel
3b5fdc57c0 Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-03-25 06:40:14 +01:00
Lixfel
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
D4rkr34lm
17460772e9 Addapted script side for new example hotkey script
All checks were successful
SteamWarCI Build successful
2025-03-21 18:54:26 +01:00
Lixfel
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
Lixfel
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
Lixfel
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
Lixfel
09dc28b6da Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-03-02 15:07:14 +01:00
Lixfel
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
Lixfel
d7d20e4347 Update wgs25-kampfplan.md
All checks were successful
SteamWarCI Build successful
2025-03-01 10:50:17 +01:00
Lixfel
43bd8f4a7c Update wgs25-kampfplan.md
Some checks failed
SteamWarCI Build failed
2025-03-01 10:49:03 +01:00
Lixfel
18e8627b54 Update wgs25-kampfplan.md
Some checks failed
SteamWarCI Build failed
2025-03-01 10:42:19 +01:00
Lixfel
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
TheBreadBeard
86b479fb28 Update missilewars-iii-eventplan.md
All checks were successful
SteamWarCI Build successful
2025-02-23 18:34:53 +01:00
Chaoscaot
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
Chaoscaot
cfede8f299 Update missilewars-iii-eventplan.md
All checks were successful
SteamWarCI Build successful
2025-02-23 09:47:30 +01:00
TheBreadBeard
597153ed39 Update missilewars-iii-eventplan.md
All checks were successful
SteamWarCI Build successful
2025-02-23 07:43:22 +01:00
TheBreadBeard
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
TheBreadBeard
23f35a35c4 Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:58:50 +01:00
TheBreadBeard
973f469c7b Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:58:04 +01:00
TheBreadBeard
107caafc26 Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:57:15 +01:00
TheBreadBeard
7f26845802 Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:54:28 +01:00
TheBreadBeard
37b2e82e05 Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:53:28 +01:00
TheBreadBeard
7e2ba9dbce Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:50:59 +01:00
TheBreadBeard
69426da5be Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:50:11 +01:00
TheBreadBeard
fd2ad65ad4 Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:46:58 +01:00
TheBreadBeard
a728651cca Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 21:46:04 +01:00
TheBreadBeard
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
TheBreadBeard
9b49a0f81c Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 19:46:02 +01:00
TheBreadBeard
11144043c1 Update missilewars-iii-ankündigung.md
All checks were successful
SteamWarCI Build successful
2025-01-29 19:39:12 +01:00
TheBreadBeard
e8f866ce8a Update missilewars-iii-ankündigung.md
Some checks failed
SteamWarCI Build failed
2025-01-29 19:28:01 +01:00
TheBreadBeard
e57a90feaf Create page announcements/de/missilewars-iii-ankündigung.md
Some checks failed
SteamWarCI Build failed
2025-01-29 19:19:43 +01:00
TheBreadBeard
3aa8fea1fd Update verhaltensrichtlinien.md
All checks were successful
SteamWarCI Build successful
2025-01-29 18:59:19 +01:00
TheBreadBeard
95b327951c Update verhaltensrichtlienien.md
All checks were successful
SteamWarCI Build successful
2025-01-29 18:51:55 +01:00
TheBreadBeard
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
Friderik
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
TheBreadBeard
437dfa223b Update neujahrsevent-2025-eventplan.md 2025-01-01 18:34:35 +01:00
012a56e177 Merge remote-tracking branch 'origin/master' 2025-01-01 17:09:02 +01:00
9f60071e48 Some (alot) of fixes 2025-01-01 17:08:55 +01:00
3bcadde949 Merge pull request 'Merge branch neujahrsevent-2025-eventplan' (#23) from neujahrsevent-2025-eventplan into master
Reviewed-on: https://steamwar.de/devlabs/SteamWar/Website/pulls/23
2025-01-01 15:20:47 +01:00
f5332411d2 Fixes 2025-01-01 14:25:33 +01:00
TheBreadBeard
b546c7b2b2 Update neujahrsevent-2025-eventplan.md 2025-01-01 13:45:48 +01:00
TheBreadBeard
7716aa1e89 Create page announcements/de/neujahrsevent-2025-eventplan.md 2025-01-01 13:42:09 +01:00
a7e961fc0c Fixes 2024-12-21 16:13:33 +01:00
8c6f5f5729 Fix Render 2024-12-21 16:06:23 +01:00
41a3b75c97 Fix Render 2024-12-21 16:03:45 +01:00
fe9610a970 Kampfplan 2024-12-21 15:58:22 +01:00
6b38f37711 Add Redirects 2024-12-17 20:44:53 +01:00
68b8e92661 Add latest announcements 2024-12-15 14:51:55 +01:00
a29689da0a Add latest announcements 2024-12-15 14:50:32 +01:00
292d1b6bcc Fixes 2024-12-15 12:02:54 +01:00
9e6ef73ccb Fixes 2024-12-15 11:59:28 +01:00
02ab822801 Fixes 2024-12-15 11:49:43 +01:00
d7000c084b Fixes 2024-12-14 18:10:50 +01:00
d4ac123654 Fix Modal 2024-12-08 17:21:17 +01:00
e93445d933 Merge branch 'kick-ai-bots' 2024-12-08 17:10:32 +01:00
2383cd6472 Fix Styles 2024-12-08 17:10:01 +01:00
d86a8493d1 Merge pull request 'Give AI bots the boot' (#22) from kick-ai-bots into master
Reviewed-on: https://steamwar.de/devlabs/SteamWar/Website/pulls/22
Reviewed-by: Chaoscaot <chaos@chaoscaot.de>
2024-11-26 15:34:50 +01:00
Lixfel
5f90025493 Give AI bots the boot
Mix of

- https://blog.cloudflare.com/declaring-your-aindependence-block-ai-bots-scrapers-and-crawlers-with-a-single-click/
- https://www.google.com/robots.txt
2024-11-26 15:31:44 +01:00
bbb6d87ccc Fix Build 2024-11-24 23:05:29 +01:00
0ab7d204f2 Fix Build 2024-11-24 23:00:44 +01:00
72933a46d1 Update 2024-11-24 22:57:21 +01:00
bbf13cf203 Update 2024-11-24 22:46:47 +01:00
cb65e96165 Update Backend 2024-11-23 13:28:33 +01:00
e70951c9dd Fix Stats 2024-08-15 17:10:00 +02:00
d60d29bf98 fixes... 2024-08-15 17:07:06 +02:00
2321289edb fixes... 2024-08-15 17:04:19 +02:00
a70b00ae14 Add Schem Download 2024-08-15 17:04:06 +02:00
fe37a70d26 smoll Fixes 2024-08-15 16:46:01 +02:00
f652b4a4f2 Trigger Rebuild 2024-08-15 16:42:13 +02:00
22f24e92c3 changes 2024-08-15 16:38:50 +02:00
Chaoscaot
4afd833276 Create page announcements/de/ein-test.md 2024-08-15 15:52:19 +02:00
6c6767ce05 Fix frostbite.glb Location 2024-08-14 00:58:55 +02:00
530e0d9f19 Add Public Images 2024-08-13 19:14:26 +02:00
9d95da1fbe Merge remote-tracking branch 'origin/master' 2024-08-13 13:01:36 +02:00
aafb8ad56a Fixes... 2024-08-13 13:01:30 +02:00
b9e3e68b9b Fixes... 2024-08-13 13:00:00 +02:00
4b368f73e8 Fixes... 2024-08-13 12:55:04 +02:00
74b47efcad Add Shadow 2024-08-13 12:43:25 +02:00
60c1565d9d Fixes... 2024-08-13 12:34:03 +02:00
0cf9a8a48d Change to Border 2024-08-13 12:19:04 +02:00
ee8eeb8ad1 Fix 2024-08-13 12:12:50 +02:00
40a055c2a1 Cards Idea 2024-08-13 12:09:35 +02:00
38728774fd Redesign Cards 2024-08-13 10:56:47 +02:00
be4967f34b Redesign Cards 2024-08-13 10:48:31 +02:00
52c9ed0118 *another* Fix page 2024-08-12 22:38:54 +02:00
38047b1ddf Fix page 2024-08-12 22:32:39 +02:00
39512dc131 Maybe new Image...? 2024-08-12 22:29:23 +02:00
7c18feaa98 Maybe new Image... 2024-08-12 22:21:48 +02:00
659f23f317 Maybe new Image... 2024-08-12 22:13:47 +02:00
c9a06443b2 Fixes... 2024-08-12 21:06:35 +02:00
f11769b46b refactor: Rename "server" to "bau" in common translation file 2024-08-08 10:23:45 +02:00
e84e2b7e31 Lixfel anmerkungen #3 2024-08-07 22:33:41 +02:00
154c25ca36 Lixfel anmerkungen #2 2024-08-07 22:19:04 +02:00
25a9f21ab4 Lixfel anmerkungen #1 2024-08-06 18:58:07 +02:00
d9f34e6316 Fix Lang 2024-08-05 00:48:01 +02:00
5409d4a248 Redo some Startpage :D 2024-08-05 00:37:17 +02:00
4778429452 Add experimental Search Feature 2024-08-02 01:26:05 +02:00
f0426f5225 Fix 2024-07-29 13:07:00 +02:00
4bdf059c9a Fix 2024-07-29 13:03:14 +02:00
e0d2d8b427 Another Try 2024-07-29 12:59:18 +02:00
4d5504ef68 Fixes and Changes 2024-07-29 12:55:50 +02:00
39e4a37ac5 Fix Pages 2024-05-19 22:35:36 +02:00
3862a85150 Fix Pages 2024-05-19 22:35:07 +02:00
7859c3c797 Fix Pages 2024-05-19 22:34:37 +02:00
5ce099e49d Merge branch 'refs/heads/gitbutler/integration' 2024-05-19 22:31:44 +02:00
e65f9aaaa4 Refactoring 2024-05-19 22:31:34 +02:00
aef5d2f2eb Refactoring 2024-05-19 22:30:30 +02:00
TheBreadBeard
e01f3852d5 Update password.md, fixed grammar 2024-05-19 21:22:11 +02:00
TheBreadBeard
ed5c22222a Update about.md. slight grammar fixes 2024-05-19 21:20:31 +02:00
GitButler
5ac4a80a5c GitButler Integration Commit
This is an integration commit for the virtual branches that GitButler is tracking.

Due to GitButler managing multiple virtual branches, you cannot switch back and
forth between git branches and virtual branches easily. 

If you switch to another branch, GitButler will need to be reinitialized.
If you commit on this branch, GitButler will throw it away.

Here are the branches that are currently applied:
 - Virtual branch (refs/gitbutler/Virtual-branch)
   branch head: 6e6eb5069f
   - .gitignore
 - update-frostbite-images-2024 (refs/gitbutler/update-frostbite-images-2024)
   branch head: 6e6eb5069f
   - .gitignore
 - master (refs/gitbutler/master)

Your previous branch was: 073d474bc3

The sha for that commit was: 073d474bc3

For more information about what we're doing here, check out our docs:
https://docs.gitbutler.com/features/virtual-branches/integration-branch
2024-05-03 20:52:06 +02:00
073d474bc3 Refactoring 2024-03-25 21:22:53 +01:00
6e6eb5069f feat: Add .idea directory to .gitignore
Ignore the .idea directory to prevent unnecessary files being tracked.
2024-03-25 01:34:38 +01:00
195a66fd60 Add Password Reset 2024-03-25 00:39:43 +01:00
899b41d051 Unification 2024-03-24 23:52:55 +01:00
296fe40085 Unification 2024-03-24 23:35:50 +01:00
f062f3eaf9 Add Xray Preview 2024-03-24 21:34:02 +01:00
7cf76bc7c7 Updates 2024-03-16 00:15:54 +01:00
91978ce03c Updates 2024-03-16 00:13:23 +01:00
85f570433d Updates 2024-03-16 00:11:02 +01:00
49fe9728e5 Neues Referee zeug 2024-03-15 22:41:44 +01:00
b52b2bcfe1 Update to Astro 4.5 2024-03-11 22:40:14 +01:00
f888b6946b Fix Lang 2024-03-10 08:13:13 +01:00
e800c7b345 Add WGS Kampfplan 2024-03-09 19:47:48 +01:00
0590cd349a Add more Prerendering 2024-03-09 18:22:14 +01:00
7599038c82 Fix Elo Page fr fr 2024-03-09 14:32:22 +01:00
c5e2ad9426 Fix Elo Page fr 2024-03-09 14:30:37 +01:00
72cf00934a Fix Elo Page 2024-03-09 14:25:18 +01:00
1b55503e12 Fix Elo Page 2024-03-09 14:25:08 +01:00
08371f57d1 Fix Elo Page 2024-03-09 14:23:11 +01:00
30808bfd5b Fix Elo Page 2024-03-09 14:21:26 +01:00
e08f75d849 Add Page Filter 2024-03-09 14:19:00 +01:00
82d0403c88 Add Page Filter 2024-03-09 14:16:58 +01:00
228bb43518 Fix Language Warning 2024-03-09 14:00:38 +01:00
72df9c1833 Remove migration code 2024-03-09 13:57:43 +01:00
c6d8ccdf92 Fixing... 2024-03-09 13:56:06 +01:00
9312089e96 Migrate Site to German as Default Locale 2024-03-09 13:53:43 +01:00
fd56de0451 TIFU 2024-03-06 17:13:07 +01:00
42436e5d3b TIFU 2024-03-06 17:10:40 +01:00
404404d19c Fix 2024-03-06 17:09:54 +01:00
eec0203e15 Add Ranked Page 2024-03-06 17:08:06 +01:00
8a7c8597da Add Teardown code 2024-03-06 16:58:41 +01:00
7c473d3545 Add Teardown code 2024-03-06 16:56:31 +01:00
f2e2a6ff59 Add Teardown code 2024-03-06 16:54:46 +01:00
16cb987aff Fixing... 2024-03-06 16:49:36 +01:00
3de8832689 3D Public Preview Initial Test 2024-03-06 15:46:28 +01:00
d46b3ec511 Fix Styles 2024-03-01 21:59:37 +01:00
ea46ad57dd use ViewTransitions Hero 2024-03-01 21:58:30 +01:00
474187899f Add Table Styles 2024-03-01 21:45:50 +01:00
77bf19a1c8 Remove Reload 2024-02-28 17:42:10 +01:00
c613cef5c4 Remove Reload 2024-02-28 17:38:18 +01:00
678746c89b Begin Display, Add View Transitions 2024-02-28 17:28:21 +01:00
361d7dae6a New Backdrop 2024-02-25 11:32:39 +01:00
4c1b337676 Update Dependencies and Refactor Navbar 2024-02-25 11:24:19 +01:00
04859c6858 Bouncy Cards 2024-02-25 10:03:47 +01:00
7448a77bb1 Add 3D Card Effect 2024-02-21 23:59:14 +01:00
568e8dbf7d Again 2024-02-17 02:53:15 +01:00
dee73abb90 Again 2024-02-17 02:51:58 +01:00
dc771db3e4 Adjust Chart 2024-02-17 02:49:44 +01:00
af870a70bc Optimize astro.config.mjs 2024-02-16 23:51:55 +01:00
4ed5ed3e35 Optimize WarGear Links 2024-02-16 23:48:52 +01:00
d15e37d6a1 Update Dependencies 2024-02-13 21:43:33 +01:00
deef2ae345 Use the LightMode 2024-02-13 20:31:47 +01:00
5fb5234370 use Skin Cache 2024-02-12 19:13:02 +01:00
9fd8ddb9bd Code Cleanup™ 2024-02-11 11:16:23 +01:00
4b27eb76fe Merge remote-tracking branch 'origin/master' 2024-01-07 13:38:32 +01:00
076fc7046d Fix TypeAheadSearch.svelte, Remove Logout 2024-01-07 13:38:27 +01:00
dd8a1c023b Merge pull request 'Merge branch AboutEnglish' (#17) from AboutEnglish into master
Reviewed-on: https://steamwar.de/devlabs/SteamWar/Website/pulls/17
Reviewed-by: YoyoNow <jwsteam@nidido.de>
2024-01-07 13:24:57 +01:00
YoyoNow
1c6f770f25 Update about.md 2024-01-07 13:21:07 +01:00
c76811d5f5 Merge pull request 'Merge branch fix-about' (#16) from fix-about into master
Reviewed-on: https://steamwar.de/devlabs/SteamWar/Website/pulls/16
Reviewed-by: YoyoNow <jwsteam@nidido.de>
2024-01-07 13:16:44 +01:00
Chaoscaot
b5ced01722 Update about.md 2024-01-07 13:16:17 +01:00
dd85aeca7b Update Schematic Modal 2024-01-07 13:14:51 +01:00
66c57e8aa6 Fix Freeze 2024-01-07 12:42:48 +01:00
cb6d5bfd9e Fix Freeze 2024-01-07 12:40:40 +01:00
8d64901d07 Rebuild 2024-01-06 22:47:34 +01:00
890f71d144 Update Wargear Regelwerk 2024-01-06 22:37:06 +01:00
36c7505c5d Fix Token 2024-01-06 21:36:22 +01:00
925db8b356 Fix Translation 2024-01-06 17:39:21 +01:00
fb0e5bb824 Fix API 2024-01-06 17:37:19 +01:00
6cff2bd7ec Fix Font (For real now) 2024-01-06 17:30:53 +01:00
e2ec2229b0 Fix Font (Finally) 2024-01-06 17:28:23 +01:00
c4d44ca4fa Fix Font (Hopefully) 2024-01-06 17:25:16 +01:00
ce27973c68 Add Roboto 2024-01-06 17:19:59 +01:00
5e57a8c99d Fix CI 2024-01-06 16:43:03 +01:00
0a0946e335 Remove Schem Download 2024-01-06 16:39:52 +01:00
926624542d Test Build&Release 2024-01-06 16:07:24 +01:00
dd8bd1e094 Change to IPv4 2024-01-06 15:48:25 +01:00
0f74e7f7e8 Add to Git ignore 2024-01-06 15:43:10 +01:00
4442909633 Delete 2024-01-06 15:41:46 +01:00
36c2695c8c Add Alt Button 2024-01-06 15:38:35 +01:00
9ee0fd5448 Updates 2024-01-06 15:08:54 +01:00
efd674eae1 Some Code Cleanup 2023-12-27 19:18:08 +01:00
9a16c4b560 Some Code Cleanup 2023-12-27 19:16:54 +01:00
3108d9bf20 Some Code Cleanup 2023-12-25 21:54:40 +01:00
a2687083e0 Images 2023-12-24 23:00:20 +01:00
c5effd8f7f Test CI 2023-12-23 16:50:46 +01:00
8fcab610fb Add EsLint 2023-12-23 15:36:22 +01:00
5a5cce199b Change to PW 2023-12-19 19:22:19 +01:00
Chaoscaot
aa83cddcec Update warship.md 2023-12-16 13:30:38 +01:00
Chaoscaot
d8db4ae2d3 Create page rules/en/warship.md 2023-12-16 13:29:10 +01:00
643db90b15 Updates 2023-12-15 16:11:33 +01:00
a72dd2124d Adjustments 2023-12-14 22:14:41 +01:00
ecb906e614 Add how to create token 2023-12-14 21:07:31 +01:00
2286c6a3eb Some a11y and Lighthouse testing 2023-12-10 21:52:07 +01:00
6c9c496f05 Fixing and Sitemap and Robots.txt 2023-12-10 17:14:10 +01:00
3d95bffb6a Transfer some Posts and automate original German 2023-12-10 01:48:53 +01:00
311856415e Migrate to dayjs and Astro 4.0 2023-12-07 00:17:32 +01:00
505ee26622 Comment out Helpcenter 2023-12-05 18:12:07 +01:00
224a3929aa Finish MVP 2023-12-05 17:55:48 +01:00
0fc220ce94 Refactor 2023-12-05 17:36:31 +01:00
89e6f9cff4 Some™️Pages 2023-12-05 15:36:11 +01:00
fbd52f3edb New Code Editor and fun 2023-12-03 19:31:29 +01:00
2abe554059 Scheiß Line Separator 2023-11-28 12:00:06 +01:00
3996376381 Download and Rules 2023-11-18 16:52:54 +01:00
3889f28eb8 Updates and more 2023-11-12 22:43:42 +01:00
7450ecdabb Updates and more 2023-11-05 22:27:20 +01:00
e97e86f9ac Updates and more 2023-11-03 20:31:27 +01:00
Chaoscaot
b5a54d087b Update imprint.md 2023-10-12 21:07:56 +02:00
Chaoscaot
d476a1500a Create page pages/de/imprint.md 2023-10-12 21:06:14 +02:00
c5164f2bd3 Updates and more 2023-10-12 21:02:57 +02:00
Chaoscaot
f10d4c17d6 Update about.md 2023-10-08 19:51:41 +02:00
Chaoscaot
d84de0ab46 Create page pages/en/about.md 2023-10-08 19:51:18 +02:00
Chaoscaot
0eb05e4f64 Delete Test.md 2023-10-08 15:44:30 +02:00
Chaoscaot
c620a0f0d4 Update Test.md 2023-10-08 15:42:21 +02:00
Chaoscaot
8cc3072c3c Merge pull request 'Test2' (#2) from dev into master 2023-10-08 15:30:39 +02:00
Chaoscaot
ba817a4a88 Create page pages/en/Test.md 2023-10-08 15:26:50 +02:00
e65a70aa40 Changes 2023-10-08 15:08:39 +02:00
Chaoscaot
17d46c3964 Create page src/content/pages/en/about.md 2023-10-08 15:07:29 +02:00
48961abdf6 Changes 2023-10-08 14:34:38 +02:00
Chaoscaot
51a605ffa5 2 2023-10-08 12:34:18 +02:00
Chaoscaot
0c534990e8 Save 2023-10-08 12:32:39 +02:00
Chaoscaot
e0f2702eca Changes 2023-10-01 10:04:04 +02:00
7728d9e177 Update testfile.txt 2023-09-30 16:34:40 +02:00
Chaoscaot
9f2ce1c8c6 Add testfile.txt 2023-09-30 15:16:17 +02:00
Chaoscaot
53443cf383 lil home page 2023-09-24 16:33:14 +02:00
Chaoscaot
4dca085cec Initial Commit 2023-09-20 20:52:40 +02:00
108 changed files with 1542 additions and 5384 deletions

View File

@@ -37,6 +37,10 @@
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"

View File

@@ -1,4 +1,4 @@
import { defineConfig, sharpImageService } from "astro/config";
import {defineConfig, sharpImageService} from "astro/config";
import svelte from "@astrojs/svelte";
import tailwind from "@astrojs/tailwind";
import configureI18n from "./astro-i18n.adapter";
@@ -8,8 +8,6 @@ import robotsTxt from "astro-robots-txt";
import path from "node:path";
import mdx from "@astrojs/mdx";
import starlight from "@astrojs/starlight";
// https://astro.build/config
export default defineConfig({
output: "static",
@@ -20,40 +18,14 @@ export default defineConfig({
site: "https://steamwar.de",
integrations: [
svelte(),
starlight({
disable404Route: true,
title: "SteamWar Docs",
defaultLocale: "de",
logo: {
src: "./src/images/logo.png",
},
social: [
{ icon: "discord", label: "Discord", href: "https://steamwar.de/discord" },
{ icon: "document", label: "Gitea", href: "https://git.steamwar.de" },
],
sidebar: [
{ label: "Startseite", slug: "docs" },
{ label: "Bau", badge: "WIP", items: ["docs/bausystem", { label: "Script System", items: ["docs/bausystem/script"] }] },
{ label: "Kampfsystem", badge: "WIP", items: ["docs/fightsystem"] },
{ label: "Minigames", badge: "WIP", items: ["docs/minigames"] },
{ label: "Schematicsystem", badge: "WIP", items: ["docs/schematicsystem"] },
{ label: "API", badge: "WIP", items: ["docs/api"] },
],
editLink: {
baseUrl: "https://git.steamwar.de/SteamWar/Website/src/branch/master/",
},
}),
tailwind({
configFile: "./tailwind.config.js",
applyBaseStyles: false,
}),
configureI18n(),
sitemap({
i18n: {
defaultLocale: "en",
locales: {
en: "en-US",
de: "de-DE",
defaultLocale: "en", locales: {
en: "en-US", de: "de-DE",
},
},
}),
@@ -77,7 +49,7 @@ export default defineConfig({
{ userAgent: "omgili", disallow: "/" },
{ userAgent: "OmigliBot", disallow: "/" },
{ userAgent: "PerplexityBot", disallow: "/" },
{ userAgent: "Timpibot", disallow: "/" },
{ userAgent: "Timpibot", disallow: "/" }
],
}),
mdx(),
@@ -94,8 +66,8 @@ export default defineConfig({
"@layouts": path.resolve("./src/layouts"),
"@repo": path.resolve("./src/components/repo"),
"@stores": path.resolve("./src/components/stores"),
$lib: path.resolve("./src"),
"$lib": path.resolve("./src"),
},
},
},
});
});

View File

@@ -58,8 +58,6 @@
"dependencies": {
"@astrojs/mdx": "^4.3.0",
"@astrojs/sitemap": "^3.4.0",
"@astrojs/starlight": "^0.34.4",
"@astrojs/starlight-tailwind": "^4.0.1",
"@codemirror/commands": "^6.8.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/view": "^6.36.8",
@@ -86,9 +84,11 @@
"svelte-spa-router": "^4.0.1"
},
"pnpm": {
"ignoredBuiltDependencies": [
"esbuild"
],
"onlyBuiltDependencies": [
"@parcel/watcher",
"esbuild",
"sharp"
]
}

View File

@@ -24,24 +24,35 @@
import type { ExtendedEvent } from "@type/event.ts";
import "@styles/table.css";
let {
event,
group,
rows = 1,
}: {
event: ExtendedEvent;
group: number;
rows?: number;
} = $props();
export let event: ExtendedEvent;
export let group: number;
export let rows: number = 1;
let teamPoints = $derived(
Object.entries(event.groups.find((g) => g.id === group)?.points ?? {})
.map(([teamId, points]) => ({
team: event.teams.find((t) => t.id === Number(teamId))!!,
points: points,
}))
.sort((a, b) => b.points - a.points)
);
$: teamPoints = event.teams
.map((team) => {
let fights = event.fights.filter((fight) => fight.blueTeam.id === team.id || fight.redTeam.id === team.id);
if (group !== undefined) {
fights = fights.filter((fight) => fight.group?.id === group);
}
const points = fights.reduce((acc, fight) => {
if (fight.ergebnis === 1 && fight.blueTeam.id === team.id) {
return acc + (fight.group?.pointsPerWin ?? 3);
} else if (fight.ergebnis === 2 && fight.redTeam.id === team.id) {
return acc + (fight.group?.pointsPerWin ?? 3);
} else if (fight.ergebnis === 3) {
return acc + (fight.group?.pointsPerDraw ?? 1);
} else {
return acc + (fight.group?.pointsPerLoss ?? 0);
}
}, 0);
return {
team,
points,
};
})
.sort((a, b) => b.points - a.points);
</script>
<div class="p-3 bg-gray-200 dark:bg-neutral-800 rounded-2xl w-3/4 mx-auto">

View File

@@ -1,7 +1,7 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2026 SteamWar.de-Serverteam
- Copyright (C) 2023 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
@@ -18,20 +18,20 @@
-->
<script lang="ts">
import { preventDefault } from "svelte/legacy";
import { l } from "@utils/util.ts";
import { t } from "astro-i18n";
import { get } from "svelte/store";
import { navigate } from "astro:transitions/client";
import { onMount } from "svelte";
import { authV2Repo } from "./repo/authv2.ts";
import { preventDefault } from 'svelte/legacy';
import {l} from "@utils/util.ts";
import {t} from "astro-i18n";
import {get} from "svelte/store";
import {navigate} from "astro:transitions/client";
let username: string = $state("");
let pw: string = $state("");
let error: string = $state("");
async function login() {
let { authV2Repo } = await import("./repo/authv2.ts");
let {authV2Repo} = await import("./repo/authv2.ts");
if (username === "" || pw === "") {
pw = "";
error = t("login.error");
@@ -52,26 +52,6 @@
error = t("login.error");
}
}
onMount(() => {
if (window.location.hash.includes("access_token")) {
const params = new URLSearchParams(window.location.hash.substring(1));
const accessToken = params.get("access_token");
if (accessToken) {
(async () => {
let auth = await $authV2Repo.loginDiscord(accessToken);
if (!auth) {
pw = "";
error = t("login.error");
return;
}
navigate(l("/dashboard"));
})();
}
}
});
</script>
<form class="bg-gray-100 dark:bg-neutral-900 p-12 rounded-2xl shadow-2xl border-2 border-gray-600 flex flex-col" onsubmit={preventDefault(login)}>
@@ -83,16 +63,12 @@
<input type="password" id="password" name="password" placeholder={t("login.placeholder.password")} bind:value={pw} />
</div>
<p class="mt-2">
<a class="text-neutral-500 hover:underline" href={l("/set-password")}>{t("login.setPassword")}</a>
</p>
<a class="text-neutral-500 hover:underline" href={l("/set-password")}>{t("login.setPassword")}</a></p>
{#if error}
<p class="mt-2 text-red-500">{error}</p>
{/if}
<button class="btn mt-4 !mx-0 justify-center" type="submit" onclick={preventDefault(login)}>{t("login.submit")}</button>
<a class="btn mt-4 !mx-0 justify-center" href="https://discord.com/oauth2/authorize?client_id=869606970099904562&response_type=token&redirect_uri=https%3A%2F%2Fsteamwar.de%2Flogin&scope=identify">
{t("login.discord")}
</a>
</form>
<style lang="postcss">
@@ -103,4 +79,4 @@
label {
@apply text-neutral-300;
}
</style>
</style>

View File

@@ -81,7 +81,6 @@
</button>
<div>
<a class="btn btn-gray" href={l("/announcements")}>{t("navbar.links.home.announcements")}</a>
<a class="btn btn-gray" href={l("/events")}>{t("navbar.links.home.events")}</a>
<a class="btn btn-gray" href={l("/downloads")}>{t("navbar.links.home.downloads")}</a>
<a class="btn btn-gray" href={l("/faq")}>{t("navbar.links.home.faq")}</a>
<a class="btn btn-gray" href={l("/code-of-conduct")}>{t("navbar.links.rules.coc")}</a>

View File

@@ -18,103 +18,83 @@
-->
<script lang="ts">
import { Input, Label, Select } from "flowbite-svelte";
import {Input, Label, Select} from "flowbite-svelte";
import TypeAheadSearch from "./TypeAheadSearch.svelte";
import { gamemodes, groups, maps } from "@stores/stores.ts";
import type { Team } from "@type/team.ts";
import {gamemodes, groups, maps, players} from "@stores/stores.ts";
import type {Team} from "@type/team.ts";
interface Props {
teams?: Team[];
blueTeam: string;
redTeam: string;
start?: string;
gamemode?: string;
map?: string;
spectatePort?: string | null;
group?: string | null;
groupSearch?: string;
}
interface Props {
teams?: Team[];
blueTeam: string;
redTeam: string;
start?: string;
gamemode?: string;
map?: string;
spectatePort?: string | null;
group?: string | null;
groupSearch?: string;
}
let {
teams = [],
blueTeam = $bindable(),
redTeam = $bindable(),
start = $bindable(""),
gamemode = $bindable(""),
map = $bindable(""),
spectatePort = $bindable(null),
group = $bindable(""),
groupSearch = $bindable(""),
}: Props = $props();
let {
teams = [],
blueTeam = $bindable(),
redTeam = $bindable(),
start = $bindable(""),
gamemode = $bindable(""),
map = $bindable(""),
spectatePort = $bindable(null),
group = $bindable(""),
groupSearch = $bindable("")
}: Props = $props();
let selectableTeams = $derived(
teams
.map((team) => {
return {
name: team.name,
value: team.id.toString(),
};
})
.sort((a, b) => a.name.localeCompare(b.name))
);
let selectableTeams = $derived(teams.map(team => {
return {
name: team.name,
value: team.id.toString()
};
}).sort((a, b) => a.name.localeCompare(b.name)));
let selectableGamemodes = $derived(
$gamemodes
.map((gamemode) => {
return {
name: gamemode,
value: gamemode,
};
})
.sort((a, b) => a.name.localeCompare(b.name))
);
let selectableGamemodes = $derived($gamemodes.map(gamemode => {
return {
name: gamemode,
value: gamemode
};
}).sort((a, b) => a.name.localeCompare(b.name)));
let customGamemode = $derived(!selectableGamemodes.some((e) => e.name === gamemode) && gamemode !== "");
let selectableCustomGamemode = $derived([
...selectableGamemodes,
{
...selectableGamemodes, {
name: gamemode + " (custom)",
value: gamemode,
},
value: gamemode
}
]);
let mapsStore = $derived(maps(gamemode));
let selectableMaps = $derived(
$mapsStore
.map((map) => {
return {
name: map,
value: map,
};
})
.sort((a, b) => a.name.localeCompare(b.name))
);
let selectableMaps = $derived($mapsStore.map(map => {
return {
name: map,
value: map
};
}).sort((a, b) => a.name.localeCompare(b.name)));
let customMap = $derived(!selectableMaps.some((e) => e.name === map) && map !== "");
let selectableCustomMaps = $derived([
...selectableMaps,
{
...selectableMaps, {
name: map + " (custom)",
value: map,
},
value: map
}
]);
let selectableGroups = $derived([
{
name: "None",
value: "",
},
{
value: groupSearch,
name: `Create: '${groupSearch}'`,
},
...$groups
.map((group) => {
return {
name: group,
value: group,
};
})
.sort((a, b) => a.name.localeCompare(b.name)),
]);
let selectableGroups = $derived([{
name: "None",
value: ""
}, {
value: groupSearch,
name: `Create: '${groupSearch}'`
}, ...$groups.map(group => {
return {
name: group,
value: group
};
}).sort((a, b) => a.name.localeCompare(b.name))]);
</script>
<div class="m-2">
@@ -127,29 +107,32 @@
</div>
<div class="mt-4">
<Label for="fight-start">Start</Label>
<Input id="fight-start" bind:value={start}>
<Input id="fight-start" bind:value={start} >
{#snippet children({ props })}
<input type="datetime-local" {...props} bind:value={start} />
{/snippet}
<input type="datetime-local" {...props} bind:value={start}/>
{/snippet}
</Input>
</div>
<div class="m-2">
<Label for="fight-gamemode">Gamemode</Label>
<Select items={customGamemode ? selectableCustomGamemode : selectableGamemodes} bind:value={gamemode} id="fight-gamemode"></Select>
<Select items={customGamemode ? selectableCustomGamemode : selectableGamemodes} bind:value={gamemode}
id="fight-gamemode"></Select>
</div>
<div class="m-2">
<Label for="fight-maps">Map</Label>
<Select items={customMap ? selectableCustomMaps : selectableMaps} bind:value={map} id="fight-maps" disabled={customGamemode} class={customGamemode ? "cursor-not-allowed" : ""}></Select>
<Select items={customMap ? selectableCustomMaps : selectableMaps} bind:value={map} id="fight-maps"
disabled={customGamemode} class={customGamemode ? "cursor-not-allowed" : ""}></Select>
</div>
<div class="m-2">
<Label for="fight-port">Spectate Port</Label>
<Input id="fight-port" bind:value={spectatePort}>
<Input id="fight-port" bind:value={spectatePort} >
{#snippet children({ props })}
<input type="number" inputmode="numeric" {...props} bind:value={spectatePort} />
{/snippet}
<input type="number" inputmode="numeric" {...props} bind:value={spectatePort}/>
{/snippet}
</Input>
</div>
<div class="m-2">
<Label for="fight-kampf">Group</Label>
<TypeAheadSearch items={selectableGroups} bind:selected={group} bind:searchValue={groupSearch} all></TypeAheadSearch>
<TypeAheadSearch items={selectableGroups} bind:selected={group} bind:searchValue={groupSearch}
all></TypeAheadSearch>
</div>

View File

@@ -18,37 +18,21 @@
-->
<script lang="ts">
import { run, preventDefault } from "svelte/legacy";
import { run, preventDefault } from 'svelte/legacy';
import { Button, Card, Checkbox, Input, Label, Navbar, NavBrand, Radio, Spinner } from "flowbite-svelte";
import { ArrowLeftOutline } from "flowbite-svelte-icons";
import { capitalize } from "../util.ts";
import { permsRepo } from "@repo/perms.ts";
import { me } from "@stores/me.ts";
import {Button, Card, Checkbox, Input, Label, Navbar, NavBrand, Radio, Spinner} from "flowbite-svelte";
import {ArrowLeftOutline} from "flowbite-svelte-icons";
import {players} from "@stores/stores.ts";
import {capitalize} from "../util.ts";
import {permsRepo} from "@repo/perms.ts";
import {me} from "@stores/me.ts";
import SWButton from "@components/styled/SWButton.svelte";
import SWModal from "@components/styled/SWModal.svelte";
import { userRepo } from "@repo/user.ts";
import { dataRepo } from "@repo/data.ts";
import type { Player } from "@type/data";
import {userRepo} from "@repo/user.ts";
let search = $state("");
let playersList: Player[] = $state([]);
let debounceTimer: NodeJS.Timeout;
function fetchPlayers(searchTerm: string) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
const res = await $dataRepo.queryPlayers(searchTerm || undefined, undefined, undefined, 100, 0, undefined, undefined);
playersList = res.players;
}, 300);
}
$effect(() => {
fetchPlayers(search);
});
let selectedPlayer: string | null = $state(null);
let selectedPlayerName: string = $state("");
let playerPerms = $state(loadPlayer(selectedPlayer));
let prefixEdit = $state("PREFIX_NONE");
@@ -62,7 +46,7 @@
if (!id) {
return;
}
return $permsRepo.getPerms(id).then((value) => {
return $permsRepo.getPerms(id).then(value => {
activePerms = value.perms;
prefixEdit = value.prefix.name;
return value;
@@ -72,7 +56,7 @@
function togglePerm(perm: string) {
return () => {
if (activePerms.includes(perm)) {
activePerms = activePerms.filter((value) => value !== perm);
activePerms = activePerms.filter(value => value !== perm);
} else {
activePerms = [...activePerms, perm];
}
@@ -80,7 +64,7 @@
}
function save() {
playerPerms!.then(async (perms) => {
playerPerms!.then(async perms => {
if (perms.prefix.name != prefixEdit) {
await $permsRepo.setPrefix(selectedPlayer!, prefixEdit);
}
@@ -115,20 +99,24 @@
resetPasswordRepeat = "";
resetPasswordModal = false;
}
let lowerCaseSearch = $derived(search.toLowerCase());
let filteredPlayers = $derived($players.filter(value => value.name.toLowerCase().includes(lowerCaseSearch)));
let player = $derived($players.find(value => value.uuid === selectedPlayer));
run(() => {
playerPerms = loadPlayer(selectedPlayer);
});
playerPerms = loadPlayer(selectedPlayer);
});
</script>
<div class="flex flex-col h-screen overflow-hidden">
<Navbar>
<Navbar >
{#snippet children({ hidden, toggle })}
<NavBrand href="#">
<ArrowLeftOutline></ArrowLeftOutline>
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white"> Permissions </span>
</NavBrand>
{/snippet}
<NavBrand href="#">
<ArrowLeftOutline></ArrowLeftOutline>
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white">
Permissions
</span>
</NavBrand>
{/snippet}
</Navbar>
<div class="p-4 flex-1 overflow-hidden">
@@ -136,19 +124,14 @@
<Card class="h-full flex flex-col overflow-hidden !max-w-full">
<div class="border-b border-b-gray-600 pb-2">
<Label for="user_search" class="mb-2">Search Users...</Label>
<Input type="text" id="user_search" placeholder="Name..." bind:value={search} />
<Input type="text" id="user_search" placeholder="Name..." bind:value={search}/>
</div>
{#if playersList.length < 100}
{#if filteredPlayers.length < 100}
<ul class="flex-1 overflow-scroll">
{#each playersList as player (player.uuid)}
<li
class="p-4 transition-colors hover:bg-gray-700 cursor-pointer"
{#each filteredPlayers as player (player.uuid)}
<li class="p-4 transition-colors hover:bg-gray-700 cursor-pointer"
class:text-orange-500={player.uuid === selectedPlayer}
onclick={preventDefault(() => {
selectedPlayer = player.uuid;
selectedPlayerName = player.name;
})}
>
onclick={preventDefault(() => selectedPlayer = player.uuid)}>
{player.name}
</li>
{/each}
@@ -157,7 +140,7 @@
</Card>
<Card class="!max-w-full" style="grid-column: 2/4">
{#if selectedPlayer}
<h1 class="text-3xl">{selectedPlayerName}</h1>
<h1 class="text-3xl">{player.name}</h1>
{#await permsFuture}
<Spinner></Spinner>
{:then perms}
@@ -166,27 +149,39 @@
{:then player}
<h1>Prefix</h1>
{#each Object.entries(perms.prefixes) as [key, prefix]}
<Radio name="prefix" bind:group={prefixEdit} value={prefix.name}>{capitalize(prefix.name.substring(7).toLowerCase())}</Radio>
<Radio name="prefix" bind:group={prefixEdit}
value={prefix.name}>{capitalize(prefix.name.substring(7).toLowerCase())}</Radio>
{/each}
<h1>Permissions</h1>
{#each perms.perms as perm}
<Checkbox checked={activePerms.includes(perm)} onclick={togglePerm(perm)}>{capitalize(perm.toLowerCase())}</Checkbox>
<Checkbox checked={activePerms.includes(perm)}
onclick={togglePerm(perm)}>{capitalize(perm.toLowerCase())}</Checkbox>
{/each}
<div class="mt-4">
<Button disabled={prefixEdit === (player?.prefix.name ?? "") && activePerms === (player?.perms ?? [])} onclick={save}>Save</Button>
<Button disabled={prefixEdit === (player?.prefix.name ?? "") && activePerms === (player?.perms ?? [])}
onclick={save}>Save
</Button>
{#if $me != null && $me.perms.includes("ADMINISTRATION")}
<Button onclick={() => (resetPasswordModal = true)}>Reset Password</Button>
<Button onclick={() => resetPasswordModal = true}>
Reset Password
</Button>
<SWModal bind:open={resetPasswordModal} title="Reset Password">
<Label for="new_password">New Password</Label>
<Input type="password" id="new_password" placeholder="New Password" bind:value={resetPassword} />
<Input type="password" id="new_password" placeholder="New Password" bind:value={resetPassword}/>
<Label for="repeat_password">Repeat Password</Label>
<Input type="password" id="repeat_password" placeholder="Repeat Password" bind:value={resetPasswordRepeat} />
<Input type="password" id="repeat_password" placeholder="Repeat Password" bind:value={resetPasswordRepeat}/>
{#snippet footer()}
<Button class="ml-auto mr-4" onclick={resetResetPassword}>Cancel</Button>
<Button disabled={resetPassword === "" || resetPassword !== resetPasswordRepeat} onclick={resetPW}>Reset Password</Button>
{/snippet}
<Button class="ml-auto mr-4" onclick={resetResetPassword}>
Cancel
</Button>
<Button disabled={resetPassword === "" || resetPassword !== resetPasswordRepeat} onclick={resetPW}>
Reset Password
</Button>
{/snippet}
</SWModal>
{/if}
</div>

View File

@@ -36,9 +36,8 @@
} from "flowbite-svelte-icons";
import FightCard from "./FightCard.svelte";
import CreateFightModal from "./modals/CreateFightModal.svelte";
import {groups} from "@stores/stores.ts";
import {groups, players} from "@stores/stores.ts";
import TypeAheadSearch from "../../components/TypeAheadSearch.svelte";
import PlayerSelector from "@components/ui/PlayerSelector.svelte";
import {fightRepo, type UpdateFight} from "@repo/fight.ts";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
@@ -98,6 +97,12 @@
}
let spectatePortOpen = $state(false);
let selectPlayers = $derived($players.map(player => {
return {
name: player.name,
value: player.uuid
};
}).sort((a, b) => a.name.localeCompare(b.name)));
let spectatePort = $state("");
async function updateSpectatePort() {
@@ -257,12 +262,12 @@
<TypeAheadSearch items={selectPlayers} bind:selected={spectatePort}></TypeAheadSearch>
</div>
{#snippet footer()}
<Modal bind:open={spectatePortOpen} title="Change Kampfleiter" size="sm">
<div class="m-2">
<Label for="fight-kampf">Kampfleiter</Label>
<PlayerSelector bind:value={spectatePort} placeholder="Search player..." />
</div>
{#snippet footer()}
<Button class="ml-auto" onclick={updateSpectatePort}>Change</Button>
<Button onclick={() => spectatePortOpen = false} color="alternative">Cancel</Button>
{/snippet}
</Modal>
<Modal bind:open={groupChangeOpen} title="Change Group" size="sm">
<div class="m-2">

View File

@@ -18,19 +18,20 @@
-->
<script lang="ts">
import type { ExtendedEvent } from "@type/event.ts";
import { Button } from "flowbite-svelte";
import { PlusOutline } from "flowbite-svelte-icons";
import type {ExtendedEvent} from "@type/event.ts";
import {Button} from "flowbite-svelte";
import {PlusOutline} from "flowbite-svelte-icons";
import SWModal from "@components/styled/SWModal.svelte";
import SWButton from "@components/styled/SWButton.svelte";
import PlayerSelector from "@components/ui/PlayerSelector.svelte";
import { eventRepo } from "@repo/event.ts";
import TypeAheadSearch from "@components/admin/components/TypeAheadSearch.svelte";
import {players} from "@stores/stores.ts";
import {eventRepo} from "@repo/event.ts";
interface Props {
data: ExtendedEvent;
}
interface Props {
data: ExtendedEvent;
}
let { data }: Props = $props();
let { data }: Props = $props();
let searchValue = $state("");
let selectedPlayer: string | null = $state(null);
@@ -41,19 +42,17 @@
async function addReferee() {
if (selectedPlayer) {
referees = (
await $eventRepo.updateEvent(data.event.id.toString(), {
deadline: null,
end: null,
maxTeamMembers: null,
name: null,
publicSchemsOnly: null,
removeReferee: null,
schemType: null,
start: null,
addReferee: [selectedPlayer],
})
).referees;
referees = (await $eventRepo.updateEvent(data.event.id.toString(), {
deadline: null,
end: null,
maxTeamMembers: null,
name: null,
publicSchemsOnly: null,
removeReferee: null,
schemType: null,
start: null,
addReferee: [selectedPlayer]
})).referees;
}
reset();
@@ -61,20 +60,18 @@
function removeReferee(id: string) {
return async () => {
referees = (
await $eventRepo.updateEvent(data.event.id.toString(), {
deadline: null,
end: null,
maxTeamMembers: null,
name: null,
publicSchemsOnly: null,
addReferee: null,
schemType: null,
start: null,
removeReferee: [id],
})
).referees;
};
referees = (await $eventRepo.updateEvent(data.event.id.toString(), {
deadline: null,
end: null,
maxTeamMembers: null,
name: null,
publicSchemsOnly: null,
addReferee: null,
schemType: null,
start: null,
removeReferee: [id],
})).referees;
}
}
function reset() {
@@ -87,7 +84,9 @@
{#each referees as referee}
<li class="flex flex-grow justify-between">
{referee.name}
<SWButton onclick={removeReferee(referee.uuid)}>Entfernen</SWButton>
<SWButton onclick={removeReferee(referee.uuid)}>
Entfernen
</SWButton>
</li>
{/each}
@@ -96,22 +95,23 @@
{/if}
</ul>
<Button class="fixed bottom-6 right-6 !p-4 z-10 shadow-lg" onclick={() => (showAdd = true)}>
<PlusOutline />
<Button class="fixed bottom-6 right-6 !p-4 z-10 shadow-lg" onclick={() => showAdd = true}>
<PlusOutline/>
</Button>
<SWModal title="Schiedsrichter hinzufügen" bind:open={showAdd}>
<div class="flex flex-grow justify-center h-80">
<div>
<PlayerSelector bind:value={selectedPlayer} placeholder="Search player..." />
<TypeAheadSearch bind:searchValue bind:selected={selectedPlayer}
items={$players.map(v => ({ name: v.name, value: v.uuid }))}/>
</div>
</div>
{#snippet footer()}
<div class="flex flex-grow justify-end">
<SWButton onclick={reset} type="gray">Abbrechen</SWButton>
<SWButton onclick={addReferee}>Hinzufügen</SWButton>
</div>
{/snippet}
<div class="flex flex-grow justify-end">
<SWButton onclick={reset} type="gray">Abbrechen</SWButton>
<SWButton onclick={addReferee}>Hinzufügen</SWButton>
</div>
{/snippet}
</SWModal>
<style>

View File

@@ -18,14 +18,12 @@
-->
<script lang="ts">
import { Card } from "@components/ui/card";
import {createEventDispatcher} from "svelte";
interface Props {
children?: import('svelte').Snippet;
}
interface Props {
children?: import("svelte").Snippet;
ondrop: (event: DragEvent) => void;
}
let { children, ondrop }: Props = $props();
let { children }: Props = $props();
let dragover = $state(false);
@@ -34,16 +32,19 @@
dragover = true;
}
const dispatch = createEventDispatcher();
function handleDrop(ev: DragEvent) {
ev.preventDefault();
dragover = false;
ondrop(ev);
dispatch("drop", ev);
}
</script>
<Card class="w-56 p-4 rounded m-px {dragover ? 'border-white' : ''}" ondrop={handleDrop} ondragover={handleDragOver} ondragleave={() => (dragover = false)} role="none">
<div class="w-56 bg-gray-800 p-4 rounded" class:border={dragover} class:m-px={!dragover} ondrop={handleDrop}
ondragover={handleDragOver} ondragleave={() => dragover = false} role="none">
{@render children?.()}
</Card>
</div>
<style>
div {

View File

@@ -18,28 +18,28 @@
-->
<script lang="ts">
import type { Team } from "@type/team.ts";
import { brightness, colorFromTeam, lighten } from "../../util";
import { createBubbler } from 'svelte/legacy';
interface Props {
team: Team;
ondragstart: (event: DragEvent) => void;
}
const bubble = createBubbler();
import type {Team} from "@type/team.ts";
import {brightness, colorFromTeam, lighten} from "../../util";
let { team, ondragstart }: Props = $props();
interface Props {
team: Team;
}
let { team }: Props = $props();
let hover = $state(false);
</script>
<div
class="rounded w-fit p-2 border-gray-600 border cursor-grab select-none m-1 flex place-items-center"
style:background-color={hover ? lighten(colorFromTeam(team)) : colorFromTeam(team)}
class:text-black={brightness(colorFromTeam(team))}
draggable="true"
{ondragstart}
onmouseenter={() => (hover = true)}
onmouseleave={() => (hover = false)}
role="figure"
>
<div class="rounded w-fit p-2 border-gray-600 border cursor-grab select-none m-1 flex place-items-center"
style:background-color={hover ? lighten(colorFromTeam(team)) : colorFromTeam(team)}
class:text-black={brightness(colorFromTeam(team))} draggable="true"
ondragstart={bubble('dragstart')}
onmouseenter={() => hover = true}
onmouseleave={() => hover = false}
role="figure">
<span>{team.name}</span>
</div>

View File

@@ -1,95 +0,0 @@
<script lang="ts">
import dayjs from "dayjs";
import "dayjs/locale/de";
import type { ExtendedEvent } from "../types/event";
import { Button } from "../ui/button";
import { ChevronLeft, ChevronRight } from "lucide-svelte";
import * as Card from "../ui/card";
import EventCard from "./EventCard.svelte";
import SWButton from "@components/styled/SWButton.svelte";
const {
events,
}: {
events: { slug: string; data: { event: ExtendedEvent } }[];
} = $props();
let currentYear = $state(dayjs().year());
// Group events by month
let eventsByMonth = $derived.by(() => {
const grouped = new Map<string, typeof events>();
events.forEach((event) => {
const eventDate = dayjs(event.data.event.event.start).locale("de");
if (eventDate.year() === currentYear) {
const monthKey = eventDate.format("YYYY-MM");
if (!grouped.has(monthKey)) {
grouped.set(monthKey, []);
}
grouped.get(monthKey)!.push(event);
}
});
return grouped;
});
// Generate all 12 months for the current year
let months = $derived.by(() => {
return Array.from({ length: 12 }, (_, i) => {
const monthDate = dayjs().locale("de").year(currentYear).month(i);
const monthKey = monthDate.format("YYYY-MM");
return {
date: monthDate,
key: monthKey,
name: monthDate.format("MMMM"),
events: eventsByMonth.get(monthKey) || [],
};
});
});
function prevYear() {
currentYear = currentYear - 1;
}
function nextYear() {
currentYear = currentYear + 1;
}
</script>
<div>
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-white">
{currentYear}
</h2>
<div class="flex gap-2">
<SWButton onclick={prevYear} type="gray">
<ChevronLeft size={20} />
</SWButton>
<SWButton onclick={nextYear} type="gray">
<ChevronRight size={20} />
</SWButton>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{#each months as month}
<EventCard title={month.name} unsized={true}>
{#if month.events.length > 0}
{#each month.events as event}
<a href={`/events/${event.slug}/`} class="block p-2 bg-slate-800 hover:bg-slate-700 rounded border border-slate-600 transition-colors group">
<div class="text-sm font-semibold text-white group-hover:text-blue-400 transition-colors">
{event.data.event.event.name}
</div>
<div class="text-xs text-gray-400 mt-1">
{dayjs(event.data.event.event.start).format("MMM D, YYYY • HH:mm")}
</div>
</a>
{/each}
{:else}
<p class="text-gray-500 text-sm italic">Keine Events für diesen Monat</p>
{/if}
</EventCard>
{/each}
</div>
</div>

View File

@@ -1,108 +0,0 @@
<script lang="ts">
import { fightConnector } from "./connections.svelte";
import { onMount, onDestroy } from "svelte";
let root: HTMLElement | null = null;
let refresh = $state(0);
function handleScroll() {
refresh++;
}
function getScrollableParent(el: HTMLElement | null): HTMLElement | null {
let node: HTMLElement | null = el?.parentElement ?? null;
while (node) {
const style = getComputedStyle(node);
const canScrollX = (style.overflowX === "auto" || style.overflowX === "scroll") && node.scrollWidth > node.clientWidth;
const canScrollY = (style.overflowY === "auto" || style.overflowY === "scroll") && node.scrollHeight > node.clientHeight;
if (canScrollX || canScrollY) return node;
node = node.parentElement;
}
return null;
}
let cleanup: (() => void) | null = null;
onMount(() => {
const scrollParent = getScrollableParent(root);
const target: EventTarget | null = scrollParent ?? window;
target?.addEventListener("scroll", handleScroll, { passive: true } as AddEventListenerOptions);
window.addEventListener("resize", handleScroll, { passive: true });
cleanup = () => {
target?.removeEventListener?.("scroll", handleScroll as EventListener);
window.removeEventListener("resize", handleScroll as EventListener);
};
});
onDestroy(() => {
cleanup?.();
cleanup = null;
});
</script>
<div bind:this={root} class="connection-renderer-root">
{#key refresh}
{#each $fightConnector.showedConnections as connection}
{@const fromLeft = connection.fromElement.offsetLeft + connection.fromElement.offsetWidth}
{@const toLeft = connection.toElement.offsetLeft}
{@const fromTop = connection.fromElement.offsetTop + connection.fromElement.offsetHeight / 2}
{@const toTop = connection.toElement.offsetTop + connection.toElement.offsetHeight / 2}
{@const horizontalDistance = toLeft - fromLeft}
{@const verticalDistance = toTop - fromTop}
<!-- Apply horizontal offset only to the mid bridge and second segment fan-out; also shift vertical line to keep continuity -->
{@const midLeft = fromLeft + horizontalDistance / 2 + connection.offset}
{@const firstSegmentWidth = horizontalDistance / 2}
{@const secondSegmentWidth = horizontalDistance / 2}
<div
class="horizontal-line"
style="
background-color: {connection.color};
left: {fromLeft}px;
top: {fromTop + connection.offset / 4}px;
width: {firstSegmentWidth + connection.offset + 2}px;
"
></div>
<div
class="vertical-line"
style="
background-color: {connection.color};
left: {midLeft}px;
top: {Math.min(fromTop + connection.offset / 4, toTop + connection.offset / 4)}px;
height: {Math.abs(toTop + connection.offset / 4 - (fromTop + connection.offset / 4))}px;
"
></div>
<div
class="horizontal-line"
style="
background-color: {connection.color};
left: {midLeft}px;
top: {toTop + connection.offset / 4}px;
width: {secondSegmentWidth - connection.offset}px;
"
></div>
{/each}
{/key}
</div>
<style>
.connection-renderer-root {
position: static;
pointer-events: none;
}
.vertical-line {
position: absolute;
width: 2px;
z-index: -10;
pointer-events: none;
}
.horizontal-line {
position: absolute;
height: 2px;
z-index: -10;
pointer-events: none;
}
</style>

View File

@@ -1,165 +0,0 @@
<script lang="ts">
import type { ExtendedEvent, EventFight, ResponseGroups, ResponseRelation } from "@type/event.ts";
import type { DoubleEleminationViewConfig } from "./types";
import EventCard from "./EventCard.svelte";
import EventFightChip from "./EventFightChip.svelte";
import { onMount, onDestroy, tick } from "svelte";
import { fightConnector } from "./connections.svelte.ts";
const { event, config }: { event: ExtendedEvent; config: DoubleEleminationViewConfig } = $props();
const defaultGroup: ResponseGroups = {
id: -1,
name: "Double Elimination",
pointsPerWin: 0,
pointsPerLoss: 0,
pointsPerDraw: 0,
type: "ELIMINATION_STAGE",
points: null,
};
function indexRelations(ev: ExtendedEvent): Map<number, ResponseRelation[]> {
const map = new Map<number, ResponseRelation[]>();
for (const rel of ev.relations) {
const list = map.get(rel.fight) ?? [];
list.push(rel);
map.set(rel.fight, list);
}
return map;
}
const relationsByFight = indexRelations(event);
const fightMap = new Map<number, EventFight>(event.fights.map((f) => [f.id, f]));
function collectBracket(startFinalId: number): EventFight[][] {
const finalFight = fightMap.get(startFinalId);
if (!finalFight) return [];
const bracketGroupId = finalFight.group?.id ?? null;
const stages: EventFight[][] = [];
let layer: EventFight[] = [finalFight];
const visited = new Set<number>([finalFight.id]);
while (layer.length) {
stages.push(layer);
const next: EventFight[] = [];
for (const fight of layer) {
const rels = relationsByFight.get(fight.id) ?? [];
for (const rel of rels) {
if (rel.type === "FIGHT" && rel.fromFight) {
const src = fightMap.get(rel.fromFight.id) ?? rel.fromFight;
if (!src) continue;
// Only traverse within the same bracket (group) to avoid cross-bracket pollution
if (bracketGroupId !== null && src.group?.id !== bracketGroupId) continue;
if (!visited.has(src.id)) {
visited.add(src.id);
next.push(src);
}
}
}
}
layer = next;
}
stages.reverse();
return stages;
}
const winnersStages = $derived(collectBracket(config.winnersFinalFight));
const losersStages = $derived(collectBracket(config.losersFinalFight));
const grandFinal = fightMap.get(config.grandFinalFight);
function stageName(count: number, isWinners: boolean): string {
switch (count) {
case 1:
return isWinners ? "Finale (W)" : "Finale (L)";
case 2:
return isWinners ? "Halbfinale (W)" : "Halbfinale (L)";
case 4:
return isWinners ? "Viertelfinale (W)" : "Viertelfinale (L)";
case 8:
return isWinners ? "Achtelfinale (W)" : "Achtelfinale (L)";
default:
return `Runde (${count}) ${isWinners ? "W" : "L"}`;
}
}
let connector: any;
const unsubscribe = fightConnector.subscribe((v) => (connector = v));
onDestroy(() => {
connector.clearAllConnections();
unsubscribe();
});
function buildConnections() {
if (!connector) return;
connector.clearAllConnections();
// Track offsets per source fight and team to stagger multiple outgoing lines for visual clarity
const fightTeamOffsetMap = new Map<string, number>();
const step = 8; // px separation between parallel lines
for (const rel of event.relations) {
if (rel.type !== "FIGHT" || !rel.fromFight) continue;
const fromId = rel.fromFight.id;
const fromEl = document.getElementById(`fight-${fromId}`) as HTMLElement | null;
const toEl = document.getElementById(`fight-${rel.fight}-team-${rel.team.toLowerCase()}`) as HTMLElement | null;
if (!fromEl || !toEl) continue;
// Use team-signed offsets so BLUE goes left (negative), RED goes right (positive)
const key = `${fromId}:${rel.team}`;
const index = fightTeamOffsetMap.get(key) ?? 0;
const sign = rel.team === "BLUE" ? -1 : 1;
const offset = sign * (index + 1) * step;
const color = rel.fromPlace === 0 ? "#60a5fa" : "#f87171";
connector.addConnection(fromEl, toEl, color, offset);
fightTeamOffsetMap.set(key, index + 1);
}
}
onMount(async () => {
await tick();
buildConnections();
});
</script>
{#if !grandFinal}
<p class="text-gray-400 italic">Konfiguration unvollständig (Grand Final fehlt).</p>
{:else}
{#key winnersStages.length + ":" + losersStages.length}
<!-- Build a grid where rows: winners (stages), losers (stages), with losers offset by one stage/column -->
{@const totalColumns = Math.max(winnersStages.length, losersStages.length + 1) + 1}
<div class="grid gap-x-16 gap-y-6 items-start" style={`grid-template-columns: repeat(${totalColumns}, max-content);`}>
<!-- Winners heading spans all columns -->
<h2 class="font-bold text-center">Winners Bracket</h2>
<!-- Winners stages in row 2 -->
{#each winnersStages as stage, i}
<div style={`grid-row: 2; grid-column: ${i + 1};`}>
<EventCard title={stageName(stage.length, true)}>
{#each stage as fight}
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
{/each}
</EventCard>
</div>
{/each}
<!-- Place Grand Final at the far right, aligned with winners row -->
<div style={`grid-row: 2; grid-column: ${totalColumns};`} class="self-center">
<EventCard title="Grand Final">
{#if grandFinal}
<EventFightChip fight={grandFinal} group={grandFinal.group ?? defaultGroup} />
{/if}
</EventCard>
</div>
<!-- Losers heading spans all columns -->
<h2 class="font-bold text-center" style="grid-row: 3; grid-column: 1 / {totalColumns};">Losers Bracket</h2>
<!-- Losers stages in row 4, offset by one column to the right -->
{#each losersStages as stage, j}
<div style={`grid-row: 4; grid-column: ${j + 2};`} class="mt-2">
<EventCard title={stageName(stage.length, false)}>
{#each stage as fight}
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
{/each}
</EventCard>
</div>
{/each}
</div>
{/key}
{/if}

View File

@@ -1,120 +0,0 @@
<script lang="ts">
import type { ExtendedEvent, EventFight, ResponseGroups, ResponseRelation } from "@type/event.ts";
import type { EleminationViewConfig } from "./types";
import EventCard from "./EventCard.svelte";
import EventFightChip from "./EventFightChip.svelte";
import { onMount, onDestroy, tick } from "svelte";
import { FightConnector, fightConnector } from "./connections.svelte.ts";
const { event, config }: { event: ExtendedEvent; config: EleminationViewConfig } = $props();
const defaultGroup: ResponseGroups = {
id: -1,
name: "Elimination",
pointsPerWin: 0,
pointsPerLoss: 0,
pointsPerDraw: 0,
type: "ELIMINATION_STAGE",
points: null,
};
function buildStages(ev: ExtendedEvent, finalFightId: number): EventFight[][] {
const fightMap = new Map<number, EventFight>(ev.fights.map((f) => [f.id, f]));
const relationsByFight = new Map<number, ResponseRelation[]>();
for (const rel of ev.relations) {
const list = relationsByFight.get(rel.fight) ?? [];
list.push(rel);
relationsByFight.set(rel.fight, list);
}
const finalFight = fightMap.get(finalFightId);
if (!finalFight) return [];
const stages: EventFight[][] = [];
let currentLayer: EventFight[] = [finalFight];
const visited = new Set<number>([finalFight.id]);
while (currentLayer.length) {
stages.push(currentLayer);
const nextLayer: EventFight[] = [];
for (const fight of currentLayer) {
const rels = relationsByFight.get(fight.id) ?? [];
for (const rel of rels) {
if (rel.type === "FIGHT" && rel.fromFight) {
const src = fightMap.get(rel.fromFight.id) ?? rel.fromFight;
if (src && !visited.has(src.id)) {
visited.add(src.id);
nextLayer.push(src);
}
}
}
}
currentLayer = nextLayer;
}
stages.reverse();
return stages;
}
function stageName(index: number, fights: EventFight[]): string {
const count = fights.length;
switch (count) {
case 1:
return `Finale`;
case 2:
return "Halbfinale";
case 4:
return "Viertelfinale";
case 8:
return "Achtelfinale";
case 16:
return "Sechzehntelfinale";
default:
return `Runde ${index + 1}`;
}
}
const stages = $derived(buildStages(event, config.finalFight));
const connector = $fightConnector;
onDestroy(() => {
connector.clearAllConnections();
});
function buildConnections() {
if (!connector) return;
connector.clearConnections();
for (const rel of event.relations) {
if (rel.type !== "FIGHT" || !rel.fromFight) continue;
const fromEl = document.getElementById(`fight-${rel.fromFight.id}`) as HTMLElement | null;
const toEl = document.getElementById(`fight-${rel.fight}-team-${rel.team.toLowerCase()}`) as HTMLElement | null;
if (fromEl && toEl) {
connector.addConnection(fromEl, toEl, "#9ca3af");
}
}
}
onMount(async () => {
await tick();
buildConnections();
});
</script>
{#if stages.length === 0}
<p class="text-gray-400 italic">Keine Eliminationsdaten gefunden.</p>
{:else}
<div class="flex gap-12">
{#each stages as stage, index}
<div class="flex flex-col justify-center">
<EventCard title={stageName(index, stage)}>
{#each stage as fight}
<EventFightChip {fight} group={fight.group ?? defaultGroup} />
{/each}
</EventCard>
</div>
{/each}
</div>
{/if}

View File

@@ -1,22 +0,0 @@
<script lang="ts">
import type { Snippet } from "svelte";
const {
title,
children,
unsized = false,
}: {
title: string;
children: Snippet;
unsized?: boolean;
} = $props();
</script>
<div class="flex flex-col gap-1 {unsized ? '' : 'w-72 m-4'}">
<div class="bg-gray-100 text-black font-bold px-2 rounded uppercase">
{title}
</div>
<div class="border border-gray-600 rounded p-2 flex flex-col gap-2 bg-slate-900">
{@render children()}
</div>
</div>

View File

@@ -1,13 +0,0 @@
<script lang="ts">
import type { Snippet } from "svelte";
const {
children,
}: {
children: Snippet;
} = $props();
</script>
<div class="bg-neutral-900 border border-gray-700 rounded-lg overflow-hidden">
{@render children()}
</div>

View File

@@ -1,42 +0,0 @@
<script lang="ts">
import type { EventFight, ResponseGroups } from "@components/types/event";
import EventCardOutline from "./EventCardOutline.svelte";
import EventTeamChip from "./EventTeamChip.svelte";
import { fightConnector } from "./connections.svelte.ts";
let {
fight,
group,
}: {
fight: EventFight;
group: ResponseGroups;
} = $props();
function getScore(group: ResponseGroups, fight: EventFight, blueTeam: boolean): string {
if (!fight.hasFinished) return "-";
if (fight.ergebnis === 1) {
return blueTeam ? group.pointsPerWin.toString() : group.pointsPerLoss.toString();
} else if (fight.ergebnis === 2) {
return blueTeam ? group.pointsPerLoss.toString() : group.pointsPerWin.toString();
} else {
return group.pointsPerDraw.toString();
}
}
</script>
<EventCardOutline>
<EventTeamChip
team={{
id: -1,
kuerzel: new Date(fight.start).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
name: new Date(fight.start).toLocaleDateString([]),
color: "-1",
}}
time={true}
/>
<div id={"fight-" + fight.id}>
<EventTeamChip team={fight.blueTeam} score={getScore(group, fight, true)} showWinner={true} isWinner={fight.ergebnis === 1} noWinner={fight.ergebnis === 0} id="fight-{fight.id}-team-blue" />
<EventTeamChip team={fight.redTeam} score={getScore(group, fight, false)} showWinner={true} isWinner={fight.ergebnis === 2} noWinner={fight.ergebnis === 0} id="fight-{fight.id}-team-red" />
</div>
</EventCardOutline>

View File

@@ -1,50 +0,0 @@
<script lang="ts">
import type { ExtendedEvent } from "@type/event.ts";
import type { EventViewConfig } from "./types";
import { onMount } from "svelte";
import { eventRepo } from "@components/repo/event";
import GroupDisplay from "./GroupDisplay.svelte";
import ConnectionRenderer from "./ConnectionRenderer.svelte";
import EleminationDisplay from "./EleminationDisplay.svelte";
import DoubleEleminationDisplay from "./DoubleEleminationDisplay.svelte";
const { event, viewConfig }: { event: ExtendedEvent; viewConfig: EventViewConfig } = $props();
let loadedEvent = $state<ExtendedEvent>(event);
onMount(() => {
loadEvent();
});
async function loadEvent() {
loadedEvent = await $eventRepo.getEvent(event.event.id.toString());
}
let selectedView = $state<string>(Object.keys(viewConfig)[0]);
</script>
<div class="flex gap-4 overflow-x-auto mb-4">
{#each Object.entries(viewConfig) as [name, view]}
<button
class="mb-8 border-gray-700 border rounded-lg p-4 w-60 hover:bg-gray-700 hover:shadow-lg transition-shadow hover:border-gray-500"
class:bg-gray-800={selectedView === name}
onclick={() => (selectedView = name)}
>
<h1 class="text-left">{view.name}</h1>
</button>
{/each}
</div>
{#if selectedView}
{@const view = viewConfig[selectedView]}
<div class="overflow-x-scroll relative">
<ConnectionRenderer />
{#if view.view.type === "GROUP"}
<GroupDisplay event={loadedEvent} config={view.view} />
{:else if view.view.type === "ELEMINATION"}
<EleminationDisplay event={loadedEvent} config={view.view} />
{:else if view.view.type === "DOUBLE_ELEMINATION"}
<DoubleEleminationDisplay event={loadedEvent} config={view.view} />
{/if}
</div>
{/if}

View File

@@ -1,122 +0,0 @@
<script lang="ts">
import type { ExtendedEvent } from "../types/event";
import dayjs from "dayjs";
import * as Card from "../ui/card";
const { events }: { events: { slug: string; data: { event: ExtendedEvent } }[] } = $props();
// Categorize events into current, upcoming and past.
const now = dayjs();
const sorted = [...events].sort((a, b) => a.data.event.event.start - b.data.event.event.start);
const currentEvents = sorted
.filter((e) => {
const start = dayjs(e.data.event.event.start);
const end = dayjs(e.data.event.event.end);
return start.isBefore(now) && end.isAfter(now);
})
.sort((a, b) => a.data.event.event.end - b.data.event.event.end);
const currentEvent = currentEvents[0];
const upcomingEvents = sorted.filter((e) => dayjs(e.data.event.event.start).isAfter(now));
const pastEvents = sorted.filter((e) => dayjs(e.data.event.event.end).isBefore(now)).sort((a, b) => b.data.event.event.end - a.data.event.event.end);
</script>
{#if currentEvent}
<div class="mb-8">
<h2 class="text-xl font-semibold text-white mb-4">Aktuelles Event</h2>
<div class="grid grid-cols-1">
<a href={`/events/${currentEvent.slug}/`} class="group block h-full">
<Card.Root class="h-full overflow-hidden border-slate-700 bg-slate-800 transition-all hover:-translate-y-1 hover:shadow-xl">
<div class="h-32 bg-gradient-to-br from-blue-600 to-purple-700 relative">
<div class="absolute bottom-0 left-0 p-4 bg-gradient-to-t from-slate-900 to-transparent w-full">
<div class="inline-block bg-slate-900/80 backdrop-blur text-white text-xs font-bold px-2 py-1 rounded mb-1 border border-slate-600">
{dayjs(currentEvent.data.event.event.start).format("DD.MM.YYYY")}
</div>
</div>
</div>
<Card.Header>
<Card.Title class="text-white group-hover:text-blue-400 transition-colors">
{currentEvent.data.event.event.name}
</Card.Title>
</Card.Header>
<Card.Content>
<p class="text-gray-400 text-sm line-clamp-2">
Läuft seit {dayjs(currentEvent.data.event.event.start).format("HH:mm")}
</p>
<div class="mt-4 flex items-center text-sm text-blue-400 font-medium">
Details <span class="ml-1 transition-transform group-hover:translate-x-1"></span>
</div>
</Card.Content>
</Card.Root>
</a>
</div>
</div>
{/if}
{#if upcomingEvents.length}
<div class="mb-8">
<h2 class="text-xl font-semibold text-white mb-4">Bevorstehende Events</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each upcomingEvents as event}
<a href={`/events/${event.slug}/`} class="group block h-full">
<Card.Root class="h-full overflow-hidden border-slate-700 bg-slate-800 transition-all hover:-translate-y-1 hover:shadow-xl">
<div class="h-32 bg-gradient-to-br from-blue-600 to-purple-700 relative">
<div class="absolute bottom-0 left-0 p-4 bg-gradient-to-t from-slate-900 to-transparent w-full">
<div class="inline-block bg-slate-900/80 backdrop-blur text-white text-xs font-bold px-2 py-1 rounded mb-1 border border-slate-600">
{dayjs(event.data.event.event.start).format("DD.MM.YYYY")}
</div>
</div>
</div>
<Card.Header>
<Card.Title class="text-white group-hover:text-blue-400 transition-colors">
{event.data.event.event.name}
</Card.Title>
</Card.Header>
<Card.Content>
<p class="text-gray-400 text-sm line-clamp-2">
Startet um {dayjs(event.data.event.event.start).format("HH:mm")}
</p>
<div class="mt-4 flex items-center text-sm text-blue-400 font-medium">
Details <span class="ml-1 transition-transform group-hover:translate-x-1"></span>
</div>
</Card.Content>
</Card.Root>
</a>
{/each}
</div>
</div>
{/if}
{#if pastEvents.length}
<div class="mb-4">
<h2 class="text-xl font-semibold text-white mb-4">Vergangene Events</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 opacity-70">
{#each pastEvents as event}
<a href={`/events/${event.slug}/`} class="group block h-full">
<Card.Root class="h-full overflow-hidden border-slate-700 bg-slate-800 transition-all hover:-translate-y-1 hover:shadow-xl">
<div class="h-32 bg-gradient-to-br from-blue-600 to-purple-700 relative">
<div class="absolute bottom-0 left-0 p-4 bg-gradient-to-t from-slate-900 to-transparent w-full">
<div class="inline-block bg-slate-900/80 backdrop-blur text-white text-xs font-bold px-2 py-1 rounded mb-1 border border-slate-600">
{dayjs(event.data.event.event.start).format("DD.MM.YYYY")}
</div>
</div>
</div>
<Card.Header>
<Card.Title class="text-white group-hover:text-blue-400 transition-colors">
{event.data.event.event.name}
</Card.Title>
</Card.Header>
<Card.Content>
<p class="text-gray-400 text-sm line-clamp-2">
Stattgefunden um {dayjs(event.data.event.event.start).format("HH:mm")}
</p>
<div class="mt-4 flex items-center text-sm text-blue-400 font-medium">
Details <span class="ml-1 transition-transform group-hover:translate-x-1"></span>
</div>
</Card.Content>
</Card.Root>
</a>
{/each}
</div>
</div>
{/if}

View File

@@ -1,21 +0,0 @@
<script lang="ts">
import type { ExtendedEvent } from "../types/event";
import { Button } from "../ui/button";
import { Calendar } from "lucide-svelte";
import { List } from "lucide-svelte";
import EventList from "./EventList.svelte";
import CalendarView from "./Calendar.svelte";
const { events }: { events: { slug: string; data: { event: ExtendedEvent } }[] } = $props();
let viewMode = $state<"list" | "calendar">("list");
</script>
<div class="flex flex-col gap-6">
<div class="flex justify-between items-center">
<h1 class="text-3xl font-bold text-white">Events</h1>
</div>
<CalendarView {events} />
<EventList {events} />
</div>

View File

@@ -1,48 +0,0 @@
<script lang="ts">
import type { Team } from "@type/team.ts";
import { fightConnector } from "./connections.svelte";
import { teamHoverService } from "./team-hover.svelte";
const {
team,
score = "",
time = false,
showWinner = false,
isWinner = false,
noWinner = false,
id,
}: {
team: Team;
score?: string;
time?: boolean;
showWinner?: boolean;
isWinner?: boolean;
noWinner?: boolean;
id?: string;
} = $props();
let hoverService = $teamHoverService;
</script>
<button
class="flex justify-between px-2 w-full team-chip text-left {time ? 'py-1 hover:bg-gray-800' : 'py-3 cursor-pointer'} team-{team.id} {hoverService.currentHover === team.id
? 'bg-gray-800'
: ''} {showWinner ? 'border-l-4' : ''} {showWinner && isWinner ? 'border-l-yellow-500' : 'border-l-gray-950'}"
onmouseenter={() => team.id === -1 || hoverService.setHover(team.id)}
onmouseleave={() => team.id === -1 || hoverService.clearHover()}
{id}
>
<div class="flex">
<div class="w-12 {time ? 'font-bold' : ''}">{team.kuerzel}</div>
<span class={time ? "font-mono" : "font-bold"}>{team.name}</span>
</div>
<div class="{showWinner && isWinner && 'font-bold'} {isWinner ? 'text-yellow-400' : ''} {noWinner ? 'font-bold' : ''}">
{score}
</div>
</button>
<style>
.team-chip:not(:last-child) {
@apply border-b border-b-gray-700;
}
</style>

View File

@@ -1,70 +0,0 @@
<script lang="ts">
import type { EventFight, ExtendedEvent, ResponseGroups } from "@type/event.ts";
import type { GroupViewConfig } from "./types";
import EventCard from "./EventCard.svelte";
import EventCardOutline from "./EventCardOutline.svelte";
import EventTeamChip from "./EventTeamChip.svelte";
import EventFightChip from "./EventFightChip.svelte";
const {
event,
config,
}: {
event: ExtendedEvent;
config: GroupViewConfig;
} = $props();
// Groups fights into rounds: a round starts at the first fight's start;
// all fights starting within 10 minutes (600_000 ms) of that are in the same round.
function detectRounds(fights: EventFight[]): EventFight[][] {
if (!fights || fights.length === 0) return [];
const TEN_MIN_MS = 10 * 60 * 1000;
const sorted = [...fights].sort((a, b) => a.start - b.start);
const rounds: EventFight[][] = [];
let currentRound: EventFight[] = [];
let roundStart = sorted[0].start;
for (const fight of sorted) {
if (fight.start - roundStart <= TEN_MIN_MS) {
currentRound.push(fight);
} else {
if (currentRound.length) rounds.push(currentRound);
currentRound = [fight];
roundStart = fight.start;
}
}
if (currentRound.length) rounds.push(currentRound);
return rounds;
}
</script>
{#each config.groups as groupId}
{@const group = event.groups.find((g) => g.id === groupId)!!}
{@const fights = event.fights.filter((f) => f.group?.id === groupId)}
{@const rounds = detectRounds(fights)}
<div class="flex">
<div>
<EventCard title={group.name}>
<EventCardOutline>
{#each Object.entries(group.points ?? {}).sort((v1, v2) => v2[1] - v1[1]) as points}
{@const [teamId, point] = points}
{@const team = event.teams.find((t) => t.id.toString() === teamId)!!}
<EventTeamChip {team} score={point.toString()} />
{/each}
</EventCardOutline>
</EventCard>
</div>
{#each rounds as round, index}
<div>
<EventCard title="Runde {index + 1}">
{#each round as fight}
<EventFightChip {fight} {group} />
{/each}
</EventCard>
</div>
{/each}
</div>
{/each}

View File

@@ -1,54 +0,0 @@
<script lang="ts">
import { onMount } from "svelte";
import type { ExtendedEvent } from "@components/types/event";
import type { Team } from "@components/types/team";
import { eventRepo } from "@components/repo/event";
const {
event,
}: {
event: ExtendedEvent;
} = $props();
let teams: Team[] = $state(event.teams);
const colorMap: Record<string, string> = {
"0": "#000000",
"1": "#0000AA",
"2": "#00AA00",
"3": "#00AAAA",
"4": "#AA0000",
"5": "#AA00AA",
"6": "#FFAA00",
"7": "#AAAAAA",
"8": "#555555",
"9": "#5555FF",
a: "#55FF55",
b: "#55FFFF",
c: "#FF5555",
d: "#FF55FF",
e: "#FFFF55",
f: "#FFFFFF",
};
onMount(async () => {
teams = await $eventRepo.listTeams(event.event.id.toString());
});
</script>
<div class="py-2 border-t border-t-gray-600">
<h1 class="text-2xl font-bold mb-4">Angemeldete Teams</h1>
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2">
{#each teams as team}
<div class="bg-neutral-800 p-2 rounded-md border border-neutral-700 border-l-4 flex flex-row items-center gap-2" style="border-left-color: {colorMap[team.color] || '#FFFFFF'}">
<span class="text-sm font-mono text-neutral-400 shrink-0 w-8 text-center">{team.kuerzel}</span>
<span class="font-bold truncate" title={team.name}>
{team.name}
</span>
</div>
{/each}
{#if teams.length === 0}
<p class="col-span-full text-center text-neutral-400">Keine Teams angemeldet.</p>
{/if}
</div>
</div>

View File

@@ -1,55 +0,0 @@
import { readonly, writable } from "svelte/store";
class FightConnection {
constructor(
public readonly fromElement: HTMLElement,
public readonly toElement: HTMLElement,
public readonly color: string = "white",
public readonly background: boolean,
public readonly offset: number = 0
) {}
}
export class FightConnector {
private connections: FightConnection[] = $state([]);
get allConnections(): FightConnection[] {
return this.connections;
}
get showedConnections(): FightConnection[] {
const showBackground = this.connections.some((conn) => !conn.background);
return showBackground ? this.connections.filter((conn) => !conn.background) : this.connections;
}
addTeamConnection(teamId: number): void {
const teamElements = document.getElementsByClassName(`team-${teamId}`);
const teamArray = Array.from(teamElements);
teamArray.sort((a, b) => {
const rectA = a.getBoundingClientRect();
const rectB = b.getBoundingClientRect();
return rectA.top - rectB.top || rectA.left - rectB.left;
});
for (let i = 1; i < teamElements.length; i++) {
const fromElement = teamElements[i - 1] as HTMLElement;
const toElement = teamElements[i] as HTMLElement;
this.connections.push(new FightConnection(fromElement, toElement, "white", false));
}
}
addConnection(fromElement: HTMLElement, toElement: HTMLElement, color: string = "white", offset: number = 0): void {
this.connections.push(new FightConnection(fromElement, toElement, color, true, offset));
}
clearConnections(): void {
this.connections = this.connections.filter((conn) => conn.background);
}
clearAllConnections(): void {
this.connections = [];
}
}
const fightConnectorInternal = writable(new FightConnector());
export const fightConnector = readonly(fightConnectorInternal);

View File

@@ -1,19 +0,0 @@
import { get, writable } from "svelte/store";
import { fightConnector } from "./connections.svelte";
class TeamHoverService {
public currentHover = $state<number | undefined>(undefined);
private fightConnector = get(fightConnector);
setHover(teamId: number): void {
this.currentHover = teamId;
this.fightConnector.addTeamConnection(teamId);
}
clearHover(): void {
this.currentHover = undefined;
this.fightConnector.clearConnections();
}
}
export const teamHoverService = writable(new TeamHoverService());

View File

@@ -1,34 +0,0 @@
import { z } from "astro:content";
export const GroupViewSchema = z.object({
type: z.literal("GROUP"),
groups: z.array(z.number()),
});
export type GroupViewConfig = z.infer<typeof GroupViewSchema>;
export const EleminationViewSchema = z.object({
type: z.literal("ELEMINATION"),
finalFight: z.number(),
});
export type EleminationViewConfig = z.infer<typeof EleminationViewSchema>;
// Double elimination config: needs final fight (grand final) and entry fights for winners & losers brackets
export const DoubleEleminationViewSchema = z.object({
type: z.literal("DOUBLE_ELEMINATION"),
winnersFinalFight: z.number(), // Final fight of winners bracket (feeds into grand final)
losersFinalFight: z.number(), // Final fight of losers bracket (feeds into grand final)
grandFinalFight: z.number(), // Grand final fight id
});
export type DoubleEleminationViewConfig = z.infer<typeof DoubleEleminationViewSchema>;
export const EventViewConfigSchema = z.record(
z.object({
name: z.string(),
view: z.discriminatedUnion("type", [GroupViewSchema, EleminationViewSchema, DoubleEleminationViewSchema]),
})
);
export type EventViewConfig = z.infer<typeof EventViewConfigSchema>;

View File

@@ -21,23 +21,21 @@
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";
import Pages from "@components/moderator/pages/pages/Pages.svelte";
import Generator from "@components/moderator/pages/generators/Generator.svelte";
import AuditLog from "@components/moderator/pages/logs/AuditLog.svelte";
import { Tooltip } from "bits-ui";
const routes: RouteDefinition = {
"/": Dashboard,
"/events": Events,
"/players": Players,
"/event/:id": Event,
"/event/:id/generate": Generator,
"/pages": Pages,
"/logs": AuditLog,
};
</script>
@@ -46,10 +44,11 @@
<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} onclick={() => navigate("/admin")} />
<Label for="new-ui-switch">New UI!</Label>
</div>
</div>
</div>
<Tooltip.Provider>
<Router {routes} />
</Tooltip.Provider>
<Router {routes} />
</div>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import GroupSelector from "./GroupSelector.svelte";
import type { EventFight, EventFightEdit, ResponseGroups, ResponseRelation, SWEvent } from "@type/event";
import type { EventFight, EventFightEdit, ResponseGroups, SWEvent } from "@type/event";
import { fromAbsolute } from "@internationalized/date";
import { Label } from "@components/ui/label";
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
@@ -11,36 +11,46 @@
import { ChevronsUpDown, Check } from "lucide-svelte";
import { Button } from "@components/ui/button";
import { cn } from "@components/utils";
import type { Team } from "@components/types/team";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import type { Snippet } from "svelte";
import { Input } from "@components/ui/input";
import TeamSelector from "./TeamSelector.svelte";
import type { EventModel } from "../pages/event/eventmodel.svelte";
let {
fight,
teams,
event,
actions,
onSave,
data,
groups = $bindable(),
}: {
fight: EventFight | null;
teams: Team[];
event: SWEvent;
groups: ResponseGroups[];
actions: Snippet<[boolean, () => void]>;
onSave: (fight: EventFightEdit) => void;
data: EventModel;
} = $props();
let fightModus = $state(fight?.spielmodus);
let fightMap = $state(fight?.map);
let fightBlueTeam = $state(fight?.blueTeam);
let fightRedTeam = $state(fight?.redTeam);
let fightStart = $state(fight?.start ? fromAbsolute(fight.start, "Europe/Berlin") : fromAbsolute(data.event.start, "Europe/Berlin"));
let fightStart = $state(fight?.start ? fromAbsolute(fight.start, "Europe/Berlin") : fromAbsolute(event.start, "Europe/Berlin"));
let fightErgebnis = $state(fight?.ergebnis ?? 0);
let fightSpectatePort = $state(fight?.spectatePort?.toString() ?? null);
let fightGroup = $state(fight?.group?.id ?? null);
let selectedGroup = $derived(groups.find((group) => group.id === fightGroup));
let mapsStore = $derived(maps(fightModus ?? "null"));
let gamemodeSelectOpen = $state(false);
let mapSelectOpen = $state(false);
let blueTeamSelectOpen = $state(false);
let redTeamSelectOpen = $state(false);
let createOpen = $state(false);
let groupSelectOpen = $state(false);
let dirty = $derived(
fightModus !== fight?.spielmodus ||
@@ -141,10 +151,128 @@
</Command>
</PopoverContent>
</Popover>
<Label>Blue Team</Label>
<TeamSelector bind:selectedTeam={fightBlueTeam} {data} fightId={fight?.id} team="BLUE" />
<Label>Red Team</Label>
<TeamSelector bind:selectedTeam={fightRedTeam} {data} fightId={fight?.id} team="RED" />
<Label for="fight-blue-team">Blue Team</Label>
<Popover bind:open={blueTeamSelectOpen}>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" class="justify-between" {...props} role="combobox">
{teams.find((value) => value.id === fightBlueTeam?.id)?.name || fightBlueTeam?.name || "Select a team..."}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search Teams..." />
<CommandList>
<CommandEmpty>No team found.</CommandEmpty>
<CommandGroup>
<CommandItem
value={"-1"}
onSelect={() => {
fightBlueTeam = {
id: -1,
name: "?",
color: "7",
kuerzel: "?",
};
blueTeamSelectOpen = false;
}}
keywords={["?"]}>???</CommandItem
>
<CommandItem
value={"0"}
onSelect={() => {
fightBlueTeam = {
id: 0,
name: "Public",
color: "7",
kuerzel: "PUB",
};
blueTeamSelectOpen = false;
}}
keywords={["PUB", "Public"]}>PUB</CommandItem
>
</CommandGroup>
<CommandGroup heading="Teams">
{#each teams as team}
<CommandItem
value={team.name}
onSelect={() => {
fightBlueTeam = team;
blueTeamSelectOpen = false;
}}
>
<Check class={cn("mr-2 size-4", team.id !== fightBlueTeam?.id && "text-transparent")} />
{team.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Label for="fight-red-team">Red Team</Label>
<Popover bind:open={redTeamSelectOpen}>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" class="justify-between" {...props} role="combobox">
{teams.find((value) => value.id === fightRedTeam?.id)?.name || fightRedTeam?.name || "Select a team..."}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search Teams..." />
<CommandList>
<CommandEmpty>No team found.</CommandEmpty>
<CommandGroup>
<CommandItem
value={"-1"}
onSelect={() => {
fightRedTeam = {
id: -1,
name: "?",
color: "7",
kuerzel: "?",
};
redTeamSelectOpen = false;
}}
keywords={["?"]}>???</CommandItem
>
<CommandItem
value={"0"}
onSelect={() => {
fightRedTeam = {
id: 0,
name: "Public",
color: "7",
kuerzel: "PUB",
};
redTeamSelectOpen = false;
}}
keywords={["PUB", "Public"]}>PUB</CommandItem
>
</CommandGroup>
<CommandGroup heading="Teams">
{#each teams as team}
<CommandItem
value={team.name}
onSelect={() => {
fightRedTeam = team;
redTeamSelectOpen = false;
}}
>
<Check class={cn("mr-2 size-4", team.id !== fightRedTeam?.id && "text-transparent")} />
{team.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Label>Start</Label>
<DateTimePicker bind:value={fightStart} />
{#if fight !== null}
@@ -162,7 +290,7 @@
{/if}
<Label for="fight-group">Gruppe</Label>
<GroupSelector event={data.event} bind:value={fightGroup} bind:groups={data.groups}></GroupSelector>
<GroupSelector {event} bind:value={fightGroup} bind:groups></GroupSelector>
<Label for="spectate-port">Spectate Port</Label>
<Input id="spectate-port" bind:value={fightSpectatePort} type="number" placeholder="2001" />
</div>

View File

@@ -25,7 +25,7 @@
let groupSelectOpen = $state(false);
async function handleGroupSave(group: GroupUpdateEdit) {
let g = await $eventRepo.createGroup(event.id, group);
let g = await $eventRepo.createGroup(event.id.toString(), group);
groups.push(g);
value = g.id;
createOpen = false;

View File

@@ -1,253 +0,0 @@
<script lang="ts">
import type { ResponseRelation } from "@components/types/event";
import type { Team } from "@components/types/team";
import { Button } from "@components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/ui/tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@components/ui/tooltip";
import { cn } from "@components/utils";
import { Check, ChevronsUpDown, GitPullRequestArrow, Plus } from "lucide-svelte";
import type { EventModel } from "../pages/event/eventmodel.svelte";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import { Label } from "@components/ui/label";
import { eventRepo } from "@components/repo/event";
interface Props {
selectedTeam: Team | undefined;
open?: boolean;
team: "BLUE" | "RED";
data: EventModel;
fightId?: number;
onSelect?: (team: Team) => void;
}
let { selectedTeam = $bindable(), data, team, open = $bindable(false), fightId, onSelect }: Props = $props();
const currentRelation = $derived(data.relations.find((r) => r.fight === fightId && r.team === team));
let fromType = $state<"FIGHT" | "GROUP">(currentRelation?.type ?? "FIGHT");
let fromFight = $state<string | undefined>(currentRelation?.fromFight?.id?.toString());
let fromFightData = $derived(data.fights.find((f) => f.id.toString() === fromFight));
let fromGroup = $state<string | undefined>(currentRelation?.fromGroup?.id?.toString());
let fromGroupData = $derived(data.groups.find((g) => g.id.toString() === fromGroup));
let fromPlace = $state<string | undefined>(currentRelation?.fromPlace?.toString());
let relationOpen = $state(false);
async function saveRelation() {
relationOpen = false;
if (currentRelation === undefined) {
await $eventRepo.createRelation(data.event.id, {
fightId: fightId!,
team,
fromType,
fromId: fromType === "FIGHT" ? parseInt(fromFight!) : parseInt(fromGroup!),
fromPlace: parseInt(fromPlace!),
});
} else {
await $eventRepo.updateRelation(data.event.id, currentRelation.id, {
from: {
fromType,
fromId: fromType === "FIGHT" ? parseInt(fromFight!) : parseInt(fromGroup!),
fromPlace: parseInt(fromPlace!),
},
});
}
data.relations = await $eventRepo.listRelations(data.event.id);
reset();
}
async function clearRelation() {
relationOpen = false;
if (currentRelation !== undefined) {
await $eventRepo.deleteRelation(data.event.id, currentRelation.id);
data.relations = await $eventRepo.listRelations(data.event.id);
}
reset();
}
function reset() {
fromType = currentRelation?.type ?? "FIGHT";
fromFight = currentRelation?.fromFight?.id.toString();
fromGroup = currentRelation?.fromGroup?.id.toString();
fromPlace = currentRelation?.fromPlace.toString();
}
let canSave = $derived(
(fromType !== currentRelation?.type ||
fromFight !== (currentRelation?.fromFight?.id.toString() ?? "") ||
fromGroup !== (currentRelation?.fromGroup?.id.toString() ?? "") ||
fromPlace !== (currentRelation?.fromPlace.toString() ?? "")) &&
((fromType === "FIGHT" && fromFight !== "" && fromPlace !== "") || (fromType === "GROUP" && fromGroup !== "" && fromPlace !== ""))
);
</script>
<div class="flex gap-2">
<Popover bind:open>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" class="justify-between flex-1" {...props} role="combobox">
{#if selectedTeam?.id === -1}
???
{:else if selectedTeam?.id === 0}
PUB
{:else}
{data.teams.find((v) => v.id === selectedTeam?.id)?.name || selectedTeam?.name || "Select a team..."}
{/if}
{#if currentRelation !== undefined}
({#if currentRelation.type === "FIGHT"}
{currentRelation.fromPlace === 0 ? "Gewinner" : "Verlierer"} von {currentRelation.fromFight?.blueTeam.name} vs {currentRelation.fromFight?.redTeam.name} ({new Date(
currentRelation.fromFight?.start ?? 0
).toLocaleTimeString("de-DE", {
timeStyle: "short",
})})
{:else}
{currentRelation.fromPlace + 1}. Platz von {currentRelation.fromGroup?.name}
{/if})
{/if}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search Teams..." />
<CommandList>
<CommandEmpty>No team found.</CommandEmpty>
<CommandGroup>
<CommandItem
value={"-1"}
onSelect={() => {
selectedTeam = {
id: -1,
name: "?",
color: "7",
kuerzel: "?",
};
onSelect?.(selectedTeam);
open = false;
}}
keywords={["?"]}>???</CommandItem
>
<CommandItem
value={"0"}
onSelect={() => {
selectedTeam = {
id: 0,
name: "Public",
color: "7",
kuerzel: "PUB",
};
onSelect?.(selectedTeam);
open = false;
}}
keywords={["PUB", "Public"]}>PUB</CommandItem
>
</CommandGroup>
<CommandGroup heading="Teams">
{#each data.teams as team}
<CommandItem
value={team.name}
onSelect={() => {
selectedTeam = team;
onSelect?.(selectedTeam);
open = false;
}}
>
<Check class={cn("mr-2 size-4", team.id !== selectedTeam?.id && "text-transparent")} />
{team.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Popover bind:open={relationOpen}>
<PopoverTrigger>
{#snippet child({ props })}
<Tooltip>
<TooltipTrigger>
<Button {...props} size="icon" variant={currentRelation !== undefined ? "default" : "outline"} disabled={fightId === undefined}>
<GitPullRequestArrow />
</Button>
</TooltipTrigger>
<TooltipContent>Kampfverbindung</TooltipContent>
</Tooltip>
{/snippet}
</PopoverTrigger>
<PopoverContent>
<Tabs bind:value={fromType}>
<TabsList>
<TabsTrigger value="FIGHT">Kampf</TabsTrigger>
<TabsTrigger value="GROUP">Gruppe</TabsTrigger>
</TabsList>
<TabsContent value="FIGHT">
<Label>Kampf</Label>
<Select bind:value={fromFight} type="single" disabled={data.fights.length === 0}>
<SelectTrigger>
{fromFightData
? `${new Date(fromFightData.start).toLocaleString("de-DE", { timeStyle: "short" })}: ${fromFightData.blueTeam.kuerzel} vs. ${fromFightData.redTeam.kuerzel}`
: "Kampf auswählen..."}
</SelectTrigger>
<SelectContent>
{#each data.fights.filter((v) => v.id !== fightId) as fight (fight.id)}
<SelectItem value={fight.id.toString()}
>{new Date(fight.start).toLocaleString("de-DE", {
timeStyle: "short",
})}: {fight.blueTeam.kuerzel} vs. {fight.redTeam.kuerzel}</SelectItem
>
{/each}
</SelectContent>
</Select>
<Label>Team</Label>
<Select bind:value={fromPlace} type="single" disabled={data.fights.length === 0}>
<SelectTrigger>
{fromPlace ? (fromPlace === "0" ? "Gewinner" : "Verlierer") : "Platz auswählen..."}
</SelectTrigger>
<SelectContent>
<SelectItem value={"0"}>Gewinner</SelectItem>
<SelectItem value={"1"}>Verlierer</SelectItem>
</SelectContent>
</Select>
</TabsContent>
<TabsContent value="GROUP">
<Label>Gruppe</Label>
<Select bind:value={fromGroup} type="single" disabled={data.groups.length === 0}>
<SelectTrigger>
{fromGroupData ? fromGroupData.name : "Kampf auswählen..."}
</SelectTrigger>
<SelectContent>
{#each data.groups as group (group.id)}
<SelectItem value={group.id.toString()}>{group.name}</SelectItem>
{/each}
</SelectContent>
</Select>
<Label>Platz</Label>
<Select bind:value={fromPlace} type="single" disabled={data.fights.length === 0}>
<SelectTrigger>
{fromPlace ? `${parseInt(fromPlace) + 1}. Platz` : "Platz auswählen..."}
</SelectTrigger>
<SelectContent>
{#each Array(32) as _, i}
<SelectItem value={i.toString()}>{i + 1}. Platz</SelectItem>
{/each}
</SelectContent>
</Select>
</TabsContent>
</Tabs>
<div class="flex justify-end gap-2 mt-2">
<Button onclick={clearRelation} variant="destructive">Löschen</Button>
<Button onclick={saveRelation} disabled={!canSave}>Übernehmen</Button>
</div>
</PopoverContent>
</Popover>
</div>

View File

@@ -27,5 +27,4 @@
<a href="#/players" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/players"}> Players </a>
<a href="#/pages" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/pages"}> Pages </a>
<a href="#/schematics" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/schematics"}> Schematics </a>
<a href="#/logs" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/logs"}> Logs </a>
</nav>

View File

@@ -20,7 +20,7 @@
<script lang="ts">
import FightEditRow from "./FightEditRow.svelte";
import type { EventFightEdit } from "@type/event";
import type { EventFight, EventFightEdit, ExtendedEvent } from "@type/event";
import { createSvelteTable, FlexRender } from "@components/ui/data-table";
import { type ColumnFiltersState, getCoreRowModel, getFilteredRowModel, getGroupedRowModel, getSortedRowModel, type RowSelectionState, type SortingState } from "@tanstack/table-core";
import { columns } from "./columns";
@@ -38,6 +38,7 @@
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@components/ui/dropdown-menu";
import GroupSelector from "@components/moderator/components/GroupSelector.svelte";
import { fightRepo } from "@components/repo/fight";
import type { Team } from "@components/types/team";
import type { EventModel } from "./eventmodel.svelte";
let { data = $bindable(), refresh }: { data: EventModel; refresh: () => void } = $props();
@@ -134,7 +135,7 @@
<DialogTitle>Fight Erstellen</DialogTitle>
<DialogDescription>Hier kannst du einen neuen Fight erstellen</DialogDescription>
</DialogHeader>
<FightEdit fight={null} {data} onSave={handleSave}>
<FightEdit fight={null} teams={data.teams} event={data.event} groups={data.groups} onSave={handleSave}>
{#snippet actions(dirty, submit)}
<DialogFooter>
<Button disabled={!dirty} onclick={submit}>Speichern</Button>
@@ -194,16 +195,6 @@
<MenubarItem onclick={() => (groupChangeOpen = true)}>Gruppe Ändern</MenubarItem>
<MenubarItem disabled>Startzeit Verschieben</MenubarItem>
<MenubarItem disabled>Spectate Port Ändern</MenubarItem>
<MenubarItem
onclick={async () => {
let selectedGroups = table.getSelectedRowModel().rows.map((row) => row.original);
for (const g of selectedGroups) {
await $fightRepo.deleteFight(data.event.id, g.id);
}
refresh();
}}>Kämpfe Löschen</MenubarItem
>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
@@ -212,9 +203,8 @@
<MenubarItem onclick={() => (createOpen = true)}>Fight Erstellen</MenubarItem>
<MenubarGroup>
<MenubarGroupHeading>Generatoren</MenubarGroupHeading>
<a href="#/event/{data.event.id}/generate">
<MenubarItem>Gruppenphase</MenubarItem>
</a>
<MenubarItem disabled>Gruppenphase</MenubarItem>
<MenubarItem disabled>K.O. Phase</MenubarItem>
</MenubarGroup>
</MenubarContent>
</MenubarMenu>
@@ -268,14 +258,12 @@
{group?.name ?? "Keine Gruppe"}
</TableCell>
<TableCell class="text-right">
{#if group}
<Button variant="ghost" size="icon" onclick={() => openGroupEditDialog(group!)}>
<EditIcon />
</Button>
<Button variant="ghost" size="icon" onclick={() => openGroupResultsDialog(group!)}>
<GroupIcon />
</Button>
{/if}
<Button variant="ghost" size="icon" onclick={() => openGroupEditDialog(group)}>
<EditIcon />
</Button>
<Button variant="ghost" size="icon" onclick={() => openGroupResultsDialog(group)}>
<GroupIcon />
</Button>
<DropdownMenu>
<DropdownMenuTrigger>
<Button variant="ghost" size="icon">
@@ -303,7 +291,13 @@
</TableCell>
{/each}
<TableCell class="text-right">
<FightEditRow fight={row.original} {data} onupdate={(update) => (data._fights = data._fights.map((v) => (v.id === update.id ? update : v)))} {refresh}></FightEditRow>
<FightEditRow
fight={row.original}
teams={data.teams}
bind:groups={data.groups}
event={data.event}
onupdate={(update) => (data.fights = data.fights.map((v) => (v.id === update.id ? update : v)))}
></FightEditRow>
</TableCell>
</TableRow>
{/each}

View File

@@ -1,21 +1,18 @@
<script lang="ts">
import type { EventFight, EventFightEdit, ResponseGroups, ResponseRelation, SWEvent } from "@type/event";
import type { EventFight, EventFightEdit, ResponseGroups, SWEvent } from "@type/event";
import { Button } from "@components/ui/button";
import { EditIcon, CopyIcon } from "lucide-svelte";
import { EditIcon, MenuIcon, GroupIcon } from "lucide-svelte";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@components/ui/dialog";
import FightEdit from "@components/moderator/components/FightEdit.svelte";
import type { Team } from "@components/types/team";
import { fightRepo } from "@components/repo/fight";
import { eventRepo } from "@components/repo/event";
import type { EventModel } from "./eventmodel.svelte";
let { fight, onupdate, refresh, data }: { fight: EventFight; onupdate: (update: EventFight) => void; refresh: () => void; data: EventModel } = $props();
let { fight, teams, groups = $bindable(), event, onupdate }: { fight: EventFight; teams: Team[]; groups: ResponseGroups[]; event: SWEvent; onupdate: (update: EventFight) => void } = $props();
let editOpen = $state(false);
let duplicateOpen = $state(false);
async function handleSave(fightData: EventFightEdit) {
let f = await $fightRepo.updateFight(data.event.id, fight.id, {
let f = await $fightRepo.updateFight(event.id, fight.id, {
...fightData,
blueTeam: fightData.blueTeam.id,
redTeam: fightData.redTeam.id,
@@ -26,23 +23,6 @@
editOpen = false;
}
async function handlyCopy(fightData: EventFightEdit) {
await $eventRepo.createFight(data.event.id.toString(), {
...fightData,
blueTeam: fightData.blueTeam.id,
redTeam: fightData.redTeam.id,
});
refresh();
duplicateOpen = false;
}
async function handleDelete() {
await $fightRepo.deleteFight(data.event.id, fight.id);
refresh();
}
</script>
<div>
@@ -57,34 +37,13 @@
<DialogTitle>Fight bearbeiten</DialogTitle>
<DialogDescription>Hier kannst du die Daten des Kampfes bearbeiten.</DialogDescription>
</DialogHeader>
<FightEdit {fight} {data} onSave={handleSave}>
<FightEdit {fight} {teams} bind:groups {event} onSave={handleSave}>
{#snippet actions(dirty, submit)}
<DialogFooter>
<Button variant="destructive" onclick={handleDelete}>Löschen</Button>
<Button disabled={!dirty} onclick={submit}>Speichern</Button>
</DialogFooter>
{/snippet}
</FightEdit>
</DialogContent>
</Dialog>
<Dialog bind:open={duplicateOpen}>
<DialogTrigger>
<Button variant="ghost" size="icon">
<CopyIcon />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Fight duplizieren</DialogTitle>
<DialogDescription>Hier kannst du die Daten des duplizierten Fights ändern</DialogDescription>
</DialogHeader>
<FightEdit {fight} {data} onSave={handlyCopy}>
{#snippet actions(dirty, submit)}
<DialogFooter>
<Button onclick={submit}>Speichern</Button>
</DialogFooter>
{/snippet}
</FightEdit>
</DialogContent>
</Dialog>
</div>

View File

@@ -19,10 +19,12 @@
<script lang="ts">
import { Table, TableBody, TableCell, TableCaption, TableHead, TableHeader, TableRow } from "@components/ui/table";
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 PlayerSelector from "@components/ui/PlayerSelector.svelte";
import { players } from "@stores/stores";
const { event }: { event: ExtendedEvent } = $props();
@@ -37,6 +39,8 @@
await $eventRepo.deleteReferees(event.event.id.toString(), [value]);
referees = await $eventRepo.listReferees(event.event.id.toString());
}
let playerSearch = $state("");
</script>
<Table>
@@ -56,7 +60,27 @@
</TableRow>
{/each}
</TableBody>
<TableCaption>
<PlayerSelector placeholder="Hinzufügen" onSelect={(player) => addReferee(player.uuid)} />
</TableCaption>
<Popover>
<TableCaption>
<PopoverTrigger>
<Button>Hinzufügen</Button>
</PopoverTrigger>
</TableCaption>
<PopoverContent class="p-0">
<Command shouldFilter={false}>
<CommandInput bind:value={playerSearch} placeholder="Search players..." />
<CommandList>
<CommandEmpty>No Players found :(</CommandEmpty>
<CommandGroup heading="Players">
{#each $players
.filter((v) => v.name.includes(playerSearch))
.filter((v, i) => i < 50)
.filter((v) => !referees.some((k) => k.uuid === v.uuid)) as player (player.uuid)}
<CommandItem value={player.name} onSelect={() => addReferee(player.uuid)} keywords={[player.uuid]}>{player.name}</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</Table>

View File

@@ -20,10 +20,12 @@
<script lang="ts">
import { Button } from "@components/ui/button";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell, TableCaption } from "@components/ui/table";
import type { ExtendedEvent } from "@type/event.ts";
import { eventRepo } from "@repo/event";
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
import { teams } from "@components/stores/stores";
import type { Team } from "@components/types/team";
import type { EventModel } from "./eventmodel.svelte";
let { event = $bindable() }: { event: EventModel } = $props();

View File

@@ -20,9 +20,9 @@
import { Checkbox } from "@components/ui/checkbox";
import { renderComponent } from "@components/ui/data-table";
import type { ColumnDef } from "@tanstack/table-core";
import type { EventFightModel } from "./eventmodel.svelte";
import type { EventFight } from "@type/event.ts";
export const columns: ColumnDef<EventFightModel>[] = [
export const columns: ColumnDef<EventFight> = [
{
id: "auswahl",
header: ({ table }) => {
@@ -32,7 +32,7 @@ export const columns: ColumnDef<EventFightModel>[] = [
onCheckedChange: () => {
if (!table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected()) {
const now = new Date();
const rows = table.getRowModel().rows.filter((row) => new Date(row.original.start) > now);
const rows = table.getRowModel().rows.filter((row) => new Date(row.original.date) > now);
if (rows.length > 0) {
rows.forEach((row) => {
@@ -57,7 +57,7 @@ export const columns: ColumnDef<EventFightModel>[] = [
},
},
{
accessorFn: (r) => r.blueTeam.nameWithRelation + " vs " + r.redTeam.nameWithRelation,
accessorFn: (r) => r.blueTeam.name + " vs " + r.redTeam.name,
id: "begegnung",
header: "Begegnung",
},

View File

@@ -1,68 +1,21 @@
import type { ResponseUser } from "@components/repo/event";
import type { EventFight, ExtendedEvent, ResponseGroups, ResponseRelation, SWEvent } from "@components/types/event";
import type { Team } from "@components/types/team";
import { derived } from "svelte/store";
export class EventModel {
public event: SWEvent = $state({} as SWEvent);
public teams: Array<Team> = $state([]);
public groups: Array<ResponseGroups> = $state([]);
public _fights: Array<EventFight> = $state([]);
public fights: Array<EventFight> = $state([]);
public referees: Array<ResponseUser> = $state([]);
public relations: Array<ResponseRelation> = $state([]);
public fights = $derived(this.remapFights(this._fights, this.relations));
constructor(data: ExtendedEvent) {
this.event = data.event;
this.relations = data.relations;
this.teams = data.teams;
this.groups = data.groups;
this._fights = data.fights;
this.fights = data.fights;
this.referees = data.referees;
}
private remapFights(v: Array<EventFight>, rels: Array<ResponseRelation>) {
return v.map((fight) => {
let f = JSON.parse(JSON.stringify(fight)) as EventFight;
let blueTeamRelation = "";
let redTeamRelation = "";
let relations = rels.filter((relation) => relation.fight === f.id);
relations.forEach((relation) => {
let str = "";
if (relation.type === "FIGHT") {
str = `${relation.fromPlace === 0 ? "Gewinner" : "Verlierer"} von ${relation.fromFight?.blueTeam.name} vs ${relation.fromFight?.redTeam.name} (${new Date(
relation.fromFight?.start ?? 0
).toLocaleTimeString("de-DE", {
timeStyle: "short",
})})`;
} else {
str = `${relation.fromPlace + 1}. Platz von ${relation.fromGroup?.name}`;
}
if (relation.team === "BLUE") {
blueTeamRelation = str;
} else {
redTeamRelation = str;
}
});
return {
...f,
blueTeam: {
...f.blueTeam,
nameWithRelation: blueTeamRelation ? `${f.blueTeam.name} (${blueTeamRelation})` : f.blueTeam.name,
},
redTeam: {
...f.redTeam,
nameWithRelation: redTeamRelation ? `${f.redTeam.name} (${redTeamRelation})` : f.redTeam.name,
},
};
});
this.relations = data.relations;
}
}
export type EventFightModel = (typeof EventModel.prototype.fights)[number];

View File

@@ -1,31 +0,0 @@
<script lang="ts">
import type { ExtendedEvent } from "@components/types/event";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@components/ui/tabs";
import GroupPhaseGenerator from "./gens/group/GroupPhaseGenerator.svelte";
import SingleEliminationGenerator from "./gens/elimination/SingleEliminationGenerator.svelte";
import DoubleEliminationGenerator from "./gens/elimination/DoubleEliminationGenerator.svelte";
let {
data,
}: {
data: ExtendedEvent;
} = $props();
</script>
<div class="m-4">
<Tabs value="group">
<TabsList class="mb-4">
<TabsTrigger value="group">Gruppenphase</TabsTrigger>
<TabsTrigger value="ko">K.O. Phase</TabsTrigger>
<TabsTrigger value="double">Double Elimination</TabsTrigger>
</TabsList>
<TabsContent value="group">
<GroupPhaseGenerator {data} />
</TabsContent>
<TabsContent value="ko">
<SingleEliminationGenerator {data} />
</TabsContent>
<TabsContent value="double">
<DoubleEliminationGenerator {data} />
</TabsContent>
</Tabs>
</div>

View File

@@ -1,22 +0,0 @@
<script lang="ts">
import { eventRepo } from "@components/repo/event";
import FightsGenerator from "./FightsGenerator.svelte";
interface Props {
params: { id: number };
}
let { params }: Props = $props();
let id = params.id;
let future = $eventRepo.getEvent(id.toString());
</script>
{#await future}
<p>Loading...</p>
{:then event}
<FightsGenerator data={event} />
{:catch error}
<p class="text-red-500">Error loading event: {error.message}</p>
{/await}

View File

@@ -1,515 +0,0 @@
<script lang="ts">
import type { ExtendedEvent } from "@components/types/event";
import type { Team } from "@components/types/team";
import { eventRepo } from "@components/repo/event";
import { fightRepo } from "@components/repo/fight";
import { gamemodes, maps } from "@components/stores/stores";
import { Button } from "@components/ui/button";
import { Card } from "@components/ui/card";
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
import { Label } from "@components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import { Slider } from "@components/ui/slider";
import { fromAbsolute } from "@internationalized/date";
import dayjs from "dayjs";
import { Plus, Shuffle } from "lucide-svelte";
import { replace } from "svelte-spa-router";
let { data }: { data: ExtendedEvent } = $props();
// Seed model (reuse from single elimination)
interface SeedTeamSlot {
kind: "TEAM";
id: number;
}
interface SeedGroupSlot {
kind: "GROUP";
groupId: number;
place: number;
}
interface SeedFightSlot {
kind: "FIGHT";
fightId: number;
place: 0 | 1;
}
type SeedSlot = SeedTeamSlot | SeedGroupSlot | SeedFightSlot;
let seedSlots = $state<SeedSlot[]>(data.teams.map((t) => ({ kind: "TEAM", id: t.id })));
const teams = $derived(new Map<number, Team>(data.teams.map((t) => [t.id, t])));
function shuffleTeams() {
const teamIndices = seedSlots.map((v, i) => ({ v, i })).filter((x) => x.v.kind === "TEAM");
const shuffledIds = teamIndices.map((x) => (x.v as SeedTeamSlot).id).sort(() => Math.random() - 0.5);
let p = 0;
seedSlots = seedSlots.map((slot) => (slot.kind === "TEAM" ? { kind: "TEAM", id: shuffledIds[p++] } : slot));
}
function moveSlot(index: number, dir: -1 | 1) {
const ni = index + dir;
if (ni < 0 || ni >= seedSlots.length) return;
const copy = [...seedSlots];
const [item] = copy.splice(index, 1);
copy.splice(ni, 0, item);
seedSlots = copy;
}
function removeSlot(index: number) {
seedSlots = seedSlots.filter((_, i) => i !== index);
}
function addUnknown() {
seedSlots = [...seedSlots, { kind: "TEAM", id: -1 }];
}
let selectedAddTeam = $state<number>(data.teams[0]?.id ?? 0);
function addTeam() {
if (selectedAddTeam !== undefined) seedSlots = [...seedSlots, { kind: "TEAM", id: selectedAddTeam }];
}
let selectedGroup = $state<number | null>(data.groups[0]?.id ?? null);
let selectedGroupPlace = $state<number>(0);
function addGroupPlace() {
if (selectedGroup != null) seedSlots = [...seedSlots, { kind: "GROUP", groupId: selectedGroup, place: selectedGroupPlace }];
}
let selectedFight = $state<number | null>(data.fights[0]?.id ?? null);
let selectedFightPlace = $state<0 | 1>(0);
function addFightPlace() {
if (selectedFight != null) seedSlots = [...seedSlots, { kind: "FIGHT", fightId: selectedFight, place: selectedFightPlace }];
}
// Config
let startTime = $state(fromAbsolute(data.event.start, "Europe/Berlin"));
let roundTime = $state(30);
let startDelay = $state(30);
let gamemode = $state("");
let map = $state("");
let selectableGamemodes = $derived($gamemodes.map((g) => ({ name: g, value: g })).sort((a, b) => a.name.localeCompare(b.name)));
let mapsStore = $derived(maps(gamemode));
let selectableMaps = $derived($mapsStore.map((m) => ({ name: m, value: m })).sort((a, b) => a.name.localeCompare(b.name)));
// Build winners bracket rounds (same as single elimination seeding)
interface BracketFightPreview {
blue: SeedSlot;
red: SeedSlot;
}
type BracketRoundPreview = BracketFightPreview[];
function buildWinnersRounds(order: SeedSlot[]): BracketRoundPreview[] {
const n = order.length;
if (n < 2) return [];
if ((n & (n - 1)) !== 0) return []; // power of two required
const rounds: BracketRoundPreview[] = [];
let round: BracketRoundPreview = [];
for (let i = 0; i < order.length; i += 2) round.push({ blue: order[i], red: order[i + 1] });
rounds.push(round);
let prevWinners = round.map((f) => f.blue);
while (prevWinners.length > 1) {
const next: BracketRoundPreview = [];
for (let i = 0; i < prevWinners.length; i += 2) next.push({ blue: prevWinners[i], red: prevWinners[i + 1] });
rounds.push(next);
prevWinners = next.map((f) => f.blue);
}
return rounds;
}
let winnersRounds = $derived(buildWinnersRounds(seedSlots));
// Losers bracket structure: each losers round takes losers from previous winners round or previous losers round.
// Simplified pairing: For each winners round except final, collect losers and pair them sequentially; then advance until one remains for losers final.
function buildLosersTemplate(wRounds: BracketRoundPreview[]): BracketRoundPreview[] {
const losersRounds: BracketRoundPreview[] = [];
if (wRounds.length < 2) return losersRounds;
// Round 1 losers (from winners round 1)
const firstLosersPairs: BracketRoundPreview = [];
wRounds[0].forEach((f) => firstLosersPairs.push({ blue: f.blue, red: f.red })); // placeholders (will label as losers)
losersRounds.push(firstLosersPairs);
// Subsequent losers rounds shrink similarly
let remaining = firstLosersPairs.length; // number of fights that feed losers next stage
while (remaining > 1) {
const next: BracketRoundPreview = [];
for (let i = 0; i < remaining; i += 2) next.push(firstLosersPairs[i]); // placeholder reuse
losersRounds.push(next);
remaining = next.length;
}
return losersRounds;
}
let losersRounds = $derived(buildLosersTemplate(winnersRounds));
let generateDisabled = $derived(gamemode !== "" && map !== "" && winnersRounds.length > 0 && seedSlots.length >= 4);
// Type helpers
function slotLabel(slot: SeedSlot): string {
if (slot.kind === "TEAM") return teams.get(slot.id)?.name ?? "???";
if (slot.kind === "GROUP") {
const gname = data.groups.find((g) => g.id === slot.groupId)?.name ?? "?";
return `(Grp ${gname} Platz ${slot.place + 1})`;
}
if (slot.kind === "FIGHT") {
const f = data.fights.find((x) => x.id === slot.fightId);
const when = f ? new Date(f.start).toLocaleTimeString("de-DE", { timeStyle: "short" }) : "?";
const vs = f ? `${f.blueTeam.kuerzel} vs. ${f.redTeam.kuerzel}` : "Kampf";
return `${slot.place === 0 ? "Gewinner" : "Verlierer"} von ${vs} (${when})`;
}
return "???";
}
async function generateDouble() {
if (!generateDisabled) return;
const eventId = data.event.id;
// Create two groups: winners & losers + grand final group (optional combine winners)
const winnersGroup = await $eventRepo.createGroup(eventId, { name: "Winners", type: "ELIMINATION_STAGE" });
const losersGroup = await $eventRepo.createGroup(eventId, { name: "Losers", type: "ELIMINATION_STAGE" });
const finalGroup = await $eventRepo.createGroup(eventId, { name: "Final", type: "ELIMINATION_STAGE" });
function fallbackTeamId(slot: SeedSlot): number {
if (slot.kind === "GROUP" || slot.kind === "FIGHT") return -1;
if (slot.kind === "TEAM") return slot.id;
return data.teams[0].id;
}
const winnersFightIdsByRound: number[][] = [];
for (let r = 0; r < winnersRounds.length; r++) {
const round = winnersRounds[r];
const ids: number[] = [];
for (let i = 0; i < round.length; i++) {
let finalMap = map;
if (finalMap === "%random%" && selectableMaps.length > 0) finalMap = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
const f = await $fightRepo.createFight(eventId, {
blueTeam: fallbackTeamId(round[i].blue),
redTeam: fallbackTeamId(round[i].red),
group: winnersGroup.id,
map: finalMap,
spectatePort: null,
spielmodus: gamemode,
start: dayjs(
startTime
.copy()
.add({ minutes: roundTime * r })
.add({ seconds: startDelay * i })
.toDate()
),
});
// Attach relations for GROUP/FIGHT seeds
const pair = round[i];
if (pair.blue.kind === "GROUP") await $eventRepo.createRelation(eventId, { fightId: f.id, team: "BLUE", fromType: "GROUP", fromId: pair.blue.groupId, fromPlace: pair.blue.place });
if (pair.red.kind === "GROUP") await $eventRepo.createRelation(eventId, { fightId: f.id, team: "RED", fromType: "GROUP", fromId: pair.red.groupId, fromPlace: pair.red.place });
if (pair.blue.kind === "FIGHT") await $eventRepo.createRelation(eventId, { fightId: f.id, team: "BLUE", fromType: "FIGHT", fromId: pair.blue.fightId, fromPlace: pair.blue.place });
if (pair.red.kind === "FIGHT") await $eventRepo.createRelation(eventId, { fightId: f.id, team: "RED", fromType: "FIGHT", fromId: pair.red.fightId, fromPlace: pair.red.place });
ids.push(f.id);
}
winnersFightIdsByRound.push(ids);
}
// Progression in winners bracket
for (let r = 1; r < winnersFightIdsByRound.length; r++) {
const prev = winnersFightIdsByRound[r - 1];
const curr = winnersFightIdsByRound[r];
for (let i = 0; i < curr.length; i++) {
const target = curr[i];
const srcA = prev[i * 2];
const srcB = prev[i * 2 + 1];
await $eventRepo.createRelation(eventId, { fightId: target, team: "BLUE", fromType: "FIGHT", fromId: srcA, fromPlace: 0 });
await $eventRepo.createRelation(eventId, { fightId: target, team: "RED", fromType: "FIGHT", fromId: srcB, fromPlace: 0 });
}
}
// Losers bracket (canonical pattern):
// L1: losers of WBR1 paired; then for r=2..(k-1):
// Major: winners of previous L vs losers of WBRr
// Minor: winners of that major paired (except after last WBR where we go to LB final vs WB final loser)
const losersFightIdsByRound: number[][] = [];
let losersRoundIndex = 0;
const k = winnersFightIdsByRound.length; // number of winners rounds
// L1 from WBR1 losers
{
const wb1 = winnersFightIdsByRound[0];
const ids: number[] = [];
for (let i = 0; i < wb1.length; i += 2) {
let finalMap = map;
if (finalMap === "%random%" && selectableMaps.length > 0) finalMap = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
const lf = await $fightRepo.createFight(eventId, {
blueTeam: -1,
redTeam: -1,
group: losersGroup.id,
map: finalMap,
spectatePort: null,
spielmodus: gamemode,
start: dayjs(
startTime
.copy()
.add({ minutes: roundTime * (k + losersRoundIndex) })
.add({ seconds: startDelay * (i / 2) })
.toDate()
),
});
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "BLUE", fromType: "FIGHT", fromId: wb1[i], fromPlace: 1 });
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "RED", fromType: "FIGHT", fromId: wb1[i + 1], fromPlace: 1 });
ids.push(lf.id);
}
losersFightIdsByRound.push(ids);
losersRoundIndex++;
}
// For each subsequent winners round except the final
for (let wr = 1; wr < k - 1; wr++) {
const prevLBRound = losersFightIdsByRound[losersFightIdsByRound.length - 1];
// Major: winners of prevLBRound vs losers of current WBR (wr)
{
const ids: number[] = [];
for (let j = 0; j < winnersFightIdsByRound[wr].length; j++) {
let finalMap = map;
if (finalMap === "%random%" && selectableMaps.length > 0) finalMap = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
const lf = await $fightRepo.createFight(eventId, {
blueTeam: -1,
redTeam: -1,
group: losersGroup.id,
map: finalMap,
spectatePort: null,
spielmodus: gamemode,
start: dayjs(
startTime
.copy()
.add({ minutes: roundTime * (k + losersRoundIndex) })
.add({ seconds: startDelay * j })
.toDate()
),
});
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "BLUE", fromType: "FIGHT", fromId: prevLBRound[j], fromPlace: 0 });
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "RED", fromType: "FIGHT", fromId: winnersFightIdsByRound[wr][j], fromPlace: 1 });
ids.push(lf.id);
}
losersFightIdsByRound.push(ids);
losersRoundIndex++;
}
// Minor: pair winners of last LBRound among themselves (if more than 1)
{
const last = losersFightIdsByRound[losersFightIdsByRound.length - 1];
if (last.length > 1) {
const ids: number[] = [];
for (let j = 0; j < last.length; j += 2) {
let finalMap = map;
if (finalMap === "%random%" && selectableMaps.length > 0) finalMap = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
const lf = await $fightRepo.createFight(eventId, {
blueTeam: -1,
redTeam: -1,
group: losersGroup.id,
map: finalMap,
spectatePort: null,
spielmodus: gamemode,
start: dayjs(
startTime
.copy()
.add({ minutes: roundTime * (k + losersRoundIndex) })
.add({ seconds: startDelay * (j / 2) })
.toDate()
),
});
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "BLUE", fromType: "FIGHT", fromId: last[j], fromPlace: 0 });
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "RED", fromType: "FIGHT", fromId: last[j + 1], fromPlace: 0 });
ids.push(lf.id);
}
losersFightIdsByRound.push(ids);
losersRoundIndex++;
}
}
}
// Final losers round: winners of last LBRound vs loser of Winners Final (last WBR)
const winnersFinal = winnersFightIdsByRound[k - 1][0];
const lastLBRound = losersFightIdsByRound[losersFightIdsByRound.length - 1];
let losersFinal: number | undefined = undefined;
if (lastLBRound && lastLBRound.length >= 1) {
let finalMap2 = map;
if (finalMap2 === "%random%" && selectableMaps.length > 0) finalMap2 = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
const lf = await $fightRepo.createFight(eventId, {
blueTeam: -1,
redTeam: -1,
group: losersGroup.id,
map: finalMap2,
spectatePort: null,
spielmodus: gamemode,
start: dayjs(
startTime
.copy()
.add({ minutes: roundTime * (k + losersRoundIndex) })
.toDate()
),
});
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "BLUE", fromType: "FIGHT", fromId: lastLBRound[lastLBRound.length - 1], fromPlace: 0 });
await $eventRepo.createRelation(eventId, { fightId: lf.id, team: "RED", fromType: "FIGHT", fromId: winnersFinal, fromPlace: 1 });
losersFinal = lf.id;
losersFightIdsByRound.push([lf.id]);
losersRoundIndex++;
}
// Grand Final
let finalMap = map;
if (finalMap === "%random%" && selectableMaps.length > 0) finalMap = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
const grandFinal = await $fightRepo.createFight(eventId, {
blueTeam: -1,
redTeam: -1,
group: finalGroup.id,
map: finalMap,
spectatePort: null,
spielmodus: gamemode,
start: dayjs(
startTime
.copy()
.add({ minutes: roundTime * (k + losersRoundIndex) })
.toDate()
),
});
await $eventRepo.createRelation(eventId, { fightId: grandFinal.id, team: "BLUE", fromType: "FIGHT", fromId: winnersFinal, fromPlace: 0 });
if (losersFinal !== undefined) await $eventRepo.createRelation(eventId, { fightId: grandFinal.id, team: "RED", fromType: "FIGHT", fromId: losersFinal, fromPlace: 0 });
await replace("#/event/" + eventId);
}
</script>
<Card class="p-4 mb-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Double Elimination Bracket</h2>
<div class="flex gap-2">
<Button onclick={shuffleTeams} aria-label="Shuffle Teams"><Shuffle size={16} /> Shuffle</Button>
</div>
</div>
{#if seedSlots.length < 4}
<p class="text-gray-400">Mindestens vier Seeds benötigt.</p>
{:else if winnersRounds.length === 0}
<p class="text-yellow-400">Seedanzahl muss eine Zweierpotenz sein. Aktuell: {seedSlots.length}</p>
{/if}
<div class="grid lg:grid-cols-3 gap-6">
<div class="space-y-4">
<Label>Seeds</Label>
<ul class="mt-2 space-y-1">
{#each seedSlots as slot, i (i)}
<li class="flex items-center gap-2 text-sm">
<span class="w-6 text-right">{i + 1}.</span>
<span class="flex-1 truncate">{slotLabel(slot)}</span>
<div class="flex gap-1">
<Button size="sm" onclick={() => moveSlot(i, -1)} disabled={i === 0}></Button>
<Button size="sm" onclick={() => moveSlot(i, 1)} disabled={i === seedSlots.length - 1}></Button>
<Button size="sm" variant="destructive" onclick={() => removeSlot(i)}>✕</Button>
</div>
</li>
{/each}
</ul>
<div class="space-y-2">
<Label>Hinzufügen</Label>
<div class="flex flex-col gap-2">
<div class="flex flex-wrap gap-2 items-center">
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedAddTeam}>
{#each data.teams as t}<option value={t.id}>{t.name}</option>{/each}
</select>
<Button size="sm" onclick={addTeam}>Team</Button>
<Button size="sm" onclick={addUnknown}>???</Button>
</div>
{#if data.groups.length > 0}
<div class="flex flex-wrap gap-2 items-center">
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedGroup}>
{#each data.groups as g}<option value={g.id}>{g.name}</option>{/each}
</select>
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedGroupPlace}>
{#each Array(16) as _, idx}<option value={idx}>{idx + 1}. Platz</option>{/each}
</select>
<Button size="sm" onclick={addGroupPlace}>Gruppenplatz</Button>
</div>
{/if}
{#if data.fights.length > 0}
<div class="flex flex-wrap gap-2 items-center">
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedFight}>
{#each data.fights as f}
<option value={f.id}>{new Date(f.start).toLocaleTimeString("de-DE", { timeStyle: "short" })}: {f.blueTeam.kuerzel} vs. {f.redTeam.kuerzel}</option>
{/each}
</select>
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedFightPlace}>
<option value={0}>Gewinner</option>
<option value={1}>Verlierer</option>
</select>
<Button size="sm" onclick={addFightPlace}>Kampfplatz</Button>
</div>
{/if}
</div>
</div>
</div>
<div>
<Label>Konfiguration</Label>
<DateTimePicker bind:value={startTime} />
<div class="mt-4">
<Label>Rundenzeit: {roundTime}m</Label>
<Slider type="single" bind:value={roundTime} step={5} min={5} max={60} />
</div>
<div class="mt-4">
<Label>Startverzögerung: {startDelay}s</Label>
<Slider type="single" bind:value={startDelay} step={5} min={0} max={60} />
</div>
<div class="mt-4">
<Label>Spielmodus</Label>
<Select type="single" bind:value={gamemode}>
<SelectTrigger>{gamemode}</SelectTrigger>
<SelectContent>
{#each selectableGamemodes as gm}<SelectItem value={gm.value}>{gm.name}</SelectItem>{/each}
</SelectContent>
</Select>
</div>
<div class="mt-4">
<Label>Map</Label>
<Select type="single" bind:value={map}>
<SelectTrigger>{map}</SelectTrigger>
<SelectContent>
<SelectItem value="%random%">Zufällige Map</SelectItem>
{#each selectableMaps as mp}<SelectItem value={mp.value}>{mp.name}</SelectItem>{/each}
</SelectContent>
</Select>
</div>
</div>
<div class="space-y-6">
<div>
<Label>Winners Bracket Vorschau</Label>
{#if winnersRounds.length > 0}
<div class="flex gap-6 overflow-x-auto mt-2">
{#each winnersRounds as round, r}
<div>
<h3 class="font-semibold mb-2">W Runde {r + 1}</h3>
<ul class="space-y-1">
{#each round as fight, i}
<li class="p-2 border border-gray-700 rounded text-xs">
<span class="text-gray-400"
>{new Intl.DateTimeFormat("de-DE", { hour: "2-digit", minute: "2-digit" }).format(
startTime
.copy()
.add({ minutes: roundTime * r, seconds: startDelay * i })
.toDate()
)}</span
>
: {slotLabel(fight.blue)} vs. {slotLabel(fight.red)}
</li>
{/each}
</ul>
</div>
{/each}
</div>
{/if}
</div>
<div>
<Label>Losers Bracket (vereinfachte Vorschau)</Label>
{#if losersRounds.length > 0}
<div class="flex gap-6 overflow-x-auto mt-2">
{#each losersRounds as round, r}
<div>
<h3 class="font-semibold mb-2">L Runde {r + 1}</h3>
<ul class="space-y-1">
{#each round as fight, i}
<li class="p-2 border border-gray-700 rounded text-xs">
Verlierer Paar {i + 1} (aus W Runde {r + 1})
</li>
{/each}
</ul>
</div>
{/each}
</div>
{/if}
</div>
</div>
</div>
<Button class="!p-4 fixed bottom-4 right-4" disabled={!generateDisabled} onclick={generateDouble} aria-label="Double Bracket generieren">
<Plus />
</Button>
</Card>
<!-- minimal styles only -->

View File

@@ -1,364 +0,0 @@
<script lang="ts">
import type { ExtendedEvent } from "@components/types/event";
import type { Team } from "@components/types/team";
import { eventRepo } from "@components/repo/event";
import { fightRepo } from "@components/repo/fight";
import { gamemodes, maps } from "@components/stores/stores";
import { Button } from "@components/ui/button";
import { Card } from "@components/ui/card";
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
import { Label } from "@components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import { Slider } from "@components/ui/slider";
import { fromAbsolute } from "@internationalized/date";
import dayjs from "dayjs";
import { Plus, Shuffle } from "lucide-svelte";
import { replace } from "svelte-spa-router";
let { data }: { data: ExtendedEvent } = $props();
// --- Seeding model: support teams, group results, unknown placeholders ---
interface SeedTeamSlot {
kind: "TEAM";
id: number;
}
interface SeedGroupSlot {
kind: "GROUP";
groupId: number;
place: number;
}
interface SeedUnknownSlot {
kind: "UNKNOWN";
uid: number;
}
interface SeedFightSlot {
kind: "FIGHT";
fightId: number;
place: 0 | 1;
} // 0 winner, 1 loser
type SeedSlot = SeedTeamSlot | SeedGroupSlot | SeedUnknownSlot | SeedFightSlot;
let seedSlots = $state<SeedSlot[]>(data.teams.map((t) => ({ kind: "TEAM", id: t.id })));
const teams = $derived(new Map<number, Team>(data.teams.map((t) => [t.id, t])));
let unknownCounter = 1;
function shuffleTeams() {
const teamIndices = seedSlots.map((v, i) => ({ v, i })).filter((x) => x.v.kind === "TEAM");
const shuffledIds = teamIndices.map((x) => (x.v as SeedTeamSlot).id).sort(() => Math.random() - 0.5);
let p = 0;
seedSlots = seedSlots.map((slot) => (slot.kind === "TEAM" ? { kind: "TEAM", id: shuffledIds[p++] } : slot));
}
function moveSlot(index: number, dir: -1 | 1) {
const newIndex = index + dir;
if (newIndex < 0 || newIndex >= seedSlots.length) return;
const copy = [...seedSlots];
const [item] = copy.splice(index, 1);
copy.splice(newIndex, 0, item);
seedSlots = copy;
}
function removeSlot(index: number) {
seedSlots = seedSlots.filter((_, i) => i !== index);
}
function addUnknown() {
seedSlots = [...seedSlots, { kind: "UNKNOWN", uid: unknownCounter++ }];
}
let selectedAddTeam = $state<number>(data.teams[0]?.id ?? 0);
function addTeam() {
if (selectedAddTeam !== undefined) seedSlots = [...seedSlots, { kind: "TEAM", id: selectedAddTeam }];
}
let selectedGroup = $state<number | null>(data.groups[0]?.id ?? null);
let selectedGroupPlace = $state<number>(0);
function addGroupPlace() {
if (selectedGroup != null) seedSlots = [...seedSlots, { kind: "GROUP", groupId: selectedGroup, place: selectedGroupPlace }];
}
// Fight seed selection
let selectedFight = $state<number | null>(data.fights[0]?.id ?? null);
let selectedFightPlace = $state<0 | 1>(0);
function addFightPlace() {
if (selectedFight != null) seedSlots = [...seedSlots, { kind: "FIGHT", fightId: selectedFight, place: selectedFightPlace }];
}
// Config inputs
let startTime = $state(fromAbsolute(data.event.start, "Europe/Berlin"));
let roundTime = $state(30); // minutes per round
let startDelay = $state(30); // seconds between fights inside round
let gamemode = $state("");
let map = $state("");
// Gamemode / Map selection stores
let selectableGamemodes = $derived($gamemodes.map((g) => ({ name: g, value: g })).sort((a, b) => a.name.localeCompare(b.name)));
let mapsStore = $derived(maps(gamemode));
let selectableMaps = $derived($mapsStore.map((m) => ({ name: m, value: m })).sort((a, b) => a.name.localeCompare(b.name)));
// Derived: bracket rounds preview
interface BracketFightPreview {
blue: SeedSlot;
red: SeedSlot;
}
type BracketRoundPreview = BracketFightPreview[];
function buildBracketSeeds(order: SeedSlot[]): BracketRoundPreview[] {
const n = order.length;
if (n < 2) return [];
// Require power of two for now; simplest implementation
if ((n & (n - 1)) !== 0) return [];
let rounds: BracketRoundPreview[] = [];
let round: BracketRoundPreview = [];
for (let i = 0; i < order.length; i += 2) round.push({ blue: order[i], red: order[i + 1] });
rounds.push(round);
// Higher rounds placeholders using first team from each prior pairing as seed representative
let prevWinners = round.map((fight) => fight.blue);
while (prevWinners.length > 1) {
const nextRound: BracketRoundPreview = [];
for (let i = 0; i < prevWinners.length; i += 2) {
nextRound.push({ blue: prevWinners[i], red: prevWinners[i + 1] });
}
rounds.push(nextRound);
prevWinners = nextRound.map((f) => f.blue);
}
return rounds;
}
let bracketRounds = $derived(buildBracketSeeds(seedSlots));
let generateDisabled = $derived(gamemode !== "" && map !== "" && bracketRounds.length > 0 && seedSlots.length >= 2);
async function generateBracket() {
if (!generateDisabled) return;
const eventId = data.event.id;
// create elimination group
const group = await $eventRepo.createGroup(eventId, { name: "Elimination", type: "ELIMINATION_STAGE" });
// Create fights round by round & keep ids for relation wiring
const fightIdsByRound: number[][] = [];
function fallbackTeamId(slot: SeedSlot): number {
// If this seed is a relation (GROUP or FIGHT), use -1 as requested
if (slot.kind === "GROUP" || slot.kind === "FIGHT") return -1;
if (slot.kind === "TEAM") return slot.id;
// UNKNOWN stays as a concrete placeholder team or -1? Keep concrete team to avoid backend errors.
return data.teams[0].id;
}
for (let r = 0; r < bracketRounds.length; r++) {
const round = bracketRounds[r];
const ids: number[] = [];
for (let i = 0; i < round.length; i++) {
const pair = round[i];
let finalMap = map;
if (finalMap === "%random%" && selectableMaps.length > 0) finalMap = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
const fight = await $fightRepo.createFight(eventId, {
blueTeam: fallbackTeamId(pair.blue),
redTeam: fallbackTeamId(pair.red),
group: group.id,
map: finalMap,
spectatePort: null,
spielmodus: gamemode,
start: dayjs(
startTime
.copy()
.add({ minutes: roundTime * r })
.add({ seconds: startDelay * i })
.toDate()
),
});
if (pair.blue.kind === "GROUP") await $eventRepo.createRelation(eventId, { fightId: fight.id, team: "BLUE", fromType: "GROUP", fromId: pair.blue.groupId, fromPlace: pair.blue.place });
if (pair.red.kind === "GROUP") await $eventRepo.createRelation(eventId, { fightId: fight.id, team: "RED", fromType: "GROUP", fromId: pair.red.groupId, fromPlace: pair.red.place });
if (pair.blue.kind === "FIGHT") await $eventRepo.createRelation(eventId, { fightId: fight.id, team: "BLUE", fromType: "FIGHT", fromId: pair.blue.fightId, fromPlace: pair.blue.place });
if (pair.red.kind === "FIGHT") await $eventRepo.createRelation(eventId, { fightId: fight.id, team: "RED", fromType: "FIGHT", fromId: pair.red.fightId, fromPlace: pair.red.place });
ids.push(fight.id);
}
fightIdsByRound.push(ids);
}
// Wire relations: for each fight in rounds >0, reference winners of two source fights from previous round
for (let r = 1; r < fightIdsByRound.length; r++) {
const prev = fightIdsByRound[r - 1];
const current = fightIdsByRound[r];
for (let i = 0; i < current.length; i++) {
const targetFightId = current[i];
const srcA = prev[i * 2];
const srcB = prev[i * 2 + 1];
// Winner assumed place 1
await $eventRepo.createRelation(eventId, {
fightId: targetFightId,
team: "BLUE",
fromType: "FIGHT",
fromId: srcA,
fromPlace: 1,
});
await $eventRepo.createRelation(eventId, {
fightId: targetFightId,
team: "RED",
fromType: "FIGHT",
fromId: srcB,
fromPlace: 1,
});
}
}
// Redirect back to event view
await replace("#/event/" + eventId);
}
// Helpers for template rendering with TS type guards
function isTeam(slot: SeedSlot): slot is SeedTeamSlot {
return slot.kind === "TEAM";
}
function isGroup(slot: SeedSlot): slot is SeedGroupSlot {
return slot.kind === "GROUP";
}
function slotLabel(slot: SeedSlot): string {
if (isTeam(slot)) return teams.get(slot.id)?.name ?? "Team";
if (isGroup(slot)) {
const gname = data.groups.find((g) => g.id === slot.groupId)?.name ?? "?";
return `(Grp ${gname} Platz ${slot.place + 1})`;
}
if (slot.kind === "FIGHT") {
const f = data.fights.find((x) => x.id === slot.fightId);
const when = f ? new Date(f.start).toLocaleTimeString("de-DE", { timeStyle: "short" }) : "?";
const vs = f ? `${f.blueTeam.kuerzel} vs. ${f.redTeam.kuerzel}` : "Kampf";
return `${slot.place === 0 ? "Gewinner" : "Verlierer"} von ${vs} (${when})`;
}
return "???";
}
</script>
<Card class="p-4 mb-4">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">Single Elimination Bracket</h2>
<div class="flex gap-2">
<Button onclick={shuffleTeams} aria-label="Shuffle Teams"><Shuffle size={16} /> Shuffle</Button>
</div>
</div>
{#if seedSlots.length < 2}
<p class="text-gray-400">Mindestens zwei Seeds benötigt.</p>
{:else if bracketRounds.length === 0}
<p class="text-yellow-400">Anzahl der Seeds muss eine Zweierpotenz sein (2,4,8,16,...). Aktuell: {seedSlots.length}</p>
{/if}
<div class="grid md:grid-cols-2 gap-4">
<div class="space-y-4">
<div>
<Label>Seeds / Reihenfolge</Label>
<ul class="mt-2 space-y-1">
{#each seedSlots as slot, i (i)}
<li class="flex items-center gap-2 text-sm">
<span class="w-6 text-right">{i + 1}.</span>
<span class="flex-1 truncate">{slotLabel(slot)}</span>
<div class="flex gap-1">
<Button size="sm" onclick={() => moveSlot(i, -1)} disabled={i === 0}></Button>
<Button size="sm" onclick={() => moveSlot(i, 1)} disabled={i === seedSlots.length - 1}></Button>
<Button size="sm" variant="destructive" onclick={() => removeSlot(i)}>✕</Button>
</div>
</li>
{/each}
</ul>
</div>
<div class="space-y-2">
<Label>Seed hinzufügen</Label>
<div class="flex flex-col gap-2">
<div class="flex flex-wrap gap-2 items-center">
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedAddTeam}>
{#each data.teams as t}<option value={t.id}>{t.name}</option>{/each}
</select>
<Button size="sm" onclick={addTeam}>Team</Button>
<Button size="sm" onclick={addUnknown}>???</Button>
</div>
<div class="flex flex-wrap gap-2 items-center">
{#if data.groups.length > 0}
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedGroup}>
{#each data.groups as g}<option value={g.id}>{g.name}</option>{/each}
</select>
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedGroupPlace}>
{#each Array(16) as _, idx}<option value={idx}>{idx + 1}. Platz</option>{/each}
</select>
<Button size="sm" onclick={addGroupPlace}>Gruppenplatz</Button>
{/if}
</div>
<div class="flex flex-wrap gap-2 items-center">
{#if data.fights.length > 0}
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedFight}>
{#each data.fights as f}
<option value={f.id}>{new Date(f.start).toLocaleTimeString("de-DE", { timeStyle: "short" })}: {f.blueTeam.kuerzel} vs. {f.redTeam.kuerzel}</option>
{/each}
</select>
<select class="bg-gray-800 border border-gray-700 rounded px-2 py-1" bind:value={selectedFightPlace}>
<option value={0}>Gewinner</option>
<option value={1}>Verlierer</option>
</select>
<Button size="sm" onclick={addFightPlace}>Kampfplatz</Button>
{/if}
</div>
</div>
<p class="text-xs text-gray-500">Gruppen- oder Kampfplätze erzeugen Relationen beim Generieren. ??? bleibt Platzhalter.</p>
</div>
</div>
<div>
<Label>Startzeit</Label>
<DateTimePicker bind:value={startTime} />
<div class="mt-4">
<Label>Rundenzeit: {roundTime}m</Label>
<Slider type="single" bind:value={roundTime} step={5} min={5} max={60} />
</div>
<div class="mt-4">
<Label>Startverzögerung: {startDelay}s</Label>
<Slider type="single" bind:value={startDelay} step={5} min={0} max={60} />
</div>
<div class="mt-4">
<Label>Spielmodus</Label>
<Select type="single" bind:value={gamemode}>
<SelectTrigger>{gamemode}</SelectTrigger>
<SelectContent>
{#each selectableGamemodes as gm}
<SelectItem value={gm.value}>{gm.name}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="mt-4">
<Label>Map</Label>
<Select type="single" bind:value={map}>
<SelectTrigger>{map}</SelectTrigger>
<SelectContent>
<SelectItem value="%random%">Zufällige Map</SelectItem>
{#each selectableMaps as mp}
<SelectItem value={mp.value}>{mp.name}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
</div>
</div>
<div class="mt-6">
<Label>Vorschau</Label>
{#if bracketRounds.length > 0}
<div class="flex gap-8 overflow-x-auto mt-2">
{#each bracketRounds as round, r}
<div>
<h3 class="font-semibold mb-2">Runde {r + 1}</h3>
<ul class="space-y-1">
{#each round as fight, i}
<li class="p-2 border border-gray-700 rounded text-sm">
<span class="text-gray-400"
>{new Intl.DateTimeFormat("de-DE", { hour: "2-digit", minute: "2-digit" }).format(
startTime
.copy()
.add({ minutes: roundTime * r, seconds: startDelay * i })
.toDate()
)}</span
>
: {slotLabel(fight.blue)} &nbsp;vs.&nbsp; {slotLabel(fight.red)}
</li>
{/each}
</ul>
</div>
{/each}
</div>
{/if}
</div>
<Button class="!p-4 fixed bottom-4 right-4" disabled={!generateDisabled} onclick={generateBracket} aria-label="Bracket generieren">
<Plus />
</Button>
</Card>
<!-- no component-scoped styles needed -->

View File

@@ -1,306 +0,0 @@
<script lang="ts">
import DragAcceptor from "@components/admin/pages/generate/DragAcceptor.svelte";
import TeamChip from "@components/admin/pages/generate/TeamChip.svelte";
import { eventRepo } from "@components/repo/event";
import { fightRepo } from "@components/repo/fight";
import { gamemodes, maps } from "@components/stores/stores";
import type { ExtendedEvent } from "@components/types/event";
import type { Team } from "@components/types/team";
import { Button } from "@components/ui/button";
import { Card } from "@components/ui/card";
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
import { Dialog } from "@components/ui/dialog";
import { Input } from "@components/ui/input";
import { Label } from "@components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import { Slider } from "@components/ui/slider";
import { fromAbsolute, fromDate, parseDateTime, parseDuration } from "@internationalized/date";
import dayjs from "dayjs";
import { Plus } from "lucide-svelte";
import { replace } from "svelte-spa-router";
let {
data,
}: {
data: ExtendedEvent;
} = $props();
let teams = $derived(new Map<number, Team>(data.teams.map((team) => [team.id, team])));
let groups: number[][] = $state([]);
let teamsNotInGroup = $derived(data.teams.filter((team) => !groups.flat().includes(team.id)));
function dragToNewGroup(event: DragEvent) {
event.preventDefault();
let teamId = parseInt(event.dataTransfer!.getData("team"));
groups = [...groups.map((value) => value.filter((value1) => value1 != teamId)), [teamId]].filter((value) => value.length > 0);
}
function teamDragStart(ev: DragEvent, team: Team) {
ev.dataTransfer!.setData("team", team.id.toString());
}
let resetDragOver = $state(false);
function resetDragOverEvent(ev: DragEvent) {
resetDragOver = true;
ev.preventDefault();
}
function dropReset(ev: DragEvent) {
ev.preventDefault();
let teamId = parseInt(ev.dataTransfer!.getData("team"));
groups = groups.map((group) => group.filter((team) => team !== teamId)).filter((group) => group.length > 0);
resetDragOver = false;
}
function dropGroup(ev: DragEvent, groupIndex: number) {
ev.preventDefault();
let teamId = parseInt(ev.dataTransfer!.getData("team"));
groups = groups.map((group, i) => (i === groupIndex ? [...group.filter((value) => value != teamId), teamId] : group.filter((value) => value != teamId))).filter((group) => group.length > 0);
}
let startTime = $state(fromAbsolute(data.event.start, "Europe/Berlin"));
let gamemode = $state("");
let map = $state("");
let selectableGamemodes = $derived(
$gamemodes
.map((gamemode) => {
return {
name: gamemode,
value: gamemode,
};
})
.sort((a, b) => a.name.localeCompare(b.name))
);
let mapsStore = $derived(maps(gamemode));
let selectableMaps = $derived(
$mapsStore
.map((map) => {
return {
name: map,
value: map,
};
})
.sort((a, b) => a.name.localeCompare(b.name))
);
let roundTime = $state(30);
let startDelay = $state(30);
let showAutoGrouping = $state(false);
let groupCount = $state(Math.floor(data.teams.length / 2));
function createGroups() {
let teams = data.teams.map((team) => team.id).sort(() => Math.random() - 0.5);
groups = [];
for (let i = 0; i < groupCount; i++) {
groups.push([]);
}
while (teams.length > 0) {
groups[teams.length % groupCount].push(teams.pop() as number);
}
showAutoGrouping = false;
groups = groups.filter((group) => group.length > 0);
}
function generateGroups(groups: number[][]): number[][][][] {
const groupFights: number[][][][] = [];
groups.forEach((group) => {
let round = group.length + (group.length % 2) - 1;
let groupFight = [];
for (let i = 0; i < round; i++) {
let availableTeams = [...group];
if (group.length % 2 === 1) {
availableTeams = availableTeams.filter((team, index) => index !== i);
}
let roundFights = [];
while (availableTeams.length > 0) {
let team1 = availableTeams.pop() as number;
let team2 = availableTeams.at(i % availableTeams.length) as number;
availableTeams = availableTeams.filter((team) => team !== team2);
let fight = [team1, team2];
fight.sort(() => Math.random() - 0.5);
roundFights.push(fight);
}
groupFight.push(roundFights);
}
groupFights.push(groupFight);
});
return groupFights;
}
let groupsFights = $derived(generateGroups(groups));
let generateDisabled = $derived(groupsFights.length > 0 && groupsFights.every((value) => value.every((value1) => value1.length > 0)) && gamemode !== "" && map !== "");
async function generateFights() {
groupsFights.forEach((group, i) => {
$eventRepo
.createGroup(data.event.id, {
name: "Gruppe " + (i + 1),
type: "GROUP_STAGE",
})
.then((v) => {
group.forEach((round, j) => {
round.forEach(async (fight, k) => {
const blueTeam = teams.get(fight[0])!;
const redTeam = teams.get(fight[1])!;
let karte = map;
if (karte === "%random%") {
karte = selectableMaps[Math.floor(Math.random() * selectableMaps.length)].value;
}
await $fightRepo.createFight(data.event.id, {
blueTeam: blueTeam.id,
redTeam: redTeam.id,
group: v.id,
map: karte,
spectatePort: null,
spielmodus: gamemode,
start: dayjs(
startTime
.copy()
.add({
minutes: roundTime * j,
})
.add({
seconds: startDelay * (k + i * round.length),
})
.toDate()
),
});
});
});
});
});
await replace("#/event/" + data.event.id);
}
</script>
<div class="flex justify-between">
<Card
id="reseter"
class="flex w-fit p-2 border border-gray-700 rounded h-20 pt-6 relative {resetDragOver ? 'border-white' : ''}"
ondragover={resetDragOverEvent}
ondragleave={() => (resetDragOver = false)}
ondrop={dropReset}
role="group"
>
{#each teamsNotInGroup as team (team.id)}
<TeamChip {team} ondragstart={(ev) => teamDragStart(ev, team)} />
{/each}
</Card>
<div class="flex items-center mr-4">
<Button onclick={() => (showAutoGrouping = true)}>Automatische Gruppen</Button>
</div>
</div>
<div class="flex mt-4 gap-4 border-b border-gray-700 pb-4">
{#each groups as group, i (i)}
<DragAcceptor ondrop={(ev) => dropGroup(ev, i)}>
<h1>Gruppe {i + 1} ({group.length})</h1>
{#each group as teamId (teamId)}
<TeamChip team={teams.get(teamId)!} ondragstart={(ev) => teamDragStart(ev, teams.get(teamId)!)} />
{/each}
</DragAcceptor>
{/each}
<DragAcceptor ondrop={dragToNewGroup}>
<h1>Neue Gruppe</h1>
</DragAcceptor>
</div>
<div class="border-b mt-4 border-gray-700 pb-4">
<Label for="event-end">Startzeit</Label>
<DateTimePicker bind:value={startTime} />
<div class="mt-2">
<Label for="event-roundtime">Rundenzeit: {roundTime}m</Label>
<Slider id="event-roundtime" type="single" bind:value={roundTime} step={1} min={5} max={60} />
</div>
<div class="mt-2">
<Label for="event-member">Startverzögerung: {startDelay}</Label>
<Slider id="event-member" type="single" bind:value={startDelay} step={1} min={0} max={30} />
</div>
<div class="mt-2">
<Label for="fight-gamemode">Spielmodus</Label>
<Select type="single" bind:value={gamemode}>
<SelectTrigger id="fight-gamemode">{gamemode}</SelectTrigger>
<SelectContent>
{#each selectableGamemodes as gamemodeOption}
<SelectItem value={gamemodeOption.value}>{gamemodeOption.name}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
<div class="mt-2">
<Label for="fight-maps">Map</Label>
<Select type="single" bind:value={map}>
<SelectTrigger id="fight-maps">{map}</SelectTrigger>
<SelectContent>
<SelectItem value="%random%">Zufällige Map</SelectItem>
{#each selectableMaps as mapOption}
<SelectItem value={mapOption.value}>{mapOption.name}</SelectItem>
{/each}
</SelectContent>
</Select>
</div>
</div>
<div class="text-center mx-2">
{#each groupsFights as fightsGroup, i}
<div>
<h1 class="text-4xl">Gruppe: {i + 1}</h1>
{#each fightsGroup as fightsRound, j}
<div class="border-b border-gray-700">
<h1 class="text-2xl">Runde: {j + 1}</h1>
{#each fightsRound as fightTeams, k}
<div class="text-left p-4">
<span class="p-2 border border-gray-700 rounded"
>{new Intl.DateTimeFormat("de-DE", {
day: "2-digit",
month: "2-digit",
year: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).format(
startTime
.copy()
.add({
minutes: roundTime * j,
seconds: startDelay * (k + i * fightsRound.length),
})
.toDate()
)}</span
>
{teams.get(fightTeams[0])!.name} vs. {teams.get(fightTeams[1])!.name}
</div>
{/each}
</div>
{/each}
</div>
{/each}
</div>
<Button class="!p-4 fixed bottom-4 right-4" disabled={!generateDisabled} onclick={generateFights}>
<Plus />
</Button>
<style lang="scss">
:global(#reseter::before) {
content: "Reset";
position: absolute;
top: 0;
color: white;
}
:global(#reseter) {
min-width: 14rem;
}
</style>

View File

@@ -1,191 +0,0 @@
<script lang="ts">
import { createSvelteTable, FlexRender } from "@components/ui/data-table";
import { columns } from "./columns";
import { getCoreRowModel, getPaginationRowModel, type PaginationState } from "@tanstack/table-core";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@components/ui/table";
import { auditLog } from "@components/repo/auditlog";
import { now, ZonedDateTime } from "@internationalized/date";
import { AuditLogEntrySchema, type AuditLogEntry } from "@components/types/auditlog";
import { Button } from "@components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import { Input } from "@components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
import { Check } from "lucide-svelte";
import { cn } from "@components/utils";
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
import PlayerSelector from "@components/ui/PlayerSelector.svelte";
let debounceTimer: NodeJS.Timeout;
const debounce = <T,>(value: T, func: (value: T) => void) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
func(value);
}, 300);
};
let actionText = $state("");
let serverText = $state("");
let fullText = $state("");
let actors = $state<number[]>([]);
let actionTypes = $state<string[]>([]);
let timeGreater = $state<ZonedDateTime>(now("Europe/Berlin").subtract({ months: 1 }));
let timeLess = $state<ZonedDateTime>(now("Europe/Berlin"));
let serverOwner = $state<number[]>([]);
let velocity = $state(false);
let sorting = $state("DESC");
let pagination = $state<PaginationState>({
pageIndex: 0,
pageSize: 25,
});
let data = $state<AuditLogEntry[]>([]);
let rows = $state(0);
$effect(() => {
$auditLog
.get(
actionText || undefined,
serverText || undefined,
fullText || undefined,
actors.length > 0 ? actors : undefined,
actionTypes.length > 0 ? actionTypes : undefined,
timeGreater ? timeGreater.toDate().getTime() : undefined,
timeLess ? timeLess.toDate().getTime() : undefined,
serverOwner.length > 0 ? serverOwner : undefined,
velocity,
pagination.pageIndex,
pagination.pageSize,
sorting || undefined
)
.then((res) => {
data = res.entries;
rows = res.rows;
});
});
const table = createSvelteTable({
get data() {
return data;
},
columns,
state: {
get pagination() {
return pagination;
},
},
onPaginationChange: (updater) => {
if (typeof updater === "function") {
pagination = updater(pagination);
} else {
pagination = updater;
}
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
manualPagination: true,
get rowCount() {
return rows;
},
});
let playerSearch = $state("");
let ownerSearch = $state("");
</script>
<div class="p-4">
<div class="rounded border mb-4 p-2 flex lg:flex-row flex-col">
<Input
class="w-48 mr-2"
placeholder="Suchen..."
value={fullText}
onchange={(e) =>
debounce(e.currentTarget.value, (v) => {
fullText = v;
})}
oninput={(e) =>
debounce(e.currentTarget.value, (v) => {
fullText = v;
})}
/>
<Select type="multiple" value={actionTypes} onValueChange={(e) => (actionTypes = e)}>
<SelectTrigger class="w-48 mr-2" placeholder="Aktionstypen auswählen...">Aktionstypen ({actionTypes.length})</SelectTrigger>
<SelectContent>
{#each ["JOIN", "LEAVE", "COMMAND", "SENSITIVE_COMMAND", "CHAT", "GUI_OPEN", "GUI_CLOSE", "GUI_CLICK"] as option}
<SelectItem value={option}>{option}</SelectItem>
{/each}
</SelectContent>
</Select>
<div class="mr-2">
<PlayerSelector bind:value={actors} multiple placeholder="Spieler Filter" />
</div>
<div class="mr-2">
<PlayerSelector bind:value={serverOwner} multiple placeholder="Server Owner" />
</div>
<div class="mr-2">
<DateTimePicker bind:value={timeGreater} />
</div>
<div class="mr-2">
<DateTimePicker bind:value={timeLess} />
</div>
<Select type="single" value={sorting} onValueChange={(e) => (sorting = e)}>
<SelectTrigger class="w-48 mr-2">{sorting === "ASC" ? "Aufsteigend" : "Absteigend"}</SelectTrigger>
<SelectContent>
<SelectItem value="ASC">Aufsteigend</SelectItem>
<SelectItem value="DESC">Absteigend</SelectItem>
</SelectContent>
</Select>
</div>
<div class="rounded border">
<Table>
<TableHeader>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<TableRow>
{#each headerGroup.headers as header (header.id)}
<TableHead colspan={header.colSpan}>
{#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>
{#each row.getVisibleCells() as cell (cell.id)}
<TableCell class="p-2 align-top">
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
</TableCell>
{/each}
</TableRow>
{:else}
<TableRow>
<TableCell colspan={columns.length} class="h-24 text-center">Keine Einträge gefunden.</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div>
<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>
<Button variant="outline" size="sm" onclick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>Previous</Button>
<Button variant="outline" size="sm" onclick={() => table.nextPage()} disabled={!table.getCanNextPage()}>Next</Button>
</div>
</div>

View File

@@ -1,35 +0,0 @@
import type { AuditLogEntry } from "@components/types/auditlog";
import type { ColumnDef } from "@tanstack/table-core";
export const columns: ColumnDef<AuditLogEntry>[] = [
{
accessorKey: "id",
header: "ID",
},
{
accessorKey: "time",
header: "Time",
cell: (info) => new Date(info.getValue<number>()).toLocaleString(),
},
{
accessorKey: "server",
header: "Server",
},
{
accessorKey: "serverOwner",
header: "Server Owner",
cell: (info) => info.getValue<string | null>() || "N/A",
},
{
accessorKey: "actor",
header: "Spieler",
},
{
accessorKey: "actionType",
header: "Action Type",
},
{
accessorKey: "actionText",
header: "Action Text",
},
];

View File

@@ -37,8 +37,10 @@ export class OpenEditPage {
contentToSave += "---\n\n";
}
contentToSave += this.content;
const encodedContent = btoa(new TextEncoder().encode(contentToSave).reduce((data, byte) => data + String.fromCharCode(byte), ""));
await get(pageRepo).updatePage(this.pageId, contentToSave, this.sha, prompt("Was hast du geändert?", `Updated ${this.pageTitle}`) ?? `Updated ${this.pageTitle}`, this.manager.branch);
console.log(encodedContent);
//await get(pageRepo).updatePage(this.pageId, this.sha, encodedContent, this.manager.branch);
this.dirty = false;
this.manager.reloadImages();
}

View File

@@ -18,27 +18,24 @@
-->
<script lang="ts">
import { permissions } 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";
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();
const {
perms, uuid
}: { perms: string[], uuid: string } = $props();
let value = $state(perms);
let prevValue = $state(perms);
$effect(() => {
value = perms;
prevValue = perms;
});
function onChange(change: string[]) {
$permissions.perms.forEach((perm) => {
$permissions.perms.forEach(perm => {
if (prevValue.includes(perm) && !change.includes(perm)) {
$permsRepo.removePerm(uuid, perm);
$permsRepo.removePerm(uuid, perm)
} else if (!prevValue.includes(perm) && change.includes(perm)) {
$permsRepo.addPerm(uuid, perm);
$permsRepo.addPerm(uuid, perm)
}
});
@@ -56,4 +53,4 @@
<SelectItem value={permission}>{permission}</SelectItem>
{/each}
</SelectContent>
</Select>
</Select>

View File

@@ -17,132 +17,16 @@
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import { createSvelteTable, FlexRender } from "@components/ui/data-table";
import { columns } from "./columns";
import { getCoreRowModel, getPaginationRowModel, type PaginationState } from "@tanstack/table-core";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@components/ui/table";
import { dataRepo } from "@repo/data";
import type { Player } from "@type/data";
import { Button } from "@components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import { Input } from "@components/ui/input";
<script>
import Table from "@components/moderator/pages/players/Table.svelte";
let debounceTimer: NodeJS.Timeout;
const debounce = <T,>(value: T, func: (value: T) => void) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
func(value);
}, 300);
};
import {dataRepo} from "@repo/data";
let search = $state("");
let pagination = $state<PaginationState>({
pageIndex: 0,
pageSize: 25,
});
let data = $state<Player[]>([]);
let rows = $state(0);
$effect(() => {
$dataRepo.queryPlayers(search || undefined, undefined, undefined, pagination.pageSize, pagination.pageIndex, true, false).then((res) => {
data = res.entries;
rows = res.rows;
});
});
const table = createSvelteTable({
get data() {
return data;
},
columns,
state: {
get pagination() {
return pagination;
},
},
onPaginationChange: (updater) => {
if (typeof updater === "function") {
pagination = updater(pagination);
} else {
pagination = updater;
}
},
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
manualPagination: true,
get rowCount() {
return rows;
},
});
let playersFuture = $state($dataRepo.getPlayers())
</script>
<div class="p-4">
<div class="rounded border mb-4 p-2 flex lg:flex-row flex-col">
<Input
class="w-48 mr-2"
placeholder="Search players..."
value={search}
onchange={(e) =>
debounce(e.currentTarget.value, (v) => {
search = v;
})}
oninput={(e) =>
debounce(e.currentTarget.value, (v) => {
search = v;
})}
/>
</div>
<div class="rounded border">
<Table>
<TableHeader>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<TableRow>
{#each headerGroup.headers as header (header.id)}
<TableHead colspan={header.colSpan}>
{#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>
{#each row.getVisibleCells() as cell (cell.id)}
<TableCell class="p-2 align-top">
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
</TableCell>
{/each}
</TableRow>
{:else}
<TableRow>
<TableCell colspan={columns.length} class="h-24 text-center">No players found.</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div>
<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>
<Button variant="outline" size="sm" onclick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>Previous</Button>
<Button variant="outline" size="sm" onclick={() => table.nextPage()} disabled={!table.getCanNextPage()}>Next</Button>
</div>
</div>
{#await playersFuture}
<p>Loading...</p>
{:then players}
<Table data={players} />
{/await}

View File

@@ -18,18 +18,16 @@
-->
<script lang="ts">
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import { permissions } from "@stores/stores.ts";
import { permsRepo } from "@repo/perms.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();
const {
prefix, uuid
}: { prefix: string, uuid: string } = $props();
let value = $state(prefix);
$effect(() => {
value = prefix;
});
function onChange(change: string) {
$permsRepo.setPrefix(uuid, change);
@@ -46,4 +44,4 @@
<SelectItem value={prefix[0]}>{prefix[1].chatPrefix === "" ? "None" : prefix[1].chatPrefix}</SelectItem>
{/each}
</SelectContent>
</Select>
</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

@@ -17,8 +17,8 @@
* 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 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";
@@ -36,20 +36,25 @@ export const columns: ColumnDef<Player[]> = [
accessorKey: "prefix",
header: "Prefix",
cell: ({ row }) => {
return renderComponent(PrefixDropdown, {
prefix: row.getValue("prefix"),
uuid: row.getValue("uuid"),
});
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"),
});
return renderComponent(
PermissionsDropdown,
{
perms: row.getValue("perms"),
uuid: row.getValue("uuid"),
},
);
},
},
];
];

View File

@@ -1,40 +0,0 @@
import { derived } from "svelte/store";
import { fetchWithToken, tokenStore } from "./repo";
import { PagedAutidLogSchema } from "@components/types/auditlog";
export class AuditLogRepo {
async get(
actionText: string | undefined,
serverText: string | undefined,
fullText: string | undefined,
actor: number[] | undefined,
actionType: string[] | undefined,
timeFrom: number | undefined,
timeTo: number | undefined,
serverOwner: number[] | undefined,
velocity: boolean | undefined,
page: number,
pageSize: number,
sorting: string | undefined
) {
const params = new URLSearchParams();
if (actionText) params.append("actionText", actionText);
if (serverText) params.append("serverText", serverText);
if (fullText) params.append("fullText", fullText);
if (actor) actor.forEach((a) => params.append("actor", a.toString()));
if (actionType) actionType.forEach((a) => params.append("actionType", a));
if (timeFrom) params.append("timeGreater", timeFrom.toString());
if (timeTo) params.append("timeLess", timeTo.toString());
if (serverOwner) serverOwner.forEach((s) => params.append("serverOwner", s.toString()));
if (velocity !== undefined) params.append("velocity", velocity.toString());
params.append("page", page.toString());
params.append("limit", pageSize.toString());
if (sorting) params.append("sorting", sorting);
return await fetchWithToken("", `/auditlog?${params.toString()}`)
.then((value) => value.json())
.then((data) => PagedAutidLogSchema.parse(data));
}
}
export const auditLog = derived(tokenStore, ($token) => new AuditLogRepo());

View File

@@ -17,34 +17,49 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { readable, writable } from "svelte/store";
import { ResponseUserSchema } from "@components/types/data";
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() {
this.request("/data/me").then((value) => {
if (value.ok) {
loggedIn.set(true);
} else {
loggedIn.set(false);
}
});
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 {
await this.request("/auth", {
const login = await this.request("/auth", {
method: "POST",
body: JSON.stringify({
name,
password,
keepLoggedIn: true,
}),
})
.then((value) => value.json())
.then((value) => ResponseUserSchema.parse(value));
}).then(value => value.json()).then(value => AuthTokenSchema.parse(value));
loggedIn.set(true);
this.setLoginState(login);
return true;
} catch (e) {
@@ -52,43 +67,118 @@ export class AuthV2Repo {
}
}
async loginDiscord(token: string) {
try {
await this.request("/auth/discord", {
method: "POST",
body: token,
headers: {
"Content-Type": "text/plain",
},
})
.then((value) => value.json())
.then((value) => ResponseUserSchema.parse(value));
loggedIn.set(true);
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);
}
async request(url: string, params: RequestInit = {}) {
return fetch(`${import.meta.env.PUBLIC_API_SERVER}${url}`, {
...params,
credentials: "include",
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: {
"Content-Type": "application/json",
...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());
export const authV2Repo = readable(new AuthV2Repo());

View File

@@ -17,8 +17,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type { Player, PlayerList, Server } from "@type/data.ts";
import { PlayerListSchema, PlayerSchema, ServerSchema } from "@type/data.ts";
import type { Player, Server } from "@type/data.ts";
import { PlayerSchema, ServerSchema } from "@type/data.ts";
import { fetchWithToken, tokenStore } from "./repo.ts";
import { derived, get } from "svelte/store";
import { TeamSchema, type Team } from "@components/types/team.ts";
@@ -38,28 +38,10 @@ export class DataRepo {
.then(PlayerSchema.parse);
}
public async queryPlayers(
name: string | undefined,
uuid: string | undefined,
team: number[] | undefined,
limit: number | undefined,
page: number | undefined,
includePerms: boolean | undefined,
includeId: boolean | undefined
): Promise<PlayerList> {
let query = new URLSearchParams();
if (name) query.append("name", name);
if (uuid) query.append("uuid", uuid);
if (team) team.forEach((t) => query.append("team", t.toString()));
if (limit) query.append("limit", limit.toString());
if (page) query.append("page", page.toString());
if (includePerms !== undefined) query.append("includePerms", includePerms.toString());
if (includeId !== undefined) query.append("includeId", includeId.toString());
return await fetchWithToken(this.token, "/data/admin/users?" + query.toString())
public async getPlayers(): Promise<Player[]> {
return await fetchWithToken(get(tokenStore), "/data/admin/users")
.then((value) => value.json())
.then(PlayerListSchema.parse);
.then(PlayerSchema.array().parse);
}
public async getTeams(): Promise<Team[]> {

View File

@@ -150,7 +150,7 @@ export class EventRepo {
.then((value) => value.json())
.then((value) => z.array(ResponseGroupsSchema).parse(value));
}
public async createGroup(eventId: number, group: CreateEventGroup): Promise<ResponseGroups> {
public async createGroup(eventId: string, group: CreateEventGroup): Promise<ResponseGroups> {
CreateEventGroupSchema.parse(group);
return await fetchWithToken(this.token, `/events/${eventId}/groups`, {
method: "POST",
@@ -186,12 +186,12 @@ export class EventRepo {
}
// Relations
public async listRelations(eventId: number): Promise<ResponseRelation[]> {
public async listRelations(eventId: string): Promise<ResponseRelation[]> {
return await fetchWithToken(this.token, `/events/${eventId}/relations`)
.then((value) => value.json())
.then((value) => z.array(ResponseRelationSchema).parse(value));
}
public async createRelation(eventId: number, relation: CreateEventRelation): Promise<ResponseRelation> {
public async createRelation(eventId: string, relation: CreateEventRelation): Promise<ResponseRelation> {
CreateEventRelationSchema.parse(relation);
return await fetchWithToken(this.token, `/events/${eventId}/relations`, {
method: "POST",
@@ -206,7 +206,7 @@ export class EventRepo {
.then((value) => value.json())
.then(ResponseRelationSchema.parse);
}
public async updateRelation(eventId: number, relationId: number, relation: UpdateEventRelation): Promise<ResponseRelation> {
public async updateRelation(eventId: string, relationId: string, relation: UpdateEventRelation): Promise<ResponseRelation> {
UpdateEventRelationSchema.parse(relation);
return await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, {
method: "PUT",
@@ -216,7 +216,7 @@ export class EventRepo {
.then((value) => value.json())
.then(ResponseRelationSchema.parse);
}
public async deleteRelation(eventId: number, relationId: number): Promise<boolean> {
public async deleteRelation(eventId: string, relationId: string): Promise<boolean> {
const res = await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, {
method: "DELETE",
});

View File

@@ -31,7 +31,7 @@ export interface CreateFight {
redTeam: number;
start: Dayjs;
spectatePort: number | null;
group: number | null;
group: string | null;
}
export interface UpdateFight {
@@ -57,6 +57,7 @@ export class FightRepo {
return await fetchWithToken(this.token, `/events/${eventId}/fights`, {
method: "POST",
body: JSON.stringify({
event: eventId,
spielmodus: fight.spielmodus,
map: fight.map,
blueTeam: fight.blueTeam,

View File

@@ -17,31 +17,20 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { readonly, writable } from "svelte/store";
import {readonly, writable} from "svelte/store";
import type { Readable, Subscriber, Unsubscriber } from "svelte/store";
import type {Readable, Subscriber, Unsubscriber} from "svelte/store";
export interface Cached<T> extends Readable<T> {
reload: () => void;
future: Promise<T>;
}
export function cached<T>(normal: T, init: () => Promise<T>): Cached<T> {
const store = writable<T>(normal);
const future = new Promise<T>((resolve) => {
let f = false;
store.subscribe((value) => {
if (f) {
resolve(value);
} else {
f = true;
}
});
});
let first = true;
const reload = () => {
init().then((data) => {
init().then(data => {
store.set(data);
});
};
@@ -56,7 +45,6 @@ export function cached<T>(normal: T, init: () => Promise<T>): Cached<T> {
return store.subscribe(run, invalidate);
},
reload,
future,
};
}
@@ -70,7 +58,7 @@ export function cachedFamily<T, K>(normal: K, init: (arg0: T) => Promise<K>): (a
let first = true;
const reload = () => {
init(arg).then((data) => {
init(arg).then(data => {
store.set(data);
});
};

View File

@@ -31,6 +31,10 @@ import { permsRepo } from "@repo/perms.ts";
export const schemTypes = cached<SchematicType[]>([], () => fetchWithToken(get(tokenStore), "/data/admin/schematicTypes").then((res) => res.json()));
export const players = cached<Player[]>([], async () => {
return get(dataRepo).getPlayers();
});
export const teams = cached<Team[]>([], async () => {
return get(dataRepo).getTeams();
});
@@ -51,7 +55,7 @@ export const gamemodes = cached<string[]>([], async () => {
});
export const maps = cachedFamily<string, string[]>([], async (gamemode) => {
if ((await gamemodes.future).every((value) => value !== gamemode)) return [];
if (get(gamemodes).every((value) => value !== gamemode)) return [];
const res = await fetchWithToken(get(tokenStore), `/data/admin/gamemodes/${gamemode}/maps`);
if (!res.ok) {

View File

@@ -1,19 +0,0 @@
import { z } from "zod";
export const AuditLogEntrySchema = z.object({
id: z.number(),
time: z.number(),
server: z.string(),
serverOwner: z.string().nullable(),
actor: z.string(),
actionType: z.enum(["JOIN", "LEAVE", "COMMAND", "SENSITIVE_COMMAND", "CHAT", "GUI_OPEN", "GUI_CLOSE", "GUI_CLICK"]),
actionText: z.string(),
});
export const PagedAutidLogSchema = z.object({
entries: z.array(AuditLogEntrySchema),
rows: z.number(),
});
export type AuditLogEntry = z.infer<typeof AuditLogEntrySchema>;
export type PagedAuditLog = z.infer<typeof PagedAutidLogSchema>;

View File

@@ -29,20 +29,12 @@ export type SchematicType = z.infer<typeof SchematicTypeSchema>;
export const PlayerSchema = z.object({
name: z.string(),
uuid: z.string(),
prefix: z.string().nullable(),
perms: z.array(z.string()).nullable(),
id: z.number().nullable(),
prefix: z.string(),
perms: z.array(z.string()),
});
export type Player = z.infer<typeof PlayerSchema>;
export const PlayerListSchema = z.object({
entries: z.array(PlayerSchema),
rows: z.number(),
});
export type PlayerList = z.infer<typeof PlayerListSchema>;
export const ServerSchema = z.object({
description: z.any(),
players: z.object({

View File

@@ -60,11 +60,10 @@ export type ResponseGroups = z.infer<typeof ResponseGroupsSchema>;
export const ResponseRelationSchema = z.object({
id: z.number(),
fight: z.number(),
team: z.enum(["RED", "BLUE"]),
fight: EventFightSchema,
type: z.enum(["FIGHT", "GROUP"]),
fromFight: EventFightSchema.optional(),
fromGroup: ResponseGroupsSchema.optional(),
fromFight: EventFightSchema.nullable(),
fromGroup: ResponseGroupsSchema.nullable(),
fromPlace: z.number(),
});

View File

@@ -17,11 +17,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { z } from "zod";
import {z} from "zod";
export const PrefixSchema = z.object({
name: z.string().startsWith("PREFIX_"),
colorCode: z.string().startsWith("§"),
colorCode: z.string().length(2).startsWith("§"),
chatPrefix: z.string(),
});

View File

@@ -17,12 +17,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { z } from "zod";
import {z} from "zod";
export const TeamSchema = z.object({
id: z.number(),
name: z.string(),
kuerzel: z.string().min(1).max(16),
kuerzel: z.string().min(1).max(4),
color: z.string().max(1),
});

View File

@@ -1,122 +0,0 @@
<script lang="ts">
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
import { Button } from "@components/ui/button";
import { Check, ChevronsUpDown } from "lucide-svelte";
import { cn } from "@components/utils";
import { dataRepo } from "@repo/data";
import type { Player } from "@type/data";
let {
value = $bindable(null),
multiple = false,
placeholder = "Select player...",
onSelect,
}: {
value?: number | number[] | null;
multiple?: boolean;
placeholder?: string;
onSelect?: (player: Player) => void;
} = $props();
let open = $state(false);
let search = $state("");
let players: Player[] = $state([]);
let loading = $state(false);
let debounceTimer: NodeJS.Timeout;
function fetchPlayers(searchTerm: string) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
loading = true;
try {
const res = await $dataRepo.queryPlayers(searchTerm || undefined, undefined, undefined, 50, 0, false, true);
players = res.entries;
} finally {
loading = false;
}
}, 300);
}
$effect(() => {
fetchPlayers(search);
});
function handleSelect(player: Player) {
if (onSelect) {
onSelect(player);
}
if (multiple) {
if (Array.isArray(value)) {
if (value.includes(player.id!)) {
value = value.filter((v) => v !== player.id);
} else {
value = [...value, player.id!];
}
} else {
value = [player.id!];
}
} else {
if (value === player.id) {
value = null; // Deselect
} else {
value = player.id;
open = false;
}
}
}
function isSelected(id: number) {
if (multiple) {
return Array.isArray(value) && value.includes(id);
}
return value === id;
}
let triggerLabel = $derived.by(() => {
if (multiple) {
if (Array.isArray(value) && value.length > 0) {
return `${placeholder} (${value.length})`;
}
return placeholder;
} else {
// We might need to fetch the selected player's name if it's not in the current list
// For now, let's just show the placeholder or "Selected"
// Ideally we would have a way to resolve the name from the UUID if it's not in `players`
// But `players` only contains search results.
// If we want to show the name, we might need to fetch it or pass it in.
// Given the context of AuditLog, it shows "Spieler Filter (count)".
// Given RefereesList, it's a button "Hinzufügen".
return placeholder;
}
});
</script>
<Popover bind:open>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" class={cn("justify-between", Array.isArray(value) && !value?.length && "text-muted-foreground")} {...props} role="combobox" aria-expanded={open}>
{triggerLabel}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command shouldFilter={false}>
<CommandInput bind:value={search} placeholder="Search players..." />
<CommandList>
<CommandEmpty>No players found.</CommandEmpty>
<CommandGroup>
{#each players as player (player.uuid)}
<CommandItem value={player.id?.toString()} onSelect={() => handleSelect(player)}>
<Check class={cn("mr-2 size-4", isSelected(player.id!) ? "opacity-100" : "opacity-0")} />
{player.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>

View File

@@ -24,15 +24,14 @@
import { ScrollArea } from "$lib/components/ui/scroll-area";
import { CalendarIcon } from "lucide-svelte";
import { cn } from "@components/utils";
import { fromDate, type ZonedDateTime } from "@internationalized/date";
import Input from "../input/input.svelte";
import type {ZonedDateTime} from "@internationalized/date";
let {
value = $bindable(),
onChange,
onChange
}: {
value: ZonedDateTime;
onChange?: ((date: ZonedDateTime | undefined) => void) | undefined;
value: ZonedDateTime
onChange?: ((date: ZonedDateTime | undefined) => void) | undefined
} = $props();
let isOpen = $state(false);
@@ -64,7 +63,13 @@
<Popover bind:open={isOpen}>
<PopoverTrigger>
<Button variant="outline" class={cn("w-full justify-start text-left font-normal", !value && "text-muted-foreground")}>
<Button
variant="outline"
class={cn(
"w-full justify-start text-left font-normal",
!value && "text-muted-foreground"
)}
>
<CalendarIcon class="mr-2 h-4 w-4" />
{#if value}
{new Intl.DateTimeFormat("de-DE", {
@@ -81,14 +86,23 @@
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Input type="datetime-local" value={value.toString().slice(0, 16)} onchange={(e) => handleDateSelect(fromDate(e.target.valueAsDate, "Europe/Berlin"))} />
<div class="sm:flex">
<Calendar mode="single" bind:value onValueChange={(date) => handleDateSelect(date)} initialFocus />
<Calendar
mode="single"
bind:value
onValueChange={(date) => handleDateSelect(date)}
initialFocus
/>
<div class="flex flex-col sm:flex-row sm:h-[300px] divide-y sm:divide-y-0 sm:divide-x">
<ScrollArea class="w-64 sm:w-auto">
<div class="flex sm:flex-col p-2">
{#each [...hours].reverse() as hour}
<Button size="icon" variant={value && value.hour === hour ? "default" : "ghost"} class="sm:w-full shrink-0 aspect-square" onclick={() => handleTimeChange("hour", hour)}>
<Button
size="icon"
variant={value && value.hour === hour ? "default" : "ghost"}
class="sm:w-full shrink-0 aspect-square"
onclick={() => handleTimeChange("hour", hour)}
>
{hour}
</Button>
{/each}
@@ -99,12 +113,12 @@
<div class="flex sm:flex-col p-2">
{#each Array.from({ length: 60 }, (_, i) => i) as minute}
<Button
size="icon"
variant={value && value.minute === minute ? "default" : "ghost"}
class="sm:w-full shrink-0 aspect-square"
onclick={() => handleTimeChange("minute", minute)}
size="icon"
variant={value && value.minute === minute ? "default" : "ghost"}
class="sm:w-full shrink-0 aspect-square"
onclick={() => handleTimeChange("minute", minute)}
>
{minute.toString().padStart(2, "0")}
{minute.toString().padStart(2, '0')}
</Button>
{/each}
</div>

View File

@@ -1,6 +1,29 @@
import Root from "./input.svelte";
export {
Root,
//
Root as Input,
export type FormInputEvent<T extends Event = Event> = T & {
currentTarget: EventTarget & HTMLInputElement;
};
export type InputEvents = {
blur: FormInputEvent<FocusEvent>;
change: FormInputEvent<Event>;
click: FormInputEvent<MouseEvent>;
focus: FormInputEvent<FocusEvent>;
focusin: FormInputEvent<FocusEvent>;
focusout: FormInputEvent<FocusEvent>;
keydown: FormInputEvent<KeyboardEvent>;
keypress: FormInputEvent<KeyboardEvent>;
keyup: FormInputEvent<KeyboardEvent>;
mouseover: FormInputEvent<MouseEvent>;
mouseenter: FormInputEvent<MouseEvent>;
mouseleave: FormInputEvent<MouseEvent>;
mousemove: FormInputEvent<MouseEvent>;
paste: FormInputEvent<ClipboardEvent>;
input: FormInputEvent<InputEvent>;
wheel: FormInputEvent<WheelEvent>;
};
export {
Root,
//
Root as Input,
};

View File

@@ -1,39 +1,42 @@
<script lang="ts">
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
import { cn } from "@components/utils";
import { type WithElementRef } from "bits-ui";
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
type Props = WithElementRef<Omit<HTMLInputAttributes, "type"> & ({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })>;
let { ref = $bindable(null), value = $bindable(), type, files = $bindable(), class: className, ...restProps }: Props = $props();
import type { HTMLInputAttributes } from "svelte/elements";
import type { InputEvents } from "./index.js";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLInputAttributes;
type $$Events = InputEvents;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"] = undefined;
export { className as class };
// Workaround for https://github.com/sveltejs/svelte/issues/9305
// Fixed in Svelte 5, but not backported to 4.x.
export let readonly: $$Props["readonly"] = undefined;
</script>
{#if type === "file"}
<input
bind:this={ref}
data-slot="input"
class={cn(
"selection:bg-primary dark:bg-input/30 selection:text-primary-foreground border-input ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 pt-1.5 text-sm font-medium outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
type="file"
bind:files
bind:value
{...restProps}
/>
{:else}
<input
bind:this={ref}
data-slot="input"
class={cn(
"border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{type}
bind:value
{...restProps}
/>
{/if}
<input
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
bind:value
{readonly}
on:blur
on:change
on:click
on:focus
on:focusin
on:focusout
on:keydown
on:keypress
on:keyup
on:mouseover
on:mouseenter
on:mouseleave
on:mousemove
on:paste
on:input
on:wheel|passive
{...$$restProps}
/>

View File

@@ -1,45 +0,0 @@
---
title: WarShip Halloween Event 2025
key: 2025-halloween
description: Das WarShip Halloween Event 2025 für die Community
created: 2025-10-27T00:00:00.000Z
tags:
- event
- warship
---
Ahoi Community,
das diesjährige Halloween-Event nähert sich, die Tage werden langsam kürzer und die Nächte länger. Es geht auf dem Herbst zu und erinnert daran, dass das Jahr wieder halb vorbei ist. Dieses Mal im Spielmodus Warship. Das im Format 6 gegen 6 ausgetragen wird. Neben dem eigentlichen Turnier wird das Außendesign bewertet. Die Bewertung des Außendedigns wird zu 70% Das SW Builderteam übernehmen und 30% die Userbewertung. Die prozentuale Bewertung soll dazu dienen, dass große Teams Ihr eigenes Design nicht hoch puschen können.
Das Event findet am 08.11.2025 in der Version 1.21 mit dem aktuellen Regelwerk statt.
~~Anmelde + Einsendeschluss 03.11.2025~~
**Neue Fristen**:
Einsendeschluss: 06.11.2025 23:59 Uhr
Hotfixschluss: 07.11.2025 23:59 Uhr
Der Anmeldeschluss bleibt der 03.11.2025
zusätzlich wird es mit einem Designcontest begleitet.
Design Regel: Halloween
Arena: Lucifus
Design Bewertung
- Userbewertung (30%) wird über den Discord Community Server von SW organisiert. (Bilder vom Außendesign werden gepostet und per Abstimmung ausgelost)
- Builderbewertung (70%) läuft nach folgende Kriterien ab.
- Form des WS
- Farbgestaltung
- Muster
- Thematisierung: Thema Halloween / Grusel
Es wird also 3 Sieges- Plätze geben welch wie Folgt ermittelt wird.
- Gesamtsieger: Höchste Fight Platzierung und Design Platzierung im Durchschnitt
- Event- Sieger : Höchste Fight Platzierung
- Designsieger: Bestes Design
Das Warshipdesign vom Gesamtsieger wird bis zum nächsten Halloween in der Lobby ausgestellt. Wir freuen uns auf zahlreiche Anmeldungen und sind gespannt, welche Designs uns erwarten!
Das Serverteam

View File

@@ -1,39 +0,0 @@
---
title: SpaceCraft Eventplan
description: Der Kampfplan für das SpaceCraft Event am 29.6.
key: SC-Eventplan
created: 2025-06-28T00:00:00.000Z
tags:
- SpaceCraft
- Event
image: ../../../images/SpaceCraftWinners3.png
---
### Infos:
Eventleitung: TheBreadBeard
Sollten fights zu schnell vorbei sein, ist eine vorverschiebung der folgenden fights möglich. In diesem Fall wird eine Pause von 10 Minuten garantiert.
## Gruppen
<group-table data-event="72" data-group="2"> </group-table>
<group-table data-event="72" data-group="3"> </group-table>
## Tabelle
![](https://cdn.discordapp.com/attachments/690535200865910794/1388779302782501014/SPaceship_Eventplan.png?ex=68623904&is=6860e784&hm=5f32d5f34449aee7e2b58b9c16d657f0d3b08e2cd5b341a7b31559a1b1b30f27&)
## Fights
### Gruppenphase
**Gruppe 1**
<fight-table data-event="72" data-group="2"> </group-table>
**Gruppe 2**
<fight-table data-event="72" data-group="3"> </group-table>
### KO-Phase
<fight-table data-event="72" data-group="4"> </group-table>

View File

@@ -1,25 +0,0 @@
---
title: MiniWarGearLiga Ankündigung
description: Die MiniWargear-Liga 2025 findet am 27./28. September statt.
key: mwgl
created: 2025-07-28T00:00:00.000Z
tags:
- event
- miniwargear
image: ../../../images/generated-image(8).png
---
**Ahoi Liebe Community,**
Es ist wieder so weit die MiniWargear-Liga findet auch dieses Jahr wieder statt.
Infos zum Event:
* Die MWGL findet am Wochenende des **27./28.09.2025** um **16 Uhr** statt.
* Einsendeschluss ist der **22.09.2025** um 23:59 Uhr bis dahin muss ein MWG gemäß Regelwerk vorhanden sein.
* Hotfixes (geringe Änderungen wie z.B. das Fixen von Läufen usw.) dürfen bis zum **25.09.2025** um 23:59 Uhr nachgereicht werden.
* Max. **4 Kämpfer** pro Team
* Normales SW-MWG-Regelwerk mit automatischen Kanonen
* Es wird einen eigenen Schem-Typ geben.
* Der Schem-Name muss mit dem Teamkürzel enden.
* Gefightet wird mit getrenntem Spectate- und Fightserver (wie bei der WGS)

View File

@@ -1,98 +0,0 @@
---
title: MiniWarGearLiga Eventplan
description: Der Eventplan für die MiniWargear-Liga 2025
key: mwgl
created: 2025-09-27T00:00:00.000Z
tags:
- event
- miniwargear
image: ../../../images/generated-image(8).png
---
## Spieltag 27.09.2025
### Gruppenphase 1
##### Gruppen
| Gruppe 1 | Gruppe 2 | Gruppe 3 |
|----------|----------|----------|
| BF | Borg | MLT! |
| KT | EXO | Salo |
| ED | PL | FK |
In nachfolgenden Tabellen wird `W` für `Winner`/`Gewinner` stehen und `L` für `Loser`/`Verlierer`.
| 27.09.2025 | Arena 1 | Ergebnis | Arena 2 | Ergebnis | Arena 3 | Ergebnis |
|------------|----------|:--------:|-------------|:--------:|--------------|:--------:|
| 16:00 | BF vs KT | BF | Borg vs EXO | Borg | MLT! vs Salo | MLT! |
| 16:30 | BF vs ED | ED | Borg vs PL | Borg | MLT! vs FK | FK |
| 17:00 | KT vs BF | BF | EXO vs PL | PL | Salo vs MLT! | Salo |
| 17:30 | ED vs BF | BF | Borg vs PL | Borg | FK vs Salo | Salo |
### Gruppenphase 2
##### Gruppen
| Gruppe 1 | Gruppe 2 | Gruppe 3 |
|----------|----------|----------|
| BF | ED | MLT! |
| Borg | PL | EXO |
| Salo | FK | KT |
In nachfolgender Tabelle steht `Gr.` für `Gruppe` und das Wort `Platz` wird weggelassen.
| 27.09.2025 | Arena 1 | Ergebnis | Arena 2 | Ergebnis | Arena 3 | Ergebnis |
|------------|--------------|:--------:|----------|:--------:|-------------|:--------:|
| 18:00 | BF vs Borg | Borg | ED vs PL | PL | EXO vs KT | EXO |
| 18:30 | Borg vs Salo | Borg | PL vs FK | FK | EXO vs MLT! | MLT! |
| 19:00 | BF vs Salo | Salo | ED vs PL | PL | KT vs EXO | EXO |
| 19:30 | Borg vs Salo | Salo | FK vs PL | FK | MLT! vs EXO | EXO |
KT wird disqualifiziert und tritt morgen nicht mehr an. Das Team belegt somit den 9. und damit letzten Platz.
## Spieltag 28.09.2025
### Leiter
Die fights werden auf 5 Minuten an den vorherigen vorgezogen.
| 28.09.2025 | | Ergebnis |
|------------|-------------|:--------:|
| 16:00 | MLT! vs EXO | EXO |
| 16:15 | EXO vs ED | ED |
| 16:25 | ED vs PL | PL |
| 16:40 | PL vs FK | FK |
| 16:55 | FK vs BF | FK |
### Spiel um Platz 3
| 28.09.2025 | | Ergebnis |
|------------|------------|:--------:|
| 17:15 | FK vs Borg | Borg |
| 17:25 | FK vs Borg | Borg |
| Entfällt | FK vs Borg | / |
### Spiel um Platz 2 und 1
| 28.09.2025 | | Ergebnis |
|------------|--------------|:--------:|
| 17:45 | Borg vs Salo | Salo |
| 18:00 | Borg vs Salo | Borg |
| 18:10 | Borg vs Salo | Borg |
| 18:20 | Borg vs Salo | Borg |
| entfällt | Borg vs Salo | / |
## Endplatzierung
| Platz | Team |
|-------|------|
| 1. | Borg |
| 2. | Salo |
| 3. | FK |
| 4. | BF |
| 5. | PL |
| 6. | ED |
| 7. | EXO |
| 8. | MLT! |
| 9. | KT |

View File

@@ -17,7 +17,7 @@ Die neue Physik nutzt ihr mit ***/nogravity*** auf dem Bau
### Alle Infos zum Event:
- Die Technik und das Design der Publics werden nochmal seperat Public gestellt. So könnt ihr, wenn ihr keine oder weniger eigene Technik bauen wollt, einfacher die Public technik verwenden.
- Das Event findet am 29.06.2025 um 16 Uhr statt
- Das Event findet am 19.06.2025 um 16 Uhr statt
- Es gibt kein Entern
- Kits können wie bei WG und MWG selbst erstellt werden
- es wird Redstone TKO verwendet
@@ -37,13 +37,12 @@ Die neue Physik nutzt ihr mit ***/nogravity*** auf dem Bau
- Wasser darf in Kanonen verwendet werden
- Dadurch könnt ihr TNT einfacher komprimieren. Wasser muss nicht mehr den Zweck erfüllen, die Kanone vor eigenem Schaden zu bewahren
- 32 Projektile pro Seite
- Unterpunkt: Crossbows
- Es gibt keinen Techhider
# Regelwerk für das Event:
## Definitionen
Ein AirShip ist eine beidseitig bewaffnete Struktur in Minecraft 1.20 und sieht flugfähig aus.
Ein AirShip ist eine beidseitig bewaffnete Struktur in Minecraft 1.19 und sieht flugfähig aus.
## Maße
@@ -58,41 +57,16 @@ Ein AirShip ist eine beidseitig bewaffnete Struktur in Minecraft 1.20 und sieht
- 51 breit
- 32 hoch
Technik, welche für die Funktion der Kanonen nicht relevant ist, darf sich in maßen auch im Design-Bereich befinden.
Entfernung von Mitte zu Mitte des Gegners: 100 Block
Es dürfen keine Blöcke über die Grenze des Designbereiches bewegt werden
Panzerung darf nur im Technikbereich verbaut werden.
## Projektile
Ein Projektil ist ein gezündetes TNT, welches in die gegnerische Hälfte wechselt. Eine Kanone ist eine Vorrichtung zum Beschleunigen von Projektilen. Es ist verboten, mehrere Kanonen als eine Einzige auszugeben.
Jeder Seite stehen bis zu 32 Projektile zur Verfügung. Sie müssen auf der dem Gegner zugewandten Seite oder, wenn beidseitig, in der Mitte gezündet werden
## Crossbows
Kanonen, welche, ohne nachgeladen werden zu müssen, mehrere Schüsse abgeben können, Müssen:
- Jeden Schuss das TNT an den selben Punkten zünden.
- Für jedes Projektil das die Kanone für einen Schuss maximal zünden könnte, mindestens 1,5 Redstone Tick zwischen den Schüssen warten.
##### Beispiele
| Projektile | Ticks |
|-------|------------|
| 1 | 1,5 |
| 2 | 3 |
| 3 | 4,5 |
| 4 | 6 |
| ... | ... |
| 8 | 12 |
| 12 | 18 |
| 16 | 24 |
| ... | ... |
| 32 | 48 |
## Siegesbedingung
- Ein AirShip wird zu 60% (nach prozentualer Redstonekomponenten) zerstört.
@@ -102,63 +76,10 @@ Kanonen, welche, ohne nachgeladen werden zu müssen, mehrere Schüsse abgeben k
## Blöcke
Es dürfen maximal je 32 TNT und 32 Werfer verbaut werden. Verbaute Blöcke dürfen einen TNT-Widerstand von maximal 6 haben. Ausgenommen davon ist Wasser, welches ausschließlich in Kanonen verbaut werden darf.
Es dürfen maximal je 30 TNT und 32 Werfer verbaut werden. Verbaute Blöcke dürfen einen TNT-Widerstand von maximal 6 haben. Ausgenommen davon ist Wasser, welches ausschließlich in Kanonen verbaut werden darf.
Inventar-Blöcke dürfen nur mit Blumen, Honigflaschen und Pferderüstungen gefüllt sein. Zusätzlich dürfen sich in Kisten und Fässern TNT sowie in Werfern 2·64 Feuerbälle, 2·64 reguläre Pfeile oder 1 Eimer zum Aufheben von powdered Snow befinden.
Zusätzlich verboten: Monster Spawner, Eis, Nether Portal, Silberfischsteine, Leuchtfeuer, unsichtbare Blöcke (Ausnahme: structure_void) Das Duplizieren von Blöcken und Entities ist verboten.
Blöcke aus dem eigenen AS dürfen nicht in großer Menge entfernt werden.
Bug-Using ist nicht erwünscht.
# Versteckte Blöcke (Ersetzt durch Stein)
- WATER
- NOTE_BLOCK
- POWERED_RAIL
- DETECTOR_RAIL
- PISTON
- PISTON_HEAD
- STICKY_PISTON
- TNT
- CHEST
- TRAPPED_CHEST
- REDSTONE_WIRE
- STONE_PRESSURE_PLATE
- IRON_DOOR
- OAK_PRESSURE_PLATE
- SPRUCE_PRESSURE_PLATE
- BIRCH_PRESSURE_PLATE
- JUNGLE_PRESSURE_PLATE
- ACACIA_PRESSURE_PLATE
- DARK_OAK_PRESSURE_PLATE
- REDSTONE_TORCH
- REDSTONE_WALL_TORCH
- REPEATER
- BREWING_STAND
- TRIPWIRE_HOOK
- TRIPWIRE
- HEAVY_WEIGHTED_PRESSURE_PLATE
- LIGHT_WEIGHTED_PRESSURE_PLATE
- COMPARATOR
- REDSTONE_BLOCK
- HOPPER
- ACTIVATOR_RAIL
- DROPPER
- SLIME_BLOCK
- OBSERVER
- HONEY_BLOCK
- LEVER
# Zusatzinhalte folgender Blöcke (z.B. Text, Inhalt von Inventaren) werden mit versteckt:
- SIGN
- DISPENSER
- CHEST
- TRAPPED_CHEST
- FURNACE
- BREWING_STAND
- HOPPER
- DROPPER
- SHULKER_BOX
- JUKEBOX
- COMPARATOR
Bug-Using ist nicht erwünscht.

View File

@@ -2,31 +2,29 @@
title: SteamWar Arcade Eventplan
key: sw-arcade-eventplan
description: Der Eventplan für SteamWar Arcade
created: 2025-05-14T00:00:00.000Z
created: 2025-05-14
tags:
- event
- towerrun
- misslewars
- tntleague
- arcade
- event
- towerrun
- misslewars
- tntleague
- arcade
image: ../../../images/sw-arcade-image.png
---
### Infos:
Eventleitung: Chaoscaot
Eventleitung: Chaoscaot
Fights werden nach Möglichkeit mit einer Pause von 10 Minuten vorverschoben.
# Kämpfe
## Punkte aus der Gruppenphase
<group-table data-event="70"> </group-table>
### Fights
<fight-table data-event="70" data-group="1"> </fight-table>
<fight-table data-event="70" data-group="Gruppe 1"> </fight-table>
## Auswahl des Spielmodis

View File

@@ -1,43 +0,0 @@
---
title: WarGear Event
description: "Das erste WarGear Event in der 1.21 auf SteamWar!"
key: steamwar-wg-event-21
created: 2025-08-12
tags:
- event
- wargear
image: ../../../images/generated-image(11).png
---
**Ahoi, liebe Community,**
lange ist es her seit dem letzten WarGear-Event. Nun ist es so weit: Am **29. und 30. November** findet ein neues WarGear-Event **mit** SFAs statt.
## Übersicht
- **Datum:** 29.11.: Gruppenphase, 30.11.: KO-Phase
- **Spielmodus:** Standard **und** Pro WarGear
- **Teamgröße**: 6
- **Anmeldeschluss:** 22. November
- **Einsendeschluss:** 24. November
- **Hotfix-Schluss:** 27. November
Bei der SFA muss sich an eines der Regelwerke gehalten werden. Standard- und Pro-WarGear treten gleichwertig gegeneinander an.
## Sonderregeln
**Version:** 1.21.6 (aktuellste Bau-Version)
Es wird einen eigenen Schematic-Typen geben.
### Windcharges
Werden beim Überfliegen der Mittellinie entfernt.
### Cobwebs & Powder Snow
Dürfen uneingeschränkt benutzt werden, jedoch nicht als Panzerung. Die Bewertung liegt im Ermessen des Prüfers.
**Verantwortlicher:** Chaoscaot
**Frohes Bauen!**

View File

@@ -17,10 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { defineCollection, reference, z } from "astro:content";
import { docsLoader } from "@astrojs/starlight/loaders";
import { docsSchema } from "@astrojs/starlight/schema";
import { EventViewConfigSchema } from "@components/event/types";
import {defineCollection, reference, z} from "astro:content";
export const pagesSchema = z.object({
title: z.string().min(1).max(80),
@@ -58,7 +55,8 @@ export const downloads = defineCollection({
schema: z.object({
name: z.string(),
description: z.string(),
url: z.string().url().or(z.record(z.string(), z.string())),
url: z.string().url()
.or(z.record(z.string(), z.string())),
sourceUrl: z.string().url().optional(),
}),
});
@@ -73,64 +71,45 @@ export const rules = defineCollection({
export const announcements = defineCollection({
type: "content",
schema: ({ image }) =>
z.object({
title: z.string(),
description: z.string(),
author: z.string().optional(),
image: image().optional(),
tags: z.array(z.string()),
created: z.date(),
key: z.string(),
}),
schema: ({image}) => z.object({
title: z.string(),
description: z.string(),
author: z.string().optional(),
image: image().optional(),
tags: z.array(z.string()),
created: z.date(),
key: z.string(),
}),
});
export const publics = defineCollection({
type: "data",
schema: ({ image }) =>
z.object({
name: z.string(),
description: z.string(),
id: z.number().positive(),
creator: z.string().array().optional(),
showcase: z.string().url().optional(),
camera: z
.object({
fov: z.number().optional(),
near: z.number().optional(),
far: z.number().optional(),
distance: z.number().optional(),
})
.optional(),
image: image(),
alt: image().optional(),
xray: image().optional(),
gamemode: reference("modes"),
"3d": z.boolean().optional().default(true),
}),
});
export const events = defineCollection({
type: "content",
schema: ({ image }) =>
z.object({
eventId: z.number().positive(),
image: image().optional(),
mode: reference("modes").optional(),
hideTeamSize: z.boolean().optional().default(false),
verwantwortlich: z.string().optional(),
viewConfig: EventViewConfigSchema.optional(),
}),
schema: ({image}) => z.object({
"name": z.string(),
"description": z.string(),
"id": z.number().positive(),
"creator": z.string().array().optional(),
"showcase": z.string().url().optional(),
"camera": z.object({
"fov": z.number().optional(),
"near": z.number().optional(),
"far": z.number().optional(),
"distance": z.number().optional(),
}).optional(),
"image": image(),
"alt": image().optional(),
"xray": image().optional(),
"gamemode": reference("modes"),
"3d": z.boolean().optional().default(true),
}),
});
export const collections = {
pages: pages,
help: help,
modes: modes,
rules: rules,
downloads: downloads,
announcements: announcements,
publics: publics,
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
events: events,
"pages": pages,
"help": help,
"modes": modes,
"rules": rules,
"downloads": downloads,
"announcements": announcements,
"publics": publics,
};

View File

@@ -1,5 +0,0 @@
---
title: Überblick
---
WIP

View File

@@ -1,5 +0,0 @@
---
title: Überblick
---
WIP

View File

@@ -1,5 +0,0 @@
---
title: Script Überblick
---
WIP

View File

@@ -1,5 +0,0 @@
---
title: Überblick
---
WIP

View File

@@ -1,29 +0,0 @@
---
title: Startseite
desciption: Startseite der SteamWar Dokumentation
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
Herzlich Willkommen in der SteamWar Dokumentation!
## SteamWar beitreten
SteamWar ist ein Minecraft Java Server.
<Tabs>
<TabItem label="Java Edition">
- IP: `steamwar.de`
- Empfohlene Version: `1.21.6`
</TabItem>
<TabItem label="Bedrock Edition">
- IP: `steamwar.de`
- Port: `19132`
- Version: `Aktuellste`
</TabItem>
</Tabs>
## Open-Source
Die SteamWar Software ist Open-Source und auf der internen [Gitea](https://git.steamwar.de) verfügbar. Jeglicher Code ist unter der [GNU Affero General Public License v3.0](https://www.gnu.org/licenses/agpl-3.0.en.html) lizenziert. Das bedeutet, dass du den Code frei nutzen, modifizieren und verteilen kannst, solange du den Code ebenfalls unter der AGPL lizenziert verfügbar machst.
Bugs und Feature Requests können im [Gitea Issue Tracker](https://git.steamwar.de/SteamWar/SteamWar/issues) erstellt werden. Contributions sind immer willkommen! Bitte erstelle einen Pull Request im [SteamWar Repository](https://git.steamwar.de/SteamWar/SteamWar).

View File

@@ -1,5 +0,0 @@
---
title: Überblick
---
WIP

View File

@@ -1,5 +0,0 @@
---
title: Überblick
---
WIP

View File

@@ -2,9 +2,7 @@
"name": "AdvancedScripts",
"description": "Ein Fabric-Mod, der für den BauServer von SteamWar Hotkeys für das ScriptSystem hinzufügt. Hierzu werden die einzelnen Zeichen an den Server gesendet und vom Server verarbeitet.",
"url": {
"1.21.6": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.2.3/AdvancedScripts-2.2.3.jar",
"1.21.4": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.2.0/AdvancedScripts-2.2.0.jar",
"1.21.3": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.2.1/AdvancedScripts-2.2.1.jar",
"1.20.6": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.1.0/AdvancedScripts-2.1.0.jar",
"1.19.3": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.0.0/AdvancedScripts-2.0.0.jar",
"Hotkey script": "https://git.steamwar.de/SteamWar/SteamWar/src/branch/main/BauSystem/hotkeys.lua"

View File

@@ -1,54 +0,0 @@
---
eventId: 74
mode: "warship"
verwantwortlicher: "JajaKings"
viewConfig:
groups:
name: "Gruppenphase"
view:
type: "GROUP"
groups: [9, 10]
final:
name: "Finalphase"
view:
type: "DOUBLE_ELEMINATION"
winnersFinalFight: 1594
losersFinalFight: 1590
grandFinalFight: 1595
---
Ahoi Community,
das diesjährige Halloween-Event nähert sich, die Tage werden langsam kürzer und die Nächte länger. Es geht auf dem Herbst zu und erinnert daran, dass das Jahr wieder halb vorbei ist. Dieses Mal im Spielmodus Warship. Das im Format 6 gegen 6 ausgetragen wird. Neben dem eigentlichen Turnier wird das Außendesign bewertet. Die Bewertung des Außendedigns wird zu 70% Das SW Builderteam übernehmen und 30% die Userbewertung. Die prozentuale Bewertung soll dazu dienen, dass große Teams Ihr eigenes Design nicht hoch puschen können.
Das Event findet am 08.11.2025 in der Version 1.21 mit dem aktuellen Regelwerk statt.
~~Anmelde + Einsendeschluss 03.11.2025~~
**Neue Fristen**:
Einsendeschluss: 06.11.2025 23:59 Uhr
Hotfixschluss: 07.11.2025 23:59 Uhr
Der Anmeldeschluss bleibt der 03.11.2025
zusätzlich wird es mit einem Designcontest begleitet.
Design Regel: Halloween
Arena: Lucifus
Design Bewertung
- Userbewertung (30%) wird über den Discord Community Server von SW organisiert. (Bilder vom Außendesign werden gepostet und per Abstimmung ausgelost)
- Builderbewertung (70%) läuft nach folgende Kriterien ab.
- Form des WS
- Farbgestaltung
- Muster
- Thematisierung: Thema Halloween / Grusel
Es wird also 3 Sieges- Plätze geben welch wie Folgt ermittelt wird.
- Gesamtsieger: Höchste Fight Platzierung und Design Platzierung im Durchschnitt
- Event- Sieger : Höchste Fight Platzierung
- Designsieger: Bestes Design
Das Warshipdesign vom Gesamtsieger wird bis zum nächsten Halloween in der Lobby ausgestellt. Wir freuen uns auf zahlreiche Anmeldungen und sind gespannt, welche Designs uns erwarten!
Das Serverteam

View File

@@ -1,47 +0,0 @@
---
eventId: 76
mode: microwargear
verwantwortlicher: SteamWar
---
**Ahoi, liebe Community,**
es ist wieder Zeit, das Jahr neigt sich dem Ende und damit ist es wieder Zeit für das Neujahrsevent!
## Übersicht
- **Datum:** 01.01.2026 Start gegen 15 Uhr
- **Spielmodus:** MicroWarGear (Eigener Schematic Typ)
- **Teamgröße**: 3 Personen
- **Anmeldeschluss:** 28. Dezember 2025 (23:59 Uhr)
- **Einsendeschluss:** 28. Dezember 2025 (23:59 Uhr)
- **Hotfix-Schluss:** 31. Dezember
## Sonderregeln
- Maße: **13x13x13**
- Freiluftbrücken erlaubt
- Version 1.21
- Jedes Team darf nur eine schematic einsenden.
- Alle Eventschematics werden nach dem Event zu MiniWarGears
## Weitere Hinweise
- Techhider wird aktiv sein
- Kampfleiter darf zum Schuss auffordern
- Auto Tech KO wird deaktiviert
- Es wird ein eigenen Schemtypen geben
- Turniersystem: All vs All
**Eventleiter:** AdmiralSeekrank
**Sonst noch wichtiges zu wissen**
Alle Absprachungen werden **nur** mit dem Eventleiter getroffen. Absprachungen mit anderen Personen gelten nicht! Fragen bezüglich des Events werden vom Eventleiter bearbeitet.
Jedes Team wird ein konkreten Ansprechpartner für das Event stellen. In der Regel ist dies der jeweilige Teamleader. Sollte eine andere Person dies übernehmen, ist diese Person dem Eventleiter mitzuteilen! Die Ansprechperson hat die Aufgabe, sich für Rückfragen bereitzustellen, Organisatorische Anliegen mit dem Eventleiter zu klären und die Schematic einzusenden.
Schemnamen müssen mit dem Team Kürzel enden. Sollte eine andere Person als der Ansprechpartner die Schem einsenden, ist dies vom Ansprechpartner mit dem Eventleiter abzuklären.
Der Kampfplan wird drei Tage vor dem Event erstellt um Organisatorische Fehler zu beheben und Nachfragen zu beantworten. Am Eventtag wird der Kampfplan nicht mehr geändert.
Eine Formelle Abmeldung nach Anmeldeschluss ist nicht möglich. Sollte ein Team trotz Anmeldung nicht Teilnehmen aber dies (*Vor beginn des Events*) dem Eventleiter mitteilen, werden die betroffene Kämpfe automatisch als Verloren Gewertet bzw. bei frühzeitiger Abmeldung, wird der Kampfplan nachträglich geändert. (In diesem Fall, je früher eine Absage erfolgt, umso besser)
Sollte ein Team ohne jede Abmeldung einfach nicht antreten, werden alle Kämpfe automatisch als Verloren gewertet und zusätzlich wird das Team mit einer Halb- Jährigen Eventsperre versehen.
**Wir wünschen Euch viel Spaß beim Event und eine schöne Vorweihnachtliche Zeit!**

View File

@@ -1,50 +0,0 @@
---
eventId: 75
mode: "wargear"
verwantwortlicher: "Chaoscaot"
image: ../../images/generated-image(11).png
viewConfig:
groups:
name: Gruppenphase
view:
type: "GROUP"
groups: [11]
elim:
name: Finale
view:
type: "ELEMINATION"
finalFight: 1613
---
**Ahoi, liebe Community,**
lange ist es her seit dem letzten WarGear-Event. Nun ist es so weit: Am **29. und 30. November** findet ein neues WarGear-Event **mit** SFAs statt.
## Übersicht
- **Datum:** 29.11.: Gruppenphase, 30.11.: KO-Phase
- **Spielmodus:** Standard **und** Pro WarGear
- **Teamgröße**: 6
- **Anmeldeschluss:** 22. November
- **Einsendeschluss:** 24. November
- **Hotfix-Schluss:** 27. November
Bei der SFA muss sich an eines der Regelwerke gehalten werden. Standard- und Pro-WarGear treten gleichwertig gegeneinander an.
## Sonderregeln
**Version:** 1.21.6 (aktuellste Bau-Version)
Es wird einen eigenen Schematic-Typen geben.
### Windcharges
Werden beim Überfliegen der Mittellinie entfernt.
### Cobwebs & Powder Snow
Dürfen uneingeschränkt benutzt werden, jedoch nicht als Panzerung. Die Bewertung liegt im Ermessen des Prüfers.
**Verantwortlicher:** Chaoscaot
**Frohes Bauen!**

View File

@@ -1,5 +0,0 @@
{
"translationKey": "sc",
"main": false,
"ranked": false
}

View File

@@ -62,7 +62,7 @@ Manuelle Kanonen dürfen vor dem Kampfgeschehen kein TNT beinhalten. Diese werde
### Automatische Kanonen
Automatische Kanonen sind Kanonen, welche vor dem Kampfgeschehen TNT beinhalten und ohne nachgeladen zu werden mehrere Schüsse abgeben können. Zu beachten ist, dass die Projektile aller Schüsse immer von dem/den exakt gleichen Punkt-/en aus gezündet und abgeschossen werden müssen. Außerdem müssen alle Treibladungen am dem/den exakt gleichen Punkt-/en gezündet werden. Vor Fightbeginn dürfen automatische Kanonen vollständig leergeschossen werden. Automatische Kanonen müssen von der Kommandozentrale aus aktivierbar sein. Dies kann auch durch den Verbau des Autostarters innerhalb der Kommandozentrale erfolgen. Des weiteren muss eine Möglichkeit innerhalb der Kommandozentrale gegeben sein die automatische Kanone vollständig vor Fightbeginn leerschießen zu können.
Automatische Kanonen sind Kanonen, welche vor dem Kampfgeschehen TNT beinhalten und ohne nachgeladen zu werden mehrere Schüsse abgeben können. Zu beachten ist, dass die Projektile aller Schüsse immer von dem/den exakt gleichen Punkt-/en aus abgeschossen werden müssen. Vor Fightbeginn dürfen automatische Kanonen vollständig leergeschossen werden. Automatische Kanonen müssen von der Kommandozentrale aus aktivierbar sein. Dies kann auch durch den Verbau des Autostarters innerhalb der Kommandozentrale erfolgen. Des weiteren muss eine Möglichkeit innerhalb der Kommandozentrale gegeben sein die automatische Kanone vollständig vor Fightbeginn leerschießen zu können.
Das wiederverwenden des Abschusswinkels einer Automatischen Kanone zählt immer als zweite Kanone. Auch das Nachladen der Automatischen Kanone zählt als zweite Kanone.

View File

@@ -7,115 +7,75 @@ mode: warship
## Definitionen
### §1 WarShip
### WarShip
1. Ein WarShip ist eine bewaffnete, schwimmende Struktur mit der optischen Erscheinung eines Schiffes. Der Schwimmkörper muss dabei einen Großteil des WarShips ausmachen.
2. Ein WarShip kann optional ein Design aufweisen, das andere im Wasser schwimmende/befindliche Dinge oder Tiere repräsentiert, sofern das gewählte Design gänzlich implementiert wird.
3. Jedes WarShip muss beidseitig gleich bewaffnet sein.
Ein WarShip ist eine bewaffnete, schwimmende Struktur in Minecraft mit der optischen Erscheinung eines Schiffes. Der Schwimmkörper muss dabei einen Großteil des WarShips ausmachen. Ein WarShip kann optional ein Design aufweisen, das andere im Wasser schwimmende/befindliche Dinge oder Tiere repräsentiert, sofern das gewählte Design gänzlich implementiert wird. Jedes WarShip muss beidseitig gleich bewaffnet sein.
### §2 Projektil
### Projektil
1. Ein Projektil ist ein TNT, welches im gezündeten Zustand in die gegnerische Hälfte wechselt.
2. Projektile müssen auf der dem Gegner zugewandten Schiffsseite gezündet werden.
3. Die Zündung eines Projektils muss innerhalb des Technikbereiches stattfinden, über der Wasserlinie darf es auch innerhalb des Ausfahrbereichs gezündet werden.
Ein Projektil ist ein TNT, welches im gezündeten Zustand in die gegnerische Hälfte wechselt. Projektile müssen auf der dem Gegner zugewandten Schiffsseite gezündet werden. Unter Wasser gezündete Projektile müssen innerhalb des Technikbereiches, oberhalb der Wasserlinie gezündete Projektile innerhalb des Ausfahrbereichs gezündet werden.
### §3 Kanone
### Kanone
1. Eine Kanone ist eine Vorrichtung zum Beschleunigen von maximal 2 Projektilen.
2. Eine Kanone muss manuell beladen werden und darf maximal alle 2s schießen.
3. Kanonen dürfen nicht gezielt Projektile anderer Kanonen beeinflussen.
Eine Kanone ist eine Vorrichtung zum Beschleunigen von Projektilen. Eine Kanone darf maximal 2 Projektile verschießen. Eine Kanone muss manuell beladen werden. Eine Kanone darf maximal alle 2s schießen. Es dürfen maximal 32 Kanonen pro Seite verbaut werden. Kanonen sind der einzige Ort, an dem Wasser verbaut werden darf, wenn es keinen anderen Zweck hat, als diese vor selbst verursachten Schaden zu bewahren und TNT zu transportieren.
### §4 Raketen und Flugmaschinen
## Maße
1. Flugmaschinen sind automatisch bewegliche Blockkonstruktion, welche die Ausfahrmaße des WarShips verlassen.
2. Raketen sind mit TNT bestückte Flugmaschinen
3. Ein Raketenmagazin enthält mehrere Raketen und schickt sie auf nahezu gleicher Flugbahn zum Gegner.
4. Raketen und Flugmaschinen dürfen sich während des Fluges nicht in mehrere flugfähige Einheiten aufteilen.
- Länge: 230 Block
- Breite: 35 Block (+ 4 Block Design pro Seite)
- Höhe: 30 Block + 20 Block Design
- Tiefe: Bis zu 8 Block unter dem Meeresspiegel
### §5 Brücke
Bei jedem WarShip müssen sich mindestens 10% der absoluten Blöcke (45.000 Blöcke) über der Wasserlinie befinden.
1. Eine Brücke ist der optisch zentrale Kontroll- und Steuerbereich eines Schiffes.
2. Die Brücke muss folgende Anforderung erfüllen:
Im Designbereich dürfen sich keine kampfrelevanten (Kanonen, Schleim/Honigfahrzeuge, Schilde) Techniken befinden. Eine Durchpanzerung des Designbereiches ist nicht zulässig. Der Designbereich ist ausschließlich für einzelne überstehende Designobjekte wie beispielsweise Kanonenrohre, Segel, Banner oder Bullaugen da.
- Mindestens 50 begehbare Blöcke.
- Mindesthöhe von 2 Blöcken im gesamten Brückenraum
- Steuerung von mindestens zwei zum Gegner gerichteten Scheinwerfern
- Optische Brückeneinrichtung (z. B. Instrumente, Anzeigen)
## Blöcke
### §6 Bereiche
Es dürfen nur Blöcke mit einem TNT-Widerstand von maximal 6 verbaut werden. Vor und im Kampf dürfen sich in allen Blöcken mit Inventar nur Blumen, Honigflaschen und Pferderüstungen befinden, in Kisten und Fässer darf auch TNT sein. Es dürfen maximal 32 Werfer pro Seite verbaut werden. In Werfern dürfen sich 2×64 Feuerbälle oder 2×64 Pfeile (ohne Effekte) befinden.
1. Der Technikbereich ist der einzige Bereich, der im nicht ausgefahrenen Zustand, kampfrelevante Technik enthalten darf.
2. Der Designbereich ist ausschließlich für einzelne überstehende Designobjekte wie beispielsweise Kanonenrohre, Segel, Banner oder Bullaugen da.
3. Eine Durchpanzerung des Designbereiches ist nicht zulässig.
4. Ausfahrbereich wird in §8 Abs. 2 definiert.
Das Verbauen von unsichtbaren Mauern oder anderen unsichtbaren Blöcken mit Hitbox ist verboten.
Ebenso verboten sind: Monsterspawner, Eis, Netherportalblock, Alle Silberfischsteine.
## Parameter
Das Missbrauchen von unverschiebbaren Blöcken als Panzerung ist verboten.
Wasser darf nicht zum Schutz des eigenen WarShips missbraucht werden.
### §7 Ausstattung
## Design
1. Es dürfen maximal,
Größere Hohlräume im Rumpf zum Ausweichen feindlicher Schüsse sind nicht gestattet, auch nicht während des Kampfes. Jedes WarShip braucht eine Flagge.
- 6 Flugmaschinen, (Flugmaschinen die dazu dienen, TNT von einigen Raketen zu zünden oder andere eigene Flugmaschinen zu stoppen, werden nicht gezählt)
- 8 Raketen (separat von den Flugmaschinen gezählt), wobei ein Raketenmagazin als 2 Raketen zählt,
- 28 Kanonen,
- 32 Werfer,
pro Seite verbaut werden.
Ein WarShip benötigt einen fortbewegungsfähigen Rumpf mit entsprechendem Antrieb (z.B. Segel, Schiffsschrauben).
Der Rumpf muss mindestens einen Block tief unter Wasser sowie mindestens 5 Block über dem Meeresspiegel sein (dies gilt auch während des Kampfes relativ zur Wasseroberfläche). Der Rumpf wird ab Wasserienie gemessen und darf max. 16 Blöcke hoch sein.
2. Es dürften maximal 1000 der vorverbauten Slime-, Honig- & TNT-Blöcke den Ausfahrbereich, als Bestandteil von Flugmaschinen, verlassen.
3. Jedes WarShip benötigt eine Brücke.
4. Jedes WarShip braucht eine Flagge.
Jedes WarShip benötigt eine Brücke, welche die folgenden Kriterien erfüllt:
### §8 Maße
- Min. 50 begehbare Blöcke
- Min. 2 Block hoch im gesamten Brückenraum
- Ansteuerung von min. zwei zum Gegner gewandten Scheinwerfer
- Optische Brückeneinrichtung
1. Die Maße eines Schiffs dürfen folgende Werte nicht überschreiten:
## Anti-Lag-Regeln
- Länge: 175 Blöcke
- Breite: 31 Blöcke + 4 Blöcke Designbereich pro Seite
- Höhe über dem Meeresspiegel: 30 Blöcke + 20 Blöcke Designbereich
- Tiefe unter dem Meeresspiegel: 8 Blöcke
Clocks müssen sich mit Ende ihres Einsatzzweckes selbst abschalten.
2. Ein WarShip darf sich maximal 12 Blöcke vom Technikbereich ausfahren. Ausgenommen davon sind Flugmaschinen. Dieser Bereich wird als Ausfahrbereich deklariert.
3. Bei jedem WarShip müssen sich mindestens 10% der absoluten Blöcke (30.000 Blöcke) über der Wasserlinie befinden.
Sämtliche Redstonetechnik zum Schutz des eigenen WarShips muss ihre Aktivität vor dem Verteilen der Kits eingestellt haben.
### §9 Blöcke
## Raketen und Flugmaschinen
1. Es dürfen nur Blöcke mit einem TNT-Widerstand von maximal 6 verbaut werden.
2. Inventarblöcke dürfen nur Honigflaschen, Pferderüstungen und Blumen enthalten.
1. In Kisten und Fässern darf sich auch TNT befinden.
1. Werfer dürfen maximal 2 Stacks (2 × 64) Feuerbälle und Pfeile (ohne Effekte) enthalten. (Eimer innerhalb von Werfern der in Powder Snow schaut)
3. Es ist verboten, Monsterspawner, Eis, Netherportalblock, alle Silberfischsteine und unsichtbare Blöcke mit Hitboxen zu verbauen.
4. Der Missbrauch von nicht verschiebbaren Blöcken als Panzerung ist nicht gestattet.
5. Wasser darf nur in Kanonen zum Schutz vor Selbstbeschädigung oder um TNT zu transporieren verbaut werden.
1. Wasser, Spinnennetze oder Pulverschnee darf nicht zum Schutz des eigenen WarShips missbraucht werden.
Ein WarShip darf sich maximal 12 Block vom Technikbereich an weit ausfahren, davon ausgenommen sind Raketen und Flugmaschinen. Raketen und Flugmaschinen dürfen sich im Flug nicht in mehrere Schleim/Honigfahrzeuge aufteilen.
### §10 Design
Flugmaschinen sind Schleim/Honigfahrzeuge, welche die Ausfahrmaße des WarShips verlassen und kein TNT zum Gegner transportieren. Flugmaschinen, welche TNT von Raketen zünden oder Flugmaschinen und/oder Raketen stoppen werden bei der Bestimmung der Anzahl nicht gezählt. Es dürfen maximal 8 Flugmaschinen pro Seite verbaut werden.
1. Ein WarShip benötigt einen fortbewegungsfähigen Rumpf mit entsprechendem Antrieb (z.B. Segel, Schiffsschrauben).
2. Der Rumpf muss mindestens 1 Block unter Wasser und zwischen 5 und maximal 16 Blöcken über dem Meeresspiegel liegen (auch während des Kampfes, relativ zur Wasseroberfläche).
3. Größere Hohlräume im Rumpf zum Ausweichen feindlicher Schüsse sind nicht gestattet, auch nicht während des Kampfes.
Eine Rakete ist ein Schleim/Honigfahrzeug, das TNT zum Gegner transportiert. Es dürfen maximal 12 Raketen pro Seite verbaut werden. Ein Raketenmagazin ist in der Lage, mehrere Raketen auf der nahezu gleichen Flugbahn zum Gegner zu schicken. Ein Raketenmagazin wird wie 2 Raketen gewertet.
### §11 Anti-Lag-Regeln
Es dürften maximal 1000 der vorverbauten Slime- + Honig- + TNT-Blöcke das WarShip in Flugmaschinen und Raketen verlassen.
1. Clocks müssen sich mit Ende ihres Einsatzzweckes selbst abschalten.
2. Sämtliche Redstone Technik zum Schutz des eigenen WarShips muss ihre Aktivität vor dem Verteilen der Kits eingestellt haben.
## Kampfablauf
### §12 Kampfablauf
60 Sekunden vor Kampfbeginn können Flugmaschinen und Raketen das eigene WarShip verlassen. Mit Kampfbeginn dürfen Blöcke abgebaut und platziert werden; TNT-Schaden wird aktiviert. 10 Minuten nach Kampfbeginn wird das Entern des feindlichen WarShips erlaubt. Spieler mit einem Kit, welches TNT beladen kann und nicht dem Kapitän zugeordnet ist, dürfen erst 15 Minuten nach Kampfbeginn entern.
1. 60 Sekunden vor Kampfbeginn können Flugmaschinen und Raketen das eigene WarShip verlassen.
2. Mit Kampfbeginn dürfen Blöcke abgebaut und platziert werden. Der TNT-Schaden wird aktiviert.
3. 10 Minuten nach Kampfbeginn wird das Entern des feindlichen WarShips erlaubt. Spieler mit einem Kit, welches TNT beladen kann und nicht dem Kapitän zugeordnet ist, dürfen erst 15 Minuten nach Kampfbeginn entern.
4. Der Kampf endet,
Der Kampf endet, wenn:
- wenn er länger als 20 Minuten dauert.
- wenn der Kapitän eines Teams tot ist.
- wenn ein WarShip zu 7% beschädigt wurde.
### §13 Siegeskriterium
1. Folgende drei Szenarien führen zum Sieg:
- Das erfolgreiche eliminieren des gegnerischen Kapitäns.
- Eine Beschädigung des gegnerischen WarShips von mindestens 7%.
- Das nach Ablauf der Zeit das weniger beschädigte Schiff (prozentual).
2. Sollte keines dieser Szenarien eintreffen, endet der Kampf in einem Unentschieden.
- Der Kampf länger als 20 Minuten dauert
- Der Anführer eines Teams tot ist
- Ein WarShip zu 7% beschädigt wurde

View File

@@ -1,11 +0,0 @@
---
translationKey: megawg
---
# MegaWarGear Ruleset
For technical reasons MegaWarGear-Fights are held in version 1.12.2.
MegaWarGears provide the opportunity to build without limitations.
An elaborate design with defined shape is mandatory though (you may not just build some cube).
Besides the lack of limitations regarding dimensions and amounts, MegaWarGears are supposed to be similar to regular WarGears, in that endstone is the most resistant armoring block, dispensers should not contain TNT, etc.
Since this game-mode is not meant for serious competition, an approved MegaWarGear should be fine to release as a public.

View File

@@ -1,135 +0,0 @@
---
translationKey: microwg
---
# MicroWarGear Ruleset
MicroWargears are constructed in version 1.20.
## Dimensions
Max. 7 blocks deep
Max. 7 blocks wide
Max. 7 block high
A MicroWarGear may extend at most 7 blocks into every direction.
Shield related technology may be activated from any place within the MicroWarGear.
## Materials
Blocks used in MicroWarGear construction must not exceed a blast resistance of 9.
Inventory blocks must not contain items other than flowers, honey bottles and horse armor.
Chests, shulker boxes and barrels may contain TNT.
Dispensers may only individually contain either one stack of fire charges or one stack of basic arrows.
No more than 8 dispensers may be installed.
The following materials may not be used for construction: all saplings, minecraft:ice, nether portals, lava, waterlogged leaves and roots, TNT (pre-installed), any non-block entities.
Water may only be used within cannons and only to prevent damaging your gear.
## Cannons
Every MicroWarGear must have at least one functioning cannon.
A cannon is a continuous redstone contraption which is able to damage the opponent using primed TNT.
A TNT-cannon is the only place within a MicroWarGear where water may be placed and only if it does not leave the cannon or form water-shields.
Furthermore a cannon may not intentionally damage itself.
A cannon may at most shoot 8 projectiles at once.
Additionally, a single main-cannon can be installed, which may shoot up to 12 projectiles.
## Command Bridge
A MicroWarGear must contain a command bridge, either in the form of a clearly distinguishable room, open air command bridge or crawlspace command bridge.
The command bridge must be separated from the rest of the MicroWarGear by doors, fence gates, trapdoors or pistons.
A command bridge must adhere to the following conditions:
- At least 25 m² (1 block = 1 meter)
- A window through which the opponent is visible directly (not required for open air command bridges)
- Controls for at least 2 headlights that are visible from the opponent's position
- These controls must save their state until manually activated again
- The command bridge must be the only place where dispensers may be activated, which aim at the opponent
## Design
MicroWarGears must have a visual design.
The outermost layer of a MicroWarGear must have a blast resistance of at most 6.
It is expected that there is a continuous design structure across the entire front of the MicroWarGear.
At least 2 different kinds of blocks must be used in a design (not counting redstone components).
A "continuous design structure" means, that no substantial surface areas have too little or no depth to them.
Depth variation may also be achieved using walls or stairs.
## Bug-Using
The duplication of any blocks or entities is forbidden.
Excessive use of blocks that are replaced by the tech hider is also forbidden.
## Definitions
### Projectile
A projectile is any primed TNT entity, which leaves the extension limits of a MicroWarGear (7 blocks) towards the opponent.
### Propellant
A propellant is any primed TNT entity, which by exploding accellerates projectiles towards the opponent.
The propellant of any one cannon must only affect projectiles of that same cannon.
## Hidden Blocks (Replaced with Endstone)
- WATER
- NOTE_BLOCK
- POWERED_RAIL
- DETECTOR_RAIL
- PISTON
- PISTON_HEAD
- STICKY_PISTON
- TNT
- CHEST
- TRAPPED_CHEST
- REDSTONE_WIRE
- STONE_PRESSURE_PLATE
- IRON_DOOR
- OAK_PRESSURE_PLATE
- SPRUCE_PRESSURE_PLATE
- BIRCH_PRESSURE_PLATE
- JUNGLE_PRESSURE_PLATE
- ACACIA_PRESSURE_PLATE
- DARK_OAK_PRESSURE_PLATE
- REDSTONE_TORCH
- REDSTONE_WALL_TORCH
- REPEATER
- BREWING_STAND
- TRIPWIRE_HOOK
- TRIPWIRE
- HEAVY_WEIGHTED_PRESSURE_PLATE
- LIGHT_WEIGHTED_PRESSURE_PLATE
- COMPARATOR
- REDSTONE_BLOCK
- HOPPER
- ACTIVATOR_RAIL
- DROPPER
- SLIME_BLOCK
- OBSERVER
- HONEY_BLOCK
- LEVER
- SCULK_SENSOR
- POLISHED_BLACKSTONE_PRESSURE_PLATE
- MANGROVE_PRESSURE_PLATE
- CRIMSON_PRESSURE_PLATE
- WARPED_PRESSURE_PLATE
## The Contents of the Following Blocks Are Also Hidden
- SIGN
- DISPENSER
- CHEST
- TRAPPED_CHEST
- FURNACE
- BREWING_STAND
- HOPPER
- DROPPER
- SHULKER_BOX
- JUKEBOX
- COMPARATOR
<hr>
Whether or not a MicroWarGear is rules compliant is up to the examiners.

View File

@@ -1,158 +0,0 @@
---
translationKey: mwg
mode: MiniWarGear
---
# MiniWarGear-Ruleset
MiniWarGears are constructed in version 1.20.
## Dimensions
- Max. 20 blocks deep (+ 1 block for design on each side) (22)
- Max. 35 blocks wide (+ 1 block for design on each side) (37)
- Max. 26 blocks high
A MiniWarGear may extend at most 7 blocks into every direction.
All shield related technology may only be activated from within the command bridge.
## Materials
Blocks used for the construction of a MiniWarGear must not exceed a blast resistance of 9.
A maximum of 120 TNT may be pre-installed.
Inventory blocks must not contain items other than flowers, honey bottles or horse armor.
Dispensers may only individually contain either one stack of fire charges or one stack of basic arrows.
No more than 16 dispensers may be installed.
The following materials may not be used for construction: all saplings, minecraft:ice, nether portals, lava, waterlogged leaves and roots, TNT (pre-installed), any non-block entities.
Water may only be used within cannons and only to prevent damaging your gear.
## Cannons
A cannon is a continuous redstone contraption which is able to damage the opponent using primed TNT.
A TNT-cannon is the only place within a MiniWarGear where water may be placed and only if it does not leave the cannon or form water-shields.
Furthermore a cannon may not intentionally damage itself.
A cannon may at most shoot 8 projectiles at once.
Additionally, a single main-cannon can be installed, which may shoot up to 12 projectiles.
The main-cannon must be a manual cannon.
A MiniWarGear may be equipped with up to 9 cannons.
It is forbidden to try to pass off multiple cannons as a single one.
It is also forbidden to try to pass off a single cannon as multiple.
Whether or not this is the case is up to the examiners and fight-judges.
Manual cannons are TNT-cannons, which require manual loading.
They may not be pre-loaded at the time of construction.
Furthermore manual cannons may fire up to three individual times after being loaded once.
All projectils of a manual cannon which is able to fire multiple times like this must be launched from the exact same launch-point.
That launch-point is defined by the first shot of a salve the cannon performs.
Automatic cannons are TNT-cannons which can fire at least 5 individual times, without being manually loaded.
They must be pre-loaded at the time of construction.
To qualify for being allowed to be pre-loaded the cannon must fire at least 5 times.
All projectils of an automatic cannon must be launched from the exact same launch-point.
The first 5 shots of an automatic cannon must have the exact same number of projectiles.
After the 6th shot the amount of projectiles may decrease, and must not increase.
Between individual shots of an automatic cannon must be at least 4 seconds of delay (40 redstone ticks, 80 game ticks).
A MiniWarGear may be equipped with up to two automatic cannons.
## Brücke
A MiniWarGear must feature a command bridge in the form of a clearly distinguishable room.
The command bridge must be separated from the rest of the MiniWarGear by doors, fence gates, trapdoors or pistons.
A command bridge must adhere to the following conditions:
- At least 25 m² (1 block = 1 meter)
- A periodic, acoustic (note block / bell) and optical damage sensor
- A window through which the opponent is visible directly
- Controls for at least 4 headlights that are visible from the opposing position
- Controls for automatic cannons (if any are installed)
- Controls for shield technology (if there is any)
- The command bridge is the only place where dispensers which aim at the opponent may be controlled
## Design
MiniWarGears must (besides at least one cannon) feature a visual design.
The outermost layer of a MiniWarGear must only have a blast resistance of at most 6.
It is expected that there is a continuous design structure across the entire front of a MiniWarGear.
At least 2 different kinds of blocks must be used in a design (not counting redstone components).
A "continuous design structure" means, that no substantial surface areas have little to no depth.
Depth variation may also in part be achieved using walls or stairs, but this should not be overused.
Whether or not this is the case is up to the examiner.
## Bug-Using
The creation of TNT in a MiniWarGear is forbidden.
Excessive use of blocks which are hidden by the tech-hider is also forbidden.
## Definitions
### Projectile
A Projectile is any primed TNT entity, which is accelerated by propellant.
Furthermore a projectile is any primed TNT entity, which leaves the extension limits of a MiniWarGear (7 blocks) towards the opponent.
### Propellant
A propellant is any primed TNT entity, which by exploding accellerates projectiles towards the opposing half of the arena.
The propellant of any one cannon must only affect projectiles of that same cannon.
## Hidden Blocks (Replaced with Endstone)
- WATER
- NOTE_BLOCK
- POWERED_RAIL
- DETECTOR_RAIL
- PISTON
- PISTON_HEAD
- STICKY_PISTON
- TNT
- CHEST
- TRAPPED_CHEST
- REDSTONE_WIRE
- STONE_PRESSURE_PLATE
- IRON_DOOR
- OAK_PRESSURE_PLATE
- SPRUCE_PRESSURE_PLATE
- BIRCH_PRESSURE_PLATE
- JUNGLE_PRESSURE_PLATE
- ACACIA_PRESSURE_PLATE
- DARK_OAK_PRESSURE_PLATE
- REDSTONE_TORCH
- REDSTONE_WALL_TORCH
- REPEATER
- BREWING_STAND
- TRIPWIRE_HOOK
- TRIPWIRE
- HEAVY_WEIGHTED_PRESSURE_PLATE
- LIGHT_WEIGHTED_PRESSURE_PLATE
- COMPARATOR
- REDSTONE_BLOCK
- HOPPER
- ACTIVATOR_RAIL
- DROPPER
- SLIME_BLOCK
- OBSERVER
- HONEY_BLOCK
- LEVER
- SCULK_SENSOR
- POLISHED_BLACKSTONE_PRESSURE_PLATE
- MANGROVE_PRESSURE_PLATE
- CRIMSON_PRESSURE_PLATE
- WARPED_PRESSURE_PLATE
## The Contents of the Following Blocks Are Also Hidden:
- SIGN
- DISPENSER
- CHEST
- TRAPPED_CHEST
- FURNACE
- BREWING_STAND
- HOPPER
- DROPPER
- SHULKER_BOX
- JUKEBOX
- COMPARATOR

View File

@@ -1,31 +0,0 @@
---
translationKey: qg
---
# QuickGear-Ruleset
QuickGears are constructed in version 1.20.
## Dimensions
Max. 20 blocks deep (+ 1 block for design on each side) (22)
Max. 35 blocks wide (+ 1 block for design on each side) (37)
Max. 26 blocks high
No block may leave a QuickGear.
## Materials
All blocks in a QuickGear must by destructible by TNT explosions (except for water).
There must not be any pre-installed TNT blocks in a QuickGear.
Blocks with inventories may only contain flowers, honey bottles and horse armor.
Dispensers may only individually contain one stack of fire charges or one stack of arrows (without effects).
## Design
A design is very welcome, but not required.
## Bug-Using
Primed TNT may only be created from TNT blocks that players have placed.
The duplication of TNT is prohibited.

View File

@@ -1,265 +1,262 @@
{
"home": {
"page": "SteamWar - Startseite",
"title": {
"first": "Steam",
"second": "War"
},
"subtitle": {
"1": "WarGears, AirShips, WarShips",
"2": "Spieler Online: ",
"3": "Version: 1.12 - 1.21"
},
"join": "Jetzt Spielen",
"benefits": {
"fights": {
"title": "Spannende Kämpfe",
"description": {
"1": "Messe dich mit anderen Spielern in der Arena und zeige, dass du der beste bist.",
"2": "Von der kleinen Kampfmaschine bis zum riesigen Schlachtschiff kann alles gebaut werden."
}
},
"bau": {
"title": "Eigene Bauserver",
"description": "Jeder Spieler bekommt einen eigenen Bauserver, auf dem ohne Einschränkungen und Lags mit FaWe und Axiom gebaut werden kann."
},
"minigames": {
"title": "Minigames",
"description": {
"1": "Neben der Arena gibt es auch noch Minigames, die du mit anderen Spielern spielen kannst.",
"2": "Klassiker wie MissleWars, TowerRun oder TNTLeague warten auf dich."
}
},
"open": {
"title": "Free & Open",
"description": "Das Spielen auf SteamWar ist komplett Kostenlos und unsere Software ist Open Source."
},
"read": "Mehr Lesen"
},
"prefix": {
"Admin": "Administrator",
"Dev": "Developer",
"Mod": "Moderator",
"Sup": "Supporter",
"Arch": "Architekt",
"User": "Spieler",
"YT": "YouTuber"
"home": {
"page": "SteamWar - Startseite",
"title": {
"first": "Steam",
"second": "War"
},
"subtitle": {
"1": "WarGears, AirShips, WarShips",
"2": "Spieler Online: ",
"3": "Version: 1.12 - 1.21"
},
"join": "Jetzt Spielen",
"benefits": {
"fights": {
"title": "Spannende Kämpfe",
"description": {
"1": "Messe dich mit anderen Spielern in der Arena und zeige, dass du der beste bist.",
"2": "Von der kleinen Kampfmaschine bis zum riesigen Schlachtschiff kann alles gebaut werden."
}
},
"status": {
"loading": "Lade...",
"status": "Status",
"online": "Online",
"offline": "Offline",
"players": "Spieler: {# count #}",
"version": "Version: {# version #}"
},
"navbar": {
"title": "SteamWar",
"logo": {
"alt": "SteamWar Logo"
},
"links": {
"home": {
"title": "Start",
"announcements": "Ankündigungen",
"events": "Events",
"about": "Über Uns",
"downloads": "Downloads",
"faq": "FAQ"
},
"rules": {
"title": "Spielmodi",
"wg": "WarGear",
"mwg": "MiniWarGear",
"ws": "WarShip",
"as": "AirShip",
"qg": "QuickGear",
"rotating": "Rotierend",
"megawg": "MegaWarGear",
"micro": "MicroWarGear",
"sf": "StreetFight",
"general": "Allgemein",
"coc": "Verhaltensrichtlinien",
"publics": "Publics",
"ranked": "Ranked"
},
"help": {
"title": "Hilfe",
"docs": "Dokumentation"
},
"account": "Konto",
"ranked": {
"mw": "MissileWars"
}
},
"bau": {
"title": "Eigene Bauserver",
"description": "Jeder Spieler bekommt einen eigenen Bauserver, auf dem ohne Einschränkungen und Lags mit FaWe und Axiom gebaut werden kann."
},
"minigames": {
"title": "Minigames",
"description": {
"1": "Neben der Arena gibt es auch noch Minigames, die du mit anderen Spielern spielen kannst.",
"2": "Klassiker wie MissleWars, TowerRun oder TNTLeague warten auf dich."
}
},
"open": {
"title": "Free & Open",
"description": "Das Spielen auf SteamWar ist komplett Kostenlos und unsere Software ist Open Source."
},
"read": "Mehr Lesen"
},
"wg": {
"title": "WarGear"
"prefix": {
"Admin": "Administrator",
"Dev": "Developer",
"Mod": "Moderator",
"Sup": "Supporter",
"Arch": "Architekt",
"User": "Spieler",
"YT": "YouTuber"
}
},
"status": {
"loading": "Lade...",
"status": "Status",
"online": "Online",
"offline": "Offline",
"players": "Spieler: {# count #}",
"version": "Version: {# version #}"
},
"navbar": {
"title": "SteamWar",
"logo": {
"alt": "SteamWar Logo"
},
"bwg": {
"title": "Basic WarGear"
},
"swg": {
"title": "(Standard) WarGear"
},
"pwg": {
"title": "Pro WarGear"
},
"mwg": {
"title": "MiniWarGear"
},
"ws": {
"title": "WarShip"
},
"as": {
"title": "AirShip"
},
"qg": {
"title": "QuickGear"
},
"sf": {
"title": "StreetFight"
},
"megawg": {
"title": "MegaWarGear"
},
"microwg": {
"title": "MicroWarGear"
},
"mw": {
"title": "MissileWars"
},
"footer": {
"imprint": "Impressum",
"privacy": "Datenschutzerklärung",
"coc": "Verhaltensrichtlinien",
"stats": "Statistiken",
"gamemodes": "Spielmodi",
"links": {
"home": {
"title": "Start",
"announcements": "Ankündigungen",
"join": "Jetzt Spielen"
},
"elo": {
"place": "Platz",
"name": "Name",
"elo": "Elo"
},
"tag": {
"title": "Tag: {# tag #} - SteamWar"
},
"announcements": {
"table": {
"time": "Startzeit",
"blue": "Blaues Team",
"red": "Rotes Team",
"winner": "Sieger",
"notPlayed": "Nicht gespielt",
"draw": "Unentschieden",
"points": "Punkte",
"team": "Team"
}
},
"blog": {
"title": "Ankündigungen - SteamWar"
},
"dashboard": {
"page": "SteamWar - Dashboard",
"title": "Hallo, {# name #}!",
"rank": "Rang: {# rank #}",
"permissions": "Berechtigungen:",
"buttons": {
"logout": "Abmelden",
"admin": "Admin Panel"
},
"stats": {
"playtime": "Spielzeit: {# playtime #}",
"fights": "Kämpfe: {# fights #}",
"checked": "Freigegebene Schematics: {# checked #}"
},
"schematic": {
"upload": "Hochladen",
"dir": "Ordner",
"home": "Schematics",
"head": {
"type": "Typ",
"name": "Name",
"owner": "Besitzer",
"updated": "Aktualisiert",
"replaceColor": "Farbe ersetzen",
"allowReplay": "Wiederholung erlauben"
},
"info": {
"path": "Pfad: {# path #}",
"replaceColor": "Farbe ersetzen: ",
"allowReplay": "Replay gestattet: ",
"type": "Typ: {# type #}",
"updated": "Zuletzt geändert: {# updated #}",
"item": "Item: {# item #}",
"members": "Zugriff: {# members #}",
"btn": {
"close": "Schließen"
}
},
"cancel": "Abbrechen",
"title": "Schematic hochladen",
"errors": {
"invalidEnding": "Diese Dateiendung kann nicht Hochgeladen werden.",
"noFile": "Keine Datei.",
"upload": "Fehler beim Hochladen, Überprüfe deine Schematic!"
}
}
},
"login": {
"page": "SteamWar - Login",
"title": "Login",
"placeholder": {
"username": "Nutzername...",
"password": "***************"
},
"label": {
"username": "Nutzername",
"password": "Passwort",
"repeat": "Passwort Wiederholen"
},
"setPassword": "Wie setze ich mein Passwort?",
"submit": "Login",
"discord": "Mit Discord Einloggen",
"error": "Falscher Nutzername oder falsches Passwort"
},
"ranked": {
"title": "{# mode #} - Rangliste"
},
"rules": {
"page": "SteamWar - Regelwerke",
"wg": {
"description": "Heute werden die Schlachtfelder der Erde von schwerem Geschütz bestimmt. Mit unserem traditionellen Regelwerk sind auch die WarGears arenenverwüstende Schwergewichte. Aufgrund der Kanonentechnik mit den meisten Projektilen erwarten dich bei WarGears harte und kurzweilige Kämpfe."
},
"mwg": {
"description": "Im heutigen Straßenkampf hat massives Gerät keinen Platz, weswegen kleinere Maschinen auch heute noch ihre Berechtigung haben. Mit den etwas kleineren Kanonen sind MiniWarGears genau das richtige für Einsteiger, Gelegenheitsspieler und Experimentierfreudige."
},
"ws": {
"description": "Lange Zeit waren Kriegsschiffe das Nonplusultra der Kriegsführung. In Sachen Raketen- und Slimetechnik gilt das auch heute noch für Warships. Durch die begrenzte Kanonenstärke bieten WarShips lange, intensive und abwechslungsreiche Kämpfe, womit es immer neue Technik in der Arena gibt. Durch das Entern verlagert sich ein WarShip-Kampf nach einiger Zeit in das Wasser und bietet damit spannende PvP-Action."
},
"as": {
"description": "Der Traum vom Fliegen beflügelt die Menschheit schon seit Jahrtausenden. Der Spielmodus AirShips bietet dir die nahezu unbegrenzten Möglichkeiten des Himmels. Egal, ob du mit 15 2 Projektil-Kanonen oder 2 15 Projektil-Kanonen antrittst, du hast stets eine realistische Chance auf den Sieg. Denn: Alles hat seinen Preis."
},
"qg": {
"description": "Nicht immer besteht die Zeit für einen langen Bau. Manchmal muss es schnell gehen. Für diese Fälle gibt es QuickGears. Ohne Qualitätsprüfung und mit nur einem Klick kannst du hier ein Gefährt erstellen. Die Qualität ist dabei nicht immer die beste, aber für einen schnellen Kampf reicht es allemal."
},
"rules": "Regelwerk »",
"announcements": "Ankündigungen »",
"ranking": "Rangliste »",
"title": "{# mode #} - Regelwerk",
"publics": "Publics »"
"about": "Über Uns",
"downloads": "Downloads",
"faq": "FAQ"
},
"rules": {
"title": "Spielmodi",
"wg": "WarGear",
"mwg": "MiniWarGear",
"ws": "WarShip",
"as": "AirShip",
"qg": "QuickGear",
"rotating": "Rotierend",
"megawg": "MegaWarGear",
"micro": "MicroWarGear",
"sf": "StreetFight",
"general": "Allgemein",
"coc": "Verhaltensrichtlinien",
"publics": "Publics",
"ranked": "Ranked"
},
"help": {
"title": "Hilfe",
"docs": "Dokumentation"
},
"account": "Konto",
"ranked": {
"mw": "MissileWars"
}
}
},
"wg": {
"title": "WarGear"
},
"bwg": {
"title": "Basic WarGear"
},
"swg": {
"title": "(Standard) WarGear"
},
"pwg": {
"title": "Pro WarGear"
},
"mwg": {
"title": "MiniWarGear"
},
"ws": {
"title": "WarShip"
},
"as": {
"title": "AirShip"
},
"qg": {
"title": "QuickGear"
},
"sf": {
"title": "StreetFight"
},
"megawg": {
"title": "MegaWarGear"
},
"microwg": {
"title": "MicroWarGear"
},
"mw": {
"title": "MissileWars"
},
"footer": {
"imprint": "Impressum",
"privacy": "Datenschutzerklärung",
"coc": "Verhaltensrichtlinien",
"stats": "Statistiken",
"gamemodes": "Spielmodi",
"announcements": "Ankündigungen",
"join": "Jetzt Spielen"
},
"elo": {
"place": "Platz",
"name": "Name",
"elo": "Elo"
},
"tag": {
"title": "Tag: {# tag #} - SteamWar"
},
"announcements": {
"table": {
"time": "Startzeit",
"blue": "Blaues Team",
"red": "Rotes Team",
"winner": "Sieger",
"notPlayed": "Nicht gespielt",
"draw": "Unentschieden",
"points": "Punkte",
"team": "Team"
}
},
"blog": {
"title": "Ankündigungen - SteamWar"
},
"dashboard": {
"page": "SteamWar - Dashboard",
"title": "Hallo, {# name #}!",
"rank": "Rang: {# rank #}",
"permissions": "Berechtigungen:",
"buttons": {
"logout": "Abmelden",
"admin": "Admin Panel"
},
"stats": {
"title": "Kampf Statistiken"
"playtime": "Spielzeit: {# playtime #}",
"fights": "Kämpfe: {# fights #}",
"checked": "Freigegebene Schematics: {# checked #}"
},
"ranking": {
"heading": "{# mode #} Rangliste"
},
"404": {
"title": "404 - Seite nicht gefunden",
"description": "Seite nicht gefunden"
"schematic": {
"upload": "Hochladen",
"dir": "Ordner",
"home": "Schematics",
"head": {
"type": "Typ",
"name": "Name",
"owner": "Besitzer",
"updated": "Aktualisiert",
"replaceColor": "Farbe ersetzen",
"allowReplay": "Wiederholung erlauben"
},
"info": {
"path": "Pfad: {# path #}",
"replaceColor": "Farbe ersetzen: ",
"allowReplay": "Replay gestattet: ",
"type": "Typ: {# type #}",
"updated": "Zuletzt geändert: {# updated #}",
"item": "Item: {# item #}",
"members": "Zugriff: {# members #}",
"btn": {
"close": "Schließen"
}
},
"cancel": "Abbrechen",
"title": "Schematic hochladen",
"errors": {
"invalidEnding": "Diese Dateiendung kann nicht Hochgeladen werden.",
"noFile": "Keine Datei.",
"upload": "Fehler beim Hochladen, Überprüfe deine Schematic!"
}
}
},
"login": {
"title": "Login",
"placeholder": {
"username": "Nutzername...",
"password": "***************"
},
"label": {
"username": "Nutzername",
"password": "Passwort",
"repeat": "Passwort Wiederholen"
},
"setPassword": "Wie setzte ich mein Passwort?",
"submit": "Login",
"error": "Falscher Nutzername oder falsches Passwort"
},
"ranked": {
"title": "{# mode #} - Rangliste"
},
"rules": {
"page": "SteamWar - Regelwerke",
"wg": {
"description": "Heute werden die Schlachtfelder der Erde von schwerem Geschütz bestimmt. Mit unserem traditionellen Regelwerk sind auch die WarGears arenenverwüstende Schwergewichte. Aufgrund der Kanonentechnik mit den meisten Projektilen erwarten dich bei WarGears harte und kurzweilige Kämpfe."
},
"mwg": {
"description": "Im heutigen Straßenkampf hat massives Gerät keinen Platz, weswegen kleinere Maschinen auch heute noch ihre Berechtigung haben. Mit den etwas kleineren Kanonen sind MiniWarGears genau das richtige für Einsteiger, Gelegenheitsspieler und Experimentierfreudige."
},
"ws": {
"description": "Lange Zeit waren Kriegsschiffe das Nonplusultra der Kriegsführung. In Sachen Raketen- und Slimetechnik gilt das auch heute noch für Warships. Durch die begrenzte Kanonenstärke bieten WarShips lange, intensive und abwechslungsreiche Kämpfe, womit es immer neue Technik in der Arena gibt. Durch das Entern verlagert sich ein WarShip-Kampf nach einiger Zeit in das Wasser und bietet damit spannende PvP-Action."
},
"as": {
"description": "Der Traum vom Fliegen beflügelt die Menschheit schon seit Jahrtausenden. Der Spielmodus AirShips bietet dir die nahezu unbegrenzten Möglichkeiten des Himmels. Egal, ob du mit 15 2 Projektil-Kanonen oder 2 15 Projektil-Kanonen antrittst, du hast stets eine realistische Chance auf den Sieg. Denn: Alles hat seinen Preis."
},
"qg": {
"description": "Nicht immer besteht die Zeit für einen langen Bau. Manchmal muss es schnell gehen. Für diese Fälle gibt es QuickGears. Ohne Qualitätsprüfung und mit nur einem Klick kannst du hier ein Gefährt erstellen. Die Qualität ist dabei nicht immer die beste, aber für einen schnellen Kampf reicht es allemal."
},
"rules": "Regelwerk »",
"announcements": "Ankündigungen »",
"ranking": "Rangliste »",
"title": "{# mode #} - Regelwerk",
"publics": "Publics »"
},
"stats": {
"title": "Kampf Statistiken"
},
"ranking": {
"heading": "{# mode #} Rangliste"
},
"404": {
"title": "404 - Seite nicht gefunden",
"description": "Seite nicht gefunden"
}
}

View File

@@ -1,197 +1,196 @@
{
"navbar": {
"links": {
"home": {
"title": "Home",
"announcements": "Announcements",
"about": "About",
"downloads": "Downloads",
"faq": "FAQ"
},
"rules": {
"title": "Game modes",
"rotating": "Rotating",
"general": "General",
"coc": "Code of Conduct"
},
"help": {
"title": "Help",
"center": "Helpcenter",
"docs": "Docs"
},
"account": "Account"
}
},
"status": {
"loading": "Loading...",
"players": "Players: {# count #}"
},
"home": {
"page": "SteamWar - Home",
"subtitle": {
"1": "WarGears, AirShips, WarShips",
"2": "Players Online: "
},
"join": "Join Now",
"benefits": {
"fights": {
"title": "Exciting Fights",
"description": {
"1": "Compete with other players in the arena and show that you are the best.",
"2": "From small combat machines to huge battleships, everything can be built."
}
},
"bau": {
"title": "Own Build Server",
"description": "Every player gets their own build server to ensure maximum performance and minimal limitations with leading tools like FaWe or Axiom"
},
"minigames": {
"title": "Minigames",
"description": {
"1": "Besides the Arena, you can also play minigames with other players.",
"2": "like MissleWars, Towerrun or TNTLeague"
}
},
"open": {
"description": "Playing on SteamWar is completely free and our software is open source."
},
"read": "Read More"
},
"prefix": {
"Admin": "Admin",
"Dev": "Developer",
"Mod": "Moderator",
"Sup": "Supporter",
"Arch": "Builder",
"User": "User",
"YT": "YouTuber"
}
},
"footer": {
"imprint": "Imprint",
"privacy": "Privacy Policy",
"coc": "Code of Conduct",
"stats": "Stats",
"gamemodes": "Game modes",
"navbar": {
"links": {
"home": {
"title": "Home",
"announcements": "Announcements",
"join": "Join Now"
"about": "About",
"downloads": "Downloads",
"faq": "FAQ"
},
"rules": {
"title": "Game modes",
"rotating": "Rotating",
"general": "General",
"coc": "Code of Conduct"
},
"help": {
"title": "Help",
"center": "Helpcenter",
"docs": "Docs"
},
"account": "Account"
}
},
"status": {
"loading": "Loading...",
"players": "Players: {# count #}"
},
"home": {
"page": "SteamWar - Home",
"subtitle": {
"1": "WarGears, AirShips, WarShips",
"2": "Players Online: "
},
"ranking": {
"heading": "{# mode #} Rankings"
},
"announcements": {
"table": {
"time": "Time",
"blue": "Blue Team",
"red": "Red Team",
"winner": "Winner",
"notPlayed": "Not Played",
"draw": "Draw",
"team": "Team",
"points": "Points"
"join": "Join Now",
"benefits": {
"fights": {
"title": "Exciting Fights",
"description": {
"1": "Compete with other players in the arena and show that you are the best.",
"2": "From small combat machines to huge battleships, everything can be built."
}
},
"elo": {
"place": "Place"
},
"warning": {
"title": "This page is not available in your language.",
"text": "The page you are trying to access is not available in your language. You can still access the original page in German."
},
"blog": {
"title": "Announcements - SteamWar"
},
"dashboard": {
"title": "Hello, {# name #}!",
"rank": "Rank: {# rank #}",
"permissions": "Permssions:",
"buttons": {
"logout": "Logout"
},
"stats": {
"playtime": "Playtime: {# playtime #}",
"fights": "Fights: {# fights #}",
"checked": "Accepted Schematics: {# checked #}"
},
"schematic": {
"upload": "Upload",
"cancel": "Cancel",
"title": "Upload Schematic",
"dir": "Directory",
"head": {
"type": "Type",
"owner": "Owner",
"updated": "Updated",
"replaceColor": "Replace Color",
"allowReplay": "Allow Replay"
},
"info": {
"path": "Path: {# path #}",
"replaceColor": "Replace Color: ",
"allowReplay": "Allow Replay: ",
"type": "Type: {# type #}",
"updated": "Updated: {# updated #}",
"item": "Item: {# item #}",
"members": "Members: {# members #}",
"btn": {
"download": "Download",
"close": "Close"
}
},
"errors": {
"invalidEnding": "This file extension cannot be uploaded.",
"noFile": "No file.",
"upload": "Error uploading, check your schematic!"
}
},
"bau": {
"title": "Own Build Server",
"description": "Every player gets their own build server to ensure maximum performance and minimal limitations with leading tools like FaWe or Axiom"
},
"minigames": {
"title": "Minigames",
"description": {
"1": "Besides the Arena, you can also play minigames with other players.",
"2": "like MissleWars, Towerrun or TNTLeague"
}
},
"open": {
"description": "Playing on SteamWar is completely free and our software is open source."
},
"read": "Read More"
},
"login": {
"page": "SteamWar - Login",
"title": "Login",
"placeholder": {
"username": "Username...",
"password": "***************"
},
"label": {
"username": "Username",
"password": "Password",
"repeat": "Repeat Password"
},
"setPassword": "How to set a Password",
"submit": "Login",
"discord": "Login with Discord",
"error": "Invalid username or password"
},
"ranked": {
"title": "{# mode #} - Ranking"
},
"rules": {
"page": "SteamWar - Rules",
"wg": {
"description": "Today, the battlefields of Earth are dominated by heavy artillery. With our traditional rules, WarGears are also arena-wrecking heavyweights. Due to the cannon technology with the most projectiles, you can expect hard and short-lived battles in WarGears."
},
"as": {
"description": "The dream of flying has inspired humanity for millennia. The AirShips game mode offers you the almost unlimited possibilities of the sky. Whether you compete with 15 2-projectile cannons or 2 15-projectile cannons, you always have a realistic chance of winning. Because: Everything has its price."
},
"ws": {
"description": "For a long time, warships were the ultimate weapon of war. This is still true for Warships today in terms of rocket and slime technology. Due to the limited cannon power, WarShips offer long, intense and varied battles, with new techniques always being introduced in the arena. After a while, a WarShip battle shifts to the water through boarding, providing exciting PvP action."
},
"mwg": {
"description": "In today's urban warfare, there is no place for heavy equipment, which is why smaller machines still have their place today. With their slightly smaller cannons, MiniWarGears are the perfect choice for beginners, casual players, and those who like to experiment."
},
"qg": {
"description": "Sometimes there is no time for a long construction. Sometimes it has to be quick. For these cases there are QuickGears. Without quality control and with just one click you can create a vehicle here. The quality is not always the best, but for a quick fight it is enough."
},
"rules": "Rules »",
"announcements": "Announcements »",
"ranking": "Ranking »",
"title": "{# mode #} - Rules"
"prefix": {
"Admin": "Admin",
"Dev": "Developer",
"Mod": "Moderator",
"Sup": "Supporter",
"Arch": "Builder",
"User": "User",
"YT": "YouTuber"
}
},
"footer": {
"imprint": "Imprint",
"privacy": "Privacy Policy",
"coc": "Code of Conduct",
"stats": "Stats",
"gamemodes": "Game modes",
"announcements": "Announcements",
"join": "Join Now"
},
"ranking": {
"heading": "{# mode #} Rankings"
},
"announcements": {
"table": {
"time": "Time",
"blue": "Blue Team",
"red": "Red Team",
"winner": "Winner",
"notPlayed": "Not Played",
"draw": "Draw",
"team": "Team",
"points": "Points"
}
},
"elo": {
"place": "Place"
},
"warning": {
"title": "This page is not available in your language.",
"text": "The page you are trying to access is not available in your language. You can still access the original page in German."
},
"blog": {
"title": "Announcements - SteamWar"
},
"dashboard": {
"title": "Hello, {# name #}!",
"rank": "Rank: {# rank #}",
"permissions": "Permssions:",
"buttons": {
"logout": "Logout"
},
"stats": {
"title": "Fight Statistics"
"playtime": "Playtime: {# playtime #}",
"fights": "Fights: {# fights #}",
"checked": "Accepted Schematics: {# checked #}"
},
"404": {
"title": "404 - Page not found",
"description": "Page not found"
"schematic": {
"upload": "Upload",
"cancel": "Cancel",
"title": "Upload Schematic",
"dir": "Directory",
"head": {
"type": "Type",
"owner": "Owner",
"updated": "Updated",
"replaceColor": "Replace Color",
"allowReplay": "Allow Replay"
},
"info": {
"path": "Path: {# path #}",
"replaceColor": "Replace Color: ",
"allowReplay": "Allow Replay: ",
"type": "Type: {# type #}",
"updated": "Updated: {# updated #}",
"item": "Item: {# item #}",
"members": "Members: {# members #}",
"btn": {
"download": "Download",
"close": "Close"
}
},
"errors": {
"invalidEnding": "This file extension cannot be uploaded.",
"noFile": "No file.",
"upload": "Error uploading, check your schematic!"
}
}
},
"login": {
"page": "SteamWar - Login",
"title": "Login",
"placeholder": {
"username": "Username...",
"password": "***************"
},
"label": {
"username": "Username",
"password": "Password",
"repeat": "Repeat Password"
},
"setPassword": "How to set a Password",
"submit": "Login",
"error": "Invalid username or password"
},
"ranked": {
"title": "{# mode #} - Ranking"
},
"rules": {
"page": "SteamWar - Rules",
"wg": {
"description": "Today, the battlefields of Earth are dominated by heavy artillery. With our traditional rules, WarGears are also arena-wrecking heavyweights. Due to the cannon technology with the most projectiles, you can expect hard and short-lived battles in WarGears."
},
"as": {
"description": "The dream of flying has inspired humanity for millennia. The AirShips game mode offers you the almost unlimited possibilities of the sky. Whether you compete with 15 2-projectile cannons or 2 15-projectile cannons, you always have a realistic chance of winning. Because: Everything has its price."
},
"ws": {
"description": "For a long time, warships were the ultimate weapon of war. This is still true for Warships today in terms of rocket and slime technology. Due to the limited cannon power, WarShips offer long, intense and varied battles, with new techniques always being introduced in the arena. After a while, a WarShip battle shifts to the water through boarding, providing exciting PvP action."
},
"mwg": {
"description": "In today's urban warfare, there is no place for heavy equipment, which is why smaller machines still have their place today. With their slightly smaller cannons, MiniWarGears are the perfect choice for beginners, casual players, and those who like to experiment."
},
"qg": {
"description": "Sometimes there is no time for a long construction. Sometimes it has to be quick. For these cases there are QuickGears. Without quality control and with just one click you can create a vehicle here. The quality is not always the best, but for a quick fight it is enough."
},
"rules": "Rules »",
"announcements": "Announcements »",
"ranking": "Ranking »",
"title": "{# mode #} - Rules"
},
"stats": {
"title": "Fight Statistics"
},
"404": {
"title": "404 - Page not found",
"description": "Page not found"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -2,7 +2,7 @@
import NavbarLayout from "./NavbarLayout.astro";
import BackgroundImage from "../components/BackgroundImage.astro";
const { title, description, wide = false } = Astro.props;
const { title, description } = Astro.props;
---
<NavbarLayout title={title} description={description}>
@@ -10,11 +10,8 @@ const { title, description, wide = false } = Astro.props;
<div class="h-screen w-screen fixed -z-10">
<BackgroundImage />
</div>
<div
class="mx-auto p-8 rounded-b-md border-x-gray-100 shadow-md pt-14 relative
text-white backdrop-blur-3xl"
style={wide ? "width: clamp(80%, 75em, 100%);" : "width: min(100%, 75em);"}
>
<div class="mx-auto p-8 rounded-b-md border-x-gray-100 shadow-md pt-14 relative
text-white backdrop-blur-3xl" style="width: min(100%, 75em);">
<slot />
</div>
</NavbarLayout>

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