make admin page mobile friendly
All checks were successful
deploy / build-and-deploy (push) Successful in 23s
All checks were successful
deploy / build-and-deploy (push) Successful in 23s
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Gemini 3 <google-gemini-noreply@google.com>
This commit is contained in:
@@ -28,8 +28,8 @@
|
|||||||
data={admins}
|
data={admins}
|
||||||
count={true}
|
count={true}
|
||||||
keys={[
|
keys={[
|
||||||
{ key: 'username', label: 'Username', width: 30 },
|
{ key: 'username', label: 'Username' },
|
||||||
{ key: 'permissions', label: 'Berechtigungen', width: 60, transform: permissionsBadge }
|
{ key: 'permissions', label: 'Berechtigungen', transform: permissionsBadge }
|
||||||
]}
|
]}
|
||||||
onEdit={(admin) => (editPopupAdmin = admin)}
|
onEdit={(admin) => (editPopupAdmin = admin)}
|
||||||
onDelete={onAdminDelete}
|
onDelete={onAdminDelete}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@
|
|||||||
data={blockedUsers}
|
data={blockedUsers}
|
||||||
count={true}
|
count={true}
|
||||||
keys={[
|
keys={[
|
||||||
{ key: 'uuid', label: 'UUID', width: 20, sortable: true },
|
{ key: 'uuid', label: 'UUID', sortable: true },
|
||||||
{ key: 'comment', label: 'Kommentar', width: 70 }
|
{ key: 'comment', label: 'Kommentar' }
|
||||||
]}
|
]}
|
||||||
onEdit={(blockedUser) => (blockedUserEditPopupBlockedUser = blockedUser)}
|
onEdit={(blockedUser) => (blockedUserEditPopupBlockedUser = blockedUser)}
|
||||||
onDelete={onBlockedUserDelete}
|
onDelete={onBlockedUserDelete}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
data={directInvitations}
|
data={directInvitations}
|
||||||
count={true}
|
count={true}
|
||||||
keys={[
|
keys={[
|
||||||
{ key: 'url', label: 'Link', width: 50 },
|
{ key: 'url', label: 'Link' },
|
||||||
{ key: 'user.username', label: 'Registrierter Nutzer', width: 40, sortable: true }
|
{ key: 'user.username', label: 'Registrierter Nutzer', sortable: true }
|
||||||
]}
|
]}
|
||||||
onDelete={onDirectInvitationDelete}
|
onDelete={onDirectInvitationDelete}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -12,15 +12,18 @@
|
|||||||
let { feedback }: Props = $props();
|
let { feedback }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative bg-base-200 rounded-lg w-[calc(100%-1rem)] m-2 flex px-6 py-4 gap-2" hidden={feedback === null}>
|
<div
|
||||||
|
class="relative bg-base-200 rounded-lg w-[calc(100%-1rem)] m-2 flex flex-col lg:flex-row px-4 lg:px-6 py-4 gap-4"
|
||||||
|
hidden={feedback === null}
|
||||||
|
>
|
||||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick={() => (feedback = null)}>✕</button>
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick={() => (feedback = null)}>✕</button>
|
||||||
<div class="w-96">
|
<div class="w-full lg:w-96">
|
||||||
<Input value={feedback?.event} label="Event" readonly />
|
<Input value={feedback?.event} label="Event" readonly />
|
||||||
<Input value={feedback?.title} label="Titel" readonly />
|
<Input value={feedback?.title} label="Titel" readonly />
|
||||||
<Input value={feedback?.username} label="Nutzer" readonly />
|
<Input value={feedback?.username} label="Nutzer" readonly />
|
||||||
</div>
|
</div>
|
||||||
<div class="divider divider-horizontal"></div>
|
<div class="divider lg:divider-horizontal"></div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<Textarea value={feedback?.content} label="Inhalt" rows={9} readonly dynamicWidth />
|
<Textarea value={feedback?.content} label="Inhalt" rows={6} readonly dynamicWidth />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,15 +20,15 @@
|
|||||||
{dateFormat.format(new Date(value))}
|
{dateFormat.format(new Date(value))}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class="h-screen flex flex-col justify-between">
|
<div class="h-full flex flex-col justify-between">
|
||||||
<DataTable
|
<DataTable
|
||||||
data={feedbacks}
|
data={feedbacks}
|
||||||
count={true}
|
count={true}
|
||||||
keys={[
|
keys={[
|
||||||
{ key: 'event', label: 'Event', width: 10, sortable: true },
|
{ key: 'event', label: 'Event', sortable: true },
|
||||||
{ key: 'username', label: 'Nutzer', width: 10, sortable: true },
|
{ key: 'username', label: 'Nutzer', sortable: true },
|
||||||
{ key: 'lastChanged', label: 'Datum', width: 10, sortable: true, transform: date },
|
{ key: 'lastChanged', label: 'Datum', sortable: true, transform: date },
|
||||||
{ key: 'content', label: 'Inhalt', width: 10 }
|
{ key: 'content', label: 'Inhalt' }
|
||||||
]}
|
]}
|
||||||
onClick={(feedback) => (activeFeedback = feedback)}
|
onClick={(feedback) => (activeFeedback = feedback)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -89,7 +89,10 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative bg-base-200 rounded-lg w-[calc(100%-1rem)] m-2 flex px-6 py-4 gap-2" hidden={report === null}>
|
<div
|
||||||
|
class="relative bg-base-200 rounded-lg w-[calc(100%-1rem)] m-2 flex flex-col lg:flex-row px-4 lg:px-6 py-4 gap-4"
|
||||||
|
hidden={report === null}
|
||||||
|
>
|
||||||
<div class="absolute right-2 top-2">
|
<div class="absolute right-2 top-2">
|
||||||
<div class="dropdown dropdown-end">
|
<div class="dropdown dropdown-end">
|
||||||
<div tabindex="0" role="button" class="btn btn-sm btn-circle btn-ghost">
|
<div tabindex="0" role="button" class="btn btn-sm btn-circle btn-ghost">
|
||||||
@@ -102,35 +105,38 @@
|
|||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-circle btn-ghost" onclick={() => (report = null)}>✕</button>
|
<button class="btn btn-sm btn-circle btn-ghost" onclick={() => (report = null)}>✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-[34rem]">
|
<div class="w-full lg:w-[34rem]">
|
||||||
<UserSearch value={report?.reporter.username} label="Report Ersteller" readonly mustMatch />
|
<UserSearch value={report?.reporter.username} label="Report Ersteller" readonly mustMatch />
|
||||||
<UserSearch
|
<UserSearch
|
||||||
value={report?.reported?.username}
|
value={report?.reported?.username}
|
||||||
label="Reporteter Spieler"
|
label="Reporteter Spieler"
|
||||||
onSubmit={(user) => (reportedUser = user)}
|
onSubmit={(user) => (reportedUser = user)}
|
||||||
/>
|
/>
|
||||||
<Textarea bind:value={notice} label="Interne Notizen" rows={10} />
|
<Textarea bind:value={notice} label="Interne Notizen" rows={6} />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<Input value={report?.reason} label="Grund" readonly dynamicWidth />
|
<Input value={report?.reason} label="Grund" readonly dynamicWidth />
|
||||||
<Textarea value={report?.body} label="Inhalt" readonly dynamicWidth rows={9} />
|
<Textarea value={report?.body} label="Inhalt" readonly dynamicWidth rows={6} />
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend class="fieldset-legend">Anhänge</legend>
|
<legend class="fieldset-legend">Anhänge</legend>
|
||||||
<div class="h-16.5 rounded border border-dashed flex">
|
<div class="min-h-16.5 rounded border border-dashed border-base-content/20 flex flex-wrap p-1">
|
||||||
{#each reportAttachments as reportAttachment (reportAttachment.hash)}
|
{#each reportAttachments as reportAttachment (reportAttachment.hash)}
|
||||||
<div>
|
<div>
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<div class="cursor-zoom-in" onclick={() => (previewReportAttachment = reportAttachment)}>
|
<div class="cursor-zoom-in p-1" onclick={() => (previewReportAttachment = reportAttachment)}>
|
||||||
{#if reportAttachment.type === 'image'}
|
{#if reportAttachment.type === 'image'}
|
||||||
<img
|
<img
|
||||||
src={location.pathname + '/attachment/' + reportAttachment.hash}
|
src={location.pathname + '/attachment/' + reportAttachment.hash}
|
||||||
alt={reportAttachment.hash}
|
alt={reportAttachment.hash}
|
||||||
class="w-16 h-16"
|
class="w-16 h-16 object-cover rounded"
|
||||||
/>
|
/>
|
||||||
{:else if reportAttachment.type === 'video'}
|
{:else if reportAttachment.type === 'video'}
|
||||||
<!-- svelte-ignore a11y_media_has_caption -->
|
<!-- svelte-ignore a11y_media_has_caption -->
|
||||||
<video src={location.pathname + '/attachment/' + reportAttachment.hash} class="w-16 h-16"></video>
|
<video
|
||||||
|
src={location.pathname + '/attachment/' + reportAttachment.hash}
|
||||||
|
class="w-16 h-16 object-cover rounded"
|
||||||
|
></video>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,9 +144,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider divider-horizontal"></div>
|
<div class="divider lg:divider-horizontal"></div>
|
||||||
<div class="flex flex-col w-[42rem]">
|
<div class="flex flex-col w-full lg:w-[42rem]">
|
||||||
<Textarea bind:value={statement} label="Öffentliche Report Antwort" dynamicWidth rows={7} />
|
<Textarea bind:value={statement} label="Öffentliche Report Antwort" dynamicWidth rows={6} />
|
||||||
<Select
|
<Select
|
||||||
bind:value={status}
|
bind:value={status}
|
||||||
values={{ open: 'In Bearbeitung', closed: 'Bearbeitet' }}
|
values={{ open: 'In Bearbeitung', closed: 'Bearbeitet' }}
|
||||||
@@ -150,7 +156,7 @@
|
|||||||
/>
|
/>
|
||||||
<Select bind:value={strikeReason} values={strikeReasonValues} label="Vergehen" dynamicWidth></Select>
|
<Select bind:value={strikeReason} values={strikeReasonValues} label="Vergehen" dynamicWidth></Select>
|
||||||
<div class="divider mt-0 mb-2"></div>
|
<div class="divider mt-0 mb-2"></div>
|
||||||
<button class="btn mt-auto" onclick={onSaveButtonClick}>Speichern</button>
|
<button class="btn btn-primary mt-auto" onclick={onSaveButtonClick}>Speichern</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import BottomBar from '@app/admin/reports/BottomBar.svelte';
|
import BottomBar from '@app/admin/reports/BottomBar.svelte';
|
||||||
import { onMount, untrack } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import DataTable from '@components/admin/table/DataTable.svelte';
|
import DataTable from '@components/admin/table/DataTable.svelte';
|
||||||
import { type StrikeReasons, getStrikeReasons, reports } from '@app/admin/reports/reports.ts';
|
import { type StrikeReasons, getStrikeReasons, reports } from '@app/admin/reports/reports.ts';
|
||||||
|
|
||||||
@@ -49,16 +49,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class="h-screen flex flex-col justify-between">
|
<div class="h-full flex flex-col justify-between">
|
||||||
<DataTable
|
<DataTable
|
||||||
data={reports}
|
data={reports}
|
||||||
count={true}
|
count={true}
|
||||||
keys={[
|
keys={[
|
||||||
{ key: 'reason', label: 'Grund', width: 35 },
|
{ key: 'reason', label: 'Grund' },
|
||||||
{ key: 'reporter.username', label: 'Report Ersteller', width: 15 },
|
{ key: 'reporter.username', label: 'Report Ersteller' },
|
||||||
{ key: 'reported.username', label: 'Reporteter Spieler', width: 15 },
|
{ key: 'reported.username', label: 'Reporteter Spieler' },
|
||||||
{ key: 'createdAt', label: 'Datum', width: 15, sortable: true, transform: date },
|
{ key: 'createdAt', label: 'Datum', sortable: true, transform: date },
|
||||||
{ key: 'status.status', label: 'Bearbeitungsstatus', width: 10, sortable: true, transform: status }
|
{ key: 'status.status', label: 'Bearbeitungsstatus', sortable: true, transform: status }
|
||||||
]}
|
]}
|
||||||
onClick={(report) => (activeReport = report)}
|
onClick={(report) => (activeReport = report)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -99,16 +99,16 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="h-full flex flex-col items-center justify-between">
|
<div class="min-h-full flex flex-col items-center justify-between">
|
||||||
<div class="grid grid-cols-2 w-full">
|
<div class="grid grid-cols-1 lg:grid-cols-2 w-full px-4 lg:px-12 gap-8">
|
||||||
{#each settingsInput as setting (setting.name)}
|
{#each settingsInput as setting (setting.name)}
|
||||||
<div class="mx-12">
|
<div class="flex flex-col">
|
||||||
<div class="divider">{setting.name}</div>
|
<div class="divider font-bold">{setting.name}</div>
|
||||||
<div class="flex flex-col gap-5">
|
<div class="flex flex-col gap-6">
|
||||||
{#each setting.entries as entry (entry.name)}
|
{#each setting.entries as entry (entry.name)}
|
||||||
<label class="flex justify-between">
|
<label class="flex flex-col sm:flex-row justify-between gap-2">
|
||||||
<span class="mt-[.125rem] text-sm w-1/2">{entry.name}</span>
|
<span class="text-sm font-medium sm:w-1/2">{entry.name}</span>
|
||||||
<div class="w-1/2">
|
<div class="sm:w-1/2 flex sm:justify-end">
|
||||||
{#if entry.type === 'checkbox'}
|
{#if entry.type === 'checkbox'}
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -122,7 +122,7 @@
|
|||||||
{:else if entry.type === 'text'}
|
{:else if entry.type === 'text'}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered"
|
class="input input-bordered w-full sm:max-w-xs"
|
||||||
onchange={(e) => {
|
onchange={(e) => {
|
||||||
entry.onChange(e.currentTarget.value);
|
entry.onChange(e.currentTarget.value);
|
||||||
changes = dynamicSettings.getChanges();
|
changes = dynamicSettings.getChanges();
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
/>
|
/>
|
||||||
{:else if entry.type === 'textarea'}
|
{:else if entry.type === 'textarea'}
|
||||||
<textarea
|
<textarea
|
||||||
class="textarea"
|
class="textarea textarea-bordered w-full sm:max-w-xs min-h-24"
|
||||||
value={entry.value}
|
value={entry.value}
|
||||||
onchange={(e) => {
|
onchange={(e) => {
|
||||||
entry.onChange(e.currentTarget.value);
|
entry.onChange(e.currentTarget.value);
|
||||||
@@ -146,9 +146,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="py-12">
|
||||||
<button
|
<button
|
||||||
class="btn btn-success mt-auto mb-8"
|
class="btn btn-success btn-lg px-12"
|
||||||
class:btn-disabled={Object.keys(changes).length === 0}
|
class:btn-disabled={Object.keys(changes).length === 0}
|
||||||
onclick={onSaveSettingsClick}>Speichern</button
|
onclick={onSaveSettingsClick}>Speichern</button
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -32,9 +32,9 @@
|
|||||||
data={strikeReasons}
|
data={strikeReasons}
|
||||||
count={true}
|
count={true}
|
||||||
keys={[
|
keys={[
|
||||||
{ key: 'name', label: 'Name', width: 20 },
|
{ key: 'name', label: 'Name' },
|
||||||
{ key: 'weight', label: 'Gewichtung', width: 50, sortable: true },
|
{ key: 'weight', label: 'Gewichtung', sortable: true },
|
||||||
{ key: 'id', label: 'Id', width: 20 }
|
{ key: 'id', label: 'Id' }
|
||||||
]}
|
]}
|
||||||
onDelete={onBlockedUserDelete}
|
onDelete={onBlockedUserDelete}
|
||||||
onEdit={(strikeReason) => (editPopupStrikeReason = strikeReason)}
|
onEdit={(strikeReason) => (editPopupStrikeReason = strikeReason)}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@
|
|||||||
<fieldset class="fieldset border border-base-200 rounded-box px-4">
|
<fieldset class="fieldset border border-base-200 rounded-box px-4">
|
||||||
<legend class="fieldset-legend">Account UUID finder</legend>
|
<legend class="fieldset-legend">Account UUID finder</legend>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex gap-3">
|
<div class="flex flex-col sm:flex-row gap-3">
|
||||||
<Input bind:value={username} />
|
<Input bind:value={username} dynamicWidth />
|
||||||
<Select bind:value={edition} values={{ java: 'Java', bedrock: 'Bedrock' }} />
|
<Select bind:value={edition} values={{ java: 'Java', bedrock: 'Bedrock' }} dynamicWidth />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<button class="btn w-4/6" class:disabled={!username} onclick={onSubmit}>UUID finden</button>
|
<button class="btn w-4/6" class:disabled={!username} onclick={onSubmit}>UUID finden</button>
|
||||||
|
|||||||
@@ -47,13 +47,13 @@
|
|||||||
data={users}
|
data={users}
|
||||||
count={true}
|
count={true}
|
||||||
keys={[
|
keys={[
|
||||||
{ key: 'firstname', label: 'Vorname', width: 15, sortable: true },
|
{ key: 'firstname', label: 'Vorname', sortable: true },
|
||||||
{ key: 'lastname', label: 'Nachname', width: 15, sortable: true },
|
{ key: 'lastname', label: 'Nachname', sortable: true },
|
||||||
{ key: 'birthday', label: 'Geburtstag', width: 5, sortable: true },
|
{ key: 'birthday', label: 'Geburtstag', sortable: true },
|
||||||
{ key: 'telephone', label: 'Telefon', width: 12, sortable: true },
|
{ key: 'telephone', label: 'Telefon', sortable: true },
|
||||||
{ key: 'username', label: 'Username', width: 15, sortable: true },
|
{ key: 'username', label: 'Username', sortable: true },
|
||||||
{ key: 'edition', label: 'Edition', width: 5, sortable: true, transform: edition },
|
{ key: 'edition', label: 'Edition', sortable: true, transform: edition },
|
||||||
{ key: 'uuid', label: 'UUID', width: 23 }
|
{ key: 'uuid', label: 'UUID' }
|
||||||
]}
|
]}
|
||||||
extraActions={blockAction}
|
extraActions={blockAction}
|
||||||
onEdit={(user) => (editPopupUser = user)}
|
onEdit={(user) => (editPopupUser = user)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts" generics="T">
|
||||||
import Input from '@components/input/Input.svelte';
|
import Input from '@components/input/Input.svelte';
|
||||||
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
|
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
|
||||||
import Textarea from '@components/input/Textarea.svelte';
|
import Textarea from '@components/input/Textarea.svelte';
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
let modalForm: HTMLFormElement;
|
let modalForm: HTMLFormElement;
|
||||||
|
|
||||||
// types
|
// types
|
||||||
interface Props<T> {
|
interface Props {
|
||||||
texts: {
|
texts: {
|
||||||
title: string;
|
title: string;
|
||||||
submitButtonTitle: string;
|
submitButtonTitle: string;
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
['number']: {};
|
['number']: {};
|
||||||
['password']: {};
|
['password']: {};
|
||||||
['select']: {
|
['select']: {
|
||||||
options: Record<string, string>;
|
values: Record<string, string>;
|
||||||
};
|
};
|
||||||
['tel']: {};
|
['tel']: {};
|
||||||
['text']: {};
|
['text']: {};
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// input
|
// input
|
||||||
let { texts, target, keys = [], onSubmit, onClose, open = $bindable() }: Props<any> = $props();
|
let { texts, target, keys = [], onSubmit, onClose, open = $bindable() }: Props = $props();
|
||||||
|
|
||||||
onInit();
|
onInit();
|
||||||
|
|
||||||
@@ -101,10 +101,12 @@
|
|||||||
let submitEnabled = $state(false);
|
let submitEnabled = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!open) return;
|
if (open) {
|
||||||
|
onInit();
|
||||||
onInit();
|
modal?.show();
|
||||||
modal.show();
|
} else {
|
||||||
|
modal?.close();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect.pre(() => {
|
$effect.pre(() => {
|
||||||
@@ -160,27 +162,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onModalClose() {
|
function onModalClose() {
|
||||||
setTimeout(() => {
|
open = false;
|
||||||
open = false;
|
target = null;
|
||||||
target = null;
|
modalForm.reset();
|
||||||
modalForm.reset();
|
onClose?.();
|
||||||
onClose?.();
|
}
|
||||||
}, 300);
|
|
||||||
|
function portal(node: HTMLElement) {
|
||||||
|
document.body.appendChild(node);
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
if (node.parentNode) node.parentNode.removeChild(node);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<dialog class="modal" bind:this={modal} onclose={onModalClose}>
|
<dialog class="modal fixed inset-0 z-[9999]" use:portal bind:this={modal} onclose={onModalClose}>
|
||||||
<form method="dialog" class="modal-box overflow-visible" bind:this={modalForm}>
|
<form method="dialog" class="modal-box overflow-visible" bind:this={modalForm}>
|
||||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick={onCancelButtonClick}>✕</button>
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick={onCancelButtonClick}>✕</button>
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<h3 class="text-xl font-geist font-bold">{texts.title}</h3>
|
<h3 class="text-xl font-geist font-bold">{texts.title}</h3>
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
{#each keys as key (key)}
|
{#each keys as key (key)}
|
||||||
<div
|
<div class="grid grid-cols-1 gap-x-4" class:md:grid-cols-2={key.length === 2}>
|
||||||
class="grid grid-flow-col gap-4"
|
|
||||||
class:grid-cols-1={key.length === 1}
|
|
||||||
class:grid-cols-2={key.length === 2}
|
|
||||||
>
|
|
||||||
{#each key as k (k)}
|
{#each key as k (k)}
|
||||||
{#if k.type === 'color' || k.type === 'date' || k.type === 'datetime-local' || k.type === 'number' || k.type === 'tel' || k.type === 'text'}
|
{#if k.type === 'color' || k.type === 'date' || k.type === 'datetime-local' || k.type === 'number' || k.type === 'tel' || k.type === 'text'}
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
keys: {
|
keys: {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
width?: number;
|
|
||||||
sortable?: boolean;
|
sortable?: boolean;
|
||||||
transform?: Snippet<[T]>;
|
transform?: Snippet<[T]>;
|
||||||
}[];
|
}[];
|
||||||
@@ -28,20 +27,20 @@
|
|||||||
let { data, count, keys, onClick, onEdit, onDelete, extraActions }: Props<any> = $props();
|
let { data, count, keys, onClick, onEdit, onDelete, extraActions }: Props<any> = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="max-h-screen overflow-x-auto">
|
<div class="max-h-full overflow-x-auto">
|
||||||
<table class="table table-pin-rows">
|
<table class="table table-pin-rows w-full table-auto lg:w-fit lg:min-w-full mx-auto">
|
||||||
<thead>
|
<thead>
|
||||||
<SortableTr {data}>
|
<SortableTr {data}>
|
||||||
{#if count}
|
{#if count}
|
||||||
<SortableTh style="width: 5%">#</SortableTh>
|
<SortableTh style="width: 1%" class="text-left">#</SortableTh>
|
||||||
{/if}
|
{/if}
|
||||||
{#each keys as key (key.key)}
|
{#each keys as key (key.key)}
|
||||||
<SortableTh style={key.width ? `width: ${key.width}%` : undefined} key={key.sortable ? key.key : undefined}
|
<SortableTh class="text-left whitespace-nowrap" key={key.sortable ? key.key : undefined}>
|
||||||
>{key.label}</SortableTh
|
{key.label}
|
||||||
>
|
</SortableTh>
|
||||||
{/each}
|
{/each}
|
||||||
{#if onEdit || onDelete || extraActions}
|
{#if onEdit || onDelete || extraActions}
|
||||||
<SortableTh style="width: 5%"></SortableTh>
|
<SortableTh style="width: 1%" class="text-left"></SortableTh>
|
||||||
{/if}
|
{/if}
|
||||||
</SortableTr>
|
</SortableTr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -49,10 +48,10 @@
|
|||||||
{#each $data as d, i (d)}
|
{#each $data as d, i (d)}
|
||||||
<tr class="hover:bg-base-200" onclick={() => onClick?.(d)}>
|
<tr class="hover:bg-base-200" onclick={() => onClick?.(d)}>
|
||||||
{#if count}
|
{#if count}
|
||||||
<td>{i + 1}</td>
|
<td class="text-left">{i + 1}</td>
|
||||||
{/if}
|
{/if}
|
||||||
{#each keys as key (key.key)}
|
{#each keys as key (key.key)}
|
||||||
<td>
|
<td class="text-left whitespace-nowrap">
|
||||||
{#if key.transform}
|
{#if key.transform}
|
||||||
{@render key.transform(getObjectEntryByKey(key.key, d))}
|
{@render key.transform(getObjectEntryByKey(key.key, d))}
|
||||||
{:else}
|
{:else}
|
||||||
@@ -61,7 +60,7 @@
|
|||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
{#if onEdit || onDelete || extraActions}
|
{#if onEdit || onDelete || extraActions}
|
||||||
<td class="px-3 whitespace-nowrap">
|
<td class="px-3 whitespace-nowrap text-left">
|
||||||
{#if extraActions}
|
{#if extraActions}
|
||||||
{@render extraActions(d)}
|
{@render extraActions(d)}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -31,9 +31,9 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<th {...restProps}>
|
<th {...restProps} class:text-left={true}>
|
||||||
{#if key}
|
{#if key}
|
||||||
<button class="flex items-center gap-1" onclick={() => onButtonClick()}>
|
<button class="flex items-center justify-start gap-1" onclick={() => onButtonClick()}>
|
||||||
<span>{@render children?.()}</span>
|
<span>{@render children?.()}</span>
|
||||||
{#if $headerKey === key && asc}
|
{#if $headerKey === key && asc}
|
||||||
<span class="iconify iconify-[heroicons--chevron-up-16-solid]"></span>
|
<span class="iconify iconify-[heroicons--chevron-up-16-solid]"></span>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
rows?: number;
|
rows?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { id, value = $bindable(), label, required, readonly, size, dynamicWidth, rows }: Props = $props();
|
let { id, value = $bindable(), label, required, readonly, size, dynamicWidth, rows = 3 }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</legend>
|
</legend>
|
||||||
<textarea
|
<textarea
|
||||||
{id}
|
{id}
|
||||||
class="textarea"
|
class="textarea min-h-16"
|
||||||
class:textarea-sm={size === 'sm'}
|
class:textarea-sm={size === 'sm'}
|
||||||
class:w-full={dynamicWidth}
|
class:w-full={dynamicWidth}
|
||||||
class:validator={required}
|
class:validator={required}
|
||||||
|
|||||||
@@ -82,66 +82,101 @@ const adminTabs = [
|
|||||||
|
|
||||||
<BaseLayout title={title}>
|
<BaseLayout title={title}>
|
||||||
<ClientRouter />
|
<ClientRouter />
|
||||||
<div class="flex flex-row max-h-screen h-screen overflow-hidden">
|
<div class="drawer lg:drawer-open">
|
||||||
<div class="bg-base-200 w-72 flex flex-col justify-between overflow-scroll">
|
<input id="admin-drawer" type="checkbox" class="drawer-toggle" />
|
||||||
<ul class="menu">
|
<div class="drawer-content flex flex-col h-screen overflow-hidden">
|
||||||
{
|
<!-- Mobile Navbar -->
|
||||||
preTabs.map((tab) => (
|
<div class="navbar bg-base-200 lg:hidden border-b border-base-300 shrink-0">
|
||||||
<li>
|
<div class="navbar-start">
|
||||||
<a href={tab.href}>
|
<label for="admin-drawer" class="btn btn-square btn-ghost drawer-button">
|
||||||
<span class="iconify" class:list={tab.iconClass} />
|
<span class="iconify iconify-[heroicons--bars-3]"></span>
|
||||||
<span>{tab.name}</span>
|
</label>
|
||||||
</a>
|
</div>
|
||||||
</li>
|
<div class="navbar-center">
|
||||||
))
|
<a class="text-xl font-bold">{title}</a>
|
||||||
}
|
</div>
|
||||||
<div class="divider mx-1 my-1"></div>
|
<div class="navbar-end">
|
||||||
{
|
<!-- Optional: Profile or logout shortcut -->
|
||||||
adminTabs.map(
|
</div>
|
||||||
(tab) =>
|
</div>
|
||||||
tab.enabled && (
|
|
||||||
|
<!-- Content Area -->
|
||||||
|
<div class="flex-1 overflow-auto relative p-4 lg:p-6">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar / Drawer Side -->
|
||||||
|
<div class="drawer-side z-20">
|
||||||
|
<label for="admin-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||||
|
<aside
|
||||||
|
class="bg-base-200 w-max max-w-[85vw] h-full flex flex-col justify-between border-r border-base-300 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col min-w-max">
|
||||||
|
<div class="p-4 hidden lg:block border-b border-base-300">
|
||||||
|
<span class="text-xl font-bold">Admin Panel</span>
|
||||||
|
</div>
|
||||||
|
<ul class="menu p-2">
|
||||||
|
{
|
||||||
|
preTabs.map((tab) => (
|
||||||
<li>
|
<li>
|
||||||
<a href={tab.href}>
|
<a href={tab.href}>
|
||||||
<span class="iconify" class:list={tab.iconClass} />
|
<span class="iconify" class:list={tab.iconClass} />
|
||||||
<span>{tab.name}</span>
|
<span>{tab.name}</span>
|
||||||
</a>
|
</a>
|
||||||
{tab.subTabs && (
|
|
||||||
<ul>
|
|
||||||
{tab.subTabs.map((subTab) => (
|
|
||||||
<li>
|
|
||||||
<a href={subTab.href}>
|
|
||||||
<span class="iconify" class:list={subTab.iconClass} />
|
|
||||||
<span>{subTab.name}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</li>
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<div class="divider mx-1 my-1"></div>
|
||||||
|
{
|
||||||
|
adminTabs.map(
|
||||||
|
(tab) =>
|
||||||
|
tab.enabled && (
|
||||||
|
<li>
|
||||||
|
<a href={tab.href}>
|
||||||
|
<span class="iconify" class:list={tab.iconClass} />
|
||||||
|
<span>{tab.name}</span>
|
||||||
|
</a>
|
||||||
|
{tab.subTabs && (
|
||||||
|
<ul>
|
||||||
|
{tab.subTabs.map((subTab) => (
|
||||||
|
<li>
|
||||||
|
<a href={subTab.href}>
|
||||||
|
<span class="iconify" class:list={subTab.iconClass} />
|
||||||
|
<span>{subTab.name}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
</ul>
|
||||||
</ul>
|
</div>
|
||||||
<ul class="menu w-full">
|
<div class="flex flex-col w-0 min-w-full">
|
||||||
{
|
<ul class="menu w-full p-2">
|
||||||
Astro.slots.has('actions') && (
|
{
|
||||||
<fieldset class="fieldset bg-base-300 border-base-100 rounded-box border p-2">
|
Astro.slots.has('actions') && (
|
||||||
<slot name="actions" />
|
<div class="mb-2">
|
||||||
</fieldset>
|
<span class="menu-title px-2 uppercase text-xs font-semibold opacity-60">Actions</span>
|
||||||
)
|
<fieldset class="fieldset bg-base-300 border-base-100 rounded-box border p-2">
|
||||||
}
|
<slot name="actions" />
|
||||||
<div class="divider mx-1 my-0"></div>
|
</fieldset>
|
||||||
<li>
|
</div>
|
||||||
<button id="logout">
|
)
|
||||||
<span class="iconify iconify-[heroicons--arrow-left-end-on-rectangle]"></span>
|
}
|
||||||
<span>Ausloggen</span>
|
<div class="divider mx-1 my-0"></div>
|
||||||
</button>
|
<li>
|
||||||
</li>
|
<button id="logout" class="text-error whitespace-nowrap">
|
||||||
</ul>
|
<span class="iconify iconify-[heroicons--arrow-left-end-on-rectangle]"></span>
|
||||||
</div>
|
<span>Ausloggen</span>
|
||||||
|
</button>
|
||||||
<div class="w-full overflow-scroll relative">
|
</li>
|
||||||
<slot />
|
</ul>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import Popup from '@components/popup/Popup.svelte';
|
|||||||
|
|
||||||
<AdminLoginLayout title="Login">
|
<AdminLoginLayout title="Login">
|
||||||
<div class="flex justify-center items-center w-full h-screen">
|
<div class="flex justify-center items-center w-full h-screen">
|
||||||
<div class="card w-96 px-6 py-6 shadow-lg">
|
<div class="card w-full max-w-sm px-6 py-6 shadow-lg">
|
||||||
<h1 class="text-3xl text-center">Admin Login</h1>
|
<h1 class="text-3xl text-center">Admin Login</h1>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<form id="login" class="flex flex-col items-center">
|
<form id="login" class="flex flex-col items-center">
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
|
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
|
||||||
import ConfirmPopup from '@components/popup/ConfirmPopup.svelte';
|
import ConfirmPopup from '@components/popup/ConfirmPopup.svelte';
|
||||||
import Popup from '@components/popup/Popup.svelte';
|
import Popup from '@components/popup/Popup.svelte';
|
||||||
import Select from '@components/input/Select.svelte';
|
|
||||||
import Textarea from '@components/input/Textarea.svelte';
|
import Textarea from '@components/input/Textarea.svelte';
|
||||||
import Input from '@components/input/Input.svelte';
|
import Input from '@components/input/Input.svelte';
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ const information = [
|
|||||||
<div>
|
<div>
|
||||||
<h2 class="text-3xl text-black dark:text-white mb-8">Über uns</h2>
|
<h2 class="text-3xl text-black dark:text-white mb-8">Über uns</h2>
|
||||||
<p>
|
<p>
|
||||||
Wir sind ein kleines <a class="link" href="admins">Team</a> von Minecraft-Enthusiasten, das bereits im 8. Jahr
|
Wir sind ein kleines <a class="link" href="admins">Team</a> von Minecraft-Enthusiasten, das bereits im 8. Jahr in
|
||||||
in Folge Minecraft CraftAttack organisiert. Jahr für Jahr arbeiten wir daran, das Spielerlebnis zu verbessern und
|
Folge Minecraft CraftAttack organisiert. Jahr für Jahr arbeiten wir daran, das Spielerlebnis zu verbessern und steigeren
|
||||||
steigeren die Teilnehmerzahl.
|
die Teilnehmerzahl.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Unser Ziel bei diesem ab dem <span class="italic"
|
Unser Ziel bei diesem ab dem <span class="italic"
|
||||||
|
|||||||
@@ -20,5 +20,4 @@ const signupDisabledSubMessage = signupSetting[SettingKey.SignupDisabledSubMessa
|
|||||||
message: signupDisabledMessage,
|
message: signupDisabledMessage,
|
||||||
subMessage: signupDisabledSubMessage
|
subMessage: signupDisabledSubMessage
|
||||||
}}
|
}}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user