Yet another huge sync update

This commit is contained in:
zkldi
2021-08-17 21:47:47 +01:00
parent b71e56f106
commit f54afcb30c
44 changed files with 1428 additions and 235 deletions
+1 -2
View File
@@ -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",
+23 -50
View File
@@ -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>
);
}
+13 -4
View File
@@ -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 <></>;
+22 -7
View File
@@ -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}.`;
+10 -4
View File
@@ -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
View File
@@ -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
}
+11
View File
@@ -0,0 +1,11 @@
export const BMS_TABLES = {
insane: "🟊",
overjoy: "🟊🟊",
normal: "☆",
normal2: "▽",
insane2: "▼",
stella: "st",
satellite: "sl",
dpNormal: "δ",
dpInsane: "★",
};
+12 -2
View File
@@ -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
+1 -1
View File
@@ -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);
+63
View File
@@ -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);
}
+25
View File
@@ -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>
));
}
+16 -6
View File
@@ -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) {