diff --git a/package.json b/package.json
index 3d1d2f4..d275bc2 100644
--- a/package.json
+++ b/package.json
@@ -1,83 +1,95 @@
{
- "name": "steamwar-website",
- "type": "module",
- "version": "0.0.1",
- "scripts": {
- "dev": "astro dev",
- "start": "astro dev",
- "build": "astro build",
- "preview": "astro preview",
- "astro": "astro",
- "i18n:extract": "astro-i18n extract",
- "i18n:generate:pages": "astro-i18n generate:pages --purge",
- "i18n:generate:types": "astro-i18n generate:types",
- "i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types",
- "clean:dist": "rm -rf dist",
- "clean:node_modules": "rm -rf node_modules",
- "ci": "pnpm install && pnpm run i18n:sync && pnpm run build"
- },
- "devDependencies": {
- "@astrojs/svelte": "^7.0.4",
- "@astrojs/tailwind": "^5.1.5",
- "@astropub/icons": "^0.2.0",
- "@internationalized/date": "^3.7.0",
- "@lucide/svelte": "^0.488.0",
- "@types/color": "^4.2.0",
- "@types/node": "^22.9.3",
- "@types/three": "^0.170.0",
- "@typescript-eslint/eslint-plugin": "^8.15.0",
- "@typescript-eslint/parser": "^8.15.0",
- "autoprefixer": "^10.4.20",
- "bits-ui": "1.3.4",
- "clsx": "^2.1.1",
- "cmdk-sv": "^0.0.18",
- "cssnano": "^7.0.6",
- "embla-carousel-svelte": "^8.5.2",
- "esbuild": "^0.24.0",
- "eslint": "^9.15.0",
- "eslint-plugin-astro": "^1.3.1",
- "eslint-plugin-jsx-a11y": "^6.10.2",
- "eslint-plugin-svelte": "^2.46.0",
- "formsnap": "1.0.1",
- "lucide-svelte": "^0.476.0",
- "mode-watcher": "^0.5.1",
- "paneforge": "^0.0.6",
- "postcss-nesting": "^13.0.1",
- "sass": "^1.81.0",
- "svelte": "^5.16.0",
- "svelte-sonner": "^0.3.28",
- "tailwind-merge": "^2.5.5",
- "tailwind-variants": "^0.3.1",
- "tailwindcss": "^3.4.15",
- "three": "^0.170.0",
- "typescript": "^5.7.2",
- "vaul-svelte": "^0.3.2",
- "zod": "^3.23.8"
- },
- "dependencies": {
- "@astrojs/mdx": "^4.0.7",
- "@astrojs/sitemap": "^3.2.1",
- "@codemirror/commands": "^6.8.0",
- "@codemirror/lang-json": "^6.0.1",
- "@ddietr/codemirror-themes": "^1.4.4",
- "@tanstack/table-core": "^8.21.2",
- "astro": "5.7.14",
- "astro-i18n": "^2.2.4",
- "astro-robots-txt": "^1.0.0",
- "astro-seo": "^0.8.4",
- "chart.js": "^4.4.6",
- "chartjs-adapter-dayjs-4": "^1.0.4",
- "chartjs-adapter-moment": "^1.0.1",
- "color": "^4.2.3",
- "dayjs": "^1.11.13",
- "easymde": "^2.18.0",
- "flowbite": "^2.5.2",
- "flowbite-svelte": "^0.47.3",
- "flowbite-svelte-icons": "^2.0.2",
- "qs": "^6.13.1",
- "sharp": "^0.33.5",
- "svelte-awesome": "^3.3.5",
- "svelte-codemirror-editor": "^1.4.1",
- "svelte-spa-router": "^4.0.1"
- }
+ "name": "steamwar-website",
+ "type": "module",
+ "version": "0.0.1",
+ "scripts": {
+ "dev": "astro dev",
+ "start": "astro dev",
+ "build": "astro build",
+ "preview": "astro preview",
+ "astro": "astro",
+ "i18n:extract": "astro-i18n extract",
+ "i18n:generate:pages": "astro-i18n generate:pages --purge",
+ "i18n:generate:types": "astro-i18n generate:types",
+ "i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types",
+ "clean:dist": "rm -rf dist",
+ "clean:node_modules": "rm -rf node_modules",
+ "ci": "pnpm install && pnpm run i18n:sync && pnpm run build"
+ },
+ "devDependencies": {
+ "@astrojs/svelte": "^7.1.0",
+ "@astrojs/tailwind": "^5.1.5",
+ "@astropub/icons": "^0.2.0",
+ "@internationalized/date": "^3.8.1",
+ "@lucide/svelte": "^0.488.0",
+ "@types/color": "^4.2.0",
+ "@types/js-yaml": "^4.0.9",
+ "@types/node": "^22.15.23",
+ "@types/three": "^0.170.0",
+ "@typescript-eslint/eslint-plugin": "^8.33.0",
+ "@typescript-eslint/parser": "^8.33.0",
+ "autoprefixer": "^10.4.21",
+ "bits-ui": "1.3.4",
+ "clsx": "^2.1.1",
+ "cmdk-sv": "^0.0.18",
+ "cssnano": "^7.0.7",
+ "embla-carousel-svelte": "^8.6.0",
+ "esbuild": "^0.24.2",
+ "eslint": "^9.27.0",
+ "eslint-plugin-astro": "^1.3.1",
+ "eslint-plugin-jsx-a11y": "^6.10.2",
+ "eslint-plugin-svelte": "^2.46.1",
+ "formsnap": "1.0.1",
+ "lucide-svelte": "^0.476.0",
+ "mode-watcher": "^0.5.1",
+ "paneforge": "^0.0.6",
+ "postcss-nesting": "^13.0.1",
+ "sass": "^1.89.0",
+ "svelte": "^5.33.4",
+ "svelte-sonner": "^0.3.28",
+ "tailwind-merge": "^2.6.0",
+ "tailwind-variants": "^0.3.1",
+ "tailwindcss": "^3.4.17",
+ "three": "^0.170.0",
+ "typescript": "^5.8.3",
+ "vaul-svelte": "^0.3.2",
+ "zod": "^3.25.31"
+ },
+ "dependencies": {
+ "@astrojs/mdx": "^4.3.0",
+ "@astrojs/sitemap": "^3.4.0",
+ "@codemirror/commands": "^6.8.1",
+ "@codemirror/lang-json": "^6.0.1",
+ "@codemirror/view": "^6.36.8",
+ "@ddietr/codemirror-themes": "^1.5.1",
+ "@tanstack/table-core": "^8.21.3",
+ "astro": "5.7.14",
+ "astro-i18n": "^2.2.4",
+ "astro-robots-txt": "^1.0.0",
+ "astro-seo": "^0.8.4",
+ "chart.js": "^4.4.9",
+ "chartjs-adapter-dayjs-4": "^1.0.4",
+ "chartjs-adapter-moment": "^1.0.1",
+ "codemirror": "^6.0.1",
+ "color": "^4.2.3",
+ "dayjs": "^1.11.13",
+ "easymde": "^2.20.0",
+ "flowbite": "^2.5.2",
+ "flowbite-svelte": "^0.47.4",
+ "flowbite-svelte-icons": "^2.2.0",
+ "js-yaml": "^4.1.0",
+ "qs": "^6.14.0",
+ "sharp": "^0.33.5",
+ "svelte-awesome": "^3.3.5",
+ "svelte-spa-router": "^4.0.1"
+ },
+ "pnpm": {
+ "ignoredBuiltDependencies": [
+ "esbuild"
+ ],
+ "onlyBuiltDependencies": [
+ "@parcel/watcher",
+ "sharp"
+ ]
+ }
}
diff --git a/src/components/FightTable.svelte b/src/components/FightTable.svelte
index 8102329..fbbe51c 100644
--- a/src/components/FightTable.svelte
+++ b/src/components/FightTable.svelte
@@ -19,25 +19,27 @@
-->
@@ -55,13 +57,15 @@
- {#each window(event.fights.filter(f => group === undefined ? true : f.group === group), rows) as fights}
+ {#each window( event.fights.filter((f) => (group === undefined ? true : f.group?.id === group)), rows ) as fights}
{#each fights as fight (fight.id)}
- {Intl.DateTimeFormat(astroI18n.locale, {
- hour: "numeric",
- minute: "numeric",
- }).format(new Date(fight.start))}
+ {Intl.DateTimeFormat(astroI18n.locale, {
+ hour: "numeric",
+ minute: "numeric",
+ }).format(new Date(fight.start))}
{fight.blueTeam.kuerzel}
{fight.redTeam.kuerzel}
{getWinner(fight)}
@@ -70,4 +74,4 @@
{/each}
-
\ No newline at end of file
+
diff --git a/src/components/GroupTable.svelte b/src/components/GroupTable.svelte
index 9b70167..9897329 100644
--- a/src/components/GroupTable.svelte
+++ b/src/components/GroupTable.svelte
@@ -19,33 +19,40 @@
-->
diff --git a/src/components/admin/pages/edit/Editor.svelte b/src/components/admin/pages/edit/Editor.svelte
index ef3f5e0..a4fcda3 100644
--- a/src/components/admin/pages/edit/Editor.svelte
+++ b/src/components/admin/pages/edit/Editor.svelte
@@ -18,23 +18,22 @@
-->
-
{
- if (dirty) {
- return "You have unsaved changes. Are you sure you want to leave?";
- }
-}}/>
+
+ {
+ if (dirty) {
+ return "You have unsaved changes. Are you sure you want to leave?";
+ }
+ }}
+/>
{#await pageFuture}
-
+
{:then p}
{#snippet end()}
-
-
- Delete
-
-
- Save
-
-
- {/snippet}
+
+ Delete
+ Save
+
+ {/snippet}
{#if page?.name.endsWith("md") || page?.name.endsWith("mdx")}
-
- {:else}
-
dirty = true}/>
- {/if}
+
+ {:else}{/if}
{:catch error}
{error.message}
-{/await}
\ No newline at end of file
+{/await}
diff --git a/src/components/dashboard/UserInfo.svelte b/src/components/dashboard/UserInfo.svelte
index aeb8a20..024a585 100644
--- a/src/components/dashboard/UserInfo.svelte
+++ b/src/components/dashboard/UserInfo.svelte
@@ -18,19 +18,19 @@
-->
\ No newline at end of file
+
+
diff --git a/src/components/moderator/components/FightEdit.svelte b/src/components/moderator/components/FightEdit.svelte
new file mode 100644
index 0000000..561dfa1
--- /dev/null
+++ b/src/components/moderator/components/FightEdit.svelte
@@ -0,0 +1,298 @@
+
+
+
+
Modus
+
+
+ {#snippet child({ props })}
+
+ {$gamemodes.find((value) => value === fightModus) || fightModus || "Select a modus type..."}
+
+
+ {/snippet}
+
+
+
+
+
+ No fight modus found.
+
+ {#each $gamemodes as modus}
+ {
+ fightModus = modus;
+ gamemodeSelectOpen = false;
+ }}
+ >
+
+ {modus}
+
+ {/each}
+
+
+
+
+
+
Map
+
+
+ {#snippet child({ props })}
+
+ {$mapsStore.find((value) => value === fightMap) || fightMap || "Select a map..."}
+
+
+ {/snippet}
+
+
+
+
+
+ No map found.
+
+ {#each $mapsStore as map}
+ {
+ fightMap = map;
+ mapSelectOpen = false;
+ }}
+ >
+
+ {map}
+
+ {/each}
+
+
+
+
+
+
Blue Team
+
+
+ {#snippet child({ props })}
+
+ {teams.find((value) => value.id === fightBlueTeam?.id)?.name || fightBlueTeam?.name || "Select a team..."}
+
+
+ {/snippet}
+
+
+
+
+
+ No team found.
+
+ {
+ fightBlueTeam = {
+ id: -1,
+ name: "?",
+ color: "7",
+ kuerzel: "?",
+ };
+ blueTeamSelectOpen = false;
+ }}
+ keywords={["?"]}>???
+ {
+ fightBlueTeam = {
+ id: 0,
+ name: "Public",
+ color: "7",
+ kuerzel: "PUB",
+ };
+ blueTeamSelectOpen = false;
+ }}
+ keywords={["PUB", "Public"]}>PUB
+
+
+ {#each teams as team}
+ {
+ fightBlueTeam = team;
+ blueTeamSelectOpen = false;
+ }}
+ >
+
+ {team.name}
+
+ {/each}
+
+
+
+
+
+
Red Team
+
+
+ {#snippet child({ props })}
+
+ {teams.find((value) => value.id === fightRedTeam?.id)?.name || fightRedTeam?.name || "Select a team..."}
+
+
+ {/snippet}
+
+
+
+
+
+ No team found.
+
+ {
+ fightRedTeam = {
+ id: -1,
+ name: "?",
+ color: "7",
+ kuerzel: "?",
+ };
+ redTeamSelectOpen = false;
+ }}
+ keywords={["?"]}>???
+ {
+ fightRedTeam = {
+ id: 0,
+ name: "Public",
+ color: "7",
+ kuerzel: "PUB",
+ };
+ redTeamSelectOpen = false;
+ }}
+ keywords={["PUB", "Public"]}>PUB
+
+
+ {#each teams as team}
+ {
+ fightRedTeam = team;
+ redTeamSelectOpen = false;
+ }}
+ >
+
+ {team.name}
+
+ {/each}
+
+
+
+
+
+
Start
+
+ {#if fight !== null}
+
Ergebnis
+
(fightErgebnis = +v)}>
+
+ {fightErgebnis === 0 ? "Unentschieden" : (fightErgebnis === 1 ? fightBlueTeam?.name : fightRedTeam?.name) + " gewinnt"}
+
+
+ Unentschieden
+ {fightBlueTeam?.name ?? "Team Blau"} gewinnt
+ {fightRedTeam?.name ?? "Team Blau"} gewinnt
+
+
+ {/if}
+
+
Gruppe
+
+
Spectate Port
+
+
+
+{@render actions(dirty && !loading, submit)}
diff --git a/src/components/moderator/components/GroupEdit.svelte b/src/components/moderator/components/GroupEdit.svelte
new file mode 100644
index 0000000..c5087d4
--- /dev/null
+++ b/src/components/moderator/components/GroupEdit.svelte
@@ -0,0 +1,78 @@
+
+
+
+ Name
+
+
+ Typ
+ {
+ if (v) groupType = v as "GROUP_STAGE" | "ELIMINATION_STAGE";
+ }}
+ >
+
+ {groupType === "GROUP_STAGE" ? "Gruppenphase" : "Eliminierungsphase"}
+
+
+ Gruppenphase
+ Eliminierungsphase
+
+
+
+ {#if groupType === "GROUP_STAGE" && group !== null}
+ Punkte pro Sieg
+
+
+ Punkte pro Niederlage
+
+
+ Punkte pro Unentschieden
+
+ {/if}
+
+
+{@render actions(group === null ? canSave : dirty, submit)}
diff --git a/src/components/moderator/components/GroupSelector.svelte b/src/components/moderator/components/GroupSelector.svelte
new file mode 100644
index 0000000..f4fe463
--- /dev/null
+++ b/src/components/moderator/components/GroupSelector.svelte
@@ -0,0 +1,103 @@
+
+
+
+
+
+ {#snippet child({ props })}
+
+ {selectedGroup?.name || "Keine Gruppe"}
+
+
+ {/snippet}
+
+
+
+
+
+
+ (createOpen = true)}>
+
+ Neue Gruppe
+
+
+
+ {
+ value = null;
+ groupSelectOpen = false;
+ }}
+ >
+ {#if value === null}
+
+ {:else}
+
+ {/if}
+ Keine Gruppe
+
+
+ {#each groups as group}
+ {
+ value = group.id;
+ groupSelectOpen = false;
+ }}
+ >
+
+ {group.name}
+
+ {/each}
+
+
+
+
+
+
+
+
+ Neue Gruppe erstellen
+ Hier kannst du eine neue Gruppe erstellen
+
+
+ {#snippet actions(dirty, submit)}
+
+ Speichern
+
+ {/snippet}
+
+
+
diff --git a/src/components/moderator/layout/NavLinks.svelte b/src/components/moderator/layout/NavLinks.svelte
index 2e4ff27..1b540e7 100644
--- a/src/components/moderator/layout/NavLinks.svelte
+++ b/src/components/moderator/layout/NavLinks.svelte
@@ -18,23 +18,13 @@
-->
-
- Dashboard
-
-
- Events
-
-
- Players
-
-
- Pages
-
-
- Schematics
-
-
\ No newline at end of file
+ Dashboard
+ Events
+ Players
+ Pages
+ Schematics
+
diff --git a/src/components/moderator/pages/event/Event.svelte b/src/components/moderator/pages/event/Event.svelte
index b9b8cf5..273e42c 100644
--- a/src/components/moderator/pages/event/Event.svelte
+++ b/src/components/moderator/pages/event/Event.svelte
@@ -18,8 +18,11 @@
-->
-{#await event}
+{#if loaded}
+
+{:else}
Loading...
-{:then data}
-
-{/await}
\ No newline at end of file
+{/if}
diff --git a/src/components/moderator/pages/event/EventEdit.svelte b/src/components/moderator/pages/event/EventEdit.svelte
index bb3d0f8..505db45 100644
--- a/src/components/moderator/pages/event/EventEdit.svelte
+++ b/src/components/moderator/pages/event/EventEdit.svelte
@@ -106,6 +106,15 @@
No schematic type found.
+ {
+ eventSchematicType = null;
+ }}
+ >
+
+ Keinen
+
{#each $schemTypes as type}
-
+
+
+
+ Fight Erstellen
+ Hier kannst du einen neuen Fight erstellen
+
+
+ {#snippet actions(dirty, submit)}
+
+ Speichern
+
+ {/snippet}
+
+
+
+
+{#if selectedGroup}
+
+{/if}
+
+{#if selectedGroupForResults}
+
+{/if}
+
+
+
+
+ Gruppe Ändern
+ Hier kannst du die Gruppe der ausgewählten Kämpfe ändern
+
+
+
+ {
+ groupChangeOpen = false;
+ let group = data.groups.find((g) => g.id === groupChangeSelected);
+ if (group) {
+ let selectedGroups = table.getSelectedRowModel().rows.map((row) => row.original);
+ for (const g of selectedGroups) {
+ await $fightRepo.updateFight(data.event.id, g.id, {
+ group: group.id,
+ spielmodus: null,
+ map: null,
+ blueTeam: null,
+ redTeam: null,
+ start: null,
+ spectatePort: null,
+ });
+ }
+
+ refresh();
+ }
+ }}>Speichern
+
+
+
+
+
Mehrfach Bearbeiten
- Gruppe Ändern
+ (groupChangeOpen = true)}>Gruppe Ändern
Startzeit Verschieben
Spectate Port Ändern
@@ -97,7 +200,7 @@
Erstellen
- Fight Erstellen
+ (createOpen = true)}>Fight Erstellen
Generatoren
Gruppenphase
@@ -105,7 +208,24 @@
+
+ Gruppen
+
+ {#each data.groups as group (group.id)}
+
+
+ {group.name}
+
+
+ openGroupEditDialog(group)}>Bearbeiten
+ openGroupResultsDialog(group)}>Gruppen Ergebnisse
+
+
+ {/each}
+
+
+ Neu laden
@@ -119,21 +239,48 @@
{/if}
{/each}
+
{/each}
{#each table.getRowModel().rows as groupRow (groupRow.id)}
{#if groupRow.getIsGrouped()}
-
-
+ {@const group = data.groups.find((g) => g.id == groupRow.getValue("group"))}
+
+
groupRow.toggleSelected()}
class="mr-4"
/>
- Gruppe: {groupRow.getValue("group") ?? "Keine"}
+ {group?.name ?? "Keine Gruppe"}
+
+
+ openGroupEditDialog(group)}>
+
+
+ openGroupResultsDialog(group)}>
+
+
+
+
+
+
+
+
+
+ navigator.clipboard.writeText(` `)}
+ >Punkte Tabelle
+ navigator.clipboard.writeText(` `)}
+ >Kampf Tabelle
+
+
{#each groupRow.subRows as row (row.id)}
@@ -143,6 +290,15 @@
{/each}
+
+ (data.fights = data.fights.map((v) => (v.id === update.id ? update : v)))}
+ >
+
{/each}
{:else}
diff --git a/src/components/moderator/pages/event/EventView.svelte b/src/components/moderator/pages/event/EventView.svelte
index 223a4f5..0b6bedd 100644
--- a/src/components/moderator/pages/event/EventView.svelte
+++ b/src/components/moderator/pages/event/EventView.svelte
@@ -18,13 +18,13 @@
-->
@@ -35,12 +35,12 @@
Teams
-
+
Referees
-
+
diff --git a/src/components/moderator/pages/event/FightEditRow.svelte b/src/components/moderator/pages/event/FightEditRow.svelte
new file mode 100644
index 0000000..2ecbdbe
--- /dev/null
+++ b/src/components/moderator/pages/event/FightEditRow.svelte
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+ Fight bearbeiten
+ Hier kannst du die Daten des Kampfes bearbeiten.
+
+
+ {#snippet actions(dirty, submit)}
+
+ Speichern
+
+ {/snippet}
+
+
+
+
diff --git a/src/components/moderator/pages/event/GroupEditDialog.svelte b/src/components/moderator/pages/event/GroupEditDialog.svelte
new file mode 100644
index 0000000..1a52863
--- /dev/null
+++ b/src/components/moderator/pages/event/GroupEditDialog.svelte
@@ -0,0 +1,45 @@
+
+
+{#if group}
+
+
+
+ Gruppe Bearbeiten: {group.name}
+ Hier kannst du die Gruppendetails bearbeiten.
+
+
+ {#snippet actions(dirty, submit)}
+
+ Löschen
+
+ (open = false)}>Abbrechen
+ Speichern
+
+
+ {/snippet}
+
+
+
+{/if}
diff --git a/src/components/moderator/pages/event/GroupResultsDialog.svelte b/src/components/moderator/pages/event/GroupResultsDialog.svelte
new file mode 100644
index 0000000..cf626f0
--- /dev/null
+++ b/src/components/moderator/pages/event/GroupResultsDialog.svelte
@@ -0,0 +1,48 @@
+
+
+
+
+
+ Ergebnisse: {group?.name}
+
+ Punkte: Sieg: {group?.pointsPerWin}, Unentschieden: {group?.pointsPerDraw}, Niederlage: {group?.pointsPerLoss}
+
+
+ {#if group.points !== null}
+
+
+
+ Team
+ Spiele
+ Punkte
+
+
+
+ {#each Object.entries(group.points).toSorted((a, b) => b[1] - a[1]) as [teamIdString, points] (teamIdString)}
+ {@const teamId = Number(teamIdString)}
+ {@const team = teams.find((t) => t.id === teamId) as ResponseTeam | undefined}
+ {@const playedGames = fights.filter((f) => f.hasFinished && f.group?.id === group.id && (f.blueTeam.id === teamId || f.redTeam.id === teamId)).length}
+
+ {team?.name ?? "?"} ({team?.kuerzel ?? "?"})
+ {playedGames}
+ {points}
+
+ {/each}
+
+
+ {:else}
+ Noch keine Ergebnisse für diese Gruppe vorhanden oder keine Spiele zugeordnet.
+ {/if}
+
+ (open = false)}>Schließen
+
+
+
diff --git a/src/components/moderator/pages/event/RefereesList.svelte b/src/components/moderator/pages/event/RefereesList.svelte
index b4d9556..63ee74b 100644
--- a/src/components/moderator/pages/event/RefereesList.svelte
+++ b/src/components/moderator/pages/event/RefereesList.svelte
@@ -28,22 +28,16 @@
const { event }: { event: ExtendedEvent } = $props();
- let referees = $state(event.event.referees);
+ let referees = $state(event.referees);
async function addReferee(value: string) {
- referees = (
- await $eventRepo.updateEvent(event.event.id.toString(), {
- addReferee: [value],
- })
- ).referees;
+ await $eventRepo.updateReferees(event.event.id.toString(), [value]);
+ referees = await $eventRepo.listReferees(event.event.id.toString());
}
async function removeReferee(value: string) {
- referees = (
- await $eventRepo.updateEvent(event.event.id.toString(), {
- removeReferee: [value],
- })
- ).referees;
+ await $eventRepo.deleteReferees(event.event.id.toString(), [value]);
+ referees = await $eventRepo.listReferees(event.event.id.toString());
}
let playerSearch = $state("");
@@ -61,7 +55,7 @@
{referee.name}
- removeReferee(referee.uuid)}>Remove
+ removeReferee(referee.uuid)} variant="outline" size="sm">{referee.name} entfernen
{/each}
@@ -69,7 +63,7 @@
- Add
+ Hinzufügen
diff --git a/src/components/moderator/pages/event/TeamTable.svelte b/src/components/moderator/pages/event/TeamTable.svelte
index 111be01..14463f8 100644
--- a/src/components/moderator/pages/event/TeamTable.svelte
+++ b/src/components/moderator/pages/event/TeamTable.svelte
@@ -21,14 +21,29 @@
import { Button } from "@components/ui/button";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell, TableCaption } from "@components/ui/table";
import type { ExtendedEvent } from "@type/event.ts";
+ import { eventRepo } from "@repo/event";
+ import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
+ import { teams } from "@components/stores/stores";
+ import type { Team } from "@components/types/team";
+ import type { EventModel } from "./eventmodel.svelte";
- const { event }: { event: ExtendedEvent } = $props();
+ let { event = $bindable() }: { event: EventModel } = $props();
+
+ async function addTeam(value: number) {
+ await $eventRepo.updateTeams(event.event.id.toString(), [value]);
+ event.teams = await $eventRepo.listTeams(event.event.id.toString());
+ }
+
+ async function removeTeam(value: number) {
+ await $eventRepo.deleteTeams(event.event.id.toString(), [value]);
+ event.teams = await $eventRepo.listTeams(event.event.id.toString());
+ }
+
+ let teamSearch = $state("");
-
- Add Team
-
Team
@@ -37,12 +52,12 @@
- {#each event.teams as team (team.id)}
+ {#each event.teams as t (t.id)}
- {team.kuerzel}
- {team.name}
+ {t.kuerzel}
+ {t.name}
- Remove
+ removeTeam(t.id)} variant="outline" size="sm">{t.name} abmelden
{/each}
@@ -52,4 +67,27 @@
{/if}
+
+
+
+ Team Anmelden
+
+
+
+
+
+
+ No teams found :(
+
+ {#each $teams
+ .filter((v) => v.name.includes(teamSearch))
+ .filter((v) => !event.teams.some((k) => k.id === v.id))
+ .filter((v, i) => i < 50) as t (t.id)}
+ addTeam(t.id)} keywords={[t.name, t.kuerzel]}>{t.name}
+ {/each}
+
+
+
+
+
diff --git a/src/components/moderator/pages/event/columns.ts b/src/components/moderator/pages/event/columns.ts
index 527d2c5..a3d0eb3 100644
--- a/src/components/moderator/pages/event/columns.ts
+++ b/src/components/moderator/pages/event/columns.ts
@@ -63,7 +63,7 @@ export const columns: ColumnDef = [
},
{
header: "Gruppe",
- accessorKey: "group",
+ accessorKey: "group.id",
id: "group",
},
{
@@ -77,4 +77,28 @@ export const columns: ColumnDef = [
});
},
},
+ {
+ header: "Spielmodus",
+ accessorKey: "spielmodus",
+ },
+ {
+ header: "Map",
+ accessorKey: "map",
+ },
+ {
+ header: "Ergebnis",
+ accessorKey: "ergebnis",
+ cell: ({ row }) => {
+ const fight = row.original;
+ if (!fight.hasFinished) {
+ return "Noch nicht gespielt";
+ } else if (fight.ergebnis === 1) {
+ return fight.blueTeam.name + " hat gewonnen";
+ } else if (fight.ergebnis === 2) {
+ return fight.redTeam.name + " hat gewonnen";
+ } else {
+ return "Unentschieden";
+ }
+ },
+ },
];
diff --git a/src/components/moderator/pages/event/eventmodel.svelte.ts b/src/components/moderator/pages/event/eventmodel.svelte.ts
new file mode 100644
index 0000000..731fbc3
--- /dev/null
+++ b/src/components/moderator/pages/event/eventmodel.svelte.ts
@@ -0,0 +1,21 @@
+import type { ResponseUser } from "@components/repo/event";
+import type { EventFight, ExtendedEvent, ResponseGroups, ResponseRelation, SWEvent } from "@components/types/event";
+import type { Team } from "@components/types/team";
+
+export class EventModel {
+ public event: SWEvent = $state({} as SWEvent);
+ public teams: Array = $state([]);
+ public groups: Array = $state([]);
+ public fights: Array = $state([]);
+ public referees: Array = $state([]);
+ public relations: Array = $state([]);
+
+ constructor(data: ExtendedEvent) {
+ this.event = data.event;
+ this.teams = data.teams;
+ this.groups = data.groups;
+ this.fights = data.fights;
+ this.referees = data.referees;
+ this.relations = data.relations;
+ }
+}
diff --git a/src/components/moderator/pages/events/Events.svelte b/src/components/moderator/pages/events/Events.svelte
index e039b80..17340ca 100644
--- a/src/components/moderator/pages/events/Events.svelte
+++ b/src/components/moderator/pages/events/Events.svelte
@@ -20,12 +20,136 @@
-
+
+
+
Events
+
+
+ {#snippet child({ props })}
+
+
+ Create Event
+
+ {/snippet}
+
+
+
+ Create New Event
+ Fill in the details for the new event. Click create when you're done.
+
+
+
+ Name
+
+
+
+
+ {#if errorMsg}
+
{errorMsg}
+ {/if}
+
+
+
+ {#snippet child({ props })}
+ Cancel
+ {/snippet}
+
+
+ {#if isSubmitting}
+ Creating...
+ {:else}
+ Create Event
+ {/if}
+
+
+
+
+
+
{#await eventsFuture}
Loading...
{:then events}
@@ -45,7 +169,5 @@
{/each}
- {:catch e}
-
{/await}
-
\ No newline at end of file
+
diff --git a/src/components/moderator/pages/pages/EditorWithTabs.svelte b/src/components/moderator/pages/pages/EditorWithTabs.svelte
new file mode 100644
index 0000000..a9e17da
--- /dev/null
+++ b/src/components/moderator/pages/pages/EditorWithTabs.svelte
@@ -0,0 +1,116 @@
+
+
+
+
+ {#each manager.pages as tab, index}
+ {@const isActive = manager.openPageIndex === index}
+ (manager.openPageIndex = index)}
+ >
+
+ {tab.pageTitle}
+ {
+ e.stopPropagation();
+ manager.closePage(index);
+ }}>
+
+ {/each}
+
+
+
+ {#if manager.selectedPage}
+
+ manager.selectedPage?.save()}>Speichern
+
+
+ {#if manager.selectedPage.path.startsWith("src/content/announcements/")}
+
+
+
+ {/if}
+
+ {/if}
+
+
+
diff --git a/src/components/moderator/pages/pages/FrontmatterEditor.svelte b/src/components/moderator/pages/pages/FrontmatterEditor.svelte
new file mode 100644
index 0000000..0c889e4
--- /dev/null
+++ b/src/components/moderator/pages/pages/FrontmatterEditor.svelte
@@ -0,0 +1,122 @@
+
+
+
+
+ Frontmatter
+
+
+
+
+
+ {#each Object.entries(manager.selectedPage?.frontmatter || {}) as [key, value]}
+
+ {/each}
+
+ {
+ manager.selectedPage!.frontmatter[`new_key_${Object.keys(manager.selectedPage!.frontmatter).length}`] = "";
+ manager.selectedPage!.dirty = true;
+ }}
+ class="text-sm text-blue-500 hover:text-blue-700"
+ >
+ + Add field
+
+ {
+ manager.selectedPage!.frontmatter[`new_array_${Object.keys(manager.selectedPage!.frontmatter).length}`] = [];
+ manager.selectedPage!.dirty = true;
+ }}
+ class="text-sm text-green-500 hover:text-green-700"
+ >
+ + Add array
+
+
+
+
diff --git a/src/components/moderator/pages/pages/Pages.svelte b/src/components/moderator/pages/pages/Pages.svelte
new file mode 100644
index 0000000..c71a8ec
--- /dev/null
+++ b/src/components/moderator/pages/pages/Pages.svelte
@@ -0,0 +1,155 @@
+
+
+
diff --git a/src/components/moderator/pages/pages/PagesList.svelte b/src/components/moderator/pages/pages/PagesList.svelte
new file mode 100644
index 0000000..5a730fd
--- /dev/null
+++ b/src/components/moderator/pages/pages/PagesList.svelte
@@ -0,0 +1,116 @@
+
+
+ (open = !open)}>
+
+ {#if open}
+
+ {:else}
+
+ {/if}
+
+ {page.name}/
+
+
+
+
+
+
+
+
+{#if open}
+
+
+ {#if newPage}
+
+ {#if newPageName.endsWith(".json")}
+
+ {:else if newPageName.endsWith(".md") || newPageName.endsWith(".mdx")}
+
+ {:else}
+
+ {/if}
+
+
+ {/if}
+ {#each Object.values(page.dirs) as subPage (subPage.name)}
+
+ {/each}
+ {#each Object.values(page.files) as file (file.id)}
+
manager.openPage(file.id)}>
+ {#if file.name.endsWith(".json")}
+
+ {:else if file.name.endsWith(".md") || file.name.endsWith(".mdx")}
+
+ {:else}
+
+ {/if}
+ {file.name}
+
+ {/each}
+
+
+{/if}
diff --git a/src/components/moderator/pages/pages/page.svelte.ts b/src/components/moderator/pages/pages/page.svelte.ts
new file mode 100644
index 0000000..1487f77
--- /dev/null
+++ b/src/components/moderator/pages/pages/page.svelte.ts
@@ -0,0 +1,228 @@
+import { base64ToBytes } from "@components/admin/util";
+import { pageRepo } from "@components/repo/page";
+import type { ListPage, PageList } from "@components/types/page";
+import { get } from "svelte/store";
+import yaml from "js-yaml";
+
+export class OpenEditPage {
+ public content: string = "";
+ public frontmatter: { [key: string]: string | string[] | Date } = $state({});
+ public dirty: boolean = $state(false);
+
+ public readonly fileType: string;
+
+ public constructor(
+ private manager: PageManager,
+ public readonly pageId: number,
+ public readonly pageTitle: string,
+ public readonly sha: string,
+ public readonly originalContent: string,
+ public readonly path: string
+ ) {
+ this.fileType = this.path.split(".").pop() || "md";
+
+ this.content = this.removeFrontmatter(originalContent);
+ this.frontmatter = this.parseFrontmatter(originalContent);
+ }
+
+ public async save(): Promise {
+ if (!this.dirty) {
+ return;
+ }
+
+ let contentToSave = "";
+ if (this.frontmatter) {
+ contentToSave += "---\n";
+ contentToSave += yaml.dump(this.frontmatter);
+ contentToSave += "---\n\n";
+ }
+ contentToSave += this.content;
+ const encodedContent = btoa(new TextEncoder().encode(contentToSave).reduce((data, byte) => data + String.fromCharCode(byte), ""));
+
+ console.log(encodedContent);
+ //await get(pageRepo).updatePage(this.pageId, this.sha, encodedContent, this.manager.branch);
+ this.dirty = false;
+ this.manager.reloadImages();
+ }
+
+ public focus(): boolean {
+ let index = this.manager.pages.indexOf(this);
+
+ if (index === this.manager.openPageIndex) {
+ return true;
+ }
+
+ this.manager.openPageIndex = this.manager.pages.indexOf(this);
+ return false;
+ }
+
+ private parseFrontmatter(content: string): { [key: string]: string | string[] | Date } {
+ const lines = content.split("\n");
+ let inFrontmatter = false;
+ const frontmatterLines: string[] = [];
+
+ for (const line of lines) {
+ if (line.trim() === "---") {
+ if (inFrontmatter) {
+ break; // End of frontmatter
+ }
+ inFrontmatter = true;
+ continue;
+ }
+ if (inFrontmatter) {
+ frontmatterLines.push(line);
+ }
+ }
+
+ if (frontmatterLines.length === 0) {
+ return {};
+ }
+
+ try {
+ // You'll need to install js-yaml: npm install js-yaml @types/js-yaml
+ return (yaml.load(frontmatterLines.join("\n")) || {}) as { [key: string]: string | string[] | Date };
+ } catch (error) {
+ console.error("Failed to parse YAML frontmatter:", error);
+ return {};
+ }
+ }
+
+ private removeFrontmatter(content: string): string {
+ const lines = content.split("\n");
+ let inFrontmatter = false;
+ const result: string[] = [];
+
+ for (const line of lines) {
+ if (line.trim() === "---") {
+ inFrontmatter = !inFrontmatter;
+ continue;
+ }
+ if (!inFrontmatter) {
+ result.push(line);
+ }
+ }
+
+ return result.join("\n").trim();
+ }
+}
+
+export interface DirTree {
+ name: string;
+ dirs: { [key: string]: DirTree };
+ files: { [key: string]: ListPage };
+}
+
+export class PageManager {
+ public reloadImages() {
+ this.updater = this.updater + 1;
+ }
+ public branch: string = $state("master");
+ public pages: OpenEditPage[] = $state([]);
+ public branches: string[] = $state([]);
+
+ constructor() {
+ this.reloadBranches();
+ }
+
+ public reloadBranches() {
+ get(pageRepo)
+ .getBranches()
+ .then((branches) => {
+ this.branches = branches;
+ });
+ }
+
+ private updater = $state(0);
+
+ public openPageIndex: number = $state(-1);
+ public pagesLoad = $derived(get(pageRepo).listPages(this.branch).then(this.convertToTree).then(this._t(this.updater)));
+ public imagesLoad = $derived(get(pageRepo).listImages(this.branch).then(this._t(this.updater)));
+
+ private _t(n: number): (v: T) => T {
+ return (v: T) => v;
+ }
+
+ public selectedPage = $derived(this.openPageIndex >= 0 ? this.pages[this.openPageIndex] : undefined);
+
+ private convertToTree(pages: PageList): DirTree {
+ const tree: DirTree = { dirs: {}, files: {}, name: "/" };
+
+ pages.forEach((page) => {
+ const pathParts = page.path.split("/").filter((part) => part !== "");
+ let current = tree;
+
+ // Navigate/create directory structure
+ for (let i = 0; i < pathParts.length - 1; i++) {
+ const dir = pathParts[i];
+ if (!current.dirs[dir]) {
+ current.dirs[dir] = { dirs: {}, files: {}, name: dir };
+ }
+ current = current.dirs[dir];
+ }
+
+ // Add file to the final directory
+ const fileName = pathParts[pathParts.length - 1];
+ current.files[fileName] = page;
+ });
+
+ return tree;
+ }
+
+ public async openPage(pageId: number) {
+ const existingPage = this.existingPage(pageId);
+ if (existingPage) {
+ existingPage.focus();
+ return;
+ }
+
+ let r = await get(pageRepo).getPage(pageId, this.branch);
+ if (!r) {
+ return;
+ }
+
+ const newPage = new OpenEditPage(this, pageId, r.name, r.sha, new TextDecoder().decode(base64ToBytes(r.content)), r.path);
+ this.pages.push(newPage);
+ newPage.focus();
+ }
+
+ public existingPage(pageId: number): OpenEditPage | undefined {
+ return this.pages.find((page) => page.pageId === pageId);
+ }
+
+ public closePage(index: number) {
+ if (index < 0 || index >= this.pages.length) {
+ return;
+ }
+
+ const page = this.pages[index];
+ if (page.dirty) {
+ if (!confirm(`The page "${page.pageTitle}" has unsaved changes. Are you sure you want to close it?`)) {
+ return;
+ }
+ }
+
+ this.pages.splice(index, 1);
+ if (this.openPageIndex >= index) {
+ this.openPageIndex = Math.max(0, this.openPageIndex - 1);
+ }
+
+ if (this.openPageIndex < 0 && this.pages.length > 0) {
+ this.openPageIndex = 0;
+ }
+
+ if (this.pages.length === 0) {
+ this.openPageIndex = -1;
+ }
+ }
+
+ public async createPage(path: string, newPageName: string): Promise {
+ await get(pageRepo).createFile(path, this.branch, newPageName, newPageName);
+ this.branch = this.branch;
+ }
+
+ public anyUnsavedChanges() {
+ return this.pages.some((page) => page.dirty);
+ }
+}
+
+export const manager = $state(new PageManager());
diff --git a/src/components/repo/data.ts b/src/components/repo/data.ts
index 04dea11..b6a87db 100644
--- a/src/components/repo/data.ts
+++ b/src/components/repo/data.ts
@@ -17,26 +17,38 @@
* along with this program. If not, see .
*/
-import type {Player, Server} from "@type/data.ts";
-import {PlayerSchema, ServerSchema} from "@type/data.ts";
-import {fetchWithToken, tokenStore} from "./repo.ts";
-import {derived, get} from "svelte/store";
+import type { Player, Server } from "@type/data.ts";
+import { PlayerSchema, ServerSchema } from "@type/data.ts";
+import { fetchWithToken, tokenStore } from "./repo.ts";
+import { derived, get } from "svelte/store";
+import { TeamSchema, type Team } from "@components/types/team.ts";
export class DataRepo {
- constructor(private token: string) {
- }
+ constructor(private token: string) {}
public async getServer(): Promise {
- return await fetchWithToken(this.token, "/data/server").then(value => value.json()).then(ServerSchema.parse);
+ return await fetchWithToken(this.token, "/data/server")
+ .then((value) => value.json())
+ .then(ServerSchema.parse);
}
public async getMe(): Promise {
- return await fetchWithToken(this.token, "/data/me").then(value => value.json()).then(PlayerSchema.parse);
+ return await fetchWithToken(this.token, "/data/me")
+ .then((value) => value.json())
+ .then(PlayerSchema.parse);
}
public async getPlayers(): Promise {
- return await fetchWithToken(get(tokenStore), "/data/admin/users").then(value => value.json()).then(PlayerSchema.array().parse);
+ return await fetchWithToken(get(tokenStore), "/data/admin/users")
+ .then((value) => value.json())
+ .then(PlayerSchema.array().parse);
+ }
+
+ public async getTeams(): Promise {
+ return await fetchWithToken(get(tokenStore), "/data/admin/teams")
+ .then((value) => value.json())
+ .then(TeamSchema.array().parse);
}
}
-export const dataRepo = derived(tokenStore, ($token) => new DataRepo($token));
\ No newline at end of file
+export const dataRepo = derived(tokenStore, ($token) => new DataRepo($token));
diff --git a/src/components/repo/event.ts b/src/components/repo/event.ts
index 5f4f1dd..334f6a3 100644
--- a/src/components/repo/event.ts
+++ b/src/components/repo/event.ts
@@ -17,12 +17,26 @@
* along with this program. If not, see .
*/
-import type {ExtendedEvent, ShortEvent, SWEvent} from "@type/event";
-import {fetchWithToken, tokenStore} from "./repo";
-import {ExtendedEventSchema, ShortEventSchema, SWEventSchema} from "@type/event.js";
-import {z} from "zod";
-import type {Dayjs} from "dayjs";
-import {derived} from "svelte/store";
+import type { ExtendedEvent, ShortEvent, SWEvent, EventFight, ResponseGroups, ResponseRelation, ResponseTeam } from "@type/event";
+import { fetchWithToken, tokenStore } from "./repo";
+import {
+ ExtendedEventSchema,
+ ShortEventSchema,
+ SWEventSchema,
+ EventFightSchema,
+ ResponseGroupsSchema,
+ ResponseRelationSchema,
+ ResponseTeamSchema,
+ CreateEventGroupSchema,
+ UpdateEventGroupSchema,
+ CreateEventRelationSchema,
+ UpdateEventRelationSchema,
+} from "@type/event.js";
+import type { CreateEventGroup, UpdateEventGroup, CreateEventRelation, UpdateEventRelation } from "@type/event.js";
+import { z } from "zod";
+import type { Dayjs } from "dayjs";
+import { derived } from "svelte/store";
+import { ResponseUserSchema } from "@components/types/data";
export interface CreateEvent {
name: string;
@@ -42,19 +56,25 @@ export interface UpdateEvent {
removeReferee?: string[] | null;
}
+export interface ResponseUser {
+ name: string;
+ uuid: string;
+ prefix: string;
+ perms: string[];
+}
+
export class EventRepo {
- constructor(private token: string) {
- }
+ constructor(private token: string) {}
public async listEvents(): Promise {
return await fetchWithToken(this.token, "/events")
- .then(value => value.json())
- .then(value => z.array(ShortEventSchema).parse(value));
+ .then((value) => value.json())
+ .then((value) => z.array(ShortEventSchema).parse(value));
}
public async getEvent(id: string): Promise {
return await fetchWithToken(this.token, `/events/${id}`)
- .then(value => value.json())
+ .then((value) => value.json())
.then(ExtendedEventSchema.parse);
}
@@ -66,7 +86,8 @@ export class EventRepo {
start: +event.start,
end: +event.end,
}),
- }).then(value => value.json())
+ })
+ .then((value) => value.json())
.then(SWEventSchema.parse);
}
@@ -87,7 +108,8 @@ export class EventRepo {
headers: {
"Content-Type": "application/json",
},
- }).then(value => value.json())
+ })
+ .then((value) => value.json())
.then(SWEventSchema.parse);
}
@@ -98,6 +120,154 @@ export class EventRepo {
return res.ok;
}
+
+ // Fights
+ public async listFights(eventId: string): Promise {
+ return await fetchWithToken(this.token, `/events/${eventId}/fights`)
+ .then((value) => value.json())
+ .then((value) => z.array(EventFightSchema).parse(value));
+ }
+ public async createFight(eventId: string, fight: any): Promise {
+ delete fight.ergebnis;
+ return await fetchWithToken(this.token, `/events/${eventId}/fights`, {
+ method: "POST",
+ body: JSON.stringify(fight),
+ headers: { "Content-Type": "application/json" },
+ })
+ .then((value) => value.json())
+ .then(EventFightSchema.parse);
+ }
+ public async deleteFight(eventId: string, fightId: string): Promise {
+ const res = await fetchWithToken(this.token, `/events/${eventId}/fights/${fightId}`, {
+ method: "DELETE",
+ });
+ return res.ok;
+ }
+
+ // Groups
+ public async listGroups(eventId: string): Promise {
+ return await fetchWithToken(this.token, `/events/${eventId}/groups`)
+ .then((value) => value.json())
+ .then((value) => z.array(ResponseGroupsSchema).parse(value));
+ }
+ public async createGroup(eventId: string, group: CreateEventGroup): Promise {
+ CreateEventGroupSchema.parse(group);
+ return await fetchWithToken(this.token, `/events/${eventId}/groups`, {
+ method: "POST",
+ body: JSON.stringify({
+ name: group.name,
+ type: group.type,
+ }),
+ headers: { "Content-Type": "application/json" },
+ })
+ .then((value) => value.json())
+ .then(ResponseGroupsSchema.parse);
+ }
+ public async getGroup(eventId: string, groupId: string): Promise {
+ return await fetchWithToken(this.token, `/events/${eventId}/groups/${groupId}`)
+ .then((value) => value.json())
+ .then(ResponseGroupsSchema.parse);
+ }
+ public async updateGroup(eventId: string, groupId: string, group: UpdateEventGroup): Promise {
+ UpdateEventGroupSchema.parse(group);
+ return await fetchWithToken(this.token, `/events/${eventId}/groups/${groupId}`, {
+ method: "PUT",
+ body: JSON.stringify(group),
+ headers: { "Content-Type": "application/json" },
+ })
+ .then((value) => value.json())
+ .then(ResponseGroupsSchema.parse);
+ }
+ public async deleteGroup(eventId: string, groupId: string): Promise {
+ const res = await fetchWithToken(this.token, `/events/${eventId}/groups/${groupId}`, {
+ method: "DELETE",
+ });
+ return res.ok;
+ }
+
+ // Relations
+ public async listRelations(eventId: string): Promise {
+ return await fetchWithToken(this.token, `/events/${eventId}/relations`)
+ .then((value) => value.json())
+ .then((value) => z.array(ResponseRelationSchema).parse(value));
+ }
+ public async createRelation(eventId: string, relation: CreateEventRelation): Promise {
+ CreateEventRelationSchema.parse(relation);
+ return await fetchWithToken(this.token, `/events/${eventId}/relations`, {
+ method: "POST",
+ body: JSON.stringify(relation),
+ headers: { "Content-Type": "application/json" },
+ })
+ .then((value) => value.json())
+ .then(ResponseRelationSchema.parse);
+ }
+ public async getRelation(eventId: string, relationId: string): Promise {
+ return await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`)
+ .then((value) => value.json())
+ .then(ResponseRelationSchema.parse);
+ }
+ public async updateRelation(eventId: string, relationId: string, relation: UpdateEventRelation): Promise {
+ UpdateEventRelationSchema.parse(relation);
+ return await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, {
+ method: "PUT",
+ body: JSON.stringify(relation),
+ headers: { "Content-Type": "application/json" },
+ })
+ .then((value) => value.json())
+ .then(ResponseRelationSchema.parse);
+ }
+ public async deleteRelation(eventId: string, relationId: string): Promise {
+ const res = await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, {
+ method: "DELETE",
+ });
+ return res.ok;
+ }
+
+ // Teams
+ public async listTeams(eventId: string): Promise {
+ return await fetchWithToken(this.token, `/events/${eventId}/teams`)
+ .then((value) => value.json())
+ .then((value) => z.array(ResponseTeamSchema).parse(value));
+ }
+ public async updateTeams(eventId: string, teams: number[]): Promise {
+ const res = await fetchWithToken(this.token, `/events/${eventId}/teams`, {
+ method: "PUT",
+ body: JSON.stringify(teams),
+ headers: { "Content-Type": "application/json" },
+ });
+ return res.ok;
+ }
+ public async deleteTeams(eventId: string, teams: number[]): Promise {
+ const res = await fetchWithToken(this.token, `/events/${eventId}/teams`, {
+ method: "DELETE",
+ body: JSON.stringify(teams),
+ headers: { "Content-Type": "application/json" },
+ });
+ return res.ok;
+ }
+
+ // Referees
+ public async listReferees(eventId: string): Promise {
+ return await fetchWithToken(this.token, `/events/${eventId}/referees`)
+ .then((value) => value.json())
+ .then((value) => z.array(ResponseUserSchema).parse(value));
+ }
+ public async updateReferees(eventId: string, refereeUuids: string[]): Promise {
+ const res = await fetchWithToken(this.token, `/events/${eventId}/referees`, {
+ method: "PUT",
+ body: JSON.stringify(refereeUuids),
+ headers: { "Content-Type": "application/json" },
+ });
+ return res.status === 204;
+ }
+ public async deleteReferees(eventId: string, refereeUuids: string[]): Promise {
+ const res = await fetchWithToken(this.token, `/events/${eventId}/referees`, {
+ method: "DELETE",
+ body: JSON.stringify(refereeUuids),
+ headers: { "Content-Type": "application/json" },
+ });
+ return res.status === 204;
+ }
}
export const eventRepo = derived(tokenStore, ($token) => new EventRepo($token));
diff --git a/src/components/repo/fight.ts b/src/components/repo/fight.ts
index a3d3a91..7020217 100644
--- a/src/components/repo/fight.ts
+++ b/src/components/repo/fight.ts
@@ -17,12 +17,12 @@
* along with this program. If not, see .
*/
-import type {EventFight} from "@type/event.js";
-import {fetchWithToken, tokenStore} from "./repo";
-import {z} from "zod";
-import {EventFightSchema} from "@type/event.js";
-import type {Dayjs} from "dayjs";
-import {derived} from "svelte/store";
+import type { EventFight } from "@type/event.js";
+import { fetchWithToken, tokenStore } from "./repo";
+import { z } from "zod";
+import { EventFightSchema } from "@type/event.js";
+import type { Dayjs } from "dayjs";
+import { derived } from "svelte/store";
export interface CreateFight {
spielmodus: string;
@@ -39,23 +39,22 @@ export interface UpdateFight {
map: string | null;
blueTeam: number | null;
redTeam: number | null;
- start: Dayjs | null;
+ start: number | null;
spectatePort: number | null;
- group: string | null;
+ group: number | null;
}
export class FightRepo {
- constructor(private token: string) {
- }
+ constructor(private token: string) {}
public async listFights(eventId: number): Promise {
return await fetchWithToken(this.token, `/events/${eventId}/fights`)
- .then(value => value.json())
- .then(value => z.array(EventFightSchema).parse(value));
+ .then((value) => value.json())
+ .then((value) => z.array(EventFightSchema).parse(value));
}
public async createFight(eventId: number, fight: CreateFight): Promise {
- return await fetchWithToken(this.token, "/fights", {
+ return await fetchWithToken(this.token, `/events/${eventId}/fights`, {
method: "POST",
body: JSON.stringify({
event: eventId,
@@ -67,28 +66,25 @@ export class FightRepo {
spectatePort: fight.spectatePort,
group: fight.group,
}),
- }).then(value => value.json())
+ })
+ .then((value) => value.json())
.then(EventFightSchema.parse);
}
- public async updateFight(fightId: number, fight: UpdateFight): Promise {
- return await fetchWithToken(this.token, `/fights/${fightId}`, {
+ public async updateFight(eventId: number, fightId: number, fight: UpdateFight): Promise {
+ return await fetchWithToken(this.token, `/events/${eventId}/fights/${fightId}`, {
method: "PUT",
body: JSON.stringify({
- spielmodus: fight.spielmodus,
- map: fight.map,
- blueTeam: fight.blueTeam,
- redTeam: fight.redTeam,
+ ...fight,
start: fight.start?.valueOf(),
- spectatePort: fight.spectatePort,
- group: fight.group,
}),
- }).then(value => value.json())
+ })
+ .then((value) => value.json())
.then(EventFightSchema.parse);
}
- public async deleteFight(fightId: number): Promise {
- const res = await fetchWithToken(this.token, `/fights/${fightId}`, {
+ public async deleteFight(eventId: number, fightId: number): Promise {
+ const res = await fetchWithToken(this.token, `/events/${eventId}/fights/${fightId}`, {
method: "DELETE",
});
diff --git a/src/components/repo/page.ts b/src/components/repo/page.ts
index f42b1d8..f3373b2 100644
--- a/src/components/repo/page.ts
+++ b/src/components/repo/page.ts
@@ -17,27 +17,26 @@
* along with this program. If not, see .
*/
-import type {Page, PageList} from "@type/page.ts";
-import {fetchWithToken, tokenStore} from "./repo.ts";
-import {PageListSchema, PageSchema} from "@type/page.ts";
-import {bytesToBase64} from "../admin/util.ts";
-import {z} from "zod";
-import {derived} from "svelte/store";
+import type { Page, PageList } from "@type/page.ts";
+import { fetchWithToken, tokenStore } from "./repo.ts";
+import { PageListSchema, PageSchema } from "@type/page.ts";
+import { bytesToBase64 } from "../admin/util.ts";
+import { z } from "zod";
+import { derived } from "svelte/store";
export class PageRepo {
- constructor(private token: string) {
- }
+ constructor(private token: string) {}
public async listPages(branch: string = "master"): Promise {
return await fetchWithToken(this.token, `/page?branch=${branch}`)
- .then(value => value.json())
+ .then((value) => value.json())
.then(PageListSchema.parse)
- .then(value => value.map(value1 => ({...value1, path: value1.path.replace("src/content/", "")})));
+ .then((value) => value.map((value1) => ({ ...value1, path: value1.path.replace("src/content/", "") })));
}
public async getPage(id: number, branch: string = "master"): Promise {
return await fetchWithToken(this.token, `/page/${id}?branch=${branch}`)
- .then(value => value.json())
+ .then((value) => value.json())
.then(PageSchema.parse);
}
@@ -46,42 +45,57 @@ export class PageRepo {
method: "PUT",
body: JSON.stringify({
content: bytesToBase64(new TextEncoder().encode(content)),
- sha, message,
+ sha,
+ message,
}),
});
}
public async getBranches(): Promise {
return await fetchWithToken(this.token, "/page/branch")
- .then(value => value.json())
- .then(value => z.array(z.string()).parse(value));
+ .then((value) => value.json())
+ .then((value) => z.array(z.string()).parse(value));
}
public async createBranch(branch: string): Promise {
- await fetchWithToken(this.token, "/page/branch", {method: "POST", body: JSON.stringify({branch})});
+ await fetchWithToken(this.token, "/page/branch", { method: "POST", body: JSON.stringify({ branch }) });
}
public async deleteBranch(branch: string): Promise {
- await fetchWithToken(this.token, "/page/branch", {method: "DELETE", body: JSON.stringify({branch})});
+ await fetchWithToken(this.token, "/page/branch", { method: "DELETE", body: JSON.stringify({ branch }) });
}
public async createFile(path: string, branch: string = "master", slug: string | null = null, title: string | null = null): Promise {
- await fetchWithToken(this.token, `/page?branch=${branch}`, {method: "POST", body: JSON.stringify({path, slug, title})});
+ await fetchWithToken(this.token, `/page?branch=${branch}`, { method: "POST", body: JSON.stringify({ path, slug, title }) });
}
public async merge(branch: string, message: string): Promise {
await fetchWithToken(this.token, "/page/branch/merge", {
method: "POST",
- body: JSON.stringify({branch, message}),
+ body: JSON.stringify({ branch, message }),
});
}
public async deletePage(id: number, message: string, sha: string, branch: string = "master"): Promise {
await fetchWithToken(this.token, `/page/${id}?branch=${branch}`, {
method: "DELETE",
- body: JSON.stringify({message, sha}),
+ body: JSON.stringify({ message, sha }),
+ });
+ }
+
+ public async listImages(branch: string = "master"): Promise {
+ return await fetchWithToken(this.token, `/page/images?branch=${branch}`)
+ .then((value) => value.json())
+ .then(PageListSchema.parse)
+ .then((value) => value.map((value1) => ({ ...value1, path: value1.path.replace("src/content/", "") })));
+ }
+
+ public async createImage(name: string, data: string, branch: string = "master"): Promise {
+ await fetchWithToken(this.token, `/page/images?branch=${branch}`, {
+ method: "POST",
+ body: JSON.stringify({ name, data }),
});
}
}
-export const pageRepo = derived(tokenStore, ($token) => new PageRepo($token));
\ No newline at end of file
+export const pageRepo = derived(tokenStore, ($token) => new PageRepo($token));
diff --git a/src/components/stores/stores.ts b/src/components/stores/stores.ts
index f1d53f0..a44b7c7 100644
--- a/src/components/stores/stores.ts
+++ b/src/components/stores/stores.ts
@@ -17,41 +17,45 @@
* along with this program. If not, see .
*/
-import type {Player, SchematicType} from "@type/data";
-import {PlayerSchema} from "@type/data.ts";
-import {cached, cachedFamily} from "./cached";
-import type {Team} from "@type/team.ts";
-import {TeamSchema} from "@type/team";
-import {derived, get, writable} from "svelte/store";
-import {z} from "zod";
-import {fetchWithToken, tokenStore} from "@repo/repo.ts";
-import {pageRepo} from "@repo/page.ts";
-import {dataRepo} from "@repo/data.ts";
-import {permsRepo} from "@repo/perms.ts";
+import type { Player, SchematicType } from "@type/data";
+import { PlayerSchema } from "@type/data.ts";
+import { cached, cachedFamily } from "./cached";
+import type { Team } from "@type/team.ts";
+import { TeamSchema } from "@type/team";
+import { derived, get, writable } from "svelte/store";
+import { z } from "zod";
+import { fetchWithToken, tokenStore } from "@repo/repo.ts";
+import { pageRepo } from "@repo/page.ts";
+import { dataRepo } from "@repo/data.ts";
+import { permsRepo } from "@repo/perms.ts";
-export const schemTypes = cached([], () =>
- fetchWithToken(get(tokenStore), "/data/admin/schematicTypes")
- .then(res => res.json()));
+export const schemTypes = cached([], () => fetchWithToken(get(tokenStore), "/data/admin/schematicTypes").then((res) => res.json()));
export const players = cached([], async () => {
- const res = await fetchWithToken(get(tokenStore), "/data/admin/users");
- return z.array(PlayerSchema).parse(await res.json());
+ return get(dataRepo).getPlayers();
});
-export const permissions = cached({
- perms: [],
- prefixes: {},
-}, async () => {
- return get(permsRepo).listPerms();
+export const teams = cached([], async () => {
+ return get(dataRepo).getTeams();
});
+export const permissions = cached(
+ {
+ perms: [],
+ prefixes: {},
+ },
+ async () => {
+ return get(permsRepo).listPerms();
+ }
+);
+
export const gamemodes = cached([], async () => {
const res = await fetchWithToken(get(tokenStore), "/data/admin/gamemodes");
return z.array(z.string()).parse(await res.json());
});
export const maps = cachedFamily([], async (gamemode) => {
- if (get(gamemodes).every(value => value !== gamemode)) return [];
+ if (get(gamemodes).every((value) => value !== gamemode)) return [];
const res = await fetchWithToken(get(tokenStore), `/data/admin/gamemodes/${gamemode}/maps`);
if (!res.ok) {
@@ -66,17 +70,12 @@ export const groups = cached([], async () => {
return z.array(z.string()).parse(await res.json());
});
-export const teams = cached([], async () => {
- const res = await fetchWithToken(get(tokenStore), "/team");
- return z.array(TeamSchema).parse(await res.json());
-});
-
export const branches = cached([], async () => {
const res = await get(pageRepo).getBranches();
return z.array(z.string()).parse(res);
});
-export const server = derived(dataRepo, $dataRepo => $dataRepo.getServer());
+export const server = derived(dataRepo, ($dataRepo) => $dataRepo.getServer());
export const isWide = writable(typeof window !== "undefined" && window.innerWidth >= 640);
diff --git a/src/components/types/data.ts b/src/components/types/data.ts
index cc97a67..2098b68 100644
--- a/src/components/types/data.ts
+++ b/src/components/types/data.ts
@@ -17,7 +17,7 @@
* along with this program. If not, see .
*/
-import {z} from "zod";
+import { z } from "zod";
export const SchematicTypeSchema = z.object({
name: z.string(),
@@ -57,3 +57,12 @@ export const ResponseErrorSchema = z.object({
});
export type ResponseError = z.infer;
+
+export const ResponseUserSchema = z.object({
+ name: z.string(),
+ uuid: z.string(),
+ prefix: z.string(),
+ perms: z.array(z.string()),
+});
+
+export type ResponseUser = z.infer;
diff --git a/src/components/types/event.ts b/src/components/types/event.ts
index 68c2cac..90db0cc 100644
--- a/src/components/types/event.ts
+++ b/src/components/types/event.ts
@@ -17,9 +17,57 @@
* along with this program. If not, see .
*/
-import {z} from "zod";
-import {TeamSchema} from "./team.js";
-import {PlayerSchema} from "./data.js";
+import { z } from "zod";
+import { TeamSchema } from "./team.js";
+import { PlayerSchema, ResponseUserSchema } from "./data.js";
+
+export const ResponseGroupsSchema = z.object({
+ id: z.number(),
+ name: z.string(),
+ pointsPerWin: z.number(),
+ pointsPerLoss: z.number(),
+ pointsPerDraw: z.number(),
+ type: z.enum(["GROUP_STAGE", "ELIMINATION_STAGE"]),
+ points: z.record(z.string(), z.number()).nullable(),
+});
+
+export const EventFightSchema = z.object({
+ id: z.number(),
+ spielmodus: z.string(),
+ map: z.string(),
+ blueTeam: TeamSchema,
+ redTeam: TeamSchema,
+ start: z.number(),
+ ergebnis: z.number(),
+ spectatePort: z.number().nullable(),
+ group: ResponseGroupsSchema.nullable(),
+ hasFinished: z.boolean(),
+});
+
+export type EventFight = z.infer;
+
+export const EventFightEditSchema = EventFightSchema.omit({
+ id: true,
+ group: true,
+ hasFinished: true,
+}).extend({
+ group: z.number().nullable(),
+});
+
+export type EventFightEdit = z.infer;
+
+export type ResponseGroups = z.infer;
+
+export const ResponseRelationSchema = z.object({
+ id: z.number(),
+ fight: EventFightSchema,
+ type: z.enum(["FIGHT", "GROUP"]),
+ fromFight: EventFightSchema.nullable(),
+ fromGroup: ResponseGroupsSchema.nullable(),
+ fromPlace: z.number(),
+});
+
+export type ResponseRelation = z.infer;
export const ShortEventSchema = z.object({
id: z.number(),
@@ -35,29 +83,69 @@ export const SWEventSchema = ShortEventSchema.extend({
maxTeamMembers: z.number(),
schemType: z.string().nullable(),
publicSchemsOnly: z.boolean(),
- referees: z.array(PlayerSchema),
});
export type SWEvent = z.infer;
-export const EventFightSchema = z.object({
- id: z.number(),
- spielmodus: z.string(),
- map: z.string(),
- blueTeam: TeamSchema,
- redTeam: TeamSchema,
- start: z.number(),
- ergebnis: z.number(),
- spectatePort: z.number().nullable(),
- group: z.string().nullable(),
-});
-
-export type EventFight = z.infer;
-
export const ExtendedEventSchema = z.object({
event: SWEventSchema,
teams: z.array(TeamSchema),
+ groups: z.array(ResponseGroupsSchema),
fights: z.array(EventFightSchema),
+ referees: z.array(ResponseUserSchema),
+ relations: z.array(ResponseRelationSchema),
});
export type ExtendedEvent = z.infer;
+
+export const ResponseTeamSchema = z.object({
+ id: z.number(),
+ name: z.string(),
+ kuerzel: z.string(),
+ color: z.string(),
+});
+
+export type ResponseTeam = z.infer;
+
+export const CreateEventGroupSchema = z.object({
+ name: z.string(),
+ type: z.enum(["GROUP_STAGE", "ELIMINATION_STAGE"]),
+});
+export type CreateEventGroup = z.infer;
+
+export const UpdateEventGroupSchema = z.object({
+ name: z.string().nullable().optional(),
+ type: z.enum(["GROUP_STAGE", "ELIMINATION_STAGE"]).nullable().optional(),
+ pointsPerWin: z.number().nullable().optional(),
+ pointsPerLoss: z.number().nullable().optional(),
+ pointsPerDraw: z.number().nullable().optional(),
+});
+export type UpdateEventGroup = z.infer;
+
+export const GroupEditSchema = ResponseGroupsSchema.omit({
+ id: true,
+ points: true,
+});
+export type GroupUpdateEdit = z.infer;
+
+export const CreateEventRelationSchema = z.object({
+ fightId: z.number(),
+ team: z.enum(["RED", "BLUE"]),
+ fromType: z.enum(["FIGHT", "GROUP"]),
+ fromId: z.number(),
+ fromPlace: z.number(),
+});
+export type CreateEventRelation = z.infer;
+
+export const UpdateFromRelationSchema = z.object({
+ fromType: z.enum(["FIGHT", "GROUP"]),
+ fromId: z.number(),
+ fromPlace: z.number(),
+});
+export type UpdateFromRelation = z.infer;
+
+export const UpdateEventRelationSchema = z.object({
+ team: z.enum(["RED", "BLUE"]).nullable().optional(),
+ from: UpdateFromRelationSchema.nullable().optional(),
+});
+export type UpdateEventRelation = z.infer;
diff --git a/src/images/left.png b/src/images/left.png
new file mode 100644
index 0000000..1490f7d
Binary files /dev/null and b/src/images/left.png differ
diff --git a/src/pages/ankuendigungen/[...slug].astro b/src/pages/ankuendigungen/[...slug].astro
index 814a82b..eca0dcc 100644
--- a/src/pages/ankuendigungen/[...slug].astro
+++ b/src/pages/ankuendigungen/[...slug].astro
@@ -1,29 +1,29 @@
---
-import {astroI18n, createGetStaticPaths} from "astro-i18n";
-import {getCollection, CollectionEntry} from "astro:content";
+import { astroI18n, createGetStaticPaths } from "astro-i18n";
+import { getCollection, CollectionEntry } from "astro:content";
import PageLayout from "@layouts/PageLayout.astro";
-import {TagSolid, CalendarMonthSolid} from "flowbite-svelte-icons";
+import { TagSolid, CalendarMonthSolid } from "flowbite-svelte-icons";
import TagComponent from "@components/TagComponent.astro";
import LanguageWarning from "@components/LanguageWarning.astro";
-import {SEO} from "astro-seo";
+import { SEO } from "astro-seo";
import localBau from "@images/2022-03-28_13.18.25.png";
-import {getImage, Image} from "astro:assets";
+import { getImage, Image } from "astro:assets";
import "@styles/table.css";
export const getStaticPaths = createGetStaticPaths(async () => {
- 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 posts.map(value => ({
+ return posts.map((value) => ({
params: {
slug: value.slug.split("/").slice(1).join("/"),
},
@@ -35,12 +35,12 @@ export const getStaticPaths = createGetStaticPaths(async () => {
});
interface Props {
- post: CollectionEntry<"announcements">,
- german: boolean
+ post: CollectionEntry<"announcements">;
+ german: boolean;
}
-const {post, german} = Astro.props;
-const {Content} = await post.render();
+const { post, german } = Astro.props;
+const { Content } = await post.render();
const ogImage = await getImage({
src: post.data.image || localBau,
@@ -52,64 +52,66 @@ const ogImage = await getImage({
-
- {post.data.image && (
-
-
-
- )}
+ {
+ post.data.image && (
+
+
+
+ )
+ }
{post.data.title}
-
+
- {post.data.tags.map(tag => (
-
- ))}
+ {post.data.tags.map((tag) => )}
-
- {Intl.DateTimeFormat(astroI18n.locale, {
- day: "numeric",
- month: "short",
- year: "numeric",
- }).format(post.data.created)}
- {post.data.author && (
-
-
- {post.data.author}
-
- )}
+
+ {
+ Intl.DateTimeFormat(astroI18n.locale, {
+ day: "numeric",
+ month: "short",
+ year: "numeric",
+ }).format(post.data.created)
+ }
+ {
+ post.data.author && (
+
+
+ {post.data.author}
+
+ )
+ }
- {german && (
-
- )}
-
+ {german && }
+