Big Update 4

This commit is contained in:
zkldi
2021-09-20 18:12:48 +01:00
parent a91c6d1d94
commit f35b99649c
40 changed files with 917 additions and 210 deletions
+1 -1
View File
@@ -75,7 +75,7 @@
"resolve": "1.12.0",
"resolve-url-loader": "3.1.0",
"socicon": "3.0.5",
"tachi-common": "0.2.18"
"tachi-common": "0.2.25"
},
"devDependencies": {
"@types/humanize-duration": "3.25.1",
+4 -4
View File
@@ -77,7 +77,7 @@ specifiers:
serve: 11.2.0
socicon: 3.0.5
style-loader: 1.0.0
tachi-common: 0.2.18
tachi-common: 0.2.25
terser-webpack-plugin: 1.4.1
ts-pnp: 1.1.4
typescript: 4.3.5
@@ -142,7 +142,7 @@ dependencies:
resolve: 1.12.0
resolve-url-loader: 3.1.0
socicon: 3.0.5
tachi-common: 0.2.18
tachi-common: 0.2.25
devDependencies:
'@types/humanize-duration': 3.25.1
@@ -11178,8 +11178,8 @@ packages:
strip-ansi: 6.0.0
dev: true
/tachi-common/0.2.18:
resolution: {integrity: sha512-moa+Bmo6ilwJIyxQfHNkxNOKxQCgXFWezdLGwwvhCAzAfpLtiT0B6/QXKJUrkEVOqAy1ujRsLHZdK+NR87rFvg==}
/tachi-common/0.2.25:
resolution: {integrity: sha512-FSMwEWWcvP5Vw2heSD6vD7Iz1/lI+bO1HF0+0iURw5Ybj16yJZQ9wBiaIz2hotNweMhlQ73v57tj2TMUmFyG9w==}
dependencies:
monk: 7.3.4
transitivePeerDependencies:
+3 -1
View File
@@ -10,7 +10,9 @@ import { LoadingScreen } from "components/layout/screens/LoadingScreen";
import { QueryClientProvider, QueryClient } from "react-query";
import { UserSettingsContextProvider } from "context/UserSettingsContext";
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5 } } });
const queryClient = new QueryClient({
defaultOptions: { queries: { staleTime: 1000 * 60 * 5, retry: 1 } },
});
export default function App({ basename }: { basename: string }) {
return (
@@ -0,0 +1,28 @@
import useSetSubheader from "components/layout/header/useSetSubheader";
import Card from "components/layout/page/Card";
import DebugContent from "components/util/DebugContent";
import React from "react";
import { GetGameConfig, FormatGame, GetGamePTConfig } from "tachi-common";
import { GamePT } from "types/react";
export default function GPTDevInfo({ game, playtype }: GamePT) {
useSetSubheader(
["Games", GetGameConfig(game).name, playtype, "Dev Info"],
[game, playtype],
`${FormatGame(game, playtype)} Dev Info`
);
const gameConfig = GetGameConfig(game);
const gptConfig = GetGamePTConfig(game, playtype);
return (
<>
<Card header="Game Configuration">
<DebugContent data={gameConfig} />
</Card>
<Card className="mt-4" header="GPT Configuration">
<DebugContent data={gptConfig} />
</Card>
</>
);
}
@@ -0,0 +1,14 @@
import useSetSubheader from "components/layout/header/useSetSubheader";
import React from "react";
import { FormatGame, GetGameConfig } from "tachi-common";
import { GamePT } from "types/react";
export default function GPTLeaderboardsPage({ game, playtype }: GamePT) {
useSetSubheader(
["Games", GetGameConfig(game).name, playtype, "Leaderboards"],
[game, playtype],
`${FormatGame(game, playtype)} Leaderboards`
);
return <div>leaderboard</div>;
}
@@ -0,0 +1,317 @@
import { ResponsiveBar } from "@nivo/bar";
import { BarChartTooltip } from "components/charts/ChartTooltip";
import ClassBadge from "components/game/ClassBadge";
import useSetSubheader from "components/layout/header/useSetSubheader";
import Card from "components/layout/page/Card";
import ScoreTable from "components/tables/scores/ScoreTable";
import ApiError from "components/util/ApiError";
import AsyncLoader from "components/util/AsyncLoader";
import Divider from "components/util/Divider";
import Icon from "components/util/Icon";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import { TachiConfig } from "lib/config";
import React, { useEffect, useState } from "react";
import { Col, Form, Row } from "react-bootstrap";
import { Link } from "react-router-dom";
import {
ChartDocument,
ClassAchievementDocument,
FormatGame,
gameClasses,
GamePTConfig,
GetGameConfig,
GetGamePTConfig,
integer,
PublicUserDocument,
ScoreDocument,
SongDocument,
} from "tachi-common";
import { RecentClassesReturn } from "types/api-returns";
import { GamePT } from "types/react";
import { ScoreDataset } from "types/tables";
import { APIFetchV1 } from "util/api";
import { DEFAULT_BAR_PROPS } from "util/charts";
import { CreateChartMap, CreateSongMap, CreateUserMap } from "util/data";
import { UppercaseFirst } from "util/misc";
import { MillisToSince } from "util/time";
export default function GPTMainPage({ game, playtype }: GamePT) {
useSetSubheader(
["Games", GetGameConfig(game).name, playtype],
[game, playtype],
FormatGame(game, playtype)
);
const gptConfig = GetGamePTConfig(game, playtype);
return (
<>
<Card header="Recent Highlighted Scores">
<RecentHighlightedScoresComponent game={game} playtype={playtype} />
</Card>
{Object.keys(gptConfig.classHumanisedFormat).length !== 0 && (
<>
<Divider />
<Card header="Recently Achieved Classes">
<RecentClassesComponent game={game} playtype={playtype} />
</Card>
<Divider />
<Card header="Class Distributions">
<ClassDistributionComponent game={game} playtype={playtype} />
</Card>
</>
)}
</>
);
}
function RecentHighlightedScoresComponent({ game, playtype }: GamePT) {
const { data, isLoading, error } = useApiQuery<{
scores: ScoreDocument[];
users: PublicUserDocument[];
charts: ChartDocument[];
songs: SongDocument[];
}>(`/games/${game}/${playtype}/scores/highlighted`);
if (error) {
return <ApiError error={error} />;
}
if (!data || isLoading) {
return <Loading />;
}
const dataset: ScoreDataset = [];
const songMap = CreateSongMap(data.songs);
const chartMap = CreateChartMap(data.charts);
const userMap = CreateUserMap(data.users);
for (const [index, score] of data.scores.entries()) {
dataset.push({
...score,
__related: {
chart: chartMap.get(score.chartID)!,
song: songMap.get(score.songID)!,
index,
user: userMap.get(score.userID)!,
},
});
}
return (
<Row>
<Col className="text-center" xs={12}>
Here's what players have recently marked as highlighted scores!
</Col>
<Col xs={12}>
<Divider />
</Col>
<Col xs={12}>
<ScoreTable userCol dataset={dataset} game={game} playtype={playtype} />
</Col>
</Row>
);
}
function RecentClassesComponent({ game, playtype }: GamePT) {
const { data, isLoading, error } = useApiQuery<RecentClassesReturn>(
`/games/${game}/${playtype}/recent-classes`
);
if (error) {
return <ApiError error={error} />;
}
if (!data || isLoading) {
return <Loading />;
}
// if (data.classes.length === 0) {
// return <div className="text-center">Looks like nobodies got anything here... yet!</div>;
// }
const userMap = new Map<integer, PublicUserDocument>();
for (const user of data.users) {
userMap.set(user.id, user);
}
return (
<div>
{data.classes.length === 0 ? (
<span>Looks like nobodies ever got a class for this game. Ah well.</span>
) : (
data.classes.map((c, i) => {
const user = userMap.get(c.userID)!;
return <AchievedClassRow key={i} classInfo={c} user={user} />;
})
)}
</div>
);
}
function AchievedClassRow({
classInfo,
user,
}: {
classInfo: ClassAchievementDocument;
user: PublicUserDocument;
}) {
const game = classInfo.game;
const playtype = classInfo.playtype;
return (
<div className="py-2 timeline-element">
<Link to={`/dashboard/users/${user.username}/games/${game}/${playtype}`}>
{user.username}
</Link>{" "}
achieved{" "}
<ClassBadge
classSet={classInfo.classSet}
game={game}
playtype={playtype}
classValue={classInfo.classValue}
/>
{classInfo.classOldValue && (
<>
{" "}
(Raised from{" "}
<ClassBadge
classSet={classInfo.classSet}
game={game}
playtype={playtype}
classValue={classInfo.classOldValue}
/>
)
</>
)}
<span className="float-right d-none d-lg-inline">
<Icon type="clock" />
<span className="pl-1">{MillisToSince(classInfo.timeAchieved)}</span>
</span>
</div>
);
}
function ClassDistributionComponent({ game, playtype }: GamePT) {
const gptConfig = GetGamePTConfig(game, playtype);
const [className, setClassName] = useState<gameClasses.AllClassSets>(
Object.keys(gptConfig.classHumanisedFormat)[0] as gameClasses.AllClassSets
);
useEffect(() => {
const gptConfig = GetGamePTConfig(game, playtype);
setClassName(Object.keys(gptConfig.classHumanisedFormat)[0] as gameClasses.AllClassSets);
}, [game, playtype]);
return (
<Row>
<Col xs={12}>
This will show the distribution of classes among all the players on{" "}
{TachiConfig.name}!
</Col>
<Col xs={12}>
<Divider />
</Col>
<Col xs={12} lg={4} className="offset-lg-4">
<Form.Control
as="select"
value={className}
onChange={e => setClassName(e.target.value as gameClasses.AllClassSets)}
>
{Object.keys(gptConfig.classHumanisedFormat).map(e => (
<option value={e} key={e}>
{UppercaseFirst(e)}
</option>
))}
</Form.Control>
</Col>
<Col xs={12}>
<Divider />
</Col>
<Col xs={12}>
<AsyncLoader
promiseFn={async () => {
const res = await APIFetchV1<Record<string, integer>>(
`/games/${game}/${playtype}/class-distribution?class=${className}`
);
if (!res.success) {
throw new Error(res.description);
}
return res.body;
}}
>
{d => <ClassDistChart data={d} gptConfig={gptConfig} className={className} />}
</AsyncLoader>
</Col>
</Row>
);
}
function ClassDistChart({
data,
gptConfig,
className,
}: {
data: Record<string, integer>;
gptConfig: GamePTConfig;
className: gameClasses.AllClassSets;
}) {
const dataset = [];
for (const key in gptConfig.classHumanisedFormat[className]) {
dataset.push({
id: key,
count: data[key] ?? 0,
});
}
if (dataset.length === 0) {
return (
<div style={{ height: 400 }}>
<div className="d-flex justify-content-center align-items-center h-100">
<span>Looks like nobody has any of these classes yet.</span>
</div>
</div>
);
}
return (
<div style={{ height: 200 + dataset.length * 10 }}>
<ResponsiveBar
data={dataset}
keys={["count"]}
indexBy="id"
layout="horizontal"
margin={{ left: 80, bottom: 40, top: 20, right: 20 }}
colors={v => "#cc527a"}
axisLeft={{
format: v => gptConfig.classHumanisedFormat[className][v]?.display,
}}
tooltip={d => (
<BarChartTooltip
point={d}
renderFn={d => (
<div className="w-100 text-center">
{
gptConfig.classHumanisedFormat[className][
d.indexValue as number
].display
}
: {(d.data as any).count}
</div>
)}
/>
)}
{...DEFAULT_BAR_PROPS}
/>
</div>
);
}
@@ -0,0 +1,133 @@
import useSetSubheader from "components/layout/header/useSetSubheader";
import BMSDifficultyCell from "components/tables/cells/BMSDifficultyCell";
import DifficultyCell from "components/tables/cells/DifficultyCell";
import TitleCell from "components/tables/cells/TitleCell";
import TachiTable from "components/tables/components/TachiTable";
import ApiError from "components/util/ApiError";
import DebounceSearch from "components/util/DebounceSearch";
import DebugContent from "components/util/DebugContent";
import Divider from "components/util/Divider";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import React, { useState } from "react";
import { Col, Row } from "react-bootstrap";
import {
GetGameConfig,
FormatGame,
ChartDocument,
SongDocument,
integer,
GetGamePTConfig,
} from "tachi-common";
import { GamePT } from "types/react";
import { CreateSongMap } from "util/data";
import { NumericSOV, StrSOV } from "util/sorts";
export default function GPTSongsPage({ game, playtype }: GamePT) {
useSetSubheader(
["Games", GetGameConfig(game).name, playtype, "Songs"],
[game, playtype],
`${FormatGame(game, playtype)} Songs`
);
const [search, setSearch] = useState("");
return (
<Row>
<Col xs={12}>
<DebounceSearch
className="form-control-lg"
setSearch={setSearch}
placeholder="Search songs and charts..."
/>
<Divider />
</Col>
<Col xs={12}>
<SearchSongsTable game={game} search={search} playtype={playtype} />
</Col>
</Row>
);
}
function SearchSongsTable({ game, playtype, search }: { search: string } & GamePT) {
const params = new URLSearchParams({ search });
const { data, isLoading, error } = useApiQuery<{
charts: (ChartDocument & { __playcount: integer })[];
songs: SongDocument[];
}>(`/games/${game}/${playtype}/charts${search !== "" ? `?${params.toString()}` : ""}`);
if (error) {
return <ApiError error={error} />;
}
if (!data || isLoading) {
return <Loading />;
}
const songMap = CreateSongMap(data.songs);
const dataset = [];
for (const chart of data.charts) {
dataset.push({
...chart,
__related: {
song: songMap.get(chart.songID)!,
},
});
}
const gptConfig = GetGamePTConfig(game, playtype);
return (
<>
{search === "" && (
<div className="w-100 text-center">
Displaying the most played charts for {FormatGame(game, playtype)}
</div>
)}
<TachiTable
entryName="Charts"
dataset={dataset}
headers={[
[
"Chart",
"Chart",
NumericSOV(chart => {
for (const tierlist of gptConfig.tierlists) {
if (chart.tierlistInfo[tierlist]) {
return chart.tierlistInfo[tierlist]!.value;
}
}
return chart.levelNum;
}),
],
["Song Title", "Song", StrSOV(x => x.__related.song.title)],
["Playcount", "Playcount", NumericSOV(x => x.__playcount)],
]}
searchFunctions={{
title: x => x.__related.song.title,
artist: x => x.__related.song.artist,
playcount: x => x.__playcount,
difficulty: x => x.difficulty,
level: x => x.levelNum,
}}
rowFunction={d => (
<tr>
{game === "bms" ? (
<BMSDifficultyCell
chart={(d as unknown) as ChartDocument<"bms:7K" | "bms:14K">}
/>
) : (
<DifficultyCell game={game} chart={d} />
)}
<TitleCell chart={d} game={game} song={d.__related.song} />
<td>{d.__playcount}</td>
</tr>
)}
/>
</>
);
}
@@ -215,6 +215,7 @@ function RecentSessionScoreInfo({
chart,
song: songMap.get(chart.songID)!,
index: 0,
user: reqUser,
},
});
}
@@ -245,7 +246,6 @@ function RecentSessionScoreInfo({
dataset={dataset as any}
pageLen={5}
game={game}
reqUser={reqUser}
playtype={playtype as any}
/>
</div>
@@ -140,7 +140,7 @@ function AlgSelector({
);
}
function useFetchPBs(url: string) {
function useFetchPBs(url: string, reqUser: PublicUserDocument) {
const { isLoading, error, data } = useQuery(url, async () => {
const res = await APIFetchV1<{
pbs: PBScoreDocument<"iidx:SP">[];
@@ -152,7 +152,7 @@ function useFetchPBs(url: string) {
throw res;
}
return FormatData(res.body.pbs, res.body.songs, res.body.charts);
return FormatData(res.body.pbs, res.body.songs, res.body.charts, reqUser);
});
return { isLoading, error: error as UnsuccessfulAPIResponse, data };
@@ -175,7 +175,7 @@ function PBsOverview({
} & GamePT) {
const [search, setSearch] = useState("");
const { isLoading, error, data } = useFetchPBs(url);
const { isLoading, error, data } = useFetchPBs(url, reqUser);
return (
<div className="row">
@@ -214,7 +214,7 @@ function FormatData<
D extends PBScoreDocument | ScoreDocument,
I extends IDStrings = IDStrings,
G extends Game = Game
>(d: D[], songs: SongDocument<G>[], charts: ChartDocument<I>[]) {
>(d: D[], songs: SongDocument<G>[], charts: ChartDocument<I>[], reqUser: PublicUserDocument) {
const songMap = new Map();
const chartMap = new Map();
@@ -232,13 +232,14 @@ function FormatData<
song: songMap.get(e.songID),
chart: chartMap.get(e.chartID),
index: i,
user: reqUser,
},
}));
return data;
}
function useFetchScores(url: string) {
function useFetchScores(url: string, reqUser: PublicUserDocument) {
const { isLoading, error, data } = useQuery(url, async () => {
const res = await APIFetchV1<{
scores: ScoreDocument[];
@@ -250,7 +251,7 @@ function useFetchScores(url: string) {
throw res;
}
return FormatData(res.body.scores, res.body.songs, res.body.charts);
return FormatData(res.body.scores, res.body.songs, res.body.charts, reqUser);
});
return { isLoading, error: error as UnsuccessfulAPIResponse, data };
@@ -268,7 +269,8 @@ function PBsSearch({
alg?: ScoreCalculatedDataLookup[IDStrings];
} & GamePT) {
const { isLoading, error, data } = useFetchPBs(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs?search=${search}`
`/users/${reqUser.id}/games/${game}/${playtype}/pbs?search=${search}`,
reqUser
);
return (
@@ -289,7 +291,8 @@ function ScoresOverview({ reqUser, game, playtype }: { reqUser: PublicUserDocume
const [search, setSearch] = useState("");
const { isLoading, error, data } = useFetchScores(
`/users/${reqUser.id}/games/${game}/${playtype}/scores/recent`
`/users/${reqUser.id}/games/${game}/${playtype}/scores/recent`,
reqUser
);
return (
@@ -307,12 +310,7 @@ function ScoresOverview({ reqUser, game, playtype }: { reqUser: PublicUserDocume
style={{ height: 500 }}
{...{ isLoading, dataset: data, error }}
>
<ScoreTable
dataset={data!}
game={game}
reqUser={reqUser}
playtype={playtype as any}
/>
<ScoreTable dataset={data!} game={game} playtype={playtype as any} />
</LoadingWrapper>
) : (
<ScoresSearch {...{ reqUser, game, playtype, search }} />
@@ -329,12 +327,13 @@ function ScoresSearch({
search,
}: { reqUser: PublicUserDocument; search: string } & GamePT) {
const { isLoading, error, data } = useFetchScores(
`/users/${reqUser.id}/games/${game}/${playtype}/scores?search=${search}`
`/users/${reqUser.id}/games/${game}/${playtype}/scores?search=${search}`,
reqUser
);
return (
<LoadingWrapper style={{ height: 500 }} {...{ isLoading, error, dataset: data }}>
<ScoreTable dataset={data!} game={game} reqUser={reqUser} playtype={playtype as any} />
<ScoreTable dataset={data!} game={game} playtype={playtype as any} />
</LoadingWrapper>
);
}
@@ -140,7 +140,7 @@ function FolderInfoComponent({
Lamps
</SelectButton>
</div>
<Divider className="my-4" />
<Divider />
{dataset}
</div>
</div>
@@ -55,14 +55,14 @@ export default function FolderTablePage({ reqUser, game, playtype }: Props) {
}
}, [data]);
if (isLoading || !data) {
return <Loading />;
}
if (error) {
return <ApiError error={error} />;
}
if (isLoading || !data) {
return <Loading />;
}
return (
<>
<InputGroup>
@@ -82,7 +82,7 @@ export default function FolderTablePage({ reqUser, game, playtype }: Props) {
))}
</Form.Control>
</InputGroup>
<Divider className="my-4" />
<Divider />
{table && <TableFolderViewer {...{ reqUser, game, playtype, table }} />}
</>
);
@@ -118,18 +118,18 @@ function TableFolderViewer({ reqUser, game, playtype, table }: Props & { table:
}
}, [data]);
if (isLoading || !data || !hasLoadedFolderMap) {
return <Loading />;
}
if (error) {
return <ApiError error={error} />;
}
if (isLoading || !data || !hasLoadedFolderMap) {
return <Loading />;
}
return (
<>
<TableBarChart table={table} dataMap={dataMap} />
<Divider className="my-4" />
<Divider />
<TableFolderTable
reqUser={reqUser}
game={game}
@@ -267,7 +267,7 @@ function TableBarChart({
</div>
</div>
<div className="col-12">
<Divider className="my-4" />
<Divider />
</div>
<div className="col-12">
@@ -67,6 +67,7 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
__related: {
pb: pbMap.get(chart.chartID) ?? null,
song: songMap.get(chart.songID)!,
user: reqUser,
},
});
}
@@ -92,14 +93,14 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
);
}, [folderDataset]);
if (isLoading || !data || !folderDataset) {
return <Loading />;
}
if (error) {
return <ApiError error={error} />;
}
if (isLoading || !data || !folderDataset) {
return <Loading />;
}
return (
<div className="row">
<div className="col-12">
@@ -107,7 +108,7 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
</div>
<div className="col-12">{folderInfoHeader}</div>
<div className="col-12">
<Divider className="my-4" />
<Divider />
</div>
<div className="col-12 d-flex">
<div className="btn-group mx-auto">
@@ -124,7 +125,7 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
</div>
</div>
<div className="col-12">
<Divider className="my-4" />
<Divider />
</div>
<div className="col-12">
{mode === "normal" ? (
@@ -174,7 +175,7 @@ function TierlistBreakdown({ game, folderDataset, playtype, reqUser }: InfoProps
<Col xs={12}>
<Card header="Tierlist View Configuration">
<span>Here you can select what tierlist blocks to show!</span>
<Divider className="my-4" />
<Divider />
{gptConfig.tierlists.length > 1 &&
gptConfig.tierlists.map(e => (
<>
@@ -201,7 +202,7 @@ function TierlistBreakdown({ game, folderDataset, playtype, reqUser }: InfoProps
</Card>
</Col>
<Col xs={12}>
<Divider className="my-4" />
<Divider />
</Col>
<Col xs={12}>
<TierlistInfoLadder
+4
View File
@@ -26,6 +26,10 @@ export function Routes() {
{!user && ClientConfig.MANDATE_LOGIN ? <LoginPage /> : <Redirect to="/dashboard" />}
</Route>
<Route path="/michael">
<ErrorPage statusCode={999} customMessage="He has been summoned." />
</Route>
<Route path="/dashboard">
{!user && ClientConfig.MANDATE_LOGIN ? <LoginPage /> : <DashboardRoutes />}
</Route>
+37 -7
View File
@@ -1,8 +1,14 @@
import PlaytypeSelect from "app/pages/dashboard/games/_game/PlaytypeSelect";
import GPTSongsPage from "app/pages/dashboard/games/_game/_playtype/GPTSongsPage";
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 { ErrorPage } from "app/pages/ErrorPage";
import { GPTBottomNav, GPTHeaderBody } from "components/game/GPTHeader";
import LayoutHeaderContainer from "components/layout/LayoutHeaderContainer";
import React from "react";
import { Redirect, Route, Switch, useParams } from "react-router-dom";
import { Game, GetGameConfig } from "tachi-common";
import { FormatGame, Game, GetGameConfig } from "tachi-common";
import { IsSupportedGame, IsSupportedPlaytype } from "util/asserts";
export default function GameRoutes() {
@@ -29,7 +35,7 @@ export default function GameRoutes() {
)}
</Route>
<Route exact path="/dashboard/games/:game/:playtype">
<Route path="/dashboard/games/:game/:playtype">
<GamePlaytypeRoutes game={game} />
</Route>
@@ -53,10 +59,34 @@ function GamePlaytypeRoutes({ game }: { game: Game }) {
}
return (
<Switch>
<Route exact path="/dashboard/games/:game/:playtype">
{game} {playtype}
</Route>
</Switch>
<>
<LayoutHeaderContainer
footer={<GPTBottomNav baseUrl={`/dashboard/games/${game}/${playtype}`} />}
header={FormatGame(game, playtype)}
>
<GPTHeaderBody game={game} playtype={playtype} />
</LayoutHeaderContainer>
<Switch>
<Route exact path="/dashboard/games/:game/:playtype">
<GPTMainPage game={game} playtype={playtype} />
</Route>
<Route exact path="/dashboard/games/:game/:playtype/songs">
<GPTSongsPage 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>
<Route path="*">
<ErrorPage statusCode={404} />
</Route>
</Switch>
</>
);
}
+9 -9
View File
@@ -9,9 +9,9 @@ import UserGamesPage from "app/pages/dashboard/users/UserGamesPage";
import UserSettingsPage from "app/pages/dashboard/users/UserSettingsPage";
import { ErrorPage } from "app/pages/ErrorPage";
import RequireAuthAsUserParam from "components/auth/RequireAuthAsUserParam";
import LayoutHeaderContainer from "components/layout/LayoutHeaderContainer";
import { UGPTBottomNav, UGPTHeaderBody } from "components/user/UGPTHeader";
import { UserBottomNav, UserHeaderBody } from "components/user/UserHeader";
import UserHeaderContainer from "components/user/UserHeaderContainer";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import { BackgroundContext } from "context/BackgroundContext";
@@ -52,10 +52,6 @@ export default function UserRoutes() {
};
}, [reqUser]);
if (isLoading || !reqUser) {
return null;
}
if (error && error.statusCode === 404) {
return <ErrorPage statusCode={404} customMessage="This user does not exist!" />;
}
@@ -64,6 +60,10 @@ export default function UserRoutes() {
return <ErrorPage statusCode={error.statusCode} customMessage={error.description} />;
}
if (isLoading || !reqUser) {
return null;
}
return (
<Switch>
<Route path="/dashboard/users/:userID">
@@ -81,7 +81,7 @@ export default function UserRoutes() {
function UserProfileRoutes({ reqUser }: { reqUser: PublicUserDocument }) {
return (
<>
<UserHeaderContainer
<LayoutHeaderContainer
header={`${reqUser.username}'s Profile`}
footer={
<UserBottomNav
@@ -91,7 +91,7 @@ function UserProfileRoutes({ reqUser }: { reqUser: PublicUserDocument }) {
}
>
<UserHeaderBody reqUser={reqUser} />
</UserHeaderContainer>
</LayoutHeaderContainer>
<Route exact path="/dashboard/users/:userID">
<UserPage reqUser={reqUser} />
</Route>
@@ -211,7 +211,7 @@ function UserGamePlaytypeRoutes({ reqUser, game }: { reqUser: PublicUserDocument
return (
<>
<UserHeaderContainer
<LayoutHeaderContainer
header={`${reqUser.username}'s ${FormatGame(game, playtype)} Profile`}
footer={
<UGPTBottomNav
@@ -221,7 +221,7 @@ function UserGamePlaytypeRoutes({ reqUser, game }: { reqUser: PublicUserDocument
}
>
<UGPTHeaderBody reqUser={reqUser} game={game} playtype={playtype} stats={stats} />
</UserHeaderContainer>
</LayoutHeaderContainer>
<Switch>
<Route exact path="/dashboard/users/:userID/games/:game/:playtype">
<OverviewPage reqUser={reqUser} game={game} playtype={playtype} />
+72
View File
@@ -0,0 +1,72 @@
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);
const navItems = [
<NavItem key="overview" to={`${baseUrl}/`}>
Overview
</NavItem>,
<NavItem key="songs" to={`${baseUrl}/songs`}>
Songs
</NavItem>,
<NavItem key="leaderboards" to={`${baseUrl}/leaderboards`}>
Leaderboards
</NavItem>,
];
if (settings?.preferences.developerMode) {
navItems.push(
<NavItem key="dev-info" to={`${baseUrl}/dev-info`}>
Developer Info
</NavItem>
);
}
return (
<div className="row align-items-center mb-0">
<Navbar>{navItems}</Navbar>
</div>
);
}
@@ -0,0 +1,17 @@
import React from "react";
import { Link } from "react-router-dom";
import { PublicUserDocument } from "tachi-common";
import { GamePT } from "types/react";
export default function UserCell({ user, game, playtype }: { user: PublicUserDocument } & GamePT) {
return (
<td>
<Link
className="gentle-link"
to={`/dashboard/users/${user.username}/games/${game}/${playtype}`}
>
{user.username}
</Link>
</td>
);
}
@@ -36,14 +36,14 @@ export interface ScoreDropdownProps<I extends IDStrings = IDStrings> {
export default function GenericScoreDropdown<I extends IDStrings = IDStrings>({
game,
playtype,
reqUser,
user,
chart,
scoreState,
thisScore,
defaultView = "moreInfo",
DocComponent = props => GenericScoreContentDropdown({ renderScoreInfo: false, ...props }),
}: {
reqUser: PublicUserDocument;
user: PublicUserDocument;
chart: ChartDocument;
scoreState: ScoreState;
defaultView?: "vsPB" | "moreInfo" | "history" | "debug";
@@ -57,12 +57,12 @@ export default function GenericScoreDropdown<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/${user.id}/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/${user.id}/games/${game}/${playtype}/scores/${chart.chartID}`);
if (error) {
return <>An error has occured. Whoops.</>;
@@ -12,12 +12,12 @@ export default function IIDXScoreDropdown({
thisScore,
game,
playtype,
reqUser,
user,
chart,
scoreState,
}: {
thisScore: ScoreDocument<"iidx:SP" | "iidx:DP">;
reqUser: PublicUserDocument;
user: PublicUserDocument;
chart: ChartDocument<"iidx:SP" | "iidx:DP">;
scoreState: {
highlight: boolean;
@@ -27,7 +27,7 @@ export default function IIDXScoreDropdown({
};
} & GamePT) {
const { isLoading, error, data } = useApiQuery<UGPTChartPBComposition<"iidx:SP" | "iidx:DP">>(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`
`/users/${user.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`
);
if (error) {
@@ -44,7 +44,7 @@ export default function IIDXScoreDropdown({
return (
<GenericScoreDropdown<"iidx:SP" | "iidx:DP">
{...{ game, playtype, chart, reqUser, scoreState, thisScore }}
{...{ game, playtype, chart, reqUser: user, scoreState, thisScore, user }}
DocComponent={props =>
GenericScoreContentDropdown({
...props,
@@ -16,14 +16,14 @@ export default function PlayHistory({
isLoading: boolean;
error: UnsuccessfulAPIFetchResponse | null;
} & GamePT) {
if (isLoading || !data) {
return <Loading />;
}
if (error) {
return <>{error.description}</>;
}
if (isLoading || !data) {
return <Loading />;
}
return (
<div className="col-12 col-lg-10">
<HistoryScoreTable dataset={data} game={game} playtype={playtype} />
@@ -77,7 +77,7 @@ function Row({
game: Game;
}) {
return (
<tr className={sc.highlight ? "highlighted-row" : ""}>
<tr>
<ScoreCoreCells score={sc} game={game} rating={rating as any} />
<TimestampCell time={sc.timeAchieved} service={sc.service} />
</tr>
@@ -143,7 +143,6 @@ function Row({
scoreState={scoreState}
/>
}
className={scoreState.highlight ? "highlighted-row" : ""}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<BMSDifficultyCell chart={pb.__related.chart} />
@@ -109,7 +109,7 @@ function Row({
scoreState={scoreState}
/>
}
className={scoreState.highlight ? "highlighted-row" : ""}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<DifficultyCell game={pb.game} chart={pb.__related.chart} />
@@ -97,7 +97,6 @@ function Row({
scoreState={scoreState}
/>
}
className={scoreState.highlight ? "highlighted-row" : ""}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<DifficultyCell game="museca" chart={pb.__related.chart} />
@@ -96,7 +96,7 @@ function Row({
scoreState={scoreState}
/>
}
className={scoreState.highlight ? "highlighted-row" : ""}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<DifficultyCell game="sdvx" chart={pb.__related.chart} />
@@ -8,6 +8,7 @@ import BMSDifficultyCell from "../cells/BMSDifficultyCell";
import IndicatorsCell from "../cells/IndicatorsCell";
import TimestampCell from "../cells/TimestampCell";
import TitleCell from "../cells/TitleCell";
import UserCell from "../cells/UserCell";
import DropdownRow from "../components/DropdownRow";
import TachiTable, { Header } from "../components/TachiTable";
import { useScoreState } from "../components/UseScoreState";
@@ -16,46 +17,50 @@ import BMSScoreCoreCells from "../game-core-cells/BMSScoreCoreCells";
import IndicatorHeader from "../headers/IndicatorHeader";
export default function BMSScoreTable({
reqUser,
dataset,
pageLen,
playtype,
userCol = false,
}: {
reqUser: PublicUserDocument;
dataset: ScoreDataset<"bms:7K" | "bms:14K">;
pageLen?: integer;
playtype: Playtypes["bms"];
userCol?: boolean;
}) {
const headers: Header<ScoreDataset<"bms:7K" | "bms:14K">[0]>[] = [
[
"Chart",
"Chart",
NumericSOV(
x =>
x.__related.chart.tierlistInfo["sgl-EC"]?.value ??
x.__related.chart.tierlistInfo["sgl-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)],
["Sieglinde", "sgl.", NumericSOV(x => x.calculatedData.sieglinde ?? 0)],
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
];
if (userCol) {
headers.unshift(["User", "User", StrSOV(x => x.__related.user.username)]);
}
return (
<TachiTable
dataset={dataset}
pageLen={pageLen}
headers={
[
[
"Chart",
"Chart",
NumericSOV(
x =>
x.__related.chart.tierlistInfo["sgl-EC"]?.value ??
x.__related.chart.tierlistInfo["sgl-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)],
["Sieglinde", "sgl.", NumericSOV(x => x.calculatedData.sieglinde ?? 0)],
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
] as Header<ScoreDataset<"bms:7K" | "bms:14K">[0]>[]
}
headers={headers}
entryName="Scores"
searchFunctions={CreateDefaultScoreSearchParams("bms", playtype)}
rowFunction={sc => (
<Row playtype={playtype} key={sc.scoreID} sc={sc} reqUser={reqUser} />
<Row playtype={playtype} key={sc.scoreID} sc={sc} userCol={userCol} />
)}
/>
);
@@ -63,22 +68,21 @@ export default function BMSScoreTable({
function Row({
sc,
reqUser,
playtype,
userCol,
}: {
sc: ScoreDataset<"bms:7K" | "bms:14K">[0];
reqUser: PublicUserDocument;
playtype: Playtype;
userCol: boolean;
}) {
const scoreState = useScoreState(sc);
return (
<DropdownRow
className={scoreState.highlight ? "highlighted-row" : ""}
dropdown={
<GenericScoreDropdown
chart={sc.__related.chart}
reqUser={reqUser}
user={sc.__related.user}
game={sc.game}
thisScore={sc}
playtype={sc.playtype}
@@ -86,6 +90,7 @@ function Row({
/>
}
>
{userCol && <UserCell game={sc.game} playtype={sc.playtype} user={sc.__related.user} />}
<BMSDifficultyCell chart={sc.__related.chart} />
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell
@@ -7,8 +7,9 @@ import { CreateDefaultScoreSearchParams } from "util/tables/create-search";
import DifficultyCell from "../cells/DifficultyCell";
import IndicatorsCell from "../cells/IndicatorsCell";
import TimestampCell from "../cells/TimestampCell";
import UserCell from "../cells/UserCell";
import DropdownRow from "../components/DropdownRow";
import TachiTable from "../components/TachiTable";
import TachiTable, { Header } from "../components/TachiTable";
import { useScoreState } from "../components/UseScoreState";
import GenericScoreDropdown from "../dropdowns/GenericScoreDropdown";
import GenericScoreCoreCells from "../game-core-cells/GenericScoreCoreCells";
@@ -21,37 +22,51 @@ export default function GenericScoreTable({
playtype,
showScore,
game,
userCol = false,
}: {
reqUser: PublicUserDocument;
dataset: ScoreDataset;
pageLen?: integer;
playtype: Playtype;
game: Game;
userCol?: boolean;
showScore?: boolean;
}) {
const gptConfig = GetGamePTConfig(game, playtype);
const headers: Header<ScoreDataset[0]>[] = [
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
IndicatorHeader,
["Song", "Song", StrSOV(x => x.__related.song.title)],
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
[
gptConfig.defaultScoreRatingAlg,
gptConfig.defaultScoreRatingAlg,
NumericSOV(x => x.calculatedData[gptConfig.defaultScoreRatingAlg] ?? 0),
],
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
];
if (userCol) {
headers.unshift(["User", "User", StrSOV(x => x.__related.user.username)]);
}
return (
<TachiTable
dataset={dataset}
pageLen={pageLen}
headers={[
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
IndicatorHeader,
["Song", "Song", StrSOV(x => x.__related.song.title)],
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
[
gptConfig.defaultScoreRatingAlg,
gptConfig.defaultScoreRatingAlg,
NumericSOV(x => x.calculatedData[gptConfig.defaultScoreRatingAlg] ?? 0),
],
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
]}
headers={headers}
entryName="Scores"
searchFunctions={CreateDefaultScoreSearchParams(game, playtype)}
rowFunction={sc => (
<Row key={sc.scoreID} sc={sc} reqUser={reqUser} showScore={showScore} />
<Row
key={sc.scoreID}
sc={sc}
reqUser={reqUser}
showScore={showScore}
userCol={userCol}
/>
)}
/>
);
@@ -61,20 +76,22 @@ function Row({
sc,
reqUser,
showScore,
userCol,
}: {
sc: ScoreDataset[0];
reqUser: PublicUserDocument;
showScore?: boolean;
userCol: boolean;
}) {
const scoreState = useScoreState(sc);
return (
<DropdownRow
className={scoreState.highlight ? "highlighted-row" : ""}
dropdown={
<GenericScoreDropdown
chart={sc.__related.chart}
reqUser={reqUser}
user={sc.__related.user}
game={sc.game}
thisScore={sc}
playtype={sc.playtype}
@@ -82,6 +99,7 @@ function Row({
/>
}
>
{userCol && <UserCell game={sc.game} playtype={sc.playtype} user={sc.__related.user} />}
<DifficultyCell chart={sc.__related.chart} game={sc.game} />
<IndicatorsCell highlight={scoreState.highlight} />
<GenericScoreCoreCells sc={sc} showScore={showScore} />
@@ -10,6 +10,7 @@ import DifficultyCell from "../cells/DifficultyCell";
import IndicatorsCell from "../cells/IndicatorsCell";
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";
@@ -19,60 +20,64 @@ import IIDXScoreCoreCells from "../game-core-cells/IIDXScoreCoreCells";
import IndicatorHeader from "../headers/IndicatorHeader";
export default function IIDXScoreTable({
reqUser,
dataset,
pageLen,
playtype,
userCol = false,
}: {
reqUser: PublicUserDocument;
dataset: ScoreDataset<"iidx:SP" | "iidx:DP">;
pageLen?: integer;
playtype: Playtypes["iidx"];
userCol?: boolean;
}) {
const defaultRating = useScoreRatingAlg<"iidx:SP" | "iidx:DP">("iidx", playtype);
const [rating, setRating] = useState<ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"]>(
defaultRating
);
const headers: Header<ScoreDataset<"iidx:SP" | "iidx:DP">[0]>[] = [
[
"Chart",
"Chart",
NumericSOV(
x =>
x.__related.chart.tierlistInfo["kt-NC"]?.value ??
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)],
[
"Rating",
"Rating",
NumericSOV(x => x.calculatedData[rating] ?? 0),
(thProps: ZTableTHProps) => (
<SelectableRating<"iidx:SP" | "iidx:DP">
key={nanoid()}
game="iidx"
playtype={playtype}
rating={rating}
setRating={setRating}
{...thProps}
/>
),
],
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
];
if (userCol) {
headers.unshift(["User", "User", StrSOV(x => x.__related.user.username)]);
}
return (
<TachiTable
dataset={dataset}
pageLen={pageLen}
headers={
[
[
"Chart",
"Chart",
NumericSOV(
x =>
x.__related.chart.tierlistInfo["kt-NC"]?.value ??
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)],
[
"Rating",
"Rating",
NumericSOV(x => x.calculatedData[rating] ?? 0),
(thProps: ZTableTHProps) => (
<SelectableRating<"iidx:SP" | "iidx:DP">
key={nanoid()}
game="iidx"
playtype={playtype}
rating={rating}
setRating={setRating}
{...thProps}
/>
),
],
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
] as Header<ScoreDataset<"iidx:SP" | "iidx:DP">[0]>[]
}
headers={headers}
entryName="Scores"
searchFunctions={CreateDefaultScoreSearchParams("iidx", playtype)}
rowFunction={sc => (
@@ -80,8 +85,8 @@ export default function IIDXScoreTable({
key={sc.scoreID}
playtype={playtype}
sc={sc}
reqUser={reqUser}
rating={rating}
userCol={userCol}
/>
)}
/>
@@ -90,31 +95,32 @@ export default function IIDXScoreTable({
function Row({
sc,
reqUser,
rating,
playtype,
userCol,
}: {
sc: ScoreDataset<"iidx:SP" | "iidx:DP">[0];
reqUser: PublicUserDocument;
rating: ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"];
playtype: Playtype;
userCol: boolean;
}) {
const scoreState = useScoreState(sc);
return (
<DropdownRow
className={scoreState.highlight ? "highlighted-row" : ""}
dropdown={
<IIDXScoreDropdown
chart={sc.__related.chart}
game="iidx"
playtype={sc.playtype}
reqUser={reqUser}
user={sc.__related.user}
thisScore={sc}
scoreState={scoreState}
/>
}
>
{userCol && <UserCell game={sc.game} playtype={sc.playtype} user={sc.__related.user} />}
<DifficultyCell chart={sc.__related.chart} game="iidx" />
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell
@@ -7,6 +7,7 @@ import DifficultyCell from "../cells/DifficultyCell";
import IndicatorsCell from "../cells/IndicatorsCell";
import TimestampCell from "../cells/TimestampCell";
import TitleCell from "../cells/TitleCell";
import UserCell from "../cells/UserCell";
import DropdownRow from "../components/DropdownRow";
import TachiTable, { Header } from "../components/TachiTable";
import { useScoreState } from "../components/UseScoreState";
@@ -18,32 +19,38 @@ export default function MusecaScoreTable({
reqUser,
dataset,
pageLen,
userCol = false,
}: {
reqUser: PublicUserDocument;
dataset: ScoreDataset<"museca:Single">;
pageLen?: integer;
playtype: Playtypes["museca"];
userCol: boolean;
}) {
const headers: Header<ScoreDataset<"museca:Single">[0]>[] = [
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
IndicatorHeader,
["Song", "Song", StrSOV(x => x.__related.song.title)],
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Near-Miss", "Nr. Ms.", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
["KtRating", "KtRating", NumericSOV(x => x.calculatedData.ktRating ?? 0)],
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
];
if (userCol) {
headers.unshift(["User", "User", StrSOV(x => x.__related.user.username)]);
}
return (
<TachiTable
dataset={dataset}
pageLen={pageLen}
headers={
[
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
IndicatorHeader,
["Song", "Song", StrSOV(x => x.__related.song.title)],
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Near-Miss", "Nr. Ms.", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
["KtRating", "KtRating", NumericSOV(x => x.calculatedData.ktRating ?? 0)],
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
] as Header<ScoreDataset<"museca:Single">[0]>[]
}
headers={headers}
entryName="Scores"
searchFunctions={CreateDefaultScoreSearchParams("museca", "Single")}
rowFunction={sc => <Row key={sc.scoreID} sc={sc} reqUser={reqUser} />}
rowFunction={sc => <Row key={sc.scoreID} sc={sc} reqUser={reqUser} userCol={userCol} />}
/>
);
}
@@ -51,19 +58,21 @@ export default function MusecaScoreTable({
function Row({
sc,
reqUser,
userCol,
}: {
sc: ScoreDataset<"museca:Single">[0];
reqUser: PublicUserDocument;
userCol: boolean;
}) {
const scoreState = useScoreState(sc);
return (
<DropdownRow
className={scoreState.highlight ? "highlighted-row" : ""}
dropdown={
<GenericScoreDropdown
chart={sc.__related.chart}
reqUser={reqUser}
user={sc.__related.user}
game={sc.game}
thisScore={sc}
playtype={sc.playtype}
@@ -71,6 +80,7 @@ function Row({
/>
}
>
{userCol && <UserCell game={sc.game} playtype={sc.playtype} user={sc.__related.user} />}
<DifficultyCell chart={sc.__related.chart} game="museca" />
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell
@@ -7,6 +7,7 @@ import DifficultyCell from "../cells/DifficultyCell";
import IndicatorsCell from "../cells/IndicatorsCell";
import TimestampCell from "../cells/TimestampCell";
import TitleCell from "../cells/TitleCell";
import UserCell from "../cells/UserCell";
import DropdownRow from "../components/DropdownRow";
import TachiTable, { Header } from "../components/TachiTable";
import { useScoreState } from "../components/UseScoreState";
@@ -18,46 +19,59 @@ export default function SDVXScoreTable({
reqUser,
dataset,
pageLen,
userCol = false,
}: {
reqUser: PublicUserDocument;
dataset: ScoreDataset<"sdvx:Single">;
pageLen?: integer;
playtype: Playtypes["sdvx"];
userCol?: boolean;
}) {
const headers: Header<ScoreDataset<"sdvx:Single">[0]>[] = [
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
IndicatorHeader,
["Song", "Song", StrSOV(x => x.__related.song.title)],
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Near - Miss", "Nr. Ms.", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
["VF6", "VF6", NumericSOV(x => x.calculatedData.VF6 ?? 0)],
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
];
if (userCol) {
headers.unshift(["User", "User", StrSOV(x => x.__related.user.username)]);
}
return (
<TachiTable
dataset={dataset}
pageLen={pageLen}
headers={
[
["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
IndicatorHeader,
["Song", "Song", StrSOV(x => x.__related.song.title)],
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Near - Miss", "Nr. Ms.", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
["VF6", "VF6", NumericSOV(x => x.calculatedData.VF6 ?? 0)],
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
] as Header<ScoreDataset<"sdvx:Single">[0]>[]
}
headers={headers}
entryName="Scores"
searchFunctions={CreateDefaultScoreSearchParams("museca", "Single")}
rowFunction={sc => <Row key={sc.scoreID} sc={sc} reqUser={reqUser} />}
rowFunction={sc => <Row key={sc.scoreID} sc={sc} reqUser={reqUser} userCol={userCol} />}
/>
);
}
function Row({ sc, reqUser }: { sc: ScoreDataset<"sdvx:Single">[0]; reqUser: PublicUserDocument }) {
function Row({
sc,
reqUser,
userCol,
}: {
sc: ScoreDataset<"sdvx:Single">[0];
reqUser: PublicUserDocument;
userCol: boolean;
}) {
const scoreState = useScoreState(sc);
return (
<DropdownRow
className={scoreState.highlight ? "highlighted-row" : ""}
dropdown={
<GenericScoreDropdown
chart={sc.__related.chart}
reqUser={reqUser}
user={sc.__related.user}
game={sc.game}
thisScore={sc}
playtype={sc.playtype}
@@ -65,6 +79,7 @@ function Row({ sc, reqUser }: { sc: ScoreDataset<"sdvx:Single">[0]; reqUser: Pub
/>
}
>
{userCol && <UserCell game={sc.game} playtype={sc.playtype} user={sc.__related.user} />}
<DifficultyCell chart={sc.__related.chart} game="sdvx" />
<IndicatorsCell highlight={scoreState.highlight} />
<TitleCell
@@ -11,21 +11,21 @@ import MusecaScoreTable from "./MusecaScoreTable";
export default function ScoreTable({
dataset,
indexCol = true,
reqUser,
userCol = false,
playtype,
pageLen = 10,
game,
}: {
dataset: ScoreDataset;
indexCol?: boolean;
reqUser: PublicUserDocument;
userCol?: boolean;
pageLen?: integer;
} & 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, pageLen } as any;
const props = { dataset, indexCol, playtype, pageLen, userCol } as any;
if (game === "iidx") {
return <IIDXScoreTable {...props} />;
} else if (game === "bms") {
@@ -62,14 +62,14 @@ export default function UGPTStatContainer({
}
);
if (isLoading || !data) {
return <Loading />;
}
if (error) {
return <>{(error as any).description}</>;
}
if (isLoading || !data) {
return <Loading />;
}
return (
<StatDisplay
game={game}
+1 -1
View File
@@ -1,5 +1,5 @@
import React from "react";
export default function Divider({ className }: { className?: string }) {
export default function Divider({ className = "my-4" }: { className?: string }) {
return <div className={`separator separator-solid opacity-7 ${className ?? ""}`}></div>;
}
@@ -15,13 +15,13 @@ export default function LoadingWrapper({
children: JSX.Element | JSX.Element[];
style?: CSSProperties;
}) {
if (isLoading || !dataset) {
return <Loading style={style} />;
}
if (error) {
return <h3>An error has occured. {error.description}</h3>;
}
if (isLoading || !dataset) {
return <Loading style={style} />;
}
return <>{children}</>;
}
@@ -58,6 +58,7 @@ export function useZTable<D>(originalDataset: D[], providedOptions?: Partial<ZTa
if (sortMode !== null) {
mutatedSet = mutatedSet.slice().sort(sortFunctions[sortMode]);
if (reverseSort) {
mutatedSet.reverse();
}
+15
View File
@@ -1,6 +1,9 @@
import {
ChartDocument,
ClassAchievementDocument,
ClassDelta,
FolderDocument,
GamePTConfig,
Grades,
IDStrings,
IDStringToGame,
@@ -121,3 +124,15 @@ export interface UGPTFolderReturns<I extends IDStrings = IDStrings> {
charts: ChartDocument<I>[];
pbs: PBScoreDocument<I>[];
}
export interface GPTStatsReturn {
config: GamePTConfig;
playerCount: integer;
chartCount: integer;
scoreCount: integer;
}
export interface RecentClassesReturn {
classes: ClassAchievementDocument[];
users: PublicUserDocument[];
}
+8 -1
View File
@@ -6,6 +6,7 @@ import {
IDStringToGame,
integer,
ScoreDocument,
PublicUserDocument,
} from "tachi-common";
export type PBDataset<I extends IDStrings = IDStrings> = (PBScoreDocument<I> & {
@@ -14,12 +15,18 @@ export type PBDataset<I extends IDStrings = IDStrings> = (PBScoreDocument<I> & {
})[];
export type ScoreDataset<I extends IDStrings = IDStrings> = (ScoreDocument<I> & {
__related: { chart: ChartDocument<I>; song: SongDocument<IDStringToGame[I]>; index: integer };
__related: {
chart: ChartDocument<I>;
song: SongDocument<IDStringToGame[I]>;
index: integer;
user: PublicUserDocument;
};
})[];
export type FolderDataset<I extends IDStrings = IDStrings> = (ChartDocument<I> & {
__related: {
pb: PBScoreDocument<I> | null;
song: SongDocument<IDStringToGame[I]>;
user: PublicUserDocument;
};
})[];
+11
View File
@@ -6,6 +6,7 @@ import {
SessionScoreInfo,
SongDocument,
ScoreDocument,
PublicUserDocument,
} from "tachi-common";
export function GetPBs(scoreInfo: SessionScoreInfo[]) {
@@ -22,6 +23,16 @@ export function CreateSongMap<G extends Game = Game>(songs: SongDocument<G>[]) {
return songMap;
}
export function CreateUserMap(users: PublicUserDocument[]) {
const userMap = new Map<integer, PublicUserDocument>();
for (const user of users) {
userMap.set(user.id, user);
}
return userMap;
}
export function CreateChartIDMap<T extends { chartID: string }>(arr: T[]): Map<string, T> {
const map = new Map();
+4
View File
@@ -1,3 +1,5 @@
import { IDStrings } from "tachi-common";
export const neutralSplashes = ["A Rhythm Game Score Manager", "The Rhythm Game Score Manager"];
export const loggedInSplashes = [
@@ -67,6 +69,8 @@ export const loggedInSplashes = [
"~𝒕𝒉𝒆 𝒈𝒆𝒂𝒓𝒔 𝒕𝒖𝒓𝒏 𝒐𝒏𝒄𝒆 𝒎𝒐𝒓𝒆~",
"<script>alert(1)</script>",
"come on down",
"The preparation for a dive is always a tense time.",
"Music Is Math",
];
// Stuff that's bad for the kids, you know?