From a5cc53e3379c370d62fda0908d01fd2b54e4a4a0 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Sat, 27 Nov 2021 20:28:15 +0000 Subject: [PATCH] Add Session Page --- .../games/_game/_playtype/SessionsPage.tsx | 13 +- .../_game/_playtype/SpecificSessionPage.tsx | 298 +++++++++++ client/src/app/routes/UserRoutes.tsx | 4 + client/src/components/imports/ImportInfo.tsx | 2 +- .../components/sessions/SessionOverview.tsx | 165 ++++++ .../sessions/SessionRaiseBreakdown.tsx | 476 +++++++++++++++++ .../dropdowns/GenericSessionDropdown.tsx | 478 +----------------- .../tables/sessions/GenericSessionTable.tsx | 29 +- client/src/components/util/Icon.tsx | 5 +- client/src/types/api-returns.ts | 8 - client/src/util/splashes.ts | 1 + 11 files changed, 987 insertions(+), 492 deletions(-) create mode 100644 client/src/app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage.tsx create mode 100644 client/src/components/sessions/SessionOverview.tsx create mode 100644 client/src/components/sessions/SessionRaiseBreakdown.tsx diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/SessionsPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/SessionsPage.tsx index 215ac17c9..f43bb9e1d 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/SessionsPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/SessionsPage.tsx @@ -97,12 +97,13 @@ export default function SessionsPage({ ) : ( - + )} @@ -113,8 +114,9 @@ function SearchSessionsTable({ search, game, playtype, + reqUser, baseUrl, -}: { search: string; baseUrl: string } & GamePT) { +}: { search: string; baseUrl: string; reqUser: PublicUserDocument } & GamePT) { const { isLoading, error, data } = useQuery( `${baseUrl}?search=${search}`, async () => { @@ -135,7 +137,12 @@ function SearchSessionsTable({ return ( - + ); } diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage.tsx new file mode 100644 index 000000000..9d8c788a5 --- /dev/null +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage.tsx @@ -0,0 +1,298 @@ +import useSetSubheader from "components/layout/header/useSetSubheader"; +import SessionOverview from "components/sessions/SessionOverview"; +import SessionRaiseBreakdown from "components/sessions/SessionRaiseBreakdown"; +import ScoreTable from "components/tables/scores/ScoreTable"; +import ApiError from "components/util/ApiError"; +import Divider from "components/util/Divider"; +import FormInput from "components/util/FormInput"; +import Icon from "components/util/Icon"; +import Loading from "components/util/Loading"; +import useApiQuery from "components/util/query/useApiQuery"; +import SelectButton from "components/util/SelectButton"; +import { UserContext } from "context/UserContext"; +import React, { useContext, useMemo, useState } from "react"; +import { Badge, Button, Col, Form, Modal, Row } from "react-bootstrap"; +import { Redirect, useParams } from "react-router-dom"; +import { GetGameConfig, PublicUserDocument, SessionDocument } from "tachi-common"; +import { SessionReturns } from "types/api-returns"; +import { GamePT, SetState } from "types/react"; +import { APIFetchV1 } from "util/api"; +import { CreateChartMap, CreateScoreIDMap, CreateSongMap } from "util/data"; +import { DelayedPageReload } from "util/misc"; + +type Props = { reqUser: PublicUserDocument } & GamePT; + +export default function SpecificSessionPage({ reqUser, game, playtype }: Props) { + const { sessionID } = useParams<{ sessionID: string }>(); + + const { data, isLoading, error } = useApiQuery(`/sessions/${sessionID}`); + + if (error) { + return ; + } + + if (isLoading || !data) { + return ; + } + + if ( + data.user.id !== reqUser.id || + game !== data.session.game || + playtype !== data.session.playtype + ) { + return ( + + ); + } + + return ; +} + +function SessionPage({ data, game, playtype }: Props & { data: SessionReturns }) { + const { session, user, charts, scores, songs } = data; + + const { user: loggedInUser } = useContext(UserContext); + + useSetSubheader( + [ + "Users", + user.username, + "Games", + GetGameConfig(game).name, + playtype, + "Sessions", + session.name, + ], + [session.name, game, playtype, user], + `${user.username}: ${session.name}` + ); + + const [view, setView] = useState<"raises" | "overview" | "scores">("overview"); + + const songMap = CreateSongMap(songs); + const chartMap = CreateChartMap(charts); + const scoreMap = CreateScoreIDMap(scores); + + const scoreDataset = useMemo(() => { + const d = []; + + for (const scoreInfo of session.scoreInfo) { + const score = scoreMap.get(scoreInfo.scoreID); + + if (!score) { + console.error(`No score for scoreID ${scoreInfo.scoreID}, but one was in session?`); + continue; + } + + const chart = chartMap.get(score.chartID); + const song = songMap.get(score.songID); + + if (!chart || !song) { + console.error(`No chart for ${score.chartID} (${score.songID})?`); + continue; + } + + d.push({ + ...score, + __related: { + chart, + song, + index: 0, + user, + }, + }); + } + + return d; + }, [data]); + + const [highlight, setHighlight] = useState(session.highlight); + const [showEditModal, setShowEditModal] = useState(false); + + return ( + + +

{session.name}

+

{session.desc}

+ + + Views:{" "} + {session.views} + + + {session.highlight && ( + + Highlight! + + )} + + + {user.id === loggedInUser?.id && ( + <> + + {highlight ? ( + + ) : ( + + )} + + + )} +
+ + + Raises + + + + Overview + + + + All Scores + +
+ + + {view === "raises" ? ( + + ) : view === "overview" ? ( + + ) : ( + + + + )} + {showEditModal && ( + + )} +
+ ); +} + +function EditModal({ + session, + show, + setShow, +}: { + session: SessionDocument; + show: boolean; + setShow: SetState; +}) { + const [name, setName] = useState(session.name); + const [desc, setDesc] = useState(session.desc ?? "No Description..."); + + return ( + setShow(false)}> + + Edit Comment + + +
) => { + e.preventDefault(); + + const r = await APIFetchV1( + `/sessions/${session.sessionID}`, + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + desc, + }), + }, + true, + true + ); + + if (r.success) { + DelayedPageReload(); + } + }} + > + + + } + /> + + + +
+
+ ); +} diff --git a/client/src/app/routes/UserRoutes.tsx b/client/src/app/routes/UserRoutes.tsx index 14a6ef43b..c868d87d7 100644 --- a/client/src/app/routes/UserRoutes.tsx +++ b/client/src/app/routes/UserRoutes.tsx @@ -4,6 +4,7 @@ import FoldersMainPage from "app/pages/dashboard/users/games/_game/_playtype/fol import LeaderboardsPage from "app/pages/dashboard/users/games/_game/_playtype/LeaderboardsPage"; import OverviewPage from "app/pages/dashboard/users/games/_game/_playtype/OverviewPage"; import SessionsPage from "app/pages/dashboard/users/games/_game/_playtype/SessionsPage"; +import SpecificSessionPage from "app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage"; import UGPTSettingsPage from "app/pages/dashboard/users/games/_game/_playtype/UGPTSettingsPage"; import UserGamesPage from "app/pages/dashboard/users/UserGamesPage"; import UserIntegrationsPage from "app/pages/dashboard/users/UserIntegrationsPage"; @@ -247,6 +248,9 @@ function UserGamePlaytypeRoutes({ reqUser, game }: { reqUser: PublicUserDocument + + + diff --git a/client/src/components/imports/ImportInfo.tsx b/client/src/components/imports/ImportInfo.tsx index 50eed9e6f..d46acc7b2 100644 --- a/client/src/components/imports/ImportInfo.tsx +++ b/client/src/components/imports/ImportInfo.tsx @@ -179,7 +179,7 @@ function SessionTab({ data }: { data: Data }) { {r.session.name} diff --git a/client/src/components/sessions/SessionOverview.tsx b/client/src/components/sessions/SessionOverview.tsx new file mode 100644 index 000000000..e3f172a81 --- /dev/null +++ b/client/src/components/sessions/SessionOverview.tsx @@ -0,0 +1,165 @@ +import { ResponsiveLine } from "@nivo/line"; +import ChartTooltip from "components/charts/ChartTooltip"; +import Card from "components/layout/page/Card"; +import ScoreTable from "components/tables/scores/ScoreTable"; +import DebugContent from "components/util/DebugContent"; +import Divider from "components/util/Divider"; +import { UserContext } from "context/UserContext"; +import { ColourConfig } from "lib/config"; +import React, { useContext, useMemo } from "react"; +import { Col } from "react-bootstrap"; +import { ChartDocument, GetGamePTConfig, ScoreDocument, SongDocument } from "tachi-common"; +import { SessionReturns } from "types/api-returns"; +import { GamePT } from "types/react"; +import { ScoreDataset } from "types/tables"; +import { TACHI_CHART_THEME } from "util/constants/chart-theme"; +import { UppercaseFirst } from "util/misc"; +import { NumericSOV } from "util/sorts"; +import { FormatDuration } from "util/time"; + +export default function SessionOverview({ + sessionData, + scoreDataset, +}: { + sessionData: SessionReturns; + scoreDataset: ScoreDataset; +}) { + const { scores, session } = sessionData; + const { user } = useContext(UserContext); + + // const performanceDataset = useMemo(() => { + // const d = []; + + // for (const scoreInfo of session.scoreInfo) { + // const score = scoreMap.get(scoreInfo.scoreID); + + // if (!score) { + // console.error(`Score ${scoreInfo.scoreID} doesn't exist?`); + // continue; + // } + + // const chart = chartMap.get(score.chartID); + // const song = songMap.get(score.songID); + + // if (!chart || !song) { + // console.error(`Chart ${score.chartID} (${score.songID}) has no chart?`); + // continue; + // } + + // d.push({ + // ...score, + // __related: { + // song, + // chart, + // }, + // }); + // } + + // return d; + // }, [sessionData]); + + return ( + <> + + + e.highlight).length} /> + + + + e.highlight)} + /> + + + + ); +} + +function StatThing({ + value, + name, + md12, +}: { + value: string | number; + name: string; + md12?: boolean; +}) { + return ( + +
+
+
{value}
+
{name}
+
+
+ + ); +} + +type PerformanceData = ScoreDocument & { + __related: { + chart: ChartDocument; + song: SongDocument; + }; +}; + +function PerformanceComponent({ + dataset, + timeStart, + game, + playtype, +}: { + dataset: PerformanceData[]; + timeStart: number; +} & GamePT) { + const gptConfig = GetGamePTConfig(game, playtype); + + const data = useMemo(() => { + const data = []; + + for (const alg of gptConfig.scoreRatingAlgs) { + data.push({ + id: UppercaseFirst(alg), + data: dataset + .map(e => ({ + x: e.timeAchieved!, + y: (e.calculatedData[alg] ?? 0) < 0 ? null : e.calculatedData[alg] ?? 0, + })) + .sort(NumericSOV(e => e.x, true)), + }); + } + return data; + }, [dataset]); + + return ( + +
+ FormatDuration(timeStart - x.valueOf()), + }} + gridXValues={3} + motionConfig="stiff" + crosshairType="x" + yScale={{ type: "linear", min: "auto", max: "auto" }} + colors={[ColourConfig.primary]} + useMesh={true} + theme={TACHI_CHART_THEME} + tooltip={d => ( + } /> + )} + // legends={[]} + enableArea + /> +
+
+ ); +} diff --git a/client/src/components/sessions/SessionRaiseBreakdown.tsx b/client/src/components/sessions/SessionRaiseBreakdown.tsx new file mode 100644 index 000000000..ed36c3e65 --- /dev/null +++ b/client/src/components/sessions/SessionRaiseBreakdown.tsx @@ -0,0 +1,476 @@ +import DifficultyCell from "components/tables/cells/DifficultyCell"; +import LampCell from "components/tables/cells/LampCell"; +import ScoreCell from "components/tables/cells/ScoreCell"; +import TitleCell from "components/tables/cells/TitleCell"; +import MiniTable from "components/tables/components/MiniTable"; +import Loading from "components/util/Loading"; +import SelectButton from "components/util/SelectButton"; +import deepmerge from "deepmerge"; +import { nanoid } from "nanoid"; +import React, { useState, useEffect, useMemo } from "react"; +import { useQuery } from "react-query"; +import Divider from "components/util/Divider"; +import Icon from "components/util/Icon"; +import { + FolderDocument, + TableDocument, + Lamps, + IDStrings, + ScoreDocument, + SessionScoreInfo, + Grades, + GetGamePTConfig, + GamePTConfig, + integer, + SongDocument, + Game, + ChartDocument, +} from "tachi-common"; +import { SessionReturns } from "types/api-returns"; +import { APIFetchV1 } from "util/api"; +import { CreateSongMap, CreateChartMap, CreateScoreIDMap } from "util/data"; +import { PartialArrayRecordAssign } from "util/misc"; +import { ChangeOpacity } from "util/color-opacity"; + +export default function SessionRaiseBreakdown({ sessionData }: { sessionData: SessionReturns }) { + const game = sessionData.session.game; + const playtype = sessionData.session.playtype; + + // todo infer default table + // const gptConfig = GetGamePTConfig(game, playtype); + + const [chartIDs, setChartIDs] = useState([]); + const [tableID, setTableID] = useState(null); + const [tableFolders, setTableFolders] = useState([]); + const [folder, setFolder] = useState(null); + const [filter, setFilter] = useState<"folder" | "all" | "highlighted">("all"); + + useEffect(() => { + if (!tableID) { + return; + } + + 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; + }); + const [view, setView] = useState<"lamps" | "both" | "grades">("both"); + + useEffect(() => { + if (tableID === null && (data?.length ?? 0) > 0) { + setTableID(data![0].tableID); + } + }, [data]); + + if (error) { + return <>{(error as Error).message}; + } + + if (isLoading || !data) { + return ; + } + + return ( + <> +
+
+
+
+
+ + + New Lamps + + + + + Both + + + + New Grades + +
+
+
+ +
+ +
+ {filter === "folder" && ( + <> +
+ +
+
+ +
+
+ +
+ + )} +
+ + +
+ + + ); +} + +function SessionScoreStatBreakdown({ + sessionData, + chartIDs, + filter, + view, +}: { + sessionData: SessionReturns; + chartIDs: string[] | null; + filter: "folder" | "all" | "highlighted"; + view: "lamps" | "both" | "grades"; +}) { + const songMap = CreateSongMap(sessionData.songs); + const chartMap = CreateChartMap(sessionData.charts); + const scoreMap = CreateScoreIDMap(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, view]); + + const gptConfig = GetGamePTConfig(sessionData.session.game, sessionData.session.playtype); + + return ( + <> + {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; +}) { + const tableContents = useMemo(() => { + // 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 colours = type === "lamp" ? gptConfig.lampColours : gptConfig.gradeColours; + + const tableContents = []; + 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( + + + + ); + } + } + + return tableContents; + }, [type, counts, fullSize, game]); + + if (tableContents.length === 0) { + return ( + + Nothing... + + ); + } + + return <>{tableContents}; +} + +function BreakdownChartContents({ + score, + scoreInfo, + game, + songMap, + chartMap, + fullSize, + gptConfig, + type, +}: { + score: ScoreDocument; + scoreInfo: SessionScoreInfo; + fullSize: boolean; + game: Game; + songMap: Map; + chartMap: Map; + gptConfig: GamePTConfig; + type: "lamp" | "grade"; +}) { + const chart = chartMap.get(score.chartID)!; + const song = songMap.get(score.songID)!; + + if (!chart || !song) { + console.error(`No chart for ${score.chartID}/${score.songID}???`); + return null; + } + + 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 = ; + } + } + + console.log(game); + + if (score) { + return ( + <> + + + {preScoreCell} + ⟶ + {type === "grade" ? ( + + ) : ( + + )} + + ); + } + } + + return ( + <> + + + + ); +} diff --git a/client/src/components/tables/dropdowns/GenericSessionDropdown.tsx b/client/src/components/tables/dropdowns/GenericSessionDropdown.tsx index 260cdce42..fe5e73146 100644 --- a/client/src/components/tables/dropdowns/GenericSessionDropdown.tsx +++ b/client/src/components/tables/dropdowns/GenericSessionDropdown.tsx @@ -1,38 +1,13 @@ +import SessionRaiseBreakdown from "components/sessions/SessionRaiseBreakdown"; import DebugContent from "components/util/DebugContent"; -import Divider from "components/util/Divider"; import HasDevModeOn from "components/util/HasDevModeOn"; import Icon from "components/util/Icon"; import Loading from "components/util/Loading"; import SelectButton from "components/util/SelectButton"; -import deepmerge from "deepmerge"; -import { nanoid } from "nanoid"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useState } from "react"; import { useQuery } from "react-query"; -import { - ChartDocument, - FolderDocument, - Game, - GamePTConfig, - GetGamePTConfig, - Grades, - IDStrings, - integer, - Lamps, - ScoreDocument, - SessionScoreInfo, - SongDocument, - TableDocument, -} from "tachi-common"; -import { SpecificSessionReturns } from "types/api-returns"; +import { SessionReturns } from "types/api-returns"; import { APIFetchV1 } from "util/api"; -import { ChangeOpacity } from "util/color-opacity"; -import { CreateChartMap, CreateScoreIDMap, CreateSongMap } from "util/data"; -import { PartialArrayRecordAssign } from "util/misc"; -import DifficultyCell from "../cells/DifficultyCell"; -import LampCell from "../cells/LampCell"; -import ScoreCell from "../cells/ScoreCell"; -import TitleCell from "../cells/TitleCell"; -import MiniTable from "../components/MiniTable"; import { SessionDataset } from "../sessions/GenericSessionTable"; import DropdownStructure from "./components/DropdownStructure"; @@ -40,7 +15,7 @@ export default function GenericSessionDropdown({ data }: { data: SessionDataset[ const { isLoading, error, data: sessionData } = useQuery( `/sessions/${data.sessionID}`, async () => { - const res = await APIFetchV1(`/sessions/${data.sessionID}`); + const res = await APIFetchV1(`/sessions/${data.sessionID}`); if (!res.success) { throw new Error(res.description); @@ -89,448 +64,3 @@ export default function GenericSessionDropdown({ data }: { data: SessionDataset[ ); } - -function SessionRaiseBreakdown({ sessionData }: { sessionData: SpecificSessionReturns }) { - const game = sessionData.session.game; - const playtype = sessionData.session.playtype; - - // todo infer default table - // const gptConfig = GetGamePTConfig(game, playtype); - - const [chartIDs, setChartIDs] = useState([]); - const [tableID, setTableID] = useState(null); - const [tableFolders, setTableFolders] = useState([]); - const [folder, setFolder] = useState(null); - const [filter, setFilter] = useState<"folder" | "all" | "highlighted">("all"); - - useEffect(() => { - if (!tableID) { - return; - } - - 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; - }); - - useEffect(() => { - if (tableID === null && (data?.length ?? 0) > 0) { - setTableID(data![0].tableID); - } - }, [data]); - - 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 [view, setView] = useState<"lamps" | "both" | "grades">("both"); - - const songMap = CreateSongMap(sessionData.songs); - const chartMap = CreateChartMap(sessionData.charts); - const scoreMap = CreateScoreIDMap(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, view]); - - const gptConfig = GetGamePTConfig(sessionData.session.game, sessionData.session.playtype); - - 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; -}) { - const tableContents = useMemo(() => { - // 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 colours = type === "lamp" ? gptConfig.lampColours : gptConfig.gradeColours; - - const tableContents = []; - 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( - - - - ); - } - } - - return tableContents; - }, [type, counts, fullSize, game]); - - if (tableContents.length === 0) { - return ( - - Nothing... - - ); - } - - return <>{tableContents}; -} - -function BreakdownChartContents({ - score, - scoreInfo, - game, - songMap, - chartMap, - fullSize, - gptConfig, - type, -}: { - score: ScoreDocument; - scoreInfo: SessionScoreInfo; - fullSize: boolean; - game: Game; - songMap: Map; - chartMap: Map; - gptConfig: GamePTConfig; - type: "lamp" | "grade"; -}) { - const chart = chartMap.get(score.chartID)!; - const song = songMap.get(score.songID)!; - - if (!chart || !song) { - console.error(`No chart for ${score.chartID}/${score.songID}???`); - return null; - } - - 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 = ; - } - } - - console.log(game); - - if (score) { - return ( - <> - - - {preScoreCell} - ⟶ - {type === "grade" ? ( - - ) : ( - - )} - - ); - } - } - - return ( - <> - - - - ); -} diff --git a/client/src/components/tables/sessions/GenericSessionTable.tsx b/client/src/components/tables/sessions/GenericSessionTable.tsx index a36d0b2a0..963f99f73 100644 --- a/client/src/components/tables/sessions/GenericSessionTable.tsx +++ b/client/src/components/tables/sessions/GenericSessionTable.tsx @@ -1,11 +1,13 @@ import { useSessionRatingAlg } from "components/util/useScoreRatingAlg"; import { nanoid } from "nanoid"; import React, { useState } from "react"; +import { Link } from "react-router-dom"; import { Game, GetGamePTConfig, IDStrings, integer, + PublicUserDocument, SessionCalculatedDataLookup, SessionDocument, } from "tachi-common"; @@ -25,11 +27,13 @@ export type SessionDataset = (SessionDocument & { __related: { index: integer } export default function GenericSessionTable({ dataset, indexCol = false, + reqUser, game, playtype, }: { dataset: SessionDataset; indexCol?: boolean; + reqUser: PublicUserDocument; game: Game; playtype: Playtype; }) { @@ -84,7 +88,15 @@ export default function GenericSessionTable({ timestamp: x => x.timeStarted, [alg]: x => x.calculatedData[alg] ?? 0, }} - rowFunction={s => } + rowFunction={s => ( + + )} /> ); } @@ -92,12 +104,14 @@ export default function GenericSessionTable({ function Row({ data, rating, + reqUser, indexCol = false, }: // reqUser, { data: SessionDataset[0]; // reqUser: PublicUserDocument; rating: SessionCalculatedDataLookup[IDStrings]; + reqUser: PublicUserDocument; indexCol?: boolean; }) { const [highlight, setHighlight] = useState(data.highlight); @@ -106,13 +120,18 @@ function Row({ const sessionState = { highlight, desc, setHighlight, setDesc }; return ( - } + // dropdown={} > {indexCol && } - {data.name} + + {data.name} +
{desc} @@ -130,6 +149,6 @@ function Row({
{FormatTime(data.timeStarted)} -
+ ); } diff --git a/client/src/components/util/Icon.tsx b/client/src/components/util/Icon.tsx index 04e2a0cdb..6644f4a8d 100644 --- a/client/src/components/util/Icon.tsx +++ b/client/src/components/util/Icon.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { CSSProperties } from "react"; export default function Icon({ type, @@ -6,12 +6,14 @@ export default function Icon({ brand, onClick, colour, + style, }: { type: string; noPad?: boolean; brand?: boolean; onClick?: () => void; colour?: "info" | "primary" | "danger" | "warning"; + style?: CSSProperties; }) { return ( ); } diff --git a/client/src/types/api-returns.ts b/client/src/types/api-returns.ts index 213a04000..c3ecf0db7 100644 --- a/client/src/types/api-returns.ts +++ b/client/src/types/api-returns.ts @@ -82,14 +82,6 @@ export interface UGPTChartPBComposition { pb: PBScoreDocument; } -export interface SpecificSessionReturns { - session: SessionDocument; - scores: ScoreDocument[]; - songs: SongDocument[]; - charts: ChartDocument[]; - user: PublicUserDocument; -} - export type UGSWithRankingData = UserGameStats & { __rankingData: Record; }; diff --git a/client/src/util/splashes.ts b/client/src/util/splashes.ts index be4298eb2..648961a8c 100644 --- a/client/src/util/splashes.ts +++ b/client/src/util/splashes.ts @@ -90,6 +90,7 @@ export const loggedInSplashes = [ "Discovered Colours", "Meeting People Is Easy", "See ya later, innovator", + "bou bou bounce", ]; // Stuff that's bad for the kids, you know?