Files
OpenFrontIO/src/client/Api.ts
T
DevelopingTom f532dab704 Add end of game report window (#2598)
Resolves #1664

## Description:

Add a game ranking window, accessible through the player game history:
<img width="508" height="140" alt="image"
src="https://github.com/user-attachments/assets/51a628d9-628d-44c3-9776-d9b359b94e65"
/>

There is a lot of data players could be ranked with.
Three main ranking categories  with their own sub-categories:

<img width="371" height="264" alt="image"
src="https://github.com/user-attachments/assets/8b3b7c53-c52f-4b96-8039-23180c9181cf"
/>

### Duration:
Rank players according to their survival time

<img width="284" height="281" alt="image"
src="https://github.com/user-attachments/assets/6dfa0d11-7f5b-4f4f-81f8-f31e24ade6bf"
/>


### War:
#### Conquests:
Number of conquered players and bots

#### Bombs:
Show all bomb launched by each players. Can be sorted with each
category.
<img width="289" height="193" alt="image"
src="https://github.com/user-attachments/assets/fc0f9663-9a50-4098-b5c6-f434354accff"
/>

### Economy:
Show all gold earned by each players with trade, conquests, pirate or
total:

<img width="276" height="195" alt="image"
src="https://github.com/user-attachments/assets/a925249d-b2d2-4c61-92a5-4dbf5922b32b"
/>


### Responsiveness:


![ranking_showcase_resize](https://github.com/user-attachments/assets/5316d7f4-803f-4223-b834-783040226b7d)


## 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
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

IngloriousTom
2025-12-18 19:41:29 -08:00

180 lines
4.5 KiB
TypeScript

import { z } from "zod";
import {
PlayerProfile,
PlayerProfileSchema,
UserMeResponse,
UserMeResponseSchema,
} from "../core/ApiSchemas";
import { AnalyticsRecord, AnalyticsRecordSchema } from "../core/Schemas";
import { getAuthHeader, logOut, userAuth } from "./Auth";
export async function fetchPlayerById(
playerId: string,
): Promise<PlayerProfile | false> {
try {
const userAuthResult = await userAuth();
if (!userAuthResult) return false;
const { jwt } = userAuthResult;
const url = `${getApiBase()}/player/${encodeURIComponent(playerId)}`;
const res = await fetch(url, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${jwt}`,
},
});
if (res.status !== 200) {
console.warn(
"fetchPlayerById: unexpected status",
res.status,
res.statusText,
);
return false;
}
const json = await res.json();
const parsed = PlayerProfileSchema.safeParse(json);
if (!parsed.success) {
console.warn("fetchPlayerById: Zod validation failed", parsed.error);
return false;
}
return parsed.data;
} catch (err) {
console.warn("fetchPlayerById: request failed", err);
return false;
}
}
export async function getUserMe(): Promise<UserMeResponse | false> {
try {
const userAuthResult = await userAuth();
if (!userAuthResult) return false;
const { jwt } = userAuthResult;
// Get the user object
const response = await fetch(getApiBase() + "/users/@me", {
headers: {
authorization: `Bearer ${jwt}`,
},
});
if (response.status === 401) {
await logOut();
return false;
}
if (response.status !== 200) return false;
const body = await response.json();
const result = UserMeResponseSchema.safeParse(body);
if (!result.success) {
const error = z.prettifyError(result.error);
console.error("Invalid response", error);
return false;
}
return result.data;
} catch (e) {
return false;
}
}
export async function createCheckoutSession(
priceId: string,
colorPaletteName: string | null,
): Promise<string | false> {
try {
const response = await fetch(
`${getApiBase()}/stripe/create-checkout-session`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: await getAuthHeader(),
},
body: JSON.stringify({
priceId: priceId,
hostname: window.location.origin,
colorPaletteName: colorPaletteName,
}),
},
);
if (!response.ok) {
console.error(
"createCheckoutSession: request failed",
response.status,
response.statusText,
);
return false;
}
const json = await response.json();
return json.url;
} catch (e) {
console.error("createCheckoutSession: request failed", e);
return false;
}
}
export function getApiBase() {
const domainname = getAudience();
if (domainname === "localhost") {
const apiDomain = process?.env?.API_DOMAIN;
if (apiDomain) {
return `https://${apiDomain}`;
}
return localStorage.getItem("apiHost") ?? "http://localhost:8787";
}
return `https://api.${domainname}`;
}
export function getAudience() {
const { hostname } = new URL(window.location.href);
const domainname = hostname.split(".").slice(-2).join(".");
return domainname;
}
// Check if the user's account is linked to a Discord or email account.
export function hasLinkedAccount(
userMeResponse: UserMeResponse | false,
): boolean {
return (
userMeResponse !== false &&
(userMeResponse.user?.discord !== undefined ||
userMeResponse.user?.email !== undefined)
);
}
export async function fetchGameById(
gameId: string,
): Promise<AnalyticsRecord | false> {
try {
const url = `${getApiBase()}/game/${encodeURIComponent(gameId)}`;
const res = await fetch(url, {
headers: {
Accept: "application/json",
},
});
if (res.status !== 200) {
console.warn(
"fetchGameById: unexpected status",
res.status,
res.statusText,
);
return false;
}
const json = await res.json();
const parsed = AnalyticsRecordSchema.safeParse(json);
if (!parsed.success) {
console.warn("fetchGameById: Zod validation failed", parsed.error);
return false;
}
return parsed.data;
} catch (err) {
console.warn("fetchGameById: request failed", err);
return false;
}
}