diff --git a/README.md b/README.md index 83eb16d..15292d3 100644 --- a/README.md +++ b/README.md @@ -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 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 ( + + + + + + + + + + {props.scores.map(groupScore => )} + +
TeamPunkteerzieltkassiert
); +} + + +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 ( -