Skip to content
This repository was archived by the owner on Feb 21, 2025. It is now read-only.

Commit f414006

Browse files
committed
- Improve ordering for user join chart
- User management dashboard
1 parent 7550b58 commit f414006

File tree

8 files changed

+305
-6
lines changed

8 files changed

+305
-6
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import React from "react";
2+
import { PageContainer } from "@/components/PageContainer/PageContainer";
3+
import UsersManagementTable from "@/components/profile/UsersManagementTable";
4+
5+
const Page = () => {
6+
return (
7+
<PageContainer title={"Users"}>
8+
<UsersManagementTable />
9+
</PageContainer>
10+
);
11+
};
12+
13+
export default Page;

src/components/Navbar/Navbar.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import {
55
IconLock,
66
IconMoodSmile,
77
IconReport,
8+
IconUsers,
89
} from "@tabler/icons-react";
910
import type { NavItem } from "@/types/nav-item";
1011

1112
export const navLinks: NavItem[] = [
1213
{ label: "Dashboard", icon: IconDashboard, link: "/dashboard" },
14+
{ label: "Users", icon: IconUsers, link: "/dashboard/user" },
1315
{ label: "Reports", icon: IconReport, link: "/dashboard/report" },
1416
{
1517
label: "Achievements",

src/components/auth/SuperTokensProvider.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ export const frontendConfig = (): SuperTokensConfig => {
5959

6060
if (typeof window !== "undefined") {
6161
// we only want to call this init function on the frontend, so we check typeof window !== 'undefined'
62-
console.log("Supertokens init with routerInfo: ", routerInfo);
6362
SuperTokensReact.init(frontendConfig());
6463
}
6564

src/components/charts/UserJoinPeriodChart.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,16 @@ interface JoinPeriodItem {
1313
const profileToPeriodItems = (profiles: Profile[]): JoinPeriodItem[] => {
1414
const periodMap = new Map<string, number>();
1515

16-
for (const profile of profiles) {
16+
// Sort by createdAt ASC
17+
// (meaning newer users will be at the end of the chart)
18+
const sortedProfiles = profiles.toSorted((a, b) => {
19+
const createDateA = new Date(a.createdAt);
20+
const createDateB = new Date(b.createdAt);
21+
22+
return createDateA.getTime() - createDateB.getTime();
23+
});
24+
25+
for (const profile of sortedProfiles) {
1726
const createdAtDate = new Date(profile.createdAt);
1827
const createdAtMonth = `${createdAtDate.getMonth() + 1}`.padStart(
1928
2,
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"use client";
2+
3+
import React, { useState } from "react";
4+
import { useUserProfiles } from "@/components/profile/hooks/useUserProfiles";
5+
import { MantineReactTable, MRT_ColumnDef } from "mantine-react-table";
6+
import {
7+
CreateReportRequestDto,
8+
FindAllProfileResponseItemDto,
9+
} from "@/wrapper/server";
10+
import CenteredLoading from "@/components/general/CenteredLoading";
11+
import CenteredErrorMessage from "@/components/general/CenteredErrorMessage";
12+
import { Badge, MantineColor, Menu, Modal, Paper } from "@mantine/core";
13+
import { useCustomTable } from "@/components/table/hooks/use-custom-table";
14+
import { UserAvatarGroup } from "@/components/general/avatar/UserAvatarGroup";
15+
import { useDisclosure } from "@mantine/hooks";
16+
import ReportCreateForm from "@/components/report/form/ReportCreateForm";
17+
import sourceType = CreateReportRequestDto.sourceType;
18+
19+
const columns: MRT_ColumnDef<FindAllProfileResponseItemDto>[] = [
20+
{
21+
accessorKey: "profile.username",
22+
header: "Username",
23+
Cell: ({ row }) => {
24+
return <UserAvatarGroup userId={row.original.profile.userId} />;
25+
},
26+
},
27+
{
28+
accessorFn: (row) => {
29+
if (row.isSuspended) {
30+
return "SUSPENDED";
31+
} else if (row.isBanned) {
32+
return "BANNED";
33+
}
34+
return "NORMAL";
35+
},
36+
header: "Status",
37+
filterVariant: "select",
38+
mantineFilterSelectProps: {
39+
data: [
40+
{ label: "Normal", value: "NORMAL" },
41+
{ label: "Suspended", value: "SUSPENDED" },
42+
{ label: "Banned", value: "BANNED" },
43+
],
44+
},
45+
Cell: ({ row, renderedCellValue }) => {
46+
const item = row.original;
47+
const color: MantineColor =
48+
item.isSuspended || item.isBanned ? "red" : "green";
49+
return <Badge color={color}>{renderedCellValue}</Badge>;
50+
},
51+
},
52+
{
53+
header: "Joined at",
54+
accessorFn: (row) =>
55+
new Date(row.profile.createdAt).toLocaleString("en-US"),
56+
sortingFn: (rowA, rowB, columnId) => {
57+
const createDateA = new Date(rowA.original.profile.createdAt);
58+
const createDateB = new Date(rowB.original.profile.createdAt);
59+
60+
return createDateA.getTime() - createDateB.getTime();
61+
},
62+
id: "createdAt",
63+
},
64+
];
65+
66+
const UsersManagementTable = () => {
67+
const { data, isLoading, isError, isFetching } = useUserProfiles();
68+
69+
const [reportModalOpened, reportModalUtils] = useDisclosure();
70+
71+
const [reportedUserId, setReportedUserId] = useState<string | undefined>(
72+
undefined,
73+
);
74+
75+
const table = useCustomTable<FindAllProfileResponseItemDto>({
76+
columns,
77+
data: data ?? [],
78+
rowCount: data?.length ?? 0,
79+
state: {
80+
isLoading: isLoading,
81+
showAlertBanner: isError,
82+
showProgressBars: isFetching,
83+
},
84+
enableRowActions: true,
85+
renderRowActionMenuItems: (item) => {
86+
const profile = item.row.original.profile;
87+
return (
88+
<>
89+
<Menu.Item
90+
onClick={() => {
91+
setReportedUserId(profile.userId);
92+
reportModalUtils.open();
93+
}}
94+
>
95+
Generate report
96+
</Menu.Item>
97+
</>
98+
);
99+
},
100+
});
101+
102+
if (isLoading) {
103+
return <CenteredLoading message="Loading..." />;
104+
} else if (isError) {
105+
return (
106+
<CenteredErrorMessage
107+
message={"Failed to load users. Please try again."}
108+
/>
109+
);
110+
} else if (data == undefined) {
111+
return null;
112+
}
113+
114+
return (
115+
<Paper withBorder radius="md" p="md" mt="lg">
116+
<Modal
117+
title={"Generate report"}
118+
opened={reportModalOpened}
119+
onClose={reportModalUtils.close}
120+
>
121+
{reportedUserId && (
122+
<ReportCreateForm
123+
sourceId={reportedUserId}
124+
sourceType={sourceType.PROFILE}
125+
onSuccess={reportModalUtils.close}
126+
/>
127+
)}
128+
</Modal>
129+
<MantineReactTable table={table} />
130+
</Paper>
131+
);
132+
};
133+
134+
export default UsersManagementTable;

src/components/report/ReportsTable.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
reportCategoryToString,
88
reportSourceTypeToString,
99
} from "@/components/report/util/reportCategoryToString";
10-
import { useCustomTable } from "@/hooks/use-custom-table";
10+
import { useCustomTable } from "@/components/table/hooks/use-custom-table";
1111
import { useReports } from "@/components/report/hooks/useReports";
1212
import { Button, Paper, Title, Text, Badge } from "@mantine/core";
1313
import { PaginationState } from "@tanstack/table-core";
@@ -69,8 +69,14 @@ const columns: MRT_ColumnDef<Report>[] = [
6969
},
7070
{
7171
header: "Created At",
72-
accessorKey: "createdAt",
7372
accessorFn: (row) => new Date(row.createdAt).toLocaleString("en-US"),
73+
sortingFn: (rowA, rowB, columnId) => {
74+
const createDateA = new Date(rowA.original.createdAt);
75+
const createDateB = new Date(rowB.original.createdAt);
76+
77+
return createDateA.getTime() - createDateB.getTime();
78+
},
79+
id: "createdAt",
7480
},
7581
];
7682

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import React, { useMemo } from "react";
2+
import { CreateReportRequestDto, ReportService } from "@/wrapper/server";
3+
import { useForm } from "react-hook-form";
4+
import {
5+
Button,
6+
ComboboxItem,
7+
Select,
8+
Stack,
9+
Text,
10+
Textarea,
11+
} from "@mantine/core";
12+
import {
13+
reportCategoryToDescription,
14+
reportCategoryToString,
15+
} from "@/components/report/util/reportCategoryToString";
16+
import { useMutation } from "@tanstack/react-query";
17+
import { notifications } from "@mantine/notifications";
18+
import { z } from "zod";
19+
20+
const ReportCreateFormSchema = z.object({
21+
category: z
22+
.nativeEnum(CreateReportRequestDto.category)
23+
.default(CreateReportRequestDto.category.SPAM),
24+
reason: z.string().optional(),
25+
});
26+
27+
type ReportCreateFormValues = z.infer<typeof ReportCreateFormSchema>;
28+
29+
export interface ReportCreateFormProps {
30+
sourceId: string;
31+
sourceType: CreateReportRequestDto.sourceType;
32+
onSuccess?: () => void;
33+
}
34+
35+
const ReportCreateForm = ({
36+
sourceId,
37+
sourceType,
38+
onSuccess,
39+
}: ReportCreateFormProps) => {
40+
const { register, watch, handleSubmit, setValue } =
41+
useForm<ReportCreateFormValues>({
42+
mode: "onSubmit",
43+
defaultValues: {
44+
reason: undefined,
45+
category: CreateReportRequestDto.category.SPAM,
46+
},
47+
});
48+
49+
const categorySelectOptions = useMemo<ComboboxItem[]>(() => {
50+
return Object.values(CreateReportRequestDto.category).map((v) => {
51+
return {
52+
label: reportCategoryToString(v),
53+
value: v,
54+
};
55+
});
56+
}, []);
57+
58+
const selectedCategory = watch("category");
59+
60+
const selectedCategoryDescription = useMemo(() => {
61+
return reportCategoryToDescription(selectedCategory);
62+
}, [selectedCategory]);
63+
64+
const reportCreateMutation = useMutation({
65+
mutationFn: async (data: ReportCreateFormValues) => {
66+
await ReportService.reportControllerCreate({
67+
sourceId,
68+
sourceType,
69+
category: data.category,
70+
reason: data.reason,
71+
});
72+
},
73+
onError: () => {
74+
notifications.show({
75+
color: "red",
76+
message:
77+
"Error while sending your report. Please try again. If this persists, contact support.",
78+
});
79+
},
80+
onSuccess: () => {
81+
notifications.show({
82+
color: "green",
83+
message:
84+
"Thank you for submitting your report! It will be reviewed by our moderators as soon as possible.",
85+
});
86+
87+
if (onSuccess) onSuccess();
88+
},
89+
});
90+
91+
return (
92+
<form
93+
className={"w-full h-full"}
94+
onSubmit={handleSubmit((data) => reportCreateMutation.mutate(data))}
95+
>
96+
<Stack className={"w-full h-full"}>
97+
<Text className={"text-sm text-dimmed"}>
98+
For auditing purposes, you need to generate a report before
99+
issuing a possible suspension/ban on a user. This helps us
100+
streamline a possible review process.
101+
</Text>
102+
<Select
103+
withAsterisk
104+
value={selectedCategory}
105+
onChange={(v) => {
106+
if (v) {
107+
setValue(
108+
"category",
109+
v as CreateReportRequestDto.category,
110+
);
111+
}
112+
}}
113+
name={"category"}
114+
allowDeselect={false}
115+
label={"Report category"}
116+
data={categorySelectOptions}
117+
description={selectedCategoryDescription}
118+
/>
119+
<Textarea
120+
{...register("reason")}
121+
label={"Reason"}
122+
description={
123+
"Optional. A detailed reason may help us decide in a possible review process."
124+
}
125+
/>
126+
<Text className={"text-dimmed text-sm"}>
127+
The generated report may be handled by any GameNode
128+
moderator or admin.
129+
</Text>
130+
<Button className={"mt-2"} type={"submit"}>
131+
Submit report
132+
</Button>
133+
</Stack>
134+
</form>
135+
);
136+
};
137+
138+
export default ReportCreateForm;

src/hooks/use-custom-table.ts renamed to src/components/table/hooks/use-custom-table.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
useMantineReactTable,
44
} from "mantine-react-table";
55

6-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
76
export type CustomTableOptions<TData extends Record<string, any> = {}> = Omit<
87
MRT_TableOptions<TData>,
98
| "mantinePaginationProps"
@@ -13,7 +12,6 @@ export type CustomTableOptions<TData extends Record<string, any> = {}> = Omit<
1312
| "initialState.density"
1413
>;
1514

16-
// biome-ignore lint/complexity/noBannedTypes: <explanation>
1715
export const useCustomTable = <TData extends Record<string, any> = {}>(
1816
tableOptions: CustomTableOptions<TData>,
1917
) => {

0 commit comments

Comments
 (0)