Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

web: leaderboard #104

Merged
merged 13 commits into from
Mar 14, 2024
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
Loading