diff --git a/client/src/app/pages/dashboard/games/_game/_playtype/GPTChartPage.tsx b/client/src/app/pages/dashboard/games/_game/_playtype/GPTChartPage.tsx new file mode 100644 index 000000000..f174ce81f --- /dev/null +++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTChartPage.tsx @@ -0,0 +1,325 @@ +import useSetSubheader from "components/layout/header/useSetSubheader"; +import Card from "components/layout/page/Card"; +import LampCell from "components/tables/cells/LampCell"; +import ScoreCell from "components/tables/cells/ScoreCell"; +import MiniTable from "components/tables/components/MiniTable"; +import PBTable from "components/tables/pbs/PBTable"; +import ProfilePicture from "components/user/ProfilePicture"; +import ApiError from "components/util/ApiError"; +import Divider from "components/util/Divider"; +import Icon from "components/util/Icon"; +import Loading from "components/util/Loading"; +import SelectButton from "components/util/SelectButton"; +import { UserContext } from "context/UserContext"; +import React, { useContext, useMemo, useState } from "react"; +import { Col, Row } from "react-bootstrap"; +import { useQuery } from "react-query"; +import { Link, useParams } from "react-router-dom"; +import { + ChartDocument, + FormatDifficulty, + GamePTConfig, + GetGameConfig, + GetGamePTConfig, + integer, + PBScoreDocument, + PublicUserDocument, + SongDocument, +} from "tachi-common"; +import { ChartPBLeaderboardReturn, UGPTChartLeaderboardAdjacent } from "types/api-returns"; +import { GamePT, SetState } from "types/react"; +import { PBDataset } from "types/tables"; +import { APIFetchV1, UnsuccessfulAPIFetchResponse } from "util/api"; +import { CreateUserMap } from "util/data"; + +// This component forms a wrapper around the Real GPT Chart Page +// which handles the case where activeChart == null. +export default function GPTChartPage({ + chart, + setActiveChart, + game, + song, + playtype, + allCharts, +}: { + song: SongDocument; + chart: ChartDocument | null; + setActiveChart: SetState; + allCharts: ChartDocument[]; +} & GamePT) { + const { difficulty } = useParams<{ difficulty: string }>(); + + const gptConfig = GetGamePTConfig(game, playtype); + + const formatSongTitle = `${song.artist} - ${song.title}`; + const formatDiff = chart ? FormatDifficulty(chart, game) : "Loading..."; + + useSetSubheader( + ["Games", GetGameConfig(game).name, playtype, "Songs", formatSongTitle, formatDiff], + [game, playtype, chart], + `${formatSongTitle} (${formatDiff})` + ); + + if (!chart) { + setActiveChart(SelectRightChart(gptConfig, difficulty, allCharts)); + + return ; + } + + return ; +} + +interface ChartPBData { + leaderboard: ChartPBLeaderboardReturn; + adjacent?: UGPTChartLeaderboardAdjacent; + playcount: integer; +} + +function InternalGPTChartPage({ + chart, + game, + song, + playtype, +}: { + song: SongDocument; + chart: ChartDocument; +} & GamePT) { + const { user } = useContext(UserContext); + + const [mode, setMode] = useState<"leaderboard" | "adjacent">("leaderboard"); + + const { data, isLoading, error } = useQuery( + ["PBInfo", `${chart.chartID}`], + async () => { + const lRes = await APIFetchV1( + `/games/${game}/${playtype}/charts/${chart.chartID}/pbs` + ); + + if (!lRes.success) { + throw lRes; + } + + const pRes = await APIFetchV1<{ count: integer }>( + `/games/${game}/${playtype}/charts/${chart.chartID}/playcount` + ); + + if (!pRes.success) { + throw pRes; + } + + if (user) { + const nRes = await APIFetchV1( + `/users/${user.id}/games/${game}/${playtype}/pbs/${chart.chartID}/leaderboard-adjacent` + ); + + if (!nRes.success) { + return { leaderboard: lRes.body, playcount: pRes.body.count }; + } + + return { + leaderboard: lRes.body, + adjacent: nRes.body, + playcount: pRes.body.count, + }; + } + + return { leaderboard: lRes.body, playcount: pRes.body.count }; + } + ); + + if (error) { + return ; + } + + if (!data || isLoading) { + return ; + } + + if (data.leaderboard.pbs.length === 0) { + return
Nobody has played this chart!
; + } + + const userMap = CreateUserMap(data.leaderboard.users); + + if (data.adjacent) { + for (const user of data.adjacent.users) { + userMap.set(user.id, user); + } + } + + return ( + + +

Leaderboard

+ + +
+ + + Best 100 + + + + Your Position + +
+ + + + + +
+ ); +} + +function ChartLeaderboardTable({ + data, + userMap, + user, + game, + playtype, + mode, + chart, + song, +}: { + data: ChartPBData; + user: PublicUserDocument | null; + userMap: Map; + mode: "leaderboard" | "adjacent"; + chart: ChartDocument; + song: SongDocument; +} & GamePT) { + const dataset: PBDataset = useMemo(() => { + const ds: PBDataset = []; + + let pbs = []; + if (mode === "leaderboard") { + pbs = data.leaderboard.pbs; + } else { + pbs = [ + ...data.adjacent!.adjacentAbove, + data.adjacent!.pb, + ...data.adjacent!.adjacentBelow, + ]; + } + + for (const pb of pbs) { + ds.push({ + ...pb, + __related: { + chart, + song, + index: pb.rankingData.rank - 1, + user: userMap.get(pb.userID)!, + }, + }); + } + + return ds; + }, [data, mode, user]); + + return ; +} + +function TopShowcase({ + data, + user, + userMap, +}: { + data: ChartPBData; + user: PublicUserDocument | null; + userMap: Map; +}) { + // We have a couple of conditions. + // User is #1: col-12 #1, + // User has played: col-6 col-6, + // User has not played: col-12 #1, + + const bestPlay = data.leaderboard.pbs[0]; + const bestUser = userMap.get(bestPlay.userID)!; + + // User hasn't played, or isn't logged in or something. + if (!data.adjacent || user?.id === bestPlay.userID) { + return ( + + + + ); + } + + const thisUsersPlay = data.adjacent.pb; + + return ( + <> + + + + + + + + ); +} + +function PlayCard({ + pb, + user, + name, +}: { + pb: PBScoreDocument; + user: PublicUserDocument; + name: string; +}) { + return ( + + + + + + +

+ + {user.username} + +

+ #{pb.rankingData.rank} + /{pb.rankingData.outOf} + + + + + + + + + + + +
+
+ ); +} + +function SelectRightChart(gptConfig: GamePTConfig, chartID: string, charts: ChartDocument[]) { + if (gptConfig.difficulties.includes(chartID as any)) { + for (const chart of charts) { + if (chart.difficulty === chartID && chart.isPrimary) { + return chart; + } + } + } else { + // else, its a chart ID. + for (const chart of charts) { + if (chartID === chart.chartID) { + return chart; + } + } + } + + return null; +} diff --git a/client/src/app/pages/dashboard/games/_game/_playtype/GPTSongPage.tsx b/client/src/app/pages/dashboard/games/_game/_playtype/GPTSongPage.tsx new file mode 100644 index 000000000..6493bba73 --- /dev/null +++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTSongPage.tsx @@ -0,0 +1,29 @@ +import useSetSubheader from "components/layout/header/useSetSubheader"; +import LinkButton from "components/util/LinkButton"; +import React, { useEffect } from "react"; +import { Button, Row } from "react-bootstrap"; +import { ChartDocument, FormatDifficulty, GetGameConfig, GetGamePTConfig } from "tachi-common"; +import { SongsReturn } from "types/api-returns"; +import { GamePT, SetState } from "types/react"; +import { ChangeOpacity } from "util/color-opacity"; +import { NumericSOV } from "util/sorts"; + +export default function GPTSongPage({ + game, + playtype, + song, + charts, + setActiveChart, +}: { setActiveChart: SetState } & SongsReturn & GamePT) { + const formatSongTitle = `${song.artist} - ${song.title}`; + + useSetSubheader( + ["Games", GetGameConfig(game).name, playtype, "Songs", formatSongTitle], + [game, playtype], + formatSongTitle + ); + + setActiveChart(null); + + return
You gotta select a chart to see leaderboards!
; +} diff --git a/client/src/app/pages/dashboard/users/UserSettingsPage.tsx b/client/src/app/pages/dashboard/users/UserSettingsPage.tsx index ee963c358..bd222fcb9 100644 --- a/client/src/app/pages/dashboard/users/UserSettingsPage.tsx +++ b/client/src/app/pages/dashboard/users/UserSettingsPage.tsx @@ -114,7 +114,7 @@ function PreferencesForm({ reqUser }: { reqUser: PublicUserDocument }) { id="contentiousContent" checked={formik.values.contentiousContent} onChange={formik.handleChange} - label="Contentious Content" + label="Family Unfriendly Mode" /> Show slightly less appropriate splash texts in certain places. diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/ScoresPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/ScoresPage.tsx index ad3dad633..55ef95e8d 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/ScoresPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/ScoresPage.tsx @@ -193,7 +193,6 @@ function PBsOverview({ {...{ isLoading, error, dataset: data }} > (); @@ -60,12 +81,10 @@ function GamePlaytypeRoutes({ game }: { game: Game }) { return ( <> - } - header={FormatGame(game, playtype)} - > - - +
+ +
+ @@ -75,10 +94,13 @@ function GamePlaytypeRoutes({ game }: { game: Game }) { + + + + - @@ -90,3 +112,157 @@ function GamePlaytypeRoutes({ game }: { game: Game }) { ); } + +function SongChartRoutes({ game, playtype }: GamePT) { + const { songID } = useParams<{ songID: string }>(); + + const { data, isLoading, error } = useApiQuery( + `/games/${game}/${playtype}/songs/${songID}` + ); + + const { settings } = useContext(UserSettingsContext); + + const [activeChart, setActiveChart] = useState(null); + + useEffect(() => { + setActiveChart(null); + }, [game, playtype]); + + if (error) { + return ; + } + + if (!data || isLoading) { + return ; + } + + return ( + <> + + + + + + + + + + + + + + + + {settings?.preferences.developerMode && ( + <> + + + + + + )} + + ); +} + +function SongInfoHeader({ + game, + playtype, + song, + charts, + activeChart, + setActiveChart, +}: { activeChart: ChartDocument | null; setActiveChart: SetState } & GamePT & + SongsReturn) { + // @TODO: Custom game song formatting functions, since for some games + // (like popn) players use the genres to discuss them. + const formatSongTitle = `${song.artist} - ${song.title}`; + + const [imgShow, setImgShow] = useState(true); + + const gptConfig = GetGamePTConfig(game, playtype); + + // accidentally O(n^2) but this is a short list so who cares + const sortedCharts = charts + .slice(0) + .sort(NumericSOV(x => gptConfig.difficulties.indexOf(x.difficulty))); + + return ( + + + {imgShow && ( + + setImgShow(false)} + className="w-100" + /> + + )} + + + + {game !== "bms" && ( + +
Charts
+
+
+ {sortedCharts.map(e => ( + setActiveChart(e)} + className="btn-secondary" + key={e.chartID} + to={`/dashboard/games/${game}/${playtype}/songs/${song.id}/${e.difficulty}`} + style={{ + backgroundColor: gptConfig.difficultyColours[e.difficulty] + ? ChangeOpacity( + gptConfig.difficultyColours[e.difficulty]!, + activeChart?.chartID === e.chartID ? 0.4 : 0.2 + ) + : undefined, + }} + > + {activeChart?.chartID === e.chartID ? ( + {FormatDifficulty(e, game)} + ) : ( + FormatDifficulty(e, game) + )} + + ))} +
+ + )} + {activeChart && ( + + + + + )} +
+
+ ); +} diff --git a/client/src/components/game/GPTHeader.tsx b/client/src/components/game/GPTHeader.tsx index 27a40df28..eaf1fdd85 100644 --- a/client/src/components/game/GPTHeader.tsx +++ b/client/src/components/game/GPTHeader.tsx @@ -1,45 +1,7 @@ import Navbar from "components/nav/Navbar"; import NavItem from "components/nav/NavItem"; -import MiniTable from "components/tables/components/MiniTable"; -import ApiError from "components/util/ApiError"; -import Loading from "components/util/Loading"; -import useApiQuery from "components/util/query/useApiQuery"; import { UserSettingsContext } from "context/UserSettingsContext"; import React, { useContext } from "react"; -import { Game } from "tachi-common"; -import { GPTStatsReturn } from "types/api-returns"; -import { Playtype } from "types/tachi"; - -export function GPTHeaderBody({ game, playtype }: { game: Game; playtype: Playtype }) { - const { data, isLoading, error } = useApiQuery(`/games/${game}/${playtype}`); - - return ( - <> -
- {error ? ( - - ) : isLoading || !data ? ( - - ) : ( - - - Player Count - {data.playerCount} - - - Available Charts - {data.chartCount} - - - Total Scores - {data.scoreCount} - - - )} -
- - ); -} export function GPTBottomNav({ baseUrl }: { baseUrl: string }) { const { settings } = useContext(UserSettingsContext); diff --git a/client/src/components/game/charts/ChartInfoFormat.tsx b/client/src/components/game/charts/ChartInfoFormat.tsx new file mode 100644 index 000000000..eb48edb33 --- /dev/null +++ b/client/src/components/game/charts/ChartInfoFormat.tsx @@ -0,0 +1,108 @@ +import QuickTooltip from "components/layout/misc/QuickTooltip"; +import MiniTable from "components/tables/components/MiniTable"; +import ApiError from "components/util/ApiError"; +import Icon from "components/util/Icon"; +import Loading from "components/util/Loading"; +import Muted from "components/util/Muted"; +import useApiQuery from "components/util/query/useApiQuery"; +import { UserContext } from "context/UserContext"; +import React, { useContext } from "react"; +import { Col, Row } from "react-bootstrap"; +import { Link } from "react-router-dom"; +import { ChartDocument, FolderDocument, Game, GetGamePTConfig } from "tachi-common"; +import { GamePT } from "types/react"; + +export default function ChartInfoFormat({ + chart, + game, + playtype, +}: { chart: ChartDocument } & GamePT) { + const gptConfig = GetGamePTConfig(game, playtype); + + const withTierlists = gptConfig.tierlists.filter(e => chart.tierlistInfo[e]); + + const { data, isLoading, error } = useApiQuery( + `/games/${game}/${playtype}/charts/${chart.chartID}/folders` + ); + + const { user } = useContext(UserContext); + + if (error) { + ; + } + + if (!data || isLoading) { + return ; + } + + return ( + + +

Appears In

+ {data.length !== 0 ? ( + data.map(e => ( +
  • + {user ? ( + + {e.title} + + ) : ( + {e.title} + )} +
  • + )) + ) : ( + No folders... + )} + + + + + + {withTierlists.length !== 0 ? ( + <> + + {withTierlists.map(e => ( + + {e} + + {chart.tierlistInfo[e]!.text}{" "} + ({chart.tierlistInfo[e]!.value}) + {chart.tierlistInfo[e]!.individualDifference && ( + <> +
    + + + + + + + )} + + + ))} +
    + Disagree with these tiers? Let us know in the discord. + + ) : ( + No tierlist info. + )} + +
    + ); +} + +function ChartInfoMiddle({ game, chart }: { chart: ChartDocument; game: Game }) { + // someday + + return <>; +} diff --git a/client/src/components/game/songs/DefaultSongInfoFormat.tsx b/client/src/components/game/songs/DefaultSongInfoFormat.tsx new file mode 100644 index 000000000..2a72c9c4a --- /dev/null +++ b/client/src/components/game/songs/DefaultSongInfoFormat.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { ChartDocument, FormatDifficulty, Game, SongDocument } from "tachi-common"; + +export default function DefaultSongChartInfoFormat({ + song, + chart, + game, +}: { + song: SongDocument; + chart: ChartDocument | null; + game: Game; +}) { + return ( + <> +

    + {song.artist} - {song.title} +

    + {chart &&
    ({FormatDifficulty(chart, game)})
    } + + ); +} diff --git a/client/src/components/game/songs/SongInfoFormat.tsx b/client/src/components/game/songs/SongInfoFormat.tsx new file mode 100644 index 000000000..8d6058d65 --- /dev/null +++ b/client/src/components/game/songs/SongInfoFormat.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import { ChartDocument, FormatDifficulty, Game, SongDocument } from "tachi-common"; +import { FormatBMSTables } from "util/misc"; +import DefaultSongChartInfoFormat from "./DefaultSongInfoFormat"; + +export default function SongChartInfoFormat({ + song: tempSong, + chart: tempChart, + game, +}: { + song: SongDocument; + chart: ChartDocument | null; + game: Game; +}) { + // lazy casting hack to avoid IDStrings + const props = { song: tempSong, chart: tempChart, game } as any; + const { song, chart } = props; + + if (game === "bms") { + if (!chart) { + return ( + <> +

    {song.data.genre}

    +

    {song.title}

    +

    {song.artist}

    + + ); + } + const hasLevel = chart.data.tableFolders.length > 0; + + const levelText = hasLevel ? FormatBMSTables(chart.data.tableFolders) : "No Level"; + + return ( + <> +

    {song.data.genre}

    +

    {song.title}

    +

    {song.artist}

    + {chart &&
    ({levelText})
    } + + ); + } else if (game === "iidx") { + return ( + <> +

    {song.data.genre}

    +

    {song.title}

    +

    {song.artist}

    + {chart &&
    ({FormatDifficulty(chart, game)})
    } + + ); + } + + return ; +} diff --git a/client/src/components/tables/dropdowns/GenericPBDropdown.tsx b/client/src/components/tables/dropdowns/GenericPBDropdown.tsx index ef875a8e3..5089bf560 100644 --- a/client/src/components/tables/dropdowns/GenericPBDropdown.tsx +++ b/client/src/components/tables/dropdowns/GenericPBDropdown.tsx @@ -1,23 +1,18 @@ +import ApiError from "components/util/ApiError"; import DebugContent from "components/util/DebugContent"; +import HasDevModeOn from "components/util/HasDevModeOn"; import Icon from "components/util/Icon"; -import SelectButton from "components/util/SelectButton"; -import React, { useContext, useMemo, useState } from "react"; -import DropdownStructure from "./components/DropdownStructure"; -import { - PublicUserDocument, - ChartDocument, - ScoreDocument, - PBScoreDocument, - IDStrings, -} from "tachi-common"; -import { GamePT, SetState } from "types/react"; -import { UGPTChartPBComposition } from "types/api-returns"; import Loading from "components/util/Loading"; +import useApiQuery from "components/util/query/useApiQuery"; +import SelectButton from "components/util/SelectButton"; +import { UserSettingsContext } from "context/UserSettingsContext"; +import React, { useContext, useMemo, useState } from "react"; +import { ChartDocument, IDStrings, integer, PBScoreDocument, ScoreDocument } from "tachi-common"; +import { UGPTChartPBComposition } from "types/api-returns"; +import { GamePT, SetState } from "types/react"; +import DropdownStructure from "./components/DropdownStructure"; import GenericScoreContentDropdown from "./components/GenericScoreContentDropdown"; import PlayHistory from "./components/PlayHistory"; -import useApiQuery from "components/util/query/useApiQuery"; -import { UserSettingsContext } from "context/UserSettingsContext"; -import HasDevModeOn from "components/util/HasDevModeOn"; export interface ScoreState { highlight: boolean; @@ -33,13 +28,13 @@ export interface ScoreDropdownProps { export default function GenericPBDropdown({ game, playtype, - reqUser, chart, scoreState, defaultView = "pb", + userID, DocComponent = GenericScoreContentDropdown, }: { - reqUser: PublicUserDocument; + userID: integer; chart: ChartDocument; scoreState: ScoreState; defaultView?: "pb" | "scorePB" | "lampPB" | "history" | "debug"; @@ -55,12 +50,12 @@ export default function GenericPBDropdown({ const [view, setView] = useState(defaultView); const { isLoading, error, data } = useApiQuery>( - `/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true` + `/users/${userID}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true` ); const { isLoading: histIsLoading, error: histError, data: histData } = useApiQuery< ScoreDocument[] - >(`/users/${reqUser.id}/games/${game}/${playtype}/scores/${chart.chartID}`); + >(`/users/${userID}/games/${game}/${playtype}/scores/${chart.chartID}`); const currentScoreDoc: ScoreDocument | PBScoreDocument | null = useMemo(() => { if (!data) { @@ -73,7 +68,7 @@ export default function GenericPBDropdown({ // scores have more information than PBs. // In this case, the PB is only composed of one score, // so we should default to this instead. - return data.scores[0]; + return data.scores.filter(e => e.scoreID === data.pb.composedFrom.lampPB)[0]; } return data.pb; } @@ -89,7 +84,7 @@ export default function GenericPBDropdown({ }, [view, data]); if (error) { - return <>An error has occured. Whoops.; + return ; } if (isLoading || !data) { diff --git a/client/src/components/tables/dropdowns/IIDXPBDropdown.tsx b/client/src/components/tables/dropdowns/IIDXPBDropdown.tsx index 4d226ecb4..4aea50a1e 100644 --- a/client/src/components/tables/dropdowns/IIDXPBDropdown.tsx +++ b/client/src/components/tables/dropdowns/IIDXPBDropdown.tsx @@ -1,5 +1,11 @@ import React from "react"; -import { ChartDocument, PBScoreDocument, PublicUserDocument, ScoreDocument } from "tachi-common"; +import { + ChartDocument, + integer, + PBScoreDocument, + PublicUserDocument, + ScoreDocument, +} from "tachi-common"; import { UGPTChartPBComposition } from "types/api-returns"; import { GamePT, SetState } from "types/react"; import DropdownScoreButtons from "./components/DropdownScoreButtons"; @@ -10,11 +16,11 @@ import GenericPBDropdown, { ScoreState } from "./GenericPBDropdown"; export default function IIDXPBDropdown({ game, playtype, - reqUser, chart, scoreState, + userID, }: { - reqUser: PublicUserDocument; + userID: integer; chart: ChartDocument<"iidx:SP" | "iidx:DP">; scoreState: { highlight: boolean; @@ -23,7 +29,7 @@ export default function IIDXPBDropdown({ } & GamePT) { return ( ); diff --git a/client/src/components/tables/folders/BMSFolderTable.tsx b/client/src/components/tables/folders/BMSFolderTable.tsx index 8915eb053..65e916d07 100644 --- a/client/src/components/tables/folders/BMSFolderTable.tsx +++ b/client/src/components/tables/folders/BMSFolderTable.tsx @@ -94,7 +94,7 @@ function Row({ dropdown={ ; indexCol?: boolean; - reqUser: PublicUserDocument; showPlaycount?: boolean; playtype: "7K" | "14K"; }) { @@ -110,7 +108,6 @@ export default function BMSPBTable({ @@ -122,13 +119,11 @@ export default function BMSPBTable({ function Row({ pb, indexCol, - reqUser, showPlaycount, }: { pb: PBDataset<"bms:7K" | "bms:14K">[0]; indexCol: boolean; showPlaycount: boolean; - reqUser: PublicUserDocument; }) { const scoreState = usePBState(pb); @@ -137,7 +132,7 @@ function Row({ dropdown={ } - > {indexCol && } diff --git a/client/src/components/tables/pbs/IIDXPBTable.tsx b/client/src/components/tables/pbs/IIDXPBTable.tsx index 6f04da17c..deaa1eadd 100644 --- a/client/src/components/tables/pbs/IIDXPBTable.tsx +++ b/client/src/components/tables/pbs/IIDXPBTable.tsx @@ -1,13 +1,13 @@ import useScoreRatingAlg from "components/util/useScoreRatingAlg"; +import { UserContext } from "context/UserContext"; import { nanoid } from "nanoid"; -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { PublicUserDocument, ScoreCalculatedDataLookup } from "tachi-common"; -import { FormatDifficulty } from "tachi-common/js/utils/util"; import { PBDataset } from "types/tables"; import { IsNullish } from "util/misc"; import { NumericSOV, StrSOV } from "util/sorts"; -import { HumanFriendlyStrToGradeIndex, HumanFriendlyStrToLampIndex } from "util/str-to-num"; import { CreateDefaultPBSearchParams } from "util/tables/create-search"; +import { GetPBLeadingHeaders } from "util/tables/get-pb-leaders"; import DeltaCell from "../cells/DeltaCell"; import DifficultyCell from "../cells/DifficultyCell"; import IIDXLampCell from "../cells/IIDXLampCell"; @@ -17,36 +17,40 @@ import RankingCell from "../cells/RankingCell"; import ScoreCell from "../cells/ScoreCell"; import TimestampCell from "../cells/TimestampCell"; import TitleCell from "../cells/TitleCell"; +import UserCell from "../cells/UserCell"; import DropdownRow from "../components/DropdownRow"; import SelectableRating from "../components/SelectableRating"; import TachiTable, { Header, ZTableTHProps } from "../components/TachiTable"; import { usePBState } from "../components/UseScoreState"; import IIDXPBDropdown from "../dropdowns/IIDXPBDropdown"; -import IndicatorHeader from "../headers/IndicatorHeader"; export default function IIDXPBTable({ dataset, indexCol = true, showPlaycount = false, - reqUser, + showUser = false, + showChart = true, alg, playtype, }: { dataset: PBDataset<"iidx:SP" | "iidx:DP">; indexCol?: boolean; showPlaycount?: boolean; - reqUser: PublicUserDocument; + showUser?: boolean; + showChart?: boolean; alg?: ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"]; playtype: "SP" | "DP"; }) { const defaultRating = useScoreRatingAlg<"iidx:SP" | "iidx:DP">("iidx", playtype); + console.log(alg, defaultRating); + const [rating, setRating] = useState( alg ?? defaultRating ); const headers: Header[0]>[] = [ - [ + ...GetPBLeadingHeaders(showUser, showChart, [ "Chart", "Chart", NumericSOV( @@ -55,9 +59,7 @@ export default function IIDXPBTable({ x.__related.chart.tierlistInfo["kt-HC"]?.value ?? x.__related.chart.levelNum ), - ], - IndicatorHeader, - ["Song", "Song", StrSOV(x => x.__related.song.title)], + ]), ["Score", "Score", NumericSOV(x => x.scoreData.percent)], ["Deltas", "Deltas", NumericSOV(x => x.scoreData.percent)], ["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)], @@ -69,7 +71,7 @@ export default function IIDXPBTable({ key={nanoid()} game="iidx" - playtype="SP" + playtype={playtype} rating={rating} setRating={setRating} {...thProps} @@ -84,10 +86,16 @@ export default function IIDXPBTable({ headers.push(["Playcount", "Plays", NumericSOV(x => x.__playcount ?? 0)]); } + // if (hideRankingCell) { + + // } + if (indexCol) { headers.unshift(["#", "#", NumericSOV(x => x.__related.index)]); } + const { user } = useContext(UserContext); + return ( )} /> @@ -112,24 +122,29 @@ export default function IIDXPBTable({ function Row({ pb, indexCol, - reqUser, rating, showPlaycount, + showChart, + showUser, + user, }: { pb: PBDataset<"iidx:SP" | "iidx:DP">[0]; indexCol: boolean; - reqUser: PublicUserDocument; showPlaycount: boolean; + showUser: boolean; + showChart: boolean; rating: ScoreCalculatedDataLookup["iidx:SP"]; + user: PublicUserDocument | null; }) { const scoreState = usePBState(pb); return ( {indexCol && } - - - + {showUser && showChart && ( + <> + + + + + + )} + {showUser && !showChart && ( + <> + + + + )} + {!showUser && showChart && ( + <> + + + + + )} ; indexCol?: boolean; showPlaycount?: boolean; - reqUser: PublicUserDocument; playtype: "Single"; }) { const headers: Header[0]>[] = [ @@ -64,7 +61,6 @@ export default function MusecaPBTable({ @@ -76,13 +72,11 @@ export default function MusecaPBTable({ function Row({ pb, indexCol, - reqUser, showPlaycount, }: { pb: PBDataset<"museca:Single">[0]; indexCol: boolean; showPlaycount: boolean; - reqUser: PublicUserDocument; }) { const scoreState = usePBState(pb); @@ -91,7 +85,7 @@ function Row({ dropdown={ ; diff --git a/client/src/components/tables/pbs/SDVXPBTable.tsx b/client/src/components/tables/pbs/SDVXPBTable.tsx index 64b2f7ac0..d8f96307d 100644 --- a/client/src/components/tables/pbs/SDVXPBTable.tsx +++ b/client/src/components/tables/pbs/SDVXPBTable.tsx @@ -23,13 +23,11 @@ export default function SDVXPBTable({ dataset, indexCol = true, showPlaycount = false, - reqUser, playtype, }: { dataset: PBDataset<"sdvx:Single">; indexCol?: boolean; showPlaycount?: boolean; - reqUser: PublicUserDocument; playtype: "7K" | "14K"; }) { const headers: Header[0]>[] = [ @@ -63,7 +61,6 @@ export default function SDVXPBTable({ @@ -76,12 +73,10 @@ function Row({ pb, indexCol, showPlaycount, - reqUser, }: { pb: PBDataset<"sdvx:Single">[0]; indexCol: boolean; showPlaycount: boolean; - reqUser: PublicUserDocument; }) { const scoreState = usePBState(pb); @@ -90,13 +85,12 @@ function Row({ dropdown={ } - > {indexCol && } diff --git a/client/src/components/tables/scores/GenericScoreTable.tsx b/client/src/components/tables/scores/GenericScoreTable.tsx index a51c68ad0..4fdefd12f 100644 --- a/client/src/components/tables/scores/GenericScoreTable.tsx +++ b/client/src/components/tables/scores/GenericScoreTable.tsx @@ -87,7 +87,6 @@ function Row({ return ( void; } & JustChildren) { return ( - + {children} ); diff --git a/client/src/types/api-returns.ts b/client/src/types/api-returns.ts index 2b5562193..aea962a52 100644 --- a/client/src/types/api-returns.ts +++ b/client/src/types/api-returns.ts @@ -136,3 +136,20 @@ export interface RecentClassesReturn { classes: ClassAchievementDocument[]; users: PublicUserDocument[]; } + +export interface SongsReturn { + song: SongDocument; + charts: ChartDocument[]; +} + +export interface ChartPBLeaderboardReturn { + users: PublicUserDocument[]; + pbs: PBScoreDocument[]; +} + +export interface UGPTChartLeaderboardAdjacent { + users: PublicUserDocument[]; + pb: PBScoreDocument; + adjacentAbove: PBScoreDocument[]; + adjacentBelow: PBScoreDocument[]; +} diff --git a/client/src/types/tables.ts b/client/src/types/tables.ts index 32d5e4c64..048144c97 100644 --- a/client/src/types/tables.ts +++ b/client/src/types/tables.ts @@ -10,7 +10,12 @@ import { } from "tachi-common"; export type PBDataset = (PBScoreDocument & { - __related: { chart: ChartDocument; song: SongDocument; index: integer }; + __related: { + chart: ChartDocument; + song: SongDocument; + index: integer; + user?: PublicUserDocument; + }; __playcount?: integer; })[]; @@ -30,3 +35,9 @@ export type FolderDataset = (ChartDocument & user: PublicUserDocument; }; })[]; + +export type ChartLeaderboardDataset = (PBScoreDocument & { + __related: { + user: PublicUserDocument; + }; +})[]; diff --git a/client/src/util/api.ts b/client/src/util/api.ts index 5c9aa9bd5..122ba406b 100644 --- a/client/src/util/api.ts +++ b/client/src/util/api.ts @@ -48,9 +48,9 @@ export async function APIFetchV1( const rj = await res.json(); if (!rj.success) { - console.error(rj); + console.warn(rj); } else { - console.log(rj.description); + console.debug(rj.description); } if (!rj.success && displayFailure) { diff --git a/client/src/util/splashes.ts b/client/src/util/splashes.ts index 2ec171820..b58982465 100644 --- a/client/src/util/splashes.ts +++ b/client/src/util/splashes.ts @@ -1,5 +1,3 @@ -import { IDStrings } from "tachi-common"; - export const neutralSplashes = ["A Rhythm Game Score Manager", "The Rhythm Game Score Manager"]; export const loggedInSplashes = [ diff --git a/client/src/util/tables/create-search.ts b/client/src/util/tables/create-search.ts index 703cc80ee..cf0ae3c0c 100644 --- a/client/src/util/tables/create-search.ts +++ b/client/src/util/tables/create-search.ts @@ -47,6 +47,7 @@ export function CreateDefaultPBSearchParams( percent: x => x.scoreData.percent, ranking: x => x.rankingData.rank, highlight: x => !!x.highlight, + username: x => x.__related.user?.username ?? null, lamp: { valueGetter: x => [x.scoreData.lamp, x.scoreData.lampIndex], strToNum: HumanFriendlyStrToLampIndex(game, playtype), diff --git a/client/src/util/tables/get-pb-leaders.ts b/client/src/util/tables/get-pb-leaders.ts new file mode 100644 index 000000000..098367fcc --- /dev/null +++ b/client/src/util/tables/get-pb-leaders.ts @@ -0,0 +1,24 @@ +import { Header } from "components/tables/components/TachiTable"; +import IndicatorHeader from "components/tables/headers/IndicatorHeader"; +import { PBDataset } from "types/tables"; +import { StrSOV } from "util/sorts"; + +type PBHeader = Header; + +export function GetPBLeadingHeaders( + showUser: boolean, + showChart: boolean, + chartHeader: PBHeader +): PBHeader[] { + const userHeader: PBHeader = ["User", "User", StrSOV(x => x.__related.user!.username)]; + const songHeader: PBHeader = ["Song", "Song", StrSOV(x => x.__related.song.title)]; + + if (showUser && showChart) { + return [userHeader, chartHeader, IndicatorHeader, songHeader]; + } + if (showUser && !showChart) { + return [IndicatorHeader, userHeader]; + } + + return [chartHeader, IndicatorHeader, songHeader]; +}