mirror of
https://github.com/zkldi/Tachi.git
synced 2026-09-22 15:14:27 +03:00
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:
@@ -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/`.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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
@@ -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:"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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=""
|
||||
|
||||
@@ -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" />
|
||||
|
||||
+17
-24
@@ -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 }}
|
||||
+5
-16
@@ -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<{
|
||||
+5
-13
@@ -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">
|
||||
+11
-18
@@ -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>
|
||||
))}
|
||||
+2
-3
@@ -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";
|
||||
|
||||
+12
-12
@@ -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 &&
|
||||
|
||||
+22
-22
@@ -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 (
|
||||
|
||||
+12
-12
@@ -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,
|
||||
|
||||
+3
-3
@@ -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 () => {
|
||||
|
||||
+3
-3
@@ -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);
|
||||
|
||||
+31
-32
@@ -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);
|
||||
|
||||
+15
-13
@@ -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>
|
||||
|
||||
+5
-5
@@ -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;
|
||||
|
||||
+2
-2
@@ -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)",
|
||||
});
|
||||
|
||||
+3
-3
@@ -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} />;
|
||||
|
||||
+3
-3
@@ -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();
|
||||
|
||||
+2
-3
@@ -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",
|
||||
|
||||
+24
-24
@@ -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;
|
||||
|
||||
|
||||
+6
-6
@@ -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)}
|
||||
|
||||
+5
-5
@@ -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],
|
||||
|
||||
+5
-5
@@ -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(() => {
|
||||
|
||||
+4
-5
@@ -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();
|
||||
}
|
||||
|
||||
+4
-5
@@ -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?</>;
|
||||
|
||||
+4
-5
@@ -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>;
|
||||
|
||||
+4
-4
@@ -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);
|
||||
|
||||
|
||||
+8
-9
@@ -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>
|
||||
|
||||
+5
-5
@@ -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()],
|
||||
+3
-3
@@ -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<{
|
||||
+6
-6
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user