Account Modal - Games Tab (#4473)

## Description:

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"
/>

## Please complete the following:

- [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

## Please put your Discord username so you can be contacted if a bug or
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:12:37 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent c9850c472b
commit 1d5a6ae246
11 changed files with 1079 additions and 334 deletions
+52 -13
View File
@@ -2,7 +2,7 @@ import { z } from "zod";
import { base64urlToUuid } from "./Base64";
import { ClanTagSchema } from "./Schemas";
import { BigIntStringSchema, PlayerStatsSchema } from "./StatsSchemas";
import { Difficulty, GameMode, GameType, RankedType } from "./game/Game";
import { Difficulty, GameMode, RankedType } from "./game/Game";
function stripClanTagFromUsername(username: string): string {
return username.replace(/^\s*\[[a-zA-Z0-9]{2,5}\]\s*/u, "").trim();
@@ -160,25 +160,64 @@ export const PlayerStatsTreeSchema = z.object({
});
export type PlayerStatsTree = z.infer<typeof PlayerStatsTreeSchema>;
export const PlayerGameSchema = z.object({
gameId: z.string(),
start: z.iso.datetime(),
mode: z.enum(GameMode),
type: z.enum(GameType),
map: z.string(),
difficulty: z.enum(Difficulty),
clientId: z.string().optional(),
});
export type PlayerGame = z.infer<typeof PlayerGameSchema>;
export const PlayerProfileSchema = z.object({
createdAt: z.iso.datetime(),
user: DiscordUserSchema.optional(),
games: PlayerGameSchema.array(),
stats: PlayerStatsTreeSchema,
});
export type PlayerProfile = z.infer<typeof PlayerProfileSchema>;
// Mode buckets for GET /public/player/:publicId/games — mirrors the clan
// game-history filter (see ClanGameFilter). Resolved server-side off the
// games join (mode / ranked_type / player_teams).
export const PlayerGameModeFilters = ["ffa", "team", "hvn", "ranked"] as const;
export const PlayerGameModeFilterSchema = z.enum(PlayerGameModeFilters);
export type PlayerGameModeFilter = z.infer<typeof PlayerGameModeFilterSchema>;
// Game-type split — orthogonal to the mode filter. Matches games.type.
export const PlayerGameTypeFilters = [
"public",
"private",
"singleplayer",
] as const;
export const PlayerGameTypeFilterSchema = z.enum(PlayerGameTypeFilters);
export type PlayerGameTypeFilter = z.infer<typeof PlayerGameTypeFilterSchema>;
// "incomplete" covers games with no recorded winner (winnerType IS NULL).
export const PlayerGameResultSchema = z.enum([
"victory",
"defeat",
"incomplete",
]);
export type PlayerGameResult = z.infer<typeof PlayerGameResultSchema>;
export const PublicPlayerGameSchema = z.object({
gameId: z.string(),
start: z.iso.datetime(),
durationSeconds: z.number().int().nonnegative(),
map: z.string(),
mode: z.string(),
type: z.string(),
playerTeams: z.string().nullable(),
rankedType: z.string(),
result: PlayerGameResultSchema,
totalPlayers: z.number().int().nonnegative().nullable(),
username: z.string(),
clanTag: z.string().nullable(),
});
export type PublicPlayerGame = z.infer<typeof PublicPlayerGameSchema>;
export const PublicPlayerGamesResponseSchema = z.object({
results: PublicPlayerGameSchema.array(),
// Opaque continuation token. Round-trip verbatim as the `cursor` query
// parameter to fetch the next page; never construct or parse it. `null`
// means the server has no more rows to serve.
nextCursor: z.string().nullable(),
});
export type PublicPlayerGamesResponse = z.infer<
typeof PublicPlayerGamesResponseSchema
>;
export const PlayerLeaderboardEntrySchema = z.object({
rank: z.number(),
playerId: z.string(),