diff --git a/src/components/Navbar.svelte b/src/components/Navbar.svelte index 9a36592..2ad6af1 100644 --- a/src/components/Navbar.svelte +++ b/src/components/Navbar.svelte @@ -81,6 +81,7 @@
{t("navbar.links.home.announcements")} + {t("navbar.links.home.events")} {t("navbar.links.home.downloads")} {t("navbar.links.home.faq")} {t("navbar.links.rules.coc")} diff --git a/src/components/event/ConnectionRenderer.svelte b/src/components/event/ConnectionRenderer.svelte new file mode 100644 index 0000000..ed5cb02 --- /dev/null +++ b/src/components/event/ConnectionRenderer.svelte @@ -0,0 +1,108 @@ + + +
+ {#key refresh} + {#each $fightConnector.showedConnections as connection} + {@const fromLeft = connection.fromElement.offsetLeft + connection.fromElement.offsetWidth} + {@const toLeft = connection.toElement.offsetLeft} + {@const fromTop = connection.fromElement.offsetTop + connection.fromElement.offsetHeight / 2} + {@const toTop = connection.toElement.offsetTop + connection.toElement.offsetHeight / 2} + {@const horizontalDistance = toLeft - fromLeft} + {@const verticalDistance = toTop - fromTop} + + {@const midLeft = fromLeft + horizontalDistance / 2 + connection.offset} + {@const firstSegmentWidth = horizontalDistance / 2} + {@const secondSegmentWidth = horizontalDistance / 2} + +
+
+
+ {/each} + {/key} +
+ + diff --git a/src/components/event/DoubleEleminationDisplay.svelte b/src/components/event/DoubleEleminationDisplay.svelte new file mode 100644 index 0000000..e9465f4 --- /dev/null +++ b/src/components/event/DoubleEleminationDisplay.svelte @@ -0,0 +1,165 @@ + + +{#if !grandFinal} +

Konfiguration unvollständig (Grand Final fehlt).

+{:else} + {#key winnersStages.length + ":" + losersStages.length} + + {@const totalColumns = Math.max(winnersStages.length, losersStages.length + 1) + 1} +
+ +

Winners Bracket

+ + + {#each winnersStages as stage, i} +
+ + {#each stage as fight} + + {/each} + +
+ {/each} + + +
+ + {#if grandFinal} + + {/if} + +
+ + +

Losers Bracket

+ + + {#each losersStages as stage, j} +
+ + {#each stage as fight} + + {/each} + +
+ {/each} +
+ {/key} +{/if} diff --git a/src/components/event/EleminationDisplay.svelte b/src/components/event/EleminationDisplay.svelte new file mode 100644 index 0000000..1bbb9ab --- /dev/null +++ b/src/components/event/EleminationDisplay.svelte @@ -0,0 +1,120 @@ + + +{#if stages.length === 0} +

Keine Eliminationsdaten gefunden.

+{:else} +
+ {#each stages as stage, index} +
+ + {#each stage as fight} + + {/each} + +
+ {/each} +
+{/if} diff --git a/src/components/event/EventCard.svelte b/src/components/event/EventCard.svelte new file mode 100644 index 0000000..d0f4571 --- /dev/null +++ b/src/components/event/EventCard.svelte @@ -0,0 +1,20 @@ + + +
+
+ {title} +
+
+ {@render children()} +
+
diff --git a/src/components/event/EventCardOutline.svelte b/src/components/event/EventCardOutline.svelte new file mode 100644 index 0000000..d213b8f --- /dev/null +++ b/src/components/event/EventCardOutline.svelte @@ -0,0 +1,13 @@ + + +
+ {@render children()} +
diff --git a/src/components/event/EventFightChip.svelte b/src/components/event/EventFightChip.svelte new file mode 100644 index 0000000..fa48054 --- /dev/null +++ b/src/components/event/EventFightChip.svelte @@ -0,0 +1,42 @@ + + + + +
+ + +
+
diff --git a/src/components/event/EventFights.svelte b/src/components/event/EventFights.svelte new file mode 100644 index 0000000..f045c25 --- /dev/null +++ b/src/components/event/EventFights.svelte @@ -0,0 +1,50 @@ + + +
+ {#each Object.entries(viewConfig) as [name, view]} + + {/each} +
+ +{#if selectedView} + {@const view = viewConfig[selectedView]} +
+ + {#if view.view.type === "GROUP"} + + {:else if view.view.type === "ELEMINATION"} + + {:else if view.view.type === "DOUBLE_ELEMINATION"} + + {/if} +
+{/if} diff --git a/src/components/event/EventTeamChip.svelte b/src/components/event/EventTeamChip.svelte new file mode 100644 index 0000000..290616e --- /dev/null +++ b/src/components/event/EventTeamChip.svelte @@ -0,0 +1,48 @@ + + + + + diff --git a/src/components/event/GroupDisplay.svelte b/src/components/event/GroupDisplay.svelte new file mode 100644 index 0000000..4a8e8a1 --- /dev/null +++ b/src/components/event/GroupDisplay.svelte @@ -0,0 +1,70 @@ + + +{#each config.groups as groupId} + {@const group = event.groups.find((g) => g.id === groupId)!!} + {@const fights = event.fights.filter((f) => f.group?.id === groupId)} + {@const rounds = detectRounds(fights)} +
+
+ + + {#each Object.entries(group.points ?? {}).toSorted((v1, v2) => v2[1] - v1[1]) as points} + {@const [teamId, point] = points} + {@const team = event.teams.find((t) => t.id.toString() === teamId)!!} + + {/each} + + +
+ {#each rounds as round, index} +
+ + {#each round as fight} + + {/each} + +
+ {/each} +
+{/each} diff --git a/src/components/event/connections.svelte.ts b/src/components/event/connections.svelte.ts new file mode 100644 index 0000000..05e32cf --- /dev/null +++ b/src/components/event/connections.svelte.ts @@ -0,0 +1,55 @@ +import { readonly, writable } from "svelte/store"; + +class FightConnection { + constructor( + public readonly fromElement: HTMLElement, + public readonly toElement: HTMLElement, + public readonly color: string = "white", + public readonly background: boolean, + public readonly offset: number = 0 + ) {} +} + +export class FightConnector { + private connections: FightConnection[] = $state([]); + + get allConnections(): FightConnection[] { + return this.connections; + } + + get showedConnections(): FightConnection[] { + const showBackground = this.connections.some((conn) => !conn.background); + return showBackground ? this.connections.filter((conn) => !conn.background) : this.connections; + } + + addTeamConnection(teamId: number): void { + const teamElements = document.getElementsByClassName(`team-${teamId}`); + const teamArray = Array.from(teamElements); + teamArray.sort((a, b) => { + const rectA = a.getBoundingClientRect(); + const rectB = b.getBoundingClientRect(); + return rectA.top - rectB.top || rectA.left - rectB.left; + }); + for (let i = 1; i < teamElements.length; i++) { + const fromElement = teamElements[i - 1] as HTMLElement; + const toElement = teamElements[i] as HTMLElement; + this.connections.push(new FightConnection(fromElement, toElement, "white", false)); + } + } + + addConnection(fromElement: HTMLElement, toElement: HTMLElement, color: string = "white", offset: number = 0): void { + this.connections.push(new FightConnection(fromElement, toElement, color, true, offset)); + } + + clearConnections(): void { + this.connections = this.connections.filter((conn) => conn.background); + } + + clearAllConnections(): void { + this.connections = []; + } +} + +const fightConnectorInternal = writable(new FightConnector()); + +export const fightConnector = readonly(fightConnectorInternal); diff --git a/src/components/event/team-hover.svelte.ts b/src/components/event/team-hover.svelte.ts new file mode 100644 index 0000000..d3b17db --- /dev/null +++ b/src/components/event/team-hover.svelte.ts @@ -0,0 +1,19 @@ +import { get, writable } from "svelte/store"; +import { fightConnector } from "./connections.svelte"; + +class TeamHoverService { + public currentHover = $state(undefined); + private fightConnector = get(fightConnector); + + setHover(teamId: number): void { + this.currentHover = teamId; + this.fightConnector.addTeamConnection(teamId); + } + + clearHover(): void { + this.currentHover = undefined; + this.fightConnector.clearConnections(); + } +} + +export const teamHoverService = writable(new TeamHoverService()); diff --git a/src/components/event/types.ts b/src/components/event/types.ts new file mode 100644 index 0000000..c13b435 --- /dev/null +++ b/src/components/event/types.ts @@ -0,0 +1,34 @@ +import { z } from "astro:content"; + +export const GroupViewSchema = z.object({ + type: z.literal("GROUP"), + groups: z.array(z.number()), +}); + +export type GroupViewConfig = z.infer; + +export const EleminationViewSchema = z.object({ + type: z.literal("ELEMINATION"), + finalFight: z.number(), +}); + +export type EleminationViewConfig = z.infer; + +// Double elimination config: needs final fight (grand final) and entry fights for winners & losers brackets +export const DoubleEleminationViewSchema = z.object({ + type: z.literal("DOUBLE_ELEMINATION"), + winnersFinalFight: z.number(), // Final fight of winners bracket (feeds into grand final) + losersFinalFight: z.number(), // Final fight of losers bracket (feeds into grand final) + grandFinalFight: z.number(), // Grand final fight id +}); + +export type DoubleEleminationViewConfig = z.infer; + +export const EventViewConfigSchema = z.record( + z.object({ + name: z.string(), + view: z.discriminatedUnion("type", [GroupViewSchema, EleminationViewSchema, DoubleEleminationViewSchema]), + }) +); + +export type EventViewConfig = z.infer; diff --git a/src/components/moderator/pages/event/EventFightList.svelte b/src/components/moderator/pages/event/EventFightList.svelte index 06c6bab..e7c9814 100644 --- a/src/components/moderator/pages/event/EventFightList.svelte +++ b/src/components/moderator/pages/event/EventFightList.svelte @@ -194,6 +194,16 @@ (groupChangeOpen = true)}>Gruppe Ändern Startzeit Verschieben Spectate Port Ändern + { + let selectedGroups = table.getSelectedRowModel().rows.map((row) => row.original); + for (const g of selectedGroups) { + await $fightRepo.deleteFight(data.event.id, g.id); + } + + refresh(); + }}>Kämpfe Löschen diff --git a/src/components/moderator/pages/event/FightEditRow.svelte b/src/components/moderator/pages/event/FightEditRow.svelte index aa38da6..536d374 100644 --- a/src/components/moderator/pages/event/FightEditRow.svelte +++ b/src/components/moderator/pages/event/FightEditRow.svelte @@ -38,6 +38,11 @@ duplicateOpen = false; } + + async function handleDelete() { + await $fightRepo.deleteFight(data.event.id, fight.id); + refresh(); + }
@@ -55,6 +60,7 @@ {#snippet actions(dirty, submit)} + {/snippet} diff --git a/src/components/moderator/pages/generators/FightsGenerator.svelte b/src/components/moderator/pages/generators/FightsGenerator.svelte index c7473fc..6b8c864 100644 --- a/src/components/moderator/pages/generators/FightsGenerator.svelte +++ b/src/components/moderator/pages/generators/FightsGenerator.svelte @@ -2,6 +2,8 @@ import type { ExtendedEvent } from "@components/types/event"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@components/ui/tabs"; import GroupPhaseGenerator from "./gens/group/GroupPhaseGenerator.svelte"; + import SingleEliminationGenerator from "./gens/elimination/SingleEliminationGenerator.svelte"; + import DoubleEliminationGenerator from "./gens/elimination/DoubleEliminationGenerator.svelte"; let { data, }: { @@ -14,9 +16,16 @@ Gruppenphase K.O. Phase + Double Elimination + + + + + +
diff --git a/src/components/moderator/pages/generators/gens/elimination/DoubleEliminationGenerator.svelte b/src/components/moderator/pages/generators/gens/elimination/DoubleEliminationGenerator.svelte new file mode 100644 index 0000000..df8b608 --- /dev/null +++ b/src/components/moderator/pages/generators/gens/elimination/DoubleEliminationGenerator.svelte @@ -0,0 +1,515 @@ + + + +
+

Double Elimination Bracket

+
+ +
+
+ {#if seedSlots.length < 4} +

Mindestens vier Seeds benötigt.

+ {:else if winnersRounds.length === 0} +

Seedanzahl muss eine Zweierpotenz sein. Aktuell: {seedSlots.length}

+ {/if} +
+
+ +
    + {#each seedSlots as slot, i (i)} +
  • + {i + 1}. + {slotLabel(slot)} +
    + + + +
    +
  • + {/each} +
+
+ +
+
+ + + +
+ {#if data.groups.length > 0} +
+ + + +
+ {/if} + {#if data.fights.length > 0} +
+ + + +
+ {/if} +
+
+
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + {#if winnersRounds.length > 0} +
+ {#each winnersRounds as round, r} +
+

W Runde {r + 1}

+
    + {#each round as fight, i} +
  • + {new Intl.DateTimeFormat("de-DE", { hour: "2-digit", minute: "2-digit" }).format( + startTime + .copy() + .add({ minutes: roundTime * r, seconds: startDelay * i }) + .toDate() + )} + : {slotLabel(fight.blue)} vs. {slotLabel(fight.red)} +
  • + {/each} +
+
+ {/each} +
+ {/if} +
+
+ + {#if losersRounds.length > 0} +
+ {#each losersRounds as round, r} +
+

L Runde {r + 1}

+
    + {#each round as fight, i} +
  • + Verlierer Paar {i + 1} (aus W Runde {r + 1}) +
  • + {/each} +
+
+ {/each} +
+ {/if} +
+
+
+ +
+ + diff --git a/src/components/moderator/pages/generators/gens/elimination/SingleEliminationGenerator.svelte b/src/components/moderator/pages/generators/gens/elimination/SingleEliminationGenerator.svelte new file mode 100644 index 0000000..12b6657 --- /dev/null +++ b/src/components/moderator/pages/generators/gens/elimination/SingleEliminationGenerator.svelte @@ -0,0 +1,364 @@ + + + +
+

Single Elimination Bracket

+
+ +
+
+ {#if seedSlots.length < 2} +

Mindestens zwei Seeds benötigt.

+ {:else if bracketRounds.length === 0} +

Anzahl der Seeds muss eine Zweierpotenz sein (2,4,8,16,...). Aktuell: {seedSlots.length}

+ {/if} +
+
+
+ +
    + {#each seedSlots as slot, i (i)} +
  • + {i + 1}. + {slotLabel(slot)} +
    + + + +
    +
  • + {/each} +
+
+
+ +
+
+ + + +
+
+ {#if data.groups.length > 0} + + + + {/if} +
+
+ {#if data.fights.length > 0} + + + + {/if} +
+
+

Gruppen- oder Kampfplätze erzeugen Relationen beim Generieren. ??? bleibt Platzhalter.

+
+
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + {#if bracketRounds.length > 0} +
+ {#each bracketRounds as round, r} +
+

Runde {r + 1}

+
    + {#each round as fight, i} +
  • + {new Intl.DateTimeFormat("de-DE", { hour: "2-digit", minute: "2-digit" }).format( + startTime + .copy() + .add({ minutes: roundTime * r, seconds: startDelay * i }) + .toDate() + )} + : {slotLabel(fight.blue)}  vs.  {slotLabel(fight.red)} +
  • + {/each} +
+
+ {/each} +
+ {/if} +
+ +
+ + diff --git a/src/content/config.ts b/src/content/config.ts index 1a15f21..c502cbc 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -20,6 +20,7 @@ import { defineCollection, reference, z } from "astro:content"; import { docsLoader } from "@astrojs/starlight/loaders"; import { docsSchema } from "@astrojs/starlight/schema"; +import { EventViewConfigSchema } from "@components/event/types"; export const pagesSchema = z.object({ title: z.string().min(1).max(80), @@ -109,6 +110,19 @@ export const publics = defineCollection({ }), }); +export const events = defineCollection({ + type: "content", + schema: ({ image }) => + z.object({ + eventId: z.number().positive(), + image: image().optional(), + mode: reference("modes").optional(), + hideTeamSize: z.boolean().optional().default(false), + verwantwortlich: z.string().optional(), + viewConfig: EventViewConfigSchema.optional(), + }), +}); + export const collections = { pages: pages, help: help, @@ -118,4 +132,5 @@ export const collections = { announcements: announcements, publics: publics, docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), + events: events, }; diff --git a/src/content/events/wg-sfa.md b/src/content/events/wg-sfa.md new file mode 100644 index 0000000..38239ed --- /dev/null +++ b/src/content/events/wg-sfa.md @@ -0,0 +1,39 @@ +--- +eventId: 75 +mode: "wargear" +verwantwortlicher: "Chaoscaot" +image: ../../images/generated-image(11).png +--- + +**Ahoi, liebe Community,** + +lange ist es her seit dem letzten WarGear-Event. Nun ist es so weit: Am **29. und 30. November** findet ein neues WarGear-Event **mit** SFAs statt. + +## Übersicht + +- **Datum:** 29.11.: Gruppenphase, 30.11.: KO-Phase +- **Spielmodus:** Standard **und** Pro WarGear +- **Teamgröße**: 6 +- **Anmeldeschluss:** 22. November +- **Einsendeschluss:** 24. November +- **Hotfix-Schluss:** 27. November + +Bei der SFA muss sich an eines der Regelwerke gehalten werden. Standard- und Pro-WarGear treten gleichwertig gegeneinander an. + +## Sonderregeln + +**Version:** 1.21.6 (aktuellste Bau-Version) + +Es wird einen eigenen Schematic-Typen geben. + +### Windcharges + +Werden beim Überfliegen der Mittellinie entfernt. + +### Cobwebs & Powder Snow + +Dürfen uneingeschränkt benutzt werden, jedoch nicht als Panzerung. Die Bewertung liegt im Ermessen des Prüfers. + +**Verantwortlicher:** Chaoscaot + +**Frohes Bauen!** diff --git a/src/i18n/common/de.json b/src/i18n/common/de.json index a32b101..5e9025d 100644 --- a/src/i18n/common/de.json +++ b/src/i18n/common/de.json @@ -1,263 +1,264 @@ { - "home": { - "page": "SteamWar - Startseite", - "title": { - "first": "Steam", - "second": "War" - }, - "subtitle": { - "1": "WarGears, AirShips, WarShips", - "2": "Spieler Online: ", - "3": "Version: 1.12 - 1.21" - }, - "join": "Jetzt Spielen", - "benefits": { - "fights": { - "title": "Spannende Kämpfe", - "description": { - "1": "Messe dich mit anderen Spielern in der Arena und zeige, dass du der beste bist.", - "2": "Von der kleinen Kampfmaschine bis zum riesigen Schlachtschiff kann alles gebaut werden." + "home": { + "page": "SteamWar - Startseite", + "title": { + "first": "Steam", + "second": "War" + }, + "subtitle": { + "1": "WarGears, AirShips, WarShips", + "2": "Spieler Online: ", + "3": "Version: 1.12 - 1.21" + }, + "join": "Jetzt Spielen", + "benefits": { + "fights": { + "title": "Spannende Kämpfe", + "description": { + "1": "Messe dich mit anderen Spielern in der Arena und zeige, dass du der beste bist.", + "2": "Von der kleinen Kampfmaschine bis zum riesigen Schlachtschiff kann alles gebaut werden." + } + }, + "bau": { + "title": "Eigene Bauserver", + "description": "Jeder Spieler bekommt einen eigenen Bauserver, auf dem ohne Einschränkungen und Lags mit FaWe und Axiom gebaut werden kann." + }, + "minigames": { + "title": "Minigames", + "description": { + "1": "Neben der Arena gibt es auch noch Minigames, die du mit anderen Spielern spielen kannst.", + "2": "Klassiker wie MissleWars, TowerRun oder TNTLeague warten auf dich." + } + }, + "open": { + "title": "Free & Open", + "description": "Das Spielen auf SteamWar ist komplett Kostenlos und unsere Software ist Open Source." + }, + "read": "Mehr Lesen" + }, + "prefix": { + "Admin": "Administrator", + "Dev": "Developer", + "Mod": "Moderator", + "Sup": "Supporter", + "Arch": "Architekt", + "User": "Spieler", + "YT": "YouTuber" } - }, - "bau": { - "title": "Eigene Bauserver", - "description": "Jeder Spieler bekommt einen eigenen Bauserver, auf dem ohne Einschränkungen und Lags mit FaWe und Axiom gebaut werden kann." - }, - "minigames": { - "title": "Minigames", - "description": { - "1": "Neben der Arena gibt es auch noch Minigames, die du mit anderen Spielern spielen kannst.", - "2": "Klassiker wie MissleWars, TowerRun oder TNTLeague warten auf dich." + }, + "status": { + "loading": "Lade...", + "status": "Status", + "online": "Online", + "offline": "Offline", + "players": "Spieler: {# count #}", + "version": "Version: {# version #}" + }, + "navbar": { + "title": "SteamWar", + "logo": { + "alt": "SteamWar Logo" + }, + "links": { + "home": { + "title": "Start", + "announcements": "Ankündigungen", + "events": "Events", + "about": "Über Uns", + "downloads": "Downloads", + "faq": "FAQ" + }, + "rules": { + "title": "Spielmodi", + "wg": "WarGear", + "mwg": "MiniWarGear", + "ws": "WarShip", + "as": "AirShip", + "qg": "QuickGear", + "rotating": "Rotierend", + "megawg": "MegaWarGear", + "micro": "MicroWarGear", + "sf": "StreetFight", + "general": "Allgemein", + "coc": "Verhaltensrichtlinien", + "publics": "Publics", + "ranked": "Ranked" + }, + "help": { + "title": "Hilfe", + "docs": "Dokumentation" + }, + "account": "Konto", + "ranked": { + "mw": "MissileWars" + } } - }, - "open": { - "title": "Free & Open", - "description": "Das Spielen auf SteamWar ist komplett Kostenlos und unsere Software ist Open Source." - }, - "read": "Mehr Lesen" }, - "prefix": { - "Admin": "Administrator", - "Dev": "Developer", - "Mod": "Moderator", - "Sup": "Supporter", - "Arch": "Architekt", - "User": "Spieler", - "YT": "YouTuber" - } - }, - "status": { - "loading": "Lade...", - "status": "Status", - "online": "Online", - "offline": "Offline", - "players": "Spieler: {# count #}", - "version": "Version: {# version #}" - }, - "navbar": { - "title": "SteamWar", - "logo": { - "alt": "SteamWar Logo" - }, - "links": { - "home": { - "title": "Start", - "announcements": "Ankündigungen", - "about": "Über Uns", - "downloads": "Downloads", - "faq": "FAQ" - }, - "rules": { - "title": "Spielmodi", - "wg": "WarGear", - "mwg": "MiniWarGear", - "ws": "WarShip", - "as": "AirShip", - "qg": "QuickGear", - "rotating": "Rotierend", - "megawg": "MegaWarGear", - "micro": "MicroWarGear", - "sf": "StreetFight", - "general": "Allgemein", - "coc": "Verhaltensrichtlinien", - "publics": "Publics", - "ranked": "Ranked" - }, - "help": { - "title": "Hilfe", - "docs": "Dokumentation" - }, - "account": "Konto", - "ranked": { - "mw": "MissileWars" - } - } - }, - "wg": { - "title": "WarGear" - }, - "bwg": { - "title": "Basic WarGear" - }, - "swg": { - "title": "(Standard) WarGear" - }, - "pwg": { - "title": "Pro WarGear" - }, - "mwg": { - "title": "MiniWarGear" - }, - "ws": { - "title": "WarShip" - }, - "as": { - "title": "AirShip" - }, - "qg": { - "title": "QuickGear" - }, - "sf": { - "title": "StreetFight" - }, - "megawg": { - "title": "MegaWarGear" - }, - "microwg": { - "title": "MicroWarGear" - }, - "mw": { - "title": "MissileWars" - }, - "footer": { - "imprint": "Impressum", - "privacy": "Datenschutzerklärung", - "coc": "Verhaltensrichtlinien", - "stats": "Statistiken", - "gamemodes": "Spielmodi", - "announcements": "Ankündigungen", - "join": "Jetzt Spielen" - }, - "elo": { - "place": "Platz", - "name": "Name", - "elo": "Elo" - }, - "tag": { - "title": "Tag: {# tag #} - SteamWar" - }, - "announcements": { - "table": { - "time": "Startzeit", - "blue": "Blaues Team", - "red": "Rotes Team", - "winner": "Sieger", - "notPlayed": "Nicht gespielt", - "draw": "Unentschieden", - "points": "Punkte", - "team": "Team" - } - }, - "blog": { - "title": "Ankündigungen - SteamWar" - }, - "dashboard": { - "page": "SteamWar - Dashboard", - "title": "Hallo, {# name #}!", - "rank": "Rang: {# rank #}", - "permissions": "Berechtigungen:", - "buttons": { - "logout": "Abmelden", - "admin": "Admin Panel" - }, - "stats": { - "playtime": "Spielzeit: {# playtime #}", - "fights": "Kämpfe: {# fights #}", - "checked": "Freigegebene Schematics: {# checked #}" - }, - "schematic": { - "upload": "Hochladen", - "dir": "Ordner", - "home": "Schematics", - "head": { - "type": "Typ", - "name": "Name", - "owner": "Besitzer", - "updated": "Aktualisiert", - "replaceColor": "Farbe ersetzen", - "allowReplay": "Wiederholung erlauben" - }, - "info": { - "path": "Pfad: {# path #}", - "replaceColor": "Farbe ersetzen: ", - "allowReplay": "Replay gestattet: ", - "type": "Typ: {# type #}", - "updated": "Zuletzt geändert: {# updated #}", - "item": "Item: {# item #}", - "members": "Zugriff: {# members #}", - "btn": { - "close": "Schließen" - } - }, - "cancel": "Abbrechen", - "title": "Schematic hochladen", - "errors": { - "invalidEnding": "Diese Dateiendung kann nicht Hochgeladen werden.", - "noFile": "Keine Datei.", - "upload": "Fehler beim Hochladen, Überprüfe deine Schematic!" - } - } - }, - "login": { - "page": "SteamWar - Login", - "title": "Login", - "placeholder": { - "username": "Nutzername...", - "password": "***************" - }, - "label": { - "username": "Nutzername", - "password": "Passwort", - "repeat": "Passwort Wiederholen" - }, - "setPassword": "Wie setze ich mein Passwort?", - "submit": "Login", - "error": "Falscher Nutzername oder falsches Passwort" - }, - "ranked": { - "title": "{# mode #} - Rangliste" - }, - "rules": { - "page": "SteamWar - Regelwerke", "wg": { - "description": "Heute werden die Schlachtfelder der Erde von schwerem Geschütz bestimmt. Mit unserem traditionellen Regelwerk sind auch die WarGears arenenverwüstende Schwergewichte. Aufgrund der Kanonentechnik mit den meisten Projektilen erwarten dich bei WarGears harte und kurzweilige Kämpfe." + "title": "WarGear" + }, + "bwg": { + "title": "Basic WarGear" + }, + "swg": { + "title": "(Standard) WarGear" + }, + "pwg": { + "title": "Pro WarGear" }, "mwg": { - "description": "Im heutigen Straßenkampf hat massives Gerät keinen Platz, weswegen kleinere Maschinen auch heute noch ihre Berechtigung haben. Mit den etwas kleineren Kanonen sind MiniWarGears genau das richtige für Einsteiger, Gelegenheitsspieler und Experimentierfreudige." + "title": "MiniWarGear" }, "ws": { - "description": "Lange Zeit waren Kriegsschiffe das Nonplusultra der Kriegsführung. In Sachen Raketen- und Slimetechnik gilt das auch heute noch für Warships. Durch die begrenzte Kanonenstärke bieten WarShips lange, intensive und abwechslungsreiche Kämpfe, womit es immer neue Technik in der Arena gibt. Durch das Entern verlagert sich ein WarShip-Kampf nach einiger Zeit in das Wasser und bietet damit spannende PvP-Action." + "title": "WarShip" }, "as": { - "description": "Der Traum vom Fliegen beflügelt die Menschheit schon seit Jahrtausenden. Der Spielmodus AirShips bietet dir die nahezu unbegrenzten Möglichkeiten des Himmels. Egal, ob du mit 15 2 Projektil-Kanonen oder 2 15 Projektil-Kanonen antrittst, du hast stets eine realistische Chance auf den Sieg. Denn: Alles hat seinen Preis." + "title": "AirShip" }, "qg": { - "description": "Nicht immer besteht die Zeit für einen langen Bau. Manchmal muss es schnell gehen. Für diese Fälle gibt es QuickGears. Ohne Qualitätsprüfung und mit nur einem Klick kannst du hier ein Gefährt erstellen. Die Qualität ist dabei nicht immer die beste, aber für einen schnellen Kampf reicht es allemal." + "title": "QuickGear" }, - "rules": "Regelwerk »", - "announcements": "Ankündigungen »", - "ranking": "Rangliste »", - "title": "{# mode #} - Regelwerk", - "publics": "Publics »" - }, - "stats": { - "title": "Kampf Statistiken" - }, - "ranking": { - "heading": "{# mode #} Rangliste" - }, - "404": { - "title": "404 - Seite nicht gefunden", - "description": "Seite nicht gefunden" - } + "sf": { + "title": "StreetFight" + }, + "megawg": { + "title": "MegaWarGear" + }, + "microwg": { + "title": "MicroWarGear" + }, + "mw": { + "title": "MissileWars" + }, + "footer": { + "imprint": "Impressum", + "privacy": "Datenschutzerklärung", + "coc": "Verhaltensrichtlinien", + "stats": "Statistiken", + "gamemodes": "Spielmodi", + "announcements": "Ankündigungen", + "join": "Jetzt Spielen" + }, + "elo": { + "place": "Platz", + "name": "Name", + "elo": "Elo" + }, + "tag": { + "title": "Tag: {# tag #} - SteamWar" + }, + "announcements": { + "table": { + "time": "Startzeit", + "blue": "Blaues Team", + "red": "Rotes Team", + "winner": "Sieger", + "notPlayed": "Nicht gespielt", + "draw": "Unentschieden", + "points": "Punkte", + "team": "Team" + } + }, + "blog": { + "title": "Ankündigungen - SteamWar" + }, + "dashboard": { + "page": "SteamWar - Dashboard", + "title": "Hallo, {# name #}!", + "rank": "Rang: {# rank #}", + "permissions": "Berechtigungen:", + "buttons": { + "logout": "Abmelden", + "admin": "Admin Panel" + }, + "stats": { + "playtime": "Spielzeit: {# playtime #}", + "fights": "Kämpfe: {# fights #}", + "checked": "Freigegebene Schematics: {# checked #}" + }, + "schematic": { + "upload": "Hochladen", + "dir": "Ordner", + "home": "Schematics", + "head": { + "type": "Typ", + "name": "Name", + "owner": "Besitzer", + "updated": "Aktualisiert", + "replaceColor": "Farbe ersetzen", + "allowReplay": "Wiederholung erlauben" + }, + "info": { + "path": "Pfad: {# path #}", + "replaceColor": "Farbe ersetzen: ", + "allowReplay": "Replay gestattet: ", + "type": "Typ: {# type #}", + "updated": "Zuletzt geändert: {# updated #}", + "item": "Item: {# item #}", + "members": "Zugriff: {# members #}", + "btn": { + "close": "Schließen" + } + }, + "cancel": "Abbrechen", + "title": "Schematic hochladen", + "errors": { + "invalidEnding": "Diese Dateiendung kann nicht Hochgeladen werden.", + "noFile": "Keine Datei.", + "upload": "Fehler beim Hochladen, Überprüfe deine Schematic!" + } + } + }, + "login": { + "page": "SteamWar - Login", + "title": "Login", + "placeholder": { + "username": "Nutzername...", + "password": "***************" + }, + "label": { + "username": "Nutzername", + "password": "Passwort", + "repeat": "Passwort Wiederholen" + }, + "setPassword": "Wie setze ich mein Passwort?", + "submit": "Login", + "error": "Falscher Nutzername oder falsches Passwort" + }, + "ranked": { + "title": "{# mode #} - Rangliste" + }, + "rules": { + "page": "SteamWar - Regelwerke", + "wg": { + "description": "Heute werden die Schlachtfelder der Erde von schwerem Geschütz bestimmt. Mit unserem traditionellen Regelwerk sind auch die WarGears arenenverwüstende Schwergewichte. Aufgrund der Kanonentechnik mit den meisten Projektilen erwarten dich bei WarGears harte und kurzweilige Kämpfe." + }, + "mwg": { + "description": "Im heutigen Straßenkampf hat massives Gerät keinen Platz, weswegen kleinere Maschinen auch heute noch ihre Berechtigung haben. Mit den etwas kleineren Kanonen sind MiniWarGears genau das richtige für Einsteiger, Gelegenheitsspieler und Experimentierfreudige." + }, + "ws": { + "description": "Lange Zeit waren Kriegsschiffe das Nonplusultra der Kriegsführung. In Sachen Raketen- und Slimetechnik gilt das auch heute noch für Warships. Durch die begrenzte Kanonenstärke bieten WarShips lange, intensive und abwechslungsreiche Kämpfe, womit es immer neue Technik in der Arena gibt. Durch das Entern verlagert sich ein WarShip-Kampf nach einiger Zeit in das Wasser und bietet damit spannende PvP-Action." + }, + "as": { + "description": "Der Traum vom Fliegen beflügelt die Menschheit schon seit Jahrtausenden. Der Spielmodus AirShips bietet dir die nahezu unbegrenzten Möglichkeiten des Himmels. Egal, ob du mit 15 2 Projektil-Kanonen oder 2 15 Projektil-Kanonen antrittst, du hast stets eine realistische Chance auf den Sieg. Denn: Alles hat seinen Preis." + }, + "qg": { + "description": "Nicht immer besteht die Zeit für einen langen Bau. Manchmal muss es schnell gehen. Für diese Fälle gibt es QuickGears. Ohne Qualitätsprüfung und mit nur einem Klick kannst du hier ein Gefährt erstellen. Die Qualität ist dabei nicht immer die beste, aber für einen schnellen Kampf reicht es allemal." + }, + "rules": "Regelwerk »", + "announcements": "Ankündigungen »", + "ranking": "Rangliste »", + "title": "{# mode #} - Regelwerk", + "publics": "Publics »" + }, + "stats": { + "title": "Kampf Statistiken" + }, + "ranking": { + "heading": "{# mode #} Rangliste" + }, + "404": { + "title": "404 - Seite nicht gefunden", + "description": "Seite nicht gefunden" + } } diff --git a/src/layouts/PageLayout.astro b/src/layouts/PageLayout.astro index c4fa75f..9128d47 100644 --- a/src/layouts/PageLayout.astro +++ b/src/layouts/PageLayout.astro @@ -2,7 +2,7 @@ import NavbarLayout from "./NavbarLayout.astro"; import BackgroundImage from "../components/BackgroundImage.astro"; -const { title, description } = Astro.props; +const { title, description, wide = false } = Astro.props; --- @@ -10,8 +10,11 @@ const { title, description } = Astro.props;
-
+
diff --git a/src/pages/ankuendigungen/[...page].astro b/src/pages/ankuendigungen/[...page].astro index 25f507c..90486c5 100644 --- a/src/pages/ankuendigungen/[...page].astro +++ b/src/pages/ankuendigungen/[...page].astro @@ -1,43 +1,48 @@ --- -import {getCollection} from "astro:content"; +import { getCollection } from "astro:content"; import PageLayout from "../../layouts/PageLayout.astro"; -import {astroI18n, createGetStaticPaths, t} from "astro-i18n"; +import { astroI18n, createGetStaticPaths, t } from "astro-i18n"; import PostComponent from "../../components/PostComponent.astro"; import dayjs from "dayjs"; import TagComponent from "../../components/TagComponent.astro"; import SWPaginator from "@components/styled/SWPaginator.svelte"; export const getStaticPaths = createGetStaticPaths(async (props) => { - const posts = await getCollection("announcements", entry => entry.id.split("/")[0] === astroI18n.locale); + const posts = await getCollection("announcements", (entry) => entry.id.split("/")[0] === astroI18n.locale); - const germanPosts = await getCollection("announcements", entry => entry.id.split("/")[0] === astroI18n.fallbackLocale); + const germanPosts = await getCollection("announcements", (entry) => entry.id.split("/")[0] === astroI18n.fallbackLocale); - germanPosts.forEach(value => { - if (posts.find(post => post.data.key === value.data.key)) { + germanPosts.forEach((value) => { + if (posts.find((post) => post.data.key === value.data.key)) { return; } else { posts.push(value); } }); - return props.paginate(posts.sort((a, b) => dayjs(b.data.created).unix() - dayjs(a.data.created).unix()), { - pageSize: 5, - }); + return props.paginate( + posts.sort((a, b) => dayjs(b.data.created).unix() - dayjs(a.data.created).unix()), + { + pageSize: 5, + } + ); }); async function getTags() { const posts = await getCollection("announcements"); const tags = new Map(); - posts.forEach(post => { - post.data.tags.forEach(tag => { - if (tags.has(tag)) { - tags.set(tag, tags.get(tag) + 1); + posts.forEach((post) => { + post.data.tags.forEach((tag) => { + if (tags.has(tag.toLowerCase())) { + tags.set(tag.toLowerCase(), tags.get(tag) + 1); } else { - tags.set(tag, 1); + tags.set(tag.toLowerCase(), 1); } }); }); - return Array.from(tags).sort((a, b) => b[1] - a[1]).map(value => value[0]); + return Array.from(tags) + .sort((a, b) => b[1] - a[1]) + .map((value) => value[0]); } const { page } = Astro.props; @@ -46,15 +51,15 @@ const tags = await getTags();
- {tags.map(tag => ( - - ))} + {tags.map((tag) => )}
- {page.data.map((post) => ( -
- -
- ))} + { + page.data.map((post) => ( +
+ +
+ )) + } i == 0 ? page.url.first : page.currentPage === page.lastPage ? page.url.current.replace(page.lastPage, i + 1) : page.url.last.replace(page.lastPage, i + 1)} + pagesUrl={(i) => (i == 0 ? page.url.first : page.currentPage === page.lastPage ? page.url.current.replace(page.lastPage, i + 1) : page.url.last.replace(page.lastPage, i + 1))} /> -
\ No newline at end of file + diff --git a/src/pages/ankuendigungen/tags/[tag].astro b/src/pages/ankuendigungen/tags/[tag].astro index 2bdcbaa..effa20b 100644 --- a/src/pages/ankuendigungen/tags/[tag].astro +++ b/src/pages/ankuendigungen/tags/[tag].astro @@ -1,24 +1,24 @@ --- -import {CollectionEntry} from "astro:content"; -import {astroI18n, createGetStaticPaths, t} from "astro-i18n"; -import {getCollection} from "astro:content"; +import { CollectionEntry } from "astro:content"; +import { astroI18n, createGetStaticPaths, t } from "astro-i18n"; +import { getCollection } from "astro:content"; import PageLayout from "../../../layouts/PageLayout.astro"; -import {capitalize} from "../../../components/admin/util"; +import { capitalize } from "../../../components/admin/util"; import PostComponent from "../../../components/PostComponent.astro"; import dayjs from "dayjs"; import { ArrowLeftOutline } from "flowbite-svelte-icons"; -import {l} from "../../../util/util"; +import { l } from "../../../util/util"; import TagComponent from "../../../components/TagComponent.astro"; export const getStaticPaths = createGetStaticPaths(async () => { - let posts = (await getCollection("announcements", entry => entry.id.split("/")[0] === astroI18n.locale)); + let posts = await getCollection("announcements", (entry) => entry.id.split("/")[0] === astroI18n.locale); - const germanPosts = await getCollection("announcements", entry => entry.id.split("/")[0] === "de"); + const germanPosts = await getCollection("announcements", (entry) => entry.id.split("/")[0] === "de"); posts.sort((a, b) => dayjs(b.data.created).unix() - dayjs(a.data.created).unix()); - germanPosts.forEach(value => { - if (posts.find(post => post.data.key === value.data.key)) { + germanPosts.forEach((value) => { + if (posts.find((post) => post.data.key === value.data.key)) { return; } else { posts.push(value); @@ -28,16 +28,16 @@ export const getStaticPaths = createGetStaticPaths(async () => { posts = posts.filter((value, index) => index < 20); let groupedByTags: Record[]> = {}; - posts.forEach(post => { - post.data.tags.forEach(tag => { - if (!groupedByTags[tag]) { - groupedByTags[tag] = []; + posts.forEach((post) => { + post.data.tags.forEach((tag) => { + if (!groupedByTags[tag.toLowerCase()]) { + groupedByTags[tag.toLowerCase()] = []; } - groupedByTags[tag].push(post); + groupedByTags[tag.toLowerCase()].push(post); }); }); - return Object.keys(groupedByTags).map(tag => ({ + return Object.keys(groupedByTags).map((tag) => ({ params: { tag: tag, }, @@ -53,19 +53,21 @@ interface Props { tag: string; } -const {posts, tag} = Astro.props; +const { posts, tag } = Astro.props; --- - + - {posts.map((post, index) => ( -
- -
- ))} -
\ No newline at end of file + { + posts.map((post, index) => ( +
+ +
+ )) + } +
diff --git a/src/pages/events/[slug].astro b/src/pages/events/[slug].astro new file mode 100644 index 0000000..83066a9 --- /dev/null +++ b/src/pages/events/[slug].astro @@ -0,0 +1,85 @@ +--- +import type { ExtendedEvent } from "@components/types/event"; +import PageLayout from "@layouts/PageLayout.astro"; +import { astroI18n, createGetStaticPaths } from "astro-i18n"; +import { getCollection, type CollectionEntry } from "astro:content"; +import EventFights from "@components/event/EventFights.svelte"; + +export const getStaticPaths = createGetStaticPaths(async () => { + const events = await Promise.all( + (await getCollection("events")).map(async (event) => ({ + event: (await fetch(import.meta.env.PUBLIC_API_SERVER + "/events/" + event.data.eventId).then((value) => value.json())) as ExtendedEvent, + page: event, + })) + ); + + return events.map((event) => ({ + props: { + event: event.event, + page: event.page, + }, + params: { + slug: event.page.slug, + }, + })); +}); + +const { event, page } = Astro.props as { event: ExtendedEvent; page: CollectionEntry<"events"> }; + +const { Content } = await page.render(); +--- + + +
+

{event.event.name}

+

+ { + new Date(event.event.start).toLocaleDateString(astroI18n.locale, { + year: "numeric", + month: "numeric", + day: "numeric", + }) + } + { + new Date(event.event.start).toDateString() !== new Date(event.event.end).toDateString() + ? ` - ${new Date(event.event.end).toLocaleDateString(astroI18n.locale, { + year: "numeric", + month: "numeric", + day: "numeric", + })}` + : "" + } +

+
+
+ +
+ { + page.data.viewConfig && ( +
+

Kampfplan

+ +
+ ) + } +
+ + diff --git a/src/pages/events/index.astro b/src/pages/events/index.astro new file mode 100644 index 0000000..88a3e9d --- /dev/null +++ b/src/pages/events/index.astro @@ -0,0 +1,36 @@ +--- +import type { ExtendedEvent } from "@components/types/event"; +import PageLayout from "@layouts/PageLayout.astro"; +import { getCollection } from "astro:content"; + +const events = await Promise.all( + (await getCollection("events")).map(async (event) => ({ + ...event, + data: { + ...event.data, + event: (await fetch(import.meta.env.PUBLIC_API_SERVER + "/events/" + event.data.eventId).then((value) => value.json())) as ExtendedEvent, + }, + })) +); +--- + + + { + events.map((event) => ( + + )) + } +