Merge remote-tracking branch 'origin/master' into ticket/TURNIERE-148
# Conflicts: # js/api.js # pages/tournament.js
This commit is contained in:
commit
3df9f44c92
|
|
@ -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
|
||||
|
||||
|
|
|
|||
149
js/api.js
149
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>);
|
||||
}
|
||||
|
|
@ -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>);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>);
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
175
pages/create.js
175
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 (<UserRestrictor>
|
||||
<Option condition={isSignedIn}>
|
||||
<div className="main generic-fullpage-bg">
|
||||
<Head>
|
||||
<title>Turnier erstellen: turnie.re</title>
|
||||
</Head>
|
||||
<TurniereNavigation/>
|
||||
<div>
|
||||
<CreateTournamentCard/>
|
||||
</div>
|
||||
<Footer/>
|
||||
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>
|
||||
</Head>
|
||||
<TurniereNavigation/>
|
||||
<div>
|
||||
<CreateTournamentCard/>
|
||||
</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>);
|
||||
<Footer/>
|
||||
</div>
|
||||
</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}/>
|
||||
weiterkommen?</Label>
|
||||
<Row>
|
||||
<Col xs="3">
|
||||
<NumericInput value={this.state.groupAdvance}
|
||||
incrementText="×2" incrementCallback={this.increaseGroupAdvance}
|
||||
decrementText="÷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;
|
||||
}
|
||||
this.setState({
|
||||
groupAdvance: newGroupAdvance
|
||||
});
|
||||
}
|
||||
|
||||
if (newSize <= this.state.groupAdvance) {
|
||||
decreaseGroupAdvance() {
|
||||
const newGroupAdvance = Math.floor(this.state.groupAdvance / 2);
|
||||
|
||||
if (newGroupAdvance >= 1) {
|
||||
this.setState({
|
||||
groupSize: newSize, groupAdvance: newSize - 1
|
||||
groupAdvance: newGroupAdvance
|
||||
});
|
||||
} else {
|
||||
this.setState({groupSize: newSize});
|
||||
}
|
||||
}
|
||||
|
||||
handleGroupAdvanceInput(input) {
|
||||
const newAdvance = input.target.value;
|
||||
increaseGroupSize() {
|
||||
this.setState({groupSize: this.state.groupSize + 1});
|
||||
}
|
||||
|
||||
if (newAdvance === undefined || newAdvance <= 0 ||
|
||||
newAdvance >= this.state.groupSize) {
|
||||
return;
|
||||
decreaseGroupSize() {
|
||||
const newGroupSize = this.state.groupSize - 1;
|
||||
|
||||
if (newGroupSize >= 3) {
|
||||
this.setState({groupSize: newGroupSize});
|
||||
}
|
||||
|
||||
this.setState({groupAdvance: newAdvance});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
<div className="main generic-fullpage-bg">
|
||||
<Head>
|
||||
<title>Private Turniere: turnie.re</title>
|
||||
</Head>
|
||||
<TurniereNavigation/>
|
||||
<PrivateTournamentsPageContent/>
|
||||
<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>);
|
||||
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 isSignedIn={this.props.isSignedIn}/>
|
||||
<Footer/>
|
||||
</div>
|
||||
</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>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>);
|
||||
}
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
} else {
|
||||
this.setState({status: -1});
|
||||
}
|
||||
});
|
||||
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>);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -67,4 +71,4 @@ footer {
|
|||
background: url("/static/images/tennis-blurred.jpg") no-repeat top;
|
||||
background-size: cover;
|
||||
min-height: 100vh;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
|
||||
.btn-width {
|
||||
width: 48px;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.w-small {
|
||||
min-width: 10em;
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.stages > div:nth-child(odd) {
|
||||
.stages > div > div:nth-child(odd) {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
|
|
@ -25,4 +25,4 @@
|
|||
|
||||
.scoreInput {
|
||||
width: 11rem;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue