Fix Chart Time-Axis
Add Connection Checker Fix Card Layout Add Game-Icons
This commit is contained in:
26
package-lock.json
generated
26
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
53
src/App.tsx
53
src/App.tsx
@ -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>
|
||||||
|
@ -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,
|
||||||
}
|
}
|
@ -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)} />)}
|
||||||
|
19
src/components/GameCard.tsx
Normal file
19
src/components/GameCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Reference in New Issue
Block a user