GPTCharts Update

This commit is contained in:
zkldi
2021-09-22 12:25:16 +01:00
parent f35b99649c
commit 8b3820f08c
33 changed files with 887 additions and 144 deletions
@@ -0,0 +1,325 @@
import useSetSubheader from "components/layout/header/useSetSubheader";
import Card from "components/layout/page/Card";
import LampCell from "components/tables/cells/LampCell";
import ScoreCell from "components/tables/cells/ScoreCell";
import MiniTable from "components/tables/components/MiniTable";
import PBTable from "components/tables/pbs/PBTable";
import ProfilePicture from "components/user/ProfilePicture";
import ApiError from "components/util/ApiError";
import Divider from "components/util/Divider";
import Icon from "components/util/Icon";
import Loading from "components/util/Loading";
import SelectButton from "components/util/SelectButton";
import { UserContext } from "context/UserContext";
import React, { useContext, useMemo, useState } from "react";
import { Col, Row } from "react-bootstrap";
import { useQuery } from "react-query";
import { Link, useParams } from "react-router-dom";
import {
ChartDocument,
FormatDifficulty,
GamePTConfig,
GetGameConfig,
GetGamePTConfig,
integer,
PBScoreDocument,
PublicUserDocument,
SongDocument,
} from "tachi-common";
import { ChartPBLeaderboardReturn, UGPTChartLeaderboardAdjacent } from "types/api-returns";
import { GamePT, SetState } from "types/react";
import { PBDataset } from "types/tables";
import { APIFetchV1, UnsuccessfulAPIFetchResponse } from "util/api";
import { CreateUserMap } from "util/data";
// This component forms a wrapper around the Real GPT Chart Page
// which handles the case where activeChart == null.
export default function GPTChartPage({
chart,
setActiveChart,
game,
song,
playtype,
allCharts,
}: {
song: SongDocument;
chart: ChartDocument | null;
setActiveChart: SetState<ChartDocument | null>;
allCharts: ChartDocument[];
} & GamePT) {
const { difficulty } = useParams<{ difficulty: string }>();
const gptConfig = GetGamePTConfig(game, playtype);
const formatSongTitle = `${song.artist} - ${song.title}`;
const formatDiff = chart ? FormatDifficulty(chart, game) : "Loading...";
useSetSubheader(
["Games", GetGameConfig(game).name, playtype, "Songs", formatSongTitle, formatDiff],
[game, playtype, chart],
`${formatSongTitle} (${formatDiff})`
);
if (!chart) {
setActiveChart(SelectRightChart(gptConfig, difficulty, allCharts));
return <Loading />;
}
return <InternalGPTChartPage chart={chart} game={game} song={song} playtype={playtype} />;
}
interface ChartPBData {
leaderboard: ChartPBLeaderboardReturn;
adjacent?: UGPTChartLeaderboardAdjacent;
playcount: integer;
}
function InternalGPTChartPage({
chart,
game,
song,
playtype,
}: {
song: SongDocument;
chart: ChartDocument;
} & GamePT) {
const { user } = useContext(UserContext);
const [mode, setMode] = useState<"leaderboard" | "adjacent">("leaderboard");
const { data, isLoading, error } = useQuery<ChartPBData, UnsuccessfulAPIFetchResponse>(
["PBInfo", `${chart.chartID}`],
async () => {
const lRes = await APIFetchV1<ChartPBLeaderboardReturn>(
`/games/${game}/${playtype}/charts/${chart.chartID}/pbs`
);
if (!lRes.success) {
throw lRes;
}
const pRes = await APIFetchV1<{ count: integer }>(
`/games/${game}/${playtype}/charts/${chart.chartID}/playcount`
);
if (!pRes.success) {
throw pRes;
}
if (user) {
const nRes = await APIFetchV1<UGPTChartLeaderboardAdjacent>(
`/users/${user.id}/games/${game}/${playtype}/pbs/${chart.chartID}/leaderboard-adjacent`
);
if (!nRes.success) {
return { leaderboard: lRes.body, playcount: pRes.body.count };
}
return {
leaderboard: lRes.body,
adjacent: nRes.body,
playcount: pRes.body.count,
};
}
return { leaderboard: lRes.body, playcount: pRes.body.count };
}
);
if (error) {
return <ApiError error={error} />;
}
if (!data || isLoading) {
return <Loading />;
}
if (data.leaderboard.pbs.length === 0) {
return <div className="text-center my-8">Nobody has played this chart!</div>;
}
const userMap = CreateUserMap(data.leaderboard.users);
if (data.adjacent) {
for (const user of data.adjacent.users) {
userMap.set(user.id, user);
}
}
return (
<Row>
<Col xs={12} className="text-center mb-2">
<h4 className="display-4">Leaderboard</h4>
</Col>
<Col xs={12} className="d-flex justify-content-center mb-8">
<div className="btn-group">
<SelectButton value={mode} setValue={setMode} id="leaderboard">
<Icon type="trophy" />
Best 100
</SelectButton>
<SelectButton value={mode} setValue={setMode} id="adjacent">
<Icon type="user" />
Your Position
</SelectButton>
</div>
</Col>
<TopShowcase data={data} user={user} userMap={userMap} />
<Col xs={12} className="mt-4">
<ChartLeaderboardTable
{...{ data, game, playtype, user, userMap, chart, song, mode }}
/>
</Col>
</Row>
);
}
function ChartLeaderboardTable({
data,
userMap,
user,
game,
playtype,
mode,
chart,
song,
}: {
data: ChartPBData;
user: PublicUserDocument | null;
userMap: Map<integer, PublicUserDocument>;
mode: "leaderboard" | "adjacent";
chart: ChartDocument;
song: SongDocument;
} & GamePT) {
const dataset: PBDataset = useMemo(() => {
const ds: PBDataset = [];
let pbs = [];
if (mode === "leaderboard") {
pbs = data.leaderboard.pbs;
} else {
pbs = [
...data.adjacent!.adjacentAbove,
data.adjacent!.pb,
...data.adjacent!.adjacentBelow,
];
}
for (const pb of pbs) {
ds.push({
...pb,
__related: {
chart,
song,
index: pb.rankingData.rank - 1,
user: userMap.get(pb.userID)!,
},
});
}
return ds;
}, [data, mode, user]);
return <PBTable dataset={dataset} game={game} playtype={playtype} showChart={false} showUser />;
}
function TopShowcase({
data,
user,
userMap,
}: {
data: ChartPBData;
user: PublicUserDocument | null;
userMap: Map<integer, PublicUserDocument>;
}) {
// We have a couple of conditions.
// User is #1: col-12 #1,
// User has played: col-6 col-6,
// User has not played: col-12 #1,
const bestPlay = data.leaderboard.pbs[0];
const bestUser = userMap.get(bestPlay.userID)!;
// User hasn't played, or isn't logged in or something.
if (!data.adjacent || user?.id === bestPlay.userID) {
return (
<Col xs={12}>
<PlayCard name="Best Play" pb={bestPlay} user={bestUser} />
</Col>
);
}
const thisUsersPlay = data.adjacent.pb;
return (
<>
<Col xs={12} lg={6}>
<PlayCard name="Best Play" pb={bestPlay} user={bestUser} />
</Col>
<Col xs={12} lg={6}>
<PlayCard name="Your Score" pb={thisUsersPlay} user={user!} />
</Col>
</>
);
}
function PlayCard({
pb,
user,
name,
}: {
pb: PBScoreDocument;
user: PublicUserDocument;
name: string;
}) {
return (
<Card header={name}>
<Row className="align-items-center">
<Col lg={3}>
<ProfilePicture user={user} />
</Col>
<Col lg={3}>
<h4>
<Link
className="gentle-link"
to={`/dashboard/users/${user.username}/games/${pb.game}/${pb.playtype}`}
>
{user.username}
</Link>
</h4>
<strong className="display-4">#{pb.rankingData.rank}</strong>
<span className="text-muted">/{pb.rankingData.outOf}</span>
</Col>
<Col lg={6}>
<MiniTable headers={["Score Info"]}>
<tr>
<ScoreCell score={pb} />
</tr>
<tr>
<LampCell score={pb} />
</tr>
</MiniTable>
</Col>
</Row>
</Card>
);
}
function SelectRightChart(gptConfig: GamePTConfig, chartID: string, charts: ChartDocument[]) {
if (gptConfig.difficulties.includes(chartID as any)) {
for (const chart of charts) {
if (chart.difficulty === chartID && chart.isPrimary) {
return chart;
}
}
} else {
// else, its a chart ID.
for (const chart of charts) {
if (chartID === chart.chartID) {
return chart;
}
}
}
return null;
}
@@ -0,0 +1,29 @@
import useSetSubheader from "components/layout/header/useSetSubheader";
import LinkButton from "components/util/LinkButton";
import React, { useEffect } from "react";
import { Button, Row } from "react-bootstrap";
import { ChartDocument, FormatDifficulty, GetGameConfig, GetGamePTConfig } from "tachi-common";
import { SongsReturn } from "types/api-returns";
import { GamePT, SetState } from "types/react";
import { ChangeOpacity } from "util/color-opacity";
import { NumericSOV } from "util/sorts";
export default function GPTSongPage({
game,
playtype,
song,
charts,
setActiveChart,
}: { setActiveChart: SetState<ChartDocument | null> } & SongsReturn & GamePT) {
const formatSongTitle = `${song.artist} - ${song.title}`;
useSetSubheader(
["Games", GetGameConfig(game).name, playtype, "Songs", formatSongTitle],
[game, playtype],
formatSongTitle
);
setActiveChart(null);
return <div className="w-100 text-center">You gotta select a chart to see leaderboards!</div>;
}
@@ -114,7 +114,7 @@ function PreferencesForm({ reqUser }: { reqUser: PublicUserDocument }) {
id="contentiousContent"
checked={formik.values.contentiousContent}
onChange={formik.handleChange}
label="Contentious Content"
label="Family Unfriendly Mode"
/>
<Form.Text>
Show slightly less appropriate splash texts in certain places.
@@ -193,7 +193,6 @@ function PBsOverview({
{...{ isLoading, error, dataset: data }}
>
<PBTable
reqUser={reqUser}
dataset={data!}
game={game}
showPlaycount={showPlaycount}
@@ -276,7 +275,6 @@ function PBsSearch({
return (
<LoadingWrapper style={{ height: 500 }} {...{ isLoading, error, dataset: data }}>
<PBTable
reqUser={reqUser}
indexCol={false}
dataset={data!}
game={game}
+188 -12
View File
@@ -1,15 +1,36 @@
import PlaytypeSelect from "app/pages/dashboard/games/_game/PlaytypeSelect";
import GPTSongsPage from "app/pages/dashboard/games/_game/_playtype/GPTSongsPage";
import GPTChartPage from "app/pages/dashboard/games/_game/_playtype/GPTChartPage";
import GPTDevInfo from "app/pages/dashboard/games/_game/_playtype/GPTDevInfo";
import GPTLeaderboardsPage from "app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage";
import GPTMainPage from "app/pages/dashboard/games/_game/_playtype/GPTMainPage";
import GPTSongPage from "app/pages/dashboard/games/_game/_playtype/GPTSongPage";
import GPTSongsPage from "app/pages/dashboard/games/_game/_playtype/GPTSongsPage";
import { ErrorPage } from "app/pages/ErrorPage";
import { GPTBottomNav, GPTHeaderBody } from "components/game/GPTHeader";
import LayoutHeaderContainer from "components/layout/LayoutHeaderContainer";
import React from "react";
import ChartInfoFormat from "components/game/charts/ChartInfoFormat";
import { GPTBottomNav } from "components/game/GPTHeader";
import SongInfoFormat from "components/game/songs/SongInfoFormat";
import Card from "components/layout/page/Card";
import DebugContent from "components/util/DebugContent";
import Divider from "components/util/Divider";
import LinkButton from "components/util/LinkButton";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import { UserSettingsContext } from "context/UserSettingsContext";
import React, { useContext, useEffect, useState } from "react";
import { Col, Row } from "react-bootstrap";
import { Redirect, Route, Switch, useParams } from "react-router-dom";
import { FormatGame, Game, GetGameConfig } from "tachi-common";
import {
ChartDocument,
FormatDifficulty,
Game,
GetGameConfig,
GetGamePTConfig,
} from "tachi-common";
import { SongsReturn } from "types/api-returns";
import { GamePT, SetState } from "types/react";
import { IsSupportedGame, IsSupportedPlaytype } from "util/asserts";
import { ChangeOpacity } from "util/color-opacity";
import { NumericSOV } from "util/sorts";
export default function GameRoutes() {
const { game } = useParams<{ game: string }>();
@@ -60,12 +81,10 @@ function GamePlaytypeRoutes({ game }: { game: Game }) {
return (
<>
<LayoutHeaderContainer
footer={<GPTBottomNav baseUrl={`/dashboard/games/${game}/${playtype}`} />}
header={FormatGame(game, playtype)}
>
<GPTHeaderBody game={game} playtype={playtype} />
</LayoutHeaderContainer>
<div className="card">
<GPTBottomNav baseUrl={`/dashboard/games/${game}/${playtype}`} />
</div>
<Divider />
<Switch>
<Route exact path="/dashboard/games/:game/:playtype">
<GPTMainPage game={game} playtype={playtype} />
@@ -75,10 +94,13 @@ function GamePlaytypeRoutes({ game }: { game: Game }) {
<GPTSongsPage game={game} playtype={playtype} />
</Route>
<Route path="/dashboard/games/:game/:playtype/songs/:songID">
<SongChartRoutes game={game} playtype={playtype} />
</Route>
<Route exact path="/dashboard/games/:game/:playtype/leaderboards">
<GPTLeaderboardsPage game={game} playtype={playtype} />
</Route>
<Route exact path="/dashboard/games/:game/:playtype/dev-info">
<GPTDevInfo game={game} playtype={playtype} />
</Route>
@@ -90,3 +112,157 @@ function GamePlaytypeRoutes({ game }: { game: Game }) {
</>
);
}
function SongChartRoutes({ game, playtype }: GamePT) {
const { songID } = useParams<{ songID: string }>();
const { data, isLoading, error } = useApiQuery<SongsReturn>(
`/games/${game}/${playtype}/songs/${songID}`
);
const { settings } = useContext(UserSettingsContext);
const [activeChart, setActiveChart] = useState<ChartDocument | null>(null);
useEffect(() => {
setActiveChart(null);
}, [game, playtype]);
if (error) {
return <ErrorPage statusCode={error.statusCode} customMessage={error.description} />;
}
if (!data || isLoading) {
return <Loading />;
}
return (
<>
<SongInfoHeader
game={game}
playtype={playtype}
{...data}
activeChart={activeChart}
setActiveChart={setActiveChart}
/>
<Divider />
<Switch>
<Route exact path="/dashboard/games/:game/:playtype/songs/:songID">
<GPTSongPage
game={game}
playtype={playtype}
song={data.song}
charts={data.charts}
setActiveChart={setActiveChart}
/>
</Route>
<Route exact path="/dashboard/games/:game/:playtype/songs/:songID/:difficulty">
<GPTChartPage
game={game}
playtype={playtype}
song={data.song}
chart={activeChart}
setActiveChart={setActiveChart}
allCharts={data.charts}
/>
</Route>
<Route path="*">
<ErrorPage statusCode={404} />
</Route>
</Switch>
{settings?.preferences.developerMode && (
<>
<Divider />
<Card header="Dev Info">
<DebugContent data={data} />
</Card>
</>
)}
</>
);
}
function SongInfoHeader({
game,
playtype,
song,
charts,
activeChart,
setActiveChart,
}: { activeChart: ChartDocument | null; setActiveChart: SetState<ChartDocument | null> } & GamePT &
SongsReturn) {
// @TODO: Custom game song formatting functions, since for some games
// (like popn) players use the genres to discuss them.
const formatSongTitle = `${song.artist} - ${song.title}`;
const [imgShow, setImgShow] = useState(true);
const gptConfig = GetGamePTConfig(game, playtype);
// accidentally O(n^2) but this is a short list so who cares
const sortedCharts = charts
.slice(0)
.sort(NumericSOV(x => gptConfig.difficulties.indexOf(x.difficulty)));
return (
<Card header="Song Info">
<Row
className="align-items-center"
style={{
justifyContent: "space-evenly",
}}
>
{imgShow && (
<Col xs={12} lg={3} className="text-center">
<img
src="https://kamaitachi.xyz/static/images/gameicons/iidx/18z.png"
onError={() => setImgShow(false)}
className="w-100"
/>
</Col>
)}
<Col xs={12} lg={4} className="text-center">
<SongInfoFormat {...{ game, song, chart: activeChart }} />
</Col>
{game !== "bms" && (
<Col xs={12} lg={3} className="text-center">
<h5>Charts</h5>
<hr />
<div className="btn-group-vertical d-flex justify-content-center">
{sortedCharts.map(e => (
<LinkButton
onClick={() => setActiveChart(e)}
className="btn-secondary"
key={e.chartID}
to={`/dashboard/games/${game}/${playtype}/songs/${song.id}/${e.difficulty}`}
style={{
backgroundColor: gptConfig.difficultyColours[e.difficulty]
? ChangeOpacity(
gptConfig.difficultyColours[e.difficulty]!,
activeChart?.chartID === e.chartID ? 0.4 : 0.2
)
: undefined,
}}
>
{activeChart?.chartID === e.chartID ? (
<strong>{FormatDifficulty(e, game)}</strong>
) : (
FormatDifficulty(e, game)
)}
</LinkButton>
))}
</div>
</Col>
)}
{activeChart && (
<Col xs={12}>
<Divider />
<ChartInfoFormat playtype={playtype} chart={activeChart} game={game} />
</Col>
)}
</Row>
</Card>
);
}
-38
View File
@@ -1,45 +1,7 @@
import Navbar from "components/nav/Navbar";
import NavItem from "components/nav/NavItem";
import MiniTable from "components/tables/components/MiniTable";
import ApiError from "components/util/ApiError";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import { UserSettingsContext } from "context/UserSettingsContext";
import React, { useContext } from "react";
import { Game } from "tachi-common";
import { GPTStatsReturn } from "types/api-returns";
import { Playtype } from "types/tachi";
export function GPTHeaderBody({ game, playtype }: { game: Game; playtype: Playtype }) {
const { data, isLoading, error } = useApiQuery<GPTStatsReturn>(`/games/${game}/${playtype}`);
return (
<>
<div className="col-12 col-lg-4 offset-lg-4">
{error ? (
<ApiError error={error} />
) : isLoading || !data ? (
<Loading />
) : (
<MiniTable headers={["Stats"]} colSpan={2}>
<tr>
<td>Player Count</td>
<td>{data.playerCount}</td>
</tr>
<tr>
<td>Available Charts</td>
<td>{data.chartCount}</td>
</tr>
<tr>
<td>Total Scores</td>
<td>{data.scoreCount}</td>
</tr>
</MiniTable>
)}
</div>
</>
);
}
export function GPTBottomNav({ baseUrl }: { baseUrl: string }) {
const { settings } = useContext(UserSettingsContext);
@@ -0,0 +1,108 @@
import QuickTooltip from "components/layout/misc/QuickTooltip";
import MiniTable from "components/tables/components/MiniTable";
import ApiError from "components/util/ApiError";
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 { UserContext } from "context/UserContext";
import React, { useContext } from "react";
import { Col, Row } from "react-bootstrap";
import { Link } from "react-router-dom";
import { ChartDocument, FolderDocument, Game, GetGamePTConfig } from "tachi-common";
import { GamePT } from "types/react";
export default function ChartInfoFormat({
chart,
game,
playtype,
}: { chart: ChartDocument } & GamePT) {
const gptConfig = GetGamePTConfig(game, playtype);
const withTierlists = gptConfig.tierlists.filter(e => chart.tierlistInfo[e]);
const { data, isLoading, error } = useApiQuery<FolderDocument[]>(
`/games/${game}/${playtype}/charts/${chart.chartID}/folders`
);
const { user } = useContext(UserContext);
if (error) {
<ApiError error={error} />;
}
if (!data || isLoading) {
return <Loading />;
}
return (
<Row
className="text-center align-items-center"
style={{
paddingTop: "2.2rem",
justifyContent: "space-evenly",
}}
>
<Col xs={12} lg={3}>
<h4>Appears In</h4>
{data.length !== 0 ? (
data.map(e => (
<li key={e.folderID}>
{user ? (
<Link
className="gentle-link"
to={`/dashboard/users/${user.username}/games/${game}/${playtype}/folders/${e.folderID}`}
>
{e.title}
</Link>
) : (
<span>{e.title}</span>
)}
</li>
))
) : (
<Muted>No folders...</Muted>
)}
</Col>
<Col xs={12} lg={4}>
<ChartInfoMiddle chart={chart} game={game} />
</Col>
<Col xs={12} lg={3}>
{withTierlists.length !== 0 ? (
<>
<MiniTable headers={["Tierlist Info"]} colSpan={2}>
{withTierlists.map(e => (
<tr key={e}>
<td>{e}</td>
<td>
{chart.tierlistInfo[e]!.text}{" "}
<Muted>({chart.tierlistInfo[e]!.value})</Muted>
{chart.tierlistInfo[e]!.individualDifference && (
<>
<br />
<QuickTooltip tooltipContent="Individual Difference - The difficulty of this varies massively between people!">
<span>
<Icon type="balance-scale-left" />
</span>
</QuickTooltip>
</>
)}
</td>
</tr>
))}
</MiniTable>
<Muted>Disagree with these tiers? Let us know in the discord.</Muted>
</>
) : (
<Muted>No tierlist info.</Muted>
)}
</Col>
</Row>
);
}
function ChartInfoMiddle({ game, chart }: { chart: ChartDocument; game: Game }) {
// someday
return <></>;
}
@@ -0,0 +1,21 @@
import React from "react";
import { ChartDocument, FormatDifficulty, Game, SongDocument } from "tachi-common";
export default function DefaultSongChartInfoFormat({
song,
chart,
game,
}: {
song: SongDocument;
chart: ChartDocument | null;
game: Game;
}) {
return (
<>
<h4>
{song.artist} - {song.title}
</h4>
{chart && <h5>({FormatDifficulty(chart, game)})</h5>}
</>
);
}
@@ -0,0 +1,53 @@
import React from "react";
import { ChartDocument, FormatDifficulty, Game, SongDocument } from "tachi-common";
import { FormatBMSTables } from "util/misc";
import DefaultSongChartInfoFormat from "./DefaultSongInfoFormat";
export default function SongChartInfoFormat({
song: tempSong,
chart: tempChart,
game,
}: {
song: SongDocument;
chart: ChartDocument | null;
game: Game;
}) {
// lazy casting hack to avoid IDStrings
const props = { song: tempSong, chart: tempChart, game } as any;
const { song, chart } = props;
if (game === "bms") {
if (!chart) {
return (
<>
<h4>{song.data.genre}</h4>
<h4 style={{ fontSize: "2.5rem", fontWeight: "bold" }}>{song.title}</h4>
<h4>{song.artist}</h4>
</>
);
}
const hasLevel = chart.data.tableFolders.length > 0;
const levelText = hasLevel ? FormatBMSTables(chart.data.tableFolders) : "No Level";
return (
<>
<h4>{song.data.genre}</h4>
<h4 style={{ fontSize: "2.5rem", fontWeight: "bold" }}>{song.title}</h4>
<h4>{song.artist}</h4>
{chart && <h5>({levelText})</h5>}
</>
);
} else if (game === "iidx") {
return (
<>
<h4>{song.data.genre}</h4>
<h4 style={{ fontSize: "2.5rem", fontWeight: "bold" }}>{song.title}</h4>
<h4>{song.artist}</h4>
{chart && <h5>({FormatDifficulty(chart, game)})</h5>}
</>
);
}
return <DefaultSongChartInfoFormat {...props} />;
}
@@ -1,23 +1,18 @@
import ApiError from "components/util/ApiError";
import DebugContent from "components/util/DebugContent";
import HasDevModeOn from "components/util/HasDevModeOn";
import Icon from "components/util/Icon";
import SelectButton from "components/util/SelectButton";
import React, { useContext, useMemo, useState } from "react";
import DropdownStructure from "./components/DropdownStructure";
import {
PublicUserDocument,
ChartDocument,
ScoreDocument,
PBScoreDocument,
IDStrings,
} from "tachi-common";
import { GamePT, SetState } from "types/react";
import { UGPTChartPBComposition } from "types/api-returns";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import SelectButton from "components/util/SelectButton";
import { UserSettingsContext } from "context/UserSettingsContext";
import React, { useContext, useMemo, useState } from "react";
import { ChartDocument, IDStrings, integer, PBScoreDocument, ScoreDocument } from "tachi-common";
import { UGPTChartPBComposition } from "types/api-returns";
import { GamePT, SetState } from "types/react";
import DropdownStructure from "./components/DropdownStructure";
import GenericScoreContentDropdown from "./components/GenericScoreContentDropdown";
import PlayHistory from "./components/PlayHistory";
import useApiQuery from "components/util/query/useApiQuery";
import { UserSettingsContext } from "context/UserSettingsContext";
import HasDevModeOn from "components/util/HasDevModeOn";
export interface ScoreState {
highlight: boolean;
@@ -33,13 +28,13 @@ export interface ScoreDropdownProps<I extends IDStrings = IDStrings> {
export default function GenericPBDropdown<I extends IDStrings = IDStrings>({
game,
playtype,
reqUser,
chart,
scoreState,
defaultView = "pb",
userID,
DocComponent = GenericScoreContentDropdown,
}: {
reqUser: PublicUserDocument;
userID: integer;
chart: ChartDocument;
scoreState: ScoreState;
defaultView?: "pb" | "scorePB" | "lampPB" | "history" | "debug";
@@ -55,12 +50,12 @@ export default function GenericPBDropdown<I extends IDStrings = IDStrings>({
const [view, setView] = useState(defaultView);
const { isLoading, error, data } = useApiQuery<UGPTChartPBComposition<I>>(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`
`/users/${userID}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`
);
const { isLoading: histIsLoading, error: histError, data: histData } = useApiQuery<
ScoreDocument<I>[]
>(`/users/${reqUser.id}/games/${game}/${playtype}/scores/${chart.chartID}`);
>(`/users/${userID}/games/${game}/${playtype}/scores/${chart.chartID}`);
const currentScoreDoc: ScoreDocument<I> | PBScoreDocument<I> | null = useMemo(() => {
if (!data) {
@@ -73,7 +68,7 @@ export default function GenericPBDropdown<I extends IDStrings = IDStrings>({
// scores have more information than PBs.
// In this case, the PB is only composed of one score,
// so we should default to this instead.
return data.scores[0];
return data.scores.filter(e => e.scoreID === data.pb.composedFrom.lampPB)[0];
}
return data.pb;
}
@@ -89,7 +84,7 @@ export default function GenericPBDropdown<I extends IDStrings = IDStrings>({
}, [view, data]);
if (error) {
return <>An error has occured. Whoops.</>;
return <ApiError error={error} />;
}
if (isLoading || !data) {
@@ -1,5 +1,11 @@
import React from "react";
import { ChartDocument, PBScoreDocument, PublicUserDocument, ScoreDocument } from "tachi-common";
import {
ChartDocument,
integer,
PBScoreDocument,
PublicUserDocument,
ScoreDocument,
} from "tachi-common";
import { UGPTChartPBComposition } from "types/api-returns";
import { GamePT, SetState } from "types/react";
import DropdownScoreButtons from "./components/DropdownScoreButtons";
@@ -10,11 +16,11 @@ import GenericPBDropdown, { ScoreState } from "./GenericPBDropdown";
export default function IIDXPBDropdown({
game,
playtype,
reqUser,
chart,
scoreState,
userID,
}: {
reqUser: PublicUserDocument;
userID: integer;
chart: ChartDocument<"iidx:SP" | "iidx:DP">;
scoreState: {
highlight: boolean;
@@ -23,7 +29,7 @@ export default function IIDXPBDropdown({
} & GamePT) {
return (
<GenericPBDropdown
{...{ game, playtype, chart, reqUser, scoreState }}
{...{ game, playtype, chart, userID, scoreState }}
DocComponent={DocumentDisplay}
/>
);
@@ -94,7 +94,7 @@ function Row({
dropdown={
<GenericPBDropdown
chart={data}
reqUser={reqUser}
userID={score.userID}
game={game}
playtype={data.playtype}
scoreState={scoreState}
@@ -106,7 +106,7 @@ function Row({
dropdown={
<GenericPBDropdown
chart={data}
reqUser={reqUser}
userID={score.userID}
game={game}
playtype={data.playtype}
scoreState={scoreState}
@@ -117,7 +117,7 @@ function Row({
dropdown={
<IIDXPBDropdown
chart={data}
reqUser={reqUser}
userID={score.userID}
game={game}
playtype={data.playtype}
scoreState={scoreState}
@@ -89,7 +89,7 @@ function Row({
dropdown={
<GenericPBDropdown
chart={data}
reqUser={reqUser}
userID={score.userID}
game={game}
playtype={data.playtype}
scoreState={scoreState}
@@ -88,7 +88,7 @@ function Row({
dropdown={
<GenericPBDropdown
chart={data}
reqUser={reqUser}
userID={score.userID}
game={game}
playtype={data.playtype}
scoreState={scoreState}
@@ -1,5 +1,11 @@
import React from "react";
import { Game, IDStrings, ScoreCalculatedDataLookup, ScoreDocument } from "tachi-common";
import {
Game,
IDStrings,
PBScoreDocument,
ScoreCalculatedDataLookup,
ScoreDocument,
} from "tachi-common";
import { GamePT } from "types/react";
import BMSScoreCoreCells from "./BMSScoreCoreCells";
import GenericScoreCoreCells from "./GenericScoreCoreCells";
@@ -12,7 +18,7 @@ export default function ScoreCoreCells({
score,
rating,
}: {
score: ScoreDocument;
score: ScoreDocument | PBScoreDocument;
rating: ScoreCalculatedDataLookup[IDStrings];
game: Game;
}) {
@@ -36,12 +36,10 @@ export default function BMSPBTable({
dataset,
indexCol = true,
showPlaycount = false,
reqUser,
playtype,
}: {
dataset: PBDataset<"bms:7K" | "bms:14K">;
indexCol?: boolean;
reqUser: PublicUserDocument;
showPlaycount?: boolean;
playtype: "7K" | "14K";
}) {
@@ -110,7 +108,6 @@ export default function BMSPBTable({
<Row
pb={pb}
key={`${pb.chartID}:${pb.userID}`}
reqUser={reqUser}
showPlaycount={showPlaycount}
indexCol={indexCol}
/>
@@ -122,13 +119,11 @@ export default function BMSPBTable({
function Row({
pb,
indexCol,
reqUser,
showPlaycount,
}: {
pb: PBDataset<"bms:7K" | "bms:14K">[0];
indexCol: boolean;
showPlaycount: boolean;
reqUser: PublicUserDocument;
}) {
const scoreState = usePBState(pb);
@@ -137,7 +132,7 @@ function Row({
dropdown={
<GenericPBDropdown
chart={pb.__related.chart}
reqUser={reqUser}
userID={pb.userID}
game={pb.game}
playtype={pb.playtype}
scoreState={scoreState}
@@ -24,7 +24,6 @@ export default function GenericPBTable({
dataset,
indexCol = true,
showPlaycount = false,
reqUser,
game,
playtype,
showScore,
@@ -32,7 +31,6 @@ export default function GenericPBTable({
dataset: PBDataset;
indexCol?: boolean;
showPlaycount?: boolean;
reqUser: PublicUserDocument;
game: Game;
playtype: Playtype;
showScore?: boolean;
@@ -73,7 +71,6 @@ export default function GenericPBTable({
<Row
pb={pb}
key={`${pb.chartID}:${pb.userID}`}
reqUser={reqUser}
indexCol={indexCol}
showScore={showScore}
showPlaycount={showPlaycount}
@@ -86,14 +83,12 @@ export default function GenericPBTable({
function Row({
pb,
indexCol,
reqUser,
showScore,
showPlaycount,
}: {
pb: PBDataset[0];
indexCol: boolean;
showPlaycount: boolean;
reqUser: PublicUserDocument;
showScore?: boolean;
}) {
const scoreState = usePBState(pb);
@@ -103,13 +98,12 @@ function Row({
dropdown={
<GenericPBDropdown
chart={pb.__related.chart}
reqUser={reqUser}
userID={pb.userID}
game={pb.game}
playtype={pb.playtype}
scoreState={scoreState}
/>
}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<DifficultyCell game={pb.game} chart={pb.__related.chart} />
@@ -1,13 +1,13 @@
import useScoreRatingAlg from "components/util/useScoreRatingAlg";
import { UserContext } from "context/UserContext";
import { nanoid } from "nanoid";
import React, { useState } from "react";
import React, { useContext, useState } from "react";
import { PublicUserDocument, ScoreCalculatedDataLookup } from "tachi-common";
import { FormatDifficulty } from "tachi-common/js/utils/util";
import { PBDataset } from "types/tables";
import { IsNullish } from "util/misc";
import { NumericSOV, StrSOV } from "util/sorts";
import { HumanFriendlyStrToGradeIndex, HumanFriendlyStrToLampIndex } from "util/str-to-num";
import { CreateDefaultPBSearchParams } from "util/tables/create-search";
import { GetPBLeadingHeaders } from "util/tables/get-pb-leaders";
import DeltaCell from "../cells/DeltaCell";
import DifficultyCell from "../cells/DifficultyCell";
import IIDXLampCell from "../cells/IIDXLampCell";
@@ -17,36 +17,40 @@ import RankingCell from "../cells/RankingCell";
import ScoreCell from "../cells/ScoreCell";
import TimestampCell from "../cells/TimestampCell";
import TitleCell from "../cells/TitleCell";
import UserCell from "../cells/UserCell";
import DropdownRow from "../components/DropdownRow";
import SelectableRating from "../components/SelectableRating";
import TachiTable, { Header, ZTableTHProps } from "../components/TachiTable";
import { usePBState } from "../components/UseScoreState";
import IIDXPBDropdown from "../dropdowns/IIDXPBDropdown";
import IndicatorHeader from "../headers/IndicatorHeader";
export default function IIDXPBTable({
dataset,
indexCol = true,
showPlaycount = false,
reqUser,
showUser = false,
showChart = true,
alg,
playtype,
}: {
dataset: PBDataset<"iidx:SP" | "iidx:DP">;
indexCol?: boolean;
showPlaycount?: boolean;
reqUser: PublicUserDocument;
showUser?: boolean;
showChart?: boolean;
alg?: ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"];
playtype: "SP" | "DP";
}) {
const defaultRating = useScoreRatingAlg<"iidx:SP" | "iidx:DP">("iidx", playtype);
console.log(alg, defaultRating);
const [rating, setRating] = useState<ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"]>(
alg ?? defaultRating
);
const headers: Header<PBDataset<"iidx:SP" | "iidx:DP">[0]>[] = [
[
...GetPBLeadingHeaders(showUser, showChart, [
"Chart",
"Chart",
NumericSOV(
@@ -55,9 +59,7 @@ export default function IIDXPBTable({
x.__related.chart.tierlistInfo["kt-HC"]?.value ??
x.__related.chart.levelNum
),
],
IndicatorHeader,
["Song", "Song", StrSOV(x => x.__related.song.title)],
]),
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Deltas", "Deltas", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
@@ -69,7 +71,7 @@ export default function IIDXPBTable({
<SelectableRating<"iidx:SP" | "iidx:DP">
key={nanoid()}
game="iidx"
playtype="SP"
playtype={playtype}
rating={rating}
setRating={setRating}
{...thProps}
@@ -84,10 +86,16 @@ export default function IIDXPBTable({
headers.push(["Playcount", "Plays", NumericSOV(x => x.__playcount ?? 0)]);
}
// if (hideRankingCell) {
// }
if (indexCol) {
headers.unshift(["#", "#", NumericSOV(x => x.__related.index)]);
}
const { user } = useContext(UserContext);
return (
<TachiTable
dataset={dataset}
@@ -99,10 +107,12 @@ export default function IIDXPBTable({
<Row
pb={pb}
key={`${pb.chartID}:${pb.userID}`}
reqUser={reqUser}
showPlaycount={showPlaycount}
indexCol={indexCol}
rating={rating}
showUser={showUser}
showChart={showChart}
user={user}
/>
)}
/>
@@ -112,24 +122,29 @@ export default function IIDXPBTable({
function Row({
pb,
indexCol,
reqUser,
rating,
showPlaycount,
showChart,
showUser,
user,
}: {
pb: PBDataset<"iidx:SP" | "iidx:DP">[0];
indexCol: boolean;
reqUser: PublicUserDocument;
showPlaycount: boolean;
showUser: boolean;
showChart: boolean;
rating: ScoreCalculatedDataLookup["iidx:SP"];
user: PublicUserDocument | null;
}) {
const scoreState = usePBState(pb);
return (
<DropdownRow
className={pb.userID === user?.id ? "highlighted-row" : ""}
dropdown={
<IIDXPBDropdown
chart={pb.__related.chart}
reqUser={reqUser}
userID={pb.userID}
game="iidx"
playtype={pb.playtype}
scoreState={scoreState}
@@ -137,9 +152,27 @@ function Row({
}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<DifficultyCell chart={pb.__related.chart} game={"iidx"} />
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell song={pb.__related.song} chart={pb.__related.chart} game="iidx" />
{showUser && showChart && (
<>
<UserCell game="iidx" playtype={pb.playtype} user={pb.__related.user!} />
<DifficultyCell chart={pb.__related.chart} game={"iidx"} />
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell song={pb.__related.song} chart={pb.__related.chart} game="iidx" />
</>
)}
{showUser && !showChart && (
<>
<IndicatorsCell highlight={scoreState.highlight} />
<UserCell game="iidx" playtype={pb.playtype} user={pb.__related.user!} />
</>
)}
{!showUser && showChart && (
<>
<DifficultyCell chart={pb.__related.chart} game={"iidx"} />
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell song={pb.__related.song} chart={pb.__related.chart} game="iidx" />
</>
)}
<ScoreCell score={pb} />
<DeltaCell
game="iidx"
@@ -2,7 +2,6 @@ import React from "react";
import { PublicUserDocument } from "tachi-common";
import { PBDataset } from "types/tables";
import { NumericSOV, StrSOV } from "util/sorts";
import { HumanFriendlyStrToGradeIndex, HumanFriendlyStrToLampIndex } from "util/str-to-num";
import { CreateDefaultPBSearchParams } from "util/tables/create-search";
import DifficultyCell from "../cells/DifficultyCell";
import IndexCell from "../cells/IndexCell";
@@ -24,13 +23,11 @@ export default function MusecaPBTable({
dataset,
indexCol = true,
showPlaycount = false,
reqUser,
playtype,
}: {
dataset: PBDataset<"museca:Single">;
indexCol?: boolean;
showPlaycount?: boolean;
reqUser: PublicUserDocument;
playtype: "Single";
}) {
const headers: Header<PBDataset<"museca:Single">[0]>[] = [
@@ -64,7 +61,6 @@ export default function MusecaPBTable({
<Row
pb={pb}
key={`${pb.chartID}:${pb.userID}`}
reqUser={reqUser}
indexCol={indexCol}
showPlaycount={showPlaycount}
/>
@@ -76,13 +72,11 @@ export default function MusecaPBTable({
function Row({
pb,
indexCol,
reqUser,
showPlaycount,
}: {
pb: PBDataset<"museca:Single">[0];
indexCol: boolean;
showPlaycount: boolean;
reqUser: PublicUserDocument;
}) {
const scoreState = usePBState(pb);
@@ -91,7 +85,7 @@ function Row({
dropdown={
<GenericPBDropdown
chart={pb.__related.chart}
reqUser={reqUser}
userID={pb.userID}
game={pb.game}
playtype={pb.playtype}
scoreState={scoreState}
+5 -3
View File
@@ -11,23 +11,25 @@ import SDVXPBTable from "./SDVXPBTable";
export default function PBTable({
dataset,
indexCol = true,
reqUser,
playtype,
showPlaycount,
game,
alg,
showChart = true,
showUser = false,
}: {
dataset: PBDataset;
indexCol?: boolean;
showPlaycount?: boolean;
reqUser: PublicUserDocument;
alg?: ScoreCalculatedDataLookup[IDStrings];
showChart?: boolean;
showUser?: boolean;
} & GamePT) {
// We're just going to ignore all these errors
// and assume we just won't make a mistake.
// ever.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const props = { dataset, indexCol, reqUser, playtype, showPlaycount, alg } as any;
const props = { dataset, indexCol, playtype, showPlaycount, alg, showChart, showUser } as any;
if (game === "iidx") {
return <IIDXPBTable {...props} />;
@@ -23,13 +23,11 @@ export default function SDVXPBTable({
dataset,
indexCol = true,
showPlaycount = false,
reqUser,
playtype,
}: {
dataset: PBDataset<"sdvx:Single">;
indexCol?: boolean;
showPlaycount?: boolean;
reqUser: PublicUserDocument;
playtype: "7K" | "14K";
}) {
const headers: Header<PBDataset<"sdvx:Single">[0]>[] = [
@@ -63,7 +61,6 @@ export default function SDVXPBTable({
<Row
pb={pb}
key={`${pb.chartID}:${pb.userID}`}
reqUser={reqUser}
indexCol={indexCol}
showPlaycount={showPlaycount}
/>
@@ -76,12 +73,10 @@ function Row({
pb,
indexCol,
showPlaycount,
reqUser,
}: {
pb: PBDataset<"sdvx:Single">[0];
indexCol: boolean;
showPlaycount: boolean;
reqUser: PublicUserDocument;
}) {
const scoreState = usePBState(pb);
@@ -90,13 +85,12 @@ function Row({
dropdown={
<GenericPBDropdown
chart={pb.__related.chart}
reqUser={reqUser}
userID={pb.userID}
game={pb.game}
playtype={pb.playtype}
scoreState={scoreState}
/>
}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<DifficultyCell game="sdvx" chart={pb.__related.chart} />
@@ -87,7 +87,6 @@ function Row({
return (
<DropdownRow
dropdown={
<GenericScoreDropdown
chart={sc.__related.chart}
@@ -1,7 +1,7 @@
import useScoreRatingAlg from "components/util/useScoreRatingAlg";
import { nanoid } from "nanoid";
import React, { useState } from "react";
import { integer, Playtypes, PublicUserDocument, ScoreCalculatedDataLookup } from "tachi-common";
import { integer, Playtypes, ScoreCalculatedDataLookup } from "tachi-common";
import { ScoreDataset } from "types/tables";
import { Playtype } from "types/tachi";
import { NumericSOV, StrSOV } from "util/sorts";
@@ -108,7 +108,6 @@ function Row({
return (
<DropdownRow
dropdown={
<IIDXScoreDropdown
chart={sc.__related.chart}
@@ -68,7 +68,6 @@ function Row({
return (
<DropdownRow
dropdown={
<GenericScoreDropdown
chart={sc.__related.chart}
+6 -2
View File
@@ -1,4 +1,4 @@
import React from "react";
import React, { CSSProperties } from "react";
import { Link } from "react-router-dom";
import { JustChildren } from "types/react";
@@ -6,12 +6,16 @@ export default function LinkButton({
to,
className = "btn-primary",
children,
style,
onClick,
}: {
to: string;
className?: string;
style?: CSSProperties;
onClick?: () => void;
} & JustChildren) {
return (
<Link to={to} className={`btn ${className}`}>
<Link onClick={onClick} to={to} className={`btn ${className}`} style={style}>
{children}
</Link>
);
+17
View File
@@ -136,3 +136,20 @@ export interface RecentClassesReturn {
classes: ClassAchievementDocument[];
users: PublicUserDocument[];
}
export interface SongsReturn<I extends IDStrings = IDStrings> {
song: SongDocument<IDStringToGame[I]>;
charts: ChartDocument<I>[];
}
export interface ChartPBLeaderboardReturn<I extends IDStrings = IDStrings> {
users: PublicUserDocument[];
pbs: PBScoreDocument<I>[];
}
export interface UGPTChartLeaderboardAdjacent<I extends IDStrings = IDStrings> {
users: PublicUserDocument[];
pb: PBScoreDocument<I>;
adjacentAbove: PBScoreDocument<I>[];
adjacentBelow: PBScoreDocument<I>[];
}
+12 -1
View File
@@ -10,7 +10,12 @@ import {
} from "tachi-common";
export type PBDataset<I extends IDStrings = IDStrings> = (PBScoreDocument<I> & {
__related: { chart: ChartDocument<I>; song: SongDocument<IDStringToGame[I]>; index: integer };
__related: {
chart: ChartDocument<I>;
song: SongDocument<IDStringToGame[I]>;
index: integer;
user?: PublicUserDocument;
};
__playcount?: integer;
})[];
@@ -30,3 +35,9 @@ export type FolderDataset<I extends IDStrings = IDStrings> = (ChartDocument<I> &
user: PublicUserDocument;
};
})[];
export type ChartLeaderboardDataset<I extends IDStrings = IDStrings> = (PBScoreDocument<I> & {
__related: {
user: PublicUserDocument;
};
})[];
+2 -2
View File
@@ -48,9 +48,9 @@ export async function APIFetchV1<T = unknown>(
const rj = await res.json();
if (!rj.success) {
console.error(rj);
console.warn(rj);
} else {
console.log(rj.description);
console.debug(rj.description);
}
if (!rj.success && displayFailure) {
-2
View File
@@ -1,5 +1,3 @@
import { IDStrings } from "tachi-common";
export const neutralSplashes = ["A Rhythm Game Score Manager", "The Rhythm Game Score Manager"];
export const loggedInSplashes = [
+1
View File
@@ -47,6 +47,7 @@ export function CreateDefaultPBSearchParams<I extends IDStrings = IDStrings>(
percent: x => x.scoreData.percent,
ranking: x => x.rankingData.rank,
highlight: x => !!x.highlight,
username: x => x.__related.user?.username ?? null,
lamp: {
valueGetter: x => [x.scoreData.lamp, x.scoreData.lampIndex],
strToNum: HumanFriendlyStrToLampIndex(game, playtype),
+24
View File
@@ -0,0 +1,24 @@
import { Header } from "components/tables/components/TachiTable";
import IndicatorHeader from "components/tables/headers/IndicatorHeader";
import { PBDataset } from "types/tables";
import { StrSOV } from "util/sorts";
type PBHeader = Header<PBDataset[0]>;
export function GetPBLeadingHeaders(
showUser: boolean,
showChart: boolean,
chartHeader: PBHeader
): PBHeader[] {
const userHeader: PBHeader = ["User", "User", StrSOV(x => x.__related.user!.username)];
const songHeader: PBHeader = ["Song", "Song", StrSOV(x => x.__related.song.title)];
if (showUser && showChart) {
return [userHeader, chartHeader, IndicatorHeader, songHeader];
}
if (showUser && !showChart) {
return [IndicatorHeader, userHeader];
}
return [chartHeader, IndicatorHeader, songHeader];
}