Show current user rank in leaderboard (#1263)

close #1000 maybe #1178 too

*  Show current user rank in the leaderboard with +-1 user (only on leaderboard 
*  Extend auto_main script to use random user.
*  Support colSpan in the DataTable component (I haven't verified colSpan in header yet, leave that until we need it)
*  Refactor OasstError to include the path and request method.
This commit is contained in:
notmd
2023-02-07 14:49:50 +09:00
committed by GitHub
parent 931e12f31e
commit 952e021c88
8 changed files with 396 additions and 254 deletions
@@ -1,6 +1,6 @@
import { Card, CardBody, Link, Text } from "@chakra-ui/react";
import { useTranslation } from "next-i18next";
import NextLink from "next/link";
import { useTranslation } from "next-i18next";
import { LeaderboardTable } from "src/components/LeaderboardTable";
import { LeaderboardTimeFrame } from "src/types/Leaderboard";
@@ -19,7 +19,7 @@ export function LeaderboardWidget() {
</div>
<Card>
<CardBody>
<LeaderboardTable timeFrame={LeaderboardTimeFrame.day} limit={5} rowPerPage={5} />
<LeaderboardTable timeFrame={LeaderboardTimeFrame.day} limit={5} rowPerPage={5} hideCurrentUserRanking />
</CardBody>
</Card>
</div>
+33 -4
View File
@@ -23,7 +23,7 @@ import {
Tr,
useDisclosure,
} from "@chakra-ui/react";
import { ColumnDef, flexRender, getCoreRowModel, Row, useReactTable } from "@tanstack/react-table";
import { Cell, ColumnDef, flexRender, getCoreRowModel, Row, useReactTable } from "@tanstack/react-table";
import { Filter } from "lucide-react";
import { useTranslation } from "next-i18next";
import { ChangeEvent, ReactNode } from "react";
@@ -31,6 +31,7 @@ import { useDebouncedCallback } from "use-debounce";
export type DataTableColumnDef<T> = ColumnDef<T> & {
filterable?: boolean;
span?: number | ((cell: Cell<T, unknown>) => number | undefined);
};
// TODO: stricter type
@@ -126,9 +127,7 @@ export const DataTable = <T,>({
const props = typeof rowProps === "function" ? rowProps(row) : rowProps;
return (
<Tr key={row.id} {...props}>
{row.getVisibleCells().map((cell) => (
<Td key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</Td>
))}
<DataTableRow row={row}></DataTableRow>
</Tr>
);
})}
@@ -139,6 +138,36 @@ export const DataTable = <T,>({
);
};
type WithSpanCell<T> = Cell<T, unknown> & { span?: number };
const DataTableRow = <T,>({ row }: { row: Row<T> }) => {
const cells: WithSpanCell<T>[] = row.getVisibleCells();
const renderCells: WithSpanCell<T>[] = [];
for (let i = 0; i < cells.length; i++) {
const cell = cells[i];
const span = (cell.column.columnDef as DataTableColumnDef<T>).span;
const spanValue = typeof span === "function" ? span(cell) : span;
if (spanValue && spanValue > 1) {
i += spanValue - 1; // skip next `spanValue - 1` cell
}
cell.span = spanValue;
renderCells.push(cell);
}
return (
<>
{renderCells.map((cell) => {
return (
<Td key={cell.id} colSpan={cell.span}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Td>
);
})}
</>
);
};
const FilterModal = ({
label,
onChange,
@@ -1,5 +1,6 @@
import { CircularProgress, useColorModeValue, useToken } from "@chakra-ui/react";
import { Box, CircularProgress, Flex, useColorModeValue, useToken } from "@chakra-ui/react";
import { createColumnHelper } from "@tanstack/react-table";
import { MoreHorizontal } from "lucide-react";
import { useTranslation } from "next-i18next";
import React, { useCallback, useMemo, useState } from "react";
import { get } from "src/lib/api";
@@ -7,9 +8,11 @@ import { colors } from "src/styles/Theme/colors";
import { LeaderboardEntity, LeaderboardReply, LeaderboardTimeFrame } from "src/types/Leaderboard";
import useSWRImmutable from "swr/immutable";
import { DataTable, DataTableRowPropsCallback } from "../DataTable";
import { DataTable, DataTableColumnDef, DataTableRowPropsCallback } from "../DataTable";
const columnHelper = createColumnHelper<LeaderboardEntity>();
type WindowLeaderboardEntity = LeaderboardEntity & { isSpaceRow?: boolean };
const columnHelper = createColumnHelper<WindowLeaderboardEntity>();
/**
* Presents a grid of leaderboard entries with more detailed information.
@@ -18,10 +21,12 @@ export const LeaderboardTable = ({
timeFrame,
limit: limit,
rowPerPage,
hideCurrentUserRanking,
}: {
timeFrame: LeaderboardTimeFrame;
limit: number;
rowPerPage: number;
hideCurrentUserRanking?: boolean;
}) => {
const { t } = useTranslation("leaderboard");
@@ -29,15 +34,19 @@ export const LeaderboardTable = ({
data: reply,
isLoading,
error,
} = useSWRImmutable<LeaderboardReply>(`/api/leaderboard?time_frame=${timeFrame}&limit=${limit}`, get, {
revalidateOnMount: true,
});
const columns = useMemo(
} = useSWRImmutable<LeaderboardReply & { user_stats_window: LeaderboardReply["leaderboard"] }>(
`/api/leaderboard?time_frame=${timeFrame}&limit=${limit}&includeUserStats=${!hideCurrentUserRanking}`,
get
);
const columns: DataTableColumnDef<WindowLeaderboardEntity>[] = useMemo(
() => [
columnHelper.accessor("rank", {
header: t("rank"),
}),
{
...columnHelper.accessor("rank", {
header: t("rank"),
cell: ({ row, getValue }) => (row.original.isSpaceRow ? <SpaceRow></SpaceRow> : getValue()),
}),
span: (cell) => (cell.row.original.isSpaceRow ? 6 : undefined),
},
columnHelper.accessor("display_name", {
header: t("user"),
}),
@@ -63,15 +72,72 @@ export const LeaderboardTable = ({
}, [t, reply?.last_updated]);
const [page, setPage] = useState(1);
const data = useMemo(() => {
const data: WindowLeaderboardEntity[] = useMemo(() => {
if (!reply) {
return [];
}
const start = (page - 1) * rowPerPage;
return reply?.leaderboard.slice(start, start + rowPerPage) || [];
}, [rowPerPage, page, reply?.leaderboard]);
const end = start + rowPerPage;
const leaderBoardEntities = reply.leaderboard.slice(start, end);
if (hideCurrentUserRanking) {
return leaderBoardEntities;
}
const userStatsWindow: WindowLeaderboardEntity[] = reply.user_stats_window;
const userStats = userStatsWindow.find((stats) => stats.highlighted);
if (userStats.rank > end) {
leaderBoardEntities.push(
{ isSpaceRow: true } as WindowLeaderboardEntity,
...reply.user_stats_window.filter(
(stats) =>
leaderBoardEntities.findIndex((leaderBoardEntity) => leaderBoardEntity.user_id === stats.user_id) === -1
) // filter to avoid duplicated row
);
}
return leaderBoardEntities;
}, [page, rowPerPage, reply, hideCurrentUserRanking]);
const rowProps = useLeaderboardRowProps();
if (isLoading) {
return <CircularProgress isIndeterminate></CircularProgress>;
}
if (error) {
return <span>Unable to load leaderboard</span>;
}
const maxPage = Math.ceil(reply.leaderboard.length / rowPerPage);
return (
<DataTable
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}
></DataTable>
);
};
const SpaceRow = () => {
const color = useColorModeValue("gray.600", "gray.400");
return (
<Flex justify="center">
<Box as={MoreHorizontal} color={color}></Box>
</Flex>
);
};
const useLeaderboardRowProps = () => {
const borderColor = useToken("colors", useColorModeValue(colors.light.active, colors.dark.active));
const rowProps = useCallback<DataTableRowPropsCallback<LeaderboardEntity>>(
return useCallback<DataTableRowPropsCallback<WindowLeaderboardEntity>>(
(row) => {
return row.original.highlighted
const rowData = row.original;
return rowData.highlighted
? {
sx: {
// https://stackoverflow.com/questions/37963524/how-to-apply-border-radius-to-tr-in-bootstrap
@@ -93,28 +159,4 @@ export const LeaderboardTable = ({
},
[borderColor]
);
if (isLoading) {
return <CircularProgress isIndeterminate></CircularProgress>;
}
if (error) {
return <span>Unable to load leaderboard</span>;
}
const maxPage = Math.ceil(reply.leaderboard.length / rowPerPage);
return (
<DataTable
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}
></DataTable>
);
};