mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-07 22:38:02 +03:00
Yet another huge sync update
This commit is contained in:
+1
-2
@@ -66,7 +66,6 @@
|
||||
"react-bootstrap": "1.0.1",
|
||||
"react-dom": "16.12.0",
|
||||
"react-hot-toast": "2.0.0",
|
||||
"react-inlinesvg": "1.2.0",
|
||||
"react-perfect-scrollbar": "1.5.8",
|
||||
"react-query": "3.18.1",
|
||||
"react-router": "5.2.0",
|
||||
@@ -75,7 +74,7 @@
|
||||
"resolve": "1.12.0",
|
||||
"resolve-url-loader": "3.1.0",
|
||||
"socicon": "3.0.5",
|
||||
"tachi-common": "github:zkldi/tachi-common"
|
||||
"tachi-common": "0.1.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/humanize-duration": "3.25.1",
|
||||
|
||||
Generated
+23
-50
@@ -65,7 +65,6 @@ specifiers:
|
||||
react-dev-utils: ^9.1.0
|
||||
react-dom: 16.12.0
|
||||
react-hot-toast: 2.0.0
|
||||
react-inlinesvg: 1.2.0
|
||||
react-perfect-scrollbar: 1.5.8
|
||||
react-query: 3.18.1
|
||||
react-router: 5.2.0
|
||||
@@ -78,7 +77,7 @@ specifiers:
|
||||
serve: 11.2.0
|
||||
socicon: 3.0.5
|
||||
style-loader: 1.0.0
|
||||
tachi-common: github:zkldi/tachi-common
|
||||
tachi-common: 0.1.28
|
||||
terser-webpack-plugin: 1.4.1
|
||||
ts-pnp: 1.1.4
|
||||
typescript: 4.3.5
|
||||
@@ -134,7 +133,6 @@ dependencies:
|
||||
react-bootstrap: 1.0.1_react-dom@16.12.0+react@17.0.2
|
||||
react-dom: 16.12.0_react@17.0.2
|
||||
react-hot-toast: 2.0.0_react-dom@16.12.0+react@17.0.2
|
||||
react-inlinesvg: 1.2.0_react@17.0.2
|
||||
react-perfect-scrollbar: 1.5.8_react-dom@16.12.0+react@17.0.2
|
||||
react-query: 3.18.1_react-dom@16.12.0+react@17.0.2
|
||||
react-router: 5.2.0_react@17.0.2
|
||||
@@ -143,7 +141,7 @@ dependencies:
|
||||
resolve: 1.12.0
|
||||
resolve-url-loader: 3.1.0
|
||||
socicon: 3.0.5
|
||||
tachi-common: github.com/zkldi/tachi-common/960879126ca93099d412866ca0f3a8327979034f_typescript@4.3.5
|
||||
tachi-common: 0.1.28_typescript@4.3.5
|
||||
|
||||
devDependencies:
|
||||
'@types/humanize-duration': 3.25.1
|
||||
@@ -5926,10 +5924,6 @@ packages:
|
||||
strip-eof: 1.0.0
|
||||
dev: true
|
||||
|
||||
/exenv/1.2.2:
|
||||
resolution: {integrity: sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=}
|
||||
dev: false
|
||||
|
||||
/expand-brackets/2.1.4:
|
||||
resolution: {integrity: sha1-t3c14xXOMPa27/D4OwQVGiJEliI=}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -8286,7 +8280,7 @@ packages:
|
||||
bl: 2.2.1
|
||||
bson: 1.1.6
|
||||
denque: 1.5.0
|
||||
optional-require: 1.1.4
|
||||
optional-require: 1.1.7
|
||||
safe-buffer: 5.2.1
|
||||
optionalDependencies:
|
||||
saslprep: 1.0.3
|
||||
@@ -8750,8 +8744,8 @@ packages:
|
||||
webpack: 4.41.0
|
||||
dev: false
|
||||
|
||||
/optional-require/1.1.4:
|
||||
resolution: {integrity: sha512-YIabCRxu1Qqlxowh5MBaT8vrvC5fa5QehT4iJw8OthkTC1ZXe7aMxtZHtwnNwJRfFgiqPof2RZeguXo4weS9mA==}
|
||||
/optional-require/1.1.7:
|
||||
resolution: {integrity: sha512-cIeRZocXsZnZYn+SevbtSqNlLbeoS4mLzuNn4fvXRMDRNhTGg0sxuKXl0FnZCtnew85LorNxIbZp5OeliILhMw==}
|
||||
engines: {node: '>=4'}
|
||||
dependencies:
|
||||
require-at: 1.0.6
|
||||
@@ -10083,14 +10077,6 @@ packages:
|
||||
resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==}
|
||||
dev: false
|
||||
|
||||
/react-from-dom/0.3.1_react@17.0.2:
|
||||
resolution: {integrity: sha512-PeNBa8iuzoD7qHA9O7YpGnXFvC+XFFwStmFh2/r2zJAvEIaRg6EwOj+EPcDIFwyYBhqPIItxIx/dGdeWiFivjQ==}
|
||||
peerDependencies:
|
||||
react: ^15.0.0 || ^16.0.0
|
||||
dependencies:
|
||||
react: 17.0.2
|
||||
dev: false
|
||||
|
||||
/react-hot-toast/2.0.0_react-dom@16.12.0+react@17.0.2:
|
||||
resolution: {integrity: sha512-J0J2rcSvKetlziVquwESgk85pV9JB0Dz3RJIZcAEv7CWU2y2z8BQqxmZHZUxjPKIBGPqXAocZch4Kj4oUdX4+w==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -10105,16 +10091,6 @@ packages:
|
||||
- csstype
|
||||
dev: false
|
||||
|
||||
/react-inlinesvg/1.2.0_react@17.0.2:
|
||||
resolution: {integrity: sha512-IsznU+UzpUwDGzBWbf0bfSRA5Jbqz87xeoqLM/nSIDPkoHksInF1wCGybTSn4sIui+30TqboRQP1wAelNTkdog==}
|
||||
peerDependencies:
|
||||
react: ^16.3.0
|
||||
dependencies:
|
||||
exenv: 1.2.2
|
||||
react: 17.0.2
|
||||
react-from-dom: 0.3.1_react@17.0.2
|
||||
dev: false
|
||||
|
||||
/react-is/16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@@ -11499,6 +11475,24 @@ packages:
|
||||
strip-ansi: 6.0.0
|
||||
dev: true
|
||||
|
||||
/tachi-common/0.1.28_typescript@4.3.5:
|
||||
resolution: {integrity: sha512-UibZ7mx7y27I3SuxTyLINBhipGDM1pxqBymkWfpnm+GIhGt6iVk6USWhYOsMJhaSaNcgG1hmIrWn9rUmwVAELg==}
|
||||
dependencies:
|
||||
monk: 7.3.4
|
||||
tap: 15.0.9_typescript@4.3.5
|
||||
transitivePeerDependencies:
|
||||
- aws4
|
||||
- bson-ext
|
||||
- flow-remove-types
|
||||
- kerberos
|
||||
- mongodb-client-encryption
|
||||
- mongodb-extjson
|
||||
- snappy
|
||||
- supports-color
|
||||
- ts-node
|
||||
- typescript
|
||||
dev: false
|
||||
|
||||
/tap-mocha-reporter/5.0.1:
|
||||
resolution: {integrity: sha512-1knFWOwd4khx/7uSEnUeaP9IPW3w+sqTgJMhrwah6t46nZ8P25atOKAjSvVDsT67lOPu0nfdOqUwoyKn+3E5pA==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -12629,24 +12623,3 @@ packages:
|
||||
y18n: 4.0.3
|
||||
yargs-parser: 18.1.3
|
||||
dev: false
|
||||
|
||||
github.com/zkldi/tachi-common/960879126ca93099d412866ca0f3a8327979034f_typescript@4.3.5:
|
||||
resolution: {tarball: https://codeload.github.com/zkldi/tachi-common/tar.gz/960879126ca93099d412866ca0f3a8327979034f}
|
||||
id: github.com/zkldi/tachi-common/960879126ca93099d412866ca0f3a8327979034f
|
||||
name: tachi-common
|
||||
version: 0.1.0
|
||||
dependencies:
|
||||
monk: 7.3.4
|
||||
tap: 15.0.9_typescript@4.3.5
|
||||
transitivePeerDependencies:
|
||||
- aws4
|
||||
- bson-ext
|
||||
- flow-remove-types
|
||||
- kerberos
|
||||
- mongodb-client-encryption
|
||||
- mongodb-extjson
|
||||
- snappy
|
||||
- supports-color
|
||||
- ts-node
|
||||
- typescript
|
||||
dev: false
|
||||
|
||||
@@ -106,10 +106,11 @@ function LeaderboardsPageContent({
|
||||
style={{
|
||||
backgroundColor:
|
||||
reqUser.id === s.userID ? ChangeOpacity(COLOUR_SET.gold, 0.15) : undefined,
|
||||
height: reqUser.id === s.userID ? "50px" : undefined,
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
<strong>#{stats.thisUsersRanking.ranking - stats.above.length + i}</strong>
|
||||
<strong>#{i}</strong>
|
||||
{reqUser.id === s.userID && (
|
||||
<small className="text-muted">/{stats.thisUsersRanking.outOf}</small>
|
||||
)}
|
||||
@@ -151,14 +152,18 @@ function LeaderboardsPageContent({
|
||||
{bestNearbyUser > 1 &&
|
||||
leaderboard.gameStats
|
||||
.slice(0, bestNearbyUser)
|
||||
.map((s, i) => <LeaderboardRow key={s.userID} s={s} i={i} />)}
|
||||
.map((s, i) => <LeaderboardRow key={s.userID} s={s} i={i + 1} />)}
|
||||
{bestNearbyUser > 4 && (
|
||||
<tr style={{ lineHeight: "0.5rem" }}>
|
||||
<td colSpan={4}>...</td>
|
||||
</tr>
|
||||
)}
|
||||
{[...stats.above, stats.thisUsersStats, ...stats.below].map((s, i) => (
|
||||
<LeaderboardRow key={s.userID} s={s} i={i} />
|
||||
<LeaderboardRow
|
||||
key={s.userID}
|
||||
s={s}
|
||||
i={stats.thisUsersRanking.ranking - stats.above.length + i}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
</MiniTable>
|
||||
|
||||
@@ -15,8 +15,8 @@ import {
|
||||
UGPTSettings,
|
||||
FormatChart,
|
||||
Game,
|
||||
AnySongDocument,
|
||||
AnyChartDocument,
|
||||
SongDocument,
|
||||
ChartDocument,
|
||||
FolderDocument,
|
||||
GetGamePTConfig,
|
||||
UserGameStats,
|
||||
@@ -190,10 +190,10 @@ function RecentSessionScoreInfo({
|
||||
sessionData: SessionReturns;
|
||||
reqUser: PublicUserDocument;
|
||||
} & GamePT) {
|
||||
const highlightedScores = sessionData.scores.find(e => e.highlight);
|
||||
const highlightedScores = sessionData.scores.filter(e => e.highlight);
|
||||
|
||||
const [mode, setMode] = useState<"highlight" | "best" | "recent">(
|
||||
highlightedScores ? "highlight" : "best"
|
||||
highlightedScores.length > 0 ? "highlight" : "best"
|
||||
);
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
@@ -204,7 +204,7 @@ function RecentSessionScoreInfo({
|
||||
const dataset = useMemo(() => {
|
||||
let scoreSet = sessionData.scores;
|
||||
if (mode === "highlight") {
|
||||
scoreSet = sessionData.scores.filter(e => e.highlight);
|
||||
scoreSet = highlightedScores;
|
||||
} else if (mode === "best") {
|
||||
scoreSet.sort((a, b) =>
|
||||
NumericSOV<ScoreDocument>(
|
||||
@@ -253,7 +253,12 @@ function RecentSessionScoreInfo({
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-12 mt-4">
|
||||
<IIDXScoreTable dataset={dataset as any} pageLen={5} reqUser={reqUser} />
|
||||
<IIDXScoreTable
|
||||
dataset={dataset as any}
|
||||
pageLen={5}
|
||||
reqUser={reqUser}
|
||||
playtype={playtype as any}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -739,7 +744,7 @@ function StatDisplay({
|
||||
const { stat, value, related } = statData;
|
||||
|
||||
if (stat.mode === "chart") {
|
||||
const { song, chart } = related as { song: AnySongDocument; chart: AnyChartDocument };
|
||||
const { song, chart } = related as { song: SongDocument; chart: ChartDocument };
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import useSetSubheader from "components/layout/header/useSetSubheader";
|
||||
import IIDXPBTable from "components/tables/pbs/IIDXPBTable";
|
||||
import DebounceSearch from "components/util/DebounceSearch";
|
||||
import React, { useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
@@ -21,6 +20,7 @@ import IIDXScoreTable from "components/tables/scores/IIDXScoreTable";
|
||||
import LoadingWrapper from "components/util/LoadingWrapper";
|
||||
import Icon from "components/util/Icon";
|
||||
import SelectButton from "components/util/SelectButton";
|
||||
import PBTable from "components/tables/pbs/PBTable";
|
||||
|
||||
export default function ScoresPage({
|
||||
reqUser,
|
||||
@@ -45,7 +45,7 @@ export default function ScoresPage({
|
||||
<div className="btn-group mb-4">
|
||||
<SelectButton id="best" setValue={setScoreSet} value={scoreSet}>
|
||||
<Icon type="trophy" />
|
||||
Best Scores
|
||||
Best PBs
|
||||
</SelectButton>
|
||||
<SelectButton id="recent" setValue={setScoreSet} value={scoreSet}>
|
||||
<Icon type="history" />
|
||||
@@ -104,9 +104,10 @@ function PBsOverview({ reqUser, game, playtype }: { reqUser: PublicUserDocument
|
||||
style={{ height: 500 }}
|
||||
{...{ isLoading, error, dataset: data }}
|
||||
>
|
||||
<IIDXPBTable
|
||||
<PBTable
|
||||
reqUser={reqUser}
|
||||
dataset={data!}
|
||||
game={game}
|
||||
playtype={playtype as "SP" | "DP"}
|
||||
/>
|
||||
</LoadingWrapper>
|
||||
@@ -176,10 +177,11 @@ function PBsSearch({
|
||||
|
||||
return (
|
||||
<LoadingWrapper style={{ height: 500 }} {...{ isLoading, error, dataset: data }}>
|
||||
<IIDXPBTable
|
||||
<PBTable
|
||||
reqUser={reqUser}
|
||||
indexCol={false}
|
||||
dataset={data!}
|
||||
game={game}
|
||||
playtype={playtype as "SP" | "DP"}
|
||||
/>
|
||||
</LoadingWrapper>
|
||||
@@ -208,7 +210,11 @@ function ScoresOverview({ reqUser, game, playtype }: { reqUser: PublicUserDocume
|
||||
style={{ height: 500 }}
|
||||
{...{ isLoading, dataset: data, error }}
|
||||
>
|
||||
<IIDXScoreTable dataset={data!} reqUser={reqUser} />
|
||||
<IIDXScoreTable
|
||||
dataset={data!}
|
||||
reqUser={reqUser}
|
||||
playtype={playtype as any}
|
||||
/>
|
||||
</LoadingWrapper>
|
||||
) : (
|
||||
<ScoresSearch {...{ reqUser, game, playtype, search }} />
|
||||
@@ -230,7 +236,7 @@ function ScoresSearch({
|
||||
|
||||
return (
|
||||
<LoadingWrapper style={{ height: 500 }} {...{ isLoading, error, dataset: data }}>
|
||||
<IIDXScoreTable dataset={data!} reqUser={reqUser} />
|
||||
<IIDXScoreTable dataset={data!} reqUser={reqUser} playtype={playtype as any} />
|
||||
</LoadingWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function IIDXLampChart({
|
||||
data={realData}
|
||||
margin={{ top: 30, bottom: 50, left: 50, right: 50 }}
|
||||
xScale={{ type: "linear" }}
|
||||
axisBottom={{ format: x => (Math.floor(Number(x) / 4) + 1).toString() }}
|
||||
axisBottom={{ format: x => Math.floor(Number(x) / 4).toString() }}
|
||||
motionConfig="stiff"
|
||||
crosshairType="x"
|
||||
yScale={{ type: "linear", min: 0, max: 100 }}
|
||||
@@ -119,7 +119,7 @@ export default function IIDXLampChart({
|
||||
point={d.point}
|
||||
renderFn={p => (
|
||||
<div>
|
||||
Measure {Math.floor(Number(p.data.xFormatted) / 4 + 1).toString()}:{" "}
|
||||
Measure {Math.floor(Number(p.data.xFormatted) / 4).toString()}:{" "}
|
||||
{p.data.yFormatted}%
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,7 +4,7 @@ import Divider from "components/util/Divider";
|
||||
import { UserContext } from "context/UserContext";
|
||||
import { useQuery } from "react-query";
|
||||
import { APIFetchV1 } from "util/api";
|
||||
import { UserGameStats, FormatGame } from "tachi-common";
|
||||
import { UserGameStats, FormatGame, GetGameConfig } from "tachi-common";
|
||||
import MenuLink from "./MenuLink";
|
||||
import MenuDropdown from "./MenuDropdown";
|
||||
import Loading from "components/util/Loading";
|
||||
@@ -12,6 +12,7 @@ import KTLayoutHeader from "_metronic/_assets/js/layout/base/header";
|
||||
import KTLayoutHeaderMenu from "_metronic/_assets/js/layout/base/header-menu";
|
||||
import AllGames from "./AllGames";
|
||||
import { UserGameStatsContext } from "context/UserGameStatsContext";
|
||||
import { TachiConfig } from "lib/config";
|
||||
|
||||
export function HeaderMenu() {
|
||||
const { user } = useContext(UserContext);
|
||||
@@ -46,6 +47,33 @@ export function HeaderMenu() {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
const userProfileLinks = [];
|
||||
|
||||
if (user && ugs && ugs.length !== 0) {
|
||||
const ugsMap = new Map();
|
||||
for (const s of ugs) {
|
||||
ugsMap.set(`${s.game}:${s.playtype}`, s);
|
||||
}
|
||||
|
||||
for (const game of TachiConfig.supportedGames) {
|
||||
for (const playtype of GetGameConfig(game).validPlaytypes) {
|
||||
const e = ugsMap.get(`${game}:${playtype}`);
|
||||
|
||||
if (!e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
userProfileLinks.push(
|
||||
<MenuLink
|
||||
key={`${e.game}:${e.playtype}`}
|
||||
name={FormatGame(e.game, e.playtype)}
|
||||
to={`/dashboard/users/${user.username}/games/${e.game}/${e.playtype}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="header-menu-wrapper header-menu-wrapper-left" id="kt_header_menu_wrapper">
|
||||
<div
|
||||
@@ -63,18 +91,8 @@ export function HeaderMenu() {
|
||||
</div>
|
||||
</div>
|
||||
<ul className="menu-nav">
|
||||
{/* <MenuLink name="Dashboard" to="/dashboard" /> */}
|
||||
|
||||
{user && ugs && ugs.length !== 0 && (
|
||||
<MenuDropdown name="Your Profiles">
|
||||
{ugs.map(e => (
|
||||
<MenuLink
|
||||
key={`${e.game}:${e.playtype}`}
|
||||
name={FormatGame(e.game, e.playtype)}
|
||||
to={`/dashboard/users/${user.username}/games/${e.game}/${e.playtype}`}
|
||||
/>
|
||||
))}
|
||||
</MenuDropdown>
|
||||
<MenuDropdown name="Your Profiles">{userProfileLinks}</MenuDropdown>
|
||||
)}
|
||||
|
||||
<AllGames />
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { AnySongDocument, Game, GetGameConfig, integer, PublicUserDocument } from "tachi-common";
|
||||
import { SongDocument, Game, GetGameConfig, integer, PublicUserDocument } from "tachi-common";
|
||||
import { APIFetchV1 } from "util/api";
|
||||
import { toAbsoluteUrl } from "_metronic/_helpers";
|
||||
import SVG from "react-inlinesvg";
|
||||
import { JustChildren } from "types/react";
|
||||
import { Link } from "react-router-dom";
|
||||
import Divider from "components/util/Divider";
|
||||
import { NO_OP, PREVENT_DEFAULT } from "util/misc";
|
||||
import Loading from "components/util/Loading";
|
||||
|
||||
interface SearchReturns {
|
||||
users: PublicUserDocument[];
|
||||
songs: (AnySongDocument & { __textScore: number; game: Game })[];
|
||||
songs: (SongDocument & { __textScore: number; game: Game })[];
|
||||
}
|
||||
|
||||
function SearchResult({
|
||||
@@ -29,6 +30,14 @@ function SearchResult({
|
||||
}
|
||||
|
||||
function SearchResults({ results }: { results: SearchReturns }) {
|
||||
if (!results.users.length && !results.songs.length) {
|
||||
return (
|
||||
<div className="font-size-sm font-weight-bolder text-uppercase mb-2 text-center">
|
||||
Found nothing :(
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="quick-search-result">
|
||||
{results?.users.length ? (
|
||||
@@ -110,6 +119,10 @@ export default function SearchBar() {
|
||||
};
|
||||
}, [ref]);
|
||||
|
||||
useEffect(() => {
|
||||
setShow(search !== "");
|
||||
}, [search]);
|
||||
|
||||
// debouncer result-getter
|
||||
function UpdateSearch(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
setSearch(event.target.value);
|
||||
@@ -130,11 +143,7 @@ export default function SearchBar() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (r.body.users.length === 0 && r.body.songs.length === 0) {
|
||||
setResults(null);
|
||||
} else {
|
||||
setResults(r.body);
|
||||
}
|
||||
setResults(r.body);
|
||||
}
|
||||
);
|
||||
}, 250);
|
||||
@@ -144,55 +153,44 @@ export default function SearchBar() {
|
||||
|
||||
return (
|
||||
<div ref={ref} className="align-self-center">
|
||||
<div
|
||||
className="topbar-item"
|
||||
onClick={() => {
|
||||
if (show === false) {
|
||||
setShow(true);
|
||||
// @ts-expect-error temp
|
||||
// Magic-level hack: wait 10ms before focusing this element because react fails otherwise
|
||||
setTimeout(() => inputRef.current.focus(), 10);
|
||||
} else {
|
||||
setShow(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="btn btn-icon btn-hover-transparent-white btn-lg mr-1">
|
||||
<span className="svg-icon svg-icon-xl">
|
||||
{/* <SVG src={toAbsoluteUrl("/media/svg/icons/General/Search.svg")} /> */}
|
||||
</span>
|
||||
</div>
|
||||
<div className="topbar-item mr-2">
|
||||
<form className="quick-search-form" onSubmit={PREVENT_DEFAULT}>
|
||||
<div className="input-group">
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
value={search}
|
||||
onChange={UpdateSearch}
|
||||
onClick={() => {
|
||||
if (search !== "") {
|
||||
setShow(true);
|
||||
}
|
||||
}}
|
||||
placeholder="Search users, songs, charts..."
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div style={{ position: "relative", right: "350px" }}>
|
||||
<div style={{ position: "relative", top: "20px" }}>
|
||||
<div
|
||||
className={`dropdown-menu p-0 m-0 dropdown-menu-right dropdown-menu-anim-up dropdown-menu-lg dropdown-menu dropdown-menu-right ${
|
||||
show ? "show" : "hide"
|
||||
}`}
|
||||
style={{
|
||||
position: "absolute",
|
||||
borderRadius: "0 0 5px 5px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
id="kt_quick_search_dropdown"
|
||||
className="quick-search quick-search-dropdown quick-search-has-result"
|
||||
>
|
||||
<form className="quick-search-form" onSubmit={() => 0}>
|
||||
<div className="input-group">
|
||||
<input
|
||||
className="form-control"
|
||||
type="text"
|
||||
ref={inputRef}
|
||||
value={search}
|
||||
onChange={UpdateSearch}
|
||||
placeholder="Search users, songs, charts..."
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<div
|
||||
style={{ maxHeight: "325px", overflowY: "scroll" }}
|
||||
className="quick-search-wrapper"
|
||||
>
|
||||
{results && search !== "" && <SearchResults results={results} />}
|
||||
{results ? <SearchResults results={results} /> : <Loading />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, { useContext } from "react";
|
||||
import { UserNotificationsDropdown } from "../../../_metronic/layout/components/extras/dropdowns/UserNotificationsDropdown";
|
||||
import { UserProfileDropdown } from "components/layout/header/UserProfileDropdown";
|
||||
import { UserContext } from "context/UserContext";
|
||||
import LinkButton from "components/util/LinkButton";
|
||||
@@ -14,8 +13,7 @@ export function Topbar() {
|
||||
|
||||
{user ? (
|
||||
<>
|
||||
<UserNotificationsDropdown />
|
||||
|
||||
{/* commented out quick panel stuff for future */}
|
||||
{/* <OverlayTrigger
|
||||
placement="bottom"
|
||||
overlay={<Tooltip id="quick-panel-tooltip">Quick panel</Tooltip>}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { APIFetchV1, ToAPIURL } from "util/api";
|
||||
import toast from "react-hot-toast";
|
||||
import { UserContext } from "context/UserContext";
|
||||
import DropdownToggleOverride from "components/util/DropdownToggleOverride";
|
||||
import Icon from "components/util/Icon";
|
||||
|
||||
export function UserProfileDropdown({ user }: { user: PublicUserDocument }) {
|
||||
const { setUser } = useContext(UserContext);
|
||||
@@ -70,7 +71,7 @@ export function UserProfileDropdown({ user }: { user: PublicUserDocument }) {
|
||||
<div className="navi-text">
|
||||
<div className="font-weight-bold cursor-pointer">My Settings</div>
|
||||
<div className="text-muted">
|
||||
Manage your profile, API Tokens, and more!
|
||||
Manage your profile, service integrations, and more!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import QuickTooltip from "components/layout/misc/QuickTooltip";
|
||||
import Icon from "components/util/Icon";
|
||||
import React from "react";
|
||||
import { ChartDocument, Game, COLOUR_SET } from "tachi-common";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { FormatBMSTables } from "util/misc";
|
||||
|
||||
export default function BMSDifficultyCell({
|
||||
chart,
|
||||
}: {
|
||||
chart: ChartDocument<"bms:7K" | "bms:14K">;
|
||||
}) {
|
||||
const hasLevel = chart.data.tableFolders.length > 0;
|
||||
|
||||
const levelText = hasLevel ? FormatBMSTables(chart.data.tableFolders) : "No Level";
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(hasLevel ? COLOUR_SET.red : COLOUR_SET.gray, 0.2),
|
||||
}}
|
||||
>
|
||||
<span>{levelText}</span>
|
||||
{!chart.isPrimary && (
|
||||
<QuickTooltip text="This chart is an alternate, old chart.">
|
||||
<div>
|
||||
<Icon type="exclamation-triangle" />
|
||||
</div>
|
||||
</QuickTooltip>
|
||||
)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Tooltip } from "@material-ui/core";
|
||||
import QuickTooltip from "components/layout/misc/QuickTooltip";
|
||||
import Icon from "components/util/Icon";
|
||||
import React from "react";
|
||||
import {
|
||||
AnyChartDocument,
|
||||
ChartDocument,
|
||||
FormatDifficulty,
|
||||
FormatDifficultyShort,
|
||||
Game,
|
||||
@@ -11,7 +10,15 @@ import {
|
||||
} from "tachi-common";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
|
||||
export default function DifficultyCell({ game, chart }: { game: Game; chart: AnyChartDocument }) {
|
||||
export default function DifficultyCell({
|
||||
game,
|
||||
chart,
|
||||
alwaysShort,
|
||||
}: {
|
||||
game: Game;
|
||||
chart: ChartDocument;
|
||||
alwaysShort?: boolean;
|
||||
}) {
|
||||
const gptConfig = GetGamePTConfig(game, chart.playtype);
|
||||
|
||||
return (
|
||||
@@ -20,8 +27,12 @@ export default function DifficultyCell({ game, chart }: { game: Game; chart: Any
|
||||
backgroundColor: ChangeOpacity(gptConfig.difficultyColours[chart.difficulty]!, 0.2),
|
||||
}}
|
||||
>
|
||||
<span className="d-none d-lg-block">{FormatDifficulty(chart, game)}</span>
|
||||
<span className="d-lg-none">{FormatDifficultyShort(chart, game)}</span>
|
||||
{!alwaysShort && (
|
||||
<span className="d-none d-lg-block">{FormatDifficulty(chart, game)}</span>
|
||||
)}
|
||||
<span className={!alwaysShort ? "d-lg-none" : ""}>
|
||||
{FormatDifficultyShort(chart, game)}
|
||||
</span>
|
||||
{!chart.isPrimary && (
|
||||
<QuickTooltip text="This chart is an alternate, old chart.">
|
||||
<div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { IsScore } from "util/asserts";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { IsNotNullish, IsNullish } from "util/misc";
|
||||
|
||||
export default function IIDXLampCell({
|
||||
sc,
|
||||
@@ -24,6 +25,16 @@ export default function IIDXLampCell({
|
||||
}
|
||||
}
|
||||
|
||||
let bpText;
|
||||
|
||||
if (IsNotNullish(sc.scoreData.hitMeta.bp) && IsNotNullish(sc.scoreData.hitMeta.comboBreak)) {
|
||||
bpText = `[BP: ${sc.scoreData.hitMeta.bp}, CBRK: ${sc.scoreData.hitMeta.comboBreak}]`;
|
||||
} else if (IsNotNullish(sc.scoreData.hitMeta.bp)) {
|
||||
bpText = `[BP: ${sc.scoreData.hitMeta.bp}]`;
|
||||
} else if (IsNotNullish(sc.scoreData.hitMeta.comboBreak)) {
|
||||
bpText = `[CBRK: ${sc.scoreData.hitMeta.comboBreak}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
@@ -31,14 +42,14 @@ export default function IIDXLampCell({
|
||||
}}
|
||||
>
|
||||
<strong>{sc.scoreData.lamp}</strong>
|
||||
<br />
|
||||
<small>
|
||||
[BP: {sc.scoreData.hitMeta.bp ?? "No Data"}
|
||||
{sc.scoreData.hitMeta.comboBreak
|
||||
? `, CBRK: ${sc.scoreData.hitMeta.comboBreak}`
|
||||
: ""}
|
||||
]
|
||||
</small>
|
||||
|
||||
{bpText && (
|
||||
<>
|
||||
<br />
|
||||
<small>{bpText}</small>
|
||||
</>
|
||||
)}
|
||||
|
||||
{sc.scoreData.lamp === "FAILED" && (
|
||||
<>
|
||||
<br />
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { IsScore } from "util/asserts";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { IsNotNullish, IsNullish } from "util/misc";
|
||||
|
||||
export default function LampCell({ sc }: { sc: ScoreDocument | PBScoreDocument }) {
|
||||
const gptConfig = GetGamePTConfig(sc.game, sc.playtype);
|
||||
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(gptConfig.lampColours[sc.scoreData.lamp], 0.2),
|
||||
}}
|
||||
>
|
||||
<strong>{sc.scoreData.lamp}</strong>
|
||||
</td>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import React from "react";
|
||||
import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { GamePT } from "types/react";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { FormatMillions } from "util/misc";
|
||||
|
||||
export default function SDVXScoreCell({
|
||||
game,
|
||||
playtype,
|
||||
score,
|
||||
}: GamePT & {
|
||||
score: PBScoreDocument | ScoreDocument;
|
||||
}) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
return (
|
||||
<td
|
||||
style={{
|
||||
backgroundColor: ChangeOpacity(gptConfig.gradeColours[score.scoreData.grade], 0.2),
|
||||
}}
|
||||
>
|
||||
<strong>{score.scoreData.grade}</strong>
|
||||
<br />
|
||||
{FormatMillions(score.scoreData.score)}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
@@ -2,13 +2,19 @@ import React from "react";
|
||||
import { GetGamePTConfig, PBScoreDocument, ScoreDocument } from "tachi-common";
|
||||
import { GamePT } from "types/react";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import { FormatMillions } from "util/misc";
|
||||
|
||||
export default function ScoreCell({
|
||||
game,
|
||||
playtype,
|
||||
score,
|
||||
showScore = true,
|
||||
}: GamePT & { score: PBScoreDocument | ScoreDocument; showScore?: boolean }) {
|
||||
scoreRenderFn,
|
||||
}: GamePT & {
|
||||
score: PBScoreDocument | ScoreDocument;
|
||||
showScore?: boolean;
|
||||
scoreRenderFn?: (s: number) => string;
|
||||
}) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
return (
|
||||
@@ -23,7 +29,13 @@ export default function ScoreCell({
|
||||
{showScore && (
|
||||
<>
|
||||
<br />
|
||||
<small className="text-muted">[{score.scoreData.score}]</small>
|
||||
<small className="text-muted">
|
||||
[
|
||||
{scoreRenderFn
|
||||
? scoreRenderFn(score.scoreData.score)
|
||||
: score.scoreData.score}
|
||||
]
|
||||
</small>
|
||||
</>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { Playtype } from "types/tachi";
|
||||
import { FormatMillions } from "util/misc";
|
||||
import ScoreCell from "./ScoreCell";
|
||||
import { Game, ScoreDocument, PBScoreDocument } from "tachi-common";
|
||||
|
||||
export default function ScoreCellLarge(props: {
|
||||
game: Game;
|
||||
playtype: Playtype;
|
||||
score: ScoreDocument | PBScoreDocument;
|
||||
}) {
|
||||
return <ScoreCell {...props} scoreRenderFn={FormatMillions} />;
|
||||
}
|
||||
@@ -1,24 +1,29 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { AnyChartDocument, AnySongDocument, Game } from "tachi-common";
|
||||
import { GamePT } from "types/react";
|
||||
import { ChartDocument, SongDocument, Game } from "tachi-common";
|
||||
import { CreateChartLink } from "util/data";
|
||||
|
||||
export default function TitleCell({
|
||||
game,
|
||||
song,
|
||||
chart,
|
||||
noArtist,
|
||||
}: {
|
||||
song: AnySongDocument;
|
||||
chart: AnyChartDocument;
|
||||
song: SongDocument;
|
||||
chart: ChartDocument;
|
||||
game: Game;
|
||||
noArtist?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<td style={{ textAlign: "left", minWidth: "140px" }}>
|
||||
<Link className="song-title-link" to={CreateChartLink(chart, game)}>
|
||||
{song.title}
|
||||
<br />
|
||||
<small>{song.artist}</small>
|
||||
{!noArtist && (
|
||||
<>
|
||||
<br />
|
||||
<small>{song.artist}</small>
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
</td>
|
||||
);
|
||||
|
||||
@@ -7,13 +7,15 @@ import React, { useState } from "react";
|
||||
import { Modal, OverlayTrigger, Tooltip } from "react-bootstrap";
|
||||
import { GetValueGetter, ValueGetterOrHybrid } from "util/ztable/search";
|
||||
|
||||
function AdditionalTableProps({ value }: { value: string | number | [string, number] }) {
|
||||
function AdditionalTableProps({ value }: { value: string | number | [string, number] | null }) {
|
||||
let typeName;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
typeName = "Hybrid";
|
||||
} else if (typeof value === "string") {
|
||||
typeName = "Text";
|
||||
} else if (value === null) {
|
||||
typeName = "Cannot Infer :(";
|
||||
} else {
|
||||
typeName = "Number";
|
||||
}
|
||||
|
||||
@@ -8,15 +8,15 @@ export default function MiniTable({
|
||||
children,
|
||||
headers,
|
||||
colSpan = 1,
|
||||
}: { className?: string; headers: string[]; colSpan?: integer } & JustChildren) {
|
||||
}: { className?: string; headers: string[]; colSpan?: integer | integer[] } & JustChildren) {
|
||||
return (
|
||||
<table
|
||||
className={`table table-hover table-striped table-vertical-center text-center ${className}`}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
{headers.map(e => (
|
||||
<th colSpan={colSpan} key={nanoid()}>
|
||||
{headers.map((e, i) => (
|
||||
<th colSpan={Array.isArray(colSpan) ? colSpan[i] : colSpan} key={nanoid()}>
|
||||
{e}
|
||||
</th>
|
||||
))}
|
||||
|
||||
@@ -12,8 +12,8 @@ export default function PageSelector({
|
||||
currentPage: integer;
|
||||
maxPage: integer;
|
||||
}) {
|
||||
const elipseStart = currentPage > 4;
|
||||
const elipseEnd = maxPage - currentPage > 3;
|
||||
const elipseStart = currentPage > 4 && maxPage > 5;
|
||||
const elipseEnd = maxPage - currentPage > 3 && maxPage > 5;
|
||||
|
||||
function PageButton({ page }: { page: integer }) {
|
||||
return (
|
||||
|
||||
@@ -140,11 +140,7 @@ export default function TachiTable<D>({
|
||||
<table className="table table-striped table-hover table-vertical-center text-center table-responsive-md">
|
||||
<thead>{headersRow}</thead>
|
||||
<tbody>
|
||||
<NoDataWrapper>
|
||||
{window.map((e, i) => (
|
||||
<React.Fragment key={i}>{rowFunction(e)}</React.Fragment>
|
||||
))}
|
||||
</NoDataWrapper>
|
||||
<NoDataWrapper>{window.map(e => rowFunction(e))}</NoDataWrapper>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import Icon from "components/util/Icon";
|
||||
import SelectButton from "components/util/SelectButton";
|
||||
import { SessionDataset } from "../sessions/GenericSessionTable";
|
||||
import DropdownStructure from "./components/DropdownStructure";
|
||||
import { useQuery } from "react-query";
|
||||
import { APIFetchV1 } from "util/api";
|
||||
import { SpecificSessionReturns } from "types/api-returns";
|
||||
import Loading from "components/util/Loading";
|
||||
import DebugContent from "components/util/DebugContent";
|
||||
import { CreateChartMap, CreateScoreMap, CreateSongMap } from "util/data";
|
||||
import {
|
||||
Lamps,
|
||||
Grades,
|
||||
ScoreDocument,
|
||||
IDStrings,
|
||||
GetGamePTConfig,
|
||||
GamePTConfig,
|
||||
integer,
|
||||
SongDocument,
|
||||
ChartDocument,
|
||||
Game,
|
||||
FolderDocument,
|
||||
TableDocument,
|
||||
SessionScoreInfo,
|
||||
} from "tachi-common";
|
||||
import { PartialArrayRecordAssign } from "util/misc";
|
||||
import MiniTable from "../components/MiniTable";
|
||||
import { ChangeOpacity } from "util/color-opacity";
|
||||
import TitleCell from "../cells/TitleCell";
|
||||
import DifficultyCell from "../cells/DifficultyCell";
|
||||
import Divider from "components/util/Divider";
|
||||
import ScoreCell from "../cells/ScoreCell";
|
||||
import deepmerge from "deepmerge";
|
||||
import IIDXLampCell from "../cells/IIDXLampCell";
|
||||
|
||||
export default function GenericSessionDropdown({ data }: { data: SessionDataset[0] }) {
|
||||
const { isLoading, error, data: sessionData } = useQuery(
|
||||
`/sessions/${data.sessionID}`,
|
||||
async () => {
|
||||
const res = await APIFetchV1<SpecificSessionReturns>(`/sessions/${data.sessionID}`);
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(res.description);
|
||||
}
|
||||
|
||||
return res.body;
|
||||
}
|
||||
);
|
||||
|
||||
const [view, setView] = useState<"raises" | "highlights" | "graphs" | "debug">("graphs");
|
||||
|
||||
if (error) {
|
||||
return <>{(error as Error).message}</>;
|
||||
}
|
||||
|
||||
if (isLoading || !sessionData) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
let content;
|
||||
|
||||
if (view === "raises") {
|
||||
content = <SessionRaiseBreakdown sessionData={sessionData} />;
|
||||
} else if (view === "debug") {
|
||||
content = <DebugContent data={sessionData} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownStructure
|
||||
buttons={
|
||||
<>
|
||||
<SelectButton setValue={setView} value={view} id="graphs">
|
||||
<Icon type="chart-area" />
|
||||
Graphs
|
||||
</SelectButton>
|
||||
<SelectButton setValue={setView} value={view} id="raises">
|
||||
<Icon type="receipt" />
|
||||
Raises
|
||||
</SelectButton>
|
||||
<SelectButton setValue={setView} value={view} id="highlights">
|
||||
<Icon type="star" />
|
||||
Highlights
|
||||
</SelectButton>
|
||||
|
||||
<SelectButton setValue={setView} value={view} id="debug">
|
||||
<Icon type="bug" />
|
||||
Debug Info
|
||||
</SelectButton>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{content}
|
||||
</DropdownStructure>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionRaiseBreakdown({ sessionData }: { sessionData: SpecificSessionReturns }) {
|
||||
const game = sessionData.session.game;
|
||||
const playtype = sessionData.session.playtype;
|
||||
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const [chartIDs, setChartIDs] = useState<string[] | null>([]);
|
||||
const [tableID, setTableID] = useState<string>("levels");
|
||||
const [tableFolders, setTableFolders] = useState<FolderDocument[]>([]);
|
||||
const [folder, setFolder] = useState<FolderDocument | null>(null);
|
||||
const [filter, setFilter] = useState<"folder" | "all" | "highlighted">("all");
|
||||
|
||||
useEffect(() => {
|
||||
APIFetchV1(`/games/${game}/${playtype}/tables/${tableID}`).then(r => {
|
||||
if (!r.success) {
|
||||
throw new Error(r.description);
|
||||
}
|
||||
|
||||
// @ts-expect-error todo
|
||||
setTableFolders(r.body.folders);
|
||||
// @ts-expect-error todo
|
||||
setFolder(r.body.folders[r.body.folders.length - 1]);
|
||||
});
|
||||
}, [tableID]);
|
||||
|
||||
useEffect(() => {
|
||||
if (filter !== "folder" || !folder) {
|
||||
setChartIDs(null);
|
||||
return;
|
||||
}
|
||||
|
||||
APIFetchV1(`/games/${game}/${playtype}/folders/${folder.folderID}`).then(r => {
|
||||
if (!r.success) {
|
||||
throw new Error(r.description);
|
||||
}
|
||||
|
||||
// @ts-expect-error todo
|
||||
setChartIDs(r.body.charts.map(e => e.chartID));
|
||||
});
|
||||
}, [folder, tableID, filter]);
|
||||
|
||||
const { isLoading, error, data } = useQuery(`/games/${game}/${playtype}/tables`, async () => {
|
||||
const res = await APIFetchV1<TableDocument[]>(`/games/${game}/${playtype}/tables`);
|
||||
|
||||
if (!res.success) {
|
||||
throw new Error(res.description);
|
||||
}
|
||||
|
||||
return res.body;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return <>{(error as Error).message}</>;
|
||||
}
|
||||
|
||||
if (isLoading || !data) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-12">
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<div className="btn-group">
|
||||
<SelectButton value={filter} setValue={setFilter} id="folder">
|
||||
<Icon type="folder-open" />
|
||||
Folder Filter
|
||||
</SelectButton>
|
||||
<SelectButton value={filter} setValue={setFilter} id="all">
|
||||
<Icon type="plus" />
|
||||
No Filter
|
||||
</SelectButton>
|
||||
<SelectButton value={filter} setValue={setFilter} id="highlighted">
|
||||
<Icon type="star" />
|
||||
Highlights Only
|
||||
</SelectButton>
|
||||
</div>
|
||||
</div>
|
||||
{filter === "folder" && (
|
||||
<>
|
||||
<div className="col-12">
|
||||
<Divider className="mt-4 mb-4" />
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<select
|
||||
className="form-control"
|
||||
value={tableID}
|
||||
onChange={e => setTableID(e.target.value)}
|
||||
>
|
||||
{data.map(e => (
|
||||
<option key={e.tableID} value={e.tableID}>
|
||||
{e.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<select
|
||||
className="form-control"
|
||||
value={folder?.folderID}
|
||||
onChange={e =>
|
||||
setFolder(
|
||||
tableFolders.find(f => f.folderID === e.target.value)!
|
||||
)
|
||||
}
|
||||
>
|
||||
{tableFolders.map(e => (
|
||||
<option key={e.folderID} value={e.folderID}>
|
||||
{e.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Divider className="mt-4 mb-4" />
|
||||
</div>
|
||||
<SessionScoreStatBreakdown {...{ sessionData, chartIDs, filter }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SessionScoreStatBreakdown({
|
||||
sessionData,
|
||||
chartIDs,
|
||||
filter,
|
||||
}: {
|
||||
sessionData: SpecificSessionReturns;
|
||||
chartIDs: string[] | null;
|
||||
filter: "folder" | "all" | "highlighted";
|
||||
}) {
|
||||
const songMap = CreateSongMap(sessionData.songs);
|
||||
const chartMap = CreateChartMap(sessionData.charts);
|
||||
const scoreMap = CreateScoreMap(sessionData.scores);
|
||||
|
||||
const [newLamps, newGrades] = useMemo(() => {
|
||||
const newLamps: Partial<Record<
|
||||
Lamps[IDStrings],
|
||||
{ score: ScoreDocument; scoreInfo: SessionScoreInfo }[]
|
||||
>> = {};
|
||||
const newGrades: Partial<Record<
|
||||
Grades[IDStrings],
|
||||
{ score: ScoreDocument; scoreInfo: SessionScoreInfo }[]
|
||||
>> = {};
|
||||
|
||||
for (const scoreInfo of sessionData.session.scoreInfo) {
|
||||
const score = scoreMap.get(scoreInfo.scoreID);
|
||||
|
||||
if (!score) {
|
||||
console.error(
|
||||
`Session score info contains scoreID ${scoreInfo.scoreID}, but no score exists?`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filter === "folder" && chartIDs && !chartIDs.includes(score.chartID)) {
|
||||
continue;
|
||||
} else if (filter === "highlighted" && !score.highlight) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (scoreInfo.isNewScore) {
|
||||
PartialArrayRecordAssign(newLamps, score.scoreData.lamp, { score, scoreInfo });
|
||||
PartialArrayRecordAssign(newGrades, score.scoreData.grade, { score, scoreInfo });
|
||||
} else {
|
||||
if (scoreInfo.lampDelta > 0) {
|
||||
PartialArrayRecordAssign(newLamps, score.scoreData.lamp, { score, scoreInfo });
|
||||
}
|
||||
|
||||
if (scoreInfo.gradeDelta > 0) {
|
||||
PartialArrayRecordAssign(newGrades, score.scoreData.grade, {
|
||||
score,
|
||||
scoreInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return [newLamps, newGrades];
|
||||
}, [chartIDs, filter]);
|
||||
|
||||
const gptConfig = GetGamePTConfig(sessionData.session.game, sessionData.session.playtype);
|
||||
|
||||
const [view, setView] = useState<"lamps" | "both" | "grades">("both");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-12 d-none d-lg-block">
|
||||
<div className="btn-group">
|
||||
<SelectButton value={view} setValue={setView} id="lamps">
|
||||
<Icon type="lightbulb" />
|
||||
New Lamps
|
||||
</SelectButton>
|
||||
|
||||
<SelectButton value={view} setValue={setView} id="both">
|
||||
<Icon type="bolt" />
|
||||
Both
|
||||
</SelectButton>
|
||||
<SelectButton value={view} setValue={setView} id="grades">
|
||||
<Icon type="sort-alpha-up" />
|
||||
New Grades
|
||||
</SelectButton>
|
||||
</div>
|
||||
<Divider className="mt-4 mb-4" />
|
||||
</div>
|
||||
{view === "both" ? (
|
||||
<>
|
||||
<div className="col-12 col-lg-6">
|
||||
<MiniTable
|
||||
headers={["Lamp", "New Lamps"]}
|
||||
colSpan={[1, 2]}
|
||||
className="table-sm"
|
||||
>
|
||||
<ElementStatTable
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newLamps}
|
||||
game={sessionData.session.game}
|
||||
type="lamp"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6">
|
||||
<MiniTable
|
||||
headers={["Grade", "New Grades"]}
|
||||
colSpan={[1, 2]}
|
||||
className="table-sm"
|
||||
>
|
||||
<ElementStatTable
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newGrades}
|
||||
game={sessionData.session.game}
|
||||
type="grade"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
</>
|
||||
) : view === "grades" ? (
|
||||
<div className="col-12">
|
||||
<MiniTable headers={["Grade", "New Grades"]} colSpan={[1, 100]}>
|
||||
<ElementStatTable
|
||||
fullSize
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newGrades}
|
||||
game={sessionData.session.game}
|
||||
type="grade"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
) : (
|
||||
<div className="col-12">
|
||||
<MiniTable headers={["Grade", "New Lamps"]} colSpan={[1, 100]}>
|
||||
<ElementStatTable
|
||||
fullSize
|
||||
chartMap={chartMap}
|
||||
songMap={songMap}
|
||||
counts={newLamps}
|
||||
game={sessionData.session.game}
|
||||
type="lamp"
|
||||
gptConfig={gptConfig}
|
||||
/>
|
||||
</MiniTable>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ElementStatTable({
|
||||
type,
|
||||
counts,
|
||||
gptConfig,
|
||||
songMap,
|
||||
chartMap,
|
||||
game,
|
||||
fullSize = false,
|
||||
}: {
|
||||
type: "lamp" | "grade";
|
||||
counts: Record<string, { score: ScoreDocument; scoreInfo: SessionScoreInfo }[]>;
|
||||
gptConfig: GamePTConfig;
|
||||
songMap: Map<integer, SongDocument<Game>>;
|
||||
chartMap: Map<string, ChartDocument<IDStrings>>;
|
||||
game: Game;
|
||||
fullSize?: boolean;
|
||||
}) {
|
||||
function BreakdownChartContents({
|
||||
score,
|
||||
scoreInfo,
|
||||
}: {
|
||||
score: ScoreDocument;
|
||||
scoreInfo: SessionScoreInfo;
|
||||
}) {
|
||||
const chart = chartMap.get(score.chartID)!;
|
||||
const song = songMap.get(score.songID)!;
|
||||
|
||||
if (fullSize) {
|
||||
let preScoreCell = <td>No Play</td>;
|
||||
|
||||
if (!scoreInfo.isNewScore) {
|
||||
const oldGradeIndex = score.scoreData.gradeIndex - scoreInfo.gradeDelta;
|
||||
const oldLampIndex = score.scoreData.lampIndex - scoreInfo.lampDelta;
|
||||
|
||||
const mockScore = deepmerge(score, {
|
||||
scoreData: {
|
||||
score: score.scoreData.score - scoreInfo.scoreDelta,
|
||||
percent: score.scoreData.percent - scoreInfo.percentDelta,
|
||||
grade: gptConfig.grades[oldGradeIndex],
|
||||
gradeIndex: oldGradeIndex,
|
||||
lamp: gptConfig.lamps[oldLampIndex],
|
||||
lampIndex: oldLampIndex,
|
||||
},
|
||||
}) as ScoreDocument;
|
||||
|
||||
if (type === "grade") {
|
||||
preScoreCell = (
|
||||
<ScoreCell score={mockScore} game={game} playtype={mockScore.playtype} />
|
||||
);
|
||||
} else {
|
||||
preScoreCell = (
|
||||
// @ts-expect-error temp
|
||||
<IIDXLampCell sc={mockScore} game={game} playtype={mockScore.playtype} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (score) {
|
||||
return (
|
||||
<>
|
||||
<TitleCell chart={chart} game={game} song={song} />
|
||||
<DifficultyCell chart={chart} game={game} />
|
||||
{preScoreCell}
|
||||
<td>⟶</td>
|
||||
{type === "grade" ? (
|
||||
<ScoreCell {...{ score, game, playtype: score.playtype }} />
|
||||
) : (
|
||||
// @ts-expect-error temp
|
||||
<IIDXLampCell sc={score} game={game} playtype={score.playtype} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleCell noArtist chart={chart} game={game} song={song} />
|
||||
<DifficultyCell alwaysShort chart={chart} game={game} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// relements.. haha
|
||||
const relevantElements =
|
||||
type === "lamp"
|
||||
? gptConfig.lamps.slice(gptConfig.lamps.indexOf(gptConfig.clearLamp) - 1).reverse()
|
||||
: gptConfig.grades.slice(gptConfig.grades.indexOf(gptConfig.clearGrade) - 1).reverse();
|
||||
|
||||
const tableContents = [];
|
||||
|
||||
const colours = type === "lamp" ? gptConfig.lampColours : gptConfig.gradeColours;
|
||||
|
||||
for (const element of relevantElements) {
|
||||
if (!counts[element] || !counts[element].length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstData = counts[element][0];
|
||||
|
||||
tableContents.push(
|
||||
<tr key={element}>
|
||||
<td
|
||||
style={{
|
||||
// @ts-expect-error this is a hack due to the funky type of colours and element.
|
||||
backgroundColor: ChangeOpacity(colours[element], 0.1),
|
||||
}}
|
||||
rowSpan={counts[element]!.length}
|
||||
>
|
||||
{element}
|
||||
</td>
|
||||
<BreakdownChartContents {...firstData} />
|
||||
</tr>
|
||||
);
|
||||
|
||||
for (const data of counts[element]!.slice(1)) {
|
||||
tableContents.push(
|
||||
<tr key={data.score.scoreID}>
|
||||
<BreakdownChartContents {...data} />
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (tableContents.length === 0) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={3}>Nothing...</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{tableContents}</>;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
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 } from "react";
|
||||
import React, { useMemo, useState, useEffect } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { ChartDocument, PublicUserDocument } from "tachi-common";
|
||||
import { UGPTChartPBComposition } from "types/api-returns";
|
||||
@@ -29,7 +30,9 @@ export default function IIDXPBDropdown({
|
||||
setHighlight: SetState<boolean>;
|
||||
};
|
||||
} & GamePT) {
|
||||
const [view, setView] = useState<"pb" | "scorePB" | "lampPB" | "bpPB" | "history">("pb");
|
||||
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`,
|
||||
@@ -78,11 +81,17 @@ export default function IIDXPBDropdown({
|
||||
<Icon type="history" />
|
||||
Play History
|
||||
</SelectButton>
|
||||
<SelectButton setValue={setView} value={view} id="debug">
|
||||
<Icon type="bug" />
|
||||
Debug Info
|
||||
</SelectButton>
|
||||
</>
|
||||
}
|
||||
>
|
||||
{view === "history" ? (
|
||||
<></>
|
||||
) : view === "debug" ? (
|
||||
<DebugContent data={data} />
|
||||
) : (
|
||||
<DocumentDisplay pbData={data} view={view} scoreState={scoreState} />
|
||||
)}
|
||||
@@ -119,6 +128,10 @@ function DocumentDisplay({
|
||||
|
||||
const [comment, setComment] = useState(IsScore(currentDoc) ? currentDoc.comment : null);
|
||||
|
||||
useEffect(() => {
|
||||
setComment(IsScore(currentDoc) ? currentDoc.comment : null);
|
||||
}, [currentDoc]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-9">
|
||||
|
||||
@@ -13,6 +13,7 @@ import { IIDXGraphsComponent, ModsTable } from "./components/IIDXScoreDropdownPa
|
||||
import JudgementTable from "./components/JudgementTable";
|
||||
import CommentContainer from "./components/CommentContainer";
|
||||
import ScoreEditButtons from "./components/ScoreEditButtons";
|
||||
import DebugContent from "components/util/DebugContent";
|
||||
|
||||
export default function IIDXScoreDropdown({
|
||||
thisScore,
|
||||
@@ -32,7 +33,7 @@ export default function IIDXScoreDropdown({
|
||||
setComment: SetState<string | null>;
|
||||
};
|
||||
} & GamePT) {
|
||||
const [view, setView] = useState<"vsPB" | "moreInfo" | "history">("moreInfo");
|
||||
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`,
|
||||
@@ -83,6 +84,8 @@ export default function IIDXScoreDropdown({
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
} else if (view === "debug") {
|
||||
content = <DebugContent data={data} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -101,6 +104,10 @@ export default function IIDXScoreDropdown({
|
||||
<Icon type="history" />
|
||||
Play History
|
||||
</SelectButton>
|
||||
<SelectButton setValue={setView} value={view} id="debug">
|
||||
<Icon type="bug" />
|
||||
Debug Info
|
||||
</SelectButton>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 IIDXLampChart from "components/charts/IIDXLampChart";
|
||||
import MiniTable from "components/tables/components/MiniTable";
|
||||
import { IsScore } from "util/asserts";
|
||||
|
||||
export function ScoreInfo({ score }: { score: ScoreDocument<"iidx:SP" | "iidx:DP"> }) {
|
||||
return (
|
||||
@@ -71,7 +72,7 @@ export function IIDXGraphsComponent({
|
||||
}: {
|
||||
score: ScoreDocument<"iidx:SP" | "iidx:DP"> | PBScoreDocument<"iidx:SP" | "iidx:DP">;
|
||||
}) {
|
||||
const [lamp, setLamp] = useState(LampToKey(score.scoreData.lamp));
|
||||
const [lamp, setLamp] = useState(LampToKey(score));
|
||||
|
||||
let gaugeStatus: "none" | "single" | "gsm" = "none";
|
||||
|
||||
@@ -85,7 +86,7 @@ export function IIDXGraphsComponent({
|
||||
if (gaugeStatus === "gsm") {
|
||||
return false;
|
||||
} else if (gaugeStatus === "single") {
|
||||
return r !== LampToKey(score.scoreData.lamp);
|
||||
return r !== LampToKey(score);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -163,7 +164,24 @@ function GraphComponent({ type, values }: { type: LampTypes; values: (number | n
|
||||
);
|
||||
}
|
||||
|
||||
function LampToKey(lamp: Lamps["iidx:SP"]): LampTypes {
|
||||
function LampToKey(
|
||||
score: ScoreDocument<"iidx:SP" | "iidx:DP"> | PBScoreDocument<"iidx:SP" | "iidx:DP">
|
||||
): LampTypes {
|
||||
const lamp = score.scoreData.lamp;
|
||||
|
||||
if (IsScore(score) && score.scoreMeta.gauge) {
|
||||
switch (score.scoreMeta.gauge) {
|
||||
case "EASY":
|
||||
case "NORMAL":
|
||||
case "HARD":
|
||||
return score.scoreMeta.gauge;
|
||||
case "ASSISTED EASY":
|
||||
return "EASY";
|
||||
case "EX HARD":
|
||||
return "EX_HARD";
|
||||
}
|
||||
}
|
||||
|
||||
if (lamp === "CLEAR") {
|
||||
return "NORMAL";
|
||||
} else if (lamp === "EASY CLEAR") {
|
||||
@@ -172,6 +190,12 @@ function LampToKey(lamp: Lamps["iidx:SP"]): LampTypes {
|
||||
return "HARD";
|
||||
} else if (lamp === "EX HARD CLEAR") {
|
||||
return "EX_HARD";
|
||||
} else if (lamp === "FULL COMBO") {
|
||||
// @hack - attempt to guess what gauge they used?
|
||||
if ((score.scoreData.hitMeta.gaugeHistory?.[0] ?? 0) > 22) {
|
||||
return "EX_HARD";
|
||||
}
|
||||
return "NORMAL";
|
||||
}
|
||||
|
||||
return "NORMAL";
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import React, { useState } from "react";
|
||||
import TitleCell from "../cells/TitleCell";
|
||||
import RankingCell from "../cells/RankingCell";
|
||||
import TimestampCell from "../cells/TimestampCell";
|
||||
import { NumericSOV, StrSOV } from "util/sorts";
|
||||
import { PBDataset } from "types/tables";
|
||||
import { PublicUserDocument, ChartDocument } from "tachi-common";
|
||||
import TachiTable, { Header } from "../components/TachiTable";
|
||||
import ScoreCell from "../cells/ScoreCell";
|
||||
import IndexCell from "../cells/IndexCell";
|
||||
import DeltaCell from "../cells/DeltaCell";
|
||||
import { HumanFriendlyStrToGradeIndex, HumanFriendlyStrToLampIndex } from "util/str-to-num";
|
||||
import DropdownRow from "../components/DropdownRow";
|
||||
import { FormatBMSTables, IsNullish } from "util/misc";
|
||||
import LampCell from "../cells/LampCell";
|
||||
import BMSDifficultyCell from "../cells/BMSDifficultyCell";
|
||||
import { BMS_TABLES } from "util/constants/bms-tables";
|
||||
import { ValueGetterOrHybrid } from "util/ztable/search";
|
||||
|
||||
function GetBMSTableVal(chart: ChartDocument<"bms:7K" | "bms:14K">, key: string) {
|
||||
for (const table of chart.data.tableFolders) {
|
||||
if (table.table === key) {
|
||||
return Number(table.level);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function BMSPBTable({
|
||||
dataset,
|
||||
indexCol = true,
|
||||
reqUser,
|
||||
playtype,
|
||||
}: {
|
||||
dataset: PBDataset<"bms:7K" | "bms:14K">;
|
||||
indexCol?: boolean;
|
||||
reqUser: PublicUserDocument;
|
||||
playtype: "7K" | "14K";
|
||||
}) {
|
||||
const headers: Header<PBDataset<"bms:7K" | "bms:14K">[0]>[] = [
|
||||
["Chart", "Ch.", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
["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)],
|
||||
["Ranking", "Rank", NumericSOV(x => x.rankingData.rank)],
|
||||
["Last Raised", "Last Raised", NumericSOV(x => x.timeAchieved ?? 0)],
|
||||
];
|
||||
|
||||
if (indexCol) {
|
||||
headers.unshift(["#", "#", NumericSOV(x => x.__related.index)]);
|
||||
}
|
||||
|
||||
let tableSearchFns: Record<
|
||||
string,
|
||||
ValueGetterOrHybrid<PBDataset<"bms:7K" | "bms:14K">[0]>
|
||||
> = {};
|
||||
if (playtype === "7K") {
|
||||
tableSearchFns = {
|
||||
insane: x => GetBMSTableVal(x.__related.chart, BMS_TABLES.insane),
|
||||
overjoy: x => GetBMSTableVal(x.__related.chart, BMS_TABLES.overjoy),
|
||||
insane2: x => GetBMSTableVal(x.__related.chart, BMS_TABLES.insane2),
|
||||
normal: x => GetBMSTableVal(x.__related.chart, BMS_TABLES.normal),
|
||||
normal2: x => GetBMSTableVal(x.__related.chart, BMS_TABLES.normal2),
|
||||
st: x => GetBMSTableVal(x.__related.chart, BMS_TABLES.stella),
|
||||
sl: x => GetBMSTableVal(x.__related.chart, BMS_TABLES.satellite),
|
||||
satellite: x => GetBMSTableVal(x.__related.chart, BMS_TABLES.satellite),
|
||||
stella: x => GetBMSTableVal(x.__related.chart, BMS_TABLES.stella),
|
||||
};
|
||||
} else {
|
||||
tableSearchFns = {
|
||||
insane: x => GetBMSTableVal(x.__related.chart, BMS_TABLES.dpInsane),
|
||||
normal: x => GetBMSTableVal(x.__related.chart, BMS_TABLES.dpNormal),
|
||||
sl: x => GetBMSTableVal(x.__related.chart, BMS_TABLES.satellite),
|
||||
satellite: x => GetBMSTableVal(x.__related.chart, BMS_TABLES.satellite),
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<TachiTable
|
||||
dataset={dataset}
|
||||
headers={headers}
|
||||
entryName="PBs"
|
||||
searchFunctions={{
|
||||
artist: x => x.__related.song.artist,
|
||||
title: x => x.__related.song.title,
|
||||
difficulty: x => FormatBMSTables(x.__related.chart.data.tableFolders),
|
||||
...tableSearchFns,
|
||||
score: x => x.scoreData.score,
|
||||
percent: x => x.scoreData.percent,
|
||||
ranking: x => x.rankingData.rank,
|
||||
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),
|
||||
},
|
||||
}}
|
||||
defaultSortMode={indexCol ? "#" : undefined}
|
||||
rowFunction={pb => (
|
||||
<Row
|
||||
pb={pb}
|
||||
key={`${pb.chartID}:${pb.userID}`}
|
||||
reqUser={reqUser}
|
||||
indexCol={indexCol}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({
|
||||
pb,
|
||||
indexCol,
|
||||
reqUser,
|
||||
}: {
|
||||
pb: PBDataset<"bms:7K" | "bms:14K">[0];
|
||||
indexCol: boolean;
|
||||
reqUser: PublicUserDocument;
|
||||
}) {
|
||||
const [highlight, setHighlight] = useState(pb.highlight);
|
||||
|
||||
const scoreState = { highlight, setHighlight };
|
||||
|
||||
return (
|
||||
<DropdownRow
|
||||
dropdown={
|
||||
<></>
|
||||
// <IIDXPBDropdown
|
||||
// chart={pb.__related.chart}
|
||||
// reqUser={reqUser}
|
||||
// game="bms"
|
||||
// playtype={pb.playtype}
|
||||
// scoreState={scoreState}
|
||||
// />
|
||||
}
|
||||
className={highlight ? "highlighted-row" : ""}
|
||||
>
|
||||
{indexCol && <IndexCell index={pb.__related.index} />}
|
||||
<BMSDifficultyCell chart={pb.__related.chart} />
|
||||
<TitleCell song={pb.__related.song} chart={pb.__related.chart} game="bms" />
|
||||
<ScoreCell score={pb} game="bms" playtype={pb.playtype} />
|
||||
<DeltaCell
|
||||
game="bms"
|
||||
playtype={pb.playtype}
|
||||
score={pb.scoreData.score}
|
||||
percent={pb.scoreData.percent}
|
||||
grade={pb.scoreData.grade}
|
||||
/>
|
||||
<LampCell sc={pb} />
|
||||
<td>
|
||||
{!IsNullish(pb.calculatedData.sieglinde)
|
||||
? pb.calculatedData.sieglinde!.toFixed(2)
|
||||
: "N/A"}
|
||||
</td>
|
||||
<RankingCell rankingData={pb.rankingData} />
|
||||
<TimestampCell time={pb.timeAchieved} />
|
||||
</DropdownRow>
|
||||
);
|
||||
}
|
||||
@@ -6,12 +6,7 @@ import TimestampCell from "../cells/TimestampCell";
|
||||
import { NumericSOV, StrSOV } from "util/sorts";
|
||||
import SelectableRating from "../components/SelectableRating";
|
||||
import { PBDataset } from "types/tables";
|
||||
import {
|
||||
GetGamePTConfig,
|
||||
PublicUserDocument,
|
||||
ScoreCalculatedDataLookup,
|
||||
PBScoreDocument,
|
||||
} from "tachi-common";
|
||||
import { GetGamePTConfig, PublicUserDocument, ScoreCalculatedDataLookup } from "tachi-common";
|
||||
import TachiTable, { Header, ZTableTHProps } from "../components/TachiTable";
|
||||
import DifficultyCell from "../cells/DifficultyCell";
|
||||
import ScoreCell from "../cells/ScoreCell";
|
||||
@@ -63,7 +58,7 @@ export default function IIDXPBTable({
|
||||
),
|
||||
],
|
||||
["Ranking", "Rank", NumericSOV(x => x.rankingData.rank)],
|
||||
["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)],
|
||||
["Last Raised", "Last Raised", NumericSOV(x => x.timeAchieved ?? 0)],
|
||||
];
|
||||
|
||||
if (indexCol) {
|
||||
@@ -94,7 +89,13 @@ export default function IIDXPBTable({
|
||||
}}
|
||||
defaultSortMode={indexCol ? "#" : undefined}
|
||||
rowFunction={pb => (
|
||||
<Row pb={pb} reqUser={reqUser} indexCol={indexCol} rating={rating} />
|
||||
<Row
|
||||
pb={pb}
|
||||
key={`${pb.chartID}:${pb.userID}`}
|
||||
reqUser={reqUser}
|
||||
indexCol={indexCol}
|
||||
rating={rating}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -127,7 +128,6 @@ function Row({
|
||||
/>
|
||||
}
|
||||
className={highlight ? "highlighted-row" : ""}
|
||||
key={pb.chartID}
|
||||
>
|
||||
{indexCol && <IndexCell index={pb.__related.index} />}
|
||||
<DifficultyCell chart={pb.__related.chart} game={"iidx"} />
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { GamePT } from "types/react";
|
||||
import { PBDataset } from "types/tables";
|
||||
import { PublicUserDocument } from "tachi-common";
|
||||
import IIDXPBTable from "./IIDXPBTable";
|
||||
import BMSPBTable from "./BMSPBTable";
|
||||
import SDVXPBTable from "./SDVXPBTable";
|
||||
|
||||
export default function PBTable({
|
||||
dataset,
|
||||
indexCol = true,
|
||||
reqUser,
|
||||
playtype,
|
||||
game,
|
||||
}: {
|
||||
dataset: PBDataset;
|
||||
indexCol?: 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;
|
||||
if (game === "iidx") {
|
||||
return <IIDXPBTable {...props} />;
|
||||
} else if (game === "bms") {
|
||||
return <BMSPBTable {...props} />;
|
||||
} else if (game === "sdvx") {
|
||||
return <SDVXPBTable {...props} />;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import React, { useState } from "react";
|
||||
import TitleCell from "../cells/TitleCell";
|
||||
import RankingCell from "../cells/RankingCell";
|
||||
import TimestampCell from "../cells/TimestampCell";
|
||||
import { NumericSOV, StrSOV } from "util/sorts";
|
||||
import { PBDataset } from "types/tables";
|
||||
import { PublicUserDocument } from "tachi-common";
|
||||
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 SDVXScoreCell from "../cells/SDVXScoreCell";
|
||||
|
||||
export default function SDVXPBTable({
|
||||
dataset,
|
||||
indexCol = true,
|
||||
reqUser,
|
||||
playtype,
|
||||
}: {
|
||||
dataset: PBDataset<"sdvx:Single">;
|
||||
indexCol?: boolean;
|
||||
reqUser: PublicUserDocument;
|
||||
playtype: "7K" | "14K";
|
||||
}) {
|
||||
const headers: Header<PBDataset<"sdvx:Single">[0]>[] = [
|
||||
["Chart", "Ch.", NumericSOV(x => x.__related.chart.levelNum)],
|
||||
["Song", "Song", StrSOV(x => x.__related.song.title)],
|
||||
["Score", "Score", 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)],
|
||||
["Last Raised", "Last Raised", NumericSOV(x => x.timeAchieved ?? 0)],
|
||||
];
|
||||
|
||||
if (indexCol) {
|
||||
headers.unshift(["#", "#", NumericSOV(x => x.__related.index)]);
|
||||
}
|
||||
|
||||
return (
|
||||
<TachiTable
|
||||
dataset={dataset}
|
||||
headers={headers}
|
||||
entryName="PBs"
|
||||
searchFunctions={{
|
||||
artist: x => x.__related.song.artist,
|
||||
title: x => x.__related.song.title,
|
||||
difficulty: x => x.__related.chart.difficulty,
|
||||
level: x => x.__related.chart.levelNum,
|
||||
score: x => x.scoreData.score,
|
||||
percent: x => x.scoreData.percent,
|
||||
ranking: x => x.rankingData.rank,
|
||||
lamp: {
|
||||
valueGetter: x => [x.scoreData.lamp, x.scoreData.lampIndex],
|
||||
strToNum: HumanFriendlyStrToLampIndex("sdvx", playtype),
|
||||
},
|
||||
grade: {
|
||||
valueGetter: x => [x.scoreData.grade, x.scoreData.gradeIndex],
|
||||
strToNum: HumanFriendlyStrToGradeIndex("sdvx", playtype),
|
||||
},
|
||||
}}
|
||||
defaultSortMode={indexCol ? "#" : undefined}
|
||||
rowFunction={pb => (
|
||||
<Row
|
||||
pb={pb}
|
||||
key={`${pb.chartID}:${pb.userID}`}
|
||||
reqUser={reqUser}
|
||||
indexCol={indexCol}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({
|
||||
pb,
|
||||
indexCol,
|
||||
reqUser,
|
||||
}: {
|
||||
pb: PBDataset<"sdvx:Single">[0];
|
||||
indexCol: boolean;
|
||||
reqUser: PublicUserDocument;
|
||||
}) {
|
||||
const [highlight, setHighlight] = useState(pb.highlight);
|
||||
|
||||
const scoreState = { highlight, setHighlight };
|
||||
|
||||
return (
|
||||
<DropdownRow
|
||||
dropdown={
|
||||
<></>
|
||||
// <IIDXPBDropdown
|
||||
// chart={pb.__related.chart}
|
||||
// reqUser={reqUser}
|
||||
// game="sdvx"
|
||||
// playtype={pb.playtype}
|
||||
// scoreState={scoreState}
|
||||
// />
|
||||
}
|
||||
className={highlight ? "highlighted-row" : ""}
|
||||
>
|
||||
{indexCol && <IndexCell index={pb.__related.index} />}
|
||||
<DifficultyCell game="sdvx" chart={pb.__related.chart} />
|
||||
<TitleCell song={pb.__related.song} chart={pb.__related.chart} game="sdvx" />
|
||||
<SDVXScoreCell score={pb} game="sdvx" playtype={pb.playtype} />
|
||||
<LampCell sc={pb} />
|
||||
<td>
|
||||
{!IsNullish(pb.calculatedData.VF6) ? pb.calculatedData.VF6!.toFixed(3) : "No Data."}
|
||||
</td>
|
||||
<RankingCell rankingData={pb.rankingData} />
|
||||
<TimestampCell time={pb.timeAchieved} />
|
||||
</DropdownRow>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
integer,
|
||||
ScoreCalculatedDataLookup,
|
||||
PublicUserDocument,
|
||||
Playtypes,
|
||||
} from "tachi-common";
|
||||
import TachiTable, { ZTableTHProps } from "../components/TachiTable";
|
||||
import DifficultyCell from "../cells/DifficultyCell";
|
||||
@@ -26,14 +27,16 @@ export default function IIDXScoreTable({
|
||||
reqUser,
|
||||
dataset,
|
||||
pageLen,
|
||||
playtype,
|
||||
}: {
|
||||
reqUser: PublicUserDocument;
|
||||
dataset: ScoreDataset<"iidx:SP">;
|
||||
dataset: ScoreDataset<"iidx:SP" | "iidx:DP">;
|
||||
pageLen?: integer;
|
||||
playtype: Playtypes["iidx"];
|
||||
}) {
|
||||
const gptConfig = GetGamePTConfig<"iidx:SP">("iidx", "SP");
|
||||
const gptConfig = GetGamePTConfig<"iidx:SP" | "iidx:DP">("iidx", playtype);
|
||||
|
||||
const [rating, setRating] = useState<ScoreCalculatedDataLookup["iidx:SP"]>(
|
||||
const [rating, setRating] = useState<ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"]>(
|
||||
gptConfig.defaultScoreRatingAlg
|
||||
);
|
||||
|
||||
@@ -52,10 +55,10 @@ export default function IIDXScoreTable({
|
||||
"Rating",
|
||||
NumericSOV(x => x.calculatedData[rating] ?? 0),
|
||||
(thProps: ZTableTHProps) => (
|
||||
<SelectableRating<"iidx:SP">
|
||||
<SelectableRating<"iidx:SP" | "iidx:DP">
|
||||
key={nanoid()}
|
||||
game="iidx"
|
||||
playtype="SP"
|
||||
playtype={playtype}
|
||||
rating={rating}
|
||||
setRating={setRating}
|
||||
{...thProps}
|
||||
@@ -81,7 +84,7 @@ export default function IIDXScoreTable({
|
||||
strToNum: HumanFriendlyStrToGradeIndex("iidx", "SP"),
|
||||
},
|
||||
}}
|
||||
rowFunction={sc => <Row sc={sc} reqUser={reqUser} rating={rating} />}
|
||||
rowFunction={sc => <Row key={sc.scoreID} sc={sc} reqUser={reqUser} rating={rating} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -93,7 +96,7 @@ function Row({
|
||||
}: {
|
||||
sc: ScoreDataset<"iidx:SP" | "iidx:DP">[0];
|
||||
reqUser: PublicUserDocument;
|
||||
rating: ScoreCalculatedDataLookup["iidx:SP"];
|
||||
rating: ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"];
|
||||
}) {
|
||||
const [highlight, setHighlight] = useState(sc.highlight);
|
||||
const [comment, setComment] = useState(sc.comment);
|
||||
@@ -126,7 +129,7 @@ function Row({
|
||||
/>
|
||||
<IIDXLampCell sc={sc} />
|
||||
<td>
|
||||
{IsNullish(sc.calculatedData[rating])
|
||||
{!IsNullish(sc.calculatedData[rating])
|
||||
? sc.calculatedData[rating]!.toFixed(2)
|
||||
: "No Data."}
|
||||
</td>
|
||||
|
||||
@@ -4,16 +4,21 @@ import {
|
||||
GetGamePTConfig,
|
||||
IDStrings,
|
||||
integer,
|
||||
PublicUserDocument,
|
||||
ScoreCalculatedDataLookup,
|
||||
SessionCalculatedDataLookup,
|
||||
SessionDocument,
|
||||
SessionScoreInfo,
|
||||
} from "tachi-common";
|
||||
import { ScoreDataset } from "types/tables";
|
||||
import { Playtype } from "types/tachi";
|
||||
import { GetPBs } from "util/data";
|
||||
import { NumericSOV, StrSOV } from "util/sorts";
|
||||
import { FormatDuration, FormatTime, MillisToSince } from "util/time";
|
||||
import IndexCell from "../cells/IndexCell";
|
||||
import DropdownRow from "../components/DropdownRow";
|
||||
import TachiTable, { Header } from "../components/TachiTable";
|
||||
import GenericSessionDropdown from "../dropdowns/GenericSessionDropdown";
|
||||
import IIDXScoreDropdown from "../dropdowns/IIDXScoreDropdown";
|
||||
|
||||
export type SessionDataset = (SessionDocument & { __related: { index: integer } })[];
|
||||
|
||||
@@ -56,35 +61,57 @@ export default function GenericSessionTable({
|
||||
scores: x => x.scoreInfo.length,
|
||||
pbs: x => GetPBs(x.scoreInfo).length,
|
||||
pbRate: x => GetPBs(x.scoreInfo).length / x.scoreInfo.length,
|
||||
duration: x => x.timeEnded - x.timeStarted,
|
||||
duration: x => (x.timeEnded - x.timeStarted) / (1000 * 60),
|
||||
timestamp: x => x.timeStarted,
|
||||
}}
|
||||
rowFunction={s => (
|
||||
<tr key={s.sessionID}>
|
||||
{indexCol && <IndexCell index={s.__related.index} />}
|
||||
<td style={{ minWidth: "140px" }}>
|
||||
{s.name}
|
||||
<br />
|
||||
<small className="text-muted">{s.desc}</small>
|
||||
</td>
|
||||
<td>
|
||||
{s.scoreInfo.length}
|
||||
<br />
|
||||
<small className="text-muted">PBs: {GetPBs(s.scoreInfo).length}</small>
|
||||
</td>
|
||||
<td>
|
||||
{s.calculatedData[rating]
|
||||
? s.calculatedData[rating]?.toFixed(2)
|
||||
: "No Data."}
|
||||
</td>
|
||||
<td>{FormatDuration(s.timeEnded - s.timeStarted)}</td>
|
||||
<td>
|
||||
{MillisToSince(s.timeStarted)}
|
||||
<br />
|
||||
<small className="text-muted">{FormatTime(s.timeStarted)}</small>
|
||||
</td>
|
||||
</tr>
|
||||
<Row data={s} key={s.sessionID} rating={rating} indexCol={indexCol} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({
|
||||
data,
|
||||
rating,
|
||||
indexCol = false,
|
||||
}: // reqUser,
|
||||
{
|
||||
data: SessionDataset[0];
|
||||
// reqUser: PublicUserDocument;
|
||||
rating: SessionCalculatedDataLookup[IDStrings];
|
||||
indexCol?: boolean;
|
||||
}) {
|
||||
const [highlight, setHighlight] = useState(data.highlight);
|
||||
const [desc, setDesc] = useState(data.desc);
|
||||
|
||||
const sessionState = { highlight, desc, setHighlight, setDesc };
|
||||
|
||||
return (
|
||||
<DropdownRow
|
||||
className={highlight ? "highlighted-row" : ""}
|
||||
dropdown={<GenericSessionDropdown data={data} />}
|
||||
>
|
||||
{indexCol && <IndexCell index={data.__related.index} />}
|
||||
<td style={{ minWidth: "140px" }}>
|
||||
{data.name}
|
||||
<br />
|
||||
<small className="text-muted">{desc}</small>
|
||||
</td>
|
||||
<td>
|
||||
{data.scoreInfo.length}
|
||||
<br />
|
||||
<small className="text-muted">PBs: {GetPBs(data.scoreInfo).length}</small>
|
||||
</td>
|
||||
<td>
|
||||
{data.calculatedData[rating] ? data.calculatedData[rating]?.toFixed(2) : "No Data."}
|
||||
</td>
|
||||
<td>{FormatDuration(data.timeEnded - data.timeStarted)}</td>
|
||||
<td>
|
||||
{MillisToSince(data.timeStarted)}
|
||||
<br />
|
||||
<small className="text-muted">{FormatTime(data.timeStarted)}</small>
|
||||
</td>
|
||||
</DropdownRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React from "react";
|
||||
import { UserBadges } from "tachi-common";
|
||||
import { Badge } from "react-bootstrap";
|
||||
|
||||
export default function ProfileBadges({ badges }: { badges: UserBadges[] }) {
|
||||
return (
|
||||
<>
|
||||
{badges.map((e, i) => (
|
||||
<ProfileBadge key={i} variant={e} />
|
||||
<span key={i} className="mt-1">
|
||||
<ProfileBadge variant={e} />
|
||||
</span>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
@@ -13,11 +16,17 @@ export default function ProfileBadges({ badges }: { badges: UserBadges[] }) {
|
||||
|
||||
export function ProfileBadge({ variant }: { variant: UserBadges }) {
|
||||
if (variant === "alpha") {
|
||||
return <div className="badge badge-success mt-1">Alpha Tester</div>;
|
||||
return <Badge variant="warning">Alpha Tester</Badge>;
|
||||
} else if (variant === "beta") {
|
||||
return <div className="badge badge-info mt-1">Beta Tester</div>;
|
||||
return <Badge variant="info">Beta Tester</Badge>;
|
||||
} else if (variant === "devTeam") {
|
||||
return <div className="badge badge-primary mt-1">Dev Team</div>;
|
||||
return <Badge variant="primary">Dev Team</Badge>;
|
||||
} else if (variant === "contributor") {
|
||||
// discord contributor colour
|
||||
return <Badge style={{ backgroundColor: "#1abc9c" }}>Contributor</Badge>;
|
||||
} else if (variant === "significant-contributor") {
|
||||
// discord sig. contributor colour
|
||||
return <Badge style={{ backgroundColor: "#e62e22" }}>Significant Contributor</Badge>;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import MiniTable from "components/tables/components/MiniTable";
|
||||
import Divider from "components/util/Divider";
|
||||
import React from "react";
|
||||
import { Game, GetGameConfig, GetGamePTConfig, IDStrings, PublicUserDocument } from "tachi-common";
|
||||
import {
|
||||
Game,
|
||||
ScoreCalculatedDataLookup,
|
||||
GetGamePTConfig,
|
||||
IDStrings,
|
||||
PublicUserDocument,
|
||||
FormatGame,
|
||||
} from "tachi-common";
|
||||
import { UGPTStatsReturn } from "types/api-returns";
|
||||
import { Playtype } from "types/tachi";
|
||||
import { MillisToSince } from "util/time";
|
||||
@@ -9,7 +16,7 @@ import ProfilePicture from "./ProfilePicture";
|
||||
import Navbar from "components/nav/Navbar";
|
||||
import NavItem from "components/nav/NavItem";
|
||||
import ClassBadge from "components/game/ClassBadge";
|
||||
import { UppercaseFirst } from "util/misc";
|
||||
import { FormatGPTRating, UppercaseFirst } from "util/misc";
|
||||
import ProfileBadges from "./ProfileBadges";
|
||||
import { GameClassSets } from "tachi-common/js/game-classes";
|
||||
|
||||
@@ -24,7 +31,6 @@ export default function UGPTHeader({
|
||||
playtype: Playtype;
|
||||
stats: UGPTStatsReturn;
|
||||
}) {
|
||||
const gameConfig = GetGameConfig(game);
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
return (
|
||||
@@ -33,7 +39,7 @@ export default function UGPTHeader({
|
||||
<div className="card card-custom">
|
||||
<div className="card-header">
|
||||
<h4>
|
||||
{reqUser.username}'s {gameConfig.name} {playtype} Profile
|
||||
{reqUser.username}'s {FormatGame(game, playtype)} Profile
|
||||
</h4>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
@@ -66,7 +72,8 @@ export default function UGPTHeader({
|
||||
<tr>
|
||||
<td>Last Played</td>
|
||||
<td>
|
||||
{stats.mostRecentScore.timeAchieved === null
|
||||
{stats.mostRecentScore === null ||
|
||||
stats.mostRecentScore.timeAchieved === null
|
||||
? "Unknown."
|
||||
: MillisToSince(stats.mostRecentScore.timeAchieved)}
|
||||
</td>
|
||||
@@ -74,7 +81,8 @@ export default function UGPTHeader({
|
||||
<tr>
|
||||
<td>First Play</td>
|
||||
<td>
|
||||
{stats.firstScore.timeAchieved === null
|
||||
{stats.firstScore === null ||
|
||||
stats.firstScore.timeAchieved === null
|
||||
? "Unknown."
|
||||
: MillisToSince(stats.firstScore.timeAchieved)}
|
||||
</td>
|
||||
@@ -112,7 +120,14 @@ export default function UGPTHeader({
|
||||
{Object.entries(stats.gameStats.ratings).map(([k, v]) => (
|
||||
<tr key={k}>
|
||||
<td>{UppercaseFirst(k)}</td>
|
||||
<td>{v.toFixed(2)}</td>
|
||||
<td>
|
||||
{FormatGPTRating(
|
||||
game,
|
||||
playtype,
|
||||
k as ScoreCalculatedDataLookup[IDStrings],
|
||||
v
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
export default function DebugContent({ data }: { data: unknown }) {
|
||||
return (
|
||||
<textarea
|
||||
readOnly
|
||||
className="w-100 text-monospace"
|
||||
style={{ height: "400px" }}
|
||||
value={JSON.stringify(data, null, 4)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export function useZTable<D>(originalDataset: D[], providedOptions?: Partial<ZTa
|
||||
const displayStr = useMemo(() => {
|
||||
if (dataset.length === 0) {
|
||||
if (search !== "") {
|
||||
return `Displaying no ${entryName}. Your search might be too narrow.`;
|
||||
return `Displaying no ${entryName}. Your filter might be too narrow.`;
|
||||
}
|
||||
|
||||
return `Displaying no ${entryName}.`;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {
|
||||
AnyChartDocument,
|
||||
AnySongDocument,
|
||||
FolderDocument,
|
||||
IDStrings,
|
||||
integer,
|
||||
@@ -49,8 +47,8 @@ export type UGPTPreferenceStatsReturn =
|
||||
stat: ShowcaseStatChart;
|
||||
value: { value: number };
|
||||
related: {
|
||||
song: AnySongDocument;
|
||||
chart: AnyChartDocument;
|
||||
song: SongDocument;
|
||||
chart: ChartDocument;
|
||||
};
|
||||
}
|
||||
| {
|
||||
@@ -74,3 +72,11 @@ export interface UGPTChartPBComposition<I extends IDStrings = IDStrings> {
|
||||
chart: ChartDocument<I>;
|
||||
pb: PBScoreDocument<I>;
|
||||
}
|
||||
|
||||
export interface SpecificSessionReturns<I extends IDStrings = IDStrings> {
|
||||
session: SessionDocument<I>;
|
||||
scores: ScoreDocument<I>[];
|
||||
songs: SongDocument<IDStringToGame[I]>[];
|
||||
charts: ChartDocument<I>[];
|
||||
user: PublicUserDocument;
|
||||
}
|
||||
|
||||
+10
-3
@@ -1,5 +1,7 @@
|
||||
import toast from "react-hot-toast";
|
||||
import { SuccessfulAPIResponse, UnsuccessfulAPIResponse } from "tachi-common";
|
||||
import { HumaniseError } from "./humanise-error";
|
||||
import { SendErrorToast, SendSuccessToast } from "./toaster";
|
||||
|
||||
const BASE_OPTIONS = {
|
||||
credentials: "include",
|
||||
@@ -52,11 +54,16 @@ export async function APIFetchV1<T = unknown>(
|
||||
}
|
||||
|
||||
if (!rj.success && displayFailure) {
|
||||
toast.error(rj.description, { duration: 10_000 });
|
||||
// probably a prudence error...
|
||||
if (rj.description.includes("[K:")) {
|
||||
SendErrorToast(HumaniseError(rj.description));
|
||||
} else {
|
||||
SendErrorToast(rj.description);
|
||||
}
|
||||
}
|
||||
|
||||
if (displaySuccess) {
|
||||
toast.success(rj.description);
|
||||
if (displaySuccess && rj.success) {
|
||||
SendSuccessToast(rj.description);
|
||||
// toast success
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export const BMS_TABLES = {
|
||||
insane: "🟊",
|
||||
overjoy: "🟊🟊",
|
||||
normal: "☆",
|
||||
normal2: "▽",
|
||||
insane2: "▼",
|
||||
stella: "st",
|
||||
satellite: "sl",
|
||||
dpNormal: "δ",
|
||||
dpInsane: "★",
|
||||
};
|
||||
+12
-2
@@ -1,11 +1,11 @@
|
||||
import {
|
||||
AnyChartDocument,
|
||||
ChartDocument,
|
||||
Game,
|
||||
IDStrings,
|
||||
integer,
|
||||
SessionScoreInfo,
|
||||
SongDocument,
|
||||
ScoreDocument,
|
||||
} from "tachi-common";
|
||||
|
||||
export function GetPBs(scoreInfo: SessionScoreInfo[]) {
|
||||
@@ -32,7 +32,17 @@ export function CreateChartMap<I extends IDStrings = IDStrings>(charts: ChartDoc
|
||||
return chartMap;
|
||||
}
|
||||
|
||||
export function CreateChartLink(chart: AnyChartDocument, game: Game) {
|
||||
export function CreateScoreMap<I extends IDStrings = IDStrings>(scores: ScoreDocument<I>[]) {
|
||||
const scoreMap = new Map<string, ScoreDocument<I>>();
|
||||
|
||||
for (const score of scores) {
|
||||
scoreMap.set(score.scoreID, score);
|
||||
}
|
||||
|
||||
return scoreMap;
|
||||
}
|
||||
|
||||
export function CreateChartLink(chart: ChartDocument, game: Game) {
|
||||
if (chart.isPrimary) {
|
||||
return `/dashboard/games/${game}/${chart.playtype}/songs/${
|
||||
chart.songID
|
||||
|
||||
@@ -9,7 +9,7 @@ export function AbsoluteGradeDelta<I extends IDStrings = IDStrings>(
|
||||
) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
const maxScore = Math.ceil(score * (100 / percent));
|
||||
const maxScore = Math.floor(score * (100 / percent));
|
||||
|
||||
const gradeIndex =
|
||||
typeof gradeOrIndex === "number" ? gradeOrIndex : gptConfig.grades.indexOf(gradeOrIndex);
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Playtype } from "types/tachi";
|
||||
import { GetGamePTConfig, Game, ScoreCalculatedDataLookup, IDStrings } from "tachi-common";
|
||||
|
||||
export function RFA<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
export const NO_OP = () => void 0;
|
||||
|
||||
export const PREVENT_DEFAULT: React.FormEventHandler<HTMLFormElement> = e => e.preventDefault();
|
||||
|
||||
export function UppercaseFirst(str: string) {
|
||||
return str[0].toUpperCase() + str.substring(1);
|
||||
}
|
||||
@@ -11,3 +16,61 @@ export function UppercaseFirst(str: string) {
|
||||
export function IsNullish(value: unknown) {
|
||||
return value === null || value === undefined;
|
||||
}
|
||||
|
||||
export function IsNotNullish(value: unknown) {
|
||||
return !IsNullish(value);
|
||||
}
|
||||
|
||||
export function PartialArrayRecordAssign<K extends string | number | symbol, T>(
|
||||
record: Partial<Record<K, T[]>>,
|
||||
key: K,
|
||||
data: T
|
||||
) {
|
||||
if (record[key]) {
|
||||
record[key]!.push(data);
|
||||
} else {
|
||||
record[key] = [data];
|
||||
}
|
||||
}
|
||||
|
||||
export function FormatBMSTables(bmsTables: { table: string; level: string }[]) {
|
||||
return bmsTables.map(e => `${e.table}${e.level}`).join(", ");
|
||||
}
|
||||
|
||||
export function FormatGPTRating(
|
||||
game: Game,
|
||||
playtype: Playtype,
|
||||
key: ScoreCalculatedDataLookup[IDStrings],
|
||||
value: number
|
||||
) {
|
||||
const gptConfig = GetGamePTConfig(game, playtype);
|
||||
|
||||
if (gptConfig.scoreRatingAlgFormatters[key]) {
|
||||
return gptConfig.scoreRatingAlgFormatters[key]!(value);
|
||||
}
|
||||
|
||||
return value.toFixed(2);
|
||||
}
|
||||
|
||||
export function ReverseStr(str: string) {
|
||||
return str
|
||||
.split("")
|
||||
.reverse()
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function FormatMillions(v: number) {
|
||||
let str = "";
|
||||
|
||||
const valueStr = v.toFixed();
|
||||
|
||||
for (let i = 0; i < valueStr.length; i++) {
|
||||
const backwardsIndex = valueStr.length - i - 1;
|
||||
if (i % 3 === 0 && i > 0) {
|
||||
str += ",";
|
||||
}
|
||||
str += valueStr[backwardsIndex];
|
||||
}
|
||||
|
||||
return ReverseStr(str);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import Icon from "components/util/Icon";
|
||||
import React from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export function SendErrorToast(text: string) {
|
||||
toast.error(t => (
|
||||
<div>
|
||||
{text}{" "}
|
||||
<span onClick={() => toast.dismiss(t.id)}>
|
||||
<Icon type="times" />
|
||||
</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
|
||||
export function SendSuccessToast(text: string) {
|
||||
toast.success(t => (
|
||||
<div>
|
||||
{text}{" "}
|
||||
<span onClick={() => toast.dismiss(t.id)}>
|
||||
<Icon type="times" />
|
||||
</span>
|
||||
</div>
|
||||
));
|
||||
}
|
||||
@@ -128,9 +128,13 @@ function GetData(dataValue: string | number | [string, number]) {
|
||||
*/
|
||||
function DirectiveMatch(
|
||||
directive: Directive,
|
||||
dataValue: string | number | [string, number],
|
||||
dataValue: string | number | [string, number] | null,
|
||||
directiveNumValue?: number
|
||||
) {
|
||||
if (dataValue === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!directive.mode) {
|
||||
return NeutralMatch(directive.value, GetStrData(dataValue));
|
||||
}
|
||||
@@ -160,11 +164,11 @@ function DirectiveMatch(
|
||||
* a number for >= style comparisons. The value getter must return a tri-tuple of its string value,
|
||||
* its numerical value, and a function to convert the users string request to a number.
|
||||
*/
|
||||
export type ValueGetter<D> = (data: D) => string | number;
|
||||
export type ValueGetter<D> = (data: D) => string | number | null;
|
||||
|
||||
export type ValueGetterOrHybrid<D> =
|
||||
| {
|
||||
valueGetter: (data: D) => [string, number];
|
||||
valueGetter: (data: D) => [string, number] | null;
|
||||
strToNum: (s: string) => number | null;
|
||||
}
|
||||
| ValueGetter<D>;
|
||||
@@ -186,9 +190,15 @@ export function ComposeSearchFunction<D>(
|
||||
const directives = ParseDirectives(search);
|
||||
|
||||
if (directives.length === 0) {
|
||||
return allGetters.some(vgOrHybrid =>
|
||||
NeutralMatch(search, GetStrData(GetValueGetter(vgOrHybrid)(data)))
|
||||
);
|
||||
return allGetters.some(vgOrHybrid => {
|
||||
const v = GetValueGetter(vgOrHybrid)(data);
|
||||
|
||||
if (v === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return NeutralMatch(search, GetStrData(v));
|
||||
});
|
||||
}
|
||||
|
||||
for (const directive of directives) {
|
||||
|
||||
Reference in New Issue
Block a user