Account Modal - Games Tab (#4473)

Continuation of https://github.com/openfrontio/infra/pull/386, adds play
games sessions

<img width="971" height="771" alt="image"
src="https://github.com/user-attachments/assets/42c6bcbb-d690-4cd1-b859-3299a03f4350"
/>

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory

regression is found:

w.o.n

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ryan
2026-07-01 14:25:25 -07:00
committed by evanpelle
co-authored by Claude Opus 4.8
parent de08d9814f
commit 580460c969
11 changed files with 1077 additions and 333 deletions
+52
View File
@@ -3,8 +3,12 @@ import { z } from "zod";
import type { NewsItem } from "../core/ApiSchemas";
import {
NewsItemSchema,
PlayerGameModeFilter,
PlayerGameTypeFilter,
PlayerProfile,
PlayerProfileSchema,
PublicPlayerGamesResponse,
PublicPlayerGamesResponseSchema,
RankedLeaderboardResponse,
RankedLeaderboardResponseSchema,
UserMeResponse,
@@ -53,6 +57,54 @@ export async function fetchPlayerById(
}
}
// GET /public/player/:publicId/games — keyset-paginated personal game history.
// Public (no auth). `filter` (mode bucket) and `type` (game-type split) are
// orthogonal; `cursor` is the opaque token from the previous response's
// nextCursor — round-trip verbatim, never construct it.
export async function fetchPublicPlayerGames(
publicId: string,
opts: {
filter?: PlayerGameModeFilter;
type?: PlayerGameTypeFilter;
cursor?: string;
} = {},
): Promise<PublicPlayerGamesResponse | { error: "failed" }> {
try {
const url = new URL(
`${getApiBase()}/public/player/${encodeURIComponent(publicId)}/games`,
);
if (opts.filter) url.searchParams.set("filter", opts.filter);
if (opts.type) url.searchParams.set("type", opts.type);
if (opts.cursor) url.searchParams.set("cursor", opts.cursor);
const res = await fetch(url.toString(), {
headers: { Accept: "application/json" },
});
if (!res.ok) {
console.warn(
"fetchPublicPlayerGames: unexpected status",
res.status,
res.statusText,
);
return { error: "failed" };
}
const json = await res.json();
const parsed = PublicPlayerGamesResponseSchema.safeParse(json);
if (!parsed.success) {
console.warn(
"fetchPublicPlayerGames: Zod validation failed",
parsed.error,
);
return { error: "failed" };
}
return parsed.data;
} catch (err) {
console.warn("fetchPublicPlayerGames: request failed", err);
return { error: "failed" };
}
}
let __userMe: Promise<UserMeResponse | false> | null = null;
export async function getUserMe(): Promise<UserMeResponse | false> {
if (__userMe !== null) {