User reports (#3232)

This commit is contained in:
Kalle
2026-07-18 12:46:07 +03:00
committed by GitHub
parent 7d1e680a74
commit ef0d32ef18
65 changed files with 879 additions and 11 deletions

View File

@@ -0,0 +1,53 @@
import { useTranslation } from "react-i18next";
import { SendouDialog } from "~/components/elements/Dialog";
import { toastQueue } from "~/components/elements/Toast";
import { FormMessage } from "~/components/FormMessage";
import { SendouForm } from "~/form";
import { userReportPage } from "~/utils/urls";
import { reportUserSchema } from "../user-report-schemas";
/**
* Modal for reporting a user to the staff, posting to the `/user-report/:id` resource
* route. Re-reporting the same user overwrites the previous report. Rendered wherever
* a `UserCard` lives.
*/
export function ReportUserDialog({
userId,
username,
onClose,
}: {
userId: number;
username: string;
onClose: () => void;
}) {
const { t } = useTranslation(["user"]);
return (
<SendouDialog
heading={t("user:card.report.header", { name: username })}
onClose={onClose}
>
<SendouForm
schema={reportUserSchema}
action={userReportPage(userId)}
onSuccess={() => {
toastQueue.add(
{ message: "Report sent to the staff", variant: "success" },
{ timeout: 5000 },
);
onClose();
}}
>
{({ FormField }) => (
<>
<FormField name="category" />
<FormField name="description" />
<FormMessage type="info">
{t("user:card.report.falseReportsWarning")}
</FormMessage>
</>
)}
</SendouForm>
</SendouDialog>
);
}

View File

@@ -0,0 +1,6 @@
.container {
height: 200px;
background-color: var(--color-bg-high);
border-radius: var(--radius-box);
padding: var(--s-2);
}

View File

@@ -0,0 +1,104 @@
import {
BarElement,
CategoryScale,
Chart as ChartJS,
LinearScale,
Tooltip,
} from "chart.js";
import { format } from "date-fns";
import * as React from "react";
import { Bar } from "react-chartjs-2";
import { useHydrated } from "~/hooks/useHydrated";
import styles from "./ReportsBarChart.module.css";
ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip);
/**
* Bar graph of reports made against a user, one bar per calendar month.
* Theme colors are read from CSS variables like `app/components/Chart.tsx` does.
*/
export function ReportsBarChart({
monthlyCounts,
}: {
monthlyCounts: Array<{
/** Start of the calendar month as a JavaScript timestamp */
month: number;
count: number;
}>;
}) {
const isHydrated = useHydrated();
const [colors, setColors] = React.useState({
bar: "",
border: "",
borderHigh: "",
text: "",
});
React.useEffect(() => {
const resolve = () => {
const get = (v: string) =>
getComputedStyle(document.documentElement).getPropertyValue(v).trim();
setColors({
bar: get("--color-text-accent"),
border: get("--color-border"),
borderHigh: get("--color-border-high"),
text: get("--color-text-high"),
});
};
resolve();
const root = document.documentElement;
const observer = new MutationObserver(resolve);
observer.observe(root, { attributes: true, attributeFilter: ["class"] });
return () => observer.disconnect();
}, []);
if (!isHydrated) {
return <div className={styles.container} />;
}
const scaleDefaults = {
grid: { color: colors.border },
border: { color: colors.borderHigh },
ticks: { color: colors.text },
};
return (
<div className={styles.container}>
<Bar
data={{
labels: monthlyCounts.map(({ month }) =>
format(new Date(month), "MMM yy"),
),
datasets: [
{
data: monthlyCounts.map(({ count }) => count),
backgroundColor: colors.bar,
},
],
}}
options={{
animation: false,
maintainAspectRatio: false,
scales: {
x: {
...scaleDefaults,
grid: { display: false },
ticks: { ...scaleDefaults.ticks, maxRotation: 0 },
},
y: {
...scaleDefaults,
beginAtZero: true,
ticks: { ...scaleDefaults.ticks, precision: 0 },
},
},
plugins: {
legend: { display: false },
},
}}
/>
</div>
);
}