Huge Updates...

This commit is contained in:
zkldi
2021-08-23 02:14:10 +01:00
parent 3d8f9c2f84
commit 4a8f2e3969
48 changed files with 991 additions and 630 deletions
+1 -2
View File
@@ -70,11 +70,10 @@
"react-query": "3.18.1",
"react-router": "5.2.0",
"react-router-dom": "5.1.2",
"react-table": "7.7.0",
"resolve": "1.12.0",
"resolve-url-loader": "3.1.0",
"socicon": "3.0.5",
"tachi-common": "0.1.31"
"tachi-common": "0.1.40"
},
"devDependencies": {
"@types/humanize-duration": "3.25.1",
+4 -14
View File
@@ -69,7 +69,6 @@ specifiers:
react-query: 3.18.1
react-router: 5.2.0
react-router-dom: 5.1.2
react-table: 7.7.0
resolve: 1.12.0
resolve-url-loader: 3.1.0
sass: 1.32.8
@@ -77,7 +76,7 @@ specifiers:
serve: 11.2.0
socicon: 3.0.5
style-loader: 1.0.0
tachi-common: 0.1.31
tachi-common: 0.1.40
terser-webpack-plugin: 1.4.1
ts-pnp: 1.1.4
typescript: 4.3.5
@@ -137,11 +136,10 @@ dependencies:
react-query: 3.18.1_react-dom@16.12.0+react@17.0.2
react-router: 5.2.0_react@17.0.2
react-router-dom: 5.1.2_react@17.0.2
react-table: 7.7.0_react@17.0.2
resolve: 1.12.0
resolve-url-loader: 3.1.0
socicon: 3.0.5
tachi-common: 0.1.31_typescript@4.3.5
tachi-common: 0.1.40_typescript@4.3.5
devDependencies:
'@types/humanize-duration': 3.25.1
@@ -10220,14 +10218,6 @@ packages:
tiny-warning: 1.0.3
dev: false
/react-table/7.7.0_react@17.0.2:
resolution: {integrity: sha512-jBlj70iBwOTvvImsU9t01LjFjy4sXEtclBovl3mTiqjz23Reu0DKnRza4zlLtOPACx6j2/7MrQIthIK1Wi+LIA==}
peerDependencies:
react: ^16.8.3 || ^17.0.0-0
dependencies:
react: 17.0.2
dev: false
/react-transition-group/4.4.2_react-dom@16.12.0+react@17.0.2:
resolution: {integrity: sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==}
peerDependencies:
@@ -11483,8 +11473,8 @@ packages:
strip-ansi: 6.0.0
dev: true
/tachi-common/0.1.31_typescript@4.3.5:
resolution: {integrity: sha512-jaJhv/zW+ye/GAKrYBiJb31shP4hSFhpssM56LUccZJKppDtd56lUu6fFtPWgydt3EIG7fVLqmTQJtDC0WRugg==}
/tachi-common/0.1.40_typescript@4.3.5:
resolution: {integrity: sha512-oj52sHcb2x+Jcg/qyKx1ras/b/9AiW5n1yYa+1pw/6SD8ZcHtzXkSFnUhq0oFbcwPvfy+4m7ygczMfhw9BsN2g==}
dependencies:
monk: 7.3.4
tap: 15.0.9_typescript@4.3.5
+3
View File
@@ -29,6 +29,9 @@ export function ErrorPage({
case 404:
message = "This page does not exist. If you think it should exist, let us know!";
break;
case 429:
message = "You're being rate limited. If this was unexpected, please report this.";
break;
case 500:
message = "Looks like the server has failed. This has been reported!";
break;
@@ -12,87 +12,5 @@ export default function UserPage({ reqUser }: { reqUser: PublicUserDocument }) {
return <Loading />;
}
// this all sucks
return (
<div className="card mb-5 mb-xxl-8 w-100">
<div className="card-body pt-9 pb-0">
<div className="d-flex flex-wrap flex-sm-nowrap mx-4 align-items-center">
<div className="mr-6">
<img
src={ToAPIURL(`/users/${reqUser.id}/pfp`)}
alt={`${reqUser.username}'s Profile Picture`}
className="rounded"
style={{
width: "128px",
height: "128px",
boxShadow: "0px 0px 10px 0px #000000",
}}
/>
</div>
<div className="align-self-start">
<div className="font-weight-bolder font-size-h1">
<div>{reqUser.username}</div>
</div>
</div>
<div className="ml-auto align-self-center">
<ul>
{reqUser.socialMedia.discord ? (
<li>
<i className="flaticon2 flaticon-twitter-logo"></i>{" "}
{reqUser.socialMedia.discord}
</li>
) : null}
{reqUser.socialMedia.discord ? (
<li>
<i className="flaticon2 flaticon-twitter-logo"></i>{" "}
{reqUser.socialMedia.discord}
</li>
) : null}
{reqUser.socialMedia.discord ? (
<li>
<i className="flaticon2 flaticon-twitter-logo"></i>{" "}
{reqUser.socialMedia.discord}
</li>
) : null}
{reqUser.socialMedia.discord ? (
<li>
<i className="flaticon2 flaticon-twitter-logo"></i>{" "}
{reqUser.socialMedia.discord}
</li>
) : null}
{reqUser.socialMedia.discord ? (
<li>
<i className="flaticon2 flaticon-twitter-logo"></i>{" "}
{reqUser.socialMedia.discord}
</li>
) : null}
</ul>
</div>
</div>
<Divider className="mt-4 mb-4" />
<div className="d-flex overflow-auto h-55px">
<ul
className="nav flex-nowrap d-flex w-100"
style={{ justifyContent: "space-evenly" }}
>
<li className="nav-item active">
<a className="nav-link text-active-primary me-6 active" href="#">
Overview
</a>
</li>
<li className="nav-item">
<a className="nav-link text-active-primary me-6" href="#">
Games
</a>
</li>
</ul>
</div>
</div>
</div>
);
return <></>;
}
@@ -30,7 +30,7 @@ export default function ScoresPage({
}: {
reqUser: PublicUserDocument;
} & GamePT) {
const [scoreSet, setScoreSet] = useState<"recent" | "best">("best");
const [scoreSet, setScoreSet] = useState<"recent" | "best" | "all" | "playcount">("best");
const gameConfig = GetGameConfig(game);
@@ -52,13 +52,41 @@ export default function ScoresPage({
<Icon type="history" />
Recent Scores
</SelectButton>
<SelectButton id="playcount" setValue={setScoreSet} value={scoreSet}>
<Icon type="mortar-pestle" />
Most Played
</SelectButton>
<SelectButton id="all" setValue={setScoreSet} value={scoreSet}>
<Icon type="database" />
All PBs
</SelectButton>
</div>
</div>
<div className="col-12 mt-4">
{scoreSet === "best" ? (
<PBsOverview {...{ reqUser, game, playtype }} />
) : (
<PBsOverview
url={`/users/${reqUser.id}/games/${game}/${playtype}/pbs/best`}
{...{ reqUser, game, playtype }}
/>
) : scoreSet === "recent" ? (
<ScoresOverview {...{ reqUser, game, playtype }} />
) : scoreSet === "all" ? (
<PBsOverview
url={`/users/${reqUser.id}/games/${game}/${playtype}/pbs/all`}
reqUser={reqUser}
game={game}
playtype={playtype}
indexCol={false}
/>
) : (
<PBsOverview
url={`/users/${reqUser.id}/games/${game}/${playtype}/most-played`}
reqUser={reqUser}
game={game}
playtype={playtype}
indexCol
showPlaycount
/>
)}
</div>
</div>
@@ -83,12 +111,22 @@ function useFetchPBs(url: string) {
return { isLoading, error: error as UnsuccessfulAPIResponse, data };
}
function PBsOverview({ reqUser, game, playtype }: { reqUser: PublicUserDocument } & GamePT) {
function PBsOverview({
reqUser,
game,
playtype,
indexCol = true,
showPlaycount = false,
url,
}: {
reqUser: PublicUserDocument;
url: string;
indexCol?: boolean;
showPlaycount?: boolean;
} & GamePT) {
const [search, setSearch] = useState("");
const { isLoading, error, data } = useFetchPBs(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/best`
);
const { isLoading, error, data } = useFetchPBs(url);
return (
<div className="row">
@@ -109,6 +147,8 @@ function PBsOverview({ reqUser, game, playtype }: { reqUser: PublicUserDocument
reqUser={reqUser}
dataset={data!}
game={game}
showPlaycount={showPlaycount}
indexCol={indexCol}
playtype={playtype as "SP" | "DP"}
/>
</LoadingWrapper>
+55 -11
View File
@@ -4,13 +4,22 @@ import OverviewPage from "app/pages/dashboard/users/games/_game/_playtype/Overvi
import SessionsPage from "app/pages/dashboard/users/games/_game/_playtype/SessionsPage";
import { ErrorPage } from "app/pages/ErrorPage";
import RequireAuthAsUserParam from "components/auth/RequireAuthAsUserParam";
import UGPTHeader from "components/user/UGPTHeader";
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 { BackgroundContext } from "context/BackgroundContext";
import React, { useContext, useEffect, useState } from "react";
import { useQuery } from "react-query";
import { Redirect, Route, Switch, useParams } from "react-router-dom";
import { Game, GetGameConfig, PublicUserDocument, UserGameStats, UGPTSettings } from "tachi-common";
import {
Game,
GetGameConfig,
PublicUserDocument,
UserGameStats,
UGPTSettings,
FormatGame,
} from "tachi-common";
import { UGPTStatsReturn } from "types/api-returns";
import { APIFetchV1, APIFetchV1Return, ToAPIURL } from "util/api";
import { IsSupportedGame, IsSupportedPlaytype } from "util/asserts";
@@ -30,6 +39,10 @@ export default function UserRoutes() {
} else {
setReqUser(res.body);
}
return () => {
setReqUser(null);
};
})();
}, [params.userID]);
@@ -50,21 +63,43 @@ export default function UserRoutes() {
return (
<Switch>
<Route exact path="/dashboard/users/:userID">
<UserPage reqUser={reqUser} />
</Route>
<Route exact path="/dashboard/users/:userID/settings">
<RequireAuthAsUserParam>Settings Page</RequireAuthAsUserParam>
</Route>
<Route path="/dashboard/users/:userID/games/:game">
<UserGameRoutes reqUser={reqUser} />
</Route>
<Route path="/dashboard/users/:userID">
<UserProfileRoutes reqUser={reqUser} />
</Route>
</Switch>
);
}
function UserProfileRoutes({ reqUser }: { reqUser: PublicUserDocument }) {
return (
<>
<UserHeaderContainer
header={`${reqUser.username}'s Profile`}
footer={
<UserBottomNav
reqUser={reqUser}
baseUrl={`/dashboard/users/${reqUser.username}`}
/>
}
>
<UserHeaderBody reqUser={reqUser} />
</UserHeaderContainer>
<Switch>
<Route exact path="/dashboard/users/:userID">
<UserPage reqUser={reqUser} />
</Route>
<Route exact path="/dashboard/users/:userID/settings">
<RequireAuthAsUserParam>Settings Page</RequireAuthAsUserParam>
</Route>
</Switch>
</>
);
}
function UserGameRoutes({ reqUser }: { reqUser: PublicUserDocument }) {
const { game } = useParams<{ game: string }>();
@@ -153,7 +188,16 @@ function UserGamePlaytypeRoutes({ reqUser, game }: { reqUser: PublicUserDocument
return (
<>
<UGPTHeader reqUser={reqUser} game={game} playtype={playtype} stats={stats} />
<UserHeaderContainer
header={`${reqUser.username}'s ${FormatGame(game, playtype)} Profile`}
footer={
<UGPTBottomNav
baseUrl={`/dashboard/users/${reqUser.username}/games/${game}/${playtype}`}
/>
}
>
<UGPTHeaderBody reqUser={reqUser} game={game} playtype={playtype} stats={stats} />
</UserHeaderContainer>
<Switch>
<Route exact path="/dashboard/users/:userID/games/:game/:playtype">
<OverviewPage
-2
View File
@@ -4,7 +4,6 @@ import { HeaderMobile } from "./header/HeaderMobile";
import { Footer } from "./footer/Footer";
import { LayoutInit } from "../../_metronic/layout/components/LayoutInit";
import { SubHeader } from "./subheader/SubHeader";
import { QuickPanel } from "../../_metronic/layout/components/extras/offcanvas/QuickPanel";
import { AnimateLoading } from "../../_metronic/_partials/controls";
import { JustChildren } from "types/react";
import BackgroundImage from "components/layout/misc/BackgroundImage";
@@ -39,7 +38,6 @@ export function Layout({ children }: JustChildren) {
</div>
</div>
</div>
<QuickPanel />
<LayoutInit />
</>
@@ -54,9 +54,7 @@ function SearchResults({ results }: { results: SearchReturns }) {
>
<strong>{u.username}</strong>
<span className="font-size-sm font-weight-bold text-muted">
{u.about.length >= 50
? `${u.about.substring(0, 47)}...`
: u.about}
{u.status}
</span>
</SearchResult>
))}
+14 -6
View File
@@ -4,18 +4,26 @@ import { useLocation } from "react-router-dom";
import Tab from "@material-ui/core/Tab";
export default function Navbar({ children }: { children: JSX.Element[] }) {
const links = children.map(e =>
e.props.to[e.props.to.length - 1] === "/"
? e.props.to.substring(0, e.props.to.length - 1)
: e.props.to
);
const links = children
.filter(e => e.props.to)
.map(e =>
e.props.to[e.props.to.length - 1] === "/"
? e.props.to.substring(0, e.props.to.length - 1)
: e.props.to
);
const location = useLocation();
const value = useMemo(() => {
const loc = location.pathname.split(/([#?]|\/$)/u)[0];
return links.indexOf(loc);
const x = links.indexOf(loc);
if (x === -1) {
return 0;
}
return x;
}, [location]);
return (
@@ -1,10 +1,17 @@
import React from "react";
import { Badge } from "react-bootstrap";
import { integer } from "tachi-common";
import { FormatTime, MillisToSince } from "util/time";
export default function TimestampCell({ time }: { time: integer | null }) {
export default function TimestampCell({
time,
service,
}: {
time: integer | null;
service?: string;
}) {
return (
<td>
<td style={{ minWidth: "140px", maxWidth: "200px" }}>
{time ? (
<>
{MillisToSince(time)}
@@ -15,6 +22,12 @@ export default function TimestampCell({ time }: { time: integer | null }) {
) : (
"No Data."
)}
{service && (
<>
<br />
<small className="text-muted">Played On: {service}</small>
</>
)}
</td>
);
}
@@ -8,11 +8,13 @@ export default function TitleCell({
song,
chart,
noArtist,
comment,
}: {
song: SongDocument;
chart: ChartDocument;
game: Game;
noArtist?: boolean;
comment?: string | null;
}) {
return (
<td style={{ textAlign: "left", minWidth: "140px", maxWidth: "300px" }}>
@@ -25,6 +27,12 @@ export default function TitleCell({
</>
)}
</Link>
{comment && (
<>
<br />
<small>"{comment}"</small>
</>
)}
</td>
);
}
@@ -77,12 +77,14 @@ export default function TachiTable<D>({
defaultSortMode,
defaultReverseSort,
searchFunctions,
noTopDisplayStr = false,
}: {
dataset: D[];
rowFunction: (data: D) => JSX.Element;
headers: Header<D>[];
entryName?: string;
entryName: string;
pageLen?: integer;
noTopDisplayStr?: boolean;
defaultSortMode?: string;
defaultReverseSort?: boolean;
searchFunctions?: Record<string, ValueGetterOrHybrid<D>>;
@@ -123,18 +125,26 @@ export default function TachiTable<D>({
return (
<div className="justify-content-center w-100">
{searchFunctions && (
<div className="col-12 col-lg-3 px-0 ml-auto input-group">
<input
className="form-control filter-directives-enabled"
onChange={e => setSearch(e.target.value)}
type="text"
placeholder={`Filter ${entryName}`}
value={search}
/>
<FilterDirectivesIndicator searchFunctions={searchFunctions} doc={dataset[0]} />
</div>
)}
<div className="row">
{!noTopDisplayStr && (
<div className="d-none d-lg-flex col-lg-6 align-self-center">{displayStr}</div>
)}
{searchFunctions && (
<div className="col-12 col-lg-3 ml-auto input-group">
<input
className="form-control filter-directives-enabled"
onChange={e => setSearch(e.target.value)}
type="text"
placeholder={`Filter ${entryName}`}
value={search}
/>
<FilterDirectivesIndicator
searchFunctions={searchFunctions}
doc={dataset[0]}
/>
</div>
)}
</div>
<div className="col-12 px-0 mt-4 mb-4">
<table className="table table-striped table-hover table-vertical-center text-center table-responsive-md">
@@ -0,0 +1,24 @@
import { useState, useEffect } from "react";
import { PBDataset, ScoreDataset } from "types/tables";
export function useScoreState(sc: ScoreDataset[0]) {
const [highlight, setHighlight] = useState(sc.highlight);
const [comment, setComment] = useState(sc.comment);
useEffect(() => {
sc.comment = comment;
sc.highlight = highlight;
}, [comment, highlight]);
return { highlight, comment, setHighlight, setComment };
}
export function usePBState(pb: PBDataset[0]) {
const [highlight, setHighlight] = useState(pb.highlight);
useEffect(() => {
pb.highlight = highlight;
}, [highlight]);
return { highlight, setHighlight };
}
@@ -17,6 +17,8 @@ import { UGPTChartPBComposition } from "types/api-returns";
import Loading from "components/util/Loading";
import { UserContext } from "context/UserContext";
import GenericScoreContentDropdown from "./components/GenericScoreContentDropdown";
import PlayHistory from "./components/PlayHistory";
import useApiQuery from "components/util/query/useApiQuery";
export interface ScoreState {
highlight: boolean;
@@ -26,6 +28,7 @@ export interface ScoreState {
export interface ScoreDropdownProps<I extends IDStrings = IDStrings> {
score: ScoreDocument<I> | PBScoreDocument<I>;
scoreState: ScoreState;
pbData: UGPTChartPBComposition<I>;
}
export default function GenericPBDropdown<I extends IDStrings = IDStrings>({
@@ -44,26 +47,21 @@ export default function GenericPBDropdown<I extends IDStrings = IDStrings>({
DocComponent?: (props: {
score: ScoreDocument<I> | PBScoreDocument<I>;
scoreState: ScoreState;
showSingleScoreNote?: boolean;
pbData: UGPTChartPBComposition<I>;
}) => JSX.Element;
} & GamePT) {
const { user } = useContext(UserContext);
const [view, setView] = useState(defaultView);
const { isLoading, error, data } = useQuery(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`,
async () => {
const res = await APIFetchV1<UGPTChartPBComposition<I>>(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`
);
if (!res.success) {
throw new Error(res.description);
}
return res.body;
}
const { isLoading, error, data } = useApiQuery<UGPTChartPBComposition<I>>(
`/users/${reqUser.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}`);
const currentScoreDoc: ScoreDocument<I> | PBScoreDocument<I> | null = useMemo(() => {
if (!data) {
// dont worry about this null, it never gets below the rquery checks
@@ -71,6 +69,12 @@ export default function GenericPBDropdown<I extends IDStrings = IDStrings>({
}
if (view === "pb") {
if (data.pb.composedFrom.lampPB === data.pb.composedFrom.scorePB) {
// 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.pb;
}
@@ -96,14 +100,32 @@ export default function GenericPBDropdown<I extends IDStrings = IDStrings>({
);
}
// @TODO this doesn't support alternate PBs.
const isComposedFromSingleScore = data.pb.composedFrom.lampPB === data.pb.composedFrom.scorePB;
let body;
if (view === "history") {
body = <></>; // todo
body = (
<PlayHistory
data={histData}
error={histError}
isLoading={histIsLoading}
game={game}
playtype={playtype}
/>
);
} else if (view === "debug") {
body = <DebugContent data={data} />;
} else {
body = <DocComponent score={currentScoreDoc!} scoreState={scoreState} />;
body = (
<DocComponent
score={currentScoreDoc!}
showSingleScoreNote={isComposedFromSingleScore}
pbData={data}
scoreState={scoreState}
/>
);
}
return (
@@ -114,17 +136,21 @@ export default function GenericPBDropdown<I extends IDStrings = IDStrings>({
<Icon type="trophy" />
PB Info
</SelectButton>
<SelectButton setValue={setView} value={view} id="scorePB">
<Icon type="star-half-alt" />
Best Score
</SelectButton>
<SelectButton setValue={setView} value={view} id="lampPB">
<Icon type="lightbulb" />
Best Lamp
</SelectButton>
<SelectButton setValue={setView} value={view} id="history" disabled>
{!isComposedFromSingleScore && (
<>
<SelectButton setValue={setView} value={view} id="scorePB">
<Icon type="star-half-alt" />
Best Score
</SelectButton>
<SelectButton setValue={setView} value={view} id="lampPB">
<Icon type="lightbulb" />
Best Lamp
</SelectButton>
</>
)}
<SelectButton setValue={setView} value={view} id="history">
<Icon type="history" />
Play History
Play History{histData && ` (${histData.length})`}
</SelectButton>
{user?.authLevel === "admin" && (
<SelectButton setValue={setView} value={view} id="debug">
@@ -1,7 +1,7 @@
import DebugContent from "components/util/DebugContent";
import Icon from "components/util/Icon";
import SelectButton from "components/util/SelectButton";
import React, { useContext, useMemo, useState } from "react";
import React, { useContext, useState } from "react";
import DropdownStructure from "./components/DropdownStructure";
import {
PublicUserDocument,
@@ -17,6 +17,9 @@ import { UGPTChartPBComposition } from "types/api-returns";
import Loading from "components/util/Loading";
import { UserContext } from "context/UserContext";
import GenericScoreContentDropdown from "./components/GenericScoreContentDropdown";
import PlayHistory from "./components/PlayHistory";
import useApiQuery from "components/util/query/useApiQuery";
import HasDevModeOn from "components/util/HasDevModeOn";
export interface ScoreState {
highlight: boolean;
@@ -46,26 +49,20 @@ export default function GenericScoreDropdown<I extends IDStrings = IDStrings>({
DocComponent?: (props: {
score: ScoreDocument<I> | PBScoreDocument<I>;
scoreState: ScoreState;
pbData: UGPTChartPBComposition;
}) => JSX.Element;
} & GamePT) {
const { user } = useContext(UserContext);
const [view, setView] = useState(defaultView);
const { isLoading, error, data } = useQuery(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`,
async () => {
const res = await APIFetchV1<UGPTChartPBComposition<I>>(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`
);
if (!res.success) {
throw new Error(res.description);
}
return res.body;
}
const { isLoading, error, data } = useApiQuery<UGPTChartPBComposition<I>>(
`/users/${reqUser.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}`);
if (error) {
return <>An error has occured. Whoops.</>;
}
@@ -81,11 +78,19 @@ export default function GenericScoreDropdown<I extends IDStrings = IDStrings>({
let body;
if (view === "history") {
body = <></>; // todo
body = (
<PlayHistory
error={histError}
isLoading={histIsLoading}
game={game}
playtype={playtype}
data={histData}
/>
);
} else if (view === "debug") {
body = <DebugContent data={data} />;
} else if (view === "moreInfo") {
body = <DocComponent score={thisScore as any} scoreState={scoreState} />;
body = <DocComponent score={thisScore as any} scoreState={scoreState} pbData={data} />;
}
return (
@@ -100,16 +105,16 @@ export default function GenericScoreDropdown<I extends IDStrings = IDStrings>({
<Icon type="balance-scale-right" />
Versus PB
</SelectButton>
<SelectButton setValue={setView} value={view} id="history" disabled>
<SelectButton setValue={setView} value={view} id="history">
<Icon type="history" />
Play History
</SelectButton>
{user?.authLevel === "admin" && (
<HasDevModeOn>
<SelectButton setValue={setView} value={view} id="debug">
<Icon type="bug" />
Debug Info
</SelectButton>
)}
</HasDevModeOn>
</>
}
>
@@ -1,21 +1,10 @@
import DebugContent from "components/util/DebugContent";
import Icon from "components/util/Icon";
import Loading from "components/util/Loading";
import SelectButton from "components/util/SelectButton";
import React, { useMemo, useState, useEffect } from "react";
import { useQuery } from "react-query";
import React from "react";
import { ChartDocument, PBScoreDocument, PublicUserDocument, ScoreDocument } from "tachi-common";
import { UGPTChartPBComposition } from "types/api-returns";
import { GamePT, SetState } from "types/react";
import { APIFetchV1 } from "util/api";
import { IsScore } from "util/asserts";
import CommentContainer from "./components/CommentContainer";
import DropdownScoreButtons from "./components/DropdownScoreButtons";
import DropdownStructure from "./components/DropdownStructure";
import { IIDXGraphsComponent, ModsTable, ScoreInfo } from "./components/IIDXScoreDropdownParts";
import { IIDXGraphsComponent } from "./components/IIDXScoreDropdownParts";
import JudgementTable from "./components/JudgementTable";
import PBNote from "./components/PBNote";
import ScoreEditButtons from "./components/ScoreEditButtons";
import GenericPBDropdown, { ScoreState } from "./GenericPBDropdown";
export default function IIDXPBDropdown({
@@ -32,37 +21,6 @@ export default function IIDXPBDropdown({
setHighlight: SetState<boolean>;
};
} & GamePT) {
const [view, setView] = useState<"pb" | "scorePB" | "lampPB" | "bpPB" | "history" | "debug">(
"pb"
);
const { isLoading, error, data } = useQuery(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`,
async () => {
const res = await APIFetchV1<UGPTChartPBComposition<"iidx:SP">>(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`
);
if (!res.success) {
throw new Error(res.description);
}
return res.body;
}
);
if (error) {
return <>An error has occured. Nice.</>;
}
if (isLoading || !data) {
return (
<div style={{ height: "200px" }} className="d-flex align-items-center">
<Loading />
</div>
);
}
return (
<GenericPBDropdown
{...{ game, playtype, chart, reqUser, scoreState }}
@@ -74,16 +32,18 @@ export default function IIDXPBDropdown({
function DocumentDisplay({
score,
scoreState,
pbData,
}: {
score: PBScoreDocument<"iidx:SP" | "iidx:DP"> | ScoreDocument<"iidx:SP" | "iidx:DP">;
scoreState: ScoreState;
pbData: UGPTChartPBComposition<"iidx:SP" | "iidx:DP">;
}) {
return (
<>
<div className="col-9">
<div className="row">
<IIDXGraphsComponent score={score} />
<DropdownScoreButtons {...{ score, scoreState }} />
<DropdownScoreButtons {...{ score, scoreState, pbData }} />
</div>
</div>
<div className="col-3 align-self-center">
@@ -1,19 +1,12 @@
import Icon from "components/util/Icon";
import Loading from "components/util/Loading";
import SelectButton from "components/util/SelectButton";
import React, { useState } from "react";
import React from "react";
import { UGPTChartPBComposition } from "types/api-returns";
import { GamePT, SetState } from "types/react";
import { APIFetchV1 } from "util/api";
import DropdownStructure from "./components/DropdownStructure";
import { PublicUserDocument, ChartDocument, ScoreDocument } from "tachi-common";
import { useQuery } from "react-query";
import { IsScore } from "util/asserts";
import { IIDXGraphsComponent, ModsTable } from "./components/IIDXScoreDropdownParts";
import JudgementTable from "./components/JudgementTable";
import CommentContainer from "./components/CommentContainer";
import ScoreEditButtons from "./components/ScoreEditButtons";
import DebugContent from "components/util/DebugContent";
import { IIDXGraphsComponent } from "./components/IIDXScoreDropdownParts";
import GenericScoreDropdown from "./GenericScoreDropdown";
import useApiQuery from "components/util/query/useApiQuery";
import GenericScoreContentDropdown from "./components/GenericScoreContentDropdown";
export default function IIDXScoreDropdown({
thisScore,
@@ -33,21 +26,8 @@ export default function IIDXScoreDropdown({
setComment: SetState<string | null>;
};
} & GamePT) {
const [view, setView] = useState<"vsPB" | "moreInfo" | "history" | "debug">("moreInfo");
const { isLoading, error, data } = useQuery(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`,
async () => {
const res = await APIFetchV1<UGPTChartPBComposition<"iidx:SP">>(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`
);
if (!res.success) {
throw new Error(res.description);
}
return res.body;
}
const { isLoading, error, data } = useApiQuery<UGPTChartPBComposition<"iidx:SP" | "iidx:DP">>(
`/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true`
);
if (error) {
@@ -62,52 +42,18 @@ export default function IIDXScoreDropdown({
);
}
let content = null;
if (view === "moreInfo") {
content = (
<>
<div className="col-9">
<div className="row">
<IIDXGraphsComponent score={thisScore} />
<CommentContainer comment={scoreState.comment} />
<ScoreEditButtons score={thisScore} scoreState={scoreState} />
</div>
</div>
<div className="col-3 align-self-center">
<JudgementTable score={thisScore} />
{IsScore(thisScore) && <ModsTable score={thisScore} />}
</div>
</>
);
} else if (view === "debug") {
content = <DebugContent data={data} />;
}
return (
<DropdownStructure
buttons={
<>
<SelectButton setValue={setView} value={view} id="moreInfo">
<Icon type="chart-bar" />
This Score
</SelectButton>
<SelectButton setValue={setView} value={view} id="vsPB">
<Icon type="balance-scale-right" />
Versus PB
</SelectButton>
<SelectButton setValue={setView} value={view} id="history" disabled>
<Icon type="history" />
Play History
</SelectButton>
<SelectButton setValue={setView} value={view} id="debug">
<Icon type="bug" />
Debug Info
</SelectButton>
</>
<GenericScoreDropdown<"iidx:SP" | "iidx:DP">
{...{ game, playtype, chart, reqUser, scoreState, thisScore }}
DocComponent={props =>
GenericScoreContentDropdown({
...props,
renderScoreInfo: false,
// let the record show that i tried fixing this
// for a while, but gave up.
GraphComponent: IIDXGraphsComponent as any,
})
}
>
{content}
</DropdownStructure>
/>
);
}
@@ -6,7 +6,7 @@ import { ScoreInfo } from "./GenericScoreContentDropdown";
import PBNote from "./PBNote";
import ScoreEditButtons from "./ScoreEditButtons";
export default function DropdownScoreButtons({ score, scoreState }: ScoreDropdownProps) {
export default function DropdownScoreButtons({ score, scoreState, pbData }: ScoreDropdownProps) {
const [comment, setComment] = useState(IsScore(score) ? score.comment : null);
useEffect(() => {
@@ -26,6 +26,12 @@ export default function DropdownScoreButtons({ score, scoreState }: ScoreDropdow
</>
) : (
<div className="col-12">
<CommentContainer
comment={pbData.scores
.map(e => e.comment)
.filter(e => e !== null)
.join("; ")}
/>
<PBNote />
</div>
)}
@@ -2,8 +2,10 @@ import DeltaCell from "components/tables/cells/DeltaCell";
import LampCell from "components/tables/cells/LampCell";
import ScoreCell from "components/tables/cells/ScoreCell";
import TimestampCell from "components/tables/cells/TimestampCell";
import ExternalLink from "components/util/ExternalLink";
import React, { useEffect, useState } from "react";
import { PBScoreDocument, ScoreDocument } from "tachi-common";
import { UGPTChartPBComposition } from "types/api-returns";
import { SetState } from "types/react";
import { IsScore } from "util/asserts";
import CommentContainer from "./CommentContainer";
@@ -31,7 +33,7 @@ export function ScoreInfo({ score }: { score: ScoreDocument }) {
grade={score.scoreData.grade}
/>
<LampCell score={score} />
<TimestampCell time={score.timeAchieved} />
<TimestampCell time={score.timeAchieved} service={score.service} />
</tr>
</tbody>
</table>
@@ -43,10 +45,18 @@ export default function GenericScoreContentDropdown({
score,
scoreState,
renderScoreInfo = true,
showSingleScoreNote = false,
GraphComponent = null,
pbData,
}: {
score: ScoreDocument | PBScoreDocument;
scoreState: { highlight: boolean; setHighlight: SetState<boolean> };
renderScoreInfo?: boolean;
showSingleScoreNote?: boolean;
pbData: UGPTChartPBComposition;
GraphComponent?:
| (({ score }: { score: ScoreDocument | PBScoreDocument }) => JSX.Element)
| null;
}) {
const [comment, setComment] = useState(IsScore(score) ? score.comment : null);
@@ -58,13 +68,28 @@ export default function GenericScoreContentDropdown({
<>
<div className="col-9">
<div className="row h-100 justify-content-center">
<div className="d-flex align-items-center" style={{ height: "200px" }}>
<span className="text-muted">No graphs available :(</span>
</div>
{GraphComponent ? (
<GraphComponent score={score} />
) : (
<div className="d-flex align-items-center" style={{ height: "200px" }}>
<span className="text-muted">No graphs available :(</span>
</div>
)}
{IsScore(score) ? (
<>
{renderScoreInfo && <ScoreInfo score={score} />}
{renderScoreInfo && !showSingleScoreNote && <ScoreInfo score={score} />}
<CommentContainer comment={comment} />
{showSingleScoreNote && (
<div className="col-12">
<PBNote />
<br />
<small>
In this case, your best lamp and your best score were the
same!
</small>
</div>
)}
<ScoreEditButtons
score={score}
scoreState={{ ...scoreState, comment, setComment }}
@@ -72,6 +97,12 @@ export default function GenericScoreContentDropdown({
</>
) : (
<div className="col-12 align-self-end">
<CommentContainer
comment={pbData.scores
.map(e => e.comment)
.filter(e => e !== null)
.join("; ")}
/>
<PBNote />
</div>
)}
@@ -3,9 +3,9 @@ import IIDXLampCell from "components/tables/cells/IIDXLampCell";
import ScoreCell from "components/tables/cells/ScoreCell";
import TimestampCell from "components/tables/cells/TimestampCell";
import SelectNav from "components/util/SelectNav";
import React, { useState } from "react";
import { ScoreDocument, PBScoreDocument, Lamps } from "tachi-common";
import { Nav } from ".pnpm/react-bootstrap@1.0.1_react-dom@16.12.0+react@17.0.2/node_modules/react-bootstrap";
import React, { useEffect, useState } from "react";
import { ScoreDocument, PBScoreDocument } from "tachi-common";
import { Nav } from "react-bootstrap";
import IIDXLampChart from "components/charts/IIDXLampChart";
import MiniTable from "components/tables/components/MiniTable";
import { IsScore } from "util/asserts";
@@ -92,6 +92,10 @@ export function IIDXGraphsComponent({
return true;
};
useEffect(() => {
setLamp(LampToKey(score));
}, [score]);
return (
<>
<div className="col-12 d-flex justify-content-center">
@@ -0,0 +1,32 @@
import HistoryScoreTable from "components/tables/history-scores/HistoryScoreTable";
import Loading from "components/util/Loading";
import React from "react";
import { ScoreDocument } from "tachi-common";
import { GamePT } from "types/react";
import { UnsuccessfulAPIFetchResponse } from "util/api";
export default function PlayHistory({
data,
isLoading,
error,
game,
playtype,
}: {
data?: ScoreDocument[];
isLoading: boolean;
error: UnsuccessfulAPIFetchResponse | null;
} & GamePT) {
if (isLoading || !data) {
return <Loading />;
}
if (error) {
return <>{error.description}</>;
}
return (
<div className="col-12 col-lg-10">
<HistoryScoreTable dataset={data} game={game} playtype={playtype} />
</div>
);
}
@@ -53,9 +53,12 @@ export default function ScoreEditButtons({
<Button
variant="success"
onClick={() =>
ModifyScore(score.scoreID, { highlight: false }).then(
r => r && setHighlight(false)
)
ModifyScore(score.scoreID, { highlight: false }).then(r => {
if (r) {
setHighlight(false);
score.highlight = false;
}
})
}
>
<Icon noPad type="star" />
@@ -66,9 +69,12 @@ export default function ScoreEditButtons({
<Button
variant="outline-secondary"
onClick={() =>
ModifyScore(score.scoreID, { highlight: true }).then(
r => r && setHighlight(true)
)
ModifyScore(score.scoreID, { highlight: true }).then(r => {
if (r) {
setHighlight(true);
score.highlight = true;
}
})
}
>
<Icon noPad type="star" />
@@ -82,6 +88,7 @@ export default function ScoreEditButtons({
comment={comment}
setComment={setComment}
scoreID={score.scoreID}
score={score}
/>
</div>
);
@@ -113,12 +120,14 @@ function CommentModal({
comment,
setComment,
scoreID,
score,
}: {
show: boolean;
setShow: SetState<boolean>;
comment: string | null;
setComment: SetState<string | null>;
scoreID: string;
score: ScoreDocument;
}) {
const [innerComment, setInnerComment] = useState(comment ?? "");
@@ -135,6 +144,7 @@ function CommentModal({
ModifyScore(scoreID, { comment: innerComment }).then(r => {
if (r) {
setComment(innerComment);
score.comment = innerComment;
setShow(false);
}
});
@@ -150,7 +160,7 @@ function CommentModal({
onChange={e => setInnerComment(e.target.value)}
/>
<div className="input-group-append">
<Button variant="outline-primary" type="submit">
<Button variant="primary" type="submit">
Submit
</Button>
</div>
@@ -159,11 +169,12 @@ function CommentModal({
<QuickTooltip text="Remove your comment on this score.">
<Button
variant="outline-secondary"
variant="outline-danger"
onClick={() =>
ModifyScore(scoreID, { comment: null }).then(r => {
if (r) {
setComment(null);
score.comment = null;
setShow(false);
}
})
@@ -0,0 +1,23 @@
import React from "react";
import { ScoreDocument } from "tachi-common";
import DeltaCell from "../cells/DeltaCell";
import LampCell from "../cells/LampCell";
import RatingCell from "../cells/RatingCell";
import ScoreCell from "../cells/ScoreCell";
export default function BMSScoreCoreCells({ sc }: { sc: ScoreDocument<"bms:7K" | "bms:14K"> }) {
return (
<>
<ScoreCell score={sc} />
<DeltaCell
game="bms"
playtype={sc.playtype}
score={sc.scoreData.score}
percent={sc.scoreData.percent}
grade={sc.scoreData.grade}
/>
<LampCell score={sc} />
<RatingCell score={sc} />
</>
);
}
@@ -0,0 +1,21 @@
import React from "react";
import { ScoreDocument } from "tachi-common";
import LampCell from "../cells/LampCell";
import RatingCell from "../cells/RatingCell";
import ScoreCell from "../cells/ScoreCell";
export default function GenericScoreCoreCells({
sc,
showScore,
}: {
sc: ScoreDocument;
showScore?: boolean;
}) {
return (
<>
<ScoreCell score={sc} showScore={showScore} />
<LampCell score={sc} />
<RatingCell score={sc} />
</>
);
}
@@ -0,0 +1,33 @@
import React from "react";
import { ScoreDocument, ScoreCalculatedDataLookup } from "tachi-common";
import { IsNullish } from "util/misc";
import DeltaCell from "../cells/DeltaCell";
import IIDXLampCell from "../cells/IIDXLampCell";
import ScoreCell from "../cells/ScoreCell";
export default function IIDXScoreCoreCells({
sc,
rating,
}: {
sc: ScoreDocument<"iidx:SP" | "iidx:DP">;
rating: ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"];
}) {
return (
<>
<ScoreCell score={sc} />
<DeltaCell
game="iidx"
playtype={sc.playtype}
score={sc.scoreData.score}
percent={sc.scoreData.percent}
grade={sc.scoreData.grade}
/>
<IIDXLampCell sc={sc} />
<td>
{!IsNullish(sc.calculatedData[rating])
? sc.calculatedData[rating]!.toFixed(2)
: "No Data."}
</td>
</>
);
}
@@ -0,0 +1,17 @@
import React from "react";
import { ScoreDocument } from "tachi-common";
import LampCell from "../cells/LampCell";
import MillionsScoreCell from "../cells/MillionsScoreCell";
import MusecaJudgementCell from "../cells/MusecaJudgementCell";
import RatingCell from "../cells/RatingCell";
export default function MusecaScoreCoreCells({ sc }: { sc: ScoreDocument<"museca:Single"> }) {
return (
<>
<MillionsScoreCell score={sc} />
<MusecaJudgementCell score={sc} />
<LampCell score={sc} />
<RatingCell score={sc} />
</>
);
}
@@ -0,0 +1,17 @@
import React from "react";
import { ScoreDocument } from "tachi-common";
import { IsNullish } from "util/misc";
import LampCell from "../cells/LampCell";
import MillionsScoreCell from "../cells/MillionsScoreCell";
import SDVXJudgementCell from "../cells/SDVXJudgementCell";
export default function SDVXScoreCoreCells({ sc }: { sc: ScoreDocument<"sdvx:Single"> }) {
return (
<>
<MillionsScoreCell score={sc} />
<SDVXJudgementCell score={sc} />
<LampCell score={sc} />
<td>{!IsNullish(sc.calculatedData.VF6) ? sc.calculatedData.VF6!.toFixed(3) : "N/A"}</td>
</>
);
}
@@ -0,0 +1,39 @@
import React from "react";
import { Game, IDStrings, ScoreCalculatedDataLookup, ScoreDocument } from "tachi-common";
import { GamePT } from "types/react";
import BMSScoreCoreCells from "./BMSScoreCoreCells";
import GenericScoreCoreCells from "./GenericScoreCoreCells";
import IIDXScoreCoreCells from "./IIDXScoreCoreCells";
import MusecaScoreCoreCells from "./MusecaScoreCoreCells";
import SDVXScoreCoreCells from "./SDVXCoreCells";
export default function ScoreCoreCells({
game,
score,
rating,
}: {
score: ScoreDocument;
rating: ScoreCalculatedDataLookup[IDStrings];
game: Game;
}) {
const sc = score as any; // lazy hack
if (game === "iidx") {
return (
<IIDXScoreCoreCells
rating={rating as ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"]}
sc={sc}
/>
);
} else if (game === "bms") {
return <BMSScoreCoreCells sc={sc} />;
} else if (game === "sdvx" || game === "usc") {
return <SDVXScoreCoreCells sc={sc} />;
} else if (game === "maimai") {
return <GenericScoreCoreCells showScore={false} sc={sc} />;
} else if (game === "museca") {
return <MusecaScoreCoreCells sc={sc} />;
}
return <GenericScoreCoreCells sc={sc} />;
}
@@ -0,0 +1,86 @@
import { nanoid } from "nanoid";
import React, { useState } from "react";
import {
Game,
GetGamePTConfig,
IDStrings,
integer,
ScoreCalculatedDataLookup,
ScoreDocument,
} from "tachi-common";
import { SetState } from "types/react";
import { Playtype } from "types/tachi";
import { NumericSOV } from "util/sorts";
import TimestampCell from "../cells/TimestampCell";
import SelectableRating from "../components/SelectableRating";
import TachiTable, { ZTableTHProps } from "../components/TachiTable";
import IIDXScoreCoreCells from "../game-core-cells/IIDXScoreCoreCells";
import ScoreCoreCells from "../game-core-cells/ScoreCoreCells";
export default function HistoryScoreTable({
dataset,
pageLen = 5,
playtype,
game,
}: {
dataset: ScoreDocument[];
pageLen?: integer;
playtype: Playtype;
game: Game;
}) {
const gptConfig = GetGamePTConfig(game, playtype);
const [rating, setRating] = useState(gptConfig.defaultScoreRatingAlg);
return (
<TachiTable
dataset={dataset}
pageLen={pageLen}
noTopDisplayStr
entryName="Scores"
headers={[
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Info", "Info", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
game === "iidx"
? [
"Rating",
"Rating",
NumericSOV(x => x.calculatedData[rating] ?? 0),
(thProps: ZTableTHProps) => (
<SelectableRating
key={nanoid()}
game="iidx"
playtype={playtype}
rating={rating}
setRating={setRating}
{...thProps}
/>
),
]
: ["Rating", "Rating", NumericSOV(x => x.calculatedData[rating] ?? 0)],
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
]}
defaultSortMode="Timestamp"
defaultReverseSort
rowFunction={sc => <Row key={sc.scoreID} sc={sc} game={game} rating={rating} />}
/>
);
}
function Row({
sc,
rating,
game,
}: {
sc: ScoreDocument;
rating: ScoreCalculatedDataLookup[IDStrings];
game: Game;
}) {
return (
<tr className={sc.highlight ? "highlighted-row" : ""}>
<ScoreCoreCells score={sc} game={game} rating={rating as any} />
<TimestampCell time={sc.timeAchieved} service={sc.service} />
</tr>
);
}
@@ -17,6 +17,7 @@ import BMSDifficultyCell from "../cells/BMSDifficultyCell";
import { BMS_TABLES } from "util/constants/bms-tables";
import { ValueGetterOrHybrid } from "util/ztable/search";
import GenericPBDropdown from "../dropdowns/GenericPBDropdown";
import { usePBState } from "../components/UseScoreState";
function GetBMSTableVal(chart: ChartDocument<"bms:7K" | "bms:14K">, key: string) {
for (const table of chart.data.tableFolders) {
@@ -31,12 +32,14 @@ function GetBMSTableVal(chart: ChartDocument<"bms:7K" | "bms:14K">, key: string)
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";
}) {
const headers: Header<PBDataset<"bms:7K" | "bms:14K">[0]>[] = [
@@ -46,10 +49,14 @@ export default function BMSPBTable({
["Deltas", "Deltas", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
["Sieglinde", "sgl.", NumericSOV(x => x.calculatedData.sieglinde ?? 0)],
["Ranking", "Rank", NumericSOV(x => x.rankingData.rank)],
["Site Ranking", "Site Rank", NumericSOV(x => x.rankingData.rank)],
["Last Raised", "Last Raised", NumericSOV(x => x.timeAchieved ?? 0)],
];
if (showPlaycount) {
headers.push(["Playcount", "Plays", NumericSOV(x => x.__playcount ?? 0)]);
}
if (indexCol) {
headers.unshift(["#", "#", NumericSOV(x => x.__related.index)]);
}
@@ -107,6 +114,7 @@ export default function BMSPBTable({
pb={pb}
key={`${pb.chartID}:${pb.userID}`}
reqUser={reqUser}
showPlaycount={showPlaycount}
indexCol={indexCol}
/>
)}
@@ -118,14 +126,14 @@ function Row({
pb,
indexCol,
reqUser,
showPlaycount,
}: {
pb: PBDataset<"bms:7K" | "bms:14K">[0];
indexCol: boolean;
showPlaycount: boolean;
reqUser: PublicUserDocument;
}) {
const [highlight, setHighlight] = useState(pb.highlight);
const scoreState = { highlight, setHighlight };
const scoreState = usePBState(pb);
return (
<DropdownRow
@@ -138,7 +146,7 @@ function Row({
scoreState={scoreState}
/>
}
className={highlight ? "highlighted-row" : ""}
className={scoreState.highlight ? "highlighted-row" : ""}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<BMSDifficultyCell chart={pb.__related.chart} />
@@ -159,6 +167,7 @@ function Row({
</td>
<RankingCell rankingData={pb.rankingData} />
<TimestampCell time={pb.timeAchieved} />
{showPlaycount && <td>{pb.__playcount ?? 0}</td>}
</DropdownRow>
);
}
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import TitleCell from "../cells/TitleCell";
import RankingCell from "../cells/RankingCell";
import TimestampCell from "../cells/TimestampCell";
@@ -9,19 +9,18 @@ import TachiTable, { Header } from "../components/TachiTable";
import IndexCell from "../cells/IndexCell";
import { HumanFriendlyStrToGradeIndex, HumanFriendlyStrToLampIndex } from "util/str-to-num";
import DropdownRow from "../components/DropdownRow";
import { IsNullish } from "util/misc";
import LampCell from "../cells/LampCell";
import DifficultyCell from "../cells/DifficultyCell";
import MillionsScoreCell from "../cells/MillionsScoreCell";
import GenericPBDropdown from "../dropdowns/GenericPBDropdown";
import SDVXJudgementCell from "../cells/SDVXJudgementCell";
import ScoreCell from "../cells/ScoreCell";
import RatingCell from "../cells/RatingCell";
import { Playtype } from "types/tachi";
import { usePBState } from "../components/UseScoreState";
export default function GenericPBTable({
dataset,
indexCol = true,
showPlaycount = false,
reqUser,
game,
playtype,
@@ -29,6 +28,7 @@ export default function GenericPBTable({
}: {
dataset: PBDataset;
indexCol?: boolean;
showPlaycount?: boolean;
reqUser: PublicUserDocument;
game: Game;
playtype: Playtype;
@@ -46,10 +46,14 @@ export default function GenericPBTable({
gptConfig.defaultScoreRatingAlg,
NumericSOV(x => x.calculatedData[gptConfig.defaultScoreRatingAlg] ?? 0),
],
["Ranking", "Rank", NumericSOV(x => x.rankingData.rank)],
["Site Ranking", "Site Rank", NumericSOV(x => x.rankingData.rank)],
["Last Raised", "Last Raised", NumericSOV(x => x.timeAchieved ?? 0)],
];
if (showPlaycount) {
headers.push(["Playcount", "Plays", NumericSOV(x => x.__playcount ?? 0)]);
}
if (indexCol) {
headers.unshift(["#", "#", NumericSOV(x => x.__related.index)]);
}
@@ -84,6 +88,7 @@ export default function GenericPBTable({
reqUser={reqUser}
indexCol={indexCol}
showScore={showScore}
showPlaycount={showPlaycount}
/>
)}
/>
@@ -95,15 +100,15 @@ function Row({
indexCol,
reqUser,
showScore,
showPlaycount,
}: {
pb: PBDataset[0];
indexCol: boolean;
showPlaycount: boolean;
reqUser: PublicUserDocument;
showScore?: boolean;
}) {
const [highlight, setHighlight] = useState(pb.highlight);
const scoreState = { highlight, setHighlight };
const scoreState = usePBState(pb);
return (
<DropdownRow
@@ -116,7 +121,7 @@ function Row({
scoreState={scoreState}
/>
}
className={highlight ? "highlighted-row" : ""}
className={scoreState.highlight ? "highlighted-row" : ""}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<DifficultyCell game={pb.game} chart={pb.__related.chart} />
@@ -126,6 +131,7 @@ function Row({
<RatingCell score={pb} />
<RankingCell rankingData={pb.rankingData} />
<TimestampCell time={pb.timeAchieved} />
{showPlaycount && <td>{pb.__playcount ?? 0}</td>}
</DropdownRow>
);
}
@@ -18,15 +18,18 @@ import DropdownRow from "../components/DropdownRow";
import IIDXPBDropdown from "../dropdowns/IIDXPBDropdown";
import IIDXLampCell from "../cells/IIDXLampCell";
import { IsNullish } from "util/misc";
import { usePBState } from "../components/UseScoreState";
export default function IIDXPBTable({
dataset,
indexCol = true,
showPlaycount = false,
reqUser,
playtype,
}: {
dataset: PBDataset<"iidx:SP" | "iidx:DP">;
indexCol?: boolean;
showPlaycount?: boolean;
reqUser: PublicUserDocument;
playtype: "SP" | "DP";
}) {
@@ -57,10 +60,14 @@ export default function IIDXPBTable({
/>
),
],
["Ranking", "Rank", NumericSOV(x => x.rankingData.rank)],
["Site Ranking", "Site Rank", NumericSOV(x => x.rankingData.rank)],
["Last Raised", "Last Raised", NumericSOV(x => x.timeAchieved ?? 0)],
];
if (showPlaycount) {
headers.push(["Playcount", "Plays", NumericSOV(x => x.__playcount ?? 0)]);
}
if (indexCol) {
headers.unshift(["#", "#", NumericSOV(x => x.__related.index)]);
}
@@ -93,6 +100,7 @@ export default function IIDXPBTable({
pb={pb}
key={`${pb.chartID}:${pb.userID}`}
reqUser={reqUser}
showPlaycount={showPlaycount}
indexCol={indexCol}
rating={rating}
/>
@@ -106,15 +114,15 @@ function Row({
indexCol,
reqUser,
rating,
showPlaycount,
}: {
pb: PBDataset<"iidx:SP" | "iidx:DP">[0];
indexCol: boolean;
reqUser: PublicUserDocument;
showPlaycount: boolean;
rating: ScoreCalculatedDataLookup["iidx:SP"];
}) {
const [highlight, setHighlight] = useState(pb.highlight);
const scoreState = { highlight, setHighlight };
const scoreState = usePBState(pb);
return (
<DropdownRow
@@ -127,7 +135,7 @@ function Row({
scoreState={scoreState}
/>
}
className={highlight ? "highlighted-row" : ""}
className={scoreState.highlight ? "highlighted-row" : ""}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<DifficultyCell chart={pb.__related.chart} game={"iidx"} />
@@ -148,6 +156,7 @@ function Row({
</td>
<RankingCell rankingData={pb.rankingData} />
<TimestampCell time={pb.timeAchieved} />
{showPlaycount && <td>{pb.__playcount ?? 0}</td>}
</DropdownRow>
);
}
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import TitleCell from "../cells/TitleCell";
import RankingCell from "../cells/RankingCell";
import TimestampCell from "../cells/TimestampCell";
@@ -9,23 +9,24 @@ import TachiTable, { Header } from "../components/TachiTable";
import IndexCell from "../cells/IndexCell";
import { HumanFriendlyStrToGradeIndex, HumanFriendlyStrToLampIndex } from "util/str-to-num";
import DropdownRow from "../components/DropdownRow";
import { IsNullish } from "util/misc";
import LampCell from "../cells/LampCell";
import DifficultyCell from "../cells/DifficultyCell";
import MillionsScoreCell from "../cells/MillionsScoreCell";
import GenericPBDropdown from "../dropdowns/GenericPBDropdown";
import SDVXJudgementCell from "../cells/SDVXJudgementCell";
import RatingCell from "../cells/RatingCell";
import MusecaJudgementCell from "../cells/MusecaJudgementCell";
import { usePBState } from "../components/UseScoreState";
export default function MusecaPBTable({
dataset,
indexCol = true,
showPlaycount = false,
reqUser,
playtype,
}: {
dataset: PBDataset<"museca:Single">;
indexCol?: boolean;
showPlaycount?: boolean;
reqUser: PublicUserDocument;
playtype: "Single";
}) {
@@ -33,13 +34,17 @@ export default function MusecaPBTable({
["Chart", "Ch.", NumericSOV(x => x.__related.chart.levelNum)],
["Song", "Song", StrSOV(x => x.__related.song.title)],
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Judgements", "Judge", 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)],
["Ranking", "Rank", NumericSOV(x => x.rankingData.rank)],
["Site Ranking", "Site Rank", NumericSOV(x => x.rankingData.rank)],
["Last Raised", "Last Raised", NumericSOV(x => x.timeAchieved ?? 0)],
];
if (showPlaycount) {
headers.push(["Playcount", "Plays", NumericSOV(x => x.__playcount ?? 0)]);
}
if (indexCol) {
headers.unshift(["#", "#", NumericSOV(x => x.__related.index)]);
}
@@ -73,6 +78,7 @@ export default function MusecaPBTable({
key={`${pb.chartID}:${pb.userID}`}
reqUser={reqUser}
indexCol={indexCol}
showPlaycount={showPlaycount}
/>
)}
/>
@@ -83,14 +89,14 @@ function Row({
pb,
indexCol,
reqUser,
showPlaycount,
}: {
pb: PBDataset<"museca:Single">[0];
indexCol: boolean;
showPlaycount: boolean;
reqUser: PublicUserDocument;
}) {
const [highlight, setHighlight] = useState(pb.highlight);
const scoreState = { highlight, setHighlight };
const scoreState = usePBState(pb);
return (
<DropdownRow
@@ -103,7 +109,7 @@ function Row({
scoreState={scoreState}
/>
}
className={highlight ? "highlighted-row" : ""}
className={scoreState.highlight ? "highlighted-row" : ""}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<DifficultyCell game="museca" chart={pb.__related.chart} />
@@ -114,6 +120,7 @@ function Row({
<RatingCell score={pb} />
<RankingCell rankingData={pb.rankingData} />
<TimestampCell time={pb.timeAchieved} />
{showPlaycount && <td>{pb.__playcount ?? 0}</td>}
</DropdownRow>
);
}
+3 -1
View File
@@ -13,17 +13,19 @@ export default function PBTable({
indexCol = true,
reqUser,
playtype,
showPlaycount,
game,
}: {
dataset: PBDataset;
indexCol?: boolean;
showPlaycount?: boolean;
reqUser: PublicUserDocument;
} & 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 } as any;
const props = { dataset, indexCol, reqUser, playtype, showPlaycount } as any;
if (game === "iidx") {
return <IIDXPBTable {...props} />;
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import TitleCell from "../cells/TitleCell";
import RankingCell from "../cells/RankingCell";
import TimestampCell from "../cells/TimestampCell";
@@ -15,15 +15,18 @@ import DifficultyCell from "../cells/DifficultyCell";
import MillionsScoreCell from "../cells/MillionsScoreCell";
import GenericPBDropdown from "../dropdowns/GenericPBDropdown";
import SDVXJudgementCell from "../cells/SDVXJudgementCell";
import { usePBState } from "../components/UseScoreState";
export default function SDVXPBTable({
dataset,
indexCol = true,
showPlaycount = false,
reqUser,
playtype,
}: {
dataset: PBDataset<"sdvx:Single">;
indexCol?: boolean;
showPlaycount?: boolean;
reqUser: PublicUserDocument;
playtype: "7K" | "14K";
}) {
@@ -31,13 +34,17 @@ export default function SDVXPBTable({
["Chart", "Ch.", NumericSOV(x => x.__related.chart.levelNum)],
["Song", "Song", StrSOV(x => x.__related.song.title)],
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Judgements", "Judge", 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)],
["Ranking", "Rank", NumericSOV(x => x.rankingData.rank)],
["Site Ranking", "Site Rank", NumericSOV(x => x.rankingData.rank)],
["Last Raised", "Last Raised", NumericSOV(x => x.timeAchieved ?? 0)],
];
if (showPlaycount) {
headers.push(["Playcount", "Plays", NumericSOV(x => x.__playcount ?? 0)]);
}
if (indexCol) {
headers.unshift(["#", "#", NumericSOV(x => x.__related.index)]);
}
@@ -71,6 +78,7 @@ export default function SDVXPBTable({
key={`${pb.chartID}:${pb.userID}`}
reqUser={reqUser}
indexCol={indexCol}
showPlaycount={showPlaycount}
/>
)}
/>
@@ -80,15 +88,15 @@ export default function SDVXPBTable({
function Row({
pb,
indexCol,
showPlaycount,
reqUser,
}: {
pb: PBDataset<"sdvx:Single">[0];
indexCol: boolean;
showPlaycount: boolean;
reqUser: PublicUserDocument;
}) {
const [highlight, setHighlight] = useState(pb.highlight);
const scoreState = { highlight, setHighlight };
const scoreState = usePBState(pb);
return (
<DropdownRow
@@ -101,7 +109,7 @@ function Row({
scoreState={scoreState}
/>
}
className={highlight ? "highlighted-row" : ""}
className={scoreState.highlight ? "highlighted-row" : ""}
>
{indexCol && <IndexCell index={pb.__related.index} />}
<DifficultyCell game="sdvx" chart={pb.__related.chart} />
@@ -114,6 +122,7 @@ function Row({
</td>
<RankingCell rankingData={pb.rankingData} />
<TimestampCell time={pb.timeAchieved} />
{showPlaycount && <td>{pb.__playcount ?? 0}</td>}
</DropdownRow>
);
}
@@ -4,7 +4,7 @@ import TitleCell from "../cells/TitleCell";
import TimestampCell from "../cells/TimestampCell";
import { NumericSOV, StrSOV } from "util/sorts";
import { ScoreDataset } from "types/tables";
import { integer, PublicUserDocument, Playtypes } from "tachi-common";
import { integer, PublicUserDocument, Playtypes, ScoreDocument } from "tachi-common";
import TachiTable from "../components/TachiTable";
import ScoreCell from "../cells/ScoreCell";
import DeltaCell from "../cells/DeltaCell";
@@ -16,6 +16,10 @@ import BMSDifficultyCell from "../cells/BMSDifficultyCell";
import { Playtype } from "types/tachi";
import GenericScoreDropdown from "../dropdowns/GenericScoreDropdown";
import SDVXJudgementCell from "../cells/SDVXJudgementCell";
import { useScoreState } from "../components/UseScoreState";
import RatingCell from "../cells/RatingCell";
import BMSScoreCoreCells from "../game-core-cells/BMSScoreCoreCells";
import { CreateDefaultScoreSearch } from "util/tables";
export default function BMSScoreTable({
reqUser,
@@ -43,22 +47,7 @@ export default function BMSScoreTable({
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
]}
entryName="Scores"
searchFunctions={{
artist: x => x.__related.song.artist,
title: x => x.__related.song.title,
difficulty: x => FormatDifficulty(x.__related.chart, "bms"),
level: x => x.__related.chart.levelNum,
score: x => x.scoreData.score,
percent: x => x.scoreData.percent,
lamp: {
valueGetter: x => [x.scoreData.lamp, x.scoreData.lampIndex],
strToNum: HumanFriendlyStrToLampIndex("bms", playtype),
},
grade: {
valueGetter: x => [x.scoreData.grade, x.scoreData.gradeIndex],
strToNum: HumanFriendlyStrToGradeIndex("bms", playtype),
},
}}
searchFunctions={CreateDefaultScoreSearch<"bms:7K" | "bms:14K">("bms", playtype)}
rowFunction={sc => (
<Row playtype={playtype} key={sc.scoreID} sc={sc} reqUser={reqUser} />
)}
@@ -75,14 +64,11 @@ function Row({
reqUser: PublicUserDocument;
playtype: Playtype;
}) {
const [highlight, setHighlight] = useState(sc.highlight);
const [comment, setComment] = useState(sc.comment);
const scoreState = { highlight, comment, setHighlight, setComment };
const scoreState = useScoreState(sc);
return (
<DropdownRow
className={highlight ? "highlighted-row" : ""}
className={scoreState.highlight ? "highlighted-row" : ""}
dropdown={
<GenericScoreDropdown
chart={sc.__related.chart}
@@ -95,22 +81,14 @@ function Row({
}
>
<BMSDifficultyCell chart={sc.__related.chart} />
<TitleCell song={sc.__related.song} chart={sc.__related.chart} game="bms" />
<ScoreCell score={sc} />
<DeltaCell
<TitleCell
song={sc.__related.song}
chart={sc.__related.chart}
game="bms"
playtype={playtype}
score={sc.scoreData.score}
percent={sc.scoreData.percent}
grade={sc.scoreData.grade}
comment={sc.comment}
/>
<LampCell score={sc} />
<td>
{!IsNullish(sc.calculatedData.sieglinde)
? sc.calculatedData.sieglinde!.toFixed(2)
: "N/A"}
</td>
<TimestampCell time={sc.timeAchieved} />
<BMSScoreCoreCells sc={sc} />
<TimestampCell time={sc.timeAchieved} service={sc.scoreMeta.client} />
</DropdownRow>
);
}
@@ -16,6 +16,9 @@ import MillionsScoreCell from "../cells/MillionsScoreCell";
import GenericScoreDropdown from "../dropdowns/GenericScoreDropdown";
import RatingCell from "../cells/RatingCell";
import { Playtype } from "types/tachi";
import { useScoreState } from "../components/UseScoreState";
import GenericScoreCoreCells from "../game-core-cells/GenericScoreCoreCells";
import { CreateDefaultScoreSearch } from "util/tables";
export default function GenericScoreTable({
reqUser,
@@ -51,22 +54,7 @@ export default function GenericScoreTable({
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
]}
entryName="Scores"
searchFunctions={{
artist: x => x.__related.song.artist,
title: x => x.__related.song.title,
difficulty: x => FormatDifficulty(x.__related.chart, game),
level: x => x.__related.chart.levelNum,
score: x => x.scoreData.score,
percent: x => x.scoreData.percent,
lamp: {
valueGetter: x => [x.scoreData.lamp, x.scoreData.lampIndex],
strToNum: HumanFriendlyStrToLampIndex(game, playtype),
},
grade: {
valueGetter: x => [x.scoreData.grade, x.scoreData.gradeIndex],
strToNum: HumanFriendlyStrToGradeIndex(game, playtype),
},
}}
searchFunctions={CreateDefaultScoreSearch(game, playtype)}
rowFunction={sc => (
<Row key={sc.scoreID} sc={sc} reqUser={reqUser} showScore={showScore} />
)}
@@ -83,14 +71,11 @@ function Row({
reqUser: PublicUserDocument;
showScore?: boolean;
}) {
const [highlight, setHighlight] = useState(sc.highlight);
const [comment, setComment] = useState(sc.comment);
const scoreState = { highlight, comment, setHighlight, setComment };
const scoreState = useScoreState(sc);
return (
<DropdownRow
className={highlight ? "highlighted-row" : ""}
className={scoreState.highlight ? "highlighted-row" : ""}
dropdown={
<GenericScoreDropdown
chart={sc.__related.chart}
@@ -103,11 +88,8 @@ function Row({
}
>
<DifficultyCell chart={sc.__related.chart} game={sc.game} />
<TitleCell song={sc.__related.song} chart={sc.__related.chart} game={sc.game} />
<ScoreCell score={sc} showScore={showScore} />
<LampCell score={sc} />
<RatingCell score={sc} />
<TimestampCell time={sc.timeAchieved} />
<GenericScoreCoreCells sc={sc} showScore={showScore} />
<TimestampCell time={sc.timeAchieved} service={sc.service} />
</DropdownRow>
);
}
@@ -11,6 +11,7 @@ import {
ScoreCalculatedDataLookup,
PublicUserDocument,
Playtypes,
ScoreDocument,
} from "tachi-common";
import TachiTable, { ZTableTHProps } from "../components/TachiTable";
import DifficultyCell from "../cells/DifficultyCell";
@@ -23,6 +24,10 @@ import DropdownRow from "../components/DropdownRow";
import IIDXScoreDropdown from "../dropdowns/IIDXScoreDropdown";
import { IsNullish } from "util/misc";
import { Playtype } from "types/tachi";
import { useScoreState } from "../components/UseScoreState";
import IIDXScoreCoreCells from "../game-core-cells/IIDXScoreCoreCells";
import { CreateDefaultScoreSearch } from "util/tables";
import RankingCell from "../cells/RankingCell";
export default function IIDXScoreTable({
reqUser,
@@ -69,22 +74,7 @@ export default function IIDXScoreTable({
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
]}
entryName="Scores"
searchFunctions={{
artist: x => x.__related.song.artist,
title: x => x.__related.song.title,
difficulty: x => FormatDifficulty(x.__related.chart, "iidx"),
level: x => x.__related.chart.levelNum,
score: x => x.scoreData.score,
percent: x => x.scoreData.percent,
lamp: {
valueGetter: x => [x.scoreData.lamp, x.scoreData.lampIndex],
strToNum: HumanFriendlyStrToLampIndex("iidx", playtype),
},
grade: {
valueGetter: x => [x.scoreData.grade, x.scoreData.gradeIndex],
strToNum: HumanFriendlyStrToGradeIndex("iidx", playtype),
},
}}
searchFunctions={CreateDefaultScoreSearch<"iidx:SP" | "iidx:DP">("iidx", playtype)}
rowFunction={sc => (
<Row
key={sc.scoreID}
@@ -109,14 +99,11 @@ function Row({
rating: ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"];
playtype: Playtype;
}) {
const [highlight, setHighlight] = useState(sc.highlight);
const [comment, setComment] = useState(sc.comment);
const scoreState = { highlight, comment, setHighlight, setComment };
const scoreState = useScoreState(sc);
return (
<DropdownRow
className={highlight ? "highlighted-row" : ""}
className={scoreState.highlight ? "highlighted-row" : ""}
dropdown={
<IIDXScoreDropdown
chart={sc.__related.chart}
@@ -129,22 +116,14 @@ function Row({
}
>
<DifficultyCell chart={sc.__related.chart} game="iidx" />
<TitleCell song={sc.__related.song} chart={sc.__related.chart} game="iidx" />
<ScoreCell score={sc} />
<DeltaCell
<TitleCell
song={sc.__related.song}
comment={sc.comment}
chart={sc.__related.chart}
game="iidx"
playtype={playtype}
score={sc.scoreData.score}
percent={sc.scoreData.percent}
grade={sc.scoreData.grade}
/>
<IIDXLampCell sc={sc} />
<td>
{!IsNullish(sc.calculatedData[rating])
? sc.calculatedData[rating]!.toFixed(2)
: "No Data."}
</td>
<TimestampCell time={sc.timeAchieved} />
<IIDXScoreCoreCells sc={sc} rating={rating} />
<TimestampCell time={sc.timeAchieved} service={sc.service} />
</DropdownRow>
);
}
@@ -1,10 +1,10 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { FormatDifficulty } from "tachi-common/js/utils/util";
import TitleCell from "../cells/TitleCell";
import TimestampCell from "../cells/TimestampCell";
import { NumericSOV, StrSOV } from "util/sorts";
import { ScoreDataset } from "types/tables";
import { integer, PublicUserDocument, Playtypes } from "tachi-common";
import { integer, PublicUserDocument, Playtypes, ScoreDocument } from "tachi-common";
import TachiTable from "../components/TachiTable";
import DifficultyCell from "../cells/DifficultyCell";
import ScoreCell from "../cells/ScoreCell";
@@ -16,6 +16,9 @@ import MillionsScoreCell from "../cells/MillionsScoreCell";
import GenericScoreDropdown from "../dropdowns/GenericScoreDropdown";
import RatingCell from "../cells/RatingCell";
import MusecaJudgementCell from "../cells/MusecaJudgementCell";
import { useScoreState } from "../components/UseScoreState";
import MusecaScoreCoreCells from "../game-core-cells/MusecaScoreCoreCells";
import { CreateDefaultScoreSearch } from "util/tables";
export default function MusecaScoreTable({
reqUser,
@@ -35,29 +38,14 @@ export default function MusecaScoreTable({
["Chart", "Ch.", NumericSOV(x => x.__related.chart.levelNum)],
["Song", "Song", StrSOV(x => x.__related.song.title)],
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Judgements", "Judge", 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)],
]}
entryName="Scores"
searchFunctions={{
artist: x => x.__related.song.artist,
title: x => x.__related.song.title,
difficulty: x => FormatDifficulty(x.__related.chart, "museca"),
level: x => x.__related.chart.levelNum,
score: x => x.scoreData.score,
percent: x => x.scoreData.percent,
lamp: {
valueGetter: x => [x.scoreData.lamp, x.scoreData.lampIndex],
strToNum: HumanFriendlyStrToLampIndex("museca", "Single"),
},
grade: {
valueGetter: x => [x.scoreData.grade, x.scoreData.gradeIndex],
strToNum: HumanFriendlyStrToGradeIndex("museca", "Single"),
},
}}
searchFunctions={CreateDefaultScoreSearch<"museca:Single">("museca", "Single")}
rowFunction={sc => <Row key={sc.scoreID} sc={sc} reqUser={reqUser} />}
/>
);
@@ -70,14 +58,11 @@ function Row({
sc: ScoreDataset<"museca:Single">[0];
reqUser: PublicUserDocument;
}) {
const [highlight, setHighlight] = useState(sc.highlight);
const [comment, setComment] = useState(sc.comment);
const scoreState = { highlight, comment, setHighlight, setComment };
const scoreState = useScoreState(sc);
return (
<DropdownRow
className={highlight ? "highlighted-row" : ""}
className={scoreState.highlight ? "highlighted-row" : ""}
dropdown={
<GenericScoreDropdown
chart={sc.__related.chart}
@@ -90,12 +75,14 @@ function Row({
}
>
<DifficultyCell chart={sc.__related.chart} game="museca" />
<TitleCell song={sc.__related.song} chart={sc.__related.chart} game="museca" />
<MillionsScoreCell score={sc} />
<MusecaJudgementCell score={sc} />
<LampCell score={sc} />
<RatingCell score={sc} />
<TimestampCell time={sc.timeAchieved} />
<TitleCell
song={sc.__related.song}
comment={sc.comment}
chart={sc.__related.chart}
game="museca"
/>
<MusecaScoreCoreCells sc={sc} />
<TimestampCell time={sc.timeAchieved} service={sc.service} />
</DropdownRow>
);
}
@@ -4,7 +4,7 @@ import TitleCell from "../cells/TitleCell";
import TimestampCell from "../cells/TimestampCell";
import { NumericSOV, StrSOV } from "util/sorts";
import { ScoreDataset } from "types/tables";
import { integer, PublicUserDocument, Playtypes } from "tachi-common";
import { integer, PublicUserDocument, Playtypes, ScoreDocument } from "tachi-common";
import TachiTable from "../components/TachiTable";
import DifficultyCell from "../cells/DifficultyCell";
import ScoreCell from "../cells/ScoreCell";
@@ -14,6 +14,10 @@ import { IsNullish } from "util/misc";
import LampCell from "../cells/LampCell";
import MillionsScoreCell from "../cells/MillionsScoreCell";
import GenericScoreDropdown from "../dropdowns/GenericScoreDropdown";
import SDVXJudgementCell from "../cells/SDVXJudgementCell";
import { useScoreState } from "../components/UseScoreState";
import SDVXScoreCoreCells from "../game-core-cells/SDVXCoreCells";
import { CreateDefaultScoreSearch } from "util/tables";
export default function SDVXScoreTable({
reqUser,
@@ -33,42 +37,25 @@ export default function SDVXScoreTable({
["Chart", "Ch.", NumericSOV(x => x.__related.chart.levelNum)],
["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)],
]}
entryName="Scores"
searchFunctions={{
artist: x => x.__related.song.artist,
title: x => x.__related.song.title,
difficulty: x => FormatDifficulty(x.__related.chart, "sdvx"),
level: x => x.__related.chart.levelNum,
score: x => x.scoreData.score,
percent: x => x.scoreData.percent,
lamp: {
valueGetter: x => [x.scoreData.lamp, x.scoreData.lampIndex],
strToNum: HumanFriendlyStrToLampIndex("sdvx", "Single"),
},
grade: {
valueGetter: x => [x.scoreData.grade, x.scoreData.gradeIndex],
strToNum: HumanFriendlyStrToGradeIndex("sdvx", "Single"),
},
}}
searchFunctions={CreateDefaultScoreSearch<"sdvx:Single">("sdvx", "Single")}
rowFunction={sc => <Row key={sc.scoreID} sc={sc} reqUser={reqUser} />}
/>
);
}
function Row({ sc, reqUser }: { sc: ScoreDataset<"sdvx:Single">[0]; reqUser: PublicUserDocument }) {
const [highlight, setHighlight] = useState(sc.highlight);
const [comment, setComment] = useState(sc.comment);
const scoreState = { highlight, comment, setHighlight, setComment };
const scoreState = useScoreState(sc);
return (
<DropdownRow
className={highlight ? "highlighted-row" : ""}
className={scoreState.highlight ? "highlighted-row" : ""}
dropdown={
<GenericScoreDropdown
chart={sc.__related.chart}
@@ -81,11 +68,14 @@ function Row({ sc, reqUser }: { sc: ScoreDataset<"sdvx:Single">[0]; reqUser: Pub
}
>
<DifficultyCell chart={sc.__related.chart} game="sdvx" />
<TitleCell song={sc.__related.song} chart={sc.__related.chart} game="sdvx" />
<MillionsScoreCell score={sc} />
<LampCell score={sc} />
<td>{!IsNullish(sc.calculatedData.VF6) ? sc.calculatedData.VF6!.toFixed(3) : "N/A"}</td>
<TimestampCell time={sc.timeAchieved} />
<TitleCell
song={sc.__related.song}
comment={sc.comment}
chart={sc.__related.chart}
game="sdvx"
/>
<SDVXScoreCoreCells sc={sc} />
<TimestampCell time={sc.timeAchieved} service={sc.service} />
</DropdownRow>
);
}
+81 -117
View File
@@ -20,7 +20,7 @@ import { FormatGPTRating, UppercaseFirst } from "util/misc";
import ProfileBadges from "./ProfileBadges";
import { GameClassSets } from "tachi-common/js/game-classes";
export default function UGPTHeader({
export function UGPTHeaderBody({
reqUser,
game,
playtype,
@@ -34,124 +34,88 @@ export default function UGPTHeader({
const gptConfig = GetGamePTConfig(game, playtype);
return (
<div className="row">
<div className="col-12">
<div className="card card-custom">
<div className="card-header">
<h4>
{reqUser.username}'s {FormatGame(game, playtype)} Profile
</h4>
</div>
<div className="card-body">
<div className="row align-items-center">
<div className="col-12 col-lg-3">
<div className="d-flex justify-content-center mb-3">
<ProfilePicture user={reqUser} />
</div>
<div
className="d-flex align-items-center"
style={{ flexDirection: "column" }}
>
<ProfileBadges badges={reqUser.badges} />
</div>
<div className="d-block d-lg-none">
<Divider className="mt-4 mb-4" />
</div>
</div>
<div className="col-12 col-md-6 col-lg-3">
<MiniTable
className="table-sm text-center"
headers={["Player Info"]}
colSpan={2}
>
<tr>
<td>Playcount</td>
<td>{stats.totalScores}</td>
</tr>
<tr>
<td>Last Played</td>
<td>
{stats.mostRecentScore === null ||
stats.mostRecentScore.timeAchieved === null
? "Unknown."
: MillisToSince(stats.mostRecentScore.timeAchieved)}
</td>
</tr>
<tr>
<td>First Play</td>
<td>
{stats.firstScore === null ||
stats.firstScore.timeAchieved === null
? "Unknown."
: MillisToSince(stats.firstScore.timeAchieved)}
</td>
</tr>
</MiniTable>
</div>
<div className="col-12 col-md-6 col-lg-3">
<MiniTable
className="table-sm text-center"
headers={["Player Stats"]}
colSpan={2}
>
<>
{(Object.keys(
gptConfig.classHumanisedFormat
) as GameClassSets[IDStrings][]).map(k => (
<tr key={k}>
<td>{UppercaseFirst(k)}</td>
<td>
{stats.gameStats.classes[k] ? (
<ClassBadge
showSetOnHover={false}
key={`${k}:${stats.gameStats.classes[k]}`}
game={game}
playtype={playtype}
classSet={k}
classValue={stats.gameStats.classes[k]!}
/>
) : (
"No Data"
)}
</td>
</tr>
))}
{Object.entries(stats.gameStats.ratings).map(([k, v]) => (
<tr key={k}>
<td>{UppercaseFirst(k)}</td>
<td>
{FormatGPTRating(
game,
playtype,
k as ScoreCalculatedDataLookup[IDStrings],
v
)}
</td>
</tr>
))}
</>
</MiniTable>
</div>
<div className="col-12 col-lg-3">
<RankingData
ranking={stats.rankingData.ranking}
outOf={stats.rankingData.outOf}
/>
</div>
</div>
</div>
<div className="card-footer pb-0 pt-0">
<BottomNav
baseUrl={`/dashboard/users/${reqUser.username}/games/${game}/${playtype}`}
/>
</div>
<>
<div className="col-12 col-lg-3">
<div className="d-flex justify-content-center mb-3">
<ProfilePicture user={reqUser} />
</div>
<div className="d-flex align-items-center" style={{ flexDirection: "column" }}>
<ProfileBadges badges={reqUser.badges} />
</div>
<div className="d-block d-lg-none">
<Divider className="mt-4 mb-4" />
</div>
</div>
<div className="col-12">
<Divider className="mt-8 mb-4" />
<div className="col-12 col-md-6 col-lg-3">
<MiniTable className="table-sm text-center" headers={["Player Info"]} colSpan={2}>
<tr>
<td>Playcount</td>
<td>{stats.totalScores}</td>
</tr>
<tr>
<td>Last Played</td>
<td>
{stats.mostRecentScore === null ||
stats.mostRecentScore.timeAchieved === null
? "Unknown."
: MillisToSince(stats.mostRecentScore.timeAchieved)}
</td>
</tr>
<tr>
<td>First Play</td>
<td>
{stats.firstScore === null || stats.firstScore.timeAchieved === null
? "Unknown."
: MillisToSince(stats.firstScore.timeAchieved)}
</td>
</tr>
</MiniTable>
</div>
</div>
<div className="col-12 col-md-6 col-lg-3">
<MiniTable className="table-sm text-center" headers={["Player Stats"]} colSpan={2}>
<>
{(Object.keys(
gptConfig.classHumanisedFormat
) as GameClassSets[IDStrings][]).map(k => (
<tr key={k}>
<td>{UppercaseFirst(k)}</td>
<td>
{stats.gameStats.classes[k] ? (
<ClassBadge
showSetOnHover={false}
key={`${k}:${stats.gameStats.classes[k]}`}
game={game}
playtype={playtype}
classSet={k}
classValue={stats.gameStats.classes[k]!}
/>
) : (
"No Data"
)}
</td>
</tr>
))}
{Object.entries(stats.gameStats.ratings).map(([k, v]) => (
<tr key={k}>
<td>{UppercaseFirst(k)}</td>
<td>
{FormatGPTRating(
game,
playtype,
k as ScoreCalculatedDataLookup[IDStrings],
v
)}
</td>
</tr>
))}
</>
</MiniTable>
</div>
<div className="col-12 col-lg-3">
<RankingData ranking={stats.rankingData.ranking} outOf={stats.rankingData.outOf} />
</div>
</>
);
}
@@ -169,7 +133,7 @@ function RankingData({ ranking, outOf }: { ranking: number; outOf: number }) {
);
}
function BottomNav({ baseUrl }: { baseUrl: string }) {
export function UGPTBottomNav({ baseUrl }: { baseUrl: string }) {
return (
<div className="row align-items-center mb-0">
<Navbar>
+61
View File
@@ -0,0 +1,61 @@
import Divider from "components/util/Divider";
import React, { useContext } from "react";
import { PublicUserDocument } from "tachi-common";
import ProfilePicture from "./ProfilePicture";
import Navbar from "components/nav/Navbar";
import NavItem from "components/nav/NavItem";
import ProfileBadges from "./ProfileBadges";
import { UserContext } from "context/UserContext";
export function UserHeaderBody({ reqUser }: { reqUser: PublicUserDocument }) {
return (
<>
<div className="col-12 col-lg-3">
<div className="d-flex justify-content-center mb-3">
<ProfilePicture user={reqUser} />
</div>
<div className="d-flex align-items-center" style={{ flexDirection: "column" }}>
<ProfileBadges badges={reqUser.badges} />
</div>
<div className="d-block d-lg-none">
<Divider className="mt-4 mb-4" />
</div>
</div>
</>
);
}
export function UserBottomNav({
baseUrl,
reqUser,
}: {
baseUrl: string;
reqUser: PublicUserDocument;
}) {
const { user } = useContext(UserContext);
const isRequestedUser = !!(user && user.id === reqUser.id);
const navItems = [
<NavItem key="about" to={`${baseUrl}/`}>
About
</NavItem>,
<NavItem key="games" to={`${baseUrl}/games`}>
Games
</NavItem>,
];
if (isRequestedUser) {
navItems.push(
<NavItem key="settings" to={`${baseUrl}/settings`}>
Settings
</NavItem>
);
}
return (
<div className="row align-items-center mb-0">
<Navbar>{navItems}</Navbar>
</div>
);
}
@@ -0,0 +1,31 @@
import Divider from "components/util/Divider";
import React from "react";
export default function UserHeaderContainer({
header,
footer,
children,
}: {
children: React.ReactChild;
footer: React.ReactChild;
header: React.ReactChild;
}) {
return (
<div className="row">
<div className="col-12">
<div className="card card-custom">
<div className="card-header">
<h4>{header}</h4>
</div>
<div className="card-body">
<div className="row align-items-center">{children}</div>
</div>
<div className="card-footer pb-0 pt-0">{footer}</div>
</div>
</div>
<div className="col-12">
<Divider className="mt-8 mb-4" />
</div>
</div>
);
}
@@ -0,0 +1,7 @@
import React from "react";
export default function HasDevModeOn({ children }: { children: React.ReactChild }) {
// settings mock context?
return <>{children}</>;
}
@@ -92,7 +92,9 @@ export function useZTable<D>(originalDataset: D[], providedOptions?: Partial<ZTa
return `Displaying ${(page - 1) * pageLen + 1} to ${Math.min(
page * pageLen,
dataset.length
)} of ${dataset.length} ${entryName.toLowerCase()}${search !== "" ? " (Filtered)" : ""}.`;
)} of ${dataset.length} ${entryName.toLowerCase()}${
search !== "" ? ` (Filtered from ${originalDataset.length})` : ""
}.`;
}, [page, dataset]);
// Create a sliding window that can be used for pagination.
+1
View File
@@ -10,6 +10,7 @@ import {
export type PBDataset<I extends IDStrings = IDStrings> = (PBScoreDocument<I> & {
__related: { chart: ChartDocument<I>; song: SongDocument<IDStringToGame[I]>; index: integer };
__playcount?: integer;
})[];
export type ScoreDataset<I extends IDStrings = IDStrings> = (ScoreDocument<I> & {
+1 -1
View File
@@ -9,7 +9,7 @@ export function AbsoluteGradeDelta<I extends IDStrings = IDStrings>(
) {
const gptConfig = GetGamePTConfig(game, playtype);
const maxScore = Math.floor(score * (100 / percent));
const maxScore = Math.round(score * (100 / percent));
const gradeIndex =
typeof gradeOrIndex === "number" ? gradeOrIndex : gptConfig.grades.indexOf(gradeOrIndex);
+27
View File
@@ -0,0 +1,27 @@
import { FormatDifficulty, Game, IDStrings } from "tachi-common";
import { ScoreDataset } from "types/tables";
import { Playtype } from "types/tachi";
import { HumanFriendlyStrToLampIndex, HumanFriendlyStrToGradeIndex } from "./str-to-num";
import { ValueGetterOrHybrid } from "./ztable/search";
export function CreateDefaultScoreSearch<I extends IDStrings = IDStrings>(
game: Game,
playtype: Playtype
): Record<string, ValueGetterOrHybrid<ScoreDataset<I>[0]>> {
return {
artist: x => x.__related.song.artist,
title: x => x.__related.song.title,
difficulty: x => FormatDifficulty(x.__related.chart, game),
level: x => x.__related.chart.levelNum,
score: x => x.scoreData.score,
percent: x => x.scoreData.percent,
lamp: {
valueGetter: x => [x.scoreData.lamp, x.scoreData.lampIndex],
strToNum: HumanFriendlyStrToLampIndex(game, playtype),
},
grade: {
valueGetter: x => [x.scoreData.grade, x.scoreData.gradeIndex],
strToNum: HumanFriendlyStrToGradeIndex(game, playtype),
},
};
}