Merge remote-tracking branch 'origin/master' into ticket/TURNIERE-148

# Conflicts:
#	js/api.js
#	pages/tournament.js
This commit is contained in:
Felix Hamme 2019-06-19 16:39:48 +02:00
commit 3df9f44c92
28 changed files with 1833 additions and 981 deletions

View File

@ -2,6 +2,9 @@
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/300915a8466f4f059150b543e9a6d1b0)](https://app.codacy.com/app/JP1998/turniere-frontend?utm_source=github.com&utm_medium=referral&utm_content=turniere/turniere-frontend&utm_campaign=Badge_Grade_Dashboard)
## Quick install with Docker
[turnie.re - Quickstart](https://github.com/turniere/turniere-quickstart)
## Development Setup
### Prerequisites

149
js/api.js
View File

@ -6,12 +6,8 @@ import {errorMessages} from './constants';
import {actionTypesUserinfo, defaultStateUserinfo} from './redux/userInfo';
import {actionTypesTournamentinfo, defaultStateTournamentinfo} from './redux/tournamentInfo';
import {
actionTypesTournamentStatistics, defaultStateTournamentStatistics,
transformTournamentInfoToStatistics, transformTournamentStatsToStatistics
} from './redux/tournamentStatistics';
import {actionTypesTournamentlist, defaultStateTournamentlist} from './redux/tournamentList';
import {deleteRequest, getRequest, patchRequest, postRequest} from './redux/backendApi';
import {deleteRequest, getRequest, patchRequest, postRequest, putRequest} from './redux/backendApi';
function storeOptionalToken(response) {
@ -54,6 +50,7 @@ const reducerUserinfo = (state = defaultStateUserinfo, action) => {
__store.dispatch({
type: actionTypesUserinfo.REGISTER_RESULT_SUCCESS
});
action.parameters.successCallback();
storeOptionalToken(resp);
}).catch(error => {
if (error.response) {
@ -74,6 +71,7 @@ const reducerUserinfo = (state = defaultStateUserinfo, action) => {
}
});
}
action.parameters.errorCallback();
});
return Object.assign({}, state, {});
case actionTypesUserinfo.REGISTER_RESULT_SUCCESS:
@ -153,6 +151,16 @@ const reducerUserinfo = (state = defaultStateUserinfo, action) => {
__store.dispatch({type: actionTypesUserinfo.CLEAR});
});
return Object.assign({}, state, {});
case actionTypesUserinfo.CHANGE_MAIL:
putRequest(action.state, '/users', {
email: action.parameters.newMail
}).then(resp => {
storeOptionalToken(resp);
action.parameters.successCallback();
}).catch(() => {
action.parameters.errorCallback();
});
return Object.assign({}, state, {});
case actionTypesUserinfo.REHYDRATE:
return Object.assign({}, state, action.parameters, {error: false, errorMessages: []});
case actionTypesUserinfo.CLEAR:
@ -176,8 +184,11 @@ const reducerTournamentinfo = (state = defaultStateTournamentinfo, action) => {
case actionTypesTournamentinfo.CREATE_TOURNAMENT:
postRequest(action.state, '/tournaments', action.parameters.tournament).then(resp => {
storeOptionalToken(resp);
action.parameters.successCallback();
}).catch(() => {
action.parameters.successCallback(resp.data);
}).catch(error => {
if (error.response) {
storeOptionalToken(error.response);
}
action.parameters.errorCallback();
});
return Object.assign({}, state, {});
@ -239,6 +250,31 @@ const reducerTournamentinfo = (state = defaultStateTournamentinfo, action) => {
action.parameters.errorCallback();
});
return Object.assign({}, state, {});
case actionTypesTournamentinfo.SUBMIT_MATCH_SCORES:
patchRequest(action.state, '/match_scores/' + action.parameters.scoreIdTeam1, {
points: action.parameters.scoreTeam1
}).then(resp => {
storeOptionalToken(resp);
patchRequest(action.state, '/match_scores/' + action.parameters.scoreIdTeam2, {
points: action.parameters.scoreTeam2
}).then(resp => {
storeOptionalToken(resp);
action.parameters.successCallback();
}).catch(error => {
if (error.response) {
storeOptionalToken(error.response);
}
action.parameters.errorCallback();
});
}).catch(error => {
if (error.response) {
storeOptionalToken(error.response);
}
action.parameters.errorCallback();
});
return Object.assign({}, state, {});
case actionTypesTournamentinfo.END_MATCH:
patchRequest(action.state, '/matches/' + action.parameters.matchId, {
state: 'finished'
@ -259,52 +295,6 @@ const reducerTournamentinfo = (state = defaultStateTournamentinfo, action) => {
}
};
const reducerTournamentStatistics = (state = defaultStateTournamentStatistics, action) => {
switch (action.type) {
case actionTypesTournamentStatistics.REQUEST_TOURNAMENT_STATISTICS:
getRequest(action.state, '/tournaments/' + action.parameters.code).then(resp => {
storeOptionalToken(resp);
__store.dispatch({
type: actionTypesTournamentStatistics.INT_REQUEST_TOURNAMENT_STATISTICS,
state: action.state,
parameters: {
code: action.parameters.code,
tournamentInfo: transformTournamentInfoToStatistics(resp.data),
successCallback: action.parameters.successCallback,
errorCallback: action.parameters.errorCallback
}
});
}).catch(error => {
if (error.response) {
storeOptionalToken(error.response);
}
action.parameters.errorCallback();
});
return state;
case actionTypesTournamentStatistics.INT_REQUEST_TOURNAMENT_STATISTICS:
getRequest(action.state, '/tournaments/' + action.parameters.code + '/statistics').then(resp => {
storeOptionalToken(resp);
__store.dispatch({
type: actionTypesTournamentStatistics.REQUEST_TOURNAMENT_STATISTICS_SUCCESS,
parameters: {
tournamentStatistics: transformTournamentStatsToStatistics(resp.data),
successCallback: action.parameters.successCallback
}
});
}).catch(error => {
if (error.response) {
storeOptionalToken(error.response);
}
action.parameters.errorCallback();
});
return Object.assign({}, state, action.parameters.tournamentInfo);
case actionTypesTournamentStatistics.REQUEST_TOURNAMENT_STATISTICS_SUCCESS:
action.parameters.successCallback();
return Object.assign({}, state, action.parameters.tournamentStatistics);
default: return state;
}
};
const reducerTournamentlist = (state = defaultStateTournamentlist, action) => {
switch (action.type) {
case actionTypesTournamentlist.FETCH:
@ -332,14 +322,12 @@ const reducerTournamentlist = (state = defaultStateTournamentlist, action) => {
const reducers = {
userinfo: reducerUserinfo,
tournamentinfo: reducerTournamentinfo,
tournamentStatistics: reducerTournamentStatistics,
tournamentlist: reducerTournamentlist
};
const defaultApplicationState = {
userinfo: defaultStateUserinfo,
tournamentinfo: defaultStateTournamentinfo,
tournamentStatistics: defaultStateTournamentStatistics,
tournamentlist: defaultStateTournamentlist
};
@ -371,13 +359,15 @@ export function verifyCredentials() {
}
}
export function register(username, email, password) {
export function register(username, email, password, successCallback, errorCallback) {
__store.dispatch({
type: actionTypesUserinfo.REGISTER,
parameters: {
username: username,
email: email,
password: password
password: password,
successCallback: successCallback,
errorCallback: errorCallback
},
state: __store.getState()
});
@ -405,6 +395,18 @@ export function logout(successCallback) {
});
}
export function changeMail(newMail, successCallback, errorCallback) {
__store.dispatch({
type: actionTypesUserinfo.CHANGE_MAIL,
parameters: {
newMail: newMail,
successCallback: successCallback,
errorCallback: errorCallback
},
state: __store.getState()
});
}
export function createTournament(data, successCallback, errorCallback) {
__store.dispatch({
type: actionTypesTournamentinfo.CREATE_TOURNAMENT,
@ -429,18 +431,6 @@ export function requestTournament(code, successCallback, errorCallback) {
});
}
export function requestTournamentStatistics(code, successCallback, errorCallback) {
__store.dispatch({
type: actionTypesTournamentStatistics.REQUEST_TOURNAMENT_STATISTICS,
parameters: {
code: code,
successCallback: successCallback,
errorCallback: errorCallback
},
state: __store.getState()
});
}
export function updateTeamName(team, successCB, errorCB) {
__store.dispatch({
type: actionTypesTournamentinfo.MODIFY_TOURNAMENT,
@ -478,6 +468,21 @@ export function endMatch(matchId, successCallback, errorCallback) {
});
}
export function submitMatchScores(scoreTeam1, scoreIdTeam1, scoreTeam2, scoreIdTeam2, successCallback, errorCallback) {
__store.dispatch({
type: actionTypesTournamentinfo.SUBMIT_MATCH_SCORES,
parameters: {
scoreTeam1: scoreTeam1,
scoreIdTeam1: scoreIdTeam1,
scoreTeam2: scoreTeam2,
scoreIdTeam2: scoreIdTeam2,
successCallback: successCallback,
errorCallback: errorCallback
},
state: __store.getState()
});
}
export function getState() {
return __store.getState();
}
@ -512,10 +517,6 @@ function rehydrateApplicationState() {
type: actionTypesTournamentlist.REHYDRATE,
parameters: Object.assign({}, persistedState.tournamentlist)
});
__store.dispatch({
type: actionTypesTournamentStatistics.REHYDRATE,
parameters: Object.assign({}, persistedState.tournamentstatistics)
});
applicationHydrated = true;
}
applicationHydrated = true;
}

View File

@ -0,0 +1,60 @@
import React from 'react';
import {Button, Input, InputGroup, InputGroupAddon, Table} from 'reactstrap';
export function EditableMatchTable(props) {
return (<Table className='mb-0'>
<tbody>
<tr>
<td className='scoreInput border-top-0'>
<ScoreInput score={props.match.team1.score} update={props.updateScoreTeam1}/>
</td>
<td className='align-middle border-top-0'>{props.match.team1.name}</td>
</tr>
<tr>
<td className='scoreInput'>
<ScoreInput score={props.match.team2.score} update={props.updateScoreTeam2}/>
</td>
<td className='align-middle'>{props.match.team2.name}</td>
</tr>
</tbody>
</Table>);
}
class ScoreInput extends React.Component {
constructor(props) {
super(props);
this.state = {score: this.props.score};
this.inputScore = this.inputScore.bind(this);
this.increaseScore = this.increaseScore.bind(this);
this.decreaseScore = this.decreaseScore.bind(this);
}
inputScore(event) {
const newScore = event.target.value;
this.setState({score: newScore});
this.props.update(newScore);
}
increaseScore() {
const newScore = Number(this.state.score) + 1;
this.setState({score: newScore});
this.props.update(newScore);
}
decreaseScore() {
const newScore = Number(this.state.score) - 1;
this.setState({score: newScore});
this.props.update(newScore);
}
render() {
return (<InputGroup>
<InputGroupAddon addonType="prepend"><Button onClick={this.decreaseScore} color='danger'
outline={true}>-1</Button></InputGroupAddon>
<Input className='font-weight-bold' value={this.state.score} onChange={this.inputScore} type='number'
step='1' placeholder='0'/>
<InputGroupAddon addonType="append"><Button onClick={this.increaseScore}
color='success'>+1</Button></InputGroupAddon>
</InputGroup>);
}
}

100
js/components/GroupStage.js Normal file
View File

@ -0,0 +1,100 @@
import {Button, Card, CardBody, Col, Collapse, Row, Table} from 'reactstrap';
import {Match} from './Match';
import React, {Component} from 'react';
import {getGroup} from '../redux/tournamentApi';
import {notify} from 'react-notify-toast';
export default class GroupStage extends Component {
constructor(props) {
super(props);
this.state = {showMatches: this.props.showMatches};
this.toggleShowMatches = this.toggleShowMatches.bind(this);
}
toggleShowMatches() {
this.setState({showMatches: !this.state.showMatches});
}
render() {
return (<div className='py-5 px-5'>
<h1 className='custom-font'>
Gruppenphase
<ShowMatchesToggleButton show={this.state.showMatches} toggle={this.toggleShowMatches}/>
</h1>
<Row className='mt-3'>
{this.props.groups.map(group => <Group group={group} key={group.id} isSignedIn={this.props.isSignedIn}
isOwner={this.props.isOwner} showMatches={this.state.showMatches}/>)}
</Row>
</div>);
}
}
function ShowMatchesToggleButton(props) {
return (<Button onClick={props.toggle} className='float-right default-font-family'>
{props.show ? 'Spiele ausblenden' : 'Spiele anzeigen'}
</Button>);
}
class Group extends Component {
constructor(props) {
super(props);
this.state = props.group;
this.reload = this.reload.bind(this);
this.onReloadSuccess = this.onReloadSuccess.bind(this);
this.onReloadError = this.onReloadError.bind(this);
}
reload() {
getGroup(this.state.id, this.onReloadSuccess, this.onReloadError);
}
onReloadSuccess(status, updatedGroup) {
this.setState(updatedGroup);
}
onReloadError() {
notify.show('Die Gruppe konnte nicht aktualisiert werden.', 'warning', 2000);
}
render() {
return (<Col className='minw-25'>
<Card>
<CardBody>
<h3 className='custom-font'>Gruppe {this.state.number}</h3>
<Collapse isOpen={this.props.showMatches}>
{this.state.matches.map((match => (
<Match match={match} isSignedIn={this.props.isSignedIn} isOwner={this.props.isOwner}
onChange={this.reload} key={match.id}/>)))}
</Collapse>
<GroupScoresTable scores={this.state.scores}/>
</CardBody>
</Card>
</Col>);
}
}
function GroupScoresTable(props) {
return (<Table className='mt-4' striped size='sm' responsive>
<thead>
<tr>
<th>Team</th>
<th>Punkte</th>
<th>erzielt</th>
<th>kassiert</th>
</tr>
</thead>
<tbody>
{props.scores.map(groupScore => <GroupScoresTableRow score={groupScore} key={groupScore.id}/>)}
</tbody>
</Table>);
}
function GroupScoresTableRow(props) {
return (<tr>
<td>{props.score.team.name}</td>
<td>{props.score.group_points}</td>
<td>{props.score.received_points}</td>
<td>{props.score.scored_points}</td>
</tr>);
}

View File

@ -1,19 +1,9 @@
import {
Button,
Card,
CardBody,
Input,
InputGroup,
InputGroupAddon,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
Table
} from 'reactstrap';
import {Card, CardBody} from 'reactstrap';
import React from 'react';
import {endMatch, startMatch} from '../api';
import {notify} from 'react-notify-toast';
import {MatchModal} from './MatchModal';
import {MatchTable} from './MatchTable';
export class Match extends React.Component {
@ -31,6 +21,7 @@ export class Match extends React.Component {
this.onEndMatchSuccess = this.onEndMatchSuccess.bind(this);
this.onEndMatchError = this.onEndMatchError.bind(this);
this.getMatchFinishedMessage = this.getMatchFinishedMessage.bind(this);
this.changeScores = this.changeScores.bind(this);
}
toggleModal() {
@ -67,6 +58,7 @@ export class Match extends React.Component {
updatedMatch.winnerTeamId = winner === null ? null : winner.id;
this.setState({match: updatedMatch});
this.toggleModal();
this.props.onFinish !== undefined && this.props.onFinish();
}
onEndMatchError() {
@ -74,6 +66,14 @@ export class Match extends React.Component {
notify.show('Das Match konnte nicht beendet werden.', 'error', 3000);
}
changeScores(scoreTeam1, scoreTeam2) {
const updatedMatch = this.state.match;
updatedMatch.team1.score = scoreTeam1;
updatedMatch.team2.score = scoreTeam2;
this.setState({match: updatedMatch});
this.props.onChange !== undefined && this.props.onChange();
}
getMatchFinishedMessage() {
const match = this.state.match;
if (match.winnerTeamId === null) {
@ -124,150 +124,8 @@ export class Match extends React.Component {
</Card>
<small className='text-muted'>{smallMessage}</small>
<MatchModal title='Match' isOpen={this.state.modal} toggle={this.toggleModal} match={this.state.match}
startMatch={this.startMatch} endMatch={this.endMatch}/>
startMatch={this.startMatch} endMatch={this.endMatch} changeScores={this.changeScores}/>
</div>);
}
}
function MatchModal(props) {
let title;
let actionButton = '';
// possible states: single_team not_ready not_started in_progress finished
switch (props.match.state) {
case 'in_progress':
title = 'Spiel läuft';
actionButton = <Button color='primary' onClick={props.endMatch}>Spiel beenden</Button>;
break;
case 'finished':
title = 'Spiel beendet';
break;
case 'single_team':
title = 'kein Gegner, Team kommt weiter';
break;
case 'not_ready':
title = 'Spiel kann noch nicht gestartet werden';
break;
case 'not_started':
title = 'Spiel kann gestartet werden';
actionButton = <Button color='primary' onClick={props.startMatch}>Spiel starten</Button>;
break;
}
return (<Modal isOpen={props.isOpen} toggle={props.toggle}>
<ModalHeader toggle={props.toggle}>{title}</ModalHeader>
<ModalBody>
{props.matchState === 'in_progress' ? <EditableMatchTable match={props.match}/> :
<MatchTable match={props.match} matchStatus={props.matchState}/>}
</ModalBody>
<ModalFooter>
{actionButton}
<Button color='secondary' onClick={props.toggle}>Abbrechen</Button>
</ModalFooter>
</Modal>);
}
function MatchTable(props) {
let team1Class;
let team2Class;
// possible states: single_team not_ready not_started in_progress finished
switch (props.matchState) {
case 'in_progress':
break;
case 'finished':
if (props.match.winnerTeamId === undefined) {
break;
}
if (props.winnerTeamId === props.match.team1.id) {
team1Class = 'font-weight-bold';
team2Class = 'lost-team';
}
if (props.winnerTeamId === props.match.team2.id) {
team1Class = 'lost-team';
team2Class = 'font-weight-bold';
}
break;
case 'single_team':
team2Class = 'text-muted';
break;
case 'not_ready':
break;
case 'not_started':
break;
}
if (props.match.state === 'single_team') {
return (<Table className='mb-0'>
<tbody>
<tr>
<td className={'border-top-0 ' + team1Class}>{props.match.team1.name}</td>
</tr>
<tr>
<td className={props.borderColor + ' ' + team2Class}>kein Gegner</td>
</tr>
</tbody>
</Table>);
} else {
return (<Table className='mb-0'>
<tbody>
<tr>
<th className='stage border-top-0'>{props.match.team1.score}</th>
<td className={'border-top-0 ' + team1Class}>{props.match.team1.name}</td>
</tr>
<tr>
<th className={'stage ' + props.borderColor}>{props.match.team2.score}</th>
<td className={props.borderColor + ' ' + team2Class}>{props.match.team2.name}</td>
</tr>
</tbody>
</Table>);
}
}
function EditableMatchTable(props) {
return (<Table className='mb-0'>
<tbody>
<tr>
<td className='scoreInput border-top-0'>
<ScoreInput score={props.match.team1.score}/>
</td>
<td className='align-middle border-top-0'>{props.match.team1.name}</td>
</tr>
<tr>
<td className='scoreInput'>
<ScoreInput score={props.match.team2.score}/>
</td>
<td className='align-middle'>{props.match.team2.name}</td>
</tr>
</tbody>
</Table>);
}
class ScoreInput extends React.Component {
constructor(props) {
super(props);
this.state = {score: this.props.score};
this.updateScore = this.updateScore.bind(this);
this.increaseScore = this.increaseScore.bind(this);
this.decreaseScore = this.decreaseScore.bind(this);
}
updateScore(event) {
this.setState({score: event.target.value});
}
increaseScore() {
this.setState({score: Number(this.state.score) + 1});
}
decreaseScore() {
this.setState({score: Number(this.state.score) - 1});
}
render() {
return (<InputGroup>
<InputGroupAddon addonType="prepend"><Button onClick={this.decreaseScore} color='danger'
outline={true}>-1</Button></InputGroupAddon>
<Input className='font-weight-bold' value={this.state.score} onChange={this.updateScore} type='number'
step='1' placeholder='0'/>
<InputGroupAddon addonType="append"><Button onClick={this.increaseScore}
color='success'>+1</Button></InputGroupAddon>
</InputGroup>);
}
}

View File

@ -0,0 +1,88 @@
import {Button, Modal, ModalBody, ModalFooter, ModalHeader} from 'reactstrap';
import React, {Component} from 'react';
import {EditableMatchTable} from './EditableMatchTable';
import {MatchTable} from './MatchTable';
import {submitMatchScores} from '../api';
import {notify} from 'react-notify-toast';
export class MatchModal extends Component {
constructor(props) {
super(props);
this.state = {scoreTeam1: this.props.match.team1.score, scoreTeam2: this.props.match.team2.score};
this.updateScoreTeam1 = this.updateScoreTeam1.bind(this);
this.updateScoreTeam2 = this.updateScoreTeam2.bind(this);
this.submitScores = this.submitScores.bind(this);
this.onSubmitScoresError = this.onSubmitScoresError.bind(this);
this.onSubmitScoresSuccess = this.onSubmitScoresSuccess.bind(this);
}
updateScoreTeam1(newScore) {
this.setState({scoreTeam1: newScore});
}
updateScoreTeam2(newScore) {
this.setState({scoreTeam2: newScore});
}
submitScores() {
const match = this.props.match;
submitMatchScores(this.state.scoreTeam1, match.team1.scoreId, this.state.scoreTeam2, match.team2.scoreId,
this.onSubmitScoresSuccess, this.onSubmitScoresError);
}
onSubmitScoresError() {
this.props.toggle();
notify.show('Der Spielstand konnte nicht geändert werden.', 'error', 2500);
}
onSubmitScoresSuccess() {
this.props.toggle();
this.props.changeScores(this.state.scoreTeam1, this.state.scoreTeam2);
notify.show('Der Spielstand wurde geändert.', 'success', 2000);
}
render() {
let title;
let actionButton = '';
let submitScoresButton = '';
let matchTable = <MatchTable match={this.props.match} matchStatus={this.props.match.state}/>;
// possible states: single_team not_ready not_started in_progress finished
switch (this.props.match.state) {
case 'in_progress':
title = 'Spiel läuft';
submitScoresButton = <Button color='primary' onClick={this.submitScores}>Spielstand ändern</Button>;
if (!this.props.match.allowUndecided && this.props.match.team1.score === this.props.match.team2.score) {
actionButton = <Button color='primary' disabled>Spiel beenden</Button>;
} else {
actionButton = <Button color='primary' onClick={this.props.endMatch}>Spiel beenden</Button>;
}
matchTable = <EditableMatchTable match={this.props.match} updateScoreTeam1={this.updateScoreTeam1}
updateScoreTeam2={this.updateScoreTeam2}/>;
break;
case 'finished':
title = 'Spiel beendet';
break;
case 'single_team':
title = 'kein Gegner, Team kommt weiter';
break;
case 'not_ready':
title = 'Spiel kann noch nicht gestartet werden';
break;
case 'not_started':
title = 'Spiel kann gestartet werden';
actionButton = <Button color='primary' onClick={this.props.startMatch}>Spiel starten</Button>;
break;
}
return (<Modal isOpen={this.props.isOpen} toggle={this.props.toggle}>
<ModalHeader toggle={this.props.toggle}>{title}</ModalHeader>
<ModalBody>
{matchTable}
</ModalBody>
<ModalFooter>
{submitScoresButton}
{actionButton}
<Button color='secondary' onClick={this.props.toggle}>Abbrechen</Button>
</ModalFooter>
</Modal>);
}
}

View File

@ -0,0 +1,57 @@
import {Table} from 'reactstrap';
import React from 'react';
export function MatchTable(props) {
let team1Class;
let team2Class;
// possible states: single_team not_ready not_started in_progress finished
switch (props.matchState) {
case 'in_progress':
break;
case 'finished':
if (props.match.winnerTeamId === undefined) {
break;
}
if (props.winnerTeamId === props.match.team1.id) {
team1Class = 'font-weight-bold';
team2Class = 'lost-team';
}
if (props.winnerTeamId === props.match.team2.id) {
team1Class = 'lost-team';
team2Class = 'font-weight-bold';
}
break;
case 'single_team':
team2Class = 'text-muted';
break;
case 'not_ready':
break;
case 'not_started':
break;
}
if (props.match.state === 'single_team') {
return (<Table className='mb-0'>
<tbody>
<tr>
<td className={'border-top-0 ' + team1Class}>{props.match.team1.name}</td>
</tr>
<tr>
<td className={props.borderColor + ' ' + team2Class}>kein Gegner</td>
</tr>
</tbody>
</Table>);
} else {
return (<Table className='mb-0'>
<tbody>
<tr>
<th className='stage border-top-0'>{props.match.team1.score}</th>
<td className={'border-top-0 ' + team1Class}>{props.match.team1.name}</td>
</tr>
<tr>
<th className={'stage ' + props.borderColor}>{props.match.team2.score}</th>
<td className={props.borderColor + ' ' + team2Class}>{props.match.team2.name}</td>
</tr>
</tbody>
</Table>);
}
}

View File

@ -0,0 +1,48 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Button, InputGroup, InputGroupAddon, Input} from 'reactstrap';
import '../../static/css/numericinput.css';
export default class NumericInput extends React.Component {
render() {
return (<InputGroup>
<InputGroupAddon addonType="prepend">
<Button onClick={this.props.decrementCallback} className="btn-width" color={this.props.decrementColor}
outline={this.props.decrementOutline}>{this.props.decrementText}</Button>
</InputGroupAddon>
<Input className='font-weight-bold' value={this.props.value}
disabled type='number'/>
<InputGroupAddon addonType="append">
<Button onClick={this.props.incrementCallback} className="btn-width" color={this.props.incrementColor}
outline={this.props.incrementOutline}>{this.props.incrementText}</Button>
</InputGroupAddon>
</InputGroup>);
}
}
NumericInput.propTypes = {
decrementText: PropTypes.string.isRequired,
decrementCallback: PropTypes.func.isRequired,
decrementColor: PropTypes.oneOf([
'primary', 'secondary', 'success', 'info', 'warning', 'danger'
]),
decrementOutline: PropTypes.bool,
incrementText: PropTypes.string.isRequired,
incrementCallback: PropTypes.func.isRequired,
incrementColor: PropTypes.oneOf([
'primary', 'secondary', 'success', 'info', 'warning', 'danger'
]),
incrementOutline: PropTypes.bool,
value: PropTypes.number.isRequired
};
NumericInput.defaultProps = {
decrementColor: 'primary',
decrementOutline: true,
incrementColor: 'primary',
incrementOutline: true
};

View File

@ -0,0 +1,66 @@
import {Stage} from './Stage';
import React, {Component} from 'react';
import {getStage} from '../redux/tournamentApi';
import {notify} from 'react-notify-toast';
export class PlayoffStages extends Component {
constructor(props) {
super(props);
this.state = {stages: this.props.playoffStages};
this.updateStage = this.updateStage.bind(this);
this.updateNextStage = this.updateNextStage.bind(this);
this.onUpdateStageSuccess = this.onUpdateStageSuccess.bind(this);
this.onUpdateStageError = this.onUpdateStageError.bind(this);
}
updateStage(id) {
getStage(id, this.onUpdateStageSuccess, this.onUpdateStageError);
}
updateNextStage(changedStageId) {
let found = false;
for (const stage of this.state.stages) {
if (found) {
this.updateStage(stage.id);
return;
}
if (stage.id === changedStageId) {
found = true;
}
}
}
onUpdateStageSuccess(status, updatedStage) {
const updatedStageIndex = this.state.stages.findIndex(stage => stage.id === updatedStage.id);
if (updatedStageIndex === -1) {
this.onUpdateStageError();
return;
}
const updatedStages = this.state.stages;
updatedStages[updatedStageIndex] = updatedStage;
this.setState({stages: updatedStages});
}
onUpdateStageError() {
notify.show('Die nachfolgende Stage konnte nicht aktualisiert werden.', 'error', 2500);
}
render() {
return (<div>
{this.props.playoffStages.map(stage => <Stage isSignedIn={this.props.isSignedIn}
isOwner={this.props.isOwner} updateNextStage={() => this.updateNextStage(stage.id)}
level={getLevelName(stage.level)} matches={stage.matches}
key={stage.level}/>)}
</div>);
}
}
function getLevelName(levelNumber) {
const names = ['Finale', 'Halbfinale', 'Viertelfinale', 'Achtelfinale'];
if (levelNumber < names.length) {
return names[levelNumber];
} else {
return Math.pow(2, levelNumber) + 'tel-Finale';
}
}

View File

@ -0,0 +1,31 @@
import React from 'react';
import {connect} from 'react-redux';
import Head from 'next/head';
import {TurniereNavigation} from './Navigation';
import {Login} from './Login';
import {Footer} from './Footer';
class RequireLogin extends React.Component {
render() {
if (this.props.isSignedIn) {
return this.props.children;
}
const loginHint = this.props.loginMessage === undefined ?
'Sie müssen angemeldet sein, um diesen Inhalt anzuzeigen!' : this.props.loginMessage;
return (<div className="main generic-fullpage-bg">
<Head>
<title>Anmeldung</title>
</Head>
<TurniereNavigation/>
<div>
<Login hint={loginHint}/>
</div>
<Footer/>
</div>);
}
}
export default connect(state => {
return {isSignedIn: state.userinfo.isSignedIn};
})(RequireLogin);

18
js/components/Stage.js Normal file
View File

@ -0,0 +1,18 @@
import {Col, Container, Row} from 'reactstrap';
import {Match} from './Match';
import React from 'react';
export function Stage(props) {
const {isSignedIn, isOwner, updateNextStage} = props;
return (<div>
<Container className='py-5'>
<h1 className='custom-font'>{props.level}</h1>
<Row>
{props.matches.map((match => (
<Col className='minw-25' key={match.id}><Match match={match} isSignedIn={isSignedIn}
isOwner={isOwner} onFinish={updateNextStage}/></Col>)))}
</Row>
</Container>
</div>);
}

View File

@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import {Alert, Collapse} from 'reactstrap';
export class WarningPopup extends React.Component {
render() {
return (<Collapse isOpen={this.props.shown}>
<Alert className='mt-2 py-1' color='danger'>
{this.props.text}
</Alert>
</Collapse>);
}
}
WarningPopup.propTypes = {
text: PropTypes.string.isRequired,
shown: PropTypes.bool.isRequired
};

View File

@ -29,6 +29,12 @@ export function patchRequest(state, url, data) {
});
}
export function putRequest(state, url, data) {
return axios.put(apiUrl + url, data, {
headers: generateHeaders(state)
});
}
function generateHeaders(state) {
if (state.userinfo.isSignedIn) {
return {

112
js/redux/tournamentApi.js Normal file
View File

@ -0,0 +1,112 @@
import {getRequest} from './backendApi';
import {getState} from '../api';
export function getTournament(code, successCallback, errorCallback) {
getRequest(getState(), '/tournaments/' + code)
.then(response => {
successCallback(response.status, convertTournament(response.data));
})
.catch(errorCallback);
}
export function getGroup(groupId, successCallback, errorCallback) {
getRequest(getState(), '/groups/' + groupId)
.then(response => {
successCallback(response.status, convertGroup(response.data));
})
.catch(errorCallback);
}
export function getStage(stageId, successCallback, errorCallback) {
getRequest(getState(), '/stages/' + stageId)
.then(response => {
successCallback(response.status, convertPlayoffStage(response.data));
})
.catch(errorCallback);
}
function convertTournament(apiTournament) {
let groupStage = null;
const playoffStages = [];
for (const apiStage of apiTournament.stages) {
if (apiStage.groups.length > 0) {
// group stage
groupStage = {groups: apiStage.groups.map(group => convertGroup(group))};
} else {
// playoff stage
playoffStages.push(convertPlayoffStage(apiStage));
}
}
return {
id: apiTournament.id,
code: apiTournament.code,
description: apiTournament.description,
name: apiTournament.name,
isPublic: apiTournament.public,
ownerUsername: apiTournament.owner_username,
groupStage: groupStage,
playoffStages: playoffStages
};
}
function convertPlayoffStage(apiStage) {
return {
id: apiStage.id, level: apiStage.level, matches: apiStage.matches.map(match => convertMatch(match, false))
};
}
function convertGroup(apiGroup) {
return {
id: apiGroup.id,
number: apiGroup.number,
scores: apiGroup.group_scores,
matches: apiGroup.matches.map(match => convertMatch(match, true))
};
}
function convertMatch(apiMatch, allowUndecided) {
const result = {
id: apiMatch.id, state: apiMatch.state, allowUndecided: allowUndecided,
winnerTeamId: apiMatch.winner === null ? null : apiMatch.winner.id
};
if (apiMatch.match_scores.length === 2) {
result.team1 = {
name: apiMatch.match_scores[0].team.name,
id: apiMatch.match_scores[0].team.id,
score: apiMatch.match_scores[0].points,
scoreId: apiMatch.match_scores[0].id
};
result.team2 = {
name: apiMatch.match_scores[1].team.name,
id: apiMatch.match_scores[1].team.id,
score: apiMatch.match_scores[1].points,
scoreId: apiMatch.match_scores[1].id
};
} else if (apiMatch.match_scores.length === 1) {
result.team1 = {
name: apiMatch.match_scores[0].team.name,
id: apiMatch.match_scores[0].team.id,
score: apiMatch.match_scores[0].points,
scoreId: apiMatch.match_scores[0].id
};
result.team2 = {
name: 'TBD',
id: null,
score: 0
};
} else {
result.team1 = {
name: 'TBD',
id: null,
score: 0
};
result.team2 = {
name: 'TBD',
id: null,
score: 0
};
}
return result;
}

View File

@ -9,6 +9,7 @@ export const actionTypesTournamentinfo = {
'MODIFY_TOURNAMENT_ERROR': 'MODIFY_TOURNAMENT_ERROR',
'START_MATCH': 'START_MATCH',
'SUBMIT_MATCH_SCORES': 'SUBMIT_MATCH_SCORES',
'END_MATCH': 'END_MATCH',
'REHYDRATE': 'TOURNAMENTINFO_REHYDRATE',

View File

@ -13,6 +13,8 @@ export const actionTypesUserinfo = {
'VERIFY_CREDENTIALS_SUCCESS': 'VERIFY_CREDENTIALS_SUCCESS',
'VERIFY_CREDENTIALS_ERROR': 'VERIFY_CREDENTIALS_ERROR',
'CHANGE_MAIL': 'CHANGE_MAIL',
'STORE_AUTH_HEADERS': 'STORE_AUTH_HEADERS',
'REHYDRATE': 'USERINFO_REHYDRATE',

View File

@ -20,6 +20,7 @@
"express": "^4.16.4",
"next": "^7.0.2",
"react": "^16.6.1",
"react-bootstrap": "^1.0.0-beta.9",
"react-dom": "^16.6.1",
"react-favicon": "^0.0.14",
"react-notify-toast": "^0.5.0",

View File

@ -2,27 +2,34 @@ import Head from 'next/head';
import React from 'react';
import {notify} from 'react-notify-toast';
import {connect} from 'react-redux';
import posed from 'react-pose';
import {
Button, Card, CardBody, Container, CustomInput, Form, FormGroup, Input, Label
Button,
Card,
CardBody,
Col,
Collapse,
Container,
CustomInput,
Form,
FormGroup,
Input,
Label,
Row
} from 'reactstrap';
import {TurniereNavigation} from '../js/components/Navigation';
import {Footer} from '../js/components/Footer';
import {UserRestrictor, Option} from '../js/components/UserRestrictor';
import {Login} from '../js/components/Login';
import EditableStringList from '../js/components/EditableStringList';
import {createTournament} from '../js/api';
import {WarningPopup} from '../js/components/WarningPopup';
import Router from 'next/router';
import '../static/css/everypage.css';
import RequireLogin from '../js/components/RequireLogin';
import NumericInput from '../js/components/NumericInput';
class CreatePage extends React.Component {
render() {
const {isSignedIn} = this.props;
return (<UserRestrictor>
<Option condition={isSignedIn}>
return (<RequireLogin loginMessage='Sie müssen angemeldet sein, um ein neues Turnier zu erstellen.'>
<div className="main generic-fullpage-bg">
<Head>
<title>Turnier erstellen: turnie.re</title>
@ -33,20 +40,7 @@ class CreatePage extends React.Component {
</div>
<Footer/>
</div>
</Option>
<Option condition={true}>
<div className="main generic-fullpage-bg">
<Head>
<title>Anmeldung</title>
</Head>
<TurniereNavigation/>
<div>
<Login hint="Sie müssen angemeldet sein, um diesen Inhalt anzuzeigen!"/>
</div>
<Footer/>
</div>
</Option>
</UserRestrictor>);
</RequireLogin>);
}
}
@ -68,14 +62,6 @@ function CreateTournamentCard() {
</Container>);
}
const GroupphaseFader = posed.div({
visible: {
opacity: 1, height: 150
}, hidden: {
opacity: 0, height: 0
}
});
class CreateTournamentForm extends React.Component {
constructor(props) {
super(props);
@ -93,8 +79,12 @@ class CreateTournamentForm extends React.Component {
this.handleNameInput = this.handleNameInput.bind(this);
this.handleDescriptionInput = this.handleDescriptionInput.bind(this);
this.handlePublicInput = this.handlePublicInput.bind(this);
this.handleGroupSizeInput = this.handleGroupSizeInput.bind(this);
this.handleGroupAdvanceInput = this.handleGroupAdvanceInput.bind(this);
this.increaseGroupAdvance = this.increaseGroupAdvance.bind(this);
this.decreaseGroupAdvance = this.decreaseGroupAdvance.bind(this);
this.increaseGroupSize = this.increaseGroupSize.bind(this);
this.decreaseGroupSize = this.decreaseGroupSize.bind(this);
this.generateTournamentCreationObject = this.generateTournamentCreationObject.bind(this);
this.valuesAreCredible = this.valuesAreCredible.bind(this);
this.create = this.create.bind(this);
}
@ -121,20 +111,34 @@ class CreateTournamentForm extends React.Component {
checked={this.state.groupPhaseEnabled}
onChange={this.handleGroupPhaseEnabledInput}/>
</FormGroup>
<GroupphaseFader pose={this.state.groupPhaseEnabled ? 'visible' : 'hidden'}
className="groupphasefader">
<Collapse isOpen={this.state.groupPhaseEnabled}>
<FormGroup>
<Label for="teams-per-group">Anzahl Teams pro Gruppe</Label>
<Input type="number" name="teams-per-group" min="3"
value={this.state.groupSize} onChange={this.handleGroupSizeInput}/>
<Row>
<Col xs="3">
<NumericInput value={this.state.groupSize}
incrementText="+1" incrementCallback={this.increaseGroupSize}
decrementText="-1" decrementCallback={this.decreaseGroupSize}/>
</Col>
</Row>
<WarningPopup text='Es gibt noch unvollständige Gruppen.' shown={this.areGroupsIncomplete()}/>
</FormGroup>
<FormGroup>
<Label for="teams-group-to-playoff">Wie viele Teams sollen nach der Gruppenphase
weiterkommen?</Label>
<Input type="number" name="teams-group-to-playoff" min="1" max={this.state.groupSize - 1}
value={this.state.groupAdvance} onChange={this.handleGroupAdvanceInput}/>
<Row>
<Col xs="3">
<NumericInput value={this.state.groupAdvance}
incrementText="&#215;2" incrementCallback={this.increaseGroupAdvance}
decrementText="&#247;2" decrementCallback={this.decreaseGroupAdvance}/>
</Col>
</Row>
<WarningPopup
text={'Füge bitte noch ' + (this.state.groupAdvance - this.state.teams.length)
+ ' Team(s) hinzu, um ' + this.state.groupAdvance + ' Team(s) im Playoff zu haben.'}
shown={this.state.teams.length < this.state.groupAdvance}/>
</FormGroup>
</GroupphaseFader>
</Collapse>
</Form>
<h3 className="custom-font mt-4">Teams</h3>
<EditableStringList
@ -153,6 +157,10 @@ class CreateTournamentForm extends React.Component {
</div>);
}
areGroupsIncomplete() {
return this.state.groups.filter(group => group.length !== this.state.groupSize).length !== 0;
}
teamListUpdate(list) {
this.setState({teams: list});
}
@ -161,31 +169,34 @@ class CreateTournamentForm extends React.Component {
this.setState({groups: list});
}
handleGroupSizeInput(input) {
const newSize = input.target.value;
increaseGroupAdvance() {
const newGroupAdvance = this.state.groupAdvance * 2;
if (newSize === undefined || newSize < 2) {
return;
}
if (newSize <= this.state.groupAdvance) {
this.setState({
groupSize: newSize, groupAdvance: newSize - 1
groupAdvance: newGroupAdvance
});
}
decreaseGroupAdvance() {
const newGroupAdvance = Math.floor(this.state.groupAdvance / 2);
if (newGroupAdvance >= 1) {
this.setState({
groupAdvance: newGroupAdvance
});
} else {
this.setState({groupSize: newSize});
}
}
handleGroupAdvanceInput(input) {
const newAdvance = input.target.value;
if (newAdvance === undefined || newAdvance <= 0 ||
newAdvance >= this.state.groupSize) {
return;
increaseGroupSize() {
this.setState({groupSize: this.state.groupSize + 1});
}
this.setState({groupAdvance: newAdvance});
decreaseGroupSize() {
const newGroupSize = this.state.groupSize - 1;
if (newGroupSize >= 3) {
this.setState({groupSize: newGroupSize});
}
}
handleGroupPhaseEnabledInput(input) {
@ -205,17 +216,35 @@ class CreateTournamentForm extends React.Component {
}
create() {
createTournament({
if (this.valuesAreCredible()) {
createTournament(this.generateTournamentCreationObject(), data => {
Router.push('/t/' + data.id);
}, () => {
notify.show('Das Turnier konnte nicht erstellt werden.', 'warning', 5000);
});
} else {
notify.show('Bitte korrigiere deine Eingaben zuerst.', 'warning', 5000);
}
}
valuesAreCredible() {
return this.state.teams.length >= this.state.groupAdvance && !this.areGroupsIncomplete();
}
generateTournamentCreationObject() {
const tournament = {
'name': this.state.name,
'description': this.state.description,
'public': this.state.public,
'group_stage': this.state.groupPhaseEnabled,
'teams': createTeamArray(this.state.groupPhaseEnabled, this.state.groups, this.state.teams)
}, () => {
notify.show('Das Turnier wurde erfolgreich erstellt.', 'success', 5000);
}, () => {
notify.show('Das Turnier konnte nicht erstellt werden.', 'warning', 5000);
});
};
if (this.state.groupPhaseEnabled) {
tournament.playoff_teams_amount = this.state.groupAdvance;
}
return tournament;
}
}

View File

@ -38,7 +38,8 @@ function PublicTournaments(props) {
<PublicTournamentsCard/>
</Container>
<Container className="pb-5 pt-3">
<a href='/private' className="btn btn-success shadow">zu den privaten Turnieren</a>
<a href='/private' className="btn btn-primary shadow">zu den privaten Turnieren</a>
<a href='/create' className="ml-3 btn btn-success shadow">neues Turnier erstellen</a>
</Container>
</div>);
} else {

View File

@ -6,41 +6,23 @@ import {Card, CardBody, Container} from 'reactstrap';
import {TurniereNavigation} from '../js/components/Navigation';
import {Footer} from '../js/components/Footer';
import {Option, UserRestrictor} from '../js/components/UserRestrictor';
import {Login} from '../js/components/Login';
import '../static/css/everypage.css';
import TournamentList from '../js/components/TournamentList';
import RequireLogin from '../js/components/RequireLogin';
class PrivateTournamentsPage extends React.Component {
render() {
const {isSignedIn} = this.props;
return (<UserRestrictor>
<Option condition={isSignedIn}>
return (<RequireLogin loginMessage='Sie müssen angemeldet sein, um Ihre privaten Turniere zu sehen.'>
<div className="main generic-fullpage-bg">
<Head>
<title>Private Turniere: turnie.re</title>
</Head>
<TurniereNavigation/>
<PrivateTournamentsPageContent/>
<PrivateTournamentsPageContent isSignedIn={this.props.isSignedIn}/>
<Footer/>
</div>
</Option>
<Option condition={true}>
<div className="main generic-fullpage-bg">
<Head>
<title>Anmeldung</title>
</Head>
<TurniereNavigation/>
<div>
<Login
hint="Sie müssen angemeldet sein, um diesen Inhalt anzuzeigen!"/>
</div>
<Footer/>
</div>
</Option>
</UserRestrictor>);
</RequireLogin>);
}
}
@ -53,13 +35,14 @@ const PrivateTournamentListPage = connect(mapStateToProperties)(PrivateTournamen
export default PrivateTournamentListPage;
function PrivateTournamentsPageContent() {
function PrivateTournamentsPageContent(props) {
return (<div>
<Container className="pt-5">
<PrivateTournamentsCard/>
</Container>
<Container className="pb-5 pt-3">
<a href='/list' className="btn btn-success shadow">zu den öffentlichen Turnieren</a>
<a href='/list' className="btn btn-primary shadow">zu den öffentlichen Turnieren</a>
{props.isSignedIn && <a href='/create' className="ml-3 btn btn-success shadow">neues Turnier erstellen</a>}
</Container>
</div>);
}

100
pages/profile.js Normal file
View File

@ -0,0 +1,100 @@
import Head from 'next/head';
import React, {Component} from 'react';
import {Button, Container, Form, Input, InputGroup, InputGroupAddon, Table} from 'reactstrap';
import {TurniereNavigation} from '../js/components/Navigation';
import {BigImage} from '../js/components/BigImage';
import {Footer} from '../js/components/Footer';
import 'bootstrap/dist/css/bootstrap.min.css';
import '../static/css/everypage.css';
import '../static/css/profile.css';
import {connect} from 'react-redux';
import {changeMail} from '../js/api';
import {notify} from 'react-notify-toast';
import RequireLogin from '../js/components/RequireLogin';
function ContentContainer(props) {
return (<Container className="pb-5">
<UserData name={props.name} email={props.email}/>
<h3 className='custom-font mt-5'>E-Mail-Adresse ändern</h3>
<NewMailAddressInput email={props.email}/>
</Container>);
}
export default class ProfilePage extends React.Component {
render() {
return (<RequireLogin loginMessage='Sie müssen angemeldet sein, um Ihr Profil einzusehen.'>
<Head>
<title>Profil: turnie.re</title>
</Head>
<TurniereNavigation/>
<BigImage text="turnie.re-Account"/>
<div className='main'>
<Content/>
</div>
<Footer/>
</RequireLogin>);
}
}
const Content = connect(state => {
return {email: state.userinfo.uid, name: state.userinfo.username};
})(ContentContainer);
function UserData(props) {
return (<Table>
<tbody>
<tr>
<th className='w-small'>Name</th>
<td className='w-100'>{props.name}</td>
</tr>
<tr>
<th className='w-small'>E-Mail-Adresse</th>
<td className='mw-100'>{props.email}</td>
</tr>
</tbody>
</Table>);
}
class NewMailAddressInput extends Component {
constructor(props) {
super(props);
this.state = {email: ''};
this.submit = this.submit.bind(this);
this.onChange = this.onChange.bind(this);
this.onSubmitSuccess = this.onSubmitSuccess.bind(this);
NewMailAddressInput.onSubmitError = NewMailAddressInput.onSubmitError.bind(this);
}
submit(event) {
event.preventDefault();
changeMail(this.state.email, this.onSubmitSuccess, NewMailAddressInput.onSubmitError);
}
onSubmitSuccess() {
this.setState({email: ''});
notify.show('E-Mail-Adresse geändert.', 'success', 2500);
}
static onSubmitError() {
notify.show('Die E-Mail-Adresse konnte nicht geändert werden.', 'error', 3000);
}
onChange(input) {
this.setState({email: input.target.value});
}
render() {
return (<Form onSubmit={this.submit}>
<InputGroup>
<Input type='email' placeholder={this.props.email} onChange={this.onChange} value={this.state.email}
required/>
<InputGroupAddon addonType='append'>
<Button color='primary' type='submit'>eintragen</Button>
</InputGroupAddon>
</InputGroup>
</Form>);
}
}

View File

@ -1,8 +1,12 @@
import Head from 'next/head';
import React from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {notify} from 'react-notify-toast';
import Router from 'next/router';
import {
Button, Card, CardBody, Container, Form, FormGroup, FormText, Input, Label
Button, Card, CardBody, Container, Form, FormGroup, FormText, Input, Label,
Modal, ModalHeader, ModalBody, ModalFooter
} from 'reactstrap';
import {TurniereNavigation} from '../js/components/Navigation';
@ -69,12 +73,15 @@ class RegisterForm extends React.Component {
super(props);
this.state = {
username: '', email: '', password: ''
username: '', email: '', password: '',
showRegisterSuccessModal: false, code: -1
};
}
render() {
return (<Form>
<RegisterSuccessModal isOpen={this.state.showRegisterSuccessModal}
close={() => this.closeRegisterSuccessModal()}/>
<FormGroup>
<Label for="username">Benutzername</Label>
<Input name="username" value={this.state.username} onChange={this.handleUsernameInput.bind(this)}/>
@ -95,12 +102,30 @@ class RegisterForm extends React.Component {
<FormText className="mb-2 mt-4">
Du akzeptierst die <a href="/privacy">Datenschutzbestimmungen</a>, wenn du auf Registrieren klickst.
</FormText>
<Button onClick={register.bind(this, this.state.username, this.state.email, this.state.password)}
<Button onClick={() => this.registerUser()}
color="success" size="lg" className="w-100 shadow-sm">Registrieren</Button>
<VisibleRegisterErrorList/>
</Form>);
}
showRegisterSuccessModal() {
this.setState({
showRegisterSuccessModal: true
});
}
closeRegisterSuccessModal() {
Router.push('/');
}
registerUser() {
register(this.state.username, this.state.email, this.state.password, () => {
this.showRegisterSuccessModal();
}, () => {
notify.show('Sie konnten nicht registriert werden.', 'warning', 5000);
});
}
handlePasswordInput(input) {
this.setState({password: input.target.value});
}
@ -114,6 +139,26 @@ class RegisterForm extends React.Component {
}
}
class RegisterSuccessModal extends React.Component {
render() {
return (<Modal isOpen={this.props.isOpen} toggle={this.props.close}>
<ModalHeader toggle={this.props.close}>Erfolgreich registriert</ModalHeader>
<ModalBody>
Sie wurden erfolgreich registriert. Um Ihren Account nutzen zu können
müssen Sie den Verifizierungslink, den wir Ihnen per E-Mail zugesandt haben, aufrufen.
</ModalBody>
<ModalFooter>
<Button color='secondary' onClick={this.props.close}>Verstanden</Button>
</ModalFooter>
</Modal>);
}
}
RegisterSuccessModal.proptypes = {
isOpen: PropTypes.bool.isRequired,
close: PropTypes.func.isRequired
};
function AccountRequirementMarketing() {
return (<Container>
<Card id="account-requirement">

View File

@ -1,39 +1,74 @@
import Head from 'next/head';
import React from 'react';
import {connect} from 'react-redux';
import {Col, Container, Row} from 'reactstrap';
import {Container, ListGroup, ListGroupItem} from 'reactstrap';
import Navbar from 'react-bootstrap/Navbar';
import {ErrorPageComponent} from '../js/components/ErrorComponents';
import {Footer} from '../js/components/Footer';
import {TurniereNavigation} from '../js/components/Navigation';
import {BigImage} from '../js/components/BigImage';
import {TournamentInformationView} from '../js/components/TournamentInformationView';
import {getState} from '../js/api';
import {getRequest} from '../js/redux/backendApi';
import 'bootstrap/dist/css/bootstrap.min.css';
import '../static/css/everypage.css';
import '../static/css/tournament.css';
import {Match} from '../js/components/Match';
import {getTournament} from '../js/redux/tournamentApi';
import {PlayoffStages} from '../js/components/PlayoffStages';
import GroupStage from '../js/components/GroupStage';
class PrivateTournamentPage extends React.Component {
render() {
const {ownerUsername, playoffStages} = this.props.tournament;
const {ownerUsername, playoffStages, groupStage} = this.props.tournament;
const {isSignedIn, username} = this.props;
const isOwner = username === ownerUsername;
// TODO: Change href-prop of the anchor tag to contain the tournament code
return (<div className='pb-5'>
<TournamentInformationView tournament={this.props.tournament} currentpage='tournament'/>
<div className='stages pt-5'>
{playoffStages.map(stage => <Stage isSignedIn={isSignedIn} isOwner={username === ownerUsername}
level={getLevelName(stage.level)} matches={stage.matches}
key={stage.level}/>)}
<TournamentBigImage {...this.props.tournament}/>
<StatusBar tournament={this.props.tournament} isOwner={isOwner} isSignedIn={isSignedIn}/>
<div className='stages'>
{groupStage != null &&
<div><GroupStage groups={groupStage.groups} isSignedIn={isSignedIn} isOwner={isOwner}
showMatches={playoffStages !== null}/></div>}
<PlayoffStages playoffStages={playoffStages} isSignedIn={isSignedIn}
isOwner={isOwner}/>
</div>
</div>);
}
}
function StatusBar(props) {
return (<Navbar sticky='top' bg='light' className='border-bottom border-top'>
<Container className='px-3'>
<Navbar.Brand>
{props.tournament.name}
<EditButton id={props.id} isOwner={props.isOwner} isSignedIn={props.isSignedIn}/>
</Navbar.Brand>
</Container>
</Navbar>);
}
function TournamentBigImage(props) {
return (<div className="big-image mb-0">
<h1 className="display-1">{props.name}</h1>
<Container>
<TournamentProperties {...props}/>
</Container>
</div>);
}
function TournamentProperties(props) {
return (<ListGroup className='text-dark text-left shadow'>
{props.description && <ListGroupItem>{props.description}</ListGroupItem>}
<ListGroupItem>
{props.isPublic ? 'Das Turnier ist öffentlich.' : 'Das Turnier ist privat.'}
</ListGroupItem>
<ListGroupItem>Turnier-Code: <b>{props.code}</b></ListGroupItem>
<ListGroupItem>von <b>{props.ownerUsername}</b></ListGroupItem>
</ListGroup>);
}
function mapStateToTournamentPageProperties(state) {
const {isSignedIn, username} = state.userinfo;
return {isSignedIn, username};
@ -41,108 +76,18 @@ function mapStateToTournamentPageProperties(state) {
const TournamentPage = connect(mapStateToTournamentPageProperties)(PrivateTournamentPage);
function getLevelName(levelNumber) {
const names = ['Finale', 'Halbfinale', 'Viertelfinale', 'Achtelfinale'];
if (levelNumber < names.length) {
return names[levelNumber];
function EditButton(props) {
const {id, isOwner, isSignedIn} = props;
if (isSignedIn && isOwner) {
return (<a href={'/t/' + id + '/edit'} className='ml-3 btn btn-outline-secondary default-font-family'>
Turnier bearbeiten
</a>);
} else {
return Math.pow(2, levelNumber) + 'tel-Finale';
return null;
}
}
function Stage(props) {
const {isSignedIn, isOwner} = props;
return (<div>
<Container className='py-5'>
<h1 className='custom-font'>{props.level}</h1>
<Row>
{props.matches.map((match => (
<Col className='minw-25' key={match.id}><Match match={match} isSignedIn={isSignedIn}
isOwner={isOwner}/></Col>)))}
</Row>
</Container>
</div>);
}
function convertTournament(apiTournament) {
let groupStage = null;
const playoffStages = [];
for (const stage of apiTournament.stages) {
if (stage.groups.length > 0) {
// group stage
groupStage = {groups: stage.groups.map(group => convertGroup(group))};
} else {
// playoff stage
playoffStages.push({
id: stage.id, level: stage.level, matches: stage.matches.map(match => convertMatch(match))
});
}
}
return {
id: apiTournament.id,
code: apiTournament.code,
description: apiTournament.description,
name: apiTournament.name,
isPublic: apiTournament.public,
ownerUsername: apiTournament.owner_username,
groupStage: groupStage,
playoffStages: playoffStages
};
}
function convertGroup(apiGroup) {
return {
id: apiGroup.id,
number: apiGroup.number,
scores: apiGroup.group_scores,
matches: apiGroup.matches.map(match => convertMatch(match))
};
}
function convertMatch(apiMatch) {
const result = {
id: apiMatch.id, state: apiMatch.state, winnerTeamId: apiMatch.winner === null ? null : apiMatch.winner.id
};
if (apiMatch.match_scores.length === 2) {
result.team1 = {
name: apiMatch.match_scores[0].team.name,
id: apiMatch.match_scores[0].team.id,
score: apiMatch.match_scores[0].points
};
result.team2 = {
name: apiMatch.match_scores[1].team.name,
id: apiMatch.match_scores[1].team.id,
score: apiMatch.match_scores[1].points
};
} else if (apiMatch.match_scores.length === 1) {
result.team1 = {
name: apiMatch.match_scores[0].team.name,
id: apiMatch.match_scores[0].team.id,
score: apiMatch.match_scores[0].points
};
result.team2 = {
name: 'TBD',
id: null,
score: 0
};
} else {
result.team1 = {
name: 'TBD',
id: null,
score: 0
};
result.team2 = {
name: 'TBD',
id: null,
score: 0
};
}
return result;
}
class Main extends React.Component {
static async getInitialProps({query}) {
return {query};
@ -154,22 +99,24 @@ class Main extends React.Component {
this.state = {
tournament: null
};
this.onTournamentRequestSuccess = this.onTournamentRequestSuccess.bind(this);
this.onTournamentRequestError = this.onTournamentRequestError.bind(this);
}
componentDidMount() {
const code = this.props.query.code;
getTournament(this.props.query.code, this.onTournamentRequestSuccess, this.onTournamentRequestError);
}
getRequest(getState(), '/tournaments/' + code)
.then(response => {
this.setState({status: response.status, tournament: convertTournament(response.data)});
})
.catch(err => {
if (err.response) {
this.setState({status: err.response.status});
onTournamentRequestSuccess(requestStatus, tournament) {
this.setState({status: requestStatus, tournament: tournament});
}
onTournamentRequestError(error) {
if (error.response) {
this.setState({status: error.response.status});
} else {
this.setState({status: -1});
}
});
}
@ -184,7 +131,6 @@ class Main extends React.Component {
<title>{tournamentName}: turnie.re</title>
</Head>
<TurniereNavigation/>
<BigImage text={tournamentName}/>
<TournamentPage tournament={tournament}/>
<Footer/>
</div>);

View File

@ -7,6 +7,10 @@
font-family: Halt, sans-serif;
}
.default-font-family {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.navbar-brand {
font-family: Halt, sans-serif;
font-size: 2em;

View File

@ -0,0 +1,4 @@
.btn-width {
width: 48px;
}

3
static/css/profile.css Normal file
View File

@ -0,0 +1,3 @@
.w-small {
min-width: 10em;
}

View File

@ -6,7 +6,7 @@
text-decoration: line-through;
}
.stages > div:nth-child(odd) {
.stages > div > div:nth-child(odd) {
background-color: #f8f8f8;
}

1299
yarn.lock

File diff suppressed because it is too large Load Diff