update admin pagination
All checks were successful
delpoy / build-and-deploy (push) Successful in 1m16s

This commit is contained in:
bytedream 2024-11-28 01:44:35 +01:00
parent 676bfc23d8
commit bd33727aa6
6 changed files with 214 additions and 226 deletions

View File

@ -0,0 +1,51 @@
<script lang="ts">
import { onMount, tick } from 'svelte';
export let onUpdate: () => Promise<any> = Promise.resolve;
let bodyElem: HTMLTableSectionElement;
function intersectionViewer() {
let updating = false;
let intersectionElement =
bodyElem.rows.item(bodyElem.rows.length - 5) || bodyElem.lastElementChild;
new IntersectionObserver(
async (entries, observer) => {
if (entries.filter((e) => e.isIntersecting).length === 0 || updating) return;
updating = true;
const rows = bodyElem.rows.length;
await onUpdate();
await tick();
observer.disconnect();
updating = false;
if (rows === bodyElem.rows.length) return;
intersectionViewer();
},
{ threshold: 1.0 }
).observe(intersectionElement!);
}
onMount(async () => {
await onUpdate();
await tick();
new MutationObserver((entries) => {
if (entries.filter((e) => e.removedNodes.length > 0).length === 0) return;
intersectionViewer();
}).observe(bodyElem, { childList: true });
intersectionViewer();
});
</script>
<tbody bind:this={bodyElem}>
<slot />
</tbody>

View File

@ -16,30 +16,26 @@
import { goto } from '$app/navigation';
import Search from '$lib/components/Input/Search.svelte';
import { usernameSuggestions } from '$lib/utils';
import PaginationTableBody from '$lib/components/PaginationTable/PaginationTableBody.svelte';
export let data: PageData;
let currentPageReports: (typeof Report.prototype.dataValues)[] = [];
let currentPageTotal = 0;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let currentPageReportsRequest: Promise<any> = Promise.resolve();
let reportsPerPage = 50;
let reportPage = 0;
let reports: (typeof Report.prototype.dataValues)[] = [];
let reportsPerRequest = 25;
let reportFilter = { draft: false, status: null, reporter: null, reported: null };
let activeReport: typeof Report.prototype.dataValues | null = null;
async function fetchPageReports(
page: number,
filter: typeof reportFilter | { hash: string }
): Promise<{ reports: typeof currentPageReports; count: number }> {
async function fetchReports(
filter?: typeof reportFilter | { hash: string }
): Promise<{ reports: typeof reports; count: number }> {
if (!browser) return { reports: [], count: 0 };
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/reports`, {
method: 'POST',
body: JSON.stringify({
...filter,
limit: reportsPerPage,
from: reportsPerPage * page
...(filter ?? reportFilter),
limit: reportsPerRequest,
from: reports.length
})
});
@ -51,20 +47,15 @@
return await response.json();
}
$: currentPageReportsRequest = fetchPageReports(reportPage, reportFilter).then((r) => {
currentPageReports = r.reports;
currentPageTotal = r.count;
});
async function openHashReport() {
if (!window.location.hash) return;
const requestedHash = window.location.hash.substring(1);
let report = currentPageReports.find((r) => r.url_hash === requestedHash);
let report = reports.find((r) => r.url_hash === requestedHash);
if (!report) {
const hashReport = (await fetchPageReports(0, { hash: requestedHash })).reports[0];
const hashReport = (await fetchReports({ hash: requestedHash })).reports[0];
if (hashReport) {
currentPageReports = [hashReport, ...currentPageReports];
reports = [hashReport, ...reports];
report = hashReport;
} else {
await goto(window.location.href.split('#')[0], { replaceState: true });
@ -75,10 +66,8 @@
activeReport = report;
activeReport.originalStatus = report;
}
onMount(async () => {
await currentPageReportsRequest;
await openHashReport();
onMount(async () => {
if (browser) window.addEventListener('hashchange', openHashReport);
});
onDestroy(() => {
@ -102,6 +91,8 @@
let saveActiveReportChangesModal: HTMLDialogElement;
let newReportModal: HTMLDialogElement;
$: if (reportFilter) fetchReports().then((r) => (reports = r.reports));
</script>
<div class="h-full flex flex-row">
@ -127,8 +118,11 @@
<th>Reportstatus</th>
</tr>
</thead>
<tbody>
{#each currentPageReports as report}
<PaginationTableBody
onUpdate={async () =>
await fetchReports().then((res) => (reports = [...reports, ...res.reports]))}
>
{#each reports as report}
<tr
class="hover [&>*]:text-sm cursor-pointer"
class:bg-base-200={activeReport?.url_hash === report.url_hash}
@ -195,22 +189,8 @@
</div>
</td>
</tr>
</tbody>
</PaginationTableBody>
</table>
<div class="flex justify-center items-center mb-2 mt-4 w-full">
<div class="join">
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
{#each Array(currentPageReports.length === reportsPerPage || reportPage > 0 ? Math.ceil(currentPageTotal / reportsPerPage) || 1 : 1) as _, i}
<button
class="join-item btn"
class:btn-active={i === reportPage}
on:click={() => {
reportPage = i;
}}>{i + 1}</button
>
{/each}
</div>
</div>
</div>
{#if activeReport}
<div
@ -358,7 +338,7 @@
} else {
activeReport.reported = undefined;
}
currentPageReports = [...currentPageReports];
reports = [...reports];
if (activeReport.originalStatus !== 'reviewed' && activeReport.status === 'reviewed') {
$reportCount -= 1;
} else if (
@ -381,8 +361,8 @@
<NewReportModal
on:submit={(e) => {
if (!e.detail.draft) $reportCount += 1;
currentPageReports = [e.detail, ...currentPageReports];
activeReport = currentPageReports[0];
reports = [e.detail, ...reports];
activeReport = reports[0];
newReportModal.close();
}}
/>

View File

@ -4,81 +4,42 @@
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 type { Report, User } from '$lib/server/database';
import { buttonTriggeredRequest } from '$lib/components/utils';
import { browser } from '$app/environment';
import HeaderBar from './HeaderBar.svelte';
import SortableTr from '$lib/components/Table/SortableTr.svelte';
import SortableTh from '$lib/components/Table/SortableTh.svelte';
import NewUserModal from './NewUserModal.svelte';
import PaginationTableBody from '$lib/components/PaginationTable/PaginationTableBody.svelte';
export let data: PageData;
let currentPageUsers: (typeof User.prototype.dataValues)[] = [];
let currentPageUsersRequest: Promise<void> = new Promise((resolve) => resolve());
let usersPerPage = 200;
let userPage = 0;
let userFilter = { name: null, playertype: null };
let users: (typeof User.prototype.dataValues)[] = [];
let usersPerRequest = 25;
let userFilter: { [k: string]: any } = { name: null, playertype: null };
let userTableContainerElement: HTMLDivElement;
let newUserModal: HTMLDialogElement;
function fetchPageUsers(page: number) {
if (!browser) return;
async function fetchUsers(): Promise<typeof users> {
if (!browser) return [];
if (userTableContainerElement) userTableContainerElement.scrollTop = 0;
// 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({ ...userFilter, limit: usersPerPage, from: usersPerPage * page })
});
if (response.ok) {
currentPageUsers = await response.json();
resolve();
} else {
reject(Error());
}
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, {
method: 'POST',
body: JSON.stringify({ ...userFilter, limit: usersPerRequest, from: users.length })
});
return await response.json();
}
$: fetchPageUsers(userPage);
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
function fetchFilterPageUsers(_: any) {
userPage == 0 ? fetchPageUsers(0) : (userPage = 0);
}
$: fetchFilterPageUsers(userFilter);
let sortKey: string | null = null;
let sortAsc = false;
$: if (sortKey != null)
currentPageUsers = currentPageUsers.sort((entryA, entryB) => {
const multiplyValue = sortAsc ? -1 : 1;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const a = entryA[sortKey];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const b = entryB[sortKey];
switch (typeof a) {
case 'number':
return (a - b) * multiplyValue;
case 'string':
return a.localeCompare(b) * multiplyValue;
default:
return (a - b) * multiplyValue;
}
});
async function updateUser(user: typeof User.prototype.dataValues) {
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, {
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) {
@ -89,15 +50,15 @@
})
});
if (response.ok) {
currentPageUsers.splice(
currentPageUsers.findIndex((v) => v.id == id),
users.splice(
users.findIndex((v) => v.id == id),
1
);
currentPageUsers = currentPageUsers;
} else {
throw new Error();
users = users;
}
}
$: if (userFilter) fetchUsers().then((u) => (users = u));
</script>
<div class="h-full flex flex-col overflow-hidden">
@ -109,141 +70,120 @@
<!-- prettier-ignore -->
<SortableTr class="[&>th]:bg-base-100 [&>th]:z-[1] [&>th]:sticky [&>th]:top-0">
<th />
<SortableTh on:sort={(e) => { sortKey = 'firstname'; sortAsc = e.detail.asc }}>Vorname</SortableTh>
<SortableTh on:sort={(e) => { sortKey = 'lastname'; sortAsc = e.detail.asc }}>Nachname</SortableTh>
<SortableTh on:sort={(e) => { sortKey = 'birthday'; sortAsc = e.detail.asc }}>Geburtstag</SortableTh>
<SortableTh on:sort={(e) => { sortKey = 'telephone'; sortAsc = e.detail.asc }}>Telefon</SortableTh>
<SortableTh on:sort={(e) => { sortKey = 'username'; sortAsc = e.detail.asc }}>Username</SortableTh>
<SortableTh on:sort={(e) => { sortKey = 'playertype'; sortAsc = e.detail.asc }}>Minecraft Edition</SortableTh>
<SortableTh on:sort={(e) => { sortKey = 'password'; sortAsc = e.detail.asc }}>Passwort</SortableTh>
<SortableTh on:sort={(e) => { sortKey = 'uuid'; sortAsc = e.detail.asc }}>UUID</SortableTh>
<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 />
</SortableTr>
</thead>
<tbody>
{#key currentPageUsersRequest}
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
{#await currentPageUsersRequest then _}
{#each currentPageUsers as user, i}
<tr>
<td>{i + 1 + userPage * usersPerPage}</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="noauth">Java noauth</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;
}}
>
<Check size="18" />
</button>
<button
class="btn btn-sm btn-square"
on:click={() => {
user.edit = false;
user = user.before;
}}
>
<NoSymbol size="18" />
</button>
{:else}
<button
class="btn btn-sm btn-square"
on:click={() => {
user.before = structuredClone(user);
user.edit = true;
}}
>
<PencilSquare size="18" />
</button>
<button
class="btn btn-sm btn-square"
on:click={(e) => buttonTriggeredRequest(e, deleteUser(user.id))}
>
<Trash size="18" />
</button>
{/if}
</div>
</td>
</tr>
{/each}
{/await}
<PaginationTableBody
onUpdate={async () => {
await fetchUsers().then((u) => (users = [...users, ...u]));
}}
>
{#each users as user, i}
<tr>
<td colspan="100">
<div class="flex justify-center items-center">
<button class="btn btn-sm" on:click={() => newUserModal.show()}>
<Plus />
<span>Neuer Spieler</span>
</button>
<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="noauth">Java noauth</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;
}}
>
<Check size="18" />
</button>
<button
class="btn btn-sm btn-square"
on:click={() => {
user.edit = false;
user = user.before;
}}
>
<NoSymbol size="18" />
</button>
{:else}
<button
class="btn btn-sm btn-square"
on:click={() => {
user.before = structuredClone(user);
user.edit = true;
}}
>
<PencilSquare size="18" />
</button>
<button
class="btn btn-sm btn-square"
on:click={(e) => buttonTriggeredRequest(e, deleteUser(user.id))}
>
<Trash size="18" />
</button>
{/if}
</div>
</td>
</tr>
{/key}
</tbody>
</table>
<div class="flex justify-center items-center mb-2 mt-4 w-full">
<div class="join">
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
{#each Array(currentPageUsers.length === usersPerPage || userPage > 0 ? Math.ceil(data.count / usersPerPage) || 1 : 1) as _, i}
<button
class="join-item btn"
class:btn-active={i === userPage}
on:click={() => {
userPage = i;
}}>{i + 1}</button
>
{/each}
</div>
</div>
<tr>
<td colspan="100">
<div class="flex justify-center items-center">
<button class="btn btn-sm" on:click={() => newUserModal.show()}>
<Plus />
<span>Neuer Spieler</span>
</button>
</div>
</td>
</tr>
</PaginationTableBody>
</table>
</div>
</div>
<dialog class="modal" bind:this={newUserModal}>
<NewUserModal
on:submit={(e) => {
currentPageUsers = [...currentPageUsers, e.detail];
users = [...users, e.detail];
newUserModal.close();
}}
/>

View File

@ -43,7 +43,8 @@ export const POST = (async ({ request, cookies }) => {
where: usersFindOptions,
attributes: data.slim ? ['username', 'uuid'] : undefined,
offset: data.from || 0,
limit: data.limit || 100
limit: data.limit || 100,
order: data.sort ? [[data.sort.key, data.sort.asc ? 'ASC' : 'DESC']] : undefined
});
return new Response(JSON.stringify(users));

View File

@ -2,7 +2,7 @@
import Select from '$lib/components/Input/Select.svelte';
import Input from '$lib/components/Input/Input.svelte';
export let userFilter = {
export let userFilter: { [k: string]: any } = {
name: null,
playertype: null
};

View File

@ -8,7 +8,23 @@ export const UserListSchema = z.object({
playertype: z.enum(['java', 'bedrock', 'noauth']).nullish(),
search: z.string().nullish(),
slim: z.boolean().nullish()
slim: z.boolean().nullish(),
sort: z
.object({
key: z.enum([
'firstname',
'lastname',
'birthday',
'telephone',
'username',
'playertype',
'password',
'uuid'
]),
asc: z.boolean()
})
.nullish()
});
export const UserEditSchema = z.object({