Skip to content

Commit

Permalink
implement trollboard UI (#1301)
Browse files Browse the repository at this point in the history
* implement trollboard UI

* remove unneeded code

* add link to user

* user link in leaderboard
  • Loading branch information
notmd committed Feb 8, 2023
1 parent 59dbfea commit a46e4a2
Show file tree
Hide file tree
Showing 13 changed files with 389 additions and 85 deletions.
28 changes: 14 additions & 14 deletions website/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion website/public/locales/en/side_menu.json
Expand Up @@ -8,5 +8,6 @@
"users": "Users",
"users_dashboard": "Users Dashboard",
"status": "Status",
"status_dashboard": "Status Dashboard"
"status_dashboard": "Status Dashboard",
"trollboard": "Trollboard"
}
5 changes: 5 additions & 0 deletions website/src/components/Layout.tsx
Expand Up @@ -74,6 +74,11 @@ export const getAdminLayout = (page: React.ReactElement) => (
pathname: "/admin",
icon: Users,
},
{
labelID: "trollboard",
pathname: "/admin/trollboard",
icon: BarChart2,
},
{
labelID: "status",
pathname: "/admin/status",
Expand Down
107 changes: 38 additions & 69 deletions website/src/components/LeaderboardTable/LeaderboardTable.tsx
@@ -1,15 +1,16 @@
import { Box, CircularProgress, Flex, useColorModeValue, useToken } from "@chakra-ui/react";
import { Box, CircularProgress, Flex, Link, useColorModeValue } from "@chakra-ui/react";
import { createColumnHelper } from "@tanstack/react-table";
import { MoreHorizontal } from "lucide-react";
import NextLink from "next/link";
import { useSession } from "next-auth/react";
import { useTranslation } from "next-i18next";
import React, { useCallback, useMemo, useState } from "react";
import { get } from "src/lib/api";
import { colors } from "src/styles/Theme/colors";
import React, { useMemo } from "react";
import { LeaderboardEntity, LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";
import useSWRImmutable from "swr/immutable";

import { DataTable, DataTableColumnDef, DataTableRowPropsCallback } from "../DataTable";

import { DataTable, DataTableColumnDef } from "../DataTable";
import { useBoardPagination } from "./useBoardPagination";
import { useBoardRowProps } from "./useBoardRowProps";
import { useFetchBoard } from "./useFetchBoard";
type WindowLeaderboardEntity = LeaderboardEntity & { isSpaceRow?: boolean };

const columnHelper = createColumnHelper<WindowLeaderboardEntity>();
Expand All @@ -34,10 +35,13 @@ export const LeaderboardTable = ({
data: reply,
isLoading,
error,
} = useSWRImmutable<LeaderboardReply & { user_stats_window?: LeaderboardReply["leaderboard"] }>(
`/api/leaderboard?time_frame=${timeFrame}&limit=${limit}&includeUserStats=${!hideCurrentUserRanking}`,
get
lastUpdated,
} = useFetchBoard<LeaderboardReply & { user_stats_window?: LeaderboardReply["leaderboard"] }>(
`/api/leaderboard?time_frame=${timeFrame}&limit=${limit}&includeUserStats=${!hideCurrentUserRanking}`
);
const { data: session } = useSession();

const isAdmin = session?.user?.role === "admin";
const columns: DataTableColumnDef<WindowLeaderboardEntity>[] = useMemo(
() => [
{
Expand All @@ -49,6 +53,14 @@ export const LeaderboardTable = ({
},
columnHelper.accessor("display_name", {
header: t("user"),
cell: ({ getValue, row }) =>
isAdmin ? (
<Link as={NextLink} href={`/admin/manage_user/${row.original.user_id}`}>
{getValue()}
</Link>
) : (
getValue()
),
}),
columnHelper.accessor("leader_score", {
header: t("score"),
Expand All @@ -63,40 +75,32 @@ export const LeaderboardTable = ({
header: t("label"),
}),
],
[t]
[isAdmin, t]
);

const lastUpdated = useMemo(() => {
const val = new Date(reply?.last_updated);
return t("last_updated_at", { val, formatParams: { val: { dateStyle: "full", timeStyle: "short" } } });
}, [t, reply?.last_updated]);

const [page, setPage] = useState(1);
const {
data: paginatedData,
end,
...pagnationProps
} = useBoardPagination({ rowPerPage, data: reply?.leaderboard, limit });
const data: WindowLeaderboardEntity[] = useMemo(() => {
if (!reply) {
return [];
}
const start = (page - 1) * rowPerPage;
const end = start + rowPerPage;
const leaderBoardEntities = reply.leaderboard.slice(start, end);
if (hideCurrentUserRanking || !reply.user_stats_window) {
return leaderBoardEntities;
if (hideCurrentUserRanking || !reply?.user_stats_window) {
return paginatedData;
}
const userStatsWindow: WindowLeaderboardEntity[] = reply.user_stats_window;
const userStats = userStatsWindow.find((stats) => stats.highlighted);
if (userStats.rank > end) {
leaderBoardEntities.push(
if (userStats && userStats.rank > end) {
paginatedData.push(
{ isSpaceRow: true } as WindowLeaderboardEntity,
...reply.user_stats_window.filter(
(stats) =>
leaderBoardEntities.findIndex((leaderBoardEntity) => leaderBoardEntity.user_id === stats.user_id) === -1
(stats) => paginatedData.findIndex((leaderBoardEntity) => leaderBoardEntity.user_id === stats.user_id) === -1
) // filter to avoid duplicated row
);
}
return leaderBoardEntities;
}, [page, rowPerPage, reply, hideCurrentUserRanking]);
return paginatedData;
}, [hideCurrentUserRanking, reply?.user_stats_window, end, paginatedData]);

const rowProps = useLeaderboardRowProps();
const rowProps = useBoardRowProps<WindowLeaderboardEntity>();

if (isLoading) {
return <CircularProgress isIndeterminate></CircularProgress>;
Expand All @@ -106,19 +110,13 @@ export const LeaderboardTable = ({
return <span>Unable to load leaderboard</span>;
}

const maxPage = Math.ceil(reply.leaderboard.length / rowPerPage);

return (
<DataTable
<DataTable<WindowLeaderboardEntity>
data={data}
columns={columns}
caption={lastUpdated}
disablePagination={limit <= rowPerPage}
disableNext={page >= maxPage}
disablePrevious={page === 1}
onNextClick={() => setPage((p) => p + 1)}
onPreviousClick={() => setPage((p) => p - 1)}
rowProps={rowProps}
{...pagnationProps}
></DataTable>
);
};
Expand All @@ -131,32 +129,3 @@ const SpaceRow = () => {
</Flex>
);
};

const useLeaderboardRowProps = () => {
const borderColor = useToken("colors", useColorModeValue(colors.light.active, colors.dark.active));
return useCallback<DataTableRowPropsCallback<WindowLeaderboardEntity>>(
(row) => {
const rowData = row.original;
return rowData.highlighted
? {
sx: {
// https://stackoverflow.com/questions/37963524/how-to-apply-border-radius-to-tr-in-bootstrap
position: "relative",
"td:first-of-type:before": {
borderLeft: `6px solid ${borderColor}`,
content: `""`,
display: "block",
width: "10px",
height: "100%",
left: 0,
top: 0,
borderRadius: "6px 0 0 6px",
position: "absolute",
},
},
}
: {};
},
[borderColor]
);
};
124 changes: 124 additions & 0 deletions website/src/components/LeaderboardTable/TrollboardTable.tsx
@@ -0,0 +1,124 @@
import { Box, CircularProgress, Flex, Link } from "@chakra-ui/react";
import { createColumnHelper } from "@tanstack/react-table";
import { ThumbsDown, ThumbsUp } from "lucide-react";
import NextLink from "next/link";
import { FetchTrollBoardResponse, TrollboardEntity, TrollboardTimeFrame } from "src/types/Trollboard";

import { DataTable } from "../DataTable";
import { useBoardPagination } from "./useBoardPagination";
import { useBoardRowProps } from "./useBoardRowProps";
import { useFetchBoard } from "./useFetchBoard";
const columnHelper = createColumnHelper<TrollboardEntity>();

const toPercentage = (num: number) => `${Math.round(num * 100)}%`;

const columns = [
columnHelper.accessor("rank", {}),
columnHelper.accessor("display_name", {
header: "Display name",
cell: ({ getValue, row }) => (
<Link as={NextLink} href={`/admin/manage_user/${row.original.user_id}`}>
{getValue()}
</Link>
),
}),
columnHelper.accessor("troll_score", {
header: "Troll score",
}),
columnHelper.accessor("red_flags", {
header: "Red flags",
}),
columnHelper.accessor((row) => [row.upvotes, row.downvotes] as const, {
id: "vote",
cell: ({ getValue }) => {
const [up, down] = getValue();
return (
<Flex gap={2} justifyItems="center" alignItems="center">
<ThumbsUp></ThumbsUp>
{up}
<ThumbsDown></ThumbsDown>
{down}
</Flex>
);
},
}),
columnHelper.accessor((row) => row.spam + row.spam_prompts, {
header: "Spam",
}),
columnHelper.accessor("lang_mismach", {
header: "Lang mismach",
}),
columnHelper.accessor("not_appropriate", {
header: "Not appropriate",
}),
columnHelper.accessor("pii", {}),
columnHelper.accessor("hate_speech", {
header: "Hate speech",
}),
columnHelper.accessor("sexual_content", {
header: "Sexual Content",
}),
columnHelper.accessor("political_content", {
header: "Political Content",
}),
columnHelper.accessor("quality", {
cell: ({ getValue }) => toPercentage(getValue()),
}),
columnHelper.accessor("helpfulness", {
cell: ({ getValue }) => toPercentage(getValue()),
}),
columnHelper.accessor("humor", {
cell: ({ getValue }) => toPercentage(getValue()),
}),
columnHelper.accessor("violence", {
cell: ({ getValue }) => toPercentage(getValue()),
}),
columnHelper.accessor("toxicity", {
cell: ({ getValue }) => toPercentage(getValue()),
}),
];

export const TrollboardTable = ({
limit,
rowPerPage,
timeFrame,
}: {
timeFrame: TrollboardTimeFrame;
limit: number;
rowPerPage: number;
}) => {
const {
data: trollboardRes,
isLoading,
error,
lastUpdated,
} = useFetchBoard<FetchTrollBoardResponse>(`/api/admin/trollboard?time_frame=${timeFrame}&limit=${limit}`);

const { data, ...paginationProps } = useBoardPagination({ rowPerPage, data: trollboardRes?.trollboard, limit });
const rowProps = useBoardRowProps<TrollboardEntity>();
if (isLoading) {
return <CircularProgress isIndeterminate></CircularProgress>;
}

if (error) {
return <span>Unable to load leaderboard</span>;
}

return (
<Box
sx={{
"th,td": {
px: 2,
},
}}
>
<DataTable<TrollboardEntity>
data={data}
columns={columns}
caption={lastUpdated}
rowProps={rowProps}
{...paginationProps}
></DataTable>
</Box>
);
};
34 changes: 34 additions & 0 deletions website/src/components/LeaderboardTable/useBoardPagination.ts
@@ -0,0 +1,34 @@
import { useState } from "react";

export const useBoardPagination = <T>({
rowPerPage,
limit,
...res
}: {
rowPerPage: number;
data?: T[];
limit: number;
}) => {
const data = res.data || [];
const [page, setPage] = useState(1);
const maxPage = data ? Math.ceil(data.length / rowPerPage) : 0;
const disablePagination = limit <= rowPerPage;
const disableNext = page >= maxPage;
const disablePrevious = page === 1;
const onNextClick = () => setPage((p) => p + 1);
const onPreviousClick = () => setPage((p) => p - 1);
const start = (page - 1) * rowPerPage;
const end = start + rowPerPage;
const entities = data.slice(start, end);

return {
page,
data: entities,
end,
disablePrevious,
disableNext,
disablePagination,
onNextClick,
onPreviousClick,
};
};

0 comments on commit a46e4a2

Please sign in to comment.