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/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@fontsource/inter": "^5.2.5", "@fontsource/inter": "^5.2.5",
"@mui/icons-material": "^7.1.0",
"@mui/joy": "^5.0.0-beta.52", "@mui/joy": "^5.0.0-beta.52",
"@mui/material": "^7.1.0", "@mui/material": "^7.1.0",
"@mui/x-charts": "^8.4.0", "@mui/x-charts": "^8.4.0",
@ -1173,6 +1174,31 @@
"url": "https://opencollective.com/mui-org" "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": { "node_modules/@mui/joy": {
"version": "5.0.0-beta.52", "version": "5.0.0-beta.52",
"resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.52.tgz", "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/react": "^11.14.0",
"@emotion/styled": "^11.14.0", "@emotion/styled": "^11.14.0",
"@fontsource/inter": "^5.2.5", "@fontsource/inter": "^5.2.5",
"@mui/icons-material": "^7.1.0",
"@mui/joy": "^5.0.0-beta.52", "@mui/joy": "^5.0.0-beta.52",
"@mui/material": "^7.1.0", "@mui/material": "^7.1.0",
"@mui/x-charts": "^8.4.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 {useEffect, useState} from "react";
import type IGame from "./api/types/IGame.ts"; import type IGame from "./api/types/IGame.ts";
import {GamesContext} from "./api/contexts/GamesContext.tsx"; 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 {GetGames, GetPlayers} from "./api/endpoints/Data.tsx";
import { import {
AccordionGroup, AccordionGroup,
Box, Box, CircularProgress,
Divider, Divider,
Input, Input,
Stack, Stack,
@ -17,9 +17,13 @@ import {
import GameAccordionItem from "./components/GameAccordionItem.tsx"; import GameAccordionItem from "./components/GameAccordionItem.tsx";
import PlayerAccordionItem from "./components/PlayerAccordionItem.tsx"; import PlayerAccordionItem from "./components/PlayerAccordionItem.tsx";
import StatsDrawer from "./components/StatsDrawer.tsx"; import StatsDrawer from "./components/StatsDrawer.tsx";
import {Cancel, CheckCircleOutline} from '@mui/icons-material';
export default function App() { 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 [players, setPlayers] = useState<IPlayer[]>([]);
const [games, setGames] = useState<IGame[]>([]); const [games, setGames] = useState<IGame[]>([]);
@ -34,15 +38,56 @@ export default function App() {
} }
useEffect(() => { useEffect(() => {
if(!connected)
{
setPlayers([]);
setGames([]);
return;
}
GetPlayers(apiUri).then(setPlayers); GetPlayers(apiUri).then(setPlayers);
GetGames(apiUri).then(setGames); 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]); }, [apiUri]);
return ( return (
<ApiUriContext value={apiUri}> <ApiUriContext value={apiUri}>
<PlayersContext.Provider value={{players: players}}> <PlayersContext.Provider value={{players: players}}>
<GamesContext value={{games: games}}> <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}> <Stack direction={"row"} spacing={2}>
<Box sx={{width:'50%'}}> <Box sx={{width:'50%'}}>
<Typography level={"h2"}>Players</Typography> <Typography level={"h2"}>Players</Typography>

View File

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

View File

@ -3,7 +3,7 @@ import {
Accordion, Accordion,
AccordionDetails, AccordionDetails,
AccordionSummary, AccordionSummary,
Stack Stack, Typography
} from "@mui/joy"; } from "@mui/joy";
import type IPlayer from "../api/types/IPlayer.ts"; import type IPlayer from "../api/types/IPlayer.ts";
import {useContext, useEffect, useState} from "react"; import {useContext, useEffect, useState} from "react";
@ -25,7 +25,7 @@ export default function GameAccordionItem({game, OpenDrawer} : {game: IGame, Ope
return ( return (
<Accordion key={game.appId} onChange={(_, expanded) => setExpanded(expanded)}> <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> <AccordionDetails>
<Stack flexWrap={"wrap"} direction={"row"} useFlexGap={true} spacing={1}> <Stack flexWrap={"wrap"} direction={"row"} useFlexGap={true} spacing={1}>
{players?.map((player) => <PlayerCard player={player} onClick={() => OpenDrawer(player, game)} />)} {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, Accordion,
AccordionDetails, AccordionDetails,
AccordionSummary, AccordionSummary,
AspectRatio,
Card,
CardContent,
Stack, Typography Stack, Typography
} from "@mui/joy"; } from "@mui/joy";
import type IPlayer from "../api/types/IPlayer.ts"; import type IPlayer from "../api/types/IPlayer.ts";
import {useContext, useEffect, useState} from "react"; import {useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../api/fetchApi.tsx"; import {ApiUriContext} from "../api/fetchApi.tsx";
import {GetGamesOfPlayer} from "../api/endpoints/Data.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}) { export default function PlayerAccordionItem({player, OpenDrawer} : {player: IPlayer, OpenDrawer : (player: IPlayer, game: IGame) => void}) {
const apiUri = useContext(ApiUriContext); const apiUri = useContext(ApiUriContext);
@ -27,18 +25,10 @@ export default function PlayerAccordionItem({player, OpenDrawer} : {player: IPla
return ( return (
<Accordion key={player.steamId} onChange={(_, expanded) => setExpanded(expanded)}> <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> <AccordionDetails>
<Stack flexWrap={"wrap"} direction={"row"} useFlexGap={true} spacing={1}> <Stack flexWrap={"wrap"} direction={"row"} useFlexGap={true} spacing={1}>
{games?.map((game) => {games?.map((game) => <GameCard game={game} onClick={() => OpenDrawer(player, 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>
)}
</Stack> </Stack>
</AccordionDetails> </AccordionDetails>
</Accordion> </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}) { export default function PlayerCard({player, onClick} : {player: IPlayer | null, onClick?: React.MouseEventHandler<HTMLDivElement> | undefined}) {
return ( return (
<AspectRatio ratio={3} sx={{width: '192px'}}> <AspectRatio ratio={4} sx={{width: '256px'}}>
<Card onClick={onClick}> <Card onClick={onClick}>
<CardContent sx={{width: "100%"}}> <CardContent>
<Stack direction="row" spacing={1} justifyContent={"flex-start"} sx={{width: "100%"}} alignContent={"center"}> <Stack direction="row" spacing={1} sx={{minWidth: "100%", width: "fit-content"}} alignContent={"center"}>
<AspectRatio ratio={1} sx={{width: '64px'}}> <AspectRatio ratio={1} sx={{width: '64px'}}>
<img src={player?.avatarUrl} /> <img src={player?.avatarUrl} />
</AspectRatio> </AspectRatio>
<Typography level={"h4"} alignContent={"center"}>{player?.name}</Typography> <Typography level={"title-lg"} alignContent={"center"} overflow={"hidden"} noWrap>{player?.name}</Typography>
</Stack> </Stack>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -1,12 +1,13 @@
import type IPlayer from "../api/types/IPlayer.ts"; import type IPlayer from "../api/types/IPlayer.ts";
import type IGame from "../api/types/IGame.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 {type Dispatch, useContext, useEffect, useState} from "react";
import {ApiUriContext} from "../api/fetchApi.tsx"; import {ApiUriContext} from "../api/fetchApi.tsx";
import {GetTimelineGame} from "../api/endpoints/TimeTrack.tsx"; import {GetTimelineGame} from "../api/endpoints/TimeTrack.tsx";
import type ITrackedTime from "../api/types/ITrackedTime.ts"; import type ITrackedTime from "../api/types/ITrackedTime.ts";
import {LineChart} from "@mui/x-charts"; import {LineChart} from "@mui/x-charts";
import PlayerCard from "./PlayerCard.tsx"; 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>}) { 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)}> <Drawer anchor={"bottom"} size={"lg"} open={open} onClose={() => setOpen(false)}>
<ModalClose /> <ModalClose />
<DialogTitle> <DialogTitle>
<Typography level={"h4"} alignContent={"center"}>{game?.name}</Typography> <GameCard game={game} />
<PlayerCard player={player} /> <PlayerCard player={player} />
</DialogTitle> </DialogTitle>
<DialogContent> <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"}]} series={[{data: trackedTime?.map(t => Number(t.timePlayed))??[], label: "Minutes Played"}]}
sx={{height: "80%"}}/> sx={{height: "80%"}}/>
</DialogContent> </DialogContent>