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

View File

@ -4,81 +4,42 @@
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 { User } from '$lib/server/database'; import type { Report, 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';
import SortableTr from '$lib/components/Table/SortableTr.svelte'; import SortableTr from '$lib/components/Table/SortableTr.svelte';
import SortableTh from '$lib/components/Table/SortableTh.svelte'; import SortableTh from '$lib/components/Table/SortableTh.svelte';
import NewUserModal from './NewUserModal.svelte'; import NewUserModal from './NewUserModal.svelte';
import PaginationTableBody from '$lib/components/PaginationTable/PaginationTableBody.svelte';
export let data: PageData; export let data: PageData;
let currentPageUsers: (typeof User.prototype.dataValues)[] = []; let users: (typeof User.prototype.dataValues)[] = [];
let currentPageUsersRequest: Promise<void> = new Promise((resolve) => resolve()); let usersPerRequest = 25;
let usersPerPage = 200; let userFilter: { [k: string]: any } = { name: null, playertype: null };
let userPage = 0;
let userFilter = { name: null, playertype: null };
let userTableContainerElement: HTMLDivElement; let userTableContainerElement: HTMLDivElement;
let newUserModal: HTMLDialogElement; let newUserModal: HTMLDialogElement;
function fetchPageUsers(page: number) { async function fetchUsers(): Promise<typeof users> {
if (!browser) return; if (!browser) return [];
if (userTableContainerElement) userTableContainerElement.scrollTop = 0; if (userTableContainerElement) userTableContainerElement.scrollTop = 0;
// eslint-disable-next-line no-async-promise-executor const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, {
currentPageUsersRequest = new Promise(async (resolve, reject) => { method: 'POST',
const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, { body: JSON.stringify({ ...userFilter, limit: usersPerRequest, from: users.length })
method: 'POST',
body: JSON.stringify({ ...userFilter, limit: usersPerPage, from: usersPerPage * page })
});
if (response.ok) {
currentPageUsers = await response.json();
resolve();
} else {
reject(Error());
}
}); });
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) { 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', method: 'PATCH',
body: JSON.stringify(user) body: JSON.stringify(user)
}); });
if (!response.ok) {
throw new Error();
}
} }
async function deleteUser(id: number) { async function deleteUser(id: number) {
@ -89,15 +50,15 @@
}) })
}); });
if (response.ok) { if (response.ok) {
currentPageUsers.splice( users.splice(
currentPageUsers.findIndex((v) => v.id == id), users.findIndex((v) => v.id == id),
1 1
); );
currentPageUsers = currentPageUsers; users = users;
} else {
throw new Error();
} }
} }
$: if (userFilter) fetchUsers().then((u) => (users = u));
</script> </script>
<div class="h-full flex flex-col overflow-hidden"> <div class="h-full flex flex-col overflow-hidden">
@ -109,141 +70,120 @@
<!-- 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 />
<SortableTh on:sort={(e) => { sortKey = 'firstname'; sortAsc = e.detail.asc }}>Vorname</SortableTh> <SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'firstname', asc: e.detail.asc}}}>Vorname</SortableTh>
<SortableTh on:sort={(e) => { sortKey = 'lastname'; sortAsc = e.detail.asc }}>Nachname</SortableTh> <SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'lastname', asc: e.detail.asc}}}>Nachname</SortableTh>
<SortableTh on:sort={(e) => { sortKey = 'birthday'; sortAsc = e.detail.asc }}>Geburtstag</SortableTh> <SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'birthday', asc: e.detail.asc}}}>Geburtstag</SortableTh>
<SortableTh on:sort={(e) => { sortKey = 'telephone'; sortAsc = e.detail.asc }}>Telefon</SortableTh> <SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'telephone', asc: e.detail.asc}}}>Telefon</SortableTh>
<SortableTh on:sort={(e) => { sortKey = 'username'; sortAsc = e.detail.asc }}>Username</SortableTh> <SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'username', asc: e.detail.asc}}}>Username</SortableTh>
<SortableTh on:sort={(e) => { sortKey = 'playertype'; sortAsc = e.detail.asc }}>Minecraft Edition</SortableTh> <SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'playertype', asc: e.detail.asc}}}>Minecraft Edition</SortableTh>
<SortableTh on:sort={(e) => { sortKey = 'password'; sortAsc = e.detail.asc }}>Passwort</SortableTh> <SortableTh on:sort={(e) => userFilter = {...userFilter, sort: {key: 'password', asc: 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: 'uuid', asc: e.detail.asc}}}>UUID</SortableTh>
<th /> <th />
</SortableTr> </SortableTr>
</thead> </thead>
<tbody> <PaginationTableBody
{#key currentPageUsersRequest} onUpdate={async () => {
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars --> await fetchUsers().then((u) => (users = [...users, ...u]));
{#await currentPageUsersRequest then _} }}
{#each currentPageUsers as user, i} >
<tr> {#each users as user, i}
<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}
<tr> <tr>
<td colspan="100"> <td>{i + 1}</td>
<div class="flex justify-center items-center"> <td>
<button class="btn btn-sm" on:click={() => newUserModal.show()}> <Input type="text" bind:value={user.firstname} disabled={!user.edit} size="sm" />
<Plus /> </td>
<span>Neuer Spieler</span> <td>
</button> <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> </div>
</td> </td>
</tr> </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} {/each}
</div> <tr>
</div> <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>
</div> </div>
<dialog class="modal" bind:this={newUserModal}> <dialog class="modal" bind:this={newUserModal}>
<NewUserModal <NewUserModal
on:submit={(e) => { on:submit={(e) => {
currentPageUsers = [...currentPageUsers, e.detail]; users = [...users, e.detail];
newUserModal.close(); newUserModal.close();
}} }}
/> />

View File

@ -43,7 +43,8 @@ export const POST = (async ({ request, cookies }) => {
where: usersFindOptions, where: usersFindOptions,
attributes: data.slim ? ['username', 'uuid'] : undefined, attributes: data.slim ? ['username', 'uuid'] : undefined,
offset: data.from || 0, 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)); return new Response(JSON.stringify(users));

View File

@ -2,7 +2,7 @@
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 = { export let userFilter: { [k: string]: any } = {
name: null, name: null,
playertype: null playertype: null
}; };

View File

@ -8,7 +8,23 @@ export const UserListSchema = z.object({
playertype: z.enum(['java', 'bedrock', 'noauth']).nullish(), playertype: z.enum(['java', 'bedrock', 'noauth']).nullish(),
search: z.string().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({ export const UserEditSchema = z.object({