@@ -122,12 +151,13 @@ function SongChartRoutes({ game, playtype }: GamePT) {
const { settings } = useContext(UserSettingsContext);
+ const gptConfig = GetGamePTConfig(game, playtype);
+
const [activeChart, setActiveChart] = useState
(null);
useEffect(() => {
setActiveChart(null);
}, [game, playtype]);
-
if (error) {
return ;
}
@@ -148,12 +178,12 @@ function SongChartRoutes({ game, playtype }: GamePT) {
- x.levelNum, true))[0]
+ .difficulty
+ }`}
/>
@@ -193,10 +223,6 @@ function SongInfoHeader({
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);
diff --git a/client/src/app/routes/UserRoutes.tsx b/client/src/app/routes/UserRoutes.tsx
index bcea10762..1e8bb9c11 100644
--- a/client/src/app/routes/UserRoutes.tsx
+++ b/client/src/app/routes/UserRoutes.tsx
@@ -165,8 +165,8 @@ function UserGamePlaytypeRoutes({ reqUser, game }: { reqUser: PublicUserDocument
);
if (!settingsRes.success) {
- console.error(settingsRes);
- throw settingsRes;
+ setSettings(null);
+ return;
}
setSettings(settingsRes.body);
diff --git a/client/src/components/layout/header/UserProfileDropdown.tsx b/client/src/components/layout/header/UserProfileDropdown.tsx
index ec17ab874..f4b17ce95 100644
--- a/client/src/components/layout/header/UserProfileDropdown.tsx
+++ b/client/src/components/layout/header/UserProfileDropdown.tsx
@@ -1,13 +1,13 @@
-import React, { useContext, useState } from "react";
-import { Link } from "react-router-dom";
-import Dropdown from "react-bootstrap/Dropdown";
import Divider from "components/util/Divider";
-import { PublicUserDocument } from "tachi-common";
-import { Button } from "react-bootstrap";
-import { APIFetchV1, ToAPIURL } from "util/api";
-import toast from "react-hot-toast";
-import { UserContext } from "context/UserContext";
import DropdownToggleOverride from "components/util/DropdownToggleOverride";
+import { UserContext } from "context/UserContext";
+import React, { useContext } from "react";
+import { Button } from "react-bootstrap";
+import Dropdown from "react-bootstrap/Dropdown";
+import toast from "react-hot-toast";
+import { Link } from "react-router-dom";
+import { PublicUserDocument } from "tachi-common";
+import { APIFetchV1, ToAPIURL } from "util/api";
export function UserProfileDropdown({ user }: { user: PublicUserDocument }) {
const { setUser } = useContext(UserContext);
diff --git a/client/src/components/tables/components/TachiTable.tsx b/client/src/components/tables/components/TachiTable.tsx
index 5da98e720..6ce1d06f6 100644
--- a/client/src/components/tables/components/TachiTable.tsx
+++ b/client/src/components/tables/components/TachiTable.tsx
@@ -1,6 +1,6 @@
import SmallText from "components/util/SmallText";
import { useZTable, ZTableSortFn } from "components/util/table/useZTable";
-import React, { useState } from "react";
+import React, { useContext, useState } from "react";
import { Button } from "react-bootstrap";
import { integer } from "tachi-common";
import NoDataWrapper from "./NoDataWrapper";
@@ -8,6 +8,9 @@ import PageSelector from "./PageSelector";
import SortableTH from "./SortableTH";
import FilterDirectivesIndicator from "./FilterDirectivesIndicator";
import { ComposeSearchFunction, ValueGetterOrHybrid } from "util/ztable/search";
+import { UserSettingsContext } from "context/UserSettingsContext";
+import Icon from "components/util/Icon";
+import { CopyToClipboard } from "util/misc";
export interface ZTableTHProps {
changeSort: (str: string) => void;
@@ -123,6 +126,8 @@ export default function TachiTable({
reverseSort,
});
+ const { settings } = useContext(UserSettingsContext);
+
return (
@@ -156,8 +161,16 @@ export default function TachiTable({
-
{displayStr}
-
+
{displayStr}
+ {settings?.preferences.developerMode && (
+
+ CopyToClipboard(window)} variant="outline-info">
+
+ Export Data
+
+
+ )}
+
, key: string) {
for (const table of chart.data.tableFolders) {
@@ -37,14 +39,18 @@ export default function BMSPBTable({
indexCol = true,
showPlaycount = false,
playtype,
+ showUser = false,
+ showChart = true,
}: {
dataset: PBDataset<"bms:7K" | "bms:14K">;
indexCol?: boolean;
showPlaycount?: boolean;
playtype: "7K" | "14K";
+ showUser?: boolean;
+ showChart?: boolean;
}) {
const headers: Header[0]>[] = [
- [
+ ...GetPBLeadingHeaders(showUser, showChart, [
"Chart",
"Chart",
NumericSOV(
@@ -53,9 +59,7 @@ export default function BMSPBTable({
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)],
@@ -102,7 +106,7 @@ export default function BMSPBTable({
dataset={dataset}
headers={headers}
entryName="PBs"
- searchFunctions={CreateDefaultPBSearchParams("bms", playtype)}
+ searchFunctions={{ ...CreateDefaultPBSearchParams("bms", playtype), ...tableSearchFns }}
defaultSortMode={indexCol ? "#" : undefined}
rowFunction={pb => (
)}
/>
@@ -120,10 +126,14 @@ function Row({
pb,
indexCol,
showPlaycount,
+ showUser,
+ showChart,
}: {
pb: PBDataset<"bms:7K" | "bms:14K">[0];
indexCol: boolean;
showPlaycount: boolean;
+ showUser: boolean;
+ showChart: boolean;
}) {
const scoreState = usePBState(pb);
@@ -140,9 +150,13 @@ function Row({
}
>
{indexCol && }
-
-
-
+ }
+ />
[] = [
- ["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
- IndicatorHeader,
- ["Song", "Song", StrSOV(x => x.__related.song.title)],
+ ...GetPBLeadingHeaders(showUser, showChart, [
+ "Chart",
+ "Chart",
+ NumericSOV(x => x.__related.chart.levelNum),
+ ]),
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
[
@@ -74,6 +82,8 @@ export default function GenericPBTable({
indexCol={indexCol}
showScore={showScore}
showPlaycount={showPlaycount}
+ showUser={showUser}
+ showChart={showChart}
/>
)}
/>
@@ -85,11 +95,15 @@ function Row({
indexCol,
showScore,
showPlaycount,
+ showUser,
+ showChart,
}: {
pb: PBDataset[0];
indexCol: boolean;
showPlaycount: boolean;
showScore?: boolean;
+ showUser: boolean;
+ showChart: boolean;
}) {
const scoreState = usePBState(pb);
@@ -106,9 +120,12 @@ function Row({
}
>
{indexCol && }
-
-
-
+
diff --git a/client/src/components/tables/pbs/IIDXPBTable.tsx b/client/src/components/tables/pbs/IIDXPBTable.tsx
index deaa1eadd..1b816d729 100644
--- a/client/src/components/tables/pbs/IIDXPBTable.tsx
+++ b/client/src/components/tables/pbs/IIDXPBTable.tsx
@@ -5,7 +5,7 @@ import React, { useContext, useState } from "react";
import { PublicUserDocument, ScoreCalculatedDataLookup } from "tachi-common";
import { PBDataset } from "types/tables";
import { IsNullish } from "util/misc";
-import { NumericSOV, StrSOV } from "util/sorts";
+import { NumericSOV } from "util/sorts";
import { CreateDefaultPBSearchParams } from "util/tables/create-search";
import { GetPBLeadingHeaders } from "util/tables/get-pb-leaders";
import DeltaCell from "../cells/DeltaCell";
@@ -23,6 +23,7 @@ import SelectableRating from "../components/SelectableRating";
import TachiTable, { Header, ZTableTHProps } from "../components/TachiTable";
import { usePBState } from "../components/UseScoreState";
import IIDXPBDropdown from "../dropdowns/IIDXPBDropdown";
+import PBLeadingRows from "./PBLeadingRows";
export default function IIDXPBTable({
dataset,
@@ -140,7 +141,7 @@ function Row({
return (
{indexCol && }
- {showUser && showChart && (
- <>
-
-
-
-
- >
- )}
- {showUser && !showChart && (
- <>
-
-
- >
- )}
- {!showUser && showChart && (
- <>
-
-
-
- >
- )}
+
;
indexCol?: boolean;
showPlaycount?: boolean;
+ showUser?: boolean;
+ showChart?: boolean;
playtype: "Single";
}) {
const headers: Header[0]>[] = [
- ["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
- IndicatorHeader,
- ["Song", "Song", StrSOV(x => x.__related.song.title)],
+ ...GetPBLeadingHeaders(showUser, showChart, [
+ "Chart",
+ "Chart",
+ NumericSOV(x => x.__related.chart.levelNum),
+ ]),
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Near - Miss", "Nr. Ms.", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
@@ -63,6 +71,8 @@ export default function MusecaPBTable({
key={`${pb.chartID}:${pb.userID}`}
indexCol={indexCol}
showPlaycount={showPlaycount}
+ showChart={showChart}
+ showUser={showUser}
/>
)}
/>
@@ -73,10 +83,14 @@ function Row({
pb,
indexCol,
showPlaycount,
+ showChart,
+ showUser,
}: {
pb: PBDataset<"museca:Single">[0];
indexCol: boolean;
showPlaycount: boolean;
+ showChart: boolean;
+ showUser: boolean;
}) {
const scoreState = usePBState(pb);
@@ -93,9 +107,7 @@ function Row({
}
>
{indexCol && }
-
-
-
+
diff --git a/client/src/components/tables/pbs/PBLeadingRows.tsx b/client/src/components/tables/pbs/PBLeadingRows.tsx
new file mode 100644
index 000000000..b6643fe63
--- /dev/null
+++ b/client/src/components/tables/pbs/PBLeadingRows.tsx
@@ -0,0 +1,51 @@
+import React from "react";
+import { SetState } from "types/react";
+import { PBDataset } from "types/tables";
+import DifficultyCell from "../cells/DifficultyCell";
+import IndicatorsCell from "../cells/IndicatorsCell";
+import TitleCell from "../cells/TitleCell";
+import UserCell from "../cells/UserCell";
+
+export default function PBLeadingRows({
+ showUser,
+ showChart,
+ pb,
+ scoreState,
+ overrideDiffCell,
+}: {
+ showUser: boolean;
+ showChart: boolean;
+ pb: PBDataset[0];
+ scoreState: { highlight: boolean; setHighlight: SetState };
+ overrideDiffCell?: JSX.Element;
+}) {
+ const game = pb.game;
+
+ const diffCell = overrideDiffCell || ;
+
+ return (
+ <>
+ {showUser && showChart && (
+ <>
+
+ {diffCell}
+
+
+ >
+ )}
+ {showUser && !showChart && (
+ <>
+
+
+ >
+ )}
+ {!showUser && showChart && (
+ <>
+ {diffCell}
+
+
+ >
+ )}
+ >
+ );
+}
diff --git a/client/src/components/tables/pbs/SDVXPBTable.tsx b/client/src/components/tables/pbs/SDVXPBTable.tsx
index d8f96307d..b2e7ef70b 100644
--- a/client/src/components/tables/pbs/SDVXPBTable.tsx
+++ b/client/src/components/tables/pbs/SDVXPBTable.tsx
@@ -4,6 +4,7 @@ import { PBDataset } from "types/tables";
import { IsNullish } from "util/misc";
import { NumericSOV, StrSOV } from "util/sorts";
import { CreateDefaultPBSearchParams } from "util/tables/create-search";
+import { GetPBLeadingHeaders } from "util/tables/get-pb-leaders";
import DifficultyCell from "../cells/DifficultyCell";
import IndexCell from "../cells/IndexCell";
import IndicatorsCell from "../cells/IndicatorsCell";
@@ -18,22 +19,29 @@ import TachiTable, { Header } from "../components/TachiTable";
import { usePBState } from "../components/UseScoreState";
import GenericPBDropdown from "../dropdowns/GenericPBDropdown";
import IndicatorHeader from "../headers/IndicatorHeader";
+import PBLeadingRows from "./PBLeadingRows";
export default function SDVXPBTable({
dataset,
indexCol = true,
showPlaycount = false,
playtype,
+ showUser = false,
+ showChart = true,
}: {
dataset: PBDataset<"sdvx:Single">;
indexCol?: boolean;
showPlaycount?: boolean;
playtype: "7K" | "14K";
+ showUser?: boolean;
+ showChart?: boolean;
}) {
const headers: Header[0]>[] = [
- ["Chart", "Chart", NumericSOV(x => x.__related.chart.levelNum)],
- IndicatorHeader,
- ["Song", "Song", StrSOV(x => x.__related.song.title)],
+ ...GetPBLeadingHeaders(showUser, showChart, [
+ "Chart",
+ "Chart",
+ NumericSOV(x => x.__related.chart.levelNum),
+ ]),
["Score", "Score", NumericSOV(x => x.scoreData.percent)],
["Near - Miss", "Nr. Ms.", NumericSOV(x => x.scoreData.percent)],
["Lamp", "Lamp", NumericSOV(x => x.scoreData.lampIndex)],
@@ -63,6 +71,8 @@ export default function SDVXPBTable({
key={`${pb.chartID}:${pb.userID}`}
indexCol={indexCol}
showPlaycount={showPlaycount}
+ showChart={showChart}
+ showUser={showUser}
/>
)}
/>
@@ -73,10 +83,14 @@ function Row({
pb,
indexCol,
showPlaycount,
+ showChart,
+ showUser,
}: {
pb: PBDataset<"sdvx:Single">[0];
indexCol: boolean;
showPlaycount: boolean;
+ showChart: boolean;
+ showUser: boolean;
}) {
const scoreState = usePBState(pb);
@@ -93,9 +107,7 @@ function Row({
}
>
{indexCol && }
-
-
-
+
diff --git a/client/src/components/util/ApiError.tsx b/client/src/components/util/ApiError.tsx
index 83b5b58d5..d8f77d87f 100644
--- a/client/src/components/util/ApiError.tsx
+++ b/client/src/components/util/ApiError.tsx
@@ -1,6 +1,14 @@
-import React from "react";
+import { UserSettingsContext } from "context/UserSettingsContext";
+import React, { useContext } from "react";
import { UnsuccessfulAPIFetchResponse } from "util/api";
export default function ApiError({ error }: { error: UnsuccessfulAPIFetchResponse }) {
- return An error has occured: {error.description}
;
+ const { settings } = useContext(UserSettingsContext);
+
+ return (
+
+ An error has occured
+ {settings?.preferences.developerMode ? ` (${error.description})` : ""}
+
+ );
}
diff --git a/client/src/types/api-returns.ts b/client/src/types/api-returns.ts
index aea962a52..d0699a1b2 100644
--- a/client/src/types/api-returns.ts
+++ b/client/src/types/api-returns.ts
@@ -153,3 +153,15 @@ export interface UGPTChartLeaderboardAdjacent {
adjacentAbove: PBScoreDocument[];
adjacentBelow: PBScoreDocument[];
}
+
+export interface ScoreLeaderboardReturns {
+ users: PublicUserDocument[];
+ songs: SongDocument[];
+ charts: ChartDocument[];
+ pbs: PBScoreDocument[];
+}
+
+export interface UserLeaderboardReturns {
+ users: PublicUserDocument[];
+ gameStats: UserGameStats[];
+}
diff --git a/client/src/types/tables.ts b/client/src/types/tables.ts
index 048144c97..dae29ebdf 100644
--- a/client/src/types/tables.ts
+++ b/client/src/types/tables.ts
@@ -7,6 +7,7 @@ import {
integer,
ScoreDocument,
PublicUserDocument,
+ UserGameStats,
} from "tachi-common";
export type PBDataset = (PBScoreDocument & {
@@ -41,3 +42,10 @@ export type ChartLeaderboardDataset = (PBScoreD
user: PublicUserDocument;
};
})[];
+
+export type UGSDataset = (UserGameStats & {
+ __related: {
+ user: PublicUserDocument;
+ index: integer;
+ };
+})[];
diff --git a/client/src/util/misc.ts b/client/src/util/misc.ts
index 298550432..568e50187 100644
--- a/client/src/util/misc.ts
+++ b/client/src/util/misc.ts
@@ -1,5 +1,14 @@
import { Playtype } from "types/tachi";
-import { GetGamePTConfig, Game, ScoreCalculatedDataLookup, IDStrings, integer } from "tachi-common";
+import {
+ GetGamePTConfig,
+ Game,
+ ScoreCalculatedDataLookup,
+ IDStrings,
+ integer,
+ ChartDocument,
+ GamePTConfig,
+} from "tachi-common";
+import toast from "react-hot-toast";
export function RFA(arr: T[]): T {
return arr[Math.floor(Math.random() * arr.length)];
@@ -127,3 +136,31 @@ export function ComposeInverseExpFn(pow: number) {
export function ComposeLogFn(base: number) {
return (x: number) => Math.log(x) / Math.log(base);
}
+
+export 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;
+}
+
+export function CopyToClipboard(data: unknown) {
+ toast.success("Copied data to clipboard.");
+ navigator.clipboard.writeText(JSON.stringify(data, null, "\t"));
+}
diff --git a/client/src/util/ztable/search.ts b/client/src/util/ztable/search.ts
index 599171b53..84eb7eb51 100644
--- a/client/src/util/ztable/search.ts
+++ b/client/src/util/ztable/search.ts
@@ -7,11 +7,12 @@ export interface SearchDirective {
value: string;
}
-type DirectiveModes = "!" | ">" | ">=" | "<" | "<=" | "~";
+type DirectiveModes = "$" | ">" | ">=" | "<" | "<=" | "~" | "!";
export interface Directive {
key: string;
- // :! - EXACT
+ // :$ - EXACT
+ // :! - NOT
// :> - GREATER THAN
// :>= - GREATER THAN OR EQ
// :< - LESS THAN
@@ -29,7 +30,7 @@ export interface Directive {
// VALUE = /("(?:\\"|[^"])+"|[^ ]+)/ - Parses the value.
export function ParseDirectives(search: string): Directive[] {
- const parsedSearch = search.matchAll(/([^ ]+):(>=|<=|!|>|<|~)?("(?:\\"|[^"])+"|[^ ]+)/gu);
+ const parsedSearch = search.matchAll(/([^ ]+):(>=|<=|!|\$|>|<|~)?("(?:\\"|[^"])+"|[^ ]+)/gu);
const searchDirectives = [];
for (const ps of parsedSearch) {
@@ -62,7 +63,9 @@ type MatchFn = (searchValue: string, dataValue: string | number) => boolean;
const Matchers: Record = {
// Exact: Make sure this string is exactly the data value.
- "!": (sv, dv) => sv === dv.toString(),
+ $: (sv, dv) => sv === dv.toString(),
+ // Not: this value is not equal to the provided value. Case insensitive.
+ "!": (sv, dv) => sv.toLowerCase() !== dv.toString().toLowerCase(),
// LT, LTE, GT, GTE: If the data value is a number, compare numerically. If the data value is a string
// compare alphabetically.
// equality for alphabetical is implemented as .startsWith case insensitively.