2 Commits

Author SHA1 Message Date
4d100fcafc Started implementing tutorials
Some checks failed
SteamWarCI Build failed
2025-04-23 10:39:19 +02:00
82f5ab48b8 Add tracer tutorial 2025-01-16 21:47:03 +01:00
334 changed files with 3227 additions and 11018 deletions

View File

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

View File

@ -1,17 +0,0 @@
{
"$schema": "https://shadcn-svelte.com/schema.json",
"style": "default",
"tailwind": {
"config": "tailwind.config.js",
"css": "src\\styles\\app.css",
"baseColor": "slate"
},
"aliases": {
"components": "$lib/components",
"utils": "$lib/components/utils",
"ui": "$lib/components/ui",
"hooks": "$lib/hooks"
},
"typescript": true,
"registry": "https://next.shadcn-svelte.com/registry"
}

View File

@ -14,54 +14,40 @@
"i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types", "i18n:sync": "pnpm run i18n:generate:pages && pnpm run i18n:generate:types",
"clean:dist": "rm -rf dist", "clean:dist": "rm -rf dist",
"clean:node_modules": "rm -rf node_modules", "clean:node_modules": "rm -rf node_modules",
"ci": "pnpm install && pnpm run i18n:sync && pnpm run build" "ci": "pnpm run clean:dist && pnpm install && pnpm run i18n:sync && pnpm run build"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/svelte": "^7.0.4", "@astrojs/svelte": "^6.0.2",
"@astrojs/tailwind": "^5.1.5", "@astrojs/tailwind": "^5.1.2",
"@astropub/icons": "^0.2.0", "@astropub/icons": "^0.2.0",
"@internationalized/date": "^3.7.0",
"@types/color": "^4.2.0", "@types/color": "^4.2.0",
"@types/node": "^22.9.3", "@types/node": "^22.9.3",
"@types/three": "^0.170.0", "@types/three": "^0.170.0",
"@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0", "@typescript-eslint/parser": "^8.15.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"bits-ui": "1.3.4",
"clsx": "^2.1.1",
"cmdk-sv": "^0.0.18",
"cssnano": "^7.0.6", "cssnano": "^7.0.6",
"embla-carousel-svelte": "^8.5.2",
"esbuild": "^0.24.0", "esbuild": "^0.24.0",
"eslint": "^9.15.0", "eslint": "^9.15.0",
"eslint-plugin-astro": "^1.3.1", "eslint-plugin-astro": "^1.3.1",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-svelte": "^2.46.0", "eslint-plugin-svelte": "^2.46.0",
"formsnap": "1.0.1",
"lucide-svelte": "^0.476.0",
"mode-watcher": "^0.5.1",
"paneforge": "^0.0.6",
"postcss-nesting": "^13.0.1", "postcss-nesting": "^13.0.1",
"sass": "^1.81.0", "sass": "^1.81.0",
"svelte": "^5.16.0", "svelte": "^5.16.0",
"svelte-sonner": "^0.3.28",
"tailwind-merge": "^2.5.5", "tailwind-merge": "^2.5.5",
"tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.15", "tailwindcss": "^3.4.15",
"three": "^0.170.0", "three": "^0.170.0",
"typescript": "^5.7.2", "typescript": "^5.7.2"
"vaul-svelte": "^0.3.2",
"zod": "^3.23.8"
}, },
"dependencies": { "dependencies": {
"@astrojs/mdx": "^4.0.7", "@astrojs/mdx": "^3.1.9",
"@astrojs/sitemap": "^3.2.1", "@astrojs/sitemap": "^3.2.1",
"@codemirror/commands": "^6.8.0",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@ddietr/codemirror-themes": "^1.4.4", "@ddietr/codemirror-themes": "^1.4.4",
"@tanstack/table-core": "^8.21.2", "astro": "^4.16.14",
"astro": "^5.1.8",
"astro-i18n": "^2.2.4", "astro-i18n": "^2.2.4",
"astro-pagefind": "^1.6.0",
"astro-robots-txt": "^1.0.0", "astro-robots-txt": "^1.0.0",
"astro-seo": "^0.8.4", "astro-seo": "^0.8.4",
"chart.js": "^4.4.6", "chart.js": "^4.4.6",
@ -77,6 +63,7 @@
"sharp": "^0.33.5", "sharp": "^0.33.5",
"svelte-awesome": "^3.3.5", "svelte-awesome": "^3.3.5",
"svelte-codemirror-editor": "^1.4.1", "svelte-codemirror-editor": "^1.4.1",
"svelte-spa-router": "^4.0.1" "svelte-spa-router": "^4.0.1",
"zod": "^3.23.8"
} }
} }

5750
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 406 KiB

View File

@ -1,21 +0,0 @@
{
"name": "MyWebSite",
"short_name": "MySite",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -20,7 +20,6 @@
<script lang="ts"> <script lang="ts">
import {t} from "astro-i18n"; import {t} from "astro-i18n";
import {statsRepo} from "@repo/stats.ts"; import {statsRepo} from "@repo/stats.ts";
import "@styles/table.css"
interface Props { interface Props {
@ -65,3 +64,7 @@
<p>{error.message}</p> <p>{error.message}</p>
{/await} {/await}
<style>
@import "../styles/table.css";
</style>

View File

@ -79,8 +79,6 @@
}) })
}, },
options: { options: {
maintainAspectRatio: false,
scales: { scales: {
x: { x: {
type: "time", type: "time",
@ -107,5 +105,5 @@
</script> </script>
<div> <div>
<canvas height="500" bind:this={canvas}></canvas> <canvas bind:this={canvas}></canvas>
</div> </div>

View File

@ -19,7 +19,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import {window} from "./utils.ts"; import {window} from "./util.ts";
import {astroI18n, t} from "astro-i18n"; import {astroI18n, t} from "astro-i18n";
import type {EventFight, ExtendedEvent} from "@type/event"; import type {EventFight, ExtendedEvent} from "@type/event";
import "@styles/table.css"; import "@styles/table.css";
@ -55,7 +55,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each window(event.fights.filter(f => group === undefined ? true : f.group === group), rows) as fights} {#each window(event.fights.filter(f => f.group === group), rows) as fights}
<tr> <tr>
{#each fights as fight (fight.id)} {#each fights as fight (fight.id)}
<td>{Intl.DateTimeFormat(astroI18n.locale, { <td>{Intl.DateTimeFormat(astroI18n.locale, {

View File

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

View File

@ -31,7 +31,8 @@
let error: string = $state(""); let error: string = $state("");
async function login() { async function login() {
let {authV2Repo} = await import("./repo/authv2.ts"); let {tokenStore} = await import("./repo/repo.ts");
let {authRepo} = await import("./repo/auth.ts");
if (username === "" || pw === "") { if (username === "" || pw === "") {
pw = ""; pw = "";
error = t("login.error"); error = t("login.error");
@ -39,14 +40,15 @@
} }
try { try {
let auth = await get(authV2Repo).login(username, pw); let auth = await get(authRepo).login(username, pw);
if (!auth) { if (auth == undefined) {
pw = ""; pw = "";
error = t("login.error"); error = t("login.error");
return; return;
} }
await navigate(l("/dashboard")); tokenStore.set(auth);
navigate(l("/dashboard"));
} catch (e: any) { } catch (e: any) {
pw = ""; pw = "";
error = t("login.error"); error = t("login.error");
@ -73,7 +75,9 @@
<style lang="postcss"> <style lang="postcss">
input { input {
@apply border-2 rounded-md p-2 shadow-2xl w-80 dark:bg-neutral-800 focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:border-transparent text-black; @apply border-2 rounded-md p-2 shadow-2xl w-80
dark:bg-neutral-800
focus:outline-none focus:ring-2 focus:ring-neutral-500 focus:border-transparent;
} }
label { label {

View File

@ -19,13 +19,13 @@
<script lang="ts"> <script lang="ts">
import "../styles/button.css"; import "../styles/button.css";
import {CaretDownOutline, SearchOutline} from "flowbite-svelte-icons"; import { CaretDownOutline, SearchOutline } from "flowbite-svelte-icons";
import {t} from "astro-i18n"; import { t } from "astro-i18n";
import {l} from "../util/util"; import { l } from "../util/util";
import {onMount} from "svelte"; import { onMount } from "svelte";
import {loggedIn} from "@repo/authv2.ts"; import { loggedIn } from "@repo/authv2.ts";
interface Props { interface Props {
logo?: import('svelte').Snippet; logo?: import("svelte").Snippet;
} }
let { logo }: Props = $props(); let { logo }: Props = $props();
@ -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) {
@ -56,15 +56,27 @@
} }
</script> </script>
<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
<div class="flex flex-row items-center justify-evenly md:justify-between match"> 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}
>
<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 dark: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>
</a> </a>
<div class="flex justify-center flex-wrap"> <div class="flex justify-center flex-wrap">
@ -73,13 +85,24 @@
<a href={l("/")}> <a href={l("/")}>
<span class="btn__text">{t("navbar.links.home.title")}</span> <span class="btn__text">{t("navbar.links.home.title")}</span>
</a> </a>
<CaretDownOutline class="ml-2 mt-auto"/> <CaretDownOutline class="ml-2 mt-auto" />
</button> </button>
<div> <div>
<a class="btn btn-gray" href={l("/announcements")}>{t("navbar.links.home.announcements")}</a> <a class="btn btn-gray" href={l("/announcements")}
<a class="btn btn-gray" href={l("/downloads")}>{t("navbar.links.home.downloads")}</a> >{t("navbar.links.home.announcements")}</a
<a class="btn btn-gray" href={l("/faq")}>{t("navbar.links.home.faq")}</a> >
<a class="btn btn-gray" href={l("/code-of-conduct")}>{t("navbar.links.rules.coc")}</a> <a class="btn btn-gray" href={l("/downloads")}
>{t("navbar.links.home.downloads")}</a
>
<a class="btn btn-gray" href={l("/tutorials")}
>{t("navbar.links.home.tutorials")}</a
>
<a class="btn btn-gray" href={l("/faq")}
>{t("navbar.links.home.faq")}</a
>
<a class="btn btn-gray" href={l("/code-of-conduct")}
>{t("navbar.links.rules.coc")}</a
>
</div> </div>
</div> </div>
<div class="btn-dropdown"> <div class="btn-dropdown">
@ -87,22 +110,38 @@
<a rel="prefetch" href={l("/rules")}> <a rel="prefetch" href={l("/rules")}>
<span class="btn__text">{t("navbar.links.rules.title")}</span> <span class="btn__text">{t("navbar.links.rules.title")}</span>
</a> </a>
<CaretDownOutline class="ml-2 mt-auto"/> <CaretDownOutline class="ml-2 mt-auto" />
</button> </button>
<div> <div>
<a href={l("/rules/wargear")} class="btn btn-gray">{t("navbar.links.rules.wg")}</a> <a href={l("/rules/wargear")} class="btn btn-gray"
<a href={l("/rules/miniwargear")} class="btn btn-gray">{t("navbar.links.rules.mwg")}</a> >{t("navbar.links.rules.wg")}</a
<a href={l("/rules/warship")} class="btn btn-gray">{t("navbar.links.rules.ws")}</a> >
<a href={l("/rules/airship")} class="btn btn-gray">{t("navbar.links.rules.as")}</a> <a href={l("/rules/miniwargear")} class="btn btn-gray"
<a href={l("/rules/quickgear")} class="btn btn-gray">{t("navbar.links.rules.qg")}</a> >{t("navbar.links.rules.mwg")}</a
>
<a href={l("/rules/warship")} class="btn btn-gray"
>{t("navbar.links.rules.ws")}</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
>
<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"
class="btn btn-gray">{t("navbar.links.rules.megawg")}</a> >{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"
<a href={l("/rules/streetfight")} class="btn btn-gray">{t("navbar.links.rules.sf")}</a> >{t("navbar.links.rules.micro")}</a
>
<a href={l("/rules/streetfight")} class="btn btn-gray"
>{t("navbar.links.rules.sf")}</a
>
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.ranked")}</h2> <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
>
</div> </div>
</div> </div>
<!-- TODO: Add help center <!-- TODO: Add help center

View File

@ -1,5 +1,5 @@
--- ---
import type {CollectionEntry} from "astro:content"; import {CollectionEntry} from "astro:content";
import {l} from "../util/util"; import {l} from "../util/util";
import {astroI18n} from "astro-i18n"; import {astroI18n} from "astro-i18n";
import {Image} from "astro:assets"; import {Image} from "astro:assets";

View File

@ -22,40 +22,38 @@
import wrap from "svelte-spa-router/wrap"; import wrap from "svelte-spa-router/wrap";
import Router, {replace} from "svelte-spa-router"; import Router, {replace} from "svelte-spa-router";
import {get} from "svelte/store"; import {get} from "svelte/store";
import {loggedIn} from "@repo/authv2.ts"; import {tokenStore} from "@repo/repo";
const routes: RouteDefinition = { const routes: RouteDefinition = {
"/": wrap({asyncComponent: () => import("./pages/Home.svelte"), conditions: detail => get(loggedIn)}), "/": wrap({asyncComponent: () => import("./pages/Home.svelte"), conditions: detail => get(tokenStore) != ""}),
"/perms": wrap({ "/perms": wrap({
asyncComponent: () => import("./pages/Perms.svelte"), asyncComponent: () => import("./pages/Perms.svelte"),
conditions: detail => get(loggedIn) conditions: detail => get(tokenStore) != ""
}), }),
"/login": wrap({ "/login": wrap({
asyncComponent: () => import("./pages/Login.svelte"), asyncComponent: () => import("./pages/Login.svelte"),
conditions: detail => !get(loggedIn) conditions: detail => get(tokenStore) == ""
}), }),
"/event/:id": wrap({ "/event/:id": wrap({
asyncComponent: () => import("./pages/Event.svelte"), asyncComponent: () => import("./pages/Event.svelte"),
conditions: detail => get(loggedIn) conditions: detail => get(tokenStore) != ""
}), }),
"/event/:id/generate": wrap({ "/event/:id/generate": wrap({
asyncComponent: () => import("./pages/Generate.svelte"), asyncComponent: () => import("./pages/Generate.svelte"),
conditions: detail => get(loggedIn) conditions: detail => get(tokenStore) != ""
}), }),
"/edit": wrap({ "/edit": wrap({
asyncComponent: () => import("./pages/Edit.svelte"), asyncComponent: () => import("./pages/Edit.svelte"),
conditions: detail => get(loggedIn) conditions: detail => get(tokenStore) != ""
}), }),
"/display/:event": wrap({ "/display/:event": wrap({
asyncComponent: () => import("./pages/Display.svelte"), asyncComponent: () => import("./pages/Display.svelte"),
conditions: detail => get(loggedIn) conditions: detail => get(tokenStore) != ""
}), }),
"*": wrap({asyncComponent: () => import("./pages/NotFound.svelte")}) "*": wrap({asyncComponent: () => import("./pages/NotFound.svelte")})
}; };
function conditionsFailed(event: ConditionsFailedEvent) { function conditionsFailed(event: ConditionsFailedEvent) {
console.log(event)
if (event.detail.location === "/login") { if (event.detail.location === "/login") {
replace("/"); replace("/");
} else { } else {

View File

@ -168,11 +168,11 @@
</div> </div>
<div> <div>
{#if selectedBranch !== "master"} {#if selectedBranch !== "master"}
<Button onclick={() => createFile()} color="alternative" disabled={!selectedPath}>Create File <Button onclick={createFile} color="alternative" disabled={!selectedPath}>Create File
</Button> </Button>
<Button onclick={() => deleteBranch(false)} color="none">Delete Branch</Button> <Button onclick={() => deleteBranch(false)} color="none">Delete Branch</Button>
{:else} {:else}
<Button onclick={() => createBranch()}>Create Branch</Button> <Button onclick={createBranch}>Create Branch</Button>
{/if} {/if}
</div> </div>
</div> </div>

View File

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

View File

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

View File

@ -31,7 +31,7 @@
let request = getRequest(); let request = getRequest();
function getRequest() { function getRequest() {
return $statsRepo.getUserStats(user.uuid) return $statsRepo.getUserStats(user.id)
} }
</script> </script>
@ -43,5 +43,8 @@
maximumFractionDigits: 2 maximumFractionDigits: 2
}).format(data.playtime)})}h</p> }).format(data.playtime)})}h</p>
<p>{t("dashboard.stats.fights", {fights: data.fights})}</p> <p>{t("dashboard.stats.fights", {fights: data.fights})}</p>
{#if user.perms.includes("CHECK")}
<p>{t("dashboard.stats.checked", {checked: data.acceptedSchematics})}</p> <p>{t("dashboard.stats.checked", {checked: data.acceptedSchematics})}</p>
{/if}
{:catch error}
{/await} {/await}

View File

@ -21,7 +21,7 @@
import {createEventDispatcher} from "svelte"; import {createEventDispatcher} from "svelte";
import {schemRepo} from "@repo/schem.ts"; import {schemRepo} from "@repo/schem.ts";
import SWModal from "@components/styled/SWModal.svelte"; import SWModal from "@components/styled/SWModal.svelte";
import {t} from "astro-i18n"; import {t} from "astro-i18n"
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@ -29,13 +29,11 @@
open?: boolean; open?: boolean;
} }
let {open = $bindable(false)}: Props = $props(); let { open = $bindable(false) }: Props = $props();
async function upload(e: Event) { async function upload() {
e.stopPropagation();
if (uploadFile == null) { if (uploadFile == null) {
error = "dashboard.schematic.errors.noFile"; return
return;
} }
let file = uploadFile[0]; let file = uploadFile[0];
@ -44,46 +42,32 @@
let type = name.split(".").pop(); let type = name.split(".").pop();
if (type !== "schem" && type !== "schematic") { if (type !== "schem" && type !== "schematic") {
error = "dashboard.schematic.errors.invalidEnding"; return
return;
} }
let content = await file.arrayBuffer(); let content = await file.arrayBuffer();
// @ts-ignore
let b64 = btoa(String.fromCharCode.apply(null, new Uint8Array(content))); let b64 = btoa(String.fromCharCode.apply(null, new Uint8Array(content)));
try {
await $schemRepo.uploadSchematic(name, b64); await $schemRepo.uploadSchematic(name, b64);
open = false; open = false;
value = ""; uploadFile = null;
dispatch("reset"); dispatch("reset")
} catch (e) {
error = "dashboard.schematic.errors.upload";
}
}
function reset(e: Event) {
e.stopPropagation();
open = false
value = "";
} }
let uploadFile: FileList | null = $state(null); let uploadFile: FileList | null = $state(null);
let value = $state("");
let error = $state(null)
</script> </script>
<SWModal title={t("dashboard.schematic.title")} bind:open> <SWModal title={t("dashboard.schematic.title")} bind:open>
<form> <form>
<label for="schem-upload">{t("dashboard.schematic.title")}</label> <input type="file" bind:files={uploadFile} />
<input type="file" id="schem-upload" bind:files={uploadFile} class="overflow-ellipsis" bind:value accept=".schem, .schematic"/>
{#if error !== null}
<p class="text-red-400">{t(error)}</p>
{/if}
</form> </form>
{#snippet footer()} {#snippet footer()}
<button class="btn" onclick={upload}>{t("dashboard.schematic.upload")}</button>
<button class="btn btn-gray" onclick={reset}>{t("dashboard.schematic.cancel")}</button> <button class="btn !ml-auto" onclick={upload}>{t("dashboard.schematic.upload")}</button>
<button class="btn btn-gray" onclick={() => open = false}>{t("dashboard.schematic.cancel")}</button>
{/snippet} {/snippet}
</SWModal> </SWModal>

View File

@ -22,9 +22,9 @@
import type {Player} from "@type/data.ts"; import type {Player} from "@type/data.ts";
import {l} from "@utils/util.ts"; import {l} from "@utils/util.ts";
import Statistics from "./Statistics.svelte"; import Statistics from "./Statistics.svelte";
import {authV2Repo} from "@repo/authv2.ts"; import {authRepo} from "@repo/auth.ts";
import {tokenStore} from "@repo/repo.ts";
import Card from "@components/Card.svelte"; import Card from "@components/Card.svelte";
import {navigate} from "astro:transitions/client";
interface Props { interface Props {
user: Player; user: Player;
@ -33,8 +33,9 @@
let { user }: Props = $props(); let { user }: Props = $props();
async function logout() { async function logout() {
await $authV2Repo.logout(); await $authRepo.logout()
await navigate(l("/login")); tokenStore.set("")
window.location.href = l("/login")
} }
</script> </script>
@ -55,7 +56,7 @@
</div> </div>
<div> <div>
<h1 class="text-4xl font-bold">{t("dashboard.title", {name: user.name})}</h1> <h1 class="text-4xl font-bold">{t("dashboard.title", {name: user.name})}</h1>
<p>{t("dashboard.rank", {rank: t("home.prefix." + (user.prefix || "User"))})}</p> <p>{t("dashboard.rank", {rank: t("home.prefix." + user.prefix)})}</p>
<Statistics {user} /> <Statistics {user} />
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,47 +0,0 @@
<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import type {ExtendedEvent} from "@type/event.ts";
import EventEdit from "@components/moderator/pages/event/EventEdit.svelte";
import EventFightList from "@components/moderator/pages/event/EventFightList.svelte";
import RefereesList from "@components/moderator/pages/event/RefereesList.svelte";
const {
event
}: { event: ExtendedEvent } = $props();
</script>
<div class="flex flex-col m-4 p-4 rounded-md border gap-4">
<div class="flex flex-col md:flex-row">
<div class="md:w-1/3">
<h1 class="text-2xl font-bold mb-4">{event.event.name}</h1>
<EventEdit event={event.event} />
</div>
<div class="md:ml-4 md:pl-4 md:border-l md:w-1/3">
<h2>Teams</h2>
</div>
<div class="md:ml-4 md:pl-4 md:border-l md:w-1/3">
<h2>Referees</h2>
<RefereesList event={event} />
</div>
</div>
<EventFightList data={event} />
</div>

View File

@ -1,92 +0,0 @@
<!--
- 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 {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table/index.js";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@components/ui/command/index.js";
import {Popover, PopoverContent, PopoverTrigger} from "@components/ui/popover/index.js";
import {Button} from "@components/ui/button/index.js";
import type {ExtendedEvent} from "@type/event.ts";
import { eventRepo } from "@repo/event";
import { players } from "@stores/stores"
const {
event
}: { event: ExtendedEvent } = $props();
let referees = $state(event.event.referees)
async function addReferee(value: string) {
referees = (await $eventRepo.updateEvent(event.event.id.toString(), {
addReferee: [value]
})).referees;
}
async function removeReferee(value: string) {
referees = (await $eventRepo.updateEvent(event.event.id.toString(), {
removeReferee: [value]
})).referees;
}
</script>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{#each referees as referee (referee.uuid)}
<TableRow>
<TableCell>{referee.name}</TableCell>
<TableCell>
<Button onclick={() => removeReferee(referee.uuid)}>Remove</Button>
</TableCell>
</TableRow>
{/each}
</TableBody>
</Table>
<Popover>
<PopoverTrigger>
<Button>
Add
</Button>
</PopoverTrigger>
<PopoverContent class="p-0">
<Command>
<CommandInput placeholder="Search players..." />
<CommandList>
<CommandEmpty>No Players found :(</CommandEmpty>
<CommandGroup heading="Players">
{#each $players.filter(v => v.perms.length > 0).filter(v => !referees.some(k => k.uuid === v.uuid)) as player (player.uuid)}
<CommandItem value={player.uuid} onSelect={() => addReferee(player.uuid)}>{player.name}</CommandItem>
{/each}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import {onDestroy, onMount} from "svelte"; import {onDestroy, onMount} from "svelte";
import type { CollectionEntry } from "astro:content"; import { CollectionEntry } from "astro:content";
interface Props { interface Props {
pub: CollectionEntry<"publics">; pub: CollectionEntry<"publics">;

View File

@ -0,0 +1,44 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2023 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {fetchWithToken, tokenStore} from "./repo.ts";
import {derived} from "svelte/store";
export class AuthRepo {
constructor(private token: string) {
}
public async login(username: string, password: string): Promise<string> {
return await fetchWithToken(this.token, "/auth/login", {
body: JSON.stringify({
username,
password,
}),
method: "POST",
}).then(value => value.json()).then(value => value.token);
}
public async logout(): Promise<void> {
await fetchWithToken(this.token, "/auth/tokens/logout", {
method: "POST",
});
}
}
export const authRepo = derived(tokenStore, ($token) => new AuthRepo($token));

View File

@ -1,184 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {readable, writable} from "svelte/store";
import dayjs, {type Dayjs} from "dayjs";
import {type AuthToken, AuthTokenSchema} from "@type/auth.ts";
export class AuthV2Repo {
private accessToken: string | undefined;
private accessTokenExpires: Dayjs | undefined;
private refreshToken: string | undefined;
private refreshTokenExpires: Dayjs | undefined;
constructor() {
if (typeof localStorage === "undefined") {
return;
}
this.accessToken = localStorage.getItem("sw-access-token") ?? undefined;
if (this.accessToken) {
this.accessTokenExpires = dayjs(localStorage.getItem("sw-access-token-expires") ?? "");
}
this.refreshToken = localStorage.getItem("sw-refresh-token") ?? undefined;
if (this.refreshToken) {
loggedIn.set(true);
this.refreshTokenExpires = dayjs(localStorage.getItem("sw-refresh-token-expires") ?? "");
}
}
async login(name: string, password: string) {
if (this.accessToken !== undefined || this.refreshToken !== undefined) {
throw new Error("Already logged in");
}
try {
const login = await this.request("/auth", {
method: "POST",
body: JSON.stringify({
name,
password,
keepLoggedIn: true,
}),
}).then(value => value.json()).then(value => AuthTokenSchema.parse(value));
this.setLoginState(login);
return true;
} catch (e) {
return false;
}
}
async logout() {
if (this.accessToken === undefined) {
return;
}
await this.request("/auth", {
method: "DELETE",
});
this.resetAccessToken();
this.resetRefreshToken();
}
private setLoginState(tokens: AuthToken) {
this.setAccessToken(tokens.accessToken.token, dayjs(tokens.accessToken.expires));
this.setRefreshToken(tokens.refreshToken.token, dayjs(tokens.refreshToken.expires));
loggedIn.set(true);
}
private setAccessToken(token: string, expires: Dayjs) {
this.accessToken = token;
this.accessTokenExpires = expires;
localStorage.setItem("sw-access-token", token);
localStorage.setItem("sw-access-token-expires", expires.toString());
}
private resetAccessToken() {
if (this.accessToken === undefined) {
return;
}
this.accessToken = undefined;
this.accessTokenExpires = undefined;
localStorage.removeItem("sw-access-token");
localStorage.removeItem("sw-access-token-expires");
}
private setRefreshToken(token: string, expires: Dayjs) {
this.refreshToken = token;
this.refreshTokenExpires = expires;
localStorage.setItem("sw-refresh-token", token);
localStorage.setItem("sw-refresh-token-expires", expires.toString());
}
private resetRefreshToken() {
if (this.refreshToken === undefined) {
return;
}
this.refreshToken = undefined;
this.refreshTokenExpires = undefined;
localStorage.removeItem("sw-refresh-token");
localStorage.removeItem("sw-refresh-token-expires");
loggedIn.set(false);
}
private async refresh() {
if (this.refreshToken === undefined || this.refreshTokenExpires === undefined || this.refreshTokenExpires.isBefore(dayjs().add(10, "seconds"))) {
this.resetRefreshToken();
this.resetAccessToken();
return;
}
const response = await this.requestWithToken(this.refreshToken!, "/auth", {
method: "PUT",
}).then(value => {
if (value.status === 401) {
this.resetRefreshToken();
this.resetAccessToken();
return undefined;
}
return value.json();
}).then(value => AuthTokenSchema.parse(value));
this.setLoginState(response);
}
async request(url: string, params: RequestInit = {}, retryCount: number = 0) {
if (this.accessToken !== undefined && this.accessTokenExpires !== undefined && this.accessTokenExpires.isBefore(dayjs().add(10, "seconds"))) {
await this.refresh();
}
return this.requestWithToken(this.accessToken ?? "", url, params, retryCount);
}
private async requestWithToken(token: string, url: string, params: RequestInit = {}, retryCount: number = 0): Promise<Response> {
if (retryCount >= 3) {
throw new Error("Too many retries");
}
return fetch(`${import.meta.env.PUBLIC_API_SERVER}${url}`, {...params,
headers: {
...(token !== "" ? {"Authorization": "Bearer " + (token)} : {}),
"Content-Type": "application/json", ...params.headers,
},
})
.then(async value => {
if (value.status === 401 && url !== "/auth") {
try {
await this.refresh();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_e) { /* empty */ }
return this.request(url, params, retryCount + 1);
}
return value;
});
}
}
export const loggedIn = writable(false);
export const authV2Repo = readable(new AuthV2Repo());

View File

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

View File

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

View File

@ -17,9 +17,31 @@
* 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 {get, writable} from "svelte/store"; import {writable} from "svelte/store";
import {authV2Repo} from "@repo/authv2.ts";
export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) => get(authV2Repo).request(url, params); export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) =>
fetch(`${import.meta.env.PUBLIC_API_SERVER}${url}`, {...params,
headers: {
...(token !== "" ? {"Authorization": "Bearer " + (token)} : {}),
"Content-Type": "application/json", ...params.headers,
},
})
.then(value => {
if (value.status === 401) {
tokenStore.set("");
}
return value;
});
export const tokenStore = writable(""); export function getLocalStorage() {
if (typeof localStorage === "undefined") {
return {
getItem: () => "",
setItem: () => {},
};
}
return localStorage;
}
export const tokenStore = writable((getLocalStorage().getItem("sw-session") ?? ""));
tokenStore.subscribe((value) => getLocalStorage().setItem("sw-session", value));

View File

@ -20,7 +20,6 @@
import {fetchWithToken, tokenStore} from "./repo.ts"; import {fetchWithToken, tokenStore} from "./repo.ts";
import {type Schematic, SchematicSchema} from "@type/schem.ts"; import {type Schematic, SchematicSchema} from "@type/schem.ts";
import {derived} from "svelte/store"; import {derived} from "svelte/store";
import {ResponseErrorSchema} from "@type/data.ts";
export class SchematicRepo { export class SchematicRepo {
constructor(private token: string) { constructor(private token: string) {
@ -41,7 +40,7 @@ export class SchematicRepo {
name, name,
content, content,
}), }),
}).then(value => value.json()).then(SchematicSchema.or(ResponseErrorSchema).parse); });
} }
} }

View File

@ -36,7 +36,7 @@ export class StatsRepo {
} }
public async getUserStats(id: string): Promise<UserStats> { public async getUserStats(id: string): Promise<UserStats> {
return await fetchWithToken(this.token, `/stats/user`).then(value => value.json()).then(UserStatsSchema.parse); return await fetchWithToken(this.token, `/stats/user/${id}`).then(value => value.json()).then(UserStatsSchema.parse);
} }
} }

View File

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

View File

@ -19,7 +19,7 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte"; import {onMount} from "svelte";
import {stopPropagation} from "@components/utils.ts"; import {stopPropagation} from "@components/util.ts";
interface Props { interface Props {
title: string; title: string;
@ -68,18 +68,16 @@
}) })
</script> </script>
<dialog bind:this={dialog} onclose={close} onclick={(e) => dialog.close()} aria-hidden="true" class="max-h-full min-w-md w-fit rounded-lg shadow-lg dark:bg-neutral-800 dark:text-neutral-100"> <dialog bind:this={dialog} onclose={close} onclick={(e) => dialog.close()} aria-hidden="true" class="max-h-full max-w-md w-full rounded-lg shadow-lg dark:bg-neutral-800 dark:text-neutral-100">
<div onclick={stopPropagation(onclick)} aria-hidden="true" class="w-fit"> <div onclick={stopPropagation(onclick)} aria-hidden="true">
<div class="p-6 border-b border-neutral-200 dark:border-neutral-700"> <div class="p-6 border-b border-neutral-200 dark:border-neutral-700">
<h1 class="text-4xl font-bold">{title}</h1> <h1 class="text-4xl font-bold">{title}</h1>
</div> </div>
<div class="p-6 main border-b border-neutral-200 dark:border-neutral-700"> <div class="p-6 main border-b border-neutral-200 dark:border-neutral-700">
{@render children?.()} {@render children?.()}
</div> </div>
<div class="mx-4 my-2 p-6"> <div class="flex mx-4 my-2 p-6" onclick={() => dialog.close()} aria-hidden="true">
<div class="ml-auto flex justify-end" onclick={() => dialog.close()} aria-hidden="true">
{@render footer?.()} {@render footer?.()}
</div> </div>
</div> </div>
</div>
</dialog> </dialog>

View File

@ -1,34 +0,0 @@
/*
* This file is a part of the SteamWar software.
*
* Copyright (C) 2025 SteamWar.de-Serverteam
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {z} from "zod";
export const TokenSchema = z.object({
token: z.string(),
expires: z.string(),
});
export type Token = z.infer<typeof TokenSchema>;
export const AuthTokenSchema = z.object({
accessToken: TokenSchema,
refreshToken: TokenSchema,
});
export type AuthToken = z.infer<typeof AuthTokenSchema>;

View File

@ -50,10 +50,3 @@ export const ServerSchema = z.object({
}); });
export type Server = z.infer<typeof ServerSchema>; export type Server = z.infer<typeof ServerSchema>;
export const ResponseErrorSchema = z.object({
error: z.string(),
code: z.string(),
});
export type ResponseError = z.infer<typeof ResponseErrorSchema>;

View File

@ -1,25 +0,0 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { slide } from "svelte/transition";
import { cn } from "$lib/components/utils.js";
type $$Props = AccordionPrimitive.ContentProps;
let className: $$Props["class"] = undefined;
export let transition: $$Props["transition"] = slide;
export let transitionConfig: $$Props["transitionConfig"] = {
duration: 200,
};
export { className as class };
</script>
<AccordionPrimitive.Content
class={cn("overflow-hidden text-sm transition-all", className)}
{transition}
{transitionConfig}
{...$$restProps}
>
<div class="pb-4 pt-0">
<slot />
</div>
</AccordionPrimitive.Content>

View File

@ -1,14 +0,0 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
type $$Props = AccordionPrimitive.ItemProps;
let className: $$Props["class"] = undefined;
export let value: $$Props["value"];
export { className as class };
</script>
<AccordionPrimitive.Item {value} class={cn("border-b", className)} {...$$restProps}>
<slot />
</AccordionPrimitive.Item>

View File

@ -1,26 +0,0 @@
<script lang="ts">
import { Accordion as AccordionPrimitive } from "bits-ui";
import ChevronDown from "lucide-svelte/icons/chevron-down";
import { cn } from "$lib/components/utils.js";
type $$Props = AccordionPrimitive.TriggerProps;
type $$Events = AccordionPrimitive.TriggerEvents;
let className: $$Props["class"] = undefined;
export let level: AccordionPrimitive.HeaderProps["level"] = 3;
export { className as class };
</script>
<AccordionPrimitive.Header {level} class="flex">
<AccordionPrimitive.Trigger
class={cn(
"flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
className
)}
{...$$restProps}
on:click
>
<slot />
<ChevronDown class="h-4 w-4 transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>

View File

@ -1,17 +0,0 @@
import { Accordion as AccordionPrimitive } from "bits-ui";
import Content from "./accordion-content.svelte";
import Item from "./accordion-item.svelte";
import Trigger from "./accordion-trigger.svelte";
const Root = AccordionPrimitive.Root;
export {
Root,
Content,
Item,
Trigger,
//
Root as Accordion,
Content as AccordionContent,
Item as AccordionItem,
Trigger as AccordionTrigger,
};

View File

@ -1,21 +0,0 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.ActionProps;
type $$Events = AlertDialogPrimitive.ActionEvents;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AlertDialogPrimitive.Action
class={cn(buttonVariants(), className)}
{...$$restProps}
on:click
on:keydown
let:builder
>
<slot {builder} />
</AlertDialogPrimitive.Action>

View File

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

View File

@ -1,28 +0,0 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import * as AlertDialog from "./index.js";
import { cn, flyAndScale } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.ContentProps;
export let transition: $$Props["transition"] = flyAndScale;
export let transitionConfig: $$Props["transitionConfig"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AlertDialog.Portal>
<AlertDialog.Overlay />
<AlertDialogPrimitive.Content
{transition}
{transitionConfig}
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",
className
)}
{...$$restProps}
>
<slot />
</AlertDialogPrimitive.Content>
</AlertDialog.Portal>

View File

@ -1,16 +0,0 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.DescriptionProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AlertDialogPrimitive.Description
class={cn("text-muted-foreground text-sm", className)}
{...$$restProps}
>
<slot />
</AlertDialogPrimitive.Description>

View File

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

View File

@ -1,13 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...$$restProps}>
<slot />
</div>

View File

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

View File

@ -1,9 +0,0 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
type $$Props = AlertDialogPrimitive.PortalProps;
</script>
<AlertDialogPrimitive.Portal {...$$restProps}>
<slot />
</AlertDialogPrimitive.Portal>

View File

@ -1,14 +0,0 @@
<script lang="ts">
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
type $$Props = AlertDialogPrimitive.TitleProps;
let className: $$Props["class"] = undefined;
export let level: $$Props["level"] = "h3";
export { className as class };
</script>
<AlertDialogPrimitive.Title class={cn("text-lg font-semibold", className)} {level} {...$$restProps}>
<slot />
</AlertDialogPrimitive.Title>

View File

@ -1,40 +0,0 @@
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
import Title from "./alert-dialog-title.svelte";
import Action from "./alert-dialog-action.svelte";
import Cancel from "./alert-dialog-cancel.svelte";
import Portal from "./alert-dialog-portal.svelte";
import Footer from "./alert-dialog-footer.svelte";
import Header from "./alert-dialog-header.svelte";
import Overlay from "./alert-dialog-overlay.svelte";
import Content from "./alert-dialog-content.svelte";
import Description from "./alert-dialog-description.svelte";
const Root = AlertDialogPrimitive.Root;
const Trigger = AlertDialogPrimitive.Trigger;
export {
Root,
Title,
Action,
Cancel,
Portal,
Footer,
Header,
Trigger,
Overlay,
Content,
Description,
//
Root as AlertDialog,
Title as AlertDialogTitle,
Action as AlertDialogAction,
Cancel as AlertDialogCancel,
Portal as AlertDialogPortal,
Footer as AlertDialogFooter,
Header as AlertDialogHeader,
Trigger as AlertDialogTrigger,
Overlay as AlertDialogOverlay,
Content as AlertDialogContent,
Description as AlertDialogDescription,
};

View File

@ -1,13 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement>;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<div class={cn("text-sm [&_p]:leading-relaxed", className)} {...$$restProps}>
<slot />
</div>

View File

@ -1,21 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import type { HeadingLevel } from "./index.js";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLHeadingElement> & {
level?: HeadingLevel;
};
let className: $$Props["class"] = undefined;
export let level: $$Props["level"] = "h5";
export { className as class };
</script>
<svelte:element
this={level}
class={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...$$restProps}
>
<slot />
</svelte:element>

View File

@ -1,17 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { type Variant, alertVariants } from "./index.js";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLDivElement> & {
variant?: Variant;
};
let className: $$Props["class"] = undefined;
export let variant: $$Props["variant"] = "default";
export { className as class };
</script>
<div class={cn(alertVariants({ variant }), className)} {...$$restProps} role="alert">
<slot />
</div>

View File

@ -1,33 +0,0 @@
import { type VariantProps, tv } from "tailwind-variants";
import Root from "./alert.svelte";
import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte";
export const alertVariants = tv({
base: "[&>svg]:text-foreground relative w-full rounded-lg border p-4 [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4",
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
});
export type Variant = VariantProps<typeof alertVariants>["variant"];
export type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
export {
Root,
Description,
Title,
//
Root as Alert,
Description as AlertDescription,
Title as AlertTitle,
};

View File

@ -1,11 +0,0 @@
<script lang="ts">
import { AspectRatio as AspectRatioPrimitive } from "bits-ui";
type $$Props = AspectRatioPrimitive.Props;
export let ratio: $$Props["ratio"] = 4 / 3;
</script>
<AspectRatioPrimitive.Root {ratio} {...$$restProps}>
<slot />
</AspectRatioPrimitive.Root>

View File

@ -1,3 +0,0 @@
import Root from "./aspect-ratio.svelte";
export { Root, Root as AspectRatio };

View File

@ -1,16 +0,0 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
type $$Props = AvatarPrimitive.FallbackProps;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Fallback
class={cn("bg-muted flex h-full w-full items-center justify-center rounded-full", className)}
{...$$restProps}
>
<slot />
</AvatarPrimitive.Fallback>

View File

@ -1,18 +0,0 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
type $$Props = AvatarPrimitive.ImageProps;
let className: $$Props["class"] = undefined;
export let src: $$Props["src"] = undefined;
export let alt: $$Props["alt"] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Image
{src}
{alt}
class={cn("aspect-square h-full w-full", className)}
{...$$restProps}
/>

View File

@ -1,18 +0,0 @@
<script lang="ts">
import { Avatar as AvatarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
type $$Props = AvatarPrimitive.Props;
let className: $$Props["class"] = undefined;
export let delayMs: $$Props["delayMs"] = undefined;
export { className as class };
</script>
<AvatarPrimitive.Root
{delayMs}
class={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
{...$$restProps}
>
<slot />
</AvatarPrimitive.Root>

View File

@ -1,13 +0,0 @@
import Root from "./avatar.svelte";
import Image from "./avatar-image.svelte";
import Fallback from "./avatar-fallback.svelte";
export {
Root,
Image,
Fallback,
//
Root as Avatar,
Image as AvatarImage,
Fallback as AvatarFallback,
};

View File

@ -1,18 +0,0 @@
<script lang="ts">
import { type Variant, badgeVariants } from "./index.js";
import { cn } from "$lib/components/utils.js";
let className: string | undefined | null = undefined;
export let href: string | undefined = undefined;
export let variant: Variant = "default";
export { className as class };
</script>
<svelte:element
this={href ? "a" : "span"}
{href}
class={cn(badgeVariants({ variant, className }))}
{...$$restProps}
>
<slot />
</svelte:element>

View File

@ -1,21 +0,0 @@
import { type VariantProps, tv } from "tailwind-variants";
export { default as Badge } from "./badge.svelte";
export const badgeVariants = tv({
base: "focus:ring-ring inline-flex select-none items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
});
export type Variant = VariantProps<typeof badgeVariants>["variant"];

View File

@ -1,24 +0,0 @@
<script lang="ts">
import Ellipsis from "lucide-svelte/icons/ellipsis";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="presentation"
aria-hidden="true"
class={cn("flex h-9 w-9 items-center justify-center", className)}
{...$$restProps}
>
<Ellipsis class="h-4 w-4" />
<span class="sr-only">More</span>
</span>

View File

@ -1,16 +0,0 @@
<script lang="ts">
import type { HTMLLiAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<li bind:this={el} class={cn("inline-flex items-center gap-1.5", className)}>
<slot />
</li>

View File

@ -1,31 +0,0 @@
<script lang="ts">
import type { HTMLAnchorAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAnchorAttributes & {
el?: HTMLAnchorElement;
asChild?: boolean;
};
export let href: $$Props["href"] = undefined;
export let el: $$Props["el"] = undefined;
export let asChild: $$Props["asChild"] = false;
let className: $$Props["class"] = undefined;
export { className as class };
let attrs: Record<string, unknown>;
$: attrs = {
class: cn("hover:text-foreground transition-colors", className),
href,
...$$restProps,
};
</script>
{#if asChild}
<slot {attrs} />
{:else}
<a bind:this={el} {...attrs} {href}>
<slot {attrs} />
</a>
{/if}

View File

@ -1,23 +0,0 @@
<script lang="ts">
import type { HTMLOlAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLOlAttributes & {
el?: HTMLOListElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<ol
bind:this={el}
class={cn(
"text-muted-foreground flex flex-wrap items-center gap-1.5 break-words text-sm sm:gap-2.5",
className
)}
{...$$restProps}
>
<slot />
</ol>

View File

@ -1,23 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLAttributes<HTMLSpanElement> & {
el?: HTMLSpanElement;
};
export let el: $$Props["el"] = undefined;
export let className: $$Props["class"] = undefined;
export { className as class };
</script>
<span
bind:this={el}
role="link"
aria-disabled="true"
aria-current="page"
class={cn("text-foreground font-normal", className)}
{...$$restProps}
>
<slot />
</span>

View File

@ -1,25 +0,0 @@
<script lang="ts">
import type { HTMLLiAttributes } from "svelte/elements";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { cn } from "$lib/components/utils.js";
type $$Props = HTMLLiAttributes & {
el?: HTMLLIElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<li
role="presentation"
aria-hidden="true"
class={cn("[&>svg]:size-3.5", className)}
bind:this={el}
{...$$restProps}
>
<slot>
<ChevronRight />
</slot>
</li>

View File

@ -1,15 +0,0 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
type $$Props = HTMLAttributes<HTMLElement> & {
el?: HTMLElement;
};
export let el: $$Props["el"] = undefined;
let className: $$Props["class"] = undefined;
export { className as class };
</script>
<nav class={className} bind:this={el} aria-label="breadcrumb" {...$$restProps}>
<slot />
</nav>

View File

@ -1,25 +0,0 @@
import Root from "./breadcrumb.svelte";
import Ellipsis from "./breadcrumb-ellipsis.svelte";
import Item from "./breadcrumb-item.svelte";
import Separator from "./breadcrumb-separator.svelte";
import Link from "./breadcrumb-link.svelte";
import List from "./breadcrumb-list.svelte";
import Page from "./breadcrumb-page.svelte";
export {
Root,
Ellipsis,
Item,
Separator,
Link,
List,
Page,
//
Root as Breadcrumb,
Ellipsis as BreadcrumbEllipsis,
Item as BreadcrumbItem,
Separator as BreadcrumbSeparator,
Link as BreadcrumbLink,
List as BreadcrumbList,
Page as BreadcrumbPage,
};

View File

@ -1,74 +0,0 @@
<script lang="ts" module>
import type { WithElementRef } from "bits-ui";
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import { type VariantProps, tv } from "tailwind-variants";
export const buttonVariants = tv({
base: "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border-input bg-background hover:bg-accent hover:text-accent-foreground border",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
});
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
WithElementRef<HTMLAnchorAttributes> & {
variant?: ButtonVariant;
size?: ButtonSize;
};
</script>
<script lang="ts">
import { cn } from "$lib/components/utils.js";
let {
class: className,
variant = "default",
size = "default",
ref = $bindable(null),
href = undefined,
type = "button",
children,
...restProps
}: ButtonProps = $props();
</script>
{#if href}
<a
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{href}
{...restProps}
>
{@render children?.()}
</a>
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>
{@render children?.()}
</button>
{/if}

View File

@ -1,17 +0,0 @@
import Root, {
type ButtonProps,
type ButtonSize,
type ButtonVariant,
buttonVariants,
} from "./button.svelte";
export {
Root,
type ButtonProps as Props,
//
Root as Button,
buttonVariants,
type ButtonProps,
type ButtonSize,
type ButtonVariant,
};

View File

@ -1,19 +0,0 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.CellProps = $props();
</script>
<CalendarPrimitive.Cell
bind:ref
class={cn(
"[&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50 relative size-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md",
className
)}
{...restProps}
/>

View File

@ -1,30 +0,0 @@
<script lang="ts">
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/components/utils.js";
import { Calendar as CalendarPrimitive } from "bits-ui";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.DayProps = $props();
</script>
<CalendarPrimitive.Day
bind:ref
class={cn(
buttonVariants({ variant: "ghost" }),
"size-9 p-0 font-normal",
"[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground",
// Selected
"data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground data-[selected]:opacity-100",
// Disabled
"data-[disabled]:text-muted-foreground data-[disabled]:opacity-50",
// Unavailable
"data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through",
// Outside months
"data-[outside-month]:text-muted-foreground [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground data-[outside-month]:pointer-events-none data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:opacity-30",
className
)}
{...restProps}
/>

View File

@ -1,12 +0,0 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridBodyProps = $props();
</script>
<CalendarPrimitive.GridBody bind:ref class={cn(className)} {...restProps} />

View File

@ -1,12 +0,0 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridHeadProps = $props();
</script>
<CalendarPrimitive.GridHead bind:ref class={cn(className)} {...restProps} />

View File

@ -1,12 +0,0 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridRowProps = $props();
</script>
<CalendarPrimitive.GridRow bind:ref class={cn("flex", className)} {...restProps} />

View File

@ -1,16 +0,0 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.GridProps = $props();
</script>
<CalendarPrimitive.Grid
bind:ref
class={cn("w-full border-collapse space-y-1", className)}
{...restProps}
/>

View File

@ -1,16 +0,0 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeadCellProps = $props();
</script>
<CalendarPrimitive.HeadCell
bind:ref
class={cn("text-muted-foreground w-9 rounded-md text-[0.8rem] font-normal", className)}
{...restProps}
/>

View File

@ -1,16 +0,0 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeaderProps = $props();
</script>
<CalendarPrimitive.Header
bind:ref
class={cn("relative flex w-full items-center justify-between pt-1", className)}
{...restProps}
/>

View File

@ -1,12 +0,0 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: CalendarPrimitive.HeadingProps = $props();
</script>
<CalendarPrimitive.Heading bind:ref class={cn("text-sm font-medium", className)} {...restProps} />

View File

@ -1,20 +0,0 @@
<script lang="ts">
import type { WithElementRef } from "bits-ui";
import type { HTMLAttributes } from "svelte/elements";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
class={cn("mt-4 flex flex-col space-y-4 sm:flex-row sm:space-x-4 sm:space-y-0", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@ -1,28 +0,0 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import ChevronRight from "lucide-svelte/icons/chevron-right";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: CalendarPrimitive.PrevButtonProps = $props();
</script>
{#snippet Fallback()}
<ChevronRight class="size-4" />
{/snippet}
<CalendarPrimitive.NextButton
bind:ref
class={cn(
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
className
)}
children={children || Fallback}
{...restProps}
/>

View File

@ -1,28 +0,0 @@
<script lang="ts">
import { Calendar as CalendarPrimitive } from "bits-ui";
import ChevronLeft from "lucide-svelte/icons/chevron-left";
import { buttonVariants } from "$lib/components/ui/button/index.js";
import { cn } from "$lib/components/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: CalendarPrimitive.PrevButtonProps = $props();
</script>
{#snippet Fallback()}
<ChevronLeft class="size-4" />
{/snippet}
<CalendarPrimitive.PrevButton
bind:ref
class={cn(
buttonVariants({ variant: "outline" }),
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
className
)}
children={children || Fallback}
{...restProps}
/>

Some files were not shown because too many files have changed in this diff Show More