472 Commits

Author SHA1 Message Date
9b65d5d730 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 17:33:42 +02:00
8397aace8d Update Eventplan MWGL2025 2025-09-28 17:33:36 +02:00
c2b0bcc54e Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 17:21:46 +02:00
5c48f0cb85 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 17:04:14 +02:00
d30cceaad0 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 16:52:46 +02:00
41be843be4 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 16:47:39 +02:00
3768788f32 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 16:34:31 +02:00
7e6f953e44 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 16:32:28 +02:00
cad3a795a7 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 16:20:02 +02:00
48e8165417 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-28 16:10:34 +02:00
b11534490d Refactor EventFight handling to include team relation names and update type definitions
All checks were successful
SteamWarCI Build successful
2025-09-28 14:11:58 +02:00
c0f4a852b5 Refactor event handling and introduce TeamSelector component for improved fight management
All checks were successful
SteamWarCI Build successful
2025-09-28 10:26:08 +02:00
54d49cca5b Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 19:41:56 +02:00
831ea3af11 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 19:14:21 +02:00
b6a0692c50 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 19:13:48 +02:00
01394953d4 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 19:12:09 +02:00
c515b19e74 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 19:09:57 +02:00
98199cc9a0 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 19:08:31 +02:00
3f61564067 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 18:53:59 +02:00
7b0f18f65d Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 18:40:17 +02:00
4ac5d2d2b2 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 18:37:20 +02:00
8fd3e04116 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 18:10:17 +02:00
3180ad1263 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 18:09:56 +02:00
f689415b98 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 18:08:59 +02:00
894d0f8a05 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 17:56:24 +02:00
16d377e3e4 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 17:46:19 +02:00
1b2a05c204 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 17:11:35 +02:00
04969e79c3 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 16:45:08 +02:00
a949237334 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 16:09:41 +02:00
01a59d6de4 Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 13:48:50 +02:00
3daeb8b62d Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 12:45:30 +02:00
aa72de70ef Update Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 12:06:12 +02:00
324025dd57 Add Eventplan MWGL2025
All checks were successful
SteamWarCI Build successful
2025-09-27 12:02:51 +02:00
41b847b3e4 Refactor colorCode validation in PrefixSchema to allow any length string starting with "§"
All checks were successful
SteamWarCI Build successful
2025-09-16 18:07:39 +02:00
a3b4a6d0c2 Refactor event and fight repositories to use numeric IDs for groups; update datetime picker input handling; add new generator components for event fights and group phases.
All checks were successful
SteamWarCI Build successful
2025-09-16 18:03:29 +02:00
5f12a0cc7a Update kuerzel max length in TeamSchema to 16 characters
All checks were successful
SteamWarCI Build successful
2025-08-13 21:06:35 +02:00
7166575806 Fix section number for cannon count in WarShip rules
All checks were successful
SteamWarCI Build successful
2025-08-13 20:58:50 +02:00
0055e9fb9c Update WarShip rules to clarify restrictions on protective materials
All checks were successful
SteamWarCI Build successful
2025-08-13 20:55:49 +02:00
fc5a209638 Refactor WarShip rules for clarity and structure; added section numbers and improved definitions.
All checks were successful
SteamWarCI Build successful
2025-08-13 20:54:07 +02:00
c7cdc19102 Fix typo in WarGear Event announcement text
All checks were successful
SteamWarCI Build successful
2025-08-12 20:57:09 +02:00
c6bbe8c9c8 Add team size information to WarGear Event announcement
All checks were successful
SteamWarCI Build successful
2025-08-11 22:46:26 +02:00
1cec1b917e Add note about new Schematic type for WarGear Event
All checks were successful
SteamWarCI Build successful
2025-08-11 22:45:40 +02:00
13805c7f3f Add WarGear Event announcement for November 2025
All checks were successful
SteamWarCI Build successful
2025-08-11 22:41:15 +02:00
Chaoscaot
da668c574a Updated mwgl.md
All checks were successful
SteamWarCI Build successful
2025-07-28 13:42:19 +02:00
Chaoscaot
2aab86573a Add Image generated-image(8).png
All checks were successful
SteamWarCI Build successful
2025-07-28 13:41:51 +02:00
5d7eb3b8fb Merge pull request 'Merge branch mwgl' (#16) from mwgl into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #16
2025-07-28 13:02:14 +02:00
Chaoscaot
6933af1554 Updated mwgl.md
All checks were successful
SteamWarCI Build successful
2025-07-28 13:01:56 +02:00
Chaoscaot
e607ea1343 Updated mwgl.md
All checks were successful
SteamWarCI Build successful
2025-07-28 13:00:51 +02:00
Chaoscaot
b0ae4e978e Create page announcements/de/mwgl.md
All checks were successful
SteamWarCI Build successful
2025-07-28 12:57:57 +02:00
8fe273f3e0 Add Open-Source section to documentation
All checks were successful
SteamWarCI Build successful
2025-07-10 17:57:42 +02:00
1b48cbe1f4 Update edit link base URL to point to the master branch
All checks were successful
SteamWarCI Build successful
2025-07-10 13:51:09 +02:00
7276552ed1 Merge branch 'master' of https://git.steamwar.de/SteamWar/Website
All checks were successful
SteamWarCI Build successful
2025-07-10 13:49:15 +02:00
a2ef92aaad Add Docs 2025-07-10 13:49:00 +02:00
8b85cd0729 src/content/modes/spacecraft.json aktualisiert
All checks were successful
SteamWarCI Build successful
2025-06-29 22:51:13 +02:00
Chaoscaot
2d024cf64b Create page modes/spacecraft.json
Some checks failed
SteamWarCI Build failed
2025-06-29 22:49:58 +02:00
TheBreadBeard
13d76d0a97 Updated SC-Eventplan.md
All checks were successful
SteamWarCI Build successful
2025-06-29 20:34:55 +02:00
Chaoscaot
e65fadb65c Updated SC-Eventplan.md
All checks were successful
SteamWarCI Build successful
2025-06-29 20:32:23 +02:00
TheBreadBeard
6b4693b7f1 Updated SC-Eventplan.md
All checks were successful
SteamWarCI Build successful
2025-06-29 20:29:54 +02:00
TheBreadBeard
92282006fe Add Image SpaceCraftWinners3.png
All checks were successful
SteamWarCI Build successful
2025-06-29 20:29:16 +02:00
5457632598 Fix formatting of teamPoints calculation in GroupTable component for improved readability
All checks were successful
SteamWarCI Build successful
2025-06-29 19:47:35 +02:00
bed134f8e0 Fix group points retrieval in GroupTable component to ensure correct mapping of event groups
All checks were successful
SteamWarCI Build successful
2025-06-29 19:45:18 +02:00
353a415990 Refactor GroupTable component to use $props for event, group, and rows; simplify teamPoints calculation with derived state
All checks were successful
SteamWarCI Build successful
2025-06-29 19:41:43 +02:00
3c6d0f8528 Fix
All checks were successful
SteamWarCI Build successful
2025-06-29 14:00:09 +02:00
887235dc86 Enhance caching mechanism by adding future promise to cached function and updating maps retrieval logic to use it
All checks were successful
SteamWarCI Build successful
2025-06-29 13:54:03 +02:00
a99a066f0d Merge branch 'master' of https://git.steamwar.de/SteamWar/Website
All checks were successful
SteamWarCI Build successful
2025-06-29 11:24:07 +02:00
e5e3c15b07 Add refresh functionality and duplicate fight feature in FightEditRow component 2025-06-29 11:23:51 +02:00
TheBreadBeard
fb74689c39 Updated SC-Eventplan.md
All checks were successful
SteamWarCI Build successful
2025-06-29 09:15:11 +02:00
18b1f97a84 src/content/announcements/de/SC-Eventplan.md aktualisiert
All checks were successful
SteamWarCI Build successful
2025-06-29 03:07:37 +02:00
TheBreadBeard
53b81db2c4 Updated SC-Eventplan.md
Some checks failed
SteamWarCI Build failed
2025-06-29 02:44:17 +02:00
TheBreadBeard
2314b4c5b5 Updated SC-Eventplan.md
Some checks failed
SteamWarCI Build failed
2025-06-28 17:53:43 +02:00
TheBreadBeard
6a81936f77 Updated SC-Eventplan.md
Some checks failed
SteamWarCI Build failed
2025-06-28 17:32:32 +02:00
TheBreadBeard
a128de3213 Updated SC-Eventplan.md
Some checks failed
SteamWarCI Build failed
2025-06-28 17:31:58 +02:00
TheBreadBeard
6df661f885 Create page announcements/de/SC-Eventplan.md
Some checks failed
SteamWarCI Build failed
2025-06-28 17:21:11 +02:00
TheBreadBeard
a32d84ed86 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-27 16:12:51 +02:00
Chaoscaot
e60cebc9a3 Updated sw-arcade-fightplan.md
All checks were successful
SteamWarCI Build successful
2025-06-27 00:31:58 +02:00
3576d5e034 Refactor save method to remove base64 encoding for page content
All checks were successful
SteamWarCI Build successful
2025-06-27 00:31:08 +02:00
Chaoscaot
d5c7d8fc27 Updated sw-arcade-fightplan.md
Some checks failed
SteamWarCI Build failed
2025-06-27 00:29:26 +02:00
ce895e9297 Add default value to prompt for change description in page update
All checks were successful
SteamWarCI Build successful
2025-06-27 00:28:37 +02:00
7c83ad0937 Add prompt for change description in page update
All checks were successful
SteamWarCI Build successful
2025-06-27 00:28:16 +02:00
5e0a9d89b3 Fixing
All checks were successful
SteamWarCI Build successful
2025-06-27 00:24:13 +02:00
2a8b98ce5b Update copyright year to 2025 in table.css
All checks were successful
SteamWarCI Build successful
2025-06-26 23:57:55 +02:00
427818d6bf Fixing
Some checks failed
SteamWarCI Build failed
2025-06-26 23:56:45 +02:00
8424c14ca9 Remove unused import of ExtendedEvent from TeamTable.svelte
Some checks failed
SteamWarCI Build failed
2025-06-26 23:54:21 +02:00
602a7e1453 Remove unused import of Team from TeamTable.svelte
Some checks failed
SteamWarCI Build failed
2025-06-26 23:53:03 +02:00
9f31c5ff0c Remove unused import of Team from EventFightList.svelte
Some checks failed
SteamWarCI Build failed
2025-06-26 23:50:28 +02:00
8a41b98c58 Remove unused import of ExtendedEvent from EventFightList.svelte
Some checks failed
SteamWarCI Build failed
2025-06-26 23:49:09 +02:00
9fc5c500f5 Merge pull request 'Event Brackets' (#11) from event-brackets into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #11
2025-06-26 23:40:59 +02:00
bc879d7cad Add login.page to de.json
All checks were successful
SteamWarCI Build successful
2025-06-26 15:02:08 +02:00
TheBreadBeard
96f0019dc1 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-22 16:55:31 +02:00
TheBreadBeard
7418b608ab Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-21 23:33:31 +02:00
TheBreadBeard
3802b9bc26 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-13 14:21:16 +02:00
03effd2fd2 src/content/downloads/advancedscripts.json aktualisiert
All checks were successful
SteamWarCI Build successful
2025-06-12 20:35:23 +02:00
TheBreadBeard
a4669a897b Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-06 19:20:33 +02:00
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
eac0d5592d src/i18n/common/de.json aktualisiert
All checks were successful
SteamWarCI Build successful
2025-06-03 14:11:22 +02:00
TheBreadBeard
bd9aea8f35 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-03 09:53:14 +02:00
TheBreadBeard
6e715cee07 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-03 07:11:00 +02:00
TheBreadBeard
4147a1d243 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-02 23:46:46 +02:00
TheBreadBeard
46dba2a6f9 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-02 23:44:12 +02:00
TheBreadBeard
3d8ad3a129 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-02 23:40:31 +02:00
TheBreadBeard
7d50a4db12 Update spacheship-event-ankündigung-und-regelwerk.md
All checks were successful
SteamWarCI Build successful
2025-06-02 23:30:42 +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
49 changed files with 672 additions and 3211 deletions

View File

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

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

@@ -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,6 +21,9 @@
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";
@@ -44,6 +47,10 @@
<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>

View File

@@ -194,16 +194,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>
@@ -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">

View File

@@ -38,11 +38,6 @@
duplicateOpen = false;
}
async function handleDelete() {
await $fightRepo.deleteFight(data.event.id, fight.id);
refresh();
}
</script>
<div>
@@ -60,7 +55,6 @@
<FightEdit {fight} {data} onSave={handleSave}>
{#snippet actions(dirty, submit)}
<DialogFooter>
<Button variant="destructive" onclick={handleDelete}>Löschen</Button>
<Button disabled={!dirty} onclick={submit}>Speichern</Button>
</DialogFooter>
{/snippet}

View File

@@ -26,8 +26,8 @@ export class EventModel {
return v.map((fight) => {
let f = JSON.parse(JSON.stringify(fight)) as EventFight;
let blueTeamRelation = "";
let redTeamRelation = "";
let blueTeamRelation = f.blueTeam.name;
let redTeamRelation = f.redTeam.name;
let relations = rels.filter((relation) => relation.fight === f.id);
@@ -54,11 +54,11 @@ export class EventModel {
...f,
blueTeam: {
...f.blueTeam,
nameWithRelation: blueTeamRelation ? `${f.blueTeam.name} (${blueTeamRelation})` : f.blueTeam.name,
nameWithRelation: `${f.blueTeam.name} (${blueTeamRelation})`,
},
redTeam: {
...f.redTeam,
nameWithRelation: redTeamRelation ? `${f.redTeam.name} (${redTeamRelation})` : f.redTeam.name,
nameWithRelation: `${f.redTeam.name} (${redTeamRelation})`,
},
};
});

View File

@@ -2,8 +2,6 @@
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,
}: {
@@ -16,16 +14,9 @@
<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,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

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

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

@@ -77,18 +77,18 @@ Die fights werden auf 5 Minuten an den vorherigen vorgezogen.
| 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 | / |
| 17:45 | Borg vs Salo | / |
| | Borg vs Salo | / |
| | Borg vs Salo | / |
| | Borg vs Salo | / |
| | Borg vs Salo | / |
## Endplatzierung
| Platz | Team |
|-------|------|
| 1. | Borg |
| 2. | Salo |
| 1. | ??? |
| 2. | ??? |
| 3. | FK |
| 4. | BF |
| 5. | PL |

View File

@@ -20,7 +20,6 @@
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";
export const pagesSchema = z.object({
title: z.string().min(1).max(80),
@@ -110,19 +109,6 @@ export const publics = defineCollection({
}),
});
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(),
}),
});
export const collections = {
pages: pages,
help: help,
@@ -132,5 +118,4 @@ export const collections = {
announcements: announcements,
publics: publics,
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
events: events,
};

View File

@@ -13,7 +13,7 @@ SteamWar ist ein Minecraft Java Server.
<Tabs>
<TabItem label="Java Edition">
- IP: `steamwar.de`
- Empfohlene Version: `1.21.6`
- Empholene Version: `1.21.6`
</TabItem>
<TabItem label="Bedrock Edition">
- IP: `steamwar.de`

View File

@@ -2,7 +2,6 @@
"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",

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

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

@@ -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,263 @@
{
"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": {
"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",
"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"
}
}

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>

View File

@@ -1,7 +1,7 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 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
@@ -22,4 +22,4 @@ import { useAstroI18n } from "astro-i18n";
const astroI18n = useAstroI18n();
export const onRequest = sequence(astroI18n);
export const onRequest = sequence(astroI18n);

View File

@@ -1,48 +1,43 @@
---
import { getCollection } from "astro:content";
import {getCollection} from "astro:content";
import PageLayout from "../../layouts/PageLayout.astro";
import { astroI18n, createGetStaticPaths, t } from "astro-i18n";
import {astroI18n, createGetStaticPaths, t} from "astro-i18n";
import PostComponent from "../../components/PostComponent.astro";
import dayjs from "dayjs";
import TagComponent from "../../components/TagComponent.astro";
import SWPaginator from "@components/styled/SWPaginator.svelte";
export const getStaticPaths = createGetStaticPaths(async (props) => {
const posts = await getCollection("announcements", (entry) => entry.id.split("/")[0] === astroI18n.locale);
const posts = await getCollection("announcements", entry => entry.id.split("/")[0] === astroI18n.locale);
const germanPosts = await getCollection("announcements", (entry) => entry.id.split("/")[0] === astroI18n.fallbackLocale);
const germanPosts = await getCollection("announcements", entry => entry.id.split("/")[0] === astroI18n.fallbackLocale);
germanPosts.forEach((value) => {
if (posts.find((post) => post.data.key === value.data.key)) {
germanPosts.forEach(value => {
if (posts.find(post => post.data.key === value.data.key)) {
return;
} else {
posts.push(value);
}
});
return props.paginate(
posts.sort((a, b) => dayjs(b.data.created).unix() - dayjs(a.data.created).unix()),
{
pageSize: 5,
}
);
return props.paginate(posts.sort((a, b) => dayjs(b.data.created).unix() - dayjs(a.data.created).unix()), {
pageSize: 5,
});
});
async function getTags() {
const posts = await getCollection("announcements");
const tags = new Map<string, number>();
posts.forEach((post) => {
post.data.tags.forEach((tag) => {
if (tags.has(tag.toLowerCase())) {
tags.set(tag.toLowerCase(), tags.get(tag) + 1);
posts.forEach(post => {
post.data.tags.forEach(tag => {
if (tags.has(tag)) {
tags.set(tag, tags.get(tag) + 1);
} else {
tags.set(tag.toLowerCase(), 1);
tags.set(tag, 1);
}
});
});
return Array.from(tags)
.sort((a, b) => b[1] - a[1])
.map((value) => value[0]);
return Array.from(tags).sort((a, b) => b[1] - a[1]).map(value => value[0]);
}
const { page } = Astro.props;
@@ -51,15 +46,15 @@ const tags = await getTags();
<PageLayout title={t("blog.title")}>
<div class="py-2">
{tags.map((tag) => <TagComponent tag={tag} transition:name={`${tag}-tag-filter`} />)}
{tags.map(tag => (
<TagComponent tag={tag} transition:name={`${tag}-tag-filter`} />
))}
</div>
{
page.data.map((post) => (
<div>
<PostComponent post={post} />
</div>
))
}
{page.data.map((post) => (
<div>
<PostComponent post={post}/>
</div>
))}
<SWPaginator
maxPage={page.lastPage}
page={page.currentPage - 1}
@@ -67,6 +62,6 @@ const tags = await getTags();
previousUrl={page.url.prev}
firstUrl={page.url.first}
lastUrl={page.url.last}
pagesUrl={(i) => (i == 0 ? page.url.first : page.currentPage === page.lastPage ? page.url.current.replace(page.lastPage, i + 1) : page.url.last.replace(page.lastPage, i + 1))}
pagesUrl={(i) => i == 0 ? page.url.first : page.currentPage === page.lastPage ? page.url.current.replace(page.lastPage, i + 1) : page.url.last.replace(page.lastPage, i + 1)}
/>
</PageLayout>
</PageLayout>

View File

@@ -1,24 +1,24 @@
---
import { CollectionEntry } from "astro:content";
import { astroI18n, createGetStaticPaths, t } from "astro-i18n";
import { getCollection } from "astro:content";
import {CollectionEntry} from "astro:content";
import {astroI18n, createGetStaticPaths, t} from "astro-i18n";
import {getCollection} from "astro:content";
import PageLayout from "../../../layouts/PageLayout.astro";
import { capitalize } from "../../../components/admin/util";
import {capitalize} from "../../../components/admin/util";
import PostComponent from "../../../components/PostComponent.astro";
import dayjs from "dayjs";
import { ArrowLeftOutline } from "flowbite-svelte-icons";
import { l } from "../../../util/util";
import {l} from "../../../util/util";
import TagComponent from "../../../components/TagComponent.astro";
export const getStaticPaths = createGetStaticPaths(async () => {
let posts = await getCollection("announcements", (entry) => entry.id.split("/")[0] === astroI18n.locale);
let posts = (await getCollection("announcements", entry => entry.id.split("/")[0] === astroI18n.locale));
const germanPosts = await getCollection("announcements", (entry) => entry.id.split("/")[0] === "de");
const germanPosts = await getCollection("announcements", entry => entry.id.split("/")[0] === "de");
posts.sort((a, b) => dayjs(b.data.created).unix() - dayjs(a.data.created).unix());
germanPosts.forEach((value) => {
if (posts.find((post) => post.data.key === value.data.key)) {
germanPosts.forEach(value => {
if (posts.find(post => post.data.key === value.data.key)) {
return;
} else {
posts.push(value);
@@ -28,16 +28,16 @@ export const getStaticPaths = createGetStaticPaths(async () => {
posts = posts.filter((value, index) => index < 20);
let groupedByTags: Record<string, CollectionEntry<"announcements">[]> = {};
posts.forEach((post) => {
post.data.tags.forEach((tag) => {
if (!groupedByTags[tag.toLowerCase()]) {
groupedByTags[tag.toLowerCase()] = [];
posts.forEach(post => {
post.data.tags.forEach(tag => {
if (!groupedByTags[tag]) {
groupedByTags[tag] = [];
}
groupedByTags[tag.toLowerCase()].push(post);
groupedByTags[tag].push(post);
});
});
return Object.keys(groupedByTags).map((tag) => ({
return Object.keys(groupedByTags).map(tag => ({
params: {
tag: tag,
},
@@ -53,21 +53,19 @@ interface Props {
tag: string;
}
const { posts, tag } = Astro.props;
const {posts, tag} = Astro.props;
---
<PageLayout title={t("tag.title", { tag: capitalize(tag) })}>
<PageLayout title={t("tag.title", {tag: capitalize(tag)})}>
<div class="pb-2">
<a class="flex gap-2 items-center" href={l("/ankuendigungen")}>
<ArrowLeftOutline />
<TagComponent tag={tag} noLink="true" transition:name={`${tag}-tag-filter`} />
<TagComponent tag={tag} noLink="true" transition:name={`${tag}-tag-filter`}/>
</a>
</div>
{
posts.map((post, index) => (
<div>
<PostComponent post={post} />
</div>
))
}
</PageLayout>
{posts.map((post, index) => (
<div>
<PostComponent post={post}/>
</div>
))}
</PageLayout>

View File

@@ -1,87 +0,0 @@
---
import type { ExtendedEvent } from "@components/types/event";
import PageLayout from "@layouts/PageLayout.astro";
import { astroI18n, createGetStaticPaths } from "astro-i18n";
import { getCollection, type CollectionEntry } from "astro:content";
import EventFights from "@components/event/EventFights.svelte";
import TeamList from "@components/event/TeamList.svelte";
export const getStaticPaths = createGetStaticPaths(async () => {
const events = await Promise.all(
(await getCollection("events")).map(async (event) => ({
event: (await fetch(import.meta.env.PUBLIC_API_SERVER + "/events/" + event.data.eventId).then((value) => value.json())) as ExtendedEvent,
page: event,
}))
);
return events.map((event) => ({
props: {
event: event.event,
page: event.page,
},
params: {
slug: event.page.slug,
},
}));
});
const { event, page } = Astro.props as { event: ExtendedEvent; page: CollectionEntry<"events"> };
const { Content } = await page.render();
---
<PageLayout title={event.event.name} wide={true}>
<div>
<h1 class="text-2xl font-bold">{event.event.name}</h1>
<h2 class="text-md text-gray-300 mb-4">
{
new Date(event.event.start).toLocaleDateString(astroI18n.locale, {
year: "numeric",
month: "numeric",
day: "numeric",
})
}
{
new Date(event.event.start).toDateString() !== new Date(event.event.end).toDateString()
? ` - ${new Date(event.event.end).toLocaleDateString(astroI18n.locale, {
year: "numeric",
month: "numeric",
day: "numeric",
})}`
: ""
}
</h2>
</div>
<article>
<Content />
</article>
<TeamList client:load event={event} />
{
page.data.viewConfig && (
<div class="py-2 border-t border-t-gray-600">
<h1 class="text-2xl font-bold mb-4">Kampfplan</h1>
<EventFights viewConfig={page.data.viewConfig} event={event} client:load />
</div>
)
}
</PageLayout>
<style is:global>
article {
> * {
all: revert;
}
code {
@apply dark:text-neutral-400 text-neutral-800;
}
pre.astro-code {
@apply w-fit p-4 rounded-md border-2 border-gray-600 my-4;
}
a {
@apply text-neutral-800 dark:text-neutral-400 hover:underline;
}
}
</style>

View File

@@ -1,24 +0,0 @@
---
import type { ExtendedEvent } from "@components/types/event";
import PageLayout from "@layouts/PageLayout.astro";
import { getCollection } from "astro:content";
import EventPage from "@components/event/EventPage.svelte";
const events = await Promise.all(
(await getCollection("events")).map(async (event) => ({
...event,
data: {
...event.data,
event: (await fetch(
import.meta.env.PUBLIC_API_SERVER +
"/events/" +
event.data.eventId,
).then((value) => value.json())) as ExtendedEvent,
},
})),
);
---
<PageLayout title="Events">
<EventPage client:load {events} />
</PageLayout>

View File

@@ -1,21 +1,21 @@
---
import LoginComponent from "@components/Login.svelte";
import NavbarLayout from "@layouts/NavbarLayout.astro";
import { t } from "astro-i18n";
import {t} from "astro-i18n";
import BackgroundImage from "../components/BackgroundImage.astro";
---
<NavbarLayout title={t("login.page")}>
<script>
import { l } from "../util/util";
import { navigate } from "astro:transitions/client";
import { loggedIn } from "../components/repo/authv2";
import { get } from "svelte/store";
import {l} from "../util/util";
import {navigate} from "astro:transitions/client";
import {loggedIn} from "../components/repo/authv2";
import {get} from "svelte/store";
document.addEventListener("astro:page-load", () => {
if (window.location.href.endsWith("/login") || window.location.href.endsWith("/login/")) {
if (get(loggedIn)) {
navigate(l("/dashboard"), { history: "replace" });
navigate(l("/dashboard"), {history: "replace"});
}
}
});
@@ -23,8 +23,8 @@ import BackgroundImage from "../components/BackgroundImage.astro";
<div class="h-screen w-screen fixed -z-10">
<BackgroundImage />
</div>
<div class="h-screen mx-auto p-8 rounded-b-md pt-40 sm:pt-28 md:pt-14 flex flex-col justify-center items-center
dark:text-white" style="width: min(100vw, 75em);">
<LoginComponent client:load />
<div class="h-screen mx-auto p-8 rounded-b-md pt-40 sm:pt-28 md:pt-14 flex flex-col justify-center items-center
dark:text-white " style="width: min(100vw, 75em);">
<LoginComponent client:load/>
</div>
</NavbarLayout>
</NavbarLayout>