mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-10 08:18:12 +03:00
feat: initial comparePBsTable draft
This commit is contained in:
+139
-5
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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`)}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user