Compare commits
36 Commits
709ae0c6ee
...
main
Author | SHA1 | Date | |
---|---|---|---|
2f6b3521cd | |||
6789a65285 | |||
7a0db65f78 | |||
94aa6ea377 | |||
a06cc34085 | |||
9041578252 | |||
deafb65c75 | |||
2eb9891b3c | |||
36fe39845f | |||
d82ac4f275 | |||
136e0b808c | |||
d29e761efb | |||
018e239c35 | |||
1f96e3babe | |||
e5f253ebc1 | |||
6e4a7f0ac9 | |||
55f5f25e5a | |||
03ee87d7cf | |||
9a6e44b2d5 | |||
1a81b5fb06 | |||
d7b05deff2 | |||
e9e44f67a2 | |||
eeeca4ed4e | |||
7b5557bd76 | |||
29a80935ff | |||
023fd67004 | |||
daa1de302b | |||
e0c91483fb | |||
bff1a4bda6 | |||
46494ed8dc | |||
4e615fe211 | |||
54a780d999 | |||
94e9e83e93 | |||
eb39cae44c | |||
e0b9850efb | |||
12f8b9c43d |
@ -6,6 +6,7 @@ import { team } from './team.ts';
|
|||||||
import { settings } from './settings.ts';
|
import { settings } from './settings.ts';
|
||||||
import { feedback } from './feedback.ts';
|
import { feedback } from './feedback.ts';
|
||||||
import { report } from './report.ts';
|
import { report } from './report.ts';
|
||||||
|
import { tools } from './tools.ts';
|
||||||
|
|
||||||
export const server = {
|
export const server = {
|
||||||
admin,
|
admin,
|
||||||
@ -15,5 +16,6 @@ export const server = {
|
|||||||
user,
|
user,
|
||||||
report,
|
report,
|
||||||
feedback,
|
feedback,
|
||||||
settings
|
settings,
|
||||||
|
tools
|
||||||
};
|
};
|
||||||
|
@ -3,11 +3,12 @@ import { Session } from '@util/session.ts';
|
|||||||
import { Permissions } from '@util/permissions.ts';
|
import { Permissions } from '@util/permissions.ts';
|
||||||
import { db } from '@db/database.ts';
|
import { db } from '@db/database.ts';
|
||||||
import { z } from 'astro:schema';
|
import { z } from 'astro:schema';
|
||||||
import { UPLOAD_PATH } from 'astro:env/server';
|
import { MAX_UPLOAD_BYTES, UPLOAD_PATH } from 'astro:env/server';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { sendWebhook, WebhookAction } from '@util/webhook.ts';
|
import { sendWebhook, WebhookAction } from '@util/webhook.ts';
|
||||||
|
import { allowedImageTypes, allowedVideoTypes } from '@util/media.ts';
|
||||||
|
|
||||||
export const report = {
|
export const report = {
|
||||||
submitReport: defineAction({
|
submitReport: defineAction({
|
||||||
@ -15,9 +16,22 @@ export const report = {
|
|||||||
urlHash: z.string(),
|
urlHash: z.string(),
|
||||||
reason: z.string(),
|
reason: z.string(),
|
||||||
body: z.string(),
|
body: z.string(),
|
||||||
files: z.array(z.instanceof(File)).nullable()
|
files: z
|
||||||
|
.array(
|
||||||
|
z
|
||||||
|
.instanceof(File)
|
||||||
|
.refine((f) => [...allowedImageTypes, ...allowedVideoTypes].findIndex((v) => v === f.type) !== -1)
|
||||||
|
)
|
||||||
|
.nullable()
|
||||||
}),
|
}),
|
||||||
handler: async (input) => {
|
handler: async (input) => {
|
||||||
|
const fileSize = input.files?.reduce((prev, curr) => prev + curr.size, 0);
|
||||||
|
if (fileSize && fileSize > MAX_UPLOAD_BYTES) {
|
||||||
|
throw new ActionError({
|
||||||
|
code: 'BAD_REQUEST'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const report = await db.getReportByUrlHash({ urlHash: input.urlHash });
|
const report = await db.getReportByUrlHash({ urlHash: input.urlHash });
|
||||||
if (!report) {
|
if (!report) {
|
||||||
throw new ActionError({
|
throw new ActionError({
|
||||||
@ -52,8 +66,20 @@ export const report = {
|
|||||||
const hash = md5Hash.digest('hex');
|
const hash = md5Hash.digest('hex');
|
||||||
const filePath = path.join(UPLOAD_PATH!, hash);
|
const filePath = path.join(UPLOAD_PATH!, hash);
|
||||||
|
|
||||||
|
let type: 'image' | 'video';
|
||||||
|
if (allowedImageTypes.includes(file.type)) {
|
||||||
|
type = 'image';
|
||||||
|
} else if (allowedVideoTypes.includes(file.type)) {
|
||||||
|
type = 'video';
|
||||||
|
} else {
|
||||||
|
throw new ActionError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: 'Invalid file type'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await tx.addReportAttachment({
|
await tx.addReportAttachment({
|
||||||
type: 'video',
|
type: type,
|
||||||
hash: hash,
|
hash: hash,
|
||||||
reportId: report.id
|
reportId: report.id
|
||||||
});
|
});
|
||||||
@ -103,6 +129,20 @@ export const report = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
editReport: defineAction({
|
||||||
|
input: z.object({
|
||||||
|
reportId: z.number(),
|
||||||
|
reported: z.number().nullable()
|
||||||
|
}),
|
||||||
|
handler: async (input, context) => {
|
||||||
|
Session.actionSessionFromCookies(context.cookies, Permissions.Reports);
|
||||||
|
|
||||||
|
await db.editReport({
|
||||||
|
id: input.reportId,
|
||||||
|
reportedTeamId: input.reported
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
reportStatus: defineAction({
|
reportStatus: defineAction({
|
||||||
input: z.object({
|
input: z.object({
|
||||||
reportId: z.number()
|
reportId: z.number()
|
||||||
@ -137,10 +177,12 @@ export const report = {
|
|||||||
reportId: input.reportId,
|
reportId: input.reportId,
|
||||||
strikeReasonId: input.strikeReasonId
|
strikeReasonId: input.strikeReasonId
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await db.deleteStrike({ reportId: input.reportId });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (input.status === 'closed' && preReportStrike?.strikeReasonId != input.strikeReasonId) {
|
if (input.status === 'closed' && preReportStrike?.strikeReason?.id != input.strikeReasonId) {
|
||||||
const report = await db.getReportById({ id: input.reportId });
|
const report = await db.getReportById({ id: input.reportId });
|
||||||
if (report.reported) {
|
if (report.reported) {
|
||||||
const strikes = await db.getStrikesByTeamId({ teamId: report.reported.id });
|
const strikes = await db.getStrikesByTeamId({ teamId: report.reported.id });
|
||||||
@ -168,6 +210,18 @@ export const report = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
reportAttachments: defineAction({
|
||||||
|
input: z.object({
|
||||||
|
reportId: z.number()
|
||||||
|
}),
|
||||||
|
handler: async (input, context) => {
|
||||||
|
Session.actionSessionFromCookies(context.cookies, Permissions.Reports);
|
||||||
|
|
||||||
|
return {
|
||||||
|
reportAttachments: (await db.getReportAttachments(input)) ?? []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}),
|
||||||
addStrikeReason: defineAction({
|
addStrikeReason: defineAction({
|
||||||
input: z.object({
|
input: z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
@ -128,5 +128,53 @@ export const team = {
|
|||||||
teams: await db.getTeams(input)
|
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({})
|
||||||
|
};
|
||||||
|
}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
57
src/actions/tools.ts
Normal file
57
src/actions/tools.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { ActionError, defineAction } from 'astro:actions';
|
||||||
|
import { Session } from '@util/session.ts';
|
||||||
|
import { Permissions } from '@util/permissions.ts';
|
||||||
|
import { z } from 'astro:schema';
|
||||||
|
import { getBedrockUuid, getJavaUuid } from '@util/minecraft.ts';
|
||||||
|
|
||||||
|
export const tools = {
|
||||||
|
uuidFromUsername: defineAction({
|
||||||
|
input: z.object({
|
||||||
|
edition: z.enum(['java', 'bedrock']),
|
||||||
|
username: z.string()
|
||||||
|
}),
|
||||||
|
handler: async (input, context) => {
|
||||||
|
Session.actionSessionFromCookies(context.cookies, Permissions.Tools);
|
||||||
|
|
||||||
|
let uuid = null;
|
||||||
|
switch (input.edition) {
|
||||||
|
case 'java':
|
||||||
|
try {
|
||||||
|
uuid = await getJavaUuid(input.username);
|
||||||
|
} catch (_) {
|
||||||
|
throw new ActionError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: `Der Username ${input.username} existiert nicht`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (uuid == null) {
|
||||||
|
throw new ActionError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: `Während der Anfrage zur Mojang API ist ein Fehler aufgetreten`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'bedrock':
|
||||||
|
try {
|
||||||
|
uuid = await getBedrockUuid(input.username);
|
||||||
|
} catch (_) {
|
||||||
|
throw new ActionError({
|
||||||
|
code: 'NOT_FOUND',
|
||||||
|
message: `Der Username ${input.username} existiert nicht`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (uuid == null) {
|
||||||
|
throw new ActionError({
|
||||||
|
code: 'BAD_REQUEST',
|
||||||
|
message: `Während der Anfrage zum Username Resolver ist ein Fehler aufgetreten`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
uuid: uuid
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
@ -4,7 +4,6 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import DataTable from '@components/admin/table/DataTable.svelte';
|
import DataTable from '@components/admin/table/DataTable.svelte';
|
||||||
|
|
||||||
// consts
|
|
||||||
// consts
|
// consts
|
||||||
const dateFormat = new Intl.DateTimeFormat('de-DE', {
|
const dateFormat = new Intl.DateTimeFormat('de-DE', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Report, ReportStatus, StrikeReasons } from './reports.ts';
|
import { editReport, getReportAttachments, type Report, type ReportStatus, type StrikeReasons } from './reports.ts';
|
||||||
import Input from '@components/input/Input.svelte';
|
import Input from '@components/input/Input.svelte';
|
||||||
import Textarea from '@components/input/Textarea.svelte';
|
import Textarea from '@components/input/Textarea.svelte';
|
||||||
import Select from '@components/input/Select.svelte';
|
import Select from '@components/input/Select.svelte';
|
||||||
import TeamSearch from '@components/admin/search/TeamSearch.svelte';
|
import TeamSearch from '@components/admin/search/TeamSearch.svelte';
|
||||||
import { editReportStatus, getReportStatus } from '@app/admin/reports/reports.ts';
|
import { editReportStatus, getReportStatus } from '@app/admin/reports/reports.ts';
|
||||||
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
|
import { confirmPopupState } from '@components/popup/ConfirmPopup.ts';
|
||||||
|
import Icon from '@iconify/svelte';
|
||||||
|
|
||||||
|
// html bindings
|
||||||
|
let previewDialogElem: HTMLDialogElement;
|
||||||
|
|
||||||
// types
|
// types
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -17,15 +21,20 @@
|
|||||||
let { strikeReasons, report }: Props = $props();
|
let { strikeReasons, report }: Props = $props();
|
||||||
|
|
||||||
// states
|
// states
|
||||||
|
let reportedTeam = $state<{ id: number; name: string } | null>(report?.reported ?? null);
|
||||||
|
|
||||||
let status = $state<'open' | 'closed' | null>(null);
|
let status = $state<'open' | 'closed' | null>(null);
|
||||||
let notice = $state<string | null>(null);
|
let notice = $state<string | null>(null);
|
||||||
let statement = $state<string | null>(null);
|
let statement = $state<string | null>(null);
|
||||||
let strikeReason = $state<string | null>(null);
|
let strikeReason = $state<string | null>(String(report?.strike?.strikeReasonId ?? null));
|
||||||
|
|
||||||
|
let reportAttachments = $state<{ type: 'image' | 'video'; hash: string }[]>([]);
|
||||||
|
let previewReportAttachment = $state<{ type: 'image' | 'video'; hash: string } | null>(null);
|
||||||
|
|
||||||
// consts
|
// consts
|
||||||
const strikeReasonValues = strikeReasons.reduce(
|
const strikeReasonValues = strikeReasons.reduce(
|
||||||
(prev, curr) => Object.assign(prev, { [curr.id]: `${curr.name} (${curr.weight})` }),
|
(prev, curr) => Object.assign(prev, { [curr.id]: `${curr.name} (${curr.weight})` }),
|
||||||
{}
|
{ [null]: 'Kein Vergehen' }
|
||||||
);
|
);
|
||||||
|
|
||||||
// lifetime
|
// lifetime
|
||||||
@ -39,6 +48,14 @@
|
|||||||
notice = reportStatus.notice;
|
notice = reportStatus.notice;
|
||||||
statement = reportStatus.statement;
|
statement = reportStatus.statement;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
getReportAttachments(report).then((value) => {
|
||||||
|
if (value) reportAttachments = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (previewReportAttachment) previewDialogElem.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
// callbacks
|
// callbacks
|
||||||
@ -46,35 +63,76 @@
|
|||||||
$confirmPopupState = {
|
$confirmPopupState = {
|
||||||
title: 'Änderungen speichern?',
|
title: 'Änderungen speichern?',
|
||||||
message: 'Sollen die Änderungen am Report gespeichert werden?',
|
message: 'Sollen die Änderungen am Report gespeichert werden?',
|
||||||
onConfirm: async () =>
|
onConfirm: async () => {
|
||||||
editReportStatus(report!, {
|
if (reportedTeam?.id != report?.reported?.id) {
|
||||||
|
report!.reported = reportedTeam;
|
||||||
|
await editReport(report!);
|
||||||
|
}
|
||||||
|
await editReportStatus(report!, {
|
||||||
status: status,
|
status: status,
|
||||||
notice: notice,
|
notice: notice,
|
||||||
statement: statement,
|
statement: statement,
|
||||||
strikeReasonId: Number(strikeReason)
|
strikeReasonId: Number(strikeReason)
|
||||||
} as ReportStatus)
|
} as ReportStatus);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onCopyPublicLink(urlHash: string) {
|
||||||
|
navigator.clipboard.writeText(`${document.baseURI}report/${urlHash}`);
|
||||||
|
document.activeElement?.blur();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-2 bg-base-200 rounded-lg w-[calc(100%-1rem)] mx-2 flex px-6 py-4 gap-2"
|
class="absolute bottom-2 bg-base-200 rounded-lg w-[calc(100%-1rem)] mx-2 flex px-6 py-4 gap-2"
|
||||||
hidden={report === null}
|
hidden={report === null}
|
||||||
>
|
>
|
||||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick={() => (report = null)}>✕</button>
|
<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"><Icon icon="heroicons:share" /></div>
|
||||||
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||||
|
<ul tabindex="0" class="menu dropdown-content bg-base-100 rounded-box z-1 p-2 shadow-sm w-max">
|
||||||
|
<li><button onclick={() => onCopyPublicLink(report?.urlHash)}>Öffentlichen Report Link kopieren</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-circle btn-ghost" onclick={() => (report = null)}>✕</button>
|
||||||
|
</div>
|
||||||
<div class="w-[34rem]">
|
<div class="w-[34rem]">
|
||||||
<TeamSearch value={report?.reporter.name} label="Report Team" readonly mustMatch />
|
<TeamSearch value={report?.reporter.name} label="Report Team" readonly mustMatch />
|
||||||
<TeamSearch value={report?.reported?.name} label="Reportetes Team" />
|
<TeamSearch value={report?.reported?.name} label="Reportetes Team" onSubmit={(team) => (reportedTeam = team)} />
|
||||||
<Textarea bind:value={notice} label="Interne Notizen" rows={8} />
|
<Textarea bind:value={notice} label="Interne Notizen" rows={10} />
|
||||||
</div>
|
</div>
|
||||||
<div class="divider divider-horizontal"></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={12} />
|
<Textarea value={report?.body} label="Inhalt" readonly dynamicWidth rows={9} />
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Anhänge</legend>
|
||||||
|
<div class="h-16.5 rounded border border-dashed flex">
|
||||||
|
{#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)}>
|
||||||
|
{#if reportAttachment.type === 'image'}
|
||||||
|
<img
|
||||||
|
src={location.pathname + '/attachment/' + reportAttachment.hash}
|
||||||
|
alt={reportAttachment.hash}
|
||||||
|
class="w-16 h-16"
|
||||||
|
/>
|
||||||
|
{:else if reportAttachment.type === 'video'}
|
||||||
|
<!-- svelte-ignore a11y_media_has_caption -->
|
||||||
|
<video src={location.pathname + '/attachment/' + reportAttachment.hash} class="w-16 h-16"></video>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider divider-horizontal"></div>
|
<div class="divider divider-horizontal"></div>
|
||||||
<div class="flex flex-col w-[42rem]">
|
<div class="flex flex-col w-[42rem]">
|
||||||
<Textarea bind:value={statement} label="Öffentliche Report Antwort" dynamicWidth rows={5} />
|
<Textarea bind:value={statement} label="Öffentliche Report Antwort" dynamicWidth rows={7} />
|
||||||
<Select
|
<Select
|
||||||
bind:value={status}
|
bind:value={status}
|
||||||
values={{ open: 'In Bearbeitung', closed: 'Bearbeitet' }}
|
values={{ open: 'In Bearbeitung', closed: 'Bearbeitet' }}
|
||||||
@ -82,9 +140,27 @@
|
|||||||
label="Bearbeitungsstatus"
|
label="Bearbeitungsstatus"
|
||||||
dynamicWidth
|
dynamicWidth
|
||||||
/>
|
/>
|
||||||
<Select bind:value={strikeReason} values={strikeReasonValues} defaultValue="" label="Vergehen" dynamicWidth
|
<Select bind:value={strikeReason} values={strikeReasonValues} label="Vergehen" dynamicWidth></Select>
|
||||||
></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 mt-auto" onclick={onSaveButtonClick}>Speichern</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
class="modal"
|
||||||
|
bind:this={previewDialogElem}
|
||||||
|
onclose={() => setTimeout(() => (previewReportAttachment = null), 300)}
|
||||||
|
>
|
||||||
|
<div class="modal-box">
|
||||||
|
{#if previewReportAttachment?.type === 'image'}
|
||||||
|
<img src={location.pathname + '/attachment/' + previewReportAttachment.hash} alt={previewReportAttachment.hash} />
|
||||||
|
{:else if previewReportAttachment?.type === 'video'}
|
||||||
|
<!-- svelte-ignore a11y_media_has_caption -->
|
||||||
|
<video src={location.pathname + '/attachment/' + previewReportAttachment.hash} controls></video>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.3)]">
|
||||||
|
<button class="absolute top-3 right-3 btn btn-circle">✕</button>
|
||||||
|
<button class="!cursor-default">close</button>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
@ -4,6 +4,15 @@
|
|||||||
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';
|
||||||
|
|
||||||
|
// consts
|
||||||
|
const dateFormat = new Intl.DateTimeFormat('de-DE', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
// states
|
// states
|
||||||
let strikeReasons = $state<StrikeReasons>([]);
|
let strikeReasons = $state<StrikeReasons>([]);
|
||||||
let activeReport = $state<Report | null>(null);
|
let activeReport = $state<Report | null>(null);
|
||||||
@ -14,6 +23,18 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#snippet date(value: string)}
|
||||||
|
{dateFormat.format(new Date(value))}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet status(value: null | 'open' | 'closed')}
|
||||||
|
{#if value === 'open'}
|
||||||
|
<p>In Bearbeitung</p>
|
||||||
|
{:else if value === 'closed'}
|
||||||
|
<p>Bearbeitet</p>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<DataTable
|
<DataTable
|
||||||
data={reports}
|
data={reports}
|
||||||
count={true}
|
count={true}
|
||||||
@ -21,8 +42,8 @@
|
|||||||
{ key: 'reason', label: 'Grund' },
|
{ key: 'reason', label: 'Grund' },
|
||||||
{ key: 'reporter.name', label: 'Report Team' },
|
{ key: 'reporter.name', label: 'Report Team' },
|
||||||
{ key: 'reported.name', label: 'Reportetes Team' },
|
{ key: 'reported.name', label: 'Reportetes Team' },
|
||||||
{ key: 'createdAt', label: 'Datum' },
|
{ key: 'createdAt', label: 'Datum', transform: date },
|
||||||
{ key: 'report.status?.status', label: 'Bearbeitungsstatus' }
|
{ key: 'status.status', label: 'Bearbeitungsstatus', transform: status }
|
||||||
]}
|
]}
|
||||||
onClick={(report) => (activeReport = report)}
|
onClick={(report) => (activeReport = report)}
|
||||||
/>
|
/>
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
key: 'createdAt',
|
key: 'createdAt',
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
label: 'Report kann bearbeitet werden',
|
label: 'Report kann bearbeitet werden',
|
||||||
options: { convert: (v) => (v ? new Date().toISOString() : null) }
|
options: { convert: (v) => (v ? null : new Date().toISOString()) }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]}
|
]}
|
||||||
|
@ -59,6 +59,17 @@ export async function getReportStatus(report: Report) {
|
|||||||
return data.reportStatus;
|
return data.reportStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function editReport(report: Report) {
|
||||||
|
const { error } = await actions.report.editReport({
|
||||||
|
reportId: report.id,
|
||||||
|
reported: report.reported?.id ?? null
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
actionErrorPopup(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function editReportStatus(report: Report, reportStatus: ReportStatus) {
|
export async function editReportStatus(report: Report, reportStatus: ReportStatus) {
|
||||||
const { error } = await actions.report.editReportStatus({
|
const { error } = await actions.report.editReportStatus({
|
||||||
reportId: report.id,
|
reportId: report.id,
|
||||||
@ -73,6 +84,18 @@ export async function editReportStatus(report: Report, reportStatus: ReportStatu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getReportAttachments(report: Report) {
|
||||||
|
const { data, error } = await actions.report.reportAttachments({
|
||||||
|
reportId: report.id
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
actionErrorPopup(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.reportAttachments;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getStrikeReasons() {
|
export async function getStrikeReasons() {
|
||||||
const { data, error } = await actions.report.strikeReasons();
|
const { data, error } = await actions.report.strikeReasons();
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -33,7 +33,8 @@
|
|||||||
count={true}
|
count={true}
|
||||||
keys={[
|
keys={[
|
||||||
{ key: 'name', label: 'Name', width: 20 },
|
{ key: 'name', label: 'Name', width: 20 },
|
||||||
{ key: 'weight', label: 'Gewichtung', width: 70, sortable: true }
|
{ key: 'weight', label: 'Gewichtung', width: 50, sortable: true },
|
||||||
|
{ key: 'id', label: 'Id', width: 20 }
|
||||||
]}
|
]}
|
||||||
onDelete={onBlockedUserDelete}
|
onDelete={onBlockedUserDelete}
|
||||||
onEdit={(strikeReason) => (editPopupStrikeReason = strikeReason)}
|
onEdit={(strikeReason) => (editPopupStrikeReason = strikeReason)}
|
||||||
|
49
src/app/admin/teamDeaths/SidebarActions.svelte
Normal file
49
src/app/admin/teamDeaths/SidebarActions.svelte
Normal 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}
|
||||||
|
/>
|
69
src/app/admin/teamDeaths/TeamDeaths.svelte
Normal file
69
src/app/admin/teamDeaths/TeamDeaths.svelte
Normal 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}
|
||||||
|
/>
|
61
src/app/admin/teamDeaths/teamDeaths.ts
Normal file
61
src/app/admin/teamDeaths/teamDeaths.ts
Normal 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);
|
||||||
|
}
|
29
src/app/admin/tools/AccountUuidFinder.svelte
Normal file
29
src/app/admin/tools/AccountUuidFinder.svelte
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Input from '@components/input/Input.svelte';
|
||||||
|
import Select from '@components/input/Select.svelte';
|
||||||
|
import { uuidFromUsername } from '@app/admin/tools/tools.ts';
|
||||||
|
|
||||||
|
// states
|
||||||
|
let edition = $state<'java' | 'bedrock'>('java');
|
||||||
|
let username = $state('');
|
||||||
|
let uuid = $state<null | string>(null);
|
||||||
|
|
||||||
|
// callbacks
|
||||||
|
async function onSubmit() {
|
||||||
|
uuid = await uuidFromUsername(edition, username);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<button class="btn w-4/6" class:disabled={!username} onclick={onSubmit}>UUID finden</button>
|
||||||
|
</div>
|
||||||
|
<Input bind:value={uuid} readonly />
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
7
src/app/admin/tools/Tools.svelte
Normal file
7
src/app/admin/tools/Tools.svelte
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import AccountUuidFinder from '@app/admin/tools/AccountUuidFinder.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex justify-center mt-2">
|
||||||
|
<AccountUuidFinder />
|
||||||
|
</div>
|
12
src/app/admin/tools/tools.ts
Normal file
12
src/app/admin/tools/tools.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { actions } from 'astro:actions';
|
||||||
|
import { actionErrorPopup } from '@util/action.ts';
|
||||||
|
|
||||||
|
export async function uuidFromUsername(edition: 'java' | 'bedrock', username: string) {
|
||||||
|
const { data, error } = await actions.tools.uuidFromUsername({ edition: edition, username: username });
|
||||||
|
if (error) {
|
||||||
|
actionErrorPopup(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.uuid;
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Skeleton from '@assets/img/steve.png';
|
|
||||||
import type { GetDeathsRes } from '@db/schema/death.ts';
|
|
||||||
import { type ActionReturnType, actions } from 'astro:actions';
|
import { type ActionReturnType, actions } from 'astro:actions';
|
||||||
|
import type { db } from '@db/database.ts';
|
||||||
|
import crown from '@assets/img/crown.svg';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
teams: Exclude<ActionReturnType<typeof actions.team.teams>['data'], undefined>['teams'];
|
teams: Exclude<ActionReturnType<typeof actions.team.teams>['data'], undefined>['teams'];
|
||||||
deaths: GetDeathsRes;
|
deaths: Awaited<ReturnType<typeof db.getDeaths>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { teams, deaths }: Props = $props();
|
const { teams, deaths }: Props = $props();
|
||||||
@ -13,24 +13,37 @@
|
|||||||
const entries = teams.map((team) => ({
|
const entries = teams.map((team) => ({
|
||||||
...team,
|
...team,
|
||||||
memberOne: Object.assign(team.memberOne, {
|
memberOne: Object.assign(team.memberOne, {
|
||||||
dead: team.memberOne.id != null ? deaths.findIndex((d) => d.deadUserId === team.memberOne.id) !== -1 : null
|
kills: deaths.filter((d) => d.killer?.id === team.memberOne.id) ?? [],
|
||||||
|
dead: deaths.find((d) => d.killed.id === team.memberOne.id) ?? null
|
||||||
}),
|
}),
|
||||||
memberTwo: Object.assign(team.memberTwo, {
|
memberTwo: Object.assign(team.memberTwo, {
|
||||||
dead: team.memberTwo.id != null ? deaths.findIndex((d) => d.deadUserId === team.memberTwo.id) !== -1 : null
|
kills: deaths.filter((d) => d.killer?.id === team.memberTwo.id) ?? [],
|
||||||
}),
|
dead: deaths.find((d) => d.killed.id === team.memberTwo.id) ?? null
|
||||||
kills: deaths.filter((d) => d.killerUserId === team.memberOne.id || d.killerUserId === team.memberTwo.id).length
|
})
|
||||||
}));
|
}));
|
||||||
entries.sort((a, b) => {
|
entries.sort((a, b) => {
|
||||||
const aAllowed = !!a.memberOne.id && !!a.memberTwo.id && !(a.memberOne.dead && a.memberTwo.dead);
|
const aBothSignedUp = a.memberOne.id != null && a.memberTwo.id != null;
|
||||||
const bAllowed = !!b.memberOne.id && !!b.memberTwo.id && !(b.memberOne.dead && b.memberTwo.dead);
|
const aBothKills = a.memberOne.kills.length + a.memberTwo.kills.length;
|
||||||
if (!aAllowed && !bAllowed) {
|
const aBothDead = a.memberOne.dead && a.memberTwo.dead;
|
||||||
return 0;
|
|
||||||
} else if (!aAllowed || !bAllowed) {
|
const bBothSignedUp = b.memberOne.id != null && b.memberTwo.id != null;
|
||||||
return (bAllowed as unknown as number) - (aAllowed as unknown as number);
|
const bBothKills = b.memberOne.kills.length + b.memberTwo.kills.length;
|
||||||
|
const bBothDead = b.memberOne.dead && b.memberTwo.dead;
|
||||||
|
|
||||||
|
if (!aBothSignedUp || !bBothSignedUp) {
|
||||||
|
return Number(bBothSignedUp) - Number(aBothSignedUp);
|
||||||
|
} else if ((aBothDead && !bBothDead) || (!aBothDead && bBothDead)) {
|
||||||
|
return Number(!!aBothDead) - Number(!!bBothDead);
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.kills - a.kills;
|
return bBothKills - aBothKills;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const aliveTeams = entries.reduce(
|
||||||
|
(prev, curr) =>
|
||||||
|
prev + Number(curr.memberOne.id && curr.memberTwo.id && (!curr.memberOne.dead || !curr.memberTwo.dead)),
|
||||||
|
0
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card bg-base-300 shadow-sm w-full md:w-5/7 xl:w-4/7 sm:p-5 md:p-10">
|
<div class="card bg-base-300 shadow-sm w-full md:w-5/7 xl:w-4/7 sm:p-5 md:p-10">
|
||||||
@ -45,26 +58,48 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each entries as team (team.id)}
|
{#each entries as team (team.id)}
|
||||||
|
{@const teamSignedUp = !!team.memberOne.id && !!team.memberTwo.id}
|
||||||
|
{@const teamDead = !!team.memberOne.dead && !!team.memberTwo.dead}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex items-center gap-x-2">
|
<div class="flex items-center gap-x-2">
|
||||||
<div class="rounded-sm min-w-3 w-3 min-h-3 h-3" style="background-color: {team.color}"></div>
|
<div class="relative">
|
||||||
<h3 class="text-xs sm:text-xl break-all" class:text-red-200={!team.memberOne}>
|
<div class="rounded-sm min-w-3 w-3 min-h-3 h-3" style="background-color: {team.color}"></div>
|
||||||
|
{#if aliveTeams === 1 && teamSignedUp && !teamDead}
|
||||||
|
<div class="absolute h-3.5 w-3.5 -top-2.25 -right-0.25">
|
||||||
|
<img class="h-full w-full" src={crown.src} alt="" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xs sm:text-xl break-all" class:line-through={teamDead}>
|
||||||
{team.name}
|
{team.name}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
{#if !team.memberOne.id || !team.memberTwo.id}
|
{#if !teamSignedUp}
|
||||||
<span>Team unvollständig</span>
|
<span>Team unvollständig</span>
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="max-w-9 overflow-ellipsis">
|
<td class="max-w-9 overflow-ellipsis">
|
||||||
<div class="flex items-center gap-x-2">
|
<div class="flex items-center gap-x-2 w-max tooltip">
|
||||||
|
{#if team.memberOne.kills.length > 0 || team.memberOne.dead}
|
||||||
|
<div class="tooltip-content text-left space-y-1">
|
||||||
|
{#each team.memberOne.kills as kill (kill.killed.id)}
|
||||||
|
<p>🔪 {kill.killed.username}</p>
|
||||||
|
{/each}
|
||||||
|
{#if team.memberOne.dead}
|
||||||
|
<p class="mt-2 first:mt-0">{team.memberOne.dead.message}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if team.memberOne.id != null}
|
{#if team.memberOne.id != null}
|
||||||
<img
|
<div class="relative">
|
||||||
class="h-4 pixelated"
|
<img class="h-4 pixelated" src="https://mc-heads.net/head/{team.memberOne.username}/8" alt="head" />
|
||||||
src={team.memberOne.dead ? Skeleton.src : `https://mc-heads.net/head/${team.memberOne.username}/8`}
|
{#if aliveTeams === 1 && teamSignedUp && !teamDead}
|
||||||
alt="head"
|
<div class="absolute -top-1.25 -right-1.25">
|
||||||
/>
|
<img class="h-3 w-3 rotate-30" src={crown.src} alt="" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<span
|
<span
|
||||||
class="text-xs sm:text-md break-all"
|
class="text-xs sm:text-md break-all"
|
||||||
@ -74,13 +109,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="flex items-center gap-x-2">
|
<div class="flex items-center gap-x-2 w-max tooltip">
|
||||||
|
{#if team.memberTwo.kills.length > 0 || team.memberTwo.dead}
|
||||||
|
<div class="tooltip-content text-left space-y-1">
|
||||||
|
{#each team.memberTwo.kills as kill (kill.killed.id)}
|
||||||
|
<p>🔪 {kill.killed.username}</p>
|
||||||
|
{/each}
|
||||||
|
{#if team.memberTwo.dead}
|
||||||
|
<p class="mt-2 first:mt-0">{team.memberTwo.dead.message}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if team.memberTwo.id != null}
|
{#if team.memberTwo.id != null}
|
||||||
<img
|
<div class="relative">
|
||||||
class="h-4 pixelated"
|
<img class="h-4 pixelated" src="https://mc-heads.net/head/{team.memberTwo.username}/8" alt="head" />
|
||||||
src={team.memberTwo.dead ? Skeleton.src : `https://mc-heads.net/head/${team.memberTwo.username}/8`}
|
{#if aliveTeams === 1 && teamSignedUp && !teamDead}
|
||||||
alt="head"
|
<div class="absolute -top-1.25 -right-1.25">
|
||||||
/>
|
<img class="h-3 w-3 rotate-30" src={crown.src} alt="" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<span
|
<span
|
||||||
class="text-xs sm:text-md break-all"
|
class="text-xs sm:text-md break-all"
|
||||||
@ -90,7 +138,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="text-xs sm:text-md">0</span>
|
<span class="text-xs sm:text-md">
|
||||||
|
{team.memberOne.kills.length + team.memberTwo.kills.length}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { popupState } from '@components/popup/Popup.ts';
|
import { popupState } from '@components/popup/Popup.ts';
|
||||||
|
import { allowedImageTypes, allowedVideoTypes } from '@util/media.ts';
|
||||||
|
|
||||||
// bindings
|
// bindings
|
||||||
let containerElem: HTMLDivElement;
|
|
||||||
let hiddenFileInputElem: HTMLInputElement;
|
let hiddenFileInputElem: HTMLInputElement;
|
||||||
let previewDialogElem: HTMLDialogElement;
|
let previewDialogElem: HTMLDialogElement;
|
||||||
|
|
||||||
// consts
|
|
||||||
const allowedImageTypes = ['image/png', 'image/jpeg', 'image/avif'];
|
|
||||||
const allowedVideoTypes = ['video/mp4'];
|
|
||||||
|
|
||||||
// types
|
// types
|
||||||
interface Props {
|
interface Props {
|
||||||
maxFilesBytes: number;
|
maxFilesBytes: number;
|
||||||
@ -62,7 +58,7 @@
|
|||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Ungültige Datei',
|
title: 'Ungültige Datei',
|
||||||
message:
|
message:
|
||||||
'Das Dateiformat wird nicht unterstützt. Nur Bilder (.png, .jpg, .jpeg, .avif) und Videos (.mp4) sind gültig'
|
'Das Dateiformat wird nicht unterstützt. Nur Bilder (.png, .jpg, .jpeg, .webp, .avif) und Videos (.mp4, .webm) sind gültig'
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -71,7 +67,7 @@
|
|||||||
$popupState = {
|
$popupState = {
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Datei zu groß',
|
title: 'Datei zu groß',
|
||||||
message: `Die Dateien dürfen insgesamt nur ${(maxFilesBytes / 1024 / 1024).toFixed(2)}MB groß sein`
|
message: `Die Dateien dürfen insgesamt nur ${bytesToHumanReadable(maxFilesBytes)} groß sein. Fall deine Anhänge größer sind, lade sie bitte auf einem externen Filehoster hoch (z.B. file.io, Google Drive, ...) und füge den Link zum teilen der Datei(en) zu den Report Details hinzu`
|
||||||
};
|
};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -126,7 +122,6 @@
|
|||||||
<!-- 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
|
<div
|
||||||
bind:this={containerElem}
|
|
||||||
class="h-12 rounded border border-dashed flex cursor-pointer"
|
class="h-12 rounded border border-dashed flex cursor-pointer"
|
||||||
class:h-26={uploadFiles.length > 0}
|
class:h-26={uploadFiles.length > 0}
|
||||||
dropzone="copy"
|
dropzone="copy"
|
||||||
|
1
src/assets/img/crown.svg
Normal file
1
src/assets/img/crown.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128"><g fill="#f79329"><path d="m91.56 50.38l14.35 44.94l-36.36-4.71z"/><path d="M105.91 96.5c-.05 0-.1 0-.15-.01l-36.37-4.71c-.39-.05-.72-.29-.9-.64s-.17-.76.01-1.1l22.02-40.23c.23-.41.69-.65 1.15-.61c.47.04.87.36 1.01.81l14.24 44.62c.14.19.22.43.22.68c0 .65-.53 1.18-1.18 1.18c0 .01-.03.01-.05.01M71.4 89.66l32.82 4.25l-12.94-40.55zM40.19 34.91a5.46 5.46 0 0 1-5.46 5.46c-3.01 0-5.46-2.45-5.46-5.46c0-3.02 2.44-5.46 5.46-5.46s5.46 2.44 5.46 5.46"/><path d="M34.73 41.54a6.65 6.65 0 0 1-6.64-6.64a6.65 6.65 0 0 1 6.64-6.64a6.65 6.65 0 0 1 6.64 6.64a6.65 6.65 0 0 1-6.64 6.64m0-10.91c-2.36 0-4.28 1.92-4.28 4.28s1.92 4.28 4.28 4.28s4.29-1.92 4.29-4.28s-1.93-4.28-4.29-4.28m58.85-1.18c3.01.18 5.31 2.77 5.13 5.78c-.17 3.01-2.77 5.3-5.77 5.13a5.45 5.45 0 0 1-5.13-5.77c.18-3.02 2.76-5.32 5.77-5.14"/><path d="m93.26 41.54l-.39-.01c-1.77-.1-3.4-.89-4.57-2.21a6.62 6.62 0 0 1-1.67-4.8a6.647 6.647 0 0 1 6.63-6.25l.39.01c3.66.22 6.46 3.38 6.24 7.03a6.64 6.64 0 0 1-6.63 6.23m.23-10.92c-2.5 0-4.37 1.77-4.5 4.03c-.07 1.14.31 2.24 1.07 3.1s1.8 1.36 2.95 1.43l.25.01c2.26 0 4.14-1.77 4.27-4.03c.14-2.36-1.67-4.39-4.03-4.54zM36.43 50.38L22.09 95.32l36.36-4.71z"/><path d="M22.09 96.5c-.34 0-.68-.15-.91-.42c-.26-.31-.34-.73-.22-1.11L35.3 50.03c.14-.45.54-.77 1.01-.81c.51-.05.92.19 1.15.61l22.02 40.23c.18.34.19.75.01 1.1c-.17.35-.51.58-.9.64l-36.36 4.71c-.04-.01-.09-.01-.14-.01m14.63-43.14L23.77 93.92l32.82-4.25z"/></g><use href="#notoV1Crown1"/><use href="#notoV1Crown1"/><defs><path id="notoV1Crown0" d="M119.5 53.43a1.18 1.18 0 0 0-1.29.22L87.25 82.71L65.16 49.72c-.22-.33-.58-.52-.98-.52c-.39 0-.76.19-.98.51l-22.19 33l-30.95-29.07a1.18 1.18 0 0 0-1.29-.22c-.43.19-.71.63-.69 1.1l1.27 47.52c0 10.33 24.06 18.43 54.78 18.43s54.78-8.1 54.78-18.4l1.27-47.55c.02-.46-.25-.9-.68-1.09"/><path id="notoV1Crown1" fill="#fcc21b" d="M72.17 28.76c0 4.51-3.66 8.17-8.17 8.17s-8.18-3.66-8.18-8.17c0-4.52 3.66-8.17 8.18-8.17s8.17 3.65 8.17 8.17m-58.72 6.15c0 3.58-2.9 6.48-6.49 6.48c-3.58 0-6.48-2.9-6.48-6.48c0-3.59 2.9-6.49 6.48-6.49c3.59 0 6.49 2.9 6.49 6.49m101.09 0c0 3.58 2.9 6.48 6.49 6.48c3.58 0 6.49-2.9 6.49-6.48a6.49 6.49 0 0 0-6.49-6.49a6.49 6.49 0 0 0-6.49 6.49"/></defs><use fill="#fcc21b" href="#notoV1Crown0"/><clipPath id="notoV1Crown2"><use href="#notoV1Crown0"/></clipPath><path fill="#d7598b" d="m119.91 78.06l.01.01l-.59 18.85h-.01c-4.2-.13-7.46-4.45-7.3-9.66c.16-5.22 3.69-9.33 7.89-9.2m-111.54 0l-.01.01l.58 18.85h.02c4.19-.13 7.46-4.45 7.29-9.66c-.16-5.22-3.69-9.33-7.88-9.2" clip-path="url(#notoV1Crown2)"/><path fill="#d7598b" d="M72.8 96.55c0 5.58-3.88 10.11-8.67 10.11c-4.78 0-8.66-4.53-8.66-10.11c0-5.59 3.88-10.11 8.66-10.11c4.79-.01 8.67 4.52 8.67 10.11"/><g fill="#ed6c30"><path d="M89.9 102.14c-.13 2.7-2.12 4.79-4.44 4.68c-2.31-.11-4.08-2.4-3.94-5.09c.14-2.71 2.13-4.8 4.44-4.68c2.31.1 4.07 2.39 3.94 5.09"/><ellipse cx="103.04" cy="98.95" rx="4.89" ry="4.2" transform="rotate(-87.013 103.044 98.958)"/></g><g fill="#ed6c30"><path d="M38.37 102.14c.13 2.7 2.12 4.79 4.44 4.68c2.31-.11 4.08-2.4 3.94-5.09c-.13-2.71-2.12-4.8-4.43-4.68c-2.32.1-4.09 2.39-3.95 5.09"/><ellipse cx="25.23" cy="98.95" rx="4.19" ry="4.89" transform="rotate(-2.987 25.234 98.957)"/></g></svg>
|
After Width: | Height: | Size: 3.2 KiB |
@ -111,11 +111,11 @@
|
|||||||
submitEnabled = false;
|
submitEnabled = false;
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
for (const k of key) {
|
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;
|
if (!k.options.validate(target[k.key])) return;
|
||||||
} else {
|
|
||||||
if (!target[k.key]) return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,20 +9,17 @@
|
|||||||
// inputs
|
// inputs
|
||||||
let { available, value = $bindable(), readonly }: Props = $props();
|
let { available, value = $bindable(), readonly }: Props = $props();
|
||||||
|
|
||||||
// idk why, but this is needed to trigger loop reactivity
|
|
||||||
let reactiveValue = $derived(value);
|
|
||||||
|
|
||||||
// callbacks
|
// callbacks
|
||||||
function onOptionSelect(e: Event) {
|
function onOptionSelect(e: Event) {
|
||||||
const selected = Number((e.target as HTMLSelectElement).value);
|
const selected = Number((e.target as HTMLSelectElement).value);
|
||||||
|
|
||||||
reactiveValue |= selected;
|
value |= selected;
|
||||||
|
|
||||||
(e.target as HTMLSelectElement).value = '-';
|
(e.target as HTMLSelectElement).value = '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBadgeRemove(flag: number) {
|
function onBadgeRemove(flag: number) {
|
||||||
reactiveValue &= ~flag;
|
value &= ~flag;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -31,20 +28,22 @@
|
|||||||
<select class="select select-xs w-min" onchange={onOptionSelect}>
|
<select class="select select-xs w-min" onchange={onOptionSelect}>
|
||||||
<option selected hidden>-</option>
|
<option selected hidden>-</option>
|
||||||
{#each Object.entries(available) as [flag, badge] (flag)}
|
{#each Object.entries(available) as [flag, badge] (flag)}
|
||||||
<option value={flag} hidden={(reactiveValue & Number(flag)) !== 0}>{badge}</option>
|
<option value={flag} hidden={(value & Number(flag)) !== 0}>{badge}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flow flex-wrap gap-2">
|
<div class="flex flow flex-wrap gap-2">
|
||||||
{#each Object.entries(available) as [flag, badge] (flag)}
|
{#key value}
|
||||||
{#if (reactiveValue & Number(flag)) !== 0}
|
{#each Object.entries(available) as [flag, badge] (flag)}
|
||||||
<div class="badge badge-outline gap-1">
|
{#if (value & Number(flag)) !== 0}
|
||||||
{#if !readonly}
|
<div class="badge badge-outline gap-1">
|
||||||
<button class="cursor-pointer" type="button" onclick={() => onBadgeRemove(Number(flag))}>✕</button>
|
{#if !readonly}
|
||||||
{/if}
|
<button class="cursor-pointer" type="button" onclick={() => onBadgeRemove(Number(flag))}>✕</button>
|
||||||
<span>{badge}</span>
|
{/if}
|
||||||
</div>
|
<span>{badge}</span>
|
||||||
{/if}
|
</div>
|
||||||
{/each}
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/key}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
|
// types
|
||||||
interface Props {
|
interface Props {
|
||||||
id?: string;
|
id?: string;
|
||||||
value?: string | null;
|
value?: string | null;
|
||||||
@ -19,6 +20,7 @@
|
|||||||
notice?: Snippet;
|
notice?: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// inputs
|
||||||
let {
|
let {
|
||||||
id,
|
id,
|
||||||
value = $bindable(),
|
value = $bindable(),
|
||||||
|
@ -61,8 +61,9 @@ CREATE TABLE IF NOT EXISTS team_draft (
|
|||||||
|
|
||||||
-- death
|
-- death
|
||||||
CREATE TABLE IF NOT EXISTS death (
|
CREATE TABLE IF NOT EXISTS death (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
message VARCHAR(1024) NOT NULL,
|
message VARCHAR(1024) NOT NULL,
|
||||||
dead_user_id INT NOT NULL,
|
dead_user_id INT NOT NULL UNIQUE,
|
||||||
killer_user_id INT,
|
killer_user_id INT,
|
||||||
FOREIGN KEY (dead_user_id) REFERENCES user(id) ON DELETE CASCADE,
|
FOREIGN KEY (dead_user_id) REFERENCES user(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (killer_user_id) REFERENCES user(id) ON DELETE CASCADE
|
FOREIGN KEY (killer_user_id) REFERENCES user(id) ON DELETE CASCADE
|
||||||
|
@ -83,9 +83,13 @@ import {
|
|||||||
setSettings
|
setSettings
|
||||||
} from './schema/settings';
|
} from './schema/settings';
|
||||||
import {
|
import {
|
||||||
addDeath,
|
|
||||||
type AddDeathReq,
|
type AddDeathReq,
|
||||||
|
addDeath,
|
||||||
death,
|
death,
|
||||||
|
deleteDeath,
|
||||||
|
type DeleteDeathReq,
|
||||||
|
editDeath,
|
||||||
|
type EditDeathReq,
|
||||||
getDeathByUserId,
|
getDeathByUserId,
|
||||||
type GetDeathByUserIdReq,
|
type GetDeathByUserIdReq,
|
||||||
getDeaths,
|
getDeaths,
|
||||||
@ -107,6 +111,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
addReport,
|
addReport,
|
||||||
type AddReportReq,
|
type AddReportReq,
|
||||||
|
editReport,
|
||||||
|
type EditReportReq,
|
||||||
getReportById,
|
getReportById,
|
||||||
type GetReportById,
|
type GetReportById,
|
||||||
getReportByUrlHash,
|
getReportByUrlHash,
|
||||||
@ -130,6 +136,8 @@ import {
|
|||||||
deleteStrikeReason
|
deleteStrikeReason
|
||||||
} from '@db/schema/strikeReason.ts';
|
} from '@db/schema/strikeReason.ts';
|
||||||
import {
|
import {
|
||||||
|
deleteStrike,
|
||||||
|
type DeleteStrikeReq,
|
||||||
editStrike,
|
editStrike,
|
||||||
type EditStrikeReq,
|
type EditStrikeReq,
|
||||||
getStrikeByReportId,
|
getStrikeByReportId,
|
||||||
@ -158,7 +166,13 @@ import {
|
|||||||
type DeleteBlockedUserReq,
|
type DeleteBlockedUserReq,
|
||||||
deleteBlockedUser
|
deleteBlockedUser
|
||||||
} from '@db/schema/blockedUser.ts';
|
} from '@db/schema/blockedUser.ts';
|
||||||
import { addReportAttachment, type AddReportAttachmentReq, reportAttachment } from '@db/schema/reportAttachment.ts';
|
import {
|
||||||
|
addReportAttachment,
|
||||||
|
type AddReportAttachmentReq,
|
||||||
|
getReportAttachments,
|
||||||
|
type GetReportAttachmentsReq,
|
||||||
|
reportAttachment
|
||||||
|
} from '@db/schema/reportAttachment.ts';
|
||||||
|
|
||||||
export class Database {
|
export class Database {
|
||||||
protected readonly db: MySql2Database<{
|
protected readonly db: MySql2Database<{
|
||||||
@ -263,11 +277,14 @@ export class Database {
|
|||||||
|
|
||||||
/* death */
|
/* death */
|
||||||
addDeath = (values: AddDeathReq) => addDeath(this.db, values);
|
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);
|
getDeathByUserId = (values: GetDeathByUserIdReq) => getDeathByUserId(this.db, values);
|
||||||
getDeaths = (values: GetDeathsReq) => getDeaths(this.db, values);
|
getDeaths = (values: GetDeathsReq) => getDeaths(this.db, values);
|
||||||
|
|
||||||
/* report */
|
/* report */
|
||||||
addReport = (values: AddReportReq) => addReport(this.db, values);
|
addReport = (values: AddReportReq) => addReport(this.db, values);
|
||||||
|
editReport = (values: EditReportReq) => editReport(this.db, values);
|
||||||
submitReport = (values: SubmitReportReq) => submitReport(this.db, values);
|
submitReport = (values: SubmitReportReq) => submitReport(this.db, values);
|
||||||
getReports = (values: GetReportsReq) => getReports(this.db, values);
|
getReports = (values: GetReportsReq) => getReports(this.db, values);
|
||||||
getReportById = (values: GetReportById) => getReportById(this.db, values);
|
getReportById = (values: GetReportById) => getReportById(this.db, values);
|
||||||
@ -275,6 +292,7 @@ export class Database {
|
|||||||
|
|
||||||
/* report attachment */
|
/* report attachment */
|
||||||
addReportAttachment = (values: AddReportAttachmentReq) => addReportAttachment(this.db, values);
|
addReportAttachment = (values: AddReportAttachmentReq) => addReportAttachment(this.db, values);
|
||||||
|
getReportAttachments = (values: GetReportAttachmentsReq) => getReportAttachments(this.db, values);
|
||||||
|
|
||||||
/* report status */
|
/* report status */
|
||||||
getReportStatus = (values: GetReportStatusReq) => getReportStatus(this.db, values);
|
getReportStatus = (values: GetReportStatusReq) => getReportStatus(this.db, values);
|
||||||
@ -288,6 +306,7 @@ export class Database {
|
|||||||
|
|
||||||
/* strikes */
|
/* strikes */
|
||||||
editStrike = (values: EditStrikeReq) => editStrike(this.db, values);
|
editStrike = (values: EditStrikeReq) => editStrike(this.db, values);
|
||||||
|
deleteStrike = (values: DeleteStrikeReq) => deleteStrike(this.db, values);
|
||||||
getStrikeByReportId = (values: GetStrikeByReportIdReq) => getStrikeByReportId(this.db, values);
|
getStrikeByReportId = (values: GetStrikeByReportIdReq) => getStrikeByReportId(this.db, values);
|
||||||
getStrikesByTeamId = (values: GetStrikesByTeamIdReq) => getStrikesByTeamId(this.db, values);
|
getStrikesByTeamId = (values: GetStrikesByTeamIdReq) => getStrikesByTeamId(this.db, values);
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { int, mysqlTable, varchar } from 'drizzle-orm/mysql-core';
|
import { alias, int, mysqlTable, varchar } from 'drizzle-orm/mysql-core';
|
||||||
import type { MySql2Database } from 'drizzle-orm/mysql2';
|
import type { MySql2Database } from 'drizzle-orm/mysql2';
|
||||||
import { user } from './user.ts';
|
import { user } from './user.ts';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
@ -6,6 +6,7 @@ import { eq } from 'drizzle-orm';
|
|||||||
type Database = MySql2Database<{ death: typeof death }>;
|
type Database = MySql2Database<{ death: typeof death }>;
|
||||||
|
|
||||||
export const death = mysqlTable('death', {
|
export const death = mysqlTable('death', {
|
||||||
|
id: int('id').primaryKey().autoincrement(),
|
||||||
message: varchar('message', { length: 1024 }).notNull(),
|
message: varchar('message', { length: 1024 }).notNull(),
|
||||||
deadUserId: int('dead_user_id')
|
deadUserId: int('dead_user_id')
|
||||||
.notNull()
|
.notNull()
|
||||||
@ -15,19 +16,46 @@ export const death = mysqlTable('death', {
|
|||||||
|
|
||||||
export type AddDeathReq = {
|
export type AddDeathReq = {
|
||||||
message: string;
|
message: string;
|
||||||
killerUserId?: number;
|
killerUserId?: number | null;
|
||||||
deadUserId: number;
|
deadUserId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EditDeathReq = {
|
||||||
|
id: number;
|
||||||
|
message: string;
|
||||||
|
killerUserId?: number | null;
|
||||||
|
deadUserId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteDeathReq = {
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type GetDeathByUserIdReq = {
|
export type GetDeathByUserIdReq = {
|
||||||
userId: number;
|
userId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetDeathsReq = {};
|
export type GetDeathsReq = {};
|
||||||
export type GetDeathsRes = (typeof death.$inferSelect)[];
|
|
||||||
|
|
||||||
export async function addDeath(db: Database, values: AddDeathReq) {
|
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) {
|
export async function getDeathByUserId(db: Database, values: GetDeathByUserIdReq) {
|
||||||
@ -36,7 +64,24 @@ export async function getDeathByUserId(db: Database, values: GetDeathByUserIdReq
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
export async function getDeaths(db: Database, _values: GetDeathsReq) {
|
||||||
export async function getDeaths(db: Database, values: GetDeathsReq): Promise<GetDeathsReq> {
|
const killed = alias(user, 'killed');
|
||||||
return db.query.death.findMany();
|
const killer = alias(user, 'killer');
|
||||||
|
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
id: death.id,
|
||||||
|
message: death.message,
|
||||||
|
killed: {
|
||||||
|
id: killed.id,
|
||||||
|
username: killed.username
|
||||||
|
},
|
||||||
|
killer: {
|
||||||
|
id: killer.id,
|
||||||
|
username: killer.username
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.from(death)
|
||||||
|
.innerJoin(killed, eq(death.deadUserId, killed.id))
|
||||||
|
.leftJoin(killer, eq(death.killerUserId, killer.id));
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ import { reportStatus } from './reportStatus.ts';
|
|||||||
import { generateRandomString } from '@util/random.ts';
|
import { generateRandomString } from '@util/random.ts';
|
||||||
import { team } from '@db/schema/team.ts';
|
import { team } from '@db/schema/team.ts';
|
||||||
import { BASE_PATH } from 'astro:env/server';
|
import { BASE_PATH } from 'astro:env/server';
|
||||||
|
import { strikeReason } from '@db/schema/strikeReason.ts';
|
||||||
|
import { strike } from '@db/schema/strike.ts';
|
||||||
|
|
||||||
type Database = MySql2Database<{ report: typeof report }>;
|
type Database = MySql2Database<{ report: typeof report }>;
|
||||||
|
|
||||||
@ -26,6 +28,11 @@ export type AddReportReq = {
|
|||||||
reportedTeamId?: number | null;
|
reportedTeamId?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EditReportReq = {
|
||||||
|
id: number;
|
||||||
|
reportedTeamId: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type SubmitReportReq = {
|
export type SubmitReportReq = {
|
||||||
urlHash: string;
|
urlHash: string;
|
||||||
reason: string;
|
reason: string;
|
||||||
@ -63,6 +70,12 @@ export async function addReport(db: Database, values: AddReportReq) {
|
|||||||
return Object.assign(r[0], { url: `${BASE_PATH}/report/${urlHash}` });
|
return Object.assign(r[0], { url: `${BASE_PATH}/report/${urlHash}` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function editReport(db: Database, values: EditReportReq) {
|
||||||
|
return db.update(report).set({
|
||||||
|
reportedTeamId: values.reportedTeamId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function submitReport(db: Database, values: SubmitReportReq) {
|
export async function submitReport(db: Database, values: SubmitReportReq) {
|
||||||
return db
|
return db
|
||||||
.update(report)
|
.update(report)
|
||||||
@ -100,6 +113,7 @@ export async function getReports(db: Database, values: GetReportsReq) {
|
|||||||
id: report.id,
|
id: report.id,
|
||||||
reason: report.reason,
|
reason: report.reason,
|
||||||
body: report.body,
|
body: report.body,
|
||||||
|
urlHash: report.urlHash,
|
||||||
createdAt: report.createdAt,
|
createdAt: report.createdAt,
|
||||||
reporter: {
|
reporter: {
|
||||||
id: reporterTeam.id,
|
id: reporterTeam.id,
|
||||||
@ -113,12 +127,17 @@ export async function getReports(db: Database, values: GetReportsReq) {
|
|||||||
status: reportStatus.status,
|
status: reportStatus.status,
|
||||||
notice: reportStatus.notice,
|
notice: reportStatus.notice,
|
||||||
statement: reportStatus.statement
|
statement: reportStatus.statement
|
||||||
|
},
|
||||||
|
strike: {
|
||||||
|
strikeReasonId: strikeReason.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.from(report)
|
.from(report)
|
||||||
.innerJoin(reporterTeam, eq(report.reporterTeamId, reporterTeam.id))
|
.innerJoin(reporterTeam, eq(report.reporterTeamId, reporterTeam.id))
|
||||||
.leftJoin(reportedTeam, eq(report.reportedTeamId, reportedTeam.id))
|
.leftJoin(reportedTeam, eq(report.reportedTeamId, reportedTeam.id))
|
||||||
.leftJoin(reportStatus, eq(report.id, reportStatus.reportId))
|
.leftJoin(reportStatus, eq(report.id, reportStatus.reportId))
|
||||||
|
.leftJoin(strike, eq(report.id, strike.reportId))
|
||||||
|
.leftJoin(strikeReason, eq(strike.strikeReasonId, strikeReason.id))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
values.reporter != null ? eq(report.reporterTeamId, reporterIdSubquery!.id) : undefined,
|
values.reporter != null ? eq(report.reporterTeamId, reporterIdSubquery!.id) : undefined,
|
||||||
|
@ -21,7 +21,6 @@ export type AddReportAttachmentReq = {
|
|||||||
|
|
||||||
export type GetReportAttachmentsReq = {
|
export type GetReportAttachmentsReq = {
|
||||||
reportId: number;
|
reportId: number;
|
||||||
fileIds: number[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function addReportAttachment(db: Database, values: AddReportAttachmentReq) {
|
export async function addReportAttachment(db: Database, values: AddReportAttachmentReq) {
|
||||||
|
@ -22,6 +22,10 @@ export type EditStrikeReq = {
|
|||||||
strikeReasonId: number;
|
strikeReasonId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DeleteStrikeReq = {
|
||||||
|
reportId: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type GetStrikeByReportIdReq = {
|
export type GetStrikeByReportIdReq = {
|
||||||
reportId: number;
|
reportId: number;
|
||||||
};
|
};
|
||||||
@ -46,13 +50,21 @@ export async function editStrike(db: Database, values: EditStrikeReq) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteStrike(db: Database, values: DeleteStrikeReq) {
|
||||||
|
return db.delete(strike).where(eq(strike.reportId, values.reportId)).limit(1);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getStrikeByReportId(db: Database, values: GetStrikeByReportIdReq) {
|
export async function getStrikeByReportId(db: Database, values: GetStrikeByReportIdReq) {
|
||||||
return db.query.strike.findFirst({
|
const strikes = await db
|
||||||
with: {
|
.select({
|
||||||
strikeReason: true
|
strike,
|
||||||
},
|
strikeReason
|
||||||
where: eq(strike.reportId, values.reportId)
|
})
|
||||||
});
|
.from(strike)
|
||||||
|
.where(eq(strike.reportId, values.reportId))
|
||||||
|
.leftJoin(strikeReason, eq(strike.strikeReasonId, strikeReason.id));
|
||||||
|
|
||||||
|
return strikes[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getStrikesByTeamId(db: Database, values: GetStrikesByTeamIdReq) {
|
export async function getStrikesByTeamId(db: Database, values: GetStrikesByTeamIdReq) {
|
||||||
|
@ -40,6 +40,13 @@ const adminTabs = [
|
|||||||
href: 'admin/teams',
|
href: 'admin/teams',
|
||||||
name: 'Teams',
|
name: 'Teams',
|
||||||
icon: 'heroicons:users',
|
icon: 'heroicons:users',
|
||||||
|
subTabs: [
|
||||||
|
{
|
||||||
|
href: 'admin/teams/dead',
|
||||||
|
name: 'Tote Spieler',
|
||||||
|
icon: 'heroicons:x-mark'
|
||||||
|
}
|
||||||
|
],
|
||||||
enabled: session?.permissions.users
|
enabled: session?.permissions.users
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -72,6 +79,12 @@ const adminTabs = [
|
|||||||
name: 'Einstellungen',
|
name: 'Einstellungen',
|
||||||
icon: 'heroicons:adjustments-horizontal',
|
icon: 'heroicons:adjustments-horizontal',
|
||||||
enabled: session?.permissions.settings
|
enabled: session?.permissions.settings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: 'admin/tools',
|
||||||
|
name: 'Tools',
|
||||||
|
icon: 'heroicons:wrench-screwdriver',
|
||||||
|
enabled: session?.permissions.tools
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
@ -92,26 +105,29 @@ const adminTabs = [
|
|||||||
}
|
}
|
||||||
<div class="divider mx-1 my-1"></div>
|
<div class="divider mx-1 my-1"></div>
|
||||||
{
|
{
|
||||||
adminTabs.map((tab) => (
|
adminTabs.map(
|
||||||
<li>
|
(tab) =>
|
||||||
<a href={tab.href}>
|
tab.enabled && (
|
||||||
<Icon name={tab.icon} />
|
<li>
|
||||||
<span>{tab.name}</span>
|
<a href={tab.href}>
|
||||||
</a>
|
<Icon name={tab.icon} />
|
||||||
{tab.subTabs && (
|
<span>{tab.name}</span>
|
||||||
<ul>
|
</a>
|
||||||
{tab.subTabs.map((subTab) => (
|
{tab.subTabs && (
|
||||||
<li>
|
<ul>
|
||||||
<a href={subTab.href}>
|
{tab.subTabs.map((subTab) => (
|
||||||
<Icon name={subTab.icon} />
|
<li>
|
||||||
<span>{subTab.name}</span>
|
<a href={subTab.href}>
|
||||||
</a>
|
<Icon name={subTab.icon} />
|
||||||
</li>
|
<span>{subTab.name}</span>
|
||||||
))}
|
</a>
|
||||||
</ul>
|
</li>
|
||||||
)}
|
))}
|
||||||
</li>
|
</ul>
|
||||||
))
|
)}
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
Astro.slots.has('actions') && (
|
Astro.slots.has('actions') && (
|
||||||
|
27
src/pages/admin/reports/attachment/[fileHash].ts
Normal file
27
src/pages/admin/reports/attachment/[fileHash].ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { Session } from '@util/session.ts';
|
||||||
|
import { Permissions } from '@util/permissions.ts';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { UPLOAD_PATH } from 'astro:env/server';
|
||||||
|
|
||||||
|
export const GET: APIRoute = async ({ params, cookies }) => {
|
||||||
|
Session.actionSessionFromCookies(cookies, Permissions.Reports);
|
||||||
|
|
||||||
|
if (!UPLOAD_PATH) return new Response(null, { status: 404 });
|
||||||
|
|
||||||
|
const fileHash = params.fileHash as string;
|
||||||
|
const filePath = path.join(UPLOAD_PATH, fileHash);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) return new Response(null, { status: 404 });
|
||||||
|
|
||||||
|
const fileStat = fs.statSync(filePath);
|
||||||
|
const fileStream = fs.createReadStream(filePath);
|
||||||
|
|
||||||
|
return new Response(fileStream as any, {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Length': fileStat.size.toString()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
16
src/pages/admin/teams/dead.astro
Normal file
16
src/pages/admin/teams/dead.astro
Normal 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>
|
@ -6,7 +6,7 @@ import { Session } from '@util/session.ts';
|
|||||||
import { Permissions } from '@util/permissions.ts';
|
import { Permissions } from '@util/permissions.ts';
|
||||||
import { BASE_PATH } from 'astro:env/server';
|
import { BASE_PATH } from 'astro:env/server';
|
||||||
|
|
||||||
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Admin);
|
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Users);
|
||||||
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
|
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
|
||||||
---
|
---
|
||||||
|
|
14
src/pages/admin/tools/index.astro
Normal file
14
src/pages/admin/tools/index.astro
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
import { Session } from '@util/session';
|
||||||
|
import { Permissions } from '@util/permissions';
|
||||||
|
import { BASE_PATH } from 'astro:env/server';
|
||||||
|
import AdminLayout from '@layouts/admin/AdminLayout.astro';
|
||||||
|
import Tools from '@app/admin/tools/Tools.svelte';
|
||||||
|
|
||||||
|
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Tools);
|
||||||
|
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
|
||||||
|
---
|
||||||
|
|
||||||
|
<AdminLayout title="Reports">
|
||||||
|
<Tools client:load />
|
||||||
|
</AdminLayout>
|
@ -6,7 +6,7 @@ import { Session } from '@util/session.ts';
|
|||||||
import { Permissions } from '@util/permissions.ts';
|
import { Permissions } from '@util/permissions.ts';
|
||||||
import { BASE_PATH } from 'astro:env/server';
|
import { BASE_PATH } from 'astro:env/server';
|
||||||
|
|
||||||
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Admin);
|
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Users);
|
||||||
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
|
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ import { Session } from '@util/session.ts';
|
|||||||
import { Permissions } from '@util/permissions.ts';
|
import { Permissions } from '@util/permissions.ts';
|
||||||
import { BASE_PATH } from 'astro:env/server';
|
import { BASE_PATH } from 'astro:env/server';
|
||||||
|
|
||||||
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Admin);
|
const session = Session.sessionFromCookies(Astro.cookies, Permissions.Users);
|
||||||
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
|
if (!session) return Astro.redirect(`${BASE_PATH}/admin`);
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import type { APIRoute } from 'astro';
|
|||||||
import { z } from 'astro:schema';
|
import { z } from 'astro:schema';
|
||||||
import { API_SECRET } from 'astro:env/server';
|
import { API_SECRET } from 'astro:env/server';
|
||||||
import { db } from '@db/database.ts';
|
import { db } from '@db/database.ts';
|
||||||
|
import { sendWebhook, WebhookAction } from '@util/webhook.ts';
|
||||||
|
|
||||||
const postSchema = z.object({
|
const postSchema = z.object({
|
||||||
reporter: z.string(),
|
reporter: z.string(),
|
||||||
@ -75,6 +76,7 @@ export const PUT: APIRoute = async ({ request }) => {
|
|||||||
const report = await tx.addReport({
|
const report = await tx.addReport({
|
||||||
reporterTeamId: reporterTeam?.team.id,
|
reporterTeamId: reporterTeam?.team.id,
|
||||||
reportedTeamId: reportedTeam.team.id,
|
reportedTeamId: reportedTeam.team.id,
|
||||||
|
createdAt: new Date(),
|
||||||
reason: parsed.reason,
|
reason: parsed.reason,
|
||||||
body: parsed.body
|
body: parsed.body
|
||||||
});
|
});
|
||||||
@ -92,5 +94,14 @@ export const PUT: APIRoute = async ({ request }) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const strikes = await db.getStrikesByTeamId({ teamId: reportedTeam.team.id });
|
||||||
|
const teamMembers = await db.getTeamMembersByTeamId({ teamId: reportedTeam.team.id });
|
||||||
|
|
||||||
|
// send webhook in background
|
||||||
|
sendWebhook(WebhookAction.Strike, {
|
||||||
|
users: teamMembers.map((tm) => tm.user.uuid!),
|
||||||
|
totalWeight: strikes.map((strike) => strike.reason.weight).reduce((a, b) => a + b, 0)
|
||||||
|
});
|
||||||
|
|
||||||
return new Response(null, { status: 200 });
|
return new Response(null, { status: 200 });
|
||||||
};
|
};
|
||||||
|
@ -103,10 +103,14 @@ const information = [
|
|||||||
|
|
||||||
<div class="bg-base-100 flex flex-col space-y-10 items-center py-10">
|
<div class="bg-base-100 flex flex-col space-y-10 items-center py-10">
|
||||||
<h2 id="teams" class="text-4xl mb-10">Teams</h2>
|
<h2 id="teams" class="text-4xl mb-10">Teams</h2>
|
||||||
<p class="text-sm text-center mb-2 mx-1">
|
{
|
||||||
Bei unvollständigen Teams muss sich der zweite Mitspieler noch registrieren. Unvollständige Teams werden bei
|
signupEnabled && (
|
||||||
Anmeldeschluss gelöscht.
|
<p class="text-sm text-center mb-2 mx-1">
|
||||||
</p>
|
Bei unvollständigen Teams muss sich der zweite Mitspieler noch registrieren. Unvollständige Teams werden bei
|
||||||
|
Anmeldeschluss gelöscht.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
<Teams {teams} {deaths} />
|
<Teams {teams} {deaths} />
|
||||||
</div>
|
</div>
|
||||||
</WebsiteLayout>
|
</WebsiteLayout>
|
||||||
|
2
src/util/media.ts
Normal file
2
src/util/media.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const allowedImageTypes = ['image/png', 'image/jpeg', 'image/webp', 'image/avif'];
|
||||||
|
export const allowedVideoTypes = ['video/mp4', 'video/webm'];
|
@ -12,3 +12,46 @@ export async function getJavaUuid(username: string) {
|
|||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return `${id.substring(0, 8)}-${id.substring(8, 12)}-${id.substring(12, 16)}-${id.substring(16, 20)}-${id.substring(20)}`;
|
return `${id.substring(0, 8)}-${id.substring(8, 12)}-${id.substring(12, 16)}-${id.substring(16, 20)}-${id.substring(20)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://github.com/carlop3333/XUIDGrabber/blob/main/grabber.js
|
||||||
|
export async function getBedrockUuid(username: string): Promise<string> {
|
||||||
|
const initialPageResponse = await fetch('https://cxkes.me/xbox/xuid');
|
||||||
|
const initialPageContent = await initialPageResponse.text();
|
||||||
|
const token = /name="_token"\svalue="(?<token>\w+)"/.exec(initialPageContent)?.groups?.token;
|
||||||
|
|
||||||
|
const cookies = initialPageResponse.headers.get('set-cookie')?.split(' ');
|
||||||
|
if (token === undefined || cookies === undefined || cookies.length < 11) return null;
|
||||||
|
|
||||||
|
const requestBody = new URLSearchParams();
|
||||||
|
requestBody.set('_token', token);
|
||||||
|
requestBody.set('gamertag', username);
|
||||||
|
|
||||||
|
const resultPageResponse = await fetch('https://cxkes.me/xbox/xuid', {
|
||||||
|
method: 'post',
|
||||||
|
body: requestBody,
|
||||||
|
// prettier-ignore
|
||||||
|
headers: {
|
||||||
|
'Host': 'www.cxkes.me',
|
||||||
|
'Accept-Encoding': 'gzip, deflate,br',
|
||||||
|
'Content-Length': Buffer.byteLength(requestBody.toString()).toString(),
|
||||||
|
'Origin': 'https://www.cxkes.me',
|
||||||
|
'DNT': '1',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
'Referer': 'https://www.cxkes.me/xbox/xuid',
|
||||||
|
'Cookie': `${cookies[0]} ${cookies[10].slice(0, cookies[10].length - 1)}`,
|
||||||
|
'Upgrade-Insecure-Requests': '1',
|
||||||
|
'Sec-Fectch-Dest': 'document',
|
||||||
|
'Sec-Fetch-Mode': 'navigate',
|
||||||
|
'Sec-Fetch-Site': 'same-origin',
|
||||||
|
'Sec-Fetch-User': '?1',
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en-US,es;q=0.8,en-US;q=0.5,en;q=0.3',
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const resultPageContent = await resultPageResponse.text();
|
||||||
|
let xuid: string | undefined;
|
||||||
|
if ((xuid = /id="xuidHex">(?<xuid>\w+)</.exec(resultPageContent)?.groups?.xuid) === undefined) throw new Error();
|
||||||
|
return `00000000-0000-0000-${xuid.substring(0, 4)}-${xuid.substring(4)}`;
|
||||||
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
export function getObjectEntryByKey(key: string, data: { [key: string]: any }): any | undefined {
|
export function getObjectEntryByKey(key: string, data: { [key: string]: any }): any | undefined {
|
||||||
let entry = data;
|
let entry = data;
|
||||||
for (const part of key.split('.')) {
|
for (const part of key.split('.')) {
|
||||||
if ((entry = entry[part]) === undefined) {
|
entry = entry[part];
|
||||||
return undefined;
|
if (entry === null || typeof entry !== 'object') return entry;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
@ -53,10 +53,10 @@ export class Permissions {
|
|||||||
return (this.value & Permissions.Reports.value) != 0;
|
return (this.value & Permissions.Reports.value) != 0;
|
||||||
}
|
}
|
||||||
get feedback() {
|
get feedback() {
|
||||||
return (this.value & Permissions.Reports.value) != 0;
|
return (this.value & Permissions.Feedback.value) != 0;
|
||||||
}
|
}
|
||||||
get settings() {
|
get settings() {
|
||||||
return (this.value & Permissions.Reports.value) != 0;
|
return (this.value & Permissions.Settings.value) != 0;
|
||||||
}
|
}
|
||||||
get tools() {
|
get tools() {
|
||||||
return (this.value & Permissions.Tools.value) != 0;
|
return (this.value & Permissions.Tools.value) != 0;
|
||||||
|
@ -18,11 +18,12 @@ export async function sendWebhook<T extends WebhookAction>(action: T, data: Webh
|
|||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(WEBHOOK_ENDPOINT, {
|
const response = await fetch(WEBHOOK_ENDPOINT, {
|
||||||
body: JSON.stringify(data),
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
'x-webhook-action': action
|
'x-webhook-action': action
|
||||||
},
|
},
|
||||||
keepalive: false
|
body: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 200) return;
|
if (response.status === 200) return;
|
||||||
|
Reference in New Issue
Block a user