add admin user page

This commit is contained in:
2023-08-29 04:23:03 +02:00
parent 1fb71fe899
commit 10b1c01d51
8 changed files with 402 additions and 17 deletions

View File

@ -0,0 +1,290 @@
<script lang="ts">
import type { PageData } from './$types';
import { IconOutline, IconSolid } 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 { User } from '$lib/server/database';
import { buttonTriggeredRequest } from '$lib/components/utils';
import { browser } from '$app/environment';
export let data: PageData;
let headers = [
{
name: 'Vorname',
key: 'firstname',
asc: false
},
{
name: 'Nachname',
key: 'lastname',
asc: false
},
{ name: 'Geburtstag', key: 'birthday', asc: false, sort: (a, b) => a.birthday - b.birthday },
{ name: 'Telefon', key: 'telephone', asc: false, sort: (a, b) => a.telephone - b.telephone },
{
name: 'Username',
key: 'username',
asc: false
},
{
name: 'Minecraft Edition',
key: 'playertype',
asc: false
},
{
name: 'Passwort',
key: 'password',
asc: false
},
{ name: 'UUID', key: 'uuid', asc: false }
];
let ascHeader: (typeof headers)[0] | null = null;
let currentPageUsers: (typeof User.prototype.dataValues)[] = [];
let currentPageUsersRequest: Promise<void> = new Promise((resolve) => resolve());
let usersCache: (typeof User.prototype.dataValues)[][] = [];
let usersPerPage = 50;
let userPage = 0;
let userTableContainerElement: HTMLDivElement;
function fetchPageUsers(page: number) {
if (!browser) return;
if (userTableContainerElement) userTableContainerElement.scrollTop = 0;
if (usersCache[page]) {
currentPageUsers = usersCache[page];
return;
}
// eslint-disable-next-line no-async-promise-executor
currentPageUsersRequest = new Promise(async (resolve, reject) => {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, {
method: 'POST',
body: JSON.stringify({
limit: usersPerPage,
from: usersPerPage * page
})
});
if (response.ok) {
const pageUsers = await response.json();
currentPageUsers = usersCache[page] = pageUsers;
resolve();
} else {
reject(Error());
}
});
}
async function sortUsers(key: string, reverse: boolean) {
const multiplyValue = reverse ? -1 : 1;
currentPageUsers.sort((entryA, entryB) => {
const a = entryA[key];
const b = entryB[key];
switch (typeof a) {
case 'number':
return (a - b) * multiplyValue;
case 'string':
return a.localeCompare(b) * multiplyValue;
default:
return (a - b) * multiplyValue;
}
});
currentPageUsers = currentPageUsers;
}
$: fetchPageUsers(userPage);
async function updateUser(user: typeof User.prototype.dataValues) {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, {
method: 'PATCH',
body: JSON.stringify(user)
});
if (!response.ok) {
throw new Error();
}
}
async function deleteUser(id: number) {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, {
method: 'DELETE',
body: JSON.stringify({
id: id
})
});
if (response.ok) {
currentPageUsers.splice(
currentPageUsers.findIndex((v) => v.id == id),
1
);
currentPageUsers = currentPageUsers;
} else {
throw new Error();
}
}
</script>
<div>
<div class="h-[90vh] overflow-scroll" bind:this={userTableContainerElement}>
<table class="table relative">
<thead>
<tr class="[&>th]:bg-base-100 [&>th]:z-[1] [&>th]:sticky [&>th]:top-0">
<th />
{#each headers as header}
<th>
<button
class="flex items-center"
on:click={() => {
sortUsers(header.key, (ascHeader = ascHeader == header ? null : header));
}}
>
{header.name}
<span class="ml-1">
<IconSolid
name={ascHeader === header ? 'chevron-up-solid' : 'chevron-down-solid'}
width="12"
height="12"
/>
</span>
</button>
</th>
{/each}
<th />
</tr>
</thead>
<tbody>
{#key currentPageUsersRequest}
{#await currentPageUsersRequest}
{#each Array(usersPerPage) as _, i}
<tr class="animate-pulse text-transparent">
<td>{i + 1}</td>
<td><Input type="text" disabled={true} size="sm" /></td>
<td><Input type="text" disabled={true} size="sm" /></td>
<td><Input type="date" disabled={true} size="sm" /></td>
<td><Input type="tel" disabled={true} size="sm" /></td>
<td><Input type="text" disabled={true} size="sm" /></td>
<td
><Select id="edition" disabled={true} size="sm">
<option value="java">Java Edition</option>
<option value="bedrock">Bedrock Edition</option>
<option value="cracked">Java cracked</option>
</Select></td
>
<td><Input type="text" disabled={true} size="sm" /></td>
<td><Input type="text" disabled={true} size="sm" /></td>
<td
><div class="flex gap-1">
<button class="btn btn-sm btn-square" disabled />
</div></td
>
</tr>
{/each}
{:then _}
{#each currentPageUsers as user, i}
<tr>
<td>{i + 1}</td>
<td>
<Input type="text" bind:value={user.firstname} disabled={!user.edit} size="sm" />
</td>
<td>
<Input type="text" bind:value={user.lastname} disabled={!user.edit} size="sm" />
</td>
<td>
<Input
type="date"
value={new Date(user.birthday).toISOString().split('T')[0]}
on:input={(e) => (user.birthday = e.detail.target.valueAsDate.toISOString())}
disabled={!user.edit}
size="sm"
/>
</td>
<td>
<Input type="tel" bind:value={user.telephone} disabled={!user.edit} size="sm" />
</td>
<td>
<Input type="text" bind:value={user.username} disabled={!user.edit} size="sm" />
</td>
<td>
<Select id="edition" bind:value={user.playertype} disabled={!user.edit} size="sm">
<option value="java">Java Edition</option>
<option value="bedrock">Bedrock Edition</option>
<option value="cracked">Java cracked</option>
</Select>
</td>
<td>
<Input type="text" bind:value={user.password} disabled={!user.edit} size="sm" />
</td>
<td>
<Input
id="uuid"
type="text"
bind:value={user.uuid}
disabled={!user.edit}
size="sm"
/>
</td>
<td>
<div class="flex gap-1">
{#if user.edit}
<button
class="btn btn-sm btn-square"
on:click={async (e) => {
await buttonTriggeredRequest(e, updateUser(user));
user.edit = false;
}}
>
<IconOutline name="check-outline" width="18" height="18" />
</button>
<button
class="btn btn-sm btn-square"
on:click={() => {
user.edit = false;
user = user.before;
}}
>
<IconOutline name="no-symbol-outline" width="18" height="18" />
</button>
{:else}
<button
class="btn btn-sm btn-square"
on:click={() => {
user.before = structuredClone(user);
user.edit = true;
}}
>
<IconOutline name="pencil-square-outline" width="18" height="18" />
</button>
<button
class="btn btn-sm btn-square"
on:click={(e) => buttonTriggeredRequest(e, deleteUser(user.id))}
>
<IconOutline name="trash-outline" width="18" height="18" />
</button>
{/if}
</div>
</td>
</tr>
{/each}
{/await}
{/key}
</tbody>
</table>
</div>
<div class="flex justify-center w-full mt-4 mb-6">
<div class="join">
{#each Array(Math.ceil(data.count / usersPerPage) || 1) as _, i}
<button
class="join-item btn"
class:btn-active={i === userPage}
on:click={() => {
userPage = i;
}}>{i + 1}</button
>
{/each}
</div>
</div>
</div>