feat: initial comparePBsTable draft

This commit is contained in:
zkldi
2022-11-01 00:59:19 +00:00
parent 73f929b3e4
commit faf4ff16ed
13 changed files with 394 additions and 48 deletions
@@ -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 <div>You have no settings. How did you get here?</div>;
}
const { data, error } = useApiQuery(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/best-union?withUser=nythil`
const [selectedUser, setSelectedUser] = useState<string | null>(null);
const { data, error } = useApiQuery<Array<PublicUserDocument>>(
`/users/${reqUser.id}/games/${game}/${playtype}/rivals`,
undefined,
[settings.rivals.join(",")]
);
if (error) {
return <ApiError error={error} />;
}
if (!data) {
return <Loading />;
}
return (
<div>
<DebugContent data={data} />
<Card header="Rival Select">
<Form.Control
as="select"
value={selectedUser ?? ""}
onChange={(e) => setSelectedUser(e.target.value || null)}
>
<option value="">Please Select...</option>
{data.map((e) => (
<option key={e.username} value={e.username}>
{e.username}
</option>
))}
</Form.Control>
<div className="form-text">Select a rival to compare your best 100 PBs with.</div>
</Card>
{selectedUser !== null && (
<>
<Divider />
<RivalBest100Compare
reqUser={reqUser}
game={game}
playtype={playtype}
withUser={selectedUser}
/>
</>
)}
</div>
);
}
function RivalBest100Compare({
reqUser,
game,
playtype,
withUser,
}: {
reqUser: PublicUserDocument;
withUser: string;
} & GamePT) {
const { data, error } = useApiQuery<BestPBsUnion>(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/best-union?withUser=${withUser}`
);
if (error) {
return <ApiError error={error} />;
}
if (!data) {
return <Loading />;
}
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 (
<Row>
<UserCard username={reqUser.username} game={game} playtype={playtype} />
<UserCard username={withUser} game={game} playtype={playtype} />
<Col xs={12}>
<Divider />
</Col>
<ComparePBsTable
baseUser={reqUser.username}
compareUser={withUser}
dataset={dataset}
game={game}
playtype={playtype}
/>
</Row>
);
}
function UserCard({ username, game, playtype }: { username: string } & GamePT) {
const { data, error } = useApiQuery<UGPTStatsReturn>(
`/users/${username}/games/${game}/${playtype}`
);
if (error) {
return <ApiError error={error} />;
}
return (
<Col xs={12} lg={6}>
<Card>
<Row className="align-items-center">
<Col lg={3}>
<ProfilePicture user={username} />
</Col>
<Col lg={3}>
<h4>
<Link
className="gentle-link"
to={`/dashboard/users/${username}/games/${game}/${playtype}`}
>
{username}
</Link>
</h4>
</Col>
<Col lg={6}>
{data ? <UGPTRatingsTable ugs={data.gameStats} /> : <Loading />}
</Col>
</Row>
</Card>
</Col>
);
}
+10 -8
View File
@@ -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 (
<div className={`card card-custom ${className ? className : ""}`} style={style}>
<div className="card-header">
{typeof header === "string" ? (
<h3 className="text-center mb-0">{header}</h3>
) : (
header
)}
</div>
{header && (
<div className="card-header">
{typeof header === "string" ? (
<h3 className="text-center mb-0">{header}</h3>
) : (
header
)}
</div>
)}
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
@@ -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 (
<td
style={{
backgroundColor: ChangeOpacity(gptConfig.gradeColours[sc.scoreData.grade], 0.2),
}}
>
<strong>{sc.scoreData.grade}</strong>
<br />
<b>{sc.scoreData.percent.toFixed(2)}%</b>
<br />
{FormatMillions(sc.scoreData.score)}
</td>
);
}
@@ -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 (
<>
<td
style={{
backgroundColor: ChangeOpacity(gptConfig.gradeColours[sc.scoreData.grade], 0.2),
}}
>
<strong>{sc.scoreData.grade}</strong>
<br />
{FormatMillions(sc.scoreData.score)}
</td>
<MillionsScoreCell score={sc} />
<CHUNITHMJudgementCell score={sc} />
<LampCell score={sc} />
<RatingCell score={sc} rating={rating} />
@@ -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 (
<>
<td
style={{
backgroundColor: ChangeOpacity(gptConfig.gradeColours[sc.scoreData.grade], 0.2),
}}
>
<strong>{sc.scoreData.grade}</strong>
<br />
<b>{sc.scoreData.percent.toFixed(2)}%</b>
<br />
{FormatMillions(sc.scoreData.score)}
</td>
<JubeatScoreCell sc={sc} />
<JubeatJudgementCell score={sc} />
<LampCell score={sc} />
<RatingCell score={sc} rating={rating} />
@@ -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 (
<>
<ScoreCell score={sc} />
<IIDXLampCell sc={sc} chart={chart as ChartDocument<"iidx:SP" | "iidx:DP">} />
</>
);
} else if (game === "bms" || game === "pms") {
return (
<>
<ScoreCell score={sc} />
<BMSOrPMSLampCell score={sc} />
</>
);
} else if (
game === "sdvx" ||
game === "usc" ||
game === "museca" ||
game === "wacca" ||
game === "chunithm" ||
game === "ddr" ||
game === "gitadora"
) {
return (
<>
<MillionsScoreCell score={sc} />
<SDVXLampCell score={sc} />
</>
);
} else if (game === "maimai") {
return (
<>
<ScoreCell score={sc} />
<LampCell score={sc} />
</>
);
} else if (game === "jubeat") {
return (
<>
<JubeatScoreCell sc={sc} />
<LampCell score={sc} />
</>
);
} else if (game === "popn") {
return (
<>
<MillionsScoreCell score={sc} />
<PopnLampCell score={sc} />
</>
);
} else if (game === "itg") {
return (
<>
<ScoreCell showScore={false} score={sc} />
<LampCell score={sc} />
</>
);
}
return <>No cells defined for {game}. Report this!</>;
}
@@ -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,
@@ -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<I extends IDStrings = IDStrings>({
dataset,
game,
baseUser,
compareUser,
}: {
dataset: ComparePBsDataset<I>;
game: Game;
playtype: Playtype;
baseUser: string;
compareUser: string;
}) {
const headers: Header<ComparePBsDataset[0]>[] = [
// ["#", "#"],
ChartHeader(game, (d) => d.chart),
["Song", "Song", StrSOV((x) => x.song.title)],
["", "", () => 1, () => <td colSpan={2}>{baseUser}</td>],
[
"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, () => <td colSpan={2}>{compareUser}</td>],
];
return (
<TachiTable
dataset={dataset}
headers={headers}
entryName="PBs"
noTopDisplayStr
defaultSortMode="#"
rowFunction={(data) => <Row data={data} game={game} />}
/>
);
}
function Row<I extends IDStrings = IDStrings>({
data,
game,
}: {
data: ComparePBsDataset<I>[0];
game: Game;
}) {
return (
<tr>
<DifficultyCell chart={data.chart} game={game} alwaysShort />
<TitleCell game={game} song={data.song} chart={data.chart} />
{data.base ? (
<ShortScoreCoreCells score={data.base} game={game} chart={data.chart} />
) : (
<td colSpan={2}>Not Played</td>
)}
<td>some comparison thing here</td>
{data.compare ? (
<ShortScoreCoreCells score={data.compare} game={game} chart={data.chart} />
) : (
<td colSpan={2}>Not Played</td>
)}
</tr>
);
}
+22 -1
View File
@@ -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 (
<img
src={src ? src : ToAPIURL(`/users/${user}/pfp`)}
alt={`${user}'s Profile Picture`}
className="rounded"
style={{
width: "128px",
height: "128px",
boxShadow: "0px 0px 10px 0px #000000",
}}
/>
);
}
return (
<img
src={src ? src : ToAPIURL(`/users/${user.id}/pfp`)}
+1 -1
View File
@@ -4,7 +4,7 @@ import NavItem from "components/nav/NavItem";
import MiniTable from "components/tables/components/MiniTable";
import Divider from "components/util/Divider";
import React from "react";
import { Game, GetGamePTConfig, PublicUserDocument } from "tachi-common";
import { Game, PublicUserDocument } from "tachi-common";
import { UGPTStatsReturn } from "types/api-returns";
import { Playtype } from "types/tachi";
import ProfileBadges from "./ProfileBadges";
+7
View File
@@ -201,3 +201,10 @@ export interface ImportIDReturn {
import: ImportDocument;
user: PublicUserDocument;
}
export interface BestPBsUnion {
baseUserPBs: (PBScoreDocument & { __personalRanking: integer })[];
withUserPBs: (PBScoreDocument & { __personalRanking: integer })[];
songs: SongDocument[];
charts: ChartDocument[];
}
+7
View File
@@ -56,3 +56,10 @@ export type RivalChartDataset<I extends IDStrings = IDStrings> = (PublicUserDocu
index: number;
};
})[];
export type ComparePBsDataset<I extends IDStrings = IDStrings> = Array<{
base: (PBScoreDocument<I> & { __personalRanking: integer }) | null;
compare: (PBScoreDocument<I> & { __personalRanking: integer }) | null;
chart: ChartDocument;
song: SongDocument;
}>;
@@ -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],