New Code Editor and fun

Dieser Commit ist enthalten in:
Chaoscaot 2023-12-03 19:31:29 +01:00
Ursprung 2abe554059
Commit fbd52f3edb
53 geänderte Dateien mit 1330 neuen und 489 gelöschten Zeilen

Datei anzeigen

@ -18,6 +18,9 @@ export default defineAstroI18nConfig({
"code-of-conduct": "verhaltensrichtlinien", "code-of-conduct": "verhaltensrichtlinien",
rules: "regeln", rules: "regeln",
ranked: "rangliste", ranked: "rangliste",
faq: "haeufige-fragen",
stats: "statistiken",
announcements: "ankuendigungen",
} }
}, },
}) })

Datei anzeigen

@ -11,7 +11,5 @@ export default defineConfig({
}, },
compressHTML: true, compressHTML: true,
integrations: [svelte(), tailwind(), configureI18n()], integrations: [svelte(), tailwind(), configureI18n()],
vite: { vite: {},
});
}
});

Datei anzeigen

@ -20,30 +20,32 @@
"@astropub/icons": "^0.2.0", "@astropub/icons": "^0.2.0",
"@types/color": "^3.0.6", "@types/color": "^3.0.6",
"@types/crypto-js": "^4.2.1", "@types/crypto-js": "^4.2.1",
"@types/node": "^20.9.3", "@types/node": "^20.10.1",
"cssnano": "^6.0.1", "cssnano": "^6.0.1",
"esbuild": "^0.19.7", "esbuild": "^0.19.8",
"postcss-nesting": "^12.0.1", "postcss-nesting": "^12.0.1",
"sass": "^1.69.5", "sass": "^1.69.5",
"svelte": "^4.2.7", "svelte": "^4.2.8",
"tailwind-merge": "^2.0.0", "tailwind-merge": "^2.0.0",
"tailwindcss": "^3.3.5" "tailwindcss": "^3.3.5"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.2.2",
"@ddietr/codemirror-themes": "^1.4.2", "@ddietr/codemirror-themes": "^1.4.2",
"astro": "^3.5.6", "astro": "^3.6.4",
"astro-i18n": "^2.1.18", "astro-i18n": "^2.1.18",
"chart.js": "^4.4.0",
"chartjs-adapter-moment": "^1.0.1",
"color": "^4.2.3", "color": "^4.2.3",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"easymde": "^2.18.0",
"flowbite": "^1.8.1", "flowbite": "^1.8.1",
"flowbite-svelte": "^0.44.19", "flowbite-svelte": "^0.44.20",
"flowbite-svelte-icons": "^0.4.5", "flowbite-svelte-icons": "^0.4.5",
"moment": "^2.29.4", "moment": "^2.29.4",
"sharp": "^0.32.6", "sharp": "^0.32.6",
"svelte-awesome": "^3.2.1", "svelte-awesome": "^3.2.1",
"svelte-codemirror-editor": "^1.1.0", "svelte-codemirror-editor": "^1.2.0",
"svelte-spa-router": "^3.3.0", "svelte-spa-router": "^3.3.0",
"zod": "^3.22.4" "zod": "^3.22.4"
} }

Datei-Diff unterdrückt, da er zu groß ist Diff laden

Datei anzeigen

@ -0,0 +1,44 @@
<script lang="ts">
import {statsRepo} from "./repo/repo.ts";
export let gamemode: string;
let request = getRequest();
function getRequest() {
return $statsRepo.getRankings(gamemode);
}
</script>
{#await request}
<p>Loading...</p>
{:then data}
<div>
<table>
<tr class="font-bold">
<td>Platz</td>
<td>Spieler</td>
<td>Elo</td>
</tr>
{#each data as player, i}
<tr>
<td>{`${i + 1}.`}</td>
<td>{player.name}</td>
<td>{player.elo}</td>
</tr>
{/each}
</table>
</div>
{:catch error}
<p>{error.message}</p>
{/await}
<style lang="scss">
table {
@apply w-full;
}
div {
@apply p-3 bg-gray-200 dark:bg-gray-800 rounded-2xl;
}
</style>

Datei anzeigen

@ -0,0 +1,19 @@
<script lang="ts">
import {statsRepo} from "./repo/repo.ts";
import FightStatsChart from "./FightStatsChart.svelte";
import {t} from "astro-i18n";
let request = getStats();
function getStats() {
return $statsRepo.getFightStats();
}
</script>
{#await request}
<p>{t("status.loading")}</p>
{:then stats}
<FightStatsChart data={stats} />
{:catch error}
<p>error: {error}</p>
{/await}

Datei anzeigen

@ -0,0 +1,73 @@
<script lang="ts">
import {onMount} from "svelte"
import {
Chart,
LineController,
LineElement,
PointElement,
LinearScale,
TimeScale,
Tooltip,
Legend,
Title
} from "chart.js"
import type {FightStats} from "./types/stats.ts"
import 'chartjs-adapter-moment'
export let data: FightStats;
let chart;
let canvas: HTMLCanvasElement;
onMount(async () => {
Chart.register(LineController, LineElement, PointElement, LinearScale, TimeScale, Tooltip, Legend, Title)
if (document.body.parentElement!.classList.contains("dark")) {
Chart.defaults.color = "#fff"
}
const colors = ["#abfa91", "#279900", "#00ffbe", "#9297fb", "#050b9d", "#b60fff", "#8dddfc", "#0880ad", "#41ff00", "#039973", "#96fce2", "#0009ff", "#7501a4", "#e2a2fb", "#00b9ff"];
const map = new Map<string, {x: Date, y: number}[]>()
for (const {date, count, gamemode} of data) {
if (!map.has(gamemode)) {
map.set(gamemode, [])
}
map.get(gamemode)!!.push({x: new Date(date), y: count})
}
chart = new Chart(
canvas,
{
type: "line",
data: {
datasets: Array.from(map.entries()).map(([gamemode, data]) => {
const color = colors.pop()
return {
label: gamemode,
fill: false,
borderColor: color,
backgroundColor: color,
spanGaps: true,
data
};
})
},
options: {
scales: {
x: {
type: "time",
time: {
unit: "day"
}
},
y: {
beginAtZero: true
}
}
}
}
)
})
</script>
<div>
<canvas bind:this={canvas} />
</div>

Datei anzeigen

@ -0,0 +1,50 @@
<script lang="ts">
import {eventRepo} from "./repo/repo.ts";
import {astroI18n} from "astro-i18n";
import type {ExtendedEvent} from "./types/event.ts";
export let event: ExtendedEvent;
export let group: string;
export let rows: number = 1;
function window<T>(arr: T[], len: number): T[][] {
let result: T[][] = [];
for (let i = 0; i < arr.length; i += len) {
result.push(arr.slice(i, i + len));
}
return result;
}
</script>
<div>
<table>
<tr class="font-bold border-b">
{#each Array(rows) as _}
<td>Time</td>
<td>Blue Team</td>
<td>Red Team</td>
{/each}
</tr>
{#each window(event.fights.filter(f => f.group === group), rows) as fights}
<tr>
{#each fights as fight}
<td>{Intl.DateTimeFormat(astroI18n.locale, {
hour: "numeric",
minute: "numeric",
}).format(new Date(fight.start))}</td>
<td class:font-bold={fight.ergebnis === 1} class:italic={fight.ergebnis === 3}>{fight.blueTeam.kuerzel}</td>
<td class:font-bold={fight.ergebnis === 2} class:italic={fight.ergebnis === 3}>{fight.redTeam.kuerzel}</td>
{/each}
</tr>
{/each}
</table>
</div>
<style lang="scss">
table {
@apply w-full;
}
div {
@apply p-3 bg-gray-200 dark:bg-gray-800 rounded-2xl w-3/4 mx-auto;
}
</style>

Datei anzeigen

@ -0,0 +1,37 @@
---
import {CollectionEntry} from "astro:content"
import {l} from "../util/util";
import {astroI18n} from "astro-i18n";
import {Image} from "astro:assets";
import TagComponent from "./TagComponent.astro";
interface Props {
post: CollectionEntry<'announcements'>
}
const { post } = Astro.props as Props;
---
<a href={l(`/announcements/${post.slug.split("/").slice(1).join("/")}`)}>
<div class="p-4 flex flex-row">
{post.data.image != null ? (
<div class="flex-shrink-0 pr-2">
<Image src={post.data.image} alt="Post Image" class="rounded-2xl shadow-2xl object-cover h-32 w-32 max-w-none transition-transform hover:scale-105" />
</div>
) : null}
<div>
<h2 class="text-2xl font-bold">{post.data.title}</h2>
<div class="text-gray-500">{Intl.DateTimeFormat(astroI18n.locale, {
day: "numeric",
month: "long",
year: "numeric"
}).format(post.data.created)}</div>
<div>{post.data.description}</div>
<div>
{post.data.tags.map((tag) => (
<TagComponent tag={tag} />
))}
</div>
</div>
</div>
</a>

Datei anzeigen

@ -0,0 +1,16 @@
---
import {l} from "../util/util";
import {capitalize} from "./admin/util";
interface Props {
tag: string;
}
const {tag} = Astro.props;
---
<a href={l(`/announcements/tags/${tag}`)}>
<span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 shadow-2xl">{capitalize(tag)}</span>
</a>

Datei anzeigen

@ -6,7 +6,7 @@
import {eventRepo} from "../../repo/repo.js"; import {eventRepo} from "../../repo/repo.js";
import TeamList from "./event/TeamList.svelte"; import TeamList from "./event/TeamList.svelte";
export let params: { id: number } = {}; export let params: { id: number };
let id = params.id; let id = params.id;
let event = $eventRepo.getEvent(id.toString()); let event = $eventRepo.getEvent(id.toString());

Datei anzeigen

@ -4,20 +4,21 @@
import {players} from "../../stores/stores.ts"; import {players} from "../../stores/stores.ts";
import {permsRepo} from "../../repo/repo.ts"; import {permsRepo} from "../../repo/repo.ts";
import {capitalize} from "../util.ts"; import {capitalize} from "../util.ts";
import type {Player} from "../../types/data.ts";
let search = ""; let search = "";
$: lowerCaseSearch = search.toLowerCase(); $: lowerCaseSearch = search.toLowerCase();
$: filteredPlayers = $players.filter(value => value.name.toLowerCase().includes(lowerCaseSearch)); $: filteredPlayers = $players.filter(value => value.name.toLowerCase().includes(lowerCaseSearch));
let selectedPlayer = null; let selectedPlayer: number | null = null;
$: player = $players.find(value => value.id === selectedPlayer); $: player = $players.find(value => value.id === selectedPlayer);
let playerPerms = loadPlayer(selectedPlayer); let playerPerms = loadPlayer(selectedPlayer);
$: playerPerms = loadPlayer(selectedPlayer); $: playerPerms = loadPlayer(selectedPlayer);
let prefixEdit = "PREFIX_NONE"; let prefixEdit = "PREFIX_NONE";
let activePerms = []; let activePerms: string[] = [];
function loadPlayer(id: number) { function loadPlayer(id: number | null) {
if (!id) { if (!id) {
return; return;
} }
@ -39,20 +40,20 @@
} }
function save() { function save() {
playerPerms.then(async perms => { playerPerms!.then(async perms => {
if (perms.prefix.name != prefixEdit) { if (perms.prefix.name != prefixEdit) {
await $permsRepo.setPrefix(selectedPlayer, prefixEdit); await $permsRepo.setPrefix(selectedPlayer!, prefixEdit);
} }
for (let value of activePerms) { for (let value of activePerms) {
if (!perms.perms.includes(value)) { if (!perms.perms.includes(value)) {
await $permsRepo.addPerm(selectedPlayer, value); await $permsRepo.addPerm(selectedPlayer!, value);
} }
} }
for (let value of perms.perms) { for (let value of perms.perms) {
if (!activePerms.includes(value)) { if (!activePerms.includes(value)) {
await $permsRepo.removePerm(selectedPlayer, value); await $permsRepo.removePerm(selectedPlayer!, value);
} }
} }

Datei anzeigen

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import {Spinner, Toolbar, ToolbarButton, ToolbarGroup, Tooltip} from "flowbite-svelte"; import {Spinner, Toolbar, ToolbarButton, ToolbarGroup, Tooltip} from "flowbite-svelte";
import {markdown} from "@codemirror/lang-markdown";
import {json} from "@codemirror/lang-json"; import {json} from "@codemirror/lang-json";
import CodeMirror from "svelte-codemirror-editor"; import CodeMirror from "svelte-codemirror-editor";
import {pageRepo} from "../../../repo/repo.ts"; import {pageRepo} from "../../../repo/repo.ts";
@ -8,6 +7,7 @@
import type {Page} from "../../../types/page.ts"; import type {Page} from "../../../types/page.ts";
import {materialDark} from '@ddietr/codemirror-themes/material-dark.js' import {materialDark} from '@ddietr/codemirror-themes/material-dark.js'
import {createEventDispatcher} from "svelte"; import {createEventDispatcher} from "svelte";
import MDEMarkdownEditor from "./MDEMarkdownEditor.svelte";
export let pageId: number; export let pageId: number;
export let branch: string; export let branch: string;
@ -56,7 +56,11 @@
</ToolbarGroup> </ToolbarGroup>
</Toolbar> </Toolbar>
</div> </div>
<CodeMirror bind:value={pageContent} lang={page?.name.endsWith("md") ? markdown() : json()} theme={materialDark} /> {#if page?.name.endsWith("md")}
<MDEMarkdownEditor bind:value={pageContent} />
{:else}
<CodeMirror bind:value={pageContent} lang={json()} theme={materialDark} />
{/if}
</div> </div>
{:catch error} {:catch error}
<p>{error.message}</p> <p>{error.message}</p>

Datei anzeigen

@ -0,0 +1,37 @@
<script lang="ts">
import {onDestroy, onMount} from "svelte";
import EasyMDE from "easymde";
import "easymde/dist/easymde.min.css"
export let value: string;
let editor: HTMLTextAreaElement;
let mde: EasyMDE;
onMount(() => {
mde = new EasyMDE({
element: editor,
initialValue: value,
spellChecker: false,
})
mde.codemirror.on("change", () => {
value = mde.value();
})
})
onDestroy(() => {
mde.toTextArea();
mde.cleanup();
})
</script>
<textarea bind:this={editor} class="editor-preview">
</textarea>
<style>
:global(.editor-preview) {
& * {
all: revert;
color: black;
}
}
</style>

Datei anzeigen

@ -0,0 +1,25 @@
<script lang="ts">
import type {Player} from "../types/data.ts";
import {statsRepo} from "../repo/repo.ts";
import {t} from "astro-i18n"
export let user: Player;
let request = getRequest();
function getRequest() {
return $statsRepo.getUserStats(user.id)
}
</script>
{#await request}
<p>{t("status.loading")}</p>
{:then data}
<p>Playtime: {data.playtime}h</p>
<p>Fights: {data.fights}</p>
{#if user.perms.includes("CHECK")}
<p>Schematics Checked: {data.acceptedSchematics}</p>
{/if}
{:catch error}
<p>error: {error}</p>
{/await}

Datei anzeigen

@ -3,6 +3,7 @@
import type {Player} from "../types/data.ts"; import type {Player} from "../types/data.ts";
import {tokenStore} from "../repo/repo.ts"; import {tokenStore} from "../repo/repo.ts";
import {l} from "../../util/util.ts"; import {l} from "../../util/util.ts";
import Statistics from "./Statistics.svelte";
export let user: Player; export let user: Player;
@ -31,11 +32,6 @@
<div> <div>
<h1 class="text-4xl font-bold">{t("dashboard.title", {name: user.name})}</h1> <h1 class="text-4xl font-bold">{t("dashboard.title", {name: user.name})}</h1>
<p>{t("dashboard.rank", {rank: t("home.prefix." + user.prefix)})}</p> <p>{t("dashboard.rank", {rank: t("home.prefix." + user.prefix)})}</p>
<p>{t("dashboard.permissions")}</p> <Statistics {user} />
<ul>
{#each user.perms as permission}
<li class="list-disc ml-6">{permission}</li>
{/each}
</ul>
</div> </div>
</div> </div>

Datei anzeigen

@ -6,10 +6,10 @@ export class DataRepo {
constructor(private token: string) {} constructor(private token: string) {}
public async getServer(): Promise<Server> { public async getServer(): Promise<Server> {
return await fetchWithToken(this.token, "/data/server").then(value => value.json()).then(value => ServerSchema.parse(value)); return await fetchWithToken(this.token, "/data/server").then(value => value.json()).then(ServerSchema.parse);
} }
public async getMe(): Promise<Player> { public async getMe(): Promise<Player> {
return await fetchWithToken(this.token, "/data/me").then(value => value.json()).then(value => PlayerSchema.parse(value)); return await fetchWithToken(this.token, "/data/me").then(value => value.json()).then(PlayerSchema.parse);
} }
} }

Datei anzeigen

@ -25,44 +25,31 @@ export class EventRepo {
constructor(private token: string) {} constructor(private token: string) {}
public async listEvents(): Promise<ShortEvent[]> { public async listEvents(): Promise<ShortEvent[]> {
const res = await fetchWithToken(this.token, "/events"); return await fetchWithToken(this.token, "/events")
.then(value => value.json())
if (res.ok) { .then(value => z.array(ShortEventSchema).parse(value));
return z.array(ShortEventSchema).parse(await res.json());
} else {
throw new Error("Could not fetch events: " + res.statusText);
}
} }
public async getEvent(id: string): Promise<ExtendedEvent> { public async getEvent(id: string): Promise<ExtendedEvent> {
const res = await fetchWithToken(this.token, `/events/${id}`); return await fetchWithToken(this.token, `/events/${id}`)
.then(value => value.json())
if (res.ok) { .then(ExtendedEventSchema.parse);
return ExtendedEventSchema.parse(await res.json());
} else {
throw new Error("Could not fetch event: " + res.statusText);
}
} }
public async createEvent(event: CreateEvent): Promise<SWEvent> { public async createEvent(event: CreateEvent): Promise<SWEvent> {
const res = await fetchWithToken(this.token, "/events", { return await fetchWithToken(this.token, "/events", {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
name: event.name, name: event.name,
start: +event.start, start: +event.start,
end: +event.end end: +event.end
}), }),
}); }).then(value => value.json())
.then(SWEventSchema.parse);
if (res.ok) {
return SWEventSchema.parse(await res.json());
} else {
throw new Error("Could not create event: " + res.statusText);
}
} }
public async updateEvent(id: string, event: UpdateEvent): Promise<SWEvent> { public async updateEvent(id: string, event: UpdateEvent): Promise<SWEvent> {
const res = await fetchWithToken(this.token, `/events/${id}`, { return await fetchWithToken(this.token, `/events/${id}`, {
method: "PUT", method: "PUT",
body: JSON.stringify({ body: JSON.stringify({
name: event.name, name: event.name,
@ -77,13 +64,8 @@ export class EventRepo {
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
} }
}); }).then(value => value.json())
.then(SWEventSchema.parse);
if (res.ok) {
return SWEventSchema.parse(await res.json());
} else {
throw new Error("Could not update event: " + res.statusText);
}
} }
public async deleteEvent(id: string): Promise<boolean> { public async deleteEvent(id: string): Promise<boolean> {

Datei anzeigen

@ -28,17 +28,13 @@ export class FightRepo {
constructor(private token: string) {} constructor(private token: string) {}
public async listFights(eventId: number): Promise<EventFight[]> { public async listFights(eventId: number): Promise<EventFight[]> {
const res = await fetchWithToken(this.token, `/events/${eventId}/fights`); return await fetchWithToken(this.token, `/events/${eventId}/fights`)
.then(value => value.json())
if (res.ok) { .then(value => z.array(EventFightSchema).parse(value));
return z.array(EventFightSchema).parse(await res.json());
} else {
throw new Error("Could not fetch fights: " + res.statusText);
}
} }
public async createFight(eventId: number, fight: CreateFight): Promise<EventFight> { public async createFight(eventId: number, fight: CreateFight): Promise<EventFight> {
let res = await fetchWithToken(this.token, `/fights`, { return await fetchWithToken(this.token, `/fights`, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
event: eventId, event: eventId,
@ -50,17 +46,12 @@ export class FightRepo {
kampfleiter: fight.kampfleiter, kampfleiter: fight.kampfleiter,
group: fight.group group: fight.group
}) })
}) }).then(value => value.json())
.then(EventFightSchema.parse)
if (res.ok) {
return EventFightSchema.parse(await res.json());
} else {
throw new Error("Could not create fight: " + res.statusText);
}
} }
public async updateFight(fightId: number, fight: UpdateFight): Promise<EventFight> { public async updateFight(fightId: number, fight: UpdateFight): Promise<EventFight> {
let res = await fetchWithToken(this.token, `/fights/${fightId}`, { return await fetchWithToken(this.token, `/fights/${fightId}`, {
method: "PUT", method: "PUT",
body: JSON.stringify({ body: JSON.stringify({
spielmodus: fight.spielmodus, spielmodus: fight.spielmodus,
@ -71,13 +62,8 @@ export class FightRepo {
kampfleiter: fight.kampfleiter, kampfleiter: fight.kampfleiter,
group: fight.group group: fight.group
}) })
}) }).then(value => value.json())
.then(EventFightSchema.parse)
if (res.ok) {
return EventFightSchema.parse(await res.json());
} else {
throw new Error("Could not update fight: " + res.statusText);
}
} }
public async deleteFight(fightId: number): Promise<void> { public async deleteFight(fightId: number): Promise<void> {

Datei anzeigen

@ -2,6 +2,7 @@ import type {Page, PageList} from "../types/page.ts";
import {fetchWithToken} from "./repo.ts"; import {fetchWithToken} from "./repo.ts";
import {PageListSchema, PageSchema} from "../types/page.ts"; import {PageListSchema, PageSchema} from "../types/page.ts";
import {bytesToBase64} from "../admin/util.ts"; import {bytesToBase64} from "../admin/util.ts";
import {z} from "zod";
export class PageRepo { export class PageRepo {
constructor(private token: string) {} constructor(private token: string) {}
@ -9,13 +10,14 @@ export class PageRepo {
public async listPages(branch: string = "master"): Promise<PageList> { public async listPages(branch: string = "master"): Promise<PageList> {
return await fetchWithToken(this.token, `/page?branch=${branch}`) return await fetchWithToken(this.token, `/page?branch=${branch}`)
.then(value => value.json()) .then(value => value.json())
.then(value => PageListSchema.parse(value).map(value1 => ({...value1, path: value1.path.replace("src/content/", "")}))) .then(PageListSchema.parse)
.then(value => value.map(value1 => ({...value1, path: value1.path.replace("src/content/", "")})))
} }
public async getPage(id: number, branch: string = "master"): Promise<Page> { public async getPage(id: number, branch: string = "master"): Promise<Page> {
return await fetchWithToken(this.token, `/page/${id}?branch=${branch}`) return await fetchWithToken(this.token, `/page/${id}?branch=${branch}`)
.then(value => value.json()) .then(value => value.json())
.then(value => PageSchema.parse(value)) .then(PageSchema.parse)
} }
public async updatePage(id: number, content: string, sha: string, message: string, branch: string = "master"): Promise<void> { public async updatePage(id: number, content: string, sha: string, message: string, branch: string = "master"): Promise<void> {
@ -31,6 +33,7 @@ export class PageRepo {
public async getBranches(): Promise<string[]> { public async getBranches(): Promise<string[]> {
return await fetchWithToken(this.token, "/page/branch") return await fetchWithToken(this.token, "/page/branch")
.then(value => value.json()) .then(value => value.json())
.then(value => z.array(z.string()).parse(value))
} }
public async createBranch(branch: string): Promise<void> { public async createBranch(branch: string): Promise<void> {

Datei anzeigen

@ -16,13 +16,7 @@ export class PermsRepo {
} }
public async getPerms(userId: number): Promise<UserPerms> { public async getPerms(userId: number): Promise<UserPerms> {
const res = await fetchWithToken(this.token, `/perms/user/${userId}`); return await fetchWithToken(this.token, `/perms/user/${userId}`).then(value => value.json()).then(UserPermsSchema.parse);
if (res.ok) {
return UserPermsSchema.parse(await res.json());
} else {
throw new Error("Could not fetch perms: " + res.statusText);
}
} }
public async setPrefix(userId: number, prefix: string): Promise<void> { public async setPrefix(userId: number, prefix: string): Promise<void> {

Datei anzeigen

@ -7,6 +7,7 @@ import {DataRepo} from "./data.ts";
import { AES, enc, format } from "crypto-js"; import { AES, enc, format } from "crypto-js";
import {SchematicRepo} from "./schem.ts"; import {SchematicRepo} from "./schem.ts";
import {StatsRepo} from "./stats.ts";
export { EventRepo } from "./event.js" export { EventRepo } from "./event.js"
@ -31,3 +32,4 @@ export const permsRepo = derived(tokenStore, ($token) => new PermsRepo($token))
export const pageRepo = derived(tokenStore, ($token) => new PageRepo($token)) export const pageRepo = derived(tokenStore, ($token) => new PageRepo($token))
export const dataRepo = derived(tokenStore, ($token) => new DataRepo($token)) export const dataRepo = derived(tokenStore, ($token) => new DataRepo($token))
export const schemRepo = derived(tokenStore, ($token) => new SchematicRepo($token)) export const schemRepo = derived(tokenStore, ($token) => new SchematicRepo($token))
export const statsRepo = derived(tokenStore, ($token) => new StatsRepo($token))

Datei anzeigen

@ -6,19 +6,19 @@ export class SchematicRepo {
constructor(private token: string) {} constructor(private token: string) {}
public async getRootSchematicList(): Promise<SchematicList> { public async getRootSchematicList(): Promise<SchematicList> {
return await fetchWithToken(this.token, "/schem").then(value => value.json()).then(value => SchematicListSchema.parse(value)); return await fetchWithToken(this.token, "/schem").then(value => value.json()).then(SchematicListSchema.parse);
} }
public async getSchematicList(id: number): Promise<SchematicList> { public async getSchematicList(id: number): Promise<SchematicList> {
return await fetchWithToken(this.token, `/schem/${id}/list`).then(value => value.json()).then(value => SchematicListSchema.parse(value)); return await fetchWithToken(this.token, `/schem/${id}/list`).then(value => value.json()).then(SchematicListSchema.parse);
} }
public async getSchematicInfo(id: number): Promise<SchematicInfo> { public async getSchematicInfo(id: number): Promise<SchematicInfo> {
return await fetchWithToken(this.token, `/schem/${id}`).then(value => value.json()).then(value => SchematicInfoSchema.parse(value)); return await fetchWithToken(this.token, `/schem/${id}`).then(value => value.json()).then(SchematicInfoSchema.parse);
} }
public async createDownload(id: number): Promise<SchematicCode> { public async createDownload(id: number): Promise<SchematicCode> {
return await fetchWithToken(this.token, `/schem/${id}/download`).then(value => value.json()).then(value => SchematicCodeSchema.parse(value)); return await fetchWithToken(this.token, `/schem/${id}/download`).then(value => value.json()).then(SchematicCodeSchema.parse);
} }
public async uploadSchematic(name: string, content: string) { public async uploadSchematic(name: string, content: string) {

20
src/components/repo/stats.ts Normale Datei
Datei anzeigen

@ -0,0 +1,20 @@
import type {FightStats, Ranking, UserStats} from "../types/stats.ts";
import {fetchWithToken} from "./repo.ts";
import {FightStatsSchema, RankingSchema, UserStatsSchema} from "../types/stats.ts";
export class StatsRepo {
constructor(private token: string) {}
public async getRankings(gamemode: string): Promise<Ranking> {
return await fetchWithToken(this.token, `/stats/ranked/${gamemode}`).then(value => value.json()).then(RankingSchema.parse);
}
public async getFightStats(): Promise<FightStats> {
return await fetchWithToken(this.token, `/stats/fights`).then(value => value.json()).then(FightStatsSchema.parse);
}
public async getUserStats(id: number): Promise<UserStats> {
return await fetchWithToken(this.token, `/stats/user/${id}`).then(value => value.json()).then(UserStatsSchema.parse);
}
}

Datei anzeigen

@ -0,0 +1,26 @@
import {z} from "zod";
export const RankingSchema = z.array(z.object({
name: z.string(),
elo: z.number(),
}))
export type Ranking = z.infer<typeof RankingSchema>;
export const FightStatsSchema = z.array(z.object({
date: z.string(),
gamemode: z.string(),
count: z.number(),
}))
export type FightStats = z.infer<typeof FightStatsSchema>;
export const UserStatsSchema = z.object({
eventFightParticipation: z.number(),
eventParticipation: z.number(),
acceptedSchematics: z.number(),
fights: z.number(),
playtime: z.number()
})
export type UserStats = z.infer<typeof UserStatsSchema>;

Datei anzeigen

@ -0,0 +1,13 @@
---
title: First Announcement
description: First announcement for the 2021 edition of the conference.
created: 2021-01-01
tags:
- blog
- event
---
# First Announcement
<fight-table data-event="7" data-group="Gruppe 1" data-rows="2" />
<fight-table data-event="7" data-group="Gruppe 1" data-rows="2" />

Datei anzeigen

@ -0,0 +1,26 @@
---
title: First Announcement
description: First announcement for the 2021 edition of the conference. With realy long text to test the layout. But this is not enough, so I will add some more
created: 2023-12-03
tags:
- event
- miniwargear
---
Liebe Community!
Das Jahr 2023 neigt sich dem Ende zu und 2024 steht bereits in den Startlöschern.
Auf diesem Wege Lade ich Euch zum traditionelle MicroWargearEvent auf Steamwar ein. Bereits zum dritten man in Folge findet das Event jedes Jahr zum 1.01. statt und wollen diese Tradition weiterführen. Ihr konntet Euch bereits seit einigen Wochen anmelden. Mit dem Befehl /team event MicroNeuJahr24 könnt Ihr Euer Team ingame anmelden. Alternativ besteht die Anmeldemöglichkeit ebenso auf unserem Community DC server unter dem Bereich Event.
- Wann? 1.01.2024 c.a 16 Uhr
- Wo? Steamwar.de Minecraft Server
- Version: 1.20
- Regelwerk: MicroWarGear Regelwerk – SteamWar
- max 3 Fighter pro Team
das Serverteam würde sich freuen, wenn es wieder viele Teams gibt welch an diesem Event teilnehmen und wünschen allen Viel Spaß, Erfolg und einen Guten Rutsch!
Viele Grüßen
Steamwar – Serverteam

Datei anzeigen

@ -0,0 +1,11 @@
---
title: Second Announcement
description: Second announcement for the 2022 edition of the conference.
created: 2022-01-01
tags:
- blog
---
# First Announcement
<fight-table data-event="7" data-group="Gruppe 1" />

Datei anzeigen

@ -20,18 +20,6 @@ export const help = defineCollection({
}) })
}) })
export const event = defineCollection({
type: "content",
schema: z.object({
name: z.string(),
start: z.date(),
end: z.date(),
mode: z.string().optional(),
leader: z.string().array().optional(),
eventId: z.number().gte(0)
})
})
export const modes = defineCollection({ export const modes = defineCollection({
type: "data", type: "data",
schema: z.object({ schema: z.object({
@ -59,11 +47,22 @@ export const rules = defineCollection({
}) })
}) })
export const announcements = defineCollection({
type: "content",
schema: ({image}) => z.object({
title: z.string(),
description: z.string(),
image: image().optional(),
tags: z.array(z.string()),
created: z.date()
})
})
export const collections = { export const collections = {
'pages': pages, 'pages': pages,
'help': help, 'help': help,
'event': event,
'modes': modes, 'modes': modes,
'rules': rules, 'rules': rules,
'downloads': downloads 'downloads': downloads,
'announcements': announcements
} }

Datei anzeigen

@ -1,4 +1,5 @@
{ {
"translationKey": "as", "translationKey": "as",
"main": true "main": true,
"ranked": true
} }

Datei anzeigen

@ -1,7 +1,7 @@
--- ---
title: FAQ title: FAQ
description: Frequently asked questions description: Frequently asked questions
slug: faq slug: haeufige-fragen
--- ---
# Server/ IP-Adresse # Server/ IP-Adresse

Datei anzeigen

@ -1,5 +1,29 @@
--- ---
title: About Us title: Über uns
description: About SteamWar description: Über SteamWar
slug: about slug: about
--- ---
# Sicherheit
Wir legen Wert auf Sicherheit.
Deshalb hat niemand außer dir und denen, denen du die Rechte dazu gebt, ingame Zugriff auf deine Schematics und deine Bauwelt. Nur AdmiralSeekrank, YoyoNow, Chaoscaot und Lixfel haben (außerhalb von Minecraft) aus administrativen Gründen Zugriff darauf.
<hr>
# Stabilität
Unsere Server sollen laggfrei und stabil laufen.
Deswegen läuft jede Arena und jede Bauwelt auf einem eigenen Server. Damit bleibt deine Bauwelt so stabil und laggfrei wie möglich, solange du sie nicht selbst zum Absturz bringst :).
<hr>
# Komfort
Unser Bauserver bietet einmalige Funktionen, welche dir beim Entwickeln deiner Technik hilfreich sein werden.
AirShips
Um eine Mitte zwischen den Spielmodi WarShip und WarGear zu finden, haben wir den Spielmodus AirShips entwickelt! Mit einem möglichst einfachen Regelwerk versuchen wir dabei, ein maximal spannendes und interessantes Spielerlebnis zu gestalten.

144
src/content/pages/en/faq.md Normale Datei
Datei anzeigen

@ -0,0 +1,144 @@
---
title: FAQ
description: Frequently asked questions
slug: faq
---
# Server/ IP-Adresse
#### Was ist die IP? Wie kann ich auf den Server joinen?
Steamwar.de
#### Mit welcher Version kann ich auf den Server joinen?
SteamWar läuft auf der 1.19.4, du kannst aber mit den Versionen 1.12.x -1.19.4 joinen.
#### Kann ich auch mit der Bedrock-Version joinen?
Ja, über die IP: steamwar.de
# Allgemeines
#### Wo finde ich die Verhaltensrichtlinien/ Serverregeln
Über diese Website unter „Startseite“ oder indem du hier drauf klickst
# Minigames/ Fights
#### Wie kann ich Kämpfe starten?
Mit „/fight“ kannst du Kämpfe mit dem aktuellen WarGear Regelwerk und der WG Submodi starten. Du kannst aber auch die Lobby-Portale nutzen, indem du entweder durch die großen Portale am Spawn gehst, oder im Teleporter auf „Arenen“ drückst, und dann dort über die Portale eine Runde startest.
#### Wie kann ich Kämpfe mit alten Regelwerken starten?
Mit „/historic“ kannst du alle historischen Spielmodi spielen, bis zur Version 1.7.
#### Wie kann ich gegen einen bestimmten Spieler kämpfen?
Mit „/challenge“ kannst du den gewählten Spieler herausfordern. So können auch z. B. Private MissileWars Runden gestartet werden, in der jeder Spieler eingeladen werden muss, um teilnehmen zu können. Der Challenge-Befehl funktioniert nicht mit historischen Spielmodi.
#### Was sind rotierende Modi?
Für manche Modi gibt es nicht genug Nachfrage, um eine dauerhafte Aufnahme dieser in die regulär spielbaren Modi zu rechtfertigen. Diese Modi kann man daher alle paar Monate spielen und werden allgemein als rotierende Modi bezeichnet. Es steht immer ein rotierender Modus zur Verfügung, und dieser wechselt ungefähr jeden Monat. Zu den rotierenden Modi gehören z. B. MicroWarGear und MegaWarGear.
#### Wie kann ich anderen Kämpfen beitreten?
Entweder klickst du auf die entsprechende Nachricht im Chat, trittst einer Arena mit „/arena [arena]“ bei, oder folgst einem Spieler mit „/join [spieler]“
#### Nach welchen Regeln muss ich meine Kampfmaschinen bauen?
Die Regelwerke findest du hier auf der Website unter dem Menüpunkt „Spielmodi“
# Bauserver
#### Was ist ein Bauserver?
Ein Bauserver ist ein privater Server, auf welchem Redstoneschaltungen, Kanonen und Schematics für unsere Spielmodi im Kreativmodus mit einer großen Liste an Hilfsmitteln gebaut werden können.
#### Wie kann ich meinen Bauserver starten?
Entweder mit „/bau“, „/b“ oder „/build“. Solltest du auf einen Bau einer anderen Spielversion wollen, benutze „/b [version]“.
#### Wie lasse ich andere auf meinen Bau?
Entweder über „/b addmember [spieler]“ oder klicke auf die Nachricht im Chat, wenn sie versuchen deinem Bau per command beizutreten. Du wirst sie erkennen, wenn sie erscheint.
#### Wie trete ich anderen Baus bei?
Entweder über „/b tp [spieler]“ oder „/join [spieler]“, wenn du weißt, dass er sich auf seinem Bau befindet. Solltest du auf den Bau einer anderen Spielversion wollen, gebe „/b tp [spieler] [version]“ ein.
#### Wie gebe ich anderen Spielern Rechte?
Für Worldrechte (Regionsverwaltung) „/b toggleworld [spieler]“
Für Worldedit „/b togglewe [spieler]“
#### Wie kann ich die verschiedenen Funktionen meines Baus nutzen?
Am Anfang ist es am einfachsten, das Bau-GUI zu nutzen. Das geht entweder über „/gui“ oder indem du deine Taste zum Hand wechseln zweimal schnell hintereinander drückst (standardmäßig „F“)
#### Wie schreibe ich im lokalen Chat bzw. im Bau Chat?
„/bc [Nachricht]“, „/local [Nachricht]“ oder „+ [Nachricht]“
# Schematics
#### Wo kann ich meine Schematics hochladen?
Entweder über https://steamwar.de/startseite/schematicupload/
Oder schicke unserem Discord Bot (Steamwar Bot#9952) die Schematic per Privatnachricht. Dafür musst du deinen Minecraft und Discord Account bei uns verknüpft haben.
#### Wie kann ich meine Schematics herunterladen?
Über die ingame Download Funktion im Schem-GUI „//schem gui“, und „//schem download [schematic]“,
#### Wie kann ich meine Schematic auf die Arena bringen/ freigeben lassen?
„//schem changetype [schemname] [Typ]“ oder wenn du dich mit den Typen nicht auskennst „//schem changetype [schemname]“. Danach muss die Schematic von einem Supporter oder Moderator geprüft werden.
#### Wie erfahre ich den Grund für die Ablehnung meiner Schematic?
Wenn du zum Zeitpunkt der Ablehnung offline warst „//schem search [schemname]“
Wenn du zum Zeitpunkt der Ablehnung online bist, steht eine Nachricht im Chat.
Über eine Privatnachricht vom SteamWar Discord Bot. Verbinde dafür deinen Minecraft und Discord Account (Informationen dazu unter dem Punkt „Discord“)
#### Wie erstelle ich eine Schematic/ speichere ich mein WarGear, WarShip usw.?
Gehe auf die Region mit deinem Bauwerk. Gebe nun „/select build“ ein. Stelle dich jetzt mittig vor den Baubereich (normalerweise der gelbe Glasblock), und gebe „//copy“ ein. Jetzt kannst du dein Bauwerk mit „//schem save [schemname]“ speichern.
Gib „//wand“ ein, nimm das Item in die Hand und wähle mit Linksklick bspw. die untere linke Ecke des Bereichs aus, in welchem sich dein Bauwerk befindet, und wähle dann mit Rechtsklick die schräg gegenüberliegende Ecke aus. Alternativ kannst du die Ecken auch mit „//pos1“ und „//pos2“ auswählen. Dafür müssen dann die Füße deines Charakters in der jeweiligen Ecke bei Senden des Commands befinden. Jetzt sollte sich, wenn du das Item in der Hand hältst, dein Bauwerk innerhalb eines Rahmens aus Partikeln befinden. Sollte dies nicht der Fall sein, passe die Ecken an, bis sich dein Bauwerk in dem Rahmen befindet. Gehe nun an die Stelle, wo du dein Bauwerk kopieren möchtest, was üblicherweise mittig auf dem Boden vor diesem ist (der gelbe Glasblock im Boden). Gebe jetzt „//copy“ ein. Um das Ganze letztendlich zu speichern, gebe „//schem save [schemname]“ ein.
#### Wie sende ich ein Public ein?
Informiere ein entsprechendes Ratsmitglied (einsehbar auf unserem Discord Server) und füge alle Mitglieder des entsprechenden Rates auf die Schem hinzu. Danach wird der Antrag innerhalb des Rates diskutiert. Sollte der Rat dem Antrag zustimmen, muss ein Admin entscheiden, ob es Public wird. Wenn ja, wirst du aufgefordert alle wieder von der Schematic zu entfernen und es wird zur Public Schem geändert.
# Discord
#### Wo finde ich den Discord Server?
[steamwar.de/discord](https://steamwar.de/discord)
#### Wo finde ich den Social Media Discord Server?
[Discord](https://discord.gg/PvXFsRvZfB)
#### Wie verbinde ich meinen Discord und Minecraft Account?
Klicke in dem Discord Channel „#「👮」regel-infos“ auf den Button „Minecraft verknüpfen“. Folge dann den von dem SteamWar Bot angewiesenen Schritten.
# Support
#### Wie öffne ich ein Support Ticket?
* Auf unserem Discord-Server über den Channel „#「❓」support“. Dort kannst du Fragen stellen, Bugs und Spieler melden, sowie Features vorschlagen und Entbannungsanträge stellen.
#### Wie melde ich schnell einen Bug?
„/bug [beschreibung des bugs]“
# Events
#### Wie kann ich mich für ein Event anmelden?
„/team event [eventname]“ oder, wenn du deinen Minecraft Account mit deinem Discord Accout verknüpft hast, über den Discord Channel „#「📣」events“ auf unserem Discord-Server.
#### Kann ich mit zwei Accounts gleichzeitig teilnehmen?
Nein
#### Kann ich während eines Events noch Spieler in mein Team einladen?
Nein. Während eines laufenden Events sind Neuaufnahmen in ein Team nicht möglich.
# Forum
#### Wo finde ich das Forum?
Auf unserem Discord Server unter der Kategorie „Forum“
# Website
#### Wie melde ich mich auf der Website an?
Tritt unserem Minecraft Server bei, und gebe „/webpw [passwort]“ ein
# Teams
#### Wie trete ich einem Team bei?
Mit „/team join [Team]“. Mit „/team“ siehst du alle weiteren Commands.
# Sonstiges
#### Wo erhalte ich weitere Hilfe?
Mit „/tutorial“ kannst du dir Tutorial-Welten von Spielern ansehen.

29
src/content/pages/en/join.md Normale Datei
Datei anzeigen

@ -0,0 +1,29 @@
---
title: Join Now!
description: How to join SteamWar.de
slug: join-now
---
# Minecraft Java Edition
1. Klicke in Minecraft auf »Mehrspieler«
2. Klicke auf »Server hinzufügen«
3. Gib unter »Serveradresse« »**steamwar.de**« ein
4. Drücke auf »Fertig«
5. Doppelklicke auf den Server. Viel Spaß!
Hinweis: Wir empfehlen derzeit die Minecraft-Version **1.20.1** (du kannst aber auch gerne eine andere Version verwenden)
# Minecraft Bedrock Edition
Serveradresse: `steamwar.de`
Port: `19132`
Möchtest du mit der Bedrock Edition den gleichen Nutzernamen, Bauwelt und Schematics deines Java-Accounts nutzen, musst du die hier beschriebenen Anweisungen durchführen: https://wiki.geysermc.org/floodgate/linking/
Möchtest du mit einer Konsolenfassung von Minecraft (wo keine freie Serverwahl möglich ist) auf SteamWar spielen musst du die folgenden Zusatzschritte durchführen: https://wiki.geysermc.org/geyser/using-geyser-with-consoles/
# Discord
Der Link zu unserem Discord Server ist https://discord.gg/J6fENfz6Zf oder Steamwar.de/discord

4
src/env.d.ts vendored
Datei anzeigen

@ -10,9 +10,9 @@
type PrimaryLocale = "en" type PrimaryLocale = "en"
type SecondaryLocale = "de" type SecondaryLocale = "de"
type Locale = PrimaryLocale | SecondaryLocale type Locale = PrimaryLocale | SecondaryLocale
type RouteParameters = {"/dashboard":undefined;"/downloads":undefined;"/":undefined;"/login":undefined;"/[...slug]":{"slug":unknown;};"/admin":undefined;"/help":undefined;"/help/[...slug]":{"slug":unknown;};"/ranked/[...gamemode]":{"gamemode":unknown;};"/rules":undefined;"/rules/[...slug]":{"slug":unknown;};} type RouteParameters = {"/dashboard":undefined;"/downloads":undefined;"/":undefined;"/login":undefined;"/[...slug]":{"slug":unknown;};"/admin":undefined;"/announcements":undefined;"/announcements/[...slug]":{"slug":unknown;};"/announcements/tags/[tag]":{"tag":unknown;};"/help":undefined;"/help/[...slug]":{"slug":unknown;};"/ranked/[...gamemode]":{"gamemode":unknown;};"/rules":undefined;"/rules/[...slug]":{"slug":unknown;};"/stats/fight":undefined;}
type Route = keyof RouteParameters type Route = keyof RouteParameters
type TranslationVariables = {"dashboard.page":object|undefined;"dashboard.title":{"name"?:unknown;}|undefined;"dashboard.rank":{"rank"?:unknown;}|undefined;"dashboard.permissions":object|undefined;"dashboard.buttons.logout":object|undefined;"dashboard.buttons.admin":object|undefined;"dashboard.schematic.upload":object|undefined;"dashboard.schematic.home":object|undefined;"dashboard.schematic.dir":object|undefined;"dashboard.schematic.head.type":object|undefined;"dashboard.schematic.head.name":object|undefined;"dashboard.schematic.head.owner":object|undefined;"dashboard.schematic.head.updated":object|undefined;"dashboard.schematic.head.replaceColor":object|undefined;"dashboard.schematic.head.allowReplay":object|undefined;"dashboard.schematic.info.path":{"path"?:unknown;}|undefined;"dashboard.schematic.info.replaceColor":object|undefined;"dashboard.schematic.info.allowReplay":object|undefined;"dashboard.schematic.info.type":{"type"?:unknown;}|undefined;"dashboard.schematic.info.updated":{"updated"?:unknown;}|undefined;"dashboard.schematic.info.item":{"item"?:unknown;}|undefined;"dashboard.schematic.info.members":{"members"?:unknown;}|undefined;"dashboard.schematic.info.btn.download":object|undefined;"dashboard.schematic.info.btn.close":object|undefined;"login.page":object|undefined;"login.title":object|undefined;"login.placeholder.username":object|undefined;"login.placeholder.token":object|undefined;"login.label.username":object|undefined;"login.label.token":object|undefined;"login.generateToken":object|undefined;"login.submit":object|undefined;"login.error":object|undefined;"page":object|undefined;"wg.title":object|undefined;"wg.description":object|undefined;"as.title":object|undefined;"as.description":object|undefined;"ws.title":object|undefined;"ws.description":object|undefined;"mwg.title":object|undefined;"mwg.description":object|undefined;"rules":object|undefined;"council":object|undefined;"ranking":object|undefined;"navbar.title":object|undefined;"navbar.logo.alt":object|undefined;"navbar.links.home.title":object|undefined;"navbar.links.home.announcements":object|undefined;"navbar.links.home.about":object|undefined;"navbar.links.home.downloads":object|undefined;"navbar.links.home.faq":object|undefined;"navbar.links.rules.title":object|undefined;"navbar.links.rules.gamemode":object|undefined;"navbar.links.rules.wg":object|undefined;"navbar.links.rules.mwg":object|undefined;"navbar.links.rules.ws":object|undefined;"navbar.links.rules.as":object|undefined;"navbar.links.rules.rotating":object|undefined;"navbar.links.rules.megawg":object|undefined;"navbar.links.rules.micro":object|undefined;"navbar.links.rules.sf":object|undefined;"navbar.links.rules.general":object|undefined;"navbar.links.rules.coc":object|undefined;"navbar.links.help.title":object|undefined;"navbar.links.help.center":object|undefined;"navbar.links.help.docs":object|undefined;"navbar.links.account":object|undefined;"status.loading":object|undefined;"status.status":object|undefined;"status.online":object|undefined;"status.offline":object|undefined;"status.players":{"count"?:unknown;}|undefined;"status.version":{"version"?:unknown;}|undefined;"home.page":object|undefined;"home.title.first":object|undefined;"home.title.second":object|undefined;"home.subtitle.1":object|undefined;"home.subtitle.2":object|undefined;"home.subtitle.3":object|undefined;"home.join":object|undefined;"home.benefits.historic.title":object|undefined;"home.benefits.historic.description.1":object|undefined;"home.benefits.historic.description.2":object|undefined;"home.benefits.server.title":object|undefined;"home.benefits.server.description":object|undefined;"home.benefits.events.title":object|undefined;"home.benefits.events.description.1":object|undefined;"home.benefits.events.description.2":object|undefined;"home.prefix.Admin":object|undefined;"home.prefix.Dev":object|undefined;"home.prefix.Mod":object|undefined;"home.prefix.Sup":object|undefined;"home.prefix.Arch":object|undefined;"footer.imprint":object|undefined;"footer.privacy":object|undefined;"footer.coc":object|undefined;"footer.stats":object|undefined;"footer.gamemodes":object|undefined;"footer.announcements":object|undefined;"footer.join":object|undefined;} type TranslationVariables = {"dashboard.page":object|undefined;"dashboard.title":{"name"?:unknown;}|undefined;"dashboard.rank":{"rank"?:unknown;}|undefined;"dashboard.permissions":object|undefined;"dashboard.buttons.logout":object|undefined;"dashboard.buttons.admin":object|undefined;"dashboard.schematic.upload":object|undefined;"dashboard.schematic.home":object|undefined;"dashboard.schematic.dir":object|undefined;"dashboard.schematic.head.type":object|undefined;"dashboard.schematic.head.name":object|undefined;"dashboard.schematic.head.owner":object|undefined;"dashboard.schematic.head.updated":object|undefined;"dashboard.schematic.head.replaceColor":object|undefined;"dashboard.schematic.head.allowReplay":object|undefined;"dashboard.schematic.info.path":{"path"?:unknown;}|undefined;"dashboard.schematic.info.replaceColor":object|undefined;"dashboard.schematic.info.allowReplay":object|undefined;"dashboard.schematic.info.type":{"type"?:unknown;}|undefined;"dashboard.schematic.info.updated":{"updated"?:unknown;}|undefined;"dashboard.schematic.info.item":{"item"?:unknown;}|undefined;"dashboard.schematic.info.members":{"members"?:unknown;}|undefined;"dashboard.schematic.info.btn.download":object|undefined;"dashboard.schematic.info.btn.close":object|undefined;"login.page":object|undefined;"login.title":object|undefined;"login.placeholder.username":object|undefined;"login.placeholder.token":object|undefined;"login.label.username":object|undefined;"login.label.token":object|undefined;"login.generateToken":object|undefined;"login.submit":object|undefined;"login.error":object|undefined;"blog.title":object|undefined;"page":object|undefined;"wg.description":object|undefined;"as.description":object|undefined;"ws.description":object|undefined;"mwg.description":object|undefined;"rules":object|undefined;"council":object|undefined;"ranking":object|undefined;"title":{"mode"?:unknown;}|undefined;"stats.title":object|undefined;"navbar.title":object|undefined;"navbar.logo.alt":object|undefined;"navbar.links.home.title":object|undefined;"navbar.links.home.announcements":object|undefined;"navbar.links.home.about":object|undefined;"navbar.links.home.downloads":object|undefined;"navbar.links.home.faq":object|undefined;"navbar.links.rules.title":object|undefined;"navbar.links.rules.gamemode":object|undefined;"navbar.links.rules.wg":object|undefined;"navbar.links.rules.mwg":object|undefined;"navbar.links.rules.ws":object|undefined;"navbar.links.rules.as":object|undefined;"navbar.links.rules.rotating":object|undefined;"navbar.links.rules.megawg":object|undefined;"navbar.links.rules.micro":object|undefined;"navbar.links.rules.sf":object|undefined;"navbar.links.rules.general":object|undefined;"navbar.links.rules.coc":object|undefined;"navbar.links.help.title":object|undefined;"navbar.links.help.center":object|undefined;"navbar.links.help.docs":object|undefined;"navbar.links.account":object|undefined;"status.loading":object|undefined;"status.status":object|undefined;"status.online":object|undefined;"status.offline":object|undefined;"status.players":{"count"?:unknown;}|undefined;"status.version":{"version"?:unknown;}|undefined;"home.page":object|undefined;"home.title.first":object|undefined;"home.title.second":object|undefined;"home.subtitle.1":object|undefined;"home.subtitle.2":object|undefined;"home.subtitle.3":object|undefined;"home.join":object|undefined;"home.benefits.historic.title":object|undefined;"home.benefits.historic.description.1":object|undefined;"home.benefits.historic.description.2":object|undefined;"home.benefits.server.title":object|undefined;"home.benefits.server.description":object|undefined;"home.benefits.events.title":object|undefined;"home.benefits.events.description.1":object|undefined;"home.benefits.events.description.2":object|undefined;"home.prefix.Admin":object|undefined;"home.prefix.Dev":object|undefined;"home.prefix.Mod":object|undefined;"home.prefix.Sup":object|undefined;"home.prefix.Arch":object|undefined;"footer.imprint":object|undefined;"footer.privacy":object|undefined;"footer.coc":object|undefined;"footer.stats":object|undefined;"footer.gamemodes":object|undefined;"footer.announcements":object|undefined;"footer.join":object|undefined;"wg.title":object|undefined;"mwg.title":object|undefined;"ws.title":object|undefined;"as.title":object|undefined;"ranking.heading":{"mode"?:unknown;}|undefined;}
type Translation = keyof TranslationVariables type Translation = keyof TranslationVariables
type Environment = "none"|"node"|"browser" type Environment = "none"|"node"|"browser"
declare module "astro-i18n" { declare module "astro-i18n" {

Datei anzeigen

@ -90,5 +90,20 @@
"gamemodes": "Gamemodes", "gamemodes": "Gamemodes",
"announcements": "Announcements", "announcements": "Announcements",
"join": "Join Now" "join": "Join Now"
},
"wg": {
"title": "WarGear"
},
"mwg": {
"title": "MiniWarGear"
},
"ws": {
"title": "WarShip"
},
"as": {
"title": "AirShip"
},
"ranking": {
"heading": "{# mode #} Rankings"
} }
} }

Datei anzeigen

@ -0,0 +1,5 @@
{
"blog": {
"title": "Ankündigungen - SteamWar"
}
}

Datei anzeigen

@ -0,0 +1,5 @@
{
"blog": {
"title": "Announcements - SteamWar"
}
}

Datei anzeigen

@ -0,0 +1,5 @@
{
"tag": {
"title": "Tag: {# tag #} - SteamWar"
}
}

Datei anzeigen

@ -0,0 +1,5 @@
{
"ranked": {
"title": "{# mode #} - Rangliste"
}
}

Datei anzeigen

@ -0,0 +1,5 @@
{
"ranked": {
"title": "{# mode #} - Ranking"
}
}

Datei anzeigen

@ -1,22 +1,19 @@
{ {
"page": "SteamWar - Rules", "page": "SteamWar - Rules",
"wg": { "wg": {
"title": "WarGears",
"description": "Today, the battlefields of Earth are dominated by heavy artillery. With our traditional rules, WarGears are also arena-wrecking heavyweights. Due to the cannon technology with the most projectiles, you can expect hard and short-lived battles in WarGears." "description": "Today, the battlefields of Earth are dominated by heavy artillery. With our traditional rules, WarGears are also arena-wrecking heavyweights. Due to the cannon technology with the most projectiles, you can expect hard and short-lived battles in WarGears."
}, },
"as": { "as": {
"title": "AirShips",
"description": "The dream of flying has inspired humanity for millennia. The AirShips game mode offers you the almost unlimited possibilities of the sky. Whether you compete with 15 2-projectile cannons or 2 15-projectile cannons, you always have a realistic chance of winning. Because: Everything has its price." "description": "The dream of flying has inspired humanity for millennia. The AirShips game mode offers you the almost unlimited possibilities of the sky. Whether you compete with 15 2-projectile cannons or 2 15-projectile cannons, you always have a realistic chance of winning. Because: Everything has its price."
}, },
"ws": { "ws": {
"title": "WarShips",
"description": "For a long time, warships were the ultimate weapon of war. This is still true for Warships today in terms of rocket and slime technology. Due to the limited cannon power, WarShips offer long, intense and varied battles, with new techniques always being introduced in the arena. After a while, a WarShip battle shifts to the water through boarding, providing exciting PvP action." "description": "For a long time, warships were the ultimate weapon of war. This is still true for Warships today in terms of rocket and slime technology. Due to the limited cannon power, WarShips offer long, intense and varied battles, with new techniques always being introduced in the arena. After a while, a WarShip battle shifts to the water through boarding, providing exciting PvP action."
}, },
"mwg": { "mwg": {
"title": "MiniWarGears",
"description": "In today's urban warfare, there is no place for heavy equipment, which is why smaller machines still have their place today. With their slightly smaller cannons, MiniWarGears are the perfect choice for beginners, casual players, and those who like to experiment." "description": "In today's urban warfare, there is no place for heavy equipment, which is why smaller machines still have their place today. With their slightly smaller cannons, MiniWarGears are the perfect choice for beginners, casual players, and those who like to experiment."
}, },
"rules": "Rules »", "rules": "Rules »",
"council": "Council »", "council": "Council »",
"ranking": "Ranking »" "ranking": "Ranking »",
"title": "{# mode #} - Rules"
} }

Datei anzeigen

@ -0,0 +1,5 @@
{
"stats": {
"title": "Kampf Statistiken"
}
}

Datei anzeigen

@ -0,0 +1,5 @@
{
"stats": {
"title": "Fight Statistics"
}
}

Datei anzeigen

@ -34,7 +34,7 @@ const { title } = Astro.props;
<CaretDownOutline class="ml-2 mt-auto" /> <CaretDownOutline class="ml-2 mt-auto" />
</div> </div>
<div> <div>
<a class="btn btn-gray my-1" href={l("/")}>{t("navbar.links.home.announcements")}</a> <a class="btn btn-gray my-1" href={l("/announcements")}>{t("navbar.links.home.announcements")}</a>
<a class="btn btn-gray" href={l("/about")}>{t("navbar.links.home.about")}</a> <a class="btn btn-gray" href={l("/about")}>{t("navbar.links.home.about")}</a>
<a class="btn btn-gray" href={l("/downloads")}>{t("navbar.links.home.downloads")}</a> <a class="btn btn-gray" href={l("/downloads")}>{t("navbar.links.home.downloads")}</a>
<a class="btn btn-gray" href={l("/faq")}>{t("navbar.links.home.faq")}</a> <a class="btn btn-gray" href={l("/faq")}>{t("navbar.links.home.faq")}</a>
@ -49,10 +49,10 @@ const { title } = Astro.props;
</div> </div>
<div> <div>
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.gamemode")}</h2> <h2 class="px-2 text-gray-300">{t("navbar.links.rules.gamemode")}</h2>
<a class="btn btn-gray">{t("navbar.links.rules.wg")}</a> <a href={l("/rules/wargear")} class="btn btn-gray">{t("navbar.links.rules.wg")}</a>
<a class="btn btn-gray">{t("navbar.links.rules.mwg")}</a> <a href={l("/rules/miniwargear")} class="btn btn-gray">{t("navbar.links.rules.mwg")}</a>
<a class="btn btn-gray">{t("navbar.links.rules.ws")}</a> <a href={l("/rules/warship")} class="btn btn-gray">{t("navbar.links.rules.ws")}</a>
<a class="btn btn-gray">{t("navbar.links.rules.as")}</a> <a href={l("/rules/airship")} class="btn btn-gray">{t("navbar.links.rules.as")}</a>
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.rotating")}</h2> <h2 class="px-2 text-gray-300">{t("navbar.links.rules.rotating")}</h2>
<a class="btn btn-gray">{t("navbar.links.rules.megawg")}</a> <a class="btn btn-gray">{t("navbar.links.rules.megawg")}</a>
<a class="btn btn-gray">{t("navbar.links.rules.micro")}</a> <a class="btn btn-gray">{t("navbar.links.rules.micro")}</a>
@ -116,7 +116,7 @@ const { title } = Astro.props;
<a href={l("/join")}>{t("footer.join")}</a> <a href={l("/join")}>{t("footer.join")}</a>
<a href={l("/")}>{t("footer.announcements")}</a> <a href={l("/")}>{t("footer.announcements")}</a>
<a href={l("/rules")}>{t("footer.gamemodes")}</a> <a href={l("/rules")}>{t("footer.gamemodes")}</a>
<a href={l("/")}>{t("footer.stats")}</a> <a href={l("/stats/fight")}>{t("footer.stats")}</a>
<a href={l("/code-of-conduct")}>{t("footer.coc")}</a> <a href={l("/code-of-conduct")}>{t("footer.coc")}</a>
<a href={l("/privacy")}>{t("footer.privacy")}</a> <a href={l("/privacy")}>{t("footer.privacy")}</a>
<a href={l("/imprint")}>{t("footer.imprint")}</a> <a href={l("/imprint")}>{t("footer.imprint")}</a>

Datei anzeigen

@ -25,40 +25,8 @@ const { Content } = await page.render();
<style is:global> <style is:global>
article { article {
p { >* {
@apply my-4 leading-7; all: revert;
}
h1 {
@apply text-4xl font-bold mt-4 text-center;
}
h2 {
@apply text-3xl font-bold mt-4;
}
h3 {
@apply text-2xl font-bold mt-4;
}
h4 {
@apply text-xl font-bold mt-4;
}
a {
@apply text-blue-500 hover:text-blue-700;
}
ol>li, ul>li {
@apply ml-4;
}
ol {
@apply list-decimal;
}
ul {
@apply list-disc;
} }
code { code {

Datei anzeigen

@ -0,0 +1,101 @@
---
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 TagComponent from "../../components/TagComponent.astro";
import {l} from "../../util/util";
import {capitalize} from "../../components/admin/util";
export const getStaticPaths = createGetStaticPaths(async () => {
const posts = await getCollection('announcements', entry => entry.id.split('/')[0] === astroI18n.locale)
return posts.map(value => ({
params: {
slug: value.slug.split("/").slice(1).join("/")
},
props: {
post: value
}
}))
})
interface Props {
post: CollectionEntry<'announcements'>
}
const {post} = Astro.props;
const { Content } = await post.render();
---
<PageLayout title={post.data.title}>
<article>
<h1 class="text-4xl mb-0">{post.data.title}</h1>
<h3 class="flex items-center mt-0 text-neutral-300"><TagSolid class="w-4 h-4 mr-2" /> {post.data.tags.map(tag => (
<a href={l(`/announcements/tags/${tag}`)}>
<span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 shadow-2xl">{capitalize(tag)}</span>
</a>
))} <CalendarMonthSolid class="w-4 h-4 mr-2" /> {Intl.DateTimeFormat(astroI18n.locale, {
day: 'numeric',
month: 'short',
year: 'numeric'
}).format(post.data.created)} </h3>
<Content />
<script>
import type {ExtendedEvent} from "../../components/types/event";
import FightTable from "../../components/FightTable.svelte";
// @ts-ignore
import {get} from "svelte/store";
import {eventRepo} from "../../components/repo/repo";
const eventMounts: Map<string, ((ev: ExtendedEvent) => void)[]> = new Map();
class FightTableElement extends HTMLElement {
connectedCallback(): void {
if (!eventMounts.has(this.dataset['event'])) {
eventMounts.set(this.dataset['event'], [])
}
const rows = Number.parseInt(this.dataset['rows']);
eventMounts.get(this.dataset['event']).push(ev => {
new FightTable({
target: this,
props: {
event: ev,
group: this.dataset['group'],
rows: !isNaN(rows) ? rows : 1,
}
})
})
}
}
customElements.define('fight-table', FightTableElement);
function mountEvent() {
for (const key of eventMounts.keys()) {
get(eventRepo).getEvent(key).then(ev => {
for (const mount of eventMounts.get(key)) {
mount(ev)
}
})
}
}
mountEvent()
</script>
</article>
</PageLayout>
<style is:global>
article {
>* {
all: revert;
}
code {
@apply dark:text-neutral-400 text-neutral-800;
}
pre.astro-code {
@apply w-fit p-4 rounded-md border-2 border-gray-600 my-4;
}
}
</style>

Datei anzeigen

@ -0,0 +1,20 @@
---
import { getCollection } from "astro:content"
import PageLayout from "../../layouts/PageLayout.astro";
import {astroI18n, t} from "astro-i18n";
import moment from "moment";
import PostComponent from "../../components/PostComponent.astro";
const posts = (await getCollection("announcements", (entry) => entry.id.split("/")[0] === astroI18n.locale))
.sort((a, b) => moment(b.data.created).unix() - moment(a.data.created).unix());
---
<PageLayout title={t("blog.title")}>
{posts.map((post, index) => (
<div>
<PostComponent post={post} />
{index !== posts.length - 1 && <hr/>}
</div>
))}
</PageLayout>

Datei anzeigen

@ -0,0 +1,51 @@
---
import moment from "moment/moment";
import {CollectionEntry} from "astro:content";
import {astroI18n, createGetStaticPaths, t} from "astro-i18n";
import {getCollection} from "astro:content";
import PageLayout from "../../../layouts/PageLayout.astro";
import {l} from "../../../util/util";
import {capitalize} from "../../../components/admin/util";
import PostComponent from "../../../components/PostComponent.astro";
export const getStaticPaths = createGetStaticPaths(async () => {
const posts = (await getCollection('announcements', entry => entry.id.split("/")[0] === astroI18n.locale))
.sort((a, b) => moment(b.data.created).unix() - moment(a.data.created).unix());
let groupedByTags: Record<string, CollectionEntry<'announcements'>[]> = {}
posts.forEach(post => {
post.data.tags.forEach(tag => {
if (!groupedByTags[tag]) {
groupedByTags[tag] = []
}
groupedByTags[tag].push(post)
})
})
return Object.keys(groupedByTags).map(tag => ({
params: {
tag: tag
},
props: {
posts: groupedByTags[tag],
tag: tag
}
}))
})
interface Props {
posts: CollectionEntry<'announcements'>[]
tag: string
}
const { posts, tag } = Astro.props;
---
<PageLayout title={t("tag.title", {tag: capitalize(tag)})}>
{posts.map((post, index) => (
<div>
<PostComponent post={post} />
{index !== posts.length - 1 && <hr/>}
</div>
))}
</PageLayout>

Datei anzeigen

@ -1,3 +1,31 @@
--- ---
import {createGetStaticPaths, t} from "astro-i18n";
import {getCollection, CollectionEntry} from "astro:content";
import PageLayout from "../../layouts/PageLayout.astro";
import EloTable from "../../components/EloTable.svelte";
export const getStaticPaths = createGetStaticPaths(async () => {
const modes = await getCollection("modes", entry => entry.data.ranked);
return modes.map(value => ({
props: {
mode: value
},
params: {
gamemode: value.id
}
}))
});
interface Props {
mode: CollectionEntry<"modes">
}
const { mode } = Astro.props;
---
---
<PageLayout title={t("ranked.title", {mode: t(`${mode.data.translationKey}.title`)})}>
<h1 class="text-2xl mb-2">{t("ranking.heading", { mode: t(`${mode.data.translationKey}.title`) })}</h1>
<EloTable gamemode={mode.id} client:only="svelte" />
</PageLayout>

Datei anzeigen

@ -1,22 +1,26 @@
--- ---
import {getCollection} from 'astro:content' import {getCollection, CollectionEntry} from 'astro:content'
import {astroI18n, createGetStaticPaths} from "astro-i18n"; import {astroI18n, createGetStaticPaths, t} from "astro-i18n";
import PageLayout from "../layouts/PageLayout.astro"; import PageLayout from "../../layouts/PageLayout.astro";
export const getStaticPaths = createGetStaticPaths(async () => { export const getStaticPaths = createGetStaticPaths(async () => {
let posts = await getCollection("rules"); let posts = await getCollection("rules", value => value.id.split("/")[0] === astroI18n.locale);
return posts.filter(value => value.id.split("/")[0] === astroI18n.locale).map((page) => ({ return posts.map((page) => ({
props: { page }, params: { slug: page.slug } props: { page }, params: { slug: page.slug.split("/")[1] }
}) ) }))
}) })
interface Props {
page: CollectionEntry<"rules">
}
const { page } = Astro.props; const { page } = Astro.props;
const { Content } = await page.render(); const { Content } = await page.render();
--- ---
<PageLayout title={page.data.title}> <PageLayout title={t("title", {mode: t(`${page.data.translationKey}.title`)})}>
<article> <article>
<Content /> <Content />
</article> </article>
@ -24,40 +28,8 @@ const { Content } = await page.render();
<style is:global> <style is:global>
article { article {
p { >* {
@apply my-4 leading-7; all: revert;
}
h1 {
@apply text-4xl font-bold mt-4 text-center;
}
h2 {
@apply text-3xl font-bold mt-4;
}
h3 {
@apply text-2xl font-bold mt-4;
}
h4 {
@apply text-xl font-bold mt-4;
}
a {
@apply text-blue-500 hover:text-blue-700;
}
ol>li, ul>li {
@apply ml-4;
}
ol {
@apply list-decimal;
}
ul {
@apply list-disc;
} }
code { code {

Datei anzeigen

@ -8,6 +8,7 @@ import {t} from "astro-i18n";
import {getCollection} from "astro:content"; import {getCollection} from "astro:content";
import PageLayout from "../../layouts/PageLayout.astro"; import PageLayout from "../../layouts/PageLayout.astro";
import {Image} from "astro:assets"; import {Image} from "astro:assets";
import {l} from "../../util/util";
const imageMap = { const imageMap = {
"wg": wg, "wg": wg,
@ -28,9 +29,9 @@ const modes = await getCollection("modes", entry => entry.data.main)
<h1 class="text-2xl font-bold">{t(value.data.translationKey + ".title")}</h1> <h1 class="text-2xl font-bold">{t(value.data.translationKey + ".title")}</h1>
<div>{t(value.data.translationKey + ".description")}</div> <div>{t(value.data.translationKey + ".description")}</div>
<div class="mt-2 flex flex-col"> <div class="mt-2 flex flex-col">
<a href="/public" class="text-yellow-300 hover:underline w-fit">{t("rules")}</a> <a href={l(`/rules/${value.id}`)} class="text-yellow-300 hover:underline w-fit">{t("rules")}</a>
<a href="/public" class="text-yellow-300 hover:underline w-fit">{t("council")}</a> <a href="/public" class="text-yellow-300 hover:underline w-fit">{t("council")}</a>
{value.data.ranked ? <a href="/public" class="text-yellow-300 hover:underline w-fit">{t("ranking")}</a> : null} {value.data.ranked ? <a href={l(`/ranked/${value.id}`)} class="text-yellow-300 hover:underline w-fit">{t(`ranking`)}</a> : null}
</div> </div>
</div> </div>
</div>))} </div>))}

10
src/pages/stats/fight.astro Normale Datei
Datei anzeigen

@ -0,0 +1,10 @@
---
import PageLayout from "../../layouts/PageLayout.astro";
import FightStatistics from "../../components/FightStatistics.svelte";
import {t} from "astro-i18n";
---
<PageLayout title={t("stats.title")}>
<FightStatistics client:only="svelte" />
</PageLayout>