feat: report page, take action on reports
This commit is contained in:
parent
a0ba712632
commit
cacd3a30b7
14 changed files with 502 additions and 14 deletions
|
@ -18,7 +18,7 @@ export const load = async ({ parent, fetch, cookies }) => {
|
|||
|
||||
const reports = await apiRequest<Report[]>("GET", "/moderation/reports", { fetch, cookies });
|
||||
const staleReportCount = reports.filter(
|
||||
(r) => idTimestamp(r.id).diffNow(["days"]).days >= 7,
|
||||
(r) => idTimestamp(r.id).diffNow(["days"]).days <= -7,
|
||||
).length;
|
||||
|
||||
return {
|
||||
|
|
23
Foxnouns.Frontend/src/routes/admin/reports/+page.server.ts
Normal file
23
Foxnouns.Frontend/src/routes/admin/reports/+page.server.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { apiRequest } from "$api";
|
||||
import type { Report } from "$api/models/moderation";
|
||||
|
||||
export const load = async ({ url, fetch, cookies }) => {
|
||||
const before = url.searchParams.get("before");
|
||||
const after = url.searchParams.get("after");
|
||||
const byReporter = url.searchParams.get("by-reporter");
|
||||
const byTarget = url.searchParams.get("by-target");
|
||||
const includeClosed = url.searchParams.get("include-closed") === "true";
|
||||
|
||||
const params = new URLSearchParams();
|
||||
if (before) params.set("before", before);
|
||||
if (after) params.set("after", after);
|
||||
if (byReporter) params.set("by-reporter", byReporter);
|
||||
if (byTarget) params.set("by-target", byTarget);
|
||||
if (includeClosed) params.set("include-closed", "true");
|
||||
|
||||
const reports = await apiRequest<Report[]>("GET", `/moderation/reports?${params.toString()}`, {
|
||||
fetch,
|
||||
cookies,
|
||||
});
|
||||
return { reports, url: url.toString(), byReporter, byTarget, before, after };
|
||||
};
|
125
Foxnouns.Frontend/src/routes/admin/reports/+page.svelte
Normal file
125
Foxnouns.Frontend/src/routes/admin/reports/+page.svelte
Normal file
|
@ -0,0 +1,125 @@
|
|||
<script lang="ts">
|
||||
import Link45deg from "svelte-bootstrap-icons/lib/Link45deg.svelte";
|
||||
import Funnel from "svelte-bootstrap-icons/lib/Funnel.svelte";
|
||||
import FunnelFill from "svelte-bootstrap-icons/lib/FunnelFill.svelte";
|
||||
import type { PageData } from "./$types";
|
||||
import { t } from "$lib/i18n";
|
||||
import { DateTime } from "luxon";
|
||||
import { idTimestamp } from "$lib";
|
||||
|
||||
type Props = { data: PageData };
|
||||
let { data }: Props = $props();
|
||||
|
||||
const addReporter = (id: string | null) => {
|
||||
const url = new URL(data.url);
|
||||
if (id) url.searchParams.set("by-reporter", id);
|
||||
else url.searchParams.delete("by-reporter");
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
const addTarget = (id: string | null) => {
|
||||
const url = new URL(data.url);
|
||||
if (id) url.searchParams.set("by-target", id);
|
||||
else url.searchParams.delete("by-target");
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
const addBefore = (id: string) => {
|
||||
const url = new URL(data.url);
|
||||
url.searchParams.delete("after");
|
||||
url.searchParams.set("before", id);
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
const addAfter = (id: string) => {
|
||||
const url = new URL(data.url);
|
||||
url.searchParams.delete("before");
|
||||
url.searchParams.set("after", id);
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Reports • pronouns.cc</title>
|
||||
</svelte:head>
|
||||
|
||||
<h2>Reports</h2>
|
||||
|
||||
<ul>
|
||||
{#if data.byTarget}
|
||||
<li>Filtering by target (<a href={addTarget(null)}>clear</a>)</li>
|
||||
{/if}
|
||||
{#if data.byReporter}
|
||||
<li>Filtering by reporter (<a href={addReporter(null)}>clear</a>)</li>
|
||||
{/if}
|
||||
</ul>
|
||||
|
||||
{#if data.before}
|
||||
<a href={addAfter(data.before)}>Show newer reports</a>
|
||||
{/if}
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col">User</th>
|
||||
<th scope="col">Member</th>
|
||||
<th scope="col">Reporter</th>
|
||||
<th scope="col">Reason</th>
|
||||
<th scope="col">Context?</th>
|
||||
<th scope="col">Created at</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.reports as report (report.id)}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="/admin/reports/{report.id}">
|
||||
<Link45deg />
|
||||
<span class="visually-hidden">Open report</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/@{report.target_user.username}">@{report.target_user.username}</a>
|
||||
(<a href={addTarget(report.target_user.id)}>
|
||||
{#if data.byTarget === report.target_user.id}<FunnelFill />{:else}<Funnel />{/if}
|
||||
</a>)
|
||||
</td>
|
||||
<td>
|
||||
{#if report.target_member}
|
||||
<a href="@/{report.target_user.username}/{report.target_member.name}">
|
||||
{report.target_member.name}
|
||||
</a>
|
||||
{:else}
|
||||
<em>(none)</em>
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/@{report.reporter.username}">{report.reporter.username}</a>
|
||||
(<a href={addReporter(report.reporter.id)}>
|
||||
{#if data.byReporter === report.reporter.id}<FunnelFill />{:else}<Funnel />{/if}
|
||||
</a>)
|
||||
</td>
|
||||
<td><code>{report.reason}</code></td>
|
||||
<td>
|
||||
{#if report.context}
|
||||
{$t("yes")}
|
||||
{:else}
|
||||
{$t("no")}
|
||||
{/if}
|
||||
</td>
|
||||
<td>
|
||||
{idTimestamp(report.id).toLocaleString(DateTime.DATETIME_SHORT)}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{#if data.reports.length === 100}
|
||||
<a href={addBefore(data.reports[data.reports.length - 1].id)}>Show older reports</a>
|
||||
{/if}
|
|
@ -0,0 +1,13 @@
|
|||
import { apiRequest } from "$api";
|
||||
import type { ReportDetails } from "$api/models/moderation";
|
||||
import { createModactions } from "$lib/actions/modaction";
|
||||
|
||||
export const load = async ({ params, fetch, cookies }) => {
|
||||
const resp = await apiRequest<ReportDetails>("GET", `/moderation/reports/${params.id}`, {
|
||||
fetch,
|
||||
cookies,
|
||||
});
|
||||
return { report: resp.report, user: resp.user, member: resp.member };
|
||||
};
|
||||
|
||||
export const actions = createModactions();
|
78
Foxnouns.Frontend/src/routes/admin/reports/[id]/+page.svelte
Normal file
78
Foxnouns.Frontend/src/routes/admin/reports/[id]/+page.svelte
Normal file
|
@ -0,0 +1,78 @@
|
|||
<script lang="ts">
|
||||
import type { Member } from "$api/models/member";
|
||||
import type { User } from "$api/models/user";
|
||||
import ActionForm from "$components/admin/ActionForm.svelte";
|
||||
import PartialProfileCard from "$components/admin/PartialProfileCard.svelte";
|
||||
import ProfileHeader from "$components/profile/ProfileHeader.svelte";
|
||||
import MemberCard from "$components/profile/user/MemberCard.svelte";
|
||||
import { renderMarkdown } from "$lib/markdown";
|
||||
import type { ActionData, PageData } from "./$types";
|
||||
|
||||
type Props = { data: PageData; form: ActionData };
|
||||
let { data, form }: Props = $props();
|
||||
let { report, user, member } = $derived(data);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Report on @{user.username} • pronouns.cc</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<h3>Target user</h3>
|
||||
<PartialProfileCard user={report.target_user} />
|
||||
</div>
|
||||
{#if report.target_member}
|
||||
<div class="col-md">
|
||||
<h3>Target member</h3>
|
||||
<MemberCard
|
||||
username={report.target_user.username}
|
||||
member={report.target_member}
|
||||
allPreferences={{}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="col-md">
|
||||
<h3>Reporter</h3>
|
||||
<PartialProfileCard user={report.reporter} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<h3>Reason</h3>
|
||||
<p><code>{report.reason}</code></p>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
<h3>Context</h3>
|
||||
<p>
|
||||
{#if report.context}
|
||||
{@html renderMarkdown(report.context)}
|
||||
{:else}
|
||||
<em>(no context given)</em>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<h3>Take action</h3>
|
||||
<ActionForm
|
||||
userId={report.target_user.id}
|
||||
reportId={report.id}
|
||||
memberId={report.target_member?.id}
|
||||
{form}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if report.snapshot}
|
||||
<h3>Profile at time of report</h3>
|
||||
<hr />
|
||||
{#if report.target_type === "USER"}
|
||||
{@const snapshot = report.snapshot as User}
|
||||
<ProfileHeader profile={snapshot} name="@{snapshot.username}" />
|
||||
{:else}
|
||||
{@const snapshot = report.snapshot as Member}
|
||||
<ProfileHeader profile={snapshot} name="{snapshot.name} (@{snapshot.user.username})" />
|
||||
{/if}
|
||||
{/if}
|
Loading…
Add table
Add a link
Reference in a new issue