add death to admin ui
All checks were successful
deploy / build-and-deploy (/testvaro, /opt/website-test, website-test) (push) Successful in 14s
deploy / build-and-deploy (/varo, /opt/website, website) (push) Successful in 14s

This commit is contained in:
2025-06-25 16:08:03 +02:00
parent 7a0db65f78
commit 6789a65285
12 changed files with 296 additions and 9 deletions

View File

@ -128,5 +128,53 @@ export const team = {
teams: await db.getTeams(input)
};
}
}),
addDeath: defineAction({
input: z.object({
deadUserId: z.number(),
killerUserId: z.number().nullish(),
message: z.string()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
const { id } = await db.addDeath(input);
return {
id: id
};
}
}),
editDeath: defineAction({
input: z.object({
id: z.number(),
deadUserId: z.number(),
killerUserId: z.number().nullish(),
message: z.string()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
await db.editDeath(input);
}
}),
deleteDeath: defineAction({
input: z.object({
id: z.number()
}),
handler: async (input, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
await db.deleteDeath(input);
}
}),
deaths: defineAction({
handler: async (_, context) => {
Session.actionSessionFromCookies(context.cookies, Permissions.Users);
return {
deaths: await db.getDeaths({})
};
}
})
};

View File

@ -6,7 +6,7 @@
import TeamSearch from '@components/admin/search/TeamSearch.svelte';
import { editReportStatus, getReportStatus } from '@app/admin/reports/reports.ts';
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
import Icon from "@iconify/svelte";
import Icon from '@iconify/svelte';
// html bindings
let previewDialogElem: HTMLDialogElement;

View File

@ -0,0 +1,49 @@
<script lang="ts">
import { addDeath, fetchDeaths } from '@app/admin/teamDeaths/teamDeaths.ts';
import Icon from '@iconify/svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
// states
let createPopupOpen = $state(false);
// lifecycle
$effect(() => {
fetchDeaths();
});
</script>
<div>
<button class="btn btn-soft w-full" onclick={() => (createPopupOpen = true)}>
<Icon icon="heroicons:plus-16-solid" />
<span>Neuer Spielertod</span>
</button>
</div>
<CrudPopup
texts={{
title: 'Spielertod erstellen',
submitButtonTitle: 'Erstellen',
confirmPopupTitle: 'Spielertod erstellen?',
confirmPopupMessage: 'Soll der neue Spielertod erstellt werden?'
}}
target={null}
keys={[
[
{
key: 'killed',
type: 'user-search',
label: 'Getöteter Spieler',
options: { required: true, validate: (user) => !!user?.id }
},
{
key: 'killer',
type: 'user-search',
label: 'Killer',
options: { validate: (user) => (user?.username ? !!user?.id : true) }
}
],
[{ key: 'message', type: 'textarea', label: 'Todesnachricht', options: { required: true, dynamicWidth: true } }]
]}
onSubmit={addDeath}
bind:open={createPopupOpen}
/>

View File

@ -0,0 +1,69 @@
<script lang="ts">
// state
import { type Death, deaths, deleteDeath, editDeath } from '@app/admin/teamDeaths/teamDeaths.ts';
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
import DataTable from '@components/admin/table/DataTable.svelte';
import CrudPopup from '@components/admin/popup/CrudPopup.svelte';
let editPopupDeath = $state(null);
let editPopupOpen = $derived(!!editPopupDeath);
// lifecycle
$effect(() => {
if (!editPopupOpen) editPopupDeath = null;
});
// callbacks
function onDeathDelete(death: Death) {
$confirmPopupState = {
title: 'Tod löschen?',
message: 'Soll der Tod wirklich gelöscht werden?',
onConfirm: () => deleteDeath(death)
};
}
</script>
{#snippet username(user?: { id: number; username: string })}
{user?.username}
{/snippet}
<DataTable
data={deaths}
count={true}
keys={[
{ key: 'killed', label: 'Getöteter Spieler', width: 20, transform: username },
{ key: 'killer', label: 'Killer', width: 20, transform: username },
{ key: 'message', label: 'Todesnachricht', width: 50 }
]}
onEdit={(death) => (editPopupDeath = death)}
onDelete={onDeathDelete}
/>
<CrudPopup
texts={{
title: 'Tod bearbeiten',
submitButtonTitle: 'Speichern',
confirmPopupTitle: 'Änderungen speichern?',
confirmPopupMessage: 'Sollen die Änderungen gespeichert werden?'
}}
target={editPopupDeath}
keys={[
[
{
key: 'killed',
type: 'user-search',
label: 'Getöteter Spieler',
options: { required: true, validate: (user) => !!user?.id }
},
{
key: 'killer',
type: 'user-search',
label: 'Killer',
options: { validate: (user) => (user?.username ? !!user?.id : true) }
}
],
[{ key: 'message', type: 'textarea', label: 'Todesnachricht', options: { required: true, dynamicWidth: true } }]
]}
onSubmit={editDeath}
bind:open={editPopupDeath}
/>

View File

@ -0,0 +1,61 @@
import { type ActionReturnType, actions } from 'astro:actions';
import { writable } from 'svelte/store';
import { actionErrorPopup } from '@util/action.ts';
import { addToWritableArray, deleteFromWritableArray, updateWritableArray } from '@util/state.ts';
// types
export type Deaths = Exclude<ActionReturnType<typeof actions.team.deaths>['data'], undefined>['deaths'];
export type Death = Deaths[0];
// state
export const deaths = writable<Deaths>([]);
// actions
export async function fetchDeaths() {
const { data, error } = await actions.team.deaths();
if (error) {
actionErrorPopup(error);
return;
}
deaths.set(data.deaths);
}
export async function addDeath(death: Death) {
const { data, error } = await actions.team.addDeath({
deadUserId: death.killed.id,
killerUserId: death.killer?.id,
message: death.message
});
if (error) {
actionErrorPopup(error);
return;
}
addToWritableArray(deaths, Object.assign(death, { id: data.id }));
}
export async function editDeath(death: Death) {
const { error } = await actions.team.editDeath({
id: death.id,
deadUserId: death.killed.id,
killerUserId: death.killer?.id,
message: death.message
});
if (error) {
actionErrorPopup(error);
return;
}
updateWritableArray(deaths, death, (d) => d.id == death.id);
}
export async function deleteDeath(death: Death) {
const { error } = await actions.team.deleteDeath({ id: death.id });
if (error) {
actionErrorPopup(error);
return;
}
deleteFromWritableArray(deaths, (d) => d.id == death.id);
}

View File

@ -111,11 +111,11 @@
submitEnabled = false;
for (const key of keys) {
for (const k of key) {
if (k.options?.required) {
if (k.options?.validate) {
if (k.options?.validate) {
if (k.options?.required && !target[k.key]) {
return;
} else if (k.options?.required || target[k.key]) {
if (!k.options.validate(target[k.key])) return;
} else {
if (!target[k.key]) return;
}
}
}

View File

@ -61,8 +61,9 @@ CREATE TABLE IF NOT EXISTS team_draft (
-- death
CREATE TABLE IF NOT EXISTS death (
id INT AUTO_INCREMENT PRIMARY KEY,
message VARCHAR(1024) NOT NULL,
dead_user_id INT NOT NULL,
dead_user_id INT NOT NULL UNIQUE,
killer_user_id INT,
FOREIGN KEY (dead_user_id) REFERENCES user(id) ON DELETE CASCADE,
FOREIGN KEY (killer_user_id) REFERENCES user(id) ON DELETE CASCADE

View File

@ -83,9 +83,13 @@ import {
setSettings
} from './schema/settings';
import {
addDeath,
type AddDeathReq,
addDeath,
death,
deleteDeath,
type DeleteDeathReq,
editDeath,
type EditDeathReq,
getDeathByUserId,
type GetDeathByUserIdReq,
getDeaths,
@ -273,6 +277,8 @@ export class Database {
/* death */
addDeath = (values: AddDeathReq) => addDeath(this.db, values);
editDeath = (values: EditDeathReq) => editDeath(this.db, values);
deleteDeath = (values: DeleteDeathReq) => deleteDeath(this.db, values);
getDeathByUserId = (values: GetDeathByUserIdReq) => getDeathByUserId(this.db, values);
getDeaths = (values: GetDeathsReq) => getDeaths(this.db, values);

View File

@ -6,6 +6,7 @@ import { eq } from 'drizzle-orm';
type Database = MySql2Database<{ death: typeof death }>;
export const death = mysqlTable('death', {
id: int('id').primaryKey().autoincrement(),
message: varchar('message', { length: 1024 }).notNull(),
deadUserId: int('dead_user_id')
.notNull()
@ -15,10 +16,21 @@ export const death = mysqlTable('death', {
export type AddDeathReq = {
message: string;
killerUserId?: number;
killerUserId?: number | null;
deadUserId: number;
};
export type EditDeathReq = {
id: number;
message: string;
killerUserId?: number | null;
deadUserId: number;
};
export type DeleteDeathReq = {
id: number;
};
export type GetDeathByUserIdReq = {
userId: number;
};
@ -26,7 +38,24 @@ export type GetDeathByUserIdReq = {
export type GetDeathsReq = {};
export async function addDeath(db: Database, values: AddDeathReq) {
await db.insert(death).values(values);
const ids = await db.insert(death).values(values).$returningId();
return ids[0];
}
export async function editDeath(db: Database, values: EditDeathReq) {
await db
.update(death)
.set({
message: values.message,
killerUserId: values.killerUserId,
deadUserId: values.deadUserId
})
.where(eq(death.id, values.id));
}
export async function deleteDeath(db: Database, values: DeleteDeathReq) {
await db.delete(death).where(eq(death.id, values.id));
}
export async function getDeathByUserId(db: Database, values: GetDeathByUserIdReq) {
@ -41,6 +70,7 @@ export async function getDeaths(db: Database, _values: GetDeathsReq) {
return db
.select({
id: death.id,
message: death.message,
killed: {
id: killed.id,

View File

@ -40,6 +40,13 @@ const adminTabs = [
href: 'admin/teams',
name: 'Teams',
icon: 'heroicons:users',
subTabs: [
{
href: 'admin/teams/dead',
name: 'Tote Spieler',
icon: 'heroicons:x-mark'
}
],
enabled: session?.permissions.users
},
{

View File

@ -0,0 +1,16 @@
---
import AdminLayout from '@layouts/admin/AdminLayout.astro';
import SidebarActions from '@app/admin/teamDeaths/SidebarActions.svelte';
import TeamDeaths from '@app/admin/teamDeaths/TeamDeaths.svelte';
import { Session } from '@util/session.ts';
import { Permissions } from '@util/permissions.ts';
import { BASE_PATH } from 'astro:env/server';
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Users);
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
---
<AdminLayout title="Tote Spieler">
<SidebarActions slot="actions" client:load />
<TeamDeaths client:load />
</AdminLayout>