diff --git a/client/package.json b/client/package.json index 9f550c353..3a8ed93c7 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.9" + "tachi-common": "0.2.12" }, "devDependencies": { "@types/humanize-duration": "3.25.1", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 113b39dc7..4f279ba86 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.9 + tachi-common: 0.2.12 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.9 + tachi-common: 0.2.12 devDependencies: '@types/humanize-duration': 3.25.1 @@ -11178,8 +11178,8 @@ packages: strip-ansi: 6.0.0 dev: true - /tachi-common/0.2.9: - resolution: {integrity: sha512-Z7oEv5OorOSB9VBJALzM3yaKxBUWNQGSJihLz923CNX5Wndi1L84LlLYgYXOZ5JrrnUFsSK374kaeODd3X/nEQ==} + /tachi-common/0.2.12: + resolution: {integrity: sha512-xwbQUZefLUs1sus3nrrys84qJyRMWLl/Mb4UzFYd74n0ZmOejrxpwIb18ILWww4J/IxQurPd4Id+DCP12SEW1w==} dependencies: monk: 7.3.4 transitivePeerDependencies: 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 10c1572e7..bad35b54c 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 @@ -169,6 +169,7 @@ function TableFolderTable({ return ( x.folder.title)], ["Chart Count", "Charts", NumericSOV(x => x.stats.chartCount)], @@ -250,8 +251,6 @@ function TableBarChart({ return arr.reverse(); }, [dataMap, mode]); - console.log(dataset); - return (
@@ -321,8 +320,6 @@ function OverviewBarChart({ [dataset] ); - console.log(longestTitle); - return ( `${v.toFixed(2)}%`} - margin={{ left: 10 + longestTitle.length * 10, bottom: 50, top: 20, right: 20 }} + margin={{ left: 10 + longestTitle.length * 7, bottom: 50, top: 20, right: 20 }} layout="horizontal" axisBottom={{ tickSize: 5, diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FoldersMainPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FoldersMainPage.tsx index 317eb2b12..3d88e0ccd 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FoldersMainPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FoldersMainPage.tsx @@ -8,6 +8,7 @@ import { FormatGame, GetGameConfig, PublicUserDocument } from "tachi-common"; import { GamePT } from "types/react"; import FolderSelectPage from "./FolderSelectPage"; import FolderTablePage from "./FolderTablePage"; +import SpecificFolderPage from "./SpecificFolderPage"; type Props = { reqUser: PublicUserDocument } & GamePT; @@ -47,6 +48,12 @@ export default function FoldersMainPage({ reqUser, game, playtype }: Props) { > + + +
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 new file mode 100644 index 000000000..d7a7cb229 --- /dev/null +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx @@ -0,0 +1,183 @@ +import { ResponsiveBar } from "@nivo/bar"; +import { BarChartTooltip } from "components/charts/ChartTooltip"; +import Card from "components/layout/page/Card"; +import FolderTable from "components/tables/folders/FolderTable"; +import ApiError from "components/util/ApiError"; +import Divider from "components/util/Divider"; +import Loading from "components/util/Loading"; +import useApiQuery from "components/util/query/useApiQuery"; +import React, { useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { Game, GetGamePTConfig, PublicUserDocument } from "tachi-common"; +import { UGPTFolderReturns } from "types/api-returns"; +import { FolderDataset } from "types/tables"; +import { Playtype } from "types/tachi"; +import { DEFAULT_BAR_PROPS } from "util/charts"; +import { ChangeOpacity } from "util/color-opacity"; +import { CreateChartIDMap, CreateSongMap } from "util/data"; +import { ComposeExpFn, ComposeInverseExpFn } from "util/misc"; +import { NumericSOV, StrSOV } from "util/sorts"; + +interface Props { + reqUser: PublicUserDocument; + game: Game; + playtype: Playtype; +} + +export default function SpecificFolderPage({ reqUser, game, playtype }: Props) { + const { folderID } = useParams<{ folderID: string }>(); + + const { data, isLoading, error } = useApiQuery( + `/users/${reqUser.id}/games/${game}/${playtype}/folders/${folderID}` + ); + + const folderDataset = useMemo(() => { + if (!data) { + return null; + } + + const songMap = CreateSongMap(data.songs); + const pbMap = CreateChartIDMap(data.pbs); + + const folderDataset: FolderDataset = []; + + for (const chart of data.charts) { + folderDataset.push({ + ...chart, + __related: { + pb: pbMap.get(chart.chartID) ?? null, + song: songMap.get(chart.songID)!, + }, + }); + } + + folderDataset.sort(StrSOV(x => x.__related.song.title)); + + return folderDataset; + }, [data]); + + if (isLoading || !data || !folderDataset) { + return ; + } + + if (error) { + return ; + } + + return ( +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ ); +} + +function FolderInfoHeader({ + game, + playtype, + reqUser, + folderDataset, + data, +}: Props & { folderDataset: FolderDataset; data: UGPTFolderReturns }) { + const dataset = []; + + const expScale = 1; + + const expFn = ComposeExpFn(expScale); + const invExpFn = ComposeInverseExpFn(expScale); + + for (const data of folderDataset) { + const value = data.__related.pb ? expFn(data.__related.pb.scoreData.percent) : 0; + dataset.push({ + chartID: data.chartID, + expValue: expFn(value), + value, + grade: data.__related.pb?.scoreData.grade, + }); + } + + dataset.sort(NumericSOV(x => x.expValue)); + + const dataMap = CreateChartIDMap(folderDataset); + + const gptConfig = GetGamePTConfig(game, playtype); + + return ( + +
+ ( + { + const data = dataMap.get(d.indexValue as string)!; + + return ( +
+ {data.__related.song.title} +
+ {data.__related.pb?.scoreData.percent.toFixed(2)}% +
+ ); + }} + /> + )} + key={"value"} + // @ts-expect-error temp + + colors={k => ChangeOpacity(gptConfig.gradeColours[k.data.grade], 0.5)} + // @ts-expect-error temp + + borderColor={k => gptConfig.gradeColours[k.data.grade]} + borderWidth={1} + padding={0.2} + // @ts-expect-error temp + data={dataset} + minValue={0} + maxValue={expFn(100)} + margin={{ left: 50, top: 20, bottom: 20 }} + axisLeft={{ + tickValues: gptConfig.gradeBoundaries.map(e => (e === 0 ? 0 : expFn(e))), + format: x => { + let nearest; + + const lgv = invExpFn(x); + + for (const [i, gradeBnd] of gptConfig.gradeBoundaries.entries()) { + if (Math.abs(gradeBnd - lgv) < 0.00005) { + nearest = i; + break; + } + } + + if (nearest === undefined) { + return null; + } + + return gptConfig.grades[nearest]; + }, + }} + axisBottom={null} + {...DEFAULT_BAR_PROPS} + /> +
+
+ ); +} diff --git a/client/src/components/game/ClassBadge.tsx b/client/src/components/game/ClassBadge.tsx index e9e3f51f3..b576e5bdc 100644 --- a/client/src/components/game/ClassBadge.tsx +++ b/client/src/components/game/ClassBadge.tsx @@ -36,14 +36,14 @@ export default function ClassBadge({ if (data.mouseover && showSetOnHover) { return ( - + {badgeComponent} ); } else if (data.mouseover) { - return {badgeComponent}; + return {badgeComponent}; } else if (showSetOnHover) { - {badgeComponent}; + {badgeComponent}; } return badgeComponent; diff --git a/client/src/components/layout/header/SearchBar.tsx b/client/src/components/layout/header/SearchBar.tsx index 6a91776e6..95bea41bf 100644 --- a/client/src/components/layout/header/SearchBar.tsx +++ b/client/src/components/layout/header/SearchBar.tsx @@ -63,7 +63,7 @@ function GPTSearchResult({ }} > {text}}> + {tooltipContent}}> {children} ); diff --git a/client/src/components/layout/page/CardNavButton.tsx b/client/src/components/layout/page/CardNavButton.tsx index 452771b9c..2579dbfe1 100644 --- a/client/src/components/layout/page/CardNavButton.tsx +++ b/client/src/components/layout/page/CardNavButton.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { OverlayTrigger } from "react-bootstrap"; import { Link } from "react-router-dom"; import QuickTooltip from "../misc/QuickTooltip"; @@ -17,7 +16,7 @@ export default function CardNavButton({ } return ( - + ); diff --git a/client/src/components/tables/cells/BMSDifficultyCell.tsx b/client/src/components/tables/cells/BMSDifficultyCell.tsx index d41f64c8f..988028bdb 100644 --- a/client/src/components/tables/cells/BMSDifficultyCell.tsx +++ b/client/src/components/tables/cells/BMSDifficultyCell.tsx @@ -4,6 +4,7 @@ import React from "react"; import { ChartDocument, Game, COLOUR_SET } from "tachi-common"; import { ChangeOpacity } from "util/color-opacity"; import { FormatBMSTables } from "util/misc"; +import TierlistInfoPart from "./TierlistInfoPart"; export default function BMSDifficultyCell({ chart, @@ -20,8 +21,9 @@ export default function BMSDifficultyCell({ }} > {levelText} + {!chart.isPrimary && ( - +
diff --git a/client/src/components/tables/cells/DifficultyCell.tsx b/client/src/components/tables/cells/DifficultyCell.tsx index 2ac715471..f68e7390e 100644 --- a/client/src/components/tables/cells/DifficultyCell.tsx +++ b/client/src/components/tables/cells/DifficultyCell.tsx @@ -1,5 +1,6 @@ import QuickTooltip from "components/layout/misc/QuickTooltip"; import Icon from "components/util/Icon"; +import Muted from "components/util/Muted"; import React from "react"; import { ChartDocument, @@ -9,6 +10,7 @@ import { GetGamePTConfig, } from "tachi-common"; import { ChangeOpacity } from "util/color-opacity"; +import TierlistInfoPart from "./TierlistInfoPart"; export default function DifficultyCell({ game, @@ -28,13 +30,18 @@ export default function DifficultyCell({ }} > {!alwaysShort && ( - {FormatDifficulty(chart, game)} + <> + {FormatDifficulty(chart, game)} + {Object.keys(chart.tierlistInfo).length > 0 && ( + + )} + )} {FormatDifficultyShort(chart, game)} {!chart.isPrimary && ( - +
diff --git a/client/src/components/tables/cells/IndicatorsCell.tsx b/client/src/components/tables/cells/IndicatorsCell.tsx new file mode 100644 index 000000000..29eca6631 --- /dev/null +++ b/client/src/components/tables/cells/IndicatorsCell.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { ChangeOpacity } from "util/color-opacity"; + +export default function IndicatorsCell({ highlight }: { highlight: boolean }) { + return ( + + ); +} diff --git a/client/src/components/tables/cells/TierlistInfoPart.tsx b/client/src/components/tables/cells/TierlistInfoPart.tsx new file mode 100644 index 000000000..4689bd1a6 --- /dev/null +++ b/client/src/components/tables/cells/TierlistInfoPart.tsx @@ -0,0 +1,48 @@ +import QuickTooltip from "components/layout/misc/QuickTooltip"; +import Icon from "components/util/Icon"; +import Muted from "components/util/Muted"; +import React from "react"; +import { ChartDocument } from "tachi-common"; +import MiniTable from "../components/MiniTable"; + +// I hate the fact object.keys doesnt infer types. +type K = keyof ChartDocument["tierlistInfo"]; + +export default function TierlistInfoPart({ chart }: { chart: ChartDocument }) { + const keys = Object.keys(chart.tierlistInfo) as K[]; + + return ( + + + + Tierlist + Value + + + + {keys.map(k => ( + + {k} + + {chart.tierlistInfo[k]!.text} ({chart.tierlistInfo[k]!.value}) + {chart.tierlistInfo[k]!.individualDifference && ( + <> +
+ + + )} + + + ))} + + + } + > +
+ {keys.map(k => chart.tierlistInfo[k]!.text).join(" / ")} +
+
+ ); +} diff --git a/client/src/components/tables/components/FilterDirectivesIndicator.tsx b/client/src/components/tables/components/FilterDirectivesIndicator.tsx index b34762b82..aba5df0a4 100644 --- a/client/src/components/tables/components/FilterDirectivesIndicator.tsx +++ b/client/src/components/tables/components/FilterDirectivesIndicator.tsx @@ -7,7 +7,11 @@ import React, { useState } from "react"; import { Modal, OverlayTrigger, Tooltip } from "react-bootstrap"; import { GetValueGetter, ValueGetterOrHybrid } from "util/ztable/search"; -function AdditionalTableProps({ value }: { value: string | number | [string, number] | null }) { +function AdditionalTableProps({ + value, +}: { + value: string | number | [string, number] | boolean | null; +}) { let typeName; if (Array.isArray(value)) { @@ -16,6 +20,8 @@ function AdditionalTableProps({ value }: { value: string | number | [string, num typeName = "Text"; } else if (value === null) { typeName = "Cannot Infer :("; + } else if (typeof value === "boolean") { + typeName = "Presence"; } else { typeName = "Number"; } @@ -23,7 +29,13 @@ function AdditionalTableProps({ value }: { value: string | number | [string, num return ( <> {typeName} - {Array.isArray(value) ? `${value[0]} (${value[1]})` : value} + + {typeof value === "boolean" + ? "Yes/No/True/False" + : Array.isArray(value) + ? `${value[0]} (${value[1]})` + : value} + ); } diff --git a/client/src/components/tables/components/SelectableRating.tsx b/client/src/components/tables/components/SelectableRating.tsx index 4985dbb54..7cf37d2e5 100644 --- a/client/src/components/tables/components/SelectableRating.tsx +++ b/client/src/components/tables/components/SelectableRating.tsx @@ -58,6 +58,7 @@ export default function SelectableRating({ color: "#ffffff", fontSize: "inherit", font: "inherit", + textAlign: "center", }} > {gptConfig[key].map(s => ( diff --git a/client/src/components/tables/components/UseScoreState.tsx b/client/src/components/tables/components/UseScoreState.tsx index 3083e00bb..b28f3e440 100644 --- a/client/src/components/tables/components/UseScoreState.tsx +++ b/client/src/components/tables/components/UseScoreState.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from "react"; +import { PBScoreDocument, ScoreDocument } from "tachi-common"; import { PBDataset, ScoreDataset } from "types/tables"; export function useScoreState(sc: ScoreDataset[0]) { @@ -13,7 +14,7 @@ export function useScoreState(sc: ScoreDataset[0]) { return { highlight, comment, setHighlight, setComment }; } -export function usePBState(pb: PBDataset[0]) { +export function usePBState(pb: PBScoreDocument) { const [highlight, setHighlight] = useState(pb.highlight); useEffect(() => { diff --git a/client/src/components/tables/dropdowns/GenericSessionDropdown.tsx b/client/src/components/tables/dropdowns/GenericSessionDropdown.tsx index b371d3717..5d0df08f1 100644 --- a/client/src/components/tables/dropdowns/GenericSessionDropdown.tsx +++ b/client/src/components/tables/dropdowns/GenericSessionDropdown.tsx @@ -8,7 +8,7 @@ import { APIFetchV1 } from "util/api"; import { SpecificSessionReturns } from "types/api-returns"; import Loading from "components/util/Loading"; import DebugContent from "components/util/DebugContent"; -import { CreateChartMap, CreateScoreMap, CreateSongMap } from "util/data"; +import { CreateChartMap, CreateChartIDMap, CreateSongMap } from "util/data"; import { Lamps, Grades, @@ -236,7 +236,7 @@ function SessionScoreStatBreakdown({ }) { const songMap = CreateSongMap(sessionData.songs); const chartMap = CreateChartMap(sessionData.charts); - const scoreMap = CreateScoreMap(sessionData.scores); + const scoreMap = CreateChartIDMap(sessionData.scores); const [newLamps, newGrades] = useMemo(() => { const newLamps: Partial {comment ? ( <> - + ) : ( - + @@ -49,7 +49,7 @@ export default function ScoreEditButtons({ )} {highlight ? ( - + ) : ( - +