diff --git a/src/lib/components/Input/Input.svelte b/src/lib/components/Input/Input.svelte index 2764ed3..2270809 100644 --- a/src/lib/components/Input/Input.svelte +++ b/src/lib/components/Input/Input.svelte @@ -9,6 +9,7 @@ export let placeholder: string | null = null; export let required = false; export let disabled = false; + export let readonly = false; export let size: 'xs' | 'sm' | 'md' | 'lg' = 'md'; export let pickyWidth = true; @@ -85,6 +86,7 @@ {placeholder} {required} {disabled} + {readonly} bind:this={inputElement} autocomplete="off" on:input={(e) => { diff --git a/src/lib/components/Input/Search.svelte b/src/lib/components/Input/Search.svelte index 12d15c0..dae7ef7 100644 --- a/src/lib/components/Input/Search.svelte +++ b/src/lib/components/Input/Search.svelte @@ -1,7 +1,11 @@ <script lang="ts"> + import { createEventDispatcher } from 'svelte'; + export let id: string | null = null; export let value = ''; + export let inputValue = ''; export let suggestionRequired = false; + export let emptyAllowed = false; export let searchSuggestionFunc: ( input: string ) => Promise<{ name: string; value: string }[]> = () => Promise.resolve([]); @@ -10,7 +14,8 @@ export let label: string | null = null; export let required = false; - let inputValue: string; + const dispatch = createEventDispatcher(); + let searchSuggestions: { name: string; value: string }[] = []; $: if (!suggestionRequired) value = inputValue; </script> @@ -47,13 +52,18 @@ value = searchSuggestion.value; searchSuggestions = []; e.target?.setCustomValidity(''); + dispatch('submit', { input: inputValue, value: value }); + } else if (inputValue === '' && emptyAllowed) { + dispatch('submit', { input: '', value: '' }); } }); }} on:invalid={(e) => { if (invalidMessage != null) e.target?.setCustomValidity(invalidMessage); }} - pattern={suggestionRequired ? `${value ? inputValue : 'a^'}` : null} + pattern={suggestionRequired + ? `${value ? inputValue : 'a^' + (emptyAllowed ? '|$^' : '')}` + : null} /> </div> @@ -68,6 +78,7 @@ inputValue = searchSuggestion.name; value = searchSuggestion.value; searchSuggestions = []; + dispatch('submit', { input: inputValue, value: value }); }}>{searchSuggestion.name}</button > </li> @@ -76,6 +87,7 @@ {/if} </div> +<!-- close the search suggestions box when clicking outside --> {#if inputValue && searchSuggestions.length !== 0} <button class="absolute top-0 left-0 z-10 w-full h-full cursor-default" diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts index 4f293c3..35982a8 100644 --- a/src/lib/server/database.ts +++ b/src/lib/server/database.ts @@ -56,7 +56,7 @@ export class Report extends Model { @Column({ type: DataTypes.INTEGER, allowNull: false }) @ForeignKey(() => User) declare reporter_id: number; - @Column({ type: DataTypes.INTEGER, allowNull: false }) + @Column({ type: DataTypes.INTEGER }) @ForeignKey(() => User) declare reported_id: number; @Column({ type: DataTypes.INTEGER }) diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..4d96648 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,18 @@ +import { env } from '$env/dynamic/public'; + +export async function usernameSuggestions( + username: string +): Promise<{ name: string; value: string }[]> { + const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, { + method: 'POST', + body: JSON.stringify({ + limit: 6, + search: username, + slim: true + }) + }); + const json: { username: string; uuid: string }[] = await response.json(); + return json.map((v) => { + return { name: v.username, value: v.uuid }; + }); +} diff --git a/src/routes/admin/reports/+page.svelte b/src/routes/admin/reports/+page.svelte index 507099a..fa5027f 100644 --- a/src/routes/admin/reports/+page.svelte +++ b/src/routes/admin/reports/+page.svelte @@ -13,6 +13,8 @@ import NewReportModal from './NewReportModal.svelte'; import { onDestroy, onMount } from 'svelte'; import { goto } from '$app/navigation'; + import Search from '$lib/components/Input/Search.svelte'; + import { usernameSuggestions } from '$lib/utils'; export let data: PageData; @@ -83,7 +85,8 @@ auditor: data.self?.id || -1, notice: activeReport.notice || '', statement: activeReport.statement || '', - status: activeReport.status + status: activeReport.status, + reported: activeReport.reported?.uuid || null }) }); } @@ -93,7 +96,7 @@ </script> <div class="h-screen flex flex-row"> - <div class="w-full flex flex-col"> + <div class="w-full flex flex-col overflow-scroll"> <HeaderBar bind:reportFilter /> <hr class="divider my-1 mx-8 border-none" /> <table class="table table-fixed h-fit"> @@ -140,14 +143,17 @@ </button> </td> <td> - {report.reported.username} - <button - class="pl-1" - title="Nach Reportetem Spieler filtern" - on:click|stopPropagation={() => (reportFilter.reported = report.reported.username)} - > - <IconOutline name="magnifying-glass-outline" width="14" height="14" /> - </button> + {report.reported?.username || ''} + {#if report.reported?.id} + <button + class="pl-1" + title="Nach Reportetem Spieler filtern" + on:click|stopPropagation={() => + (reportFilter.reported = report.reported.username)} + > + <IconOutline name="magnifying-glass-outline" width="14" height="14" /> + </button> + {/if} </td> <td >{new Intl.DateTimeFormat('de-DE', { @@ -185,7 +191,7 @@ </div> {#if activeReport} <div - class="relative flex flex-col w-2/5 bg-base-200/50 px-4 py-6 overflow-scroll" + class="relative flex flex-col w-2/5 h-screen bg-base-200/50 px-4 py-6 overflow-scroll" transition:fly={{ x: 200, duration: 200 }} > <div class="absolute right-2 top-2 flex justify-center"> @@ -226,19 +232,26 @@ }}>✕</button > </div> - <h3 class="font-roboto font-semibold text-2xl">Report</h3> - <div class="break-words my-2"> - <i class="font-medium">{activeReport.reporter.username}</i> hat - <i class="font-medium">{activeReport.reported.username}</i> - am {new Intl.DateTimeFormat('de-DE', { - year: 'numeric', - month: 'long', - day: '2-digit', - hour: '2-digit', - minute: '2-digit' - }).format(new Date(activeReport.createdAt))} Uhr reportet. - </div> + <h3 class="font-roboto font-semibold text-2xl mb-2">Report</h3> <div class="w-full"> + <Input readonly={true} size="sm" value={activeReport.reporter.username} pickyWidth={false}> + <span slot="label">Reporter</span> + </Input> + <Search + size="sm" + suggestionRequired={true} + emptyAllowed={true} + searchSuggestionFunc={usernameSuggestions} + invalidMessage="Es können nur registrierte Spieler reportet werden" + label="Reporteter User" + inputValue={activeReport.reported?.username || ''} + on:submit={(e) => + (activeReport.reported = { + ...activeReport.reported, + username: e.detail.input, + uuid: e.detail.value + })} + /> <Textarea readonly={true} rows={1} label="Report Grund" value={activeReport.subject} /> <Textarea readonly={true} rows={4} label="Report Details" value={activeReport.body} /> </div> @@ -301,6 +314,11 @@ value="Speichern" on:click={async () => { await updateActiveReport(); + if (activeReport.reported?.username && activeReport.reported?.id === undefined) { + activeReport.reported.id = -1; + } else { + activeReport.reported = undefined; + } currentPageReports = [...currentPageReports]; if (activeReport.originalStatus !== 'reviewed' && activeReport.status === 'reviewed') { $reportCount -= 1; diff --git a/src/routes/admin/reports/+server.ts b/src/routes/admin/reports/+server.ts index 2301f3e..eb45966 100644 --- a/src/routes/admin/reports/+server.ts +++ b/src/routes/admin/reports/+server.ts @@ -92,6 +92,7 @@ export const PATCH = (async ({ request, cookies }) => { const data: { id: number; + reported: string | null; auditor: number; notice: string | null; statement: string | null; @@ -102,9 +103,13 @@ export const PATCH = (async ({ request, cookies }) => { const report = await Report.findOne({ where: { id: data.id } }); const admin = await Admin.findOne({ where: { id: data.auditor } }); - if (report === null || (admin === null && data.auditor != -1)) + const reported = data.reported + ? await User.findOne({ where: { uuid: data.reported } }) + : undefined; + if (report === null || (admin === null && data.auditor != -1) || reported === null) return new Response(null, { status: 400 }); + report.reported_id = reported?.id || null; if (data.notice != null) report.notice = data.notice; if (data.statement != null) report.statement = data.statement; if (data.status != null) report.status = data.status; @@ -115,7 +120,7 @@ export const PATCH = (async ({ request, cookies }) => { return new Response(); }) satisfies RequestHandler; -export const PUT = (async ({ request, cookies, url }) => { +export const PUT = (async ({ request, cookies }) => { if (getSession(cookies, { permissions: [Permissions.ReportWrite] }) == null) { return new Response(null, { status: 401 @@ -124,23 +129,18 @@ export const PUT = (async ({ request, cookies, url }) => { const data: { reporter: string; - reported: string; + reported: string | null; reason: string; body: string | null; } = await request.json(); - if ( - data.reporter == null || - data.reported == null || - data.reason == null || - data.body === undefined - ) + if (data.reporter == null || data.reason == null || data.body === undefined) return new Response(null, { status: 400 }); const reporter = await User.findOne({ where: { uuid: data.reporter } }); - const reported = await User.findOne({ where: { uuid: data.reported } }); + const reported = data.reported ? await User.findOne({ where: { uuid: data.reported } }) : null; - if (reporter == null || reported == null) return new Response(null, { status: 400 }); + if (reporter == null) return new Response(null, { status: 400 }); const report = await Report.create({ subject: data.reason, @@ -150,10 +150,12 @@ export const PUT = (async ({ request, cookies, url }) => { url_hash: crypto.randomBytes(18).toString('hex'), completed: false, reporter_id: reporter.id, - reported_id: reported.id + reported_id: reported?.id || null }); report.dataValues.reporter = await User.findOne({ where: { id: report.reporter_id } }); - report.dataValues.reported = await User.findOne({ where: { id: report.reported_id } }); + report.dataValues.reported = report.reported_id + ? await User.findOne({ where: { id: report.reported_id } }) + : null; report.dataValues.auditor = null; return new Response(JSON.stringify(report), { diff --git a/src/routes/admin/reports/NewReportModal.svelte b/src/routes/admin/reports/NewReportModal.svelte index 8957c09..fbb4ab0 100644 --- a/src/routes/admin/reports/NewReportModal.svelte +++ b/src/routes/admin/reports/NewReportModal.svelte @@ -4,6 +4,7 @@ import Textarea from '$lib/components/Input/Textarea.svelte'; import Search from '$lib/components/Input/Search.svelte'; import { createEventDispatcher } from 'svelte'; + import { usernameSuggestions } from '$lib/utils'; const dispatch = createEventDispatcher(); @@ -12,27 +13,12 @@ let reason = ''; let body = ''; - async function usernameSuggestions(username: string): Promise<{ name: string; value: string }[]> { - const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/users`, { - method: 'POST', - body: JSON.stringify({ - limit: 6, - search: username, - slim: true - }) - }); - const json: { username: string; uuid: string }[] = await response.json(); - return json.map((v) => { - return { name: v.username, value: v.uuid }; - }); - } - async function newReport() { const response = await fetch(`${env.PUBLIC_BASE_PATH}/admin/reports`, { method: 'PUT', body: JSON.stringify({ reporter: reporter, - reported: reported, + reported: reported || null, reason: reason, body: body || null }) @@ -66,10 +52,10 @@ <Search size="sm" suggestionRequired={true} + emptyAllowed={true} searchSuggestionFunc={usernameSuggestions} invalidMessage="Es können nur registrierte Spieler reportet werden" label="Reporteter User" - required={true} bind:value={reported} /> </div> diff --git a/src/routes/report/+server.ts b/src/routes/report/+server.ts index f222841..3981dba 100644 --- a/src/routes/report/+server.ts +++ b/src/routes/report/+server.ts @@ -8,15 +8,14 @@ export const POST = (async ({ request, url }) => { if (env.REPORT_SECRET && url.searchParams.get('secret') !== env.REPORT_SECRET) return new Response(null, { status: 401 }); - const data: { reporter: string; reported: string; reason: string } = await request.json(); + const data: { reporter: string; reported: string | null; reason: string } = await request.json(); - if (data.reporter == null || data.reported == null || data.reason == null) - return new Response(null, { status: 400 }); + if (data.reporter == null || data.reason == null) return new Response(null, { status: 400 }); const reporter = await User.findOne({ where: { uuid: data.reporter } }); - const reported = await User.findOne({ where: { uuid: data.reported } }); + const reported = data.reported ? await User.findOne({ where: { uuid: data.reported } }) : null; - if (reporter == null || reported == null) return new Response(null, { status: 400 }); + if (reporter == null) return new Response(null, { status: 400 }); const report = await Report.create({ subject: data.reason, @@ -26,7 +25,7 @@ export const POST = (async ({ request, url }) => { url_hash: crypto.randomBytes(18).toString('hex'), completed: false, reporter_id: reporter.id, - reported_id: reported.id + reported_id: reported?.id || null }); return new Response( diff --git a/src/routes/report/[...url_hash]/+page.server.ts b/src/routes/report/[...url_hash]/+page.server.ts index 139199f..b7ed125 100644 --- a/src/routes/report/[...url_hash]/+page.server.ts +++ b/src/routes/report/[...url_hash]/+page.server.ts @@ -22,7 +22,7 @@ export const load: PageServerLoad = async ({ params }) => { name: report.reporter.username }, reported: { - name: report.reported.username + name: report.reported?.username || null } }; }; diff --git a/src/routes/report/[...url_hash]/+page.svelte b/src/routes/report/[...url_hash]/+page.svelte index 6938b40..20f4915 100644 --- a/src/routes/report/[...url_hash]/+page.svelte +++ b/src/routes/report/[...url_hash]/+page.svelte @@ -18,6 +18,7 @@ <div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}> <ReportDraft reason={data.reason} + reporterName={data.reporter.name} reportedName={data.reported.name} on:submit={() => (data.draft = false)} /> diff --git a/src/routes/report/[...url_hash]/ReportDraft.svelte b/src/routes/report/[...url_hash]/ReportDraft.svelte index ed7bb63..77257d7 100644 --- a/src/routes/report/[...url_hash]/ReportDraft.svelte +++ b/src/routes/report/[...url_hash]/ReportDraft.svelte @@ -5,7 +5,8 @@ import { page } from '$app/stores'; import { createEventDispatcher } from 'svelte'; - export let reportedName: string; + export let reporterName: string; + export let reportedName: string | null; export let reason: string; let body: string; @@ -25,7 +26,10 @@ </script> <div> - <h2 class="text-3xl text-center">Report für <code>{reportedName}</code></h2> + <h2 class="text-3xl text-center"> + Report von <span class="underline">{reporterName}</span> gegen + <span class="underline">{reportedName || 'unbekannt'}</span> + </h2> <form on:submit|preventDefault={() => submitModal.show()}> <div class="space-y-4 my-4"> <div>