Added entity reporting.
This commit is contained in:
@@ -34,6 +34,13 @@ import Admin from '#/components/admin/admin.vue';
|
||||
class="link"
|
||||
>Entity Health</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a
|
||||
href="/admin/reports"
|
||||
class="link"
|
||||
>Reports</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Admin>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
<script setup>
|
||||
import { format } from 'date-fns';
|
||||
import { inject, ref } from 'vue';
|
||||
|
||||
import Admin from '#/components/admin/admin.vue';
|
||||
import Checkbox from '#/components/form/checkbox.vue';
|
||||
|
||||
import { patch, post } from '#/src/api.js';
|
||||
import reportReasons from '#/utils/report-reasons.js';
|
||||
|
||||
const { pageProps, user } = inject('pageContext');
|
||||
const { completed } = pageProps;
|
||||
|
||||
const reports = ref(pageProps.reports);
|
||||
|
||||
async function updateReport(report, update) {
|
||||
const updatedReport = await patch(`/reports/${report.id}`, update, {
|
||||
errorFeedback: 'Failed to update report',
|
||||
appendErrorMessage: true,
|
||||
}).catch(() => null); // feedback is shown by the api module
|
||||
|
||||
if (updatedReport) {
|
||||
reports.value = reports.value.map((listedReport) => (listedReport.id === report.id
|
||||
? {
|
||||
...listedReport,
|
||||
isRead: updatedReport.isRead,
|
||||
isCompleted: updatedReport.isCompleted,
|
||||
}
|
||||
: listedReport));
|
||||
}
|
||||
}
|
||||
|
||||
async function banReporter(report) {
|
||||
if (!confirm(`Ban ${report.user.username} from submitting reports?`)) { // eslint-disable-line no-alert
|
||||
return;
|
||||
}
|
||||
|
||||
await post('/bans', {
|
||||
userId: report.user.id,
|
||||
banIp: true,
|
||||
scope: 'reports',
|
||||
}, {
|
||||
successFeedback: `Banned ${report.user.username} from submitting reports`,
|
||||
errorFeedback: `Failed to ban ${report.user.username}`,
|
||||
appendErrorMessage: true,
|
||||
}).catch(() => {}); // feedback is shown by the api module
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Admin class="page">
|
||||
<div class="header">
|
||||
<h2 class="heading">{{ completed ? 'Completed reports' : 'Open reports' }}</h2>
|
||||
|
||||
<a
|
||||
:href="completed ? '/admin/reports' : '/admin/reports?completed=true'"
|
||||
class="link toggle-completed"
|
||||
>{{ completed ? 'Show open reports' : 'Show completed reports' }}</a>
|
||||
</div>
|
||||
|
||||
<p
|
||||
v-if="reports.length === 0"
|
||||
class="empty"
|
||||
>No {{ completed ? 'completed' : 'open' }} reports.</p>
|
||||
|
||||
<table
|
||||
v-else
|
||||
class="table"
|
||||
>
|
||||
<thead class="table-header">
|
||||
<tr>
|
||||
<th class="table-heading">Site</th>
|
||||
<th class="table-heading">Reasons</th>
|
||||
<th class="table-heading">Comment</th>
|
||||
<th class="table-heading">Reporter</th>
|
||||
<th class="table-heading">Date</th>
|
||||
<th class="table-heading table-check">Read</th>
|
||||
<th class="table-heading table-check">Done</th>
|
||||
<th
|
||||
v-if="user.role === 'admin'"
|
||||
class="table-heading"
|
||||
>Ban</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="report in reports"
|
||||
:key="`report-${report.id}`"
|
||||
class="report"
|
||||
:class="{ unread: !report.isRead, completed: report.isCompleted !== completed }"
|
||||
>
|
||||
<td
|
||||
:title="report.entity?.id"
|
||||
class="table-cell table-name ellipsis"
|
||||
>
|
||||
<a
|
||||
v-if="report.entity"
|
||||
:href="`/${report.entity.type}/${report.entity.slug}`"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>{{ report.entity.name }}</a>
|
||||
</td>
|
||||
|
||||
<td class="table-cell table-reasons">
|
||||
<span
|
||||
v-for="reason in report.reasons"
|
||||
:key="`report-${report.id}-${reason}`"
|
||||
class="reason"
|
||||
:class="{ urgent: reason === 'inappropriate' }"
|
||||
>{{ reportReasons[reason] || reason }}</span>
|
||||
</td>
|
||||
|
||||
<td class="table-cell table-comment">{{ report.comment }}</td>
|
||||
|
||||
<td class="table-cell table-username ellipsis">
|
||||
<a
|
||||
v-if="report.user"
|
||||
:href="`/user/${report.user.username}`"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>{{ report.user.username }}</a>
|
||||
|
||||
<span
|
||||
v-else
|
||||
class="deleted"
|
||||
>deleted user</span>
|
||||
</td>
|
||||
|
||||
<td class="table-cell table-date">{{ format(report.createdAt, 'yyyy-MM-dd HH:mm') }}</td>
|
||||
|
||||
<td class="table-cell table-check">
|
||||
<Checkbox
|
||||
:checked="report.isRead"
|
||||
@change="(checked) => updateReport(report, { isRead: checked })"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td class="table-cell table-check">
|
||||
<Checkbox
|
||||
:checked="report.isCompleted"
|
||||
@change="(checked) => updateReport(report, { isCompleted: checked })"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td
|
||||
v-if="user.role === 'admin'"
|
||||
class="table-cell table-actions"
|
||||
>
|
||||
<Icon
|
||||
v-if="report.user"
|
||||
v-tooltip="`Ban ${report.user.username} from submitting reports`"
|
||||
icon="blocked"
|
||||
class="ban"
|
||||
@click="banReporter(report)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</Admin>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--glass);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.table {
|
||||
border-collapse: collapse;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.table-heading {
|
||||
text-align: left;
|
||||
padding: 0 .5rem .25rem .5rem;
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
padding: .25rem .5rem .25rem .5rem;
|
||||
border-collapse: collapse;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.table-name {
|
||||
max-width: 12rem;
|
||||
}
|
||||
|
||||
.table-comment {
|
||||
min-width: 15rem;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.table-date {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-check {
|
||||
width: 3rem;
|
||||
text-align: center;
|
||||
|
||||
:deep(.check-container) {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.report {
|
||||
&:nth-child(2) {
|
||||
background: var(--glass-weak-50);
|
||||
}
|
||||
|
||||
&.unread .table-name .link {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* marked completed (or reopened) on this page, it moves to the other list on reload */
|
||||
&.completed {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
|
||||
.reason {
|
||||
display: inline-block;
|
||||
padding: .25rem .5rem;
|
||||
margin: 0 .25rem .25rem 0;
|
||||
border-radius: .25rem;
|
||||
background: var(--background-dark-20);
|
||||
font-size: .9rem;
|
||||
white-space: nowrap;
|
||||
|
||||
&.urgent {
|
||||
background: var(--warn);
|
||||
color: var(--text-light);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.toggle-completed {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.deleted {
|
||||
color: var(--glass);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.ban {
|
||||
fill: var(--glass);
|
||||
|
||||
&:hover {
|
||||
fill: var(--warn);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
import { render } from 'vike/abort';
|
||||
|
||||
import { canReviewReports, fetchReports } from '#/src/reports.js';
|
||||
|
||||
export async function onBeforeRender(pageContext) {
|
||||
if (!canReviewReports(pageContext.user)) {
|
||||
throw render(404);
|
||||
}
|
||||
|
||||
const completed = pageContext.urlParsed.search.completed === 'true';
|
||||
const reports = await fetchReports({ completed }, pageContext.user);
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
title: 'Reports',
|
||||
pageProps: {
|
||||
reports,
|
||||
completed,
|
||||
},
|
||||
routeParams: {
|
||||
section: 'reports',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, inject, ref } from 'vue';
|
||||
|
||||
import Domains from '#/components/domains/domains.vue';
|
||||
import ReportDialog from '#/components/entities/report.vue';
|
||||
import EntityTile from '#/components/entities/tile.vue';
|
||||
import Movies from '#/components/movies/movies.vue';
|
||||
import Scenes from '#/components/scenes/scenes.vue';
|
||||
@@ -10,6 +11,9 @@ import Heart from '#/components/stashes/heart.vue';
|
||||
const { pageProps, routeParams, user } = inject('pageContext');
|
||||
const { entity } = pageProps;
|
||||
|
||||
const isReported = ref(pageProps.isReported);
|
||||
const showReportDialog = ref(false);
|
||||
|
||||
const children = ref(null);
|
||||
const expanded = ref(false);
|
||||
|
||||
@@ -77,8 +81,32 @@ const entityUrl = entity.affiliateUrl || entity.url || null;
|
||||
:item="entity"
|
||||
class="light"
|
||||
/>
|
||||
|
||||
<template v-if="user">
|
||||
<Icon
|
||||
v-if="isReported"
|
||||
v-tooltip="`Your report for this ${entity.type} is awaiting review`"
|
||||
icon="warning2"
|
||||
class="report reported"
|
||||
/>
|
||||
|
||||
<Icon
|
||||
v-else
|
||||
v-tooltip="`Report a problem with '${entity.name}'`"
|
||||
icon="warning2"
|
||||
class="report"
|
||||
@click="showReportDialog = true"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<ReportDialog
|
||||
v-if="showReportDialog"
|
||||
:entity="entity"
|
||||
@reported="showReportDialog = false; isReported = true;"
|
||||
@close="showReportDialog = false"
|
||||
/>
|
||||
|
||||
<Domains
|
||||
:domain="domain"
|
||||
:path="`/${entity.type}/${entity.slug}`"
|
||||
@@ -319,6 +347,24 @@ const entityUrl = entity.affiliateUrl || entity.url || null;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.report {
|
||||
width: 1.1rem;
|
||||
height: 100%;
|
||||
padding: 0 1rem;
|
||||
margin: -0.25rem 0 0 .75rem;
|
||||
fill: var(--highlight-weak-10);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
fill: var(--warn);
|
||||
}
|
||||
|
||||
&.reported {
|
||||
fill: var(--primary);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.plainurl {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getCampaignIndex, getRandomCampaigns } from '#/src/campaigns.js';
|
||||
import entityPrefixes from '#/src/entities-prefixes.js';
|
||||
import { fetchEntitiesById } from '#/src/entities.js';
|
||||
import { fetchMovies } from '#/src/movies.js';
|
||||
import { hasOpenReport } from '#/src/reports.js';
|
||||
import { fetchScenes } from '#/src/scenes.js';
|
||||
import { curateMoviesQuery } from '#/src/web/movies.js';
|
||||
import { curateScenesQuery } from '#/src/web/scenes.js';
|
||||
@@ -50,11 +51,13 @@ export async function onBeforeRender(pageContext) {
|
||||
const [
|
||||
[entity],
|
||||
entityReleases,
|
||||
isReported,
|
||||
] = await Promise.all([
|
||||
fetchEntitiesById([Number(entityId)], { includeChildren: true }, pageContext.user, {
|
||||
restriction: pageContext.geo.restriction,
|
||||
}),
|
||||
fetchReleases(pageContext, entityId),
|
||||
hasOpenReport(Number(entityId), pageContext.user),
|
||||
]);
|
||||
|
||||
const entityIds = entity.isIndependent || !entity.parent
|
||||
@@ -92,6 +95,7 @@ export async function onBeforeRender(pageContext) {
|
||||
title: entity.name,
|
||||
pageProps: {
|
||||
entity,
|
||||
isReported,
|
||||
...entityReleases,
|
||||
},
|
||||
campaigns: {
|
||||
|
||||
Reference in New Issue
Block a user