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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ export const POST = (async ({ request, cookies }) => {
}
const data = parseResult.data;
let feedbackFindOptions: Attributes<Feedback> = {
const feedbackFindOptions: Attributes<Feedback> = {
content: { [Op.not]: null }
};
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 });
let feedback = await Feedback.findAll({
const feedback = await Feedback.findAll({
where: feedbackFindOptions,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore

View File

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

View File

@ -3,7 +3,8 @@
import { env } from '$env/dynamic/public';
import { errorMessage } from '$lib/stores';
let passwordValue: string;
let password = $state('');
async function login() {
// eslint-disable-next-line no-async-promise-executor
loginRequest = new Promise(async (resolve) => {
@ -12,10 +13,10 @@
body: JSON.stringify(Object.fromEntries(new FormData(document.forms[0])))
});
if (response.ok) {
window.location = `${env.PUBLIC_BASE_PATH}/admin`;
window.location.href = `${env.PUBLIC_BASE_PATH}/admin`;
resolve();
} else if (response.status == 401) {
passwordValue = '';
password = '';
$errorMessage = 'Nutzername oder Passwort falsch';
resolve();
} else {
@ -25,25 +26,29 @@
});
}
let loginRequest: Promise<void> | null = null;
let loginRequest: Promise<void> | null = $state(null);
</script>
<div class="card px-14 py-6 shadow-lg">
<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="grid gap-4">
<Input id="username" name="username" type="text" required={true}>
<span slot="label">Nutzername</span>
{#snippet label()}
<span>Nutzername</span>
{/snippet}
</Input>
<Input
id="password"
name="password"
type="password"
required={true}
bind:value={passwordValue}
>
<span slot="label">Passwort</span>
<Input id="password" name="password" type="password" required={true} bind:value={password}>
{#snippet label()}
<span>Passwort</span>
{/snippet}
</Input>
</div>
</div>
@ -56,22 +61,7 @@
{#await loginRequest}
<span
class="relative top-[calc(50%-12px)] left-[calc(50%-12px)] row-[1] col-[1] loading loading-ring"
/>
{: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>
></span>
{/await}
{/if}
{/key}

View File

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

View File

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

View File

@ -3,15 +3,15 @@
import { env } from '$env/dynamic/public';
import Textarea from '$lib/components/Input/Textarea.svelte';
import Search from '$lib/components/Input/Search.svelte';
import { createEventDispatcher } from 'svelte';
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 reported: string;
let reason = '';
let body = '';
let reporter: string | undefined = $state();
let reported: string | undefined = $state();
let reason = $state('');
let body = $state('');
async function newReport() {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/reports`, {
@ -23,7 +23,7 @@
body: body || null
})
});
if (response.ok) dispatch('submit', await response.json());
if (response.ok) onSubmit(await response.json());
}
let globalCloseForm: HTMLFormElement;
@ -35,7 +35,10 @@
<form method="dialog" class="modal-box" bind:this={reportForm}>
<button
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>
<div class="space-y-2 mt-2 px-1 max-h-[70vh] overflow-y-scroll">
@ -59,9 +62,11 @@
bind:value={reported}
/>
</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}>
<span slot="label">Report Grund</span>
{#snippet label()}
<span>Report Grund</span>
{/snippet}
</Input>
<div>
<Textarea rows={4} label="Details über den Report Grund" bind:value={body} />
@ -71,9 +76,9 @@
<Input
type="submit"
value="Erstellen"
on:click={(e) => {
onclick={(e) => {
if (reportForm.checkValidity()) {
e.detail.preventDefault();
e.preventDefault();
confirmDialog.show();
}
}}
@ -81,8 +86,8 @@
<Input
type="submit"
value="Abbrechen"
on:click={(e) => {
e.detail.preventDefault();
onclick={(e) => {
e.preventDefault();
globalCloseForm.submit();
}}
/>
@ -111,7 +116,7 @@
<Input
type="submit"
value="Erstellen"
on:click={async () => {
onclick={async () => {
await newReport();
globalCloseForm.submit();
}}

View File

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

View File

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

View File

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

View File

@ -3,7 +3,13 @@ import { Permissions } from '$lib/permissions';
import { error, type RequestHandler } from '@sveltejs/kit';
import { User } from '$lib/server/database';
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';
export const POST = (async ({ request, cookies }) => {

View File

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

View File

@ -3,16 +3,33 @@
import { env } from '$env/dynamic/public';
import Select from '$lib/components/Input/Select.svelte';
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 lastname: string;
let birthday: string;
let phone: string;
let username: string;
let playertype = 'java';
let firstname: string = $state('');
let lastname: string = $state('');
let birthday: string = $state('');
let phone: string = $state('');
let username: string = $state('');
let playertype = $state('java');
async function newUser() {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, {
@ -27,7 +44,7 @@
})
});
if (response.ok) {
dispatch('submit', {
onSubmit({
firstname: firstname,
lastname: lastname,
birthday: birthday,
@ -50,24 +67,37 @@
<form method="dialog" class="modal-box" bind:this={reportForm}>
<button
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>
<div class="grid grid-cols-2 gap-4">
<Input type="text" required bind:value={firstname}>
<span slot="label">Vorname</span>
{#snippet label()}
<span>Vorname</span>
{/snippet}
</Input>
<Input type="text" required bind:value={lastname}>
<span slot="label">Nachname</span>
{#snippet label()}
<span>Nachname</span>
{/snippet}
</Input>
<Input type="date" required bind:value={birthday}>
<span slot="label">Geburtstag</span>
{#snippet label()}
<span>Geburtstag</span>
{/snippet}
</Input>
<Input type="tel" bind:value={phone}>
<span slot="label">Telefonnummer</span>
{#snippet label()}
<span>Telefonnummer</span>
{/snippet}
</Input>
<Input type="text" required bind:value={username}>
<span slot="label">Minecraft-Spielername</span>
{#snippet label()}
<span>Minecraft-Spielername</span>
{/snippet}
</Input>
<Select required label="Edition" bind:value={playertype}>
<option value="java">Java Edition</option>
@ -78,9 +108,9 @@
<Input
type="submit"
value="Hinzufügen"
on:click={(e) => {
onclick={(e) => {
if (reportForm.checkValidity()) {
e.detail.preventDefault();
e.preventDefault();
confirmDialog.show();
}
}}
@ -88,8 +118,8 @@
<Input
type="submit"
value="Abbrechen"
on:click={(e) => {
e.detail.preventDefault();
onclick={(e) => {
e.preventDefault();
globalCloseForm.submit();
}}
/>
@ -104,7 +134,7 @@
<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>
<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" />
</div>
</form>