Compare commits
78 Commits
Eventplan-
...
develop/da
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b391b193e | |||
| c05c032e3f | |||
| da6f741806 | |||
| 6b54791331 | |||
| 36685bffd1 | |||
| caf9ea6cf1 | |||
| d505265910 | |||
| 78e1a7b726 | |||
| cd485e8dda | |||
| 182c402c7e | |||
| 098f5b9270 | |||
| cf0c66c910 | |||
| c8156ea47e | |||
| 20a47ca6b6 | |||
| 2d601b9c4d | |||
| 48586f1a50 | |||
| 7153cacbab | |||
| 73cee211f2 | |||
| 83074df7ef | |||
| d1c926c093 | |||
| f8a16acfeb | |||
| 9ca63cd286 | |||
| a2456c8b46 | |||
| 0952035091 | |||
| 9c8c02f679 | |||
| 3b5fdc57c0 | |||
| 733c63946f | |||
| fd846250ab | |||
| 17460772e9 | |||
| 9a20860072 | |||
| 8f51723a3b | |||
| 8ad2f283aa | |||
| 39f1af8b73 | |||
| 266c4cb4ea | |||
| f3df3c0000 | |||
| cb78fc598b | |||
| ba7ecc1a8e | |||
| 6ea92f9383 | |||
| 998770bf59 | |||
| a231032555 | |||
| 3aa3731bcb | |||
| 5e80c95bfd | |||
| 09dc28b6da | |||
| fd7cf716ca | |||
| 73bd6a5e96 | |||
| 9c02cc1f4d | |||
| de8457fe45 | |||
| 4fbe01f987 | |||
| 86d90e3fd2 | |||
| bccd5eb5a0 | |||
| 53afe70b27 | |||
| 4bbdaa06a9 | |||
| f03867b9a7 | |||
| 23e10eef0f | |||
| 4c72f4f26b | |||
| 624ba7f296 | |||
| d7d20e4347 | |||
| 43bd8f4a7c | |||
| 18e8627b54 | |||
| 0efc46c7e2 | |||
| 62fff0c0b2 | |||
| 86b479fb28 | |||
| 489402292d | |||
| b53ce04a75 | |||
| 069a9973a4 | |||
| c3410de1d7 | |||
| a23c514102 | |||
| bf8110af6c | |||
| 349f71af1c | |||
| dda37127ca | |||
| 6d210eb0ff | |||
| cfede8f299 | |||
| 597153ed39 | |||
| 697e903a26 | |||
| 1433784369 | |||
| 2c63a33bda | |||
| 87265e5ccc | |||
| 75f1a6528b |
@ -7,7 +7,6 @@ import sitemap from "@astrojs/sitemap";
|
|||||||
import robotsTxt from "astro-robots-txt";
|
import robotsTxt from "astro-robots-txt";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import mdx from "@astrojs/mdx";
|
import mdx from "@astrojs/mdx";
|
||||||
import pagefind from "astro-pagefind";
|
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@ -20,9 +19,8 @@ export default defineConfig({
|
|||||||
integrations: [
|
integrations: [
|
||||||
svelte(),
|
svelte(),
|
||||||
tailwind({
|
tailwind({
|
||||||
configFile: "./tailwind.config.cjs",
|
configFile: "./tailwind.config.js",
|
||||||
}),
|
}),
|
||||||
pagefind(),
|
|
||||||
configureI18n(),
|
configureI18n(),
|
||||||
sitemap({
|
sitemap({
|
||||||
i18n: {
|
i18n: {
|
||||||
@ -68,6 +66,7 @@ export default defineConfig({
|
|||||||
"@layouts": path.resolve("./src/layouts"),
|
"@layouts": path.resolve("./src/layouts"),
|
||||||
"@repo": path.resolve("./src/components/repo"),
|
"@repo": path.resolve("./src/components/repo"),
|
||||||
"@stores": path.resolve("./src/components/stores"),
|
"@stores": path.resolve("./src/components/stores"),
|
||||||
|
"$lib": path.resolve("./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
17
components.json
Normal file
17
components.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
}
|
||||||
20
package.json
20
package.json
@ -20,25 +20,38 @@
|
|||||||
"@astrojs/svelte": "^7.0.4",
|
"@astrojs/svelte": "^7.0.4",
|
||||||
"@astrojs/tailwind": "^5.1.5",
|
"@astrojs/tailwind": "^5.1.5",
|
||||||
"@astropub/icons": "^0.2.0",
|
"@astropub/icons": "^0.2.0",
|
||||||
|
"@internationalized/date": "^3.7.0",
|
||||||
"@types/color": "^4.2.0",
|
"@types/color": "^4.2.0",
|
||||||
"@types/node": "^22.9.3",
|
"@types/node": "^22.9.3",
|
||||||
"@types/three": "^0.170.0",
|
"@types/three": "^0.170.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||||
"@typescript-eslint/parser": "^8.15.0",
|
"@typescript-eslint/parser": "^8.15.0",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"bits-ui": "1.3.4",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk-sv": "^0.0.18",
|
||||||
"cssnano": "^7.0.6",
|
"cssnano": "^7.0.6",
|
||||||
|
"embla-carousel-svelte": "^8.5.2",
|
||||||
"esbuild": "^0.24.0",
|
"esbuild": "^0.24.0",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.15.0",
|
||||||
"eslint-plugin-astro": "^1.3.1",
|
"eslint-plugin-astro": "^1.3.1",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-svelte": "^2.46.0",
|
"eslint-plugin-svelte": "^2.46.0",
|
||||||
|
"formsnap": "1.0.1",
|
||||||
|
"lucide-svelte": "^0.476.0",
|
||||||
|
"mode-watcher": "^0.5.1",
|
||||||
|
"paneforge": "^0.0.6",
|
||||||
"postcss-nesting": "^13.0.1",
|
"postcss-nesting": "^13.0.1",
|
||||||
"sass": "^1.81.0",
|
"sass": "^1.81.0",
|
||||||
"svelte": "^5.16.0",
|
"svelte": "^5.16.0",
|
||||||
|
"svelte-sonner": "^0.3.28",
|
||||||
"tailwind-merge": "^2.5.5",
|
"tailwind-merge": "^2.5.5",
|
||||||
|
"tailwind-variants": "^0.3.1",
|
||||||
"tailwindcss": "^3.4.15",
|
"tailwindcss": "^3.4.15",
|
||||||
"three": "^0.170.0",
|
"three": "^0.170.0",
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2",
|
||||||
|
"vaul-svelte": "^0.3.2",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^4.0.7",
|
"@astrojs/mdx": "^4.0.7",
|
||||||
@ -46,9 +59,9 @@
|
|||||||
"@codemirror/commands": "^6.8.0",
|
"@codemirror/commands": "^6.8.0",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@ddietr/codemirror-themes": "^1.4.4",
|
"@ddietr/codemirror-themes": "^1.4.4",
|
||||||
|
"@tanstack/table-core": "^8.21.2",
|
||||||
"astro": "^5.1.8",
|
"astro": "^5.1.8",
|
||||||
"astro-i18n": "^2.2.4",
|
"astro-i18n": "^2.2.4",
|
||||||
"astro-pagefind": "^1.6.0",
|
|
||||||
"astro-robots-txt": "^1.0.0",
|
"astro-robots-txt": "^1.0.0",
|
||||||
"astro-seo": "^0.8.4",
|
"astro-seo": "^0.8.4",
|
||||||
"chart.js": "^4.4.6",
|
"chart.js": "^4.4.6",
|
||||||
@ -64,7 +77,6 @@
|
|||||||
"sharp": "^0.33.5",
|
"sharp": "^0.33.5",
|
||||||
"svelte-awesome": "^3.3.5",
|
"svelte-awesome": "^3.3.5",
|
||||||
"svelte-codemirror-editor": "^1.4.1",
|
"svelte-codemirror-editor": "^1.4.1",
|
||||||
"svelte-spa-router": "^4.0.1",
|
"svelte-spa-router": "^4.0.1"
|
||||||
"zod": "^3.23.8"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11714
pnpm-lock.yaml
generated
11714
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
public/apple-touch-icon.png
Normal file
BIN
public/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
public/favicon-96x96.png
Normal file
BIN
public/favicon-96x96.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
3
public/favicon.svg
Normal file
3
public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 406 KiB |
21
public/site.webmanifest
Normal file
21
public/site.webmanifest
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
BIN
public/web-app-manifest-192x192.png
Normal file
BIN
public/web-app-manifest-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
BIN
public/web-app-manifest-512x512.png
Normal file
BIN
public/web-app-manifest-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@ -20,6 +20,7 @@
|
|||||||
<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 {
|
||||||
@ -64,7 +65,3 @@
|
|||||||
<p>{error.message}</p>
|
<p>{error.message}</p>
|
||||||
{/await}
|
{/await}
|
||||||
|
|
||||||
<style>
|
|
||||||
@import "../styles/table.css";
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|||||||
@ -79,6 +79,8 @@
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
type: "time",
|
type: "time",
|
||||||
@ -105,5 +107,5 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<canvas bind:this={canvas}></canvas>
|
<canvas height="500" bind:this={canvas}></canvas>
|
||||||
</div>
|
</div>
|
||||||
@ -19,7 +19,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {window} from "./util.ts";
|
import {window} from "./utils.ts";
|
||||||
import {astroI18n, t} from "astro-i18n";
|
import {astroI18n, t} from "astro-i18n";
|
||||||
import type {EventFight, ExtendedEvent} from "@type/event";
|
import type {EventFight, ExtendedEvent} from "@type/event";
|
||||||
import "@styles/table.css";
|
import "@styles/table.css";
|
||||||
@ -55,7 +55,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each window(event.fights.filter(f => f.group === group), rows) as fights}
|
{#each window(event.fights.filter(f => group === undefined ? true : 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, {
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {window} from "./util.ts";
|
import {window} from "./utils.ts";
|
||||||
import {t} from "astro-i18n";
|
import {t} from "astro-i18n";
|
||||||
import type {ExtendedEvent} from "@type/event.ts";
|
import type {ExtendedEvent} from "@type/event.ts";
|
||||||
import "@styles/table.css"
|
import "@styles/table.css"
|
||||||
|
|||||||
@ -31,8 +31,7 @@
|
|||||||
let error: string = $state("");
|
let error: string = $state("");
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
let {tokenStore} = await import("./repo/repo.ts");
|
let {authV2Repo} = await import("./repo/authv2.ts");
|
||||||
let {authRepo} = await import("./repo/auth.ts");
|
|
||||||
if (username === "" || pw === "") {
|
if (username === "" || pw === "") {
|
||||||
pw = "";
|
pw = "";
|
||||||
error = t("login.error");
|
error = t("login.error");
|
||||||
@ -40,15 +39,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let auth = await get(authRepo).login(username, pw);
|
let auth = await get(authV2Repo).login(username, pw);
|
||||||
if (auth == undefined) {
|
if (!auth) {
|
||||||
pw = "";
|
pw = "";
|
||||||
error = t("login.error");
|
error = t("login.error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tokenStore.set(auth);
|
await navigate(l("/dashboard"));
|
||||||
navigate(l("/dashboard"));
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
pw = "";
|
pw = "";
|
||||||
error = t("login.error");
|
error = t("login.error");
|
||||||
|
|||||||
@ -23,24 +23,35 @@
|
|||||||
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";
|
||||||
interface Props {
|
interface Props {
|
||||||
logo?: import('svelte').Snippet;
|
logo?: import('svelte').Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { logo }: Props = $props();
|
let { logo }: Props = $props();
|
||||||
|
|
||||||
let navbar: HTMLDivElement = $state();
|
let navbar = $state<HTMLDivElement>();
|
||||||
let searchOpen = $state(false);
|
let searchOpen = $state(false);
|
||||||
|
|
||||||
|
let accountBtn = $state<HTMLAnchorElement>();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($loggedIn) {
|
||||||
|
accountBtn!.href = l("/dashboard");
|
||||||
|
} else {
|
||||||
|
accountBtn!.href = l("/login");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
handleScroll();
|
handleScroll();
|
||||||
})
|
})
|
||||||
|
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
if (window.scrollY > 0) {
|
if (window.scrollY > 0) {
|
||||||
navbar.classList.add("before:scale-y-100");
|
navbar!.classList.add("before:scale-y-100");
|
||||||
} else {
|
} else {
|
||||||
navbar.classList.remove("before:scale-y-100");
|
navbar!.classList.remove("before:scale-y-100");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@ -90,6 +101,8 @@
|
|||||||
<a href={l("/rules/microwargear")}
|
<a href={l("/rules/microwargear")}
|
||||||
class="btn btn-gray">{t("navbar.links.rules.micro")}</a>
|
class="btn btn-gray">{t("navbar.links.rules.micro")}</a>
|
||||||
<a href={l("/rules/streetfight")} class="btn btn-gray">{t("navbar.links.rules.sf")}</a>
|
<a href={l("/rules/streetfight")} class="btn btn-gray">{t("navbar.links.rules.sf")}</a>
|
||||||
|
<h2 class="px-2 text-gray-300">{t("navbar.links.rules.ranked")}</h2>
|
||||||
|
<a href={l("/rangliste/missilewars")} class="btn btn-gray">{t("navbar.links.ranked.mw")}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- TODO: Add help center
|
<!-- TODO: Add help center
|
||||||
@ -106,7 +119,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
-->
|
-->
|
||||||
<a class="btn" href={l("/login")}>
|
<a class="btn" href={l("/login")} bind:this={accountBtn}>
|
||||||
<span class="btn__text">{t("navbar.links.account")}</span>
|
<span class="btn__text">{t("navbar.links.account")}</span>
|
||||||
</a>
|
</a>
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
import {CollectionEntry} from "astro:content";
|
import type {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";
|
||||||
|
|||||||
@ -22,38 +22,40 @@
|
|||||||
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 {tokenStore} from "@repo/repo";
|
import {loggedIn} from "@repo/authv2.ts";
|
||||||
|
|
||||||
const routes: RouteDefinition = {
|
const routes: RouteDefinition = {
|
||||||
"/": wrap({asyncComponent: () => import("./pages/Home.svelte"), conditions: detail => get(tokenStore) != ""}),
|
"/": wrap({asyncComponent: () => import("./pages/Home.svelte"), conditions: detail => get(loggedIn)}),
|
||||||
"/perms": wrap({
|
"/perms": wrap({
|
||||||
asyncComponent: () => import("./pages/Perms.svelte"),
|
asyncComponent: () => import("./pages/Perms.svelte"),
|
||||||
conditions: detail => get(tokenStore) != ""
|
conditions: detail => get(loggedIn)
|
||||||
}),
|
}),
|
||||||
"/login": wrap({
|
"/login": wrap({
|
||||||
asyncComponent: () => import("./pages/Login.svelte"),
|
asyncComponent: () => import("./pages/Login.svelte"),
|
||||||
conditions: detail => get(tokenStore) == ""
|
conditions: detail => !get(loggedIn)
|
||||||
}),
|
}),
|
||||||
"/event/:id": wrap({
|
"/event/:id": wrap({
|
||||||
asyncComponent: () => import("./pages/Event.svelte"),
|
asyncComponent: () => import("./pages/Event.svelte"),
|
||||||
conditions: detail => get(tokenStore) != ""
|
conditions: detail => get(loggedIn)
|
||||||
}),
|
}),
|
||||||
"/event/:id/generate": wrap({
|
"/event/:id/generate": wrap({
|
||||||
asyncComponent: () => import("./pages/Generate.svelte"),
|
asyncComponent: () => import("./pages/Generate.svelte"),
|
||||||
conditions: detail => get(tokenStore) != ""
|
conditions: detail => get(loggedIn)
|
||||||
}),
|
}),
|
||||||
"/edit": wrap({
|
"/edit": wrap({
|
||||||
asyncComponent: () => import("./pages/Edit.svelte"),
|
asyncComponent: () => import("./pages/Edit.svelte"),
|
||||||
conditions: detail => get(tokenStore) != ""
|
conditions: detail => get(loggedIn)
|
||||||
}),
|
}),
|
||||||
"/display/:event": wrap({
|
"/display/:event": wrap({
|
||||||
asyncComponent: () => import("./pages/Display.svelte"),
|
asyncComponent: () => import("./pages/Display.svelte"),
|
||||||
conditions: detail => get(tokenStore) != ""
|
conditions: detail => get(loggedIn)
|
||||||
}),
|
}),
|
||||||
"*": 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 {
|
||||||
|
|||||||
@ -38,6 +38,7 @@
|
|||||||
</NavBrand>
|
</NavBrand>
|
||||||
<NavHamburger onclick={toggle}/>
|
<NavHamburger onclick={toggle}/>
|
||||||
<NavUl {hidden}>
|
<NavUl {hidden}>
|
||||||
|
<NavLi href="/admin/new">New UI</NavLi>
|
||||||
<NavLi href="#/edit">Edit Pages</NavLi>
|
<NavLi href="#/edit">Edit Pages</NavLi>
|
||||||
<NavLi href="#/perms">Permissions</NavLi>
|
<NavLi href="#/perms">Permissions</NavLi>
|
||||||
</NavUl>
|
</NavUl>
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
dirty?: boolean;
|
dirty?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { pageId, branch, dirty = $bindable(false) }: Props = $props();
|
let { pageId, branch = $bindable(), dirty = $bindable(false) }: Props = $props();
|
||||||
|
|
||||||
let dispatcher = createEventDispatcher();
|
let dispatcher = createEventDispatcher();
|
||||||
|
|
||||||
@ -97,7 +97,7 @@
|
|||||||
{#if page?.name.endsWith("md") || page?.name.endsWith("mdx")}
|
{#if page?.name.endsWith("md") || page?.name.endsWith("mdx")}
|
||||||
<MDEMarkdownEditor bind:value={pageContent} bind:dirty/>
|
<MDEMarkdownEditor bind:value={pageContent} bind:dirty/>
|
||||||
{:else}
|
{:else}
|
||||||
<CodeMirror bind:value={pageContent} lang={json()} theme={materialDark} on:change={() => dirty = true}/>
|
<CodeMirror bind:value={pageContent} lang={json()} theme={materialDark} onchange={() => dirty = true}/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:catch error}
|
{:catch error}
|
||||||
|
|||||||
@ -43,7 +43,5 @@
|
|||||||
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}
|
|
||||||
{/await}
|
{/await}
|
||||||
@ -21,19 +21,21 @@
|
|||||||
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();
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { open = $bindable(false) }: Props = $props();
|
let {open = $bindable(false)}: Props = $props();
|
||||||
|
|
||||||
async function upload() {
|
async function upload(e: Event) {
|
||||||
|
e.stopPropagation();
|
||||||
if (uploadFile == null) {
|
if (uploadFile == null) {
|
||||||
return
|
error = "dashboard.schematic.errors.noFile";
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
let file = uploadFile[0];
|
let file = uploadFile[0];
|
||||||
|
|
||||||
@ -42,33 +44,46 @@
|
|||||||
let type = name.split(".").pop();
|
let type = name.split(".").pop();
|
||||||
|
|
||||||
if (type !== "schem" && type !== "schematic") {
|
if (type !== "schem" && type !== "schematic") {
|
||||||
return
|
error = "dashboard.schematic.errors.invalidEnding";
|
||||||
|
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)));
|
||||||
|
|
||||||
let response = await $schemRepo.uploadSchematic(name, b64);
|
try {
|
||||||
|
await $schemRepo.uploadSchematic(name, b64);
|
||||||
|
|
||||||
open = false;
|
open = false;
|
||||||
|
value = "";
|
||||||
|
dispatch("reset");
|
||||||
|
} catch (e) {
|
||||||
|
error = "dashboard.schematic.errors.upload";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reset(e: Event) {
|
||||||
|
e.stopPropagation();
|
||||||
|
open = false
|
||||||
value = "";
|
value = "";
|
||||||
dispatch("reset")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let uploadFile: FileList | null = $state(null);
|
let uploadFile: FileList | null = $state(null);
|
||||||
let value = $state("");
|
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>
|
||||||
<input type="file" bind:files={uploadFile} bind:value />
|
<label for="schem-upload">{t("dashboard.schematic.title")}</label>
|
||||||
|
<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 !ml-auto" onclick={upload}>{t("dashboard.schematic.upload")}</button>
|
<button class="btn btn-gray" onclick={reset}>{t("dashboard.schematic.cancel")}</button>
|
||||||
<button class="btn btn-gray" onclick={() => open = false}>{t("dashboard.schematic.cancel")}</button>
|
{/snippet}
|
||||||
|
|
||||||
{/snippet}
|
|
||||||
</SWModal>
|
</SWModal>
|
||||||
@ -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 {authRepo} from "@repo/auth.ts";
|
import {authV2Repo} from "@repo/authv2.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,9 +33,8 @@
|
|||||||
let { user }: Props = $props();
|
let { user }: Props = $props();
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await $authRepo.logout()
|
await $authV2Repo.logout();
|
||||||
tokenStore.set("")
|
await navigate(l("/login"));
|
||||||
window.location.href = l("/login")
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
56
src/components/moderator/App.svelte
Normal file
56
src/components/moderator/App.svelte
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type {RouteDefinition} from "svelte-spa-router";
|
||||||
|
import Router from "svelte-spa-router";
|
||||||
|
import NavLinks from "@components/moderator/layout/NavLinks.svelte";
|
||||||
|
import {Switch} from "@components/ui/switch";
|
||||||
|
import {Label} from "@components/ui/label";
|
||||||
|
import {navigate} from "astro:transitions/client";
|
||||||
|
import Players from "@components/moderator/pages/players/Players.svelte";
|
||||||
|
import Events from "@components/moderator/pages/events/Events.svelte";
|
||||||
|
import Dashboard from "@components/moderator/pages/dashboard/Dashboard.svelte";
|
||||||
|
import Event from "@components/moderator/pages/event/Event.svelte";
|
||||||
|
|
||||||
|
const routes: RouteDefinition = {
|
||||||
|
"/": Dashboard,
|
||||||
|
"/events": Events,
|
||||||
|
"/players": Players,
|
||||||
|
"/event/:id": Event
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col bg-background min-w-full min-h-screen">
|
||||||
|
<div class="border-b">
|
||||||
|
<div class="flex h-16 items-center px-4">
|
||||||
|
<a href="/" class="text-sm font-bold transition-colors text-primary">
|
||||||
|
SteamWar
|
||||||
|
</a>
|
||||||
|
<NavLinks />
|
||||||
|
<div class="ml-auto flex items-center space-x-4">
|
||||||
|
<Switch id="new-ui-switch" checked={true} on:click={() => navigate("/admin")} />
|
||||||
|
<Label for="new-ui-switch">New UI!</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<main class="flex flex-col">
|
||||||
|
<Router {routes} />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
42
src/components/moderator/components/EventCard.svelte
Normal file
42
src/components/moderator/components/EventCard.svelte
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type {ShortEvent} from "@type/event.ts";
|
||||||
|
import {Card, CardContent, CardHeader, CardTitle} from "@components/ui/card";
|
||||||
|
|
||||||
|
let { event }: { event: ShortEvent } = $props();
|
||||||
|
|
||||||
|
let sameDate = $derived(new Intl.DateTimeFormat().format(event.start) === new Intl.DateTimeFormat().format(event.end));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{event.name}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{#if !sameDate}
|
||||||
|
<p>Startet: {new Intl.DateTimeFormat().format(event.start)}</p>
|
||||||
|
<p>Endet: {new Intl.DateTimeFormat().format(event.end)}</p>
|
||||||
|
{:else}
|
||||||
|
<p>Am: {new Intl.DateTimeFormat().format(event.start)}</p>
|
||||||
|
<p> </p>
|
||||||
|
{/if}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
40
src/components/moderator/layout/NavLinks.svelte
Normal file
40
src/components/moderator/layout/NavLinks.svelte
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {location} from "svelte-spa-router";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="flex items-center space-x-4 lg:space-x-6 mx-6">
|
||||||
|
<a href="#/" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/"}>
|
||||||
|
Dashboard
|
||||||
|
</a>
|
||||||
|
<a href="#/events" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/events"}>
|
||||||
|
Events
|
||||||
|
</a>
|
||||||
|
<a href="#/players" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/players"}>
|
||||||
|
Players
|
||||||
|
</a>
|
||||||
|
<a href="#/pages" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/pages"}>
|
||||||
|
Pages
|
||||||
|
</a>
|
||||||
|
<a href="#/schematics" class="hover:text-primary text-sm font-medium transition-colors" class:text-muted={$location !== "/schematics"}>
|
||||||
|
Schematics
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
22
src/components/moderator/pages/dashboard/Dashboard.svelte
Normal file
22
src/components/moderator/pages/dashboard/Dashboard.svelte
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
<h1 class="font-bold text-xl">SteamWar Dashboard</h1>
|
||||||
|
</div>
|
||||||
38
src/components/moderator/pages/event/Event.svelte
Normal file
38
src/components/moderator/pages/event/Event.svelte
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {eventRepo} from "@repo/event.ts";
|
||||||
|
import EventView from "@components/moderator/pages/event/EventView.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
params: { id: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
let { params }: Props = $props();
|
||||||
|
|
||||||
|
let id = params.id;
|
||||||
|
let event = $eventRepo.getEvent(id.toString());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await event}
|
||||||
|
<p>Loading...</p>
|
||||||
|
{:then data}
|
||||||
|
<EventView event={data} />
|
||||||
|
{/await}
|
||||||
128
src/components/moderator/pages/event/EventEdit.svelte
Normal file
128
src/components/moderator/pages/event/EventEdit.svelte
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Input} from "@components/ui/input";
|
||||||
|
import {Label} from "@components/ui/label";
|
||||||
|
import {Popover, PopoverContent, PopoverTrigger} from "@components/ui/popover";
|
||||||
|
import type {SWEvent} from "@type/event.ts"
|
||||||
|
import DateTimePicker from "@components/ui/datetime-picker/DateTimePicker.svelte";
|
||||||
|
import {fromAbsolute} from "@internationalized/date";
|
||||||
|
import {Button} from "@components/ui/button";
|
||||||
|
import {ChevronsUpDown} from "lucide-svelte";
|
||||||
|
import {Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList} from "@components/ui/command";
|
||||||
|
import {schemTypes} from "@stores/stores.ts";
|
||||||
|
import Check from "lucide-svelte/icons/check";
|
||||||
|
import {cn} from "@components/utils.ts";
|
||||||
|
import {Switch} from "@components/ui/switch";
|
||||||
|
import {eventRepo} from "@repo/event.ts";
|
||||||
|
|
||||||
|
const { event }: { event: SWEvent } = $props();
|
||||||
|
|
||||||
|
let rootEvent: SWEvent = $state(event)
|
||||||
|
|
||||||
|
let eventName = $state(rootEvent.name);
|
||||||
|
let eventDeadline = $state(fromAbsolute(rootEvent.deadline, "Europe/Berlin"));
|
||||||
|
let eventStart = $state(fromAbsolute(rootEvent.start, "Europe/Berlin"));
|
||||||
|
let eventEnd = $state(fromAbsolute(rootEvent.end, "Europe/Berlin"));
|
||||||
|
let eventTeamSize = $state(rootEvent.maxTeamMembers);
|
||||||
|
let eventSchematicType = $state(rootEvent.schemType);
|
||||||
|
let eventPublicsOnly = $state(rootEvent.publicSchemsOnly);
|
||||||
|
|
||||||
|
let dirty = $derived(eventName !== rootEvent.name ||
|
||||||
|
eventDeadline.toDate().getTime() !== rootEvent.deadline ||
|
||||||
|
eventStart.toDate().getTime() !== rootEvent.start ||
|
||||||
|
eventEnd.toDate().getTime() !== rootEvent.end ||
|
||||||
|
eventTeamSize !== rootEvent.maxTeamMembers ||
|
||||||
|
eventSchematicType !== rootEvent.schemType ||
|
||||||
|
eventPublicsOnly !== rootEvent.publicSchemsOnly);
|
||||||
|
|
||||||
|
async function updateEvent() {
|
||||||
|
rootEvent = await $eventRepo.updateEvent(event.id.toString(), {
|
||||||
|
name: eventName,
|
||||||
|
deadline: eventDeadline.toDate().getTime(),
|
||||||
|
start: eventStart.toDate().getTime(),
|
||||||
|
end: eventEnd.toDate().getTime(),
|
||||||
|
maxTeamMembers: eventTeamSize,
|
||||||
|
schemType: eventSchematicType,
|
||||||
|
publicSchemsOnly: eventPublicsOnly,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<Label for="event-name">Name</Label>
|
||||||
|
<Input id="event-name" bind:value={eventName} />
|
||||||
|
<Label>Deadline</Label>
|
||||||
|
<DateTimePicker bind:value={eventDeadline} />
|
||||||
|
<Label>Start</Label>
|
||||||
|
<DateTimePicker bind:value={eventStart} />
|
||||||
|
<Label>End</Label>
|
||||||
|
<DateTimePicker bind:value={eventEnd} />
|
||||||
|
<Label for="event-size">Teamsize</Label>
|
||||||
|
<Input id="event-size" bind:value={eventTeamSize} type="number" />
|
||||||
|
<Label>Schematic Type</Label>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger>
|
||||||
|
{#snippet child({ props })}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="justify-between"
|
||||||
|
{...props}
|
||||||
|
role="combobox"
|
||||||
|
>
|
||||||
|
{$schemTypes.find(value => value.db === eventSchematicType)?.name || eventSchematicType || "Select a schematic type..."}
|
||||||
|
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
{/snippet}
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search schematic types..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>No schematic type found.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{#each $schemTypes as type}
|
||||||
|
<CommandItem
|
||||||
|
value={type.db}
|
||||||
|
onSelect={() => {
|
||||||
|
eventSchematicType = type.db;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
class={cn(
|
||||||
|
"mr-2 size-4",
|
||||||
|
eventSchematicType !== type.db && "text-transparent"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{type.name}
|
||||||
|
</CommandItem>
|
||||||
|
{/each}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<Label for="event-publics">Publics Schematics Only</Label>
|
||||||
|
<Switch id="event-publics" bind:checked={eventPublicsOnly} />
|
||||||
|
<div class="flex flex-row justify-end border-t pt-2 gap-4">
|
||||||
|
<Button variant="destructive">Delete</Button>
|
||||||
|
<Button disabled={!dirty} onclick={updateEvent}>Update</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
107
src/components/moderator/pages/event/EventFightList.svelte
Normal file
107
src/components/moderator/pages/event/EventFightList.svelte
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import type {ExtendedEvent} from "@type/event";
|
||||||
|
import {createSvelteTable, FlexRender} from "@components/ui/data-table";
|
||||||
|
import {
|
||||||
|
type ColumnFiltersState,
|
||||||
|
getCoreRowModel, getFilteredRowModel,
|
||||||
|
getPaginationRowModel, getSortedRowModel,
|
||||||
|
type SortingState,
|
||||||
|
} from "@tanstack/table-core";
|
||||||
|
import { columns } from "./columns"
|
||||||
|
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table";
|
||||||
|
|
||||||
|
let { data }: { data: ExtendedEvent } = $props();
|
||||||
|
|
||||||
|
let sorting = $state<SortingState>([]);
|
||||||
|
let columnFilters = $state<ColumnFiltersState>([]);
|
||||||
|
|
||||||
|
const table = createSvelteTable({
|
||||||
|
get data() {
|
||||||
|
return data.fights;
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
get sorting() {
|
||||||
|
return sorting;
|
||||||
|
},
|
||||||
|
get columnFilters() {
|
||||||
|
return columnFilters;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onSortingChange: (updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
sorting = updater(sorting);
|
||||||
|
} else {
|
||||||
|
sorting = updater;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onColumnFiltersChange: (updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
columnFilters = updater(columnFilters);
|
||||||
|
} else {
|
||||||
|
columnFilters = updater;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||||
|
<TableRow>
|
||||||
|
{#each headerGroup.headers as header (header.id)}
|
||||||
|
<TableHead>
|
||||||
|
{#if !header.isPlaceholder}
|
||||||
|
<FlexRender
|
||||||
|
content={header.column.columnDef.header}
|
||||||
|
context={header.getContext()}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</TableHead>
|
||||||
|
{/each}
|
||||||
|
</TableRow>
|
||||||
|
{/each}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{#each table.getRowModel().rows as row (row.id)}
|
||||||
|
<TableRow data-state={row.getIsSelected() && "selected"}>
|
||||||
|
{#each row.getVisibleCells() as cell (cell.id)}
|
||||||
|
<TableCell>
|
||||||
|
<FlexRender
|
||||||
|
content={cell.column.columnDef.cell}
|
||||||
|
context={cell.getContext()}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
{/each}
|
||||||
|
</TableRow>
|
||||||
|
{:else}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colspan={columns.length} class="h-24 text-center">
|
||||||
|
No results.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{/each}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
47
src/components/moderator/pages/event/EventView.svelte
Normal file
47
src/components/moderator/pages/event/EventView.svelte
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import 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>
|
||||||
92
src/components/moderator/pages/event/RefereesList.svelte
Normal file
92
src/components/moderator/pages/event/RefereesList.svelte
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<!--
|
||||||
|
- 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>
|
||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* This file is a part of the SteamWar software.
|
* This file is a part of the SteamWar software.
|
||||||
*
|
*
|
||||||
* Copyright (C) 2023 SteamWar.de-Serverteam
|
* Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU Affero General Public License as published by
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -17,17 +17,16 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function window<T>(arr: T[], len: number): T[][] {
|
import type {ColumnDef} from "@tanstack/table-core";
|
||||||
const result: T[][] = [];
|
import type {EventFight} from "@type/event.ts";
|
||||||
for (let i = 0; i < arr.length; i += len) {
|
|
||||||
result.push(arr.slice(i, i + len));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stopPropagation(a: any) {
|
export const columns: ColumnDef<EventFight> = [
|
||||||
return (e: Event) => {
|
{
|
||||||
e.stopPropagation();
|
accessorFn: (r) => r.blueTeam.name,
|
||||||
a(e);
|
header: "Team Blue",
|
||||||
};
|
},
|
||||||
}
|
{
|
||||||
|
accessorFn: (r) => r.redTeam.name,
|
||||||
|
header: "Team Red",
|
||||||
|
},
|
||||||
|
];
|
||||||
51
src/components/moderator/pages/events/Events.svelte
Normal file
51
src/components/moderator/pages/events/Events.svelte
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { eventRepo } from "@repo/event.ts";
|
||||||
|
import EventCard from "@components/moderator/components/EventCard.svelte";
|
||||||
|
|
||||||
|
let eventsFuture = $state($eventRepo.listEvents());
|
||||||
|
let millis = Date.now();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
{#await eventsFuture}
|
||||||
|
<p>Loading...</p>
|
||||||
|
{:then events}
|
||||||
|
<h1 class="mt-5 scroll-m-20 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0">Upcoming</h1>
|
||||||
|
<div class="grid gap-4 p-4 border-b" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))">
|
||||||
|
{#each events.filter((e) => e.start > millis) as event (event.id)}
|
||||||
|
<a href="#/event/{event.id}">
|
||||||
|
<EventCard {event} />
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<h1 class="mt-5 scroll-m-20 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0">Past</h1>
|
||||||
|
<div class="grid gap-4 p-4" style="grid-template-columns: repeat(auto-fill, minmax(300px, 1fr))">
|
||||||
|
{#each events.filter((e) => e.start < millis).reverse() as event (event.id)}
|
||||||
|
<a href="#/event/{event.id}">
|
||||||
|
<EventCard {event} />
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:catch e}
|
||||||
|
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {permissions, players} from "@stores/stores.ts";
|
||||||
|
import {Select, SelectContent, SelectItem} from "@components/ui/select";
|
||||||
|
import {SelectTrigger} from "@components/ui/select/index.js";
|
||||||
|
import {permsRepo} from "@repo/perms.ts";
|
||||||
|
|
||||||
|
const {
|
||||||
|
perms, uuid
|
||||||
|
}: { perms: string[], uuid: string } = $props();
|
||||||
|
|
||||||
|
let value = $state(perms);
|
||||||
|
let prevValue = $state(perms);
|
||||||
|
|
||||||
|
function onChange(change: string[]) {
|
||||||
|
$permissions.perms.forEach(perm => {
|
||||||
|
if (prevValue.includes(perm) && !change.includes(perm)) {
|
||||||
|
$permsRepo.removePerm(uuid, perm)
|
||||||
|
} else if (!prevValue.includes(perm) && change.includes(perm)) {
|
||||||
|
$permsRepo.addPerm(uuid, perm)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
prevValue = change;
|
||||||
|
value = change;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Select type="multiple" bind:value onValueChange={onChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
{value.length} Permissions
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{#each $permissions.perms as permission (permission)}
|
||||||
|
<SelectItem value={permission}>{permission}</SelectItem>
|
||||||
|
{/each}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
32
src/components/moderator/pages/players/Players.svelte
Normal file
32
src/components/moderator/pages/players/Players.svelte
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Table from "@components/moderator/pages/players/Table.svelte";
|
||||||
|
|
||||||
|
import {dataRepo} from "@repo/data";
|
||||||
|
|
||||||
|
let playersFuture = $state($dataRepo.getPlayers())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#await playersFuture}
|
||||||
|
<p>Loading...</p>
|
||||||
|
{:then players}
|
||||||
|
<Table data={players} />
|
||||||
|
{/await}
|
||||||
47
src/components/moderator/pages/players/PrefixDropdown.svelte
Normal file
47
src/components/moderator/pages/players/PrefixDropdown.svelte
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {Select, SelectContent, SelectItem, SelectTrigger} from "@components/ui/select";
|
||||||
|
import {permissions} from "@stores/stores.ts";
|
||||||
|
import {permsRepo} from "@repo/perms.ts";
|
||||||
|
|
||||||
|
const {
|
||||||
|
prefix, uuid
|
||||||
|
}: { prefix: string, uuid: string } = $props();
|
||||||
|
|
||||||
|
let value = $state(prefix);
|
||||||
|
|
||||||
|
function onChange(change: string) {
|
||||||
|
$permsRepo.setPrefix(uuid, change);
|
||||||
|
|
||||||
|
value = $permissions.prefixes[change].chatPrefix;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Select type="single" bind:value onValueChange={onChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
{value === "" ? "None" : value}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{#each Object.entries($permissions.prefixes) as prefix (prefix[1].name)}
|
||||||
|
<SelectItem value={prefix[0]}>{prefix[1].chatPrefix === "" ? "None" : prefix[1].chatPrefix}</SelectItem>
|
||||||
|
{/each}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
174
src/components/moderator/pages/players/Table.svelte
Normal file
174
src/components/moderator/pages/players/Table.svelte
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<!--
|
||||||
|
- This file is a part of the SteamWar software.
|
||||||
|
-
|
||||||
|
- Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
-
|
||||||
|
- This program is free software: you can redistribute it and/or modify
|
||||||
|
- it under the terms of the GNU Affero General Public License as published by
|
||||||
|
- the Free Software Foundation, either version 3 of the License, or
|
||||||
|
- (at your option) any later version.
|
||||||
|
-
|
||||||
|
- This program is distributed in the hope that it will be useful,
|
||||||
|
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
- GNU Affero General Public License for more details.
|
||||||
|
-
|
||||||
|
- You should have received a copy of the GNU Affero General Public License
|
||||||
|
- along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
type ColumnFiltersState,
|
||||||
|
getCoreRowModel, getFilteredRowModel,
|
||||||
|
getPaginationRowModel, getSortedRowModel,
|
||||||
|
type PaginationState,
|
||||||
|
type SortingState,
|
||||||
|
} from "@tanstack/table-core";
|
||||||
|
import {
|
||||||
|
createSvelteTable,
|
||||||
|
FlexRender,
|
||||||
|
} from "@components/ui/data-table/index";
|
||||||
|
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@components/ui/table";
|
||||||
|
import {Button} from "@components/ui/button";
|
||||||
|
import {Input} from "@components/ui/input";
|
||||||
|
import {Select} from "@components/ui/select";
|
||||||
|
import {SelectContent, SelectItem, SelectTrigger} from "@components/ui/select/index.js";
|
||||||
|
import type {Player} from "@type/data";
|
||||||
|
import { columns } from "./columns";
|
||||||
|
|
||||||
|
let { data }: { data: Player[] } = $props();
|
||||||
|
|
||||||
|
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 25 });
|
||||||
|
let sorting = $state<SortingState>([]);
|
||||||
|
let columnFilters = $state<ColumnFiltersState>([]);
|
||||||
|
|
||||||
|
const table = createSvelteTable({
|
||||||
|
get data() {
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
get pagination() {
|
||||||
|
return pagination;
|
||||||
|
},
|
||||||
|
get sorting() {
|
||||||
|
return sorting;
|
||||||
|
},
|
||||||
|
get columnFilters() {
|
||||||
|
return columnFilters;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onPaginationChange: (updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
pagination = updater(pagination);
|
||||||
|
} else {
|
||||||
|
pagination = updater;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSortingChange: (updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
sorting = updater(sorting);
|
||||||
|
} else {
|
||||||
|
sorting = updater;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onColumnFiltersChange: (updater) => {
|
||||||
|
if (typeof updater === "function") {
|
||||||
|
columnFilters = updater(columnFilters);
|
||||||
|
} else {
|
||||||
|
columnFilters = updater;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="rounded-md border m-4">
|
||||||
|
<div class="flex items-center p-4 border-b">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter Players..."
|
||||||
|
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
|
||||||
|
onchange={(e) => {
|
||||||
|
table.getColumn("name")?.setFilterValue(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
oninput={(e) => {
|
||||||
|
table.getColumn("name")?.setFilterValue(e.currentTarget.value);
|
||||||
|
}}
|
||||||
|
class="max-w-sm"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center px-4">
|
||||||
|
<Select type="single" value={pagination.pageSize.toString()} onValueChange={(e) => pagination = { pageSize: +e, pageIndex: 0 }}>
|
||||||
|
<SelectTrigger>{pagination.pageSize}</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="5">5</SelectItem>
|
||||||
|
<SelectItem value="10">10</SelectItem>
|
||||||
|
<SelectItem value="25">25</SelectItem>
|
||||||
|
<SelectItem value="50">50</SelectItem>
|
||||||
|
<SelectItem value="100">100</SelectItem>
|
||||||
|
<SelectItem value="200">200</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
|
||||||
|
<TableRow>
|
||||||
|
{#each headerGroup.headers as header (header.id)}
|
||||||
|
<TableHead>
|
||||||
|
{#if !header.isPlaceholder}
|
||||||
|
<FlexRender
|
||||||
|
content={header.column.columnDef.header}
|
||||||
|
context={header.getContext()}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</TableHead>
|
||||||
|
{/each}
|
||||||
|
</TableRow>
|
||||||
|
{/each}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{#each table.getRowModel().rows as row (row.id)}
|
||||||
|
<TableRow data-state={row.getIsSelected() && "selected"}>
|
||||||
|
{#each row.getVisibleCells() as cell (cell.id)}
|
||||||
|
<TableCell>
|
||||||
|
<FlexRender
|
||||||
|
content={cell.column.columnDef.cell}
|
||||||
|
context={cell.getContext()}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
{/each}
|
||||||
|
</TableRow>
|
||||||
|
{:else}
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colspan={columns.length} class="h-24 text-center">
|
||||||
|
No results.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
{/each}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<div class="flex items-center justify-end space-x-2 p-4 border-t">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<span>{pagination.pageIndex + 1}/{table.getPageCount()}</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onclick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
60
src/components/moderator/pages/players/columns.ts
Normal file
60
src/components/moderator/pages/players/columns.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/*
|
||||||
|
* This file is a part of the SteamWar software.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2025 SteamWar.de-Serverteam
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {ColumnDef} from "@tanstack/table-core";
|
||||||
|
import type {Player} from "@type/data.ts";
|
||||||
|
import { renderComponent } from "@components/ui/data-table";
|
||||||
|
import PermissionsDropdown from "@components/moderator/pages/players/PermissionsDropdown.svelte";
|
||||||
|
import PrefixDropdown from "@components/moderator/pages/players/PrefixDropdown.svelte";
|
||||||
|
|
||||||
|
export const columns: ColumnDef<Player[]> = [
|
||||||
|
{
|
||||||
|
accessorKey: "uuid",
|
||||||
|
header: "UUID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: "Name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "prefix",
|
||||||
|
header: "Prefix",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return renderComponent(
|
||||||
|
PrefixDropdown, {
|
||||||
|
prefix: row.getValue("prefix"),
|
||||||
|
uuid: row.getValue("uuid"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "perms",
|
||||||
|
header: "Permissions",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return renderComponent(
|
||||||
|
PermissionsDropdown,
|
||||||
|
{
|
||||||
|
perms: row.getValue("perms"),
|
||||||
|
uuid: row.getValue("uuid"),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -1,44 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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));
|
|
||||||
184
src/components/repo/authv2.ts
Normal file
184
src/components/repo/authv2.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
/*
|
||||||
|
* 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());
|
||||||
@ -20,7 +20,7 @@
|
|||||||
import type {Player, Server} from "@type/data.ts";
|
import type {Player, Server} from "@type/data.ts";
|
||||||
import {PlayerSchema, ServerSchema} from "@type/data.ts";
|
import {PlayerSchema, ServerSchema} from "@type/data.ts";
|
||||||
import {fetchWithToken, tokenStore} from "./repo.ts";
|
import {fetchWithToken, tokenStore} from "./repo.ts";
|
||||||
import {derived} from "svelte/store";
|
import {derived, get} from "svelte/store";
|
||||||
|
|
||||||
export class DataRepo {
|
export class DataRepo {
|
||||||
constructor(private token: string) {
|
constructor(private token: string) {
|
||||||
@ -33,6 +33,10 @@ export class DataRepo {
|
|||||||
public async getMe(): Promise<Player> {
|
public async getMe(): Promise<Player> {
|
||||||
return await fetchWithToken(this.token, "/data/me").then(value => value.json()).then(PlayerSchema.parse);
|
return await fetchWithToken(this.token, "/data/me").then(value => value.json()).then(PlayerSchema.parse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getPlayers(): Promise<Player[]> {
|
||||||
|
return await fetchWithToken(get(tokenStore), "/data/admin/users").then(value => value.json()).then(PlayerSchema.array().parse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dataRepo = derived(tokenStore, ($token) => new DataRepo($token));
|
export const dataRepo = derived(tokenStore, ($token) => new DataRepo($token));
|
||||||
@ -31,15 +31,15 @@ export interface CreateEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateEvent {
|
export interface UpdateEvent {
|
||||||
name: string | null;
|
name?: string | null;
|
||||||
start: Dayjs | null;
|
start?: Dayjs | number | null;
|
||||||
end: Dayjs | null;
|
end?: Dayjs | number | null;
|
||||||
deadline: Dayjs | null;
|
deadline?: Dayjs | number | null;
|
||||||
maxTeamMembers: number | null;
|
maxTeamMembers?: number | null;
|
||||||
schemType: string | null;
|
schemType?: string | null;
|
||||||
publicSchemsOnly: boolean | null;
|
publicSchemsOnly?: boolean | null;
|
||||||
addReferee: string[] | null;
|
addReferee?: string[] | null;
|
||||||
removeReferee: string[] | null;
|
removeReferee?: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EventRepo {
|
export class EventRepo {
|
||||||
|
|||||||
@ -17,31 +17,9 @@
|
|||||||
* 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 {writable} from "svelte/store";
|
import {get, writable} from "svelte/store";
|
||||||
|
import {authV2Repo} from "@repo/authv2.ts";
|
||||||
|
|
||||||
export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) =>
|
export const fetchWithToken = (token: string, url: string, params: RequestInit = {}) => get(authV2Repo).request(url, params);
|
||||||
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 function getLocalStorage() {
|
export const tokenStore = writable("");
|
||||||
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));
|
|
||||||
|
|||||||
@ -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/${id}`).then(value => value.json()).then(UserStatsSchema.parse);
|
return await fetchWithToken(this.token, `/stats/user`).then(value => value.json()).then(UserStatsSchema.parse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import {z} from "zod";
|
|||||||
import {fetchWithToken, tokenStore} from "@repo/repo.ts";
|
import {fetchWithToken, tokenStore} from "@repo/repo.ts";
|
||||||
import {pageRepo} from "@repo/page.ts";
|
import {pageRepo} from "@repo/page.ts";
|
||||||
import {dataRepo} from "@repo/data.ts";
|
import {dataRepo} from "@repo/data.ts";
|
||||||
|
import {permsRepo} from "@repo/perms.ts";
|
||||||
|
|
||||||
export const schemTypes = cached<SchematicType[]>([], () =>
|
export const schemTypes = cached<SchematicType[]>([], () =>
|
||||||
fetchWithToken(get(tokenStore), "/data/admin/schematicTypes")
|
fetchWithToken(get(tokenStore), "/data/admin/schematicTypes")
|
||||||
@ -37,6 +38,13 @@ export const players = cached<Player[]>([], async () => {
|
|||||||
return z.array(PlayerSchema).parse(await res.json());
|
return z.array(PlayerSchema).parse(await res.json());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const permissions = cached({
|
||||||
|
perms: [],
|
||||||
|
prefixes: {},
|
||||||
|
}, async () => {
|
||||||
|
return get(permsRepo).listPerms();
|
||||||
|
});
|
||||||
|
|
||||||
export const gamemodes = cached<string[]>([], async () => {
|
export const gamemodes = cached<string[]>([], async () => {
|
||||||
const res = await fetchWithToken(get(tokenStore), "/data/admin/gamemodes");
|
const res = await fetchWithToken(get(tokenStore), "/data/admin/gamemodes");
|
||||||
return z.array(z.string()).parse(await res.json());
|
return z.array(z.string()).parse(await res.json());
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte";
|
import {onMount} from "svelte";
|
||||||
import {stopPropagation} from "@components/util.ts";
|
import {stopPropagation} from "@components/utils.ts";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
@ -68,16 +68,18 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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">
|
<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">
|
||||||
<div onclick={stopPropagation(onclick)} aria-hidden="true">
|
<div onclick={stopPropagation(onclick)} aria-hidden="true" class="w-fit">
|
||||||
<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="flex mx-4 my-2 p-6" onclick={() => dialog.close()} aria-hidden="true">
|
<div class="mx-4 my-2 p-6">
|
||||||
{@render footer?.()}
|
<div class="ml-auto flex justify-end" onclick={() => dialog.close()} aria-hidden="true">
|
||||||
|
{@render footer?.()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|||||||
34
src/components/types/auth.ts
Normal file
34
src/components/types/auth.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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>;
|
||||||
25
src/components/ui/accordion/accordion-content.svelte
Normal file
25
src/components/ui/accordion/accordion-content.svelte
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<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>
|
||||||
14
src/components/ui/accordion/accordion-item.svelte
Normal file
14
src/components/ui/accordion/accordion-item.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<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>
|
||||||
26
src/components/ui/accordion/accordion-trigger.svelte
Normal file
26
src/components/ui/accordion/accordion-trigger.svelte
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<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>
|
||||||
17
src/components/ui/accordion/index.ts
Normal file
17
src/components/ui/accordion/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
21
src/components/ui/alert-dialog/alert-dialog-action.svelte
Normal file
21
src/components/ui/alert-dialog/alert-dialog-action.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<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>
|
||||||
21
src/components/ui/alert-dialog/alert-dialog-cancel.svelte
Normal file
21
src/components/ui/alert-dialog/alert-dialog-cancel.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<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>
|
||||||
28
src/components/ui/alert-dialog/alert-dialog-content.svelte
Normal file
28
src/components/ui/alert-dialog/alert-dialog-content.svelte
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<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>
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
<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>
|
||||||
16
src/components/ui/alert-dialog/alert-dialog-footer.svelte
Normal file
16
src/components/ui/alert-dialog/alert-dialog-footer.svelte
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<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>
|
||||||
13
src/components/ui/alert-dialog/alert-dialog-header.svelte
Normal file
13
src/components/ui/alert-dialog/alert-dialog-header.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<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>
|
||||||
21
src/components/ui/alert-dialog/alert-dialog-overlay.svelte
Normal file
21
src/components/ui/alert-dialog/alert-dialog-overlay.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<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}
|
||||||
|
/>
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { AlertDialog as AlertDialogPrimitive } from "bits-ui";
|
||||||
|
|
||||||
|
type $$Props = AlertDialogPrimitive.PortalProps;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AlertDialogPrimitive.Portal {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</AlertDialogPrimitive.Portal>
|
||||||
14
src/components/ui/alert-dialog/alert-dialog-title.svelte
Normal file
14
src/components/ui/alert-dialog/alert-dialog-title.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<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>
|
||||||
40
src/components/ui/alert-dialog/index.ts
Normal file
40
src/components/ui/alert-dialog/index.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
13
src/components/ui/alert/alert-description.svelte
Normal file
13
src/components/ui/alert/alert-description.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<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>
|
||||||
21
src/components/ui/alert/alert-title.svelte
Normal file
21
src/components/ui/alert/alert-title.svelte
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<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>
|
||||||
17
src/components/ui/alert/alert.svelte
Normal file
17
src/components/ui/alert/alert.svelte
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<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>
|
||||||
33
src/components/ui/alert/index.ts
Normal file
33
src/components/ui/alert/index.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
11
src/components/ui/aspect-ratio/aspect-ratio.svelte
Normal file
11
src/components/ui/aspect-ratio/aspect-ratio.svelte
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<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>
|
||||||
3
src/components/ui/aspect-ratio/index.ts
Normal file
3
src/components/ui/aspect-ratio/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Root from "./aspect-ratio.svelte";
|
||||||
|
|
||||||
|
export { Root, Root as AspectRatio };
|
||||||
16
src/components/ui/avatar/avatar-fallback.svelte
Normal file
16
src/components/ui/avatar/avatar-fallback.svelte
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<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>
|
||||||
18
src/components/ui/avatar/avatar-image.svelte
Normal file
18
src/components/ui/avatar/avatar-image.svelte
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<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}
|
||||||
|
/>
|
||||||
18
src/components/ui/avatar/avatar.svelte
Normal file
18
src/components/ui/avatar/avatar.svelte
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<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>
|
||||||
13
src/components/ui/avatar/index.ts
Normal file
13
src/components/ui/avatar/index.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
18
src/components/ui/badge/badge.svelte
Normal file
18
src/components/ui/badge/badge.svelte
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<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>
|
||||||
21
src/components/ui/badge/index.ts
Normal file
21
src/components/ui/badge/index.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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"];
|
||||||
24
src/components/ui/breadcrumb/breadcrumb-ellipsis.svelte
Normal file
24
src/components/ui/breadcrumb/breadcrumb-ellipsis.svelte
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<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>
|
||||||
16
src/components/ui/breadcrumb/breadcrumb-item.svelte
Normal file
16
src/components/ui/breadcrumb/breadcrumb-item.svelte
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<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>
|
||||||
31
src/components/ui/breadcrumb/breadcrumb-link.svelte
Normal file
31
src/components/ui/breadcrumb/breadcrumb-link.svelte
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<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}
|
||||||
23
src/components/ui/breadcrumb/breadcrumb-list.svelte
Normal file
23
src/components/ui/breadcrumb/breadcrumb-list.svelte
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<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>
|
||||||
23
src/components/ui/breadcrumb/breadcrumb-page.svelte
Normal file
23
src/components/ui/breadcrumb/breadcrumb-page.svelte
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<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>
|
||||||
25
src/components/ui/breadcrumb/breadcrumb-separator.svelte
Normal file
25
src/components/ui/breadcrumb/breadcrumb-separator.svelte
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<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>
|
||||||
15
src/components/ui/breadcrumb/breadcrumb.svelte
Normal file
15
src/components/ui/breadcrumb/breadcrumb.svelte
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<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>
|
||||||
25
src/components/ui/breadcrumb/index.ts
Normal file
25
src/components/ui/breadcrumb/index.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
74
src/components/ui/button/button.svelte
Normal file
74
src/components/ui/button/button.svelte
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<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}
|
||||||
17
src/components/ui/button/index.ts
Normal file
17
src/components/ui/button/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
19
src/components/ui/calendar/calendar-cell.svelte
Normal file
19
src/components/ui/calendar/calendar-cell.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<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}
|
||||||
|
/>
|
||||||
30
src/components/ui/calendar/calendar-day.svelte
Normal file
30
src/components/ui/calendar/calendar-day.svelte
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<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}
|
||||||
|
/>
|
||||||
12
src/components/ui/calendar/calendar-grid-body.svelte
Normal file
12
src/components/ui/calendar/calendar-grid-body.svelte
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<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} />
|
||||||
12
src/components/ui/calendar/calendar-grid-head.svelte
Normal file
12
src/components/ui/calendar/calendar-grid-head.svelte
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<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} />
|
||||||
12
src/components/ui/calendar/calendar-grid-row.svelte
Normal file
12
src/components/ui/calendar/calendar-grid-row.svelte
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<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} />
|
||||||
16
src/components/ui/calendar/calendar-grid.svelte
Normal file
16
src/components/ui/calendar/calendar-grid.svelte
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<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}
|
||||||
|
/>
|
||||||
16
src/components/ui/calendar/calendar-head-cell.svelte
Normal file
16
src/components/ui/calendar/calendar-head-cell.svelte
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<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}
|
||||||
|
/>
|
||||||
16
src/components/ui/calendar/calendar-header.svelte
Normal file
16
src/components/ui/calendar/calendar-header.svelte
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<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}
|
||||||
|
/>
|
||||||
12
src/components/ui/calendar/calendar-heading.svelte
Normal file
12
src/components/ui/calendar/calendar-heading.svelte
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<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} />
|
||||||
20
src/components/ui/calendar/calendar-months.svelte
Normal file
20
src/components/ui/calendar/calendar-months.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<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>
|
||||||
28
src/components/ui/calendar/calendar-next-button.svelte
Normal file
28
src/components/ui/calendar/calendar-next-button.svelte
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<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}
|
||||||
|
/>
|
||||||
28
src/components/ui/calendar/calendar-prev-button.svelte
Normal file
28
src/components/ui/calendar/calendar-prev-button.svelte
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<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}
|
||||||
|
/>
|
||||||
61
src/components/ui/calendar/calendar.svelte
Normal file
61
src/components/ui/calendar/calendar.svelte
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Calendar as CalendarPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||||
|
import * as Calendar from "./index.js";
|
||||||
|
import { cn } from "$lib/components/utils.js";
|
||||||
|
|
||||||
|
let {
|
||||||
|
ref = $bindable(null),
|
||||||
|
value = $bindable(),
|
||||||
|
placeholder = $bindable(),
|
||||||
|
class: className,
|
||||||
|
weekdayFormat = "short",
|
||||||
|
...restProps
|
||||||
|
}: WithoutChildrenOrChild<CalendarPrimitive.RootProps> = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Discriminated Unions + Destructing (required for bindable) do not
|
||||||
|
get along, so we shut typescript up by casting `value` to `never`.
|
||||||
|
-->
|
||||||
|
<CalendarPrimitive.Root
|
||||||
|
bind:value={value as never}
|
||||||
|
bind:ref
|
||||||
|
bind:placeholder
|
||||||
|
{weekdayFormat}
|
||||||
|
class={cn("p-3", className)}
|
||||||
|
{...restProps}
|
||||||
|
>
|
||||||
|
{#snippet children({ months, weekdays })}
|
||||||
|
<Calendar.Header>
|
||||||
|
<Calendar.PrevButton />
|
||||||
|
<Calendar.Heading />
|
||||||
|
<Calendar.NextButton />
|
||||||
|
</Calendar.Header>
|
||||||
|
<Calendar.Months>
|
||||||
|
{#each months as month}
|
||||||
|
<Calendar.Grid>
|
||||||
|
<Calendar.GridHead>
|
||||||
|
<Calendar.GridRow class="flex">
|
||||||
|
{#each weekdays as weekday}
|
||||||
|
<Calendar.HeadCell>
|
||||||
|
{weekday.slice(0, 2)}
|
||||||
|
</Calendar.HeadCell>
|
||||||
|
{/each}
|
||||||
|
</Calendar.GridRow>
|
||||||
|
</Calendar.GridHead>
|
||||||
|
<Calendar.GridBody>
|
||||||
|
{#each month.weeks as weekDates}
|
||||||
|
<Calendar.GridRow class="mt-2 w-full">
|
||||||
|
{#each weekDates as date}
|
||||||
|
<Calendar.Cell {date} month={month.value}>
|
||||||
|
<Calendar.Day />
|
||||||
|
</Calendar.Cell>
|
||||||
|
{/each}
|
||||||
|
</Calendar.GridRow>
|
||||||
|
{/each}
|
||||||
|
</Calendar.GridBody>
|
||||||
|
</Calendar.Grid>
|
||||||
|
{/each}
|
||||||
|
</Calendar.Months>
|
||||||
|
{/snippet}
|
||||||
|
</CalendarPrimitive.Root>
|
||||||
30
src/components/ui/calendar/index.ts
Normal file
30
src/components/ui/calendar/index.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import Root from "./calendar.svelte";
|
||||||
|
import Cell from "./calendar-cell.svelte";
|
||||||
|
import Day from "./calendar-day.svelte";
|
||||||
|
import Grid from "./calendar-grid.svelte";
|
||||||
|
import Header from "./calendar-header.svelte";
|
||||||
|
import Months from "./calendar-months.svelte";
|
||||||
|
import GridRow from "./calendar-grid-row.svelte";
|
||||||
|
import Heading from "./calendar-heading.svelte";
|
||||||
|
import GridBody from "./calendar-grid-body.svelte";
|
||||||
|
import GridHead from "./calendar-grid-head.svelte";
|
||||||
|
import HeadCell from "./calendar-head-cell.svelte";
|
||||||
|
import NextButton from "./calendar-next-button.svelte";
|
||||||
|
import PrevButton from "./calendar-prev-button.svelte";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Day,
|
||||||
|
Cell,
|
||||||
|
Grid,
|
||||||
|
Header,
|
||||||
|
Months,
|
||||||
|
GridRow,
|
||||||
|
Heading,
|
||||||
|
GridBody,
|
||||||
|
GridHead,
|
||||||
|
HeadCell,
|
||||||
|
NextButton,
|
||||||
|
PrevButton,
|
||||||
|
//
|
||||||
|
Root as Calendar,
|
||||||
|
};
|
||||||
13
src/components/ui/card/card-content.svelte
Normal file
13
src/components/ui/card/card-content.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<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("p-6", className)} {...$$restProps}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user