diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx
index 72e05ca08..03cad597e 100644
--- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx
+++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalCompareTop100Page.tsx
@@ -1,9 +1,20 @@
-import DebugContent from "components/util/DebugContent";
+import { CreateSongMap } from "util/data";
+import Card from "components/layout/page/Card";
+import ComparePBsTable from "components/tables/rivals/ComparePBsTable";
+import ProfilePicture from "components/user/ProfilePicture";
+import UGPTRatingsTable from "components/user/UGPTStatsOverview";
+import ApiError from "components/util/ApiError";
+import Divider from "components/util/Divider";
+import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import useLUGPTSettings from "components/util/useLUGPTSettings";
-import React from "react";
+import React, { useState } from "react";
+import { Col, Form, Row } from "react-bootstrap";
+import { Link } from "react-router-dom";
import { PublicUserDocument } from "tachi-common";
+import { BestPBsUnion, UGPTStatsReturn } from "types/api-returns";
import { GamePT } from "types/react";
+import { ComparePBsDataset } from "types/tables";
export default function RivalCompareTop100Page({
reqUser,
@@ -18,13 +29,136 @@ export default function RivalCompareTop100Page({
return
You have no settings. How did you get here?
;
}
- const { data, error } = useApiQuery(
- `/users/${reqUser.id}/games/${game}/${playtype}/pbs/best-union?withUser=nythil`
+ const [selectedUser, setSelectedUser] = useState(null);
+
+ const { data, error } = useApiQuery>(
+ `/users/${reqUser.id}/games/${game}/${playtype}/rivals`,
+ undefined,
+ [settings.rivals.join(",")]
);
+ if (error) {
+ return ;
+ }
+
+ if (!data) {
+ return ;
+ }
+
return (
-
+
+ setSelectedUser(e.target.value || null)}
+ >
+
+ {data.map((e) => (
+
+ ))}
+
+ Select a rival to compare your best 100 PBs with.
+
+ {selectedUser !== null && (
+ <>
+
+
+ >
+ )}
);
}
+
+function RivalBest100Compare({
+ reqUser,
+ game,
+ playtype,
+ withUser,
+}: {
+ reqUser: PublicUserDocument;
+ withUser: string;
+} & GamePT) {
+ const { data, error } = useApiQuery(
+ `/users/${reqUser.id}/games/${game}/${playtype}/pbs/best-union?withUser=${withUser}`
+ );
+
+ if (error) {
+ return ;
+ }
+
+ if (!data) {
+ return ;
+ }
+
+ const basePBLookup = new Map(data.baseUserPBs.map((e) => [e.chartID, e]));
+ const comparePBLookup = new Map(data.withUserPBs.map((e) => [e.chartID, e]));
+
+ const songMap = CreateSongMap(data.songs);
+
+ const dataset: ComparePBsDataset = data.charts.map((chart) => ({
+ chart,
+ base: basePBLookup.get(chart.chartID) ?? null,
+ compare: comparePBLookup.get(chart.chartID) ?? null,
+ song: songMap.get(chart.songID)!,
+ }));
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
+
+function UserCard({ username, game, playtype }: { username: string } & GamePT) {
+ const { data, error } = useApiQuery(
+ `/users/${username}/games/${game}/${playtype}`
+ );
+
+ if (error) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {username}
+
+
+
+
+ {data ? : }
+
+
+
+
+ );
+}
diff --git a/client/src/components/layout/page/Card.tsx b/client/src/components/layout/page/Card.tsx
index 7d288b726..b7ce6228d 100644
--- a/client/src/components/layout/page/Card.tsx
+++ b/client/src/components/layout/page/Card.tsx
@@ -8,20 +8,22 @@ export default function Card({
className,
style,
}: {
- header: JSX.Element | string;
+ header?: JSX.Element | string;
footer?: JSX.Element | string;
className?: string;
style?: CSSProperties;
} & JustChildren) {
return (
-
- {typeof header === "string" ? (
-
{header}
- ) : (
- header
- )}
-
+ {header && (
+
+ {typeof header === "string" ? (
+
{header}
+ ) : (
+ header
+ )}
+
+ )}
{children}
{footer &&
{footer}
}
diff --git a/client/src/components/tables/cells/JubeatScoreCell.tsx b/client/src/components/tables/cells/JubeatScoreCell.tsx
new file mode 100644
index 000000000..0584527eb
--- /dev/null
+++ b/client/src/components/tables/cells/JubeatScoreCell.tsx
@@ -0,0 +1,22 @@
+import { ChangeOpacity } from "util/color-opacity";
+import { FormatMillions } from "util/misc";
+import React from "react";
+import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
+
+export default function JubeatScoreCell({ sc }: { sc: ScoreDocument | PBScoreDocument }) {
+ const gptConfig = GetGamePTConfig(sc.game, sc.playtype);
+
+ return (
+
+ {sc.scoreData.grade}
+
+ {sc.scoreData.percent.toFixed(2)}%
+
+ {FormatMillions(sc.scoreData.score)}
+ |
+ );
+}
diff --git a/client/src/components/tables/game-core-cells/CHUNITHMCoreCells.tsx b/client/src/components/tables/game-core-cells/CHUNITHMCoreCells.tsx
index 6ffa03391..e1eef2267 100644
--- a/client/src/components/tables/game-core-cells/CHUNITHMCoreCells.tsx
+++ b/client/src/components/tables/game-core-cells/CHUNITHMCoreCells.tsx
@@ -1,9 +1,8 @@
-import { ChangeOpacity } from "util/color-opacity";
-import { FormatMillions } from "util/misc";
import React from "react";
-import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
+import { PBScoreDocument, ScoreDocument } from "tachi-common";
import CHUNITHMJudgementCell from "../cells/CHUNITHMJudgementCell";
import LampCell from "../cells/LampCell";
+import MillionsScoreCell from "../cells/MillionsScoreCell";
import RatingCell from "../cells/RatingCell";
export default function CHUNITHMCoreCells({
@@ -13,19 +12,9 @@ export default function CHUNITHMCoreCells({
sc: ScoreDocument<"chunithm:Single"> | PBScoreDocument<"chunithm:Single">;
rating: keyof ScoreDocument["calculatedData"];
}) {
- const gptConfig = GetGamePTConfig(sc.game, sc.playtype);
-
return (
<>
-
- {sc.scoreData.grade}
-
- {FormatMillions(sc.scoreData.score)}
- |
+
diff --git a/client/src/components/tables/game-core-cells/JubeatCoreCells.tsx b/client/src/components/tables/game-core-cells/JubeatCoreCells.tsx
index 409e7f7d5..40009dc05 100644
--- a/client/src/components/tables/game-core-cells/JubeatCoreCells.tsx
+++ b/client/src/components/tables/game-core-cells/JubeatCoreCells.tsx
@@ -1,8 +1,7 @@
-import { ChangeOpacity } from "util/color-opacity";
-import { FormatMillions } from "util/misc";
import React from "react";
-import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
+import { PBScoreDocument, ScoreDocument } from "tachi-common";
import JubeatJudgementCell from "../cells/JubeatJudgementCell";
+import JubeatScoreCell from "../cells/JubeatScoreCell";
import LampCell from "../cells/LampCell";
import RatingCell from "../cells/RatingCell";
@@ -13,21 +12,9 @@ export default function JubeatCoreCells({
sc: ScoreDocument<"jubeat:Single"> | PBScoreDocument<"jubeat:Single">;
rating: keyof ScoreDocument["calculatedData"];
}) {
- const gptConfig = GetGamePTConfig(sc.game, sc.playtype);
-
return (
<>
-
- {sc.scoreData.grade}
-
- {sc.scoreData.percent.toFixed(2)}%
-
- {FormatMillions(sc.scoreData.score)}
- |
+
diff --git a/client/src/components/tables/game-core-cells/ShortScoreCoreCells.tsx b/client/src/components/tables/game-core-cells/ShortScoreCoreCells.tsx
new file mode 100644
index 000000000..d24fc6485
--- /dev/null
+++ b/client/src/components/tables/game-core-cells/ShortScoreCoreCells.tsx
@@ -0,0 +1,84 @@
+import IIDXLampCell from "components/tables/cells/IIDXLampCell";
+import JubeatScoreCell from "components/tables/cells/JubeatScoreCell";
+import LampCell from "components/tables/cells/LampCell";
+import MillionsScoreCell from "components/tables/cells/MillionsScoreCell";
+import PopnLampCell from "components/tables/cells/PopnLampCell";
+import ScoreCell from "components/tables/cells/ScoreCell";
+import SDVXLampCell from "components/tables/cells/SDVXLampCell";
+import React from "react";
+import { ChartDocument, Game, PBScoreDocument, ScoreDocument } from "tachi-common";
+import BMSOrPMSLampCell from "../cells/BMSOrPMSLampCell";
+import BMSCoreCells from "./BMSCoreCells";
+
+export default function ShortScoreCoreCells({
+ game,
+ score,
+ chart,
+}: {
+ score: ScoreDocument | PBScoreDocument;
+ chart: ChartDocument;
+ game: Game;
+}) {
+ const sc = score as any; // lazy hack
+
+ if (game === "iidx") {
+ return (
+ <>
+
+ } />
+ >
+ );
+ } else if (game === "bms" || game === "pms") {
+ return (
+ <>
+
+
+ >
+ );
+ } else if (
+ game === "sdvx" ||
+ game === "usc" ||
+ game === "museca" ||
+ game === "wacca" ||
+ game === "chunithm" ||
+ game === "ddr" ||
+ game === "gitadora"
+ ) {
+ return (
+ <>
+
+
+ >
+ );
+ } else if (game === "maimai") {
+ return (
+ <>
+
+
+ >
+ );
+ } else if (game === "jubeat") {
+ return (
+ <>
+
+
+ >
+ );
+ } else if (game === "popn") {
+ return (
+ <>
+
+
+ >
+ );
+ } else if (game === "itg") {
+ return (
+ <>
+
+
+ >
+ );
+ }
+
+ return <>No cells defined for {game}. Report this!>;
+}
diff --git a/client/src/components/tables/headers/GameHeaders.tsx b/client/src/components/tables/headers/GameHeaders.tsx
index 76a3ab57a..08e11cf2a 100644
--- a/client/src/components/tables/headers/GameHeaders.tsx
+++ b/client/src/components/tables/headers/GameHeaders.tsx
@@ -10,13 +10,19 @@ import {
ScoreDocument,
} from "tachi-common";
import { SetState } from "types/react";
-import { FolderDataset, PBDataset, RivalChartDataset, ScoreDataset } from "types/tables";
+import {
+ ComparePBsDataset,
+ FolderDataset,
+ PBDataset,
+ RivalChartDataset,
+ ScoreDataset,
+} from "types/tables";
import { Playtype } from "types/tachi";
import SelectableRating from "../components/SelectableRating";
import { Header, ZTableTHProps } from "../components/TachiTable";
export function GetGPTCoreHeaders<
- Dataset extends FolderDataset | PBDataset | ScoreDataset | RivalChartDataset
+ Dataset extends FolderDataset | PBDataset | ScoreDataset | RivalChartDataset | ComparePBsDataset
>(
game: Game,
playtype: Playtype,
diff --git a/client/src/components/tables/rivals/ComparePBsTable.tsx b/client/src/components/tables/rivals/ComparePBsTable.tsx
new file mode 100644
index 000000000..f7a52a016
--- /dev/null
+++ b/client/src/components/tables/rivals/ComparePBsTable.tsx
@@ -0,0 +1,87 @@
+import { NumericSOV, StrSOV } from "util/sorts";
+import React from "react";
+import { Game, IDStrings, Playtype } from "tachi-common";
+import { ESDCompare } from "tachi-common/lib/esd";
+import { ComparePBsDataset } from "types/tables";
+import DifficultyCell from "../cells/DifficultyCell";
+import TitleCell from "../cells/TitleCell";
+import TachiTable, { Header } from "../components/TachiTable";
+import ShortScoreCoreCells from "../game-core-cells/ShortScoreCoreCells";
+import ChartHeader from "../headers/ChartHeader";
+
+export default function ComparePBsTable({
+ dataset,
+ game,
+ baseUser,
+ compareUser,
+}: {
+ dataset: ComparePBsDataset;
+ game: Game;
+ playtype: Playtype;
+ baseUser: string;
+ compareUser: string;
+}) {
+ const headers: Header[] = [
+ // ["#", "#"],
+ ChartHeader(game, (d) => d.chart),
+ ["Song", "Song", StrSOV((x) => x.song.title)],
+ ["", "", () => 1, () => {baseUser} | ],
+ [
+ "Vs.",
+ "Vs.",
+ NumericSOV((x) => {
+ if (!x.base) {
+ return -Infinity;
+ }
+
+ if (!x.compare) {
+ return Infinity;
+ }
+
+ if (x.base.scoreData.esd && x.compare.scoreData.esd) {
+ return ESDCompare(x.base.scoreData.esd, x.compare.scoreData.esd);
+ }
+
+ return x.base.scoreData.percent - x.compare.scoreData.percent;
+ }),
+ ],
+ ["", "", () => 1, () => {compareUser} | ],
+ ];
+
+ return (
+
}
+ />
+ );
+}
+
+function Row({
+ data,
+ game,
+}: {
+ data: ComparePBsDataset[0];
+ game: Game;
+}) {
+ return (
+
+
+
+ {data.base ? (
+
+ ) : (
+ | Not Played |
+ )}
+ some comparison thing here |
+ {data.compare ? (
+
+ ) : (
+ Not Played |
+ )}
+
+ );
+}
diff --git a/client/src/components/user/ProfilePicture.tsx b/client/src/components/user/ProfilePicture.tsx
index b9d5372eb..be8722506 100644
--- a/client/src/components/user/ProfilePicture.tsx
+++ b/client/src/components/user/ProfilePicture.tsx
@@ -2,7 +2,28 @@ import { ToAPIURL } from "util/api";
import React from "react";
import { PublicUserDocument } from "tachi-common";
-export default function ProfilePicture({ user, src }: { user: PublicUserDocument; src?: string }) {
+export default function ProfilePicture({
+ user,
+ src,
+}: {
+ user: PublicUserDocument | string;
+ src?: string;
+}) {
+ if (typeof user === "string") {
+ return (
+
+ );
+ }
+
return (
= (PublicUserDocu
index: number;
};
})[];
+
+export type ComparePBsDataset = Array<{
+ base: (PBScoreDocument & { __personalRanking: integer }) | null;
+ compare: (PBScoreDocument & { __personalRanking: integer }) | null;
+ chart: ChartDocument;
+ song: SongDocument;
+}>;
diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.ts
index b159e21f0..05f244f56 100644
--- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.ts
+++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/pbs/router.ts
@@ -238,7 +238,7 @@ router.get("/best-union", prValidate({ alg: "*string", withUser: "string" }), as
// then fetch both of their scores on each.
const baseUserPBs = await GetPBsWithUserRankings(user.id, chartIDs, alg);
- const withUserPBs = await GetPBsWithUserRankings(user.id, chartIDs, alg);
+ const withUserPBs = await GetPBsWithUserRankings(otherUser.id, chartIDs, alg);
const { songs, charts } = await GetRelevantSongsAndCharts(
[...baseUserPBs, ...withUserPBs],