update to svelte 5
All checks were successful
delpoy / build-and-deploy (push) Successful in 35s

This commit is contained in:
bytedream 2024-12-02 00:28:43 +01:00
parent abffa440a1
commit 95968148a6
53 changed files with 2199 additions and 2002 deletions

View File

@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@ -1,30 +0,0 @@
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};

40
eslint.config.mjs Normal file
View File

@ -0,0 +1,40 @@
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import ts from 'typescript-eslint';
export default ts.config(
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs['flat/recommended'],
prettier,
...svelte.configs['flat/prettier'],
{
languageOptions: {
globals: {
...globals.browser,
...globals.node
}
}
},
{
files: ['**/*.svelte'],
languageOptions: {
parserOptions: {
parser: ts.parser
}
}
},
{
ignores: ['build/', '.svelte-kit/', 'dist/']
},
{
rules: {
// expressions are often used to check if a props function is defined before calling it
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-explicit-any': 'off'
}
}
);

2746
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,33 +15,36 @@
"devDependencies": { "devDependencies": {
"@fontsource/nunito": "^5.1.0", "@fontsource/nunito": "^5.1.0",
"@fontsource/roboto": "^5.1.0", "@fontsource/roboto": "^5.1.0",
"@sveltejs/adapter-node": "^5.2.8", "@sveltejs/adapter-node": "^5.2.9",
"@sveltejs/kit": "^2.7.1", "@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.1",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
"@types/node": "^22.7.7", "@types/node": "^22.10.1",
"@types/validator": "^13.12.2", "@types/validator": "^13.12.2",
"@typescript-eslint/eslint-plugin": "^8.10.0",
"@typescript-eslint/parser": "^8.10.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"daisyui": "^4.12.13", "daisyui": "^4.12.14",
"eslint": "^9.13.0", "eslint": "^9.16.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.45.1", "eslint-plugin-svelte": "^2.46.1",
"postcss": "^8.4.47", "globals": "^15.13.0",
"prettier": "^3.3.3", "postcss": "^8.4.49",
"prettier-plugin-svelte": "^3.2.7", "prettier": "^3.4.1",
"sass": "^1.80.3", "prettier-plugin-svelte": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.9",
"publint": "^0.2.12",
"sass": "^1.81.0",
"skinview3d": "^3.1.0", "skinview3d": "^3.1.0",
"svelte": "^4.2.19", "svelte": "^5.3.0",
"svelte-check": "^4.0.5", "svelte-check": "^4.1.0",
"svelte-heros-v2": "^1.3.0", "svelte-heros-v2": "^2.0.1",
"svelte-multicssclass": "^2.1.1", "svelte-multicssclass": "^2.1.1",
"svelte-preprocess": "^6.0.3", "svelte-preprocess": "^6.0.3",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.15",
"tslib": "^2.8.0", "tslib": "^2.8.1",
"typescript": "^5.6.3", "typescript": "^5.7.2",
"vite": "^5.4.9", "typescript-eslint": "^8.16.0",
"vitest": "^2.1.3", "vite": "^6.0.1",
"vitest": "^2.1.6",
"zod": "^3.23.8" "zod": "^3.23.8"
}, },
"type": "module", "type": "module",

View File

@ -2,10 +2,7 @@
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
// start date in milliseconds. if undefined, start will be Date.now let { start, end }: { start?: number; end: number } = $props();
export let start: number | undefined = undefined;
// end date in milliseconds
export let end: number;
let title = `Spielstart ist am ${new Date(env.PUBLIC_START_DATE).toLocaleString('de-DE', { let title = `Spielstart ist am ${new Date(env.PUBLIC_START_DATE).toLocaleString('de-DE', {
day: '2-digit', day: '2-digit',
@ -26,7 +23,7 @@
]; ];
} }
let [days, hours, minutes, seconds] = getUntil(); let [days, hours, minutes, seconds] = $state(getUntil());
let intervalId = setInterval(() => { let intervalId = setInterval(() => {
[days, hours, minutes, seconds] = getUntil(); [days, hours, minutes, seconds] = getUntil();
if (start) start += 1000; if (start) start += 1000;
@ -41,25 +38,25 @@
> >
<div class="flex flex-col p-2 bg-gray-200 rounded-box bg-opacity-5 backdrop-blur-sm" {title}> <div class="flex flex-col p-2 bg-gray-200 rounded-box bg-opacity-5 backdrop-blur-sm" {title}>
<span class="countdown font-mono text-3xl sm:text-6xl"> <span class="countdown font-mono text-3xl sm:text-6xl">
<span class="m-auto" style="--value:{days};" /> <span class="m-auto" style="--value:{days};"></span>
</span> </span>
Tage Tage
</div> </div>
<div class="flex flex-col p-2 bg-gray-200 rounded-box bg-opacity-5 backdrop-blur-sm" {title}> <div class="flex flex-col p-2 bg-gray-200 rounded-box bg-opacity-5 backdrop-blur-sm" {title}>
<span class="countdown font-mono text-3xl sm:text-6xl"> <span class="countdown font-mono text-3xl sm:text-6xl">
<span class="m-auto" style="--value:{hours};" /> <span class="m-auto" style="--value:{hours};"></span>
</span> </span>
Stunden Stunden
</div> </div>
<div class="flex flex-col p-2 bg-gray-200 rounded-box bg-opacity-5 backdrop-blur-sm" {title}> <div class="flex flex-col p-2 bg-gray-200 rounded-box bg-opacity-5 backdrop-blur-sm" {title}>
<span class="countdown font-mono text-3xl sm:text-6xl"> <span class="countdown font-mono text-3xl sm:text-6xl">
<span class="m-auto" style="--value:{minutes};" /> <span class="m-auto" style="--value:{minutes};"></span>
</span> </span>
Minuten Minuten
</div> </div>
<div class="flex flex-col p-2 bg-gray-200 rounded-box bg-opacity-5 backdrop-blur-sm" {title}> <div class="flex flex-col p-2 bg-gray-200 rounded-box bg-opacity-5 backdrop-blur-sm" {title}>
<span class="countdown font-mono text-3xl sm:text-6xl"> <span class="countdown font-mono text-3xl sm:text-6xl">
<span class="m-auto" style="--value:{seconds};" /> <span class="m-auto" style="--value:{seconds};"></span>
</span> </span>
Sekunden Sekunden
</div> </div>

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
export let size = '24'; let { size = '24', fill = 'currentColor' } = $props();
export let fill = 'currentColor';
</script> </script>
<svg xmlns="http://www.w3.org/2000/svg" height={size} width={size} {fill} viewBox="0 0 512 512" <svg xmlns="http://www.w3.org/2000/svg" height={size} width={size} {fill} viewBox="0 0 512 512"

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
export let size = '24'; let { size = '24', fill = 'currentColor' } = $props();
export let fill = 'currentColor';
</script> </script>
<svg xmlns="http://www.w3.org/2000/svg" height={size} width={size} {fill} viewBox="0 0 512 512" <svg xmlns="http://www.w3.org/2000/svg" height={size} width={size} {fill} viewBox="0 0 512 512"

View File

@ -1,12 +1,17 @@
<script lang="ts"> <script lang="ts">
// eslint-disable-next-line no-undef let {
type T = $$Generic; id,
name,
export let id: string | null = null; disabled = false,
export let name: string | null = null; available = {},
export let disabled = false; value = $bindable([])
export let available: string[] | { [key: string]: T } = {}; }: {
export let value: T[] = []; id?: string;
name?: string;
disabled?: boolean;
available?: string[] | { [key: string]: any };
value: any[];
} = $props();
</script> </script>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
@ -15,7 +20,7 @@
{name} {name}
class="select select-bordered select-xs" class="select select-bordered select-xs"
disabled={disabled || available.length === 0} disabled={disabled || available.length === 0}
on:change={(e) => { onchange={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
value.push(Object.values(available)[Object.keys(available).indexOf(e.target.value)]); value.push(Object.values(available)[Object.keys(available).indexOf(e.target.value)]);
@ -42,7 +47,7 @@
<button <button
{disabled} {disabled}
class:pointer-events-none={disabled} class:pointer-events-none={disabled}
on:click={() => { onclick={() => {
value.splice(i, 1); value.splice(i, 1);
value = value; value = value;
}}>✕</button }}>✕</button

View File

@ -1,30 +1,46 @@
<script lang="ts"> <script lang="ts">
import { Eye, EyeSlash } from 'svelte-heros-v2'; import { Eye, EyeSlash } from 'svelte-heros-v2';
import { createEventDispatcher } from 'svelte'; import type { Snippet } from 'svelte';
export let id: string | null = null; let {
export let name: string | null = null; label,
export let type = 'text'; notice,
export let value: string | null = null; id,
export let placeholder: string | null = null; name,
export let pattern: RegExp | null = null; type = 'text',
export let required = false; value = $bindable(),
export let disabled = false; placeholder,
export let readonly = false; pattern,
export let checked = false; required = false,
export let size: 'xs' | 'sm' | 'md' | 'lg' = 'md'; disabled = false,
export let pickyWidth = true; readonly = false,
export let containerClass = ''; checked = false,
size = 'md',
export let inputElement: HTMLInputElement | undefined = undefined; pickyWidth = true,
containerClass = '',
const dispatch = createEventDispatcher(); inputElement = $bindable(),
function input(e: Event & { currentTarget: EventTarget & HTMLInputElement }) { oninput,
dispatch('input', e); onclick
} }: {
function click(e: Event) { label?: Snippet;
dispatch('click', e); notice?: Snippet;
} id?: string;
name?: string;
type?: string;
value?: string;
placeholder?: string;
pattern?: RegExp;
required?: boolean;
disabled?: boolean;
readonly?: boolean;
checked?: boolean;
size?: 'xs' | 'sm' | 'md' | 'lg';
pickyWidth?: boolean;
containerClass?: string;
inputElement?: HTMLInputElement;
oninput?: (e: Event & { currentTarget: EventTarget & HTMLInputElement }) => void;
onclick?: (e: Event) => void;
} = $props();
let initialType = type; let initialType = type;
@ -50,15 +66,15 @@
{disabled} {disabled}
bind:value bind:value
bind:this={inputElement} bind:this={inputElement}
on:input={input} {oninput}
on:click={click} {onclick}
/> />
{:else} {:else}
<div> <div>
{#if $$slots.label} {#if label}
<label class="label" for={id}> <label class="label" for={id}>
<span class="label-text"> <span class="label-text">
<slot name="label" /> {@render label()}
{#if required} {#if required}
<span class="text-red-700">*</span> <span class="text-red-700">*</span>
{/if} {/if}
@ -93,31 +109,23 @@
{readonly} {readonly}
bind:this={inputElement} bind:this={inputElement}
autocomplete="off" autocomplete="off"
on:input={(e) => { oninput={(e: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
value = e.target?.value; value = e.currentTarget.value;
if (pattern && !pattern.test(e.target?.value)) { if (pattern && !pattern.test(value)) return;
if (inputElement?.value.endsWith(e.data)) { oninput && oninput(e);
value = e.target?.value.substring(0, e.target?.value.length - e.data.length);
}
return;
}
return input(e);
}} }}
on:paste={(e) => { onpaste={(e) => {
if ( if (pattern && e.clipboardData && !pattern.test(e.clipboardData.getData('text'))) {
pattern &&
!pattern.test((e.clipboardData || window.clipboardData).getData('text'))
) {
e.preventDefault(); e.preventDefault();
} }
}} }}
on:click={click} {onclick}
/> />
{#if initialType === 'password'} {#if initialType === 'password'}
<button <button
class="absolute right-3" class="absolute right-3"
type="button" type="button"
on:click={() => { onclick={() => {
type = type === 'password' ? 'text' : 'password'; type = type === 'password' ? 'text' : 'password';
}} }}
> >
@ -129,9 +137,11 @@
</button> </button>
{/if} {/if}
</div> </div>
{#if $$slots.notice} {#if notice}
<label class="label" for={id}> <label class="label" for={id}>
<span class="label-text-alt"><slot name="notice" /></span> <span class="label-text-alt">
{@render notice()}
</span>
</label> </label>
{/if} {/if}
</div> </div>

View File

@ -1,23 +1,34 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; let {
id,
value = $bindable(),
inputValue = $bindable(),
suggestionRequired = false,
emptyAllowed = false,
searchSuggestionFunc = () => Promise.resolve([]),
invalidMessage,
size = 'md',
label,
required = false,
onsubmit
}: {
id?: string;
value?: string;
inputValue?: string;
suggestionRequired?: boolean;
emptyAllowed?: boolean;
searchSuggestionFunc?: (input: string) => Promise<{ name: string; value: string }[]>;
invalidMessage?: string;
size?: 'xs' | 'sm' | 'md' | 'lg';
label?: string;
required?: boolean;
onsubmit?: (event: Event & { input: string; value: string }) => void;
} = $props();
export let id: string | null = null; let searchSuggestions: { name: string; value: string }[] = $state([]);
export let value = ''; $effect(() => {
export let inputValue = ''; if (!suggestionRequired) value = inputValue;
export let suggestionRequired = false; });
export let emptyAllowed = false;
export let searchSuggestionFunc: (
input: string
) => Promise<{ name: string; value: string }[]> = () => Promise.resolve([]);
export let invalidMessage: string | null = null;
export let size: 'xs' | 'sm' | 'md' | 'lg' = 'md';
export let label: string | null = null;
export let required = false;
const dispatch = createEventDispatcher();
let searchSuggestions: { name: string; value: string }[] = [];
$: if (!suggestionRequired) value = inputValue;
</script> </script>
<div class="relative"> <div class="relative">
@ -42,7 +53,7 @@
{id} {id}
{required} {required}
bind:value={inputValue} bind:value={inputValue}
on:input={(e) => { oninput={(e: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
value = ''; value = '';
searchSuggestionFunc(inputValue).then((v) => { searchSuggestionFunc(inputValue).then((v) => {
searchSuggestions = v; searchSuggestions = v;
@ -51,15 +62,15 @@
inputValue = searchSuggestion.name; inputValue = searchSuggestion.name;
value = searchSuggestion.value; value = searchSuggestion.value;
searchSuggestions = []; searchSuggestions = [];
e.target?.setCustomValidity(''); e.currentTarget.setCustomValidity('');
dispatch('submit', { input: inputValue, value: value }); onsubmit && onsubmit(Object.assign(e, { input: inputValue, value: value }));
} else if (inputValue === '' && emptyAllowed) { } else if (inputValue === '' && emptyAllowed) {
dispatch('submit', { input: '', value: '' }); onsubmit && onsubmit(Object.assign(e, { input: '', value: '' }));
} }
}); });
}} }}
on:invalid={(e) => { oninvalid={(e: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
if (invalidMessage != null) e.target?.setCustomValidity(invalidMessage); invalidMessage && e.currentTarget.setCustomValidity(invalidMessage);
}} }}
pattern={suggestionRequired pattern={suggestionRequired
? `${value ? inputValue : 'a^' + (emptyAllowed ? '|$^' : '')}` ? `${value ? inputValue : 'a^' + (emptyAllowed ? '|$^' : '')}`
@ -74,11 +85,11 @@
<button <button
class="block w-full overflow-hidden text-ellipsis whitespace-nowrap" class="block w-full overflow-hidden text-ellipsis whitespace-nowrap"
title="{searchSuggestion.name} ({searchSuggestion.value})" title="{searchSuggestion.name} ({searchSuggestion.value})"
on:click|preventDefault={() => { onclick={(e) => {
inputValue = searchSuggestion.name; inputValue = searchSuggestion.name;
value = searchSuggestion.value; value = searchSuggestion.value;
searchSuggestions = []; searchSuggestions = [];
dispatch('submit', { input: inputValue, value: value }); onsubmit && onsubmit(Object.assign(e, { input: inputValue, value: value }));
}}>{searchSuggestion.name}</button }}>{searchSuggestion.name}</button
> >
</li> </li>
@ -90,7 +101,8 @@
<!-- close the search suggestions box when clicking outside --> <!-- close the search suggestions box when clicking outside -->
{#if inputValue && searchSuggestions.length !== 0} {#if inputValue && searchSuggestions.length !== 0}
<button <button
aria-label=" "
class="absolute top-0 left-0 z-10 w-full h-full cursor-default" class="absolute top-0 left-0 z-10 w-full h-full cursor-default"
on:click={() => (searchSuggestions = [])} onclick={() => (searchSuggestions = [])}
/> ></button>
{/if} {/if}

View File

@ -1,20 +1,31 @@
<script lang="ts"> <script lang="ts">
// eslint-disable-next-line no-undef import type { Snippet } from 'svelte';
import { createEventDispatcher } from 'svelte';
type T = $$Generic; let {
children,
export let id: string | null = null; id,
export let name: string | null = null; name,
export let value: T | null = null; value = $bindable(),
export let label: string | null = null; label,
export let notice: string | null = null; notice,
export let required = false; required = false,
export let disabled = false; disabled = false,
export let size: 'xs' | 'sm' | 'md' | 'lg' = 'md'; size = 'md',
export let pickyWidth = true; pickyWidth = true,
onChange
let dispatch = createEventDispatcher(); }: {
children: Snippet;
id?: string;
name?: string;
value?: any;
label?: string;
notice?: string;
required?: boolean;
disabled?: boolean;
size?: 'xs' | 'sm' | 'md' | 'lg';
pickyWidth?: boolean;
onChange?: ({ value }: { value: any }) => void;
} = $props();
</script> </script>
<div> <div>
@ -40,9 +51,9 @@
{required} {required}
{disabled} {disabled}
bind:value bind:value
on:change={(e) => dispatch('change', { value: value })} onchange={() => onChange && onChange({ value: value })}
> >
<slot /> {@render children()}
</select> </select>
{#if notice} {#if notice}
<label class="label" for={id}> <label class="label" for={id}>

View File

@ -41,7 +41,7 @@
{rows} {rows}
bind:value bind:value
on:click={(e) => dispatch('click', e)} on:click={(e) => dispatch('click', e)}
/> ></textarea>
{#if notice} {#if notice}
<label class="label" for={id}> <label class="label" for={id}>
<span class="label-text-alt">{notice}</span> <span class="label-text-alt">{notice}</span>

View File

@ -1,13 +1,11 @@
<script lang="ts"> <script lang="ts">
import { onMount, tick } from 'svelte'; import { onMount, type Snippet, tick } from 'svelte';
export let onUpdate: () => Promise<any> = Promise.resolve; let { children, onUpdate }: { children: Snippet; onUpdate: () => Promise<any> } = $props();
let bodyElem: HTMLTableSectionElement; let bodyElem: HTMLTableSectionElement;
let intersectionElem: HTMLElement;
let intersectionObserver: IntersectionObserver;
let intersectionElement: HTMLElement;
async function getIntersectionElement(): Promise<HTMLElement> { async function getIntersectionElement(): Promise<HTMLElement> {
if (!bodyElem.lastElementChild) { if (!bodyElem.lastElementChild) {
await new Promise<void>((resolve) => { await new Promise<void>((resolve) => {
@ -26,10 +24,10 @@
await onUpdate(); await onUpdate();
await tick(); await tick();
intersectionObserver = new IntersectionObserver( const intersectionObserver = new IntersectionObserver(
async (entries, observer) => { async (entries, observer) => {
if (entries.filter((e) => e.isIntersecting).length === 0 || !entries) return; if (entries.filter((e) => e.isIntersecting).length === 0 || !entries) return;
observer.unobserve(intersectionElement); observer.unobserve(intersectionElem);
const rows = bodyElem.rows.length; const rows = bodyElem.rows.length;
@ -37,7 +35,7 @@
await tick(); await tick();
if (rows === bodyElem.rows.length) return; if (rows === bodyElem.rows.length) return;
observer.observe((intersectionElement = await getIntersectionElement())); observer.observe((intersectionElem = await getIntersectionElement()));
}, },
{ threshold: 0.25 } { threshold: 0.25 }
); );
@ -51,14 +49,14 @@
return; return;
} }
if (intersectionElement) intersectionObserver.unobserve(intersectionElement); if (intersectionElem) intersectionObserver.unobserve(intersectionElem);
intersectionObserver.observe((intersectionElement = await getIntersectionElement())); intersectionObserver.observe((intersectionElem = await getIntersectionElement()));
}).observe(bodyElem, { childList: true }); }).observe(bodyElem, { childList: true });
intersectionObserver.observe((intersectionElement = await getIntersectionElement())); intersectionObserver.observe((intersectionElem = await getIntersectionElement()));
}); });
</script> </script>
<tbody bind:this={bodyElem}> <tbody bind:this={bodyElem}>
<slot /> {@render children()}
</tbody> </tbody>

View File

@ -1,17 +1,20 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, getContext, onDestroy } from 'svelte'; import { getContext, onDestroy, type Snippet } from 'svelte';
import type { Writable } from 'svelte/store'; import type { Writable } from 'svelte/store';
import { ChevronDown, ChevronUp } from 'svelte-heros-v2'; import { ChevronDown, ChevronUp } from 'svelte-heros-v2';
let { children, onSort }: { children: Snippet; onSort: ({ asc }: { asc: boolean }) => void } =
$props();
let id = crypto.randomUUID(); let id = crypto.randomUUID();
let asc = false;
let asc = $state(false);
let { ascHeader } = getContext('sortableTr') as { ascHeader: Writable<null | string> }; let { ascHeader } = getContext('sortableTr') as { ascHeader: Writable<null | string> };
ascHeader.subscribe((v) => { ascHeader.subscribe((v) => {
if (v !== id) asc = false; if (v !== id) asc = false;
}); });
let dispatch = createEventDispatcher();
onDestroy(() => { onDestroy(() => {
if ($ascHeader === id) $ascHeader = null; if ($ascHeader === id) $ascHeader = null;
}); });
@ -20,12 +23,14 @@
<th> <th>
<button <button
class="flex flex-center" class="flex flex-center"
on:click={() => { onclick={() => {
dispatch('sort', { asc: (asc = !asc) }); onSort({ asc: (asc = !asc) });
$ascHeader = id; $ascHeader = id;
}} }}
> >
<span class="mr-1"><slot /></span> <span class="mr-1">
{@render children()}
</span>
{#if $ascHeader === id && asc} {#if $ascHeader === id && asc}
<ChevronUp variation="solid" /> <ChevronUp variation="solid" />
{:else} {:else}

View File

@ -1,12 +1,14 @@
<script lang="ts"> <script lang="ts">
import { setContext } from 'svelte'; import { setContext, type Snippet } from 'svelte';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
let { children, ...restProps }: { children: Snippet; [x: string]: unknown } = $props();
setContext('sortableTr', { setContext('sortableTr', {
ascHeader: writable(null) ascHeader: writable(null)
}); });
</script> </script>
<tr {...$$restProps}> <tr {...restProps}>
<slot /> {@render children()}
</tr> </tr>

View File

@ -3,15 +3,10 @@
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
export let timeout = 2000; let { children, timeout = 2000, show = false } = $props();
export let show = false;
export function reset() { let progressValue = $state(100);
progressValue = 1; let intervalClear: ReturnType<typeof setInterval> | undefined = $state();
}
let progressValue = 100;
let intervalClear: ReturnType<typeof setInterval> | undefined;
function startTimout() { function startTimout() {
intervalClear = setInterval(() => { intervalClear = setInterval(() => {
@ -23,10 +18,11 @@
}, timeout / 100); }, timeout / 100);
} }
$: if (show) { $effect(() => {
if (!show) return;
progressValue = 0; progressValue = 0;
startTimout(); startTimout();
} });
onDestroy(() => clearInterval(intervalClear)); onDestroy(() => clearInterval(intervalClear));
</script> </script>
@ -36,23 +32,23 @@
in:fly={{ x: 0, duration: 200 }} in:fly={{ x: 0, duration: 200 }}
out:fly={{ x: 400, duration: 400 }} out:fly={{ x: 400, duration: 400 }}
class="toast" class="toast"
on:mouseenter={() => { onmouseenter={() => {
clearInterval(intervalClear); clearInterval(intervalClear);
progressValue = 1; progressValue = 1;
}} }}
on:mouseleave={startTimout} onmouseleave={startTimout}
role="alert" role="alert"
> >
<div class="alert alert-error border-none relative text-gray-900 overflow-hidden"> <div class="alert alert-error border-none relative text-gray-900 overflow-hidden">
<div class="flex gap-2 z-10"> <div class="flex gap-2 z-10">
<ExclamationCircle /> <ExclamationCircle />
<slot /> {@render children()}
</div> </div>
<progress <progress
class="progress progress-error absolute bottom-0 h-[3px] w-full bg-[rgba(0,0,0,0.6)]" class="progress progress-error absolute bottom-0 h-[3px] w-full bg-[rgba(0,0,0,0.6)]"
value={progressValue} value={progressValue}
max="100" max="100"
/> ></progress>
</div> </div>
</div> </div>
{/if} {/if}

View File

@ -1,4 +1,4 @@
import { DataTypes, Op } from 'sequelize'; import { DataTypes } from 'sequelize';
import { env } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
import { building, dev } from '$app/environment'; import { building, dev } from '$app/environment';
import * as bcrypt from 'bcrypt'; import * as bcrypt from 'bcrypt';
@ -53,6 +53,8 @@ export class Report extends Model {
declare notice: string; declare notice: string;
@Column({ type: DataTypes.STRING }) @Column({ type: DataTypes.STRING })
declare statement: string; declare statement: string;
@Column({ type: DataTypes.DATE })
declare striked_at: Date | null;
@Column({ type: DataTypes.INTEGER, allowNull: false }) @Column({ type: DataTypes.INTEGER, allowNull: false })
@ForeignKey(() => User) @ForeignKey(() => User)
declare reporter_id: number; declare reporter_id: number;
@ -66,17 +68,26 @@ export class Report extends Model {
@ForeignKey(() => StrikeReason) @ForeignKey(() => StrikeReason)
declare strike_reason_id: number | null; declare strike_reason_id: number | null;
@BelongsTo(() => User, 'reporter_id') @BelongsTo(() => User, {
onDelete: 'CASCADE',
foreignKey: 'reporter_id'
})
declare reporter: User; declare reporter: User;
@BelongsTo(() => User, 'reported_id') @BelongsTo(() => User, {
onDelete: 'CASCADE',
foreignKey: 'reported_id'
})
declare reported: User; declare reported: User;
@BelongsTo(() => Admin, 'auditor_id') @BelongsTo(() => Admin, {
onDelete: 'CASCADE',
foreignKey: 'auditor_id'
})
declare auditor: Admin; declare auditor: Admin;
@BelongsTo(() => StrikeReason, 'strike_reason_id') @BelongsTo(() => StrikeReason, {
onDelete: 'CASCADE',
foreignKey: 'strike_reason_id'
})
declare strike_reason: StrikeReason; declare strike_reason: StrikeReason;
@Column({ type: DataTypes.DATE })
declare striked_at: Date | null;
} }
@Table({ modelName: 'strike_reason', underscored: true, createdAt: false, updatedAt: false }) @Table({ modelName: 'strike_reason', underscored: true, createdAt: false, updatedAt: false })
@ -110,7 +121,10 @@ export class Feedback extends Model {
@ForeignKey(() => User) @ForeignKey(() => User)
declare user_id: number; declare user_id: number;
@BelongsTo(() => User, 'user_id') @BelongsTo(() => User, {
onDelete: 'CASCADE',
foreignKey: 'user_id'
})
declare user: User; declare user: User;
} }
@ -153,7 +167,6 @@ export class Settings extends Model {
@Column({ @Column({
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get(this: Settings): any { get(this: Settings): any {
const value = this.getDataValue('value'); const value = this.getDataValue('value');
return value != null ? JSON.parse(value) : null; return value != null ? JSON.parse(value) : null;

View File

@ -4,7 +4,9 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
let navPaths = [ let { children } = $props();
let navPaths = $state([
{ {
name: 'Startseite', name: 'Startseite',
sprite: `${env.PUBLIC_BASE_PATH}/img/menu-home.png`, sprite: `${env.PUBLIC_BASE_PATH}/img/menu-home.png`,
@ -41,24 +43,24 @@
href: `${env.PUBLIC_BASE_PATH}/team`, href: `${env.PUBLIC_BASE_PATH}/team`,
active: false active: false
} }
]; ]);
let showMenuPermanent = $state(false);
let onAdminPage = $state(false);
let isTouch = $state(false);
let windowHeight = $state(0);
let showMenuPermanent = false; $effect(() => {
onAdminPage =
let onAdminPage = false; $page.url.pathname.startsWith(`${env.PUBLIC_BASE_PATH}/admin`) &&
$: onAdminPage = $page.url.pathname !== `${env.PUBLIC_BASE_PATH}/admin/login`;
$page.url.pathname.startsWith(`${env.PUBLIC_BASE_PATH}/admin`) && });
$page.url.pathname !== `${env.PUBLIC_BASE_PATH}/admin/login`; $effect(() => {
$: {
for (let i = 0; i < navPaths.length; i++) { for (let i = 0; i < navPaths.length; i++) {
navPaths[i].active = navPaths[i].href === $page.url.pathname; navPaths[i].active = navPaths[i].href === $page.url.pathname;
} }
} });
let isTouch = false; let navElem: HTMLDivElement;
let nav: HTMLDivElement;
$: windowHeight = 0;
</script> </script>
<svelte:window bind:innerHeight={windowHeight} /> <svelte:window bind:innerHeight={windowHeight} />
@ -67,7 +69,7 @@
on:touchend={(e) => { on:touchend={(e) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
if (isTouch && !nav.contains(e.target)) showMenuPermanent = false; if (isTouch && !navElem.contains(e.target)) showMenuPermanent = false;
}} }}
/> />
@ -81,20 +83,20 @@
<main> <main>
<div class="min-h-[calc(100vh-3.5rem)] h-full w-full" class:min-h-screen={onAdminPage}> <div class="min-h-[calc(100vh-3.5rem)] h-full w-full" class:min-h-screen={onAdminPage}>
<slot /> {@render children()}
</div> </div>
</main> </main>
<nav> <nav>
<div <div
class="fixed bottom-4 right-4 sm:left-4 sm:right-[initial] group/menu-bar flex flex-col-reverse justify-center items-center z-50 main-menu" class="fixed bottom-4 right-4 sm:left-4 sm:right-[initial] group/menu-bar flex flex-col-reverse justify-center items-center z-50 main-menu"
class:hidden={onAdminPage} class:hidden={onAdminPage}
bind:this={nav} bind:this={navElem}
> >
<button <button
class={isTouch class={isTouch
? 'btn btn-square relative w-16 h-16' ? 'btn btn-square relative w-16 h-16'
: 'btn btn-square group/menu-button relative w-16 h-16'} : 'btn btn-square group/menu-button relative w-16 h-16'}
on:click={() => { onclick={() => {
if (!isTouch) { if (!isTouch) {
let activePath = navPaths.find((path) => path.active); let activePath = navPaths.find((path) => path.active);
if (activePath !== undefined) { if (activePath !== undefined) {
@ -103,7 +105,7 @@
showMenuPermanent = !showMenuPermanent; showMenuPermanent = !showMenuPermanent;
} }
}} }}
on:touchend={() => { ontouchend={() => {
isTouch = true; isTouch = true;
showMenuPermanent = !showMenuPermanent; showMenuPermanent = !showMenuPermanent;
}} }}
@ -137,13 +139,13 @@
<a <a
class="btn btn-square border-none group/menu-item relative w-[3.5rem] h-[3.5rem] flex justify-center items-center" class="btn btn-square border-none group/menu-item relative w-[3.5rem] h-[3.5rem] flex justify-center items-center"
href={navPath.href} href={navPath.href}
on:click={() => goto(navPath.href)} onclick={() => goto(navPath.href)}
> >
<div <div
style="background-image: url('{env.PUBLIC_BASE_PATH}/img/menu-inventory-bar.png'); background-position: -{i * style="background-image: url('{env.PUBLIC_BASE_PATH}/img/menu-inventory-bar.png'); background-position: -{i *
3.5}rem 0;" 3.5}rem 0;"
class="block w-full h-full bg-no-repeat bg-horizontal-sprite pixelated" class="block w-full h-full bg-no-repeat bg-horizontal-sprite pixelated"
/> ></div>
<div class="absolute flex justify-center items-center w-full h-full"> <div class="absolute flex justify-center items-center w-full h-full">
<img class="w-1/2 h-1/2 pixelated" src={navPath.sprite} alt={navPath.name} /> <img class="w-1/2 h-1/2 pixelated" src={navPath.sprite} alt={navPath.name} />
</div> </div>

View File

@ -4,7 +4,8 @@
import { Clock, User, WrenchScrewdriver } from 'svelte-heros-v2'; import { Clock, User, WrenchScrewdriver } from 'svelte-heros-v2';
import Crosshairs from '$lib/components/CustomIcons/Crosshairs.svelte'; import Crosshairs from '$lib/components/CustomIcons/Crosshairs.svelte';
import Skull from '$lib/components/CustomIcons/Skull.svelte'; import Skull from '$lib/components/CustomIcons/Skull.svelte';
import type { PageData } from './$types';
let { data } = $props();
let information = [ let information = [
{ {
@ -23,8 +24,6 @@
'Jeder ist willkommen und kann mitspielen. Dazu benötigst Du nur einen Minecraft-Account und schon bist Du Teil unser Community :)' 'Jeder ist willkommen und kann mitspielen. Dazu benötigst Du nur einen Minecraft-Account und schon bist Du Teil unser Community :)'
} }
]; ];
export let data: PageData;
</script> </script>
<svelte:head> <svelte:head>
@ -39,7 +38,7 @@
<img src="{env.PUBLIC_BASE_PATH}/img/craftattack.webp" alt="Craftattack 7" /> <img src="{env.PUBLIC_BASE_PATH}/img/craftattack.webp" alt="Craftattack 7" />
<div class="flex flex-col gap-5 lg:gap-14 w-full mt-2 lg:mt-5 lg:w-10/12 h-full"> <div class="flex flex-col gap-5 lg:gap-14 w-full mt-2 lg:mt-5 lg:w-10/12 h-full">
<div> <div>
<div class="divider" /> <div class="divider"></div>
<div class="flex flex-col md:flex-row xl:flex-col gap-5"> <div class="flex flex-col md:flex-row xl:flex-col gap-5">
{#each information as info} {#each information as info}
<div> <div>
@ -48,7 +47,7 @@
</div> </div>
{/each} {/each}
</div> </div>
<div class="divider" /> <div class="divider"></div>
</div> </div>
<div class="flex justify-center"> <div class="flex justify-center">
<a <a

View File

@ -11,10 +11,11 @@
} from 'svelte-heros-v2'; } from 'svelte-heros-v2';
import { buttonTriggeredRequest } from '$lib/components/utils'; import { buttonTriggeredRequest } from '$lib/components/utils';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import type { LayoutData } from './$types';
import { adminCount, errorMessage, reportCount, feedbackCount } from '$lib/stores'; import { adminCount, errorMessage, reportCount, feedbackCount } from '$lib/stores';
import ErrorToast from '$lib/components/Toast/ErrorToast.svelte'; import ErrorToast from '$lib/components/Toast/ErrorToast.svelte';
let { children, data } = $props();
async function logout() { async function logout() {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/logout`, { const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/logout`, {
method: 'POST' method: 'POST'
@ -26,7 +27,6 @@
} }
} }
export let data: LayoutData;
if (data.reportCount) $reportCount = data.reportCount; if (data.reportCount) $reportCount = data.reportCount;
if (data.feedbackCount) $feedbackCount = data.feedbackCount; if (data.feedbackCount) $feedbackCount = data.feedbackCount;
if (data.adminCount) $adminCount = data.adminCount; if (data.adminCount) $adminCount = data.adminCount;
@ -85,9 +85,10 @@
<ul class="menu menu-horizontal h-10 p-0 flex items-center bg-base-300 rounded-lg"> <ul class="menu menu-horizontal h-10 p-0 flex items-center bg-base-300 rounded-lg">
{#each tabs as tab} {#each tabs as tab}
{#if tab.enabled} {#if tab.enabled}
{@const Icon = tab.icon}
<li> <li>
<a href={tab.path}> <a href={tab.path}>
<svelte:component this={tab.icon} /> <Icon />
<span class="mr-1" class:underline={$page.url.pathname === tab.path}>{tab.name}</span> <span class="mr-1" class:underline={$page.url.pathname === tab.path}>{tab.name}</span>
{#if tab.badge != null} {#if tab.badge != null}
<div class="badge">{tab.badge}</div> <div class="badge">{tab.badge}</div>
@ -100,7 +101,7 @@
<div class="absolute top-0 right-0 flex items-center h-full"> <div class="absolute top-0 right-0 flex items-center h-full">
<ul class="menu menu-vertical"> <ul class="menu menu-vertical">
<li> <li>
<button on:click={(e) => buttonTriggeredRequest(e, logout())}> <button onclick={(e) => buttonTriggeredRequest(e, logout())}>
<ArrowLeftOnRectangle /> <ArrowLeftOnRectangle />
<span>Ausloggen</span> <span>Ausloggen</span>
</button> </button>
@ -109,11 +110,11 @@
</div> </div>
</div> </div>
<div class="h-full w-full -mt-12 pt-12 overflow-y-scroll overflow-x-hidden"> <div class="h-full w-full -mt-12 pt-12 overflow-y-scroll overflow-x-hidden">
<slot /> {@render children()}
</div> </div>
{:else} {:else}
<div class="h-full w-full"> <div class="h-full w-full">
<slot /> {@render children()}
</div> </div>
{/if} {/if}

View File

@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import { BookOpen, Cog6Tooth, Flag, UserGroup, Users } from 'svelte-heros-v2'; import { BookOpen, Cog6Tooth, Flag, UserGroup, Users } from 'svelte-heros-v2';
export let data: PageData; let { data } = $props();
let tabs = [ let tabs = [
{ {
@ -42,13 +41,14 @@
<div class="flex justify-around items-center h-screen"> <div class="flex justify-around items-center h-screen">
{#each tabs as tab} {#each tabs as tab}
{#if tab.enabled} {#if tab.enabled}
{@const Icon = tab.icon}
<div class="flex flex-col gap-4 justify-center items-center"> <div class="flex flex-col gap-4 justify-center items-center">
<a <a
class="h-48 w-48 border flex justify-center items-center rounded-xl duration-100 hover:bg-base-200" class="h-48 w-48 border flex justify-center items-center rounded-xl duration-100 hover:bg-base-200"
href={tab.path} href={tab.path}
title={tab.name} title={tab.name}
> >
<svelte:component this={tab.icon} width="10rem" height="10rem" /> <Icon />
</a> </a>
<span>{tab.name}</span> <span>{tab.name}</span>
</div> </div>

View File

@ -1,16 +1,18 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types';
import Badges from '$lib/components/Input/Badges.svelte'; import Badges from '$lib/components/Input/Badges.svelte';
import { Check, NoSymbol, PencilSquare, Trash, UserPlus } from 'svelte-heros-v2'; import { Check, NoSymbol, PencilSquare, Trash, UserPlus } from 'svelte-heros-v2';
import Input from '$lib/components/Input/Input.svelte'; import Input from '$lib/components/Input/Input.svelte';
import { Permissions } from '$lib/permissions'; import { Permissions } from '$lib/permissions';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import ErrorToast from '$lib/components/Toast/ErrorToast.svelte';
import { buttonTriggeredRequest } from '$lib/components/utils'; import { buttonTriggeredRequest } from '$lib/components/utils';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { adminCount } from '$lib/stores'; import { adminCount } from '$lib/stores';
let { data } = $props();
let admins = $state(data.admins);
let allPermissionBadges = { let allPermissionBadges = {
Admin: Permissions.Admin, Admin: Permissions.Admin,
Users: Permissions.Users, Users: Permissions.Users,
@ -19,9 +21,9 @@
Settings: Permissions.Settings Settings: Permissions.Settings
}; };
let newAdminUsername: string; let newAdminUsername = $state('');
let newAdminPassword: string; let newAdminPassword = $state('');
let newAdminPermissions: number[]; let newAdminPermissions = $state([]);
async function addAdmin(username: string, password: string, permissions: Permissions) { async function addAdmin(username: string, password: string, permissions: Permissions) {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/admin`, { const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/admin`, {
@ -36,8 +38,11 @@
let res = await response.json(); let res = await response.json();
$adminCount += 1; $adminCount += 1;
res.permissions = new Permissions(res.permissions).asArray(); res.permissions = new Permissions(res.permissions).asArray();
data.admins.push(res); admins.push(res);
data.admins = data.admins;
newAdminUsername = '';
newAdminPassword = '';
newAdminPermissions = [];
} else { } else {
throw new Error(); throw new Error();
} }
@ -79,35 +84,32 @@
await goto(`${env.PUBLIC_BASE_PATH}/`); await goto(`${env.PUBLIC_BASE_PATH}/`);
} else { } else {
$adminCount -= 1; $adminCount -= 1;
data.admins.splice( admins.splice(
data.admins.findIndex((v) => v.id == id), admins.findIndex((v) => v.id == id),
1 1
); );
data.admins = data.admins; admins = admins;
} }
} else { } else {
throw new Error(); throw new Error();
} }
} }
let errorMessage = ''; let permissions = $state(new Permissions(data.permissions));
export let data: PageData;
let permissions = new Permissions(data.permissions);
</script> </script>
<table class="table table-zebra w-full"> <table class="table table-zebra w-full">
<thead> <thead>
<tr> <tr>
<th /> <th></th>
<th>Benutzername</th> <th>Benutzername</th>
<th>Passwort</th> <th>Passwort</th>
<th>Berechtigungen</th> <th>Berechtigungen</th>
<th /> <th></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each data.admins as admin, i} {#each admins as admin, i}
<tr> <tr>
<td>{i + 1}</td> <td>{i + 1}</td>
<td><Input type="text" bind:value={admin.username} disabled={!admin.edit} size="sm" /></td> <td><Input type="text" bind:value={admin.username} disabled={!admin.edit} size="sm" /></td>
@ -133,7 +135,7 @@
<span class="w-min" class:cursor-not-allowed={!permissions.admin()}> <span class="w-min" class:cursor-not-allowed={!permissions.admin()}>
<button <button
class="btn btn-sm btn-square" class="btn btn-sm btn-square"
on:click={async (e) => { onclick={async (e) => {
await buttonTriggeredRequest( await buttonTriggeredRequest(
e, e,
updateAdmin( updateAdmin(
@ -153,9 +155,9 @@
<span class="w-min" class:cursor-not-allowed={!permissions.admin()}> <span class="w-min" class:cursor-not-allowed={!permissions.admin()}>
<button <button
class="btn btn-sm btn-square" class="btn btn-sm btn-square"
on:click={() => { onclick={() => {
admin.edit = false; admin.edit = false;
admin = admin.before; admins[i] = admin.before;
}} }}
> >
<NoSymbol size="18" /> <NoSymbol size="18" />
@ -165,9 +167,9 @@
<span class="w-min" class:cursor-not-allowed={!permissions.admin()}> <span class="w-min" class:cursor-not-allowed={!permissions.admin()}>
<button <button
class="btn btn-sm btn-square" class="btn btn-sm btn-square"
on:click={() => { onclick={() => {
admin.before = $state.snapshot(admin);
admin.edit = true; admin.edit = true;
admin.before = structuredClone(admin);
}} }}
> >
<PencilSquare size="18" /> <PencilSquare size="18" />
@ -176,7 +178,7 @@
<span class="w-min" class:cursor-not-allowed={!permissions.admin()}> <span class="w-min" class:cursor-not-allowed={!permissions.admin()}>
<button <button
class="btn btn-sm btn-square" class="btn btn-sm btn-square"
on:click={(e) => buttonTriggeredRequest(e, deleteAdmin(admin.id))} onclick={(e) => buttonTriggeredRequest(e, deleteAdmin(admin.id))}
> >
<Trash size="18" /> <Trash size="18" />
</button> </button>
@ -187,7 +189,7 @@
</tr> </tr>
{/each} {/each}
<tr> <tr>
<td>{data.admins.length + 1}</td> <td>{admins.length + 1}</td>
<td><Input type="text" bind:value={newAdminUsername} size="sm" /></td> <td><Input type="text" bind:value={newAdminUsername} size="sm" /></td>
<td><Input type="password" bind:value={newAdminPassword} size="sm" /></td> <td><Input type="password" bind:value={newAdminPassword} size="sm" /></td>
<td><Badges bind:value={newAdminPermissions} available={allPermissionBadges} /></td> <td><Badges bind:value={newAdminPermissions} available={allPermissionBadges} /></td>
@ -196,7 +198,7 @@
<button <button
class="btn btn-sm btn-square" class="btn btn-sm btn-square"
disabled={!newAdminUsername || !newAdminPassword} disabled={!newAdminUsername || !newAdminPassword}
on:click={async (e) => { onclick={async (e) => {
await buttonTriggeredRequest( await buttonTriggeredRequest(
e, e,
addAdmin(newAdminUsername, newAdminPassword, new Permissions(newAdminPermissions)) addAdmin(newAdminUsername, newAdminPassword, new Permissions(newAdminPermissions))
@ -213,7 +215,3 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<ErrorToast show={errorMessage !== ''}>
<span />
</ErrorToast>

View File

@ -11,10 +11,10 @@
import Textarea from '$lib/components/Input/Textarea.svelte'; import Textarea from '$lib/components/Input/Textarea.svelte';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
let feedbacks: (typeof Feedback.prototype.dataValues)[] = []; let feedbacks: (typeof Feedback.prototype.dataValues)[] = $state([]);
let feedbacksPerRequest = 25; let feedbacksPerRequest = 25;
let feedbackFilter = { event: null, content: null, username: null }; let feedbackFilter = $state({ event: null, content: null, username: null });
let activeFeedback: typeof Feedback.prototype.dataValues | null = null; let activeFeedback: typeof Feedback.prototype.dataValues | null = $state(null);
async function fetchFeedback(extendedFilter?: { async function fetchFeedback(extendedFilter?: {
limit?: number; limit?: number;
@ -57,13 +57,14 @@
onDestroy(() => { onDestroy(() => {
if (browser) window.removeEventListener('hashchange', openHashReport); if (browser) window.removeEventListener('hashchange', openHashReport);
}); });
$: if (feedbackFilter) fetchFeedback({ from: 0 }).then((r) => (feedbacks = r));
</script> </script>
<div class="h-full flex flex-row"> <div class="h-full flex flex-row">
<div class="w-full flex flex-col overflow-hidden"> <div class="w-full flex flex-col overflow-hidden">
<HeaderBar bind:feedbackFilter /> <HeaderBar
bind:feedbackFilter
onUpdate={() => fetchFeedback({ from: 0 }).then((r) => (feedbacks = r))}
/>
<hr class="divider my-1 mx-8 border-none" /> <hr class="divider my-1 mx-8 border-none" />
<table class="table table-fixed h-fit"> <table class="table table-fixed h-fit">
<thead> <thead>
@ -82,7 +83,7 @@
<tr <tr
class="hover [&>*]:text-sm cursor-pointer" class="hover [&>*]:text-sm cursor-pointer"
class:bg-base-200={activeFeedback?.url_hash === feedback.url_hash} class:bg-base-200={activeFeedback?.url_hash === feedback.url_hash}
on:click={async () => { onclick={async () => {
await goto(`${window.location.href.split('#')[0]}#${feedback.url_hash}`, { await goto(`${window.location.href.split('#')[0]}#${feedback.url_hash}`, {
replaceState: true replaceState: true
}); });
@ -96,8 +97,10 @@
<button <button
class="pl-1" class="pl-1"
title="Nach Ersteller filtern" title="Nach Ersteller filtern"
on:click|stopPropagation={() => onclick={(e) => {
(feedbackFilter.username = feedback.user?.username)} e.stopPropagation();
feedbackFilter.username = feedback.user.username;
}}
> >
<MagnifyingGlass size="14" /> <MagnifyingGlass size="14" />
</button> </button>
@ -125,18 +128,18 @@
> >
<div class="absolute right-2 top-2 flex justify-center"> <div class="absolute right-2 top-2 flex justify-center">
<form class="dropdown dropdown-end"> <form class="dropdown dropdown-end">
<!-- svelte-ignore a11y-no-noninteractive-tabindex a11y-label-has-associated-control --> <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<label tabindex="0" class="btn btn-sm btn-circle btn-ghost text-center"> <label tabindex="0" class="btn btn-sm btn-circle btn-ghost text-center">
<Share size="1rem" /> <Share size="1rem" />
</label> </label>
<!-- svelte-ignore a11y-no-noninteractive-tabindex --> <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<ul <ul
tabindex="0" tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-max" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-max"
> >
<li> <li>
<button <button
on:click={() => { onclick={() => {
navigator.clipboard.writeText( navigator.clipboard.writeText(
`${window.location.protocol}//${window.location.host}${env.PUBLIC_BASE_PATH}/admin/reports#${activeFeedback.url_hash}` `${window.location.protocol}//${window.location.host}${env.PUBLIC_BASE_PATH}/admin/reports#${activeFeedback.url_hash}`
); );
@ -145,7 +148,7 @@
Internen Link kopieren Internen Link kopieren
</button> </button>
<button <button
on:click={() => onclick={() =>
navigator.clipboard.writeText( navigator.clipboard.writeText(
`${window.location.protocol}//${window.location.host}${env.PUBLIC_BASE_PATH}/report/${activeFeedback.url_hash}` `${window.location.protocol}//${window.location.host}${env.PUBLIC_BASE_PATH}/report/${activeFeedback.url_hash}`
)}>Öffentlichen Link kopieren</button )}>Öffentlichen Link kopieren</button
@ -155,7 +158,7 @@
</form> </form>
<button <button
class="btn btn-sm btn-circle btn-ghost" class="btn btn-sm btn-circle btn-ghost"
on:click={() => { onclick={() => {
activeFeedback = null; activeFeedback = null;
goto(window.location.href.split('#')[0], { replaceState: true }); goto(window.location.href.split('#')[0], { replaceState: true });
}}>✕</button }}>✕</button
@ -169,7 +172,9 @@
value={activeFeedback.user?.username || ''} value={activeFeedback.user?.username || ''}
pickyWidth={false} pickyWidth={false}
> >
<span slot="label">Nutzer</span> {#snippet label()}
<span>Nutzer</span>
{/snippet}
</Input> </Input>
<Textarea readonly={true} rows={4} label="Inhalt" value={activeFeedback.content} /> <Textarea readonly={true} rows={4} label="Inhalt" value={activeFeedback.content} />
</div> </div>

View File

@ -20,7 +20,7 @@ export const POST = (async ({ request, cookies }) => {
} }
const data = parseResult.data; const data = parseResult.data;
let feedbackFindOptions: Attributes<Feedback> = { const feedbackFindOptions: Attributes<Feedback> = {
content: { [Op.not]: null } content: { [Op.not]: null }
}; };
if (data.event) Object.assign(feedbackFindOptions, { event: { [Op.like]: `%${data.event}%` } }); if (data.event) Object.assign(feedbackFindOptions, { event: { [Op.like]: `%${data.event}%` } });
@ -35,7 +35,7 @@ export const POST = (async ({ request, cookies }) => {
}); });
if (data.hash) Object.assign(feedbackFindOptions, { url_hash: data.hash }); if (data.hash) Object.assign(feedbackFindOptions, { url_hash: data.hash });
let feedback = await Feedback.findAll({ const feedback = await Feedback.findAll({
where: feedbackFindOptions, where: feedbackFindOptions,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore

View File

@ -1,21 +1,26 @@
<script lang="ts"> <script lang="ts">
import Input from '$lib/components/Input/Input.svelte'; import Input from '$lib/components/Input/Input.svelte';
export let feedbackFilter: { [k: string]: any } = { let {
event: null, feedbackFilter = $bindable({ event: null, content: null, username: null }),
content: null, onUpdate
username: null }: { feedbackFilter: { [k: string]: any }; onUpdate: () => void } = $props();
};
</script> </script>
<form class="flex flex-row justify-center space-x-4 mx-4 my-2"> <form class="flex flex-row justify-center space-x-4 mx-4 my-2">
<Input size="sm" placeholder="Alle" bind:value={feedbackFilter.username}> <Input size="sm" placeholder="Alle" bind:value={feedbackFilter.username} oninput={onUpdate}>
<span slot="label">Nutzer</span> {#snippet label()}
<span>Nutzer</span>
{/snippet}
</Input> </Input>
<Input size="sm" placeholder="Alle" bind:value={feedbackFilter.event}> <Input size="sm" placeholder="Alle" bind:value={feedbackFilter.event} oninput={onUpdate}>
<span slot="label">Event</span> {#snippet label()}
<span>Event</span>
{/snippet}
</Input> </Input>
<Input size="sm" placeholder="Alle" bind:value={feedbackFilter.content}> <Input size="sm" placeholder="Alle" bind:value={feedbackFilter.content} oninput={onUpdate}>
<span slot="label">Inhalt</span> {#snippet label()}
<span>Inhalt</span>
{/snippet}
</Input> </Input>
</form> </form>

View File

@ -3,7 +3,8 @@
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import { errorMessage } from '$lib/stores'; import { errorMessage } from '$lib/stores';
let passwordValue: string; let password = $state('');
async function login() { async function login() {
// eslint-disable-next-line no-async-promise-executor // eslint-disable-next-line no-async-promise-executor
loginRequest = new Promise(async (resolve) => { loginRequest = new Promise(async (resolve) => {
@ -12,10 +13,10 @@
body: JSON.stringify(Object.fromEntries(new FormData(document.forms[0]))) body: JSON.stringify(Object.fromEntries(new FormData(document.forms[0])))
}); });
if (response.ok) { if (response.ok) {
window.location = `${env.PUBLIC_BASE_PATH}/admin`; window.location.href = `${env.PUBLIC_BASE_PATH}/admin`;
resolve(); resolve();
} else if (response.status == 401) { } else if (response.status == 401) {
passwordValue = ''; password = '';
$errorMessage = 'Nutzername oder Passwort falsch'; $errorMessage = 'Nutzername oder Passwort falsch';
resolve(); resolve();
} else { } else {
@ -25,25 +26,29 @@
}); });
} }
let loginRequest: Promise<void> | null = null; let loginRequest: Promise<void> | null = $state(null);
</script> </script>
<div class="card px-14 py-6 shadow-lg"> <div class="card px-14 py-6 shadow-lg">
<h1 class="text-center text-4xl mt-2 mb-4">Craftattack Admin Login</h1> <h1 class="text-center text-4xl mt-2 mb-4">Craftattack Admin Login</h1>
<form class="flex flex-col items-center" on:submit|preventDefault={login}> <form
class="flex flex-col items-center"
onsubmit={(e) => {
e.preventDefault();
login();
}}
>
<div class="flex flex-col justify-center items-center"> <div class="flex flex-col justify-center items-center">
<div class="grid gap-4"> <div class="grid gap-4">
<Input id="username" name="username" type="text" required={true}> <Input id="username" name="username" type="text" required={true}>
<span slot="label">Nutzername</span> {#snippet label()}
<span>Nutzername</span>
{/snippet}
</Input> </Input>
<Input <Input id="password" name="password" type="password" required={true} bind:value={password}>
id="password" {#snippet label()}
name="password" <span>Passwort</span>
type="password" {/snippet}
required={true}
bind:value={passwordValue}
>
<span slot="label">Passwort</span>
</Input> </Input>
</div> </div>
</div> </div>
@ -56,22 +61,7 @@
{#await loginRequest} {#await loginRequest}
<span <span
class="relative top-[calc(50%-12px)] left-[calc(50%-12px)] row-[1] col-[1] loading loading-ring" class="relative top-[calc(50%-12px)] left-[calc(50%-12px)] row-[1] col-[1] loading loading-ring"
/> ></span>
{:catch error}
<dialog
class="modal"
on:close={() => setTimeout(() => (loginRequest = null), 200)}
open
>
<form method="dialog" class="modal-box">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
<h3 class="font-bold text-lg">Error</h3>
<p class="py-4">{error.message}</p>
</form>
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.2)]">
<button>close</button>
</form>
</dialog>
{/await} {/await}
{/if} {/if}
{/key} {/key}

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import type { PageData } from './$types';
import type { Report } from '$lib/server/database'; import type { Report } from '$lib/server/database';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
@ -18,12 +17,12 @@
import { usernameSuggestions } from '$lib/utils'; import { usernameSuggestions } from '$lib/utils';
import PaginationTableBody from '$lib/components/PaginationTable/PaginationTableBody.svelte'; import PaginationTableBody from '$lib/components/PaginationTable/PaginationTableBody.svelte';
export let data: PageData; let { data } = $props();
let reports: (typeof Report.prototype.dataValues)[] = []; let reports: (typeof Report.prototype.dataValues)[] = $state([]);
let reportsPerRequest = 25; let reportsPerRequest = 25;
let reportFilter = { draft: false, status: null, reporter: null, reported: null }; let reportFilter = $state({ draft: false, status: null, reporter: null, reported: null });
let activeReport: typeof Report.prototype.dataValues | null = null; let activeReport: typeof Report.prototype.dataValues | null = $state(null);
async function fetchReports(extendedFilter?: { async function fetchReports(extendedFilter?: {
hash?: string; hash?: string;
@ -94,19 +93,20 @@
let saveActiveReportChangesModal: HTMLDialogElement; let saveActiveReportChangesModal: HTMLDialogElement;
let newReportModal: HTMLDialogElement; let newReportModal: HTMLDialogElement;
$: if (reportFilter) fetchReports({ from: 0 }).then((r) => (reports = r.reports));
</script> </script>
<div class="h-full flex flex-row"> <div class="h-full flex flex-row">
<div class="w-full flex flex-col overflow-scroll"> <div class="w-full flex flex-col overflow-scroll">
<div class="grid grid-cols-[5fr_1fr_10fr_1fr_5fr]"> <div class="grid grid-cols-[5fr_1fr_10fr_1fr_5fr]">
<div /> <div></div>
<div /> <div></div>
<HeaderBar bind:reportFilter /> <HeaderBar
<div class="divider divider-horizontal my-auto h-3/4" /> bind:reportFilter
onUpdate={() => fetchReports({ from: 0 }).then((r) => (reports = r.reports))}
/>
<div class="divider divider-horizontal my-auto h-3/4"></div>
<div class="flex items-center"> <div class="flex items-center">
<button class="btn" on:click={() => newReportModal.show()}> <button class="btn" onclick={() => newReportModal.show()}>
<Plus /> <Plus />
<span>Neuer Report</span> <span>Neuer Report</span>
</button> </button>
@ -140,7 +140,7 @@
<tr <tr
class="hover [&>*]:text-sm cursor-pointer" class="hover [&>*]:text-sm cursor-pointer"
class:bg-base-200={activeReport?.url_hash === report.url_hash} class:bg-base-200={activeReport?.url_hash === report.url_hash}
on:click={() => { onclick={() => {
goto(`${window.location.href.split('#')[0]}#${report.url_hash}`, { goto(`${window.location.href.split('#')[0]}#${report.url_hash}`, {
replaceState: true replaceState: true
}); });
@ -154,7 +154,10 @@
<button <button
class="pl-1" class="pl-1"
title="Nach Ersteller filtern" title="Nach Ersteller filtern"
on:click|stopPropagation={() => (reportFilter.reporter = report.reporter.username)} onclick={(e) => {
e.stopPropagation();
reportFilter.reporter = report.reporter.username;
}}
> >
<MagnifyingGlass size="14" /> <MagnifyingGlass size="14" />
</button> </button>
@ -165,8 +168,10 @@
<button <button
class="pl-1" class="pl-1"
title="Nach Reportetem Spieler filtern" title="Nach Reportetem Spieler filtern"
on:click|stopPropagation={() => onclick={(e) => {
(reportFilter.reported = report.reported.username)} e.stopPropagation();
reportFilter.reported = report.reported.username;
}}
> >
<MagnifyingGlass size="14" /> <MagnifyingGlass size="14" />
</button> </button>
@ -203,18 +208,18 @@
> >
<div class="absolute right-2 top-2 flex justify-center"> <div class="absolute right-2 top-2 flex justify-center">
<form class="dropdown dropdown-end"> <form class="dropdown dropdown-end">
<!-- svelte-ignore a11y-no-noninteractive-tabindex a11y-label-has-associated-control --> <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<label tabindex="0" class="btn btn-sm btn-circle btn-ghost text-center"> <label tabindex="0" class="btn btn-sm btn-circle btn-ghost text-center">
<Share size="1rem" /> <Share size="1rem" />
</label> </label>
<!-- svelte-ignore a11y-no-noninteractive-tabindex --> <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<ul <ul
tabindex="0" tabindex="0"
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-max" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-max"
> >
<li> <li>
<button <button
on:click={() => { onclick={() => {
navigator.clipboard.writeText( navigator.clipboard.writeText(
`${window.location.protocol}//${window.location.host}${env.PUBLIC_BASE_PATH}/admin/reports#${activeReport.url_hash}` `${window.location.protocol}//${window.location.host}${env.PUBLIC_BASE_PATH}/admin/reports#${activeReport.url_hash}`
); );
@ -223,7 +228,7 @@
Internen Link kopieren Internen Link kopieren
</button> </button>
<button <button
on:click={() => onclick={() =>
navigator.clipboard.writeText( navigator.clipboard.writeText(
`${window.location.protocol}//${window.location.host}${env.PUBLIC_BASE_PATH}/report/${activeReport.url_hash}` `${window.location.protocol}//${window.location.host}${env.PUBLIC_BASE_PATH}/report/${activeReport.url_hash}`
)}>Öffentlichen Link kopieren</button )}>Öffentlichen Link kopieren</button
@ -233,7 +238,7 @@
</form> </form>
<button <button
class="btn btn-sm btn-circle btn-ghost" class="btn btn-sm btn-circle btn-ghost"
on:click={() => { onclick={() => {
activeReport = null; activeReport = null;
goto(window.location.href.split('#')[0], { replaceState: true }); goto(window.location.href.split('#')[0], { replaceState: true });
}}>✕</button }}>✕</button
@ -242,7 +247,9 @@
<h3 class="font-roboto font-semibold text-2xl mb-2">Report</h3> <h3 class="font-roboto font-semibold text-2xl mb-2">Report</h3>
<div class="w-full"> <div class="w-full">
<Input readonly={true} size="sm" value={activeReport.reporter.username} pickyWidth={false}> <Input readonly={true} size="sm" value={activeReport.reporter.username} pickyWidth={false}>
<span slot="label">Reporter</span> {#snippet label()}
<span>Reporter</span>
{/snippet}
</Input> </Input>
<Search <Search
size="sm" size="sm"
@ -252,17 +259,17 @@
invalidMessage="Es können nur registrierte Spieler reportet werden" invalidMessage="Es können nur registrierte Spieler reportet werden"
label="Reporteter User" label="Reporteter User"
inputValue={activeReport.reported?.username || ''} inputValue={activeReport.reported?.username || ''}
on:submit={(e) => onsubmit={(e) =>
(activeReport.reported = { (activeReport.reported = {
...activeReport.reported, ...activeReport.reported,
username: e.detail.input, username: e.input,
uuid: e.detail.value uuid: e.value
})} })}
/> />
<Textarea readonly={true} rows={1} label="Report Grund" value={activeReport.subject} /> <Textarea readonly={true} rows={1} label="Report Grund" value={activeReport.subject} />
<Textarea readonly={true} rows={4} label="Report Details" value={activeReport.body} /> <Textarea readonly={true} rows={4} label="Report Details" value={activeReport.body} />
</div> </div>
<div class="divider mx-4" /> <div class="divider mx-4"></div>
<div> <div>
<div <div
class="w-full" class="w-full"
@ -318,7 +325,7 @@
<Input <Input
type="submit" type="submit"
value="Speichern" value="Speichern"
on:click={() => saveActiveReportChangesModal.show()} onclick={() => saveActiveReportChangesModal.show()}
/> />
</div> </div>
</div> </div>
@ -333,7 +340,7 @@
<Input <Input
type="submit" type="submit"
value="Speichern" value="Speichern"
on:click={async () => { onclick={async () => {
await updateActiveReport(); await updateActiveReport();
if (activeReport.reported?.username) { if (activeReport.reported?.username) {
if (activeReport.reported?.id === undefined) { if (activeReport.reported?.id === undefined) {
@ -363,9 +370,9 @@
<dialog class="modal" bind:this={newReportModal}> <dialog class="modal" bind:this={newReportModal}>
<NewReportModal <NewReportModal
on:submit={(e) => { onSubmit={(e) => {
if (!e.detail.draft) $reportCount += 1; if (!e.draft) $reportCount += 1;
reports = [e.detail, ...reports]; reports = [e, ...reports];
activeReport = reports[0]; activeReport = reports[0];
newReportModal.close(); newReportModal.close();
}} }}

View File

@ -2,28 +2,38 @@
import Select from '$lib/components/Input/Select.svelte'; import Select from '$lib/components/Input/Select.svelte';
import Input from '$lib/components/Input/Input.svelte'; import Input from '$lib/components/Input/Input.svelte';
export let reportFilter = { let {
reporter: null, reportFilter = $bindable({
reported: null, reporter: undefined,
status: null, reported: undefined,
draft: false status: undefined,
}; draft: false
}),
onUpdate
}: {
reportFilter: { reporter?: string; reported?: string; status?: string; draft: false };
onUpdate: () => void;
} = $props();
</script> </script>
<form class="flex flex-row justify-center space-x-4 mx-4 my-2"> <form class="flex flex-row justify-center space-x-4 mx-4 my-2">
<Input size="sm" placeholder="Alle" bind:value={reportFilter.reporter}> <Input size="sm" placeholder="Alle" bind:value={reportFilter.reporter} oninput={onUpdate}>
<span slot="label">Report Ersteller</span> {#snippet label()}
<span>Report Ersteller</span>
{/snippet}
</Input> </Input>
<Input size="sm" placeholder="Alle" bind:value={reportFilter.reported}> <Input size="sm" placeholder="Alle" bind:value={reportFilter.reported} oninput={onUpdate}>
<span slot="label">Reportete Spieler</span> {#snippet label()}
<span>Reporteter Spieler</span>
{/snippet}
</Input> </Input>
<Select label="Bearbeitungsstatus" size="sm" bind:value={reportFilter.status}> <Select label="Bearbeitungsstatus" size="sm" bind:value={reportFilter.status} onChange={onUpdate}>
<option value="none">Unbearbeitet</option> <option value="none">Unbearbeitet</option>
<option value="review">In Bearbeitung</option> <option value="review">In Bearbeitung</option>
<option value={null}>Unbearbeitet & In Bearbeitung</option> <option value={null}>Unbearbeitet & In Bearbeitung</option>
<option value="reviewed">Bearbeitet</option> <option value="reviewed">Bearbeitet</option>
</Select> </Select>
<Select label="Reportstatus" size="sm" bind:value={reportFilter.draft}> <Select label="Reportstatus" size="sm" bind:value={reportFilter.draft} onChange={onUpdate}>
<option value={false}>Erstellt</option> <option value={false}>Erstellt</option>
<option value={true}>Entwurf</option> <option value={true}>Entwurf</option>
</Select> </Select>

View File

@ -3,15 +3,15 @@
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import Textarea from '$lib/components/Input/Textarea.svelte'; import Textarea from '$lib/components/Input/Textarea.svelte';
import Search from '$lib/components/Input/Search.svelte'; import Search from '$lib/components/Input/Search.svelte';
import { createEventDispatcher } from 'svelte';
import { usernameSuggestions } from '$lib/utils'; import { usernameSuggestions } from '$lib/utils';
import type { Report } from '$lib/server/database';
const dispatch = createEventDispatcher(); let { onSubmit }: { onSubmit: (data: typeof Report.prototype.dataValues) => void } = $props();
let reporter: string; let reporter: string | undefined = $state();
let reported: string; let reported: string | undefined = $state();
let reason = ''; let reason = $state('');
let body = ''; let body = $state('');
async function newReport() { async function newReport() {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/reports`, { const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/reports`, {
@ -23,7 +23,7 @@
body: body || null body: body || null
}) })
}); });
if (response.ok) dispatch('submit', await response.json()); if (response.ok) onSubmit(await response.json());
} }
let globalCloseForm: HTMLFormElement; let globalCloseForm: HTMLFormElement;
@ -35,7 +35,10 @@
<form method="dialog" class="modal-box" bind:this={reportForm}> <form method="dialog" class="modal-box" bind:this={reportForm}>
<button <button
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
on:click|preventDefault={() => globalCloseForm.submit()}>✕</button onclick={(e) => {
e.preventDefault();
globalCloseForm.submit();
}}>✕</button
> >
<h3 class="font-roboto text-3xl">Neuer Report</h3> <h3 class="font-roboto text-3xl">Neuer Report</h3>
<div class="space-y-2 mt-2 px-1 max-h-[70vh] overflow-y-scroll"> <div class="space-y-2 mt-2 px-1 max-h-[70vh] overflow-y-scroll">
@ -59,9 +62,11 @@
bind:value={reported} bind:value={reported}
/> />
</div> </div>
<div class="divider mx-4 pt-3" /> <div class="divider mx-4 pt-3"></div>
<Input type="text" bind:value={reason} required={true} pickyWidth={false}> <Input type="text" bind:value={reason} required={true} pickyWidth={false}>
<span slot="label">Report Grund</span> {#snippet label()}
<span>Report Grund</span>
{/snippet}
</Input> </Input>
<div> <div>
<Textarea rows={4} label="Details über den Report Grund" bind:value={body} /> <Textarea rows={4} label="Details über den Report Grund" bind:value={body} />
@ -71,9 +76,9 @@
<Input <Input
type="submit" type="submit"
value="Erstellen" value="Erstellen"
on:click={(e) => { onclick={(e) => {
if (reportForm.checkValidity()) { if (reportForm.checkValidity()) {
e.detail.preventDefault(); e.preventDefault();
confirmDialog.show(); confirmDialog.show();
} }
}} }}
@ -81,8 +86,8 @@
<Input <Input
type="submit" type="submit"
value="Abbrechen" value="Abbrechen"
on:click={(e) => { onclick={(e) => {
e.detail.preventDefault(); e.preventDefault();
globalCloseForm.submit(); globalCloseForm.submit();
}} }}
/> />
@ -111,7 +116,7 @@
<Input <Input
type="submit" type="submit"
value="Erstellen" value="Erstellen"
on:click={async () => { onclick={async () => {
await newReport(); await newReport();
globalCloseForm.submit(); globalCloseForm.submit();
}} }}

View File

@ -16,7 +16,6 @@ export const load: PageServerLoad = async ({ parent, cookies }) => {
(prev, curr) => { (prev, curr) => {
return { ...prev, [curr.key]: curr.value }; return { ...prev, [curr.key]: curr.value };
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{} as { [key: string]: any } {} as { [key: string]: any }
); );

View File

@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
export let data: PageData;
let settings = structuredClone(data.settings); let { data } = $props();
let settings = $state($state.snapshot(data.settings));
async function change() { async function change() {
await fetch(`${env.PUBLIC_BASE_PATH}/admin/settings`, { await fetch(`${env.PUBLIC_BASE_PATH}/admin/settings`, {
@ -23,7 +25,7 @@
} as PageData['settings']) } as PageData['settings'])
}); });
data.settings = settings; data.settings = settings;
settings = structuredClone(data.settings); settings = $state.snapshot(data.settings);
} }
function returnIfNoDup<T>(value: T, original: T): T | undefined { function returnIfNoDup<T>(value: T, original: T): T | undefined {
@ -68,7 +70,7 @@
<button <button
class="btn btn-success mt-auto" class="btn btn-success mt-auto"
class:btn-disabled={JSON.stringify(data.settings) === JSON.stringify(settings)} class:btn-disabled={JSON.stringify(data.settings) === JSON.stringify(settings)}
on:click={change}>Speichern</button onclick={change}>Speichern</button
> >
</div> </div>
</div> </div>

View File

@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types';
import { Check, NoSymbol, PencilSquare, Plus, Trash } from 'svelte-heros-v2'; import { Check, NoSymbol, PencilSquare, Plus, Trash } from 'svelte-heros-v2';
import Input from '$lib/components/Input/Input.svelte'; import Input from '$lib/components/Input/Input.svelte';
import Select from '$lib/components/Input/Select.svelte'; import Select from '$lib/components/Input/Select.svelte';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import type { Report, User } from '$lib/server/database'; import type { User } from '$lib/server/database';
import { buttonTriggeredRequest } from '$lib/components/utils'; import { buttonTriggeredRequest } from '$lib/components/utils';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import HeaderBar from './HeaderBar.svelte'; import HeaderBar from './HeaderBar.svelte';
@ -13,11 +12,9 @@
import NewUserModal from './NewUserModal.svelte'; import NewUserModal from './NewUserModal.svelte';
import PaginationTableBody from '$lib/components/PaginationTable/PaginationTableBody.svelte'; import PaginationTableBody from '$lib/components/PaginationTable/PaginationTableBody.svelte';
export let data: PageData; let users: (typeof User.prototype.dataValues)[] = $state([]);
let users: (typeof User.prototype.dataValues)[] = [];
let usersPerRequest = 25; let usersPerRequest = 25;
let userFilter: { [k: string]: any } = { name: null, playertype: null }; let userFilter: { [k: string]: any } = $state({ name: null, playertype: null });
let userTableContainerElement: HTMLDivElement; let userTableContainerElement: HTMLDivElement;
let newUserModal: HTMLDialogElement; let newUserModal: HTMLDialogElement;
@ -64,18 +61,16 @@
users = users; users = users;
} }
} }
$: if (userFilter) fetchUsers({ from: 0 }).then((u) => (users = u));
</script> </script>
<div class="h-full flex flex-col overflow-hidden"> <div class="h-full flex flex-col overflow-hidden">
<div class="grid grid-cols-[10fr_1fr_10fr_1fr_10fr]"> <div class="grid grid-cols-[10fr_1fr_10fr_1fr_10fr]">
<div /> <div></div>
<div /> <div></div>
<HeaderBar bind:userFilter /> <HeaderBar bind:userFilter onUpdate={() => fetchUsers({ from: 0 }).then((u) => (users = u))} />
<div class="divider divider-horizontal my-auto h-3/4" /> <div class="divider divider-horizontal my-auto h-3/4"></div>
<div class="flex items-center"> <div class="flex items-center">
<button class="btn" on:click={() => newUserModal.show()}> <button class="btn" onclick={() => newUserModal.show()}>
<Plus /> <Plus />
<span>Neuer Spieler</span> <span>Neuer Spieler</span>
</button> </button>
@ -87,16 +82,16 @@
<thead> <thead>
<!-- prettier-ignore --> <!-- prettier-ignore -->
<SortableTr class="[&>th]:bg-base-100 [&>th]:z-[1] [&>th]:sticky [&>th]:top-0"> <SortableTr class="[&>th]:bg-base-100 [&>th]:z-[1] [&>th]:sticky [&>th]:top-0">
<th /> <th></th>
<SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'firstname', asc: e.detail.asc}}}>Vorname</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'firstname', asc: e.asc}}}>Vorname</SortableTh>
<SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'lastname', asc: e.detail.asc}}}>Nachname</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'lastname', asc: e.asc}}}>Nachname</SortableTh>
<SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'birthday', asc: e.detail.asc}}}>Geburtstag</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'birthday', asc: e.asc}}}>Geburtstag</SortableTh>
<SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'telephone', asc: e.detail.asc}}}>Telefon</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'telephone', asc: e.asc}}}>Telefon</SortableTh>
<SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'username', asc: e.detail.asc}}}>Username</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'username', asc: e.asc}}}>Username</SortableTh>
<SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'playertype', asc: e.detail.asc}}}>Minecraft Edition</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'playertype', asc: e.asc}}}>Minecraft Edition</SortableTh>
<SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'password', asc: e.detail.asc}}}>Passwort</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'password', asc: e.asc}}}>Passwort</SortableTh>
<SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'uuid', asc: e.detail.asc}}}>UUID</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'uuid', asc: e.asc}}}>UUID</SortableTh>
<th /> <th></th>
</SortableTr> </SortableTr>
</thead> </thead>
<PaginationTableBody <PaginationTableBody
@ -117,7 +112,7 @@
<Input <Input
type="date" type="date"
value={new Date(user.birthday).toISOString().split('T')[0]} value={new Date(user.birthday).toISOString().split('T')[0]}
on:input={(e) => (user.birthday = e.detail.target.valueAsDate.toISOString())} oninput={(e) => (user.birthday = e.currentTarget.valueAsDate.toISOString())}
disabled={!user.edit} disabled={!user.edit}
size="sm" size="sm"
/> />
@ -146,7 +141,7 @@
{#if user.edit} {#if user.edit}
<button <button
class="btn btn-sm btn-square" class="btn btn-sm btn-square"
on:click={async (e) => { onclick={async (e) => {
await buttonTriggeredRequest(e, updateUser(user)); await buttonTriggeredRequest(e, updateUser(user));
user.edit = false; user.edit = false;
}} }}
@ -155,9 +150,9 @@
</button> </button>
<button <button
class="btn btn-sm btn-square" class="btn btn-sm btn-square"
on:click={() => { onclick={() => {
user.edit = false; user.edit = false;
user = user.before; users[i] = user.before;
}} }}
> >
<NoSymbol size="18" /> <NoSymbol size="18" />
@ -165,8 +160,8 @@
{:else} {:else}
<button <button
class="btn btn-sm btn-square" class="btn btn-sm btn-square"
on:click={() => { onclick={() => {
user.before = structuredClone(user); user.before = $state.snapshot(user);
user.edit = true; user.edit = true;
}} }}
> >
@ -174,7 +169,7 @@
</button> </button>
<button <button
class="btn btn-sm btn-square" class="btn btn-sm btn-square"
on:click={(e) => buttonTriggeredRequest(e, deleteUser(user.id))} onclick={(e) => buttonTriggeredRequest(e, deleteUser(user.id))}
> >
<Trash size="18" /> <Trash size="18" />
</button> </button>
@ -190,8 +185,8 @@
<dialog class="modal" bind:this={newUserModal}> <dialog class="modal" bind:this={newUserModal}>
<NewUserModal <NewUserModal
on:submit={(e) => { onSubmit={(e) => {
users = [...users, e.detail]; users = [...users, e];
newUserModal.close(); newUserModal.close();
}} }}
/> />

View File

@ -3,7 +3,13 @@ import { Permissions } from '$lib/permissions';
import { error, type RequestHandler } from '@sveltejs/kit'; import { error, type RequestHandler } from '@sveltejs/kit';
import { User } from '$lib/server/database'; import { User } from '$lib/server/database';
import { type Attributes, Op } from 'sequelize'; import { type Attributes, Op } from 'sequelize';
import { ApiError, getJavaUuid, getNoAuthUuid, RateLimitError, UserNotFoundError } from '$lib/server/minecraft'; import {
ApiError,
getJavaUuid,
getNoAuthUuid,
RateLimitError,
UserNotFoundError
} from '$lib/server/minecraft';
import { UserAddSchema, UserDeleteSchema, UserEditSchema, UserListSchema } from './schema'; import { UserAddSchema, UserDeleteSchema, UserEditSchema, UserListSchema } from './schema';
export const POST = (async ({ request, cookies }) => { export const POST = (async ({ request, cookies }) => {

View File

@ -2,20 +2,34 @@
import Select from '$lib/components/Input/Select.svelte'; import Select from '$lib/components/Input/Select.svelte';
import Input from '$lib/components/Input/Input.svelte'; import Input from '$lib/components/Input/Input.svelte';
export let userFilter: { [k: string]: any } = { let {
name: null, userFilter = $bindable({ name: null, playertype: null }),
playertype: null onUpdate
}; }: { userFilter: { [k: string]: any }; onUpdate: () => void } = $props();
</script> </script>
<form class="flex flex-row justify-center items-center space-x-4 my-2 w-full"> <form class="flex flex-row justify-center items-center space-x-4 my-2 w-full">
<div class="w-full"> <div class="w-full">
<Input size="sm" placeholder="..." bind:value={userFilter.name} pickyWidth={false}> <Input
<span slot="label">Username</span> size="sm"
placeholder="..."
bind:value={userFilter.name}
pickyWidth={false}
oninput={onUpdate}
>
{#snippet label()}
<span>Username</span>
{/snippet}
</Input> </Input>
</div> </div>
<div class="w-full"> <div class="w-full">
<Select label="Edition" size="sm" bind:value={userFilter.playertype} pickyWidth={false}> <Select
label="Edition"
size="sm"
bind:value={userFilter.playertype}
pickyWidth={false}
onChange={onUpdate}
>
<option value={null}>Alle</option> <option value={null}>Alle</option>
<option value="java">Java</option> <option value="java">Java</option>
<option value="bedrock">Bedrock</option> <option value="bedrock">Bedrock</option>

View File

@ -3,16 +3,33 @@
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import Select from '$lib/components/Input/Select.svelte'; import Select from '$lib/components/Input/Select.svelte';
import { errorMessage } from '$lib/stores'; import { errorMessage } from '$lib/stores';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); let {
onSubmit
}: {
onSubmit: ({
firstname,
lastname,
birthday,
telephone,
username,
playertype
}: {
firstname: string;
lastname: string;
birthday: string;
telephone: string;
username: string;
playertype: string;
}) => void;
} = $props();
let firstname: string; let firstname: string = $state('');
let lastname: string; let lastname: string = $state('');
let birthday: string; let birthday: string = $state('');
let phone: string; let phone: string = $state('');
let username: string; let username: string = $state('');
let playertype = 'java'; let playertype = $state('java');
async function newUser() { async function newUser() {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, { const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, {
@ -27,7 +44,7 @@
}) })
}); });
if (response.ok) { if (response.ok) {
dispatch('submit', { onSubmit({
firstname: firstname, firstname: firstname,
lastname: lastname, lastname: lastname,
birthday: birthday, birthday: birthday,
@ -50,24 +67,37 @@
<form method="dialog" class="modal-box" bind:this={reportForm}> <form method="dialog" class="modal-box" bind:this={reportForm}>
<button <button
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
on:click|preventDefault={() => globalCloseForm.submit()}>✕</button onclick={(e) => {
e.preventDefault();
globalCloseForm.submit();
}}>✕</button
> >
<h3 class="font-roboto text-3xl">Neuer Spieler</h3> <h3 class="font-roboto text-3xl">Neuer Spieler</h3>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<Input type="text" required bind:value={firstname}> <Input type="text" required bind:value={firstname}>
<span slot="label">Vorname</span> {#snippet label()}
<span>Vorname</span>
{/snippet}
</Input> </Input>
<Input type="text" required bind:value={lastname}> <Input type="text" required bind:value={lastname}>
<span slot="label">Nachname</span> {#snippet label()}
<span>Nachname</span>
{/snippet}
</Input> </Input>
<Input type="date" required bind:value={birthday}> <Input type="date" required bind:value={birthday}>
<span slot="label">Geburtstag</span> {#snippet label()}
<span>Geburtstag</span>
{/snippet}
</Input> </Input>
<Input type="tel" bind:value={phone}> <Input type="tel" bind:value={phone}>
<span slot="label">Telefonnummer</span> {#snippet label()}
<span>Telefonnummer</span>
{/snippet}
</Input> </Input>
<Input type="text" required bind:value={username}> <Input type="text" required bind:value={username}>
<span slot="label">Minecraft-Spielername</span> {#snippet label()}
<span>Minecraft-Spielername</span>
{/snippet}
</Input> </Input>
<Select required label="Edition" bind:value={playertype}> <Select required label="Edition" bind:value={playertype}>
<option value="java">Java Edition</option> <option value="java">Java Edition</option>
@ -78,9 +108,9 @@
<Input <Input
type="submit" type="submit"
value="Hinzufügen" value="Hinzufügen"
on:click={(e) => { onclick={(e) => {
if (reportForm.checkValidity()) { if (reportForm.checkValidity()) {
e.detail.preventDefault(); e.preventDefault();
confirmDialog.show(); confirmDialog.show();
} }
}} }}
@ -88,8 +118,8 @@
<Input <Input
type="submit" type="submit"
value="Abbrechen" value="Abbrechen"
on:click={(e) => { onclick={(e) => {
e.detail.preventDefault(); e.preventDefault();
globalCloseForm.submit(); globalCloseForm.submit();
}} }}
/> />
@ -104,7 +134,7 @@
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button> <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
<h3 class="font-roboto text-xl mb-2">Spieler hinzufügen?</h3> <h3 class="font-roboto text-xl mb-2">Spieler hinzufügen?</h3>
<div class="flex flex-row space-x-2 mt-6"> <div class="flex flex-row space-x-2 mt-6">
<Input type="submit" value="Hinzufügen" on:click={newUser} /> <Input type="submit" value="Hinzufügen" onclick={newUser} />
<Input type="submit" value="Abbrechen" /> <Input type="submit" value="Abbrechen" />
</div> </div>
</form> </form>

View File

@ -31,8 +31,6 @@ export const POST = (async ({ request, url }) => {
await Feedback.bulkCreate(Object.values(feedback)); await Feedback.bulkCreate(Object.values(feedback));
console.log(Object.entries(feedback));
return new Response( return new Response(
JSON.stringify( JSON.stringify(
Object.entries(feedback).reduce( Object.entries(feedback).reduce(

View File

@ -1,9 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
export let data: PageData;
let faq = [ let faq = [
{ {
section: 'Allgemein', section: 'Allgemein',
@ -204,10 +201,11 @@ sind nicht gestattet.</p>`
<input type="checkbox" autocomplete="off" /> <input type="checkbox" autocomplete="off" />
<div class="collapse-title">{question.title}</div> <div class="collapse-title">{question.title}</div>
<div class="collapse-content"> <div class="collapse-content">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
<div class="ml-2">{@html question.content}</div> <div class="ml-2">{@html question.content}</div>
</div> </div>
</div> </div>
<span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600" /> <span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600"></span>
{/each} {/each}
</div> </div>
</div> </div>

View File

@ -4,11 +4,11 @@
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import Select from '$lib/components/Input/Select.svelte'; import Select from '$lib/components/Input/Select.svelte';
let content = ''; let content = $state('');
let type = 'feedback'; let type = $state('feedback');
let submitModal: HTMLDialogElement; let submitModal: HTMLDialogElement;
let sentModal: HTMLDialogElement & { type?: string } = {}; let sentModal: HTMLDialogElement;
async function submitFeedback() { async function submitFeedback() {
await fetch(`${env.PUBLIC_BASE_PATH}/feedback`, { await fetch(`${env.PUBLIC_BASE_PATH}/feedback`, {
@ -27,7 +27,12 @@
<div> <div>
<h2 class="text-3xl text-center">Feedback & Kontakt</h2> <h2 class="text-3xl text-center">Feedback & Kontakt</h2>
<form on:submit|preventDefault={() => submitModal.show()}> <form
onsubmit={(e) => {
e.preventDefault();
submitModal.show();
}}
>
<div class="space-y-4 mt-6 mb-4"> <div class="space-y-4 mt-6 mb-4">
<Select size="sm" bind:value={type}> <Select size="sm" bind:value={type}>
<option value="feedback">Feedback</option> <option value="feedback">Feedback</option>
@ -67,9 +72,8 @@
<Input <Input
type="submit" type="submit"
value="Abschicken" value="Abschicken"
on:click={async () => { onclick={async () => {
await submitFeedback(); await submitFeedback();
sentModal.type = type;
sentModal.show(); sentModal.show();
}} }}
/> />
@ -86,7 +90,7 @@
<form <form
method="dialog" method="dialog"
class="modal-box" class="modal-box"
on:submit={() => { onsubmit={() => {
content = ''; content = '';
type = 'feedback'; type = 'feedback';
}} }}
@ -94,7 +98,7 @@
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button> <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
<div> <div>
<h3 class="font-roboto font-medium text-xl"> <h3 class="font-roboto font-medium text-xl">
{sentModal.type === 'feedback' ? 'Feedback' : 'Kontaktanfrage'} abgeschickt {type === 'feedback' ? 'Feedback' : 'Kontaktanfrage'} abgeschickt
</h3> </h3>
<div class="my-4"> <div class="my-4">
{#if type === 'feedback'} {#if type === 'feedback'}
@ -112,7 +116,7 @@
<form <form
method="dialog" method="dialog"
class="modal-backdrop bg-[rgba(0,0,0,.3)]" class="modal-backdrop bg-[rgba(0,0,0,.3)]"
on:submit={() => { onsubmit={() => {
content = ''; content = '';
type = 'feedback'; type = 'feedback';
}} }}

View File

@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import type { PageData } from './$types';
import FeedbackDraft from './FeedbackDraft.svelte'; import FeedbackDraft from './FeedbackDraft.svelte';
import FeedbackSent from './FeedbackSent.svelte'; import FeedbackSent from './FeedbackSent.svelte';
export let data: PageData; let { data } = $props();
</script> </script>
<svelte:head> <svelte:head>

View File

@ -1,17 +1,14 @@
<script lang="ts"> <script lang="ts">
import Input from '$lib/components/Input/Input.svelte'; import Input from '$lib/components/Input/Input.svelte';
import Textarea from '$lib/components/Input/Textarea.svelte'; import Textarea from '$lib/components/Input/Textarea.svelte';
import { createEventDispatcher } from 'svelte';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import { page } from '$app/stores'; import { page } from '$app/stores';
export let event: string; let { event, anonymous, onSubmit }: { event: string; anonymous: false; onSubmit?: () => void } =
export let anonymous: boolean; $props();
const dispatch = createEventDispatcher(); let content = $state('');
let sendAnonymous = $state(true);
let content = '';
let sendAnonymous = true;
let submitModal: HTMLDialogElement; let submitModal: HTMLDialogElement;
@ -28,10 +25,17 @@
<div> <div>
<h2 class="text-3xl text-center">Feedback</h2> <h2 class="text-3xl text-center">Feedback</h2>
<form on:submit|preventDefault={() => submitModal.show()}> <form
onsubmit={(e) => {
e.preventDefault();
submitModal.show();
}}
>
<div class="space-y-4 my-4"> <div class="space-y-4 my-4">
<Input size="sm" pickyWidth={false} disabled value={event}> <Input size="sm" pickyWidth={false} disabled value={event}>
<span slot="label">Event</span> {#snippet label()}
<span>Event</span>
{/snippet}
</Input> </Input>
<Textarea required={true} rows={4} label="Feedback" bind:value={content} /> <Textarea required={true} rows={4} label="Feedback" bind:value={content} />
<div> <div>
@ -63,9 +67,9 @@
<Input <Input
type="submit" type="submit"
value="Abschicken" value="Abschicken"
on:click={async () => { onclick={async () => {
await submitFeedback(); await submitFeedback();
dispatch('submit'); onSubmit && onSubmit();
}} }}
/> />
<Input type="submit" value="Abbrechen" /> <Input type="submit" value="Abbrechen" />

View File

@ -2,17 +2,16 @@
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import RegistrationComplete from './RegistrationComplete.svelte'; import RegistrationComplete from './RegistrationComplete.svelte';
import Register from './Register.svelte'; import Register from './Register.svelte';
import type { PageData } from './$types';
let registered = false; let { data } = $props();
let firstname: string | null = null;
let lastname: string | null = null;
let birthday: Date | null = null;
let phone: string | null = null;
let username: string | null = null;
let edition: string | null = null;
export let data: PageData; let registered = $state(false);
let firstname: string | null = $state(null);
let lastname: string | null = $state(null);
let birthday: Date | null = $state(null);
let phone: string | null = $state(null);
let username: string | null = $state(null);
let edition: string | null = $state(null);
</script> </script>
<svelte:head> <svelte:head>
@ -36,15 +35,15 @@
{#if !registered} {#if !registered}
<div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}> <div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}>
<Register <Register
on:submit={(e) => { submit={(e) => {
registered = true; registered = true;
firstname = e.detail.firstname; firstname = e.firstname;
lastname = e.detail.lastname; lastname = e.lastname;
birthday = e.detail.birthday; birthday = e.birthday;
phone = e.detail.phone; phone = e.phone;
phone = e.detail.phone; phone = e.phone;
username = e.detail.username; username = e.username;
edition = e.detail.edition; edition = e.edition;
}} }}
/> />
</div> </div>
@ -57,7 +56,7 @@
{phone} {phone}
{username} {username}
{edition} {edition}
on:close={() => (registered = false)} close={() => (registered = false)}
/> />
</div> </div>
{/if} {/if}

View File

@ -17,6 +17,7 @@ export const POST = (async ({ request }) => {
} }
try { try {
// eslint-disable-next-line no-var
var data = RegisterSchema.parse(await request.json()); var data = RegisterSchema.parse(await request.json());
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@ -1,27 +1,54 @@
<script lang="ts"> <script lang="ts">
import Select from '$lib/components/Input/Select.svelte'; import Select from '$lib/components/Input/Select.svelte';
import Input from '$lib/components/Input/Input.svelte'; import Input from '$lib/components/Input/Input.svelte';
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import { rulesShort } from '$lib/rules'; import { rulesShort } from '$lib/rules';
import { RegisterSchema } from './schema'; import { RegisterSchema } from './schema';
import { dev } from '$app/environment'; import { dev } from '$app/environment';
const dispatch = createEventDispatcher(); let {
submit
}: {
submit: ({
firstname,
lastname,
birthday,
phone,
username,
edition
}: {
firstname: string;
lastname: string;
birthday: Date;
phone: string;
username: string;
edition: string;
}) => void;
} = $props();
const modalTimeoutSeconds = dev ? 0 : 30; const modalTimeoutSeconds = dev ? 0 : 30;
// eslint-disable-next-line @typescript-eslint/no-empty-function let checkInputs = $state(() => {});
let checkInputs = () => {}; let playertype = $state('java');
let playertype = 'java'; /* eslint-disable @typescript-eslint/ban-ts-comment */
let firstnameInput: HTMLInputElement; // @ts-ignore
let lastnameInput: HTMLInputElement; let firstnameInput: HTMLInputElement = $state();
let birthdayInput: HTMLInputElement; // @ts-ignore
let phoneInput: HTMLInputElement; let lastnameInput: HTMLInputElement = $state();
let usernameInput: HTMLInputElement; // @ts-ignore
let privacyInput: HTMLInputElement; let birthdayInput: HTMLInputElement = $state();
let logsInput: HTMLInputElement; // @ts-ignore
let rulesInput: HTMLInputElement; let phoneInput: HTMLInputElement = $state();
// @ts-ignore
let usernameInput: HTMLInputElement = $state();
// @ts-ignore
let privacyInput: HTMLInputElement = $state();
// @ts-ignore
let logsInput: HTMLInputElement = $state();
// @ts-ignore
let rulesInput: HTMLInputElement = $state();
/* eslint-enable @typescript-eslint/ban-ts-comment */
onMount(() => { onMount(() => {
checkInputs = () => { checkInputs = () => {
let allInputs = [ let allInputs = [
@ -58,10 +85,10 @@
body: JSON.stringify(Object.fromEntries(new FormData(document.forms[0]))) body: JSON.stringify(Object.fromEntries(new FormData(document.forms[0])))
}); });
if (response.ok) { if (response.ok) {
dispatch('submit', { submit({
firstname: firstnameInput.value, firstname: firstnameInput.value,
lastname: lastnameInput.value, lastname: lastnameInput.value,
birthday: birthdayInput.valueAsDate, birthday: birthdayInput.valueAsDate!,
phone: phoneInput.value, phone: phoneInput.value,
username: usernameInput.value, username: usernameInput.value,
edition: playertype == 'java' ? 'Java (PC)' : 'Bedrock (Konsolen und Handys)' edition: playertype == 'java' ? 'Java (PC)' : 'Bedrock (Konsolen und Handys)'
@ -80,17 +107,24 @@
let rulesAccepted = false; let rulesAccepted = false;
let rulesModal: HTMLDialogElement; let rulesModal: HTMLDialogElement;
let rulesModalSecondsOpened = 0; let rulesModalSecondsOpened = $state(0);
// eslint-disable-next-line no-undef
let rulesModalTimer: number | NodeJS.Timeout | undefined = undefined; let rulesModalTimer: number | NodeJS.Timeout | undefined = undefined;
let inputsInvalidMessage: string | null = 'Bitte fülle alle erforderlichen Felder aus'; let inputsInvalidMessage: string | null = $state('Bitte fülle alle erforderlichen Felder aus');
let registerRequest: Promise<void> | null = null; let registerRequest: Promise<void> | null = $state(null);
let errorMessage: string = $state('');
let errorMessage: string = '';
</script> </script>
<h1 class="text-center text-3xl lg:text-5xl">Anmeldung</h1> <h1 class="text-center text-3xl lg:text-5xl">Anmeldung</h1>
<form id="form" on:input={checkInputs} on:submit|preventDefault={sendRegister}> <form
id="form"
oninput={checkInputs}
onsubmit={(e) => {
e.preventDefault();
sendRegister();
}}
>
<div class="divider">Persönliche Angaben</div> <div class="divider">Persönliche Angaben</div>
<div class="mx-2 grid grid-cols-1 sm:grid-cols-2 gap-y-4"> <div class="mx-2 grid grid-cols-1 sm:grid-cols-2 gap-y-4">
<Input <Input
@ -100,7 +134,9 @@
required={true} required={true}
bind:inputElement={firstnameInput} bind:inputElement={firstnameInput}
> >
<span slot="label">Vorname</span> {#snippet label()}
<span>Vorname</span>
{/snippet}
</Input> </Input>
<Input <Input
id="lastname" id="lastname"
@ -109,7 +145,9 @@
required={true} required={true}
bind:inputElement={lastnameInput} bind:inputElement={lastnameInput}
> >
<span slot="label">Nachname</span> {#snippet label()}
<span>Nachname</span>
{/snippet}
</Input> </Input>
<Input <Input
id="birthday" id="birthday"
@ -118,8 +156,12 @@
required={true} required={true}
bind:inputElement={birthdayInput} bind:inputElement={birthdayInput}
> >
<span slot="label">Geburtstag</span> {#snippet label()}
<span slot="notice">Die Angabe hat keine Auswirkungen auf das Spielgeschehen</span> <span>Geburtstag</span>
{/snippet}
{#snippet notice()}
<span>Die Angabe hat keine Auswirkungen auf das Spielgeschehen</span>
{/snippet}
</Input> </Input>
<Input <Input
id="telephone" id="telephone"
@ -128,12 +170,16 @@
bind:inputElement={phoneInput} bind:inputElement={phoneInput}
pattern={new RegExp(/^[+()\s/\d]+$/)} pattern={new RegExp(/^[+()\s/\d]+$/)}
> >
<span slot="label">Telefonnummer</span> {#snippet label()}
<p slot="notice"> <span>Telefonnummer</span>
Diese nutzen wir, um Dich in der Whatsapp-Gruppe zuzuordnen und kontaktieren zu können. {/snippet}
<br /> {#snippet notice()}
<b>Die Angabe ist freiwillig, hilft den Administratoren jedoch sehr!</b> <p>
</p> Diese nutzen wir, um Dich in der Whatsapp-Gruppe zuzuordnen und kontaktieren zu können.
<br />
<b>Die Angabe ist freiwillig, hilft den Administratoren jedoch sehr!</b>
</p>
{/snippet}
</Input> </Input>
</div> </div>
<div class="divider">Spiel</div> <div class="divider">Spiel</div>
@ -145,7 +191,9 @@
required={true} required={true}
bind:inputElement={usernameInput} bind:inputElement={usernameInput}
> >
<span slot="label">Minecraft-Spielername</span> {#snippet label()}
<span>Minecraft-Spielername</span>
{/snippet}
</Input> </Input>
<Select <Select
id="playertype" id="playertype"
@ -158,7 +206,7 @@
<option value="bedrock">Bedrock Edition (Konsolen und Handys)</option> <option value="bedrock">Bedrock Edition (Konsolen und Handys)</option>
</Select> </Select>
</div> </div>
<div class="divider" /> <div class="divider"></div>
<div class="mx-2 grid gap-y-3 mb-6"> <div class="mx-2 grid gap-y-3 mb-6">
<div class="flex gap-4"> <div class="flex gap-4">
<Input <Input
@ -199,9 +247,9 @@
name="rules" name="rules"
type="checkbox" type="checkbox"
required={true} required={true}
on:input={(e) => { oninput={(e) => {
if (!rulesAccepted) { if (!rulesAccepted) {
e.detail.target.checked = false; e.currentTarget.checked = false;
rulesModal.show(); rulesModal.show();
rulesModalTimer = setInterval(() => rulesModalSecondsOpened++, 1000); rulesModalTimer = setInterval(() => rulesModalSecondsOpened++, 1000);
} }
@ -211,7 +259,8 @@
<label for="rules"> <label for="rules">
Ich bin mit den <button Ich bin mit den <button
class="link" class="link"
on:click|preventDefault={() => { onclick={(e) => {
e.preventDefault();
rulesModal.show(); rulesModal.show();
rulesModalTimer = setInterval(() => rulesModalSecondsOpened++, 1000); rulesModalTimer = setInterval(() => rulesModalSecondsOpened++, 1000);
}}>Regeln</button }}>Regeln</button
@ -238,7 +287,7 @@
{#await registerRequest} {#await registerRequest}
<span <span
class="relative top-[calc(50%-12px)] left-[calc(50%-12px)] row-[1] col-[1] loading loading-ring" class="relative top-[calc(50%-12px)] left-[calc(50%-12px)] row-[1] col-[1] loading loading-ring"
/> ></span>
{/await} {/await}
{/if} {/if}
{/key} {/key}
@ -247,7 +296,7 @@
<dialog <dialog
class="modal" class="modal"
on:close={() => { onclose={() => {
clearInterval(rulesModalTimer); clearInterval(rulesModalTimer);
rulesModalTimer = undefined; rulesModalTimer = undefined;
}} }}
@ -266,7 +315,7 @@
<p>{rulesShort.header}</p> <p>{rulesShort.header}</p>
<p class="mt-1 text-[.75rem]">{rulesShort.footer}</p> <p class="mt-1 text-[.75rem]">{rulesShort.footer}</p>
</div> </div>
<span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600" /> <span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600"></span>
</div> </div>
{#each rulesShort.sections as section, i} {#each rulesShort.sections as section, i}
<div class="collapse collapse-arrow"> <div class="collapse collapse-arrow">
@ -278,10 +327,10 @@
<p>{section.content}</p> <p>{section.content}</p>
</div> </div>
</div> </div>
<span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600" /> <span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600"></span>
{/each} {/each}
</div> </div>
<!-- svelte-ignore a11y-no-static-element-interactions a11y-click-events-have-key-events --> <!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
<div <div
class="relative w-min" class="relative w-min"
title={rulesModalSecondsOpened < modalTimeoutSeconds title={rulesModalSecondsOpened < modalTimeoutSeconds
@ -290,7 +339,7 @@
0 0
)} Sekunden akzeptiert werden` )} Sekunden akzeptiert werden`
: ''} : ''}
on:click={() => { onclick={() => {
if (rulesModalSecondsOpened < modalTimeoutSeconds) { if (rulesModalSecondsOpened < modalTimeoutSeconds) {
errorMessage = errorMessage =
'Bitte lies die Regeln aufmerksam durch. Du kannst erst in einigen Sekunden fortfahren.'; 'Bitte lies die Regeln aufmerksam durch. Du kannst erst in einigen Sekunden fortfahren.';
@ -301,7 +350,7 @@
<div <div
style="width: {Math.min((rulesModalSecondsOpened / modalTimeoutSeconds) * 100, 100)}%" style="width: {Math.min((rulesModalSecondsOpened / modalTimeoutSeconds) * 100, 100)}%"
class="h-full bg-base-300" class="h-full bg-base-300"
/> ></div>
</div> </div>
<Input <Input
id="rules-accept" id="rules-accept"
@ -309,7 +358,7 @@
value="Akzeptieren" value="Akzeptieren"
disabled={rulesModalSecondsOpened < modalTimeoutSeconds} disabled={rulesModalSecondsOpened < modalTimeoutSeconds}
containerClass="bg-transparent z-[1] relative" containerClass="bg-transparent z-[1] relative"
on:click={() => { onclick={() => {
rulesAccepted = true; rulesAccepted = true;
rulesInput.checked = true; rulesInput.checked = true;
checkInputs(); checkInputs();
@ -324,7 +373,7 @@
</dialog> </dialog>
{#if errorMessage} {#if errorMessage}
<dialog class="modal" on:close={() => setTimeout(() => (errorMessage = ''), 200)} open> <dialog class="modal" onclose={() => setTimeout(() => (errorMessage = ''), 200)} open>
<form method="dialog" class="modal-box z-50"> <form method="dialog" class="modal-box z-50">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button> <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
<h3 class="font-bold text-2xl">Achtung</h3> <h3 class="font-bold text-2xl">Achtung</h3>

View File

@ -1,17 +1,26 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import Input from '$lib/components/Input/Input.svelte'; import Input from '$lib/components/Input/Input.svelte';
import Select from '$lib/components/Input/Select.svelte'; import Select from '$lib/components/Input/Select.svelte';
export let firstname: string; let {
export let lastname: string; firstname,
export let birthday: Date; lastname,
export let phone: string | null; birthday,
export let username: string; phone,
export let edition: string; username,
edition,
const dispatch = createEventDispatcher(); close
}: {
firstname: string;
lastname: string;
birthday: Date;
phone?: string;
username: string;
edition: string;
close: () => void;
} = $props();
let startDayOptions: Intl.DateTimeFormatOptions = { let startDayOptions: Intl.DateTimeFormatOptions = {
day: '2-digit', day: '2-digit',
@ -22,7 +31,7 @@
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit'
}; };
let skin: string | null = null; let skin: string | null = $state(null);
onMount(async () => { onMount(async () => {
let skinview3d = await import('skinview3d'); let skinview3d = await import('skinview3d');
@ -70,16 +79,34 @@
uns, dich auf unserem <a class="link" href={env.PUBLIC_TS_LINK} target="_blank">TeamSpeak</a> oder uns, dich auf unserem <a class="link" href={env.PUBLIC_TS_LINK} target="_blank">TeamSpeak</a> oder
in unserem <a class="link" href={env.PUBLIC_DISCORD_LINK} target="_blank">Discord</a> begrüßen zu dürfen! in unserem <a class="link" href={env.PUBLIC_DISCORD_LINK} target="_blank">Discord</a> begrüßen zu dürfen!
</p> </p>
<div class="divider" /> <div class="divider"></div>
<div class="flex justify-around mt-2 mb-4"> <div class="flex justify-around mt-2 mb-4">
<div class="grid grid-cols-1 sm:grid-cols-2 w-full sm:w-fit gap-x-4 gap-y-2"> <div class="grid grid-cols-1 sm:grid-cols-2 w-full sm:w-fit gap-x-4 gap-y-2">
<Input value={firstname} size="sm" readonly><span slot="label">Vorname</span></Input> <Input value={firstname} size="sm" readonly>
<Input value={lastname} size="sm" readonly><span slot="label">Nachname</span></Input> {#snippet label()}
<Input value={birthday.toISOString().substring(0, 10)} type="date" size="sm" readonly <span>Vorname</span>
><span slot="label">Geburtstag</span></Input {/snippet}
</Input>
<Input value={lastname} size="sm" readonly>
{#snippet label()}
<span>Nachname</span>
{/snippet}
</Input>
<Input value={birthday.toISOString().substring(0, 10)} type="date" size="sm" readonly>
{#snippet label()}
<span>Geburtstag</span>
{/snippet}</Input
> >
<Input value={phone} size="sm" readonly><span slot="label">Telefonnummer</span></Input> <Input value={phone} size="sm" readonly>
<Input value={username} size="sm" readonly><span slot="label">Spielername</span></Input> {#snippet label()}
<span>Telefonnummer</span>
{/snippet}
</Input>
<Input value={username} size="sm" readonly>
{#snippet label()}
<span>Spielername</span>
{/snippet}
</Input>
<Select value="edition" size="sm" disabled label="Edition"> <Select value="edition" size="sm" disabled label="Edition">
<option value="edition">{edition}</option> <option value="edition">{edition}</option>
</Select> </Select>
@ -88,11 +115,11 @@
{#if skin} {#if skin}
<img class="absolute" src={skin} alt="" /> <img class="absolute" src={skin} alt="" />
{:else} {:else}
<span class="loading loading-spinner loading-lg" /> <span class="loading loading-spinner loading-lg"></span>
{/if} {/if}
</div> </div>
</div> </div>
<div class="divider" /> <div class="divider"></div>
<div class="flex justify-center gap-8"> <div class="flex justify-center gap-8">
<button class="btn" on:click={() => dispatch('close')}>Weitere Person anmelden</button> <button class="btn" onclick={close}>Weitere Person anmelden</button>
</div> </div>

View File

@ -1,3 +1,7 @@
<script lang="ts">
let { children } = $props();
</script>
<div class="flex justify-center items-center w-full"> <div class="flex justify-center items-center w-full">
<slot /> {@render children()}
</div> </div>

View File

@ -1,11 +1,12 @@
<script lang="ts"> <script lang="ts">
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import type { PageData } from './$types';
import ReportDraft from './ReportDraft.svelte'; import ReportDraft from './ReportDraft.svelte';
import ReportCompleted from './ReportCompleted.svelte'; import ReportCompleted from './ReportCompleted.svelte';
import ReportSubmitted from './ReportSubmitted.svelte'; import ReportSubmitted from './ReportSubmitted.svelte';
export let data: PageData; let { data } = $props();
let completed = $state(!data.draft);
</script> </script>
<svelte:head> <svelte:head>
@ -15,14 +16,14 @@
</svelte:head> </svelte:head>
<div class="mt-12 grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 shadow-lg"> <div class="mt-12 grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 shadow-lg">
{#if data.draft} {#if !completed}
<div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}> <div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}>
<ReportDraft <ReportDraft
reason={data.reason} reason={data.reason}
reporterName={data.reporter.name} reporterName={data.reporter.name}
reportedName={data.reported.name ?? undefined} reportedName={data.reported.name ?? undefined}
users={data.users ?? []} users={data.users ?? []}
on:submit={() => (data.draft = false)} onsubmit={() => (completed = true)}
/> />
</div> </div>
{:else if data.status === 'reviewed'} {:else if data.status === 'reviewed'}

View File

@ -3,31 +3,36 @@
import Input from '$lib/components/Input/Input.svelte'; import Input from '$lib/components/Input/Input.svelte';
import { env } from '$env/dynamic/public'; import { env } from '$env/dynamic/public';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { createEventDispatcher } from 'svelte';
import Search from '$lib/components/Input/Search.svelte'; import Search from '$lib/components/Input/Search.svelte';
import Select from '$lib/components/Input/Select.svelte'; import Select from '$lib/components/Input/Select.svelte';
export let reporterName: string; let {
export let reportedName: string | null; reporterName,
export let reason: string; reportedName = null,
export let users: string[]; reason,
users,
onsubmit
}: {
reporterName: string;
reportedName: string | null;
reason: string;
users: string[];
onsubmit: () => void;
} = $props();
let oldReported = reportedName; let reported = $state(reportedName);
$: reportedName = oldReported; let content = $state('');
let body: string;
let userErrorModal: HTMLDialogElement; let userErrorModal: HTMLDialogElement;
let submitModal: HTMLDialogElement; let submitModal: HTMLDialogElement;
let dispatch = createEventDispatcher();
async function submitReport() { async function submitReport() {
await fetch(`${env.PUBLIC_BASE_PATH}/report/${$page.params.url_hash}`, { await fetch(`${env.PUBLIC_BASE_PATH}/report/${$page.params.url_hash}`, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
reported: reportedName || null, reported: reported || null,
subject: reason, subject: reason,
body: body body: content
}) })
}); });
} }
@ -45,11 +50,12 @@
<div> <div>
<h2 class="text-3xl text-center"> <h2 class="text-3xl text-center">
Report von <span class="underline">{reporterName}</span> gegen Report von <span class="underline">{reporterName}</span> gegen
<span class="underline">{(reportedName ?? 'unbekannt') || oldReported}</span> <span class="underline">{reported !== null ? reported : 'unbekannt'}</span>
</h2> </h2>
<form <form
on:submit|preventDefault={() => { onsubmit={(e) => {
if (reportedName != null && users.findIndex((u) => u === reportedName) === -1) { e.preventDefault();
if (reported != null && users.findIndex((u) => u === reported) === -1) {
userErrorModal.show(); userErrorModal.show();
} else { } else {
submitModal.show(); submitModal.show();
@ -59,21 +65,25 @@
<div class="space-y-4 my-4"> <div class="space-y-4 my-4">
<div class="flex justify-center gap-4"> <div class="flex justify-center gap-4">
<Select <Select
value={+(reportedName === null)} value={+(reported === null)}
size="sm" size="sm"
pickyWidth={false} pickyWidth={false}
on:change={(e) => (reportedName = e.detail.value === 0 ? '' : null)} onChange={(e) => {
reported = e.value === 0 ? reportedName || '' : null;
}}
> >
<option value={0}>Ich möchte einen bestimmten Spieler reporten</option> <option value={0}>Ich möchte einen bestimmten Spieler reporten</option>
<option value={1}>Ich möchte einen unbekannten Spieler reporten</option> <option value={1}>Ich möchte einen unbekannten Spieler reporten</option>
</Select> </Select>
{#if reportedName !== null} {#if reported !== null}
<Search size="sm" bind:value={oldReported} searchSuggestionFunc={suggestNames} /> <Search size="sm" bind:value={reported} searchSuggestionFunc={suggestNames} />
{/if} {/if}
</div> </div>
<div> <div>
<Input type="text" bind:value={reason} required={true} pickyWidth={false}> <Input type="text" bind:value={reason} required={true} pickyWidth={false}>
<span slot="label">Report Grund</span> {#snippet label()}
<span>Report Grund</span>
{/snippet}
</Input> </Input>
</div> </div>
<div> <div>
@ -81,7 +91,7 @@
required={true} required={true}
rows={4} rows={4}
label="Details über den Report Grund" label="Details über den Report Grund"
bind:value={body} bind:value={content}
/> />
</div> </div>
</div> </div>
@ -121,9 +131,9 @@
<Input <Input
type="submit" type="submit"
value="Abschicken" value="Abschicken"
on:click={async () => { onclick={async () => {
await submitReport(); await submitReport();
dispatch('submit'); onsubmit();
}} }}
/> />
<Input type="submit" value="Abbrechen" /> <Input type="submit" value="Abbrechen" />

View File

@ -1,3 +1,7 @@
<script lang="ts">
let { children } = $props();
</script>
<div class="mx-4 my-6 sm:mx-48 sm:my-12"> <div class="mx-4 my-6 sm:mx-48 sm:my-12">
<slot /> {@render children()}
</div> </div>

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { rulesLong, rulesShort } from '$lib/rules'; import { rulesLong } from '$lib/rules';
</script> </script>
<svelte:head> <svelte:head>
@ -14,12 +14,12 @@
<p>0. Vorwort</p> <p>0. Vorwort</p>
</div> </div>
<div class="collapse-content"> <div class="collapse-content">
<p>{rulesShort.header}</p> <p>{rulesLong.header}</p>
<p class="mt-1 text-[.75rem]">{rulesShort.footer}</p> <p class="mt-1 text-[.75rem]">{rulesLong.footer}</p>
</div> </div>
<span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600" /> <span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600"></span>
</div> </div>
{#each rulesShort.sections as section, i} {#each rulesLong.sections as section, i}
<div class="collapse collapse-arrow"> <div class="collapse collapse-arrow">
<input type="checkbox" autocomplete="off" /> <input type="checkbox" autocomplete="off" />
<div class="collapse-title"> <div class="collapse-title">
@ -29,5 +29,5 @@
<p>{section.content}</p> <p>{section.content}</p>
</div> </div>
</div> </div>
<span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600" /> <span class="block w-full h-[1px] mx-auto mb-1 bg-gray-600"></span>
{/each} {/each}

View File

@ -1,3 +1,7 @@
<script lang="ts">
let { children } = $props();
</script>
<div class="m-auto flex flex-col justify-center items-center px-4 py-6 2xl:px-48 sm:py-12"> <div class="m-auto flex flex-col justify-center items-center px-4 py-6 2xl:px-48 sm:py-12">
<slot /> {@render children()}
</div> </div>

View File

@ -61,7 +61,7 @@
<p class="text-center text-sm font-light">{member.roles.join(' · ')}</p> <p class="text-center text-sm font-light">{member.roles.join(' · ')}</p>
{#if member.links} {#if member.links}
<div class="w-full flex items-center flex-col"> <div class="w-full flex items-center flex-col">
<div class="w-1/2 h-[1px] my-3 rounded bg-base-content" /> <div class="w-1/2 h-[1px] my-3 rounded bg-base-content"></div>
<div class="flex gap-3"> <div class="flex gap-3">
{#each member.links as link} {#each member.links as link}
<a <a

View File

@ -1,6 +1,6 @@
import type { Config } from 'tailwindcss';
import daisyui from 'daisyui'; import daisyui from 'daisyui';
/** @type {import('tailwindcss').Config} */
export default { export default {
content: ['./src/**/*.{html,js,svelte,ts}'], content: ['./src/**/*.{html,js,svelte,ts}'],
theme: { theme: {
@ -16,4 +16,4 @@ export default {
daisyui: { daisyui: {
logs: false logs: false
} }
}; } satisfies Config;