diff --git a/README.md b/README.md
index 83eb16d..15292d3 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,9 @@
[](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
diff --git a/js/api.js b/js/api.js
index 7a81e2d..ad1d6df 100644
--- a/js/api.js
+++ b/js/api.js
@@ -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;
}
diff --git a/js/components/EditableMatchTable.js b/js/components/EditableMatchTable.js
new file mode 100644
index 0000000..2589354
--- /dev/null
+++ b/js/components/EditableMatchTable.js
@@ -0,0 +1,60 @@
+import React from 'react';
+import {Button, Input, InputGroup, InputGroupAddon, Table} from 'reactstrap';
+
+export function EditableMatchTable(props) {
+ return (
+
+
+ |
+
+ |
+ {props.match.team1.name} |
+
+
+ |
+
+ |
+ {props.match.team2.name} |
+
+
+
);
+}
+
+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 (
+
+
+
+ );
+ }
+}
diff --git a/js/components/GroupStage.js b/js/components/GroupStage.js
new file mode 100644
index 0000000..76f371e
--- /dev/null
+++ b/js/components/GroupStage.js
@@ -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 (
+
+ Gruppenphase
+
+
+
+ {this.props.groups.map(group => )}
+
+ );
+ }
+}
+
+function ShowMatchesToggleButton(props) {
+ return ();
+}
+
+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 (
+
+
+ Gruppe {this.state.number}
+
+ {this.state.matches.map((match => (
+ )))}
+
+
+
+
+ );
+ }
+}
+
+function GroupScoresTable(props) {
+ return (
+
+
+ | Team |
+ Punkte |
+ erzielt |
+ kassiert |
+
+
+
+ {props.scores.map(groupScore => )}
+
+
);
+}
+
+
+function GroupScoresTableRow(props) {
+ return (
+ | {props.score.team.name} |
+ {props.score.group_points} |
+ {props.score.received_points} |
+ {props.score.scored_points} |
+
);
+}
diff --git a/js/components/Match.js b/js/components/Match.js
index ebf91a7..bded548 100644
--- a/js/components/Match.js
+++ b/js/components/Match.js
@@ -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 {
{smallMessage}
+ startMatch={this.startMatch} endMatch={this.endMatch} changeScores={this.changeScores}/>
);
}
}
-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 = ;
- 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 = ;
- break;
- }
- return (
- {title}
-
- {props.matchState === 'in_progress' ? :
- }
-
-
- {actionButton}
-
-
- );
-}
-
-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 (
-
-
- | {props.match.team1.name} |
-
-
- | kein Gegner |
-
-
-
);
- } else {
- return (
-
-
- | {props.match.team1.score} |
- {props.match.team1.name} |
-
-
- | {props.match.team2.score} |
- {props.match.team2.name} |
-
-
-
);
- }
-}
-
-function EditableMatchTable(props) {
- return (
-
-
- |
-
- |
- {props.match.team1.name} |
-
-
- |
-
- |
- {props.match.team2.name} |
-
-
-
);
-}
-
-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 (
-
-
-
- );
- }
-}
diff --git a/js/components/MatchModal.js b/js/components/MatchModal.js
new file mode 100644
index 0000000..824a8fe
--- /dev/null
+++ b/js/components/MatchModal.js
@@ -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 = ;
+ // possible states: single_team not_ready not_started in_progress finished
+ switch (this.props.match.state) {
+ case 'in_progress':
+ title = 'Spiel läuft';
+ submitScoresButton = ;
+ if (!this.props.match.allowUndecided && this.props.match.team1.score === this.props.match.team2.score) {
+ actionButton = ;
+ } else {
+ actionButton = ;
+ }
+ matchTable = ;
+ 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 = ;
+ break;
+ }
+ return (
+ {title}
+
+ {matchTable}
+
+
+ {submitScoresButton}
+ {actionButton}
+
+
+ );
+ }
+}
diff --git a/js/components/MatchTable.js b/js/components/MatchTable.js
new file mode 100644
index 0000000..fda69d9
--- /dev/null
+++ b/js/components/MatchTable.js
@@ -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 (
+
+
+ | {props.match.team1.name} |
+
+
+ | kein Gegner |
+
+
+
);
+ } else {
+ return (
+
+
+ | {props.match.team1.score} |
+ {props.match.team1.name} |
+
+
+ | {props.match.team2.score} |
+ {props.match.team2.name} |
+
+
+
);
+ }
+}
diff --git a/js/components/NumericInput.js b/js/components/NumericInput.js
new file mode 100644
index 0000000..478f2cb
--- /dev/null
+++ b/js/components/NumericInput.js
@@ -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 (
+
+
+
+
+
+
+
+ );
+ }
+}
+
+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
+};
diff --git a/js/components/PlayoffStages.js b/js/components/PlayoffStages.js
new file mode 100644
index 0000000..79fe6b6
--- /dev/null
+++ b/js/components/PlayoffStages.js
@@ -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 (
+ {this.props.playoffStages.map(stage => this.updateNextStage(stage.id)}
+ level={getLevelName(stage.level)} matches={stage.matches}
+ key={stage.level}/>)}
+
);
+ }
+}
+
+function getLevelName(levelNumber) {
+ const names = ['Finale', 'Halbfinale', 'Viertelfinale', 'Achtelfinale'];
+ if (levelNumber < names.length) {
+ return names[levelNumber];
+ } else {
+ return Math.pow(2, levelNumber) + 'tel-Finale';
+ }
+}
diff --git a/js/components/RequireLogin.js b/js/components/RequireLogin.js
new file mode 100644
index 0000000..6cccf55
--- /dev/null
+++ b/js/components/RequireLogin.js
@@ -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 (
+
+
Anmeldung
+
+
+
+
+
+
+
);
+ }
+}
+
+export default connect(state => {
+ return {isSignedIn: state.userinfo.isSignedIn};
+})(RequireLogin);
diff --git a/js/components/Stage.js b/js/components/Stage.js
new file mode 100644
index 0000000..a91a328
--- /dev/null
+++ b/js/components/Stage.js
@@ -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 (
+
+ {props.level}
+
+ {props.matches.map((match => (
+ )))}
+
+
+
);
+}
diff --git a/js/components/WarningPopup.js b/js/components/WarningPopup.js
new file mode 100644
index 0000000..0c0152a
--- /dev/null
+++ b/js/components/WarningPopup.js
@@ -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 (
+
+ {this.props.text}
+
+ );
+ }
+}
+
+WarningPopup.propTypes = {
+ text: PropTypes.string.isRequired,
+ shown: PropTypes.bool.isRequired
+};
diff --git a/js/redux/backendApi.js b/js/redux/backendApi.js
index 2653edd..f052388 100644
--- a/js/redux/backendApi.js
+++ b/js/redux/backendApi.js
@@ -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 {
diff --git a/js/redux/tournamentApi.js b/js/redux/tournamentApi.js
new file mode 100644
index 0000000..84e1085
--- /dev/null
+++ b/js/redux/tournamentApi.js
@@ -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;
+}
diff --git a/js/redux/tournamentInfo.js b/js/redux/tournamentInfo.js
index f5aedbe..b93f325 100644
--- a/js/redux/tournamentInfo.js
+++ b/js/redux/tournamentInfo.js
@@ -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',
diff --git a/js/redux/userInfo.js b/js/redux/userInfo.js
index a8172b4..d3c87ed 100644
--- a/js/redux/userInfo.js
+++ b/js/redux/userInfo.js
@@ -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',
diff --git a/package.json b/package.json
index 144ee7c..00be583 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/pages/create.js b/pages/create.js
index 97a3755..50b56e8 100644
--- a/pages/create.js
+++ b/pages/create.js
@@ -2,51 +2,45 @@ 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 (
-