Merge branch 'client' into 'main'

Merge entire client branch into main

See merge request felixalb/dcst1008-2022-group1!3
This commit is contained in:
Kristoffer Juelsenn 2022-04-26 15:53:29 +02:00
commit 6c59a9de8c
42 changed files with 18084 additions and 67 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -17,6 +17,7 @@ node_modules
.env.development.local
.env.test.local
.env.production.local
.env
npm-debug.log*
yarn-debug.log*

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,30 @@
{
"name": "tournament-server",
"version": "1.0.0",
"description": "DCST1008 Project - Server - Asura Tournament Management System",
"author": "felixalb, kristoju, jonajha, krisleri",
"private": true,
"homepage": "",
"dependencies": {
"@date-io/date-fns": "^2.11.0",
"@emotion/react": "^11.8.2",
"@emotion/styled": "^11.8.1",
"@mui/icons-material": "^5.5.1",
"@mui/lab": "^5.0.0-alpha.61",
"@mui/material": "^5.5.2",
"@mui/styled-engine-sc": "^5.5.2",
"bootstrap": "^5.1.3",
"date-fns": "^2.27.0",
"iarn": "0.0.0",
"less": "^4.1.2",
"react": "^17.0.2",
"react-bootstrap": "^2.2.1",
"react-dom": "^17.0.2",
"react-router-dom": "^6.2.2",
"react-scripts": "5.0.0",
"web-vitals": "^2.1.4"
"styled-components": "^5.3.3",
"web-vitals": "^2.1.4",
"yarn": "^1.22.18"
},
"scripts": {
"start": "react-scripts start",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

BIN
src/client/public/asura.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -7,7 +7,7 @@
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
content="Asura Tournament System"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
@ -24,20 +24,12 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"/>
<title>Asura Tournament System</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@ -1,6 +1,6 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "Asura Tournament",
"name": "Asura Tournament",
"icons": [
{
"src": "favicon.ico",

BIN
src/client/public/react.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,256 @@
import * as React from "react";
import { BrowserRouter as Router, Link, Route, Routes, useParams } from "react-router-dom";
import Appbar from "./components/AsuraBar";
import ErrorSnackbar from "./components/ErrorSnackbar";
import LoginPage from "./LoginPage";
import { Button, Box, TextField, Stack, InputLabel, Paper, TableContainer, Table, TableBody, TableHead, TableCell, TableRow, Typography, Select, MenuItem, FormControl } from '@mui/material';
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
import AddCircleIcon from '@mui/icons-material/AddCircle';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import PropTypes from 'prop-types'
function AdminCreator(props){
function postCreate(){
let adminEmail = document.getElementById("adminEmailInput").value;
if (!adminEmail) {
showError("Admin email is required");
return;
}
let formData = new FormData();
formData.append("email", adminEmail)
let body = new URLSearchParams(formData)
fetch(process.env.REACT_APP_API_URL + `/users/createBlank`, {
method: "POST",
body: body
})
.then(res => res.json())
.then(data => {
if (data.status !== "OK") {
showError(data.data);
return;
}
document.getElementById("adminEmailInput").value = "";
props.onAdminCreated();
}
)
}
return (
<Paper sx={{width: "90vw", margin: "10px auto", padding: "15px", align:'center', justifyContent:'center', flexGrow:1}} component={Stack} direction={['column']} spacing={2}>
<div align="center">
<form>
<TextField id="adminEmailInput" label="Admin Email" variant="outlined" type="email" sx={{width:['auto','50%','60%','70%']}} />
{/* <Button variant="contained" color="primary" onClick={postCreate}>Create Team</Button> */}
<Button type="submit" variant="contained" color="success" onClick={postCreate} sx={{marginLeft:['5px'],width:['fit-content','40%','30%','20%']}}>
<Box sx={{padding: "10px"}}>
Create Admin
</Box>
<AddCircleIcon />
</Button>
</form>
</div>
</Paper>
)
}
function UserList(props){
const deleteUser = (userId) => {
openConfirmDialog(function() {
fetch(process.env.REACT_APP_API_URL + `/users/${userId}`, {method: "DELETE"})
.then(res => res.json())
.then(data => {
if(data.status !== "OK"){
showError(data.data);
console.log("UWU")
return;
}
props.onUserUpdated();
})
.catch(error => showError(error));
});
}
let updateRank = (asuraId) => event => {
event.preventDefault();
let isManager = event.target.value === "manager";
let formData = new FormData();
formData.append("isManager", isManager);
let body = new URLSearchParams(formData);
fetch(process.env.REACT_APP_API_URL + `/users/${asuraId}/changeManagerStatus`, {
method: "POST",
body: body
})
.then(res => res.json())
.then(data => {
if(data.status !== "OK"){
showError(data.data);
return;
}
props.onUserUpdated();
})
.catch(error => showError(error));
}
return(
<Paper sx={{minHeight: "30vh", width:"90vw", margin:"10px auto"}} component={Stack} direction="column" justifycontent="center">
<div align="center">
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Email</TableCell>
<TableCell>Rank</TableCell>
<TableCell align="center">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{props.users.map((user) => (
<TableRow key={user.asuraId}>
<TableCell component="th" scope="row">
<b>
{user.name}
</b>
</TableCell>
<TableCell>{user.email}</TableCell>
{/* TODO Drop down menu for selecting rank */}
<TableCell>
<FormControl variant="standard">
<Select onChange={updateRank(user.asuraId)} value={user.isManager ? "manager" : "admin"} aria-label="rank" id="rankSelect">
<MenuItem value={"manager"}>Manager</MenuItem>
<MenuItem value={"admin"}>Admin</MenuItem>
</Select>
</FormControl>
</TableCell>
{/* <TableCell align="right">{team.members}</TableCell> */}
<TableCell align="center">
{/* <Button variant="contained" sx={{margin: "auto 5px"}} color="primary" onClick={() => props.setSelectedTeamId(team.id)} endIcon={<EditIcon />}>Edit</Button> */}
<Button variant="contained" sx={{margin: "auto 5px"}} color="error" onClick={() => {deleteUser(user.asuraId)}} endIcon={<DeleteIcon />}>Delete</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</Paper>
)
}
function ConfirmationDialogRaw(props) {
const { userId } = useParams();
const { onClose, value: valueProp, open, ...other } = props;
const [value, setValue] = React.useState(valueProp);
React.useEffect(() => {
if (!open) {
setValue(valueProp);
}
}, [valueProp, open]);
const handleCancel = () => {
onClose();
};
const handleConfirm = () => {
onClose();
props.handleconfirm();
};
return (
<Dialog
sx={{ '& .MuiDialog-paper': { width: '80%', maxHeight: 435 } }}
maxWidth="xs"
open={open}
keepMounted
>
<DialogTitle>Delete administrator?</DialogTitle>
<DialogContent>
Are you sure you want to delete the administrator? This action is not reversible!
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleConfirm}>Confirm</Button>
</DialogActions>
</Dialog>
);
}
ConfirmationDialogRaw.propTypes = {
onClose: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired,
};
// Confirmation window for "Delete user"
function openConfirmDialog(callback) {g_setDialogCallback(callback); g_setDialogOpen(true);}
let g_setDialogOpen = () => {};
let g_setDialogCallback = (callback) => {};
let showError = (message) => {};
export default function Users(props) {
const [openError, setOpenError] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState("");
showError = (message) => {
setOpenError(false);
setErrorMessage(message);
setOpenError(true);
}
const [users, setUsers] = React.useState([]);
const [dialogOpen, setDialogOpen] = React.useState(false);
const handleDialogClose = () => { setDialogOpen(false); };
const [dialogOnConfirm, setDialogOnConfirm] = React.useState(() => {return function(){}});
g_setDialogCallback = (callback) => { setDialogOnConfirm(()=>{ return callback })};
g_setDialogOpen = (value) => { setDialogOpen(value); };
function getUsers() {
fetch(process.env.REACT_APP_API_URL + `/users/getUsers`)
.then((res) => res.json())
.then((data) =>{
if(data.status !== "OK") {
showError(data.data);
return;
}
setUsers(data.data);
})
.catch((err) => showError(err));
}
React.useEffect(() => {
getUsers()
}, []);
if (!props.user.isLoggedIn) { return <LoginPage user={props.user} />; }
if (!props.user.isManager) {
return (<>
<Appbar user={props.user} pageTitle="Admins" />
<Paper sx={{minHeight: "30vh", width:"90vw", margin:"10px auto", padding: "25px"}} component={Stack} direction="column" justifycontent="center">
<div align="center">
<Typography variant="h4">You do not have permission to view this page. If you believe this is incorrect, please contact a manager.</Typography>
</div>
</Paper>
</>);
}
return (
<>
<Appbar user={props.user} pageTitle="Admins" />
<div className="admins">
<AdminCreator onAdminCreated={getUsers} onUserUpdated={getUsers} />
<UserList users={users} setUsers={setUsers} onUserUpdated={getUsers} />
</div>
<ErrorSnackbar message={errorMessage} open={openError} setOpen={setOpenError} />
<ConfirmationDialogRaw
id="confirmation-dialog"
keepMounted
open={dialogOpen}
onClose={handleDialogClose}
handleconfirm={dialogOnConfirm}
/>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

279
src/client/src/FrontPage.js Normal file
View File

@ -0,0 +1,279 @@
import * as React from "react";
import { BrowserRouter as Router, Link, Route, Routes } from "react-router-dom";
import TournamentCreator from "./TournamentCreator.js";
import TournamentOverview from "./TournamentOverview.js";
import TournamentManager from "./TournamentManager.js";
import TournamentHistory from "./TournamentHistory";
import TournamentTeams from "./TournamentTeams";
import LoginPage from "./LoginPage";
import ProfilePage from "./ProfilePage";
import Appbar from './components/AsuraBar';
import SuccessSnackbar from "./components/SuccessSnackbar";
import ErrorSnackbar from "./components/ErrorSnackbar";
import AdminsOverview from "./AdminsOverview";
import { Button, Container, Typography, Box, Stack, Card, CardContent, CardMedia, Paper, Grid, Icon, TextField } from "@mui/material";
import AddCircleIcon from '@mui/icons-material/AddCircle';
import KeyboardDoubleArrowDownIcon from '@mui/icons-material/KeyboardDoubleArrowDown';
import KeyboardDoubleArrowUpIcon from '@mui/icons-material/KeyboardDoubleArrowUp';
import EditIcon from '@mui/icons-material/Edit';
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
function CreateButton(props) {
return (
<Link to="/create">
<Button variant="contained" color="success">
<Box sx={{
paddingRight: '2%',
}}>
Create Tournament
</Box>
<AddCircleIcon />
</Button>
</Link>
);
}
function shorten(description, maxLength) {
if (description.length > maxLength) {
return description.substring(0, maxLength) + "...";
}
return description;
}
function TournamentListItem(props) {
const [longDescription, setLongDescription] = React.useState(false);
const maxLength = 200;
function toggleDescription() {
setLongDescription(!longDescription);
}
function Description() {
if (longDescription) {
return( <Box component={Stack} direction="row">
<Typography variant="body1" onClick={toggleDescription}>{props.tournament.description}</Typography>
<KeyboardDoubleArrowUpIcon onClick={toggleDescription} />
</Box> )
} else if (props.tournament.description.length < maxLength) {
return <Typography variant="body1" color="text.secondary" onClick={toggleDescription}>{props.tournament.description}</Typography>
} else {
return <Box component={Stack} direction="row">
<Typography variant="body1" color="text.secondary" onClick={toggleDescription}>{shorten(props.tournament.description, maxLength)}</Typography>
<KeyboardDoubleArrowDownIcon onClick={toggleDescription} />
</Box>;
}
}
function Countdown() {
const [remainingTime, setremainingTime] = React.useState(Math.abs(props.tournament.startTime - new Date()))
React.useEffect(() => {
const interval = setInterval(() =>
setremainingTime(Math.abs(props.tournament.startTime - new Date()))
, 1000);
return () => {
clearInterval(interval);
};
}, []);
let remainingDays = Math.floor(remainingTime / (1000 * 60 * 60 * 24));
let remainingHours = Math.floor(remainingTime / (1000 * 60 * 60)) - remainingDays * 24
let remainingMins = Math.floor(remainingTime / (1000 * 60)) - (remainingDays * 24 * 60 + remainingHours * 60)
let remainingSecs = Math.floor(remainingTime / 1000) - (remainingDays * 24 * 60 * 60 + remainingHours * 60 * 60 + remainingMins * 60)
if (props.tournament.startTime < new Date()) {
return (<Box>
<Typography variant="body"> Started! </Typography>
</Box>)
} else {
return(<Box>
<Typography variant="body"> Starts in: </Typography> <br />
{ remainingDays > 0 ? <Typography variant="body"> {remainingDays} days</Typography> : null }
{ remainingHours > 0 ? <Typography variant="body"> {remainingHours} hours</Typography> : null }
{ remainingMins > 0 ? <Typography variant="body"> {remainingMins} mins</Typography> : null }
{ remainingSecs > 0 ? <Typography variant="body"> {remainingSecs} secs</Typography> : null }
</Box>
)
}
}
return (
<Paper elevation={8} >
<Card>
<CardMedia
component="img"
alt="tournament image"
height="140"
image="banner2.png"
/>
<CardContent align="left">
<Typography sx={{fontSize:['2rem','2.5rem','3rem']}} component="div" align="center">{props.tournament.name} </Typography>
<Box component={Stack} direction="column">
<Typography variant="body"> Start: {props.tournament.startTime.toLocaleString()} </Typography>
<Typography variant="body"> End: {props.tournament.endTime.toLocaleString()} </Typography>
</Box>
<Typography variant="h5" color="text.primary" gutterBottom> Particpants: {props.tournament.teamCount} / {props.tournament.teamLimit} </Typography>
<Description />
<Typography variant="body" color="text.primary"><EmojiEventsIcon alt="A trohpy" color="gold"/> Prize: {props.tournament.prize} </Typography>
<Countdown />
<Box sx={{flexGrow: 1, marginTop: "20px"}}>
<Grid container spacing={2} justifyContent="center" wrap="wrap">
{ props.user.isLoggedIn &&
<Grid item>
<Link to={`/tournament/${props.tournament.id}/manage`}>
<Button className="ManageButton" variant="contained" color="primary" endIcon={<EditIcon />}>Edit Tournament</Button>
</Link>
</Grid>
}
<Grid item >
<Link to={`/tournament/${props.tournament.id}`} >
<Button variant="contained" color="success">
View Tournament
</Button>
</Link>
</Grid>
</Grid>
</Box>
</CardContent>
</Card>
</Paper>
);
}
function TournamentList(props) {
let [tournamentList, setTournamentList] = React.useState([]);
React.useEffect(() => {
fetch(process.env.REACT_APP_API_URL + `/tournament/getTournaments`)
.then(res => res.json())
.then(data => {
if (data.status !== "OK") {
console.error(data);
return;
}
let today = new Date()
let currenttournaments = []
let tournaments = Object.values(data.data);
for (let i = 0; i < tournaments.length; i++) {
tournaments[i].startTime = new Date(tournaments[i].startTime);
tournaments[i].endTime = new Date(tournaments[i].endTime);
if(today - tournaments[i].endTime <= 2*60*60*1000) {
currenttournaments.push(tournaments[i])
}
}
// tournaments.filter((tournament) => today - tournament.endTime < 24*60*60*1000)
setTournamentList(currenttournaments);
})
.catch((err) => console.log(err.message));
}, []);
return <>
<Stack spacing={3} sx={{margin: "10px auto"}}>
{tournamentList && tournamentList.map((tournamentObject) => <TournamentListItem user={props.user} key={tournamentObject.id.toString()} tournament={tournamentObject} />)}
</Stack>
</>;
}
function Home(props) {
return (
<>
<Appbar user={props.user} pageTitle="Asura Tournaments" />
<Container sx={{minHeight: "30vh", width: "90vw", padding: "20px 20px"}} component={Container} direction="column" align="center">
<Box component={Stack} direction={['column','row']} sx={{align:'center', justifyContent:'space-between', flexGrow:1}}>
<Typography sx={{fontSize:['1.5rem','2rem','2rem']}}>Tournaments</Typography>
{ props.user.isLoggedIn ?
<CreateButton /> : null
}
</Box>
<TournamentList user={props.user} />
{props.user.isLoggedIn &&
<Typography variant="h5" color="#555555">
Finished tournaments are moved to the <Link to="/history">history-page</Link>
</Typography>
}
</Container>
</>
);
}
let showSuccess = (message) => {};
let showError = (message) => {};
export default function App() {
const [user, setUser] = React.useState({});
let fetchUser = () => {
fetch(process.env.REACT_APP_API_URL + `/users/getSavedUser`)
.then(res => res.json())
.then(data => {
if (data.status !== "OK") {
setUser({ isManager: false, isLoggedIn: false });
console.log(data.data); // "No user logged in"
return;
}
let u = data.data;
u.isLoggedIn = true;
console.log("User is logged in")
setUser(u);
})
.catch((err) => {
showError(err.message);
setUser({ isManager: false, isLoggedIn: false });
});
}
// // Debug mode, allow all:
// let fetchUser = () => {
// setUser({
// name: "TEST USERTEST",
// isManager: true,
// isLoggedIn: true,
// email: "testesen@gmail.com",
// asuraId: "123456789",
// googleId: "234"
// });
// }
React.useEffect(() => {
fetchUser();
}, []);
const [openError, setOpenError] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState("");
showError = (message) => {
setOpenError(false);
setErrorMessage(message);
setOpenError(true);
}
const [openSuccess, setOpenSuccess] = React.useState(false);
const [successMessage, setSuccessMessage] = React.useState("");
showSuccess = (message) => {
setOpenSuccess(false);
setSuccessMessage(message);
setOpenSuccess(true);
}
return (
<React.StrictMode>
<Router>
<Routes>
<Route path="/" element={<Home showError={showError} showSuccess={showSuccess} user={user} />} />
<Route path="/create" element={<TournamentCreator showError={showError} showSuccess={showSuccess} user={user} />} />
<Route path="/tournament/:tournamentId" element={<TournamentOverview user={user} />} />
<Route path="/tournament/:tournamentId/manage" element={<TournamentManager showError={showError} showSuccess={showSuccess} user={user} />} />
<Route path="/tournament/:tournamentId/teams" element={<TournamentTeams showError={showError} showSuccess={showSuccess} user={user} />} />
<Route path="/history" element={<TournamentHistory showError={showError} showSuccess={showSuccess} user={user} />} />
<Route path="/login" element={<LoginPage user={user} />} />
<Route path="/profile" element={<ProfilePage user={user} />} />
<Route path="/admins" element={<AdminsOverview user={user} />} />
</Routes>
</Router>
<SuccessSnackbar message={successMessage} open={openSuccess} setOpen={setOpenSuccess} />
<ErrorSnackbar message={errorMessage} open={openError} setOpen={setOpenError} />
</React.StrictMode>
);
}

View File

@ -0,0 +1,30 @@
import * as React from "react";
import { BrowserRouter as Router, Link, Route, Routes } from "react-router-dom";
import Appbar from "./components/AsuraBar";
import ErrorSnackbar from "./components/ErrorSnackbar";
import {Button, Textfield, Stack, InputLabel, Paper, Typography} from '@mui/material';
export default function LoginPage(props) {
if (props.user.isLoggedIn) {
//Redirect to the front page if the user is logged in
window.location.href = "/";
return;
}
return (
<>
<Appbar user={props.user} pageTitle="Login" />
<Paper sx={{width: "70vw", margin: "1.5% auto", padding: "25px"}} component={Stack} direction="column" justifyContent="center" alignItems="center">
<Stack direction="column" paddingTop={'0.5%'} alignItems={'center'} textAlign={"center"} >
<Typography variant="h4" component="h4">
You must be logged in to access administrator features.
</Typography>
<a href={process.env.REACT_APP_LOGIN_URL}>
<img src="/btn_google_signing_dark.png" alt="Sign in with google" />
</a>
</Stack>
</Paper>
</>
);
}

View File

@ -0,0 +1,26 @@
import * as React from "react";
import { BrowserRouter as Router, Link, Route, Routes } from "react-router-dom";
import Appbar from "./components/AsuraBar";
import LoginPage from "./LoginPage";
import ErrorSnackbar from "./components/ErrorSnackbar";
import { Button, TextField, Stack, InputLabel, Select, Container, Slider, Paper, Box, Grid, Typography } from '@mui/material';
export default function ProfilePage(props) {
let user = props.user;
if (!props.user.isLoggedIn) { return <LoginPage user={props.user} />; }
return (<>
<Appbar user={props.user} pageTitle="Profile" />
<Paper sx={{minHeight: "30vh", width: "90vw", margin: "10px auto"}} component={Stack} direction="column" justifyContent="center">
<div align="center">
<h2><b>Your profile</b></h2>
<Box>
<h3><b>Name: </b> {user.name}</h3>
<h3><b>Email: </b> {user.email}</h3>
<h3><b>Role: </b> {user.isManager ? "Manager" : "Administrator"}</h3>
</Box>
</div>
</Paper>
</>)
}

View File

@ -0,0 +1,182 @@
import * as React from "react";
import { BrowserRouter as Router, Link, Route, Routes } from "react-router-dom";
import Appbar from "./components/AsuraBar";
import ErrorSnackbar from "./components/ErrorSnackbar";
import LoginPage from "./LoginPage";
import { Button, TextField, Stack, InputLabel, Select, Container, Slider, Paper, Box, Grid, Typography } from '@mui/material';
import DateTimePicker from '@mui/lab/DateTimePicker';
import AdapterDateFns from '@mui/lab/AdapterDateFns';
import LocalizationProvider from '@mui/lab/LocalizationProvider';
import { setDate } from "date-fns";
function postTournament(tournamentName, tournamentDescription, tournamentStartDate, tournamentEndDate, tournamentMaxTeams, tournamentPrize) {
if (!tournamentName || tournamentName === "") {
showError("Tournament name cannot be empty");
return;
}
if (!tournamentDescription || tournamentDescription === "") {
showError("Tournament description cannot be empty");
return;
}
if (!tournamentStartDate || tournamentStartDate === "" || tournamentStartDate === 0) {
showError("Tournament start date cannot be empty");
return;
}
if (!tournamentEndDate || tournamentEndDate === "" || tournamentEndDate === 0) {
showError("Tournament end date cannot be empty");
return;
}
if (!tournamentMaxTeams || isNaN(tournamentMaxTeams) || tournamentMaxTeams < 4) {
showError("Tournament max teams cannot be empty");
return;
}
if (tournamentStartDate > tournamentEndDate) {
showError("Tournament start date cannot be after end date");
return;
}
let today = new Date();
if (tournamentStartDate < today || tournamentEndDate < today) {
showError("Tournament start and end date must be after today");
return;
}
let formData = new FormData();
formData.append("name", tournamentName);
formData.append("description", tournamentDescription);
formData.append("startDate", tournamentStartDate);
formData.append("endDate", tournamentEndDate);
formData.append("teamLimit", tournamentMaxTeams);
formData.append("prize", tournamentPrize)
let body = new URLSearchParams(formData);
fetch(process.env.REACT_APP_API_URL + `/tournament/create`, {
method: "POST",
body: body
})
.then(response => response.json())
.then(data => {
if (data.status === "OK") {
alert("Tournament created successfully");
let tournamentId = data.data.tournamentId;
if (tournamentId) {
window.location.href = "/tournament/" + tournamentId;
}
} else {
showError(data.data)
}
})
.catch(error => showError(error));
}
function TournamentForm(props) {
const [maxTeamsExponent, setMaxTeamsExponent] = React.useState(2);
function sliderUpdate(event) {
setMaxTeamsExponent(event.target.value);
}
const [startTime, setStartTime] = React.useState(new Date());
const [endTime, setEndTime] = React.useState(new Date());
function submitTournament(event) {
event.preventDefault();
let maxTeams = Math.pow(2, maxTeamsExponent);
let tournamentStart = new Date(startTime.setSeconds(0, 0, 0)).valueOf() - new Date().getTimezoneOffset() * 60*1000;
let tournamentEnd = new Date(endTime.setSeconds(0, 0, 0)).valueOf() - new Date().getTimezoneOffset() * 60*1000;
postTournament(
document.getElementById("nameInput").value,
document.getElementById("descriptionInput").value,
tournamentStart,
tournamentEnd,
maxTeams,
document.getElementById("prizeInput").value
);
}
const marks = [
{ value: 2, label: "4",},
{ value: 3, label: "8",},
{ value: 4, label: "16",},
{ value: 5, label: "32",},
{ value: 6, label: "64",}
];
return (
<>
<form>
<Stack sx={{minHeight: "30vh", margin: "10px auto"}} direction="column" justifyContent="center" spacing={3} align="center">
{/* <InputLabel htmlFor="nameInput">Tournament Name: </InputLabel> */}
<TextField type="text" id="nameInput" label="Tournament Name" placeholder="Tournament Name" InputLabelProps={{shrink: true}}/>
{/* <InputLabel htmlFor="descriptionInput">Description: </InputLabel */}
<TextField type="text" multiline={true} id="descriptionInput" label="Description" placeholder="Description" InputLabelProps={{shrink: true}}/>
<TextField type="text" id="prizeInput" label="Prize" placeholder="Prize" InputLabelProps={{shrink: true}}/>
<Box flexGrow={1}>
<Grid container spacing={2} justifyContent="center">
<Grid item xs={6}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DateTimePicker label={"Start Time"} inputVariant="outlined" ampm={false} mask="____-__-__ __:__" format="yyyy-MM-dd HH:mm" inputFormat="yyyy-MM-dd HH:mm" value={startTime}
onChange={setStartTime}
renderInput={(params) => <TextField id="startDatePicker" {...params} sx={{margin: "0 2.5%"}} />}
/>
</LocalizationProvider>
</Grid>
<Grid item xs={6}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DateTimePicker label={"End Time"} inputVariant="outlined" ampm={false} mask="____-__-__ __:__" format="yyyy-MM-dd HH:mm" inputFormat="yyyy-MM-dd HH:mm" value={endTime}
onChange={setEndTime}
renderInput={(params) => <TextField id="endDatePicker" {...params} sx={{margin: "0 2.5%"}} />}
/>
</LocalizationProvider>
</Grid>
{/* <TextField type="datetime-local" id="startDatePicker" label="Start Time" InputLabelProps={{shrink: true}} sx={{width: "48%", marginRight: "2%"}} />
<TextField type="datetime-local" id="endDatePicker" label="End Time" InputLabelProps={{shrink: true}} sx={{width: "48%", marginLeft: "2%"}} /> */}
</Grid>
</Box>
<InputLabel id="max-teams-label">Maximum number of teams</InputLabel>
<Box sx={{flexGrow: 1}}>
<Grid container spacing={2} justifyContent="center">
<Grid item xs={8}>
<Container>
<Slider aria-label="Teams" valueLabelDisplay="off" step={1} marks={marks} min={2} max={6} onChange={sliderUpdate} id="max-teams-slider" name="max-teams-slider" >
</Slider>
</Container>
</Grid>
</Grid>
</Box>
{/* go brrrr */}
<br /><br />
<Button type="submit" variant="contained" onClick={submitTournament} color="primary">Create Tournament!</Button>
</Stack>
</form>
</>
);
}
let showError = (message) => {};
export default function TournamentCreator(props) {
const [openError, setOpenError] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState("");
showError = (message) => {
setOpenError(false);
setErrorMessage(message);
setOpenError(true);
}
if (!props.user.isLoggedIn) { return <LoginPage user={props.user} />; }
return (
<>
<Appbar user={props.user} pageTitle="New tournament" />
<Paper sx={{minHeight: "30vh", width: "90vw", margin: "20px auto", padding: "20px 20px"}} component={Container} direction="column" align="center">
<TournamentForm />
</Paper>
<ErrorSnackbar message={errorMessage} open={openError} setOpen={setOpenError} />
</>
);
}

View File

@ -0,0 +1,160 @@
import * as React from "react";
import { BrowserRouter as Router, Link, Route, Routes } from "react-router-dom";
import { Button, Container, Typography, Box, Stack, Card, CardContent, CardMedia, Paper, Grid, Icon, TextField } from "@mui/material";
import AddCircleIcon from '@mui/icons-material/AddCircle';
import KeyboardDoubleArrowDownIcon from '@mui/icons-material/KeyboardDoubleArrowDown';
import KeyboardDoubleArrowUpIcon from '@mui/icons-material/KeyboardDoubleArrowUp';
import Appbar from './components/AsuraBar';
import LoginPage from './LoginPage';
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
function shorten(description, maxLength) {
if (description.length > maxLength) {
return description.substring(0, maxLength) + "...";
}
return description;
}
function TournamentListItem(props) {
const [longDescription, setLongDescription] = React.useState(false);
const maxLength = 200;
function toggleDescription() {
setLongDescription(!longDescription);
}
function Description() {
if (longDescription) {
return( <Box component={Stack} direction="row">
<Typography variant="body1" onClick={toggleDescription}>{props.tournament.description}</Typography>
<KeyboardDoubleArrowUpIcon onClick={toggleDescription} />
</Box> )
} else if (props.tournament.description.length < maxLength) {
return <Typography variant="body1" color="text.secondary" onClick={toggleDescription}>{props.tournament.description}</Typography>
} else {
return <Box component={Stack} direction="row">
<Typography variant="body1" color="text.secondary" onClick={toggleDescription}>{shorten(props.tournament.description, maxLength)}</Typography>
<KeyboardDoubleArrowDownIcon onClick={toggleDescription} />
</Box>;
}
}
return (
<Paper elevation={8} >
<Card>
<CardMedia
component="img"
alt="tournament image"
height="140"
image="banner2.png"
/>
<CardContent align="left">
<Typography variant="h3" component="div" align="center">{props.tournament.name} </Typography>
<Box component={Stack} direction="column">
<Typography variant="body"> Start: {props.tournament.startTime.toLocaleString()} </Typography>
<Typography variant="body"> End: {props.tournament.endTime.toLocaleString()} </Typography>
</Box>
<Typography variant="h5" color="text.primary" gutterBottom> Participants: {props.tournament.teamCount} / {props.tournament.teamLimit} </Typography>
<Description />
<Typography variant="body" color="text.primary"><EmojiEventsIcon alt="A trohpy" color="gold" align="vertical-center"/> Prize: {props.tournament.prize} </Typography>
<Box sx={{flexGrow: 1, marginTop: "20px"}}>
<Grid container spacing={4} justifyContent="center" wrap="wrap">
<Grid item >
<Link to={`/tournament/${props.tournament.id}`} >
<Button variant="contained" color="success">
View Tournament
</Button>
</Link>
</Grid>
</Grid>
</Box>
</CardContent>
</Card>
</Paper>
);
}
function TournamentList() {
let [tournamentList, setTournamentList] = React.useState([]);
let [originalList, setOriginalList] = React.useState([])
React.useEffect(() => {
fetch(process.env.REACT_APP_API_URL + `/tournament/getTournaments`)
.then(res => res.json())
.then(data => {
if (data.status !== "OK") {
console.error(data);
return;
}
let tournamenthistory = []
let today = new Date()
let tournaments = Object.values(data.data);
for (let i = 0; i < tournaments.length; i++) {
tournaments[i].startTime = new Date(tournaments[i].startTime);
tournaments[i].endTime = new Date(tournaments[i].endTime);
if(today - tournaments[i].endTime >= 2*60*60*1000) {
tournamenthistory.push(tournaments[i])
}
}
setTournamentList(tournamenthistory);
setOriginalList(tournamenthistory)
})
.catch((err) => console.log(err.message));
}, []);
function search() {
let searchBase = []
let searchResult = []
originalList.map((tournament) => searchBase.push(tournament.name))
let input = document.getElementById("searchInput")
let inputUpperCase = input.value.toUpperCase()
for (let i = 0; i < searchBase.length; i++) {
let tournamentName = searchBase[i].toUpperCase()
if(tournamentName.indexOf(inputUpperCase) >= 0) {
searchResult.push(tournamentName)
}
}
let searchedList = []
for (let i = 0; i < originalList.length; i++) {
let name = originalList[i].name
for (let j = 0; j < searchResult.length; j++) {
if (name.toUpperCase() == searchResult[j]) {
searchedList.push(originalList[i])
}
}
}
if (input.value == "") {
setTournamentList(originalList)
} else {
setTournamentList(searchedList)
}
}
return <>
<TextField type="text" id="searchInput" label="Search" placeholder="Tournament Name" InputLabelProps={{shrink: true}} onChange={search}/>
<Stack spacing={3} sx={{margin: "10px auto"}}>
{tournamentList && tournamentList.map((tournamentObject) => <TournamentListItem key={tournamentObject.id.toString()} tournament={tournamentObject} />)}
</Stack>
</>;
}
export default function TournamentHistory(props) {
if (!props.user.isLoggedIn) { return <LoginPage user={props.user} />; }
return (
<>
<Appbar user={props.user} pageTitle="Tournament History" />
<Container sx={{minHeight: "30vh", width: "90vw", padding: "20px 20px"}} component={Container} direction="column" align="center">
<Box component={Stack} direction="row" align="center" justifyContent="center" alignItems="center" sx={{flexGrow: 1, margin:'2.5% 0'}}>
<Typography sx={{fontSize:['1.5rem','2rem','2rem']}}>Past Tournaments</Typography>
</Box>
<TournamentList />
</Container>
</>
);
}

View File

@ -0,0 +1,253 @@
import * as React from "react";
import { BrowserRouter as Router, Link, Route, Routes } from "react-router-dom";
// import { AlertContainer, alert } from "react-custom-alert";
import Appbar from "./components/AsuraBar";
import TournamentBar from "./components/TournamentBar";
import LoginPage from "./LoginPage";
import { useParams } from "react-router-dom";
import { Button, TextField, Grid, Box, Container, Paper, Stack } from "@mui/material";
import { Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "@mui/material";
import CloseIcon from '@mui/icons-material/Close';
import DeleteIcon from '@mui/icons-material/Delete';
import DateTimePicker from '@mui/lab/DateTimePicker';
import AdapterDateFns from '@mui/lab/AdapterDateFns';
import LocalizationProvider from '@mui/lab/LocalizationProvider';
import PropTypes from 'prop-types'
let submitChanges = curryTournamentId => event => {
event.preventDefault();
let tournamentId = curryTournamentId;
//TODO: use refs to get values
let tournamentName = document.getElementById("editName").value;
let tournamentDescription = document.getElementById("editDesc").value;
// let tournamentImageFile = document.getElementById("editImage").files[0];
let tournamentStartDate = document.getElementById("editStartDate").value;
let tournamentEndDate = document.getElementById("editEndDate").value;
let tournamentPrize = document.getElementById("editPrize").value
if (!tournamentName || tournamentName === "") {
showError("Tournament name cannot be empty");
return;
}
if (!tournamentDescription || tournamentDescription === "") {
showError("Tournament description cannot be empty");
return;
}
if (!tournamentStartDate || tournamentStartDate === "") {
showError("Tournament start date cannot be empty");
return;
}
if (!tournamentEndDate || tournamentEndDate === "") {
showError("Tournament end date cannot be empty");
return;
}
if (tournamentStartDate > tournamentEndDate) {
showError("Tournament start date cannot be after end date");
return;
}
let today = new Date();
if (tournamentStartDate < today || tournamentEndDate < today) {
showError("Tournament start and end date must be after today");
return;
}
tournamentStartDate = new Date(tournamentStartDate).valueOf() - new Date().getTimezoneOffset() * 60 * 1000;
tournamentEndDate = new Date(tournamentEndDate).valueOf() - new Date().getTimezoneOffset() * 60 * 1000;
let formData = new FormData();
formData.append("name", tournamentName);
formData.append("description", tournamentDescription);
formData.append("startDate", tournamentStartDate);
formData.append("endDate", tournamentEndDate);
// formData.append("teamLimit", tournamentMaxTeams);
formData.append("prize", tournamentPrize)
let body = new URLSearchParams(formData);
fetch(process.env.REACT_APP_API_URL + `/tournament/${tournamentId}/edit`, {
method: "POST",
body: body,
})
.then((response) => response.json())
.then((data) => {
if (data.status === "OK") {
showSuccess("Tournament Changed successfully");
window.location.href = `/tournament/${tournamentId}`;
} else {
showError(data.data);
}
})
.catch((error) => showError(error));
}
let deleteTournament = tournamentId => event => {
console.log(tournamentId);
event.preventDefault();
fetch(process.env.REACT_APP_API_URL + `/tournament/${tournamentId}`, {
method: "DELETE",
})
.then((response) => response.json())
.then((data) => {
if (data.status === "OK") {
showSuccess("Tournament Deleted successfully");
window.location.href = "/";
} else {
showError(data.data);
}
})
.catch((error) => showError(error));
}
function ManageTournament(props) {
const [startTime, setStartTime] = React.useState(new Date());
const [endTime, setEndTime] = React.useState(new Date());
React.useEffect(() => {
fetch(
process.env.REACT_APP_API_URL + `/tournament/${props.tournamentId}`
)
.then((res) => res.json())
.then((data) => {
if (data.status !== "OK") {
showError(data.data);
}
document.getElementById("editName").value = data.data.name;
document.getElementById("editDesc").value = data.data.description;
document.getElementById("editPrize").value = data.data.prize
// Get the time from the server, add the local timezone offset and set the input fields
let startDate = new Date(data.data.startTime.slice(0, 16));
let endDate = new Date(data.data.endTime.slice(0, 16));
let localTimeOffset = new Date().getTimezoneOffset() * 60*1000; // Minutes -> Milliseconds
startDate = new Date(startDate.getTime() - localTimeOffset);
endDate = new Date(endDate.getTime() - localTimeOffset);
setStartTime(startDate);
setEndTime(endDate);
})
.catch((err) => showError(err));
}, [props.tournamentId]);
return (
<>
<form>
<Stack sx={{minHeight: "30vh", margin: "1.5%"}} direction="column" justifyContent="center" spacing={2} align="center">
<TextField type="text" id="editName" label="Edit Name:" placeholder="Edit Name" InputLabelProps={{shrink: true}}/>
<TextField type="text" multiline={true} id="editDesc" label="Edit Description:" placeholder="Edit Description" InputLabelProps={{shrink: true}} />
<TextField type="text" id="editPrize" label="Edit Prize:" placeholder="Edit Prize" InputLabelProps={{shrink: true}}/>
<Box sx={{flexGrow: 1}}>
<Grid container spacing={2} justifyContent="center">
<Grid item xs={6}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DateTimePicker label={"Start Time"} inputVariant="outlined" ampm={false} mask="____-__-__ __:__" format="yyyy-MM-dd HH:mm" inputFormat="yyyy-MM-dd HH:mm" value={startTime}
onChange={setStartTime}
renderInput={(params) => <TextField id="editStartDate" {...params} />}
/>
</LocalizationProvider>
</Grid>
<Grid item xs={6}>
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DateTimePicker label={"End Time"} inputVariant="outlined" ampm={false} mask="____-__-__ __:__" format="yyyy-MM-dd HH:mm:" inputFormat="yyyy-MM-dd HH:mm" value={endTime}
onChange={setEndTime}
renderInput={(params) => <TextField id="editEndDate" {...params} />}
/>
</LocalizationProvider>
</Grid>
</Grid>
</Box>
<Button type="submit" variant="contained" onClick={submitChanges(props.tournamentId)} color="primary" >
Save Tournament Details
</Button>
</Stack>
</form>
</>
);
}
function ConfirmationDialogRaw(props) {
const { tournamentId } = useParams();
const { onClose, value: valueProp, open, ...other } = props;
const [value, setValue] = React.useState(valueProp);
React.useEffect(() => {
if (!open) {
setValue(valueProp);
}
}, [valueProp, open]);
const handleCancel = () => {
onClose();
};
return (
<Dialog
sx={{ '& .MuiDialog-paper': { width: '80%', maxHeight: 435 } }}
maxWidth="xs"
open={open}
{...other}
>
<DialogTitle>Delete tournament?</DialogTitle>
<DialogContent>
Are you sure you want to delete the tournament? This action is not reversible!
</DialogContent>
<DialogActions>
<Button autoFocus onClick={handleCancel}>
Cancel
</Button>
<Button onClick={deleteTournament(tournamentId)}>Confirm</Button>
</DialogActions>
</Dialog>
);
}
ConfirmationDialogRaw.propTypes = {
onClose: PropTypes.func.isRequired,
open: PropTypes.bool.isRequired,
};
let showError = (message) => {};
let showSuccess = (message) => {};
export default function TournamentManager(props) {
const { tournamentId } = useParams();
const [dialogOpen, setDialogOpen] = React.useState(false);
const handleDialogClickListItem = () => { setDialogOpen(true); };
const handleDialogClose = () => { setDialogOpen(false); };
const [openError, setOpenError] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState("");
showError = (message) => {
setOpenError(false);
setErrorMessage(message);
setOpenError(true);
}
if (!props.user.isLoggedIn) { return <LoginPage user={props.user} />; }
return (
<>
<Appbar user={props.user} pageTitle="Edit Tournament" />
<TournamentBar pageTitle="Edit Tournament"/>
<Paper sx={{minHeight: "30vh", width: "90vw", margin: "20px auto", padding: "20px 0"}} component={Container} direction="column" align="center">
<ManageTournament tournamentId={tournamentId} />
{/* <AnnounceButton /> */}
<Box sx={{width: "100%"}}>
<Button variant="contained" color="error" onClick={handleDialogClickListItem} sx={{margin: "auto 5px"}} endIcon={<DeleteIcon />}>
Delete Tournament
</Button>
<ConfirmationDialogRaw
id="confirmation-dialog"
keepMounted
open={dialogOpen}
onClose={handleDialogClose}
/>
</Box>
</Paper>
</>
);
}

View File

@ -0,0 +1,284 @@
import * as React from "react";
import { Link } from "react-router-dom";
import Appbar from './components/AsuraBar';
import TournamentBar from "./components/TournamentBar";
import ErrorSnackbar from "./components/ErrorSnackbar";
import { useParams } from 'react-router-dom'
import { Button, IconButton, Paper, Stack, CircularProgress, Box, Grid, Typography, Container } from "@mui/material";
import "./components/tournamentBracket.css";
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
import DoDisturbIcon from '@mui/icons-material/DoDisturb';
import BackspaceIcon from '@mui/icons-material/Backspace';
import AddCircleIcon from '@mui/icons-material/AddCircle';
import { fontSize } from "@mui/system";
function TournamentTier(props){
let roundTypes = ["finals", "semifinals", "quarterfinals", "eighthfinals", "sixteenthfinals", "thirtysecondfinals"];
let matches = [];
for (let i = 0; i < props.matches.length; i++) {
matches.push(<Match tournament={props.tournament} user={props.user} tier={props.tier} roundTypes={roundTypes} teams={props.teams} match={props.matches[i]} key={i} onwinnerchange={props.onwinnerchange} />);
}
return(
<>
<Box component='ul' className={`round ${roundTypes[props.tier]}`} sx={{width:['125px','200px','250px','300px','350px']}}>
<Box component='li' className="spacer">&nbsp;</Box>
{matches}
</Box>
</>
)
}
function Match(props){
let team1Name = "TBA";
let team2Name = "TBA";
if(props.match.team1Id !== null) {
team1Name = props.teams.find(team => team.id === props.match.team1Id).name;
}
if(props.match.team2Id !== null) {
team2Name = props.teams.find(team => team.id === props.match.team2Id).name;
}
let setWinner = curryTeamId => event => {
let teamId = curryTeamId;
if (!teamId || teamId == null) {
showError("No team selected");
return;
}
let formData = new FormData();
formData.append("winnerId",teamId);
let body = new URLSearchParams(formData);
fetch(process.env.REACT_APP_API_URL + `/match/${props.match.id}/setWinner`, {
method: "POST",
body: body
})
.then(response => response.json())
.then(data => {
if (data.status === "OK") {
//Refresh when winner is set successfully
props.onwinnerchange();
} else {
showError(data.data)
}
})
.catch(error => showError(error));
};
let curryUnsetContestant = teamId => (e) => {
let formData = new FormData();
formData.append("teamId", teamId);
let body = new URLSearchParams(formData);
fetch(process.env.REACT_APP_API_URL + `/match/${props.match.id}/unsetContestant`, {
method: "POST",
body: body
})
.then(response => response.json())
.then(data => {
if (data.status === "OK") {
props.onwinnerchange()
} else {
showError(data.data);
}
})
.catch(error => showError(error));
}
return (
<>
{/* Team 1 (Winner-status?) (Team name) */}
<Box component='li' className={`game game-top`}>
<Stack direction={"row"} alignItems="center" spacing={1} sx={{justifyContent:['start','space-between']}}>
<Typography noWrap className={`${props.match.winnerId !== null ? (props.match.team1Id === props.match.winnerId) ? "winner" : "loser" : ""}`} align={'center'} sx={{ maxWidth:'70%', overflow:'hidden', wordWrap:'none', fontSize:['1em','1em','1.5em','1.75em']}}>
{team1Name}
</Typography>
{ props.match.winnerId && (props.match.team1Id === props.match.winnerId) &&
<EmojiEventsIcon alt="A trohpy" sx={{width:['0.75em','1em','1.25em'], height:['0.75em','1em','1.25em']}} />
}
<Box component={Stack} direction={'row'} spacing={-1.25}>
{ props.match.team1Id !== null && !props.tournament.hasEnded && props.match.tier !== Math.log2(props.tournament.teamLimit) - 1 && props.match.winnerId === null && props.user.isLoggedIn &&
<IconButton color="error" aria-label="remove winner" component="span" onClick={curryUnsetContestant(props.match.team1Id)}><BackspaceIcon sx={{width:['0.75em','1em','1.25em'], height:['0.75em','1em','1.25em']}} /></IconButton>
}
{ props.match.team1Id !== null && props.match.winnerId === null && !props.tournament.hasEnded && props.user.isLoggedIn &&
<IconButton onClick={setWinner(props.match.team1Id)} color="success" aria-label="select winner" component="span"><AddCircleIcon sx={{width:['0.75em','1em','1.25em'], height:['0.75em','1em','1.25em']}} /></IconButton>
}
</Box>
</Stack>
</Box>
<Box component='li' className="game game-spacer">&nbsp;</Box>
{/* Team 2 (Winner-status?) (Team name) */}
<Box component='li' className={`game game-bottom`}>
<Stack direction={"row"} alignItems="center" sx={{justifyContent:['start','space-between']}}>
<Typography noWrap className={`${props.match.winnerId !== null ? (props.match.team2Id === props.match.winnerId) ? "winner" : "loser" : ""}`} sx={{maxWidth:'70%', overflow:'hidden', wordWrap:'none',fontSize:['1em','1em','1.5em','1.75em']}}>
{team2Name}
</Typography>
{ props.match.winnerId && (props.match.team2Id === props.match.winnerId) &&
<EmojiEventsIcon alt="A trohpy" sx={{width:['0.75em','1em','1.25em'], height:['0.75em','1em','1.25em']}} />
}
{ props.match.team2Id !== null && !props.tournament.hasEnded && props.match.tier !== Math.log2(props.tournament.teamLimit) - 1 && props.match.winnerId === null && props.user.isLoggedIn &&
<IconButton color="error" aria-label="remove winner" component="span" onClick={curryUnsetContestant(props.match.team2Id)}><BackspaceIcon sx={{width:['0.75em','1em','1.25em'], height:['0.75em','1em','1.25em']}} /></IconButton>
}
{ props.match.team2Id !== null && props.match.winnerId === null && !props.tournament.hasEnded && props.user.isLoggedIn &&
<IconButton onClick={setWinner(props.match.team2Id)} color="success" aria-label="select winner" component="span" ><AddCircleIcon sx={{width:['0.75em','1em','1.25em'], height:['0.75em','1em','1.25em']}} /></IconButton>
}
</Stack>
</Box>
<Box component='li' className="spacer">&nbsp;</Box>
</>
);
}
function WinnerDisplay(props) {
let unsetWinner = event => {
let formData = new FormData();
formData.append("winnerId","null");
let body = new URLSearchParams(formData);
fetch(process.env.REACT_APP_API_URL + `/match/${props.finalMatch.id}/setWinner`, {
method: "POST",
body: body
})
.then(response => response.json())
.then(data => {
if (data.status !== "OK") { showError(data.data); return;}
props.onwinnerchange();
})
.catch(error => showError(error));
};
if (!props.team) {
// Winner is not yet chosen
return <div className="winnerDisplay">
<Typography sx={{fontSize:['1em','1em','1.5em','2em']}}>
Winner is not chosen.<br /> Will it be you?
</Typography>
</div>;
}
return (
<div className="winnerDisplay">
<Typography align="center">
{props.user.isLoggedIn && !props.tournament.hasEnded && <IconButton color="error" aria-label="remove winner" component="span" onClick={unsetWinner}><BackspaceIcon /></IconButton>}
</Typography>
<Typography sx={{fontSize:['1em','1em','1.5em','2em']}} className="winner">
{props.team.name}
</Typography>
<EmojiEventsIcon alt="A trohpy" />
</div>
)
}
function BracketViewer(props){
const [matches, setMatches] = React.useState(null);
const [teams, setTeams] = React.useState(null);
let getMatches = () => {
fetch(process.env.REACT_APP_API_URL + `/tournament/${props.tournamentId}/getMatches`)
.then(res => res.json())
.then(data => {
if (data.status !== "OK") {
// Do your error thing
console.error(data);
return;
}
let allMatches = data.data;
// Group all matches by their round/tier
let tiers = allMatches.reduce((tiers, match) => {
if (!tiers[match.tier]) {
tiers[match.tier] = [];
}
tiers[match.tier].push(match);
return tiers;
}, {});
tiers = Object.values(tiers);
tiers = tiers.reverse();
setMatches(tiers);
})
.catch(err => showError(err));
fetch(process.env.REACT_APP_API_URL + `/tournament/${props.tournamentId}/getTeams`)
.then(res => res.json())
.then(data=>{
if(data.status !== "OK"){
console.error(data)
return;
}
let teams = data.data;
setTeams(teams);
})
.catch(err => showError(err));
}
React.useEffect(() => {
getMatches();
}, []);
let getFinalMatch = (tierMatches) => {
let finalMatch = tierMatches[tierMatches.length - 1][0];
return finalMatch;
};
let getWinnerTeam = (tierMatches) => {
let finalMatch = getFinalMatch(tierMatches);
if (finalMatch.winnerId === null) { return null;}
let winnerTeam = teams.find(team => team.id === finalMatch.winnerId);
return winnerTeam;
};
return (
(props.tournament && matches && teams) ?
// <div sx={{width: "100vw", height: "80vh", overflow: "scroll"}} className="bracket">
<>
<div className="bracket">
{matches.map(tierMatches => {
let tierNum = tierMatches[0].tier;
return <TournamentTier user={props.user} tournament={props.tournament} key={tierNum} tier={tierNum} matches={tierMatches} teams={teams} onwinnerchange={getMatches} />
})}
<WinnerDisplay team={getWinnerTeam(matches)} user={props.user} finalMatch={getFinalMatch(matches)} onwinnerchange={getMatches} tournament={props.tournament} />
</div>
</>
: <Box sx={{display:'flex', justifyContent:'center', alignItems:'center', position:'relative', marginTop:'5%'}}><CircularProgress size={"20vw"}/></Box>
);
}
let showError = (message) => {};
export default function TournamentOverview(props) {
const { tournamentId } = useParams();
const [tournament, setTournament] = React.useState(false);
const [openError, setOpenError] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState("");
showError = (message) => {
setOpenError(false);
setErrorMessage(message);
setOpenError(true);
}
React.useEffect(() => {
fetch(process.env.REACT_APP_API_URL + `/tournament/${tournamentId}`)
.then(res => res.json())
.then(data => {
if (data.status !== "OK") {
showError(data.data);
return;
}
let tourn = data.data;
let now = new Date();
let endTime = new Date(tourn.endTime);
tourn.hasEnded = (now - 2*60*60*1000) > endTime; // 2 hours in the past
setTournament(tourn);
})
.catch(err => showError(err));
}, [tournamentId]);
return (
<>
<Appbar user={props.user} pageTitle={tournament.name} />
{ props.user.isLoggedIn && !tournament.hasEnded &&
<TournamentBar tournamentId={tournamentId} viewTournament={true} />
}
<BracketViewer tournament={tournament} user={props.user} tournamentId={tournamentId} className="bracketViewer" />
</>
);
}

View File

@ -0,0 +1,227 @@
import * as React from "react";
import { BrowserRouter as Router, Link, Route, Routes, useParams } from "react-router-dom";
import Appbar from "./components/AsuraBar";
import TournamentBar from "./components/TournamentBar";
import ErrorSnackbar from "./components/ErrorSnackbar";
import LoginPage from "./LoginPage";
import { Button, TextField, Stack, MenuItem, Box, InputLabel, Select, Container, TableContainer, Table, TableBody, TableHead, TableCell, TableRow, Paper, Typography} from "@mui/material";
import AddCircleIcon from '@mui/icons-material/AddCircle';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
function TeamCreator(props) {
function postCreate() {
let teamName = document.getElementById("teamNameInput").value;
if (!teamName) {
showError("Team name is required");
return;
}
let formData = new FormData();
formData.append("name", teamName);
let body = new URLSearchParams(formData)
fetch(process.env.REACT_APP_API_URL + `/tournament/${props.tournamentId}/createTeam`, {
method: "POST",
body: body
})
.then(res => res.json())
.then(data => {
if (data.status !== "OK") {
showError(data.data);
return;
}
document.getElementById("teamNameInput").value = "";
props.onTeamCreated();
}
)
}
return (
<Paper sx={{width: "90vw", margin: "10px auto", padding: "15px", align:'center', justifyContent:'center', flexGrow:1}} component={Stack} direction={['column']} spacing={2}>
<div align="center">
<form>
<TextField id="teamNameInput" sx={{width:['auto','50%','60%','70%'], margin:'1% 0'}} label="Team Name" variant="outlined" />
{/* <Button variant="contained" color="primary" onClick={postCreate}>Create Team</Button> */}
<Button type="submit" variant="contained" color="success" onClick={postCreate} sx={{ margin:'1% 1%',width:['fit-content','40%','30%','20%']}}>
<Box sx={{padding: "10px"}}>
Create Team
</Box>
<AddCircleIcon />
</Button>
</form>
</div>
</Paper>
)
}
function TeamList(props) {
const deleteTeam = teamId => {
fetch(process.env.REACT_APP_API_URL + `/team/${teamId}`, {method: "DELETE"})
.then(res => res.json())
.then(data => {
if (data.status !== "OK") {
showError(data.data);
return;
}
props.setTeams(props.teams.filter(team => team.id !== teamId));
})
.catch(error => showError(error));
}
return (
<Paper sx={{minHeight: "30vh", width: "90vw", margin: "10px auto"}} component={Stack} direction="column" justifyContent="center">
<div align="center" >
<h2><b>Teams:</b></h2>
{/* TODO: scroll denne menyen, eventuelt søkefelt */}
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Team Name</TableCell>
{/* <TableCell align="right">Team Members</TableCell> */}
<TableCell align="center">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{props.teams.map((team) => (
<TableRow key={team.id}>
<TableCell component="th" scope="row"> <b>
{team.name}
</b></TableCell>
{/* <TableCell align="right">{team.members}</TableCell> */}
<TableCell align="center">
<Button variant="contained" sx={{margin: "auto 5px"}} color="primary" onClick={() => {props.setSelectedTeamId(team.id); window.scrollTo(0, document.body.scrollHeight)}} endIcon={<EditIcon />}>Edit</Button>
<Button variant="contained" sx={{margin: "auto 5px"}} color="error" onClick={() => {deleteTeam(team.id)}} endIcon={<DeleteIcon />}>Delete</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</Paper>
);
}
function TeamEditor(props) {
const [team, setTeam] = React.useState({});
React.useEffect(() => {
if (props.selectedTeamId === -1) {
setTeam({});
return;
}
fetch(process.env.REACT_APP_API_URL + `/team/${props.selectedTeamId}`)
.then(res => res.json())
.then(data => {
if (data.status !== "OK") {
showError(data);
return;
}
setTeam(data.data);
})
.catch(error => showError(error));
}, [props.selectedTeamId]);
if (props.selectedTeamId === -1 || !team) {
return (
<Paper sx={{minHeight: "30vh", width: "90vw", margin: "10px auto"}} component={Stack} direction="column" justifyContent="center">
<div align="center" >
... Create a new team or select one from the list above ...
</div>
</Paper>
)
}
function nameInputChanged(event) {
let newTeam = {...team};
newTeam.name = event.target.value;
setTeam(newTeam);
}
function handleFocus(event) {
event.currentTarget.select()
}
function saveTeam() {
let formData = new FormData();
formData.append("name", team.name);
let body = new URLSearchParams(formData)
fetch(process.env.REACT_APP_API_URL + `/team/${team.id}/edit`, {
method: "POST",
body: body
})
.then(res => res.json())
.then(data => {
if (data.status !== "OK") {
showError(data.data);
return;
}
setTeam(data.data);
props.setTeams(props.teams.map(origTeam => origTeam.id === team.id ? team : origTeam));
props.setSelectedTeamId(-1);
}
);
}
return (
<Paper sx={{minHeight: "30vh", width: "90vw", margin: "10px auto"}} component={Stack} direction="column" justifyContent="center">
<div align="center">
<h2><b>Edit Team:</b></h2>
<form>
<TextField id="newTeamNameInput" label="Team Name" value={team.name || ""} onChange={nameInputChanged} onFocus={handleFocus} sx={{width: "80%"}} />
<Button type="submit" variant="contained" sx={{margin: "auto 5px"}} color="primary" onClick={saveTeam}>Save</Button>
</form>
</div>
</Paper>
)
}
let showError = (message) => {};
export default function TournamentTeams(props) {
const [teams, setTeams] = React.useState([]);
const [selectedTeamId, setSelectedTeamId] = React.useState(-1);
const { tournamentId } = useParams();
function getTeams() {
fetch(process.env.REACT_APP_API_URL + `/tournament/${tournamentId}/getTeams`)
.then((res) => res.json())
.then((data) => {
if (data.status !== "OK") {
showError(data.data);
}
setTeams(data.data);
//setselectedTeamId(teams[0].id);
})
.catch((err) => showError(err));
}
React.useEffect(() => {
getTeams()
}, []);
const [openError, setOpenError] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState("");
showError = (message) => {
setOpenError(false);
setErrorMessage(message);
setOpenError(true);
}
if (!props.user.isLoggedIn) { return <LoginPage user={props.user} />; }
return (
<>
<Appbar user={props.user} pageTitle="Edit teams" />
<TournamentBar pageTitle="Manage Teams" />
<div className="tournamentTeams">
<TeamCreator tournamentId={tournamentId} teams={teams} onTeamCreated={getTeams} />
<TeamList teams={teams} setTeams={setTeams} selectedTeamId={selectedTeamId} setSelectedTeamId={setSelectedTeamId} />
<TeamEditor teams={teams} setTeams={setTeams} selectedTeamId={selectedTeamId} setSelectedTeamId={setSelectedTeamId} />
</div>
<ErrorSnackbar message={errorMessage} open={openError} setOpen={setOpenError} />
</>
);
}

View File

@ -0,0 +1,97 @@
import * as React from "react";
import { BrowserRouter as Router, Link, Route, Routes, History } from "react-router-dom";
import { AppBar, Typography, Toolbar, CssBaseline, Box, Button, IconButton, Grid, Menu, MenuItem, Container } from "@mui/material"
import MenuIcon from '@mui/icons-material/Menu';
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import HistoryIcon from '@mui/icons-material/History';
import EditIcon from '@mui/icons-material/Edit';
import LogoutIcon from '@mui/icons-material/Logout';
import LoginIcon from '@mui/icons-material/Login';
import logo from "./../Asura2222.png";
function LoggedInMenu(props) {
const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const logout = () => {
setAnchorEl(null);
}
return (
<>
<IconButton size="large" edge="start" color="inherit" aria-label="menu" onClick={handleClick}>
<MenuIcon />
</IconButton>
<Menu anchorEl={anchorEl} open={open} onClose={handleClose} MenuListProps={{'aria-labelledby': 'basic-button',}} sx={{position:"absolute"}}>
<Link to="/profile" style={{color:"black"}}><MenuItem onClick={handleClose}><Button startIcon={<AccountCircleIcon />}>{props.user.name}</Button></MenuItem></Link>
<Link to="/history" style={{color:"black"}}><MenuItem onClick={handleClose}><Button startIcon={<HistoryIcon />}>History</Button></MenuItem></Link>
{ props.user.isManager &&
<Link to="/admins" style={{color:"black"}}><MenuItem onClick={handleClose}><Button startIcon={<EditIcon />} >Admins</Button></MenuItem></Link>
}
<a href={`${process.env.REACT_APP_API_URL}/users/logout`} style={{color:"black"}}><MenuItem onClick={logout}><Button startIcon={<LogoutIcon />} >Logout</Button></MenuItem></a>
</Menu>
</>
);
}
function NotLoggedInButton() {
const login = () => {
}
return (
<>
<Link to="/login" style={{color:"white"}}>
<Button sx={{color:"white"}} onClick={login} endIcon={<LoginIcon />}>
Login
</Button>
</Link>
</>
);
}
export default function Appbar(props) {
return (
<>
<CssBaseline />
<AppBar position="static" color="primary">
<Toolbar>
<Box sx={{ flexGrow: 1 }}>
<Grid container spacing={2} justifyContent="space-between" alignItems="center" align="center">
<Grid item xs={2}>
<Box sx={{ width:"100%", height: "100%", justifyContent:"left", align: "center", alignItems:"center", margin: "none", padding: "none", color: "white" ,display: "flex", flexFlow: "row"}}>
<Link to="/">
<Box component="img" src={logo} alt="Tournament logo" className="mainIcon" sx={{height:['55px','65px'], width:['55px','65px']}}></Box>
</Link>
{ props.pageTitle !== "Asura Tournaments" &&
<Link to="/" style={{color:"white"}}>
<Typography component="div" align="center" sx={{fontSize:['1em','1em','1.5em','2em']}}>
Home
</Typography>
</Link>
}
</Box>
</Grid>
<Grid item xs={4} md={6} lg={8}>
<Typography component="div" sx={{fontSize:['1em','1em','1.5em','2em']}}>{props.pageTitle || ""}</Typography>
</Grid>
{ props.pageTitle !== "Login" ?
<Grid item xs={2}>
{ props.user.isLoggedIn ? <LoggedInMenu user={props.user} /> : <NotLoggedInButton /> }
</Grid> :
<Grid item xs={2}>
</Grid>
}
</Grid>
</Box>
</Toolbar>
</AppBar>
</>
);
}

View File

@ -0,0 +1,32 @@
import * as React from 'react';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Snackbar from '@mui/material/Snackbar';
import MuiAlert from '@mui/material/Alert';
const Alert = React.forwardRef(function Alert(props, ref) {
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
});
export default function showError(props) {
const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
props.setOpen(false);
};
if (props.message && props.message.length > 0) {
console.log(props.message);
}
return (
<Stack spacing={2} sx={{ width: '100%' }}>
<Snackbar open={props.open} autoHideDuration={6000} onClose={handleClose}>
<Alert onClose={handleClose} severity="error" sx={{ width: '100%' }}>
{props.message}
</Alert>
</Snackbar>
</Stack>
);
}

View File

@ -0,0 +1,19 @@
import * as React from "react";
import { BrowserRouter as Router, Link, Route, Routes } from "react-router-dom";
import { Typography } from '@mui/material'
export default function NoSuchPage() {
return(
<>
<Typography type="h3">
This page does not exist
</Typography>
<Typography type="h4">
The page you are looking for does not exist or has been moved
</Typography>
<Link to="/">
Return to the home page
</Link>
</>
)
}

View File

@ -0,0 +1,18 @@
import * as React from "react";
import { BrowserRouter as Router, Link, Route, Routes } from "react-router-dom";
import { Typography } from '@mui/material'
export default function NoSuchPage() {
return(
<>
<Typography type="h3">
You are not logged in
</Typography>
<Typography type="h4">
Your account is not in the administrators list. Try again with another account here: <Link to="/login">Login</Link>
or
<Link to="/"> Return to the home page</Link>
</Typography>
</>
)
}

View File

@ -0,0 +1,29 @@
import * as React from 'react';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Snackbar from '@mui/material/Snackbar';
import MuiAlert from '@mui/material/Alert';
const Alert = React.forwardRef(function Alert(props, ref) {
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
});
export default function showError(props) {
const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
props.setOpen(false);
};
return (
<Stack spacing={2} sx={{ width: '100%' }}>
<Snackbar open={props.open} autoHideDuration={6000} onClose={handleClose}>
<Alert onClose={handleClose} severity="success" sx={{ width: '100%' }}>
{props.message}
</Alert>
</Snackbar>
</Stack>
);
}

View File

@ -0,0 +1,63 @@
import * as React from "react";
import { useParams } from "react-router-dom";
import { BrowserRouter as Router, Link, Route, Routes, History } from "react-router-dom";
import { Stack, Paper, Typography, Box, Button, Grid, Snackbar, IconButton } from "@mui/material"
import CloseIcon from '@mui/icons-material/Close';
import MuiAlert from '@mui/material/Alert';
const Alert = React.forwardRef(function Alert(props, ref) {
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
});
function ClipboardButton(props) {
const [open, setOpen] = React.useState(false);
function copyString() {
navigator.clipboard.writeText(props.clipboardContent || "");
setOpen(true);
}
const handleClose = (event, reason) => {
if (reason === 'clickaway') { return }
setOpen(false);
};
const closeAction = <>
<IconButton size="small" aria-label="close" color="inherit" onClick={handleClose}>
<CloseIcon fontSize="small" />
</IconButton>
</>
return (
<>
<Button onClick={copyString} variant="outlined" color="primary" sx={{margin:'1.5%', fontSize:['0.75em']}} >Copy {props.name}</Button>
<Snackbar open={open} autoHideDuration={1500} onClose={handleClose} action={closeAction}>
<Alert onClose={handleClose} severity="info" sx={{ width: '100%' }}>
{props.name + " copied to clipboard"}
</Alert>
</Snackbar>
</>
);
}
function ButtonLink(props) {
return (
<Link to={`/tournament/${props.tournamentId}` + props.targetPath} >
<Button variant="contained" color="primary" disabled={props.activeTitle === props.title || props.viewTournament} sx={{fontSize:['0.7em','0.75em']}} >{props.title}</Button>
</Link>
);
}
export default function TournamentBar(props) {
const { tournamentId } = useParams();
return (
<Paper sx={{width: ["90vw",], fontSize:['1rem','1rem','1.5rem','2rem'], margin: "1.5% auto"}} component={Stack} direction="column" justifyContent="center" alignItems="center">
<Stack direction="row" paddingTop={'0.5%'} sx={{fontSize:['1rem','1rem','1.5rem','2rem'], margin:'1.5%'}} spacing={2}>
<ButtonLink targetPath="" tournamentId={tournamentId} activeTitle={props.pageTitle} title="View Tournament" viewTournament={props.viewTournament}/>
<ButtonLink targetPath="/manage" tournamentId={tournamentId} activeTitle={props.pageTitle} title="Edit Tournament" />
<ButtonLink targetPath="/teams" tournamentId={tournamentId} activeTitle={props.pageTitle} title="Manage Teams" />
</Stack>
<Stack direction="row" paddingBottom={'0.5%'}>
<ClipboardButton clipboardContent={"https://discord.gg/asura"} name="Discord Invite Link" />
<ClipboardButton clipboardContent={"https://asura.feal.no/tournament/" + tournamentId} name="Tournament Link" />
</Stack>
</Paper>
)
}

View File

@ -0,0 +1,12 @@
import * as React from "react";
import { BrowserRouter as Router, Link, Route, Routes } from "react-router-dom";
import Button from "@mui/material/Button";
export default function SaveButton(props) {
return (
<Link to="/">
<Button variant="outlined" color="primary">Save and Exit</Button>
</Link>
);
}

View File

@ -0,0 +1,71 @@
import * as React from 'react';
import ReactDOM from 'react-dom';
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
palette: {
// primary: {
// },
// secondary: {
// },
pewterblue: {
main: '#8fbcbb',
contrastText: '#fff',
},
darkskyblue: {
main: '#88c0d0',
contrastText: '#fff',
},
darkpastelblue:{
main: '#81a1c1',
contrastText: '#fff',
},
rackley: {
main: '#3270a6',
contrastText: '#fff',
},
gainsboro:{
main: '#d8dee9',
contrastText: '#fff',
},
brightgrey: {
main: '#e5e9f0',
contrastText: '#fff',
},
lightslategray: {
main: '#79869c',
contrastText: '#fff',
},
cadetblue: {
main: '#a0aaba',
contrastText: '#fff',
},
red: {
main: '#bf616a',
contrastText: '#fff',
},
copper: {
main: '#d08770',
contrastText: '#fff',
},
gold: {
main: '#ebcb8b',
contrastText: '#fff',
},
green: {
main: '#a3be8c',
contrastText: '#fff',
},
grape: {
main: '#b76bbf',
contrastText: '#fff',
},
background: {
default: '#f0f2f2',
}
// contrastThreshold: 5,
// tonalOffset: 0.2,
},
});
export default theme;

View File

@ -0,0 +1,75 @@
/*
* Flex Layout Specifics
*/
.bracket{
display:flex;
flex-direction:row;
justify-content: center;
}
.round{
display:flex;
flex-direction:column;
justify-content:center;
/* width:20vw; */
list-style:none;
padding:0;
/* font-size: 1.5rem; */
}
.round .spacer{ flex-grow:1;}
.round .spacer:first-child,
.round .spacer:last-child{ flex-grow:.5; }
.round .game-spacer{
flex-grow:1;
}
/*
* General Styles
*/
/* body{
font-family:sans-serif;
font-size:medium;
padding:10px;
line-height:1.4em;
} */
li.game{
padding-left:20px;
}
.winner{
color:green;
font-weight: bold;
}
.loser{
color:grey;
}
li.game-top{ border-bottom:1px solid #aaa; }
li.game-spacer{
border-right:1px solid #aaa;
min-height:10vh;
}
li.game-bottom{
border-top:1px solid #aaa;
}
.winnerDisplay {
display:flex;
flex-direction:row;
align-items: center;
border: 2px solid gray;
border-radius: 15px;
min-height: 10vh;
max-height: 40vh;
margin: auto 5px;
padding: 10px;
}
.winnerDisplay > p.winner {
color:green;
}
.winnerDisplay > div.winner {
margin-right: 10px;
}

View File

@ -1,13 +1,38 @@
html,
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
width: 100%;
height: 100%;
}
body {
/* <yeet> */
overflow-y: auto !important;
margin: 0 !important;
padding: 0 !important;
/* </yeet> */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #929292;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
.mainIcon{
border-radius: 50%;
/* border: 5px dotted salmon; */
border: 3px solid #1ab35a;
background-color: white;
margin: 5px;
float: left;
/* margin: 50% calc(2vw + 50%) 50% 50%; */
}
a {
text-decoration: none;
}

View File

@ -1,13 +1,15 @@
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import ListElement from "./list_component";
import App from "./FrontPage.js";
import theme from './components/theme';
import { ThemeProvider } from "@emotion/react";
ReactDOM.render(
<React.StrictMode>
<ListElement />
<ListElement />
<ListElement />
</React.StrictMode>,
<>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</>,
document.getElementById("root")
);

View File

@ -1,14 +0,0 @@
import React from "react";
import ReactDOM from "react-dom";
export default class ListElement extends React.Component {
render() {
return (
<>
<div>Turnering 1, deltakere, Dato</div>
</>
);
}
}
ReactDOM.render(<Hello />, document.getElementById("root"));

View File

@ -1 +1,87 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="200.000000pt" height="170.000000pt" viewBox="0 0 200.000000 170.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,170.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1140 1450 c0 -6 11 -33 25 -60 14 -27 25 -52 25 -55 0 -3 -12 -5
-26 -5 -16 0 -37 -12 -55 -31 -16 -18 -32 -29 -35 -26 -4 3 -14 -1 -22 -10
-15 -15 -17 -14 -22 8 -6 23 -6 23 -17 4 -11 -19 -11 -19 -48 0 -20 11 -70 46
-112 78 -82 61 -83 62 -83 48 0 -5 21 -23 48 -40 26 -17 63 -45 82 -62 l35
-30 -30 13 c-16 7 -58 29 -92 50 -35 21 -63 35 -63 31 0 -4 39 -48 88 -96 55
-57 79 -87 65 -82 -16 4 -23 2 -23 -8 0 -7 16 -18 37 -23 39 -10 50 -32 32
-66 -7 -13 -6 -16 4 -12 26 9 65 -15 96 -59 16 -25 27 -47 24 -51 -4 -3 -1 -6
5 -6 13 0 57 -79 49 -87 -3 -2 3 -13 14 -24 10 -10 19 -28 19 -39 0 -17 -3
-18 -26 -9 -29 10 -40 7 -29 -10 3 -6 17 -11 30 -11 13 0 27 -4 30 -10 3 -6
17 -10 30 -10 27 0 89 -20 158 -50 l47 -21 -29 -57 c-16 -31 -37 -65 -48 -74
-26 -24 -91 -50 -117 -46 -11 1 -28 -2 -36 -6 -12 -7 -13 -6 -2 5 23 25 12 30
-18 9 -52 -37 -33 -7 25 39 30 24 55 48 55 53 0 9 -84 -47 -126 -84 -11 -10
-26 -18 -32 -18 -7 0 -12 -5 -12 -11 0 -9 -25 -10 -89 -6 -100 7 -121 13 -121
33 0 8 -7 14 -15 14 -8 0 -13 -6 -11 -12 1 -7 1 -10 -1 -5 -9 15 -53 6 -63
-12 -14 -28 -13 -31 14 -31 15 0 26 -7 29 -20 5 -19 14 -20 241 -20 l236 0 20
-41 c20 -38 23 -40 53 -34 51 11 67 24 32 25 -16 1 -39 5 -50 10 -17 7 -14 9
15 11 24 1 28 3 13 6 -13 2 -23 7 -23 9 0 15 32 64 47 73 11 7 13 11 5 11 -11
0 69 113 89 126 4 2 34 -20 65 -49 43 -40 59 -62 60 -83 0 -16 -4 -33 -10 -37
-6 -5 -2 -6 10 -2 17 4 25 -1 38 -30 15 -30 15 -39 4 -63 l-13 -27 -140 -3
c-77 -2 -141 -5 -143 -7 -2 -2 6 -13 17 -24 20 -20 32 -21 186 -21 158 0 165
1 165 20 0 23 -553 1135 -570 1145 -5 3 -10 1 -10 -5z m10 -144 c0 -3 -4 -8
-10 -11 -5 -3 -10 -1 -10 4 0 6 5 11 10 11 6 0 10 -2 10 -4z m75 -36 c12 -24
12 -30 -1 -46 -13 -18 -14 -17 -8 7 4 19 1 28 -12 33 -14 5 -15 4 -5 -8 10
-12 9 -14 -6 -12 -19 3 -28 28 -19 51 8 21 34 8 51 -25z m40 -89 c-8 -8 -31
25 -27 38 3 10 9 7 18 -10 7 -13 11 -26 9 -28z m169 -324 c36 -64 37 -69 22
-91 -11 -17 -22 -23 -38 -20 -13 2 -17 2 -10 -2 20 -9 14 -34 -8 -34 -14 0
-20 6 -18 16 2 10 -3 18 -10 20 -7 1 -8 0 -2 -3 15 -7 2 -23 -15 -17 -8 4 -12
10 -9 15 3 5 -1 10 -8 10 -92 8 -121 17 -114 34 3 8 1 15 -4 15 -6 0 -10 -4
-10 -10 0 -5 -4 -10 -10 -10 -5 0 -10 7 -10 15 0 8 -4 15 -8 15 -4 0 -14 21
-21 48 -8 26 -25 59 -37 75 -15 17 -21 34 -16 45 6 16 7 16 10 -1 2 -9 7 -15
11 -12 4 2 21 -9 36 -25 33 -35 41 -36 69 -13 15 12 13 9 -4 -11 l-25 -29 32
17 c41 21 43 20 43 -8 0 -15 7 -26 20 -29 21 -5 28 -27 9 -27 -5 0 -7 -4 -4
-10 3 -5 17 -10 29 -10 13 0 30 -9 37 -21 14 -20 46 -31 62 -22 4 3 4 10 -2
16 -33 40 -77 136 -75 165 1 17 -3 32 -8 32 -5 0 -6 5 -2 12 4 7 16 -6 29 -33
12 -24 39 -74 59 -112z m-264 112 c0 -6 -4 -7 -10 -4 -5 3 -10 11 -10 16 0 6
5 7 10 4 6 -3 10 -11 10 -16z m363 -309 c4 -15 0 -20 -14 -20 -20 0 -23 8 -13
34 7 20 20 13 27 -14z m-73 -8 c0 -11 -58 -92 -65 -92 -9 0 15 40 36 61 11 11
17 24 14 29 -4 6 -1 10 4 10 6 0 11 -4 11 -8z m-79 -119 c-23 -40 -49 -65 -62
-61 -10 3 -3 15 23 41 39 40 55 48 39 20z m-105 -42 c-5 -8 -76 -14 -76 -6 0
7 64 23 73 18 4 -2 5 -8 3 -12z"/>
<path d="M1099 1378 c-6 -21 -5 -38 2 -38 5 0 9 9 9 20 0 16 -7 28 -11 18z"/>
<path d="M710 1130 c0 -5 7 -10 15 -10 8 0 15 5 15 10 0 6 -7 10 -15 10 -8 0
-15 -4 -15 -10z"/>
<path d="M777 1129 c7 -7 15 -10 18 -7 3 3 -2 9 -12 12 -14 6 -15 5 -6 -5z"/>
<path d="M870 1116 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
13z"/>
<path d="M824 1086 c1 -14 6 -29 11 -34 4 -4 5 -2 2 5 -4 6 -3 13 2 15 5 1 3
11 -4 22 -13 17 -14 17 -11 -8z"/>
<path d="M755 1084 c19 -16 19 -16 -5 -10 -23 6 -22 4 10 -24 39 -34 50 -37
50 -14 0 8 -4 13 -9 10 -5 -3 -14 1 -21 9 -7 8 -9 15 -5 15 4 0 2 7 -5 15 -7
8 -18 15 -24 15 -6 0 -2 -8 9 -16z"/>
<path d="M890 1050 c0 -5 10 -10 23 -10 18 0 19 2 7 10 -19 13 -30 13 -30 0z"/>
<path d="M939 1030 c-7 -19 -15 -27 -22 -23 -21 13 -36 11 -30 -3 3 -7 8 -13
11 -12 21 2 20 -8 -8 -61 l-31 -59 -37 14 c-20 9 -57 24 -81 35 -24 11 -46 18
-49 15 -6 -6 92 -52 133 -62 37 -9 32 -34 -7 -35 -31 -2 -31 -2 -5 -6 15 -2
27 -7 26 -11 0 -4 -24 -52 -52 -107 l-52 -100 -42 -1 c-24 0 -46 -3 -49 -7 -4
-3 -13 1 -21 9 -12 11 -18 12 -29 3 -22 -18 -16 2 7 27 12 13 18 28 14 34 -6
9 -10 9 -16 -1 -7 -11 -12 -12 -28 -2 -16 10 -20 9 -25 -2 -6 -17 -36 -20 -36
-5 0 7 -6 7 -17 2 -10 -5 -24 -7 -32 -4 -10 3 -13 -2 -9 -22 3 -14 2 -26 -3
-26 -4 0 -10 10 -12 23 l-4 22 -2 -22 c0 -13 -6 -23 -11 -23 -6 0 -8 -11 -4
-25 4 -17 2 -25 -6 -25 -9 0 -9 -4 1 -17 11 -15 9 -14 -9 1 -22 19 -22 19 -47
-4 -24 -23 -115 -188 -115 -209 0 -6 11 -11 24 -11 15 0 26 -7 29 -20 5 -20
12 -20 179 -18 l173 3 172 344 c95 190 173 350 173 357 0 20 -22 64 -32 64 -5
0 -14 -13 -19 -30z m-559 -490 c0 -11 -7 -20 -15 -20 -18 0 -19 12 -3 28 16
16 18 15 18 -8z"/>
<path d="M870 988 c0 -9 5 -20 10 -23 13 -8 13 5 0 25 -8 13 -10 13 -10 -2z"/>
<path d="M600 983 c0 -5 12 -16 28 -23 15 -8 31 -16 36 -19 5 -2 6 1 2 7 -3 5
-1 12 6 14 8 3 6 7 -5 11 -9 3 -17 2 -17 -4 0 -5 -11 -2 -25 7 -14 9 -25 12
-25 7z"/>
<path d="M1059 933 c-13 -16 -12 -17 4 -4 9 7 17 15 17 17 0 8 -8 3 -21 -13z"/>
<path d="M1115 750 c-16 -7 -17 -9 -3 -9 9 -1 20 4 23 9 7 11 7 11 -20 0z"/>
<path d="M1190 549 c0 -5 5 -7 10 -4 6 3 10 8 10 11 0 2 -4 4 -10 4 -5 0 -10
-5 -10 -11z"/>
<path d="M1430 529 c-22 -18 -22 -19 -3 -10 12 6 25 16 28 21 9 15 3 12 -25
-11z"/>
<path d="M1418 403 c6 -2 18 -2 25 0 6 3 1 5 -13 5 -14 0 -19 -2 -12 -5z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';