From fed0516334054af8bcd98b095c320bf7413d26ca Mon Sep 17 00:00:00 2001 From: Felix Hamme Date: Fri, 14 Jun 2019 21:51:51 +0200 Subject: [PATCH 1/6] Refactoring: Move api call and conversion out of tournament.js --- js/redux/tournamentApi.js | 92 +++++++++++++++++++++++++++++++ pages/tournament.js | 111 ++++++-------------------------------- 2 files changed, 107 insertions(+), 96 deletions(-) create mode 100644 js/redux/tournamentApi.js diff --git a/js/redux/tournamentApi.js b/js/redux/tournamentApi.js new file mode 100644 index 0000000..1d7db68 --- /dev/null +++ b/js/redux/tournamentApi.js @@ -0,0 +1,92 @@ +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); +} + +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, false)) + }); + } + } + 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, 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/pages/tournament.js b/pages/tournament.js index bd0de1b..bbe01a0 100644 --- a/pages/tournament.js +++ b/pages/tournament.js @@ -7,14 +7,13 @@ 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 {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'; class PrivateTournamentPage extends React.Component { render() { @@ -84,88 +83,6 @@ function Stage(props) { ); } -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, false)) - }); - } - } - 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, 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; -} - class Main extends React.Component { static async getInitialProps({query}) { return {query}; @@ -177,22 +94,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}); + } } From 112e5b7a8174f25dde853d2a2b11785971474237 Mon Sep 17 00:00:00 2001 From: Felix Hamme Date: Fri, 14 Jun 2019 22:03:38 +0200 Subject: [PATCH 2/6] Refactoring: Move Stage, playoff stages out of tournament.js --- js/components/PlayoffStages.js | 18 ++++++++++++++++++ js/components/Stage.js | 18 ++++++++++++++++++ pages/tournament.js | 33 ++++----------------------------- static/css/tournament.css | 4 ++-- 4 files changed, 42 insertions(+), 31 deletions(-) create mode 100644 js/components/PlayoffStages.js create mode 100644 js/components/Stage.js diff --git a/js/components/PlayoffStages.js b/js/components/PlayoffStages.js new file mode 100644 index 0000000..5387bab --- /dev/null +++ b/js/components/PlayoffStages.js @@ -0,0 +1,18 @@ +import {Stage} from './Stage'; +import React from 'react'; + +export function PlayoffStages(props) { + return (
+ {props.playoffStages.map(stage => )} +
); +} + +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/Stage.js b/js/components/Stage.js new file mode 100644 index 0000000..32a128c --- /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} = props; + + return (
+ +

{props.level}

+ + {props.matches.map((match => ( + )))} + +
+
); +} diff --git a/pages/tournament.js b/pages/tournament.js index bbe01a0..23878be 100644 --- a/pages/tournament.js +++ b/pages/tournament.js @@ -1,7 +1,7 @@ import Head from 'next/head'; import React from 'react'; import {connect} from 'react-redux'; -import {Col, Container, ListGroup, ListGroupItem, Row} from 'reactstrap'; +import {Container, ListGroup, ListGroupItem} from 'reactstrap'; import {ErrorPageComponent} from '../js/components/ErrorComponents'; import {Footer} from '../js/components/Footer'; @@ -12,8 +12,8 @@ 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'; class PrivateTournamentPage extends React.Component { render() { @@ -34,9 +34,8 @@ class PrivateTournamentPage extends React.Component {
- {playoffStages.map(stage => )} +
); } @@ -59,30 +58,6 @@ function EditButton(props) { } } -function getLevelName(levelNumber) { - const names = ['Finale', 'Halbfinale', 'Viertelfinale', 'Achtelfinale']; - if (levelNumber < names.length) { - return names[levelNumber]; - } else { - return Math.pow(2, levelNumber) + 'tel-Finale'; - } -} - -function Stage(props) { - const {isSignedIn, isOwner} = props; - - return (
- -

{props.level}

- - {props.matches.map((match => ( - )))} - -
-
); -} - class Main extends React.Component { static async getInitialProps({query}) { return {query}; diff --git a/static/css/tournament.css b/static/css/tournament.css index b00bdef..63b476a 100644 --- a/static/css/tournament.css +++ b/static/css/tournament.css @@ -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; -} \ No newline at end of file +} From 5a9d543a011ea2da7f2527e6c63ba2a719458d77 Mon Sep 17 00:00:00 2001 From: Felix Hamme Date: Fri, 14 Jun 2019 22:23:58 +0200 Subject: [PATCH 3/6] Implement group stage --- js/components/GroupStage.js | 52 +++++++++++++++++++++++++++++++++++++ pages/tournament.js | 8 ++++-- 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 js/components/GroupStage.js diff --git a/js/components/GroupStage.js b/js/components/GroupStage.js new file mode 100644 index 0000000..d5da349 --- /dev/null +++ b/js/components/GroupStage.js @@ -0,0 +1,52 @@ +import {Card, CardBody, Col, Row, Table} from 'reactstrap'; +import {Match} from './Match'; +import React from 'react'; + +export default function GroupStage(props) { + return (
+

Gruppenphase

+ + {props.groups.map(group => )} + +
); +} + +function Group(props) { + return ( + + +

Gruppe {props.group.id + 1}

+ {props.group.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/pages/tournament.js b/pages/tournament.js index 23878be..f87cd5f 100644 --- a/pages/tournament.js +++ b/pages/tournament.js @@ -14,11 +14,13 @@ import '../static/css/everypage.css'; import '../static/css/tournament.css'; 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 {id, description, isPublic, code, ownerUsername, playoffStages} = this.props.tournament; + const {id, description, isPublic, code, 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 (
@@ -34,8 +36,10 @@ class PrivateTournamentPage extends React.Component {
+ {groupStage != null && +
} + isOwner={isOwner}/>
); } From 99ca99ea38699fdc2c445ae9e042f4d6065b39ad Mon Sep 17 00:00:00 2001 From: Felix Hamme Date: Fri, 14 Jun 2019 22:56:52 +0200 Subject: [PATCH 4/6] Add a collapse button for all matches in a group stage The button defaults to show if the playoff hasn't started, otherwise to hide. --- js/components/GroupStage.js | 47 +++++++++++++++++++++++++++---------- pages/tournament.js | 3 ++- static/css/everypage.css | 6 ++++- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/js/components/GroupStage.js b/js/components/GroupStage.js index d5da349..ffb7857 100644 --- a/js/components/GroupStage.js +++ b/js/components/GroupStage.js @@ -1,15 +1,36 @@ -import {Card, CardBody, Col, Row, Table} from 'reactstrap'; +import {Button, Card, CardBody, Col, Collapse, Row, Table} from 'reactstrap'; import {Match} from './Match'; -import React from 'react'; +import React, {Component} from 'react'; -export default function GroupStage(props) { - return (
-

Gruppenphase

- - {props.groups.map(group => )} - -
); +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 (); } function Group(props) { @@ -17,8 +38,10 @@ function Group(props) {

Gruppe {props.group.id + 1}

- {props.group.matches.map((match => ( - )))} + + {props.group.matches.map((match => ( + )))} +
diff --git a/pages/tournament.js b/pages/tournament.js index f87cd5f..fcffa8e 100644 --- a/pages/tournament.js +++ b/pages/tournament.js @@ -37,7 +37,8 @@ class PrivateTournamentPage extends React.Component {
{groupStage != null && -
} +
}
diff --git a/static/css/everypage.css b/static/css/everypage.css index 4943630..5c46239 100644 --- a/static/css/everypage.css +++ b/static/css/everypage.css @@ -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; -} \ No newline at end of file +} From 8185a7b4b562d29cae53e35ab8b83835918ba522 Mon Sep 17 00:00:00 2001 From: Felix Hamme Date: Sat, 15 Jun 2019 23:20:30 +0200 Subject: [PATCH 5/6] Update group after a match score changes --- js/components/GroupStage.js | 51 +++++++++++++++++++++++++++---------- js/components/Match.js | 1 + js/redux/tournamentApi.js | 8 ++++++ 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/js/components/GroupStage.js b/js/components/GroupStage.js index ffb7857..bd7f4cf 100644 --- a/js/components/GroupStage.js +++ b/js/components/GroupStage.js @@ -1,6 +1,8 @@ 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) { @@ -33,19 +35,42 @@ function ShowMatchesToggleButton(props) { ); } -function Group(props) { - return ( - - -

Gruppe {props.group.id + 1}

- - {props.group.matches.map((match => ( - )))} - - -
-
- ); +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.id + 1}

+ + {this.state.matches.map((match => ( + )))} + + +
+
+ ); + } } function GroupScoresTable(props) { diff --git a/js/components/Match.js b/js/components/Match.js index 95293d8..e9e01b8 100644 --- a/js/components/Match.js +++ b/js/components/Match.js @@ -70,6 +70,7 @@ export class Match extends React.Component { updatedMatch.team1.score = scoreTeam1; updatedMatch.team2.score = scoreTeam2; this.setState({match: updatedMatch}); + this.props.onChange !== undefined && this.props.onChange(); } getMatchFinishedMessage() { diff --git a/js/redux/tournamentApi.js b/js/redux/tournamentApi.js index 1d7db68..92e7d72 100644 --- a/js/redux/tournamentApi.js +++ b/js/redux/tournamentApi.js @@ -9,6 +9,14 @@ export function getTournament(code, successCallback, errorCallback) { .catch(errorCallback); } +export function getGroup(groupId, successCallback, errorCallback) { + getRequest(getState(), '/groups/' + groupId) + .then(response => { + successCallback(response.status, convertGroup(response.data)); + }) + .catch(errorCallback); +} + function convertTournament(apiTournament) { let groupStage = null; const playoffStages = []; From 815065099bf144c407bbc0aa881117f10a43302d Mon Sep 17 00:00:00 2001 From: Felix Hamme Date: Sat, 15 Jun 2019 23:22:10 +0200 Subject: [PATCH 6/6] Use the actual group number for group titles --- js/components/GroupStage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/components/GroupStage.js b/js/components/GroupStage.js index bd7f4cf..76f371e 100644 --- a/js/components/GroupStage.js +++ b/js/components/GroupStage.js @@ -60,7 +60,7 @@ class Group extends Component { return ( -

Gruppe {this.state.id + 1}

+

Gruppe {this.state.number}

{this.state.matches.map((match => (