Skip to content

Commit

Permalink
- Improve ordering for user join chart
Browse files Browse the repository at this point in the history
- User management dashboard
  • Loading branch information
Lamarcke committed Nov 4, 2024
1 parent 7550b58 commit f414006
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 6 deletions.
13 changes: 13 additions & 0 deletions src/app/(dashboard)/dashboard/user/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";
import { PageContainer } from "@/components/PageContainer/PageContainer";
import UsersManagementTable from "@/components/profile/UsersManagementTable";

const Page = () => {
return (
<PageContainer title={"Users"}>
<UsersManagementTable />
</PageContainer>
);
};

export default Page;
2 changes: 2 additions & 0 deletions src/components/Navbar/Navbar.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import {
IconLock,
IconMoodSmile,
IconReport,
IconUsers,
} from "@tabler/icons-react";
import type { NavItem } from "@/types/nav-item";

export const navLinks: NavItem[] = [
{ label: "Dashboard", icon: IconDashboard, link: "/dashboard" },
{ label: "Users", icon: IconUsers, link: "/dashboard/user" },
{ label: "Reports", icon: IconReport, link: "/dashboard/report" },
{
label: "Achievements",
Expand Down
1 change: 0 additions & 1 deletion src/components/auth/SuperTokensProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export const frontendConfig = (): SuperTokensConfig => {

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

Expand Down
11 changes: 10 additions & 1 deletion src/components/charts/UserJoinPeriodChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@ interface JoinPeriodItem {
const profileToPeriodItems = (profiles: Profile[]): JoinPeriodItem[] => {
const periodMap = new Map<string, number>();

for (const profile of profiles) {
// Sort by createdAt ASC
// (meaning newer users will be at the end of the chart)
const sortedProfiles = profiles.toSorted((a, b) => {
const createDateA = new Date(a.createdAt);
const createDateB = new Date(b.createdAt);

return createDateA.getTime() - createDateB.getTime();
});

for (const profile of sortedProfiles) {
const createdAtDate = new Date(profile.createdAt);
const createdAtMonth = `${createdAtDate.getMonth() + 1}`.padStart(
2,
Expand Down
134 changes: 134 additions & 0 deletions src/components/profile/UsersManagementTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"use client";

import React, { useState } from "react";
import { useUserProfiles } from "@/components/profile/hooks/useUserProfiles";
import { MantineReactTable, MRT_ColumnDef } from "mantine-react-table";
import {
CreateReportRequestDto,
FindAllProfileResponseItemDto,
} from "@/wrapper/server";
import CenteredLoading from "@/components/general/CenteredLoading";
import CenteredErrorMessage from "@/components/general/CenteredErrorMessage";
import { Badge, MantineColor, Menu, Modal, Paper } from "@mantine/core";
import { useCustomTable } from "@/components/table/hooks/use-custom-table";
import { UserAvatarGroup } from "@/components/general/avatar/UserAvatarGroup";
import { useDisclosure } from "@mantine/hooks";
import ReportCreateForm from "@/components/report/form/ReportCreateForm";
import sourceType = CreateReportRequestDto.sourceType;

const columns: MRT_ColumnDef<FindAllProfileResponseItemDto>[] = [
{
accessorKey: "profile.username",
header: "Username",
Cell: ({ row }) => {
return <UserAvatarGroup userId={row.original.profile.userId} />;
},
},
{
accessorFn: (row) => {
if (row.isSuspended) {
return "SUSPENDED";
} else if (row.isBanned) {
return "BANNED";
}
return "NORMAL";
},
header: "Status",
filterVariant: "select",
mantineFilterSelectProps: {
data: [
{ label: "Normal", value: "NORMAL" },
{ label: "Suspended", value: "SUSPENDED" },
{ label: "Banned", value: "BANNED" },
],
},
Cell: ({ row, renderedCellValue }) => {
const item = row.original;
const color: MantineColor =
item.isSuspended || item.isBanned ? "red" : "green";
return <Badge color={color}>{renderedCellValue}</Badge>;
},
},
{
header: "Joined at",
accessorFn: (row) =>
new Date(row.profile.createdAt).toLocaleString("en-US"),
sortingFn: (rowA, rowB, columnId) => {
const createDateA = new Date(rowA.original.profile.createdAt);
const createDateB = new Date(rowB.original.profile.createdAt);

return createDateA.getTime() - createDateB.getTime();
},
id: "createdAt",
},
];

const UsersManagementTable = () => {
const { data, isLoading, isError, isFetching } = useUserProfiles();

const [reportModalOpened, reportModalUtils] = useDisclosure();

const [reportedUserId, setReportedUserId] = useState<string | undefined>(
undefined,
);

const table = useCustomTable<FindAllProfileResponseItemDto>({
columns,
data: data ?? [],
rowCount: data?.length ?? 0,
state: {
isLoading: isLoading,
showAlertBanner: isError,
showProgressBars: isFetching,
},
enableRowActions: true,
renderRowActionMenuItems: (item) => {
const profile = item.row.original.profile;
return (
<>
<Menu.Item
onClick={() => {
setReportedUserId(profile.userId);
reportModalUtils.open();
}}
>
Generate report
</Menu.Item>
</>
);
},
});

if (isLoading) {
return <CenteredLoading message="Loading..." />;
} else if (isError) {
return (
<CenteredErrorMessage
message={"Failed to load users. Please try again."}
/>
);
} else if (data == undefined) {
return null;
}

return (
<Paper withBorder radius="md" p="md" mt="lg">
<Modal
title={"Generate report"}
opened={reportModalOpened}
onClose={reportModalUtils.close}
>
{reportedUserId && (
<ReportCreateForm
sourceId={reportedUserId}
sourceType={sourceType.PROFILE}
onSuccess={reportModalUtils.close}
/>
)}
</Modal>
<MantineReactTable table={table} />
</Paper>
);
};

export default UsersManagementTable;
10 changes: 8 additions & 2 deletions src/components/report/ReportsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
reportCategoryToString,
reportSourceTypeToString,
} from "@/components/report/util/reportCategoryToString";
import { useCustomTable } from "@/hooks/use-custom-table";
import { useCustomTable } from "@/components/table/hooks/use-custom-table";
import { useReports } from "@/components/report/hooks/useReports";
import { Button, Paper, Title, Text, Badge } from "@mantine/core";
import { PaginationState } from "@tanstack/table-core";
Expand Down Expand Up @@ -69,8 +69,14 @@ const columns: MRT_ColumnDef<Report>[] = [
},
{
header: "Created At",
accessorKey: "createdAt",
accessorFn: (row) => new Date(row.createdAt).toLocaleString("en-US"),
sortingFn: (rowA, rowB, columnId) => {
const createDateA = new Date(rowA.original.createdAt);
const createDateB = new Date(rowB.original.createdAt);

return createDateA.getTime() - createDateB.getTime();
},
id: "createdAt",
},
];

Expand Down
138 changes: 138 additions & 0 deletions src/components/report/form/ReportCreateForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React, { useMemo } from "react";
import { CreateReportRequestDto, ReportService } from "@/wrapper/server";
import { useForm } from "react-hook-form";
import {
Button,
ComboboxItem,
Select,
Stack,
Text,
Textarea,
} from "@mantine/core";
import {
reportCategoryToDescription,
reportCategoryToString,
} from "@/components/report/util/reportCategoryToString";
import { useMutation } from "@tanstack/react-query";
import { notifications } from "@mantine/notifications";
import { z } from "zod";

const ReportCreateFormSchema = z.object({
category: z
.nativeEnum(CreateReportRequestDto.category)
.default(CreateReportRequestDto.category.SPAM),
reason: z.string().optional(),
});

type ReportCreateFormValues = z.infer<typeof ReportCreateFormSchema>;

export interface ReportCreateFormProps {
sourceId: string;
sourceType: CreateReportRequestDto.sourceType;
onSuccess?: () => void;
}

const ReportCreateForm = ({
sourceId,
sourceType,
onSuccess,
}: ReportCreateFormProps) => {
const { register, watch, handleSubmit, setValue } =
useForm<ReportCreateFormValues>({
mode: "onSubmit",
defaultValues: {
reason: undefined,
category: CreateReportRequestDto.category.SPAM,
},
});

const categorySelectOptions = useMemo<ComboboxItem[]>(() => {
return Object.values(CreateReportRequestDto.category).map((v) => {
return {
label: reportCategoryToString(v),
value: v,
};
});
}, []);

const selectedCategory = watch("category");

const selectedCategoryDescription = useMemo(() => {
return reportCategoryToDescription(selectedCategory);
}, [selectedCategory]);

const reportCreateMutation = useMutation({
mutationFn: async (data: ReportCreateFormValues) => {
await ReportService.reportControllerCreate({
sourceId,
sourceType,
category: data.category,
reason: data.reason,
});
},
onError: () => {
notifications.show({
color: "red",
message:
"Error while sending your report. Please try again. If this persists, contact support.",
});
},
onSuccess: () => {
notifications.show({
color: "green",
message:
"Thank you for submitting your report! It will be reviewed by our moderators as soon as possible.",
});

if (onSuccess) onSuccess();
},
});

return (
<form
className={"w-full h-full"}
onSubmit={handleSubmit((data) => reportCreateMutation.mutate(data))}
>
<Stack className={"w-full h-full"}>
<Text className={"text-sm text-dimmed"}>
For auditing purposes, you need to generate a report before
issuing a possible suspension/ban on a user. This helps us
streamline a possible review process.
</Text>
<Select
withAsterisk
value={selectedCategory}
onChange={(v) => {
if (v) {
setValue(
"category",
v as CreateReportRequestDto.category,
);
}
}}
name={"category"}
allowDeselect={false}
label={"Report category"}
data={categorySelectOptions}
description={selectedCategoryDescription}
/>
<Textarea
{...register("reason")}
label={"Reason"}
description={
"Optional. A detailed reason may help us decide in a possible review process."
}
/>
<Text className={"text-dimmed text-sm"}>
The generated report may be handled by any GameNode
moderator or admin.
</Text>
<Button className={"mt-2"} type={"submit"}>
Submit report
</Button>
</Stack>
</form>
);
};

export default ReportCreateForm;
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
useMantineReactTable,
} from "mantine-react-table";

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

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

0 comments on commit f414006

Please sign in to comment.