diff --git a/client/package.json b/client/package.json index 892213cd4..2035eb32d 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 9e6ed9f18..feaa7bb3b 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -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 diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage.tsx index 3d318ff67..0b4144484 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage.tsx @@ -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, }} > - #{stats.thisUsersRanking.ranking - stats.above.length + i} + #{i} {reqUser.id === s.userID && ( /{stats.thisUsersRanking.outOf} )} @@ -151,14 +152,18 @@ function LeaderboardsPageContent({ {bestNearbyUser > 1 && leaderboard.gameStats .slice(0, bestNearbyUser) - .map((s, i) => )} + .map((s, i) => )} {bestNearbyUser > 4 && ( ... )} {[...stats.above, stats.thisUsersStats, ...stats.below].map((s, i) => ( - + ))} diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx index 49b75d707..813440d9f 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/OverviewPage.tsx @@ -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( @@ -253,7 +253,12 @@ function RecentSessionScoreInfo({
- +
); @@ -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 ( - Best Scores + Best PBs @@ -104,9 +104,10 @@ function PBsOverview({ reqUser, game, playtype }: { reqUser: PublicUserDocument style={{ height: 500 }} {...{ isLoading, error, dataset: data }} > - @@ -176,10 +177,11 @@ function PBsSearch({ return ( - @@ -208,7 +210,11 @@ function ScoresOverview({ reqUser, game, playtype }: { reqUser: PublicUserDocume style={{ height: 500 }} {...{ isLoading, dataset: data, error }} > - + ) : ( @@ -230,7 +236,7 @@ function ScoresSearch({ return ( - + ); } diff --git a/client/src/components/charts/IIDXLampChart.tsx b/client/src/components/charts/IIDXLampChart.tsx index afdaea46f..2711d33d9 100644 --- a/client/src/components/charts/IIDXLampChart.tsx +++ b/client/src/components/charts/IIDXLampChart.tsx @@ -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 => (
- Measure {Math.floor(Number(p.data.xFormatted) / 4 + 1).toString()}:{" "} + Measure {Math.floor(Number(p.data.xFormatted) / 4).toString()}:{" "} {p.data.yFormatted}%
)} diff --git a/client/src/components/layout/header/HeaderMenu.tsx b/client/src/components/layout/header/HeaderMenu.tsx index 3bc1b9f84..fbd792d66 100644 --- a/client/src/components/layout/header/HeaderMenu.tsx +++ b/client/src/components/layout/header/HeaderMenu.tsx @@ -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 ; } + 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( + + ); + } + } + } + return (
    - {/* */} - {user && ugs && ugs.length !== 0 && ( - - {ugs.map(e => ( - - ))} - + {userProfileLinks} )} diff --git a/client/src/components/layout/header/SearchBar.tsx b/client/src/components/layout/header/SearchBar.tsx index 5f142bb48..efa242126 100644 --- a/client/src/components/layout/header/SearchBar.tsx +++ b/client/src/components/layout/header/SearchBar.tsx @@ -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 ( +
    + Found nothing :( +
    + ); + } + return (
    {results?.users.length ? ( @@ -110,6 +119,10 @@ export default function SearchBar() { }; }, [ref]); + useEffect(() => { + setShow(search !== ""); + }, [search]); + // debouncer result-getter function UpdateSearch(event: React.ChangeEvent) { 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 (
    -
    { - 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); - } - }} - > -
    - - {/* */} - -
    +
    +
    +
    + { + if (search !== "") { + setShow(true); + } + }} + placeholder="Search users, songs, charts..." + /> +
    +
    -
    +
    -
    0}> -
    - -
    -
    - {results && search !== "" && } + {results ? : }
    diff --git a/client/src/components/layout/header/Topbar.tsx b/client/src/components/layout/header/Topbar.tsx index bba9889a0..011267d0b 100644 --- a/client/src/components/layout/header/Topbar.tsx +++ b/client/src/components/layout/header/Topbar.tsx @@ -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 ? ( <> - - + {/* commented out quick panel stuff for future */} {/* Quick panel} diff --git a/client/src/components/layout/header/UserProfileDropdown.tsx b/client/src/components/layout/header/UserProfileDropdown.tsx index 5511b30e3..663ca945d 100644 --- a/client/src/components/layout/header/UserProfileDropdown.tsx +++ b/client/src/components/layout/header/UserProfileDropdown.tsx @@ -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 }) {
    My Settings
    - Manage your profile, API Tokens, and more! + Manage your profile, service integrations, and more!
    diff --git a/client/src/components/tables/cells/BMSDifficultyCell.tsx b/client/src/components/tables/cells/BMSDifficultyCell.tsx new file mode 100644 index 000000000..d41f64c8f --- /dev/null +++ b/client/src/components/tables/cells/BMSDifficultyCell.tsx @@ -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 ( + + {levelText} + {!chart.isPrimary && ( + +
    + +
    +
    + )} + + ); +} diff --git a/client/src/components/tables/cells/DifficultyCell.tsx b/client/src/components/tables/cells/DifficultyCell.tsx index 2a4f5f366..2ac715471 100644 --- a/client/src/components/tables/cells/DifficultyCell.tsx +++ b/client/src/components/tables/cells/DifficultyCell.tsx @@ -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), }} > - {FormatDifficulty(chart, game)} - {FormatDifficultyShort(chart, game)} + {!alwaysShort && ( + {FormatDifficulty(chart, game)} + )} + + {FormatDifficultyShort(chart, game)} + {!chart.isPrimary && (
    diff --git a/client/src/components/tables/cells/IIDXLampCell.tsx b/client/src/components/tables/cells/IIDXLampCell.tsx index d959cf4e1..17dbcb384 100644 --- a/client/src/components/tables/cells/IIDXLampCell.tsx +++ b/client/src/components/tables/cells/IIDXLampCell.tsx @@ -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 ( {sc.scoreData.lamp} -
    - - [BP: {sc.scoreData.hitMeta.bp ?? "No Data"} - {sc.scoreData.hitMeta.comboBreak - ? `, CBRK: ${sc.scoreData.hitMeta.comboBreak}` - : ""} - ] - + + {bpText && ( + <> +
    + {bpText} + + )} + {sc.scoreData.lamp === "FAILED" && ( <>
    diff --git a/client/src/components/tables/cells/LampCell.tsx b/client/src/components/tables/cells/LampCell.tsx new file mode 100644 index 000000000..75ea9033c --- /dev/null +++ b/client/src/components/tables/cells/LampCell.tsx @@ -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 ( + + {sc.scoreData.lamp} + + ); +} diff --git a/client/src/components/tables/cells/SDVXScoreCell.tsx b/client/src/components/tables/cells/SDVXScoreCell.tsx new file mode 100644 index 000000000..cf6f3ce4d --- /dev/null +++ b/client/src/components/tables/cells/SDVXScoreCell.tsx @@ -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 ( + + {score.scoreData.grade} +
    + {FormatMillions(score.scoreData.score)} + + ); +} diff --git a/client/src/components/tables/cells/ScoreCell.tsx b/client/src/components/tables/cells/ScoreCell.tsx index 33c5d322a..014f6515c 100644 --- a/client/src/components/tables/cells/ScoreCell.tsx +++ b/client/src/components/tables/cells/ScoreCell.tsx @@ -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 && ( <>
    - [{score.scoreData.score}] + + [ + {scoreRenderFn + ? scoreRenderFn(score.scoreData.score) + : score.scoreData.score} + ] + )} diff --git a/client/src/components/tables/cells/ScoreCellLarge.tsx b/client/src/components/tables/cells/ScoreCellLarge.tsx new file mode 100644 index 000000000..b4cbb8db1 --- /dev/null +++ b/client/src/components/tables/cells/ScoreCellLarge.tsx @@ -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 ; +} diff --git a/client/src/components/tables/cells/TitleCell.tsx b/client/src/components/tables/cells/TitleCell.tsx index 7843c9e8f..8c20f0bd0 100644 --- a/client/src/components/tables/cells/TitleCell.tsx +++ b/client/src/components/tables/cells/TitleCell.tsx @@ -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 ( {song.title} -
    - {song.artist} + {!noArtist && ( + <> +
    + {song.artist} + + )} ); diff --git a/client/src/components/tables/components/FilterDirectivesIndicator.tsx b/client/src/components/tables/components/FilterDirectivesIndicator.tsx index 5e4efa43b..b34762b82 100644 --- a/client/src/components/tables/components/FilterDirectivesIndicator.tsx +++ b/client/src/components/tables/components/FilterDirectivesIndicator.tsx @@ -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"; } diff --git a/client/src/components/tables/components/MiniTable.tsx b/client/src/components/tables/components/MiniTable.tsx index b0aad9d56..34a73c515 100644 --- a/client/src/components/tables/components/MiniTable.tsx +++ b/client/src/components/tables/components/MiniTable.tsx @@ -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 ( - {headers.map(e => ( - ))} diff --git a/client/src/components/tables/components/PageSelector.tsx b/client/src/components/tables/components/PageSelector.tsx index 558cc8f57..083a30123 100644 --- a/client/src/components/tables/components/PageSelector.tsx +++ b/client/src/components/tables/components/PageSelector.tsx @@ -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 ( diff --git a/client/src/components/tables/components/TachiTable.tsx b/client/src/components/tables/components/TachiTable.tsx index 21139b0cd..5eb0b3fea 100644 --- a/client/src/components/tables/components/TachiTable.tsx +++ b/client/src/components/tables/components/TachiTable.tsx @@ -140,11 +140,7 @@ export default function TachiTable({
    + {headers.map((e, i) => ( + {e}
    {headersRow} - - {window.map((e, i) => ( - {rowFunction(e)} - ))} - + {window.map(e => rowFunction(e))}
    diff --git a/client/src/components/tables/dropdowns/GenericSessionDropdown.tsx b/client/src/components/tables/dropdowns/GenericSessionDropdown.tsx new file mode 100644 index 000000000..1bb6c27ea --- /dev/null +++ b/client/src/components/tables/dropdowns/GenericSessionDropdown.tsx @@ -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(`/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 ; + } + + let content; + + if (view === "raises") { + content = ; + } else if (view === "debug") { + content = ; + } + + return ( + + + + Graphs + + + + Raises + + + + Highlights + + + + + Debug Info + + + } + > + {content} + + ); +} + +function SessionRaiseBreakdown({ sessionData }: { sessionData: SpecificSessionReturns }) { + const game = sessionData.session.game; + const playtype = sessionData.session.playtype; + + const gptConfig = GetGamePTConfig(game, playtype); + + const [chartIDs, setChartIDs] = useState([]); + const [tableID, setTableID] = useState("levels"); + const [tableFolders, setTableFolders] = useState([]); + const [folder, setFolder] = useState(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(`/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 ; + } + + return ( + <> +
    +
    +
    +
    + + + Folder Filter + + + + No Filter + + + + Highlights Only + +
    +
    + {filter === "folder" && ( + <> +
    + +
    +
    + +
    +
    + +
    + + )} +
    + + +
    + + + ); +} + +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> = {}; + const newGrades: Partial> = {}; + + 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 ( + <> +
    +
    + + + New Lamps + + + + + Both + + + + New Grades + +
    + +
    + {view === "both" ? ( + <> +
    + + + +
    +
    + + + +
    + + ) : view === "grades" ? ( +
    + + + +
    + ) : ( +
    + + + +
    + )} + + ); +} + +function ElementStatTable({ + type, + counts, + gptConfig, + songMap, + chartMap, + game, + fullSize = false, +}: { + type: "lamp" | "grade"; + counts: Record; + gptConfig: GamePTConfig; + songMap: Map>; + chartMap: Map>; + 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 = No Play; + + 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 = ( + + ); + } else { + preScoreCell = ( + // @ts-expect-error temp + + ); + } + } + + if (score) { + return ( + <> + + + {preScoreCell} + ⟶ + {type === "grade" ? ( + + ) : ( + // @ts-expect-error temp + + )} + + ); + } + } + + return ( + <> + + + + ); + } + + // 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( + + + {element} + + + + ); + + for (const data of counts[element]!.slice(1)) { + tableContents.push( + + + + ); + } + } + + if (tableContents.length === 0) { + return ( + + Nothing... + + ); + } + + return <>{tableContents}; +} diff --git a/client/src/components/tables/dropdowns/IIDXPBDropdown.tsx b/client/src/components/tables/dropdowns/IIDXPBDropdown.tsx index f30839036..6ccbcdc76 100644 --- a/client/src/components/tables/dropdowns/IIDXPBDropdown.tsx +++ b/client/src/components/tables/dropdowns/IIDXPBDropdown.tsx @@ -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; }; } & 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({ Play History + + + Debug Info + } > {view === "history" ? ( <> + ) : view === "debug" ? ( + ) : ( )} @@ -119,6 +128,10 @@ function DocumentDisplay({ const [comment, setComment] = useState(IsScore(currentDoc) ? currentDoc.comment : null); + useEffect(() => { + setComment(IsScore(currentDoc) ? currentDoc.comment : null); + }, [currentDoc]); + return ( <>
    diff --git a/client/src/components/tables/dropdowns/IIDXScoreDropdown.tsx b/client/src/components/tables/dropdowns/IIDXScoreDropdown.tsx index bfc27ab91..7fbbd606d 100644 --- a/client/src/components/tables/dropdowns/IIDXScoreDropdown.tsx +++ b/client/src/components/tables/dropdowns/IIDXScoreDropdown.tsx @@ -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; }; } & 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({
    ); + } else if (view === "debug") { + content = ; } return ( @@ -101,6 +104,10 @@ export default function IIDXScoreDropdown({ Play History + + + Debug Info + } > diff --git a/client/src/components/tables/dropdowns/components/IIDXScoreDropdownParts.tsx b/client/src/components/tables/dropdowns/components/IIDXScoreDropdownParts.tsx index 9f48b28f3..54171ad0d 100644 --- a/client/src/components/tables/dropdowns/components/IIDXScoreDropdownParts.tsx +++ b/client/src/components/tables/dropdowns/components/IIDXScoreDropdownParts.tsx @@ -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"; diff --git a/client/src/components/tables/pbs/BMSPBTable.tsx b/client/src/components/tables/pbs/BMSPBTable.tsx new file mode 100644 index 000000000..ac6b320af --- /dev/null +++ b/client/src/components/tables/pbs/BMSPBTable.tsx @@ -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[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[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 ( + 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 => ( + + )} + /> + ); +} + +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 ( + + // + } + className={highlight ? "highlighted-row" : ""} + > + {indexCol && } + + + + + + + {!IsNullish(pb.calculatedData.sieglinde) + ? pb.calculatedData.sieglinde!.toFixed(2) + : "N/A"} + + + + + ); +} diff --git a/client/src/components/tables/pbs/IIDXPBTable.tsx b/client/src/components/tables/pbs/IIDXPBTable.tsx index 12eefcf29..35aae2e75 100644 --- a/client/src/components/tables/pbs/IIDXPBTable.tsx +++ b/client/src/components/tables/pbs/IIDXPBTable.tsx @@ -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 => ( - + )} /> ); @@ -127,7 +128,6 @@ function Row({ /> } className={highlight ? "highlighted-row" : ""} - key={pb.chartID} > {indexCol && } diff --git a/client/src/components/tables/pbs/PBTable.tsx b/client/src/components/tables/pbs/PBTable.tsx new file mode 100644 index 000000000..e22ed5b87 --- /dev/null +++ b/client/src/components/tables/pbs/PBTable.tsx @@ -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 ; + } else if (game === "bms") { + return ; + } else if (game === "sdvx") { + return ; + } + + return <>; +} diff --git a/client/src/components/tables/pbs/SDVXPBTable.tsx b/client/src/components/tables/pbs/SDVXPBTable.tsx new file mode 100644 index 000000000..64f1e9bf2 --- /dev/null +++ b/client/src/components/tables/pbs/SDVXPBTable.tsx @@ -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[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 ( + 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 => ( + + )} + /> + ); +} + +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 ( + + // + } + className={highlight ? "highlighted-row" : ""} + > + {indexCol && } + + + + + + {!IsNullish(pb.calculatedData.VF6) ? pb.calculatedData.VF6!.toFixed(3) : "No Data."} + + + + + ); +} diff --git a/client/src/components/tables/scores/IIDXScoreTable.tsx b/client/src/components/tables/scores/IIDXScoreTable.tsx index 08e06fb36..1e6aefa33 100644 --- a/client/src/components/tables/scores/IIDXScoreTable.tsx +++ b/client/src/components/tables/scores/IIDXScoreTable.tsx @@ -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( + const [rating, setRating] = useState( gptConfig.defaultScoreRatingAlg ); @@ -52,10 +55,10 @@ export default function IIDXScoreTable({ "Rating", NumericSOV(x => x.calculatedData[rating] ?? 0), (thProps: ZTableTHProps) => ( - + 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 => } + rowFunction={sc => } /> ); } @@ -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({ /> - {IsNullish(sc.calculatedData[rating]) + {!IsNullish(sc.calculatedData[rating]) ? sc.calculatedData[rating]!.toFixed(2) : "No Data."} diff --git a/client/src/components/tables/sessions/GenericSessionTable.tsx b/client/src/components/tables/sessions/GenericSessionTable.tsx index 8d2d1f161..2949656db 100644 --- a/client/src/components/tables/sessions/GenericSessionTable.tsx +++ b/client/src/components/tables/sessions/GenericSessionTable.tsx @@ -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 => ( - - {indexCol && } - - {s.name} -
    - {s.desc} - - - {s.scoreInfo.length} -
    - PBs: {GetPBs(s.scoreInfo).length} - - - {s.calculatedData[rating] - ? s.calculatedData[rating]?.toFixed(2) - : "No Data."} - - {FormatDuration(s.timeEnded - s.timeStarted)} - - {MillisToSince(s.timeStarted)} -
    - {FormatTime(s.timeStarted)} - - + )} /> ); } + +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 ( + } + > + {indexCol && } + + {data.name} +
    + {desc} + + + {data.scoreInfo.length} +
    + PBs: {GetPBs(data.scoreInfo).length} + + + {data.calculatedData[rating] ? data.calculatedData[rating]?.toFixed(2) : "No Data."} + + {FormatDuration(data.timeEnded - data.timeStarted)} + + {MillisToSince(data.timeStarted)} +
    + {FormatTime(data.timeStarted)} + +
    + ); +} diff --git a/client/src/components/user/ProfileBadges.tsx b/client/src/components/user/ProfileBadges.tsx index 544e876d1..e49ec99c3 100644 --- a/client/src/components/user/ProfileBadges.tsx +++ b/client/src/components/user/ProfileBadges.tsx @@ -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) => ( - + + + ))} ); @@ -13,11 +16,17 @@ export default function ProfileBadges({ badges }: { badges: UserBadges[] }) { export function ProfileBadge({ variant }: { variant: UserBadges }) { if (variant === "alpha") { - return
    Alpha Tester
    ; + return Alpha Tester; } else if (variant === "beta") { - return
    Beta Tester
    ; + return Beta Tester; } else if (variant === "devTeam") { - return
    Dev Team
    ; + return Dev Team; + } else if (variant === "contributor") { + // discord contributor colour + return Contributor; + } else if (variant === "significant-contributor") { + // discord sig. contributor colour + return Significant Contributor; } return <>; diff --git a/client/src/components/user/UGPTHeader.tsx b/client/src/components/user/UGPTHeader.tsx index 8d8aad807..0f6d5c849 100644 --- a/client/src/components/user/UGPTHeader.tsx +++ b/client/src/components/user/UGPTHeader.tsx @@ -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({

    - {reqUser.username}'s {gameConfig.name} {playtype} Profile + {reqUser.username}'s {FormatGame(game, playtype)} Profile

    @@ -66,7 +72,8 @@ export default function UGPTHeader({ Last Played - {stats.mostRecentScore.timeAchieved === null + {stats.mostRecentScore === null || + stats.mostRecentScore.timeAchieved === null ? "Unknown." : MillisToSince(stats.mostRecentScore.timeAchieved)} @@ -74,7 +81,8 @@ export default function UGPTHeader({ First Play - {stats.firstScore.timeAchieved === null + {stats.firstScore === null || + stats.firstScore.timeAchieved === null ? "Unknown." : MillisToSince(stats.firstScore.timeAchieved)} @@ -112,7 +120,14 @@ export default function UGPTHeader({ {Object.entries(stats.gameStats.ratings).map(([k, v]) => ( {UppercaseFirst(k)} - {v.toFixed(2)} + + {FormatGPTRating( + game, + playtype, + k as ScoreCalculatedDataLookup[IDStrings], + v + )} + ))} diff --git a/client/src/components/util/DebugContent.tsx b/client/src/components/util/DebugContent.tsx new file mode 100644 index 000000000..d0d803ac2 --- /dev/null +++ b/client/src/components/util/DebugContent.tsx @@ -0,0 +1,12 @@ +import React from "react"; + +export default function DebugContent({ data }: { data: unknown }) { + return ( +