@@ -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 (
+
+ );
+ }
+
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) ? (
) : (