refactor admin crud popups
All checks were successful
deploy / build-and-deploy (push) Successful in 23s

This commit is contained in:
2025-05-21 17:22:20 +02:00
parent 8b18623232
commit e47268111a
46 changed files with 889 additions and 1041 deletions

View File

@@ -1,95 +0,0 @@
<script lang="ts">
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
import Input from '@components/input/Input.svelte';
import { onDestroy } from 'svelte';
import type { User } from './types.ts';
import { userCreateOrEditPopupState } from './state.ts';
// html bindings
let modal: HTMLDialogElement;
let modalForm: HTMLFormElement;
// input
let action = $state<'create' | 'edit' | null>(null);
let user = $state({} as User);
let onUpdate = $state((_: User) => {});
// lifecycle
const cancel = userCreateOrEditPopupState.subscribe((value) => {
if (value && 'create' in value) {
action = 'create';
user = {
id: -1,
username: '',
firstname: '',
lastname: '',
birthday: new Date().toISOString().slice(0, 10),
telephone: '',
uuid: ''
};
onUpdate = value?.create.onUpdate;
modal.show();
} else if (value && 'edit' in value) {
action = 'edit';
user = value.edit.user;
onUpdate = value.edit.onUpdate;
modal.show();
}
});
onDestroy(cancel);
// texts
const texts = {
create: {
title: 'Nutzer erstellen',
buttonTitle: 'Erstellen',
confirmPopupTitle: 'Nutzer erstellen?',
confirmPopupMessage: 'Sollen der neue Nutzer erstellt werden?'
},
edit: {
title: 'Nutzer bearbeiten',
buttonTitle: 'Speichern',
confirmPopupTitle: 'Änderunge speichern?',
confirmPopupMessage: 'Sollen die Änderungen gespeichert werden?'
},
null: {}
};
// callbacks
function onSaveButtonClick(e: Event) {
e.preventDefault();
$confirmPopupState = {
title: texts[action!].confirmPopupTitle,
message: texts[action!].confirmPopupMessage,
onConfirm: () => {
modalForm.submit();
onUpdate(user);
}
};
}
</script>
<dialog class="modal" bind:this={modal}>
<form method="dialog" class="modal-box" bind:this={modalForm}>
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
<div class="space-y-5">
<h3 class="text-xl font-geist font-bold">{texts[action!].title}</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 w-full sm:w-fit gap-x-4 gap-y-2">
<Input type="text" bind:value={user.firstname} label="Vorname" />
<Input type="text" bind:value={user.lastname} label="Nachname" />
<Input type="date" bind:value={user.birthday} label="Geburtstag" />
<Input type="tel" bind:value={user.telephone} label="Telefonnummer" />
<Input type="text" bind:value={user.username} label="Spielername" />
<Input type="text" bind:value={user.uuid} label="UUID" />
</div>
<div>
<button class="btn btn-success" onclick={onSaveButtonClick}>{texts[action!].buttonTitle}</button>
<button class="btn btn-error">Abbrechen</button>
</div>
</div>
</form>
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.3)]">
<button class="!cursor-default">close</button>
</form>
</dialog>

View File

@@ -1,24 +1,18 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import { userCreateOrEditPopupState } from './state.ts';
import { addUser, fetchUsers } from './actions.ts';
import Input from '@components/input/Input.svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
import { addUser, fetchUsers } from '@app/admin/users/users.ts';
// states
let usernameFilter = $state<string | null>(null);
let createPopupOpen = $state(false);
// lifecycle
$effect(() => {
fetchUsers({ username: usernameFilter });
});
// callbacks
async function onNewUserButtonClick() {
$userCreateOrEditPopupState = {
create: {
onUpdate: addUser
}
};
}
</script>
<div>
@@ -27,8 +21,34 @@
<Input bind:value={usernameFilter} label="Username" />
</fieldset>
<div class="divider my-1"></div>
<button class="btn btn-soft w-full" onclick={() => onNewUserButtonClick()}>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
<Icon icon="heroicons:plus-16-solid" />
<span>Neuer Nutzer</span>
</button>
</div>
<CrudPopup
texts={{
title: 'Nutzer erstellen',
submitButtonTitle: 'Erstellen',
confirmPopupTitle: 'Nutzer erstellen?',
confirmPopupMessage: 'Sollen der neue Nutzer erstellt werden?'
}}
target={null}
keys={[
[
{ key: 'firstname', type: 'text', label: 'Vorname', options: { required: true } },
{ key: 'lastname', type: 'text', label: 'Nachname', options: { required: true } }
],
[
{ key: 'birthday', type: 'date', label: 'Geburtstag', options: { required: true } },
{ key: 'telephone', type: 'tel', label: 'Telefonnummer', default: null }
],
[
{ key: 'username', type: 'text', label: 'Spielername', options: { required: true } },
{ key: 'uuid', type: 'text', label: 'UUID', default: null }
]
]}
onSubmit={addUser}
bind:open={createPopupOpen}
/>

View File

@@ -1,56 +1,54 @@
<script lang="ts">
import Icon from '@iconify/svelte';
import CreateOrEditPopup from './CreateOrEditPopup.svelte';
import type { User } from './types.ts';
import { userCreateOrEditPopupState, users } from './state.ts';
import { editUser } from './actions.ts';
import SortableTr from '@components/admin/table/SortableTr.svelte';
import SortableTh from '@components/admin/table/SortableTh.svelte';
import DataTable from '@components/admin/table/DataTable.svelte';
import { editUser, users } from '@app/admin/users/users.ts';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
// callbacks
async function onUserEditButtonClick(user: User) {
$userCreateOrEditPopupState = {
edit: {
user: user,
onUpdate: editUser
}
};
}
// state
let editPopupUser = $state(null);
let editPopupOpen = $derived(!!editPopupUser);
// lifecycle
$effect(() => {
if (!editPopupOpen) editPopupUser = null;
});
</script>
<div class="h-screen overflow-x-auto">
<table class="table table-pin-rows">
<thead>
<SortableTr data={users}>
<SortableTh style="width: 5%">#</SortableTh>
<SortableTh style="width: 15%" key="firstname">Vorname</SortableTh>
<SortableTh style="width: 15%" key="lastname">Nachname</SortableTh>
<SortableTh style="width: 5%" key="birthday">Geburtstag</SortableTh>
<SortableTh style="width: 12%" key="phone">Telefon</SortableTh>
<SortableTh style="width: 20%" key="username">Username</SortableTh>
<SortableTh style="width: 23%">UUID</SortableTh>
<SortableTh style="width: 5%"></SortableTh>
</SortableTr>
</thead>
<tbody>
{#each $users as user, i (user.id)}
<tr class="hover:bg-base-200">
<td>{i + 1}</td>
<td>{user.firstname}</td>
<td>{user.lastname}</td>
<td>{user.birthday}</td>
<td>{user.telephone}</td>
<td>{user.username}</td>
<td>{user.uuid}</td>
<td>
<button class="cursor-pointer" onclick={() => onUserEditButtonClick(user)}>
<Icon icon="heroicons:pencil-square" />
</button>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<DataTable
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: 20, sortable: true },
{ key: 'uuid', label: 'UUID', width: 23 }
]}
onEdit={(user) => (editPopupUser = user)}
/>
<CreateOrEditPopup />
<CrudPopup
texts={{
title: 'Nutzer bearbeiten',
submitButtonTitle: 'Speichern',
confirmPopupTitle: 'Änderungen speichern?',
confirmPopupMessage: 'Sollen die Änderungen gespeichert werden?'
}}
target={editPopupUser}
keys={[
[
{ key: 'firstname', type: 'text', label: 'Vorname', options: { required: true } },
{ key: 'lastname', type: 'text', label: 'Nachname', options: { required: true } }
],
[
{ key: 'birthday', type: 'date', label: 'Geburtstag', options: { required: true } },
{ key: 'telephone', type: 'tel', label: 'Telefonnummer' }
],
[
{ key: 'username', type: 'text', label: 'Spielername', options: { required: true } },
{ key: 'uuid', type: 'text', label: 'UUID' }
]
]}
onSubmit={editUser}
bind:open={editPopupOpen}
/>

View File

@@ -1,6 +0,0 @@
import type { UserCreateOrEditPopupState, Users } from './types.ts';
import { writable } from 'svelte/store';
export const users = writable<Users>([]);
export const userCreateOrEditPopupState = writable<UserCreateOrEditPopupState>(null);

View File

@@ -1,9 +0,0 @@
import { type ActionReturnType, actions } from 'astro:actions';
export type Users = Exclude<ActionReturnType<typeof actions.user.users>['data'], undefined>['users'];
export type User = Users[0];
export type UserCreateOrEditPopupState =
| { create: { onUpdate: (user: User) => void } }
| { edit: { user: User; onUpdate: (user: User) => void } }
| null;

View File

@@ -1,8 +1,15 @@
import { actions } from 'astro:actions';
import { users } from './state.ts';
import type { User } from './types.ts';
import { writable } from 'svelte/store';
import { type ActionReturnType, actions } from 'astro:actions';
import { actionErrorPopup } from '@util/action.ts';
// types
export type Users = Exclude<ActionReturnType<typeof actions.user.users>['data'], undefined>['users'];
export type User = Users[0];
// state
export const users = writable<Users>([]);
// actions
export async function fetchUsers(options?: { username?: string | null }) {
const { data, error } = await actions.user.users({ username: options?.username });
if (error) {