make admin page mobile friendly
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:
2026-04-20 22:05:31 +02:00
parent 31ad92a6e2
commit fdc9b24800
20 changed files with 199 additions and 153 deletions

View File

@@ -28,8 +28,8 @@
data={admins}
count={true}
keys={[
{ key: 'username', label: 'Username', width: 30 },
{ key: 'permissions', label: 'Berechtigungen', width: 60, transform: permissionsBadge }
{ key: 'username', label: 'Username' },
{ key: 'permissions', label: 'Berechtigungen', transform: permissionsBadge }
]}
onEdit={(admin) => (editPopupAdmin = admin)}
onDelete={onAdminDelete}

View File

@@ -32,8 +32,8 @@
data={blockedUsers}
count={true}
keys={[
{ key: 'uuid', label: 'UUID', width: 20, sortable: true },
{ key: 'comment', label: 'Kommentar', width: 70 }
{ key: 'uuid', label: 'UUID', sortable: true },
{ key: 'comment', label: 'Kommentar' }
]}
onEdit={(blockedUser) => (blockedUserEditPopupBlockedUser = blockedUser)}
onDelete={onBlockedUserDelete}

View File

@@ -21,8 +21,8 @@
data={directInvitations}
count={true}
keys={[
{ key: 'url', label: 'Link', width: 50 },
{ key: 'user.username', label: 'Registrierter Nutzer', width: 40, sortable: true }
{ key: 'url', label: 'Link' },
{ key: 'user.username', label: 'Registrierter Nutzer', sortable: true }
]}
onDelete={onDirectInvitationDelete}
/>

View File

@@ -12,15 +12,18 @@
let { feedback }: Props = $props();
</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>
<div class="w-96">
<div class="w-full lg:w-96">
<Input value={feedback?.event} label="Event" readonly />
<Input value={feedback?.title} label="Titel" readonly />
<Input value={feedback?.username} label="Nutzer" readonly />
</div>
<div class="divider divider-horizontal"></div>
<div class="divider lg:divider-horizontal"></div>
<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>

View File

@@ -20,15 +20,15 @@
{dateFormat.format(new Date(value))}
{/snippet}
<div class="h-screen flex flex-col justify-between">
<div class="h-full flex flex-col justify-between">
<DataTable
data={feedbacks}
count={true}
keys={[
{ key: 'event', label: 'Event', width: 10, sortable: true },
{ key: 'username', label: 'Nutzer', width: 10, sortable: true },
{ key: 'lastChanged', label: 'Datum', width: 10, sortable: true, transform: date },
{ key: 'content', label: 'Inhalt', width: 10 }
{ key: 'event', label: 'Event', sortable: true },
{ key: 'username', label: 'Nutzer', sortable: true },
{ key: 'lastChanged', label: 'Datum', sortable: true, transform: date },
{ key: 'content', label: 'Inhalt' }
]}
onClick={(feedback) => (activeFeedback = feedback)}
/>

View File

@@ -89,7 +89,10 @@
}
</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="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-sm btn-circle btn-ghost">
@@ -102,35 +105,38 @@
</div>
<button class="btn btn-sm btn-circle btn-ghost" onclick={() => (report = null)}>✕</button>
</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?.reported?.username}
label="Reporteter Spieler"
onSubmit={(user) => (reportedUser = user)}
/>
<Textarea bind:value={notice} label="Interne Notizen" rows={10} />
<Textarea bind:value={notice} label="Interne Notizen" rows={6} />
</div>
<div class="w-full">
<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">
<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)}
<div>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- 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'}
<img
src={location.pathname + '/attachment/' + reportAttachment.hash}
alt={reportAttachment.hash}
class="w-16 h-16"
class="w-16 h-16 object-cover rounded"
/>
{:else if reportAttachment.type === 'video'}
<!-- 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}
</div>
</div>
@@ -138,9 +144,9 @@
</div>
</fieldset>
</div>
<div class="divider divider-horizontal"></div>
<div class="flex flex-col w-[42rem]">
<Textarea bind:value={statement} label="Öffentliche Report Antwort" dynamicWidth rows={7} />
<div class="divider lg:divider-horizontal"></div>
<div class="flex flex-col w-full lg:w-[42rem]">
<Textarea bind:value={statement} label="Öffentliche Report Antwort" dynamicWidth rows={6} />
<Select
bind:value={status}
values={{ open: 'In Bearbeitung', closed: 'Bearbeitet' }}
@@ -150,7 +156,7 @@
/>
<Select bind:value={strikeReason} values={strikeReasonValues} label="Vergehen" dynamicWidth></Select>
<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>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
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 { type StrikeReasons, getStrikeReasons, reports } from '@app/admin/reports/reports.ts';
@@ -49,16 +49,16 @@
{/if}
{/snippet}
<div class="h-screen flex flex-col justify-between">
<div class="h-full flex flex-col justify-between">
<DataTable
data={reports}
count={true}
keys={[
{ key: 'reason', label: 'Grund', width: 35 },
{ key: 'reporter.username', label: 'Report Ersteller', width: 15 },
{ key: 'reported.username', label: 'Reporteter Spieler', width: 15 },
{ key: 'createdAt', label: 'Datum', width: 15, sortable: true, transform: date },
{ key: 'status.status', label: 'Bearbeitungsstatus', width: 10, sortable: true, transform: status }
{ key: 'reason', label: 'Grund' },
{ key: 'reporter.username', label: 'Report Ersteller' },
{ key: 'reported.username', label: 'Reporteter Spieler' },
{ key: 'createdAt', label: 'Datum', sortable: true, transform: date },
{ key: 'status.status', label: 'Bearbeitungsstatus', sortable: true, transform: status }
]}
onClick={(report) => (activeReport = report)}
/>

View File

@@ -99,16 +99,16 @@
}
</script>
<div class="h-full flex flex-col items-center justify-between">
<div class="grid grid-cols-2 w-full">
<div class="min-h-full flex flex-col items-center justify-between">
<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)}
<div class="mx-12">
<div class="divider">{setting.name}</div>
<div class="flex flex-col gap-5">
<div class="flex flex-col">
<div class="divider font-bold">{setting.name}</div>
<div class="flex flex-col gap-6">
{#each setting.entries as entry (entry.name)}
<label class="flex justify-between">
<span class="mt-[.125rem] text-sm w-1/2">{entry.name}</span>
<div class="w-1/2">
<label class="flex flex-col sm:flex-row justify-between gap-2">
<span class="text-sm font-medium sm:w-1/2">{entry.name}</span>
<div class="sm:w-1/2 flex sm:justify-end">
{#if entry.type === 'checkbox'}
<input
type="checkbox"
@@ -122,7 +122,7 @@
{:else if entry.type === 'text'}
<input
type="text"
class="input input-bordered"
class="input input-bordered w-full sm:max-w-xs"
onchange={(e) => {
entry.onChange(e.currentTarget.value);
changes = dynamicSettings.getChanges();
@@ -131,7 +131,7 @@
/>
{:else if entry.type === 'textarea'}
<textarea
class="textarea"
class="textarea textarea-bordered w-full sm:max-w-xs min-h-24"
value={entry.value}
onchange={(e) => {
entry.onChange(e.currentTarget.value);
@@ -146,9 +146,9 @@
</div>
{/each}
</div>
<div>
<div class="py-12">
<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}
onclick={onSaveSettingsClick}>Speichern</button
>

View File

@@ -32,9 +32,9 @@
data={strikeReasons}
count={true}
keys={[
{ key: 'name', label: 'Name', width: 20 },
{ key: 'weight', label: 'Gewichtung', width: 50, sortable: true },
{ key: 'id', label: 'Id', width: 20 }
{ key: 'name', label: 'Name' },
{ key: 'weight', label: 'Gewichtung', sortable: true },
{ key: 'id', label: 'Id' }
]}
onDelete={onBlockedUserDelete}
onEdit={(strikeReason) => (editPopupStrikeReason = strikeReason)}

View File

@@ -17,9 +17,9 @@
<fieldset class="fieldset border border-base-200 rounded-box px-4">
<legend class="fieldset-legend">Account UUID finder</legend>
<div>
<div class="flex gap-3">
<Input bind:value={username} />
<Select bind:value={edition} values={{ java: 'Java', bedrock: 'Bedrock' }} />
<div class="flex flex-col sm:flex-row gap-3">
<Input bind:value={username} dynamicWidth />
<Select bind:value={edition} values={{ java: 'Java', bedrock: 'Bedrock' }} dynamicWidth />
</div>
<div class="flex justify-center">
<button class="btn w-4/6" class:disabled={!username} onclick={onSubmit}>UUID finden</button>

View File

@@ -47,13 +47,13 @@
data={users}
count={true}
keys={[
{ key: 'firstname', label: 'Vorname', width: 15, sortable: true },
{ key: 'lastname', label: 'Nachname', width: 15, sortable: true },
{ key: 'birthday', label: 'Geburtstag', width: 5, sortable: true },
{ key: 'telephone', label: 'Telefon', width: 12, sortable: true },
{ key: 'username', label: 'Username', width: 15, sortable: true },
{ key: 'edition', label: 'Edition', width: 5, sortable: true, transform: edition },
{ key: 'uuid', label: 'UUID', width: 23 }
{ key: 'firstname', label: 'Vorname', sortable: true },
{ key: 'lastname', label: 'Nachname', sortable: true },
{ key: 'birthday', label: 'Geburtstag', sortable: true },
{ key: 'telephone', label: 'Telefon', sortable: true },
{ key: 'username', label: 'Username', sortable: true },
{ key: 'edition', label: 'Edition', sortable: true, transform: edition },
{ key: 'uuid', label: 'UUID' }
]}
extraActions={blockAction}
onEdit={(user) => (editPopupUser = user)}

View File

@@ -1,4 +1,4 @@
<script lang="ts">
<script lang="ts" generics="T">
import Input from '@components/input/Input.svelte';
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
import Textarea from '@components/input/Textarea.svelte';
@@ -14,7 +14,7 @@
let modalForm: HTMLFormElement;
// types
interface Props<T> {
interface Props {
texts: {
title: string;
submitButtonTitle: string;
@@ -75,7 +75,7 @@
['number']: {};
['password']: {};
['select']: {
options: Record<string, string>;
values: Record<string, string>;
};
['tel']: {};
['text']: {};
@@ -93,7 +93,7 @@
};
// input
let { texts, target, keys = [], onSubmit, onClose, open = $bindable() }: Props<any> = $props();
let { texts, target, keys = [], onSubmit, onClose, open = $bindable() }: Props = $props();
onInit();
@@ -101,10 +101,12 @@
let submitEnabled = $state(false);
$effect(() => {
if (!open) return;
if (open) {
onInit();
modal.show();
modal?.show();
} else {
modal?.close();
}
});
$effect.pre(() => {
@@ -160,27 +162,30 @@
}
function onModalClose() {
setTimeout(() => {
open = false;
target = null;
modalForm.reset();
onClose?.();
}, 300);
}
function portal(node: HTMLElement) {
document.body.appendChild(node);
return {
destroy() {
if (node.parentNode) node.parentNode.removeChild(node);
}
};
}
</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}>
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick={onCancelButtonClick}>✕</button>
<div class="space-y-5">
<h3 class="text-xl font-geist font-bold">{texts.title}</h3>
<div class="w-full flex flex-col">
{#each keys as key (key)}
<div
class="grid grid-flow-col gap-4"
class:grid-cols-1={key.length === 1}
class:grid-cols-2={key.length === 2}
>
<div class="grid grid-cols-1 gap-x-4" class:md:grid-cols-2={key.length === 2}>
{#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'}
<Input

View File

@@ -13,7 +13,6 @@
keys: {
key: string;
label: string;
width?: number;
sortable?: boolean;
transform?: Snippet<[T]>;
}[];
@@ -28,20 +27,20 @@
let { data, count, keys, onClick, onEdit, onDelete, extraActions }: Props<any> = $props();
</script>
<div class="max-h-screen overflow-x-auto">
<table class="table table-pin-rows">
<div class="max-h-full overflow-x-auto">
<table class="table table-pin-rows w-full table-auto lg:w-fit lg:min-w-full mx-auto">
<thead>
<SortableTr {data}>
{#if count}
<SortableTh style="width: 5%">#</SortableTh>
<SortableTh style="width: 1%" class="text-left">#</SortableTh>
{/if}
{#each keys as key (key.key)}
<SortableTh style={key.width ? `width: ${key.width}%` : undefined} key={key.sortable ? key.key : undefined}
>{key.label}</SortableTh
>
<SortableTh class="text-left whitespace-nowrap" key={key.sortable ? key.key : undefined}>
{key.label}
</SortableTh>
{/each}
{#if onEdit || onDelete || extraActions}
<SortableTh style="width: 5%"></SortableTh>
<SortableTh style="width: 1%" class="text-left"></SortableTh>
{/if}
</SortableTr>
</thead>
@@ -49,10 +48,10 @@
{#each $data as d, i (d)}
<tr class="hover:bg-base-200" onclick={() => onClick?.(d)}>
{#if count}
<td>{i + 1}</td>
<td class="text-left">{i + 1}</td>
{/if}
{#each keys as key (key.key)}
<td>
<td class="text-left whitespace-nowrap">
{#if key.transform}
{@render key.transform(getObjectEntryByKey(key.key, d))}
{:else}
@@ -61,7 +60,7 @@
</td>
{/each}
{#if onEdit || onDelete || extraActions}
<td class="px-3 whitespace-nowrap">
<td class="px-3 whitespace-nowrap text-left">
{#if extraActions}
{@render extraActions(d)}
{/if}

View File

@@ -31,9 +31,9 @@
}
</script>
<th {...restProps}>
<th {...restProps} class:text-left={true}>
{#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>
{#if $headerKey === key && asc}
<span class="iconify iconify-[heroicons--chevron-up-16-solid]"></span>

View File

@@ -12,7 +12,7 @@
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>
<fieldset class="fieldset">
@@ -24,7 +24,7 @@
</legend>
<textarea
{id}
class="textarea"
class="textarea min-h-16"
class:textarea-sm={size === 'sm'}
class:w-full={dynamicWidth}
class:validator={required}

View File

@@ -82,9 +82,41 @@ const adminTabs = [
<BaseLayout title={title}>
<ClientRouter />
<div class="flex flex-row max-h-screen h-screen overflow-hidden">
<div class="bg-base-200 w-72 flex flex-col justify-between overflow-scroll">
<ul class="menu">
<div class="drawer lg:drawer-open">
<input id="admin-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col h-screen overflow-hidden">
<!-- Mobile Navbar -->
<div class="navbar bg-base-200 lg:hidden border-b border-base-300 shrink-0">
<div class="navbar-start">
<label for="admin-drawer" class="btn btn-square btn-ghost drawer-button">
<span class="iconify iconify-[heroicons--bars-3]"></span>
</label>
</div>
<div class="navbar-center">
<a class="text-xl font-bold">{title}</a>
</div>
<div class="navbar-end">
<!-- Optional: Profile or logout shortcut -->
</div>
</div>
<!-- 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>
@@ -122,26 +154,29 @@ const adminTabs = [
)
}
</ul>
<ul class="menu w-full">
</div>
<div class="flex flex-col w-0 min-w-full">
<ul class="menu w-full p-2">
{
Astro.slots.has('actions') && (
<div class="mb-2">
<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" />
</fieldset>
</div>
)
}
<div class="divider mx-1 my-0"></div>
<li>
<button id="logout">
<button id="logout" class="text-error whitespace-nowrap">
<span class="iconify iconify-[heroicons--arrow-left-end-on-rectangle]"></span>
<span>Ausloggen</span>
</button>
</li>
</ul>
</div>
<div class="w-full overflow-scroll relative">
<slot />
</aside>
</div>
</div>
</BaseLayout>

View File

@@ -7,7 +7,7 @@ import Popup from '@components/popup/Popup.svelte';
<AdminLoginLayout title="Login">
<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>
<div class="divider"></div>
<form id="login" class="flex flex-col items-center">

View File

@@ -2,7 +2,6 @@
import WebsiteLayout from '@layouts/website/WebsiteLayout.astro';
import ConfirmPopup from '@components/popup/ConfirmPopup.svelte';
import Popup from '@components/popup/Popup.svelte';
import Select from '@components/input/Select.svelte';
import Textarea from '@components/input/Textarea.svelte';
import Input from '@components/input/Input.svelte';
---

View File

@@ -79,9 +79,9 @@ const information = [
<div>
<h2 class="text-3xl text-black dark:text-white mb-8">Über uns</h2>
<p>
Wir sind ein kleines <a class="link" href="admins">Team</a> von Minecraft-Enthusiasten, das bereits im 8. Jahr
in Folge Minecraft CraftAttack organisiert. Jahr für Jahr arbeiten wir daran, das Spielerlebnis zu verbessern und
steigeren die Teilnehmerzahl.
Wir sind ein kleines <a class="link" href="admins">Team</a> von Minecraft-Enthusiasten, das bereits im 8. Jahr in
Folge Minecraft CraftAttack organisiert. Jahr für Jahr arbeiten wir daran, das Spielerlebnis zu verbessern und steigeren
die Teilnehmerzahl.
</p>
<p>
Unser Ziel bei diesem ab dem <span class="italic"

View File

@@ -20,5 +20,4 @@ const signupDisabledSubMessage = signupSetting[SettingKey.SignupDisabledSubMessa
message: signupDisabledMessage,
subMessage: signupDisabledSubMessage
}}
}
/>