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 @@ -->
- - SteamWar - + SteamWar
- navigate("/admin")} /> + navigate("/admin")} />
-
- -
-
\ 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 @@ + + +
+ + + + {#snippet child({ props })} + + {/snippet} + + + + + + No fight modus found. + + {#each $gamemodes as modus} + { + fightModus = modus; + gamemodeSelectOpen = false; + }} + > + + {modus} + + {/each} + + + + + + + + + {#snippet child({ props })} + + {/snippet} + + + + + + No map found. + + {#each $mapsStore as map} + { + fightMap = map; + mapSelectOpen = false; + }} + > + + {map} + + {/each} + + + + + + + + + {#snippet child({ props })} + + {/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} + + + + + + + + + {#snippet child({ props })} + + {/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} + + + + + + + + {#if fight !== null} + + + {/if} + + + + + +
+ +{@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 @@ + + +
+ + + + + + + {#if groupType === "GROUP_STAGE" && group !== null} + + + + + + + + + {/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 })} + + {/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)} + + + + {/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 @@ --> \ 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)} + + + + {/snippet} + + + + +{#if selectedGroup} + +{/if} + +{#if selectedGroupForResults} + +{/if} + + + + + Gruppe Ändern + Hier kannst du die Gruppe der ausgewählten Kämpfe ändern + + + + + + + + +
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} + + +
@@ -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"} + + + + + + + + + + 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)} + + + + {/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)} + + +
+ + +
+
+ {/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} + + + + + 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} - + {/each} @@ -69,7 +63,7 @@ - + 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(""); - - - 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} - + {/each} @@ -52,4 +67,27 @@ {/if} + + + + + + + + + + + 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 })} + + {/snippet} + + + + Create New Event + Fill in the details for the new event. Click create when you're done. + +
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+ {#if errorMsg} +

{errorMsg}

+ {/if} +
+ + + {#snippet child({ props })} + + {/snippet} + + + +
+
+
+ {#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} + + {/each} +
+ +
+ {#if manager.selectedPage} +
+ +
+
+ {#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]} +
+
+ { + const newKey = (e.target as HTMLInputElement).value; + if (newKey !== key) { + manager.selectedPage!.frontmatter[newKey] = manager.selectedPage!.frontmatter[key]; + delete manager.selectedPage?.frontmatter[key]; + manager.selectedPage!.dirty = true; + } + }} + class="px-2 py-1 border rounded text-sm flex-shrink-0 w-32 bg-neutral-900" + placeholder="Key" + /> + : + {#if Array.isArray(value)} + Array ({value.length} items) + {:else if value instanceof Date || key === "created"} + { + const dateValue = (e.target as HTMLInputElement).value; + manager.selectedPage!.frontmatter[key] = dateValue ? new Date(dateValue) : ""; + manager.selectedPage!.dirty = true; + }} + class="px-2 py-1 border rounded text-sm flex-1 bg-neutral-900" + /> + {:else} + (manager.selectedPage!.dirty = true)} + class="px-2 py-1 border rounded text-sm flex-1 bg-neutral-900" + placeholder="Value" + /> + {/if} + +
+ {#if Array.isArray(value)} +
+ {#each value as item, index} +
+ [{index}] + (manager.selectedPage!.dirty = true)} + class="px-2 py-1 border rounded text-sm flex-1 bg-neutral-900" + placeholder="Array item" + /> + +
+ {/each} + +
+ {/if} +
+ {/each} +
+ + +
+
+
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 @@ + + +
+ + +
+
+ + + {#snippet child({ props })} + + {/snippet} + + + + + + No Branches Found. + + {#each manager.branches as branch} + { + if (manager.anyUnsavedChanges()) { + if (!confirm("You have unsaved changes. Are you sure you want to switch branches?")) { + return; + } + } + + manager.branch = branch; + manager.pages = []; + branchSelectOpen = false; + }} + > + + {branch} + + {/each} + + + + + + + + + {#snippet child({ props })} + + {/snippet} + + + {#await manager.imagesLoad} +

Loading images...

+ {:then images} +
+
+ { + const file = e.target?.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = async (event) => { + const base64 = event.target?.result?.toString().split(",")[1]; + if (base64) { + await $pageRepo.createImage(file.name, base64, manager.branch); + manager.reloadImages(); + } + }; + reader.readAsDataURL(file); + }} + /> + +
+
+ {#each images as image} + + {/each} +
+
+ {/await} +
+
+ +
+ + {#await manager.pagesLoad} +

Loading pages...

+ {:then pages} + {#each Object.values(pages.dirs) as page} + + {/each} + {/await} +
+
+ + + + +
+
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 @@ + + + + + + +{#if open} +
+
+ {#if newPage} + + {/if} + {#each Object.values(page.dirs) as subPage (subPage.name)} + + {/each} + {#each Object.values(page.files) as file (file.id)} + + {/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} - {post.data.author} - - )} + + { + Intl.DateTimeFormat(astroI18n.locale, { + day: "numeric", + month: "short", + year: "numeric", + }).format(post.data.created) + } + { + post.data.author && ( + + {post.data.author} + {post.data.author} + + ) + }
- {german && ( - - )} - + {german && } +