feat: rival ranking in UI

This commit is contained in:
zkldi
2022-10-15 02:49:41 +01:00
parent 2c3af78b7c
commit 0b54d6d862
12 changed files with 252 additions and 42 deletions
@@ -261,6 +261,7 @@ function ChartLeaderboardTable({
return (
<PBTable
defaultRankingViewMode={mode === "rivals" ? "global-no-switch" : "both-if-self"}
key={mode}
dataset={dataset}
game={game}
+4 -1
View File
@@ -37,6 +37,7 @@ import {
import { SongsReturn } from "types/api-returns";
import { GamePT, SetState } from "types/react";
import useLUGPTSettings from "components/util/useLUGPTSettings";
import { UGPTContextProvider } from "context/UGPTContext";
export default function GameRoutes() {
const { game } = useParams<{ game: string }>();
@@ -66,7 +67,9 @@ export default function GameRoutes() {
</Route>
<Route path="/dashboard/games/:game/:playtype">
<GamePlaytypeRoutes game={game} />
<UGPTContextProvider>
<GamePlaytypeRoutes game={game} />
</UGPTContextProvider>
</Route>
<Route path="*">
@@ -1,15 +1,73 @@
import useLUGPTSettings from "components/util/useLUGPTSettings";
import React from "react";
import { integer } from "rg-stats/js/util/types";
import { PBScoreDocument } from "tachi-common";
export type RankingViewMode = "global" | "rival" | "both-if-self" | "global-no-switch";
export default function RankingCell({
rankingData,
userID,
rankingViewMode,
}: {
rankingData: PBScoreDocument["rankingData"];
userID: integer;
rankingViewMode: RankingViewMode;
}) {
return (
<td>
<strong>#{rankingData.rank}</strong>
<small>/{rankingData.outOf}</small>
</td>
);
const { settings } = useLUGPTSettings();
switch (rankingViewMode) {
case "global":
case "global-no-switch":
return (
<td>
<strong>#{rankingData.rank}</strong>
<small>/{rankingData.outOf}</small>
</td>
);
case "rival":
if (!settings) {
return (
<td>
No Settings, yet tried to view rival stats? not possible. how'd you get
here. report this.
</td>
);
}
if (settings?.userID !== userID) {
return (
<td>
<strong>N/A</strong>
</td>
);
}
return (
<td>
<strong>#{rankingData.rivalRank}</strong>
<small>/{settings.rivals.length + 1}</small>
</td>
);
case "both-if-self":
if (settings?.userID === userID && rankingData.rivalRank !== null) {
return (
<td>
<strong>Global #{rankingData.rank}</strong>
<small>/{rankingData.outOf}</small>
<br />
<strong>Rival #{rankingData.rivalRank}</strong>
<small>/{settings.rivals.length + 1}</small>
</td>
);
}
return (
<td>
<strong>#{rankingData.rank}</strong>
<small>/{rankingData.outOf}</small>
</td>
);
}
}
@@ -0,0 +1,64 @@
import useLUGPTSettings from "components/util/useLUGPTSettings";
import React from "react";
import { SetState } from "types/react";
import { RankingViewMode } from "../cells/RankingCell";
import SortableTH from "./SortableTH";
import { ZTableTHProps } from "./TachiTable";
export default function SelectableRanking({
rankingViewMode,
setRankingViewMode,
changeSort,
currentSortMode,
reverseSort,
}: {
rankingViewMode: RankingViewMode;
setRankingViewMode: SetState<RankingViewMode>;
} & ZTableTHProps) {
const { settings } = useLUGPTSettings();
if (rankingViewMode === "both-if-self" || rankingViewMode === "global-no-switch" || !settings) {
return (
<SortableTH
changeSort={changeSort}
currentSortMode={currentSortMode}
name="Ranking"
reverseSort={reverseSort}
shortName="Ranking"
/>
);
}
return (
<th>
<select
onChange={(v) => setRankingViewMode(v.target.value as RankingViewMode)}
value={rankingViewMode}
style={{
backgroundColor: "#131313",
border: "none",
color: "#ffffff",
fontSize: "inherit",
font: "inherit",
textAlign: "center",
}}
>
<option value="global">Global Ranking</option>
<option value="rival">Rival Ranking</option>
</select>
<br />
<span onClick={() => changeSort("Site Ranking")}>
<i
className={`flaticon2-arrow-up icon-sm sort-icon ${
currentSortMode === "Site Ranking" && reverseSort ? "active" : ""
}`}
></i>
<i
className={`flaticon2-arrow-down icon-sm sort-icon ${
currentSortMode === "Site Ranking" && !reverseSort ? "active" : ""
}`}
></i>
</span>
</th>
);
}
@@ -6,9 +6,10 @@ import { Game, IDStrings, ScoreCalculatedDataLookup } from "tachi-common";
import { FolderDataset } from "types/tables";
import { Playtype } from "types/tachi";
import Muted from "components/util/Muted";
import useLUGPTSettings from "components/util/useLUGPTSettings";
import DifficultyCell from "../cells/DifficultyCell";
import IndicatorsCell from "../cells/IndicatorsCell";
import RankingCell from "../cells/RankingCell";
import RankingCell, { RankingViewMode } from "../cells/RankingCell";
import TimestampCell from "../cells/TimestampCell";
import TitleCell from "../cells/TitleCell";
import DropdownRow from "../components/DropdownRow";
@@ -19,6 +20,7 @@ import ScoreCoreCells from "../game-core-cells/ScoreCoreCells";
import ChartHeader from "../headers/ChartHeader";
import { GetGPTCoreHeaders } from "../headers/GameHeaders";
import { EmptyHeader, FolderIndicatorHeader } from "../headers/IndicatorHeader";
import { CreateRankingHeader } from "../headers/RankingHeader";
export default function FolderTable<I extends IDStrings = IDStrings>({
dataset,
@@ -31,7 +33,10 @@ export default function FolderTable<I extends IDStrings = IDStrings>({
}) {
const defaultRating = useScoreRatingAlg(game, playtype);
const { settings } = useLUGPTSettings();
const [rating, setRating] = useState(defaultRating);
const [rankingViewMode, setRankingViewMode] = useState<RankingViewMode>("global");
const headers: Header<FolderDataset[0]>[] = [
ChartHeader(game, (k) => k),
@@ -45,11 +50,11 @@ export default function FolderTable<I extends IDStrings = IDStrings>({
setRating,
(x) => x.__related.pb
),
[
"Site Ranking",
"Site Rank",
NumericSOV((x) => x.__related.pb?.rankingData.rank ?? -Infinity),
],
CreateRankingHeader(
rankingViewMode,
setRankingViewMode,
(k) => k.__related.pb?.rankingData
),
["Last Raised", "Last Raised", NumericSOV((x) => x.__related.pb?.timeAchieved ?? 0)],
];
@@ -60,7 +65,13 @@ export default function FolderTable<I extends IDStrings = IDStrings>({
entryName="Charts"
searchFunctions={CreateDefaultFolderSearchParams(game, playtype)}
rowFunction={(data) => (
<Row rating={rating} data={data} key={data.chartID} game={game} />
<Row
rating={rating}
data={data}
key={data.chartID}
game={game}
rankingViewMode={rankingViewMode}
/>
)}
/>
);
@@ -70,10 +81,12 @@ function Row<I extends IDStrings = IDStrings>({
data,
rating,
game,
rankingViewMode,
}: {
data: FolderDataset<I>[0];
game: Game;
rating: ScoreCalculatedDataLookup[I];
rankingViewMode: RankingViewMode;
}) {
const score = data.__related.pb;
@@ -110,7 +123,11 @@ function Row<I extends IDStrings = IDStrings>({
<Muted>PB</Muted>
</td>
<ScoreCoreCells score={score} game={game} rating={rating} chart={data} />
<RankingCell rankingData={score.rankingData} />
<RankingCell
rankingData={score.rankingData}
userID={score.userID}
rankingViewMode={rankingViewMode}
/>
<TimestampCell time={score.timeAchieved} />
</DropdownRow>
);
@@ -0,0 +1,37 @@
import { NumericSOV } from "util/sorts";
import React from "react";
import { PBScoreDocument } from "tachi-common";
import { SetState } from "types/react";
import { RankingViewMode } from "../cells/RankingCell";
import SelectableRanking from "../components/SelectableRanking";
import { Header, ZTableTHProps } from "../components/TachiTable";
export function CreateRankingHeader<T>(
rankingViewMode: RankingViewMode,
setRankingViewMode: SetState<RankingViewMode>,
kMapToRankingData: (k: T) => PBScoreDocument["rankingData"] | undefined
): Header<T> {
return [
"Site Ranking",
"Site Ranking",
NumericSOV((x) => {
const rankingData = kMapToRankingData(x);
if (!rankingData) {
return -Infinity;
}
return rankingViewMode === "rival"
? rankingData.rivalRank ?? -Infinity
: rankingData.rank;
}),
(thProps: ZTableTHProps) => (
<SelectableRanking
rankingViewMode={rankingViewMode}
setRankingViewMode={setRankingViewMode}
{...thProps}
/>
),
];
}
+16 -3
View File
@@ -8,7 +8,7 @@ import { PBDataset } from "types/tables";
import { Playtype } from "types/tachi";
import DropdownIndicatorCell from "../cells/DropdownIndicatorCell";
import IndexCell from "../cells/IndexCell";
import RankingCell from "../cells/RankingCell";
import RankingCell, { RankingViewMode } from "../cells/RankingCell";
import TimestampCell from "../cells/TimestampCell";
import DropdownRow from "../components/DropdownRow";
import TachiTable, { Header } from "../components/TachiTable";
@@ -18,6 +18,7 @@ import ScoreCoreCells from "../game-core-cells/ScoreCoreCells";
import ChartHeader from "../headers/ChartHeader";
import { GetGPTCoreHeaders } from "../headers/GameHeaders";
import { EmptyHeader } from "../headers/IndicatorHeader";
import { CreateRankingHeader } from "../headers/RankingHeader";
import PBLeadingRows from "./PBLeadingRows";
export default function PBTable<I extends IDStrings = IDStrings>({
@@ -29,6 +30,7 @@ export default function PBTable<I extends IDStrings = IDStrings>({
showChart = true,
playtype,
alg,
defaultRankingViewMode,
}: {
dataset: PBDataset<I>;
indexCol?: boolean;
@@ -38,10 +40,14 @@ export default function PBTable<I extends IDStrings = IDStrings>({
playtype: Playtype;
game: Game;
alg?: ScoreCalculatedDataLookup[I];
defaultRankingViewMode?: RankingViewMode;
}) {
const defaultRating = useScoreRatingAlg(game, playtype);
const [rating, setRating] = useState(alg ?? defaultRating);
const [rankingViewMode, setRankingViewMode] = useState<RankingViewMode>(
defaultRankingViewMode ?? "global"
);
const headers: Header<PBDataset<I>[0]>[] = [
...GetPBLeadingHeaders(
@@ -51,7 +57,7 @@ export default function PBTable<I extends IDStrings = IDStrings>({
),
EmptyHeader,
...GetGPTCoreHeaders<PBDataset>(game, playtype, rating, setRating, (x) => x),
["Site Ranking", "Site Rank", NumericSOV((x) => x.rankingData.rank)],
CreateRankingHeader(rankingViewMode, setRankingViewMode, (k) => k.rankingData),
["Last Raised", "Last Raised", NumericSOV((x) => x.timeAchieved ?? 0)],
EmptyHeader,
];
@@ -86,6 +92,7 @@ export default function PBTable<I extends IDStrings = IDStrings>({
rating={rating}
game={game}
playtype={playtype}
rankingViewMode={rankingViewMode}
/>
)}
/>
@@ -100,6 +107,7 @@ function Row<I extends IDStrings = IDStrings>({
showUser,
game,
rating,
rankingViewMode,
}: {
pb: PBDataset<I>[0];
indexCol: boolean;
@@ -110,6 +118,7 @@ function Row<I extends IDStrings = IDStrings>({
playtype: Playtype;
// ts bug?
rating: any; // ScoreCalculatedDataLookup[I];
rankingViewMode: RankingViewMode;
}) {
const scoreState = usePBState(pb);
@@ -128,7 +137,11 @@ function Row<I extends IDStrings = IDStrings>({
{indexCol && <IndexCell index={pb.__related.index} />}
<PBLeadingRows {...{ showUser, showChart, pb, scoreState }} />
<ScoreCoreCells score={pb} game={game} rating={rating} chart={pb.__related.chart} />
<RankingCell rankingData={pb.rankingData} />
<RankingCell
rankingData={pb.rankingData}
userID={pb.userID}
rankingViewMode={rankingViewMode}
/>
<TimestampCell time={pb.timeAchieved} />
{showPlaycount && <td>{pb.__playcount ?? 0}</td>}
<DropdownIndicatorCell />
@@ -4,7 +4,7 @@ import React, { useState } from "react";
import { ChartDocument, Game, IDStrings, ScoreCalculatedDataLookup } from "tachi-common";
import { RivalChartDataset } from "types/tables";
import IndexCell from "../cells/IndexCell";
import RankingCell from "../cells/RankingCell";
import RankingCell, { RankingViewMode } from "../cells/RankingCell";
import TimestampCell from "../cells/TimestampCell";
import UserCell from "../cells/UserCell";
import DropdownRow from "../components/DropdownRow";
@@ -13,6 +13,7 @@ import { GraphAndJudgementDataComponent } from "../dropdowns/components/Document
import { GPTDropdownSettings } from "../dropdowns/GPTDropdownSettings";
import ScoreCoreCells from "../game-core-cells/ScoreCoreCells";
import { GetGPTCoreHeaders } from "../headers/GameHeaders";
import { CreateRankingHeader } from "../headers/RankingHeader";
export default function RivalChartTable<I extends IDStrings = IDStrings>({
dataset,
@@ -28,6 +29,7 @@ export default function RivalChartTable<I extends IDStrings = IDStrings>({
const defaultRating = useScoreRatingAlg(game, playtype);
const [rating, setRating] = useState(defaultRating);
const [rankingViewMode, setRankingViewMode] = useState<RankingViewMode>("global");
const headers: Header<RivalChartDataset[0]>[] = [
["#", "#", NumericSOV((x) => x.__related.index)],
@@ -39,11 +41,11 @@ export default function RivalChartTable<I extends IDStrings = IDStrings>({
setRating,
(x) => x.__related.pb
),
[
"Site Ranking",
"Site Rank",
NumericSOV((x) => x.__related.pb?.rankingData.rank ?? -Infinity),
],
CreateRankingHeader(
rankingViewMode,
setRankingViewMode,
(k) => k.__related.pb?.rankingData
),
["Last Raised", "Last Raised", NumericSOV((x) => x.__related.pb?.timeAchieved ?? 0)],
];
@@ -55,7 +57,14 @@ export default function RivalChartTable<I extends IDStrings = IDStrings>({
noTopDisplayStr
defaultSortMode="#"
rowFunction={(data) => (
<Row chart={chart} rating={rating} data={data} key={data.id} game={game} />
<Row
chart={chart}
rating={rating}
data={data}
key={data.id}
game={game}
rankingViewMode={rankingViewMode}
/>
)}
/>
);
@@ -66,11 +75,13 @@ function Row<I extends IDStrings = IDStrings>({
rating,
game,
chart,
rankingViewMode,
}: {
data: RivalChartDataset<I>[0];
game: Game;
rating: ScoreCalculatedDataLookup[I];
chart: ChartDocument;
rankingViewMode: RankingViewMode;
}) {
const pb = data.__related.pb;
@@ -98,7 +109,11 @@ function Row<I extends IDStrings = IDStrings>({
<IndexCell index={data.__related.index} />
<UserCell user={data} game={game} playtype={chart.playtype} />
<ScoreCoreCells score={pb} game={game} rating={rating} chart={chart} />
<RankingCell rankingData={pb.rankingData} />
<RankingCell
rankingData={pb.rankingData}
userID={pb.userID}
rankingViewMode={rankingViewMode}
/>
<TimestampCell time={pb.timeAchieved} />
</DropdownRow>
);
-11
View File
@@ -4,7 +4,6 @@ import fetchUGPTData, { UGPTData } from "components/util/query/fetchUGPTData";
import React, { createContext, useContext, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { JustChildren, SetState } from "types/react";
import { ErrorPage } from "app/pages/ErrorPage";
import { UserContext } from "./UserContext";
/**
@@ -33,7 +32,6 @@ export const UGPTContext = createContext<{
setLoggedInData: () => void 0,
viewingData: null,
setViewingData: () => void 0,
});
@@ -106,15 +104,6 @@ export function UGPTContextProvider({ children }: JustChildren) {
return <Loading />;
}
if (!viewingData) {
return (
<ErrorPage
statusCode={400}
customMessage="This user probably doesn't play this game, or something has gone wrong."
/>
);
}
return (
<UGPTContext.Provider
value={{
+5 -3
View File
@@ -67,6 +67,7 @@ export function CreateDefaultPBSearchParams<I extends IDStrings = IDStrings>(
score: (x) => x.scoreData.score,
percent: (x) => x.scoreData.percent,
ranking: (x) => x.rankingData.rank,
rivalRanking: (x) => x.rankingData.rivalRank,
highlight: (x) => !!x.highlight,
username: (x) => x.__related.user?.username ?? null,
lamp: {
@@ -98,9 +99,10 @@ export function CreateDefaultFolderSearchParams<I extends IDStrings = IDStrings>
title: (x) => x.__related.song.title,
difficulty: (x) => x.difficulty,
level: (x) => x.levelNum,
score: (x) => x.__related.pb?.scoreData.score ?? -Infinity,
percent: (x) => x.__related.pb?.scoreData.percent ?? -Infinity,
ranking: (x) => x.__related.pb?.rankingData.rank ?? -Infinity,
score: (x) => x.__related.pb?.scoreData.score ?? null,
percent: (x) => x.__related.pb?.scoreData.percent ?? null,
ranking: (x) => x.__related.pb?.rankingData.rank ?? null,
rivalRanking: (x) => x.__related.pb?.rankingData.rivalRank ?? null,
highlight: (x) => !!x.__related.pb?.highlight,
played: (x) => !!x.__related.pb,
lamp: {
+11 -1
View File
@@ -204,6 +204,16 @@ export function GetValueGetter<D>(v: ValueGetterOrHybrid<D>) {
return v.valueGetter;
}
function GetValueInsensitive<T>(record: Record<string, T>, key: string): T | undefined {
for (const [recKey, value] of Object.entries(record)) {
if (recKey.toLowerCase() === key.toLowerCase()) {
return value;
}
}
return undefined;
}
export function ComposeSearchFunction<D>(
valueGetters: Record<string, ValueGetterOrHybrid<D>>
): ZTableSearchFn<D> {
@@ -237,7 +247,7 @@ export function ComposeSearchFunction<D>(
}
for (const directive of directives) {
const vgOrHybrid = valueGetters[directive.key];
const vgOrHybrid = GetValueInsensitive(valueGetters, directive.key);
if (!vgOrHybrid) {
continue;
@@ -89,12 +89,13 @@ export async function UpdateChartRanking(game: Game, playtype: Playtype, chartID
for (const score of scores) {
rank++;
seenUserIDs.push(score.userID);
const thisUsersRivals = allRivals[score.userID];
let rivalRank: integer | null = null;
if (thisUsersRivals) {
if (thisUsersRivals && thisUsersRivals.length > 0) {
rivalRank = thisUsersRivals.filter((e) => seenUserIDs.includes(e)).length + 1;
}