New Dashboard

This commit is contained in:
2025-03-01 20:00:46 +01:00
parent bccd5eb5a0
commit 86d90e3fd2
33 changed files with 2148 additions and 266 deletions

View File

@ -7,7 +7,6 @@ import sitemap from "@astrojs/sitemap";
import robotsTxt from "astro-robots-txt"; import robotsTxt from "astro-robots-txt";
import path from "node:path"; import path from "node:path";
import mdx from "@astrojs/mdx"; import mdx from "@astrojs/mdx";
import pagefind from "astro-pagefind";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
@ -20,9 +19,8 @@ export default defineConfig({
integrations: [ integrations: [
svelte(), svelte(),
tailwind({ tailwind({
configFile: "./tailwind.config.cjs", configFile: "./tailwind.config.js",
}), }),
pagefind(),
configureI18n(), configureI18n(),
sitemap({ sitemap({
i18n: { i18n: {
@ -68,6 +66,7 @@ export default defineConfig({
"@layouts": path.resolve("./src/layouts"), "@layouts": path.resolve("./src/layouts"),
"@repo": path.resolve("./src/components/repo"), "@repo": path.resolve("./src/components/repo"),
"@stores": path.resolve("./src/components/stores"), "@stores": path.resolve("./src/components/stores"),
"$lib": path.resolve("./src"),
}, },
}, },
}, },

View File

@ -20,25 +20,38 @@
"@astrojs/svelte": "^7.0.4", "@astrojs/svelte": "^7.0.4",
"@astrojs/tailwind": "^5.1.5", "@astrojs/tailwind": "^5.1.5",
"@astropub/icons": "^0.2.0", "@astropub/icons": "^0.2.0",
"@internationalized/date": "^3.7.0",
"@types/color": "^4.2.0", "@types/color": "^4.2.0",
"@types/node": "^22.9.3", "@types/node": "^22.9.3",
"@types/three": "^0.170.0", "@types/three": "^0.170.0",
"@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0", "@typescript-eslint/parser": "^8.15.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bits-ui": "1.3.4",
"clsx": "^2.1.1",
"cmdk-sv": "^0.0.18",
"cssnano": "^7.0.6", "cssnano": "^7.0.6",
"embla-carousel-svelte": "^8.5.2",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"eslint": "^9.15.0", "eslint": "^9.15.0",
"eslint-plugin-astro": "^1.3.1", "eslint-plugin-astro": "^1.3.1",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-svelte": "^2.46.0", "eslint-plugin-svelte": "^2.46.0",
"formsnap": "1.0.1",
"lucide-svelte": "^0.476.0",
"mode-watcher": "^0.5.1",
"paneforge": "^0.0.6",
"postcss-nesting": "^13.0.1", "postcss-nesting": "^13.0.1",
"sass": "^1.81.0", "sass": "^1.81.0",
"svelte": "^5.16.0", "svelte": "^5.16.0",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.15",
"three": "^0.170.0", "three": "^0.170.0",
"typescript": "^5.7.2" "typescript": "^5.7.2",
"vaul-svelte": "^0.3.2",
"zod": "^3.23.8"
}, },
"dependencies": { "dependencies": {
"@astrojs/mdx": "^4.0.7", "@astrojs/mdx": "^4.0.7",
@ -46,9 +59,9 @@
"@codemirror/commands": "^6.8.0", "@codemirror/commands": "^6.8.0",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@ddietr/codemirror-themes": "^1.4.4", "@ddietr/codemirror-themes": "^1.4.4",
"@tanstack/table-core": "^8.21.2",
"astro": "^5.1.8", "astro": "^5.1.8",
"astro-i18n": "^2.2.4", "astro-i18n": "^2.2.4",
"astro-pagefind": "^1.6.0",
"astro-robots-txt": "^1.0.0", "astro-robots-txt": "^1.0.0",
"astro-seo": "^0.8.4", "astro-seo": "^0.8.4",
"chart.js": "^4.4.6", "chart.js": "^4.4.6",
@ -64,7 +77,6 @@
"sharp": "^0.33.5", "sharp": "^0.33.5",
"svelte-awesome": "^3.3.5", "svelte-awesome": "^3.3.5",
"svelte-codemirror-editor": "^1.4.1", "svelte-codemirror-editor": "^1.4.1",
"svelte-spa-router": "^4.0.1", "svelte-spa-router": "^4.0.1"
"zod": "^3.23.8"
} }
} }

1022
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import {window} from "./util.ts"; import {window} from "./utils.ts";
import {astroI18n, t} from "astro-i18n"; import {astroI18n, t} from "astro-i18n";
import type {EventFight, ExtendedEvent} from "@type/event"; import type {EventFight, ExtendedEvent} from "@type/event";
import "@styles/table.css"; import "@styles/table.css";

View File

@ -19,7 +19,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import {window} from "./util.ts"; import {window} from "./utils.ts";
import {t} from "astro-i18n"; import {t} from "astro-i18n";
import type {ExtendedEvent} from "@type/event.ts"; import type {ExtendedEvent} from "@type/event.ts";
import "@styles/table.css" import "@styles/table.css"

View File

@ -38,6 +38,7 @@
</NavBrand> </NavBrand>
<NavHamburger onclick={toggle}/> <NavHamburger onclick={toggle}/>
<NavUl {hidden}> <NavUl {hidden}>
<NavLi href="/admin/new">New UI</NavLi>
<NavLi href="#/edit">Edit Pages</NavLi> <NavLi href="#/edit">Edit Pages</NavLi>
<NavLi href="#/perms">Permissions</NavLi> <NavLi href="#/perms">Permissions</NavLi>
</NavUl> </NavUl>

View File

@ -34,7 +34,7 @@
dirty?: boolean; dirty?: boolean;
} }
let { pageId, branch, dirty = $bindable(false) }: Props = $props(); let { pageId, branch = $bindable(), dirty = $bindable(false) }: Props = $props();
let dispatcher = createEventDispatcher(); let dispatcher = createEventDispatcher();
@ -97,7 +97,7 @@
{#if page?.name.endsWith("md") || page?.name.endsWith("mdx")} {#if page?.name.endsWith("md") || page?.name.endsWith("mdx")}
<MDEMarkdownEditor bind:value={pageContent} bind:dirty/> <MDEMarkdownEditor bind:value={pageContent} bind:dirty/>
{:else} {:else}
<CodeMirror bind:value={pageContent} lang={json()} theme={materialDark} on:change={() => dirty = true}/> <CodeMirror bind:value={pageContent} lang={json()} theme={materialDark} onchange={() => dirty = true}/>
{/if} {/if}
</div> </div>
{:catch error} {:catch error}

View File

@ -0,0 +1,56 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import type {RouteDefinition} from "svelte-spa-router";
import Router from "svelte-spa-router";
import NavLinks from "@components/moderator/layout/NavLinks.svelte";
import {Switch} from "@components/ui/switch";
import {Label} from "@components/ui/label";
import {navigate} from "astro:transitions/client";
import Players from "@components/moderator/pages/players/Players.svelte";
import Events from "@components/moderator/pages/events/Events.svelte";
import Dashboard from "@components/moderator/pages/dashboard/Dashboard.svelte";
import Event from "@components/moderator/pages/event/Event.svelte";
const routes: RouteDefinition = {
"/": Dashboard,
"/events": Events,
"/players": Players,
"/event/:id": Event
};
</script>
<div class="flex flex-col bg-background min-w-full min-h-screen">
<div class="border-b">
<div class="flex h-16 items-center px-4">
<a href="/" class="text-sm font-bold transition-colors text-primary">
SteamWar
</a>
<NavLinks />
<div class="ml-auto flex items-center space-x-4">
<Switch id="new-ui-switch" checked={true} on:click={() => navigate("/admin")} />
<Label for="new-ui-switch">New UI!</Label>
</div>
</div>
</div>
<main class="flex flex-col">
<Router {routes} />
</main>
</div>

View File

@ -0,0 +1,42 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import type {ShortEvent} from "@type/event.ts";
import {Card, CardContent, CardHeader, CardTitle} from "@components/ui/card";
let { event }: { event: ShortEvent } = $props();
let sameDate = $derived(new Intl.DateTimeFormat().format(event.start) === new Intl.DateTimeFormat().format(event.end));
</script>
<Card>
<CardHeader>
<CardTitle>{event.name}</CardTitle>
</CardHeader>
<CardContent>
{#if !sameDate}
<p>Startet: {new Intl.DateTimeFormat().format(event.start)}</p>
<p>Endet: {new Intl.DateTimeFormat().format(event.end)}</p>
{:else}
<p>Am: {new Intl.DateTimeFormat().format(event.start)}</p>
<p>&nbsp;</p>
{/if}
</CardContent>
</Card>

View File

@ -0,0 +1,40 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {location} from "svelte-spa-router";
</script>
<nav class="flex items-center space-x-4 lg:space-x-6 mx-6">
<a href="#/" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/"}>
Dashboard
</a>
<a href="#/events" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/events"}>
Events
</a>
<a href="#/players" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/players"}>
Players
</a>
<a href="#/pages" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/pages"}>
Pages
</a>
<a href="#/schematics" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/schematics"}>
Schematics
</a>
</nav>

View File

@ -0,0 +1,22 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<div class="p-4">
<h1 class="font-bold text-xl">SteamWar Dashboard</h1>
</div>

View File

@ -0,0 +1,38 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {eventRepo} from "@repo/event.ts";
import EventView from "@components/moderator/pages/event/EventView.svelte";
interface Props {
params: { id: number };
}
let { params }: Props = $props();
let id = params.id;
let event = $eventRepo.getEvent(id.toString());
</script>
{#await event}
<p>Loading...</p>
{:then data}
<EventView event={data} />
{/await}

View File

@ -0,0 +1,128 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {Input} from "@components/ui/input";
import {Label} from "@components/ui/label";
import {Popover, PopoverContent, PopoverTrigger} from "@components/ui/popover";
import type {SWEvent} from "@type/event.ts"
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
import {fromAbsolute} from "@internationalized/date";
import {Button} from "@components/ui/button";
import {ChevronsUpDown} from "lucide-svelte";
import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList} from "@components/ui/command";
import {schemTypes} from "@stores/stores.ts";
import Check from "lucide-svelte/icons/check";
import {cn} from "@components/utils.ts";
import {Switch} from "@components/ui/switch";
import {eventRepo} from "@repo/event.ts";
const { event }: { event: SWEvent } = $props();
let rootEvent: SWEvent = $state(event)
let eventName = $state(rootEvent.name);
let eventDeadline = $state(fromAbsolute(rootEvent.deadline, "Europe/Berlin"));
let eventStart = $state(fromAbsolute(rootEvent.start, "Europe/Berlin"));
let eventEnd = $state(fromAbsolute(rootEvent.end, "Europe/Berlin"));
let eventTeamSize = $state(rootEvent.maxTeamMembers);
let eventSchematicType = $state(rootEvent.schemType);
let eventPublicsOnly = $state(rootEvent.publicSchemsOnly);
let dirty = $derived(eventName !== rootEvent.name ||
eventDeadline.toDate().getTime() !== rootEvent.deadline ||
eventStart.toDate().getTime() !== rootEvent.start ||
eventEnd.toDate().getTime() !== rootEvent.end ||
eventTeamSize !== rootEvent.maxTeamMembers ||
eventSchematicType !== rootEvent.schemType ||
eventPublicsOnly !== rootEvent.publicSchemsOnly);
async function updateEvent() {
rootEvent = await $eventRepo.updateEvent(event.id.toString(), {
name: eventName,
deadline: eventDeadline.toDate().getTime(),
start: eventStart.toDate().getTime(),
end: eventEnd.toDate().getTime(),
maxTeamMembers: eventTeamSize,
schemType: eventSchematicType,
publicSchemsOnly: eventPublicsOnly,
})
}
</script>
<div class="flex flex-col gap-2">
<Label for="event-name">Name</Label>
<Input id="event-name" bind:value={eventName} />
<Label>Deadline</Label>
<DateTimePicker bind:value={eventDeadline} />
<Label>Start</Label>
<DateTimePicker bind:value={eventStart} />
<Label>End</Label>
<DateTimePicker bind:value={eventEnd} />
<Label for="event-size">Teamsize</Label>
<Input id="event-size" bind:value={eventTeamSize} type="number" />
<Label>Schematic Type</Label>
<Popover>
<PopoverTrigger>
{#snippet child({ props })}
<Button
variant="outline"
class="justify-between"
{...props}
role="combobox"
>
{$schemTypes.find(value => value.db === eventSchematicType)?.name || eventSchematicType || "Select a schematic type..."}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search schematic types..." />
<CommandList>
<CommandEmpty>No schematic type found.</CommandEmpty>
<CommandGroup>
{#each $schemTypes as type}
<CommandItem
value={type.db}
onSelect={() => {
eventSchematicType = type.db;
}}
>
<Check
class={cn(
"mr-2 size-4",
eventSchematicType !== type.db && "text-transparent"
)}
/>
{type.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Label for="event-publics">Publics Schematics Only</Label>
<Switch id="event-publics" bind:checked={eventPublicsOnly} />
<div class="flex flex-row justify-end border-t pt-2 gap-4">
<Button variant="destructive">Delete</Button>
<Button disabled={!dirty} onclick={updateEvent}>Update</Button>
</div>
</div>

View File

@ -0,0 +1,107 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import type {ExtendedEvent} from "@type/event";
import {createSvelteTable, FlexRender} from "@components/ui/data-table";
import {
type ColumnFiltersState,
getCoreRowModel, getFilteredRowModel,
getPaginationRowModel, getSortedRowModel,
type SortingState,
} from "@tanstack/table-core";
import { columns } from "./columns"
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table";
let { data }: { data: ExtendedEvent } = $props();
let sorting = $state<SortingState>([]);
let columnFilters = $state<ColumnFiltersState>([]);
const table = createSvelteTable({
get data() {
return data.fights;
},
state: {
get sorting() {
return sorting;
},
get columnFilters() {
return columnFilters;
},
},
onSortingChange: (updater) => {
if (typeof updater === "function") {
sorting = updater(sorting);
} else {
sorting = updater;
}
},
onColumnFiltersChange: (updater) => {
if (typeof updater === "function") {
columnFilters = updater(columnFilters);
} else {
columnFilters = updater;
}
},
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
});
</script>
<Table>
<TableHeader>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<TableRow>
{#each headerGroup.headers as header (header.id)}
<TableHead>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</TableHead>
{/each}
</TableRow>
{/each}
</TableHeader>
<TableBody>
{#each table.getRowModel().rows as row (row.id)}
<TableRow data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<TableCell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</TableCell>
{/each}
</TableRow>
{:else}
<TableRow>
<TableCell colspan={columns.length} class="h-24 text-center">
No results.
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>

View File

@ -0,0 +1,100 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import type {ExtendedEvent} from "@type/event.ts";
import EventEdit from "@components/moderator/pages/event/EventEdit.svelte";
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table/index.js";
import {Button} from "@components/ui/button";
import {Popover, PopoverTrigger} from "@components/ui/popover";
import {PopoverContent} from "@components/ui/popover/index.js";
import {CommandEmpty, CommandGroup, CommandInput, CommandList, CommandItem, Command} from "@components/ui/command";
import {players} from "@stores/stores.ts";
import {eventRepo} from "@repo/event.ts";
import EventFightList from "@components/moderator/pages/event/EventFightList.svelte";
const {
event
}: { event: ExtendedEvent } = $props();
let referees = $state(event.event.referees)
async function addReferee(value: string) {
referees = (await $eventRepo.updateEvent(event.event.id.toString(), {
addReferee: [value]
})).referees;
}
async function removeReferee(value: string) {
referees = (await $eventRepo.updateEvent(event.event.id.toString(), {
removeReferee: [value]
})).referees;
}
</script>
<div class="flex flex-col m-4 p-4 rounded-md border gap-4">
<div class="flex flex-col md:flex-row">
<div class="w-1/2">
<h1 class="text-2xl font-bold mb-4">{event.event.name}</h1>
<EventEdit event={event.event} />
</div>
<div class="ml-4 pl-4 border-l w-1/2">
<h2>Referees</h2>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#each referees as referee (referee.uuid)}
<TableRow>
<TableCell>{referee.name}</TableCell>
<TableCell>
<Button onclick={() => removeReferee(referee.uuid)}>Remove</Button>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
<Popover>
<PopoverTrigger>
<Button>
Add
</Button>
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search players..." />
<CommandList>
<CommandEmpty>No Players found :(</CommandEmpty>
<CommandGroup heading="Players">
{#each $players.filter(v => v.perms.length > 0).filter(v => !referees.some(k => k.uuid === v.uuid)) as player (player.uuid)}
<CommandItem value={player.uuid} onSelect={() => addReferee(player.uuid)}>{player.name}</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
<EventFightList data={event} />
</div>

View File

@ -1,7 +1,7 @@
/* /*
* This file is a part of the SteamWar software. * This file is a part of the SteamWar software.
* *
* Copyright (C) 2023 SteamWar.de-Serverteam * Copyright (C) 2025 SteamWar.de-Serverteam
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by * it under the terms of the GNU Affero General Public License as published by
@ -17,17 +17,16 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
export function window<T>(arr: T[], len: number): T[][] { import type {ColumnDef} from "@tanstack/table-core";
const result: T[][] = []; import type {EventFight} from "@type/event.ts";
for (let i = 0; i < arr.length; i += len) {
result.push(arr.slice(i, i + len));
}
return result;
}
export function stopPropagation(a: any) { export const columns: ColumnDef<EventFight> = [
return (e: Event) => { {
e.stopPropagation(); accessorFn: (r) => r.blueTeam.name,
a(e); header: "Team Blue",
}; },
} {
accessorFn: (r) => r.redTeam.name,
header: "Team Red",
},
];

View File

@ -0,0 +1,51 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import { eventRepo } from "@repo/event.ts";
import EventCard from "@components/moderator/components/EventCard.svelte";
let eventsFuture = $state($eventRepo.listEvents());
let millis = Date.now();
</script>
<div class="p-4">
{#await eventsFuture}
<p>Loading...</p>
{:then events}
<h1 class="mt-5 scroll-m-20 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0">Upcoming</h1>
<div class="grid gap-4 p-4 border-b" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))">
{#each events.filter((e) => e.start > millis) as event (event.id)}
<a href="#/event/{event.id}">
<EventCard {event} />
</a>
{/each}
</div>
<h1 class="mt-5 scroll-m-20 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0">Past</h1>
<div class="grid gap-4 p-4" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))">
{#each events.filter((e) => e.start < millis).reverse() as event (event.id)}
<a href="#/event/{event.id}">
<EventCard {event} />
</a>
{/each}
</div>
{:catch e}
{/await}
</div>

View File

@ -0,0 +1,56 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {permissions, players} from "@stores/stores.ts";
import {Select, SelectContent, SelectItem} from "@components/ui/select";
import {SelectTrigger} from "@components/ui/select/index.js";
import {permsRepo} from "@repo/perms.ts";
const {
perms, uuid
}: { perms: string[], uuid: string } = $props();
let value = $state(perms);
let prevValue = $state(perms);
function onChange(change: string[]) {
$permissions.perms.forEach(perm => {
if (prevValue.includes(perm) && !change.includes(perm)) {
$permsRepo.removePerm(uuid, perm)
} else if (!prevValue.includes(perm) && change.includes(perm)) {
$permsRepo.addPerm(uuid, perm)
}
});
prevValue = change;
value = change;
}
</script>
<Select type="multiple" bind:value onValueChange={onChange}>
<SelectTrigger>
{value.length} Permissions
</SelectTrigger>
<SelectContent>
{#each $permissions.perms as permission (permission)}
<SelectItem value={permission}>{permission}</SelectItem>
{/each}
</SelectContent>
</Select>

View File

@ -0,0 +1,32 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script>
import Table from "@components/moderator/pages/players/Table.svelte";
import {dataRepo} from "@repo/data";
let playersFuture = $state($dataRepo.getPlayers())
</script>
{#await playersFuture}
<p>Loading...</p>
{:then players}
<Table data={players} />
{/await}

View File

@ -0,0 +1,47 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {Select, SelectContent, SelectItem, SelectTrigger} from "@components/ui/select";
import {permissions} from "@stores/stores.ts";
import {permsRepo} from "@repo/perms.ts";
const {
prefix, uuid
}: { prefix: string, uuid: string } = $props();
let value = $state(prefix);
function onChange(change: string) {
$permsRepo.setPrefix(uuid, change);
value = $permissions.prefixes[change].chatPrefix;
}
</script>
<Select type="single" bind:value onValueChange={onChange}>
<SelectTrigger>
{value === "" ? "None" : value}
</SelectTrigger>
<SelectContent>
{#each Object.entries($permissions.prefixes) as prefix (prefix[1].name)}
<SelectItem value={prefix[0]}>{prefix[1].chatPrefix === "" ? "None" : prefix[1].chatPrefix}</SelectItem>
{/each}
</SelectContent>
</Select>

View File

@ -0,0 +1,174 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {
type ColumnFiltersState,
getCoreRowModel, getFilteredRowModel,
getPaginationRowModel, getSortedRowModel,
type PaginationState,
type SortingState,
} from "@tanstack/table-core";
import {
createSvelteTable,
FlexRender,
} from "@components/ui/data-table/index";
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table";
import {Button} from "@components/ui/button";
import {Input} from "@components/ui/input";
import {Select} from "@components/ui/select";
import {SelectContent, SelectItem, SelectTrigger} from "@components/ui/select/index.js";
import type {Player} from "@type/data";
import { columns } from "./columns";
let { data }: { data: Player[] } = $props();
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 25 });
let sorting = $state<SortingState>([]);
let columnFilters = $state<ColumnFiltersState>([]);
const table = createSvelteTable({
get data() {
return data;
},
state: {
get pagination() {
return pagination;
},
get sorting() {
return sorting;
},
get columnFilters() {
return columnFilters;
},
},
onPaginationChange: (updater) => {
if (typeof updater === "function") {
pagination = updater(pagination);
} else {
pagination = updater;
}
},
onSortingChange: (updater) => {
if (typeof updater === "function") {
sorting = updater(sorting);
} else {
sorting = updater;
}
},
onColumnFiltersChange: (updater) => {
if (typeof updater === "function") {
columnFilters = updater(columnFilters);
} else {
columnFilters = updater;
}
},
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
});
</script>
<div class="rounded-md border m-4">
<div class="flex items-center p-4 border-b">
<Input
placeholder="Filter Players..."
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
onchange={(e) => {
table.getColumn("name")?.setFilterValue(e.currentTarget.value);
}}
oninput={(e) => {
table.getColumn("name")?.setFilterValue(e.currentTarget.value);
}}
class="max-w-sm"
/>
<div class="flex items-center px-4">
<Select type="single" value={pagination.pageSize.toString()} onValueChange={(e) => pagination = { pageSize: +e, pageIndex: 0 }}>
<SelectTrigger>{pagination.pageSize}</SelectTrigger>
<SelectContent>
<SelectItem value="5">5</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="25">25</SelectItem>
<SelectItem value="50">50</SelectItem>
<SelectItem value="100">100</SelectItem>
<SelectItem value="200">200</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Table>
<TableHeader>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<TableRow>
{#each headerGroup.headers as header (header.id)}
<TableHead>
{#if !header.isPlaceholder}
<FlexRender
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if}
</TableHead>
{/each}
</TableRow>
{/each}
</TableHeader>
<TableBody>
{#each table.getRowModel().rows as row (row.id)}
<TableRow data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)}
<TableCell>
<FlexRender
content={cell.column.columnDef.cell}
context={cell.getContext()}
/>
</TableCell>
{/each}
</TableRow>
{:else}
<TableRow>
<TableCell colspan={columns.length} class="h-24 text-center">
No results.
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
<div class="flex items-center justify-end space-x-2 p-4 border-t">
<Button
variant="outline"
size="sm"
onclick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<span>{pagination.pageIndex + 1}/{table.getPageCount()}</span>
<Button
variant="outline"
size="sm"
onclick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>

View File

@ -0,0 +1,60 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import type {ColumnDef} from "@tanstack/table-core";
import type {Player} from "@type/data.ts";
import { renderComponent } from "@components/ui/data-table";
import PermissionsDropdown from "@components/moderator/pages/players/PermissionsDropdown.svelte";
import PrefixDropdown from "@components/moderator/pages/players/PrefixDropdown.svelte";
export const columns: ColumnDef<Player[]> = [
{
accessorKey: "uuid",
header: "UUID",
},
{
accessorKey: "name",
header: "Name",
},
{
accessorKey: "prefix",
header: "Prefix",
cell: ({ row }) => {
return renderComponent(
PrefixDropdown, {
prefix: row.getValue("prefix"),
uuid: row.getValue("uuid"),
},
);
},
},
{
accessorKey: "perms",
header: "Permissions",
cell: ({ row }) => {
return renderComponent(
PermissionsDropdown,
{
perms: row.getValue("perms"),
uuid: row.getValue("uuid"),
},
);
},
},
];

View File

@ -20,7 +20,7 @@
import type {Player, Server} from "@type/data.ts"; import type {Player, Server} from "@type/data.ts";
import {PlayerSchema, ServerSchema} from "@type/data.ts"; import {PlayerSchema, ServerSchema} from "@type/data.ts";
import {fetchWithToken, tokenStore} from "./repo.ts"; import {fetchWithToken, tokenStore} from "./repo.ts";
import {derived} from "svelte/store"; import {derived, get} from "svelte/store";
export class DataRepo { export class DataRepo {
constructor(private token: string) { constructor(private token: string) {
@ -33,6 +33,10 @@ export class DataRepo {
public async getMe(): Promise<Player> { public async getMe(): Promise<Player> {
return await fetchWithToken(this.token, "/data/me").then(value => value.json()).then(PlayerSchema.parse); return await fetchWithToken(this.token, "/data/me").then(value => value.json()).then(PlayerSchema.parse);
} }
public async getPlayers(): Promise<Player[]> {
return await fetchWithToken(get(tokenStore), "/data/admin/users").then(value => value.json()).then(PlayerSchema.array().parse);
}
} }
export const dataRepo = derived(tokenStore, ($token) => new DataRepo($token)); export const dataRepo = derived(tokenStore, ($token) => new DataRepo($token));

View File

@ -31,15 +31,15 @@ export interface CreateEvent {
} }
export interface UpdateEvent { export interface UpdateEvent {
name: string | null; name?: string | null;
start: Dayjs | null; start?: Dayjs | number | null;
end: Dayjs | null; end?: Dayjs | number | null;
deadline: Dayjs | null; deadline?: Dayjs | number | null;
maxTeamMembers: number | null; maxTeamMembers?: number | null;
schemType: string | null; schemType?: string | null;
publicSchemsOnly: boolean | null; publicSchemsOnly?: boolean | null;
addReferee: string[] | null; addReferee?: string[] | null;
removeReferee: string[] | null; removeReferee?: string[] | null;
} }
export class EventRepo { export class EventRepo {

View File

@ -27,6 +27,7 @@ import {z} from "zod";
import {fetchWithToken, tokenStore} from "@repo/repo.ts"; import {fetchWithToken, tokenStore} from "@repo/repo.ts";
import {pageRepo} from "@repo/page.ts"; import {pageRepo} from "@repo/page.ts";
import {dataRepo} from "@repo/data.ts"; import {dataRepo} from "@repo/data.ts";
import {permsRepo} from "@repo/perms.ts";
export const schemTypes = cached<SchematicType[]>([], () => export const schemTypes = cached<SchematicType[]>([], () =>
fetchWithToken(get(tokenStore), "/data/admin/schematicTypes") fetchWithToken(get(tokenStore), "/data/admin/schematicTypes")
@ -37,6 +38,13 @@ export const players = cached<Player[]>([], async () => {
return z.array(PlayerSchema).parse(await res.json()); return z.array(PlayerSchema).parse(await res.json());
}); });
export const permissions = cached({
perms: [],
prefixes: {},
}, async () => {
return get(permsRepo).listPerms();
});
export const gamemodes = cached<string[]>([], async () => { export const gamemodes = cached<string[]>([], async () => {
const res = await fetchWithToken(get(tokenStore), "/data/admin/gamemodes"); const res = await fetchWithToken(get(tokenStore), "/data/admin/gamemodes");
return z.array(z.string()).parse(await res.json()); return z.array(z.string()).parse(await res.json());

View File

@ -19,7 +19,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte"; import {onMount} from "svelte";
import {stopPropagation} from "@components/util.ts"; import {stopPropagation} from "@components/utils.ts";
interface Props { interface Props {
title: string; title: string;

View File

@ -0,0 +1,129 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import { Calendar } from "@components/ui/calendar";
import { Button } from "@components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "$lib/components/ui/popover";
import { ScrollArea } from "$lib/components/ui/scroll-area";
import { CalendarIcon } from "lucide-svelte";
import { cn } from "@components/utils";
import type {ZonedDateTime} from "@internationalized/date";
let {
value = $bindable(),
onChange
}: {
value: ZonedDateTime
onChange?: ((date: ZonedDateTime | undefined) => void) | undefined
} = $props();
let isOpen = $state(false);
const hours = Array.from({ length: 24 }, (_, i) => i);
function handleDateSelect(selectedDate: ZonedDateTime) {
if (selectedDate) {
value = selectedDate;
if (onChange) {
onChange(value);
}
}
}
function handleTimeChange(type: "hour" | "minute", change: number) {
if (change !== undefined) {
if (type === "hour") {
value = value.set({ hour: change });
} else if (type === "minute") {
value = value.set({ minute: change });
}
if (onChange) {
onChange(value);
}
}
}
</script>
<Popover bind:open={isOpen}>
<PopoverTrigger>
<Button
variant="outline"
class={cn(
"w-full justify-start text-left font-normal",
!value && "text-muted-foreground"
)}
>
<CalendarIcon class="mr-2 h-4 w-4" />
{#if value}
{new Intl.DateTimeFormat("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(value.toDate())}
{:else}
<span>DD.MM.YYYY, hh:mm</span>
{/if}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<div class="sm:flex">
<Calendar
mode="single"
bind:value
onValueChange={(date) => handleDateSelect(date)}
initialFocus
/>
<div class="flex flex-col sm:flex-row sm:h-[300px] divide-y sm:divide-y-0 sm:divide-x">
<ScrollArea class="w-64 sm:w-auto">
<div class="flex sm:flex-col p-2">
{#each [...hours].reverse() as hour}
<Button
size="icon"
variant={value && value.hour === hour ? "default" : "ghost"}
class="sm:w-full shrink-0 aspect-square"
onclick={() => handleTimeChange("hour", hour)}
>
{hour}
</Button>
{/each}
</div>
</ScrollArea>
<ScrollArea class="w-64 sm:w-auto">
<div class="flex sm:flex-col p-2">
{#each Array.from({ length: 60 }, (_, i) => i) as minute}
<Button
size="icon"
variant={value && value.minute === minute ? "default" : "ghost"}
class="sm:w-full shrink-0 aspect-square"
onclick={() => handleTimeChange("minute", minute)}
>
{minute.toString().padStart(2, '0')}
</Button>
{/each}
</div>
</ScrollArea>
</div>
</div>
</PopoverContent>
</Popover>

View File

@ -1,4 +1,5 @@
--- ---
import "$lib/styles/app.css";
import icon from "../images/logo.png"; import icon from "../images/logo.png";
import {getImage} from "astro:assets"; import {getImage} from "astro:assets";
import {astroI18n} from "astro-i18n"; import {astroI18n} from "astro-i18n";

View File

@ -0,0 +1,9 @@
---
import Basic from "../../layouts/Basic.astro";
import App from "@components/moderator/App.svelte";
---
<Basic clientSideRouter={false}>
<App client:only="svelte"/>
</Basic>

97
src/styles/app.css Normal file
View File

@ -0,0 +1,97 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--ring: 240 5% 64.9%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 85.7% 97.3%;
--ring: 240 3.7% 15.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -1,20 +0,0 @@
const defaultTheme = require("tailwindcss/defaultTheme");
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}',
"./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}"
],
theme: {
extend: {
colors: {
primary: { 50: '#FFF5F2', 100: '#FFF1EE', 200: '#FFE4DE', 300: '#FFD5CC', 400: '#FFBCAD', 500: '#FE795D', 600: '#EF562F', 700: '#EB4F27', 800: '#CC4522', 900: '#A5371B'},
}
},
},
plugins: [
require('flowbite/plugin')
],
darkMode: 'class'
}

86
tailwind.config.js Normal file
View File

@ -0,0 +1,86 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
const config = {
darkMode: ["class"],
content: [
"./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}",
"./node_modules/flowbite-svelte/**/*.{html,js,svelte,ts}",
],
safelist: ["dark"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border) / <alpha-value>)",
input: "hsl(var(--input) / <alpha-value>)",
ring: "hsl(var(--ring) / <alpha-value>)",
background: "hsl(var(--background) / <alpha-value>)",
foreground: "hsl(var(--foreground) / <alpha-value>)",
primary: {
DEFAULT: "hsl(var(--primary) / <alpha-value>)",
foreground: "hsl(var(--primary-foreground) / <alpha-value>)",
},
secondary: {
DEFAULT: "hsl(var(--secondary) / <alpha-value>)",
foreground: "hsl(var(--secondary-foreground) / <alpha-value>)",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground) / <alpha-value>)",
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
},
popover: {
DEFAULT: "hsl(var(--popover) / <alpha-value>)",
foreground: "hsl(var(--popover-foreground) / <alpha-value>)",
},
card: {
DEFAULT: "hsl(var(--card) / <alpha-value>)",
foreground: "hsl(var(--card-foreground) / <alpha-value>)",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
fontFamily: {
sans: [...fontFamily.sans],
},
},
},
};
export default config;

View File

@ -20,6 +20,8 @@
"@repo": ["./src/components/repo"], "@repo": ["./src/components/repo"],
"@stores/*": ["./src/components/stores/*"], "@stores/*": ["./src/components/stores/*"],
"@stores": ["./src/components/stores"], "@stores": ["./src/components/stores"],
"$lib/*": ["./src/*"],
"$lib": ["./src"]
} }
} }
} }