30 Commits

Author SHA1 Message Date
470d715a7a Add view configuration for Gruppenphase and Finale in OsterEvent
All checks were successful
SteamWarCI Build successful
2026-04-25 01:21:58 +02:00
3948009c35 Remove teamserver.json configuration file
All checks were successful
SteamWarCI Build successful
2026-04-23 23:51:03 +02:00
a410830a93 Refactor z-index and background styles for improved layout consistency
All checks were successful
SteamWarCI Build successful
2026-04-23 23:48:59 +02:00
f1d7b60fae Fix z-Index
All checks were successful
SteamWarCI Build successful
2026-04-23 23:43:54 +02:00
01077da029 Increase z-index for dropdown button to improve visibility
All checks were successful
SteamWarCI Build successful
2026-04-23 23:42:08 +02:00
a195b074a7 Preserve Pathname on Lang Change
All checks were successful
SteamWarCI Build successful
Co-authored-by: Copilot <copilot@github.com>
2026-04-23 15:07:04 +02:00
a8817115a3 Fix Safari
All checks were successful
SteamWarCI Build successful
2026-04-23 14:59:56 +02:00
5639655f99 Fix Lang Switcher
All checks were successful
SteamWarCI Build successful
2026-04-23 14:10:54 +02:00
ba4aa67ff1 Update language selection links in Navbar to use data-astro-reload attribute
Some checks failed
SteamWarCI Build failed
2026-04-23 14:08:54 +02:00
6fde748088 Merge pull request 'page-redesign' (#22) from page-redesign into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #22
Reviewed-by: YoyoNow <yoyonow@noreply.localhost>
2026-04-23 12:23:26 +02:00
01da718802 Merge branch 'master' into page-redesign
All checks were successful
SteamWarCI Build successful
2026-04-23 12:23:15 +02:00
23566e72d3 Refactor Navbar component to replace search button with language selection dropdown
All checks were successful
SteamWarCI Build successful
2026-04-23 12:22:42 +02:00
9d06dc7d0b Merge branch 'master' into page-redesign
All checks were successful
SteamWarCI Build successful
2026-04-23 12:11:04 +02:00
c41b1f9daa Refactor i18n configuration and utility functions for improved clarity and simplicity
All checks were successful
SteamWarCI Build successful
2026-04-23 11:53:56 +02:00
6daee3a58b Fix 2026-wgs.md
All checks were successful
SteamWarCI Build successful
2026-04-23 11:09:42 +02:00
4d361415ed Fix 2026-wgs.md
All checks were successful
SteamWarCI Build successful
2026-04-23 11:06:29 +02:00
78259e7263 Fix 2026-wgs.md
All checks were successful
SteamWarCI Build successful
2026-04-23 10:52:10 +02:00
76d9ee9810 Fix 2026-wgs.md
All checks were successful
SteamWarCI Build successful
2026-04-23 10:14:24 +02:00
3b1a491bc2 Fix events
All checks were successful
SteamWarCI Build successful
2026-04-23 08:35:12 +02:00
b357c89dac Remove duplicated title
All checks were successful
SteamWarCI Build successful
2026-04-22 17:51:51 +02:00
f8399e4f31 Fix grammar
All checks were successful
SteamWarCI Build successful
2026-04-22 09:51:57 +02:00
dea500e70d Add timeline.md
All checks were successful
SteamWarCI Build successful
2026-04-22 09:07:47 +02:00
2204bb8663 fixes
Some checks failed
SteamWarCI Build failed
2026-04-21 21:52:02 +02:00
0f20cc0485 Fix link case sensitivity in 2026 Oster-Event announcement
All checks were successful
SteamWarCI Build successful
2026-04-21 21:36:36 +02:00
e44a3f81e4 Add event announcements for Oster-Event 2026 and WarGear Season 2026
All checks were successful
SteamWarCI Build successful
2026-04-21 21:34:31 +02:00
43f42c03c0 Add event announcements for Oster-Event 2026 and WarGear Season 2026 2026-04-21 21:33:31 +02:00
DreamJxnas
931d59565d Übersetzung Brücke und Auflösen von Doppeldeutigkeit in der Werfer-Ansteuerung
All checks were successful
SteamWarCI Build successful
2026-04-19 16:32:29 +02:00
1426536c88 Enhance layout and styling for improved UI consistency across components 2026-03-28 21:53:31 +01:00
5d365bc744 Refactor components and pages for improved readability and consistency
- Updated BackgroundImage.astro to format props for better readability.
- Adjusted FightTable.svelte to remove unnecessary trailing commas.
- Modified GroupTable.svelte to fix sorting syntax.
- Cleaned up LanguageWarning.astro by standardizing import statements.
- Enhanced Login.svelte for better formatting and readability.
- Simplified Navbar.svelte by merging multi-line attributes into single lines.
- Streamlined PostComponent.astro by condensing Image component props.
- Improved SearchComponent.svelte for consistent spacing and formatting.
- Refined TagComponent.astro for better readability and structure.
- Updated PageLayout.astro to simplify div structure.
- Enhanced downloads.astro for improved readability and consistency.
- Cleaned up index.astro in help directory for better formatting.
- Refactored index.astro in main pages for improved readability.
- Standardized login.astro for better formatting.
- Cleaned up not-found.astro for consistent import formatting.
- Enhanced [...schem].astro for better readability.
- Refactored [mode].astro for consistent import formatting.
- Improved [...gamemode].astro for better readability.
- Cleaned up [mode].astro in regeln directory for consistent formatting.
- Refactored index.astro in regeln directory for improved readability.
- Enhanced fight.astro for consistent import formatting.
2026-03-28 15:56:56 +01:00
d2ee422d6d Refactor styles and components for improved UI consistency
- Updated login page layout by removing unnecessary classes.
- Redesigned 404 not found page with new layout and styles.
- Enhanced public pages with consistent font styling and hover effects.
- Improved ranking page header styling for better readability.
- Updated rules page with consistent font and link styles.
- Enhanced statistics page header styling.
- Refined global CSS variables for better theme consistency.
- Updated button styles for improved interaction feedback.
- Enhanced table styles for better readability and interaction.
- Updated Tailwind configuration to include new font families.
2026-03-28 15:56:32 +01:00
48 changed files with 1169 additions and 655 deletions

View File

@@ -47,7 +47,6 @@ export default defineConfig({
configFile: "./tailwind.config.js", configFile: "./tailwind.config.js",
applyBaseStyles: false, applyBaseStyles: false,
}), }),
configureI18n(),
sitemap({ sitemap({
i18n: { i18n: {
defaultLocale: "en", defaultLocale: "en",

View File

@@ -3,7 +3,13 @@ import { Image } from "astro:assets";
import localBau from "@images/90.png"; import localBau from "@images/90.png";
--- ---
<Image src={localBau} alt="Bau" widths={[240, 540, 720, 1080, 1920, localBau.width]} <Image
src={localBau}
alt="Bau"
widths={[240, 540, 720, 1080, 1920, localBau.width]}
sizes={`(max-width: 240px) 240px, (max-width: 540px) 540px, (max-width: 720px) 720px, (max-width: 1080px) 1080px, (max-width: 1920px) 1920px, ${localBau.width}px`} sizes={`(max-width: 240px) 240px, (max-width: 540px) 540px, (max-width: 720px) 720px, (max-width: 1080px) 1080px, (max-width: 1920px) 1920px, ${localBau.width}px`}
class="w-full h-full object-cover rounded-b-2xl shadow-2xl" quality={100} class="w-full h-full object-cover"
draggable="false" loading="eager"/> quality={100}
draggable="false"
loading="eager"
/>

View File

@@ -52,7 +52,7 @@
} }
let { hoverEffect = true, extraClasses = "", children }: Props = $props(); let { hoverEffect = true, extraClasses = "", children }: Props = $props();
let classes = $derived(twMerge("w-72 border-2 border-gray-100 flex flex-col items-center p-8 m-4 rounded-xl shadow-lg bg-zinc-900 dark:border-gray-800 dark:text-gray-100", extraClasses)); let classes = $derived(twMerge("flex flex-col items-center p-8 m-4 bg-[#0c0c0c] border border-[rgba(255,255,255,0.06)] text-gray-100", extraClasses));
</script> </script>
<div class={classes} bind:this={cardElement} onmousemove={rotateElement} onmouseleave={resetElement} class:hoverEffect> <div class={classes} bind:this={cardElement} onmousemove={rotateElement} onmouseleave={resetElement} class:hoverEffect>
@@ -67,10 +67,13 @@
:global(h1) { :global(h1) {
@apply text-xl font-bold mt-4; @apply text-xl font-bold mt-4;
font-family: "Barlow Condensed", sans-serif;
letter-spacing: 0.06em;
} }
:global(svg) { :global(svg) {
@apply transition-transform duration-300 ease-in-out hover:scale-110 hover:drop-shadow-2xl; color: #f59e0b;
@apply transition-transform duration-300 ease-in-out hover:scale-110;
} }
} }

View File

@@ -44,7 +44,7 @@
} }
</script> </script>
<div class="p-3 bg-gray-200 dark:bg-neutral-800 rounded-2xl w-3/4 mx-auto"> <div class="p-3 bg-[#0c0c0c] border border-[rgba(255,255,255,0.06)] w-3/4 mx-auto">
<table> <table>
<thead> <thead>
<tr class="font-bold border-b"> <tr class="font-bold border-b">
@@ -57,7 +57,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each window( event.fights.filter((f) => (group === undefined ? true : f.group?.id === group)), rows ) as fights} {#each window( event.fights.filter((f) => (group === undefined ? true : f.group?.id === group)), rows, ) as fights}
<tr> <tr>
{#each fights as fight (fight.id)} {#each fights as fight (fight.id)}
<td <td

View File

@@ -40,11 +40,11 @@
team: event.teams.find((t) => t.id === Number(teamId))!!, team: event.teams.find((t) => t.id === Number(teamId))!!,
points: points, points: points,
})) }))
.sort((a, b) => b.points - a.points) .sort((a, b) => b.points - a.points),
); );
</script> </script>
<div class="p-3 bg-gray-200 dark:bg-neutral-800 rounded-2xl w-3/4 mx-auto"> <div class="p-3 bg-[#0c0c0c] border border-[rgba(255,255,255,0.06)] w-3/4 mx-auto">
<table class="w-full"> <table class="w-full">
<thead> <thead>
<tr class="font-bold border-b"> <tr class="font-bold border-b">

View File

@@ -1,8 +1,8 @@
--- ---
import {t} from "astro-i18n"; import { t } from "astro-i18n";
--- ---
<div class="bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 p-4" role="alert"> <div class="border-l-2 border-amber-500 bg-amber-500/5 p-4" role="alert">
<p class="font-bold">{t("warning.title")}</p> <p class="font-bold text-amber-400" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.1em; text-transform: uppercase;">{t("warning.title")}</p>
<p>{t("warning.text")}</p> <p class="text-gray-400 text-sm">{t("warning.text")}</p>
</div> </div>

View File

@@ -74,8 +74,8 @@
}); });
</script> </script>
<form class="bg-gray-100 dark:bg-neutral-900 p-12 rounded-2xl shadow-2xl border-2 border-gray-600 flex flex-col" onsubmit={preventDefault(login)}> <form class="sw-login-form" onsubmit={preventDefault(login)}>
<h1 class="text-4xl text-white text-center">{t("login.title")}</h1> <h1 class="text-4xl text-white text-center" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.08em;">{t("login.title")}</h1>
<div class="ml-2 flex flex-col"> <div class="ml-2 flex flex-col">
<label for="username">{t("login.label.username")}</label> <label for="username">{t("login.label.username")}</label>
<input type="text" id="username" name="username" placeholder={t("login.placeholder.username")} bind:value={username} /> <input type="text" id="username" name="username" placeholder={t("login.placeholder.username")} bind:value={username} />
@@ -83,24 +83,70 @@
<input type="password" id="password" name="password" placeholder={t("login.placeholder.password")} bind:value={pw} /> <input type="password" id="password" name="password" placeholder={t("login.placeholder.password")} bind:value={pw} />
</div> </div>
<p class="mt-2"> <p class="mt-2">
<a class="text-neutral-500 hover:underline" href={l("/set-password")}>{t("login.setPassword")}</a> <a class="sw-link" href={l("/set-password")}>{t("login.setPassword")}</a>
</p> </p>
{#if error} {#if error}
<p class="mt-2 text-red-500">{error}</p> <p class="mt-2 text-red-500">{error}</p>
{/if} {/if}
<button class="btn mt-4 !mx-0 justify-center" type="submit" onclick={preventDefault(login)}>{t("login.submit")}</button> <button class="btn mt-4 justify-center w-full" type="submit" onclick={preventDefault(login)}>{t("login.submit")}</button>
<a class="btn mt-4 !mx-0 justify-center" href="https://discord.com/oauth2/authorize?client_id=869606970099904562&response_type=token&redirect_uri=https%3A%2F%2Fsteamwar.de%2Flogin&scope=identify"> <a
class="btn mt-4 justify-center w-full"
href="https://discord.com/oauth2/authorize?client_id=869606970099904562&response_type=token&redirect_uri=https%3A%2F%2Fsteamwar.de%2Flogin&scope=identify"
>
{t("login.discord")} {t("login.discord")}
</a> </a>
</form> </form>
<style lang="postcss"> <style lang="postcss">
.sw-login-form {
background: rgba(12, 12, 12, 0.95);
border: 1px solid rgba(255, 255, 255, 0.06);
border-top: 2px solid #f59e0b;
backdrop-filter: blur(24px);
padding: 3rem;
display: flex;
flex-direction: column;
}
input { input {
@apply border-2 rounded-md p-2 shadow-2xl w-80 dark:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:border-transparent text-black; width: 20rem;
padding: 0.6rem 0.8rem;
margin-top: 0.25rem;
margin-bottom: 0.5rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
color: #f5f5f5;
font-size: 0.85rem;
outline: none;
transition: border-color 0.2s ease;
}
input:focus {
border-color: rgba(245, 158, 11, 0.5);
} }
label { label {
@apply text-neutral-300; font-family: "Barlow Condensed", sans-serif;
font-size: 0.7rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: rgba(163, 163, 163, 0.7);
margin-top: 0.5rem;
}
.sw-link {
color: rgba(163, 163, 163, 0.5);
text-decoration: none;
font-size: 0.85rem;
border-bottom: 1px solid transparent;
transition:
color 0.2s,
border-color 0.2s;
}
.sw-link:hover {
color: #f59e0b;
border-bottom-color: #f59e0b;
} }
</style> </style>

View File

@@ -19,11 +19,11 @@
<script lang="ts"> <script lang="ts">
import "../styles/button.css"; import "../styles/button.css";
import { CaretDownOutline, SearchOutline } from "flowbite-svelte-icons"; import { CaretDownOutline, GlobeOutline } from "flowbite-svelte-icons";
import { t } from "astro-i18n"; import { t, l } from "astro-i18n";
import { l } from "../util/util";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { loggedIn } from "@repo/authv2.ts"; import { loggedIn } from "@repo/authv2.ts";
import { astroI18n } from "astro-i18n";
interface Props { interface Props {
logo?: import("svelte").Snippet; logo?: import("svelte").Snippet;
} }
@@ -35,6 +35,8 @@
let accountBtn = $state<HTMLAnchorElement>(); let accountBtn = $state<HTMLAnchorElement>();
let currentPage = $state(astroI18n.route);
$effect(() => { $effect(() => {
if ($loggedIn) { if ($loggedIn) {
accountBtn!.href = l("/dashboard"); accountBtn!.href = l("/dashboard");
@@ -45,33 +47,34 @@
onMount(() => { onMount(() => {
handleScroll(); handleScroll();
document.addEventListener("astro:page-load", () => {
astroI18n.route = location.pathname;
currentPage = astroI18n.route;
});
}); });
function handleScroll() { function handleScroll() {
if (window.scrollY > 0) { if (window.scrollY > 0) {
navbar!.classList.add("before:scale-y-100"); navbar!.classList.add("sw-nav-scrolled");
} else { } else {
navbar!.classList.remove("before:scale-y-100"); navbar!.classList.remove("sw-nav-scrolled");
} }
} }
</script> </script>
<svelte:window onscroll={handleScroll} /> <svelte:window onscroll={handleScroll} />
<nav <nav data-pagefind-ignore class="sw-nav z-20 fixed top-0 left-0 right-0 sm:px-4 py-1 transition-colors flex justify-center" bind:this={navbar}>
data-pagefind-ignore
class="z-20 fixed top-0 left-0 right-0 sm:px-4 py-1 transition-colors flex justify-center before:backdrop-blur before:shadow-2xl before:absolute before:top-0 before:left-0 before:bottom-0 before:right-0 before:-z-10 before:scale-y-0 before:transition-transform before:origin-top"
bind:this={navbar}
>
<div class="flex flex-row items-center justify-evenly md:justify-between match"> <div class="flex flex-row items-center justify-evenly md:justify-between match">
<a class="flex items-center" href={l("/")}> <a class="flex items-center" href={l("/")}>
{@render logo?.()} {@render logo?.()}
<span class="text-2xl uppercase font-bold text-white hidden md:inline-block"> <span class="sw-nav-title hidden md:inline-block">
{t("navbar.title")} {t("navbar.title")}
<span class="before:scale-y-100" style="display: none" aria-hidden="true"></span> <span class="scrolled-trigger" style="display: none" aria-hidden="true"></span>
</span> </span>
</a> </a>
<div class="flex justify-center flex-wrap"> <div class="flex justify-center flex-wrap gap-2">
<div class="btn-dropdown"> <div class="btn-dropdown">
<button class="btn btn-gray"> <button class="btn btn-gray">
<a href={l("/")}> <a href={l("/")}>
@@ -104,8 +107,6 @@
<a href={l("/rules/megawargear")} class="btn btn-gray">{t("navbar.links.rules.megawg")}</a> <a href={l("/rules/megawargear")} class="btn btn-gray">{t("navbar.links.rules.megawg")}</a>
<a href={l("/rules/microwargear")} class="btn btn-gray">{t("navbar.links.rules.micro")}</a> <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> <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>
</div> </div>
<!-- TODO: Add help center <!-- TODO: Add help center
@@ -125,11 +126,21 @@
<a class="btn" href={l("/login")} bind:this={accountBtn}> <a class="btn" href={l("/login")} bind:this={accountBtn}>
<span class="btn__text">{t("navbar.links.account")}</span> <span class="btn__text">{t("navbar.links.account")}</span>
</a> </a>
<!-- <div class="btn-dropdown">
<button class="btn my-1" onclick={() => searchOpen = true}> <button class="btn btn-gray">
<SearchOutline ariaLabel="Site Search" class="inline-block h-6"/> <GlobeOutline />
</button> </button>
--> <div>
<a
data-astro-reload
href={l(currentPage, {}, { targetLocale: typeof navigator !== "undefined" ? navigator.language.split("-")[0] : "de" })}
onclick={() => cookieStore.delete("MANUAL_LANGUAGE")}
class="btn btn-gray">Auto</a
>
<a data-astro-reload href={l(currentPage, {}, { targetLocale: "de" })} onclick={() => cookieStore.set("MANUAL_LANGUAGE", "TRUE")} class="btn btn-gray">Deutsch</a>
<a data-astro-reload href={l(currentPage, {}, { targetLocale: "en" })} onclick={() => cookieStore.set("MANUAL_LANGUAGE", "TRUE")} class="btn btn-gray">English</a>
</div>
</div>
</div> </div>
</div> </div>
</nav> </nav>
@@ -144,4 +155,29 @@
.match { .match {
width: min(100vw, 70em); width: min(100vw, 70em);
} }
:global(.sw-nav) {
backdrop-filter: none;
background: transparent;
transition:
background 0.3s ease,
backdrop-filter 0.3s ease,
border-color 0.3s ease;
border-bottom: 1px solid transparent;
}
:global(.sw-nav-scrolled) {
background: rgba(8, 8, 8, 0.85) !important;
backdrop-filter: blur(16px) !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.04) !important;
}
.sw-nav-title {
font-family: "Barlow Condensed", sans-serif;
font-size: 1.3rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: #f5f5f5;
}
</style> </style>

View File

@@ -22,26 +22,21 @@ const {
const postUrl = l(`/announcements/${post.slug.split("/").slice(1).join("/")}`); const postUrl = l(`/announcements/${post.slug.split("/").slice(1).join("/")}`);
--- ---
<Card extraClasses={`w-full items-start mx-0 ${slim ? "m-0 p-1 backdrop-blur-xl bg-transparent" : ""}`} hoverEffect={false}> <Card extraClasses={`w-full items-start mx-0 ${slim ? "m-0 p-2 backdrop-blur-xl bg-transparent border-0" : "border-t-2 border-t-amber-500/30"}`} hoverEffect={false}>
<div class={`flex flex-row ${slim ? "" : "p-4"}`}> <div class={`flex flex-row ${slim ? "" : "p-4"}`}>
{ {
post.data.image != null ? ( post.data.image != null ? (
<a href={postUrl}> <a href={postUrl}>
<div class="flex-shrink-0 pr-2"> <div class="flex-shrink-0 pr-2">
<Image <Image transition:name={post.data.title + "-image"} src={post.data.image} alt="Post Image" class="object-cover h-32 w-32 max-w-none transition-transform hover:scale-105" />
transition:name={post.data.title + "-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> </div>
</a> </a>
) : null ) : null
} }
<div> <div>
<a href={postUrl} class="flex flex-col items-start"> <a href={postUrl} class="flex flex-col items-start">
<h2 class="text-2xl font-bold" transition:name={post.data.title + "-title"}>{post.data.title}</h2> <h2 class="text-2xl font-bold" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.04em;" transition:name={post.data.title + "-title"}>{post.data.title}</h2>
<P class="text-gray-500" <P class="text-gray-500 text-sm"
>{ >{
Intl.DateTimeFormat(astroI18n.locale, { Intl.DateTimeFormat(astroI18n.locale, {
day: "numeric", day: "numeric",

View File

@@ -18,12 +18,11 @@
--> -->
<script lang="ts"> <script lang="ts">
import { slide, fade } from "svelte/transition";
import {slide, fade} from "svelte/transition"; import { onMount } from "svelte";
import {onMount} from "svelte"; import { importPagefind, type Pagefind, type PagefindDocument } from "@type/pagefind.js";
import {importPagefind, type Pagefind, type PagefindDocument} from "@type/pagefind.js";
import Card from "@components/Card.svelte"; import Card from "@components/Card.svelte";
import {l} from "@utils/util.ts"; import { l } from "@utils/util.ts";
let pagefind: Pagefind; let pagefind: Pagefind;
onMount(async () => { onMount(async () => {
@@ -36,27 +35,26 @@
async function search(e: KeyboardEvent) { async function search(e: KeyboardEvent) {
if (e.target instanceof HTMLInputElement) { if (e.target instanceof HTMLInputElement) {
let search: {results: any[]} = await pagefind.debouncedSearch(e.target.value); let search: { results: any[] } = await pagefind.debouncedSearch(e.target.value);
results = await Promise.all(search.results.slice(0, 10).map(value => value.data())) results = await Promise.all(search.results.slice(0, 10).map((value) => value.data()));
} }
} }
interface Props { interface Props {
open?: boolean; open?: boolean;
} }
let { open = $bindable(false) }: Props = $props(); let { open = $bindable(false) }: Props = $props();
</script> </script>
<button transition:fade class="fixed top-0 left-0 w-screen h-screen backdrop-blur z-20 cursor-default" onclick={() => open = false}> <button transition:fade class="fixed top-0 left-0 w-screen h-screen bg-black/60 backdrop-blur-sm z-20 cursor-default" onclick={() => (open = false)}> </button>
</button> <div transition:slide style="width: min(100%, 75em);" class="fixed top-0 left-1/2 -translate-x-1/2 h-2/3 z-30 p-4 text-white flex flex-col sw-search-panel">
<div transition:slide style="width: min(100%, 75em);" class="fixed top-0 left-1/2 -translate-x-1/2 h-2/3 dark:bg-zinc-900 rounded-b-2xl shadow-2xl z-30 p-4 text-white flex flex-col"> <input placeholder="Search..." onkeypress={search} />
<input placeholder="Search..." onkeypress={search}>
<div class="overflow-y-scroll flex-1 w-full mt-2 rounded-2xl"> <div class="overflow-y-scroll flex-1 w-full mt-2">
{#each results as result} {#each results as result}
<Card extraClasses="w-full m-0 my-2" hoverEffect={false}> <Card extraClasses="w-full m-0 my-2 border-t-2 border-t-amber-500/30" hoverEffect={false}>
<a class="grid grid-cols-3" href={l(result.url)}> <a class="grid grid-cols-3" href={l(result.url)}>
<h1>{result.meta.title}</h1> <h1>{result.meta.title}</h1>
{#each result.sub_results.slice(0, 2) as sub_result} {#each result.sub_results.slice(0, 2) as sub_result}
@@ -69,13 +67,28 @@
</div> </div>
<style lang="postcss"> <style lang="postcss">
.sw-search-panel {
background: rgba(8, 8, 8, 0.95);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
backdrop-filter: blur(24px);
}
input { input {
@apply border-2 rounded-md p-2 shadow-2xl w-full width: 100%;
dark:bg-neutral-800 padding: 0.7rem 1rem;
focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:border-transparent; background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
color: #f5f5f5;
font-size: 0.9rem;
outline: none;
transition: border-color 0.2s ease;
}
input:focus {
border-color: rgba(245, 158, 11, 0.5);
} }
label { label {
@apply text-neutral-300; color: rgba(163, 163, 163, 0.7);
} }
</style> </style>

View File

@@ -1,22 +1,31 @@
--- ---
import { l } from "../util/util";
import {l} from "../util/util"; import { capitalize } from "./admin/util";
import {capitalize} from "./admin/util";
interface Props { interface Props {
tag: string; tag: string;
noLink?: boolean; noLink?: boolean;
} }
const {tag, noLink} = Astro.props; const { tag, noLink } = Astro.props;
--- ---
{noLink {
? ( noLink ? (
<span class="inline-block bg-gray-800 rounded-full px-3 py-1 text-sm font-semibold text-white mr-2 shadow-2xl">{capitalize(tag)}</span> <span
) class="inline-block bg-transparent border border-amber-500/30 px-3 py-0.5 text-xs font-semibold text-amber-400 mr-2 uppercase tracking-wider"
: ( style="font-family: 'Barlow Condensed', sans-serif;"
>
{capitalize(tag)}
</span>
) : (
<a href={l(`/announcements/tags/${tag}`)}> <a href={l(`/announcements/tags/${tag}`)}>
<span class="inline-block bg-gray-800 rounded-full px-3 py-1 text-sm font-semibold text-white mr-2 shadow-2xl">{capitalize(tag)}</span> <span
class="inline-block bg-transparent border border-amber-500/30 px-3 py-0.5 text-xs font-semibold text-amber-400 mr-2 uppercase tracking-wider hover:border-amber-400 hover:text-amber-300 transition-colors"
style="font-family: 'Barlow Condensed', sans-serif;"
>
{capitalize(tag)}
</span>
</a> </a>
)} )
}

View File

@@ -52,7 +52,7 @@
/> />
</figure> </figure>
</Card> </Card>
<div class="flex flex-wrap"> <div class="flex flex-wrap gap-2">
<button class="btn mt-2" onclick={logout}>{t("dashboard.buttons.logout")}</button> <button class="btn mt-2" onclick={logout}>{t("dashboard.buttons.logout")}</button>
{#if user.perms.includes("MODERATION")} {#if user.perms.includes("MODERATION")}
<a class="btn w-fit mt-2" href="/admin/new" data-astro-reload>{t("dashboard.buttons.admin")}</a> <a class="btn w-fit mt-2" href="/admin/new" data-astro-reload>{t("dashboard.buttons.admin")}</a>

View File

@@ -13,7 +13,7 @@
</script> </script>
<div class="flex flex-col gap-1 {unsized ? '' : 'w-72 m-4'}"> <div class="flex flex-col gap-1 {unsized ? '' : 'w-72 m-4'}">
<div class="bg-gray-100 text-black font-bold px-2 rounded uppercase"> <div class="bg-amber-500 text-black font-bold px-2 uppercase text-xs tracking-wider" style="font-family: 'Barlow Condensed', sans-serif;">
{title} {title}
</div> </div>
<div class="border border-gray-600 rounded p-2 flex flex-col gap-2 bg-slate-900"> <div class="border border-gray-600 rounded p-2 flex flex-col gap-2 bg-slate-900">

View File

@@ -18,36 +18,21 @@
--> -->
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy'; import { preventDefault } from "svelte/legacy";
import { import { ChevronDoubleLeftOutline, ChevronDoubleRightOutline, ChevronLeftOutline, ChevronRightOutline } from "flowbite-svelte-icons";
ChevronDoubleLeftOutline,
ChevronDoubleRightOutline,
ChevronLeftOutline,
ChevronRightOutline,
} from "flowbite-svelte-icons";
interface Props {
page?: number;
maxPage: number;
firstUrl?: string;
lastUrl?: string;
previousUrl?: string;
nextUrl?: string;
pagesUrl?: (i: number) => string;
}
let { page = $bindable(0), maxPage, firstUrl = "#", lastUrl = "#", previousUrl = "#", nextUrl = "#", pagesUrl = () => "#" }: Props = $props();
interface Props {
page?: number;
maxPage: number;
firstUrl?: string;
lastUrl?: string;
previousUrl?: string;
nextUrl?: string;
pagesUrl?: (i: number) => string;
}
let {
page = $bindable(0),
maxPage,
firstUrl = "#",
lastUrl = "#",
previousUrl = "#",
nextUrl = "#",
pagesUrl = () => "#"
}: Props = $props();
const previous = () => { const previous = () => {
page = Math.max(page - 1, 0); page = Math.max(page - 1, 0);
@@ -56,20 +41,23 @@
const next = () => { const next = () => {
page = Math.min(page + 1, maxPage - 1); page = Math.min(page + 1, maxPage - 1);
}; };
let pages = $derived(new Array(maxPage).fill(0) let pages = $derived(
.map((_, i) => i + 1) new Array(maxPage)
//.slice(Math.max(page - 2, 0) - Math.abs(Math.max(page + 3 - maxPage, 0)), Math.min(page + 3, maxPage) + Math.abs(Math.min(page - 2, 0))) .fill(0)
.map(i => ({ .map((_, i) => i + 1)
name: i.toString(), //.slice(Math.max(page - 2, 0) - Math.abs(Math.max(page + 3 - maxPage, 0)), Math.min(page + 3, maxPage) + Math.abs(Math.min(page - 2, 0)))
active: i === page + 1, .map((i) => ({
i: i - 1 name: i.toString(),
}))); active: i === page + 1,
i: i - 1,
})),
);
</script> </script>
<div class="w-full flex justify-center mt-4"> <div class="w-full flex justify-center mt-4">
<ul class="inline-flex flex-wrap"> <ul class="inline-flex flex-wrap gap-1">
<li> <li>
<a href={firstUrl} onclick={preventDefault(() => page = 0)} class="btn btn-neutral h-8 px-3 text-sm flex items-center !m-0 !rounded-r-none"> <a href={firstUrl} onclick={preventDefault(() => (page = 0))} class="btn btn-neutral h-8 px-3 text-sm flex items-center !m-0 !rounded-r-none">
<span class="sr-only">Next</span> <span class="sr-only">Next</span>
<ChevronDoubleLeftOutline class="w-3 h-3" /> <ChevronDoubleLeftOutline class="w-3 h-3" />
</a> </a>
@@ -82,7 +70,7 @@
</li> </li>
{#each pages as p} {#each pages as p}
<li> <li>
<a href={pagesUrl(p.i)} onclick={preventDefault(() => page = p.i)} class="btn h-8 px-3 text-sm flex items-center !m-0 !rounded-none" class:btn-neutral={!p.active}> <a href={pagesUrl(p.i)} onclick={preventDefault(() => (page = p.i))} class="btn h-8 px-3 text-sm flex items-center !m-0 !rounded-none" class:btn-neutral={!p.active}>
<span>{p.name}</span> <span>{p.name}</span>
</a> </a>
</li> </li>
@@ -94,7 +82,7 @@
</a> </a>
</li> </li>
<li> <li>
<a href={lastUrl} onclick={preventDefault(() => page = maxPage - 1)} class="btn btn-neutral h-8 px-3 text-sm flex items-center !m-0 !rounded-l-none"> <a href={lastUrl} onclick={preventDefault(() => (page = maxPage - 1))} class="btn btn-neutral h-8 px-3 text-sm flex items-center !m-0 !rounded-l-none">
<span class="sr-only">Next</span> <span class="sr-only">Next</span>
<ChevronDoubleRightOutline class="w-3 h-3" /> <ChevronDoubleRightOutline class="w-3 h-3" />
</a> </a>

View File

@@ -4,10 +4,14 @@ key: 2026-easter_event
description: Das Oster Event 2026 steht an. Wir freuen uns auf Eure Teilnahme! description: Das Oster Event 2026 steht an. Wir freuen uns auf Eure Teilnahme!
created: 2026-03-10T00:00:00.000Z created: 2026-03-10T00:00:00.000Z
tags: tags:
- event - event
- warship - warship
--- ---
## Diese Version ist nicht die Aktuelle!
Bite schaue [hier](/events/2026-osterevent)!
Ahoi Matrosen, Ahoi Matrosen,
der Winter zieht sich langsam zurück, die ersten Blumen kämpfen sich durch den Schnee und auch auf den Werften von SteamWar beginnt wieder geschäftiges Treiben. der Winter zieht sich langsam zurück, die ersten Blumen kämpfen sich durch den Schnee und auch auf den Werften von SteamWar beginnt wieder geschäftiges Treiben.

View File

@@ -4,16 +4,21 @@ key: 2026-wgs
description: Die WarGear Season 2026 steht an. Wir freuen uns auf Eure Teilnahme! description: Die WarGear Season 2026 steht an. Wir freuen uns auf Eure Teilnahme!
created: 2026-02-18T00:00:00.000Z created: 2026-02-18T00:00:00.000Z
tags: tags:
- event - event
- wargear - wargear
--- ---
# Diese Version ist nicht die Aktuelle!
Bite schaue [hier](/events/2026-wgs)!
# WarGear Season 2026 # WarGear Season 2026
Die WarGear Season 2026 steht bevor! Die WarGear Season 2026 steht bevor!
Bereitet eure Teams vor und stellt euch einer kompetitiven Season mit klaren Regeln und fairen Bedingungen. Bereitet eure Teams vor und stellt euch einer kompetitiven Season mit klaren Regeln und fairen Bedingungen.
## Allgemeine Infos ## Allgemeine Infos
Version: 1.21.6 Version: 1.21.6
WarGear Schem Type: ??? WarGear Schem Type: ???
Anzahl Teams: ∞ Anzahl Teams: ∞
@@ -24,16 +29,19 @@ Erster Spieltag: 02.05.2026
Das Turnierformat wird zwischen dem 25.04.2026 und dem 30.04.2026 entschieden. Das Turnierformat wird zwischen dem 25.04.2026 und dem 30.04.2026 entschieden.
## Teams ## Teams
Fighter pro Team: 6 Fighter pro Team: 6
Spectate System: ja Spectate System: ja
Gebannte Spieler: bleiben gebannt Gebannte Spieler: bleiben gebannt
## Schematic ## Schematic
Hotfixes erlaubt: ja (bis max. 4 Stunden vor dem ersten Fight des Tages) Hotfixes erlaubt: ja (bis max. 4 Stunden vor dem ersten Fight des Tages)
Schematic wird während der WGS an den User SteamWar gebunden. Schematic wird während der WGS an den User SteamWar gebunden.
Regelwerk: WG21 ohne SFA, Windcharges zum komprimieren erlaubt (Regelwerk folgt in kürze) Regelwerk: WG21 ohne SFA, Windcharges zum komprimieren erlaubt (Regelwerk folgt in kürze)
## Challenge System ## Challenge System
Am 25.04.2026 wird die Anzahl Teams auf die wir die WGS auslegen festgelegt. Alle dann angemeldeten Am 25.04.2026 wird die Anzahl Teams auf die wir die WGS auslegen festgelegt. Alle dann angemeldeten
Teams kommen auf jeden Fall rein. Alle Teams die sich darüber hinaus bis zum 30.04.2026 anmelden rücken Teams kommen auf jeden Fall rein. Alle Teams die sich darüber hinaus bis zum 30.04.2026 anmelden rücken
bei Abmeldung nach oder können pro Spieltag/Wochenende das/die Team(s) auf dem letzten Platz herausfordern bei Abmeldung nach oder können pro Spieltag/Wochenende das/die Team(s) auf dem letzten Platz herausfordern

View File

@@ -1,9 +0,0 @@
{
"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",
"Download": "https://git.steamwar.de/SteamWar/SteamWarTeamserverIntegration/releases/download/latest/SteamWarTeamserverIntegration.jar"
},
"sourceUrl": "https://git.steamwar.de/SteamWar/SteamWarTeamserverIntegration"
}

View File

@@ -0,0 +1,48 @@
---
eventId: 78
mode: "warship"
verwantwortlicher: "ItonaCA"
viewConfig:
groups:
name: Gruppenphase
view:
type: "GROUP"
groups: [14]
elim:
name: Finale
view:
type: "ELEMINATION"
finalFight: 1661
---
Ahoi Matrosen,
der Winter zieht sich langsam zurück, die ersten Blumen kämpfen sich durch den Schnee und auch auf den Werften von SteamWar beginnt wieder geschäftiges Treiben.
Zu Ostern laden wir euch zu einem besonderen Warship-Event ein!
Packt eure Werkzeuge aus, bringt eure Kanonen auf Hochglanz und bereitet eure Schiffe für den Kampf vor.
Am 25. und 26. April treffen sich die Kapitäne der Flotte, um ihre neuesten Kreationen auf dem Schlachtfeld zu testen. Egal ob altgedienter Konstrukteur oder neuer Schiffsbauer jeder ist willkommen, seine Ideen zu Wasser zu lassen.
Ein Osterdesign ist gerne gesehen, aber keine Pflicht. Wer also sein Schiff mit Eiern, Frühlingsfarben oder kleinen Überraschungen schmücken möchte, darf seiner Kreativität freien Lauf lassen.
Termin
26. April
Version
Gespielt wird auf dem aktuellen 1.21 RW
Was euch erwartet
spannende Warship-Gefechte
neue Konstruktionen und kreative Designs
gemeinsames Event-Wochenende mit der Community
jede Menge Explosionen und epische Schlachten
Also: Werft die Maschinen an, hisst die Flaggen und macht eure Schiffe bereit.
Wir freuen uns auf ein spannendes Osterwochenende mit euch!

View File

@@ -0,0 +1,44 @@
---
eventId: 77
mode: "wargear"
verwantwortlicher: "JajaKings & YoyoNow"
---
# WarGear Season 2026
Die WarGear Season 2026 steht bevor!
Bereitet eure Teams vor und stellt euch einer kompetitiven Season mit klaren Regeln und fairen Bedingungen.
## Allgemeine Infos
Version: 1.21.6
WarGear Schem Type: WarGearSeason26 oder WGS26
Anzahl Teams: ∞
Anmeldeschluss: 30.04.2026 (Anzahl Teams wird am 25.04.2026 entschieden)
Einsendeschluss: 07.05.2026 (Ab dann nur noch Public Schematic wählbar)
Erster Spieltag: 02.05.2026
Das Turnierformat wird zwischen dem 25.04.2026 und dem 30.04.2026 entschieden.
## Teams
Fighter pro Team: 6
Spectate System: ja
Gebannte Spieler: bleiben gebannt
## Schematic
Hotfixes erlaubt: ja (bis max. 4 Stunden vor dem ersten Fight des Tages)
Schematic wird während der WGS an den User SteamWar gebunden.
Regelwerk: WG21 ohne SFA, Windcharges zum komprimieren erlaubt (Regelwerk folgt in kürze)
## Challenge System
Am 25.04.2026 wird die Anzahl Teams auf die wir die WGS auslegen festgelegt. Alle dann angemeldeten
Teams kommen auf jeden Fall rein. Alle Teams die sich darüber hinaus bis zum 30.04.2026 anmelden rücken
bei Abmeldung nach oder können pro Spieltag/Wochenende das/die Team(s) auf dem letzten Platz herausfordern
und bei erfolgreicher Herausforderung in die WGS eintreten.
Wenn ein Team **UNANGEKUNDIGT** an einem Spieltag fehlt, wird es durch ein Team welches nicht mehr in die WGS
gekommen ist ersetzt. Das rausgeflogene Team kann sich nicht mehr durch eine Herausforderung in die WGS
reinkämpfen.

View File

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

View File

@@ -1,5 +1,5 @@
{ {
"translationKey": "microwg", "translationKey": "microwg",
"main": false, "main": false,
"ranked": true "ranked": false
} }

View File

@@ -1,5 +1,5 @@
{ {
"translationKey": "mwg", "translationKey": "mwg",
"main": true, "main": true,
"ranked": true "ranked": false
} }

View File

@@ -1,5 +1,5 @@
{ {
"translationKey": "mw", "translationKey": "mw",
"main": false, "main": false,
"ranked": true "ranked": false
} }

View File

@@ -1,5 +1,5 @@
{ {
"translationKey": "wg", "translationKey": "wg",
"main": true, "main": true,
"ranked": true "ranked": false
} }

View File

@@ -0,0 +1,130 @@
---
title: Timeline
description: Timeline
slug: timeline
slugs:
en: timeline
# First Trail
# HullHider
# Lobby:
# - Flugschiff
# - Vulkan
# - Jetzige Lobby
---
### Dynamic Bau
Aktuell im Bau befindet sich der Dynamic Bau, womit der BauServer selbst gestaltet werden kann.
### WarGear AI
Aktuell im Bau befindet sich eine WarGear KI, gegen die man anstelle von Spielern Kämpfen kann.
## 500.000 Spielstunden
Die ersten 500.000 Spielstunden wurden am 12.05.2025 geknackt.
## 100.000 Replays
Am 23.02.2025 wurde das 100.000 replay gespeichert.
## Server Umzug
Als das Development am 12.01.2025 aufsteht wurde festgestellt, dass man nicht mehr auf SteamWar spielen kann.
Der Velocity (Proxy Server) ist in der Nacht auf den 12.01.2025 ausgefallen.
Dieser wurde kurzerhand versucht neuzustarten um festzustellen, dass aufgrund einer fehlerhaften Festplatte dieser nicht mehr startet.
Kurzerhand wurde ein letztes Backup von der Datenbank und dem Server gemacht, sodass alle Daten weiter bestehen bleiben.
Nach kurzen internen Überlegungen wurde ein neuer Server gemietet und SteamWar neu aufgesetzt.
Am 14.01.2025 um 20:43 war SteamWar unter der Domain `dampfkrieg.de` wieder erreichbar.
Einige Funktionen blieben bis zum 20.01.2025 deaktiviert.
An diesem Tag wurde die postalisch umgezogene Domain `steamwar.de` wieder aktiviert und eine neue Webseite released.
## Geburtstag von SteamWar
Am 18.04.2024 wurde SteamWar 5 Jahre alt.
## 10.000 players
Am 25.01.2024 ist der 10.000 Spieler das erste mal auf SteamWar gejoint.
## 100.000 Fights
Am 10.04.2023 wurde der 100.000 Fight ausgetragen.
Es wurden 44585 MiniWarGear Fights, 36001 WarGear Fights und 7622 WarShip Fights ausgetragen.
Hinzukommen noch 6647 AirShip Fights und 3223 MicroWarGear Fights.
Die restlichen Kämpfe teilen sich auf 35er WarGear, MegaWarGear, MPP WarGear und Sea-Giants WarShip auf.
## 10.000 replays
Am 16.03.2022 wurde das 10.000 replay in der Datenbank gespeichert.
Die Replay Tabelle war schon seit einiger Zeit die größte Tabelle der Datenbank.
## Datenverlust
Einige Bauweltmember, ignorierte Spieler, Kits und Bau-GUI Einstellungen wurden am 31.12.2021 wegen des Laden eines Backups gelöscht.
Insgesamt waren die Daten eines Zeitraumes von ca 3,5 Monaten betroffen, welche teilweise verloren gingen.
## 10.000 Schematics
Am 20.11.2022 wurde die 10.000 Schematic erstellt.
## Replay System
Am 22.08.2021 wurde das erste Replay gespeichert.
Das Replay System nimmt ab dann jeden Fight auf, welche bis heute noch abgespielt werden können.
## TheJoCraft Videos
TheJoCraft hat zwischen dem 18.06.2021 und dem 22.06.2021 4 Videos über WarGears auf YouTube hochgeladen.
Innerhalb dieser 4 Tage spielten 1090 Spieler auf SteamWar.
Davon waren 706 neue Spieler, die das erste mal gejoint sind.
Knapp unter 3390 Stunden wurden auf StemWar während diesen Tagen gespielt.
Am 21.06.2021 sind mit 217 Spielern die meisten neuen Spieler das erste mal SteamWar gejoint.
## Erster Simulator
Am 07.03.2021 wurde der erste öffentliche Simulator auf SteamWar released.
## WarGear Season
Zwischen dem 05.03.2021 und dem 10.07.2021 fand das erste Langzeit Event, die WarGearSeason, statt.
In 77 Fights haben sich 15 Teams duelliert um zu entscheiden, wer die bessere Technik und Taktik hat.
Insgesamt haben 43 Spieler an der ersten WGS teilgenommen.
## Bau Design
Am 19.01.2021 wurde die neue Bauwelt released.
Diese wurde in den Wochen davor von Miny___ und der Hilfe von YoyoNow gebaut und eingerichtet.
## 100.000 Spielstunden
Die ersten 100.000 Spielstunden wurden am 14.01.2021 geknackt.
## WarGear Liga
Die erste und einzige WarGear Liga wurde zwischen dem 09.11.2022 und 15.11.2020 ausgetragen.
Es fanden 29 Kämpfe zwischen 12 Teams statt.
29 Kämpfer haben sich in den Fights duelliert.
## 10.000 Fights
Am 11.06.2020 wurde der 10.000 Fight ausgetragen.
Dabei wurden 6186 MiniWarGear fights, 1583 WarGear fights und 671 35er WarGear fights ausgetragen.
Die restlichen Fights wurden in WarShip, AirShip, MPP WarGear und SeaGiants WarShip gespielt.
## 10.000 Spielstunden
Die ersten 10.000 Spielstunden wurden am 22.02.2020 geknackt.
## Erstes Event
Das erste Event, namens PublicClash2019, wurde am 14.09.2019 veranstaltet.
Mit 4 angemeldeten Teams wurde über 15 Kämpfen entschieden wer der Sieger ist.
Das Event wurde in MiniWarGear, WarGear, WarShip und AirShip ausgetragen.
Insgesamt haben 14 Spieler in den Kämpfen teilgenommen.
## Gründung
SteamWar wird am 18.05.2019 von AdmiralSeekrank gegründet.

View File

@@ -56,7 +56,7 @@ After the 6th shot the amount of projectiles may decrease, and must not increase
Between individual shots of an automatic cannon must be at least 4 seconds of delay (40 redstone ticks, 80 game ticks). Between individual shots of an automatic cannon must be at least 4 seconds of delay (40 redstone ticks, 80 game ticks).
A MiniWarGear may be equipped with up to two automatic cannons. A MiniWarGear may be equipped with up to two automatic cannons.
## Brücke ## Bridge
A MiniWarGear must feature a command bridge in the form of a clearly distinguishable room. A MiniWarGear must feature a command bridge in the form of a clearly distinguishable room.
The command bridge must be separated from the rest of the MiniWarGear by doors, fence gates, trapdoors or pistons. The command bridge must be separated from the rest of the MiniWarGear by doors, fence gates, trapdoors or pistons.
@@ -69,7 +69,7 @@ A command bridge must adhere to the following conditions:
- Controls for at least 4 headlights that are visible from the opposing position - Controls for at least 4 headlights that are visible from the opposing position
- Controls for automatic cannons (if any are installed) - Controls for automatic cannons (if any are installed)
- Controls for shield technology (if there is any) - Controls for shield technology (if there is any)
- The command bridge is the only place where dispensers which aim at the opponent may be controlled - The command bridge is the only place where dispensers which shoot fireballs or arrows at the opponent may be controlled
## Design ## Design

View File

@@ -5,6 +5,7 @@ import { SEO } from "astro-seo";
import { ClientRouter } from "astro:transitions"; import { ClientRouter } from "astro:transitions";
const { title, description, clientSideRouter = true, autoDarkMode = true } = Astro.props.frontmatter || Astro.props; const { title, description, clientSideRouter = true, autoDarkMode = true } = Astro.props.frontmatter || Astro.props;
import "../../public/fonts/roboto/roboto.css"; import "../../public/fonts/roboto/roboto.css";
import "../../public/fonts/barlow-condensed/barlow-condensed.css";
--- ---
<html lang={astroI18n.locale} class="dark"> <html lang={astroI18n.locale} class="dark">
@@ -44,7 +45,7 @@ import "../../public/fonts/roboto/roboto.css";
{clientSideRouter && <ClientRouter />} {clientSideRouter && <ClientRouter />}
</head> </head>
<body class="dark:bg-zinc-800"> <body>
<slot /> <slot />
</body> </body>
</html> </html>
@@ -54,5 +55,11 @@ import "../../public/fonts/roboto/roboto.css";
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
background-color: #080808;
color: #f5f5f5;
}
h1, h2, h3, h4, h5, h6 {
font-family: "Barlow Condensed", sans-serif;
} }
</style> </style>

View File

@@ -12,6 +12,9 @@ import Navbar from "@components/Navbar.svelte";
import ServerStatus from "../components/ServerStatus.svelte"; import ServerStatus from "../components/ServerStatus.svelte";
const { title, description, transparentFooter = true } = Astro.props; const { title, description, transparentFooter = true } = Astro.props;
const footerCol = "flex flex-col min-w-40 [&_a]:text-neutral-400/85 [&_a]:text-[0.85rem] [&_a]:leading-[1.9] [&_a]:no-underline [&_a]:transition-colors [&_a]:duration-200 [&_a:hover]:text-amber-500";
const footerH1 = "font-display text-[0.7rem] font-bold tracking-[0.2em] uppercase text-amber-500 mb-3";
--- ---
<Basic title={title} description={description} autoDarkMode={false}> <Basic title={title} description={description} autoDarkMode={false}>
@@ -24,17 +27,15 @@ const { title, description, transparentFooter = true } = Astro.props;
<main class="flex-1" data-pagefind-body> <main class="flex-1" data-pagefind-body>
<slot /> <slot />
</main> </main>
<footer <footer class="w-full max-w-[75em] mx-auto mt-16 px-4 pb-4 bg-[rgba(8,8,8,0.85)]">
class={`min-h-80 mt-4 pb-2 rounded-t-2xl flex flex-col ${transparentFooter ? "backdrop-blur-3xl" : "bg-neutral-900"}`} <div class="h-px bg-gradient-to-r from-transparent via-amber-500/30 to-transparent mb-10"></div>
style="width: min(100%, 75em); margin-left: auto; margin-right: auto;" <div class="flex justify-evenly items-start flex-wrap gap-8">
> <div class={`${footerCol} [&_h2]:text-[0.85rem] [&_h2]:text-neutral-400/85 [&_h2]:leading-[1.9]`}>
<div class="flex-1 flex justify-evenly items-center md:items-start mt-4 md:flex-row flex-col gap-y-4"> <h1 class={footerH1}>Serverstatus</h1>
<div class="footer-card">
<h1>Serverstatus</h1>
<ServerStatus client:only="svelte" /> <ServerStatus client:only="svelte" />
</div> </div>
<div class="footer-card"> <div class={footerCol}>
<h1>Links</h1> <h1 class={footerH1}>Links</h1>
<a href={l("/")}>{t("navbar.links.home.title")}</a> <a href={l("/")}>{t("navbar.links.home.title")}</a>
<a href={l("/join")}>{t("footer.join")}</a> <a href={l("/join")}>{t("footer.join")}</a>
<a href={l("/announcements")}>{t("footer.announcements")}</a> <a href={l("/announcements")}>{t("footer.announcements")}</a>
@@ -44,8 +45,8 @@ const { title, description, transparentFooter = true } = Astro.props;
<a href={l("/privacy-policy")}>{t("footer.privacy")}</a> <a href={l("/privacy-policy")}>{t("footer.privacy")}</a>
<a href={l("/imprint")}>{t("footer.imprint")}</a> <a href={l("/imprint")}>{t("footer.imprint")}</a>
</div> </div>
<div class="footer-card"> <div class={footerCol}>
<h1>Social Media</h1> <h1 class={footerH1}>Social Media</h1>
<a class="flex" href="/youtube"> <a class="flex" href="/youtube">
<YoutubeSolid class="mr-2" /> <YoutubeSolid class="mr-2" />
YouTube</a YouTube</a
@@ -60,26 +61,8 @@ const { title, description, transparentFooter = true } = Astro.props;
> >
</div> </div>
</div> </div>
<span class="text-sm text-white text-center mt-1">© SteamWar.de - Made with ❤️ by Chaoscaot</span> <span class="block text-center text-[0.7rem] tracking-[0.15em] text-neutral-400/40 mt-10">© SteamWar.de</span>
</footer> </footer>
</div> </div>
</Fragment> </Fragment>
</Basic> </Basic>
<style>
.footer-card {
@apply w-40 text-gray-400 flex flex-col;
> h1 {
@apply text-xl font-bold text-gray-100;
}
> a {
@apply hover:underline;
}
}
.match {
width: min(100vw, 70em);
}
</style>

View File

@@ -10,11 +10,22 @@ const { title, description, wide = false } = Astro.props;
<div class="h-screen w-screen fixed -z-10"> <div class="h-screen w-screen fixed -z-10">
<BackgroundImage /> <BackgroundImage />
</div> </div>
<div <div class="sw-page-content" style={wide ? "width: clamp(80%, 75em, 100%);" : "width: min(100%, 75em);"}>
class="mx-auto p-8 rounded-b-md border-x-gray-100 shadow-md pt-14 relative
text-white backdrop-blur-3xl"
style={wide ? "width: clamp(80%, 75em, 100%);" : "width: min(100%, 75em);"}
>
<slot /> <slot />
</div> </div>
</NavbarLayout> </NavbarLayout>
<style>
.sw-page-content {
margin: 0 auto;
padding: 3.5rem 2rem 2rem;
position: relative;
color: #f5f5f5;
background: rgba(8, 8, 8, 0.88);
backdrop-filter: blur(24px);
border-left: 1px solid rgba(255, 255, 255, 0.04);
border-right: 1px solid rgba(255, 255, 255, 0.04);
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
min-height: 60vh;
}
</style>

View File

@@ -1,19 +1,17 @@
--- ---
import {type CollectionEntry, getCollection} from "astro:content"; import { type CollectionEntry, getCollection } from "astro:content";
import {astroI18n, createGetStaticPaths} from "astro-i18n"; import { astroI18n, createGetStaticPaths } from "astro-i18n";
import PageLayout from "../layouts/PageLayout.astro"; import PageLayout from "../layouts/PageLayout.astro";
import LanguageWarning from "../components/LanguageWarning.astro"; import LanguageWarning from "../components/LanguageWarning.astro";
import "@styles/table.css"; import "@styles/table.css";
export const getStaticPaths = createGetStaticPaths(async () => { export const getStaticPaths = createGetStaticPaths(async () => {
let posts = await getCollection("pages", value => value.id.split("/")[0] === astroI18n.locale); let posts = await getCollection("pages", (value) => value.id.split("/")[0] === astroI18n.locale);
const germanPosts = await getCollection("pages", entry => entry.id.split("/")[0] === astroI18n.fallbackLocale); const germanPosts = await getCollection("pages", (entry) => entry.id.split("/")[0] === astroI18n.fallbackLocale);
germanPosts.forEach(value => { germanPosts.forEach((value) => {
if (posts.find(post => post.id.split("/")[1] === value.id.split("/")[1])) { if (posts.find((post) => post.id.split("/")[1] !== value.id.split("/")[1])) {
return;
} else {
posts.push(value); posts.push(value);
} }
}); });
@@ -41,17 +39,15 @@ export const getStaticPaths = createGetStaticPaths(async () => {
})); }));
}); });
const {page, german} = Astro.props as { page: CollectionEntry<"pages">, german: boolean }; const { page, german } = Astro.props as { page: CollectionEntry<"pages">; german: boolean };
const {Content} = await page.render(); const { Content } = await page.render();
--- ---
<PageLayout title={page.data.title}> <PageLayout title={page.data.title}>
<article> <article>
{german && ( {german && <LanguageWarning />}
<LanguageWarning/> <h1 class="text-left" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.04em;">{page.data.title}</h1>
)} <Content />
<h1 class="text-left">{page.data.title}</h1>
<Content/>
</article> </article>
</PageLayout> </PageLayout>
@@ -62,15 +58,28 @@ const {Content} = await page.render();
} }
code { code {
@apply dark:text-neutral-400 text-neutral-800; color: #fbbf24;
background: rgba(245, 158, 11, 0.08);
padding: 0.15em 0.4em;
} }
pre.astro-code { pre.astro-code {
@apply w-fit p-4 rounded-md border-2 border-gray-600 my-4; width: fit-content;
padding: 1rem;
margin: 1rem 0;
border: 1px solid rgba(255, 255, 255, 0.06);
background: #080808 !important;
} }
a { a {
@apply text-neutral-800 dark:text-neutral-400 hover:underline; color: #f59e0b;
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s ease;
}
a:hover {
border-bottom-color: #f59e0b;
} }
} }
</style> </style>

View File

@@ -73,13 +73,13 @@ const ogImage = await getImage({
{ {
post.data.image && ( post.data.image && (
<div class="absolute top-0 left-0 w-full aspect-video flex justify-center"> <div class="absolute top-0 left-0 w-full aspect-video flex justify-center">
<Image src={post.data.image} height="1080" alt="" transition:name={post.data.title + "-image"} class="rounded-2xl linear-fade object-contain h-full" /> <Image src={post.data.image} height="1080" alt="" transition:name={post.data.title + "-image"} class="linear-fade object-contain h-full" />
</div> </div>
) )
} }
<div class={post.data.image ? "absolute bottom-8 left-2" : "mb-4"}> <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> <h1 class="text-4xl mb-0" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.04em;" transition:name={post.data.title + "-title"}>{post.data.title}</h1>
<div class="flex items-center mt-2 text-neutral-800 dark:text-neutral-300"> <div class="flex items-center mt-2 text-gray-400">
<TagSolid class="w-4 h-4 mr-2" /> <TagSolid class="w-4 h-4 mr-2" />
<div transition:name={post.data.title + "-tags"}> <div transition:name={post.data.title + "-tags"}>
{post.data.tags.map((tag) => <TagComponent tag={tag} />)} {post.data.tags.map((tag) => <TagComponent tag={tag} />)}
@@ -175,11 +175,17 @@ const ogImage = await getImage({
} }
code { code {
@apply dark:text-neutral-400 text-neutral-800; color: #fbbf24;
background: rgba(245, 158, 11, 0.08);
padding: 0.15em 0.4em;
} }
pre.astro-code { pre.astro-code {
@apply w-fit p-4 rounded-md border-2 border-gray-600 my-4; width: fit-content;
padding: 1rem;
margin: 1rem 0;
border: 1px solid rgba(255, 255, 255, 0.06);
background: #080808 !important;
} }
} }
</style> </style>

View File

@@ -1,31 +1,44 @@
--- ---
import PageLayout from "../layouts/PageLayout.astro"; import PageLayout from "../layouts/PageLayout.astro";
import {getCollection} from "astro:content"; import { getCollection } from "astro:content";
import {t} from "astro-i18n"; import { t } from "astro-i18n";
import {l} from "../util/util"; import { l } from "../util/util";
const downloads = await getCollection("downloads"); const downloads = await getCollection("downloads");
--- ---
<PageLayout title="Downloads"> <PageLayout title="Downloads">
{downloads.map(e => ( {
<div class="pt-4"> downloads.map((e) => (
<h1 class="font-bold text-2xl">{e.data.name}</h1> <div class="pt-6 pb-4" style="border-bottom: 1px solid rgba(255,255,255,0.04);">
<div class="py-4">{t(e.data.description)}</div> <h1 class="font-bold text-2xl" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.06em;">
{e.data.name}
</h1>
<div class="py-3 text-gray-400 text-sm">{t(e.data.description)}</div>
<div class="flex flex-col"> <div class="flex flex-col gap-1">
{typeof e.data.url === "object" ? {typeof e.data.url === "object" ? (
Object.entries(e.data.url).map(value => ( Object.entries(e.data.url).map((value) => (
<a href={value[1].startsWith("/") ? l(value[1]) : value[1]} <a
class="text-blue-500 hover:underline w-fit">{t(value[0])}</a> href={value[1].startsWith("/") ? l(value[1]) : value[1]}
)) class="text-amber-400 hover:text-amber-300 w-fit text-sm"
: style="border-bottom: 1px solid transparent; transition: border-color 0.2s;"
<a href={e.data.url} class="text-blue-500 hover:underline w-fit">{t("Download")}</a> >
} {t(value[0])}
{e.data.sourceUrl ? </a>
<a class="text-blue-500 hover:underline w-fit" href={e.data.sourceUrl}>Quelle</a> ))
: null} ) : (
<a href={e.data.url} class="text-amber-400 hover:text-amber-300 w-fit text-sm">
{t("Download")}
</a>
)}
{e.data.sourceUrl ? (
<a class="text-amber-400 hover:text-amber-300 w-fit text-sm" href={e.data.sourceUrl}>
Quelle
</a>
) : null}
</div>
</div> </div>
</div> ))
))} }
</PageLayout> </PageLayout>

View File

@@ -1,27 +1,29 @@
--- ---
import {getCollection} from "astro:content"; import { getCollection } from "astro:content";
import {astroI18n} from "astro-i18n"; import { astroI18n } from "astro-i18n";
import {l} from "../../util/util"; import { l } from "../../util/util";
import PageLayout from "../../layouts/PageLayout.astro"; import PageLayout from "../../layouts/PageLayout.astro";
let posts = await getCollection("help", entry => entry.id.split("/")[0] === astroI18n.locale); let posts = await getCollection("help", (entry) => entry.id.split("/")[0] === astroI18n.locale);
--- ---
<PageLayout title="Helpcenter"> <PageLayout title="Helpcenter">
<div> <div>
<h1>Helpcenter</h1> <h1 style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.06em;">Helpcenter</h1>
<h1 class="text-red-700 text-4xl">WIP!</h1> <h1 class="text-amber-500 text-2xl" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.2em; text-transform: uppercase; font-size: 0.8rem;">WIP</h1>
{posts.map(value => ( {
<a href={l("/help/" + value.slug)}> posts.map((value) => (
<h2>{value.data.title}</h2> <a href={l("/help/" + value.slug)} class="block py-2 text-gray-400 hover:text-amber-400 transition-colors">
</a> <h2>{value.data.title}</h2>
))} </a>
))
}
</div> </div>
</PageLayout> </PageLayout>
<style> <style>
div { div {
@apply mx-auto bg-gray-100 px-4 py-8 rounded-b-md shadow-md pt-40 sm:pt-28 md:pt-14 margin: 0 auto;
dark:text-white dark:bg-neutral-900; padding: 3.5rem 1rem 2rem;
} }
</style> </style>

View File

@@ -3,9 +3,7 @@ import dayjs from "dayjs";
import NavbarLayout from "@layouts/NavbarLayout.astro"; import NavbarLayout from "@layouts/NavbarLayout.astro";
import { getCollection } from "astro:content"; import { getCollection } from "astro:content";
import { astroI18n } from "astro-i18n"; import { astroI18n } from "astro-i18n";
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import Card from "@components/Card.svelte";
import { CaretRight, Pause, Rocket, Crosshair1 } from "@astropub/icons"; import { CaretRight, Pause, Rocket, Crosshair1 } from "@astropub/icons";
import { t } from "astro-i18n"; import { t } from "astro-i18n";
import { l } from "@utils/util"; import { l } from "@utils/util";
@@ -31,194 +29,217 @@ germanPosts.forEach((value) => {
const latestPost = posts.sort((a, b) => dayjs(b.data.created).unix() - dayjs(a.data.created).unix()).at(0); const latestPost = posts.sort((a, b) => dayjs(b.data.created).unix() - dayjs(a.data.created).unix()).at(0);
const prefixColorMap: { const prefixColors: { [key: string]: string } = {
[key: string]: string; Admin: "#dc2626",
} = { Dev: "#0284c7",
Admin: "border-red-600 dark:border-red-800 shadow-red-600 dark:shadow-red-800", Mod: "#d97706",
Dev: "border-sky-600 dark:border-sky-800 shadow-sky-600 dark:shadow-sky-800", Sup: "#1d4ed8",
Mod: "border-amber-600 dark:border-amber-800 shadow-amber-600 dark:shadow-amber-800", Arch: "#22c55e",
Sup: "border-blue-700 dark:border-blue-900 shadow-blue-700 dark:shadow-blue-900",
Arch: "border-green-500 dark:border-green-700 shadow-green-500 dark:shadow-green-700",
}; };
const featClass =
"flex flex-col items-center text-center py-10 px-8 border-b border-white/[0.04] md:border-b-0 md:border-r md:last:border-r-0 transition-[background] duration-[400ms] hover:bg-amber-500/[0.025] [&_svg]:text-amber-500 [&_svg]:mb-5 [&_svg]:transition-transform [&_svg]:duration-300 [&:hover_svg]:scale-[1.15]";
const featNum = "font-display text-[3.5rem] font-extrabold leading-none text-amber-500/10 mb-5";
const featH3 = "font-display text-[1.15rem] font-bold text-white tracking-[0.08em] uppercase mb-3";
const featP = "text-neutral-400/85 text-[0.85rem] leading-[1.65] mt-1";
--- ---
<NavbarLayout title={t("home.page")} description="SteamWar.de Homepage" transparentFooter={false}> <NavbarLayout title={t("home.page")} description="SteamWar.de Homepage" transparentFooter={false}>
<div class="w-full h-screen relative mb-4 z-10"> <section class="relative w-full h-screen overflow-hidden">
<div style="height: calc(100vh + 1rem)"> <div class="absolute inset-0 h-[calc(100vh+1rem)]">
<BackgroundImage /> <BackgroundImage />
</div> </div>
<drop-in class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center"> <div class="absolute inset-0 [background:linear-gradient(to_bottom,rgba(0,0,0,0.6)_0%,rgba(0,0,0,0.2)_35%,rgba(0,0,0,0.45)_65%,#080808_100%)]"></div>
<h1 <div class="absolute inset-0 pointer-events-none [background:repeating-linear-gradient(0deg,transparent_0px,transparent_3px,rgba(0,0,0,0.06)_3px,rgba(0,0,0,0.06)_4px)]"></div>
class="text-4xl sm:text-6xl md:text-8xl font-extrabold text-white -translate-y-16 opacity-0 barlow tracking-wider"
style="transition: transform .7s ease-out, opacity .7s linear; filter: drop-shadow(2px 2px 5px black);" <div class="relative h-full flex flex-col items-center justify-center px-4">
> <h1 class="font-display text-[clamp(4.5rem,14vw,14rem)] font-black leading-[0.85] tracking-[-0.03em] select-none animate-[scaleIn_0.9s_0.25s_ease-out_both]">
<span class="bg-gradient-to-tr from-yellow-400 to-yellow-300 bg-clip-text text-transparent">{t("home.title.first")}</span><span class="text-neutral-600">{t("home.title.second")}</span> <span class="hero-text text-transparent">{t("home.title.first")}</span><span class="text-white [text-shadow:0_0_80px_rgba(255,255,255,0.06)]">{t("home.title.second")}</span>
</h1> </h1>
<text-carousel class="h-20 w-full relative select-none">
<h2 class="-translate-y-16">{t("home.subtitle.1")}</h2> <div class="w-0 h-0.5 bg-gradient-to-r from-transparent via-amber-500 to-transparent my-7 animate-[lineGrow_0.6s_0.7s_ease-out_both]"></div>
<text-carousel class="h-10 sm:h-12 w-full max-w-[28rem] sm:max-w-[32rem] relative pointer-events-none">
<h2>{t("home.subtitle.1")}</h2>
<h2> <h2>
{t("home.subtitle.2")} {t("home.subtitle.2")}
<PlayerCount client:idle /> <PlayerCount client:idle />
</h2> </h2>
<h2>{t("home.subtitle.3")}</h2> <h2>{t("home.subtitle.3")}</h2>
</text-carousel> </text-carousel>
<a href={l("join")} class="btn btn-ghost mt-32 px-8 flex" style="animation: normal flyIn forwards 1.2s ease-out"
>{t("home.join")} <a
<CaretRight width="24" height="24" /> href={l("join")}
class="inline-flex items-center gap-[0.6rem] mt-12 py-[0.8rem] px-[2.8rem] border border-amber-500/50 text-amber-400 font-display text-[0.85rem] tracking-[0.2em] uppercase bg-amber-500/[0.04] backdrop-blur-[12px] [clip-path:polygon(0_0,calc(100%-14px)_0,100%_14px,100%_100%,14px_100%,0_calc(100%-14px))] transition-all duration-[350ms] animate-[fadeUp_0.6s_1s_ease-out_both] hover:bg-amber-500/[0.12] hover:border-amber-500/85 hover:text-white hover:[box-shadow:0_0_40px_rgba(245,158,11,0.1),inset_0_0_40px_rgba(245,158,11,0.03)] hover:scale-[1.04] active:scale-[0.97]"
>
<span>{t("home.join")}</span>
<CaretRight width="18" height="18" />
</a> </a>
<style> </div>
@keyframes flyIn {
0% {
translate: 0 16px;
opacity: 0;
}
20% { <div class="absolute bottom-6 left-1/2 -translate-x-1/2 w-full max-w-[40rem] px-4">
translate: 0 16px;
opacity: 0;
}
100% {
translate: 0;
opacity: 1;
}
}
</style>
<script>
class TextCarousel extends HTMLElement {
current = 0;
connectedCallback() {
this._current.classList.add("!opacity-100");
for (let i = 0; i < this.children.length; i++) {
if (i !== this.current) {
this.children[i].classList.add("translate-y-8");
}
}
setInterval(() => {
this.next();
}, 5000);
}
get _current() {
return this.children[this.current];
}
next() {
this._current.classList.remove("!opacity-100");
this._current.classList.add("translate-y-8");
this._current.classList.remove("!delay-500");
this.current = (this.current + 1) % this.children.length;
this._current.classList.add("!opacity-100");
this._current.classList.remove("translate-y-8");
this._current.classList.add("!delay-500");
}
}
class DropIn extends HTMLElement {
connectedCallback() {
for (let child of this.children) {
if (child.classList.contains("opacity-0")) {
child.classList.remove("opacity-0");
child.classList.remove("-translate-y-16");
} else {
child.children[0].classList.remove("opacity-0");
child.children[0].classList.remove("-translate-y-16");
}
}
}
}
customElements.define("text-carousel", TextCarousel);
customElements.define("drop-in", DropIn);
</script>
</drop-in>
<div class="absolute left-1/2 bottom-6 -translate-x-1/2">
<PostComponent post={latestPost} slim={true} /> <PostComponent post={latestPost} slim={true} />
</div> </div>
</div> </section>
<section class="w-full flex flex-col items-center justify-center shadow-2xl rounded-b-2xl pb-8">
<div class="py-10 flex flex-col lg:flex-row"> <section class="relative bg-[#080808] overflow-hidden">
<Card client:idle> <div class="absolute -top-[100px] left-1/2 -translate-x-1/2 w-[500px] h-[250px] [background:radial-gradient(ellipse,rgba(245,158,11,0.06)_0%,transparent_70%)] pointer-events-none"></div>
<Crosshair1 height="64" width="64" /> <div class="relative z-[2] max-w-5xl mx-auto py-24 px-4">
<h1>{t("home.benefits.fights.title")}</h1> <div class="grid grid-cols-1 md:grid-cols-3">
<p class="mt-4">{t("home.benefits.fights.description.1")}</p> <article class={featClass}>
<p class="mt-4">{t("home.benefits.fights.description.2")}</p> <span class={featNum}>01</span>
</Card> <Crosshair1 height="36" width="36" />
<Card client:idle> <h3 class={featH3}>{t("home.benefits.fights.title")}</h3>
<Rocket height="64" width="64" /> <p class={featP}>{t("home.benefits.fights.description.1")}</p>
<h1>{t("home.benefits.bau.title")}</h1> <p class={featP}>{t("home.benefits.fights.description.2")}</p>
<p class="mt-4">{t("home.benefits.bau.description")}</p> </article>
</Card>
<Card client:idle> <article class={featClass}>
<Pause height="64" width="64" /> <span class={featNum}>02</span>
<h1>{t("home.benefits.minigames.title")}</h1> <Rocket height="36" width="36" />
<p class="mt-4">{t("home.benefits.minigames.description.1")}</p> <h3 class={featH3}>{t("home.benefits.bau.title")}</h3>
<p class="mt-4">{t("home.benefits.minigames.description.2")}</p> <p class={featP}>{t("home.benefits.bau.description")}</p>
</Card> </article>
<article class={featClass}>
<span class={featNum}>03</span>
<Pause height="36" width="36" />
<h3 class={featH3}>{t("home.benefits.minigames.title")}</h3>
<p class={featP}>{t("home.benefits.minigames.description.1")}</p>
<p class={featP}>{t("home.benefits.minigames.description.2")}</p>
</article>
</div>
</div> </div>
</section> </section>
<section class="w-full py-12 flex flex-wrap justify-center">
{ <section class="bg-[#0c0c0c] py-20">
Object.entries(teamMember).map(([prefix, players]) => ( <div class="max-w-5xl mx-auto px-4 flex flex-wrap gap-4">
<Fragment> {
{players.map((v, index) => ( Object.entries(teamMember).map(([prefix, players]) => (
<div class="inline-flex flex-col justify-end"> <Fragment>
{index == 0 ? <h2 class="dark:text-white text-4xl font-bold text-center md:text-left md:pl-4">{t("home.prefix." + prefix)}</h2> : null} {players.map((v, i) => (
<Card extraClasses={`pt-8 pb-10 px-8 w-fit shadow-md ${prefixColorMap[prefix]}`} client:idle> <div class="flex flex-col justify-end">
<figure class="flex flex-col items-center" style="width: 150px"> {i === 0 && (
<figcaption class="text-center mb-4 text-2xl">{v.name}</figcaption> <h2 class="font-display text-[1.35rem] font-bold tracking-[0.12em] uppercase text-[var(--rc)] pb-1 inline-block" style={`--rc: ${prefixColors[prefix] || "#666"}`}>
{t("home.prefix." + prefix)}
</h2>
)}
<div
class="flex flex-col items-center py-5 px-6 bg-white/[0.025] border-t-2 border-[var(--rc)] transition-[transform,background,box-shadow] duration-300 hover:-translate-y-1.5 hover:bg-white/5 hover:shadow-[0_12px_32px_rgba(0,0,0,0.5)] [&:hover_img]:scale-[1.08]"
style={`--rc: ${prefixColors[prefix] || "#666"}`}
>
<Image <Image
src={`${import.meta.env.PUBLIC_API_SERVER}/data/skin/${v.uuid}`} src={`${import.meta.env.PUBLIC_API_SERVER}/data/skin/${v.uuid}`}
class="transition duration-300 ease-in-out hover:scale-110 hover:backdrop-blur-lg hover:drop-shadow-2xl" alt={v.name}
alt={v.name + "s bust"} width="120"
width="150" height="120"
height="150" class="w-[120px] h-[120px] transition-transform duration-300"
/> />
</figure> <span class="font-display mt-3 text-[0.9rem] tracking-[0.1em] text-neutral-300/90">{v.name}</span>
</Card> </div>
</div> </div>
))} ))}
</Fragment> </Fragment>
)) ))
} }
</div>
</section> </section>
<script>
class TextCarousel extends HTMLElement {
current = 0;
connectedCallback() {
this._current.classList.add("!opacity-100");
for (let i = 0; i < this.children.length; i++) {
if (i !== this.current) {
this.children[i].classList.add("translate-y-4");
}
}
setInterval(() => this.next(), 5000);
}
get _current() {
return this.children[this.current];
}
next() {
this._current.classList.remove("!opacity-100");
this._current.classList.add("translate-y-4");
this._current.classList.remove("!delay-500");
this.current = (this.current + 1) % this.children.length;
this._current.classList.add("!opacity-100");
this._current.classList.remove("translate-y-4");
this._current.classList.add("!delay-500");
}
}
customElements.define("text-carousel", TextCarousel);
</script>
</NavbarLayout> </NavbarLayout>
<style> <style>
text-carousel { .hero-text {
> * { background-image: linear-gradient(160deg, #fcd34d, #f59e0b);
@apply absolute top-0 left-0 w-full text-xl sm:text-4xl italic text-white text-center opacity-0; background-clip: text;
transition: }
transform 0.5s ease-out,
opacity 0.5s linear; text-carousel > * {
text-shadow: 2px 2px 5px black; position: absolute;
top: 0;
left: 0;
width: 100%;
text-align: center;
opacity: 0;
font-family: "Barlow Condensed", sans-serif;
font-size: 0.95rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.5);
transform: translateY(8px);
transition:
transform 0.5s ease-out,
opacity 0.5s linear;
}
@media (min-width: 640px) {
text-carousel > * {
font-size: 1.15rem;
} }
} }
.barlow { @keyframes fadeUp {
font-family: from {
Barlow Condensed, opacity: 0;
sans-serif; transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.card { @keyframes scaleIn {
@apply w-72 border-2 bg-zinc-50 border-gray-100 flex flex-col items-center p-8 m-4 rounded-xl shadow-lg transition-transform duration-300 ease-in-out from {
dark:bg-zinc-900 dark:border-gray-800 dark:text-gray-100 opacity: 0;
hover:scale-105; transform: scale(0.92);
> h1 {
@apply text-xl font-bold mt-4;
} }
to {
> p { opacity: 1;
@apply mt-4; transform: scale(1);
} }
}
> svg { @keyframes lineGrow {
@apply transition-transform duration-300 ease-in-out hover:scale-110 hover:drop-shadow-2xl; from {
width: 0;
opacity: 0;
}
to {
width: 6rem;
opacity: 1;
} }
} }
</style> </style>

View File

@@ -23,8 +23,7 @@ import BackgroundImage from "../components/BackgroundImage.astro";
<div class="h-screen w-screen fixed -z-10"> <div class="h-screen w-screen fixed -z-10">
<BackgroundImage /> <BackgroundImage />
</div> </div>
<div class="h-screen mx-auto p-8 rounded-b-md pt-40 sm:pt-28 md:pt-14 flex flex-col justify-center items-center <div class="h-screen mx-auto p-8 pt-40 sm:pt-28 md:pt-14 flex flex-col justify-center items-center" style="width: min(100vw, 75em);">
dark:text-white" style="width: min(100vw, 75em);">
<LoginComponent client:load /> <LoginComponent client:load />
</div> </div>
</NavbarLayout> </NavbarLayout>

View File

@@ -1,39 +1,12 @@
--- ---
import PageLayout from "../layouts/PageLayout.astro"; import PageLayout from "../layouts/PageLayout.astro";
import {t} from "astro-i18n"; import { t } from "astro-i18n";
--- ---
<PageLayout title={t("404.title")}> <PageLayout title={t("404.title")}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 400"> <div class="flex flex-col items-center justify-center py-20">
<!-- 404 Text - heller für besseren Kontrast --> <span style="font-family: 'Barlow Condensed', sans-serif; font-size: clamp(6rem, 15vw, 12rem); font-weight: 900; line-height: 1; color: rgba(245, 158, 11, 0.1);">404</span>
<text x="400" y="150" font-family="Arial Black" font-size="120" fill="#ffffff" text-anchor="middle">404</text> <div style="width: 4rem; height: 2px; background: linear-gradient(90deg, transparent, #f59e0b, transparent); margin: 1.5rem 0;"></div>
<p style="font-family: 'Barlow Condensed', sans-serif; font-size: 1.1rem; letter-spacing: 0.15em; text-transform: uppercase; color: rgba(163, 163, 163, 0.7);">{t("404.description")}</p>
<!-- Trauriger Roboter - hellere Farben für besseren Kontrast --> </div>
<!-- Körper -->
<rect x="350" y="200" width="100" height="120" rx="10" fill="#888888"/>
<!-- Kopf -->
<rect x="365" y="160" width="70" height="60" rx="5" fill="#888888"/>
<!-- Augen -->
<circle cx="385" cy="185" r="8" fill="#ff6b6b"/>
<circle cx="415" cy="185" r="8" fill="#ff6b6b"/>
<!-- Antenne -->
<line x1="400" y1="160" x2="400" y2="140" stroke="#888888" stroke-width="4"/>
<circle cx="400" cy="135" r="5" fill="#888888"/>
<!-- Arme -->
<rect x="320" y="220" width="30" height="10" rx="5" fill="#888888"/>
<rect x="450" y="220" width="30" height="10" rx="5" fill="#888888"/>
<!-- Text unter dem Roboter - heller für besseren Kontrast -->
<text x="400" y="360" font-family="Arial" font-size="24" fill="#ffffff" text-anchor="middle">{t("404.description")}</text>
<!-- Dekorative Elemente - heller für besseren Kontrast -->
<circle cx="250" cy="100" r="5" fill="#bbbbbb"/>
<circle cx="550" cy="150" r="5" fill="#bbbbbb"/>
<circle cx="200" cy="300" r="5" fill="#bbbbbb"/>
<circle cx="600" cy="250" r="5" fill="#bbbbbb"/>
</svg>
</PageLayout> </PageLayout>

View File

@@ -1,5 +1,5 @@
--- ---
import {createGetStaticPaths} from "astro-i18n"; import { createGetStaticPaths } from "astro-i18n";
import { getCollection, CollectionEntry } from "astro:content"; import { getCollection, CollectionEntry } from "astro:content";
import PageLayout from "../../layouts/PageLayout.astro"; import PageLayout from "../../layouts/PageLayout.astro";
import PublicPreview from "@components/publics/PublicPreview.svelte"; import PublicPreview from "@components/publics/PublicPreview.svelte";
@@ -18,13 +18,13 @@ export const getStaticPaths = createGetStaticPaths(async () => {
})); }));
}); });
const { schem }: { schem: CollectionEntry<"publics">} = Astro.props; const { schem }: { schem: CollectionEntry<"publics"> } = Astro.props;
--- ---
<PageLayout title={schem.data.name}> <PageLayout title={schem.data.name}>
<h1 class="text-5xl font-bold w-fit" transition:name={schem.data.id + "-title"}>{schem.data.name}</h1> <h1 class="text-5xl font-bold w-fit" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.04em;" transition:name={schem.data.id + "-title"}>{schem.data.name}</h1>
<PublicPreview client:idle pub={schem} imageHeight={schem.data.image.height}> <PublicPreview client:idle pub={schem} imageHeight={schem.data.image.height}>
<Image class="object-contain" transition:name={schem.data.id + "-img"} src={schem.data.alt || schem.data.image} alt={schem.data.name}></Image> <Image class="object-contain" transition:name={schem.data.id + "-img"} src={schem.data.alt || schem.data.image} alt={schem.data.name} />
</PublicPreview> </PublicPreview>
<!-- <!--
<p transition:name={schem.data.id + "-desc"}>{schem.data.description}</p> <p transition:name={schem.data.id + "-desc"}>{schem.data.description}</p>

View File

@@ -1,12 +1,12 @@
--- ---
import {createGetStaticPaths} from "astro-i18n"; import { createGetStaticPaths } from "astro-i18n";
import PageLayout from "../../layouts/PageLayout.astro"; import PageLayout from "../../layouts/PageLayout.astro";
import {getCollection} from "astro:content"; import { getCollection } from "astro:content";
import {l} from "../../util/util"; import { l } from "../../util/util";
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import Card from "@components/Card.svelte"; import Card from "@components/Card.svelte";
import XRayPreview from "@components/publics/XRayPreview.svelte"; import XRayPreview from "@components/publics/XRayPreview.svelte";
import {t} from "astro-i18n"; import { t } from "astro-i18n";
const { mode } = Astro.props; const { mode } = Astro.props;
@@ -23,24 +23,28 @@ export const getStaticPaths = createGetStaticPaths(async () => {
})); }));
}); });
const publics = await getCollection("publics", entry => entry.data.gamemode.id == mode.id); const publics = await getCollection("publics", (entry) => entry.data.gamemode.id == mode.id);
--- ---
<PageLayout title="Publics"> <PageLayout title="Publics">
<h1 class="text-4xl font-bold">{t(`${mode.data.translationKey}.title`)}</h1> <h1 class="text-4xl font-bold" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.04em;">{t(`${mode.data.translationKey}.title`)}</h1>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2 mt-4">
{publics.map((pub) => ( {
<a href={l("/publics/" + pub.id)}> publics.map((pub) => (
<Card extraClasses="w-full m-0" hoverEffect={false}> <a href={l("/publics/" + pub.id)}>
<div class="flex justify-center"> <Card extraClasses="w-full m-0 border-t-2 border-t-amber-500/30" hoverEffect={false}>
<XRayPreview client:load> <div class="flex justify-center">
<Image style="width: 500px" slot="normal" src={pub.data.image} alt={pub.data.name} transition:name={pub.data.id + "-img"} /> <XRayPreview client:load>
{pub.data.xray && <Image slot="xray" class="bg-zinc-50 dark:bg-zinc-900" src={pub.data.xray} alt={pub.data.name} />} <Image style="width: 500px" slot="normal" src={pub.data.image} alt={pub.data.name} transition:name={pub.data.id + "-img"} />
</XRayPreview> {pub.data.xray && <Image slot="xray" src={pub.data.xray} alt={pub.data.name} />}
</div> </XRayPreview>
<h2 class="font-bold text-3xl" transition:name={pub.data.id + "-title"}>{pub.data.name}</h2> </div>
</Card> <h2 class="font-bold text-3xl" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.04em;" transition:name={pub.data.id + "-title"}>
</a> {pub.data.name}
))} </h2>
</Card>
</a>
))
}
</div> </div>
</PageLayout> </PageLayout>

View File

@@ -1,13 +1,13 @@
--- ---
import {createGetStaticPaths, t} from "astro-i18n"; import { createGetStaticPaths, t } from "astro-i18n";
import {getCollection, type CollectionEntry} from "astro:content"; import { getCollection, type CollectionEntry } from "astro:content";
import PageLayout from "../../layouts/PageLayout.astro"; import PageLayout from "../../layouts/PageLayout.astro";
import EloTable from "../../components/EloTable.svelte"; import EloTable from "../../components/EloTable.svelte";
export const getStaticPaths = createGetStaticPaths(async () => { export const getStaticPaths = createGetStaticPaths(async () => {
const modes = await getCollection("modes", entry => entry.data.ranked); const modes = await getCollection("modes", (entry) => entry.data.ranked);
return modes.map(value => ({ return modes.map((value) => ({
props: { props: {
mode: value, mode: value,
}, },
@@ -21,10 +21,10 @@ interface Props {
mode: CollectionEntry<"modes">; mode: CollectionEntry<"modes">;
} }
const {mode} = Astro.props; const { mode } = Astro.props;
--- ---
<PageLayout title={t("ranking.heading", {mode: t(`${mode.data.translationKey}.title`)})}> <PageLayout title={t("ranking.heading", { mode: t(`${mode.data.translationKey}.title`) })}>
<h1 class="text-2xl mb-2">{t("ranking.heading", {mode: t(`${mode.data.translationKey}.title`)})}</h1> <h1 class="text-2xl mb-2" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.06em;">{t("ranking.heading", { mode: t(`${mode.data.translationKey}.title`) })}</h1>
<EloTable gamemode={mode.id} client:load/> <EloTable gamemode={mode.id} client:load />
</PageLayout> </PageLayout>

View File

@@ -1,19 +1,19 @@
--- ---
import {getCollection, type CollectionEntry} from "astro:content"; import { getCollection, type CollectionEntry } from "astro:content";
import {astroI18n, createGetStaticPaths, t} from "astro-i18n"; import { astroI18n, createGetStaticPaths, t } from "astro-i18n";
import PageLayout from "@layouts/PageLayout.astro"; import PageLayout from "@layouts/PageLayout.astro";
import LanguageWarning from "@components/LanguageWarning.astro"; import LanguageWarning from "@components/LanguageWarning.astro";
import EloTable from "@components/EloTable.svelte"; import EloTable from "@components/EloTable.svelte";
import {l} from "../../util/util"; import { l } from "../../util/util";
import { Image } from "astro:assets"; import { Image } from "astro:assets";
export const getStaticPaths = createGetStaticPaths(async () => { export const getStaticPaths = createGetStaticPaths(async () => {
let posts = await getCollection("rules", value => value.id.split("/")[0] === astroI18n.locale); let posts = await getCollection("rules", (value) => value.id.split("/")[0] === astroI18n.locale);
const germanPosts = await getCollection("rules", entry => entry.id.split("/")[0] === astroI18n.fallbackLocale); const germanPosts = await getCollection("rules", (entry) => entry.id.split("/")[0] === astroI18n.fallbackLocale);
germanPosts.forEach(value => { germanPosts.forEach((value) => {
if (posts.find(post => post.id.split("/")[1] === value.id.split("/")[1])) { if (posts.find((post) => post.id.split("/")[1] === value.id.split("/")[1])) {
return; return;
} else { } else {
posts.push(value); posts.push(value);
@@ -27,53 +27,58 @@ export const getStaticPaths = createGetStaticPaths(async () => {
props: { props: {
page, page,
german: page.id.split("/")[0] != astroI18n.locale, german: page.id.split("/")[0] != astroI18n.locale,
mode: modes.find(value => value.id === page.id.split("/")[1].split(".")[0]), mode: modes.find((value) => value.id === page.id.split("/")[1].split(".")[0]),
publics: publics.filter(value => value.data.gamemode.id === page.id.split("/")[1].split(".")[0]), publics: publics.filter((value) => value.data.gamemode.id === page.id.split("/")[1].split(".")[0]),
}, },
params: { params: {
mode: page.slug.split("/")[1], mode: page.slug.split("/")[1],
}, },
}), }));
);
}); });
interface Props { interface Props {
page: CollectionEntry<"rules">, page: CollectionEntry<"rules">;
mode: CollectionEntry<"modes">, mode: CollectionEntry<"modes">;
publics: CollectionEntry<"publics">[], publics: CollectionEntry<"publics">[];
german: boolean german: boolean;
} }
const {page, german, mode, publics} = Astro.props; const { page, german, mode, publics } = Astro.props;
const {Content} = await page.render(); const { Content } = await page.render();
--- ---
<PageLayout title={t("rules.title", {mode: t(`${page.data.translationKey}.title`)})}> <PageLayout title={t("rules.title", { mode: t(`${page.data.translationKey}.title`) })}>
<h1 class="text-3xl font-bold">{t(`${page.data.translationKey}.title`)}</h1> <h1 class="text-3xl font-bold" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.04em;">{t(`${page.data.translationKey}.title`)}</h1>
{mode && mode.data.ranked && ( {
<Fragment> mode && mode.data.ranked && (
<EloTable gamemode={page.id.split("/")[1].split(".")[0]} client:load topFive={true}/> <Fragment>
<a class="text-neutral-800 dark:text-neutral-400 hover:underline" href={l(`/rangliste/${page.id.split("/")[1].split(".")[0]}`)}>{t("rules.ranking")}</a> <EloTable gamemode={page.id.split("/")[1].split(".")[0]} client:load topFive={true} />
</Fragment> <a class="text-amber-400 hover:text-amber-300 transition-colors text-sm" href={l(`/rangliste/${page.id.split("/")[1].split(".")[0]}`)}>
)} {t("rules.ranking")}
{publics && publics.length != 0 && ( </a>
<Fragment> </Fragment>
<div class="flex overflow-x-scroll"> )
{publics.map(value => ( }
<a href={l(`/publics/${value.id}`)} style="display: contents"> {
<Image src={value.data.image} alt={value.data.name} height={300} width={300} class="drop-shadow"></Image> publics && publics.length != 0 && (
</a> <Fragment>
))} <div class="flex overflow-x-scroll gap-2 py-4">
</div> {publics.map((value) => (
<a class="text-neutral-800 dark:text-neutral-400 hover:underline" href={l(`/publics/${page.id.split("/")[1].split(".")[0]}`)}>{t("rules.publics")}</a> <a href={l(`/publics/${value.id}`)} style="display: contents">
</Fragment> <Image src={value.data.image} alt={value.data.name} height={300} width={300} />
)} </a>
))}
</div>
<a class="text-amber-400 hover:text-amber-300 transition-colors text-sm" href={l(`/publics/${page.id.split("/")[1].split(".")[0]}`)}>
{t("rules.publics")}
</a>
</Fragment>
)
}
<article> <article>
{german && ( {german && <LanguageWarning />}
<LanguageWarning/> <Content />
)}
<Content/>
</article> </article>
</PageLayout> </PageLayout>
@@ -84,15 +89,28 @@ const {Content} = await page.render();
} }
code { code {
@apply dark:text-neutral-400 text-neutral-800; color: #fbbf24;
background: rgba(245, 158, 11, 0.08);
padding: 0.15em 0.4em;
} }
pre.astro-code { pre.astro-code {
@apply w-fit p-4 rounded-md border-2 border-gray-600 my-4; width: fit-content;
padding: 1rem;
margin: 1rem 0;
border: 1px solid rgba(255, 255, 255, 0.06);
background: #080808 !important;
} }
a { a {
@apply text-neutral-800 dark:text-neutral-400 hover:underline; color: #f59e0b;
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s ease;
}
a:hover {
border-bottom-color: #f59e0b;
} }
} }

View File

@@ -1,48 +1,83 @@
--- ---
import {t} from "astro-i18n"; import { t } from "astro-i18n";
import {getCollection, type CollectionEntry} from "astro:content"; import { getCollection, type CollectionEntry } 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 "@utils/util"; import { l } from "@utils/util";
const imageMap = { const imageMap = {
"wg": await getRandomFromMode("wargear"), wg: await getRandomFromMode("wargear"),
"mwg": await getRandomFromMode("miniwargear"), mwg: await getRandomFromMode("miniwargear"),
"as": await getRandomFromMode("airship"), as: await getRandomFromMode("airship"),
"ws": await getRandomFromMode("warship"), ws: await getRandomFromMode("warship"),
"qg": await getRandomFromMode("quickgear"), qg: await getRandomFromMode("quickgear"),
}; };
async function getRandomFromMode(mode: "wargear" | "airship" | "megawargear" | "microwargear" | "miniwargear" | "quickgear" | "streetfight" | "warship"): Promise<CollectionEntry<"publics">> { async function getRandomFromMode(mode: "wargear" | "airship" | "megawargear" | "microwargear" | "miniwargear" | "quickgear" | "streetfight" | "warship"): Promise<CollectionEntry<"publics">> {
const publics = await getCollection("publics", entry => entry.data.gamemode.id === mode); const publics = await getCollection("publics", (entry) => entry.data.gamemode.id === mode);
return publics[Math.floor(Math.random() * publics.length)]; return publics[Math.floor(Math.random() * publics.length)];
} }
const modes = await getCollection("modes", entry => entry.data.main); const modes = await getCollection("modes", (entry) => entry.data.main);
--- ---
<PageLayout title={t("rules.page")}> <PageLayout title={t("rules.page")}>
{modes.map(value => ( {
<div class="dark:bg-neutral-800 rounded-md p-4 border border-neutral-400 shadow-md my-4 flex flex-col modes.map((value) => (
md:flex-row"> <div class="sw-mode-card">
<a href={l(`/publics/${imageMap[value.data.translationKey].id}`)}> <a href={l(`/publics/${imageMap[value.data.translationKey].id}`)}>
<Image height="200" width="200" src={imageMap[value.data.translationKey].data.image} <Image
alt={t("rules." + value.data.translationKey + ".title")} class="h-full aspect-square max-w-fit"></Image> height="200"
</a> width="200"
<div class="ml-4"> src={imageMap[value.data.translationKey].data.image}
<a href={l(`/rules/${value.id}`)}> alt={t("rules." + value.data.translationKey + ".title")}
<h1 class="text-2xl font-bold">{t(value.data.translationKey + ".title")}</h1> class="h-full aspect-square max-w-fit"
<div>{t("rules." + value.data.translationKey + ".description")}</div> />
</a> </a>
<div class="mt-2 flex flex-col"> <div class="ml-4">
<a href={l(`/publics/${value.id}`)} class="text-yellow-300 hover:underline w-fit">{t("rules.publics")}</a> <a href={l(`/rules/${value.id}`)}>
{value.data.ranked <h1 class="text-2xl font-bold" style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.04em;">
? <a href={l(`/ranked/${value.id}`)} {t(value.data.translationKey + ".title")}
class="text-yellow-300 hover:underline w-fit">{t("rules.ranking")}</a> </h1>
: null} <div class="text-gray-400 text-sm mt-1">{t("rules." + value.data.translationKey + ".description")}</div>
</a>
<div class="mt-2 flex flex-col gap-0.5">
<a href={l(`/publics/${value.id}`)} class="text-amber-400 hover:text-amber-300 w-fit text-sm transition-colors">
{t("rules.publics")}
</a>
{value.data.ranked ? (
<a href={l(`/ranked/${value.id}`)} class="text-amber-400 hover:text-amber-300 w-fit text-sm transition-colors">
{t("rules.ranking")}
</a>
) : null}
</div>
</div> </div>
</div> </div>
</div>))} ))
<a href={l("/rangliste/MissileWars")}>MissileWars Rangliste</a> }
<a href={l("/rangliste/MissileWars")} class="text-amber-400 hover:text-amber-300 transition-colors">MissileWars Rangliste</a>
</PageLayout> </PageLayout>
<style>
.sw-mode-card {
display: flex;
flex-direction: column;
padding: 1.25rem;
margin: 1rem 0;
border: 1px solid rgba(255, 255, 255, 0.06);
border-top: 2px solid rgba(245, 158, 11, 0.3);
background: rgba(255, 255, 255, 0.02);
transition: background 0.3s ease;
}
.sw-mode-card:hover {
background: rgba(245, 158, 11, 0.03);
}
@media (min-width: 768px) {
.sw-mode-card {
flex-direction: row;
}
}
</style>

View File

@@ -1,10 +1,10 @@
--- ---
import PageLayout from "../../layouts/PageLayout.astro"; import PageLayout from "../../layouts/PageLayout.astro";
import FightStatistics from "../../components/FightStatistics.svelte"; import FightStatistics from "../../components/FightStatistics.svelte";
import {t} from "astro-i18n"; import { t } from "astro-i18n";
--- ---
<PageLayout title={t("stats.title")}> <PageLayout title={t("stats.title")}>
<h1>{t("stats.title")}</h1> <h1 style="font-family: 'Barlow Condensed', sans-serif; letter-spacing: 0.06em;">{t("stats.title")}</h1>
<FightStatistics client:only="svelte"/> <FightStatistics client:only="svelte" />
</PageLayout> </PageLayout>

View File

@@ -23,67 +23,77 @@
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 3.1%;
--foreground: 240 10% 3.9%; --foreground: 0 0% 96%;
--muted: 240 4.8% 95.9%; --muted: 0 0% 10%;
--muted-foreground: 240 3.8% 46.1%; --muted-foreground: 0 0% 55%;
--popover: 0 0% 100%; --popover: 0 0% 5%;
--popover-foreground: 240 10% 3.9%; --popover-foreground: 0 0% 96%;
--card: 0 0% 100%; --card: 0 0% 5%;
--card-foreground: 240 10% 3.9%; --card-foreground: 0 0% 96%;
--border: 240 5.9% 90%; --border: 0 0% 12%;
--input: 240 5.9% 90%; --input: 0 0% 12%;
--primary: 240 5.9% 10%; --primary: 38 92% 50%;
--primary-foreground: 0 0% 98%; --primary-foreground: 0 0% 4%;
--secondary: 240 4.8% 95.9%; --secondary: 0 0% 10%;
--secondary-foreground: 240 5.9% 10%; --secondary-foreground: 0 0% 96%;
--accent: 240 4.8% 95.9%; --accent: 38 92% 50%;
--accent-foreground: 240 5.9% 10%; --accent-foreground: 0 0% 4%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--ring: 240 5% 64.9%; --ring: 38 92% 50%;
--radius: 0.5rem; --radius: 0;
--sw-bg: #080808;
--sw-bg-raised: #0c0c0c;
--sw-bg-surface: #111111;
--sw-amber: #f59e0b;
--sw-amber-light: #fbbf24;
--sw-amber-dim: rgba(245, 158, 11, 0.15);
--sw-text: #f5f5f5;
--sw-text-muted: #a3a3a3;
--sw-border: rgba(255, 255, 255, 0.06);
} }
.dark { .dark {
--background: 240 10% 3.9%; --background: 0 0% 3.1%;
--foreground: 0 0% 98%; --foreground: 0 0% 96%;
--muted: 240 3.7% 15.9%; --muted: 0 0% 10%;
--muted-foreground: 240 5% 64.9%; --muted-foreground: 0 0% 55%;
--popover: 240 10% 3.9%; --popover: 0 0% 5%;
--popover-foreground: 0 0% 98%; --popover-foreground: 0 0% 96%;
--card: 240 10% 3.9%; --card: 0 0% 5%;
--card-foreground: 0 0% 98%; --card-foreground: 0 0% 96%;
--border: 240 3.7% 15.9%; --border: 0 0% 12%;
--input: 240 3.7% 15.9%; --input: 0 0% 12%;
--primary: 0 0% 98%; --primary: 38 92% 50%;
--primary-foreground: 240 5.9% 10%; --primary-foreground: 0 0% 4%;
--secondary: 240 3.7% 15.9%; --secondary: 0 0% 10%;
--secondary-foreground: 0 0% 98%; --secondary-foreground: 0 0% 96%;
--accent: 240 3.7% 15.9%; --accent: 38 92% 50%;
--accent-foreground: 0 0% 98%; --accent-foreground: 0 0% 4%;
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%; --destructive-foreground: 0 85.7% 97.3%;
--ring: 240 3.7% 15.9%; --ring: 38 92% 50%;
} }
} }
@@ -93,6 +103,13 @@
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
background-color: var(--sw-bg);
color: var(--sw-text);
}
::selection {
background: rgba(245, 158, 11, 0.3);
color: #fff;
} }
article { article {
@@ -101,15 +118,26 @@
} }
code { code {
@apply dark:text-neutral-400 text-neutral-800; color: var(--sw-amber-light);
background: rgba(245, 158, 11, 0.08);
padding: 0.15em 0.4em;
} }
pre.astro-code { pre.astro-code {
@apply w-fit p-4 rounded-md border-2 border-gray-600 my-4; @apply w-fit p-4 my-4;
border: 1px solid var(--sw-border);
background: var(--sw-bg) !important;
} }
a { a {
@apply text-neutral-800 dark:text-neutral-400 hover:underline; color: var(--sw-amber);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s ease;
}
a:hover {
border-bottom-color: var(--sw-amber);
} }
} }
} }

View File

@@ -18,56 +18,137 @@
*/ */
.btn { .btn {
@apply bg-yellow-400 font-bold py-2 px-4 rounded cursor-pointer select-none mx-0.5 md:mx-2 text-black flex flex-row; display: inline-flex;
@apply hover:bg-yellow-300 hover:text-black hover:shadow-2xl hover:scale-105; align-items: center;
transition: all 0.5s cubic-bezier(.2, 3, .67, .6), gap: 0.4rem;
background-color .1s ease-in-out, font-family: "Barlow Condensed", sans-serif;
outline-width .1s ease-in-out, font-weight: 700;
outline-color .1s ease-in-out; font-size: 0.8rem;
@apply active:scale-90; letter-spacing: 0.12em;
text-transform: uppercase;
padding: 0.5rem 1.2rem;
color: #f5f5f5;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
cursor: pointer;
user-select: none;
transition: all 0.25s ease;
clip-path: polygon(
0 0,
calc(100% - 8px) 0,
100% 8px,
100% 100%,
8px 100%,
0 calc(100% - 8px)
);
.btn__text { .btn__text {
@apply inline-block; display: inline-block;
} }
} }
.btn:hover {
background: rgba(245, 158, 11, 0.08);
border-color: rgba(245, 158, 11, 0.4);
color: #fbbf24;
}
.btn:active {
transform: scale(0.96);
}
.btn-dropdown { .btn-dropdown {
@apply relative mx-0.5 md:mx-2; position: relative;
> :nth-child(1) { > :nth-child(1) {
@apply !mx-0;
} }
> :nth-child(2) { > :nth-child(2) {
@apply hidden absolute top-full left-1/2 -translate-x-1/2 bg-gray-800 list-none text-white rounded py-2 flex-col text-sm z-20; display: none;
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
background: #111111;
border: 1px solid rgba(255, 255, 255, 0.06);
list-style: none;
padding: 0.5rem 0;
flex-direction: column;
font-size: 0.8rem;
z-index: 200;
min-width: 10rem;
}
> :nth-child(2) .btn {
clip-path: none;
width: 100%;
justify-content: flex-start;
border: none;
border-bottom: 1px solid rgba(255, 255, 255, 0.03);
} }
&:hover, &:focus-within { &:hover, &:focus-within {
> :nth-child(2) { > :nth-child(2) {
@apply flex; display: flex;
} }
} }
} }
.btn-ghost { .btn-ghost {
@apply px-6 text-xl; padding: 0.8rem 2.8rem;
@apply bg-transparent border-2 border-yellow-400 shadow-2xl shadow-yellow-400 backdrop-blur text-neutral-800 font-bold bg-gradient-to-br from-yellow-400 to-yellow-200 rounded-xl; font-size: 0.85rem;
@apply hover:border-yellow-300 hover:shadow-yellow-800 hover:scale-110; letter-spacing: 0.2em;
color: #fbbf24;
background: rgba(245, 158, 11, 0.04);
border: 1px solid rgba(245, 158, 11, 0.5);
backdrop-filter: blur(12px);
clip-path: polygon(
0 0,
calc(100% - 14px) 0,
100% 14px,
100% 100%,
14px 100%,
0 calc(100% - 14px)
);
}
.btn-ghost:hover {
background: rgba(245, 158, 11, 0.12);
border-color: rgba(245, 158, 11, 0.85);
color: #fff;
box-shadow: 0 0 40px rgba(245, 158, 11, 0.1);
transform: scale(1.04);
} }
.btn-gray { .btn-gray {
@apply bg-gray-800 text-white; background: transparent;
border-color: rgba(255, 255, 255, 0.06);
color: rgba(212, 212, 212, 0.9);
}
.btn-gray:hover {
background: rgba(245, 158, 11, 0.06);
border-color: rgba(245, 158, 11, 0.3);
color: #fbbf24;
} }
.btn-neutral { .btn-neutral {
@apply bg-neutral-800 text-white; background: rgba(255, 255, 255, 0.04);
border-color: rgba(255, 255, 255, 0.08);
color: #f5f5f5;
} }
.btn-text { .btn-text {
@apply bg-transparent underline text-white; background: transparent;
@apply hover:bg-transparent hover:outline hover:outline-1; border: none;
clip-path: none;
.btn__text { color: #f59e0b;
@apply underline; text-decoration: none;
} border-bottom: 1px solid transparent;
}
.btn-text:hover {
background: transparent;
border-bottom-color: #f59e0b;
color: #fbbf24;
} }

View File

@@ -18,20 +18,42 @@
*/ */
table { table {
@apply w-full overflow-clip; width: 100%;
overflow: clip;
:not(:has([data-no-head])) { border-collapse: collapse;
}
thead { thead {
border-bottom: 1px solid white; border-bottom: 2px solid rgba(245, 158, 11, 0.3);
font-family: "Barlow Condensed", sans-serif;
font-size: 0.7rem;
letter-spacing: 0.15em;
text-transform: uppercase;
color: #f59e0b;
}
thead th {
padding: 0.6rem 0.8rem;
text-align: left;
} }
tbody { tbody {
text-align: center; text-align: center;
}
tr:nth-child(odd) { tbody td {
@apply backdrop-brightness-125; padding: 0.5rem 0.8rem;
} border-bottom: 1px solid rgba(255, 255, 255, 0.03);
}
tbody tr {
transition: background 0.2s ease;
}
tbody tr:nth-child(odd) {
background: rgba(255, 255, 255, 0.02);
}
tbody tr:hover {
background: rgba(245, 158, 11, 0.04);
} }
} }

View File

@@ -17,25 +17,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {l as proxyL} from "astro-i18n"; import { l as proxyL } from "astro-i18n";
const locales = [ export const l = proxyL;
"en",
"de",
];
export const l = (route: string) => {
const transPath = proxyL(route);
if (import.meta.env.DEV) {
return transPath;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, locale, ...rest] = transPath.split("/");
if (locales.includes(locale)) {
return "/" + rest.join("/");
}
return transPath;
};

View File

@@ -74,7 +74,8 @@ const config = {
sm: "calc(var(--radius) - 4px)", sm: "calc(var(--radius) - 4px)",
}, },
fontFamily: { fontFamily: {
sans: [...fontFamily.sans], sans: ["Roboto", ...fontFamily.sans],
display: ["Barlow Condensed", ...fontFamily.sans],
}, },
}, },
}, },