Skip to content

Commit

Permalink
Merge pull request #104 from martius-lab/svea/web-leaderboard
Browse files Browse the repository at this point in the history
web: leaderboard
  • Loading branch information
SveaGaenzle authored Mar 14, 2024
2 parents ba1835c + 3918abc commit 1967cc5
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 16 deletions.
23 changes: 22 additions & 1 deletion comprl-web/app/db/sqlite.data.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,27 @@ export async function getUser(username: string, password: string) {
return { id: res.user_id, name: res.username, role: res.role, token: res.token } as User;
}

export async function getAllUsers() {
const db = new Database('users.db', { verbose: console.log });
const query = 'SELECT * FROM users';
const users = db.prepare(query).all();
db.close();
return users;
}


export async function getRankedUsers() {
const users = await getAllUsers();

const rankedUsers = users.sort((a, b) => {
// Sort by descending (mu - sigma)
return (b.mu - b.sigma) - (a.mu - a.sigma);
});

return rankedUsers;
}


export async function getStatistics(user_id: number) {
const gameDB = new Database('game.db', { verbose: console.log });

Expand All @@ -65,4 +86,4 @@ export async function getStatistics(user_id: number) {
gameDB.close();

return {playedGames: playedGames, wonGames: wonGames, disconnectedGames: disconnectedGames} as Statistics
}
}
185 changes: 185 additions & 0 deletions comprl-web/app/routes/_dashboard.leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import * as React from 'react';
import { useTheme } from '@mui/material';
import { Typography, Table, TableContainer, TableHead, TableBody, TableRow, TableCell, Paper, Box, TableFooter, TablePagination, IconButton } from "@mui/material";
import { FirstPage, KeyboardArrowLeft, KeyboardArrowRight, LastPage } from '@mui/icons-material';
import { LoaderFunctionArgs } from "@remix-run/node";
import { authenticator } from "~/services/auth.server";
import { getSession } from "~/services/session.server";
import { useLoaderData } from "@remix-run/react";
import { getRankedUsers } from "~/db/sqlite.data";

export async function loader({ request, params }: LoaderFunctionArgs) {
const user = await authenticator.isAuthenticated(request, {
failureRedirect: "/login",
});

const session = await getSession(request.headers.get("Cookie"));

if (params.name !== user.name) {

session.flash("popup", { message: "You don't have permission to access that page", severity: "error" });
}

const users = await getRankedUsers();
return {
users: users, loggedInUsername: user.name
};


}

interface TablePaginationActionsProps {
count: number;
page: number;
rowsPerPage: number;
onPageChange: (
event: React.MouseEvent<HTMLButtonElement>,
newPage: number,
) => void;
}

function TablePaginationActions(props: TablePaginationActionsProps) {
const theme = useTheme();
const { count, page, rowsPerPage, onPageChange } = props;

const handleFirstPageButtonClick = (
event: React.MouseEvent<HTMLButtonElement>,
) => {
onPageChange(event, 0);
};

const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, page - 1);
};

const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, page + 1);
};

const handleLastPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};

return (
<Box sx={{ flexShrink: 0, ml: 2.5 }}>
<IconButton
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
{theme.direction === 'rtl' ? <LastPage /> : <FirstPage />}
</IconButton>
<IconButton
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
{theme.direction === 'rtl' ? <KeyboardArrowRight /> : <KeyboardArrowLeft />}
</IconButton>
<IconButton
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
{theme.direction === 'rtl' ? <KeyboardArrowLeft /> : <KeyboardArrowRight />}
</IconButton>
<IconButton
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
{theme.direction === 'rtl' ? <FirstPage /> : <LastPage />}
</IconButton>
</Box>
);
}

function createData(rank: number, name: string) {
return { rank, name };
}


export default function Leaderboard() {
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10);
const usersData = useLoaderData<typeof loader>();
const { users, loggedInUsername } = usersData;
const rows = users.map((user, index) => createData(index + 1, user.username));
// Avoid a layout jump when reaching the last page with empty rows.
const emptyRows =
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - rows.length) : 0;

const handleChangePage = (
event: React.MouseEvent<HTMLButtonElement> | null,
newPage: number,
) => {
setPage(newPage);
};

const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
setRowsPerPage(parseInt(event.target.value, 10));
setPage(0);
};

return (
<div>
<Typography variant="h4" gutterBottom>
Leaderboard
</Typography>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 100 }} aria-label="custom pagination table">
<TableHead>
<TableRow>
<TableCell style={{ fontWeight: 'bold' }}>Ranking</TableCell>
<TableCell style={{ fontWeight: 'bold' }}>Username</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(rowsPerPage > 0
? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
: rows
).map((row) => (
<TableRow key={row.rank} style={{ backgroundColor: row.name === loggedInUsername ? 'lightblue' : 'inherit' }}>
<TableCell component="th" scope="row">
{row.rank}
</TableCell>
<TableCell style={{ width: '50%' }} align="left">
{row.name}
</TableCell>
</TableRow>
))}
{emptyRows > 0 && (
<TableRow style={{ height: 53 * emptyRows }}>
<TableCell colSpan={6} />
</TableRow>
)}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25, 50, { label: 'All', value: -1 }]}
colSpan={3}
count={rows.length}
rowsPerPage={rowsPerPage}
page={page}
slotProps={{
select: {
inputProps: {
'aria-label': 'rows per page',
},
native: true,
},
}}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
/>
</TableRow>
</TableFooter>
</Table>
</TableContainer>
</div>
);
}
8 changes: 7 additions & 1 deletion comprl-web/app/routes/_dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AppBar, Box, Button, CssBaseline, Drawer, IconButton, List, ListItemButton, ListItemIcon, ListItemText, Toolbar, Typography } from '@mui/material';
import { AdminPanelSettingsOutlined, LogoutOutlined, ManageSearchOutlined, MenuRounded, SmartToyOutlined } from '@mui/icons-material';
import { AdminPanelSettingsOutlined, LogoutOutlined, ManageSearchOutlined, MenuRounded, SmartToyOutlined, LeaderboardOutlined } from '@mui/icons-material';
import { Outlet, useLoaderData } from '@remix-run/react';
import { useState } from 'react';
import { LoaderFunctionArgs, json } from '@remix-run/node';
Expand Down Expand Up @@ -53,6 +53,12 @@ export default function DashboardLayout() {
</ListItemIcon>
<ListItemText primary="Home" />
</ListItemButton>
<ListItemButton sx={{ m: 1 }} href='/leaderboard'>
<ListItemIcon>
<LeaderboardOutlined />
</ListItemIcon>
<ListItemText primary="Leaderboard" />
</ListItemButton>
<ListItemButton sx={{ m: 1 }} href='/games'>
<ListItemIcon>
<ManageSearchOutlined />
Expand Down
44 changes: 30 additions & 14 deletions comprl-web/app/routes/_dashboard.usr.$name.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { LoaderFunctionArgs, redirect } from "@remix-run/node";
import { authenticator } from "~/services/auth.server";
import { commitSession, getSession } from "~/services/session.server";
import { useLoaderData } from "@remix-run/react";
import { getStatistics } from "~/db/sqlite.data";
import { getStatistics, getRankedUsers } from "~/db/sqlite.data";
import { DashboardsStatistic, DashboardPaper } from '~/components/DashboardContent';
import React from "react";

Expand All @@ -29,44 +29,60 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
}

const games = await getStatistics(user.id)
const ranked_users = await getRankedUsers();

var rank = 0;
var i = 1;
for (var r_user of ranked_users) {
if (r_user.username == user.name) {
rank = i;
}
i += 1;
}

if (!user.token) {
return { token: "no token exists", username: user.name, games: games };
return { token: "no token exists", username: user.name, games: games, rank: rank };
}


return { token: user.token, username: user.name, games: games};
return { token: user.token, username: user.name, games: games, rank: rank };
}

export default function UserDashboard() {
const { token, username, games } = useLoaderData<typeof loader>();
const { token, username, games, rank } = useLoaderData<typeof loader>();
const [selected, setSelected] = React.useState(false);
return (
<div>
<Grid container spacing={3} alignItems="stretch">
<Grid item xs={12} md={6}>
<Grid item xs={12} md={4}>
<DashboardPaper>
<Typography variant="h5" > Username </Typography>
<Typography variant="h5" > Username </Typography>
<Typography>{username}</Typography>
</DashboardPaper>
</DashboardPaper>
</Grid>
<Grid item xs={12} md={6}>
<DashboardPaper>
<Typography variant="h5" > Token
<Grid item xs={12} md={4}>
<DashboardPaper>
<Typography variant="h5" > Token
<IconButton onClick={() => { setSelected(!selected); }}>
{selected ? <VisibilityOff /> : <Visibility />}
{selected ? <VisibilityOff /> : <Visibility />}
</IconButton>
</Typography>
<Typography>{selected ? token : "*************" }</Typography>
<Typography>{selected ? token : "*************"}</Typography>
</DashboardPaper>
</Grid>
<Grid item xs={12} md={4}>
<DashboardPaper>
<Typography variant="h5" > Ranking </Typography>
<Typography>{rank}. place</Typography>
</DashboardPaper>
</Grid>
<Grid item xs={12}>
<DashboardPaper>
<Typography variant="h5"> Game Statistics </Typography>
<Typography variant="h5"> Game Statistics </Typography>
<Grid container spacing={8} padding={5}>
<Grid item xs={12} md={6} lg={3}><DashboardsStatistic value={games.playedGames.toString()} description="games played" /></Grid>
<Grid item xs={12} md={6} lg={3}><DashboardsStatistic value={games.wonGames.toString()} description="games won" /></Grid>
<Grid item xs={12} md={6} lg={3}><DashboardsStatistic value={Math.round((games.wonGames/games.playedGames)*100) + "%"} description="win rate" /></Grid>
<Grid item xs={12} md={6} lg={3}><DashboardsStatistic value={Math.round((games.wonGames / games.playedGames) * 100) + "%"} description="win rate" /></Grid>
<Grid item xs={12} md={6} lg={3}><DashboardsStatistic value={games.disconnectedGames.toString()} description="disconnects" /></Grid>
</Grid>
</DashboardPaper>
Expand Down

0 comments on commit 1967cc5

Please sign in to comment.