12 Commits

Author SHA1 Message Date
1da279bb24 feat: Add FightEdit and GroupEdit components for enhanced event management
All checks were successful
SteamWarCI Build successful
2025-05-10 22:22:12 +02:00
7d67ad0950 Refactor stores and types for improved data handling and schema definitions
All checks were successful
SteamWarCI Build successful
- Consolidated player fetching logic in stores.ts to utilize dataRepo.
- Introduced teams fetching logic in stores.ts.
- Updated permissions structure in stores.ts for better clarity.
- Enhanced data schemas in data.ts with new ResponseUser and ResponseTeam schemas.
- Expanded event-related schemas in event.ts to include groups, relations, and event creation/update structures.
- Improved code formatting for consistency and readability across files.
2025-05-08 21:47:36 +02:00
6377799e1b style: Improve code formatting and readability across multiple components
All checks were successful
SteamWarCI Build successful
2025-05-07 14:33:48 +02:00
b3598e1ee1 style: Improve code formatting and readability in FightStatistics component
All checks were successful
SteamWarCI Build successful
2025-05-06 13:42:49 +02:00
b9db5be858 style: Improve formatting and readability of WarShip rules
All checks were successful
SteamWarCI Build successful
2025-04-22 23:34:13 +02:00
3e54934806 feat: Enable autoDarkMode in Basic layout for admin new page
All checks were successful
SteamWarCI Build successful
2025-04-18 12:46:21 +02:00
98638f94fc feat: Add autoDarkMode support to Basic layout and update admin index
All checks were successful
SteamWarCI Build successful
2025-04-18 12:43:09 +02:00
4da8fe50c0 feat: Refactor EventEdit and EventFightList components for improved UI and functionality
All checks were successful
SteamWarCI Build successful
- Enhanced EventEdit component with AlertDialog for delete confirmation.
- Added Menubar component to EventFightList for batch editing options.
- Updated alert-dialog components to streamline props and improve reactivity.
- Refactored menubar components for better structure and usability.
- Improved accessibility and code readability across various components.
2025-04-16 12:55:10 +02:00
7757978668 refactor: clean up imports and improve player search functionality in RefereesList
All checks were successful
SteamWarCI Build successful
2025-04-16 00:17:10 +02:00
9eea0b2b3f feat: enhance EventFightList with grouping and selection features
All checks were successful
SteamWarCI Build successful
- Added grouping functionality to the EventFightList component, allowing fights to be grouped by their associated group.
- Implemented row selection with checkboxes for both individual fights and groups, enabling bulk selection.
- Updated columns definition to include a checkbox for selecting all rows and individual row selection checkboxes.
- Modified the checkbox component to support indeterminate state and improved styling.
- Enhanced date formatting for fight start times in the table.
2025-04-15 16:28:19 +02:00
063638d016 Add TeamTable component and improve EventView layout
All checks were successful
SteamWarCI Build successful
2025-04-14 23:31:19 +02:00
f5a778d9b4 Trigger Rebuild
All checks were successful
SteamWarCI Build successful
2025-04-14 18:21:26 +02:00
54 changed files with 3659 additions and 9941 deletions

1
.gitignore vendored
View File

@@ -26,3 +26,4 @@ pnpm-debug.log*
/src/env.d.ts /src/env.d.ts
/src/pages/en/ /src/pages/en/
/.idea /.idea
pnpm-lock.yaml

View File

@@ -21,6 +21,7 @@
"@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", "@internationalized/date": "^3.7.0",
"@lucide/svelte": "^0.488.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",

9276
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -36,23 +36,23 @@
const rotateX = (centerY - y) / 20; const rotateX = (centerY - y) / 20;
const rotateY = -(centerX - x) / 20; const rotateY = -(centerX - x) / 20;
cardElement.style.setProperty('--rotate-x', `${rotateX}deg`); cardElement.style.setProperty("--rotate-x", `${rotateX}deg`);
cardElement.style.setProperty('--rotate-y', `${rotateY}deg`); cardElement.style.setProperty("--rotate-y", `${rotateY}deg`);
} }
function resetElement() { function resetElement() {
cardElement.style.setProperty('--rotate-x', "0"); cardElement.style.setProperty("--rotate-x", "0");
cardElement.style.setProperty('--rotate-y', "0"); cardElement.style.setProperty("--rotate-y", "0");
} }
interface Props { interface Props {
hoverEffect?: boolean; hoverEffect?: boolean;
extraClasses?: string; extraClasses?: string;
children?: import('svelte').Snippet; children?: import("svelte").Snippet;
} }
let { hoverEffect = true, extraClasses = '', children }: Props = $props(); let { hoverEffect = true, extraClasses = "", children }: Props = $props();
let classes = $derived(twMerge("w-72 border-2 bg-zinc-50 border-gray-100 flex flex-col items-center p-8 m-4 rounded-xl shadow-lg dark:bg-zinc-900 dark:border-gray-800 dark:text-gray-100", extraClasses)) 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));
</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>
@@ -63,14 +63,14 @@
div { div {
transform: perspective(1000px) rotateX(var(--rotate-x, 0)) rotateY(var(--rotate-y, 0)) !important; transform: perspective(1000px) rotateX(var(--rotate-x, 0)) rotateY(var(--rotate-y, 0)) !important;
transition: scale 300ms cubic-bezier(.2, 3, .67, .6); transition: scale 300ms cubic-bezier(0.2, 3, 0.67, 0.6);
:global(h1) { :global(h1) {
@apply text-xl font-bold mt-4; @apply text-xl font-bold mt-4;
} }
:global(svg) { :global(svg) {
@apply transition-transform duration-300 ease-in-out hover:scale-110 hover:drop-shadow-2xl @apply transition-transform duration-300 ease-in-out hover:scale-110 hover:drop-shadow-2xl;
} }
} }

View File

@@ -25,12 +25,12 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { loggedIn } from "@repo/authv2.ts"; import { loggedIn } from "@repo/authv2.ts";
interface Props { interface Props {
logo?: import('svelte').Snippet; logo?: import("svelte").Snippet;
} }
let { logo }: Props = $props(); let { logo }: Props = $props();
let navbar = $state<HTMLDivElement>(); let navbar = $state<HTMLElement>();
let searchOpen = $state(false); let searchOpen = $state(false);
let accountBtn = $state<HTMLAnchorElement>(); let accountBtn = $state<HTMLAnchorElement>();
@@ -41,11 +41,11 @@
} else { } else {
accountBtn!.href = l("/login"); accountBtn!.href = l("/login");
} }
}) });
onMount(() => { onMount(() => {
handleScroll(); handleScroll();
}) });
function handleScroll() { function handleScroll() {
if (window.scrollY > 0) { if (window.scrollY > 0) {
@@ -58,11 +58,15 @@
<svelte:window onscroll={handleScroll} /> <svelte:window onscroll={handleScroll} />
<nav data-pagefind-ignore class="fixed top-0 left-0 right-0 sm:px-4 py-1 transition-colors z-10 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}> <nav
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 dark:text-white hidden md:inline-block"> <span class="text-2xl uppercase font-bold text-white 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="before:scale-y-100" style="display: none" aria-hidden="true"></span>
</span> </span>
@@ -96,10 +100,8 @@
<a href={l("/rules/airship")} class="btn btn-gray">{t("navbar.links.rules.as")}</a> <a href={l("/rules/airship")} class="btn btn-gray">{t("navbar.links.rules.as")}</a>
<a href={l("/rules/quickgear")} class="btn btn-gray">{t("navbar.links.rules.qg")}</a> <a href={l("/rules/quickgear")} class="btn btn-gray">{t("navbar.links.rules.qg")}</a>
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.rotating")}</h2> <h2 class="px-2 text-gray-300">{t("navbar.links.rules.rotating")}</h2>
<a href={l("/rules/megawargear")} <a href={l("/rules/megawargear")} class="btn btn-gray">{t("navbar.links.rules.megawg")}</a>
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> <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> <a href={l("/rangliste/missilewars")} class="btn btn-gray">{t("navbar.links.ranked.mw")}</a>

View File

@@ -8,42 +8,52 @@ import P from "./P.astro";
import Card from "@components/Card.svelte"; import Card from "@components/Card.svelte";
interface Props { interface Props {
post: CollectionEntry<"announcements"> post: CollectionEntry<"announcements">;
} }
const { post, slim }: { const {
post: CollectionEntry<"announcements">, post,
slim: boolean, slim,
}: {
post: CollectionEntry<"announcements">;
slim: boolean;
} = Astro.props as Props; } = Astro.props as Props;
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" : ""}`} hoverEffect={false}> <Card extraClasses={`w-full items-start mx-0 ${slim ? "m-0 p-1 backdrop-blur-xl bg-transparent" : ""}`} 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 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" /> <Image
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" transition:name={post.data.title + "-title"}>{post.data.title}</h2>
<P class="text-gray-500">{Intl.DateTimeFormat(astroI18n.locale, { <P class="text-gray-500"
>{
Intl.DateTimeFormat(astroI18n.locale, {
day: "numeric", day: "numeric",
month: "long", month: "long",
year: "numeric", year: "numeric",
}).format(post.data.created)}</P> }).format(post.data.created)
}</P
>
<P>{post.data.description}</P> <P>{post.data.description}</P>
</a> </a>
<div class="mt-1" transition:name={post.data.title + "-tags"}> <div class="mt-1" transition:name={post.data.title + "-tags"}>
{post.data.tags.map((tag) => ( {post.data.tags.map((tag) => <TagComponent tag={tag} />)}
<TagComponent tag={tag} />
))}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -33,19 +33,17 @@
"/": Dashboard, "/": Dashboard,
"/events": Events, "/events": Events,
"/players": Players, "/players": Players,
"/event/:id": Event "/event/:id": Event,
}; };
</script> </script>
<div class="flex flex-col bg-background min-w-full min-h-screen"> <div class="flex flex-col bg-background min-w-full min-h-screen">
<div class="border-b"> <div class="border-b">
<div class="flex h-16 items-center px-4"> <div class="flex h-16 items-center px-4">
<a href="/" class="text-sm font-bold transition-colors text-primary"> <a href="/" class="text-sm font-bold transition-colors text-primary"> SteamWar </a>
SteamWar
</a>
<NavLinks /> <NavLinks />
<div class="ml-auto flex items-center space-x-4"> <div class="ml-auto flex items-center space-x-4">
<Switch id="new-ui-switch" checked={true} on:click={() => navigate("/admin")} /> <Switch id="new-ui-switch" checked={true} onclick={() => navigate("/admin")} />
<Label for="new-ui-switch">New UI!</Label> <Label for="new-ui-switch">New UI!</Label>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,219 @@
<script lang="ts">
import type { EventFight } from "@type/event";
import { fromAbsolute, now, ZonedDateTime } from "@internationalized/date";
import { Label } from "@components/ui/label";
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
import { gamemodes, maps } from "@components/stores/stores";
import { CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, Command } from "@components/ui/command";
import { ChevronsUpDown, Check } from "lucide-svelte";
import { Button } from "@components/ui/button";
import { cn } from "@components/utils";
import type { Team } from "@components/types/team";
import { Select, SelectContent, SelectItem, SelectTrigger } from "@components/ui/select";
import type { Snippet } from "svelte";
const {
fight,
teams,
actions,
onSave,
}: {
fight: EventFight | null;
teams: Team[];
actions: Snippet<[boolean, () => void]>;
onSave: (fight: {
spielmodus: string;
map: string;
blueTeam: {
id: number;
name: string;
kuerzel: string;
color: string;
};
redTeam: {
id: number;
name: string;
kuerzel: string;
color: string;
};
start: number;
ergebnis: number;
}) => void;
} = $props();
let fightModus = $state(fight?.spielmodus);
let fightMap = $state(fight?.map);
let fightBlueTeam = $state(fight?.blueTeam);
let fightRedTeam = $state(fight?.redTeam);
let fightStart = $state(fight?.start ? fromAbsolute(fight.start, "Europe/Berlin") : now("Europe/Berlin"));
let fightErgebnis = $state(fight?.ergebnis ?? 0);
let mapsStore = $derived(maps(fightModus ?? "null"));
let dirty = $derived(
fightModus !== fight?.spielmodus ||
fightMap !== fight?.map ||
fightBlueTeam !== fight?.blueTeam ||
fightRedTeam !== fight?.redTeam ||
fightStart.toDate().getTime() !== fight?.start ||
fightErgebnis !== fight?.ergebnis
);
function submit() {
onSave({
spielmodus: fightModus!,
map: fightMap!,
blueTeam: fightBlueTeam!,
redTeam: fightRedTeam!,
start: fightStart?.toDate().getTime(),
ergebnis: fightErgebnis,
});
}
</script>
<div class="flex flex-col gap-2">
<Label for="fight-modus">Modus</Label>
<Popover>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" class="justify-between" {...props} role="combobox">
{$gamemodes.find((value) => value === fightModus) || fightModus || "Select a modus type..."}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search Fight Modus..." />
<CommandList>
<CommandEmpty>No fight modus found.</CommandEmpty>
<CommandGroup>
{#each $gamemodes as modus}
<CommandItem
value={modus}
onSelect={() => {
fightModus = modus;
}}
>
<Check class={cn("mr-2 size-4", modus !== fightModus && "text-transparent")} />
{modus}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Label for="fight-map">Map</Label>
<Popover>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" class="justify-between" {...props} role="combobox">
{$mapsStore.find((value) => value === fightMap) || fightMap || "Select a map..."}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search Maps..." />
<CommandList>
<CommandEmpty>No map found.</CommandEmpty>
<CommandGroup>
{#each $mapsStore as map}
<CommandItem
value={map}
onSelect={() => {
fightMap = map;
}}
>
<Check class={cn("mr-2 size-4", map !== fightMap && "text-transparent")} />
{map}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Label for="fight-blue-team">Blue Team</Label>
<Popover>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" class="justify-between" {...props} role="combobox">
{teams.find((value) => value === fightBlueTeam) || fightBlueTeam?.name || "Select a team..."}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search Maps..." />
<CommandList>
<CommandEmpty>No map found.</CommandEmpty>
<CommandGroup>
{#each teams as team}
<CommandItem
value={team.name}
onSelect={() => {
fightBlueTeam = team;
}}
>
<Check class={cn("mr-2 size-4", team !== fightBlueTeam && "text-transparent")} />
{team.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Label for="fight-red-team">Red Team</Label>
<Popover>
<PopoverTrigger>
{#snippet child({ props })}
<Button variant="outline" class="justify-between" {...props} role="combobox">
{teams.find((value) => value === fightRedTeam) || fightRedTeam?.name || "Select a team..."}
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
{/snippet}
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search Maps..." />
<CommandList>
<CommandEmpty>No map found.</CommandEmpty>
<CommandGroup>
{#each teams as team}
<CommandItem
value={team.name}
onSelect={() => {
fightRedTeam = team;
}}
>
<Check class={cn("mr-2 size-4", team !== fightRedTeam && "text-transparent")} />
{team.name}
</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Label>Start</Label>
<DateTimePicker bind:value={fightStart} />
<Label for="fight-ergebnis">Ergebnis</Label>
<Select type="single" value={fightErgebnis?.toString()} onValueChange={(v) => (fightErgebnis = +v)}>
<SelectTrigger>
{fightErgebnis === 0 ? "Unentschieden" : (fightErgebnis === 1 ? fightBlueTeam?.name : fightRedTeam?.name) + " gewinnt"}
</SelectTrigger>
<SelectContent>
<SelectItem value={"0"}>Unentschieden</SelectItem>
<SelectItem value={"1"}>{fightBlueTeam?.name ?? "Team Blau"} gewinnt</SelectItem>
<SelectItem value={"2"}>{fightRedTeam?.name ?? "Team Blau"} gewinnt</SelectItem>
</SelectContent>
</Select>
</div>
{@render actions(dirty, submit)}

View File

@@ -22,19 +22,9 @@
</script> </script>
<nav class="flex items-center space-x-4 lg:space-x-6 mx-6"> <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 !== "/"}> <a href="#/" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/"}> Dashboard </a>
Dashboard <a href="#/events" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={!$location.startsWith("/event")}> Events </a>
</a> <a href="#/players" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/players"}> Players </a>
<a href="#/events" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/events"}> <a href="#/pages" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/pages"}> Pages </a>
Events <a href="#/schematics" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted-foreground={$location !== "/schematics"}> Schematics </a>
</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> </nav>

View File

@@ -21,10 +21,10 @@
import { Input } from "@components/ui/input"; import { Input } from "@components/ui/input";
import { Label } from "@components/ui/label"; import { Label } from "@components/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
import type {SWEvent} from "@type/event.ts" import type { SWEvent } from "@type/event.ts";
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte"; import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
import { fromAbsolute } from "@internationalized/date"; import { fromAbsolute } from "@internationalized/date";
import {Button} from "@components/ui/button"; import { Button, buttonVariants } from "@components/ui/button";
import { ChevronsUpDown } from "lucide-svelte"; import { ChevronsUpDown } from "lucide-svelte";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
import { schemTypes } from "@stores/stores.ts"; import { schemTypes } from "@stores/stores.ts";
@@ -32,10 +32,21 @@
import { cn } from "@components/utils.ts"; import { cn } from "@components/utils.ts";
import { Switch } from "@components/ui/switch"; import { Switch } from "@components/ui/switch";
import { eventRepo } from "@repo/event.ts"; import { eventRepo } from "@repo/event.ts";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@components/ui/alert-dialog";
const { event }: { event: SWEvent } = $props(); const { event }: { event: SWEvent } = $props();
let rootEvent: SWEvent = $state(event) let rootEvent: SWEvent = $state(event);
let eventName = $state(rootEvent.name); let eventName = $state(rootEvent.name);
let eventDeadline = $state(fromAbsolute(rootEvent.deadline, "Europe/Berlin")); let eventDeadline = $state(fromAbsolute(rootEvent.deadline, "Europe/Berlin"));
@@ -45,13 +56,15 @@
let eventSchematicType = $state(rootEvent.schemType); let eventSchematicType = $state(rootEvent.schemType);
let eventPublicsOnly = $state(rootEvent.publicSchemsOnly); let eventPublicsOnly = $state(rootEvent.publicSchemsOnly);
let dirty = $derived(eventName !== rootEvent.name || let dirty = $derived(
eventName !== rootEvent.name ||
eventDeadline.toDate().getTime() !== rootEvent.deadline || eventDeadline.toDate().getTime() !== rootEvent.deadline ||
eventStart.toDate().getTime() !== rootEvent.start || eventStart.toDate().getTime() !== rootEvent.start ||
eventEnd.toDate().getTime() !== rootEvent.end || eventEnd.toDate().getTime() !== rootEvent.end ||
eventTeamSize !== rootEvent.maxTeamMembers || eventTeamSize !== rootEvent.maxTeamMembers ||
eventSchematicType !== rootEvent.schemType || eventSchematicType !== rootEvent.schemType ||
eventPublicsOnly !== rootEvent.publicSchemsOnly); eventPublicsOnly !== rootEvent.publicSchemsOnly
);
async function updateEvent() { async function updateEvent() {
rootEvent = await $eventRepo.updateEvent(event.id.toString(), { rootEvent = await $eventRepo.updateEvent(event.id.toString(), {
@@ -62,7 +75,7 @@
maxTeamMembers: eventTeamSize, maxTeamMembers: eventTeamSize,
schemType: eventSchematicType, schemType: eventSchematicType,
publicSchemsOnly: eventPublicsOnly, publicSchemsOnly: eventPublicsOnly,
}) });
} }
</script> </script>
@@ -81,13 +94,8 @@
<Popover> <Popover>
<PopoverTrigger> <PopoverTrigger>
{#snippet child({ props })} {#snippet child({ props })}
<Button <Button variant="outline" class="justify-between" {...props} role="combobox">
variant="outline" {$schemTypes.find((value) => value.db === eventSchematicType)?.name || eventSchematicType || "Select a schematic type..."}
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" /> <ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button> </Button>
{/snippet} {/snippet}
@@ -98,6 +106,15 @@
<CommandList> <CommandList>
<CommandEmpty>No schematic type found.</CommandEmpty> <CommandEmpty>No schematic type found.</CommandEmpty>
<CommandGroup> <CommandGroup>
<CommandItem
value={"null"}
onSelect={() => {
eventSchematicType = null;
}}
>
<Check class={cn("mr-2 size-4", eventSchematicType !== null && "text-transparent")} />
Keinen
</CommandItem>
{#each $schemTypes as type} {#each $schemTypes as type}
<CommandItem <CommandItem
value={type.db} value={type.db}
@@ -105,12 +122,7 @@
eventSchematicType = type.db; eventSchematicType = type.db;
}} }}
> >
<Check <Check class={cn("mr-2 size-4", eventSchematicType !== type.db && "text-transparent")} />
class={cn(
"mr-2 size-4",
eventSchematicType !== type.db && "text-transparent"
)}
/>
{type.name} {type.name}
</CommandItem> </CommandItem>
{/each} {/each}
@@ -122,7 +134,19 @@
<Label for="event-publics">Publics Schematics Only</Label> <Label for="event-publics">Publics Schematics Only</Label>
<Switch id="event-publics" bind:checked={eventPublicsOnly} /> <Switch id="event-publics" bind:checked={eventPublicsOnly} />
<div class="flex flex-row justify-end border-t pt-2 gap-4"> <div class="flex flex-row justify-end border-t pt-2 gap-4">
<Button variant="destructive">Delete</Button> <AlertDialog>
<AlertDialogTrigger class={buttonVariants({ variant: "destructive" })}>Delete</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription>This action cannot be undone.</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction disabled>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Button disabled={!dirty} onclick={updateEvent}>Update</Button> <Button disabled={!dirty} onclick={updateEvent}>Update</Button>
</div> </div>
</div> </div>

View File

@@ -18,26 +18,33 @@
--> -->
<script lang="ts"> <script lang="ts">
import FightEditRow from "./FightEditRow.svelte";
import GroupEditRow from "./GroupEditRow.svelte";
import type { ExtendedEvent } from "@type/event"; import type { ExtendedEvent } from "@type/event";
import { createSvelteTable, FlexRender } from "@components/ui/data-table"; import { createSvelteTable, FlexRender } from "@components/ui/data-table";
import { import { type ColumnFiltersState, getCoreRowModel, getFilteredRowModel, getGroupedRowModel, getSortedRowModel, type RowSelectionState, type SortingState } from "@tanstack/table-core";
type ColumnFiltersState, import { columns } from "./columns";
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"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@components/ui/table";
import { Checkbox } from "@components/ui/checkbox";
import { Menubar, MenubarContent, MenubarItem, MenubarGroup, MenubarGroupHeading, MenubarMenu, MenubarSeparator, MenubarTrigger } from "@components/ui/menubar";
import { Button } from "@components/ui/button";
import { MenuIcon } from "lucide-svelte";
let { data }: { data: ExtendedEvent } = $props(); let { data = $bindable() }: { data: ExtendedEvent } = $props();
let sorting = $state<SortingState>([]); let sorting = $state<SortingState>([]);
let columnFilters = $state<ColumnFiltersState>([]); let columnFilters = $state<ColumnFiltersState>([]);
let selection = $state<RowSelectionState>({});
const table = createSvelteTable({ const table = createSvelteTable({
get data() { get data() {
return data.fights; return data.fights;
}, },
initialState: {
columnOrder: ["auswahl", "begegnung", "group"],
},
state: { state: {
get sorting() { get sorting() {
return sorting; return sorting;
@@ -45,6 +52,12 @@
get columnFilters() { get columnFilters() {
return columnFilters; return columnFilters;
}, },
get grouping() {
return ["group"];
},
get rowSelection() {
return selection;
},
}, },
onSortingChange: (updater) => { onSortingChange: (updater) => {
if (typeof updater === "function") { if (typeof updater === "function") {
@@ -60,13 +73,47 @@
columnFilters = updater; columnFilters = updater;
} }
}, },
onRowSelectionChange: (updater) => {
if (typeof updater === "function") {
selection = updater(selection);
} else {
selection = updater;
}
},
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
getGroupedRowModel: getGroupedRowModel(),
groupedColumnMode: "remove",
getRowId: (row) => row.id.toString(),
}); });
</script> </script>
<div class="w-fit">
<Menubar>
<MenubarMenu>
<MenubarTrigger>Mehrfach Bearbeiten</MenubarTrigger>
<MenubarContent>
<MenubarItem disabled>Gruppe Ändern</MenubarItem>
<MenubarItem disabled>Startzeit Verschieben</MenubarItem>
<MenubarItem disabled>Spectate Port Ändern</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>Erstellen</MenubarTrigger>
<MenubarContent>
<MenubarItem disabled>Fight Erstellen</MenubarItem>
<MenubarGroup>
<MenubarGroupHeading>Generatoren</MenubarGroupHeading>
<MenubarItem disabled>Gruppenphase</MenubarItem>
<MenubarItem disabled>K.O. Phase</MenubarItem>
</MenubarGroup>
</MenubarContent>
</MenubarMenu>
</Menubar>
</div>
<Table> <Table>
<TableHeader> <TableHeader>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)} {#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
@@ -74,10 +121,7 @@
{#each headerGroup.headers as header (header.id)} {#each headerGroup.headers as header (header.id)}
<TableHead> <TableHead>
{#if !header.isPlaceholder} {#if !header.isPlaceholder}
<FlexRender <FlexRender content={header.column.columnDef.header} context={header.getContext()} />
content={header.column.columnDef.header}
context={header.getContext()}
/>
{/if} {/if}
</TableHead> </TableHead>
{/each} {/each}
@@ -85,22 +129,48 @@
{/each} {/each}
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{#each table.getRowModel().rows as row (row.id)} {#each table.getRowModel().rows as groupRow (groupRow.id)}
{#if groupRow.getIsGrouped()}
<TableRow class="bg-muted font-bold">
<TableCell colspan={columns.length - 1}>
<Checkbox
checked={groupRow.getIsSelected()}
indeterminate={groupRow.getIsSomeSelected() && !groupRow.getIsSelected()}
onCheckedChange={() => groupRow.toggleSelected()}
class="mr-4"
/>
{groupRow.getValue("group") ?? "Keine Gruppe"}
</TableCell>
{#if groupRow.original.group != null}
<TableCell class="text-right">
<GroupEditRow group={groupRow.original.group}></GroupEditRow>
</TableCell>
{/if}
</TableRow>
{#each groupRow.subRows as row (row.id)}
<TableRow data-state={row.getIsSelected() && "selected"}> <TableRow data-state={row.getIsSelected() && "selected"}>
{#each row.getVisibleCells() as cell (cell.id)} {#each row.getVisibleCells() as cell (cell.id)}
<TableCell> <TableCell>
<FlexRender <FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
content={cell.column.columnDef.cell} </TableCell>
context={cell.getContext()} {/each}
/> <TableCell class="text-right">
<FightEditRow fight={row.original} teams={data.teams}></FightEditRow>
</TableCell>
</TableRow>
{/each}
{:else}
<TableRow data-state={groupRow.getIsSelected() && "selected"}>
{#each groupRow.getVisibleCells() as cell (cell.id)}
<TableCell>
<FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
</TableCell> </TableCell>
{/each} {/each}
</TableRow> </TableRow>
{/if}
{:else} {:else}
<TableRow> <TableRow>
<TableCell colspan={columns.length} class="h-24 text-center"> <TableCell colspan={columns.length} class="h-24 text-center">No results.</TableCell>
No results.
</TableCell>
</TableRow> </TableRow>
{/each} {/each}
</TableBody> </TableBody>

View File

@@ -22,10 +22,9 @@
import EventEdit from "@components/moderator/pages/event/EventEdit.svelte"; import EventEdit from "@components/moderator/pages/event/EventEdit.svelte";
import EventFightList from "@components/moderator/pages/event/EventFightList.svelte"; import EventFightList from "@components/moderator/pages/event/EventFightList.svelte";
import RefereesList from "@components/moderator/pages/event/RefereesList.svelte"; import RefereesList from "@components/moderator/pages/event/RefereesList.svelte";
import TeamTable from "@components/moderator/pages/event/TeamTable.svelte";
const { let { event }: { event: ExtendedEvent } = $props();
event
}: { event: ExtendedEvent } = $props();
</script> </script>
<div class="flex flex-col m-4 p-4 rounded-md border gap-4"> <div class="flex flex-col m-4 p-4 rounded-md border gap-4">
@@ -35,13 +34,13 @@
<EventEdit event={event.event} /> <EventEdit event={event.event} />
</div> </div>
<div class="md:ml-4 md:pl-4 md:border-l md:w-1/3"> <div class="md:ml-4 md:pl-4 md:border-l md:w-1/3">
<h2>Teams</h2> <h2 class="text-xl font-bold mb-4">Teams</h2>
<TeamTable bind:event />
</div> </div>
<div class="md:ml-4 md:pl-4 md:border-l md:w-1/3"> <div class="md:ml-4 md:pl-4 md:border-l md:w-1/3">
<h2>Referees</h2> <h2 class="text-xl font-bold mb-4">Referees</h2>
<RefereesList event={event} /> <RefereesList {event} />
</div> </div>
</div> </div>
<EventFightList data={event} /> <EventFightList bind:data={event} />
</div> </div>

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import type { EventFight } from "@type/event";
import { Button } from "@components/ui/button";
import { EditIcon, MenuIcon } from "lucide-svelte";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@components/ui/dialog";
import FightEdit from "@components/moderator/components/FightEdit.svelte";
import type { Team } from "@components/types/team";
const { fight, teams }: { fight: EventFight; teams: Team[] } = $props();
function handleSave(fightData) {
// Handle the save action here
console.log("Fight data saved:", fightData);
}
</script>
<div>
<Dialog>
<DialogTrigger>
<Button variant="ghost" size="icon">
<EditIcon />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Fight bearbeiten</DialogTitle>
<DialogDescription>Hier kannst du die Daten des Kampfes bearbeiten.</DialogDescription>
</DialogHeader>
<FightEdit {fight} {teams} onSave={handleSave}>
{#snippet actions(dirty, submit)}
<DialogFooter>
<Button disabled={!dirty} onclick={submit}>Speichern</Button>
</DialogFooter>
{/snippet}
</FightEdit>
</DialogContent>
</Dialog>
<Button variant="ghost" size="icon">
<MenuIcon />
</Button>
</div>

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import type { ResponseGroups } from "@type/event";
import { Button } from "@components/ui/button";
import { MenuIcon } from "lucide-svelte";
let { group }: { group: ResponseGroups } = $props();
</script>
<div>
<Button variant="ghost" size="icon">
<MenuIcon />
</Button>
<Button variant="ghost" size="icon">
<MenuIcon />
</Button>
</div>

View File

@@ -18,38 +18,29 @@
--> -->
<script lang="ts"> <script lang="ts">
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table/index.js"; import { Table, TableBody, TableCell, TableCaption, TableHead, TableHeader, TableRow } from "@components/ui/table";
import { import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command/index.js";
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@components/ui/command/index.js";
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover/index.js"; import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover/index.js";
import { Button } from "@components/ui/button/index.js"; import { Button } from "@components/ui/button/index.js";
import type { ExtendedEvent } from "@type/event.ts"; import type { ExtendedEvent } from "@type/event.ts";
import { eventRepo } from "@repo/event"; import { eventRepo } from "@repo/event";
import { players } from "@stores/stores" import { players } from "@stores/stores";
const { const { event }: { event: ExtendedEvent } = $props();
event
}: { event: ExtendedEvent } = $props();
let referees = $state(event.event.referees) let referees = $state(event.referees);
async function addReferee(value: string) { async function addReferee(value: string) {
referees = (await $eventRepo.updateEvent(event.event.id.toString(), { await $eventRepo.updateReferees(event.event.id.toString(), [value]);
addReferee: [value] referees = await $eventRepo.listReferees(event.event.id.toString());
})).referees;
} }
async function removeReferee(value: string) { async function removeReferee(value: string) {
referees = (await $eventRepo.updateEvent(event.event.id.toString(), { await $eventRepo.deleteReferees(event.event.id.toString(), [value]);
removeReferee: [value] referees = await $eventRepo.listReferees(event.event.id.toString());
})).referees;
} }
let playerSearch = $state("");
</script> </script>
<Table> <Table>
@@ -69,24 +60,27 @@
</TableRow> </TableRow>
{/each} {/each}
</TableBody> </TableBody>
</Table>
<Popover> <Popover>
<TableCaption>
<PopoverTrigger> <PopoverTrigger>
<Button> <Button>Add</Button>
Add
</Button>
</PopoverTrigger> </PopoverTrigger>
</TableCaption>
<PopoverContent class="p-0"> <PopoverContent class="p-0">
<Command> <Command shouldFilter={false}>
<CommandInput placeholder="Search players..." /> <CommandInput bind:value={playerSearch} placeholder="Search players..." />
<CommandList> <CommandList>
<CommandEmpty>No Players found :(</CommandEmpty> <CommandEmpty>No Players found :(</CommandEmpty>
<CommandGroup heading="Players"> <CommandGroup heading="Players">
{#each $players.filter(v => v.perms.length > 0).filter(v => !referees.some(k => k.uuid === v.uuid)) as player (player.uuid)} {#each $players
<CommandItem value={player.uuid} onSelect={() => addReferee(player.uuid)}>{player.name}</CommandItem> .filter((v) => v.name.includes(playerSearch))
.filter((v, i) => i < 50)
.filter((v) => !referees.some((k) => k.uuid === v.uuid)) as player (player.uuid)}
<CommandItem value={player.name} onSelect={() => addReferee(player.uuid)} keywords={[player.uuid]}>{player.name}</CommandItem>
{/each} {/each}
</CommandGroup> </CommandGroup>
</CommandList> </CommandList>
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</Table>

View File

@@ -0,0 +1,95 @@
<!--
- 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 { Button } from "@components/ui/button";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell, TableCaption } from "@components/ui/table";
import type { ExtendedEvent } from "@type/event.ts";
import { eventRepo } from "@repo/event";
import { Popover, PopoverContent, PopoverTrigger } from "@components/ui/popover";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@components/ui/command";
import { teams } from "@components/stores/stores";
const { event = $bindable() }: { event: ExtendedEvent } = $props();
let team = $state(event.teams);
async function addTeam(value: number) {
await $eventRepo.updateTeams(event.event.id.toString(), [value]);
team = await $eventRepo.listTeams(event.event.id.toString());
event.teams = team;
}
async function removeTeam(value: number) {
await $eventRepo.deleteTeams(event.event.id.toString(), [value]);
team = await $eventRepo.listTeams(event.event.id.toString());
event.teams = team;
}
let teamSearch = $state("");
</script>
<Table>
<TableHeader>
<TableRow>
<TableHead>Team</TableHead>
<TableHead>Name</TableHead>
<TableHead>Action</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#each team as t (t.id)}
<TableRow>
<TableCell>{t.kuerzel}</TableCell>
<TableCell>{t.name}</TableCell>
<TableCell>
<Button onclick={() => removeTeam(t.id)}>Remove</Button>
</TableCell>
</TableRow>
{/each}
{#if team.length === 0}
<TableRow>
<TableCell class="text-center col-span-3">No teams available</TableCell>
</TableRow>
{/if}
</TableBody>
<Popover>
<TableCaption>
<PopoverTrigger>
<Button>Add Team</Button>
</PopoverTrigger>
</TableCaption>
<PopoverContent class="p-0">
<Command shouldFilter={false}>
<CommandInput bind:value={teamSearch} placeholder="Search teams..." />
<CommandList>
<CommandEmpty>No teams found :(</CommandEmpty>
<CommandGroup heading="Teams">
{#each $teams
.filter((v) => v.name.includes(teamSearch))
.filter((v) => !team.some((k) => k.id === v.id))
.filter((v, i) => i < 50) as t (t.id)}
<CommandItem value={t.id.toString()} onSelect={() => addTeam(t.id)} keywords={[t.name, t.kuerzel]}>{t.name}</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</Table>

View File

@@ -17,16 +17,64 @@
* 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 { Checkbox } from "@components/ui/checkbox";
import { renderComponent } from "@components/ui/data-table";
import type { ColumnDef } from "@tanstack/table-core"; import type { ColumnDef } from "@tanstack/table-core";
import type { EventFight } from "@type/event.ts"; import type { EventFight } from "@type/event.ts";
export const columns: ColumnDef<EventFight> = [ export const columns: ColumnDef<EventFight> = [
{ {
accessorFn: (r) => r.blueTeam.name, id: "auswahl",
header: "Team Blue", header: ({ table }) => {
return renderComponent(Checkbox, {
checked: table.getIsAllRowsSelected(),
indeterminate: table.getIsSomeRowsSelected(),
onCheckedChange: () => {
if (!table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected()) {
const now = new Date();
const rows = table.getRowModel().rows.filter((row) => new Date(row.original.date) > now);
if (rows.length > 0) {
rows.forEach((row) => {
row.toggleSelected();
});
} else {
table.toggleAllRowsSelected(true);
}
} else if (table.getIsSomeRowsSelected() && !table.getIsAllRowsSelected()) {
table.toggleAllRowsSelected(true);
} else {
table.toggleAllRowsSelected(false);
}
},
});
},
cell: ({ row }) => {
return renderComponent(Checkbox, {
checked: row.getIsSelected(),
onCheckedChange: row.getToggleSelectedHandler(),
});
},
}, },
{ {
accessorFn: (r) => r.redTeam.name, accessorFn: (r) => r.blueTeam.name + " vs " + r.redTeam.name,
header: "Team Red", id: "begegnung",
header: "Begegnung",
},
{
header: "Gruppe",
accessorKey: "group.name",
id: "group",
},
{
header: "Datum",
accessorKey: "start",
id: "start",
cell: ({ row }) => {
return new Date(row.getValue("start")).toLocaleString("de-DE", {
dateStyle: "short",
timeStyle: "medium",
});
},
}, },
]; ];

View File

@@ -21,21 +21,33 @@ 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, get } from "svelte/store"; import { derived, get } from "svelte/store";
import { TeamSchema, type Team } from "@components/types/team.ts";
export class DataRepo { export class DataRepo {
constructor(private token: string) { constructor(private token: string) {}
}
public async getServer(): Promise<Server> { public async getServer(): Promise<Server> {
return await fetchWithToken(this.token, "/data/server").then(value => value.json()).then(ServerSchema.parse); return await fetchWithToken(this.token, "/data/server")
.then((value) => value.json())
.then(ServerSchema.parse);
} }
public async getMe(): Promise<Player> { public async getMe(): Promise<Player> {
return await fetchWithToken(this.token, "/data/me").then(value => value.json()).then(PlayerSchema.parse); return await fetchWithToken(this.token, "/data/me")
.then((value) => value.json())
.then(PlayerSchema.parse);
} }
public async getPlayers(): Promise<Player[]> { public async getPlayers(): Promise<Player[]> {
return await fetchWithToken(get(tokenStore), "/data/admin/users").then(value => value.json()).then(PlayerSchema.array().parse); return await fetchWithToken(get(tokenStore), "/data/admin/users")
.then((value) => value.json())
.then(PlayerSchema.array().parse);
}
public async getTeams(): Promise<Team[]> {
return await fetchWithToken(get(tokenStore), "/data/admin/teams")
.then((value) => value.json())
.then(TeamSchema.array().parse);
} }
} }

View File

@@ -17,12 +17,26 @@
* 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 type {ExtendedEvent, ShortEvent, SWEvent} from "@type/event"; import type { ExtendedEvent, ShortEvent, SWEvent, EventFight, ResponseGroups, ResponseRelation, ResponseTeam } from "@type/event";
import { fetchWithToken, tokenStore } from "./repo"; import { fetchWithToken, tokenStore } from "./repo";
import {ExtendedEventSchema, ShortEventSchema, SWEventSchema} from "@type/event.js"; import {
ExtendedEventSchema,
ShortEventSchema,
SWEventSchema,
EventFightSchema,
ResponseGroupsSchema,
ResponseRelationSchema,
ResponseTeamSchema,
CreateEventGroupSchema,
UpdateEventGroupSchema,
CreateEventRelationSchema,
UpdateEventRelationSchema,
} from "@type/event.js";
import type { CreateEventGroup, UpdateEventGroup, CreateEventRelation, UpdateEventRelation } from "@type/event.js";
import { z } from "zod"; import { z } from "zod";
import type { Dayjs } from "dayjs"; import type { Dayjs } from "dayjs";
import { derived } from "svelte/store"; import { derived } from "svelte/store";
import { ResponseUserSchema } from "@components/types/data";
export interface CreateEvent { export interface CreateEvent {
name: string; name: string;
@@ -42,19 +56,25 @@ export interface UpdateEvent {
removeReferee?: string[] | null; removeReferee?: string[] | null;
} }
export class EventRepo { export interface ResponseUser {
constructor(private token: string) { name: string;
uuid: string;
prefix: string;
perms: string[];
} }
export class EventRepo {
constructor(private token: string) {}
public async listEvents(): Promise<ShortEvent[]> { public async listEvents(): Promise<ShortEvent[]> {
return await fetchWithToken(this.token, "/events") return await fetchWithToken(this.token, "/events")
.then(value => value.json()) .then((value) => value.json())
.then(value => z.array(ShortEventSchema).parse(value)); .then((value) => z.array(ShortEventSchema).parse(value));
} }
public async getEvent(id: string): Promise<ExtendedEvent> { public async getEvent(id: string): Promise<ExtendedEvent> {
return await fetchWithToken(this.token, `/events/${id}`) return await fetchWithToken(this.token, `/events/${id}`)
.then(value => value.json()) .then((value) => value.json())
.then(ExtendedEventSchema.parse); .then(ExtendedEventSchema.parse);
} }
@@ -66,7 +86,8 @@ export class EventRepo {
start: +event.start, start: +event.start,
end: +event.end, end: +event.end,
}), }),
}).then(value => value.json()) })
.then((value) => value.json())
.then(SWEventSchema.parse); .then(SWEventSchema.parse);
} }
@@ -87,7 +108,8 @@ export class EventRepo {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
}).then(value => value.json()) })
.then((value) => value.json())
.then(SWEventSchema.parse); .then(SWEventSchema.parse);
} }
@@ -98,6 +120,150 @@ export class EventRepo {
return res.ok; return res.ok;
} }
// Fights
public async listFights(eventId: string): Promise<EventFight[]> {
return await fetchWithToken(this.token, `/events/${eventId}/fights`)
.then((value) => value.json())
.then((value) => z.array(EventFightSchema).parse(value));
}
public async createFight(eventId: string, fight: any): Promise<EventFight> {
return await fetchWithToken(this.token, `/events/${eventId}/fights`, {
method: "POST",
body: JSON.stringify(fight),
headers: { "Content-Type": "application/json" },
})
.then((value) => value.json())
.then(EventFightSchema.parse);
}
public async deleteFight(eventId: string, fightId: string): Promise<boolean> {
const res = await fetchWithToken(this.token, `/events/${eventId}/fights/${fightId}`, {
method: "DELETE",
});
return res.ok;
}
// Groups
public async listGroups(eventId: string): Promise<ResponseGroups[]> {
return await fetchWithToken(this.token, `/events/${eventId}/groups`)
.then((value) => value.json())
.then((value) => z.array(ResponseGroupsSchema).parse(value));
}
public async createGroup(eventId: string, group: CreateEventGroup): Promise<ResponseGroups> {
CreateEventGroupSchema.parse(group);
return await fetchWithToken(this.token, `/events/${eventId}/groups`, {
method: "POST",
body: JSON.stringify(group),
headers: { "Content-Type": "application/json" },
})
.then((value) => value.json())
.then(ResponseGroupsSchema.parse);
}
public async getGroup(eventId: string, groupId: string): Promise<ResponseGroups> {
return await fetchWithToken(this.token, `/events/${eventId}/groups/${groupId}`)
.then((value) => value.json())
.then(ResponseGroupsSchema.parse);
}
public async updateGroup(eventId: string, groupId: string, group: UpdateEventGroup): Promise<ResponseGroups> {
UpdateEventGroupSchema.parse(group);
return await fetchWithToken(this.token, `/events/${eventId}/groups/${groupId}`, {
method: "PUT",
body: JSON.stringify(group),
headers: { "Content-Type": "application/json" },
})
.then((value) => value.json())
.then(ResponseGroupsSchema.parse);
}
public async deleteGroup(eventId: string, groupId: string): Promise<boolean> {
const res = await fetchWithToken(this.token, `/events/${eventId}/groups/${groupId}`, {
method: "DELETE",
});
return res.ok;
}
// Relations
public async listRelations(eventId: string): Promise<ResponseRelation[]> {
return await fetchWithToken(this.token, `/events/${eventId}/relations`)
.then((value) => value.json())
.then((value) => z.array(ResponseRelationSchema).parse(value));
}
public async createRelation(eventId: string, relation: CreateEventRelation): Promise<ResponseRelation> {
CreateEventRelationSchema.parse(relation);
return await fetchWithToken(this.token, `/events/${eventId}/relations`, {
method: "POST",
body: JSON.stringify(relation),
headers: { "Content-Type": "application/json" },
})
.then((value) => value.json())
.then(ResponseRelationSchema.parse);
}
public async getRelation(eventId: string, relationId: string): Promise<ResponseRelation> {
return await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`)
.then((value) => value.json())
.then(ResponseRelationSchema.parse);
}
public async updateRelation(eventId: string, relationId: string, relation: UpdateEventRelation): Promise<ResponseRelation> {
UpdateEventRelationSchema.parse(relation);
return await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, {
method: "PUT",
body: JSON.stringify(relation),
headers: { "Content-Type": "application/json" },
})
.then((value) => value.json())
.then(ResponseRelationSchema.parse);
}
public async deleteRelation(eventId: string, relationId: string): Promise<boolean> {
const res = await fetchWithToken(this.token, `/events/${eventId}/relations/${relationId}`, {
method: "DELETE",
});
return res.ok;
}
// Teams
public async listTeams(eventId: string): Promise<ResponseTeam[]> {
return await fetchWithToken(this.token, `/events/${eventId}/teams`)
.then((value) => value.json())
.then((value) => z.array(ResponseTeamSchema).parse(value));
}
public async updateTeams(eventId: string, teams: number[]): Promise<boolean> {
const res = await fetchWithToken(this.token, `/events/${eventId}/teams`, {
method: "PUT",
body: JSON.stringify(teams),
headers: { "Content-Type": "application/json" },
});
return res.ok;
}
public async deleteTeams(eventId: string, teams: number[]): Promise<boolean> {
const res = await fetchWithToken(this.token, `/events/${eventId}/teams`, {
method: "DELETE",
body: JSON.stringify(teams),
headers: { "Content-Type": "application/json" },
});
return res.ok;
}
// Referees
public async listReferees(eventId: string): Promise<ResponseUser[]> {
return await fetchWithToken(this.token, `/events/${eventId}/referees`)
.then((value) => value.json())
.then((value) => z.array(ResponseUserSchema).parse(value));
}
public async updateReferees(eventId: string, refereeUuids: string[]): Promise<boolean> {
const res = await fetchWithToken(this.token, `/events/${eventId}/referees`, {
method: "PUT",
body: JSON.stringify(refereeUuids),
headers: { "Content-Type": "application/json" },
});
return res.status === 204;
}
public async deleteReferees(eventId: string, refereeUuids: string[]): Promise<boolean> {
const res = await fetchWithToken(this.token, `/events/${eventId}/referees`, {
method: "DELETE",
body: JSON.stringify(refereeUuids),
headers: { "Content-Type": "application/json" },
});
return res.status === 204;
}
} }
export const eventRepo = derived(tokenStore, ($token) => new EventRepo($token)); export const eventRepo = derived(tokenStore, ($token) => new EventRepo($token));

File diff suppressed because it is too large Load Diff

View File

@@ -29,21 +29,25 @@ import {pageRepo} from "@repo/page.ts";
import { dataRepo } from "@repo/data.ts"; import { dataRepo } from "@repo/data.ts";
import { permsRepo } from "@repo/perms.ts"; import { permsRepo } from "@repo/perms.ts";
export const schemTypes = cached<SchematicType[]>([], () => export const schemTypes = cached<SchematicType[]>([], () => fetchWithToken(get(tokenStore), "/data/admin/schematicTypes").then((res) => res.json()));
fetchWithToken(get(tokenStore), "/data/admin/schematicTypes")
.then(res => res.json()));
export const players = cached<Player[]>([], async () => { export const players = cached<Player[]>([], async () => {
const res = await fetchWithToken(get(tokenStore), "/data/admin/users"); return get(dataRepo).getPlayers();
return z.array(PlayerSchema).parse(await res.json());
}); });
export const permissions = cached({ export const teams = cached<Team[]>([], async () => {
return get(dataRepo).getTeams();
});
export const permissions = cached(
{
perms: [], perms: [],
prefixes: {}, prefixes: {},
}, async () => { },
async () => {
return get(permsRepo).listPerms(); 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");
@@ -51,7 +55,7 @@ export const gamemodes = cached<string[]>([], async () => {
}); });
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 fetchWithToken(get(tokenStore), `/data/admin/gamemodes/${gamemode}/maps`); const res = await fetchWithToken(get(tokenStore), `/data/admin/gamemodes/${gamemode}/maps`);
if (!res.ok) { if (!res.ok) {
@@ -66,17 +70,12 @@ export const groups = cached<string[]>([], async () => {
return z.array(z.string()).parse(await res.json()); return z.array(z.string()).parse(await res.json());
}); });
export const teams = cached<Team[]>([], async () => {
const res = await fetchWithToken(get(tokenStore), "/team");
return z.array(TeamSchema).parse(await res.json());
});
export const branches = cached<string[]>([], async () => { export const branches = cached<string[]>([], async () => {
const res = await get(pageRepo).getBranches(); const res = await get(pageRepo).getBranches();
return z.array(z.string()).parse(res); return z.array(z.string()).parse(res);
}); });
export const server = derived(dataRepo, $dataRepo => $dataRepo.getServer()); export const server = derived(dataRepo, ($dataRepo) => $dataRepo.getServer());
export const isWide = writable(typeof window !== "undefined" && window.innerWidth >= 640); export const isWide = writable(typeof window !== "undefined" && window.innerWidth >= 640);

View File

@@ -57,3 +57,12 @@ export const ResponseErrorSchema = z.object({
}); });
export type ResponseError = z.infer<typeof ResponseErrorSchema>; export type ResponseError = z.infer<typeof ResponseErrorSchema>;
export const ResponseUserSchema = z.object({
name: z.string(),
uuid: z.string(),
prefix: z.string(),
perms: z.array(z.string()),
});
export type ResponseUser = z.infer<typeof ResponseUserSchema>;

View File

@@ -19,7 +19,44 @@
import { z } from "zod"; import { z } from "zod";
import { TeamSchema } from "./team.js"; import { TeamSchema } from "./team.js";
import {PlayerSchema} from "./data.js"; import { PlayerSchema, ResponseUserSchema } from "./data.js";
export const ResponseGroupsSchema = z.object({
id: z.number(),
name: z.string(),
pointsPerWin: z.number(),
pointsPerLoss: z.number(),
pointsPerDraw: z.number(),
type: z.enum(["GROUP_STAGE", "ELIMINATION_STAGE"]),
points: z.record(z.string(), z.number()).nullable(),
});
export const EventFightSchema = z.object({
id: z.number(),
spielmodus: z.string(),
map: z.string(),
blueTeam: TeamSchema,
redTeam: TeamSchema,
start: z.number(),
ergebnis: z.number(),
spectatePort: z.number().nullable(),
group: ResponseGroupsSchema.nullable(),
});
export type EventFight = z.infer<typeof EventFightSchema>;
export type ResponseGroups = z.infer<typeof ResponseGroupsSchema>;
export const ResponseRelationSchema = z.object({
id: z.number(),
fight: EventFightSchema,
type: z.enum(["FIGHT", "GROUP"]),
fromFight: EventFightSchema.nullable(),
fromGroup: ResponseGroupsSchema.nullable(),
fromPlace: z.number(),
});
export type ResponseRelation = z.infer<typeof ResponseRelationSchema>;
export const ShortEventSchema = z.object({ export const ShortEventSchema = z.object({
id: z.number(), id: z.number(),
@@ -35,29 +72,63 @@ export const SWEventSchema = ShortEventSchema.extend({
maxTeamMembers: z.number(), maxTeamMembers: z.number(),
schemType: z.string().nullable(), schemType: z.string().nullable(),
publicSchemsOnly: z.boolean(), publicSchemsOnly: z.boolean(),
referees: z.array(PlayerSchema),
}); });
export type SWEvent = z.infer<typeof SWEventSchema>; export type SWEvent = z.infer<typeof SWEventSchema>;
export const EventFightSchema = z.object({
id: z.number(),
spielmodus: z.string(),
map: z.string(),
blueTeam: TeamSchema,
redTeam: TeamSchema,
start: z.number(),
ergebnis: z.number(),
spectatePort: z.number().nullable(),
group: z.string().nullable(),
});
export type EventFight = z.infer<typeof EventFightSchema>;
export const ExtendedEventSchema = z.object({ export const ExtendedEventSchema = z.object({
event: SWEventSchema, event: SWEventSchema,
teams: z.array(TeamSchema), teams: z.array(TeamSchema),
groups: z.array(ResponseGroupsSchema),
fights: z.array(EventFightSchema), fights: z.array(EventFightSchema),
referees: z.array(ResponseUserSchema),
relations: z.array(ResponseRelationSchema),
}); });
export type ExtendedEvent = z.infer<typeof ExtendedEventSchema>; export type ExtendedEvent = z.infer<typeof ExtendedEventSchema>;
export const ResponseTeamSchema = z.object({
id: z.number(),
name: z.string(),
kuerzel: z.string(),
color: z.string(),
});
export type ResponseTeam = z.infer<typeof ResponseTeamSchema>;
export const CreateEventGroupSchema = z.object({
name: z.string(),
type: z.enum(["GROUP_STAGE", "ELIMINATION_STAGE"]),
});
export type CreateEventGroup = z.infer<typeof CreateEventGroupSchema>;
export const UpdateEventGroupSchema = z.object({
name: z.string().nullable().optional(),
type: z.enum(["GROUP_STAGE", "ELIMINATION_STAGE"]).nullable().optional(),
pointsPerWin: z.number().nullable().optional(),
pointsPerLoss: z.number().nullable().optional(),
pointsPerDraw: z.number().nullable().optional(),
});
export type UpdateEventGroup = z.infer<typeof UpdateEventGroupSchema>;
export const CreateEventRelationSchema = z.object({
fightId: z.number(),
team: z.enum(["RED", "BLUE"]),
fromType: z.enum(["FIGHT", "GROUP"]),
fromId: z.number(),
fromPlace: z.number(),
});
export type CreateEventRelation = z.infer<typeof CreateEventRelationSchema>;
export const UpdateFromRelationSchema = z.object({
fromType: z.enum(["FIGHT", "GROUP"]),
fromId: z.number(),
fromPlace: z.number(),
});
export type UpdateFromRelation = z.infer<typeof UpdateFromRelationSchema>;
export const UpdateEventRelationSchema = z.object({
team: z.enum(["RED", "BLUE"]).nullable().optional(),
from: UpdateFromRelationSchema.nullable().optional(),
});
export type UpdateEventRelation = z.infer<typeof UpdateEventRelationSchema>;

View File

@@ -3,19 +3,11 @@
import { buttonVariants } from "$lib/components/ui/button/index.js"; import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.ActionProps; let {
type $$Events = AlertDialogPrimitive.ActionEvents; ref = $bindable(null),
class: className,
let className: $$Props["class"] = undefined; ...restProps
export { className as class }; }: AlertDialogPrimitive.ActionProps = $props();
</script> </script>
<AlertDialogPrimitive.Action <AlertDialogPrimitive.Action bind:ref class={cn(buttonVariants(), className)} {...restProps} />
class={cn(buttonVariants(), className)}
{...$$restProps}
on:click
on:keydown
let:builder
>
<slot {builder} />
</AlertDialogPrimitive.Action>

View File

@@ -3,19 +3,15 @@
import { buttonVariants } from "$lib/components/ui/button/index.js"; import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.CancelProps; let {
type $$Events = AlertDialogPrimitive.CancelEvents; ref = $bindable(null),
class: className,
let className: $$Props["class"] = undefined; ...restProps
export { className as class }; }: AlertDialogPrimitive.CancelProps = $props();
</script> </script>
<AlertDialogPrimitive.Cancel <AlertDialogPrimitive.Cancel
bind:ref
class={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)} class={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
{...$$restProps} {...restProps}
on:click />
on:keydown
let:builder
>
<slot {builder} />
</AlertDialogPrimitive.Cancel>

View File

@@ -1,28 +1,26 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; import { AlertDialog as AlertDialogPrimitive, type WithoutChild } from "bits-ui";
import * as AlertDialog from "./index.js"; import AlertDialogOverlay from "./alert-dialog-overlay.svelte";
import { cn, flyAndScale } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.ContentProps; let {
ref = $bindable(null),
export let transition: $$Props["transition"] = flyAndScale; class: className,
export let transitionConfig: $$Props["transitionConfig"] = undefined; portalProps,
...restProps
let className: $$Props["class"] = undefined; }: WithoutChild<AlertDialogPrimitive.ContentProps> & {
export { className as class }; portalProps?: AlertDialogPrimitive.PortalProps;
} = $props();
</script> </script>
<AlertDialog.Portal> <AlertDialogPrimitive.Portal {...portalProps}>
<AlertDialog.Overlay /> <AlertDialogOverlay />
<AlertDialogPrimitive.Content <AlertDialogPrimitive.Content
{transition} bind:ref
{transitionConfig}
class={cn( class={cn(
"bg-background fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg sm:rounded-lg md:w-full", "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg",
className className
)} )}
{...$$restProps} {...restProps}
> />
<slot /> </AlertDialogPrimitive.Portal>
</AlertDialogPrimitive.Content>
</AlertDialog.Portal>

View File

@@ -2,15 +2,15 @@
import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.DescriptionProps; let {
ref = $bindable(null),
let className: $$Props["class"] = undefined; class: className,
export { className as class }; ...restProps
}: AlertDialogPrimitive.DescriptionProps = $props();
</script> </script>
<AlertDialogPrimitive.Description <AlertDialogPrimitive.Description
bind:ref
class={cn("text-muted-foreground text-sm", className)} class={cn("text-muted-foreground text-sm", className)}
{...$$restProps} {...restProps}
> />
<slot />
</AlertDialogPrimitive.Description>

View File

@@ -1,16 +1,20 @@
<script lang="ts"> <script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>; let {
ref = $bindable(null),
let className: $$Props["class"] = undefined; class: className,
export { className as class }; children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script> </script>
<div <div
bind:this={ref}
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...$$restProps} {...restProps}
> >
<slot /> {@render children?.()}
</div> </div>

View File

@@ -1,13 +1,20 @@
<script lang="ts"> <script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>; let {
ref = $bindable(null),
let className: $$Props["class"] = undefined; class: className,
export { className as class }; children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script> </script>
<div class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...$$restProps}> <div
<slot /> bind:this={ref}
class={cn("flex flex-col space-y-2 text-center sm:text-left", className)}
{...restProps}
>
{@render children?.()}
</div> </div>

View File

@@ -1,21 +1,19 @@
<script lang="ts"> <script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { fade } from "svelte/transition";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.OverlayProps; let {
ref = $bindable(null),
let className: $$Props["class"] = undefined; class: className,
export let transition: $$Props["transition"] = fade; ...restProps
export let transitionConfig: $$Props["transitionConfig"] = { }: AlertDialogPrimitive.OverlayProps = $props();
duration: 150,
};
export { className as class };
</script> </script>
<AlertDialogPrimitive.Overlay <AlertDialogPrimitive.Overlay
{transition} bind:ref
{transitionConfig} class={cn(
class={cn("bg-background/80 fixed inset-0 z-50 backdrop-blur-sm ", className)} "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
{...$$restProps} className
)}
{...restProps}
/> />

View File

@@ -2,13 +2,17 @@
import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.TitleProps; let {
ref = $bindable(null),
let className: $$Props["class"] = undefined; class: className,
export let level: $$Props["level"] = "h3"; level = 3,
export { className as class }; ...restProps
}: AlertDialogPrimitive.TitleProps = $props();
</script> </script>
<AlertDialogPrimitive.Title class={cn("text-lg font-semibold", className)} {level} {...$$restProps}> <AlertDialogPrimitive.Title
<slot /> bind:ref
</AlertDialogPrimitive.Title> class={cn("text-lg font-semibold", className)}
{level}
{...restProps}
/>

View File

@@ -1,9 +1,7 @@
import { AlertDialog as AlertDialogPrimitive } from "bits-ui"; import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import Title from "./alert-dialog-title.svelte"; import Title from "./alert-dialog-title.svelte";
import Action from "./alert-dialog-action.svelte"; import Action from "./alert-dialog-action.svelte";
import Cancel from "./alert-dialog-cancel.svelte"; import Cancel from "./alert-dialog-cancel.svelte";
import Portal from "./alert-dialog-portal.svelte";
import Footer from "./alert-dialog-footer.svelte"; import Footer from "./alert-dialog-footer.svelte";
import Header from "./alert-dialog-header.svelte"; import Header from "./alert-dialog-header.svelte";
import Overlay from "./alert-dialog-overlay.svelte"; import Overlay from "./alert-dialog-overlay.svelte";
@@ -12,6 +10,7 @@ import Description from "./alert-dialog-description.svelte";
const Root = AlertDialogPrimitive.Root; const Root = AlertDialogPrimitive.Root;
const Trigger = AlertDialogPrimitive.Trigger; const Trigger = AlertDialogPrimitive.Trigger;
const Portal = AlertDialogPrimitive.Portal;
export { export {
Root, Root,

View File

@@ -1,35 +1,35 @@
<script lang="ts"> <script lang="ts">
import { Checkbox as CheckboxPrimitive } from "bits-ui"; import { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import Check from "lucide-svelte/icons/check"; import Check from "@lucide/svelte/icons/check";
import Minus from "lucide-svelte/icons/minus"; import Minus from "@lucide/svelte/icons/minus";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = CheckboxPrimitive.Props; let {
type $$Events = CheckboxPrimitive.Events; ref = $bindable(null),
checked = $bindable(false),
let className: $$Props["class"] = undefined; indeterminate = $bindable(false),
export let checked: $$Props["checked"] = false; class: className,
export { className as class }; ...restProps
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
</script> </script>
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
bind:ref
class={cn( class={cn(
"border-primary ring-offset-background focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground peer box-content h-4 w-4 shrink-0 rounded-sm border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50", "border-primary ring-offset-background focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground peer box-content size-4 shrink-0 rounded-sm border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-50",
className className
)} )}
bind:checked bind:checked
{...$$restProps} bind:indeterminate
on:click {...restProps}
> >
<CheckboxPrimitive.Indicator {#snippet children({ checked, indeterminate })}
class={cn("flex h-4 w-4 items-center justify-center text-current")} <div class="flex size-4 items-center justify-center text-current">
let:isChecked {#if indeterminate}
let:isIndeterminate <Minus class="size-3.5" />
> {:else}
{#if isChecked} <Check class={cn("size-3.5", !checked && "text-transparent")} />
<Check class="h-3.5 w-3.5" />
{:else if isIndeterminate}
<Minus class="h-3.5 w-3.5" />
{/if} {/if}
</CheckboxPrimitive.Indicator> </div>
{/snippet}
</CheckboxPrimitive.Root> </CheckboxPrimitive.Root>

View File

@@ -1,10 +1,9 @@
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import Root from "./menubar.svelte"; import Root from "./menubar.svelte";
import CheckboxItem from "./menubar-checkbox-item.svelte"; import CheckboxItem from "./menubar-checkbox-item.svelte";
import Content from "./menubar-content.svelte"; import Content from "./menubar-content.svelte";
import Item from "./menubar-item.svelte"; import Item from "./menubar-item.svelte";
import Label from "./menubar-label.svelte"; import GroupHeading from "./menubar-group-heading.svelte";
import RadioItem from "./menubar-radio-item.svelte"; import RadioItem from "./menubar-radio-item.svelte";
import Separator from "./menubar-separator.svelte"; import Separator from "./menubar-separator.svelte";
import Shortcut from "./menubar-shortcut.svelte"; import Shortcut from "./menubar-shortcut.svelte";
@@ -22,7 +21,7 @@ export {
CheckboxItem, CheckboxItem,
Content, Content,
Item, Item,
Label, GroupHeading,
RadioItem, RadioItem,
Separator, Separator,
Shortcut, Shortcut,
@@ -38,7 +37,7 @@ export {
CheckboxItem as MenubarCheckboxItem, CheckboxItem as MenubarCheckboxItem,
Content as MenubarContent, Content as MenubarContent,
Item as MenubarItem, Item as MenubarItem,
Label as MenubarLabel, GroupHeading as MenubarGroupHeading,
RadioItem as MenubarRadioItem, RadioItem as MenubarRadioItem,
Separator as MenubarSeparator, Separator as MenubarSeparator,
Shortcut as MenubarShortcut, Shortcut as MenubarShortcut,

View File

@@ -1,35 +1,40 @@
<script lang="ts"> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import Check from "lucide-svelte/icons/check"; import Check from "@lucide/svelte/icons/check";
import Minus from "@lucide/svelte/icons/minus";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
import type { Snippet } from "svelte";
type $$Props = MenubarPrimitive.CheckboxItemProps; let {
type $$Events = MenubarPrimitive.CheckboxItemEvents; ref = $bindable(null),
class: className,
let className: $$Props["class"] = undefined; checked = $bindable(false),
export let checked: $$Props["checked"] = false; indeterminate = $bindable(false),
export { className as class }; children: childrenProp,
...restProps
}: WithoutChildrenOrChild<MenubarPrimitive.CheckboxItemProps> & {
children?: Snippet;
} = $props();
</script> </script>
<MenubarPrimitive.CheckboxItem <MenubarPrimitive.CheckboxItem
bind:ref
bind:checked bind:checked
bind:indeterminate
class={cn( class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className
)} )}
on:click {...restProps}
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
on:pointerdown
{...$$restProps}
> >
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> {#snippet children({ checked, indeterminate })}
<MenubarPrimitive.CheckboxIndicator> <span class="absolute left-2 flex size-3.5 items-center justify-center">
<Check class="h-4 w-4" /> {#if indeterminate}
</MenubarPrimitive.CheckboxIndicator> <Minus class="size-4" />
{:else}
<Check class={cn("size-4", !checked && "text-transparent")} />
{/if}
</span> </span>
<slot /> {@render childrenProp?.()}
{/snippet}
</MenubarPrimitive.CheckboxItem> </MenubarPrimitive.CheckboxItem>

View File

@@ -1,23 +1,24 @@
<script lang="ts"> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = MenubarPrimitive.ContentProps; let {
type $$Events = MenubarPrimitive.ContentEvents; ref = $bindable(null),
class: className,
let className: $$Props["class"] = undefined; sideOffset = 8,
export let sideOffset: $$Props["sideOffset"] = 8; alignOffset = -4,
export let alignOffset: $$Props["alignOffset"] = -4; align = "start",
export let align: $$Props["align"] = "start"; side = "bottom",
export let side: $$Props["side"] = "bottom"; portalProps,
export let transition: $$Props["transition"] = flyAndScale; ...restProps
export let transitionConfig: $$Props["transitionConfig"] = undefined; }: MenubarPrimitive.ContentProps & {
export { className as class }; portalProps?: MenubarPrimitive.PortalProps;
} = $props();
</script> </script>
<MenubarPrimitive.Portal {...portalProps}>
<MenubarPrimitive.Content <MenubarPrimitive.Content
{transition} bind:ref
{transitionConfig}
{sideOffset} {sideOffset}
{align} {align}
{alignOffset} {alignOffset}
@@ -26,8 +27,6 @@
"bg-popover text-popover-foreground z-50 min-w-[12rem] rounded-md border p-1 shadow-md focus:outline-none", "bg-popover text-popover-foreground z-50 min-w-[12rem] rounded-md border p-1 shadow-md focus:outline-none",
className className
)} )}
{...$$restProps} {...restProps}
on:keydown />
> </MenubarPrimitive.Portal>
<slot />
</MenubarPrimitive.Content>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
inset = undefined,
...restProps
}: MenubarPrimitive.GroupHeadingProps & {
inset?: boolean;
} = $props();
</script>
<MenubarPrimitive.GroupHeading
bind:ref
class={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
{...restProps}
/>

View File

@@ -2,30 +2,22 @@
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = MenubarPrimitive.ItemProps & { let {
ref = $bindable(null),
class: className,
inset = undefined,
...restProps
}: MenubarPrimitive.ItemProps & {
inset?: boolean; inset?: boolean;
}; } = $props();
type $$Events = MenubarPrimitive.ItemEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script> </script>
<MenubarPrimitive.Item <MenubarPrimitive.Item
bind:ref
class={cn( class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8", inset && "pl-8",
className className
)} )}
{...$$restProps} {...restProps}
on:click />
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
on:pointerdown
>
<slot />
</MenubarPrimitive.Item>

View File

@@ -1,35 +1,30 @@
<script lang="ts"> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive, type WithoutChild } from "bits-ui";
import Circle from "lucide-svelte/icons/circle"; import Circle from "@lucide/svelte/icons/circle";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = MenubarPrimitive.RadioItemProps; let {
type $$Events = MenubarPrimitive.RadioItemEvents; ref = $bindable(null),
class: className,
let className: $$Props["class"] = undefined; children: childrenProp,
export let value: $$Props["value"]; ...restProps
export { className as class }; }: WithoutChild<MenubarPrimitive.RadioItemProps> = $props();
</script> </script>
<MenubarPrimitive.RadioItem <MenubarPrimitive.RadioItem
{value} bind:ref
class={cn( class={cn(
"data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className
)} )}
{...$$restProps} {...restProps}
on:click
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
on:pointerdown
> >
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> {#snippet children({ checked })}
<MenubarPrimitive.RadioIndicator> <span class="absolute left-2 flex size-3.5 items-center justify-center">
<Circle class="h-2 w-2 fill-current" /> {#if checked}
</MenubarPrimitive.RadioIndicator> <Circle class="size-2 fill-current" />
{/if}
</span> </span>
<slot /> {@render childrenProp?.({ checked })}
{/snippet}
</MenubarPrimitive.RadioItem> </MenubarPrimitive.RadioItem>

View File

@@ -2,10 +2,15 @@
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = MenubarPrimitive.SeparatorProps; let {
ref = $bindable(null),
let className: $$Props["class"] = undefined; class: className,
export { className as class }; ...restProps
}: MenubarPrimitive.SeparatorProps = $props();
</script> </script>
<MenubarPrimitive.Separator class={cn("bg-muted -mx-1 my-1 h-px", className)} {...$$restProps} /> <MenubarPrimitive.Separator
bind:ref
class={cn("bg-muted -mx-1 my-1 h-px", className)}
{...restProps}
/>

View File

@@ -1,16 +1,20 @@
<script lang="ts"> <script lang="ts">
import type { HTMLAttributes } from "svelte/elements"; import type { HTMLAttributes } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement>; let {
ref = $bindable(null),
let className: $$Props["class"] = undefined; class: className,
export { className as class }; children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLSpanElement>> = $props();
</script> </script>
<span <span
bind:this={ref}
class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)} class={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
{...$$restProps} {...restProps}
> >
<slot /> {@render children?.()}
</span> </span>

View File

@@ -1,27 +1,19 @@
<script lang="ts"> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn, flyAndScale } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = MenubarPrimitive.SubContentProps; let {
type $$Events = MenubarPrimitive.SubContentEvents; ref = $bindable(null),
class: className,
let className: $$Props["class"] = undefined; ...restProps
export let transition: $$Props["transition"] = flyAndScale; }: MenubarPrimitive.SubContentProps = $props();
export let transitionConfig: $$Props["transitionConfig"] = { x: -10, y: 0 };
export { className as class };
</script> </script>
<MenubarPrimitive.SubContent <MenubarPrimitive.SubContent
{transition} bind:ref
{transitionConfig}
class={cn( class={cn(
"bg-popover text-popover-foreground z-50 min-w-max rounded-md border p-1 focus:outline-none", "bg-popover text-popover-foreground z-50 min-w-max rounded-md border p-1 focus:outline-none",
className className
)} )}
{...$$restProps} {...restProps}
on:focusout />
on:pointermove
on:keydown
>
<slot />
</MenubarPrimitive.SubContent>

View File

@@ -1,32 +1,28 @@
<script lang="ts"> <script lang="ts">
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive, type WithoutChild } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right"; import ChevronRight from "@lucide/svelte/icons/chevron-right";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = MenubarPrimitive.SubTriggerProps & { let {
ref = $bindable(null),
class: className,
inset = undefined,
children,
...restProps
}: WithoutChild<MenubarPrimitive.SubTriggerProps> & {
inset?: boolean; inset?: boolean;
}; } = $props();
type $$Events = MenubarPrimitive.SubTriggerEvents;
let className: $$Props["class"] = undefined;
export let inset: $$Props["inset"] = undefined;
export { className as class };
</script> </script>
<MenubarPrimitive.SubTrigger <MenubarPrimitive.SubTrigger
bind:ref
class={cn( class={cn(
"data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8", inset && "pl-8",
className className
)} )}
on:click {...restProps}
{...$$restProps}
on:keydown
on:focusin
on:focusout
on:pointerleave
on:pointermove
> >
<slot /> {@render children?.()}
<ChevronRight class="ml-auto h-4 w-4" /> <ChevronRight class="ml-auto size-4" />
</MenubarPrimitive.SubTrigger> </MenubarPrimitive.SubTrigger>

View File

@@ -2,22 +2,18 @@
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = MenubarPrimitive.TriggerProps; let {
type $$Events = MenubarPrimitive.TriggerEvents; ref = $bindable(null),
class: className,
let className: $$Props["class"] = undefined; ...restProps
export { className as class }; }: MenubarPrimitive.TriggerProps = $props();
</script> </script>
<MenubarPrimitive.Trigger <MenubarPrimitive.Trigger
bind:ref
class={cn( class={cn(
"data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none", "data-[highlighted]:bg-accent data-[state=open]:bg-accent data-[highlighted]:text-accent-foreground data-[state=open]:text-accent-foreground flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none",
className className
)} )}
on:click {...restProps}
on:keydown />
on:pointerenter
{...$$restProps}
>
<slot />
</MenubarPrimitive.Trigger>

View File

@@ -2,15 +2,15 @@
import { Menubar as MenubarPrimitive } from "bits-ui"; import { Menubar as MenubarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js"; import { cn } from "$lib/components/utils.js";
type $$Props = MenubarPrimitive.Props; let {
ref = $bindable(null),
let className: $$Props["class"] = undefined; class: className,
export { className as class }; ...restProps
}: MenubarPrimitive.RootProps = $props();
</script> </script>
<MenubarPrimitive.Root <MenubarPrimitive.Root
bind:ref
class={cn("bg-background flex h-10 items-center space-x-1 rounded-md border p-1", className)} class={cn("bg-background flex h-10 items-center space-x-1 rounded-md border p-1", className)}
{...$$restProps} {...restProps}
> />
<slot />
</MenubarPrimitive.Root>

View File

@@ -57,8 +57,10 @@ Jedes WarShip benötigt eine Brücke, welche die folgenden Kriterien erfüllt:
## Anti-Lag-Regeln ## Anti-Lag-Regeln
Clocks müssen sich mit Ende ihres Einsatzzweckes selbst abschalten. Clocks müssen sich mit Ende ihres Einsatzzweckes selbst abschalten.
Sämtliche Redstonetechnik zum Schutz des eigenen WarShips muss ihre Aktivität vor dem Verteilen der Kits eingestellt haben. Sämtliche Redstonetechnik zum Schutz des eigenen WarShips muss ihre Aktivität vor dem Verteilen der Kits eingestellt haben.
Raketen und Flugmaschinen
## Raketen und Flugmaschinen
Ein WarShip darf sich maximal 12 Block vom Technikbereich an weit ausfahren, davon ausgenommen sind Raketen und Flugmaschinen. Raketen und Flugmaschinen dürfen sich im Flug nicht in mehrere Schleim/Honigfahrzeuge aufteilen. Ein WarShip darf sich maximal 12 Block vom Technikbereich an weit ausfahren, davon ausgenommen sind Raketen und Flugmaschinen. Raketen und Flugmaschinen dürfen sich im Flug nicht in mehrere Schleim/Honigfahrzeuge aufteilen.

View File

@@ -3,7 +3,7 @@ import "$lib/styles/app.css";
import { astroI18n } from "astro-i18n"; import { astroI18n } from "astro-i18n";
import { SEO } from "astro-seo"; import { SEO } from "astro-seo";
import { ClientRouter } from "astro:transitions"; import { ClientRouter } from "astro:transitions";
const { title, description, clientSideRouter = 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";
--- ---
@@ -32,11 +32,13 @@ import "../../public/fonts/roboto/roboto.css";
}))} }))}
/> />
{autoDarkMode && (
<script is:inline data-astro-rerun> <script is:inline data-astro-rerun>
if (localStorage["theme-mode"] === "light" || (!("theme-mode" in localStorage) && window.matchMedia("(prefers-color-scheme: light)").matches)) { if (localStorage["theme-mode"] === "light" || (!("theme-mode" in localStorage) && window.matchMedia("(prefers-color-scheme: light)").matches)) {
document.documentElement.classList.remove("dark"); document.documentElement.classList.remove("dark");
} }
</script> </script>
)}
<slot name="head" /> <slot name="head" />

View File

@@ -11,21 +11,23 @@ import Navbar from "@components/Navbar.svelte";
import ServerStatus from "../components/ServerStatus.svelte"; import ServerStatus from "../components/ServerStatus.svelte";
const {title, description} = Astro.props; const { title, description, transparentFooter = true } = Astro.props;
--- ---
<Basic title={title} description={description}> <Basic title={title} description={description} autoDarkMode={false}>
<slot name="head" slot="head" /> <slot name="head" slot="head" />
<Fragment> <Fragment>
<div class="min-h-screen flex flex-col"> <div class="min-h-screen flex flex-col">
<Navbar client:idle> <Navbar client:idle>
<Image src={localLogo} alt={t("navbar.logo.alt")} width="44" height="44" quality="max" <Image src={localLogo} alt={t("navbar.logo.alt")} width="44" height="44" quality="max" class="mr-2 p-1 bg-black rounded-full" slot="logo" />
class="mr-2 p-1 bg-black rounded-full" slot="logo"/>
</Navbar> </Navbar>
<main class="flex-1" data-pagefind-body> <main class="flex-1" data-pagefind-body>
<slot /> <slot />
</main> </main>
<footer class="bg-gray-900 w-full min-h-80 mt-4 pb-2 rounded-t-2xl flex flex-col dark:bg-neutral-900"> <footer
class={`min-h-80 mt-4 pb-2 rounded-t-2xl flex flex-col ${transparentFooter ? "backdrop-blur-3xl" : "bg-neutral-900"}`}
style="width: min(100%, 75em); margin-left: auto; margin-right: auto;"
>
<div class="flex-1 flex justify-evenly items-center md:items-start mt-4 md:flex-row flex-col gap-y-4"> <div class="flex-1 flex justify-evenly items-center md:items-start mt-4 md:flex-row flex-col gap-y-4">
<div class="footer-card"> <div class="footer-card">
<h1>Serverstatus</h1> <h1>Serverstatus</h1>
@@ -46,13 +48,16 @@ const {title, description} = Astro.props;
<h1>Social Media</h1> <h1>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
>
<a class="flex" href="/discord"> <a class="flex" href="/discord">
<DiscordSolid class="mr-2" /> <DiscordSolid class="mr-2" />
Discord</a> Discord</a
>
<a class="flex" href="https://git.steamwar.de"> <a class="flex" href="https://git.steamwar.de">
<FileCodeSolid class="mr-2" /> <FileCodeSolid class="mr-2" />
Gitea</a> Gitea</a
>
</div> </div>
</div> </div>
<span class="text-sm text-white text-center mt-1">© SteamWar.de - Made with ❤️ by Chaoscaot</span> <span class="text-sm text-white text-center mt-1">© SteamWar.de - Made with ❤️ by Chaoscaot</span>

View File

@@ -10,8 +10,8 @@ const {title, description} = 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 class="mx-auto bg-gray-100 p-8 rounded-b-md shadow-md pt-14 relative <div class="mx-auto p-8 rounded-b-md border-x-gray-100 shadow-md pt-14 relative
dark:text-white dark:bg-neutral-900" style="width: min(100%, 75em);"> text-white backdrop-blur-3xl" style="width: min(100%, 75em);">
<slot /> <slot />
</div> </div>
</NavbarLayout> </NavbarLayout>

View File

@@ -3,6 +3,6 @@ import App from "../../components/admin/App.svelte";
import Basic from "../../layouts/Basic.astro"; import Basic from "../../layouts/Basic.astro";
--- ---
<Basic clientSideRouter={false}> <Basic clientSideRouter={false} autoDarkMode={false}>
<App client:only="svelte" /> <App client:only="svelte" />
</Basic> </Basic>

View File

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

View File

@@ -15,15 +15,14 @@ import {type Player} from "../components/types/data";
import PostComponent from "../components/PostComponent.astro"; import PostComponent from "../components/PostComponent.astro";
import BackgroundImage from "../components/BackgroundImage.astro"; import BackgroundImage from "../components/BackgroundImage.astro";
const teamMember: { [key: string]: Player[]} = await fetch(import.meta.env.PUBLIC_API_SERVER + "/data/team") const teamMember: { [key: string]: Player[] } = await fetch(import.meta.env.PUBLIC_API_SERVER + "/data/team").then((value) => value.json());
.then(value => value.json());
const posts = await getCollection("announcements", entry => entry.id.split("/")[0] === astroI18n.locale); const posts = await getCollection("announcements", (entry) => entry.id.split("/")[0] === astroI18n.locale);
const germanPosts = await getCollection("announcements", entry => entry.id.split("/")[0] === astroI18n.fallbackLocale); const germanPosts = await getCollection("announcements", (entry) => entry.id.split("/")[0] === astroI18n.fallbackLocale);
germanPosts.forEach(value => { germanPosts.forEach((value) => {
if (posts.find(post => post.data.key === value.data.key)) { if (posts.find((post) => post.data.key === value.data.key)) {
return; return;
} else { } else {
posts.push(value); posts.push(value);
@@ -43,26 +42,28 @@ const prefixColorMap: {
}; };
--- ---
<NavbarLayout title={t("home.page")} description="SteamWar.de Homepage"> <NavbarLayout title={t("home.page")} description="SteamWar.de Homepage" transparentFooter={false}>
<div class="w-full h-screen relative mb-4"> <div class="w-full h-screen relative mb-4 z-10">
<div style="height: calc(100vh + 1rem)"> <div style="height: 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"> <drop-in class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center">
<h1 class="text-4xl sm:text-6xl md:text-8xl font-extrabold text-white -translate-y-16 opacity-0 barlow tracking-wider" <h1
style="transition: transform .7s ease-out, opacity .7s linear; filter: drop-shadow(2px 2px 5px black);"> class="text-4xl sm:text-6xl md:text-8xl font-extrabold text-white -translate-y-16 opacity-0 barlow tracking-wider"
<span class="bg-gradient-to-tr from-yellow-400 to-yellow-300 bg-clip-text text-transparent">{t("home.title.first")}</span><span style="transition: transform .7s ease-out, opacity .7s linear; filter: drop-shadow(2px 2px 5px black);"
class="text-neutral-600">{t("home.title.second")}</span> >
<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>
</h1> </h1>
<text-carousel class="h-20 w-full relative select-none"> <text-carousel class="h-20 w-full relative select-none">
<h2 class="-translate-y-16">{t("home.subtitle.1")}</h2> <h2 class="-translate-y-16">{t("home.subtitle.1")}</h2>
<h2>{t("home.subtitle.2")} <h2>
{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" <a href={l("join")} class="btn btn-ghost mt-32 px-8 flex" style="animation: normal flyIn forwards 1.2s ease-out"
style="animation: normal flyIn forwards 1.2s ease-out">{t("home.join")} >{t("home.join")}
<CaretRight width="24" height="24" /> <CaretRight width="24" height="24" />
</a> </a>
<style> <style>
@@ -160,7 +161,8 @@ const prefixColorMap: {
</div> </div>
</section> </section>
<section class="w-full py-12 flex flex-wrap justify-center"> <section class="w-full py-12 flex flex-wrap justify-center">
{Object.entries(teamMember).map(([prefix, players]) => ( {
Object.entries(teamMember).map(([prefix, players]) => (
<Fragment> <Fragment>
{players.map((v, index) => ( {players.map((v, index) => (
<div class="inline-flex flex-col justify-end"> <div class="inline-flex flex-col justify-end">
@@ -168,15 +170,20 @@ const prefixColorMap: {
<Card extraClasses={`pt-8 pb-10 px-8 w-fit shadow-md ${prefixColorMap[prefix]}`} client:idle> <Card extraClasses={`pt-8 pb-10 px-8 w-fit shadow-md ${prefixColorMap[prefix]}`} client:idle>
<figure class="flex flex-col items-center" style="width: 150px"> <figure class="flex flex-col items-center" style="width: 150px">
<figcaption class="text-center mb-4 text-2xl">{v.name}</figcaption> <figcaption class="text-center mb-4 text-2xl">{v.name}</figcaption>
<Image src={`${import.meta.env.PUBLIC_API_SERVER}/data/skin/${v.uuid}`} <Image
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" class="transition duration-300 ease-in-out hover:scale-110 hover:backdrop-blur-lg hover:drop-shadow-2xl"
alt={v.name + "s bust"} width="150" height="150"/> alt={v.name + "s bust"}
width="150"
height="150"
/>
</figure> </figure>
</Card> </Card>
</div> </div>
))} ))}
</Fragment> </Fragment>
))} ))
}
</section> </section>
</NavbarLayout> </NavbarLayout>
@@ -184,13 +191,17 @@ const prefixColorMap: {
text-carousel { text-carousel {
> * { > * {
@apply absolute top-0 left-0 w-full text-xl sm:text-4xl italic text-white text-center opacity-0; @apply absolute top-0 left-0 w-full text-xl sm:text-4xl italic text-white text-center opacity-0;
transition: transform .5s ease-out, opacity .5s linear; transition:
transform 0.5s ease-out,
opacity 0.5s linear;
text-shadow: 2px 2px 5px black; text-shadow: 2px 2px 5px black;
} }
} }
.barlow { .barlow {
font-family: Barlow Condensed, sans-serif; font-family:
Barlow Condensed,
sans-serif;
} }
.card { .card {
@@ -207,7 +218,7 @@ const prefixColorMap: {
} }
> svg { > svg {
@apply transition-transform duration-300 ease-in-out hover:scale-110 hover:drop-shadow-2xl @apply transition-transform duration-300 ease-in-out hover:scale-110 hover:drop-shadow-2xl;
} }
} }
</style> </style>

View File

@@ -31,7 +31,7 @@ table {
text-align: center; text-align: center;
tr:nth-child(odd) { tr:nth-child(odd) {
@apply bg-neutral-200 dark:bg-neutral-800; @apply backdrop-brightness-125;
} }
} }
} }