diff --git a/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx b/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx index 8f859eea4..0328efefd 100644 --- a/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx +++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx @@ -41,7 +41,7 @@ export default function GPTLeaderboardsPage({ game, playtype }: GamePT) { - Score Leaderboards + PB Leaderboards @@ -53,7 +53,7 @@ export default function GPTLeaderboardsPage({ game, playtype }: GamePT) { )} diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderComparePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderComparePage.tsx new file mode 100644 index 000000000..d8658a8b7 --- /dev/null +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderComparePage.tsx @@ -0,0 +1,372 @@ +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, { useContext, useEffect, useMemo, useState } from "react"; +import { Button, Col, Form, Row } from "react-bootstrap"; +import { Link } from "react-router-dom"; +import { COLOUR_SET, FolderDocument, GetGamePTConfig, PublicUserDocument } from "tachi-common"; +import { UGPTFolderReturns, UGPTStatsReturn } from "types/api-returns"; +import { GamePT } from "types/react"; +import { ComparePBsDataset } from "types/tables"; +import UserSelectModal from "components/util/modal/UserSelectModal"; +import UserIcon from "components/util/UserIcon"; +import { UserContext } from "context/UserContext"; +import MiniTable from "components/tables/components/MiniTable"; + +export default function RivalCompareFolderPage({ + reqUser, + game, + playtype, + folder, +}: GamePT & { + reqUser: PublicUserDocument; + folder: FolderDocument; +}) { + const { settings } = useLUGPTSettings(); + const { user } = useContext(UserContext); + + const [selectedUser, setSelectedUser] = useState(null); + const [show, setShow] = useState(false); + + // honestly don't care if this errors or not + const { data, error } = useApiQuery>( + `/users/${settings?.userID}/games/${game}/${playtype}/rivals`, + undefined + // [settings?.rivals.join(",")] + ); + + if (!data && !error) { + return ; + } + + let suggestUsers = data ?? []; + + // if the user viewing this page is logged in and *is not* + // the person this page is for + // show them in the suggestions. + if (user && user.id !== reqUser.id && data) { + suggestUsers = [...suggestUsers, user]; + } + + return ( +
+ + {suggestUsers.length > 0 && ( + <> + + {suggestUsers.map((e) => ( + + + + ))} + + + + + )} + + { + setSelectedUser(user); + setShow(false); + }} + show={show} + setShow={setShow} + url={`/games/${game}/${playtype}/players`} + excludeSet={[reqUser.id]} + excludeMsg="Can't pick the same user!" + /> + + + {selectedUser !== null && folder !== null && ( + <> + + + + )} +
+ ); +} + +function FolderCompare({ + reqUser, + game, + playtype, + withUser, + folder, +}: { + reqUser: PublicUserDocument; + withUser: PublicUserDocument; + folder: FolderDocument; +} & GamePT) { + const gptConfig = GetGamePTConfig(game, playtype); + + const { data: baseData, error: baseError } = useApiQuery( + `/users/${reqUser.id}/games/${game}/${playtype}/folders/${folder.folderID}` + ); + + const { data: compareData, error: compareError } = useApiQuery( + `/users/${withUser.id}/games/${game}/${playtype}/folders/${folder.folderID}` + ); + + const [shouldIncludeNotPlayed, setShouldIncludeNotPlayed] = useState(false); + const [shouldESD, setShouldESD] = useState(gptConfig.supportsESD); + + useEffect(() => { + setShouldESD(gptConfig.supportsESD); + }, [gptConfig]); + + const dataset = useMemo(() => { + // i *LOVE* the rules of hooks! they're so convenient! + if (!baseData || !compareData) { + return []; + } + + const basePBLookup = new Map(baseData.pbs.map((e) => [e.chartID, e])); + const comparePBLookup = new Map(compareData.pbs.map((e) => [e.chartID, e])); + + const songMap = CreateSongMap(baseData.songs); + + let ds: ComparePBsDataset = baseData.charts.map((chart) => ({ + chart, + base: basePBLookup.get(chart.chartID) ?? null, + compare: comparePBLookup.get(chart.chartID) ?? null, + song: songMap.get(chart.songID)!, + })); + + if (!shouldIncludeNotPlayed) { + ds = ds.filter((e) => e.base && e.compare); + } + + return ds; + }, [shouldIncludeNotPlayed, baseData, compareData]); + + if (baseError) { + return ; + } + if (compareError) { + return ; + } + + if (!compareData || !baseData) { + return ; + } + + return ( + + + + + + + + + + setShouldIncludeNotPlayed(!shouldIncludeNotPlayed)} + label="Include charts without plays?" + /> + {gptConfig.supportsESD && ( + setShouldESD(!shouldESD)} + label="Sort score differences intelligently? (i.e. Treat the difference between 70% and 71% as smaller than the difference between 99% and 100%)" + /> + )} + + + + + ); +} + +function UserCard({ username, game, playtype }: { username: string } & GamePT) { + const { data, error } = useApiQuery( + `/users/${username}/games/${game}/${playtype}` + ); + + if (error) { + return ; + } + + return ( + + + + + + + +

+ + {username} + +

+ + + {data ? : } + +
+
+ + ); +} + +function CompareCard({ + dataset, + baseUsername, + otherUsername, +}: { + dataset: ComparePBsDataset; + + baseUsername: string; + otherUsername: string; +}) { + // this code sucks + let scoreWin = 0; + let scoreDraw = 0; + let scoreLoss = 0; + let lampWin = 0; + let lampDraw = 0; + let lampLoss = 0; + + for (const d of dataset) { + if (!d.compare && !d.base) { + scoreDraw++; + lampDraw++; + continue; + } + + if (!d.compare) { + scoreWin++; + lampWin++; + continue; + } + + if (!d.base) { + scoreLoss++; + lampLoss++; + continue; + } + + if (d.base.scoreData.percent > d.compare.scoreData.percent) { + scoreWin++; + } else if (d.base.scoreData.percent < d.compare.scoreData.percent) { + scoreLoss++; + } else { + scoreDraw++; + } + + if (d.base.scoreData.lampIndex > d.compare.scoreData.lampIndex) { + lampWin++; + } else if (d.base.scoreData.lampIndex < d.compare.scoreData.lampIndex) { + lampLoss++; + } else { + lampDraw++; + } + } + + return ( + + + + + + + {baseUsername} Win + + {scoreWin} + + + {((100 * scoreWin) / dataset.length).toFixed(2)}% + + + + Draw + {scoreDraw} + + {((100 * scoreDraw) / dataset.length).toFixed(2)}% + + + + {otherUsername} Win + {scoreLoss} + + {((100 * scoreLoss) / dataset.length).toFixed(2)}% + + + + + + + + {baseUsername} Win + + {lampWin} + + + {((100 * lampWin) / dataset.length).toFixed(2)}% + + + + Draw + {lampDraw} + + {((100 * lampDraw) / dataset.length).toFixed(2)}% + + + + {otherUsername} Loss + {lampLoss} + + {((100 * lampLoss) / dataset.length).toFixed(2)}% + + + + + + + + ); +} diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FoldersMainPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FoldersMainPage.tsx index ab7b8e111..9fef9893b 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FoldersMainPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FoldersMainPage.tsx @@ -71,10 +71,7 @@ export default function FoldersMainPage({ reqUser, game, playtype }: Props) { > - + diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx index c1dd0d1ad..b4f679b66 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx @@ -14,11 +14,13 @@ import Icon from "components/util/Icon"; import Loading from "components/util/Loading"; import Muted from "components/util/Muted"; import useApiQuery from "components/util/query/useApiQuery"; -import SelectButton from "components/util/SelectButton"; +import ReferToUser from "components/util/ReferToUser"; +import SelectLinkButton from "components/util/SelectLinkButton"; +import useUGPTBase from "components/util/useUGPTBase"; import { useFormik } from "formik"; import React, { useEffect, useMemo, useState } from "react"; import { Col, Form, Row } from "react-bootstrap"; -import { Link, useParams } from "react-router-dom"; +import { Link, Route, Switch, useParams } from "react-router-dom"; import { ChartDocument, ChartTierlistInfo, @@ -36,7 +38,7 @@ import { import { UGPTFolderReturns } from "types/api-returns"; import { FolderDataset } from "types/tables"; import { Playtype } from "types/tachi"; -import ReferToUser from "components/util/ReferToUser"; +import FolderComparePage from "./FolderComparePage"; interface Props { reqUser: PublicUserDocument; @@ -51,8 +53,6 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) { `/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}` ); - const [mode, setMode] = useState<"normal" | "ladder" | "timeline">("normal"); - const gptConfig = GetGamePTConfig(game, playtype); const folderDataset = useMemo(() => { @@ -97,6 +97,8 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) { ); }, [folderDataset]); + const base = `${useUGPTBase({ reqUser, game, playtype })}/folders/${folderID}`; + if (error) { return ; } @@ -113,47 +115,63 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
- + Normal View - + {gptConfig.tierlists.length !== 0 && // temp: tierlist view sucks for BMS and PMS game !== "bms" && game !== "pms" && ( - + Tierlist View - + )} - + Timeline View - + + + + Compare Against User +
- {mode === "normal" ? ( - - ) : mode === "ladder" ? ( - - ) : ( - - )} + + + + + + + + + + + + + +
); @@ -163,11 +181,17 @@ function TimelineView({ game, playtype, reqUser, folderID }: Props & { folderID: const gptConfig = GetGamePTConfig(game, playtype); const [type, setType] = useState<"lamp" | "grade">(gptConfig.scoreBucket); const [value, setValue] = useState( - type === "grade" ? gptConfig.grades.length - 1 : gptConfig.lamps.length - 1 + type === "grade" + ? gptConfig.grades.indexOf(gptConfig.clearGrade) + : gptConfig.lamps.indexOf(gptConfig.clearLamp) ); useEffect(() => { - setValue(type === "grade" ? gptConfig.grades.length - 1 : gptConfig.lamps.length - 1); + setValue( + type === "grade" + ? gptConfig.grades.indexOf(gptConfig.clearGrade) + : gptConfig.lamps.indexOf(gptConfig.clearLamp) + ); }, [type]); return ( diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx deleted file mode 100644 index 75cd0e1b1..000000000 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsComparePage.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import ScoreLeaderboard from "components/game/ScoreLeaderboard"; -import useSetSubheader from "components/layout/header/useSetSubheader"; -import Divider from "components/util/Divider"; -import Icon from "components/util/Icon"; -import SelectLinkButton from "components/util/SelectLinkButton"; -import useLUGPTSettings from "components/util/useLUGPTSettings"; -import useUGPTBase from "components/util/useUGPTBase"; -import React from "react"; -import { Col, Row } from "react-bootstrap"; -import { Route, Switch } from "react-router-dom"; -import { FormatGame, Game, GetGameConfig, Playtype, PublicUserDocument } from "tachi-common"; - -export default function RivalsComparePage({ - reqUser, - game, - playtype, -}: { - reqUser: PublicUserDocument; - game: Game; - playtype: Playtype; -}) { - const gameConfig = GetGameConfig(game); - - useSetSubheader( - ["Users", reqUser.username, "Games", gameConfig.name, playtype, "Rivals", "Compare"], - [reqUser, game, playtype], - `Comparing ${reqUser.username}'s ${FormatGame(game, playtype)} Rivals` - ); - - const base = `${useUGPTBase({ reqUser, game, playtype })}/rivals/compare`; - - const { settings } = useLUGPTSettings(); - - if (!settings) { - return
You have no settings. How did you get here?
; - } - - return ( - - -
- - - Compare Top 100s - - - - Best PBs - - - - Folder Comparisons - -
- - - - - - top100 - - - - - - - - folders - - - -
- ); -} diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsMainPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsMainPage.tsx index 6417ed2bc..e604add93 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsMainPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsMainPage.tsx @@ -1,3 +1,4 @@ +import ScoreLeaderboard from "components/game/ScoreLeaderboard"; import useSetSubheader from "components/layout/header/useSetSubheader"; import Divider from "components/util/Divider"; import Icon from "components/util/Icon"; @@ -8,7 +9,6 @@ import React from "react"; import { Col, Row } from "react-bootstrap"; import { Redirect, Route, Switch } from "react-router-dom"; import { FormatGame, Game, GetGameConfig, Playtype, PublicUserDocument } from "tachi-common"; -import RivalsComparePage from "./RivalsComparePage"; import RivalsManagePage from "./RivalsManagePage"; export default function RivalsMainPage({ @@ -40,21 +40,17 @@ export default function RivalsMainPage({
- - - Goals & Quests - - - - Compare + + + Rival's Bests Activity - + - Manage Folders + Manage Tracked Charts @@ -79,20 +75,14 @@ export default function RivalsMainPage({ - NOT IMPLEMENTED YET - - - - NOT IMPLEMENTED YET - - - - + diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsManagePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsManagePage.tsx index b90772aac..e1cb3dc98 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsManagePage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsManagePage.tsx @@ -202,6 +202,7 @@ function RivalsOverviewPage({ setShow={setShow} url={`/games/${game}/${playtype}/players`} excludeSet={rivals.map((e) => e.id)} + excludeMsg="Already Rivaled!" /> )} 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/cells/PBCompareCell.tsx b/client/src/components/tables/cells/PBCompareCell.tsx new file mode 100644 index 000000000..9a65d8233 --- /dev/null +++ b/client/src/components/tables/cells/PBCompareCell.tsx @@ -0,0 +1,146 @@ +import { FormatMillions } from "util/misc"; +import React from "react"; +import { COLOUR_SET, Game, PBScoreDocument, Playtype } from "tachi-common"; +import { ESDCompare } from "tachi-common/lib/esd"; +import Muted from "components/util/Muted"; + +export default function PBCompareCell({ + base, + compare, + game, + playtype, + compareType, + shouldESD, +}: { + base: PBScoreDocument | null; + compare: PBScoreDocument | null; + game: Game; + playtype: Playtype; + compareType: "score" | "lamp"; + shouldESD: boolean; +}) { + let status: "win" | "draw" | "lose"; + + // wut? + if (!base && !compare) { + status = "draw"; + } else if (!base) { + status = "lose"; + } else if (!compare) { + status = "win"; + } else if (compareType === "score") { + status = cmp(base.scoreData.score, compare.scoreData.score); + } else { + status = cmp(base.scoreData.lampIndex, compare.scoreData.lampIndex); + } + + let scoreRenderFn = (k: number) => k.toString(); + let shouldShowPercent = false; + + switch (game) { + case "chunithm": + case "ddr": + case "gitadora": + case "maimai": + case "usc": + case "sdvx": + case "popn": + case "wacca": + case "museca": + scoreRenderFn = FormatMillions; + shouldShowPercent = false; + break; + case "jubeat": + scoreRenderFn = FormatMillions; + shouldShowPercent = true; + break; + case "bms": + case "iidx": + scoreRenderFn = (k: number) => k.toString(); + shouldShowPercent = true; + break; + } + + const fmtCmpPercent = fmtNum( + (base?.scoreData.percent ?? 0) - (compare?.scoreData.percent ?? 0), + 2, + "%" + ); + const cmpScore = (base?.scoreData.score ?? 0) - (compare?.scoreData.score ?? 0); + const cmpLamp = (base?.scoreData.lampIndex ?? 0) - (compare?.scoreData.lampIndex ?? 0); + + let esdDiff; + + if (shouldESD) { + if (base?.scoreData.esd && compare?.scoreData.esd) { + esdDiff = ESDCompare(compare.scoreData.esd, base.scoreData.esd); + } + } + + return ( + + {compareType === "score" ? ( + <> + {(cmpScore > 0 ? "+" : "") + scoreRenderFn(cmpScore)} + {shouldShowPercent && ( + <> +
+ ({fmtCmpPercent}) + + )} + {esdDiff !== undefined && ( + <> +
+ {esdDiff.toFixed(2) === "0.00" ? ( + Negligable Difference + ) : ( + + Relative: {(esdDiff > 0 ? "+" : "") + esdDiff.toFixed(2)} + + )} + + )} + + ) : ( + <> + {(cmpLamp > 0 ? "+" : "") + cmpLamp} + + )} + + ); +} + +function cmp(a: number, b: number) { + if (a === b) { + return "draw"; + } else if (a > b) { + return "win"; + } + + return "lose"; +} + +function fmtNum(num: number, fixed?: number, suffix?: string) { + let str = fixed ? num.toFixed(fixed) : num.toString(); + + if (num >= 0) { + str = `+${str}`; + } + + if (suffix) { + str += suffix; + } + + return str; +} diff --git a/client/src/components/tables/components/SelectableCompareType.tsx b/client/src/components/tables/components/SelectableCompareType.tsx new file mode 100644 index 000000000..c2bcee02b --- /dev/null +++ b/client/src/components/tables/components/SelectableCompareType.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { SetState } from "types/react"; +import { ZTableTHProps } from "./TachiTable"; + +export default function SelectableCompareType({ + compareType, + setCompareType, + changeSort, + currentSortMode, + reverseSort, +}: { + compareType: "score" | "lamp"; + setCompareType: SetState<"score" | "lamp">; +} & ZTableTHProps) { + return ( + + +
+ changeSort("Vs.")}> + + + + + ); +} 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..80d3c0857 --- /dev/null +++ b/client/src/components/tables/rivals/ComparePBsTable.tsx @@ -0,0 +1,127 @@ +import { NumericSOV, StrSOV } from "util/sorts"; +import { CreatePBCompareSearchParams } from "util/tables/create-search"; +import React, { useState } 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 PBCompareCell from "../cells/PBCompareCell"; +import TitleCell from "../cells/TitleCell"; +import SelectableCompareType from "../components/SelectableCompareType"; +import TachiTable, { Header, ZTableTHProps } from "../components/TachiTable"; +import ShortScoreCoreCells from "../game-core-cells/ShortScoreCoreCells"; +import ChartHeader from "../headers/ChartHeader"; + +export default function ComparePBsTable({ + dataset, + game, + playtype, + baseUser, + compareUser, + shouldESD, +}: { + dataset: ComparePBsDataset; + game: Game; + playtype: Playtype; + baseUser: string; + compareUser: string; + shouldESD: boolean; +}) { + const [compareType, setCompareType] = useState<"score" | "lamp">("score"); + + 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 (compareType === "score") { + if (shouldESD && x.base.scoreData.esd && x.compare.scoreData.esd) { + // esd is inverted, whoops + const e = ESDCompare(x.compare.scoreData.esd, x.base.scoreData.esd); + + if (e !== 0) { + return e; + } + } + + return x.base.scoreData.percent - x.compare.scoreData.percent; + } + + return x.base.scoreData.lampIndex - x.compare.scoreData.lampIndex; + }), + (thProps: ZTableTHProps) => ( + + ), + ], + ["", "", () => 1, () => {compareUser}], + ]; + + return ( + ( + + )} + /> + ); +} + +function Row({ + data, + game, + compareType, + shouldESD, +}: { + data: ComparePBsDataset[0]; + game: Game; + compareType: "score" | "lamp"; + shouldESD: boolean; +}) { + return ( + + + + {data.base ? ( + + ) : ( + Not Played + )} + + {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 ( Leaderboard , - - Rivals - , - - Goals & Quests - , ]; if (isRequestedUser) { navItems.push( + + Rivals + , + + Goals & Quests + , Settings diff --git a/client/src/components/util/UserIcon.tsx b/client/src/components/util/UserIcon.tsx index 7962515db..3d094b340 100644 --- a/client/src/components/util/UserIcon.tsx +++ b/client/src/components/util/UserIcon.tsx @@ -9,7 +9,7 @@ export default function UserIcon({ children, game, playtype, -}: { user: PublicUserDocument } & JustChildren & Partial) { +}: { user: PublicUserDocument } & Partial & Partial) { return (
@@ -25,7 +25,7 @@ export default function UserIcon({ {user.username} -
{children}
+ {children &&
{children}
}
); } diff --git a/client/src/components/util/modal/UserSelectModal.tsx b/client/src/components/util/modal/UserSelectModal.tsx index c1783fddb..a5e2cb2b4 100644 --- a/client/src/components/util/modal/UserSelectModal.tsx +++ b/client/src/components/util/modal/UserSelectModal.tsx @@ -1,10 +1,9 @@ import { APIFetchV1 } from "util/api"; import ProfilePicture from "components/user/ProfilePicture"; -import React, { useContext, useEffect, useState } from "react"; +import React, { useEffect, useState } from "react"; import { Button, Col, Modal, Row } from "react-bootstrap"; import { integer, PublicUserDocument } from "tachi-common"; import { SetState } from "types/react"; -import { UserContext } from "context/UserContext"; import DebounceSearch from "../DebounceSearch"; import Divider from "../Divider"; @@ -14,12 +13,14 @@ export default function UserSelectModal({ setShow, url = `/users`, excludeSet = [], + excludeMsg = "N/A", }: { show: boolean; setShow: SetState; callback: (user: PublicUserDocument) => void; url?: string; excludeSet?: Array; + excludeMsg?: string; }) { const [search, setSearch] = useState(""); const [users, setUsers] = useState | null>(null); @@ -67,7 +68,7 @@ export default function UserSelectModal({

{user.username}

{excludeSet.includes(user.id) ? ( ) : (