diff --git a/client/package.json b/client/package.json index 6b2c57a96..bdbfbf1bb 100644 --- a/client/package.json +++ b/client/package.json @@ -75,7 +75,7 @@ "resolve": "1.12.0", "resolve-url-loader": "3.1.0", "socicon": "3.0.5", - "tachi-common": "0.2.18" + "tachi-common": "0.2.25" }, "devDependencies": { "@types/humanize-duration": "3.25.1", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index a6e93cade..bbb4b0ef1 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -77,7 +77,7 @@ specifiers: serve: 11.2.0 socicon: 3.0.5 style-loader: 1.0.0 - tachi-common: 0.2.18 + tachi-common: 0.2.25 terser-webpack-plugin: 1.4.1 ts-pnp: 1.1.4 typescript: 4.3.5 @@ -142,7 +142,7 @@ dependencies: resolve: 1.12.0 resolve-url-loader: 3.1.0 socicon: 3.0.5 - tachi-common: 0.2.18 + tachi-common: 0.2.25 devDependencies: '@types/humanize-duration': 3.25.1 @@ -11178,8 +11178,8 @@ packages: strip-ansi: 6.0.0 dev: true - /tachi-common/0.2.18: - resolution: {integrity: sha512-moa+Bmo6ilwJIyxQfHNkxNOKxQCgXFWezdLGwwvhCAzAfpLtiT0B6/QXKJUrkEVOqAy1ujRsLHZdK+NR87rFvg==} + /tachi-common/0.2.25: + resolution: {integrity: sha512-FSMwEWWcvP5Vw2heSD6vD7Iz1/lI+bO1HF0+0iURw5Ybj16yJZQ9wBiaIz2hotNweMhlQ73v57tj2TMUmFyG9w==} dependencies: monk: 7.3.4 transitivePeerDependencies: diff --git a/client/src/app/App.tsx b/client/src/app/App.tsx index 127f6ef59..28a357376 100644 --- a/client/src/app/App.tsx +++ b/client/src/app/App.tsx @@ -10,7 +10,9 @@ import { LoadingScreen } from "components/layout/screens/LoadingScreen"; import { QueryClientProvider, QueryClient } from "react-query"; import { UserSettingsContextProvider } from "context/UserSettingsContext"; -const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5 } } }); +const queryClient = new QueryClient({ + defaultOptions: { queries: { staleTime: 1000 * 60 * 5, retry: 1 } }, +}); export default function App({ basename }: { basename: string }) { return ( diff --git a/client/src/app/pages/dashboard/games/_game/_playtype/GPTDevInfo.tsx b/client/src/app/pages/dashboard/games/_game/_playtype/GPTDevInfo.tsx new file mode 100644 index 000000000..8e751c914 --- /dev/null +++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTDevInfo.tsx @@ -0,0 +1,28 @@ +import useSetSubheader from "components/layout/header/useSetSubheader"; +import Card from "components/layout/page/Card"; +import DebugContent from "components/util/DebugContent"; +import React from "react"; +import { GetGameConfig, FormatGame, GetGamePTConfig } from "tachi-common"; +import { GamePT } from "types/react"; + +export default function GPTDevInfo({ game, playtype }: GamePT) { + useSetSubheader( + ["Games", GetGameConfig(game).name, playtype, "Dev Info"], + [game, playtype], + `${FormatGame(game, playtype)} Dev Info` + ); + + const gameConfig = GetGameConfig(game); + const gptConfig = GetGamePTConfig(game, playtype); + + return ( + <> + + + + + + + + ); +} diff --git a/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx b/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx new file mode 100644 index 000000000..223f8afb5 --- /dev/null +++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage.tsx @@ -0,0 +1,14 @@ +import useSetSubheader from "components/layout/header/useSetSubheader"; +import React from "react"; +import { FormatGame, GetGameConfig } from "tachi-common"; +import { GamePT } from "types/react"; + +export default function GPTLeaderboardsPage({ game, playtype }: GamePT) { + useSetSubheader( + ["Games", GetGameConfig(game).name, playtype, "Leaderboards"], + [game, playtype], + `${FormatGame(game, playtype)} Leaderboards` + ); + + return
leaderboard
; +} diff --git a/client/src/app/pages/dashboard/games/_game/_playtype/GPTMainPage.tsx b/client/src/app/pages/dashboard/games/_game/_playtype/GPTMainPage.tsx new file mode 100644 index 000000000..65cdf653b --- /dev/null +++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTMainPage.tsx @@ -0,0 +1,317 @@ +import { ResponsiveBar } from "@nivo/bar"; +import { BarChartTooltip } from "components/charts/ChartTooltip"; +import ClassBadge from "components/game/ClassBadge"; +import useSetSubheader from "components/layout/header/useSetSubheader"; +import Card from "components/layout/page/Card"; +import ScoreTable from "components/tables/scores/ScoreTable"; +import ApiError from "components/util/ApiError"; +import AsyncLoader from "components/util/AsyncLoader"; +import Divider from "components/util/Divider"; +import Icon from "components/util/Icon"; +import Loading from "components/util/Loading"; +import useApiQuery from "components/util/query/useApiQuery"; +import { TachiConfig } from "lib/config"; +import React, { useEffect, useState } from "react"; +import { Col, Form, Row } from "react-bootstrap"; +import { Link } from "react-router-dom"; +import { + ChartDocument, + ClassAchievementDocument, + FormatGame, + gameClasses, + GamePTConfig, + GetGameConfig, + GetGamePTConfig, + integer, + PublicUserDocument, + ScoreDocument, + SongDocument, +} from "tachi-common"; +import { RecentClassesReturn } from "types/api-returns"; +import { GamePT } from "types/react"; +import { ScoreDataset } from "types/tables"; +import { APIFetchV1 } from "util/api"; +import { DEFAULT_BAR_PROPS } from "util/charts"; +import { CreateChartMap, CreateSongMap, CreateUserMap } from "util/data"; +import { UppercaseFirst } from "util/misc"; +import { MillisToSince } from "util/time"; + +export default function GPTMainPage({ game, playtype }: GamePT) { + useSetSubheader( + ["Games", GetGameConfig(game).name, playtype], + [game, playtype], + FormatGame(game, playtype) + ); + + const gptConfig = GetGamePTConfig(game, playtype); + + return ( + <> + + + + {Object.keys(gptConfig.classHumanisedFormat).length !== 0 && ( + <> + + + + + + + + + + )} + + ); +} + +function RecentHighlightedScoresComponent({ game, playtype }: GamePT) { + const { data, isLoading, error } = useApiQuery<{ + scores: ScoreDocument[]; + users: PublicUserDocument[]; + charts: ChartDocument[]; + songs: SongDocument[]; + }>(`/games/${game}/${playtype}/scores/highlighted`); + + if (error) { + return ; + } + + if (!data || isLoading) { + return ; + } + + const dataset: ScoreDataset = []; + + const songMap = CreateSongMap(data.songs); + const chartMap = CreateChartMap(data.charts); + const userMap = CreateUserMap(data.users); + + for (const [index, score] of data.scores.entries()) { + dataset.push({ + ...score, + __related: { + chart: chartMap.get(score.chartID)!, + song: songMap.get(score.songID)!, + index, + user: userMap.get(score.userID)!, + }, + }); + } + + return ( + + + Here's what players have recently marked as highlighted scores! + + + + + + + + + ); +} + +function RecentClassesComponent({ game, playtype }: GamePT) { + const { data, isLoading, error } = useApiQuery( + `/games/${game}/${playtype}/recent-classes` + ); + + if (error) { + return ; + } + + if (!data || isLoading) { + return ; + } + + // if (data.classes.length === 0) { + // return
Looks like nobodies got anything here... yet!
; + // } + + const userMap = new Map(); + + for (const user of data.users) { + userMap.set(user.id, user); + } + + return ( +
+ {data.classes.length === 0 ? ( + Looks like nobodies ever got a class for this game. Ah well. + ) : ( + data.classes.map((c, i) => { + const user = userMap.get(c.userID)!; + + return ; + }) + )} +
+ ); +} + +function AchievedClassRow({ + classInfo, + user, +}: { + classInfo: ClassAchievementDocument; + user: PublicUserDocument; +}) { + const game = classInfo.game; + const playtype = classInfo.playtype; + + return ( +
+ + {user.username} + {" "} + achieved{" "} + + {classInfo.classOldValue && ( + <> + {" "} + (Raised from{" "} + + ) + + )} + + + + {MillisToSince(classInfo.timeAchieved)} + +
+ ); +} + +function ClassDistributionComponent({ game, playtype }: GamePT) { + const gptConfig = GetGamePTConfig(game, playtype); + + const [className, setClassName] = useState( + Object.keys(gptConfig.classHumanisedFormat)[0] as gameClasses.AllClassSets + ); + + useEffect(() => { + const gptConfig = GetGamePTConfig(game, playtype); + setClassName(Object.keys(gptConfig.classHumanisedFormat)[0] as gameClasses.AllClassSets); + }, [game, playtype]); + + return ( + + + This will show the distribution of classes among all the players on{" "} + {TachiConfig.name}! + + + + + + setClassName(e.target.value as gameClasses.AllClassSets)} + > + {Object.keys(gptConfig.classHumanisedFormat).map(e => ( + + ))} + + + + + + + { + const res = await APIFetchV1>( + `/games/${game}/${playtype}/class-distribution?class=${className}` + ); + + if (!res.success) { + throw new Error(res.description); + } + + return res.body; + }} + > + {d => } + + + + ); +} + +function ClassDistChart({ + data, + gptConfig, + className, +}: { + data: Record; + gptConfig: GamePTConfig; + className: gameClasses.AllClassSets; +}) { + const dataset = []; + + for (const key in gptConfig.classHumanisedFormat[className]) { + dataset.push({ + id: key, + count: data[key] ?? 0, + }); + } + + if (dataset.length === 0) { + return ( +
+
+ Looks like nobody has any of these classes yet. +
+
+ ); + } + + return ( +
+ "#cc527a"} + axisLeft={{ + format: v => gptConfig.classHumanisedFormat[className][v]?.display, + }} + tooltip={d => ( + ( +
+ { + gptConfig.classHumanisedFormat[className][ + d.indexValue as number + ].display + } + : {(d.data as any).count} +
+ )} + /> + )} + {...DEFAULT_BAR_PROPS} + /> +
+ ); +} diff --git a/client/src/app/pages/dashboard/games/_game/_playtype/GPTSongsPage.tsx b/client/src/app/pages/dashboard/games/_game/_playtype/GPTSongsPage.tsx new file mode 100644 index 000000000..13e4a7a56 --- /dev/null +++ b/client/src/app/pages/dashboard/games/_game/_playtype/GPTSongsPage.tsx @@ -0,0 +1,133 @@ +import useSetSubheader from "components/layout/header/useSetSubheader"; +import BMSDifficultyCell from "components/tables/cells/BMSDifficultyCell"; +import DifficultyCell from "components/tables/cells/DifficultyCell"; +import TitleCell from "components/tables/cells/TitleCell"; +import TachiTable from "components/tables/components/TachiTable"; +import ApiError from "components/util/ApiError"; +import DebounceSearch from "components/util/DebounceSearch"; +import DebugContent from "components/util/DebugContent"; +import Divider from "components/util/Divider"; +import Loading from "components/util/Loading"; +import useApiQuery from "components/util/query/useApiQuery"; +import React, { useState } from "react"; +import { Col, Row } from "react-bootstrap"; +import { + GetGameConfig, + FormatGame, + ChartDocument, + SongDocument, + integer, + GetGamePTConfig, +} from "tachi-common"; +import { GamePT } from "types/react"; +import { CreateSongMap } from "util/data"; +import { NumericSOV, StrSOV } from "util/sorts"; + +export default function GPTSongsPage({ game, playtype }: GamePT) { + useSetSubheader( + ["Games", GetGameConfig(game).name, playtype, "Songs"], + [game, playtype], + `${FormatGame(game, playtype)} Songs` + ); + + const [search, setSearch] = useState(""); + + return ( + + + + + + + + + + ); +} + +function SearchSongsTable({ game, playtype, search }: { search: string } & GamePT) { + const params = new URLSearchParams({ search }); + + const { data, isLoading, error } = useApiQuery<{ + charts: (ChartDocument & { __playcount: integer })[]; + songs: SongDocument[]; + }>(`/games/${game}/${playtype}/charts${search !== "" ? `?${params.toString()}` : ""}`); + + if (error) { + return ; + } + + if (!data || isLoading) { + return ; + } + + const songMap = CreateSongMap(data.songs); + + const dataset = []; + + for (const chart of data.charts) { + dataset.push({ + ...chart, + __related: { + song: songMap.get(chart.songID)!, + }, + }); + } + + const gptConfig = GetGamePTConfig(game, playtype); + + return ( + <> + {search === "" && ( +
+ Displaying the most played charts for {FormatGame(game, playtype)} +
+ )} + { + for (const tierlist of gptConfig.tierlists) { + if (chart.tierlistInfo[tierlist]) { + return chart.tierlistInfo[tierlist]!.value; + } + } + + return chart.levelNum; + }), + ], + ["Song Title", "Song", StrSOV(x => x.__related.song.title)], + ["Playcount", "Playcount", NumericSOV(x => x.__playcount)], + ]} + searchFunctions={{ + title: x => x.__related.song.title, + artist: x => x.__related.song.artist, + playcount: x => x.__playcount, + difficulty: x => x.difficulty, + level: x => x.levelNum, + }} + rowFunction={d => ( + + {game === "bms" ? ( + } + /> + ) : ( + + )} + + {d.__playcount} + + )} + /> + + ); +} 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 f674b603b..bc728a617 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 @@ -215,6 +215,7 @@ function RecentSessionScoreInfo({ chart, song: songMap.get(chart.songID)!, index: 0, + user: reqUser, }, }); } @@ -245,7 +246,6 @@ function RecentSessionScoreInfo({ dataset={dataset as any} pageLen={5} game={game} - reqUser={reqUser} playtype={playtype as any} /> 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 69bb61e32..ad3dad633 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 @@ -140,7 +140,7 @@ function AlgSelector({ ); } -function useFetchPBs(url: string) { +function useFetchPBs(url: string, reqUser: PublicUserDocument) { const { isLoading, error, data } = useQuery(url, async () => { const res = await APIFetchV1<{ pbs: PBScoreDocument<"iidx:SP">[]; @@ -152,7 +152,7 @@ function useFetchPBs(url: string) { throw res; } - return FormatData(res.body.pbs, res.body.songs, res.body.charts); + return FormatData(res.body.pbs, res.body.songs, res.body.charts, reqUser); }); return { isLoading, error: error as UnsuccessfulAPIResponse, data }; @@ -175,7 +175,7 @@ function PBsOverview({ } & GamePT) { const [search, setSearch] = useState(""); - const { isLoading, error, data } = useFetchPBs(url); + const { isLoading, error, data } = useFetchPBs(url, reqUser); return (
@@ -214,7 +214,7 @@ function FormatData< D extends PBScoreDocument | ScoreDocument, I extends IDStrings = IDStrings, G extends Game = Game ->(d: D[], songs: SongDocument[], charts: ChartDocument[]) { +>(d: D[], songs: SongDocument[], charts: ChartDocument[], reqUser: PublicUserDocument) { const songMap = new Map(); const chartMap = new Map(); @@ -232,13 +232,14 @@ function FormatData< song: songMap.get(e.songID), chart: chartMap.get(e.chartID), index: i, + user: reqUser, }, })); return data; } -function useFetchScores(url: string) { +function useFetchScores(url: string, reqUser: PublicUserDocument) { const { isLoading, error, data } = useQuery(url, async () => { const res = await APIFetchV1<{ scores: ScoreDocument[]; @@ -250,7 +251,7 @@ function useFetchScores(url: string) { throw res; } - return FormatData(res.body.scores, res.body.songs, res.body.charts); + return FormatData(res.body.scores, res.body.songs, res.body.charts, reqUser); }); return { isLoading, error: error as UnsuccessfulAPIResponse, data }; @@ -268,7 +269,8 @@ function PBsSearch({ alg?: ScoreCalculatedDataLookup[IDStrings]; } & GamePT) { const { isLoading, error, data } = useFetchPBs( - `/users/${reqUser.id}/games/${game}/${playtype}/pbs?search=${search}` + `/users/${reqUser.id}/games/${game}/${playtype}/pbs?search=${search}`, + reqUser ); return ( @@ -289,7 +291,8 @@ function ScoresOverview({ reqUser, game, playtype }: { reqUser: PublicUserDocume const [search, setSearch] = useState(""); const { isLoading, error, data } = useFetchScores( - `/users/${reqUser.id}/games/${game}/${playtype}/scores/recent` + `/users/${reqUser.id}/games/${game}/${playtype}/scores/recent`, + reqUser ); return ( @@ -307,12 +310,7 @@ function ScoresOverview({ reqUser, game, playtype }: { reqUser: PublicUserDocume style={{ height: 500 }} {...{ isLoading, dataset: data, error }} > - + ) : ( @@ -329,12 +327,13 @@ function ScoresSearch({ search, }: { reqUser: PublicUserDocument; search: string } & GamePT) { const { isLoading, error, data } = useFetchScores( - `/users/${reqUser.id}/games/${game}/${playtype}/scores?search=${search}` + `/users/${reqUser.id}/games/${game}/${playtype}/scores?search=${search}`, + reqUser ); return ( - + ); } diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderSelectPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderSelectPage.tsx index 854ab230a..8612ba9f2 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderSelectPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderSelectPage.tsx @@ -140,7 +140,7 @@ function FolderInfoComponent({ Lamps
- + {dataset} diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderTablePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderTablePage.tsx index bad35b54c..007b4d179 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderTablePage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderTablePage.tsx @@ -55,14 +55,14 @@ export default function FolderTablePage({ reqUser, game, playtype }: Props) { } }, [data]); - if (isLoading || !data) { - return ; - } - if (error) { return ; } + if (isLoading || !data) { + return ; + } + return ( <> @@ -82,7 +82,7 @@ export default function FolderTablePage({ reqUser, game, playtype }: Props) { ))} - + {table && } ); @@ -118,18 +118,18 @@ function TableFolderViewer({ reqUser, game, playtype, table }: Props & { table: } }, [data]); - if (isLoading || !data || !hasLoadedFolderMap) { - return ; - } - if (error) { return ; } + if (isLoading || !data || !hasLoadedFolderMap) { + return ; + } + return ( <> - +
- +
diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx index 049d72ac7..8376ac911 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx @@ -67,6 +67,7 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) { __related: { pb: pbMap.get(chart.chartID) ?? null, song: songMap.get(chart.songID)!, + user: reqUser, }, }); } @@ -92,14 +93,14 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) { ); }, [folderDataset]); - if (isLoading || !data || !folderDataset) { - return ; - } - if (error) { return ; } + if (isLoading || !data || !folderDataset) { + return ; + } + return (
@@ -107,7 +108,7 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
{folderInfoHeader}
- +
@@ -124,7 +125,7 @@ export default function SpecificFolderPage({ reqUser, game, playtype }: Props) {
- +
{mode === "normal" ? ( @@ -174,7 +175,7 @@ function TierlistBreakdown({ game, folderDataset, playtype, reqUser }: InfoProps Here you can select what tierlist blocks to show! - + {gptConfig.tierlists.length > 1 && gptConfig.tierlists.map(e => ( <> @@ -201,7 +202,7 @@ function TierlistBreakdown({ game, folderDataset, playtype, reqUser }: InfoProps - + : } + + + + {!user && ClientConfig.MANDATE_LOGIN ? : } diff --git a/client/src/app/routes/GameRoutes.tsx b/client/src/app/routes/GameRoutes.tsx index ca104f979..bf5447859 100644 --- a/client/src/app/routes/GameRoutes.tsx +++ b/client/src/app/routes/GameRoutes.tsx @@ -1,8 +1,14 @@ import PlaytypeSelect from "app/pages/dashboard/games/_game/PlaytypeSelect"; +import GPTSongsPage from "app/pages/dashboard/games/_game/_playtype/GPTSongsPage"; +import GPTDevInfo from "app/pages/dashboard/games/_game/_playtype/GPTDevInfo"; +import GPTLeaderboardsPage from "app/pages/dashboard/games/_game/_playtype/GPTLeaderboardsPage"; +import GPTMainPage from "app/pages/dashboard/games/_game/_playtype/GPTMainPage"; import { ErrorPage } from "app/pages/ErrorPage"; +import { GPTBottomNav, GPTHeaderBody } from "components/game/GPTHeader"; +import LayoutHeaderContainer from "components/layout/LayoutHeaderContainer"; import React from "react"; import { Redirect, Route, Switch, useParams } from "react-router-dom"; -import { Game, GetGameConfig } from "tachi-common"; +import { FormatGame, Game, GetGameConfig } from "tachi-common"; import { IsSupportedGame, IsSupportedPlaytype } from "util/asserts"; export default function GameRoutes() { @@ -29,7 +35,7 @@ export default function GameRoutes() { )} - + @@ -53,10 +59,34 @@ function GamePlaytypeRoutes({ game }: { game: Game }) { } return ( - - - {game} {playtype} - - + <> + } + header={FormatGame(game, playtype)} + > + + + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/client/src/app/routes/UserRoutes.tsx b/client/src/app/routes/UserRoutes.tsx index 7547176b9..bcea10762 100644 --- a/client/src/app/routes/UserRoutes.tsx +++ b/client/src/app/routes/UserRoutes.tsx @@ -9,9 +9,9 @@ import UserGamesPage from "app/pages/dashboard/users/UserGamesPage"; import UserSettingsPage from "app/pages/dashboard/users/UserSettingsPage"; import { ErrorPage } from "app/pages/ErrorPage"; import RequireAuthAsUserParam from "components/auth/RequireAuthAsUserParam"; +import LayoutHeaderContainer from "components/layout/LayoutHeaderContainer"; import { UGPTBottomNav, UGPTHeaderBody } from "components/user/UGPTHeader"; import { UserBottomNav, UserHeaderBody } from "components/user/UserHeader"; -import UserHeaderContainer from "components/user/UserHeaderContainer"; import Loading from "components/util/Loading"; import useApiQuery from "components/util/query/useApiQuery"; import { BackgroundContext } from "context/BackgroundContext"; @@ -52,10 +52,6 @@ export default function UserRoutes() { }; }, [reqUser]); - if (isLoading || !reqUser) { - return null; - } - if (error && error.statusCode === 404) { return ; } @@ -64,6 +60,10 @@ export default function UserRoutes() { return ; } + if (isLoading || !reqUser) { + return null; + } + return ( @@ -81,7 +81,7 @@ export default function UserRoutes() { function UserProfileRoutes({ reqUser }: { reqUser: PublicUserDocument }) { return ( <> - - + @@ -211,7 +211,7 @@ function UserGamePlaytypeRoutes({ reqUser, game }: { reqUser: PublicUserDocument return ( <> - - + diff --git a/client/src/components/game/GPTHeader.tsx b/client/src/components/game/GPTHeader.tsx new file mode 100644 index 000000000..27a40df28 --- /dev/null +++ b/client/src/components/game/GPTHeader.tsx @@ -0,0 +1,72 @@ +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); + + const navItems = [ + + Overview + , + + Songs + , + + Leaderboards + , + ]; + + if (settings?.preferences.developerMode) { + navItems.push( + + Developer Info + + ); + } + + return ( +
+ {navItems} +
+ ); +} diff --git a/client/src/components/user/UserHeaderContainer.tsx b/client/src/components/layout/LayoutHeaderContainer.tsx similarity index 100% rename from client/src/components/user/UserHeaderContainer.tsx rename to client/src/components/layout/LayoutHeaderContainer.tsx diff --git a/client/src/components/tables/cells/UserCell.tsx b/client/src/components/tables/cells/UserCell.tsx new file mode 100644 index 000000000..9a362f3ff --- /dev/null +++ b/client/src/components/tables/cells/UserCell.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { PublicUserDocument } from "tachi-common"; +import { GamePT } from "types/react"; + +export default function UserCell({ user, game, playtype }: { user: PublicUserDocument } & GamePT) { + return ( + + + {user.username} + + + ); +} diff --git a/client/src/components/tables/dropdowns/GenericScoreDropdown.tsx b/client/src/components/tables/dropdowns/GenericScoreDropdown.tsx index 74448dfd4..67d31a6c4 100644 --- a/client/src/components/tables/dropdowns/GenericScoreDropdown.tsx +++ b/client/src/components/tables/dropdowns/GenericScoreDropdown.tsx @@ -36,14 +36,14 @@ export interface ScoreDropdownProps { export default function GenericScoreDropdown({ game, playtype, - reqUser, + user, chart, scoreState, thisScore, defaultView = "moreInfo", DocComponent = props => GenericScoreContentDropdown({ renderScoreInfo: false, ...props }), }: { - reqUser: PublicUserDocument; + user: PublicUserDocument; chart: ChartDocument; scoreState: ScoreState; defaultView?: "vsPB" | "moreInfo" | "history" | "debug"; @@ -57,12 +57,12 @@ export default function GenericScoreDropdown({ const [view, setView] = useState(defaultView); const { isLoading, error, data } = useApiQuery>( - `/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true` + `/users/${user.id}/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/${user.id}/games/${game}/${playtype}/scores/${chart.chartID}`); if (error) { return <>An error has occured. Whoops.; diff --git a/client/src/components/tables/dropdowns/IIDXScoreDropdown.tsx b/client/src/components/tables/dropdowns/IIDXScoreDropdown.tsx index 5e757c766..5aa7c9fb4 100644 --- a/client/src/components/tables/dropdowns/IIDXScoreDropdown.tsx +++ b/client/src/components/tables/dropdowns/IIDXScoreDropdown.tsx @@ -12,12 +12,12 @@ export default function IIDXScoreDropdown({ thisScore, game, playtype, - reqUser, + user, chart, scoreState, }: { thisScore: ScoreDocument<"iidx:SP" | "iidx:DP">; - reqUser: PublicUserDocument; + user: PublicUserDocument; chart: ChartDocument<"iidx:SP" | "iidx:DP">; scoreState: { highlight: boolean; @@ -27,7 +27,7 @@ export default function IIDXScoreDropdown({ }; } & GamePT) { const { isLoading, error, data } = useApiQuery>( - `/users/${reqUser.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true` + `/users/${user.id}/games/${game}/${playtype}/pbs/${chart.chartID}?getComposition=true` ); if (error) { @@ -44,7 +44,7 @@ export default function IIDXScoreDropdown({ return ( - {...{ game, playtype, chart, reqUser, scoreState, thisScore }} + {...{ game, playtype, chart, reqUser: user, scoreState, thisScore, user }} DocComponent={props => GenericScoreContentDropdown({ ...props, diff --git a/client/src/components/tables/dropdowns/components/PlayHistory.tsx b/client/src/components/tables/dropdowns/components/PlayHistory.tsx index e51bbf8f4..0a994b30a 100644 --- a/client/src/components/tables/dropdowns/components/PlayHistory.tsx +++ b/client/src/components/tables/dropdowns/components/PlayHistory.tsx @@ -16,14 +16,14 @@ export default function PlayHistory({ isLoading: boolean; error: UnsuccessfulAPIFetchResponse | null; } & GamePT) { - if (isLoading || !data) { - return ; - } - if (error) { return <>{error.description}; } + if (isLoading || !data) { + return ; + } + return (
diff --git a/client/src/components/tables/history-scores/HistoryScoreTable.tsx b/client/src/components/tables/history-scores/HistoryScoreTable.tsx index dde2351a1..60ad474c5 100644 --- a/client/src/components/tables/history-scores/HistoryScoreTable.tsx +++ b/client/src/components/tables/history-scores/HistoryScoreTable.tsx @@ -77,7 +77,7 @@ function Row({ game: Game; }) { return ( - + diff --git a/client/src/components/tables/pbs/BMSPBTable.tsx b/client/src/components/tables/pbs/BMSPBTable.tsx index 813c7e28f..a70c5676a 100644 --- a/client/src/components/tables/pbs/BMSPBTable.tsx +++ b/client/src/components/tables/pbs/BMSPBTable.tsx @@ -143,7 +143,6 @@ function Row({ scoreState={scoreState} /> } - className={scoreState.highlight ? "highlighted-row" : ""} > {indexCol && } diff --git a/client/src/components/tables/pbs/GenericPBTable.tsx b/client/src/components/tables/pbs/GenericPBTable.tsx index a9cae2516..ec4f9bb8b 100644 --- a/client/src/components/tables/pbs/GenericPBTable.tsx +++ b/client/src/components/tables/pbs/GenericPBTable.tsx @@ -109,7 +109,7 @@ function Row({ scoreState={scoreState} /> } - className={scoreState.highlight ? "highlighted-row" : ""} + > {indexCol && } diff --git a/client/src/components/tables/pbs/MusecaPBTable.tsx b/client/src/components/tables/pbs/MusecaPBTable.tsx index 0e7fb6af9..f7683ecd6 100644 --- a/client/src/components/tables/pbs/MusecaPBTable.tsx +++ b/client/src/components/tables/pbs/MusecaPBTable.tsx @@ -97,7 +97,6 @@ function Row({ scoreState={scoreState} /> } - className={scoreState.highlight ? "highlighted-row" : ""} > {indexCol && } diff --git a/client/src/components/tables/pbs/SDVXPBTable.tsx b/client/src/components/tables/pbs/SDVXPBTable.tsx index aad0faa92..64b2f7ac0 100644 --- a/client/src/components/tables/pbs/SDVXPBTable.tsx +++ b/client/src/components/tables/pbs/SDVXPBTable.tsx @@ -96,7 +96,7 @@ function Row({ scoreState={scoreState} /> } - className={scoreState.highlight ? "highlighted-row" : ""} + > {indexCol && } diff --git a/client/src/components/tables/scores/BMSScoreTable.tsx b/client/src/components/tables/scores/BMSScoreTable.tsx index eae13e668..a9edfe03e 100644 --- a/client/src/components/tables/scores/BMSScoreTable.tsx +++ b/client/src/components/tables/scores/BMSScoreTable.tsx @@ -8,6 +8,7 @@ import BMSDifficultyCell from "../cells/BMSDifficultyCell"; import IndicatorsCell from "../cells/IndicatorsCell"; import TimestampCell from "../cells/TimestampCell"; import TitleCell from "../cells/TitleCell"; +import UserCell from "../cells/UserCell"; import DropdownRow from "../components/DropdownRow"; import TachiTable, { Header } from "../components/TachiTable"; import { useScoreState } from "../components/UseScoreState"; @@ -16,46 +17,50 @@ import BMSScoreCoreCells from "../game-core-cells/BMSScoreCoreCells"; import IndicatorHeader from "../headers/IndicatorHeader"; export default function BMSScoreTable({ - reqUser, dataset, pageLen, playtype, + userCol = false, }: { - reqUser: PublicUserDocument; dataset: ScoreDataset<"bms:7K" | "bms:14K">; pageLen?: integer; playtype: Playtypes["bms"]; + userCol?: boolean; }) { + const headers: Header[0]>[] = [ + [ + "Chart", + "Chart", + NumericSOV( + x => + x.__related.chart.tierlistInfo["sgl-EC"]?.value ?? + x.__related.chart.tierlistInfo["sgl-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)], + ["Sieglinde", "sgl.", NumericSOV(x => x.calculatedData.sieglinde ?? 0)], + + ["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)], + ]; + + if (userCol) { + headers.unshift(["User", "User", StrSOV(x => x.__related.user.username)]); + } + return ( - x.__related.chart.tierlistInfo["sgl-EC"]?.value ?? - x.__related.chart.tierlistInfo["sgl-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)], - ["Sieglinde", "sgl.", NumericSOV(x => x.calculatedData.sieglinde ?? 0)], - - ["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)], - ] as Header[0]>[] - } + headers={headers} entryName="Scores" searchFunctions={CreateDefaultScoreSearchParams("bms", playtype)} rowFunction={sc => ( - + )} /> ); @@ -63,22 +68,21 @@ export default function BMSScoreTable({ function Row({ sc, - reqUser, playtype, + userCol, }: { sc: ScoreDataset<"bms:7K" | "bms:14K">[0]; - reqUser: PublicUserDocument; playtype: Playtype; + userCol: boolean; }) { const scoreState = useScoreState(sc); return ( } > + {userCol && } [] = [ + ["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)], + IndicatorHeader, + ["Song", "Song", StrSOV(x => x.__related.song.title)], + ["Score", "Score", NumericSOV(x => x.scoreData.percent)], + ["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)], + [ + gptConfig.defaultScoreRatingAlg, + gptConfig.defaultScoreRatingAlg, + NumericSOV(x => x.calculatedData[gptConfig.defaultScoreRatingAlg] ?? 0), + ], + ["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)], + ]; + + if (userCol) { + headers.unshift(["User", "User", StrSOV(x => x.__related.user.username)]); + } + return ( x.__related.chart.levelNum)], - IndicatorHeader, - ["Song", "Song", StrSOV(x => x.__related.song.title)], - ["Score", "Score", NumericSOV(x => x.scoreData.percent)], - ["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)], - [ - gptConfig.defaultScoreRatingAlg, - gptConfig.defaultScoreRatingAlg, - NumericSOV(x => x.calculatedData[gptConfig.defaultScoreRatingAlg] ?? 0), - ], - ["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)], - ]} + headers={headers} entryName="Scores" searchFunctions={CreateDefaultScoreSearchParams(game, playtype)} rowFunction={sc => ( - + )} /> ); @@ -61,20 +76,22 @@ function Row({ sc, reqUser, showScore, + userCol, }: { sc: ScoreDataset[0]; reqUser: PublicUserDocument; showScore?: boolean; + userCol: boolean; }) { const scoreState = useScoreState(sc); return ( } > + {userCol && } diff --git a/client/src/components/tables/scores/IIDXScoreTable.tsx b/client/src/components/tables/scores/IIDXScoreTable.tsx index e36aee557..31da73b23 100644 --- a/client/src/components/tables/scores/IIDXScoreTable.tsx +++ b/client/src/components/tables/scores/IIDXScoreTable.tsx @@ -10,6 +10,7 @@ import DifficultyCell from "../cells/DifficultyCell"; import IndicatorsCell from "../cells/IndicatorsCell"; 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"; @@ -19,60 +20,64 @@ import IIDXScoreCoreCells from "../game-core-cells/IIDXScoreCoreCells"; import IndicatorHeader from "../headers/IndicatorHeader"; export default function IIDXScoreTable({ - reqUser, dataset, pageLen, playtype, + userCol = false, }: { - reqUser: PublicUserDocument; dataset: ScoreDataset<"iidx:SP" | "iidx:DP">; pageLen?: integer; playtype: Playtypes["iidx"]; + userCol?: boolean; }) { const defaultRating = useScoreRatingAlg<"iidx:SP" | "iidx:DP">("iidx", playtype); const [rating, setRating] = useState( defaultRating ); + const headers: Header[0]>[] = [ + [ + "Chart", + "Chart", + NumericSOV( + x => + x.__related.chart.tierlistInfo["kt-NC"]?.value ?? + 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)], + [ + "Rating", + "Rating", + NumericSOV(x => x.calculatedData[rating] ?? 0), + (thProps: ZTableTHProps) => ( + + key={nanoid()} + game="iidx" + playtype={playtype} + rating={rating} + setRating={setRating} + {...thProps} + /> + ), + ], + ["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)], + ]; + + if (userCol) { + headers.unshift(["User", "User", StrSOV(x => x.__related.user.username)]); + } + return ( - x.__related.chart.tierlistInfo["kt-NC"]?.value ?? - 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)], - [ - "Rating", - "Rating", - NumericSOV(x => x.calculatedData[rating] ?? 0), - (thProps: ZTableTHProps) => ( - - key={nanoid()} - game="iidx" - playtype={playtype} - rating={rating} - setRating={setRating} - {...thProps} - /> - ), - ], - ["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)], - ] as Header[0]>[] - } + headers={headers} entryName="Scores" searchFunctions={CreateDefaultScoreSearchParams("iidx", playtype)} rowFunction={sc => ( @@ -80,8 +85,8 @@ export default function IIDXScoreTable({ key={sc.scoreID} playtype={playtype} sc={sc} - reqUser={reqUser} rating={rating} + userCol={userCol} /> )} /> @@ -90,31 +95,32 @@ export default function IIDXScoreTable({ function Row({ sc, - reqUser, rating, playtype, + userCol, }: { sc: ScoreDataset<"iidx:SP" | "iidx:DP">[0]; - reqUser: PublicUserDocument; rating: ScoreCalculatedDataLookup["iidx:SP" | "iidx:DP"]; playtype: Playtype; + userCol: boolean; }) { const scoreState = useScoreState(sc); return ( } > + {userCol && } ; pageLen?: integer; playtype: Playtypes["museca"]; + userCol: boolean; }) { + const headers: Header[0]>[] = [ + ["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)], + IndicatorHeader, + ["Song", "Song", StrSOV(x => x.__related.song.title)], + ["Score", "Score", NumericSOV(x => x.scoreData.percent)], + ["Near-Miss", "Nr. Ms.", NumericSOV(x => x.scoreData.percent)], + ["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)], + ["KtRating", "KtRating", NumericSOV(x => x.calculatedData.ktRating ?? 0)], + + ["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)], + ]; + + if (userCol) { + headers.unshift(["User", "User", StrSOV(x => x.__related.user.username)]); + } + return ( x.__related.chart.levelNum)], - IndicatorHeader, - ["Song", "Song", StrSOV(x => x.__related.song.title)], - ["Score", "Score", NumericSOV(x => x.scoreData.percent)], - ["Near-Miss", "Nr. Ms.", NumericSOV(x => x.scoreData.percent)], - ["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)], - ["KtRating", "KtRating", NumericSOV(x => x.calculatedData.ktRating ?? 0)], - - ["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)], - ] as Header[0]>[] - } + headers={headers} entryName="Scores" searchFunctions={CreateDefaultScoreSearchParams("museca", "Single")} - rowFunction={sc => } + rowFunction={sc => } /> ); } @@ -51,19 +58,21 @@ export default function MusecaScoreTable({ function Row({ sc, reqUser, + userCol, }: { sc: ScoreDataset<"museca:Single">[0]; reqUser: PublicUserDocument; + userCol: boolean; }) { const scoreState = useScoreState(sc); return ( } > + {userCol && } ; pageLen?: integer; playtype: Playtypes["sdvx"]; + userCol?: boolean; }) { + const headers: Header[0]>[] = [ + ["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)], + IndicatorHeader, + ["Song", "Song", StrSOV(x => x.__related.song.title)], + ["Score", "Score", NumericSOV(x => x.scoreData.percent)], + ["Near - Miss", "Nr. Ms.", NumericSOV(x => x.scoreData.percent)], + ["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)], + ["VF6", "VF6", NumericSOV(x => x.calculatedData.VF6 ?? 0)], + + ["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)], + ]; + + if (userCol) { + headers.unshift(["User", "User", StrSOV(x => x.__related.user.username)]); + } + return ( x.__related.chart.levelNum)], - IndicatorHeader, - ["Song", "Song", StrSOV(x => x.__related.song.title)], - ["Score", "Score", NumericSOV(x => x.scoreData.percent)], - ["Near - Miss", "Nr. Ms.", NumericSOV(x => x.scoreData.percent)], - ["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)], - ["VF6", "VF6", NumericSOV(x => x.calculatedData.VF6 ?? 0)], - - ["Timestamp", "Timestamp", NumericSOV(x => x.timeAchieved ?? 0)], - ] as Header[0]>[] - } + headers={headers} entryName="Scores" searchFunctions={CreateDefaultScoreSearchParams("museca", "Single")} - rowFunction={sc => } + rowFunction={sc => } /> ); } -function Row({ sc, reqUser }: { sc: ScoreDataset<"sdvx:Single">[0]; reqUser: PublicUserDocument }) { +function Row({ + sc, + reqUser, + userCol, +}: { + sc: ScoreDataset<"sdvx:Single">[0]; + reqUser: PublicUserDocument; + userCol: boolean; +}) { const scoreState = useScoreState(sc); return ( [0]; reqUser: Pub /> } > + {userCol && } ; } else if (game === "bms") { diff --git a/client/src/components/user/UGPTStatContainer.tsx b/client/src/components/user/UGPTStatContainer.tsx index c492a26af..4f2b57584 100644 --- a/client/src/components/user/UGPTStatContainer.tsx +++ b/client/src/components/user/UGPTStatContainer.tsx @@ -62,14 +62,14 @@ export default function UGPTStatContainer({ } ); - if (isLoading || !data) { - return ; - } - if (error) { return <>{(error as any).description}; } + if (isLoading || !data) { + return ; + } + return (
; } diff --git a/client/src/components/util/LoadingWrapper.tsx b/client/src/components/util/LoadingWrapper.tsx index 585c7194e..95e033795 100644 --- a/client/src/components/util/LoadingWrapper.tsx +++ b/client/src/components/util/LoadingWrapper.tsx @@ -15,13 +15,13 @@ export default function LoadingWrapper({ children: JSX.Element | JSX.Element[]; style?: CSSProperties; }) { - if (isLoading || !dataset) { - return ; - } - if (error) { return

An error has occured. {error.description}

; } + if (isLoading || !dataset) { + return ; + } + return <>{children}; } diff --git a/client/src/components/util/table/useZTable.tsx b/client/src/components/util/table/useZTable.tsx index 1185c7f5e..e42531922 100644 --- a/client/src/components/util/table/useZTable.tsx +++ b/client/src/components/util/table/useZTable.tsx @@ -58,6 +58,7 @@ export function useZTable(originalDataset: D[], providedOptions?: Partial { charts: ChartDocument[]; pbs: PBScoreDocument[]; } + +export interface GPTStatsReturn { + config: GamePTConfig; + playerCount: integer; + chartCount: integer; + scoreCount: integer; +} + +export interface RecentClassesReturn { + classes: ClassAchievementDocument[]; + users: PublicUserDocument[]; +} diff --git a/client/src/types/tables.ts b/client/src/types/tables.ts index 76ce255c6..32d5e4c64 100644 --- a/client/src/types/tables.ts +++ b/client/src/types/tables.ts @@ -6,6 +6,7 @@ import { IDStringToGame, integer, ScoreDocument, + PublicUserDocument, } from "tachi-common"; export type PBDataset = (PBScoreDocument & { @@ -14,12 +15,18 @@ export type PBDataset = (PBScoreDocument & { })[]; export type ScoreDataset = (ScoreDocument & { - __related: { chart: ChartDocument; song: SongDocument; index: integer }; + __related: { + chart: ChartDocument; + song: SongDocument; + index: integer; + user: PublicUserDocument; + }; })[]; export type FolderDataset = (ChartDocument & { __related: { pb: PBScoreDocument | null; song: SongDocument; + user: PublicUserDocument; }; })[]; diff --git a/client/src/util/data.ts b/client/src/util/data.ts index e84009a01..e5cf5e742 100644 --- a/client/src/util/data.ts +++ b/client/src/util/data.ts @@ -6,6 +6,7 @@ import { SessionScoreInfo, SongDocument, ScoreDocument, + PublicUserDocument, } from "tachi-common"; export function GetPBs(scoreInfo: SessionScoreInfo[]) { @@ -22,6 +23,16 @@ export function CreateSongMap(songs: SongDocument[]) { return songMap; } +export function CreateUserMap(users: PublicUserDocument[]) { + const userMap = new Map(); + + for (const user of users) { + userMap.set(user.id, user); + } + + return userMap; +} + export function CreateChartIDMap(arr: T[]): Map { const map = new Map(); diff --git a/client/src/util/splashes.ts b/client/src/util/splashes.ts index 8dd299243..2ec171820 100644 --- a/client/src/util/splashes.ts +++ b/client/src/util/splashes.ts @@ -1,3 +1,5 @@ +import { IDStrings } from "tachi-common"; + export const neutralSplashes = ["A Rhythm Game Score Manager", "The Rhythm Game Score Manager"]; export const loggedInSplashes = [ @@ -67,6 +69,8 @@ export const loggedInSplashes = [ "~𝒕𝒉𝒆 𝒈𝒆𝒂𝒓𝒔 𝒕𝒖𝒓𝒏 𝒐𝒏𝒄𝒆 𝒎𝒐𝒓𝒆~", "", "come on down", + "The preparation for a dive is always a tense time.", + "Music Is Math", ]; // Stuff that's bad for the kids, you know?