Compare commits
5 Commits
4955e72d34
...
master
Author | SHA1 | Date | |
---|---|---|---|
327885f968 | |||
1df2506b8c | |||
952d1804ad | |||
f1b7f28fe3 | |||
c91831594f |
51
src/App.tsx
51
src/App.tsx
@ -8,7 +8,7 @@ import {ApiUriContext, getData} from "./api/fetchApi.tsx";
|
||||
import {GetGames, GetPlayers} from "./api/endpoints/Data.tsx";
|
||||
import {
|
||||
AccordionGroup,
|
||||
Box, CircularProgress,
|
||||
Box, Button, CircularProgress,
|
||||
Divider,
|
||||
Input,
|
||||
Stack,
|
||||
@ -16,8 +16,10 @@ import {
|
||||
} from "@mui/joy";
|
||||
import GameAccordionItem from "./components/GameAccordionItem.tsx";
|
||||
import PlayerAccordionItem from "./components/PlayerAccordionItem.tsx";
|
||||
import StatsDrawer from "./components/StatsDrawer.tsx";
|
||||
import PlayerGameStatsDrawer from "./components/PlayerGameStatsDrawer.tsx";
|
||||
import {Cancel, CheckCircleOutline} from '@mui/icons-material';
|
||||
import {PlayerStatsDrawer} from "./components/PlayerStatsDrawer.tsx";
|
||||
import {AddPlayer} from "./api/endpoints/Actions.tsx";
|
||||
|
||||
export default function App() {
|
||||
const [apiUri, setApiUri] = useState<string>("http://127.0.0.1:5239");
|
||||
@ -29,34 +31,33 @@ export default function App() {
|
||||
|
||||
const [selectedPlayer, setSelectedPlayer] = useState<IPlayer | null>(null);
|
||||
const [selectedGame, setSelectedGame] = useState<IGame | null>(null);
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [openPlayerGameStats, setOpenPlayerGameStats] = useState<boolean>(false);
|
||||
const [openPlayerStats, setOpenPlayerStats] = useState<boolean>(false);
|
||||
|
||||
const OpenDrawer = (player: IPlayer, game: IGame) => {
|
||||
const OpenPlayerGameStatsDrawer = (player: IPlayer, game: IGame) => {
|
||||
setSelectedPlayer(player);
|
||||
setSelectedGame(game);
|
||||
setOpen(true);
|
||||
setOpenPlayerGameStats(true);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if(!connected)
|
||||
{
|
||||
setPlayers([]);
|
||||
setGames([]);
|
||||
return;
|
||||
const OpenPlayerStatsDrawer = (player: IPlayer) => {
|
||||
setSelectedPlayer(player);
|
||||
setOpenPlayerStats(true);
|
||||
}
|
||||
GetPlayers(apiUri).then(setPlayers);
|
||||
GetGames(apiUri).then(setGames);
|
||||
}, [connected]);
|
||||
|
||||
const checkConnection = () => {
|
||||
setCheckingConnection(true);
|
||||
return getData(`${apiUri}/swagger/v1/swagger.json`)
|
||||
.then(() => {
|
||||
setConnected(true);
|
||||
GetPlayers(apiUri).then(setPlayers);
|
||||
GetGames(apiUri).then(setGames);
|
||||
return Promise.resolve();
|
||||
})
|
||||
.catch(() => {
|
||||
setConnected(false)
|
||||
setPlayers([]);
|
||||
setGames([]);
|
||||
return Promise.reject();
|
||||
})
|
||||
.finally(() => {
|
||||
@ -64,7 +65,6 @@ export default function App() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const [connectionTimer, setConnectionTimer] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@ -74,10 +74,16 @@ export default function App() {
|
||||
.then(() => setConnectionTimer(setInterval(checkConnection, 5000)));
|
||||
}, [apiUri]);
|
||||
|
||||
const [addPlayerStr, setAddPlayerStr] = useState("");
|
||||
const addPlayer = () => {
|
||||
const steamId = BigInt(addPlayerStr);
|
||||
AddPlayer(apiUri, steamId);
|
||||
}
|
||||
return (
|
||||
<ApiUriContext value={apiUri}>
|
||||
<PlayersContext.Provider value={{players: players}}>
|
||||
<GamesContext value={{games: games}}>
|
||||
<Stack direction="row" spacing={2} position={"fixed"} top={5}>
|
||||
<Input type={"text"}
|
||||
placeholder={"Api Uri"}
|
||||
value={apiUri}
|
||||
@ -88,22 +94,29 @@ export default function App() {
|
||||
? <CheckCircleOutline color={"success"} />
|
||||
: <Cancel color={"error"} />}
|
||||
/>
|
||||
<Stack direction={"row"} spacing={2}>
|
||||
<Input type={"text"}
|
||||
placeholder={"Add SteamId"}
|
||||
onChange={(e) => setAddPlayerStr(e.target.value)}
|
||||
endDecorator={<Button onClick={addPlayer}>Add</Button>}
|
||||
/>
|
||||
</Stack>
|
||||
<Stack direction={"row"} spacing={2} overflow={"scroll"} position={"fixed"} top={50} width={"100%"} height={"calc(100% - 50px)"}>
|
||||
<Box sx={{width:'50%'}}>
|
||||
<Typography level={"h2"}>Players</Typography>
|
||||
<AccordionGroup>
|
||||
{players?.map((player) => <PlayerAccordionItem key={player.steamId} player={player} OpenDrawer={OpenDrawer} />)}
|
||||
{players?.map((player) => <PlayerAccordionItem key={player.steamId} player={player} OpenPlayerGameStatsDrawer={OpenPlayerGameStatsDrawer} OpenPlayerStatsDrawer={OpenPlayerStatsDrawer} />)}
|
||||
</AccordionGroup>
|
||||
</Box>
|
||||
<Divider />
|
||||
<Box sx={{width:'50%'}}>
|
||||
<Typography level={"h2"}>Games</Typography>
|
||||
<AccordionGroup>
|
||||
{games?.map((game) => <GameAccordionItem key={game.appId} game={game} OpenDrawer={OpenDrawer} />)}
|
||||
{games?.map((game) => <GameAccordionItem key={game.appId} game={game} OpenPlayerGameStatsDrawer={OpenPlayerGameStatsDrawer} />)}
|
||||
</AccordionGroup>
|
||||
</Box>
|
||||
</Stack>
|
||||
<StatsDrawer player={selectedPlayer} game={selectedGame} open={open} setOpen={setOpen} />
|
||||
<PlayerGameStatsDrawer player={selectedPlayer} game={selectedGame} open={openPlayerGameStats} setOpen={setOpenPlayerGameStats} />
|
||||
<PlayerStatsDrawer player={selectedPlayer} open={openPlayerStats} setOpen={setOpenPlayerStats} />
|
||||
</GamesContext>
|
||||
</PlayersContext.Provider>
|
||||
</ApiUriContext>
|
||||
|
@ -2,7 +2,7 @@ import {getData} from "../fetchApi.tsx";
|
||||
import type ITrackedTime from "../types/ITrackedTime.ts";
|
||||
|
||||
export function GetTimelines(apiUri: string, steamId: bigint){
|
||||
return getData(`${apiUri}/TimeTrack/${steamId}`) as Promise<Map<bigint, ITrackedTime[]>>;
|
||||
return getData(`${apiUri}/TimeTrack/${steamId}`) as Promise<{key: bigint, value: ITrackedTime[]}[]>;
|
||||
}
|
||||
|
||||
export function GetTimelineGame(apiUri: string, steamId: bigint, appId: bigint){
|
||||
@ -14,5 +14,5 @@ export function GetTotal(apiUri: string, steamId: bigint){
|
||||
}
|
||||
|
||||
export function GetTotalPerGame(apiUri: string, steamId: bigint){
|
||||
return getData(`${apiUri}/TimeTrack/${steamId}/Total/PerGame`) as Promise<Map<bigint, bigint>>;
|
||||
return getData(`${apiUri}/TimeTrack/${steamId}/Total/PerGame`) as Promise<{key: bigint, value: bigint}[]>;
|
||||
}
|
||||
|
@ -2,8 +2,8 @@ import type IGame from "../api/types/IGame.ts";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Stack, Typography
|
||||
AccordionSummary, CircularProgress,
|
||||
Stack, Tooltip, Typography
|
||||
} from "@mui/joy";
|
||||
import type IPlayer from "../api/types/IPlayer.ts";
|
||||
import {useContext, useEffect, useState} from "react";
|
||||
@ -11,24 +11,35 @@ import {GetPlayersOfGame} from "../api/endpoints/Data.tsx";
|
||||
import {ApiUriContext} from "../api/fetchApi.tsx";
|
||||
import PlayerCard from "./PlayerCard.tsx";
|
||||
|
||||
export default function GameAccordionItem({game, OpenDrawer} : {game: IGame, OpenDrawer : (player: IPlayer, game: IGame) => void}) {
|
||||
export default function GameAccordionItem({game, OpenPlayerGameStatsDrawer} : {game: IGame, OpenPlayerGameStatsDrawer : (player: IPlayer, game: IGame) => void}) {
|
||||
const apiUri = useContext(ApiUriContext);
|
||||
|
||||
const [players, setPlayers] = useState<IPlayer[]>([]);
|
||||
const [expanded, setExpanded] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if(!expanded)
|
||||
return;
|
||||
GetPlayersOfGame(apiUri, game.appId).then(setPlayers);
|
||||
setLoading(true);
|
||||
GetPlayersOfGame(apiUri, game.appId)
|
||||
.then(setPlayers)
|
||||
.finally(() => setLoading(false));
|
||||
}, [expanded]);
|
||||
|
||||
return (
|
||||
<Accordion key={game.appId} onChange={(_, expanded) => setExpanded(expanded)}>
|
||||
<AccordionSummary><img src={game.iconUrl??"favicon.ico"} /><Typography level={"h4"}>{game.name}</Typography></AccordionSummary>
|
||||
<AccordionSummary>
|
||||
<img src={game.iconUrl??"favicon.ico"} />
|
||||
<Tooltip title={game.appId}>
|
||||
<Typography level={"h4"}>{game.name}</Typography>
|
||||
</Tooltip>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Stack flexWrap={"wrap"} direction={"row"} useFlexGap={true} spacing={1}>
|
||||
{players?.map((player) => <PlayerCard player={player} onClick={() => OpenDrawer(player, game)} />)}
|
||||
<Stack flexWrap={"wrap"} direction={"row"} spacing={1} useFlexGap>
|
||||
{loading
|
||||
? <CircularProgress />
|
||||
: players?.map((player) => <PlayerCard key={player.steamId} player={player} onClick={() => OpenPlayerGameStatsDrawer(player, game)} />)}
|
||||
</Stack>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
@ -2,33 +2,52 @@ import type IGame from "../api/types/IGame.ts";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Stack, Typography
|
||||
AccordionSummary, Button, CircularProgress,
|
||||
Stack, Tooltip, 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";
|
||||
import {GetTotal} from "../api/endpoints/TimeTrack.tsx";
|
||||
|
||||
export default function PlayerAccordionItem({player, OpenDrawer} : {player: IPlayer, OpenDrawer : (player: IPlayer, game: IGame) => void}) {
|
||||
export default function PlayerAccordionItem({player, OpenPlayerGameStatsDrawer, OpenPlayerStatsDrawer} : {player: IPlayer, OpenPlayerGameStatsDrawer : (player: IPlayer, game: IGame) => void, OpenPlayerStatsDrawer : (player: IPlayer) => void}) {
|
||||
const apiUri = useContext(ApiUriContext);
|
||||
|
||||
const [games, setGames] = useState<IGame[]>([]);
|
||||
const [expanded, setExpanded] = useState<boolean>(false);
|
||||
const [loadingGames, setLoadingGames] = useState<boolean>(false);
|
||||
const [totalTime, setTotalTime] = useState<bigint>(BigInt(0));
|
||||
|
||||
useEffect(() => {
|
||||
if(!expanded)
|
||||
return;
|
||||
GetGamesOfPlayer(apiUri, player.steamId).then(setGames);
|
||||
setLoadingGames(true);
|
||||
GetGamesOfPlayer(apiUri, player.steamId)
|
||||
.then(setGames)
|
||||
.finally(() => setLoadingGames(false));
|
||||
GetTotal(apiUri, player.steamId)
|
||||
.then(setTotalTime)
|
||||
}, [expanded]);
|
||||
|
||||
return (
|
||||
<Accordion key={player.steamId} onChange={(_, expanded) => setExpanded(expanded)}>
|
||||
<AccordionSummary><img src={player.avatarUrl} /><Typography level={"h4"}>{player.name}</Typography></AccordionSummary>
|
||||
<AccordionSummary>
|
||||
<img src={player.avatarUrl} />
|
||||
<Tooltip title={player.steamId}>
|
||||
<Typography level={"h4"}>{player.name}</Typography>
|
||||
</Tooltip>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Stack flexWrap={"wrap"} direction={"row"} useFlexGap={true} spacing={1}>
|
||||
{games?.map((game) => <GameCard game={game} onClick={() => OpenDrawer(player, game)} />)}
|
||||
<Stack flexWrap={"nowrap"} direction={"row"} alignContent={"center"} spacing={2} useFlexGap marginBottom={"10px"}>
|
||||
<Button onClick={() => OpenPlayerStatsDrawer(player)}>All Games</Button>
|
||||
<Typography level={"h4"} >Total Time: {totalTime}</Typography>
|
||||
</Stack>
|
||||
<Stack flexWrap={"wrap"} direction={"row"} spacing={1} useFlexGap>
|
||||
{loadingGames
|
||||
? <CircularProgress />
|
||||
: games?.map((game) => <GameCard key={game.appId} game={game} onClick={() => OpenPlayerGameStatsDrawer(player, game)} />)}
|
||||
</Stack>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
@ -9,7 +9,7 @@ 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>}) {
|
||||
export default function PlayerGameStatsDrawer({player, game, open, setOpen} : {player: IPlayer | null, game: IGame | null, open: boolean, setOpen: Dispatch<boolean>}) {
|
||||
|
||||
const apiUri = useContext(ApiUriContext);
|
||||
|
||||
@ -30,7 +30,8 @@ export default function StatsDrawer({player, game, open, setOpen} : {player: IPl
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<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"}]}
|
||||
yAxis={[{label: "Minutes Played", scaleType: "linear", min: 0}]}
|
||||
series={[{data: trackedTime?.map(t => Number(t.timePlayed))??[], label: game?.name}]}
|
||||
sx={{height: "80%"}}/>
|
||||
</DialogContent>
|
||||
</Drawer>
|
88
src/components/PlayerStatsDrawer.tsx
Normal file
88
src/components/PlayerStatsDrawer.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import type IPlayer from "../api/types/IPlayer.ts";
|
||||
import {Autocomplete, DialogContent, DialogTitle, Drawer, ModalClose, Stack} from "@mui/joy";
|
||||
import {type Dispatch, useContext, useEffect, useState} from "react";
|
||||
import {ApiUriContext} from "../api/fetchApi.tsx";
|
||||
import {LineChart, type LineSeriesType} from "@mui/x-charts";
|
||||
import PlayerCard from "./PlayerCard.tsx";
|
||||
import {GamesContext} from "../api/contexts/GamesContext.tsx";
|
||||
import {GetTimelines} from "../api/endpoints/TimeTrack.tsx";
|
||||
import type {DatasetElementType} from "@mui/x-charts/internals";
|
||||
|
||||
export function PlayerStatsDrawer({player, open, setOpen}: {
|
||||
player: IPlayer | null,
|
||||
open: boolean,
|
||||
setOpen: Dispatch<boolean>
|
||||
}) {
|
||||
|
||||
const apiUri = useContext(ApiUriContext);
|
||||
const games = useContext(GamesContext);
|
||||
|
||||
const [trackedTimes, setTrackedTimes] = useState<DatasetElementType<string | number | Date | null | undefined>[]>([]);
|
||||
const [availableSeries, setAvailableSeries] = useState<LineSeriesType[]>([]);
|
||||
const [selectedSeries, setSelectedSeries] = useState<LineSeriesType[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !player)
|
||||
return;
|
||||
GetTimelines(apiUri, player.steamId)
|
||||
.then((tt) => {
|
||||
const times: DatasetElementType<string | number | Date | null | undefined>[] = [];
|
||||
const g: Set<bigint> = new Set<bigint>();
|
||||
tt.forEach((app) => {
|
||||
app.value.forEach((time) => {
|
||||
let o = {date: new Date(time.timeStamp)};
|
||||
// @ts-ignore
|
||||
o[`${app.key.toString()}`] = Number(time.timePlayed / 60)
|
||||
times.push(o);
|
||||
g.add(app.key);
|
||||
});
|
||||
});
|
||||
|
||||
const seriesTypes: LineSeriesType[] = [];
|
||||
const iterator = g.keys();
|
||||
let value = iterator.next();
|
||||
while (!value.done) {
|
||||
const appId = value.value;
|
||||
seriesTypes.push({
|
||||
type: "line",
|
||||
dataKey: appId.toString(),
|
||||
connectNulls: true,
|
||||
label: games.games.find(g => g.appId == appId)?.name ?? appId?.toString() ?? ""
|
||||
})
|
||||
value = iterator.next();
|
||||
}
|
||||
|
||||
setAvailableSeries(seriesTypes);
|
||||
setTrackedTimes(times);
|
||||
});
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Drawer anchor={"bottom"} size={"lg"} open={open} onClose={() => setOpen(false)}>
|
||||
<ModalClose/>
|
||||
<DialogTitle>
|
||||
<PlayerCard player={player}/>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack direction={"column"} height={"100%"}>
|
||||
<Autocomplete multiple
|
||||
options={availableSeries.sort((a, b) => (a.label as string).localeCompare(b.label as string))}
|
||||
placeholder={"Games"}
|
||||
getOptionLabel={(opt) => opt.label as string}
|
||||
isOptionEqualToValue={(a, b) => a.dataKey == b.dataKey}
|
||||
onChange={(_, value, reason) => {
|
||||
if(reason == "selectOption" || reason == "removeOption" || reason == "clear")
|
||||
setSelectedSeries(value);
|
||||
console.log(value);
|
||||
}}
|
||||
/>
|
||||
<LineChart dataset={trackedTimes}
|
||||
xAxis={[{dataKey: "date", scaleType: "utc"}]}
|
||||
yAxis={[{dataKey: "time", scaleType: "linear", min: 0}]}
|
||||
series={selectedSeries}
|
||||
/>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user