mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 23:27:55 -05:00
User reports (#3232)
This commit is contained in:
53
app/features/user-report/components/ReportUserDialog.tsx
Normal file
53
app/features/user-report/components/ReportUserDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
.container {
|
||||
height: 200px;
|
||||
background-color: var(--color-bg-high);
|
||||
border-radius: var(--radius-box);
|
||||
padding: var(--s-2);
|
||||
}
|
||||
104
app/features/user-report/components/ReportsBarChart.tsx
Normal file
104
app/features/user-report/components/ReportsBarChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user