Basic functionality

This commit is contained in:
2025-05-26 04:22:15 +02:00
parent 2f523a9219
commit 521e53fadd
6 changed files with 193 additions and 4 deletions

View File

@ -1,9 +1,54 @@
import './App.css'
import {PlayersContext} from "./api/contexts/PlayersContext.tsx";
import type IPlayer from "./api/types/IPlayer.ts";
import {useEffect, useState} from "react";
import type IGame from "./api/types/IGame.ts";
import {GamesContext} from "./api/contexts/GamesContext.tsx";
import {ApiUriContext} from "./api/fetchApi.tsx";
import {GetGames, GetPlayers} from "./api/endpoints/Data.tsx";
import {
AccordionGroup,
Box,
Divider,
Input,
Stack,
Typography
} from "@mui/joy";
import GameAccordionItem from "./components/GameAccordionItem.tsx";
import PlayerAccordionItem from "./components/PlayerAccordionItem.tsx";
export default function App() {
const [apiUri, setApiUri] = useState<string>("http://localhost:5239");
const [players, setPlayers] = useState<IPlayer[]>([]);
const [games, setGames] = useState<IGame[]>([]);
useEffect(() => {
GetPlayers(apiUri).then(setPlayers);
GetGames(apiUri).then(setGames);
}, [apiUri]);
return (
<>
</>
<ApiUriContext value={apiUri}>
<PlayersContext.Provider value={{players: players}}>
<GamesContext value={{games: games}}>
<Input type={"text"} placeholder={"Api Uri"} value={apiUri} onChange={(e) => setApiUri(e.target.value)} />
<Stack direction={"row"} spacing={2}>
<Box sx={{width:'50%'}}>
<Typography level={"h2"}>Players</Typography>
<AccordionGroup>
{players?.map((player) => <PlayerAccordionItem key={player.steamId} player={player} />)}
</AccordionGroup>
</Box>
<Divider />
<Box sx={{width:'50%'}}>
<Typography level={"h2"}>Games</Typography>
<AccordionGroup>
{games?.map((game) => <GameAccordionItem key={game.appId} game={game}/>)}
</AccordionGroup>
</Box>
</Stack>
</GamesContext>
</PlayersContext.Provider>
</ApiUriContext>
)
}

View File

@ -0,0 +1,8 @@
import {createContext} from "react";
import type IGame from "../types/IGame.ts";
export const GamesContext = createContext<{games: IGame[]}>(
{
games: []
}
);

View File

@ -0,0 +1,8 @@
import {createContext} from "react";
import type IPlayer from "../types/IPlayer.ts";
export const PlayersContext = createContext<{players: IPlayer[]}>(
{
players: []
}
);

View File

@ -70,8 +70,7 @@ function makeRequest(method: string, uri: string, content?: object | string | nu
}else
throw new Error(response.statusText);
}
let json = response.json();
return json.then((json) => json).catch(() => null);
return response.text().then(text => JSON.parseBigInt(text) as object);
})
.catch(function(err : Error){
console.error(`Error ${method}ing Data ${uri}\n${err}`);
@ -79,6 +78,39 @@ function makeRequest(method: string, uri: string, content?: object | string | nu
}).finally(() => currentlyRequestedEndpoints.splice(currentlyRequestedEndpoints.indexOf(id), 1));
}
declare global {
interface JSON {
parseBigInt: (jsonStr: string, options?: { minDigits?: number; fallbackToString?: boolean; }) => unknown;
}
}
function quoteLargeNumbers(jsonStr: string, minDigits: number): string {
const regex = new RegExp(`(-?\\d{${minDigits},})(?=\\s*[,}\\]])`, 'g');
return jsonStr.replace(regex, '"$1"');
}
JSON.parseBigInt = function (jsonStr: string, options?: { minDigits?: number; fallbackToString?: boolean;}): unknown {
const minDigits = options?.minDigits ?? 15;
const fallbackToString = options?.fallbackToString ?? true;
const safeStr = quoteLargeNumbers(jsonStr, minDigits);
return JSON.parse(safeStr, (_key, value) => {
if (typeof value === 'string') {
const bigintRegex = new RegExp(`^-?\\d{${minDigits},}$`);
if (bigintRegex.test(value)) {
try {
if (typeof BigInt !== 'undefined') return BigInt(value);
else if (fallbackToString) return value;
} catch {
return value;
}
}
}
return value;
});
};
export function isValidUri(uri: string) : boolean{
try {
new URL(uri);

View File

@ -0,0 +1,50 @@
import type IGame from "../api/types/IGame.ts";
import {
Accordion,
AccordionDetails,
AccordionSummary,
AspectRatio,
Card,
CardContent,
CardCover,
Stack, Typography
} from "@mui/joy";
import type IPlayer from "../api/types/IPlayer.ts";
import {useContext, useEffect, useState} from "react";
import {GetPlayersOfGame} from "../api/endpoints/Data.tsx";
import {ApiUriContext} from "../api/fetchApi.tsx";
export default function GameAccordionItem({game} : {game: IGame}) {
const apiUri = useContext(ApiUriContext);
const [players, setPlayers] = useState<IPlayer[]>([]);
const [expanded, setExpanded] = useState<boolean>(false);
useEffect(() => {
if(!expanded)
return;
GetPlayersOfGame(apiUri, game.appId).then(setPlayers);
}, [expanded]);
return (
<Accordion key={game.appId} onChange={(_, expanded) => setExpanded(expanded)}>
<AccordionSummary>{game.name}</AccordionSummary>
<AccordionDetails>
<Stack flexWrap={"wrap"} direction={"row"} useFlexGap={true} spacing={1}>
{players?.map((player) =>
<AspectRatio sx={{width: "64px"}} ratio={1}>
<Card key={player.steamId}>
<CardCover>
<img src={player.avatarUrl} />
</CardCover>
<CardContent>
<Typography level={"body-lg"}>{player.name}</Typography>
</CardContent>
</Card>
</AspectRatio>
)}
</Stack>
</AccordionDetails>
</Accordion>
);
}

View File

@ -0,0 +1,46 @@
import type IGame from "../api/types/IGame.ts";
import {
Accordion,
AccordionDetails,
AccordionSummary,
AspectRatio,
Card,
CardContent,
Stack, Typography
} from "@mui/joy";
import type IPlayer from "../api/types/IPlayer.ts";
import {useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../api/fetchApi.tsx";
import {GetGamesOfPlayer} from "../api/endpoints/Data.tsx";
export default function PlayerAccordionItem({player} : {player: IPlayer}) {
const apiUri = useContext(ApiUriContext);
const [games, setGames] = useState<IGame[]>([]);
const [expanded, setExpanded] = useState<boolean>(false);
useEffect(() => {
if(!expanded)
return;
GetGamesOfPlayer(apiUri, player.steamId).then(setGames);
}, [expanded]);
return (
<Accordion key={player.steamId} onChange={(_, expanded) => setExpanded(expanded)}>
<AccordionSummary>{player.name}</AccordionSummary>
<AccordionDetails>
<Stack flexWrap={"wrap"} direction={"row"} useFlexGap={true} spacing={1}>
{games?.map((game) =>
<AspectRatio sx={{width: "128px"}} ratio={2/3}>
<Card key={game.appId}>
<CardContent>
<Typography level={"body-lg"}>{game.name}</Typography>
</CardContent>
</Card>
</AspectRatio>
)}
</Stack>
</AccordionDetails>
</Accordion>
);
}