Merge branch 'client' into 'client-kristofferTournament'

# Conflicts:
#   src/client/src/TournamentCreator.js
#   src/client/src/TournamentOverview.js
This commit is contained in:
Kristoffer Juelsenn 2022-04-21 12:51:18 +00:00
commit 7865e97c28
13 changed files with 296 additions and 62 deletions

View File

@ -1,2 +1,3 @@
REACT_APP_API_URL=https://asura.feal.no/api
REACT_APP_LOGIN_URL=http://localhost:3001/auth/google
BROWSER=none

View File

@ -4869,6 +4869,7 @@
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
@ -6116,7 +6117,8 @@
"esprima": "^4.0.1",
"estraverse": "^5.2.0",
"esutils": "^2.0.2",
"optionator": "^0.8.1"
"optionator": "^0.8.1",
"source-map": "~0.6.1"
},
"bin": {
"escodegen": "bin/escodegen.js",
@ -8724,6 +8726,7 @@
"@types/node": "*",
"anymatch": "^3.0.3",
"fb-watchman": "^2.0.0",
"fsevents": "^2.3.2",
"graceful-fs": "^4.2.9",
"jest-regex-util": "^27.5.1",
"jest-serializer": "^27.5.1",
@ -10029,9 +10032,14 @@
"integrity": "sha512-EoQp/Et7OSOVu0aJknJOtlXZsnr8XE8KwuzTHOLeVSEx8pVWUICc8Q0VYRHgzyjX78nMEyC/oztWFbgyhtNfDA==",
"dependencies": {
"copy-anything": "^2.0.1",
"errno": "^0.1.1",
"graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
"make-dir": "^2.1.0",
"mime": "^1.4.1",
"needle": "^2.5.2",
"parse-node-version": "^1.0.1",
"source-map": "~0.6.0",
"tslib": "^2.3.0"
},
"bin": {
@ -12407,6 +12415,7 @@
"eslint-webpack-plugin": "^3.1.1",
"file-loader": "^6.2.0",
"fs-extra": "^10.0.0",
"fsevents": "^2.3.2",
"html-webpack-plugin": "^5.5.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^27.4.3",
@ -12826,6 +12835,9 @@
"version": "2.70.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.70.1.tgz",
"integrity": "sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA==",
"dependencies": {
"fsevents": "~2.3.2"
},
"bin": {
"rollup": "dist/bin/rollup"
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -0,0 +1,134 @@
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 {Button, Box, TextField, Stack, InputLabel, Paper, TableContainer, Table, TableBody, TableHead, TableCell, TableRow, 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 showError(error) {
alert("Something went wrong. \n" + error);
console.error(error);
}
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 + `/admins/create`, {
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"}} component={Stack} direction="column">
<div align="center">
<TextField id="adminEmailInput" sx={{ width: "70%" }} label="Admin Email" variant="outlined" />
{/* <Button variant="contained" color="primary" onClick={postCreate}>Create Team</Button> */}
<Button variant="contained" color="success" onClick={postCreate} sx={{width: "20%", marginLeft: "5px"}}>
<Box sx={{padding: "10px"}}>
Create Admin
</Box>
<AddCircleIcon />
</Button>
</div>
</Paper>
)
}
function AdminList(props){
const deleteAdmin = adminId => {
fetch(process.env.REACT_APP_API_URL + `/admins/${adminId}`, {method: "DELETE"})
.then(res => res.json())
.then(data => {
if(data.status !== "OK"){
showError(data.data);
return;
}
props.setAdmins(props.admins.filter(admin => admin.id !== adminId));
})
.catch(error => showError(error));
}
return(
<Paper sx={{minHeight: "30vh", width:"90vw", margin:"10px auto"}} component={Stack} direction="column" justifycontent="center">
<div align="center">
{/* TODO: scroll denne menyen, eventuelt søkefelt */}
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Admin</TableCell>
<TableCell align="center">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{props.admins.map((admin) => (
<TableRow key={admin.id}>
<TableCell component="th" scope="row"> <b>
{admin.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)} endIcon={<EditIcon />}>Edit</Button> */}
<Button variant="contained" sx={{margin: "auto 5px"}} color="error" onClick={() => {deleteAdmin(admin.id)}} endIcon={<DeleteIcon />}>Delete</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</Paper>
)
}
export default function Admins(props) {
const [admins, setAdmins] = React.useState([]);
const { adminId } = useParams();
function getAdmins() {
fetch(process.env.REACT_APP_API_URL + `/admins/getAdmins`)
.then((res) => res.json())
.then((data) =>{
if(data.status !== "OK") {
showError(data.data);
}
setAdmins(data.data);
})
.catch((err) => showError(err));
}
React.useEffect(() => {
getAdmins()
}, []);
return (
<>
<Appbar pageTitle="Admins" />
<div className="admins">
<AdminCreator />
<AdminList />
</div>
</>
);
}

View File

@ -3,9 +3,9 @@ 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 TournamentAnnouncement from "./TournamentAnnouncement";
import TournamentHistory from "./TournamentHistory";
import TournamentTeams from "./TournamentTeams";
import LoginPage from "./LoginPage";
import AppBar from './components/AsuraBar';
import { Button, Container, Typography, Box, Stack, Card, CardContent, CardMedia, Paper, Grid, Icon } from "@mui/material";
import AddCircleIcon from '@mui/icons-material/AddCircle';
@ -56,6 +56,34 @@ function TournamentListItem(props) {
</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: {remainingDays} Days, {remainingHours} Hours, {remainingMins} Minutes and {remainingSecs} Seconds </Typography>
</Box>
)
}
}
return (
<Paper elevation={8} >
<Card>
@ -73,7 +101,7 @@ function TournamentListItem(props) {
<Typography variant="body"> End: {props.tournament.endTime.toLocaleString()} </Typography>
</Box>
<Typography variant="h5" color="text.primary" gutterBottom> Players {props.tournament.teamCount} / {props.tournament.teamLimit} </Typography>
<Typography variant="h5" color="text.primary" gutterBottom> Players: {props.tournament.teamCount} / {props.tournament.teamLimit} </Typography>
<Description />
<Box sx={{flexGrow: 1, marginTop: "20px"}}>
@ -92,6 +120,8 @@ function TournamentListItem(props) {
</Grid>
</Grid>
</Box>
<Countdown />
</CardContent>
</Card>
</Paper>
@ -144,6 +174,9 @@ function Home() {
<CreateButton />
</Box>
<TournamentList />
<Typography variant="h5" color="#555555">
Finished tournaments are moved to the <Link to="/history">history-page</Link>
</Typography>
</Container>
</>
);
@ -160,10 +193,7 @@ export default function App() {
<Route path="/tournament/:tournamentId/manage" element={<TournamentManager />} />
<Route path="/tournament/:tournamentId/teams" element={<TournamentTeams />} />
<Route path="/history" element={<TournamentHistory />} />
<Route
path="/tournament/manage/announcement"
element={<TournamentAnnouncement />}
/>
<Route path="/login" element={<LoginPage />} />
</Routes>
</Router>
</React.StrictMode>

View File

@ -0,0 +1,19 @@
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() {
return (
<>
<AppBar pageTitle="Sign in" />
<h2>Sign in with google</h2>
<a href={process.env.REACT_APP_LOGIN_URL}>
<img src="/btn_google_signing_dark.png" alt="Sign in with google" />
</a>
</>
);
}

View File

@ -1,34 +0,0 @@
import * as React from "react";
import { BrowserRouter as Router, Link, Route, Routes } from "react-router-dom";
import Appbar from './components/AsuraBar';
function Announcement() {
return (
<form>
<label for="recipients">Recipients:</label>
<select id="recipients">
<option value="all">All</option>
<option value="mail1@gmail.com">Person 1</option>
<option value="mail2@gmail.com">Person 2</option>
<option value="mail3@gmail.com">Person 3</option>
</select>
<br />
<label for="subject">Subject:</label>
<input type="text" id="subject"></input>
<br />
<input type="text" id="contents"></input>
<Link to="/tournament/manage">
<input type="submit"></input>
</Link>
</form>
);
}
export default function TournamentAnnouncement() {
return (
<>
<Appbar />
<Announcement />
</>
);
}

View File

@ -7,6 +7,7 @@ import { Button, TextField, Stack, InputLabel, Select, Container, Slider, Paper,
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(showError, tournamentName, tournamentDescription, tournamentStartDate, tournamentEndDate, tournamentMaxTeams) {
if (!tournamentName || tournamentName === "") {
@ -81,8 +82,8 @@ function TournamentForm(props) {
function submitTournament(event) {
event.preventDefault();
let maxTeams = Math.pow(2, maxTeamsExponent);
let tournamentStart = new Date(startTime).valueOf() - new Date().getTimezoneOffset() * 60000;
let tournamentEnd = new Date(endTime).valueOf() - new Date().getTimezoneOffset() * 60000;
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(
props.showError,
document.getElementById("nameInput").value,

View File

@ -51,7 +51,7 @@ function shorten(description, maxLength) {
<Typography variant="body"> End: {props.tournament.endTime.toLocaleString()} </Typography>
</Box>
<Typography variant="h5" color="text.primary" gutterBottom> Players {props.tournament.teamCount} / {props.tournament.teamLimit} </Typography>
<Typography variant="h5" color="text.primary" gutterBottom> Players: {props.tournament.teamCount} / {props.tournament.teamLimit} </Typography>
<Description />
<Box sx={{flexGrow: 1, marginTop: "20px"}}>

View File

@ -5,7 +5,7 @@ import Appbar from './components/Appbar';
function MatchHistory() {
return(
`Hei`
`Hei på deg din gamle sei`
);
}

View File

@ -178,7 +178,39 @@ function BracketViewer(props){
})}
</div>
: <Box sx={{display:'flex', justifyContent:'center', alignItems:'center', position:'relative', marginTop:'5%'}}><CircularProgress size={"20vw"}/></Box>
);
);
}
function RemovableBar(props) {
const [endTime, setendTime] = React.useState(null);
React.useEffect(() => {
fetch(process.env.REACT_APP_API_URL + `/tournament/${props.tournamentId}`)
.then(res => res.json())
.then(data => {
if (data.status !== "OK") {
// Do your error thing
console.error(data);
return;
}
let endTime = data.data.endTime;
setendTime(endTime);
})
.catch(err => showError(err));
})
let today = new Date()
let yesterday = today.setDate(today.getDate() - 1)
let isComplete = new Date(endTime) < yesterday
if (isComplete) {
return (null)
} else {
return (<TournamentBar pageTitle="View Tournament" />)
}
}
function showError(error) {
alert("Something went wrong. \n" + error);
console.error(error);
}
export default function TournamentOverview(props) {
@ -187,7 +219,7 @@ export default function TournamentOverview(props) {
return (
<>
<Appbar pageTitle="View Tournament" />
<TournamentBar pageTitle="View Tournament" />
<RemovableBar tournamentId={tournamentId} />
<BracketViewer tournamentId={tournamentId} className="bracketViewer" />
</>
);

View File

@ -43,15 +43,18 @@ function TeamCreator(props) {
return (
<Paper sx={{width: "90vw", margin: "10px auto", padding: "15px"}} component={Stack} direction="column">
<div align="center">
<form>
<TextField id="teamNameInput" sx={{ width: "70%" }} label="Team Name" variant="outlined" />
{/* <Button variant="contained" color="primary" onClick={postCreate}>Create Team</Button> */}
<Button variant="contained" color="success" onClick={postCreate} sx={{width: "20%", marginLeft: "5px"}}>
<Button type="submit" variant="contained" color="success" onClick={postCreate} sx={{width: "20%", marginLeft: "5px"}}>
<Box sx={{padding: "10px"}}>
Create Team
</Box>
<AddCircleIcon />
</Button>
</form>
</div>
</Paper>
)
}
@ -92,7 +95,7 @@ function TeamList(props) {
</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)} endIcon={<EditIcon />}>Edit</Button>
<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>
@ -146,6 +149,10 @@ function TeamEditor(props) {
setTeam(newTeam);
}
function handleFocus(event) {
event.currentTarget.select()
}
function saveTeam() {
let formData = new FormData();
formData.append("name", team.name);
@ -172,9 +179,9 @@ function TeamEditor(props) {
<div align="center">
<h2><b>Edit Team:</b></h2>
<form>
<TextField id="teamNameInput" label="Team Name" value={team.name || ""} onChange={nameInputChanged} sx={{width: "80%"}} />
<TextField id="newTeamNameInput" label="Team Name" value={team.name || ""} onChange={nameInputChanged} onFocus={handleFocus} sx={{width: "80%"}} />
{/* <PlayerList players={players} setPlayers={setPlayers} /> */}
<Button variant="contained" sx={{margin: "auto 5px"}} color="primary" onClick={saveTeam}>Save</Button>
<Button type="submit" variant="contained" sx={{margin: "auto 5px"}} color="primary" onClick={saveTeam}>Save</Button>
</form>
</div>
</Paper>

View File

@ -1,7 +1,32 @@
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 } from "@mui/material"
import { Stack, Paper, Typography, Box, Button, Grid, Snackbar, IconButton } from "@mui/material"
import CloseIcon from '@mui/icons-material/Close';
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: "auto 5px"}} >Copy {props.name}</Button>
<Snackbar open={open} autoHideDuration={1500} onClose={handleClose} message={props.name + " copied to clipboard"} action={closeAction} />
</>
);
}
function ButtonLink(props) {
return (
@ -14,10 +39,17 @@ function ButtonLink(props) {
export default function TournamentBar(props) {
const { tournamentId } = useParams();
return (
<Paper sx={{width: "90vw", margin: "10px auto"}} component={Stack} direction="row" justifyContent="center">
<Paper sx={{width: "90vw", margin: "1.5% auto"}} component={Stack} direction="column" justifyContent="center" alignItems="center">
<Stack direction="row" paddingTop={'0.5%'}>
<ButtonLink targetPath="" tournamentId={tournamentId} activeTitle={props.pageTitle} title="View Tournament" />
<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>
)
}