feat: manual human pass over the code, untangle and remove entropy (#1668)

* fix: use 3003 for seeds port + expose ports consistently

* fix: seeds-webui license AGPL3 -> MIT

* fix: more port 3100 to 3003

* feat: remove "GPT/UGPT" as a phrase from the codebase

* fix: clean up some ridiculous legacy code hoops
This commit is contained in:
zk
2026-06-17 21:34:26 +01:00
committed by GitHub
parent eff9101f79
commit c0dca6f6a9
396 changed files with 1758 additions and 3247 deletions
@@ -21,11 +21,11 @@ MY_ACTION: {
### Action files
Place the implementation in `src/actions/my-action.ts`. Always use `.js` extensions on imports (the package uses ESM):
Place the implementation in `src/actions/my-action.ts`:
```typescript
import { MakeAction } from "#lib/actions/actions.js";
import DB from "#services/pg/db.js";
import { MakeAction } from "#lib/actions/actions";
import DB from "#services/pg/db";
import { ExpectedErr } from "bliss";
export const ACTION_MyAction = MakeAction(
@@ -75,7 +75,6 @@ return res.status(200).json({ success: true, description: "...", body: result })
```typescript
import DB from "#services/pg/db"; // routers / utils
import DB from "#services/pg/db.js"; // action files (ESM)
```
`DB` is a typed `Kysely<Database>` instance. Types come from the generated `tachi-db` workspace package (`src/generated/public/Priv*.ts`).
@@ -1,26 +0,0 @@
---
name: mongo-migration-constraints
description: Migrates server code from MongoDB to Postgres without modifying the legacy Mongo service. Never edits typescript/server/src/services/mongo/ (especially db.ts). Requires removing MONGODB_KILL imports and replacing usage with Postgres (Kysely) in migrated files. Use during Mongo-to-Postgres migration, when migrating routers/actions/utils off #services/mongo/db, or when the user says not to touch mongo/db.ts.
---
# Mongo → Postgres migration - off-limits Mongo layer
## Hard rules
1. **Do not edit** `typescript/server/src/services/mongo/db.ts` (or any file under `typescript/server/src/services/mongo/`). The Mongo service stays as-is until it is retired separately.
2. **In every file you migrate** to Postgres: **remove** `import MONGODB_KILL from "#services/mongo/db"` (and any `MONGODB_KILL` usage). Replace reads/writes with Kysely against `#services/pg/db` (and helpers in `src/lib/db-formats/` as needed).
3. **Do not** add new `MONGODB_KILL` imports to files that no longer need Mongo.
## How to migrate call sites
Follow [actions-and-pg-migration/SKILL.md](../actions-and-pg-migration/SKILL.md) for actions, routers, and the usual migration steps. Use [db-formats/SKILL.md](../db-formats/SKILL.md) for `SELECT_*` / `To*Document` shapes.
If something still truly depends on Mongo and cannot move yet, **leave that file unchanged** rather than editing `db.ts` or other mongo internals to “help” the migration.
## Quick checklist for a migrated file
- [ ] No import from `#services/mongo/db` / `./db` under `services/mongo`.
- [ ] Uses `DB` from `#services/pg/db` (or `.js` in action files per project ESM rules).
- [ ] No edits under `typescript/server/src/services/mongo/`.
-10
View File
@@ -1,10 +0,0 @@
---
name: no-oldtest-edits
description: Legacy. `*.oldtest.ts` snapshots were removed from the Tachi server after migration to Vitest `*.test.ts`. No action needed unless oldtest files are reintroduced.
---
# Legacy note
Server tests live in `*.test.ts` under `typescript/server/src/`. The former Tap `*.oldtest.ts` tree has been deleted.
If `*.oldtest.ts` files appear again, treat them as read-only snapshots and add or change behavior only in `*.test.ts` or source files.
-56
View File
@@ -1,56 +0,0 @@
---
name: no-return-await
description: Prefer `return` over `return await` in async functions when the await is redundant. Use when writing or reviewing async TypeScript/JavaScript, simplifying control flow, or fixing redundant awaits on returned promises.
---
# Prefer `return` over `return await`
## Default rule
In an `async function`, **`return await x` and `return x` are equivalent** for callers when `x` is a Promise (or thenable): both return a Promise with the same fulfillment and rejection.
Prefer **`return x`** - the extra `await` adds a microtask and obscures that you are simply forwarding the result.
```typescript
// Prefer
async function load() {
return fetchData();
}
// Avoid (unless you need await for control flow - see below)
async function load() {
return await fetchData();
}
```
## When you must `await`
Use **`await`** (including `return await`) only when you need the async function to **suspend on that operation** for control flow:
- **`try` / `catch` / `finally`**: To handle or finalize on **rejection** of the inner promise, you must `await` it inside `try`. A bare `return innerPromise()` does **not** route that rejection through `catch` / `finally` the same way.
```typescript
async function safeLoad() {
try {
// `return await` is correct here so `catch` sees rejections from fetchData()
return await fetchData();
} catch (e) {
return defaultData();
}
}
```
- **`finally`** that must run after the inner work settles (same idea - often needs `await` in the `try`).
If there is **no** `try`/`catch`/`finally` depending on that promise settling inside the function, **do not** use `return await`.
## Review checklist
- [ ] If the last statement is `return await expr` and nothing in the function uses `try`/`catch`/`finally` around that path for that promise → use `return expr`.
- [ ] If `catch` or `finally` must apply to failures of `expr` → keep `await` (often `return await expr` in the `try`).
## Why
- Clearer intent: forwarding a Promise vs. explicitly sequencing.
- Slightly leaner: no unnecessary `await` + re-wrapping.
- Aligns with common ESLint `no-return-await` guidance (with the try/catch exception above).
+1 -1
View File
@@ -11,7 +11,7 @@
"TACHI_EMAIL_SECURE": "false"
},
"postAttachCommand": "./dev/bootstrap.sh",
"forwardPorts": [3001, 3002],
"forwardPorts": [3000, 3001, 3002, 3003, 8080, 9779],
"updateRemoteUserUID": true,
"containerUser": "tachi",
"customizations": {
+1 -1
View File
@@ -56,7 +56,7 @@ function fish_greeting
echo ""
echo "Type $(cmd "just start") to start up tachi."
echo " $(rgb "The site will start on http://localhost:3000." ffff00 000000)"
echo " $(rgb "The seeds web UI will start on http://localhost:3100." ffff00 000000)"
echo " $(rgb "The seeds web UI will start on http://localhost:3003." ffff00 000000)"
echo " $(rgb "Use Ctrl+C to stop Tachi." ffff00 000000)"
echo ""
echo "You can also run:"
+2 -2
View File
@@ -153,11 +153,11 @@ services:
build:
dockerfile: Dockerfile.dev
ports:
- "8080:8080" # server
- "3000:3000" # client
- "3001:3001" # docs
- "3002:3002" # homepage
- "3100:3100" # seeds
- "3003:3003" # seeds
- "8080:8080" # server
- "9779:9779" # metrics
volumes:
- ./:/tachi
+4 -4
View File
@@ -14,7 +14,7 @@ import {
type V3Game,
} from "tachi-common";
import type { ImportDeferred, ImportPollStatus, UGPTStats } from "./return-types";
import type { ImportDeferred, ImportPollStatus, UserGameStatsReturn } from "./return-types";
import { RequestTypes, TachiServerV1Get, TachiServerV1Request } from "./fetch-tachi";
import { Sleep } from "./misc";
@@ -29,11 +29,11 @@ export async function GetUserInfo(userID: string | integer) {
return res.body;
}
export async function GetUGPTStats(userID: string | integer, game: V3Game) {
const res = await TachiServerV1Get<UGPTStats>(`/users/${userID}/games/${game}`, null);
export async function GetUserGameStats(userID: string | integer, game: V3Game) {
const res = await TachiServerV1Get<UserGameStatsReturn>(`/users/${userID}/games/${game}`, null);
if (!res.success) {
throw new Error(`Failed to fetch UGPT stats for userID ${userID}, ${game}.`);
throw new Error(`Failed to fetch UserGame stats for userID ${userID}, ${game}.`);
}
return res.body;
+1 -1
View File
@@ -36,7 +36,7 @@ export type ImportPollStatus =
};
};
export interface UGPTStats<TGame extends V3Game = V3Game> {
export interface UserGameStatsReturn<TGame extends V3Game = V3Game> {
gameStats: UserGameStats;
firstScore: ScoreDocument;
mostRecentScore: ScoreDocument;
@@ -10,7 +10,7 @@ import {
import { Env } from "../config";
import { client } from "../main";
import { GetUGPTStats, GetUserInfo } from "../utils/api-requests";
import { GetUserGameStats, GetUserInfo } from "../utils/api-requests";
import { CreateEmbed } from "../utils/embeds";
import { PrependTachiUrl } from "../utils/fetch-tachi";
import { FormatClass, GetGameChannel, UppercaseFirst } from "../utils/misc";
@@ -45,7 +45,7 @@ export async function HandleClassUpdateV1(
const minimumNecessaryScores = GetMinimumScores(game, event.set);
if (minimumNecessaryScores !== null) {
const { totalScores } = await GetUGPTStats(userDoc.id, game);
const { totalScores } = await GetUserGameStats(userDoc.id, game);
// Do not render if the user hasn't hit the score cap.
if (totalScores < minimumNecessaryScores) {
+1 -1
View File
@@ -9,7 +9,7 @@ VITE_FLO_CLIENT_ID=""
VITE_RULES_READ_TIME=1
VITE_IS_LOCAL_DEV=true
VITE_IS_DEVELOPMENT=true
VITE_SEEDS_URL="http://127.0.0.1:3100"
VITE_SEEDS_URL="http://127.0.0.1:3003"
# Where routing should "base" from.
# shouldn't be used, really.
VITE_BASE_PATH=""
+3 -3
View File
@@ -1,7 +1,7 @@
import { CustomScrollbar } from "#components/layout/CustomScrollbar";
import { LocalDevMissingSeedsBanner } from "#components/layout/LocalDevMissingSeedsBanner";
import { LoadingScreen } from "#components/layout/screens/LoadingScreen";
import { AllLUGPTStatsContextProvider } from "#context/AllLUGPTStatsContext";
import { AllYourUGStatsContextProvider } from "#context/AllYourUGStatsContext";
import { BannedContextProvider } from "#context/BannedContext";
import { NotificationsContextProvider } from "#context/NotificationsContext";
import { SubheaderContextProvider } from "#context/SubheaderContext";
@@ -30,7 +30,7 @@ export default function App({ basename }: { basename: string }) {
<LoadingScreen>
<NotificationsContextProvider>
<UserSettingsContextProvider>
<AllLUGPTStatsContextProvider>
<AllYourUGStatsContextProvider>
<BrowserRouter basename={basename}>
<Toaster />
<LocalDevMissingSeedsBanner />
@@ -38,7 +38,7 @@ export default function App({ basename }: { basename: string }) {
<Routes />
</SubheaderContextProvider>
</BrowserRouter>
</AllLUGPTStatsContextProvider>
</AllYourUGStatsContextProvider>
</UserSettingsContextProvider>
</NotificationsContextProvider>
</LoadingScreen>
@@ -1,10 +1,9 @@
import useSetSubheader from "#components/layout/header/useSetSubheader";
import { ToCDNURL } from "#util/api";
import { HistorySafeGoBack } from "#util/misc";
import React from "react";
import { useHistory } from "react-router-dom";
export function ErrorPage({
export default function ErrorPage({
statusCode,
customMessage,
}: {
@@ -1,10 +1,9 @@
import ApiError from "#components/util/ApiError";
import Loading from "#components/util/Loading";
import useApiQuery from "#components/util/query/useApiQuery";
import React from "react";
import { Link } from "react-router-dom";
import { ErrorPage } from "./ErrorPage";
import ErrorPage from "./ErrorPage";
export default function OAuth2CallbackPage({
counterWeight,
@@ -6,7 +6,6 @@ import OAuthMoreInfo from "#components/util/OAuthMoreInfo";
import useApiQuery from "#components/util/query/useApiQuery";
import useQueryString from "#components/util/useQueryString";
import { APIFetchV1 } from "#util/api";
import React from "react";
import { Link } from "react-router-dom";
import { type integer, type TachiAPIClientDocument } from "tachi-common";
@@ -1,7 +1,6 @@
import useSetSubheader from "#components/layout/header/useSetSubheader";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
export default function PrivacyPolicyPage() {
useSetSubheader(["Dashboard", "GDPR/Legal Stuff"]);
@@ -2,10 +2,10 @@ import LoginPageLayout from "#components/layout/LoginPageLayout";
import MainPageTitleContainer from "#components/util/MainPageTitleContainer";
import { APIFetchV1 } from "#util/api";
import { ShortDelayify } from "#util/misc";
import React, { useState } from "react";
import { useState } from "react";
import { Button, Form } from "react-bootstrap";
import { ErrorPage } from "./ErrorPage";
import ErrorPage from "./ErrorPage";
export default function ResetPasswordPage() {
const code = new URLSearchParams(window.location.search).get("code");
@@ -6,9 +6,9 @@ import Loading from "#components/util/Loading";
import useApiQuery from "#components/util/query/useApiQuery";
import useQueryString from "#components/util/useQueryString";
import { UserContext } from "#context/UserContext";
import React, { useContext } from "react";
import { useContext } from "react";
import { ErrorPage } from "./ErrorPage";
import ErrorPage from "./ErrorPage";
import LoginPage from "./LoginPage";
export default function VerifyEmailPage() {
@@ -11,10 +11,10 @@ export default function AdminDestructivePage() {
const [deleteScoreId, setDeleteScoreId] = useState("");
const [deleteSessionId, setDeleteSessionId] = useState("");
const [ugptUserId, setUgptUserId] = useState("");
const [ugptGame, setUgptGame] = useState<GameGroup>(TachiConfig.GAME_GROUPS[0]);
const ugptGameConfig = useMemo(() => GetGameGroupConfig(ugptGame), [ugptGame]);
const [ugptPlaytype, setUgptPlaytype] = useState<string>(
const [userGameUserID, setUserGameUserID] = useState("");
const [userGameGame, setUserGameGame] = useState<GameGroup>(TachiConfig.GAME_GROUPS[0]);
const userGameConfig = useMemo(() => GetGameGroupConfig(userGameGame), [userGameGame]);
const [userGamePlaytype, setUserGamePlaytype] = useState<string>(
() => GetGameGroupConfig(TachiConfig.GAME_GROUPS[0]).playtypes[0],
);
@@ -116,27 +116,27 @@ export default function AdminDestructivePage() {
<Col lg={6}>
<Card className="border-danger">
<Card.Header className="bg-danger bg-opacity-10 text-danger">
Destroy user game profile (UGPT)
Destroy user game profile
</Card.Header>
<Card.Body>
<Form.Group className="mb-3" controlId="ugpt-user">
<Form.Group className="mb-3" controlId="userprofile-user">
<Form.Label>User ID</Form.Label>
<Form.Control
onChange={(e) => setUgptUserId(e.target.value)}
onChange={(e) => setUserGameUserID(e.target.value)}
type="number"
value={ugptUserId}
value={userGameUserID}
/>
</Form.Group>
<Form.Group className="mb-3" controlId="ugpt-game">
<Form.Group className="mb-3" controlId="userprofile-game">
<Form.Label>Game</Form.Label>
<Form.Select
onChange={(e) => {
const g = e.target.value as GameGroup;
setUgptGame(g);
setUserGameGame(g);
const cfg = GetGameGroupConfig(g);
setUgptPlaytype(cfg.playtypes[0]);
setUserGamePlaytype(cfg.playtypes[0]);
}}
value={ugptGame}
value={userGameGame}
>
{TachiConfig.GAME_GROUPS.map((g) => (
<option key={g} value={g}>
@@ -145,13 +145,13 @@ export default function AdminDestructivePage() {
))}
</Form.Select>
</Form.Group>
<Form.Group className="mb-3" controlId="ugpt-pt">
<Form.Group className="mb-3" controlId="userprofile-pt">
<Form.Label>Playtype</Form.Label>
<Form.Select
onChange={(e) => setUgptPlaytype(e.target.value)}
value={ugptPlaytype}
onChange={(e) => setUserGamePlaytype(e.target.value)}
value={userGamePlaytype}
>
{ugptGameConfig.playtypes.map((pt) => (
{userGameConfig.playtypes.map((pt) => (
<option key={pt} value={pt}>
{pt}
</option>
@@ -159,29 +159,29 @@ export default function AdminDestructivePage() {
</Form.Select>
</Form.Group>
<Button
disabled={!ugptUserId.trim()}
disabled={!userGameUserID.trim()}
onClick={() => {
const uid = Number.parseInt(ugptUserId, 10);
const uid = Number.parseInt(userGameUserID, 10);
if (Number.isNaN(uid)) {
alert("User ID must be a number.");
return;
}
if (
!confirmDelete(
`Destroy all stats for user ${uid} (${ugptGame} ${ugptPlaytype})? This cannot be undone.`,
`Destroy all stats for user ${uid} (${userGameGame} ${userGamePlaytype})? This cannot be undone.`,
)
) {
return;
}
void APIFetchV1(
`/admin/destroy-ugpt`,
`/admin/destroy-userprofile`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
userID: uid,
game: ugptGame,
playtype: ugptPlaytype,
game: userGameGame,
playtype: userGamePlaytype,
}),
},
true,
@@ -190,7 +190,7 @@ export default function AdminDestructivePage() {
}}
variant="danger"
>
Destroy UGPT
Destroy User Profile
</Button>
</Card.Body>
</Card>
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import useApiQuery from "#components/util/query/useApiQuery";
import { ADMIN_PAGE_SIZE, ADMIN_RECENT_HOURS, JOB_STATUS } from "#lib/adminConstants";
import { MillisToSince } from "#util/time";
import React from "react";
import { Button, Form, Table } from "react-bootstrap";
import { Link, useHistory, useLocation } from "react-router-dom";
@@ -1,28 +1,19 @@
import useSetSubheader from "#components/layout/header/useSetSubheader";
import { TachiConfig } from "#lib/config";
import { APIFetchV1 } from "#util/api";
import React, { useState } from "react";
import { useState } from "react";
import { Button, Card, Col, Form, Row } from "react-bootstrap";
import {
FormatGame,
type GameGroup,
GetGameGroupConfig,
LEGACY_GameGroupPTToGame,
} from "tachi-common";
import { ALL_GAMES, FormatGame, type V3Game } from "tachi-common";
export default function AdminOperationsPage() {
useSetSubheader(["Admin", "Operations"]);
const [announcementTitle, setAnnouncementTitle] = useState("");
const [announcementGame, setAnnouncementGame] = useState<"" | GameGroup>("");
const [announcementPlaytype, setAnnouncementPlaytype] = useState("");
const [announcementGame, setAnnouncementPlaytype] = useState<V3Game | null>(null);
const [folderId, setFolderId] = useState("");
const [supporterUser, setSupporterUser] = useState("");
const announcementGameConfig = announcementGame ? GetGameGroupConfig(announcementGame) : null;
return (
<Row className="g-4">
<Col lg={6}>
@@ -38,44 +29,19 @@ export default function AdminOperationsPage() {
/>
</Form.Group>
<Form.Group className="mb-3" controlId="announcement-game">
<Form.Label>Game (optional)</Form.Label>
<Form.Label>Game</Form.Label>
<Form.Select
onChange={(e) => {
const v = e.target.value;
setAnnouncementGame(v === "" ? "" : (v as GameGroup));
setAnnouncementPlaytype("");
}}
value={announcementGame === "" ? "" : announcementGame}
onChange={(e) => setAnnouncementPlaytype(e.target.value as V3Game)}
value={announcementGame ?? ""}
>
<option value="">- Site-wide -</option>
{TachiConfig.GAME_GROUPS.map((g) => (
<option key={g} value={g}>
{g}
<option value="">-</option>
{ALL_GAMES.map((game) => (
<option key={game} value={game}>
{FormatGame(game)}
</option>
))}
</Form.Select>
</Form.Group>
{announcementGameConfig && (
<Form.Group className="mb-3" controlId="announcement-playtype">
<Form.Label>Playtype (optional)</Form.Label>
<Form.Select
onChange={(e) => setAnnouncementPlaytype(e.target.value)}
value={announcementPlaytype}
>
<option value="">-</option>
{announcementGameConfig.playtypes.map((pt) => (
<option key={pt} value={pt}>
{FormatGame(
LEGACY_GameGroupPTToGame(
announcementGame as GameGroup,
pt,
),
)}
</option>
))}
</Form.Select>
</Form.Group>
)}
<Button
disabled={!announcementTitle.trim()}
onClick={() => {
@@ -85,9 +51,6 @@ export default function AdminOperationsPage() {
if (announcementGame) {
body.game = announcementGame;
}
if (announcementGame && announcementPlaytype) {
body.playtype = announcementPlaytype;
}
void APIFetchV1(
`/admin/announcement`,
{
@@ -4,7 +4,7 @@ import { DashboardHeader } from "#components/dashboard/DashboardHeader";
import useSetSubheader from "#components/layout/header/useSetSubheader";
import SessionCalendar from "#components/sessions/SessionCalendar";
import SessionCard from "#components/sessions/SessionCard";
import UGPTProfiles from "#components/user/UGPTProfiles";
import UserGameProfiles from "#components/user/UserGameProfiles";
import ApiError from "#components/util/ApiError";
import Divider from "#components/util/Divider";
import GoalLink from "#components/util/GoalLink";
@@ -68,7 +68,7 @@ function DashboardLoggedIn({ user }: { user: UserDocument }) {
/>
</Route>
<Route exact path="/profiles">
<UGPTProfiles />
<UserGameProfiles />
</Route>
<Route exact path="/global-activity">
<Activity url="/ublock-blocks-this" />
@@ -12,7 +12,7 @@ import Loading from "#components/util/Loading";
import Muted from "#components/util/Muted";
import useApiQuery from "#components/util/query/useApiQuery";
import SelectLinkButton from "#components/util/SelectLinkButton";
import useLUGPTSettings from "#components/util/useLUGPTSettings";
import useLoggedInUserGameSettings from "#components/util/useLoggedInUserGameSettings";
import { TargetsContext } from "#context/TargetsContext";
import { UserContext } from "#context/UserContext";
import { WindowContext } from "#context/WindowContext";
@@ -20,14 +20,14 @@ import {
type ChartPBLeaderboardReturn,
type ChartRivalsReturn,
type GoalsOnChartReturn,
type UGPTChartLeaderboardAdjacent,
type UserGameChartLeaderboardAdjacent,
} from "#types/api-returns";
import { type GamePT } from "#types/react";
import { type GameProps } from "#types/react";
import { type PBDataset } from "#types/tables";
import { APIFetchV1, type UnsuccessfulAPIFetchResponse } from "#util/api";
import { CreateChartLink, CreateUserMap } from "#util/data";
import { MillisToSince } from "#util/time";
import React, { useContext, useMemo, useState } from "react";
import { useContext, useMemo, useState } from "react";
import ButtonGroup from "react-bootstrap/ButtonGroup";
import Col from "react-bootstrap/Col";
import Row from "react-bootstrap/Row";
@@ -37,33 +37,26 @@ import { Link, Route, Switch } from "react-router-dom";
import {
type ChartDocument,
FormatDifficultyLong,
GameToGameGroup,
GetGameGroupConfig,
FormatGame,
type integer,
type PBScoreDocument,
type SongDocument,
type UserDocument,
} from "tachi-common";
export default function GPTChartPage({
export default function GameChartPage({
chart,
game,
song,
}: {
chart: ChartDocument | null;
song: SongDocument;
} & GamePT) {
} & GameProps) {
const formatSongTitle = `${song.artist} - ${song.title}`;
const formatDiff = chart ? FormatDifficultyLong(chart) : "Loading...";
useSetSubheader(
[
"Games",
GetGameGroupConfig(GameToGameGroup(game)).name,
"Songs",
formatSongTitle,
formatDiff,
],
["Games", FormatGame(game), "Charts", formatSongTitle, formatDiff],
[game, chart],
`${formatSongTitle} (${formatDiff})`,
);
@@ -72,23 +65,23 @@ export default function GPTChartPage({
return <Loading />;
}
return <InternalGPTChartPage chart={chart} game={game} song={song} />;
return <InternalGameChartPage chart={chart} game={game} song={song} />;
}
interface ChartPBData {
leaderboard: ChartPBLeaderboardReturn;
adjacent?: UGPTChartLeaderboardAdjacent;
adjacent?: UserGameChartLeaderboardAdjacent;
rivals?: ChartRivalsReturn;
}
function InternalGPTChartPage({
function InternalGameChartPage({
chart,
game,
song,
}: {
chart: ChartDocument;
song: SongDocument;
} & GamePT) {
} & GameProps) {
const { user } = useContext(UserContext);
const { data, error } = useQuery<ChartPBData, UnsuccessfulAPIFetchResponse>(
@@ -103,7 +96,7 @@ function InternalGPTChartPage({
}
if (user) {
const nRes = await APIFetchV1<UGPTChartLeaderboardAdjacent>(
const nRes = await APIFetchV1<UserGameChartLeaderboardAdjacent>(
`/users/${user.id}/games/${game}/pbs/${chart.chartID}/leaderboard-adjacent`,
);
@@ -256,7 +249,7 @@ function ChartTargetInfo({
chart: ChartDocument;
song: SongDocument;
user: UserDocument;
} & GamePT) {
} & GameProps) {
const { reloadTargets } = useContext(TargetsContext);
const [shouldReload, setShouldReload] = useState(0);
@@ -302,8 +295,8 @@ function ChartLeaderboardTable({
song: SongDocument;
user: UserDocument | null;
userMap: Map<integer, UserDocument>;
} & GamePT) {
const { settings } = useLUGPTSettings();
} & GameProps) {
const { settings } = useLoggedInUserGameSettings();
const dataset: PBDataset = useMemo(() => {
const ds: PBDataset = [];
@@ -421,7 +414,7 @@ function PlayCard({
className="flex-grow-1 align-items-lg-start align-items-center justify-content-around"
direction={isLg ? "horizontal" : "vertical"}
>
<ProfilePicture toGPT={{ game: pbGame }} user={user} />
<ProfilePicture toGame={{ game: pbGame }} user={user} />
<div
className="d-flex flex-column align-self-stretch justify-content-between align-items-center"
style={{ maxHeight: 128, minWidth: 256 }}
@@ -8,26 +8,15 @@ import DebounceSearch from "#components/util/DebounceSearch";
import Divider from "#components/util/Divider";
import Loading from "#components/util/Loading";
import useApiQuery from "#components/util/query/useApiQuery";
import { type GamePT } from "#types/react";
import { type GameProps } from "#types/react";
import { CreateSongMap } from "#util/data";
import { NumericSOV, StrSOV } from "#util/sorts";
import React, { useState } from "react";
import { Col, Row } from "react-bootstrap";
import {
type ChartDocument,
FormatGame,
GameToGameGroup,
GetGameGroupConfig,
type integer,
type SongDocument,
} from "tachi-common";
import { type ChartDocument, FormatGame, type integer, type SongDocument } from "tachi-common";
export default function GPTChartsPage({ game }: GamePT) {
useSetSubheader(
["Games", GetGameGroupConfig(GameToGameGroup(game)).name, "Songs"],
[game],
`${FormatGame(game)} Songs`,
);
export default function GameChartsPage({ game }: GameProps) {
useSetSubheader(["Games", FormatGame(game), "Charts"], [game], `${FormatGame(game)} Songs`);
const [search, setSearch] = useState("");
@@ -44,7 +33,7 @@ export default function GPTChartsPage({ game }: GamePT) {
);
}
function SearchSongsTable({ game, search }: { search: string } & GamePT) {
function SearchSongsTable({ game, search }: { search: string } & GameProps) {
const params = new URLSearchParams({ search });
const { data, error } = useApiQuery<{
@@ -3,20 +3,12 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Card from "#components/layout/page/Card";
import MiniTable from "#components/tables/components/MiniTable";
import DebugContent from "#components/util/DebugContent";
import { type GamePT } from "#types/react";
import React from "react";
import {
type Classes,
FormatGame,
GameToGameGroup,
GetGameConfig,
GetGameGroupConfig,
type V3Game,
} from "tachi-common";
import { type GameProps } from "#types/react";
import { type Classes, FormatGame, GetGameConfig, type V3Game } from "tachi-common";
export default function GPTDevInfo({ game }: GamePT) {
export default function GameDevInfo({ game }: GameProps) {
useSetSubheader(
["Games", GetGameGroupConfig(GameToGameGroup(game)).name, "Dev Info"],
["Games", FormatGame(game), "Dev Info"],
[game],
`${FormatGame(game)} Dev Info`,
);
@@ -25,7 +17,7 @@ export default function GPTDevInfo({ game }: GamePT) {
return (
<>
<Card header="GPT Configuration">
<Card header="Game Configuration">
<DebugContent data={gameGroupConfig} />
</Card>
<Card className="mt-4" header="Class Badges">
@@ -13,29 +13,22 @@ import useApiQuery from "#components/util/query/useApiQuery";
import SelectButton from "#components/util/SelectButton";
import { useProfileRatingAlg } from "#components/util/useScoreRatingAlg";
import { type UserLeaderboardReturns } from "#types/api-returns";
import { type GamePT } from "#types/react";
import { type GameProps } from "#types/react";
import { type UGSDataset } from "#types/tables";
import { CreateUserMap } from "#util/data";
import {
FormatGPTProfileRating,
FormatGPTProfileRatingName,
FormatGameProfileRating,
FormatGameProfileRatingName,
getProfileRatingAlgKeysInDisplayOrder,
} from "#util/misc";
import { NumericSOV, StrSOV } from "#util/sorts";
import React, { useState } from "react";
import { Col, Form, Row } from "react-bootstrap";
import {
type AnyProfileRatingAlg,
type Classes,
FormatGame,
GameToGameGroup,
GetGameGroupConfig,
type V3Game,
} from "tachi-common";
import { type AnyProfileRatingAlg, type Classes, FormatGame, type V3Game } from "tachi-common";
export default function GPTLeaderboardsPage({ game }: GamePT) {
export default function GameLeaderboardsPage({ game }: GameProps) {
useSetSubheader(
["Games", GetGameGroupConfig(GameToGameGroup(game)).name, "Leaderboards"],
["Games", FormatGame(game), "Leaderboards"],
[game],
`${FormatGame(game)} Leaderboards`,
);
@@ -66,7 +59,7 @@ export default function GPTLeaderboardsPage({ game }: GamePT) {
);
}
function ProfileLeaderboard({ game }: GamePT) {
function ProfileLeaderboard({ game }: GameProps) {
const defaultAlg = useProfileRatingAlg(game);
const [alg, setAlg] = useState(defaultAlg);
@@ -80,7 +73,7 @@ function ProfileLeaderboard({ game }: GamePT) {
<Form.Select onChange={(e) => setAlg(e.target.value as any)} value={alg}>
{profileAlgKeys.map((e) => (
<option key={e} value={e}>
{FormatGPTProfileRatingName(game, e)}
{FormatGameProfileRatingName(game, e)}
</option>
))}
</Form.Select>
@@ -135,8 +128,8 @@ function ProfileLeaderboard({ game }: GamePT) {
...profileAlgKeys.map(
(e) =>
[
FormatGPTProfileRatingName(game, e),
FormatGPTProfileRatingName(game, e),
FormatGameProfileRatingName(game, e),
FormatGameProfileRatingName(game, e),
NumericSOV((x) => x.ratings[e] ?? -Infinity),
] as Header<UGSDataset[0]>,
),
@@ -149,7 +142,7 @@ function ProfileLeaderboard({ game }: GamePT) {
{profileAlgKeys.map((e) => (
<td key={e}>
{r.ratings[e]
? FormatGPTProfileRating(game, e, r.ratings[e]!)
? FormatGameProfileRating(game, e, r.ratings[e]!)
: "No Data."}
</td>
))}
@@ -1,10 +1,9 @@
import Activity from "#components/activity/Activity";
import useSetSubheader from "#components/layout/header/useSetSubheader";
import { type GamePT } from "#types/react";
import React from "react";
import { type GameProps } from "#types/react";
import { FormatGame, GameToGameGroup, GetGameGroupConfig } from "tachi-common";
export default function GPTMainPage({ game }: GamePT) {
export default function GameMainPage({ game }: GameProps) {
useSetSubheader(
["Games", GetGameGroupConfig(GameToGameGroup(game)).name],
[game],
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
export default function AquaArtemisExport() {
useSetSubheader(["Import Scores", "Aqua/ARTEMiS Exporter"]);
@@ -1,7 +1,6 @@
import ImportSQLiteForm from "#components/imports/ImportSQLiteForm";
import useSetSubheader from "#components/layout/header/useSetSubheader";
import { convertArcaeaDB } from "#util/db-converters/arcaea";
import React from "react";
import { Alert } from "react-bootstrap";
export default function ArcaeaST3Page() {
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
import Alert from "react-bootstrap/Alert";
export default function BarbatosPage() {
@@ -2,7 +2,6 @@ import ImportFileInfo from "#components/imports/ImportFileInfo";
import useSetSubheader from "#components/layout/header/useSetSubheader";
import { TachiConfig } from "#lib/config";
import { p } from "prudence";
import React from "react";
import {
ALL_GAMES,
type BatchManual,
@@ -3,7 +3,6 @@ import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import Muted from "#components/util/Muted";
import { TachiConfig } from "#lib/config";
import React from "react";
import Alert from "react-bootstrap/Alert";
const WIN_BAT = `
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
import Alert from "react-bootstrap/Alert";
export default function ChunitachiPage() {
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
export default function ChunithmMYTExport() {
useSetSubheader(["Import Scores", "CHUNITHM MYT Exporter"]);
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
export default function ChunithmSiteImportPage() {
useSetSubheader(["Import Scores", "CHUNITHM Site Importer"]);
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
import Alert from "react-bootstrap/Alert";
import { Link } from "react-router-dom";
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
export default function IIDXCGSiteImportPage() {
useSetSubheader(["Import Scores", "IIDX CG Site Importer"]);
@@ -3,7 +3,6 @@ import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import Muted from "#components/util/Muted";
import { TachiConfig } from "#lib/config";
import React from "react";
import { Alert } from "react-bootstrap";
export default function ITGHookPage() {
@@ -1,4 +1,4 @@
import { ErrorPage } from "#app/pages/ErrorPage";
import ErrorPage from "#app/pages/ErrorPage";
import ClassBadge from "#components/game/ClassBadge";
import ImportClassImportState from "#components/imports/ImportClassImportState";
import useSetSubheader from "#components/layout/header/useSetSubheader";
@@ -6,9 +6,9 @@ import useImport from "#components/util/import/useImport";
import Loading from "#components/util/Loading";
import useApiQuery from "#components/util/query/useApiQuery";
import { UserContext } from "#context/UserContext";
import { type UGPTStatsReturn } from "#types/api-returns";
import { type UserGameStatsReturn } from "#types/api-returns";
import { UppercaseFirst } from "#util/misc";
import React, { useContext, useEffect, useMemo, useState } from "react";
import { useContext, useEffect, useMemo, useState } from "react";
import { Alert, Button, Col, Form, Row } from "react-bootstrap";
import { useHistory } from "react-router-dom";
import {
@@ -82,7 +82,7 @@ function InnerImportClassPage({ game, userID }: { game: V3Game; userID: number }
{},
);
const { data, error, isLoading } = useApiQuery<UGPTStatsReturn>(
const { data, error, isLoading } = useApiQuery<UserGameStatsReturn>(
`/users/${userID}/games/${game}`,
);
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
import Alert from "react-bootstrap/Alert";
export default function KsHookPage() {
@@ -3,7 +3,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import Muted from "#components/util/Muted";
import { convertLR2Db } from "#util/db-converters/lr2";
import React from "react";
export default function LR2DBPage() {
useSetSubheader(["Import Scores", "LR2 Database File"]);
@@ -3,7 +3,6 @@ import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import Muted from "#components/util/Muted";
import { TachiConfig } from "#lib/config";
import React from "react";
import { Alert } from "react-bootstrap";
export default function LR2HookPage() {
@@ -3,7 +3,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import Muted from "#components/util/Muted";
import { convertBeatorajaDb } from "#util/db-converters/beatoraja";
import React from "react";
import { Link } from "react-router-dom";
export default function LR2orajaDBPage() {
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
export default function MaimaiDXSiteImportPage() {
useSetSubheader(["Import Scores", "maimai DX Site Importer"]);
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
import Alert from "react-bootstrap/Alert";
export default function MikadoPage() {
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
export default function OngekiArtemisExportPage() {
useSetSubheader(["Import Scores", "O.N.G.E.K.I. ARTEMiS Exporter"]);
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
import Alert from "react-bootstrap/Alert";
export default function OngekiInoharaPage() {
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
export default function OngekiSiteImportPage() {
useSetSubheader(["Import Scores", "O.N.G.E.K.I. Site Importer"]);
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
import Alert from "react-bootstrap/Alert";
export default function RizuPage() {
@@ -1,6 +1,5 @@
import ImportFileInfo from "#components/imports/ImportFileInfo";
import useSetSubheader from "#components/layout/header/useSetSubheader";
import React from "react";
import { type FileUploadImportTypes } from "tachi-common";
export default function SDVXEamCSVPage({
@@ -1,6 +1,5 @@
import ImportFileInfo from "#components/imports/ImportFileInfo";
import useSetSubheader from "#components/layout/header/useSetSubheader";
import React from "react";
export default function SSSXMLPage() {
useSetSubheader(["Import Scores", "SSS .xml"]);
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { TachiConfig } from "#lib/config";
import React from "react";
import Alert from "react-bootstrap/Alert";
export default function SaekawaPage() {
@@ -3,7 +3,6 @@ import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import Muted from "#components/util/Muted";
import { TachiConfig } from "#lib/config";
import React from "react";
import Alert from "react-bootstrap/Alert";
export default function SilentHookPage() {
@@ -2,7 +2,6 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import { ToServerURL } from "#util/api";
import React from "react";
import { Alert } from "react-bootstrap";
export default function USCIRPage() {
@@ -2,7 +2,6 @@ import ImportFileInfo from "#components/imports/ImportFileInfo";
import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import ExternalLink from "#components/util/ExternalLink";
import React from "react";
function RecordsParseFunction(data: string) {
if (
@@ -1,5 +1,4 @@
import { TachiConfig } from "#lib/config";
import React from "react";
import { type UserDocument } from "tachi-common";
export default function SupportBanner({ user }: { user: UserDocument }) {
@@ -1,4 +1,4 @@
import { ErrorPage } from "#app/pages/ErrorPage";
import ErrorPage from "#app/pages/ErrorPage";
import useSetSubheader from "#components/layout/header/useSetSubheader";
import NotificationRow from "#components/notifications/NotificationRow";
import MiniTable from "#components/tables/components/MiniTable";
@@ -6,7 +6,7 @@ import Loading from "#components/util/Loading";
import { NotificationsContext } from "#context/NotificationsContext";
import { UserContext } from "#context/UserContext";
import { APIFetchV1 } from "#util/api";
import React, { useContext, useEffect, useMemo } from "react";
import { useContext, useEffect, useMemo } from "react";
import { Button, Col, Row } from "react-bootstrap";
import { type UserDocument } from "tachi-common";
@@ -1,6 +1,5 @@
import useSetSubheader from "#components/layout/header/useSetSubheader";
import UGPTProfiles from "#components/user/UGPTProfiles";
import React from "react";
import UserGameProfiles from "#components/user/UserGameProfiles";
import { type UserDocument } from "tachi-common";
export default function UserGamesPage({ reqUser }: { reqUser: UserDocument }) {
@@ -10,5 +9,5 @@ export default function UserGamesPage({ reqUser }: { reqUser: UserDocument }) {
`${reqUser.username}'s Game Profiles`,
);
return <UGPTProfiles reqUser={reqUser} />;
return <UserGameProfiles reqUser={reqUser} />;
}
@@ -9,7 +9,6 @@ import useApiQuery from "#components/util/query/useApiQuery";
import { APIFetchV1 } from "#util/api";
import { CopyToClipboard } from "#util/misc";
import { FormatTime } from "#util/time";
import React from "react";
import { Button, Col, Row } from "react-bootstrap";
import { useQueryClient } from "react-query";
import { Link } from "react-router-dom";
@@ -6,13 +6,13 @@ import GentleLink from "#components/util/GentleLink";
import LinkButton from "#components/util/LinkButton";
import LoadingWrapper from "#components/util/LoadingWrapper";
import { useProfileRatingAlg } from "#components/util/useScoreRatingAlg";
import { type GPTLeaderboard, type UGPTLeaderboardAdjacent } from "#types/api-returns";
import { type GamePT, type SetState, type UGPT } from "#types/react";
import { type GameLeaderboard, type UserGameLeaderboardAdjacent } from "#types/api-returns";
import { type GameProfileProps, type GameProps, type SetState } from "#types/react";
import { APIFetchV1, type UnsuccessfulAPIFetchResponse } from "#util/api";
import { ChangeOpacity } from "#util/color-opacity";
import { FormatGPTProfileRating, FormatGPTProfileRatingName, IsNotNullish } from "#util/misc";
import { FormatGameProfileRating, FormatGameProfileRatingName, IsNotNullish } from "#util/misc";
import { StrSOV } from "#util/sorts";
import React, { useState } from "react";
import { useState } from "react";
import { useQuery } from "react-query";
import {
type Classes,
@@ -29,11 +29,11 @@ import {
} from "tachi-common";
interface LeaderboardsData {
stats: UGPTLeaderboardAdjacent;
leaderboard: GPTLeaderboard;
stats: UserGameLeaderboardAdjacent;
leaderboard: GameLeaderboard;
}
export default function LeaderboardsPage({ reqUser, game }: UGPT) {
export default function LeaderboardsPage({ reqUser, game }: GameProfileProps) {
useSetSubheader(
[
"Users",
@@ -54,13 +54,13 @@ export default function LeaderboardsPage({ reqUser, game }: UGPT) {
const { data, error } = useQuery<LeaderboardsData, UnsuccessfulAPIFetchResponse>(
url,
async () => {
const res = await APIFetchV1<UGPTLeaderboardAdjacent>(url);
const res = await APIFetchV1<UserGameLeaderboardAdjacent>(url);
if (!res.success) {
throw res;
}
const lRes = await APIFetchV1<GPTLeaderboard>(
const lRes = await APIFetchV1<GameLeaderboard>(
`/games/${game}/leaderboard?limit=3&alg=${alg}`,
);
@@ -92,7 +92,7 @@ function LeaderboardsPageContent({
data: LeaderboardsData;
reqUser: UserDocument;
setAlg: SetState<ProfileRatingAlgorithms[V3Game]>;
} & GamePT) {
} & GameProps) {
const { stats, leaderboard } = data;
const gameConfig = GetGameConfig(game);
@@ -135,7 +135,7 @@ function LeaderboardsPageContent({
</td>
<td>
{IsNotNullish(s.ratings[alg])
? FormatGPTProfileRating(game, alg, s.ratings[alg]!)
? FormatGameProfileRating(game, alg, s.ratings[alg]!)
: "No Data."}
</td>
<td>
@@ -171,7 +171,7 @@ function LeaderboardsPageContent({
>
<MiniTable
className="text-center"
headers={["Position", "User", FormatGPTProfileRatingName(game, alg), "Classes"]}
headers={["Position", "User", FormatGameProfileRatingName(game, alg), "Classes"]}
>
<>
{bestNearbyUser >= 1 &&
@@ -3,7 +3,7 @@ import ChartTooltip from "#components/charts/ChartTooltip";
import TimelineChart from "#components/charts/TimelineChart";
import useSetSubheader from "#components/layout/header/useSetSubheader";
import Card from "#components/layout/page/Card";
import UGPTStatShowcase from "#components/user/UGPTStatShowcase";
import UserGameStatShowcase from "#components/user/UserGameStatShowcase";
import ApiError from "#components/util/ApiError";
import Divider from "#components/util/Divider";
import Icon from "#components/util/Icon";
@@ -13,11 +13,11 @@ import useApiQuery from "#components/util/query/useApiQuery";
import Select from "#components/util/Select";
import SelectButton from "#components/util/SelectButton";
import { useProfileRatingAlg } from "#components/util/useScoreRatingAlg";
import { type UGPTHistory } from "#types/api-returns";
import { type GamePT, type SetState, type UGPT } from "#types/react";
import { type UserGameHistory } from "#types/api-returns";
import { type GameProfileProps, type GameProps, type SetState } from "#types/react";
import {
FormatGPTProfileRating,
FormatGPTProfileRatingName,
FormatGameProfileRating,
FormatGameProfileRatingName,
getProfileRatingAlgKeysInDisplayOrder,
UppercaseFirst,
} from "#util/misc";
@@ -27,7 +27,7 @@ import React, { useMemo, useState } from "react";
import FormSelect from "react-bootstrap/FormSelect";
import { FormatGame, GetGameConfig, type UserGameStats, type V3Game } from "tachi-common";
export default function OverviewPage({ reqUser, game }: UGPT) {
export default function OverviewPage({ reqUser, game }: GameProfileProps) {
useSetSubheader(
["Users", reqUser.username, "Games", FormatGame(game)],
[reqUser, game],
@@ -36,14 +36,14 @@ export default function OverviewPage({ reqUser, game }: UGPT) {
return (
<React.Fragment key={game}>
<UGPTStatShowcase game={game} reqUser={reqUser} />
<UserGameStatShowcase game={game} reqUser={reqUser} />
<RankingInfo game={game} reqUser={reqUser} />
<RecentActivity game={game} reqUser={reqUser} />
</React.Fragment>
);
}
function RecentActivity({ reqUser, game }: UGPT) {
function RecentActivity({ reqUser, game }: GameProfileProps) {
return (
<div className="mt-4">
<Activity handleNoActivity={null} url={`/users/${reqUser.id}/games/${game}/activity`} />
@@ -53,10 +53,10 @@ function RecentActivity({ reqUser, game }: UGPT) {
type RankingDurations = "3mo" | "all" | "month" | "week" | "year";
function RankingInfo({ reqUser, game }: UGPT) {
function RankingInfo({ reqUser, game }: GameProfileProps) {
const [duration, setDuration] = useState<RankingDurations>("3mo");
const { data, error } = useApiQuery<UGPTHistory>(
const { data, error } = useApiQuery<UserGameHistory>(
`/users/${reqUser.id}/games/${game}/history?duration=${duration}`,
);
@@ -84,10 +84,10 @@ function UserHistory({
duration,
setDuration,
}: {
data: UGPTHistory;
data: UserGameHistory;
duration: RankingDurations;
setDuration: SetState<RankingDurations>;
} & GamePT) {
} & GameProps) {
const gameConfig = GetGameConfig(game);
const [mode, setMode] = useState<"playcount" | "ranking" | "rating">("rating");
@@ -98,9 +98,9 @@ function UserHistory({
const propName = useMemo(() => {
if (mode === "rating" && rating) {
return FormatGPTProfileRatingName(game, rating);
return FormatGameProfileRatingName(game, rating);
} else if (mode === "ranking") {
return `${FormatGPTProfileRatingName(game, rating)} Ranking`;
return `${FormatGameProfileRatingName(game, rating)} Ranking`;
}
return UppercaseFirst(mode);
@@ -114,7 +114,7 @@ function UserHistory({
return "N/A";
}
return FormatGPTProfileRating(game, rating, ratingValue);
return FormatGameProfileRating(game, rating, ratingValue);
} else if (mode === "ranking") {
return (
<>
@@ -173,7 +173,7 @@ function UserHistory({
>
{getProfileRatingAlgKeysInDisplayOrder(game).map((e) => (
<option key={e} value={e}>
{FormatGPTProfileRatingName(game, e)}
{FormatGameProfileRatingName(game, e)}
</option>
))}
</FormSelect>
@@ -226,7 +226,7 @@ function UserHistory({
>
{getProfileRatingAlgKeysInDisplayOrder(game).map((e) => (
<option key={e} value={e}>
{FormatGPTProfileRatingName(game, e)}
{FormatGameProfileRatingName(game, e)}
</option>
))}
</FormSelect>
@@ -245,7 +245,7 @@ function RatingTimeline({
data,
rating,
}: {
data: UGPTHistory;
data: UserGameHistory;
game: V3Game;
rating: keyof UserGameStats["ratings"];
}) {
@@ -263,7 +263,7 @@ function RatingTimeline({
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
format: (y) => (y ? FormatGPTProfileRating(game, rating, y) : "N/A"),
format: (y) => (y ? FormatGameProfileRating(game, rating, y) : "N/A"),
}}
data={ratingDataset}
height="30rem"
@@ -272,9 +272,9 @@ function RatingTimeline({
<ChartTooltip>
<div>
{p.point.data.y
? FormatGPTProfileRating(game, rating, p.point.data.y as number)
? FormatGameProfileRating(game, rating, p.point.data.y as number)
: "N/A"}{" "}
{FormatGPTProfileRatingName(game, rating)}
{FormatGameProfileRatingName(game, rating)}
</div>
<small className="text-body-secondary">
{MillisToSince(+p.point.data.xFormatted)}
@@ -289,7 +289,7 @@ function RankingTimeline({
data,
rating,
}: {
data: UGPTHistory;
data: UserGameHistory;
rating: keyof UserGameStats["ratings"];
}) {
return (
@@ -8,10 +8,10 @@ import useApiQuery from "#components/util/query/useApiQuery";
import SelectLinkButton from "#components/util/SelectLinkButton";
import usePreferredRanking from "#components/util/usePreferredRanking";
import useScoreRatingAlg from "#components/util/useScoreRatingAlg";
import useUGPTBase from "#components/util/useUGPTBase";
import { type GamePT, type SetState, type UGPT } from "#types/react";
import { FormatGPTScoreRatingName } from "#util/misc";
import React, { useState } from "react";
import useUserGameBase from "#components/util/useUserGameBase";
import { type GameProfileProps, type GameProps, type SetState } from "#types/react";
import { FormatGameScoreRatingName } from "#util/misc";
import { useState } from "react";
import { Col, Form, Row } from "react-bootstrap";
import { Route, Switch } from "react-router-dom";
import {
@@ -34,7 +34,7 @@ export default function ScoresPage({
game,
}: {
reqUser: UserDocument;
} & GamePT) {
} & GameProps) {
const gameConfig = GetGameConfig(game);
const defaultRating = useScoreRatingAlg(game);
@@ -53,7 +53,7 @@ export default function ScoresPage({
`${reqUser.username}'s ${FormatGame(game)} Scores`,
);
const base = useUGPTBase({ reqUser, game });
const base = useUserGameBase({ reqUser, game });
return (
<Row xs={{ cols: 1 }}>
@@ -121,7 +121,7 @@ function AlgSelector({
}: {
alg: ScoreRatingAlgorithms[V3Game];
setAlg: SetState<ScoreRatingAlgorithms[V3Game]>;
} & GamePT) {
} & GameProps) {
const gameConfig = GetGameConfig(game);
return (
<Form.Group className="d-flex flex-column gap-1">
@@ -129,7 +129,7 @@ function AlgSelector({
<Form.Select onChange={(e) => setAlg(e.target.value as any)} value={alg}>
{Object.keys(gameConfig.scoreRatingAlgs).map((e) => (
<option key={e} value={e}>
{FormatGPTScoreRatingName(game, e)}
{FormatGameScoreRatingName(game, e)}
</option>
))}
</Form.Select>
@@ -163,7 +163,7 @@ function PBsOverview({
reqUser: UserDocument;
showPlaycount?: boolean;
url: string;
} & GamePT) {
} & GameProps) {
const [search, setSearch] = useState("");
const { data, error } = useFetchPBs(url, reqUser);
@@ -247,7 +247,7 @@ function PBsSearch({
alg?: ScoreRatingAlgorithms[V3Game];
reqUser: UserDocument;
search: string;
} & GamePT) {
} & GameProps) {
const { data, error } = useFetchPBs(
`/users/${reqUser.id}/games/${game}/pbs?search=${search}`,
reqUser,
@@ -260,7 +260,7 @@ function PBsSearch({
);
}
function ScoresOverview({ reqUser, game }: UGPT) {
function ScoresOverview({ reqUser, game }: GameProfileProps) {
const [search, setSearch] = useState("");
const { data, error } = useFetchScores(
@@ -294,7 +294,7 @@ function ScoresSearch({
reqUser,
game,
search,
}: { reqUser: UserDocument; search: string } & GamePT) {
}: { reqUser: UserDocument; search: string } & GameProps) {
const { data, error } = useFetchScores(
`/users/${reqUser.id}/games/${game}/scores?search=${search}`,
reqUser,
@@ -9,7 +9,7 @@ import Icon from "#components/util/Icon";
import LoadingWrapper from "#components/util/LoadingWrapper";
import SelectButton from "#components/util/SelectButton";
import { useSessionRatingAlg } from "#components/util/useScoreRatingAlg";
import { type GamePT, type UGPT } from "#types/react";
import { type GameProfileProps, type GameProps } from "#types/react";
import { APIFetchV1 } from "#util/api";
import { NumericSOV } from "#util/sorts";
import React, { useState } from "react";
@@ -23,7 +23,7 @@ import {
type UserDocument,
} from "tachi-common";
export default function SessionsPage({ reqUser, game }: UGPT) {
export default function SessionsPage({ reqUser, game }: GameProfileProps) {
const [sessionSet, setSessionSet] = useState<"best" | "highlighted" | "recent">("best");
const [search, setSearch] = useState("");
@@ -128,7 +128,7 @@ function SearchSessionsTable({
game,
reqUser,
baseUrl,
}: { baseUrl: string; reqUser: UserDocument; search: string } & GamePT) {
}: { baseUrl: string; reqUser: UserDocument; search: string } & GameProps) {
const { data, error } = useQuery<SessionDataset, UnsuccessfulAPIResponse>(
`${baseUrl}?search=${search}`,
async () => {
@@ -12,7 +12,7 @@ import useApiQuery from "#components/util/query/useApiQuery";
import { UserContext } from "#context/UserContext";
import { UserSettingsContext } from "#context/UserSettingsContext";
import { type SessionAdjacentReturns, type SessionReturns } from "#types/api-returns";
import { type UGPT } from "#types/react";
import { type GameProfileProps } from "#types/react";
import { APIFetchV1 } from "#util/api";
import { CreateChartMap, CreateScoreIDMap, CreateSongMap } from "#util/data";
import React, { useContext, useMemo, useState } from "react";
@@ -20,7 +20,7 @@ import { Badge, Button, Col, Row } from "react-bootstrap";
import { Redirect, useParams } from "react-router-dom";
import { GameToGameGroup, GetGameGroupConfig, type SessionDocument } from "tachi-common";
export default function SpecificSessionPage({ reqUser, game }: UGPT) {
export default function SpecificSessionPage({ reqUser, game }: GameProfileProps) {
const { sessionID } = useParams<{ sessionID: string }>();
const { data, error } = useApiQuery<SessionReturns>(`/sessions/${sessionID}`);
@@ -46,7 +46,7 @@ export default function SpecificSessionPage({ reqUser, game }: UGPT) {
return <SessionPage key={data.session.sessionID} {...{ data, game, reqUser }} />;
}
function SessionPage({ data, game }: { data: SessionReturns } & UGPT) {
function SessionPage({ data, game }: { data: SessionReturns } & GameProfileProps) {
const { settings } = useContext(UserSettingsContext);
const [sessionData, setSessionData] = useState(data);
@@ -1,32 +1,32 @@
import { ErrorPage } from "#app/pages/ErrorPage";
import ErrorPage from "#app/pages/ErrorPage";
import useSetSubheader from "#components/layout/header/useSetSubheader";
import Card from "#components/layout/page/Card";
import UGPTStatContainer from "#components/user/UGPTStatContainer";
import UGPTStatCreator from "#components/user/UGPTStatCreator";
import UserGameStatContainer from "#components/user/UserGameStatContainer";
import UserGameStatCreator from "#components/user/UserGameStatCreator";
import ApiError from "#components/util/ApiError";
import Divider from "#components/util/Divider";
import Icon from "#components/util/Icon";
import Loading from "#components/util/Loading";
import Muted from "#components/util/Muted";
import { type UGPTData } from "#components/util/query/fetchUGPTData";
import { type UserGameData } from "#components/util/query/fetchUserGameData";
import useApiQuery from "#components/util/query/useApiQuery";
import SelectButton from "#components/util/SelectButton";
import useQueryString from "#components/util/useQueryString";
import { UGPTContext } from "#context/UGPTContext";
import { UserGameContext } from "#context/UserGameContext";
import { TachiConfig } from "#lib/config";
import { type SetState, type UGPT } from "#types/react";
import { type GameProfileProps, type SetState } from "#types/react";
import { APIFetchV1 } from "#util/api";
import {
FormatGPTProfileRatingName,
FormatGPTScoreRatingName,
FormatGPTSessionRatingName,
FormatGameProfileRatingName,
FormatGameScoreRatingName,
FormatGameSessionRatingName,
getProfileRatingAlgKeysInDisplayOrder,
ToFixedFloor,
UppercaseFirst,
} from "#util/misc";
import deepmerge from "deepmerge";
import { useFormik } from "formik";
import React, { useContext, useEffect, useState } from "react";
import { useContext, useEffect, useState } from "react";
import { Alert, Button, Col, Form, Row } from "react-bootstrap";
import { Link } from "react-router-dom";
import {
@@ -38,17 +38,16 @@ import {
GetScoreMetrics,
type ShowcaseStatDetails,
type TableDocument,
type UGPTSettingsDocument,
type UserDocument,
type UserGameSettingsDocument,
} from "tachi-common";
export default function UGPTSettingsPage({ reqUser, game }: UGPT) {
export default function UserGameSettingsPage({ reqUser, game }: GameProfileProps) {
const query = useQueryString();
const [page, setPage] = useState<"manage" | "preferences" | "showcase">(
query.get("showcase") ? "showcase" : "preferences",
);
const gameConfig = GetGameConfig(game);
useSetSubheader(
[
@@ -62,9 +61,7 @@ export default function UGPTSettingsPage({ reqUser, game }: UGPT) {
`${reqUser.username}'s ${FormatGame(game)} Settings`,
);
const UGPT = { reqUser, game };
const { loggedInData } = useContext(UGPTContext);
const { loggedInData } = useContext(UserGameContext);
if (!loggedInData) {
return (
<ErrorPage
@@ -110,8 +107,9 @@ export default function UGPTSettingsPage({ reqUser, game }: UGPT) {
{page === "preferences" ? (
loggedInData.settings ? (
<PreferencesForm
{...UGPT}
game={game}
loggedInData={{ ...loggedInData, settings: loggedInData.settings }}
reqUser={reqUser}
/>
) : (
<div className="text-center">
@@ -124,8 +122,9 @@ export default function UGPTSettingsPage({ reqUser, game }: UGPT) {
) : page === "showcase" ? (
loggedInData.settings ? (
<ShowcaseForm
{...UGPT}
game={game}
loggedInData={{ ...loggedInData, settings: loggedInData.settings }}
reqUser={reqUser}
/>
) : (
<div className="text-center">
@@ -136,7 +135,7 @@ export default function UGPTSettingsPage({ reqUser, game }: UGPT) {
</div>
)
) : (
<ManageAccount {...UGPT} />
<ManageAccount game={game} reqUser={reqUser} />
)}
</div>
</div>
@@ -148,8 +147,8 @@ function PreferencesForm({
reqUser,
game,
loggedInData,
}: { loggedInData: { settings: UGPTSettingsDocument } & UGPTData } & UGPT) {
const { setLoggedInData } = useContext(UGPTContext);
}: { loggedInData: { settings: UserGameSettingsDocument } & UserGameData } & GameProfileProps) {
const { setLoggedInData } = useContext(UserGameContext);
const settings = loggedInData.settings;
@@ -190,7 +189,7 @@ function PreferencesForm({
if (rj.success) {
setLoggedInData({
...loggedInData,
settings: deepmerge(settings as UGPTSettingsDocument, {
settings: deepmerge(settings as UserGameSettingsDocument, {
preferences: values,
}),
});
@@ -228,7 +227,7 @@ function PreferencesForm({
>
{Object.keys(gameConfig.scoreRatingAlgs).map((e) => (
<option key={e} value={e}>
{FormatGPTScoreRatingName(game, e)}
{FormatGameScoreRatingName(game, e)}
</option>
))}
</Form.Select>
@@ -248,7 +247,7 @@ function PreferencesForm({
>
{Object.keys(gameConfig.sessionRatingAlgs).map((e) => (
<option key={e} value={e}>
{FormatGPTSessionRatingName(game, e)}
{FormatGameSessionRatingName(game, e)}
</option>
))}
</Form.Select>
@@ -268,7 +267,7 @@ function PreferencesForm({
>
{getProfileRatingAlgKeysInDisplayOrder(game).map((e) => (
<option key={e} value={e}>
{FormatGPTProfileRatingName(game, e)}
{FormatGameProfileRatingName(game, e)}
</option>
))}
</Form.Select>
@@ -424,8 +423,8 @@ function ShowcaseForm({
reqUser,
game,
loggedInData,
}: { loggedInData: { settings: UGPTSettingsDocument } & UGPTData } & UGPT) {
const { setLoggedInData } = useContext(UGPTContext);
}: { loggedInData: { settings: UserGameSettingsDocument } & UserGameData } & GameProfileProps) {
const { setLoggedInData } = useContext(UserGameContext);
const settings = loggedInData.settings;
@@ -433,7 +432,7 @@ function ShowcaseForm({
const [show, setShow] = useState(false);
const SaveChanges = async () => {
const r = await APIFetchV1<UGPTSettingsDocument>(
const r = await APIFetchV1<UserGameSettingsDocument>(
`/users/${reqUser.id}/games/${game}/showcase`,
{
method: "PUT",
@@ -472,7 +471,7 @@ function ShowcaseForm({
</div>
)}
<RenderCurrentStats {...{ reqUser, game, stats, setStats }} />
<UGPTStatCreator
<UserGameStatCreator
game={game}
onCreate={(stat) => {
setStats([...stats, stat]);
@@ -493,7 +492,7 @@ function RenderCurrentStats({
}: {
setStats: SetState<ShowcaseStatDetails[]>;
stats: ShowcaseStatDetails[];
} & UGPT) {
} & GameProfileProps) {
function RemoveStatAtIndex(index: number) {
setStats(stats.filter((e, i) => i !== index));
}
@@ -510,7 +509,7 @@ function RenderCurrentStats({
<Row className="w-100 row-gap-4" lg={{ cols: 2 }}>
{stats.map((e, i) => (
<Col className="d-flex flex-column gap-4" key={i}>
<UGPTStatContainer game={game} reqUser={reqUser} stat={e} />
<UserGameStatContainer game={game} reqUser={reqUser} stat={e} />
<Button className="w-100" onClick={() => RemoveStatAtIndex(i)} variant="danger">
Delete
</Button>
@@ -520,7 +519,7 @@ function RenderCurrentStats({
);
}
function ManageAccount({ reqUser, game }: UGPT) {
function ManageAccount({ reqUser, game }: GameProfileProps) {
const [password, setPassword] = useState("");
const [deleting, setDeleting] = useState(false);
@@ -1,20 +1,20 @@
import Card from "#components/layout/page/Card";
import ComparePBsTable from "#components/tables/rivals/ComparePBsTable";
import ProfilePicture from "#components/user/ProfilePicture";
import UGPTRatingsTable from "#components/user/UGPTStatsOverview";
import UserGameRatingsTable from "#components/user/UserGameStatsOverview";
import ApiError from "#components/util/ApiError";
import Divider from "#components/util/Divider";
import Loading from "#components/util/Loading";
import UserSelectModal from "#components/util/modal/UserSelectModal";
import useApiQuery from "#components/util/query/useApiQuery";
import useLUGPTSettings from "#components/util/useLUGPTSettings";
import useLoggedInUserGameSettings from "#components/util/useLoggedInUserGameSettings";
import UserIcon from "#components/util/UserIcon";
import { UserContext } from "#context/UserContext";
import { type UGPTFolderReturns, type UGPTStatsReturn } from "#types/api-returns";
import { type GamePT } from "#types/react";
import { type UserGameFolderReturns, type UserGameStatsReturn } from "#types/api-returns";
import { type GameProps } from "#types/react";
import { type ComparePBsDataset } from "#types/tables";
import { CreateSongMap } from "#util/data";
import React, { useContext, useMemo, useState } from "react";
import { useContext, useMemo, useState } from "react";
import Button from "react-bootstrap/Button";
import Col from "react-bootstrap/Col";
import Form from "react-bootstrap/Form";
@@ -30,8 +30,8 @@ export default function RivalCompareFolderPage({
}: {
folder: FolderDocument;
reqUser: UserDocument;
} & GamePT) {
const { settings } = useLUGPTSettings();
} & GameProps) {
const { settings } = useLoggedInUserGameSettings();
const { user } = useContext(UserContext);
const [selectedUser, setSelectedUser] = useState<UserDocument | null>(null);
@@ -118,12 +118,12 @@ function FolderCompare({
folder: FolderDocument;
reqUser: UserDocument;
withUser: UserDocument;
} & GamePT) {
const { data: baseData, error: baseError } = useApiQuery<UGPTFolderReturns>(
} & GameProps) {
const { data: baseData, error: baseError } = useApiQuery<UserGameFolderReturns>(
`/users/${reqUser.id}/games/${game}/folders/${folder.slug}`,
);
const { data: compareData, error: compareError } = useApiQuery<UGPTFolderReturns>(
const { data: compareData, error: compareError } = useApiQuery<UserGameFolderReturns>(
`/users/${withUser.id}/games/${game}/folders/${folder.slug}`,
);
@@ -186,8 +186,10 @@ function FolderCompare({
);
}
function UserCard({ user, game }: { user: UserDocument } & GamePT) {
const { data, error } = useApiQuery<UGPTStatsReturn>(`/users/${user.username}/games/${game}`);
function UserCard({ user, game }: { user: UserDocument } & GameProps) {
const { data, error } = useApiQuery<UserGameStatsReturn>(
`/users/${user.username}/games/${game}`,
);
if (error) {
return <ApiError error={error} />;
@@ -206,7 +208,7 @@ function UserCard({ user, game }: { user: UserDocument } & GamePT) {
<ProfilePicture user={user} />
</div>
<Col lg={7} sm={6} xl={6} xs={12}>
{data ? <UGPTRatingsTable ugs={data.gameStats} /> : <Loading />}
{data ? <UserGameRatingsTable ugs={data.gameStats} /> : <Loading />}
</Col>
</Card>
</Col>
@@ -2,9 +2,9 @@ import type { FolderStatsInfo } from "#types/api-returns";
import { WindowContext } from "#context/WindowContext";
import { ChangeOpacity } from "#util/color-opacity";
import { FormatGPTEnumMetric, ToFixedFloor, UppercaseFirst } from "#util/misc";
import React, { useContext } from "react";
import { type GameConfig, GetScoreMetricConf, V3Game } from "tachi-common";
import { FormatGameEnumMetric, ToFixedFloor, UppercaseFirst } from "#util/misc";
import { useContext } from "react";
import { type GameConfig, GetScoreMetricConf, type V3Game } from "tachi-common";
import breakdownStyles from "./FolderEnumDistributionBreakdown.module.scss";
@@ -48,7 +48,7 @@ function distributionRowChrome(enumColour: string | undefined): {
/**
* Styled enum value × count breakdown (counts + % of folder charts).
* Rows use GPT enum colours; optional floor at `minimumRelevantValue`; remainder row for unmatched charts.
* Rows use game enum colours; optional floor at `minimumRelevantValue`; remainder row for unmatched charts.
*/
export default function FolderEnumDistributionBreakdown({
game,
@@ -106,7 +106,7 @@ export default function FolderEnumDistributionBreakdown({
for (let vi = conf.values.length - 1; vi >= valueIndexFloor; vi--) {
const label = conf.values[vi];
const printedLabel = FormatGPTEnumMetric(game, enumMetric, label);
const printedLabel = FormatGameEnumMetric(game, enumMetric, label);
const count = bucket[label] ?? 0;
filled += count;
@@ -3,7 +3,7 @@ import type { FolderStatsInfo } from "#types/api-returns";
import QuickTooltip from "#components/layout/misc/QuickTooltip";
import Muted from "#components/util/Muted";
import { ChangeOpacity } from "#util/color-opacity";
import { FormatGPTEnumMetric, ToFixedFloor } from "#util/misc";
import { FormatGameEnumMetric, ToFixedFloor } from "#util/misc";
import React, { useLayoutEffect, useRef, useState } from "react";
import { type GameConfig, GetScoreMetricConf, V3Game } from "tachi-common";
@@ -77,7 +77,7 @@ export default function FolderEnumProgressBar({
if (count > 0) {
filled += count;
segments.push({
label: FormatGPTEnumMetric(game, enumMetric, v),
label: FormatGameEnumMetric(game, enumMetric, v),
count,
rawFill: colours?.[v] ?? "var(--bs-secondary-bg)",
});
@@ -6,7 +6,7 @@ import Loading from "#components/util/Loading";
import useApiQuery from "#components/util/query/useApiQuery";
import { TargetsContext } from "#context/TargetsContext";
import { type GoalsOnFolderReturn } from "#types/api-returns";
import { type UGPT } from "#types/react";
import { type GameProfileProps } from "#types/react";
import { CreateGoalSubDataset, CreateUserMap } from "#util/data";
import React, { useContext, useReducer, useState } from "react";
import { Button, Col } from "react-bootstrap";
@@ -18,7 +18,7 @@ export default function FolderQuestsPage({
reqUser,
}: {
folder: FolderDocument;
} & UGPT) {
} & GameProfileProps) {
const [refresh, forceRefresh] = useReducer((x) => x + 1, 0);
const { reloadTargets } = useContext(TargetsContext);
@@ -63,7 +63,7 @@ function FolderQuestsInner({
}: {
data: GoalsOnFolderReturn;
folder: FolderDocument;
} & UGPT) {
} & GameProfileProps) {
const userMap = CreateUserMap([reqUser]);
return <GoalSubInfo dataset={CreateGoalSubDataset(data, userMap)} game={game} />;
@@ -5,8 +5,8 @@ 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 useLUGPTSettings from "#components/util/useLUGPTSettings";
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react";
import useLoggedInUserGameSettings from "#components/util/useLoggedInUserGameSettings";
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from "react";
import Button from "react-bootstrap/Button";
import Collapse from "react-bootstrap/Collapse";
import Form from "react-bootstrap/Form";
@@ -22,7 +22,7 @@ const FOLDER_ROUTE_PATTERN = "/u/:userID/games/:game/folders/:folderSlug";
export default function FolderTablePage({ reqUser, game }: FolderTableScopedProps) {
const { data, error } = useApiQuery<TableDocument[]>(`/games/${game}/tables?showInactive=true`);
const { settings } = useLUGPTSettings();
const { settings } = useLoggedInUserGameSettings();
const location = useLocation();
const history = useHistory();
@@ -1,14 +1,13 @@
import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import { type UGPT } from "#types/react";
import React from "react";
import { type GameProfileProps } from "#types/react";
import { Redirect, Route, Switch } from "react-router-dom";
import { FormatGame, GameToGameGroup, GetGameGroupConfig } from "tachi-common";
import FolderTablePage from "./FolderTablePage";
import SpecificFolderPage from "./SpecificFolderPage";
export default function FoldersMainPage({ reqUser, game }: UGPT) {
export default function FoldersMainPage({ reqUser, game }: GameProfileProps) {
useSetSubheader(
[
"Users",
@@ -1,4 +1,4 @@
import { ErrorPage } from "#app/pages/ErrorPage";
import ErrorPage from "#app/pages/ErrorPage";
import FolderInfoHeader from "#components/game/folder/FolderInfoHeader";
import QuickTooltip from "#components/layout/misc/QuickTooltip";
import DifficultyCell from "#components/tables/cells/DifficultyCell";
@@ -17,17 +17,17 @@ import useApiQuery from "#components/util/query/useApiQuery";
import ReferToUser from "#components/util/ReferToUser";
import SelectButton from "#components/util/SelectButton";
import SelectLinkButton from "#components/util/SelectLinkButton";
import useUGPTBase from "#components/util/useUGPTBase";
import useUserGameBase from "#components/util/useUserGameBase";
import { WindowContext } from "#context/WindowContext";
import { GPT_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations";
import { type GPTRatingSystem } from "#lib/types";
import { type UGPTFolderReturns } from "#types/api-returns";
import { GAME_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations";
import { type GameRatingSystem } from "#lib/types";
import { type UserGameFolderReturns } from "#types/api-returns";
import { type FolderDataset } from "#types/tables";
import { ChangeOpacity } from "#util/color-opacity";
import { CreateChartIDMap, CreateChartLink, CreateSongMap } from "#util/data";
import { DistinctArr, ToFixedFloor } from "#util/misc";
import { NumericSOV, StrSOV } from "#util/sorts";
import React, {
import {
type SetStateAction,
useCallback,
useContext,
@@ -93,7 +93,7 @@ interface Props {
export default function SpecificFolderPage({ reqUser, game }: Props) {
const { folderSlug } = useParams<{ folderSlug: string }>();
const { data, error } = useApiQuery<UGPTFolderReturns>(
const { data, error } = useApiQuery<UserGameFolderReturns>(
`/users/${reqUser.id}/games/${game}/folders/${folderSlug}`,
);
@@ -163,7 +163,7 @@ export default function SpecificFolderPage({ reqUser, game }: Props) {
);
}, [data, folderSlug, folderDataset, game, onBreakdownEnumValueClick, reqUser]);
const base = `${useUGPTBase({ reqUser, game })}/folders/${folderSlug}`;
const base = `${useUserGameBase({ reqUser, game })}/folders/${folderSlug}`;
if (error?.statusCode === 404) {
return (
@@ -182,7 +182,7 @@ export default function SpecificFolderPage({ reqUser, game }: Props) {
return <Loading />;
}
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[game];
const gptImpl = GAME_CLIENT_IMPLEMENTATIONS[game];
return (
<div className="row">
@@ -257,7 +257,7 @@ function FolderNormalView({
game,
reqUser,
}: {
data: UGPTFolderReturns;
data: UserGameFolderReturns;
folderDataset: FolderDataset;
folderTableEnumPreset: FolderEnumBreakdownTablePreset | null;
} & Props) {
@@ -292,12 +292,12 @@ function FolderNormalView({
// so
type InfoProps = {
data: UGPTFolderReturns;
data: UserGameFolderReturns;
folderDataset: FolderDataset;
} & Props;
function TierlistBreakdown({ game, folderDataset, reqUser }: InfoProps) {
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[game];
const gptImpl = GAME_CLIENT_IMPLEMENTATIONS[game];
const history = useHistory();
const location = useLocation();
@@ -305,7 +305,7 @@ function TierlistBreakdown({ game, folderDataset, reqUser }: InfoProps) {
const canonicalFirstTier = gptImpl.ratingSystems[0]?.name ?? "";
const tierlist = useMemo((): string => {
const systems = gptImpl.ratingSystems as GPTRatingSystem<V3Game>[];
const systems = gptImpl.ratingSystems as GameRatingSystem<V3Game>[];
const fromQs = new URLSearchParams(location.search).get("tierlist");
if (!fromQs) {
return canonicalFirstTier;
@@ -329,7 +329,7 @@ function TierlistBreakdown({ game, folderDataset, reqUser }: InfoProps) {
);
useEffect(() => {
const systems = gptImpl.ratingSystems as GPTRatingSystem<V3Game>[];
const systems = gptImpl.ratingSystems as GameRatingSystem<V3Game>[];
if (systems.length === 0) {
return;
}
@@ -354,7 +354,7 @@ function TierlistBreakdown({ game, folderDataset, reqUser }: InfoProps) {
[folderDataset, game, tierlist],
);
const tierlistImpl = (gptImpl.ratingSystems as GPTRatingSystem<V3Game>[]).find(
const tierlistImpl = (gptImpl.ratingSystems as GameRatingSystem<V3Game>[]).find(
(rs) => rs.name === tierlist,
);
@@ -449,7 +449,7 @@ function tierlistRowEnumBucketKey(row: TierlistInfo, game: V3Game, enumMetric: s
try {
const label = EnumIndexToValue(
game,
// @ts-expect-error GPTRatingSystem.enumName matches score enums on this GPT
// @ts-expect-error GameRatingSystem.enumName matches score enums on this game
enumMetric,
idx,
);
@@ -468,7 +468,7 @@ interface TierlistBucketBarSegment {
function computeTierlistBucketBarModel(
bucket: TierlistInfo[],
game: V3Game,
tierlistImpl: GPTRatingSystem<V3Game>,
tierlistImpl: GameRatingSystem<V3Game>,
useFancyColour: boolean,
): { achieved: number; segments: TierlistBucketBarSegment[]; total: number } {
const total = bucket.length;
@@ -530,7 +530,7 @@ function computeTierlistBucketBarModel(
const enumMetric = tierlistImpl.enumName;
const conf = GetScoreEnumConfs(GetGameConfig(game))[enumMetric];
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[game];
const gptImpl = GAME_CLIENT_IMPLEMENTATIONS[game];
const counts = new Map<string, number>();
for (const row of bucket) {
@@ -749,7 +749,7 @@ function TierlistBucketsSummaryTable({
expandedBucketIndices: Set<number>;
game: V3Game;
onTierActivate: (bucketIndex: number) => void;
tierlistImpl: GPTRatingSystem<V3Game>;
tierlistImpl: GameRatingSystem<V3Game>;
useFancyColour: boolean;
}) {
if (buckets.length === 0) {
@@ -873,7 +873,7 @@ function TierlistInfoLadder({
game: V3Game;
playerStats: Record<string, { score: string | null; status: AchievedStatuses }>;
reqUser: UserDocument;
tierlistImpl: GPTRatingSystem<V3Game>;
tierlistImpl: GameRatingSystem<V3Game>;
useFancyColour: boolean;
}) {
const buckets: TierlistInfo[][] = useMemo(() => {
@@ -1091,7 +1091,7 @@ function TierlistBucket({
forceGridView: boolean;
game: V3Game;
reqUser: UserDocument;
tierlistImpl: GPTRatingSystem<V3Game>;
tierlistImpl: GameRatingSystem<V3Game>;
useFancyColour: boolean;
}) {
const {
@@ -1150,7 +1150,7 @@ function TierlistInfoBucketValues({
game: V3Game;
i: integer;
reqUser: UserDocument;
tierlistImpl: GPTRatingSystem<V3Game>;
tierlistImpl: GameRatingSystem<V3Game>;
tierlistInfo: TierlistInfo;
useFancyColour: boolean;
}) {
@@ -1167,7 +1167,7 @@ function TierlistInfoBucketValues({
let colourCss: string | undefined;
if (useFancyColour) {
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[game];
const gptImpl = GAME_CLIENT_IMPLEMENTATIONS[game];
// @ts-expect-error lol
colourCss = gptImpl.enumColours[tierlistImpl.enumName][tierlistInfo.score];
@@ -1305,7 +1305,7 @@ enum AchievedStatuses {
function FolderDatasetAchievedStatus(folderDataset: FolderDataset, game: V3Game, tierlist: string) {
const tierlistInfo: Record<string, { score: string | null; status: AchievedStatuses }> = {};
const fn = (GPT_CLIENT_IMPLEMENTATIONS[game].ratingSystems as GPTRatingSystem<V3Game>[]).find(
const fn = (GAME_CLIENT_IMPLEMENTATIONS[game].ratingSystems as GameRatingSystem<V3Game>[]).find(
(e) => e.name === tierlist,
)?.achievementFn;
@@ -1,4 +1,4 @@
import type { FolderStatsInfo, UGPTEvolutionReplayReturns } from "#types/api-returns";
import type { FolderStatsInfo, UserGameEvolutionReplayReturns } from "#types/api-returns";
import ApiError from "#components/util/ApiError";
import Icon from "#components/util/Icon";
@@ -7,12 +7,12 @@ import Muted from "#components/util/Muted";
import useApiQuery from "#components/util/query/useApiQuery";
import SelectButton from "#components/util/SelectButton";
import { useBucket } from "#components/util/useBucket";
import { GPT_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations";
import { GAME_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations";
import { ChangeOpacity } from "#util/color-opacity";
import { CreateChartIDMap, CreateSongMap } from "#util/data";
import { UppercaseFirst } from "#util/misc";
import { FormatDate, MillisToSince } from "#util/time";
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import Button from "react-bootstrap/Button";
import Collapse from "react-bootstrap/Collapse";
import Dropdown from "react-bootstrap/Dropdown";
@@ -141,7 +141,7 @@ export default function TableEvolutionReplay({
data: evo,
error: evoError,
isLoading: evolutionLoading,
} = useApiQuery<UGPTEvolutionReplayReturns>(evolutionUrl, undefined, undefined, !open);
} = useApiQuery<UserGameEvolutionReplayReturns>(evolutionUrl, undefined, undefined, !open);
const events = useMemo(() => evo?.events ?? [], [evo?.events]);
@@ -403,7 +403,7 @@ export default function TableEvolutionReplay({
const allEnumColours = useMemo(
() =>
GPT_CLIENT_IMPLEMENTATIONS[game].enumColours as
GAME_CLIENT_IMPLEMENTATIONS[game].enumColours as
| Record<string, Record<string, string>>
| undefined,
[game],
@@ -499,7 +499,7 @@ export default function TableEvolutionReplay({
<Icon
type={
/* @ts-expect-error enum icon keys align with score metrics */
GPT_CLIENT_IMPLEMENTATIONS[game].enumIcons[metric]
GAME_CLIENT_IMPLEMENTATIONS[game].enumIcons[metric]
}
/>{" "}
{UppercaseFirst(metric)}
@@ -1,8 +1,8 @@
import Icon from "#components/util/Icon";
import { UserContext } from "#context/UserContext";
import { GPT_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations";
import { GAME_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations";
import { APIFetchV1 } from "#util/api";
import React, { useContext, useLayoutEffect, useMemo, useRef, useState } from "react";
import { useContext, useLayoutEffect, useMemo, useRef, useState } from "react";
import { Link } from "react-router-dom";
import { GetGameConfig, type TableDocument } from "tachi-common";
@@ -11,7 +11,7 @@ import folderTableStyles from "./FolderTablePage.module.scss";
import {
type FolderTableScopedProps,
tableFolderSlugsDisplayOrder,
type UGPTFolderStats,
type UserGameFolderStats,
} from "./folderTableShared";
export default function TableFolderList({
@@ -24,7 +24,7 @@ export default function TableFolderList({
reqUser,
table,
}: {
dataMap: Map<string, UGPTFolderStats>;
dataMap: Map<string, UserGameFolderStats>;
enumMetric: string;
highlightFolderSlug?: string;
highlightRevealKey?: number;
@@ -36,7 +36,7 @@ export default function TableFolderList({
const enumColours = useMemo(
() =>
(
GPT_CLIENT_IMPLEMENTATIONS[game].enumColours as
GAME_CLIENT_IMPLEMENTATIONS[game].enumColours as
| Record<string, Record<string, string>>
| undefined
)?.[enumMetric],
@@ -1,14 +1,14 @@
import type { UGPTTableReturns } from "#types/api-returns";
import type { UserGameTableReturns } from "#types/api-returns";
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 { useBucket } from "#components/util/useBucket";
import React, { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { GetGameConfig, type TableDocument } from "tachi-common";
import type { FolderTableScopedProps, UGPTFolderStats } from "./folderTableShared";
import type { FolderTableScopedProps, UserGameFolderStats } from "./folderTableShared";
import TableEvolutionReplay from "./TableEvolutionReplay";
import TableFolderList from "./TableFolderList";
@@ -26,7 +26,7 @@ export default function TableFolderViewer({
onFolderRowNavigate?: () => void;
table: TableDocument;
} & FolderTableScopedProps) {
const { data, error } = useApiQuery<UGPTTableReturns>(
const { data, error } = useApiQuery<UserGameTableReturns>(
`/users/${reqUser.id}/games/${game}/tables/${table.tableID}`,
);
@@ -37,7 +37,7 @@ export default function TableFolderViewer({
setEnumMetric(bucket);
}, [bucket, table.tableID]);
const [dataMap, setDataMap] = useState<Map<string, UGPTFolderStats>>(new Map());
const [dataMap, setDataMap] = useState<Map<string, UserGameFolderStats>>(new Map());
const [hasLoadedFolderMap, setHasLoadedFolderMap] = useState(false);
useEffect(() => {
@@ -1,6 +1,6 @@
import type { FolderStatsInfo, TableEvolutionEventAPI } from "#types/api-returns";
import { GPT_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations";
import { GAME_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations";
import {
type FolderDocument,
type GameConfig,
@@ -16,7 +16,7 @@ export type FolderTableScopedProps = {
reqUser: UserDocument;
};
export interface UGPTFolderStats {
export interface UserGameFolderStats {
folder: FolderDocument;
stats: FolderStatsInfo;
}
@@ -26,11 +26,10 @@ export interface UGPTFolderStats {
*
* `table.folders` is ascending (e.g. Level 1 … 12), and by default we reverse so the
* highest folder / level renders first. Games can opt out via `reverseFolderOrder` on
* their GPT client impl, in which case we reverse again — i.e. render in the table's
* declared order (currently used for BMS/PMS).
* their game client impl.
*/
export function tableFolderSlugsDisplayOrder(table: TableDocument, game: V3Game): string[] {
const reverseAgain = GPT_CLIENT_IMPLEMENTATIONS[game].reverseFolderOrder ?? false;
const reverseAgain = GAME_CLIENT_IMPLEMENTATIONS[game].reverseFolderOrder ?? false;
return reverseAgain ? [...table.folders] : [...table.folders].reverse();
}
@@ -1,11 +1,10 @@
import Activity from "#components/activity/Activity";
import useLUGPTSettings from "#components/util/useLUGPTSettings";
import { type UGPT } from "#types/react";
import React from "react";
import useLoggedInUserGameSettings from "#components/util/useLoggedInUserGameSettings";
import { type GameProfileProps } from "#types/react";
import { Link } from "react-router-dom";
export default function RivalsActivityPage({ reqUser, game }: UGPT) {
const { settings } = useLUGPTSettings();
export default function RivalsActivityPage({ reqUser, game }: GameProfileProps) {
const { settings } = useLoggedInUserGameSettings();
if (!settings) {
return <>You have no settings. How did you get here?</>;
@@ -3,9 +3,8 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import Icon from "#components/util/Icon";
import SelectLinkButton from "#components/util/SelectLinkButton";
import useLUGPTSettings from "#components/util/useLUGPTSettings";
import useUGPTBase from "#components/util/useUGPTBase";
import React from "react";
import useLoggedInUserGameSettings from "#components/util/useLoggedInUserGameSettings";
import useUserGameBase from "#components/util/useUserGameBase";
import { Col, Row } from "react-bootstrap";
import { Route, Switch } from "react-router-dom";
import {
@@ -32,9 +31,9 @@ export default function RivalsMainPage({ reqUser, game }: { game: V3Game; reqUse
`${reqUser.username}'s ${FormatGame(game)} Rivals`,
);
const base = useUGPTBase({ reqUser, game });
const base = useUserGameBase({ reqUser, game });
const { settings } = useLUGPTSettings();
const { settings } = useLoggedInUserGameSettings();
if (!settings) {
return <div>You have no settings set. How did you cause this?</div>;
@@ -7,12 +7,12 @@ import Loading from "#components/util/Loading";
import UserSelectModal from "#components/util/modal/UserSelectModal";
import Muted from "#components/util/Muted";
import useApiQuery from "#components/util/query/useApiQuery";
import useLUGPTSettings from "#components/util/useLUGPTSettings";
import useLoggedInUserGameSettings from "#components/util/useLoggedInUserGameSettings";
import UserIcon from "#components/util/UserIcon";
import { UserContext } from "#context/UserContext";
import { APIFetchV1 } from "#util/api";
import { SendErrorToast } from "#util/toaster";
import React, { useContext, useState } from "react";
import { useContext, useState } from "react";
import { Alert, Button, Col } from "react-bootstrap";
import { Prompt } from "react-router-dom";
import {
@@ -44,7 +44,7 @@ export default function RivalsManagePage({
`Managing ${reqUser.username}'s ${FormatGame(game)} Rivals`,
);
const { settings } = useLUGPTSettings();
const { settings } = useLoggedInUserGameSettings();
const { data, error } = useApiQuery<UserDocument[]>(
`/users/${reqUser.id}/games/${game}/rivals`,
@@ -101,7 +101,7 @@ function RivalsOverviewPage({
const [rivals, setRivals] = useState(initialRivals);
const [show, setShow] = useState(false);
const { settings, setSettings } = useLUGPTSettings();
const { settings, setSettings } = useLoggedInUserGameSettings();
const [currentRivals, setCurrentRivals] = useState(initialRivals);
@@ -2,17 +2,16 @@ import useSetSubheader from "#components/layout/header/useSetSubheader";
import Divider from "#components/util/Divider";
import Icon from "#components/util/Icon";
import SelectLinkButton from "#components/util/SelectLinkButton";
import useUGPTBase from "#components/util/useUGPTBase";
import { type UGPT } from "#types/react";
import React from "react";
import useUserGameBase from "#components/util/useUserGameBase";
import { type GameProfileProps } from "#types/react";
import { Col, Row } from "react-bootstrap";
import { Route, Switch } from "react-router-dom";
import { FormatGame, GameToGameGroup, GetGameGroupConfig } from "tachi-common";
import UGPTGoalsPage from "./UGPTGoalsPage";
import UGPTQuestsPage from "./UGPTQuestsPage";
import UserGameGoalsPage from "./UserGameGoalsPage";
import UserGameQuestsPage from "./UserGameQuestsPage";
export default function TargetsPage({ reqUser, game }: UGPT) {
export default function TargetsPage({ reqUser, game }: GameProfileProps) {
useSetSubheader(
[
"Users",
@@ -25,7 +24,7 @@ export default function TargetsPage({ reqUser, game }: UGPT) {
`${reqUser.username}'s ${FormatGame(game)} Goals & Quests`,
);
const base = useUGPTBase({ reqUser, game });
const base = useUserGameBase({ reqUser, game });
return (
<Row>
@@ -43,10 +42,10 @@ export default function TargetsPage({ reqUser, game }: UGPT) {
<Col xs={12}>
<Switch>
<Route exact path="/u/:userID/games/:game/targets/goals">
<UGPTGoalsPage {...{ reqUser, game }} />
<UserGameGoalsPage {...{ reqUser, game }} />
</Route>
<Route exact path="/u/:userID/games/:game/targets">
<UGPTQuestsPage {...{ reqUser, game }} />
<UserGameQuestsPage {...{ reqUser, game }} />
</Route>
</Switch>
</Col>
@@ -7,23 +7,23 @@ import Icon from "#components/util/Icon";
import Loading from "#components/util/Loading";
import useApiQuery from "#components/util/query/useApiQuery";
import { TargetsContext } from "#context/TargetsContext";
import { type AllUGPTGoalsReturn } from "#types/api-returns";
import { type UGPT } from "#types/react";
import { type AllUserGameGoalsReturn } from "#types/api-returns";
import { type GameProfileProps } from "#types/react";
import { APIFetchV1 } from "#util/api";
import { CreateGoalSubDataset } from "#util/data";
import React, { useContext, useReducer, useState } from "react";
import { useContext, useReducer, useState } from "react";
import { Button, Col, Modal } from "react-bootstrap";
import { Link } from "react-router-dom";
import { FormatGame, type GoalDocument } from "tachi-common";
export default function UGPTGoalsPage({ reqUser, game }: UGPT) {
export default function UserGameGoalsPage({ reqUser, game }: GameProfileProps) {
const [showAdd, setShowAdd] = useState(false);
const [showDelete, setShowDelete] = useState(false);
const [editingGoal, setEditingGoal] = useState<GoalDocument | null>(null);
const { reloadTargets } = useContext(TargetsContext);
const [refresh, refetchGoals] = useReducer((x) => x + 1, 0);
const { data, error } = useApiQuery<AllUGPTGoalsReturn>(
const { data, error } = useApiQuery<AllUserGameGoalsReturn>(
`/users/${reqUser.id}/games/${game}/targets/goals`,
undefined,
[refresh.toString()],
@@ -4,11 +4,11 @@ import Divider from "#components/util/Divider";
import Loading from "#components/util/Loading";
import useApiQuery from "#components/util/query/useApiQuery";
import Select from "#components/util/Select";
import { type UGPT } from "#types/react";
import { type GameProfileProps } from "#types/react";
import { CreateGoalMap, GetGoalIDsFromQuest } from "#util/data";
import { CreateQuestSubMap } from "#util/misc";
import { NumericSOV } from "#util/sorts";
import React, { useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { Col, Row } from "react-bootstrap";
import { Link } from "react-router-dom";
import {
@@ -17,7 +17,7 @@ import {
type QuestSubscriptionDocument,
} from "tachi-common";
export default function UGPTQuestsPage({ reqUser, game }: UGPT) {
export default function UserGameQuestsPage({ reqUser, game }: GameProfileProps) {
const [show, setShow] = useState<"achieved" | "all" | "unachieved">("all");
const { data, error } = useApiQuery<{
@@ -1,22 +1,22 @@
import { GetGPTUtils, GetGPTUtilsName } from "#components/gpt-utils/GPTUtils";
import { GetGameUtils, GetGameUtilsName } from "#components/game-utils/GameUtils";
import useSetSubheader from "#components/layout/header/useSetSubheader";
import Card from "#components/layout/page/Card";
import Divider from "#components/util/Divider";
import LinkButton from "#components/util/LinkButton";
import { UserContext } from "#context/UserContext";
import { type UGPT } from "#types/react";
import React, { useContext } from "react";
import { type GameProfileProps } from "#types/react";
import { useContext } from "react";
import { Col, Row } from "react-bootstrap";
import { Link, Route, Switch } from "react-router-dom";
import { FormatGame, GameToGameGroup, GetGameGroupConfig } from "tachi-common";
export default function UGPTUtilsPage({ reqUser, game }: UGPT) {
export default function UserGameUtilsPage({ reqUser, game }: GameProfileProps) {
const { user } = useContext(UserContext);
const isViewingOwnProfile = user?.id === reqUser.id;
const utils = GetGPTUtils(game);
const pageName = GetGPTUtilsName(game, isViewingOwnProfile);
const utils = GetGameUtils(game);
const pageName = GetGameUtilsName(game, isViewingOwnProfile);
useSetSubheader(
[
@@ -21,17 +21,13 @@ import { type RawQuestDocument, type RawQuestlineDocument } from "#types/tachi";
import { APIFetchV1 } from "#util/api";
import { ChangeAtPosition, DeleteInPosition } from "#util/misc";
import { p, type PrudenceSchema } from "prudence";
import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { Alert, Badge, Button, Col, Form, Modal, Row, Spinner } from "react-bootstrap";
import {
ALL_GAMES,
FormatGame,
FormatPrError,
type GameGroup,
GetGameGroupConfig,
LEGACY_GameGroupPTToGame,
type LEGACY_GPTString,
type LEGACY_Playtype,
type V3Game,
} from "tachi-common";
@@ -205,12 +201,9 @@ export default function QuestEditor() {
// ── Derived ────────────────────────────────────────────────────────────────
const selectedQuest = selectedQuestIdx !== null ? (quests[selectedQuestIdx] ?? null) : null;
const addQuest = (gptString: LEGACY_GPTString) => {
const [game, playtype] = gptString.split(":") as [GameGroup, LEGACY_Playtype];
const v3Game: V3Game = LEGACY_GameGroupPTToGame(game, playtype);
const addQuest = (game: V3Game) => {
const newQuest: RawQuestDocument = {
game: v3Game,
game,
name: "Untitled Quest",
desc: "Please set a description.",
rawQuestData: [],
@@ -651,22 +644,23 @@ function QuestList({
onSelect,
onAddQuest,
}: {
onAddQuest: (gpt: LEGACY_GPTString) => void;
onAddQuest: (game: V3Game) => void;
onSelect: (idx: number) => void;
quests: Array<RawQuestDocument>;
selectedIdx: number | null;
}) {
const [gpt, setGpt] = useState<LEGACY_GPTString | null>(null);
const [game, setGame] = useState<V3Game | null>(null);
const allGpts: Array<{ label: string; value: LEGACY_GPTString }> =
TachiConfig.GAME_GROUPS.flatMap((gameGroup) => {
const allGames: Array<{ label: string; value: V3Game }> = TachiConfig.GAME_GROUPS.flatMap(
(gameGroup) => {
const config = GetGameGroupConfig(gameGroup);
return config.playtypes.map((pt) => ({
value: `${gameGroup}:${pt}` as LEGACY_GPTString,
label: FormatGame(LEGACY_GameGroupPTToGame(gameGroup, pt)),
return config.games.map((game) => ({
value: game,
label: FormatGame(game),
}));
});
},
);
return (
<div className="d-flex flex-column gap-1 mb-3">
@@ -691,23 +685,23 @@ function QuestList({
{/* Inline new-quest form */}
<div className="mt-2 d-flex gap-2 align-items-center">
<Form.Select
onChange={(e) => setGpt(e.target.value as LEGACY_GPTString)}
onChange={(e) => setGame(e.target.value as V3Game)}
size="sm"
style={{ flex: 1 }}
value={gpt ?? ""}
value={game ?? ""}
>
<option value="">Game</option>
{allGpts.map((g) => (
{allGames.map((g) => (
<option key={g.value} value={g.value}>
{g.label}
</option>
))}
</Form.Select>
<Button
disabled={gpt === null}
disabled={game === null}
onClick={() => {
if (gpt) {
onAddQuest(gpt);
if (game) {
onAddQuest(game);
}
}}
size="sm"
@@ -736,17 +730,18 @@ function QuestlineComposer({
quests: Array<RawQuestDocument>;
}) {
const [newName, setNewName] = useState("");
const [newGame, setNewGame] = useState<"" | LEGACY_GPTString>("");
const [newGame, setNewGame] = useState<"" | V3Game>("");
const allGpts: Array<{ label: string; value: LEGACY_GPTString }> =
TachiConfig.GAME_GROUPS.flatMap((gameGroup) => {
const allGames: Array<{ label: string; value: V3Game }> = TachiConfig.GAME_GROUPS.flatMap(
(gameGroup) => {
const config = GetGameGroupConfig(gameGroup);
return config.playtypes.map((pt) => ({
value: `${gameGroup}:${pt}` as LEGACY_GPTString,
label: FormatGame(LEGACY_GameGroupPTToGame(gameGroup, pt)),
return config.games.map((game) => ({
value: game,
label: FormatGame(game),
}));
});
},
);
return (
<div className="d-flex flex-column gap-3 mb-3">
@@ -776,13 +771,13 @@ function QuestlineComposer({
/>
<div className="d-flex gap-2">
<Form.Select
onChange={(e) => setNewGame(e.target.value as LEGACY_GPTString)}
onChange={(e) => setNewGame(e.target.value as V3Game)}
size="sm"
style={{ flex: 1 }}
value={newGame}
>
<option value="">Game</option>
{allGpts.map((g) => (
{allGames.map((g) => (
<option key={g.value} value={g.value}>
{g.label}
</option>
@@ -795,13 +790,6 @@ function QuestlineComposer({
return;
}
const [gameGroup, playtype] = newGame.split(":") as [
GameGroup,
LEGACY_Playtype,
];
const v3Game = LEGACY_GameGroupPTToGame(gameGroup, playtype);
const slug = newName
.trim()
.toLowerCase()
@@ -812,7 +800,7 @@ function QuestlineComposer({
questlineID: `${slug}-${Date.now()}`,
name: newName.trim(),
desc: "",
game: v3Game,
game: newGame,
quests: [],
});
@@ -3,10 +3,10 @@ import AdminCronJobsPage from "#app/pages/admin/AdminCronJobsPage";
import AdminDestructivePage from "#app/pages/admin/AdminDestructivePage";
import AdminJobQueuePage from "#app/pages/admin/AdminJobQueuePage";
import AdminOperationsPage from "#app/pages/admin/AdminOperationsPage";
import { ErrorPage } from "#app/pages/ErrorPage";
import ErrorPage from "#app/pages/ErrorPage";
import { AdminPanelLayout } from "#components/admin/AdminPanelLayout";
import { UserContext } from "#context/UserContext";
import React, { useContext } from "react";
import { useContext } from "react";
import { Redirect, Route, Switch } from "react-router-dom";
import { UserAuthLevels } from "tachi-common";
@@ -1,4 +1,4 @@
import { ErrorPage } from "#app/pages/ErrorPage";
import ErrorPage from "#app/pages/ErrorPage";
import ForgotPasswordPage from "#app/pages/ForgotPasswordPage";
import LoginPage from "#app/pages/LoginPage";
import OAuthRequestAuthPage from "#app/pages/OAuthRequestAuthPage";
@@ -10,7 +10,7 @@ import ErrorBoundary from "#components/util/ErrorBoundary";
import { UserContext } from "#context/UserContext";
import { ClientConfig } from "#lib/config";
import { HistorySafeGoBack } from "#util/misc";
import React, { useContext } from "react";
import { useContext } from "react";
import { Redirect, Route, Switch, useHistory } from "react-router-dom";
import ClientFileFlowRoutes from "./ClientFileFlowRoutes";
@@ -1,8 +1,8 @@
import ClientFileFlowPage from "#app/pages/ClientFileFlowPage";
import { ErrorPage } from "#app/pages/ErrorPage";
import ErrorPage from "#app/pages/ErrorPage";
import CenterLayoutPage from "#components/layout/CenterLayoutPage";
import { UserContext } from "#context/UserContext";
import React, { useContext } from "react";
import { useContext } from "react";
import { Route, Switch } from "react-router-dom";
export default function ClientFileFlowRoutes() {
@@ -3,7 +3,7 @@ import MyProposalsPage from "#app/pages/dashboard/proposals/MyProposalsPage";
import ProposalsPage from "#app/pages/dashboard/proposals/ProposalsPage";
import SearchPage from "#app/pages/dashboard/search/SearchPage";
import NotificationsPage from "#app/pages/dashboard/users/NotificationsPage";
import { ErrorPage } from "#app/pages/ErrorPage";
import ErrorPage from "#app/pages/ErrorPage";
import PrivacyPolicyPage from "#app/pages/PrivacyPolicyPage";
import { Layout } from "#components/layout/Layout";
import EmailVerify from "#components/layout/misc/EmailVerify";
@@ -13,7 +13,7 @@ import { BannedContext } from "#context/BannedContext";
import { UserContext } from "#context/UserContext";
import { TachiConfig } from "#lib/config";
import { APIFetchV1, ToAPIURL } from "#util/api";
import React, { useContext, useEffect, useState } from "react";
import { useContext, useEffect, useState } from "react";
import { Redirect, Route, Switch } from "react-router-dom";
import { DashboardPage } from "../pages/dashboard/DashboardPage";
+30 -30
View File
@@ -1,11 +1,11 @@
import GPTChartPage from "#app/pages/dashboard/games/_game/_playtype/GPTChartPage";
import GPTChartsPage from "#app/pages/dashboard/games/_game/_playtype/GPTChartsPage";
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 GameChartPage from "#app/pages/dashboard/games/_game/GameChartPage";
import GameChartsPage from "#app/pages/dashboard/games/_game/GameChartsPage";
import GameDevInfo from "#app/pages/dashboard/games/_game/GameDevInfo";
import GameLeaderboardsPage from "#app/pages/dashboard/games/_game/GameLeaderboardsPage";
import GameMainPage from "#app/pages/dashboard/games/_game/GameMainPage";
import ErrorPage from "#app/pages/ErrorPage";
import ChartInfoFormat from "#components/game/charts/ChartInfoFormat";
import { GPTBottomNav } from "#components/game/GPTHeader";
import GameBottomNav from "#components/game/GameBottomNav";
import SongChartInfoFormat from "#components/game/songs/SongChartInfoFormat";
import QuestlinePage from "#components/game/targets/QuestlinePage";
import QuestPage from "#components/game/targets/QuestPage";
@@ -18,14 +18,14 @@ import Loading from "#components/util/Loading";
import Muted from "#components/util/Muted";
import useApiQuery from "#components/util/query/useApiQuery";
import SelectButton from "#components/util/SelectButton";
import useLUGPTSettings from "#components/util/useLUGPTSettings";
import useLoggedInUserGameSettings from "#components/util/useLoggedInUserGameSettings";
import { BackgroundContext } from "#context/BackgroundContext";
import { TargetsContextProvider } from "#context/TargetsContext";
import { UGPTContextProvider } from "#context/UGPTContext";
import { UserGameContextProvider } from "#context/UserGameContext";
import { UserSettingsContext } from "#context/UserSettingsContext";
import { GPT_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations";
import { GAME_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations";
import { type SongsReturn } from "#types/api-returns";
import { type GamePT, type SetState } from "#types/react";
import { type GameProps, type SetState } from "#types/react";
import { ToCDNURL } from "#util/api";
import { IsSupportedGame } from "#util/asserts";
import { ChangeOpacity } from "#util/color-opacity";
@@ -33,7 +33,7 @@ import { CreateChartLink } from "#util/data";
import { getGameGroupBannerRelPathForWeekday } from "#util/game-group-banner-counts";
import { SelectRightChart } from "#util/misc";
import { NumericSOV, StrSOV } from "#util/sorts";
import React, { useContext, useEffect, useState } from "react";
import { useContext, useEffect, useState } from "react";
import { Col, Row } from "react-bootstrap";
import { Redirect, Route, Switch, useParams } from "react-router-dom";
import {
@@ -83,11 +83,11 @@ function V3GameRoutes() {
const game = gameParam;
return (
<UGPTContextProvider>
<UserGameContextProvider>
<TargetsContextProvider>
<GameV3Routes game={game} />
</TargetsContextProvider>
</UGPTContextProvider>
</UserGameContextProvider>
);
}
@@ -95,12 +95,12 @@ function GameV3Routes({ game }: { game: V3Game }) {
return (
<>
<div className="card">
<GPTBottomNav baseUrl={`/games/${game}`} />
<GameBottomNav baseUrl={`/games/${game}`} />
</div>
<Divider />
<Switch>
<Route exact path="/games/:game">
<GPTMainPage game={game} />
<GameMainPage game={game} />
</Route>
<Route exact path="/games/:game/songs">
@@ -108,7 +108,7 @@ function GameV3Routes({ game }: { game: V3Game }) {
</Route>
<Route exact path="/games/:game/charts">
<GPTChartsPage game={game} />
<GameChartsPage game={game} />
</Route>
<Route path="/games/:game/charts/:chartID">
@@ -120,14 +120,14 @@ function GameV3Routes({ game }: { game: V3Game }) {
</Route>
<Route path="/games/:game/(quests|questlines|goals)">
<GPTQuestRoutes game={game} />
<GameQuestRoutes game={game} />
</Route>
<Route exact path="/games/:game/leaderboards">
<GPTLeaderboardsPage game={game} />
<GameLeaderboardsPage game={game} />
</Route>
<Route exact path="/games/:game/dev-info">
<GPTDevInfo game={game} />
<GameDevInfo game={game} />
</Route>
<Route path="*">
@@ -138,7 +138,7 @@ function GameV3Routes({ game }: { game: V3Game }) {
);
}
function GPTQuestRoutes({ game }: GamePT) {
function GameQuestRoutes({ game }: GameProps) {
return (
<>
<Switch>
@@ -166,7 +166,7 @@ function GPTQuestRoutes({ game }: GamePT) {
);
}
function ChartPageRoutes({ game }: GamePT) {
function ChartPageRoutes({ game }: GameProps) {
const { chartID } = useParams<{ chartID: string }>();
const { data: singleData, error: chartErr } = useApiQuery<{
@@ -217,7 +217,7 @@ function ChartPageRoutes({ game }: GamePT) {
setActiveChart={setActiveChart}
/>
<Divider />
<GPTChartPage chart={activeChart} game={game} song={songsData.song} />
<GameChartPage chart={activeChart} game={game} song={songsData.song} />
{settings?.preferences.developerMode && (
<>
<Divider />
@@ -230,7 +230,7 @@ function ChartPageRoutes({ game }: GamePT) {
);
}
function SongChartRedirectRoutes({ game }: GamePT) {
function SongChartRedirectRoutes({ game }: GameProps) {
const { songID } = useParams<{ songID: string }>();
const { data, error } = useApiQuery<SongsReturn>(`/games/${game}/songs/${songID}`);
@@ -281,7 +281,7 @@ function SongChartRedirectRoutes({ game }: GamePT) {
);
}
function SongSongIdOnlyRedirect({ charts, game }: { charts: ChartDocument[] } & GamePT) {
function SongSongIdOnlyRedirect({ charts, game }: { charts: ChartDocument[] } & GameProps) {
const hardest = charts.slice(0).sort(NumericSOV((x) => x.levelNum, true))[0];
if (!hardest.chartID) {
@@ -296,7 +296,7 @@ function SongSongIdOnlyRedirect({ charts, game }: { charts: ChartDocument[] } &
return <Redirect to={`/games/${game}/charts/${hardest.chartID}`} />;
}
function SongDifficultyRedirect({ data, game }: { data: SongsReturn } & GamePT) {
function SongDifficultyRedirect({ data, game }: { data: SongsReturn } & GameProps) {
const { difficulty: d } = useParams<{ difficulty: string }>();
const difficulty = decodeURIComponent(d);
@@ -319,7 +319,7 @@ function SongInfoHeader({
}: {
activeChart: ChartDocument | null;
setActiveChart: SetState<ChartDocument | null>;
} & GamePT &
} & GameProps &
SongsReturn) {
const gameConfig = GetGameConfig(game);
const sortedCharts = charts.slice(0);
@@ -415,7 +415,7 @@ function SongInfoHeader({
type Props = {
activeChart: ChartDocument | null;
setActiveChart: SetState<ChartDocument | null>;
} & { song: SongDocument } & GamePT;
} & { song: SongDocument } & GameProps;
const ITG_COLOUR_LOOKUP = {
Beginner: COLOUR_SET.paleBlue,
@@ -432,7 +432,7 @@ function DifficultyButton({
setActiveChart,
activeChart,
}: { chart: ChartDocument } & Props) {
const gptImpl = GPT_CLIENT_IMPLEMENTATIONS[game];
const gptImpl = GAME_CLIENT_IMPLEMENTATIONS[game];
const diffTag = chart.difficulty;
const gameGroup = GameToGameGroup(game);
@@ -522,7 +522,7 @@ function IIDXDifficultyList({
}: {
charts: ChartDocument[];
} & Props) {
const { settings } = useLUGPTSettings<GamesForGroup["iidx"]>();
const { settings } = useLoggedInUserGameSettings<GamesForGroup["iidx"]>();
const [set, setSet] = useState<"All Scratch" | "Kichiku" | "Kiraku" | null>(null);
@@ -1,4 +1,3 @@
import React from "react";
import { Redirect } from "react-router-dom";
/**
+15 -15
View File
@@ -5,30 +5,30 @@ import RivalsMainPage from "#app/pages/dashboard/users/games/_game/_playtype/riv
import SessionsPage from "#app/pages/dashboard/users/games/_game/_playtype/SessionsPage";
import SpecificSessionPage from "#app/pages/dashboard/users/games/_game/_playtype/SpecificSessionPage";
import TargetsPage from "#app/pages/dashboard/users/games/_game/_playtype/targets/TargetsPage";
import UGPTSettingsPage from "#app/pages/dashboard/users/games/_game/_playtype/UGPTSettingsPage";
import UGPTUtilsPage from "#app/pages/dashboard/users/games/_game/_playtype/utils/UGPTUtilsPage";
import UserGameSettingsPage from "#app/pages/dashboard/users/games/_game/_playtype/UserGameSettingsPage";
import UserGameUtilsPage from "#app/pages/dashboard/users/games/_game/_playtype/utils/UserGameUtilsPage";
import UserGamesPage from "#app/pages/dashboard/users/UserGamesPage";
import UserImportsPage from "#app/pages/dashboard/users/UserImportsPage";
import UserIntegrationsPage from "#app/pages/dashboard/users/UserIntegrationsPage";
import UserInvitesPage from "#app/pages/dashboard/users/UserInvitesPage";
import UserOrphansPage from "#app/pages/dashboard/users/UserOrphansPage";
import UserSettingsPage from "#app/pages/dashboard/users/UserSettingsPage";
import { ErrorPage } from "#app/pages/ErrorPage";
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 { UserGameHeaderBody, UserGameNav } from "#components/user/UserGameHeader";
import { UserBottomNav, UserHeaderBody } from "#components/user/UserHeader";
import Loading from "#components/util/Loading";
import useApiQuery from "#components/util/query/useApiQuery";
import { BackgroundContext } from "#context/BackgroundContext";
import { TargetsContextProvider } from "#context/TargetsContext";
import { UGPTContextProvider } from "#context/UGPTContext";
import { UserContext } from "#context/UserContext";
import { UserGameContextProvider } from "#context/UserGameContext";
import { UserSettingsContext } from "#context/UserSettingsContext";
import { type UGPTStatsReturn } from "#types/api-returns";
import { type UserGameStatsReturn } from "#types/api-returns";
import { APIFetchV1, type APIFetchV1Return, ToAPIURL } from "#util/api";
import { IsSupportedGame } from "#util/asserts";
import React, { useContext, useEffect } from "react";
import { useContext, useEffect } from "react";
import { useQuery } from "react-query";
import { Redirect, Route, Switch, useHistory, useParams } from "react-router-dom";
import { FormatGame, type UserDocument, type UserGameStats, type V3Game } from "tachi-common";
@@ -170,21 +170,21 @@ function V3UserGameRoutes({ reqUser }: { reqUser: UserDocument }) {
const game = gameParam;
return (
<UGPTContextProvider>
<UserGameContextProvider>
<TargetsContextProvider>
<Inner game={game} reqUser={reqUser} />
</TargetsContextProvider>
</UGPTContextProvider>
</UserGameContextProvider>
);
}
function Inner({ reqUser, game }: { game: V3Game; reqUser: UserDocument }) {
const { user } = useContext(UserContext);
const { data, error } = useQuery<UGPTStatsReturn, APIFetchV1Return<UserGameStats>>(
const { data, error } = useQuery<UserGameStatsReturn, APIFetchV1Return<UserGameStats>>(
[reqUser.id, game],
async () => {
const res = await APIFetchV1<UGPTStatsReturn>(`/users/${reqUser.id}/games/${game}`);
const res = await APIFetchV1<UserGameStatsReturn>(`/users/${reqUser.id}/games/${game}`);
if (!res.success) {
console.error(res);
@@ -214,7 +214,7 @@ function Inner({ reqUser, game }: { game: V3Game; reqUser: UserDocument }) {
<>
<LayoutHeaderContainer
footer={
<UGPTBottomNav
<UserGameNav
baseUrl={`/u/${reqUser.username}/games/${game}`}
game={game}
isRequestedUser={reqUser.id === user?.id}
@@ -222,7 +222,7 @@ function Inner({ reqUser, game }: { game: V3Game; reqUser: UserDocument }) {
}
header={`${reqUser.username}'s ${FormatGame(game)} Profile`}
>
<UGPTHeaderBody game={game} reqUser={reqUser} stats={stats} />
<UserGameHeaderBody game={game} reqUser={reqUser} stats={stats} />
</LayoutHeaderContainer>
<Switch>
<Route exact path="/u/:userID/games/:game">
@@ -250,11 +250,11 @@ function Inner({ reqUser, game }: { game: V3Game; reqUser: UserDocument }) {
<LeaderboardsPage game={game} reqUser={reqUser} />
</Route>
<Route path="/u/:userID/games/:game/utils">
<UGPTUtilsPage game={game} reqUser={reqUser} />
<UserGameUtilsPage game={game} reqUser={reqUser} />
</Route>
<RequireAuthAsUserParam>
<Route exact path="/u/:userID/games/:game/settings">
<UGPTSettingsPage game={game} reqUser={reqUser} />
<UserGameSettingsPage game={game} reqUser={reqUser} />
</Route>
</RequireAuthAsUserParam>
<Route path="*">
@@ -1,5 +1,4 @@
import ImportAnalysers from "#app/pages/dashboard/utils/ImportAnalysers";
import React from "react";
import { Route, Switch } from "react-router-dom";
export default function UtilRoutes() {
@@ -17,7 +17,7 @@ import {
type RecordActivityReturn,
type SessionReturns,
} from "#types/api-returns";
import { type UGPT } from "#types/react";
import { type GameProfileProps } from "#types/react";
import { type ScoreDataset } from "#types/tables";
import {
type ClumpedActivity,
@@ -33,7 +33,7 @@ import { ONE_HOUR } from "#util/constants/time";
import { CreateScoreIDMap, CreateUserMap } from "#util/data";
import { NO_OP, TruncateString, UppercaseFirst } from "#util/misc";
import { FormatTime, MillisToSince } from "#util/time";
import React, { useContext, useEffect, useState } from "react";
import { useContext, useEffect, useState } from "react";
import { Button, Col, Row } from "react-bootstrap";
import { Link } from "react-router-dom";
import {
@@ -58,7 +58,7 @@ function activityUrlWithCursor(baseUrl: string, startTimeMs: number): string {
return `${path}?${params.toString()}`;
}
// Records activity for a group of users on a GPT. Also used for single users.
// Records activity for a group of users on a game. Also used for single users.
export default function Activity({
url,
handleNoActivity = (
@@ -317,7 +317,7 @@ function ScoresActivity({
>
<div className="timeline-content-title">
<span className="me-2">
<ProfilePicture size="sm" toGPT={{ game }} user={user} />
<ProfilePicture size="sm" toGame={{ game }} user={user} />
</span>
<Icon
style={{
@@ -326,7 +326,7 @@ function ScoresActivity({
type={`chevron-${show ? "down" : "right"}`}
/>
<span className="ms-2" style={{ fontSize: "1.15rem" }}>
<UGPTLink game={game} reqUser={user} /> highlighted {subMessage}!
<UserGameLink game={game} reqUser={user} /> highlighted {subMessage}!
</span>
{mutedText && (
<>
@@ -402,7 +402,7 @@ function GoalActivity({
>
<div className="timeline-content-title">
<span className="me-2">
<ProfilePicture size="sm" toGPT={{ game }} user={user} />
<ProfilePicture size="sm" toGame={{ game }} user={user} />
</span>
<Icon
style={{
@@ -411,7 +411,7 @@ function GoalActivity({
type={`chevron-${show ? "down" : "right"}`}
/>
<span className="ms-2" style={{ fontSize: "1.15rem" }}>
<UGPTLink game={game} reqUser={user} /> achieved {subMessage}!
<UserGameLink game={game} reqUser={user} /> achieved {subMessage}!
</span>
{mutedText && (
<>
@@ -470,9 +470,9 @@ function QuestActivity({
<div className="timeline-content-title">
<span style={{ fontSize: "1.15rem" }}>
<span className="me-2">
<ProfilePicture size="sm" toGPT={{ game }} user={user} />
<ProfilePicture size="sm" toGame={{ game }} user={user} />
</span>
<UGPTLink game={game} reqUser={user} /> completed the{" "}
<UserGameLink game={game} reqUser={user} /> completed the{" "}
<Link
className="text-decoration-none"
to={`/games/${game}/quests/${data.quest.questID}`}
@@ -529,7 +529,7 @@ function SessionActivity({
>
<div className="timeline-content-title">
<span className="me-2">
<ProfilePicture size="sm" toGPT={{ game }} user={user} />
<ProfilePicture size="sm" toGame={{ game }} user={user} />
</span>
<Icon
style={{
@@ -545,7 +545,7 @@ function SessionActivity({
}}
>
{/* worst string formatting ever */}
<UGPTLink game={game} reqUser={user} />{" "}
<UserGameLink game={game} reqUser={user} />{" "}
{isProbablyActive
? user.id === loggedInUser?.id
? "are having"
@@ -682,11 +682,11 @@ function ClassAchievementActivity({
<span className="me-2">
<ProfilePicture
size="sm"
toGPT={{ game: classGame }}
toGame={{ game: classGame }}
user={user}
/>
</span>
<UGPTLink game={classGame} reqUser={user} />{" "}
<UserGameLink game={classGame} reqUser={user} />{" "}
{data.source === "manual" ? (
<>
{data.classValue ? (
@@ -756,7 +756,7 @@ function ClassAchievementActivity({
);
}
function UGPTLink({ reqUser, game }: UGPT) {
function UserGameLink({ reqUser, game }: GameProfileProps) {
const { user } = useContext(UserContext);
return (
@@ -1,5 +1,4 @@
import { type JustChildren } from "#types/react";
import React from "react";
import { NavLink } from "react-router-dom";
import styles from "./AdminPanelNav.module.scss";
@@ -1,7 +1,7 @@
import { ErrorPage } from "#app/pages/ErrorPage";
import ErrorPage from "#app/pages/ErrorPage";
import { UserContext } from "#context/UserContext";
import { type JustChildren } from "#types/react";
import React, { useContext } from "react";
import { useContext } from "react";
import { useParams } from "react-router-dom";
import { UserAuthLevels } from "tachi-common";
@@ -1,6 +1,5 @@
import { type JustChildren } from "#types/react";
import { type BarDatum, type BarTooltipProps } from "@nivo/bar";
import React from "react";
/*function pointTooltipContent(point: PointTooltipProps["point"]) {
return (
@@ -1,4 +1,4 @@
import { GPT_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations";
import { GAME_CLIENT_IMPLEMENTATIONS } from "#lib/game-implementations";
import { ChangeOpacity } from "#util/color-opacity";
import { TACHI_LINE_THEME } from "#util/constants/chart-theme";
import { clamp } from "#util/misc";
@@ -11,7 +11,6 @@ import {
ResponsiveLine,
type Serie,
} from "@nivo/line";
import React from "react";
import { COLOUR_SET, type Difficulties, type GameGroup } from "tachi-common";
import ChartTooltip from "./ChartTooltip";
@@ -181,17 +180,17 @@ export default function GekichumaiScoreChart({
if (type === "Score") {
if (game === "chunithm") {
color =
GPT_CLIENT_IMPLEMENTATIONS.chunithm.difficultyColours[
GAME_CLIENT_IMPLEMENTATIONS.chunithm.difficultyColours[
difficulty as Difficulties["chunithm"]
];
} else if (game === "ongeki") {
color =
GPT_CLIENT_IMPLEMENTATIONS.ongeki.difficultyColours[
GAME_CLIENT_IMPLEMENTATIONS.ongeki.difficultyColours[
difficulty as Difficulties["ongeki"]
];
} else if (game === "maimaidx") {
color =
GPT_CLIENT_IMPLEMENTATIONS.maimaidx.difficultyColours[
GAME_CLIENT_IMPLEMENTATIONS.maimaidx.difficultyColours[
difficulty as Difficulties["maimaidx"]
];
}
@@ -1,6 +1,5 @@
import { TACHI_LINE_THEME } from "#util/constants/chart-theme";
import { ResponsiveLine, type Serie } from "@nivo/line";
import React from "react";
import { COLOUR_SET } from "tachi-common";
import ChartTooltip from "./ChartTooltip";
@@ -1,12 +1,12 @@
import Activity from "#components/activity/Activity";
import { AllLUGPTStatsContext } from "#context/AllLUGPTStatsContext";
import React, { useContext } from "react";
import { AllYourUGStatsContext } from "#context/AllYourUGStatsContext";
import { useContext } from "react";
import { type UserDocument } from "tachi-common";
import { DashboardLoggedInNoScores } from "./DashboardLoggedInNoScores";
export default function DashboardActivity({ user }: { user: UserDocument }) {
const { ugs } = useContext(AllLUGPTStatsContext);
const { ugs } = useContext(AllYourUGStatsContext);
if (ugs?.length === 0) {
return <DashboardLoggedInNoScores user={user} />;
@@ -1,5 +1,4 @@
import Navbar from "#components/nav/Navbar";
import React from "react";
export function DashboardHeader() {
const navItems = [

Some files were not shown because too many files have changed in this diff Show More