Dieser Commit ist enthalten in:
Chaoscaot 2023-10-08 14:34:38 +02:00
Ursprung 51a605ffa5
Commit 48961abdf6
17 geänderte Dateien mit 296 neuen und 35 gelöschten Zeilen

Datei anzeigen

@ -28,16 +28,17 @@
"tailwindcss": "^3.3.2" "tailwindcss": "^3.3.2"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-markdown": "^6.2.2",
"@ddietr/codemirror-themes": "^1.4.2",
"astro": "^3.1.1", "astro": "^3.1.1",
"color": "^4.2.3", "color": "^4.2.3",
"crypto-js": "^4.1.1",
"flowbite": "^1.7.0", "flowbite": "^1.7.0",
"flowbite-svelte": "^0.39.2", "flowbite-svelte": "^0.39.2",
"flowbite-svelte-icons": "^0.2.5", "flowbite-svelte-icons": "^0.2.5",
"gitea-js": "^1.20.1",
"moment": "^2.29.4", "moment": "^2.29.4",
"sharp": "^0.32.6", "sharp": "^0.32.6",
"svelte-awesome": "^3.2.0", "svelte-awesome": "^3.2.0",
"svelte-codemirror-editor": "^1.1.0",
"svelte-spa-router": "^3.3.0", "svelte-spa-router": "^3.3.0",
"zod": "^3.21.4" "zod": "^3.21.4"
} }

Datei anzeigen

@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

Vorher

Breite:  |  Höhe:  |  Größe: 749 B

Datei anzeigen

@ -11,6 +11,7 @@
'/login': wrap({asyncComponent: () => import('./pages/Login.svelte'), conditions: detail => get(tokenStore) == ""}), '/login': wrap({asyncComponent: () => import('./pages/Login.svelte'), conditions: detail => get(tokenStore) == ""}),
'/event/:id': wrap({asyncComponent: () => import('./pages/Event.svelte'), conditions: detail => get(tokenStore) != ""}), '/event/:id': wrap({asyncComponent: () => import('./pages/Event.svelte'), conditions: detail => get(tokenStore) != ""}),
'/event/:id/generate': wrap({asyncComponent: () => import('./pages/Generate.svelte'), conditions: detail => get(tokenStore) != ""}), '/event/:id/generate': wrap({asyncComponent: () => import('./pages/Generate.svelte'), conditions: detail => get(tokenStore) != ""}),
'/edit': wrap({asyncComponent: () => import('./pages/Edit.svelte'), conditions: detail => get(tokenStore) != ""}),
'*': wrap({asyncComponent: () => import('./pages/NotFound.svelte')}) '*': wrap({asyncComponent: () => import('./pages/NotFound.svelte')})
} }

Datei anzeigen

@ -1,14 +1,13 @@
<script lang="ts"> <script lang="ts">
import {Button, Dropdown, DropdownItem, Search} from 'flowbite-svelte' import {Button, Dropdown, DropdownItem, Search} from 'flowbite-svelte'
export let selected: string = null export let selected: string | null = null
export let items: {name: string, value: string}[] = [] export let items: {name: string, value: string}[] = []
export let searchValue = items.find(item => item.value === selected)?.name || '' export let searchValue = items.find(item => item.value === selected)?.name || ''
let open = false let open = false
$: filteredItems = items.filter(item => item.name.toLowerCase().includes(searchValue.toLowerCase())).filter((value, index) => index < 5) $: filteredItems = items.filter(item => item.name.toLowerCase().includes(searchValue.toLowerCase())).filter((value, index) => index < 5)
$: console.log(selected)
function selectItem(item: {name: string, value: string}) { function selectItem(item: {name: string, value: string}) {
selected = item.value selected = item.value

Datei anzeigen

@ -0,0 +1,107 @@
<script lang="ts">
import {ArrowLeftSolid} from "flowbite-svelte-icons";
import {Button, Card, Input, Label, Navbar, NavBrand, NavHamburger, NavUl, Spinner} from "flowbite-svelte";
import {pageRepo} from "../repo/repo.js";
import {mapToMap, nameRegex} from "../util.ts";
import Editor from "./edit/Editor.svelte";
import TypeAheadSearch from "../components/TypeAheadSearch.svelte";
import {branches} from "../stores/stores.ts";
let pagesFuture = $pageRepo.listPages();
let selected: number | null = null;
let selectedBranch: string = "master";
let searchValue: string = "";
$: availableBranches = $branches.map((branch) => ({
name: branch,
value: branch
}))
$: console.log(availableBranches)
async function createBranch() {
const name = prompt("Branch name:")
if (name) {
await $pageRepo.createBranch(name)
let inter = setInterval(() => {
branches.reload()
if ($branches.includes(name)) {
selectedBranch = name
searchValue = ""
clearInterval(inter)
}
}, 1000)
}
}
async function deleteBranch() {
if (selectedBranch !== "master") {
let conf = confirm("Are you sure you want to delete this branch?")
if(conf) {
await $pageRepo.deleteBranch(selectedBranch)
let inter = setInterval(() => {
branches.reload()
if (!$branches.includes(selectedBranch)) {
selectedBranch = "master"
searchValue = ""
clearInterval(inter)
}
}, 1000)
}
} else {
alert("You can't delete the master branch")
}
}
</script>
<div class="flex flex-col h-screen overflow-scroll">
<Navbar let:hidden let:toggle>
<NavBrand href="#">
<ArrowLeftSolid></ArrowLeftSolid>
<span class="ml-4 self-center whitespace-nowrap text-xl font-semibold dark:text-white">
Edit Pages
</span>
</NavBrand>
</Navbar>
<div class="p-4 flex-1">
<div class="grid md:grid-cols-3 grid-cols-1 h-full gap-8">
<Card class="h-full flex flex-col !max-w-full">
{#await pagesFuture}
<Spinner />
{:then pages}
<div class="border-b border-b-gray-600 pb-2 flex justify-between">
<TypeAheadSearch items={availableBranches} bind:selected={selectedBranch} bind:searchValue />
<div>
<Button on:click={deleteBranch} color="ghost">Delete Branch</Button>
<Button on:click={createBranch}>Create Branch</Button>
</div>
</div>
{@const pagesMap = mapToMap(pages)}
{#each pagesMap as [key, value]}
<details>
<summary class="p-4 transition-colors hover:bg-gray-700 cursor-pointer">{key}</summary>
<ul>
{#each value as page}
{@const match = nameRegex.exec(page.path) ? nameRegex.exec(page.path)[0] : ""}
{@const startIndex = page.path.indexOf(match)}
{@const endIndex = startIndex + match.length}
<li class="p-4 transition-colors hover:bg-gray-700 cursor-pointer" on:click|preventDefault={() => selected = page.id}>
<span class:text-orange-600={selected === page.id}>{page.path.substring(0, startIndex)}</span><span class="text-white" class:!text-orange-500={selected === page.id}>{match}</span><span class:text-orange-600={selected === page.id}>{page.path.substring(endIndex, page.path.length)}</span>
</li>
{/each}
</ul>
</details>
{/each}
{:catch error}
<p>{error.message}</p>
{/await}
</Card>
<Card class="!max-w-full" style="grid-column: 2/4">
{#if selected}
<Editor pageId={selected} branch={selectedBranch} />
{/if}
</Card>
</div>
</div>
</div>

Datei anzeigen

@ -21,11 +21,12 @@
<Navbar let:hidden let:toggle class="shadow-lg border-b"> <Navbar let:hidden let:toggle class="shadow-lg border-b">
<NavBrand href="#"> <NavBrand href="#">
<span class="self-center whitespace-nowrap text-xl font-semibold dark:text-white"> <span class="self-center whitespace-nowrap text-xl font-semibold dark:text-white">
Eventplanner Admin-Tool
</span> </span>
</NavBrand> </NavBrand>
<NavHamburger on:click={toggle} /> <NavHamburger on:click={toggle} />
<NavUl {hidden}> <NavUl {hidden}>
<NavLi href="#/edit">Edit Pages</NavLi>
<NavLi href="#/perms">Permissions</NavLi> <NavLi href="#/perms">Permissions</NavLi>
<NavLi on:click={() => showLogoutModal = true} class="cursor-pointer select-none">Logout</NavLi> <NavLi on:click={() => showLogoutModal = true} class="cursor-pointer select-none">Logout</NavLi>
</NavUl> </NavUl>

Datei anzeigen

@ -3,7 +3,7 @@
import {fly} from "svelte/transition"; import {fly} from "svelte/transition";
import {replace} from "svelte-spa-router"; import {replace} from "svelte-spa-router";
import {EyeOutline, EyeSlashOutline, EyeSolid} from "flowbite-svelte-icons"; import {EyeOutline, EyeSlashOutline, EyeSolid} from "flowbite-svelte-icons";
import {tokenStore} from "../repo/repo.js"; import {fetchWithToken, tokenStore} from "../repo/repo.js";
let show = false; let show = false;
let loading = false; let loading = false;
@ -12,7 +12,7 @@
async function handleSubmit() { async function handleSubmit() {
loading = true; loading = true;
let res = await fetch("https://steamwar.de/eventplanner-api/data", {headers: {"X-SW-Auth": value}}) let res = await fetchWithToken(value, "/data")
loading = false; loading = false;
if(res.ok) { if(res.ok) {
$tokenStore = value; $tokenStore = value;

Datei anzeigen

@ -0,0 +1,50 @@
<script lang="ts">
import {Spinner, Toolbar, ToolbarButton, ToolbarGroup, Tooltip} from "flowbite-svelte";
import {markdown} from "@codemirror/lang-markdown";
import CodeMirror from "svelte-codemirror-editor";
import {pageRepo} from "../../repo/repo.ts";
import {base64ToBytes} from "../../util.ts";
import type {Page} from "../../types/page.ts";
import {materialDark} from '@ddietr/codemirror-themes/material-dark.js'
import {EditOutline} from "flowbite-svelte-icons";
export let pageId: number;
export let branch: string;
$: pageFuture = $pageRepo.getPage(pageId).then(getPage);
let pageContent = "";
let page: Page | null = null;
function getPage(value: Page): Page {
page = value;
pageContent = new TextDecoder().decode(base64ToBytes(value.content));
return value;
}
function savePage() {
let message = window.prompt("Commit message:")
if (message) {
$pageRepo.updatePage(pageId, pageContent, page!.sha, message)
} else {
alert("Commit message is required")
}
}
</script>
{#await pageFuture}
<Spinner />
{:then p}
<div>
<div>
<Toolbar class="!bg-gray-900">
<ToolbarGroup slot="end">
<ToolbarButton color="primary" on:click={savePage}>
Save
</ToolbarButton>
</ToolbarGroup>
</Toolbar>
</div>
<CodeMirror bind:value={pageContent} lang={markdown()} theme={materialDark} />
</div>
{:catch error}
<p>{error.message}</p>
{/await}

Datei anzeigen

@ -0,0 +1,44 @@
import type {Page, PageList} from "../types/page.ts";
import {fetchWithToken} from "./repo.ts";
import {PageListSchema, PageSchema} from "../types/page.ts";
import {bytesToBase64} from "../util.ts";
import {branches} from "../stores/stores.ts";
export class PageRepo {
constructor(private token: string) {}
public async listPages(branch: string = "master"): Promise<PageList> {
return await fetchWithToken(this.token, `/page?branch=${branch}`)
.then(value => value.json())
.then(value => PageListSchema.parse(value))
}
public async getPage(id: number, branch: string = "master"): Promise<Page> {
return await fetchWithToken(this.token, `/page/${id}?branch=${branch}`)
.then(value => value.json())
.then(value => PageSchema.parse(value))
}
public async updatePage(id: number, content: string, sha: string, message: string, branch: string = "master"): Promise<void> {
await fetchWithToken(this.token, `/page/${id}?branch=${branch}`, {
method: "PUT",
body: JSON.stringify({
content: bytesToBase64(new TextEncoder().encode(content)),
sha, message
})
})
}
public async getBranches(): Promise<string[]> {
return await fetchWithToken(this.token, "/page/branch")
.then(value => value.json())
}
public async createBranch(branch: string): Promise<void> {
await fetchWithToken(this.token, `/page/branch`, {method: "POST", body: JSON.stringify({branch})})
}
public async deleteBranch(branch: string): Promise<void> {
await fetchWithToken(this.token, `/page/branch`, {method: "DELETE", body: JSON.stringify({branch})})
}
}

Datei anzeigen

@ -2,14 +2,17 @@ import {derived, writable} from "svelte/store";
import {EventRepo} from "./event.js"; import {EventRepo} from "./event.js";
import {FightRepo} from "./fight.js"; import {FightRepo} from "./fight.js";
import {PermsRepo} from "./perms.js"; import {PermsRepo} from "./perms.js";
import {PageRepo} from "./page.ts";
export { EventRepo } from "./event.js" export { EventRepo } from "./event.js"
export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) => fetch(`https://steamwar.de/eventplanner-api${url}`, {...params, headers: {"X-SW-Auth": token, "Content-Type": "application/json", ...params.headers}}); export const apiUrl = import.meta.env.PUBLIC_API_SERVER;
export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) => fetch(`${apiUrl}${url}`, {...params, headers: {"Authorization": "Bearer " + (token), "Content-Type": "application/json", ...params.headers}});
export const tokenStore = writable(localStorage.getItem("sw-api-token") ?? "") export const tokenStore = writable(localStorage.getItem("sw-api-token") ?? "")
tokenStore.subscribe((value) => localStorage.setItem("sw-api-token", value)) tokenStore.subscribe((value) => localStorage.setItem("sw-api-token", value))
export const eventRepo = derived(tokenStore, ($token) => new EventRepo($token)) export const eventRepo = derived(tokenStore, ($token) => new EventRepo($token))
export const fightRepo = derived(tokenStore, ($token) => new FightRepo($token)) export const fightRepo = derived(tokenStore, ($token) => new FightRepo($token))
export const permsRepo = derived(tokenStore, ($token) => new PermsRepo($token)) export const permsRepo = derived(tokenStore, ($token) => new PermsRepo($token))
export const pageRepo = derived(tokenStore, ($token) => new PageRepo($token))

Datei anzeigen

@ -29,7 +29,7 @@ export function cached<T>(normal: T, init: () => Promise<T>): Cached<T> {
}; };
} }
export function cachedFamily<T, K>(normal: K, init: (T) => Promise<K>): (T) => Cached<K> { export function cachedFamily<T, K>(normal: K, init: (arg0: T) => Promise<K>): (arg: T) => Cached<K> {
const stores: Map<T, Cached<K>> = new Map(); const stores: Map<T, Cached<K>> = new Map();
return (arg: T) => { return (arg: T) => {
if(stores.has(arg)) { if(stores.has(arg)) {

Datei anzeigen

@ -4,28 +4,27 @@ import {cached, cachedFamily} from "./cached.js";
import type {Team} from "../types/team.js"; import type {Team} from "../types/team.js";
import {TeamSchema} from "../types/team.js"; import {TeamSchema} from "../types/team.js";
import {get, writable} from "svelte/store"; import {get, writable} from "svelte/store";
import {permsRepo, tokenStore} from "../repo/repo.js"; import {apiUrl, fetchWithToken, pageRepo, permsRepo, tokenStore} from "../repo/repo.js";
import {z} from "zod"; import {z} from "zod";
export const schemTypes = cached<SchematicType[]>([], () => { export const schemTypes = cached<SchematicType[]>([], () =>
return fetch("https://steamwar.de/eventplanner-api/data/schematicTypes", {headers: {"X-SW-Auth": get(tokenStore)}}) fetchWithToken(get(tokenStore), `/data/schematicTypes`)
.then(res => res.json()) .then(res => res.json()))
})
export const players = cached<Player[]>([], async () => { export const players = cached<Player[]>([], async () => {
const res = await fetch("https://steamwar.de/eventplanner-api/data/users", {headers: {"X-SW-Auth": get(tokenStore)}}); const res = await fetchWithToken(get(tokenStore), `/data/users`);
return z.array(PlayerSchema).parse(await res.json()); return z.array(PlayerSchema).parse(await res.json());
}) })
export const gamemodes = cached<string[]>([], async () => { export const gamemodes = cached<string[]>([], async () => {
const res = await fetch("https://steamwar.de/eventplanner-api/data/gamemodes", {headers: {"X-SW-Auth": get(tokenStore)}}); const res = await fetchWithToken(get(tokenStore), `/data/gamemodes`);
return z.array(z.string()).parse(await res.json()); return z.array(z.string()).parse(await res.json());
}) })
export const maps = cachedFamily<string, string[]>([], async (gamemode) => { export const maps = cachedFamily<string, string[]>([], async (gamemode) => {
if (get(gamemodes).every(value => value !== gamemode)) return []; if (get(gamemodes).every(value => value !== gamemode)) return [];
const res = await fetch(`https://steamwar.de/eventplanner-api/data/gamemodes/${gamemode}/maps`, {headers: {"X-SW-Auth": get(tokenStore)}}); const res = await fetchWithToken(get(tokenStore), `/data/gamemodes/${gamemode}/maps`);
if (!res.ok) { if (!res.ok) {
return []; return [];
} else { } else {
@ -34,14 +33,19 @@ export const maps = cachedFamily<string, string[]>([], async (gamemode) => {
}) })
export const groups = cached<string[]>([], async () => { export const groups = cached<string[]>([], async () => {
const res = await fetch("https://steamwar.de/eventplanner-api/data/groups", {headers: {"X-SW-Auth": get(tokenStore)}}); const res = await fetchWithToken(get(tokenStore), `/data/groups`);
return z.array(z.string()).parse(await res.json()); return z.array(z.string()).parse(await res.json());
}) })
export const teams = cached<Team[]>([], async () => { export const teams = cached<Team[]>([], async () => {
const res = await fetch("https://steamwar.de/eventplanner-api/team", {headers: {"X-SW-Auth": get(tokenStore)}}); const res = await fetchWithToken(get(tokenStore), `/team`);
return z.array(TeamSchema).parse(await res.json()); return z.array(TeamSchema).parse(await res.json());
}) })
export const branches = cached<string[]>([], async () => {
const res = await get(pageRepo).getBranches();
return z.array(z.string()).parse(res);
})
export const isWide = writable(window.innerWidth >= 640); export const isWide = writable(window.innerWidth >= 640);
window.addEventListener("resize", () => isWide.set(window.innerWidth >= 640)); window.addEventListener("resize", () => isWide.set(window.innerWidth >= 640));

Datei anzeigen

@ -0,0 +1,27 @@
import {z} from "zod";
export const ListPageSchema = z.object({
path: z.string(),
name: z.string(),
sha: z.string(),
downloadUrl: z.string().url(),
id: z.number().positive()
});
export type ListPage = z.infer<typeof ListPageSchema>;
export const PageListSchema = z.array(ListPageSchema)
export type PageList = z.infer<typeof PageListSchema>;
export const PageSchema = z.object({
path: z.string(),
name: z.string(),
sha: z.string(),
downloadUrl: z.string().url(),
content: z.string(),
size: z.number().gte(0),
id: z.number().positive()
})
export type Page = z.infer<typeof PageSchema>;

Datei anzeigen

@ -1,8 +1,23 @@
import Color from "color"; import Color from "color";
import type {Team} from "./types/team.js"; import type {Team} from "./types/team.js";
import type {ListPage, PageList} from "./types/page.ts";
export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1); export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
export const nameRegex = new RegExp("(?!.*\/).+(?=\\.md)");
export function mapToMap(pages: PageList): Map<string, ListPage[]> {
const map = new Map();
for (const page of pages) {
let folder = page.path.substring(0, page.path.indexOf(nameRegex.exec(page.path)[0]));
if (!map.has(folder)) {
map.set(folder, []);
}
map.get(folder).push(page);
}
return map;
}
export function colorFromTeam(team: Team): string { export function colorFromTeam(team: Team): string {
switch (team.color) { switch (team.color) {
case "1": case "1":
@ -47,3 +62,14 @@ export function lighten(color: string) {
export function brightness(color: string) { export function brightness(color: string) {
return Color(color).isLight() return Color(color).isLight()
} }
export function base64ToBytes(base64: string) {
const binString = atob(base64);
// @ts-ignore
return Uint8Array.from(binString, (m) => m.codePointAt(0));
}
export function bytesToBase64(bytes: Uint8Array) {
const binString = String.fromCodePoint(...bytes);
return btoa(binString);
}

Datei anzeigen

@ -1,7 +1,11 @@
--- ---
import {astroI18n} from "astro-i18n"; import {astroI18n} from "astro-i18n";
import icon from '../images/logo.png';
import {getImage} from "astro:assets";
const { title, description } = Astro.props.frontmatter || Astro.props; const { title, description } = Astro.props.frontmatter || Astro.props;
const iconImage = await getImage({src: icon, height: 32, width: 32, format: 'png', quality: 100});
--- ---
<html lang={astroI18n.langCode} class="dark"> <html lang={astroI18n.langCode} class="dark">
@ -11,6 +15,7 @@ const { title, description } = Astro.props.frontmatter || Astro.props;
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="description" content={description}/> <meta name="description" content={description}/>
<link rel="icon" type="imgage/png" href={iconImage.src} />
<link rel="stylesheet" href="/fonts/barlow-condensed/barlow-condensed.css" /> <link rel="stylesheet" href="/fonts/barlow-condensed/barlow-condensed.css" />
<title>{title}</title> <title>{title}</title>
<slot name="head" /> <slot name="head" />

Datei anzeigen

@ -4,6 +4,7 @@ import { Image } from 'astro:assets';
import '../styles/button.css'; import '../styles/button.css';
import localLogo from "../images/logo.png" import localLogo from "../images/logo.png"
import {l, t} from "astro-i18n"; import {l, t} from "astro-i18n";
import {YoutubeSolid} from "flowbite-svelte-icons"
const { title } = Astro.props; const { title } = Astro.props;
--- ---
@ -13,7 +14,7 @@ const { title } = Astro.props;
<Fragment> <Fragment>
<div class="min-h-screen flex flex-col"> <div class="min-h-screen flex flex-col">
<nav-bar class="fixed top-0 left-0 right-0 px-4 transition-colors z-10 flex justify-center \ <nav-bar class="fixed top-0 left-0 right-0 px-4 transition-colors z-10 flex justify-center \
before:bg-transparent before:absolute before:top-0 before:left-0 before:bottom-0 before:right-0 before:bg-black before:-z-10 before:scale-y-0 before:transition-transform before:origin-top"> before:bg-black 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">
<div class="flex flex-col md:flex-row items-center justify-evenly md:justify-between match"> <div class="flex flex-col md:flex-row items-center justify-evenly md:justify-between match">
<a class="flex items-center" href={l("/")}> <a class="flex items-center" href={l("/")}>
<Image src={localLogo} alt={t("navbar.logo.alt")} width="44" height="44" quality="max" class="mr-2 p-1 bg-black rounded-full" /> <Image src={localLogo} alt={t("navbar.logo.alt")} width="44" height="44" quality="max" class="mr-2 p-1 bg-black rounded-full" />
@ -100,7 +101,7 @@ const { title } = Astro.props;
</div> </div>
<div class="footer-card"> <div class="footer-card">
<h1>Links</h1> <h1>Links</h1>
<a href={l("/")}>Startseite</a> <a href={l("/")}>{t("navbar.links.home.title")}</a>
<a href={l("/join")}>Join Now</a> <a href={l("/join")}>Join Now</a>
<a href={l("/")}>Announcements</a> <a href={l("/")}>Announcements</a>
<a href={l("/")}>Gamemodes</a> <a href={l("/")}>Gamemodes</a>
@ -111,10 +112,10 @@ const { title } = Astro.props;
</div> </div>
<div class="footer-card"> <div class="footer-card">
<h1>Social Media</h1> <h1>Social Media</h1>
<a>YouTube</a> <a class="flex" href="/"><YoutubeSolid class="mr-2" /> YouTube</a>
</div> </div>
</div> </div>
<span class="text-sm text-white text-center">© SteamWar.de</span> <span class="text-sm text-white text-center">© SteamWar.de - {new Date().getFullYear()}</span>
</footer> </footer>
</div> </div>
</Fragment> </Fragment>

Datei anzeigen

@ -2,7 +2,7 @@
import NavbarLayout from "../layouts/NavbarLayout.astro"; import NavbarLayout from "../layouts/NavbarLayout.astro";
import { Image } from "astro:assets"; import { Image } from "astro:assets";
import localBau from "../images/2022-03-28_13.18.25.png"; import localBau from "../images/bau.jpg";
import {l, t} from "astro-i18n"; import {l, t} from "astro-i18n";
import {CaretRight, Archive, Rocket, Bell} from "@astropub/icons" import {CaretRight, Archive, Rocket, Bell} from "@astropub/icons"
--- ---
@ -121,8 +121,9 @@ import {CaretRight, Archive, Rocket, Bell} from "@astropub/icons"
} }
.card { .card {
@apply w-72 border-2 bg-zinc-50 border-gray-100 flex flex-col items-center p-8 m-4 rounded-xl shadow-lg @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
dark:bg-zinc-900 dark:border-gray-800 dark:text-gray-100; dark:bg-zinc-900 dark:border-gray-800 dark:text-gray-100
hover:scale-105;
>h1 { >h1 {
@apply text-xl font-bold underline mt-4; @apply text-xl font-bold underline mt-4;
} }