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 ( + {`${user}'s + ); + } + 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],