Fix Chart Time-Axis

Add Connection Checker
Fix Card Layout
Add Game-Icons
This commit is contained in:
2025-05-26 17:28:09 +02:00
parent b07d8a0839
commit 4955e72d34
9 changed files with 111 additions and 27 deletions

26
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/inter": "^5.2.5",
"@mui/icons-material": "^7.1.0",
"@mui/joy": "^5.0.0-beta.52",
"@mui/material": "^7.1.0",
"@mui/x-charts": "^8.4.0",
@ -1173,6 +1174,31 @@
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-7.1.0.tgz",
"integrity": "sha512-1mUPMAZ+Qk3jfgL5ftRR06ATH/Esi0izHl1z56H+df6cwIlCWG66RXciUqeJCttbOXOQ5y2DCjLZI/4t3Yg3LA==",
"dependencies": {
"@babel/runtime": "^7.27.1"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^7.1.0",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/joy": {
"version": "5.0.0-beta.52",
"resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.52.tgz",

View File

@ -13,6 +13,7 @@
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@fontsource/inter": "^5.2.5",
"@mui/icons-material": "^7.1.0",
"@mui/joy": "^5.0.0-beta.52",
"@mui/material": "^7.1.0",
"@mui/x-charts": "^8.4.0",

View File

@ -4,11 +4,11 @@ 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 {ApiUriContext, getData} from "./api/fetchApi.tsx";
import {GetGames, GetPlayers} from "./api/endpoints/Data.tsx";
import {
AccordionGroup,
Box,
Box, CircularProgress,
Divider,
Input,
Stack,
@ -17,9 +17,13 @@ import {
import GameAccordionItem from "./components/GameAccordionItem.tsx";
import PlayerAccordionItem from "./components/PlayerAccordionItem.tsx";
import StatsDrawer from "./components/StatsDrawer.tsx";
import {Cancel, CheckCircleOutline} from '@mui/icons-material';
export default function App() {
const [apiUri, setApiUri] = useState<string>("http://localhost:5239");
const [apiUri, setApiUri] = useState<string>("http://127.0.0.1:5239");
const [connected, setConnected] = useState<boolean>(false);
const [checkingConnection, setCheckingConnection] = useState<boolean>(false);
const [players, setPlayers] = useState<IPlayer[]>([]);
const [games, setGames] = useState<IGame[]>([]);
@ -34,15 +38,56 @@ export default function App() {
}
useEffect(() => {
if(!connected)
{
setPlayers([]);
setGames([]);
return;
}
GetPlayers(apiUri).then(setPlayers);
GetGames(apiUri).then(setGames);
}, [connected]);
const checkConnection = () => {
setCheckingConnection(true);
return getData(`${apiUri}/swagger/v1/swagger.json`)
.then(() => {
setConnected(true);
return Promise.resolve();
})
.catch(() => {
setConnected(false)
return Promise.reject();
})
.finally(() => {
setCheckingConnection(false)
});
}
const [connectionTimer, setConnectionTimer] = useState<number | null>(null);
useEffect(() => {
if(connectionTimer)
clearInterval(connectionTimer);
checkConnection()
.then(() => setConnectionTimer(setInterval(checkConnection, 5000)));
}, [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)} />
<Input type={"text"}
placeholder={"Api Uri"}
value={apiUri}
onChange={(e) => setApiUri(e.target.value)}
endDecorator={checkingConnection
? <CircularProgress sx={{height: "100%"}} />
: connected
? <CheckCircleOutline color={"success"} />
: <Cancel color={"error"} />}
/>
<Stack direction={"row"} spacing={2}>
<Box sx={{width:'50%'}}>
<Typography level={"h2"}>Players</Typography>

View File

@ -1,4 +1,6 @@
export default interface IGame {
appId: bigint,
name: string
name: string,
iconUrl?: string | null,
logoUrl?: string | null,
}

View File

@ -3,7 +3,7 @@ import {
Accordion,
AccordionDetails,
AccordionSummary,
Stack
Stack, Typography
} from "@mui/joy";
import type IPlayer from "../api/types/IPlayer.ts";
import {useContext, useEffect, useState} from "react";
@ -25,7 +25,7 @@ export default function GameAccordionItem({game, OpenDrawer} : {game: IGame, Ope
return (
<Accordion key={game.appId} onChange={(_, expanded) => setExpanded(expanded)}>
<AccordionSummary>{game.name}</AccordionSummary>
<AccordionSummary><img src={game.iconUrl??"favicon.ico"} /><Typography level={"h4"}>{game.name}</Typography></AccordionSummary>
<AccordionDetails>
<Stack flexWrap={"wrap"} direction={"row"} useFlexGap={true} spacing={1}>
{players?.map((player) => <PlayerCard player={player} onClick={() => OpenDrawer(player, game)} />)}

View File

@ -0,0 +1,19 @@
import {AspectRatio, Card, CardContent, Stack, Typography} from "@mui/joy";
import type IGame from "../api/types/IGame.ts";
export default function GameCard({game, onClick} : {game: IGame | null, onClick?: React.MouseEventHandler<HTMLDivElement> | undefined}) {
return (
<AspectRatio ratio={4} sx={{width: '256px'}}>
<Card onClick={onClick}>
<CardContent>
<Stack direction="row" spacing={1} sx={{minWidth: "100%", width: "fit-content"}} alignContent={"center"}>
<AspectRatio ratio={1} sx={{width: '64px'}}>
<img src={game?.iconUrl??"favicon.ico"} />
</AspectRatio>
<Typography level={"title-lg"} alignContent={"center"} overflow={"hidden"} noWrap>{game?.name}</Typography>
</Stack>
</CardContent>
</Card>
</AspectRatio>
);
}

View File

@ -3,15 +3,13 @@ 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";
import GameCard from "./GameCard.tsx";
export default function PlayerAccordionItem({player, OpenDrawer} : {player: IPlayer, OpenDrawer : (player: IPlayer, game: IGame) => void}) {
const apiUri = useContext(ApiUriContext);
@ -27,18 +25,10 @@ export default function PlayerAccordionItem({player, OpenDrawer} : {player: IPla
return (
<Accordion key={player.steamId} onChange={(_, expanded) => setExpanded(expanded)}>
<AccordionSummary><img src={player.avatarUrl} />{player.name}</AccordionSummary>
<AccordionSummary><img src={player.avatarUrl} /><Typography level={"h4"}>{player.name}</Typography></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} onClick={() => OpenDrawer(player, game)}>
<CardContent>
<Typography level={"body-lg"}>{game.name}</Typography>
</CardContent>
</Card>
</AspectRatio>
)}
{games?.map((game) => <GameCard game={game} onClick={() => OpenDrawer(player, game)} />)}
</Stack>
</AccordionDetails>
</Accordion>

View File

@ -3,14 +3,14 @@ import type IPlayer from "../api/types/IPlayer.ts";
export default function PlayerCard({player, onClick} : {player: IPlayer | null, onClick?: React.MouseEventHandler<HTMLDivElement> | undefined}) {
return (
<AspectRatio ratio={3} sx={{width: '192px'}}>
<AspectRatio ratio={4} sx={{width: '256px'}}>
<Card onClick={onClick}>
<CardContent sx={{width: "100%"}}>
<Stack direction="row" spacing={1} justifyContent={"flex-start"} sx={{width: "100%"}} alignContent={"center"}>
<CardContent>
<Stack direction="row" spacing={1} sx={{minWidth: "100%", width: "fit-content"}} alignContent={"center"}>
<AspectRatio ratio={1} sx={{width: '64px'}}>
<img src={player?.avatarUrl} />
</AspectRatio>
<Typography level={"h4"} alignContent={"center"}>{player?.name}</Typography>
<Typography level={"title-lg"} alignContent={"center"} overflow={"hidden"} noWrap>{player?.name}</Typography>
</Stack>
</CardContent>
</Card>

View File

@ -1,12 +1,13 @@
import type IPlayer from "../api/types/IPlayer.ts";
import type IGame from "../api/types/IGame.ts";
import {DialogContent, DialogTitle, Drawer, ModalClose, Typography} from "@mui/joy";
import {DialogContent, DialogTitle, Drawer, ModalClose} from "@mui/joy";
import {type Dispatch, useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../api/fetchApi.tsx";
import {GetTimelineGame} from "../api/endpoints/TimeTrack.tsx";
import type ITrackedTime from "../api/types/ITrackedTime.ts";
import {LineChart} from "@mui/x-charts";
import PlayerCard from "./PlayerCard.tsx";
import GameCard from "./GameCard.tsx";
export default function StatsDrawer({player, game, open, setOpen} : {player: IPlayer | null, game: IGame | null, open: boolean, setOpen: Dispatch<boolean>}) {
@ -24,11 +25,11 @@ export default function StatsDrawer({player, game, open, setOpen} : {player: IPl
<Drawer anchor={"bottom"} size={"lg"} open={open} onClose={() => setOpen(false)}>
<ModalClose />
<DialogTitle>
<Typography level={"h4"} alignContent={"center"}>{game?.name}</Typography>
<GameCard game={game} />
<PlayerCard player={player} />
</DialogTitle>
<DialogContent>
<LineChart xAxis={[{data : trackedTime?.map(t => t.timeStamp)??[], scaleType: "utc", label: "Date"}]}
<LineChart xAxis={[{data : trackedTime?.map(t => new Date(t.timeStamp))??[], scaleType: "utc", label: "Date"}]}
series={[{data: trackedTime?.map(t => Number(t.timePlayed))??[], label: "Minutes Played"}]}
sx={{height: "80%"}}/>
</DialogContent>