Compare commits
72 Commits
Eventplan-
...
da6f741806
| Author | SHA1 | Date | |
|---|---|---|---|
| da6f741806 | |||
| 6b54791331 | |||
| 36685bffd1 | |||
| caf9ea6cf1 | |||
| d505265910 | |||
|
|
78e1a7b726 | ||
|
|
cd485e8dda | ||
|
|
182c402c7e | ||
|
|
098f5b9270 | ||
|
|
cf0c66c910 | ||
| c8156ea47e | |||
| 20a47ca6b6 | |||
| 2d601b9c4d | |||
| 48586f1a50 | |||
| 7153cacbab | |||
| 73cee211f2 | |||
| f8a16acfeb | |||
| 9ca63cd286 | |||
| a2456c8b46 | |||
| 0952035091 | |||
|
|
9c8c02f679 | ||
|
|
3b5fdc57c0 | ||
|
|
733c63946f | ||
| fd846250ab | |||
|
|
17460772e9 | ||
|
|
9a20860072 | ||
| 8f51723a3b | |||
| 8ad2f283aa | |||
| 39f1af8b73 | |||
| 266c4cb4ea | |||
| f3df3c0000 | |||
|
|
cb78fc598b | ||
| ba7ecc1a8e | |||
| 6ea92f9383 | |||
|
|
998770bf59 | ||
| a231032555 | |||
| 3aa3731bcb | |||
| 5e80c95bfd | |||
|
|
09dc28b6da | ||
|
|
fd7cf716ca | ||
| 73bd6a5e96 | |||
| 9c02cc1f4d | |||
| de8457fe45 | |||
| bccd5eb5a0 | |||
| 53afe70b27 | |||
| 4bbdaa06a9 | |||
| f03867b9a7 | |||
| 23e10eef0f | |||
| 4c72f4f26b | |||
| 624ba7f296 | |||
|
|
d7d20e4347 | ||
|
|
43bd8f4a7c | ||
|
|
18e8627b54 | ||
|
|
0efc46c7e2 | ||
| 62fff0c0b2 | |||
|
|
86b479fb28 | ||
|
|
489402292d | ||
| b53ce04a75 | |||
| 069a9973a4 | |||
| c3410de1d7 | |||
| a23c514102 | |||
| bf8110af6c | |||
| 349f71af1c | |||
| dda37127ca | |||
| 6d210eb0ff | |||
|
|
cfede8f299 | ||
|
|
597153ed39 | ||
|
|
697e903a26 | ||
| 1433784369 | |||
| 2c63a33bda | |||
| 87265e5ccc | |||
| 75f1a6528b |
3
.gitattributes
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
.png filter=lfs diff=lfs merge=lfs -text
|
||||
.glb filter=lfs diff=lfs merge=lfs -text
|
||||
@@ -7,7 +7,6 @@ import sitemap from "@astrojs/sitemap";
|
||||
import robotsTxt from "astro-robots-txt";
|
||||
import path from "node:path";
|
||||
import mdx from "@astrojs/mdx";
|
||||
import pagefind from "astro-pagefind";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
@@ -22,7 +21,6 @@ export default defineConfig({
|
||||
tailwind({
|
||||
configFile: "./tailwind.config.cjs",
|
||||
}),
|
||||
pagefind(),
|
||||
configureI18n(),
|
||||
sitemap({
|
||||
i18n: {
|
||||
|
||||
BIN
public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
3
public/favicon.svg
Normal file
|
After Width: | Height: | Size: 406 KiB |
21
public/site.webmanifest
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "MyWebSite",
|
||||
"short_name": "MySite",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
BIN
public/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
@@ -20,6 +20,7 @@
|
||||
<script lang="ts">
|
||||
import {t} from "astro-i18n";
|
||||
import {statsRepo} from "@repo/stats.ts";
|
||||
import "@styles/table.css"
|
||||
|
||||
|
||||
interface Props {
|
||||
@@ -64,7 +65,3 @@
|
||||
<p>{error.message}</p>
|
||||
{/await}
|
||||
|
||||
<style>
|
||||
@import "../styles/table.css";
|
||||
</style>
|
||||
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
})
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
|
||||
scales: {
|
||||
x: {
|
||||
type: "time",
|
||||
@@ -105,5 +107,5 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<canvas bind:this={canvas}></canvas>
|
||||
<canvas height="500" bind:this={canvas}></canvas>
|
||||
</div>
|
||||
@@ -55,7 +55,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each window(event.fights.filter(f => f.group === group), rows) as fights}
|
||||
{#each window(event.fights.filter(f => group === undefined ? true : f.group === group), rows) as fights}
|
||||
<tr>
|
||||
{#each fights as fight (fight.id)}
|
||||
<td>{Intl.DateTimeFormat(astroI18n.locale, {
|
||||
|
||||
@@ -31,8 +31,7 @@
|
||||
let error: string = $state("");
|
||||
|
||||
async function login() {
|
||||
let {tokenStore} = await import("./repo/repo.ts");
|
||||
let {authRepo} = await import("./repo/auth.ts");
|
||||
let {authV2Repo} = await import("./repo/authv2.ts");
|
||||
if (username === "" || pw === "") {
|
||||
pw = "";
|
||||
error = t("login.error");
|
||||
@@ -40,15 +39,14 @@
|
||||
}
|
||||
|
||||
try {
|
||||
let auth = await get(authRepo).login(username, pw);
|
||||
if (auth == undefined) {
|
||||
let auth = await get(authV2Repo).login(username, pw);
|
||||
if (!auth) {
|
||||
pw = "";
|
||||
error = t("login.error");
|
||||
return;
|
||||
}
|
||||
|
||||
tokenStore.set(auth);
|
||||
navigate(l("/dashboard"));
|
||||
await navigate(l("/dashboard"));
|
||||
} catch (e: any) {
|
||||
pw = "";
|
||||
error = t("login.error");
|
||||
|
||||
@@ -23,24 +23,35 @@
|
||||
import {t} from "astro-i18n";
|
||||
import {l} from "../util/util";
|
||||
import {onMount} from "svelte";
|
||||
import {loggedIn} from "@repo/authv2.ts";
|
||||
interface Props {
|
||||
logo?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { logo }: Props = $props();
|
||||
|
||||
let navbar: HTMLDivElement = $state();
|
||||
let navbar = $state<HTMLDivElement>();
|
||||
let searchOpen = $state(false);
|
||||
|
||||
let accountBtn = $state<HTMLAnchorElement>();
|
||||
|
||||
$effect(() => {
|
||||
if ($loggedIn) {
|
||||
accountBtn!.href = l("/dashboard");
|
||||
} else {
|
||||
accountBtn!.href = l("/login");
|
||||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
handleScroll();
|
||||
})
|
||||
|
||||
function handleScroll() {
|
||||
if (window.scrollY > 0) {
|
||||
navbar.classList.add("before:scale-y-100");
|
||||
navbar!.classList.add("before:scale-y-100");
|
||||
} else {
|
||||
navbar.classList.remove("before:scale-y-100");
|
||||
navbar!.classList.remove("before:scale-y-100");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -90,6 +101,8 @@
|
||||
<a href={l("/rules/microwargear")}
|
||||
class="btn btn-gray">{t("navbar.links.rules.micro")}</a>
|
||||
<a href={l("/rules/streetfight")} class="btn btn-gray">{t("navbar.links.rules.sf")}</a>
|
||||
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.ranked")}</h2>
|
||||
<a href={l("/rangliste/missilewars")} class="btn btn-gray">{t("navbar.links.ranked.mw")}</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- TODO: Add help center
|
||||
@@ -106,7 +119,7 @@
|
||||
</div>
|
||||
</div>
|
||||
-->
|
||||
<a class="btn" href={l("/login")}>
|
||||
<a class="btn" href={l("/login")} bind:this={accountBtn}>
|
||||
<span class="btn__text">{t("navbar.links.account")}</span>
|
||||
</a>
|
||||
<!--
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
import {CollectionEntry} from "astro:content";
|
||||
import type {CollectionEntry} from "astro:content";
|
||||
import {l} from "../util/util";
|
||||
import {astroI18n} from "astro-i18n";
|
||||
import {Image} from "astro:assets";
|
||||
|
||||
@@ -22,38 +22,40 @@
|
||||
import wrap from "svelte-spa-router/wrap";
|
||||
import Router, {replace} from "svelte-spa-router";
|
||||
import {get} from "svelte/store";
|
||||
import {tokenStore} from "@repo/repo";
|
||||
import {loggedIn} from "@repo/authv2.ts";
|
||||
|
||||
const routes: RouteDefinition = {
|
||||
"/": wrap({asyncComponent: () => import("./pages/Home.svelte"), conditions: detail => get(tokenStore) != ""}),
|
||||
"/": wrap({asyncComponent: () => import("./pages/Home.svelte"), conditions: detail => get(loggedIn)}),
|
||||
"/perms": wrap({
|
||||
asyncComponent: () => import("./pages/Perms.svelte"),
|
||||
conditions: detail => get(tokenStore) != ""
|
||||
conditions: detail => get(loggedIn)
|
||||
}),
|
||||
"/login": wrap({
|
||||
asyncComponent: () => import("./pages/Login.svelte"),
|
||||
conditions: detail => get(tokenStore) == ""
|
||||
conditions: detail => !get(loggedIn)
|
||||
}),
|
||||
"/event/:id": wrap({
|
||||
asyncComponent: () => import("./pages/Event.svelte"),
|
||||
conditions: detail => get(tokenStore) != ""
|
||||
conditions: detail => get(loggedIn)
|
||||
}),
|
||||
"/event/:id/generate": wrap({
|
||||
asyncComponent: () => import("./pages/Generate.svelte"),
|
||||
conditions: detail => get(tokenStore) != ""
|
||||
conditions: detail => get(loggedIn)
|
||||
}),
|
||||
"/edit": wrap({
|
||||
asyncComponent: () => import("./pages/Edit.svelte"),
|
||||
conditions: detail => get(tokenStore) != ""
|
||||
conditions: detail => get(loggedIn)
|
||||
}),
|
||||
"/display/:event": wrap({
|
||||
asyncComponent: () => import("./pages/Display.svelte"),
|
||||
conditions: detail => get(tokenStore) != ""
|
||||
conditions: detail => get(loggedIn)
|
||||
}),
|
||||
"*": wrap({asyncComponent: () => import("./pages/NotFound.svelte")})
|
||||
};
|
||||
|
||||
function conditionsFailed(event: ConditionsFailedEvent) {
|
||||
console.log(event)
|
||||
|
||||
if (event.detail.location === "/login") {
|
||||
replace("/");
|
||||
} else {
|
||||
|
||||
@@ -43,7 +43,5 @@
|
||||
maximumFractionDigits: 2
|
||||
}).format(data.playtime)})}h</p>
|
||||
<p>{t("dashboard.stats.fights", {fights: data.fights})}</p>
|
||||
{#if user.perms.includes("CHECK")}
|
||||
<p>{t("dashboard.stats.checked", {checked: data.acceptedSchematics})}</p>
|
||||
{/if}
|
||||
<p>{t("dashboard.stats.checked", {checked: data.acceptedSchematics})}</p>
|
||||
{/await}
|
||||
@@ -21,19 +21,21 @@
|
||||
import {createEventDispatcher} from "svelte";
|
||||
import {schemRepo} from "@repo/schem.ts";
|
||||
import SWModal from "@components/styled/SWModal.svelte";
|
||||
import {t} from "astro-i18n"
|
||||
import {t} from "astro-i18n";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
}
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
let { open = $bindable(false) }: Props = $props();
|
||||
let {open = $bindable(false)}: Props = $props();
|
||||
|
||||
async function upload() {
|
||||
async function upload(e: Event) {
|
||||
e.stopPropagation();
|
||||
if (uploadFile == null) {
|
||||
return
|
||||
error = "dashboard.schematic.errors.noFile";
|
||||
return;
|
||||
}
|
||||
let file = uploadFile[0];
|
||||
|
||||
@@ -42,33 +44,46 @@
|
||||
let type = name.split(".").pop();
|
||||
|
||||
if (type !== "schem" && type !== "schematic") {
|
||||
return
|
||||
error = "dashboard.schematic.errors.invalidEnding";
|
||||
return;
|
||||
}
|
||||
|
||||
let content = await file.arrayBuffer();
|
||||
|
||||
// @ts-ignore
|
||||
let b64 = btoa(String.fromCharCode.apply(null, new Uint8Array(content)));
|
||||
|
||||
let response = await $schemRepo.uploadSchematic(name, b64);
|
||||
try {
|
||||
await $schemRepo.uploadSchematic(name, b64);
|
||||
|
||||
open = false;
|
||||
open = false;
|
||||
value = "";
|
||||
dispatch("reset");
|
||||
} catch (e) {
|
||||
error = "dashboard.schematic.errors.upload";
|
||||
}
|
||||
}
|
||||
|
||||
function reset(e: Event) {
|
||||
e.stopPropagation();
|
||||
open = false
|
||||
value = "";
|
||||
dispatch("reset")
|
||||
}
|
||||
|
||||
let uploadFile: FileList | null = $state(null);
|
||||
let value = $state("");
|
||||
let error = $state(null)
|
||||
</script>
|
||||
|
||||
<SWModal title={t("dashboard.schematic.title")} bind:open>
|
||||
<form>
|
||||
<input type="file" bind:files={uploadFile} bind:value />
|
||||
<label for="schem-upload">{t("dashboard.schematic.title")}</label>
|
||||
<input type="file" id="schem-upload" bind:files={uploadFile} class="overflow-ellipsis" bind:value accept=".schem, .schematic"/>
|
||||
{#if error !== null}
|
||||
<p class="text-red-400">{t(error)}</p>
|
||||
{/if}
|
||||
</form>
|
||||
{#snippet footer()}
|
||||
|
||||
<button class="btn !ml-auto" onclick={upload}>{t("dashboard.schematic.upload")}</button>
|
||||
<button class="btn btn-gray" onclick={() => open = false}>{t("dashboard.schematic.cancel")}</button>
|
||||
|
||||
{/snippet}
|
||||
<button class="btn" onclick={upload}>{t("dashboard.schematic.upload")}</button>
|
||||
<button class="btn btn-gray" onclick={reset}>{t("dashboard.schematic.cancel")}</button>
|
||||
{/snippet}
|
||||
</SWModal>
|
||||
@@ -22,9 +22,9 @@
|
||||
import type {Player} from "@type/data.ts";
|
||||
import {l} from "@utils/util.ts";
|
||||
import Statistics from "./Statistics.svelte";
|
||||
import {authRepo} from "@repo/auth.ts";
|
||||
import {tokenStore} from "@repo/repo.ts";
|
||||
import {authV2Repo} from "@repo/authv2.ts";
|
||||
import Card from "@components/Card.svelte";
|
||||
import {navigate} from "astro:transitions/client";
|
||||
|
||||
interface Props {
|
||||
user: Player;
|
||||
@@ -33,9 +33,8 @@
|
||||
let { user }: Props = $props();
|
||||
|
||||
async function logout() {
|
||||
await $authRepo.logout()
|
||||
tokenStore.set("")
|
||||
window.location.href = l("/login")
|
||||
await $authV2Repo.logout();
|
||||
await navigate(l("/login"));
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2023 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {fetchWithToken, tokenStore} from "./repo.ts";
|
||||
import {derived} from "svelte/store";
|
||||
|
||||
export class AuthRepo {
|
||||
constructor(private token: string) {
|
||||
}
|
||||
|
||||
public async login(username: string, password: string): Promise<string> {
|
||||
return await fetchWithToken(this.token, "/auth/login", {
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
password,
|
||||
}),
|
||||
method: "POST",
|
||||
}).then(value => value.json()).then(value => value.token);
|
||||
}
|
||||
|
||||
public async logout(): Promise<void> {
|
||||
await fetchWithToken(this.token, "/auth/tokens/logout", {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const authRepo = derived(tokenStore, ($token) => new AuthRepo($token));
|
||||
184
src/components/repo/authv2.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2025 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {readable, writable} from "svelte/store";
|
||||
import dayjs, {type Dayjs} from "dayjs";
|
||||
import {type AuthToken, AuthTokenSchema} from "@type/auth.ts";
|
||||
|
||||
export class AuthV2Repo {
|
||||
private accessToken: string | undefined;
|
||||
private accessTokenExpires: Dayjs | undefined;
|
||||
private refreshToken: string | undefined;
|
||||
private refreshTokenExpires: Dayjs | undefined;
|
||||
|
||||
constructor() {
|
||||
if (typeof localStorage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.accessToken = localStorage.getItem("sw-access-token") ?? undefined;
|
||||
if (this.accessToken) {
|
||||
this.accessTokenExpires = dayjs(localStorage.getItem("sw-access-token-expires") ?? "");
|
||||
}
|
||||
|
||||
this.refreshToken = localStorage.getItem("sw-refresh-token") ?? undefined;
|
||||
if (this.refreshToken) {
|
||||
loggedIn.set(true);
|
||||
this.refreshTokenExpires = dayjs(localStorage.getItem("sw-refresh-token-expires") ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
async login(name: string, password: string) {
|
||||
if (this.accessToken !== undefined || this.refreshToken !== undefined) {
|
||||
throw new Error("Already logged in");
|
||||
}
|
||||
|
||||
try {
|
||||
const login = await this.request("/auth", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
password,
|
||||
keepLoggedIn: true,
|
||||
}),
|
||||
}).then(value => value.json()).then(value => AuthTokenSchema.parse(value));
|
||||
|
||||
this.setLoginState(login);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
if (this.accessToken === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.request("/auth", {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
this.resetAccessToken();
|
||||
this.resetRefreshToken();
|
||||
}
|
||||
|
||||
private setLoginState(tokens: AuthToken) {
|
||||
this.setAccessToken(tokens.accessToken.token, dayjs(tokens.accessToken.expires));
|
||||
this.setRefreshToken(tokens.refreshToken.token, dayjs(tokens.refreshToken.expires));
|
||||
loggedIn.set(true);
|
||||
}
|
||||
|
||||
private setAccessToken(token: string, expires: Dayjs) {
|
||||
this.accessToken = token;
|
||||
this.accessTokenExpires = expires;
|
||||
localStorage.setItem("sw-access-token", token);
|
||||
localStorage.setItem("sw-access-token-expires", expires.toString());
|
||||
}
|
||||
|
||||
private resetAccessToken() {
|
||||
if (this.accessToken === undefined) {
|
||||
return;
|
||||
}
|
||||
this.accessToken = undefined;
|
||||
this.accessTokenExpires = undefined;
|
||||
localStorage.removeItem("sw-access-token");
|
||||
localStorage.removeItem("sw-access-token-expires");
|
||||
}
|
||||
|
||||
private setRefreshToken(token: string, expires: Dayjs) {
|
||||
this.refreshToken = token;
|
||||
this.refreshTokenExpires = expires;
|
||||
localStorage.setItem("sw-refresh-token", token);
|
||||
localStorage.setItem("sw-refresh-token-expires", expires.toString());
|
||||
}
|
||||
|
||||
private resetRefreshToken() {
|
||||
if (this.refreshToken === undefined) {
|
||||
return;
|
||||
}
|
||||
this.refreshToken = undefined;
|
||||
this.refreshTokenExpires = undefined;
|
||||
localStorage.removeItem("sw-refresh-token");
|
||||
localStorage.removeItem("sw-refresh-token-expires");
|
||||
|
||||
loggedIn.set(false);
|
||||
}
|
||||
|
||||
private async refresh() {
|
||||
if (this.refreshToken === undefined || this.refreshTokenExpires === undefined || this.refreshTokenExpires.isBefore(dayjs().add(10, "seconds"))) {
|
||||
this.resetRefreshToken();
|
||||
this.resetAccessToken();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.requestWithToken(this.refreshToken!, "/auth", {
|
||||
method: "PUT",
|
||||
}).then(value => {
|
||||
if (value.status === 401) {
|
||||
this.resetRefreshToken();
|
||||
this.resetAccessToken();
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return value.json();
|
||||
}).then(value => AuthTokenSchema.parse(value));
|
||||
|
||||
this.setLoginState(response);
|
||||
}
|
||||
|
||||
async request(url: string, params: RequestInit = {}, retryCount: number = 0) {
|
||||
if (this.accessToken !== undefined && this.accessTokenExpires !== undefined && this.accessTokenExpires.isBefore(dayjs().add(10, "seconds"))) {
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
return this.requestWithToken(this.accessToken ?? "", url, params, retryCount);
|
||||
}
|
||||
|
||||
private async requestWithToken(token: string, url: string, params: RequestInit = {}, retryCount: number = 0): Promise<Response> {
|
||||
if (retryCount >= 3) {
|
||||
throw new Error("Too many retries");
|
||||
}
|
||||
|
||||
return fetch(`${import.meta.env.PUBLIC_API_SERVER}${url}`, {...params,
|
||||
headers: {
|
||||
...(token !== "" ? {"Authorization": "Bearer " + (token)} : {}),
|
||||
"Content-Type": "application/json", ...params.headers,
|
||||
},
|
||||
})
|
||||
.then(async value => {
|
||||
if (value.status === 401 && url !== "/auth") {
|
||||
try {
|
||||
await this.refresh();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (_e) { /* empty */ }
|
||||
|
||||
return this.request(url, params, retryCount + 1);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const loggedIn = writable(false);
|
||||
|
||||
export const authV2Repo = readable(new AuthV2Repo());
|
||||
@@ -17,31 +17,9 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {writable} from "svelte/store";
|
||||
import {get, writable} from "svelte/store";
|
||||
import {authV2Repo} from "@repo/authv2.ts";
|
||||
|
||||
export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) =>
|
||||
fetch(`${import.meta.env.PUBLIC_API_SERVER}${url}`, {...params,
|
||||
headers: {
|
||||
...(token !== "" ? {"Authorization": "Bearer " + (token)} : {}),
|
||||
"Content-Type": "application/json", ...params.headers,
|
||||
},
|
||||
})
|
||||
.then(value => {
|
||||
if (value.status === 401) {
|
||||
tokenStore.set("");
|
||||
}
|
||||
return value;
|
||||
});
|
||||
export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) => get(authV2Repo).request(url, params);
|
||||
|
||||
export function getLocalStorage() {
|
||||
if (typeof localStorage === "undefined") {
|
||||
return {
|
||||
getItem: () => "",
|
||||
setItem: () => {},
|
||||
};
|
||||
}
|
||||
return localStorage;
|
||||
}
|
||||
|
||||
export const tokenStore = writable((getLocalStorage().getItem("sw-session") ?? ""));
|
||||
tokenStore.subscribe((value) => getLocalStorage().setItem("sw-session", value));
|
||||
export const tokenStore = writable("");
|
||||
|
||||
@@ -36,7 +36,7 @@ export class StatsRepo {
|
||||
}
|
||||
|
||||
public async getUserStats(id: string): Promise<UserStats> {
|
||||
return await fetchWithToken(this.token, `/stats/user/${id}`).then(value => value.json()).then(UserStatsSchema.parse);
|
||||
return await fetchWithToken(this.token, `/stats/user`).then(value => value.json()).then(UserStatsSchema.parse);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,16 +68,18 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<dialog bind:this={dialog} onclose={close} onclick={(e) => dialog.close()} aria-hidden="true" class="max-h-full max-w-md w-full rounded-lg shadow-lg dark:bg-neutral-800 dark:text-neutral-100">
|
||||
<div onclick={stopPropagation(onclick)} aria-hidden="true">
|
||||
<dialog bind:this={dialog} onclose={close} onclick={(e) => dialog.close()} aria-hidden="true" class="max-h-full min-w-md w-fit rounded-lg shadow-lg dark:bg-neutral-800 dark:text-neutral-100">
|
||||
<div onclick={stopPropagation(onclick)} aria-hidden="true" class="w-fit">
|
||||
<div class="p-6 border-b border-neutral-200 dark:border-neutral-700">
|
||||
<h1 class="text-4xl font-bold">{title}</h1>
|
||||
</div>
|
||||
<div class="p-6 main border-b border-neutral-200 dark:border-neutral-700">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<div class="flex mx-4 my-2 p-6" onclick={() => dialog.close()} aria-hidden="true">
|
||||
{@render footer?.()}
|
||||
<div class="mx-4 my-2 p-6">
|
||||
<div class="ml-auto flex justify-end" onclick={() => dialog.close()} aria-hidden="true">
|
||||
{@render footer?.()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
34
src/components/types/auth.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is a part of the SteamWar software.
|
||||
*
|
||||
* Copyright (C) 2025 SteamWar.de-Serverteam
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {z} from "zod";
|
||||
|
||||
export const TokenSchema = z.object({
|
||||
token: z.string(),
|
||||
expires: z.string(),
|
||||
});
|
||||
|
||||
export type Token = z.infer<typeof TokenSchema>;
|
||||
|
||||
export const AuthTokenSchema = z.object({
|
||||
accessToken: TokenSchema,
|
||||
refreshToken: TokenSchema,
|
||||
});
|
||||
|
||||
export type AuthToken = z.infer<typeof AuthTokenSchema>;
|
||||
46
src/content/announcements/de/missilewars-iii-eventplan.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: MissileWars III Eventplan
|
||||
key: missilewars3-eventplan
|
||||
description: Der Eventplan für MissileWars 3
|
||||
created: 2025-02-23
|
||||
tags:
|
||||
- event
|
||||
- missilewars
|
||||
---
|
||||
|
||||
### Infos:
|
||||
Eventleitung: TheBreadBeard
|
||||
|
||||
Fights werden nach Möglichkeit mit einer Pause von 10 Minuten vorverschoben.
|
||||
# Gruppenphase
|
||||
## Punkte aus der Gruppenphase
|
||||
|
||||
<group-table data-event="67"> </group-table>
|
||||
|
||||
### Fights
|
||||
|
||||
| Start | Teams |
|
||||
|-------|------------|
|
||||
| 16:00:00 | KT vs Borg |
|
||||
| 16:00:30 | VI vs FK |
|
||||
| 16:30:00 | FK vs KT |
|
||||
| 16:30:30 | Hlcy vs VI |
|
||||
| 17:00:00 | VI vs KT |
|
||||
| 17:00:30 | Borg vs Hlcy |
|
||||
| 17:30:00 | KT vs Hlcy|
|
||||
| 17:30:30 | FK vs Borg |
|
||||
| 18:00:00 | VI vs Borg |
|
||||
| 18:00:30 | FK vs Hlcy |
|
||||
|
||||
## KO-Phase
|
||||
|
||||
| Start | Teams |
|
||||
|-------|------------|
|
||||
| 17:50:00 | VI vs Borg |
|
||||
| 18:20:00 | Hlcy vs FK |
|
||||
| 18:22:00 | VI vs FK |
|
||||
| 18:44:00 | Hlcy vs Borg |
|
||||
|
||||
## Ergebnisse
|
||||
|
||||
<fight-table data-event="67" data-group="Gruppe 1"> </fight-table>
|
||||
@@ -4,8 +4,8 @@ key: schneeballschlacht
|
||||
description: Die Schneeballschlacht auf SteamWar.
|
||||
created: 2024-11-17
|
||||
tags:
|
||||
- event
|
||||
- miniwargear
|
||||
- event
|
||||
- miniwargear
|
||||
image: ../../../images/SchneeballschlachtMWG.png
|
||||
---
|
||||
|
||||
@@ -13,26 +13,27 @@ image: ../../../images/SchneeballschlachtMWG.png
|
||||
|
||||
Die Tage werden kürzer und kälter, es geht auf die weiße Jahreszeit zu. Wenn der erste Schnee liegt, wird es Zeit, sich mit Mütze und Handschuhen zu bewaffnen und nach draußen zu eilen. Auch auf SteamWar wird dieses Jahr eine Schneeballschlacht stattfinden, im Modus MiniWarGear. Das Event wird, wie auch schon das Halloween-Event, von einer Community-Abstimmung begleitet, Das MWG-Regelwerk wurde mit einigen zusätzlichen Regeln erweitert.
|
||||
|
||||
- Das Event wird am **22.12.2024** stattfinden
|
||||
- Einsendeschluss **18.12.2024**, Hotfixes dürfen bis zum **20.12.2024** Eingesendet werden
|
||||
- Design-Regel: **Weihnachtliches/Winterliches Design**
|
||||
- Das Event wird am **22.12.2024** stattfinden
|
||||
- Einsendeschluss **18.12.2024**, Hotfixes dürfen bis zum **20.12.2024** Eingesendet werden
|
||||
- Design-Regel: **Weihnachtliches/Winterliches Design**
|
||||
|
||||
## Regeln:
|
||||
- Regelwerk: **MiniWarGear**, ohne MGs, 120 TNT trotzdem erlaubt
|
||||
- **Weißer Trockenbeton**/**White Concrete Powder** darf zum Gegner geschossen und/oder geboosted werden, beginnend ab der Ausfahrzeit
|
||||
- Solche „Schneeballkanonen“ zählen nicht zum Kanonenlimit und dürfen vorbeladen sein, solange sie keine Projektile schießen können
|
||||
- Schneeballkanonen dürfen Wasser für ihre Treibladungen benutzen
|
||||
- Der Turnieraufbau ist wie folgt:
|
||||
- Alle 20 min Start, jeweils zwei Fights je 10 min versetzt
|
||||
- Struktur: Ranglistenturnier (Gruppenphase, in KO-Phase wird jeder Platz (1, 2, 3, 4, 5, 6, …) umkämpft
|
||||
|
||||
- Regelwerk: **MiniWarGear**, ohne MGs, 120 TNT trotzdem erlaubt
|
||||
- **Weißer Trockenbeton**/**White Concrete Powder** darf zum Gegner geschossen und/oder geboosted werden, beginnend ab der Ausfahrzeit
|
||||
- Solche „Schneeballkanonen“ zählen nicht zum Kanonenlimit und dürfen vorbeladen sein, solange sie keine Projektile schießen können
|
||||
- Schneeballkanonen dürfen Wasser für ihre Treibladungen benutzen
|
||||
- Der Turnieraufbau ist wie folgt:
|
||||
- Alle 20 min Start, jeweils zwei Fights je 10 min versetzt
|
||||
- Struktur: Ranglistenturnier (Gruppenphase, in KO-Phase wird jeder Platz (1, 2, 3, 4, 5, 6, …) umkämpft
|
||||
|
||||
## Die beste „Schneeballkanone“ wird per Uservoting wie folgt besimmt
|
||||
|
||||
- Jedes Team muss am Tag des Turnieres (z.B. nach Ende der Fights) Bild/Videomaterial von ihrer Schneeballkanone per Discord an Zent4rus oder zSalos schicken, diese werden danach unter einem Thread zur Abstimmung veröffentlicht
|
||||
- Die Darstellung der Schneeballkanone (mit/ohne Techhider, mit/ohne MWG außen rum) obliegt den Teams selbst
|
||||
- Sieger von bester Schneeballkanone wird durch Uservote bestimmt (ähnlich wie bei WG – Designcontest)
|
||||
- Die Abstimmung endet drei Tage nach Veröffentlichung der Abstimmung
|
||||
- Jedes Team muss am Tag des Turnieres (z.B. nach Ende der Fights) Bild/Videomaterial von ihrer Schneeballkanone per Discord an Zent4rus oder zSalos schicken, diese werden danach unter einem Thread zur Abstimmung veröffentlicht
|
||||
- Die Darstellung der Schneeballkanone (mit/ohne Techhider, mit/ohne MWG außen rum) obliegt den Teams selbst
|
||||
- Sieger von bester Schneeballkanone wird durch Uservote bestimmt (ähnlich wie bei WG – Designcontest)
|
||||
- Die Abstimmung endet drei Tage nach Veröffentlichung der Abstimmung
|
||||
|
||||
Der Gesamtsieger wird mit folgender Formel bestimmt: 0,66 * Fightplatzierung + 0,33 * Platzierung Uservoting, bei Gleichstand bestimmt die Fightplatzierung
|
||||
Der Gesamtsieger wird mit folgender Formel bestimmt: 0,66 _ Fightplatzierung + 0,33 _ Platzierung Uservoting, bei Gleichstand bestimmt die Fightplatzierung
|
||||
|
||||
**Wir wünschen allen teilnehmenden Teams viel Erfolg!**
|
||||
28
src/content/announcements/de/sw-arcade.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: SteamWar Arcade
|
||||
description: "Das erste SteamWar Arcade Event: MissleWars, TowerRun und TNTLeague"
|
||||
key: steamwar-arcade
|
||||
created: 2025-04-14
|
||||
tags:
|
||||
- event
|
||||
- towerrun
|
||||
- misslewars
|
||||
- tntleague
|
||||
- arcade
|
||||
image: ../../../images/sw-arcade-image.png
|
||||
---
|
||||
|
||||
**Ahoi Liebe Community,**
|
||||
|
||||
Am **18. Mai** feiert SteamWar seinen 6. Geburtstag. Zur Feier dieses Meilensteins werden wir ein kleines Event veranstalten: **SteamWar Arcade**
|
||||
|
||||
- Anmeldeschluss ist der **11.5.2025**
|
||||
- In jedem Modus können bis zu **drei Personen** an einem Kampf teilnehmen
|
||||
|
||||
## Ablauf:
|
||||
|
||||
- Es gibt drei Modi zur Auswahl: **TowerRun**, **TNTLeague** und **MissleWars**
|
||||
- Die Teams stimmen 5 Minuten vor Beginn den Modus ab. Sind die Teams sich einig, wird dieser Modus gespielt. Sind die Teams sich nicht einig, wird der dritte Modus gespielt.
|
||||
- Es wird eine einfache Gruppenphase geben: alle gegen alle
|
||||
|
||||
**Wir wünschen allen teilnehmenden Teams viel Erfolg!**
|
||||
105
src/content/announcements/de/wgs25-kampfplan.md
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
title: WGS 2025 Kampfplan
|
||||
description: WGS 2025 Kampfplan
|
||||
key: wgs-2025-kampfplan
|
||||
created: 2025-03-01
|
||||
tags:
|
||||
- event
|
||||
- wargear
|
||||
---
|
||||
|
||||
# WGS 2025 Kampfplan
|
||||
|
||||
### Platzierung
|
||||
|
||||
| Platzierung | Team | Punkte |
|
||||
|-------------|-------------|--------|
|
||||
| 1. | Halcyon | 10 |
|
||||
| 2. | Happy-End | 8 |
|
||||
| 3. | ABYSS | 5 |
|
||||
| 4. | Borg | 4 |
|
||||
| 5. | BlackFire | 2 |
|
||||
| 6. | TheNoobTeam | 1 |
|
||||
|
||||
### Aktueller Spieltag
|
||||
|
||||
<fight-table data-event="68"></fight-table>
|
||||
|
||||
### Qualifikation
|
||||
|
||||
| 01.03.2025 | Begegnung | Ergebnis |
|
||||
|------------|--------------|----------|
|
||||
| 19:00 | HPY vs ABYS | HPY qualifiziert |
|
||||
| 19:30 | Borg vs Hlcy | Hlcy qualifiziert |
|
||||
| 20:00 | TNT vs BF | TNT qualifiziert |
|
||||
| 20:30 | ABYS vs Borg | Sieg ABYS |
|
||||
| 21:00 | Borg vs BF | Sieg Borg |
|
||||
| 21:30 | BF vs ABYS | ABYS, Borg qualifiziert |
|
||||
|
||||
PL hat sich nach der Qualifikation zurückgezogen, womit BF nachgerückt ist.
|
||||
|
||||
### 1. Spieltag
|
||||
|
||||
| 08.03.2025 | Begegnung | Ergebnis |
|
||||
|------------|--------------|----------|
|
||||
| 19:00 | ABYS vs Borg | Sieg ABYS |
|
||||
| 19:30 | Hlcy vs HPY | Sieg Hlcy |
|
||||
| 20:00 | BF vs TNT | Sieg TNT |
|
||||
| 20:30 | HPY vs ABYS | Sieg HPY |
|
||||
| 21:00 | Borg vs BF | Sieg Borg |
|
||||
| 21:30 | TNT vs Hlcy | Sieg Hlcy |
|
||||
|
||||
### 2. Spieltag
|
||||
|
||||
| 15.03.2025 | Begegnung | Ergebnis |
|
||||
|------------|--------------|----------|
|
||||
| 19:00 | HPY vs TNT | Sieg HPY |
|
||||
| 19:30 | ABYS vs BF | Sieg ABYS |
|
||||
| 20:00 | Borg vs Hlcy | Sieg Hlcy |
|
||||
| 20:30 | BF vs HPY | Sieg HPY |
|
||||
| 21:00 | TNT vs Borg | Sieg Borg |
|
||||
| 21:30 | Hlcy vs ABYS | Sieg Hlcy |
|
||||
|
||||
### 3. Spieltag
|
||||
|
||||
| 22.03.2025 | Begegnung | Ergebnis |
|
||||
|------------|-------------|----------|
|
||||
| 19:00 | ABYS vs TNT | Sieg ABYS |
|
||||
| 19:30 | Borg vs HPY | Sieg HPY |
|
||||
| 20:00 | Hlcy vs BF | Sieg Hlcy |
|
||||
| 20:30 | - | / |
|
||||
| 21:00 | - | / |
|
||||
| 21:30 | - | / |
|
||||
|
||||
### 4. Spieltag
|
||||
|
||||
| 29.03.2025 | Begegnung | Ergebnis |
|
||||
|------------|--------------|----------|
|
||||
| 19:00 | TNT vs ABYS | Sieg ABYS |
|
||||
| 19:30 | HPY vs Borg | Sieg HPY |
|
||||
| 20:00 | BF vs Hlcy | Sieg Hlcy |
|
||||
| 20:30 | Borg vs TNT | Sieg Borg |
|
||||
| 21:00 | HPY vs BF | Sieg HPY |
|
||||
| 21:30 | ABYS vs Hlcy | Sieg Hlcy |
|
||||
|
||||
### 5. Spieltag
|
||||
|
||||
| 05.04.2025 | Begegnung | Ergebnis |
|
||||
|------------|--------------|----------|
|
||||
| 19:00 | TNT vs HPY | Sieg HPY |
|
||||
| 19:30 | BF vs ABYS | Sieg BF |
|
||||
| 20:00 | Hlcy vs Borg | Sieg Hlcy |
|
||||
| 20:30 | ABYS vs HPY | Sieg HPY |
|
||||
| 21:00 | BF vs Borg | Sieg Borg |
|
||||
| 21:30 | Hlcy vs TNT | Sieg Hlcy |
|
||||
|
||||
### 6. Spieltag
|
||||
|
||||
| 12.04.2025 | Begegnung | Ergebnis |
|
||||
|------------|--------------|----------|
|
||||
| 19:00 | Borg vs ABYS | Sieg ABYS |
|
||||
| 19:30 | HPY vs Hlcy | Sieg Hlcy |
|
||||
| 20:00 | TNT vs BF | Sieg BF |
|
||||
| 20:30 | - | / |
|
||||
| 21:00 | - | / |
|
||||
| 21:30 | - | / |
|
||||
@@ -4,7 +4,8 @@
|
||||
"url": {
|
||||
"1.21.4": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.2.0/AdvancedScripts-2.2.0.jar",
|
||||
"1.20.6": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.1.0/AdvancedScripts-2.1.0.jar",
|
||||
"1.19.3": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.0.0/AdvancedScripts-2.0.0.jar"
|
||||
"1.19.3": "https://git.steamwar.de/SteamWar/AdvancedScripts/releases/download/2.0.0/AdvancedScripts-2.0.0.jar",
|
||||
"Hotkey script": "https://git.steamwar.de/SteamWar/SteamWar/src/branch/main/BauSystem/hotkeys.lua"
|
||||
},
|
||||
"sourceUrl": "https://git.steamwar.de/SteamWar/AdvancedScripts"
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
"name": "SteamWarTeamserver",
|
||||
"description": "Dieses Plugin ermöglicht die einfache Einbindung deines Servers in SteamWar. Wie du deinen (Team-)Server über SteamWar erreichbar machen kannst findest du hier.",
|
||||
"url": {
|
||||
"Info": "/teamserverintegration"
|
||||
"Info": "/teamserverintegration",
|
||||
"Download": "https://git.steamwar.de/SteamWar/SteamWarTeamserverIntegration/releases/download/latest/SteamWarTeamserverIntegration.jar"
|
||||
},
|
||||
"sourceUrl": "https://git.steamwar.de/SteamWar/SteamWarBungeeTeamserver"
|
||||
"sourceUrl": "https://git.steamwar.de/SteamWar/SteamWarTeamserverIntegration"
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"translationKey": "microwg",
|
||||
"main": false
|
||||
"main": false,
|
||||
"ranked": true
|
||||
}
|
||||
5
src/content/modes/missilewars.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"translationKey": "mw",
|
||||
"main": false,
|
||||
"ranked": true
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "Graf Spree",
|
||||
"name": "Graf Spee",
|
||||
"description": "A simple, lightweight description of a frostbite.",
|
||||
"id": 123,
|
||||
"creator": ["Test", "Test2"],
|
||||
"gamemode": "warship",
|
||||
"image": "../../../images/publics/grafspree/area_render.png",
|
||||
"image": "../../../images/publics/grafspee/area_render.png",
|
||||
"3d": false
|
||||
}
|
||||
3
src/content/rules/de/missilewars.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
translationKey: mw
|
||||
---
|
||||
@@ -87,7 +87,10 @@
|
||||
"title": "Hilfe",
|
||||
"docs": "Dokumentation"
|
||||
},
|
||||
"account": "Konto"
|
||||
"account": "Konto",
|
||||
"ranked": {
|
||||
"mw": "MissileWars"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wg": {
|
||||
@@ -123,6 +126,9 @@
|
||||
"microwg": {
|
||||
"title": "MicroWarGear"
|
||||
},
|
||||
"mw": {
|
||||
"title": "MissileWars"
|
||||
},
|
||||
"footer": {
|
||||
"imprint": "Impressum",
|
||||
"privacy": "Datenschutzerklärung",
|
||||
@@ -167,7 +173,7 @@
|
||||
"stats": {
|
||||
"playtime": "Spielzeit: {# playtime #}",
|
||||
"fights": "Kämpfe: {# fights #}",
|
||||
"checked": "Schematics Geprüft: {# checked #}"
|
||||
"checked": "Freigegebene Schematics: {# checked #}"
|
||||
},
|
||||
"schematic": {
|
||||
"upload": "Hochladen",
|
||||
@@ -194,7 +200,12 @@
|
||||
}
|
||||
},
|
||||
"cancel": "Abbrechen",
|
||||
"title": "Schematic hochladen"
|
||||
"title": "Schematic hochladen",
|
||||
"errors": {
|
||||
"invalidEnding": "Diese Dateiendung kann nicht Hochgeladen werden.",
|
||||
"noFile": "Keine Datei.",
|
||||
"upload": "Fehler beim Hochladen, Überprüfe deine Schematic!"
|
||||
}
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
@@ -205,7 +216,8 @@
|
||||
},
|
||||
"label": {
|
||||
"username": "Nutzername",
|
||||
"password": "Passwort"
|
||||
"password": "Passwort",
|
||||
"repeat": "Passwort Wiederholen"
|
||||
},
|
||||
"setPassword": "Wie setzte ich mein Passwort?",
|
||||
"submit": "Login",
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
"stats": {
|
||||
"playtime": "Playtime: {# playtime #}",
|
||||
"fights": "Fights: {# fights #}",
|
||||
"checked": "Schematics Checked: {# checked #}"
|
||||
"checked": "Accepted Schematics: {# checked #}"
|
||||
},
|
||||
"schematic": {
|
||||
"upload": "Upload",
|
||||
@@ -137,6 +137,11 @@
|
||||
"download": "Download",
|
||||
"close": "Close"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"invalidEnding": "This file extension cannot be uploaded.",
|
||||
"noFile": "No file.",
|
||||
"upload": "Error uploading, check your schematic!"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -149,7 +154,8 @@
|
||||
},
|
||||
"label": {
|
||||
"username": "Username",
|
||||
"password": "Password"
|
||||
"password": "Password",
|
||||
"repeat": "Repeat Password"
|
||||
},
|
||||
"setPassword": "How to set a Password",
|
||||
"submit": "Login",
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
BIN
src/images/sw-arcade-image.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
@@ -1,13 +1,9 @@
|
||||
---
|
||||
import icon from "../images/logo.png";
|
||||
import {getImage} from "astro:assets";
|
||||
import {astroI18n} from "astro-i18n";
|
||||
import {SEO} from "astro-seo";
|
||||
import {ClientRouter} from "astro:transitions";
|
||||
const {title, description, clientSideRouter = true} = Astro.props.frontmatter || Astro.props;
|
||||
import "../../public/fonts/roboto/roboto.css";
|
||||
|
||||
const iconImage = await getImage({src: icon, height: 32, width: 32, format: "png", quality: 100});
|
||||
---
|
||||
|
||||
<html lang={astroI18n.locale} class="dark">
|
||||
@@ -17,7 +13,12 @@ const iconImage = await getImage({src: icon, height: 32, width: 32, format: "png
|
||||
content="width=device-width, user-scalable=5, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<meta name="icbm" content="52.370216;4.895168"/>
|
||||
<link rel="icon" type="imgage/png" href={iconImage.src}/>
|
||||
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<meta name="apple-mobile-web-app-title" content="MyWebSite" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
|
||||
<SEO
|
||||
title={title}
|
||||
|
||||
@@ -3,7 +3,7 @@ import {Image} from "astro:assets";
|
||||
import Basic from "./Basic.astro";
|
||||
import "../styles/button.css";
|
||||
import localLogo from "../images/logo.png";
|
||||
import {YoutubeSolid, DiscordSolid} from "flowbite-svelte-icons";
|
||||
import {YoutubeSolid, DiscordSolid, FileCodeSolid} from "flowbite-svelte-icons";
|
||||
import {t} from "astro-i18n";
|
||||
import {l} from "../util/util";
|
||||
|
||||
@@ -50,6 +50,9 @@ const {title, description} = Astro.props;
|
||||
<a class="flex" href="/discord">
|
||||
<DiscordSolid class="mr-2"/>
|
||||
Discord</a>
|
||||
<a class="flex" href="https://git.steamwar.de">
|
||||
<FileCodeSolid class="mr-2"/>
|
||||
Gitea</a>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm text-white text-center mt-1">© SteamWar.de - Made with ❤️ by Chaoscaot</span>
|
||||
|
||||
@@ -77,7 +77,7 @@ const ogImage = await getImage({
|
||||
)}
|
||||
<div class={post.data.image ? "absolute bottom-8 left-2" : "mb-4"}>
|
||||
<h1 class="text-4xl mb-0" transition:name={post.data.title + "-title"}>{post.data.title}</h1>
|
||||
<div class="flex items-center mt-2 text-neutral-300">
|
||||
<div class="flex items-center mt-2 text-neutral-800 dark:text-neutral-300">
|
||||
<TagSolid class="w-4 h-4 mr-2"/>
|
||||
<div transition:name={post.data.title + "-tags"}>
|
||||
{post.data.tags.map(tag => (
|
||||
@@ -111,15 +111,13 @@ const ogImage = await getImage({
|
||||
import type {ExtendedEvent} from "@type/event";
|
||||
import {mount} from "svelte";
|
||||
|
||||
const eventMounts: Map<string, ((ev: ExtendedEvent) => void)[]> = new Map();
|
||||
const eventMounts: Map<string, Promise<ExtendedEvent>> = new Map();
|
||||
|
||||
class FightTableElement extends HTMLElement {
|
||||
connectedCallback(): void {
|
||||
if (!eventMounts.has(this.dataset["event"]!)) {
|
||||
eventMounts.set(this.dataset["event"]!, []);
|
||||
}
|
||||
loadEvent(this.dataset["event"]!);
|
||||
const rows = Number.parseInt(this.dataset["rows"]!);
|
||||
eventMounts.get(this.dataset["event"]!)!.push(ev => {
|
||||
eventMounts.get(this.dataset["event"]!)!.then(ev => {
|
||||
mount(FightTable, {
|
||||
target: this,
|
||||
props: {
|
||||
@@ -134,11 +132,9 @@ const ogImage = await getImage({
|
||||
|
||||
class GroupTableElement extends HTMLElement {
|
||||
connectedCallback(): void {
|
||||
if (!eventMounts.has(this.dataset["event"]!)) {
|
||||
eventMounts.set(this.dataset["event"]!, []);
|
||||
}
|
||||
loadEvent(this.dataset["event"]!);
|
||||
const rows = Number.parseInt(this.dataset["rows"]!);
|
||||
eventMounts.get(this.dataset["event"]!)!.push(ev => {
|
||||
eventMounts.get(this.dataset["event"]!)!.then(ev => {
|
||||
mount(GroupTable, {
|
||||
target: this,
|
||||
props: {
|
||||
@@ -154,17 +150,13 @@ const ogImage = await getImage({
|
||||
customElements.define("fight-table", FightTableElement);
|
||||
customElements.define("group-table", GroupTableElement);
|
||||
|
||||
function mountEvent() {
|
||||
for (const key of eventMounts.keys()) {
|
||||
get(eventRepo).getEvent(key).then(ev => {
|
||||
for (const mount of eventMounts.get(key)!) {
|
||||
mount(ev);
|
||||
}
|
||||
});
|
||||
function loadEvent(id: string) {
|
||||
if (!eventMounts.has(id)) {
|
||||
eventMounts.set(id, get(eventRepo).getEvent(id));
|
||||
}
|
||||
}
|
||||
|
||||
mountEvent();
|
||||
document.addEventListener("astro:before-swap", eventMounts.clear);
|
||||
</script>
|
||||
</article>
|
||||
</PageLayout>
|
||||
|
||||
@@ -8,9 +8,11 @@ import {t} from "astro-i18n";
|
||||
<script>
|
||||
import {l} from "../util/util";
|
||||
import {navigate} from "astro:transitions/client";
|
||||
import {get} from "svelte/store";
|
||||
import {loggedIn} from "../components/repo/authv2";
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
if (window.location.href.endsWith("/dashboard") || window.location.href.endsWith("/dashboard/")) {
|
||||
if ((localStorage.getItem("sw-session") ?? "") === "") {
|
||||
if (!get(loggedIn)) {
|
||||
navigate(l("/login"), {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {type Player} from "../components/types/data";
|
||||
import PostComponent from "../components/PostComponent.astro";
|
||||
import BackgroundImage from "../components/BackgroundImage.astro";
|
||||
|
||||
const teamMember: { [key: string]: Player[]} = await fetch("http://127.0.0.1:1337/data/team")
|
||||
const teamMember: { [key: string]: Player[]} = await fetch(import.meta.env.PUBLIC_API_SERVER + "/data/team")
|
||||
.then(value => value.json());
|
||||
|
||||
const posts = await getCollection("announcements", entry => entry.id.split("/")[0] === astroI18n.locale);
|
||||
|
||||
@@ -9,10 +9,12 @@ import BackgroundImage from "../components/BackgroundImage.astro";
|
||||
<script>
|
||||
import {l} from "../util/util";
|
||||
import {navigate} from "astro:transitions/client";
|
||||
import {loggedIn} from "../components/repo/authv2";
|
||||
import {get} from "svelte/store";
|
||||
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
if (window.location.href.endsWith("/login") || window.location.href.endsWith("/login/")) {
|
||||
if ((localStorage.getItem("sw-session") ?? "") !== "") {
|
||||
if (get(loggedIn)) {
|
||||
navigate(l("/dashboard"), {history: "replace"});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,4 +44,5 @@ const modes = await getCollection("modes", entry => entry.data.main);
|
||||
</div>
|
||||
</div>
|
||||
</div>))}
|
||||
<a href={l("/rangliste/MissileWars")}>MissileWars Rangliste</a>
|
||||
</PageLayout>
|
||||
@@ -31,7 +31,7 @@ table {
|
||||
text-align: center;
|
||||
|
||||
tr:nth-child(odd) {
|
||||
@apply bg-neutral-800;
|
||||
@apply bg-neutral-200 dark:bg-neutral-800;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||