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.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
|
.env
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
|
|
|
@ -1,15 +1,30 @@
|
||||||
{
|
{
|
||||||
|
|
||||||
"name": "tournament-server",
|
"name": "tournament-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "DCST1008 Project - Server - Asura Tournament Management System",
|
"description": "DCST1008 Project - Server - Asura Tournament Management System",
|
||||||
"author": "felixalb, kristoju, jonajha, krisleri",
|
"author": "felixalb, kristoju, jonajha, krisleri",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"homepage": "",
|
||||||
"dependencies": {
|
"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": "^17.0.2",
|
||||||
|
"react-bootstrap": "^2.2.1",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-router-dom": "^6.2.2",
|
||||||
"react-scripts": "5.0.0",
|
"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": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"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="theme-color" content="#000000" />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content="Web site created using create-react-app"
|
content="Asura Tournament System"
|
||||||
/>
|
/>
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
<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.
|
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`.
|
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>
|
<title>Asura Tournament System</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
<div id="root"></div>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"short_name": "React App",
|
"short_name": "Asura Tournament",
|
||||||
"name": "Create React App Sample",
|
"name": "Asura Tournament",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"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 {
|
body {
|
||||||
margin: 0;
|
width: 100%;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
height: 100%;
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
}
|
||||||
|
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;
|
sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
background-color: #929292;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||||
monospace;
|
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 React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import "./index.css";
|
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(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<>
|
||||||
<ListElement />
|
<ThemeProvider theme={theme}>
|
||||||
<ListElement />
|
<App />
|
||||||
<ListElement />
|
</ThemeProvider>
|
||||||
</React.StrictMode>,
|
</>,
|
||||||
document.getElementById("root")
|
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';
|
|