diff --git a/src/actions/team.ts b/src/actions/team.ts index f50ed22..d4b58b0 100644 --- a/src/actions/team.ts +++ b/src/actions/team.ts @@ -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({}) + }; + } }) }; diff --git a/src/app/admin/reports/BottomBar.svelte b/src/app/admin/reports/BottomBar.svelte index 5e7e65a..7afb8b4 100644 --- a/src/app/admin/reports/BottomBar.svelte +++ b/src/app/admin/reports/BottomBar.svelte @@ -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; diff --git a/src/app/admin/teamDeaths/SidebarActions.svelte b/src/app/admin/teamDeaths/SidebarActions.svelte new file mode 100644 index 0000000..0ab6ca9 --- /dev/null +++ b/src/app/admin/teamDeaths/SidebarActions.svelte @@ -0,0 +1,49 @@ + + +
+ +
+ + !!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} +/> diff --git a/src/app/admin/teamDeaths/TeamDeaths.svelte b/src/app/admin/teamDeaths/TeamDeaths.svelte new file mode 100644 index 0000000..545d73a --- /dev/null +++ b/src/app/admin/teamDeaths/TeamDeaths.svelte @@ -0,0 +1,69 @@ + + +{#snippet username(user?: { id: number; username: string })} + {user?.username} +{/snippet} + + (editPopupDeath = death)} + onDelete={onDeathDelete} +/> + + !!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} +/> diff --git a/src/app/admin/teamDeaths/teamDeaths.ts b/src/app/admin/teamDeaths/teamDeaths.ts new file mode 100644 index 0000000..76e1e34 --- /dev/null +++ b/src/app/admin/teamDeaths/teamDeaths.ts @@ -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['data'], undefined>['deaths']; +export type Death = Deaths[0]; + +// state +export const deaths = writable([]); + +// 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); +} diff --git a/src/components/admin/popup/CrudPopup.svelte b/src/components/admin/popup/CrudPopup.svelte index 79d8e40..96d7348 100644 --- a/src/components/admin/popup/CrudPopup.svelte +++ b/src/components/admin/popup/CrudPopup.svelte @@ -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; } } } diff --git a/src/db/database.sql b/src/db/database.sql index b4ae7a5..e85bf6b 100644 --- a/src/db/database.sql +++ b/src/db/database.sql @@ -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 diff --git a/src/db/database.ts b/src/db/database.ts index 92004c0..9deb710 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -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); diff --git a/src/db/schema/death.ts b/src/db/schema/death.ts index da847a5..82a4cc0 100644 --- a/src/db/schema/death.ts +++ b/src/db/schema/death.ts @@ -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, diff --git a/src/layouts/admin/AdminLayout.astro b/src/layouts/admin/AdminLayout.astro index 83ed98c..3aca329 100644 --- a/src/layouts/admin/AdminLayout.astro +++ b/src/layouts/admin/AdminLayout.astro @@ -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 }, { diff --git a/src/pages/admin/teams/dead.astro b/src/pages/admin/teams/dead.astro new file mode 100644 index 0000000..8e9509d --- /dev/null +++ b/src/pages/admin/teams/dead.astro @@ -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`); +--- + + + + + diff --git a/src/pages/admin/teams.astro b/src/pages/admin/teams/index.astro similarity index 100% rename from src/pages/admin/teams.astro rename to src/pages/admin/teams/index.astro