Merge branch 'client' into 'main'
Merge entire client branch into main See merge request felixalb/dcst1008-2022-group1!3
|
@ -17,6 +17,7 @@ node_modules
|
|||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
|
|
@ -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",
|
||||
|
|
After Width: | Height: | Size: 1.5 MiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 648 KiB |
After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 12 KiB |
|
@ -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>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"short_name": "Asura Tournament",
|
||||
"name": "Asura Tournament",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
|
|
After Width: | Height: | Size: 41 KiB |
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
After Width: | Height: | Size: 22 KiB |
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>)
|
||||
}
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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"> </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"> </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"> </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" />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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")
|
||||
);
|
||||
|
|
|
@ -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"));
|
|
@ -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 |
|
@ -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';
|