diff --git a/js/api.js b/js/api.js index ad1d6df..6112c0d 100644 --- a/js/api.js +++ b/js/api.js @@ -8,6 +8,11 @@ import {actionTypesUserinfo, defaultStateUserinfo} from './redux/userInfo'; import {actionTypesTournamentinfo, defaultStateTournamentinfo} from './redux/tournamentInfo'; import {actionTypesTournamentlist, defaultStateTournamentlist} from './redux/tournamentList'; import {deleteRequest, getRequest, patchRequest, postRequest, putRequest} from './redux/backendApi'; +import { + actionTypesTournamentStatistics, + defaultStateTournamentStatistics, + transformTournamentInfoToStatistics, transformTournamentStatsToStatistics +} from './redux/tournamentStatistics'; function storeOptionalToken(response) { @@ -319,16 +324,64 @@ const reducerTournamentlist = (state = defaultStateTournamentlist, 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 reducers = { userinfo: reducerUserinfo, tournamentinfo: reducerTournamentinfo, - tournamentlist: reducerTournamentlist + tournamentlist: reducerTournamentlist, + tournamentStatistics: reducerTournamentStatistics }; const defaultApplicationState = { userinfo: defaultStateUserinfo, tournamentinfo: defaultStateTournamentinfo, - tournamentlist: defaultStateTournamentlist + tournamentlist: defaultStateTournamentlist, + tournamentStatistics: defaultStateTournamentStatistics }; let __store; @@ -499,6 +552,18 @@ export function requestTournamentList(type, 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() + }); +} + function rehydrateApplicationState() { const persistedState = localStorage.getItem('reduxState') ? JSON.parse(localStorage.getItem('reduxState')) : @@ -517,6 +582,10 @@ function rehydrateApplicationState() { type: actionTypesTournamentlist.REHYDRATE, parameters: Object.assign({}, persistedState.tournamentlist) }); + __store.dispatch({ + type: actionTypesTournamentStatistics.REHYDRATE, + parameters: Object.assign({}, persistedState.tournamentstatistics) + }); } applicationHydrated = true; } diff --git a/js/components/DominanceShower.js b/js/components/DominanceShower.js new file mode 100644 index 0000000..5861858 --- /dev/null +++ b/js/components/DominanceShower.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { + Card, + CardBody, + CardTitle, + Table +} from 'reactstrap'; + +export class DominanceShower extends React.Component { + render() { + return ( + + + {this.props.title} + + + + + + + + + + + + + + +
{this.props.stats.team_name}
{this.props.stats.points_made}{this.props.stats.points_received}
Punkte erzieltPunkte kassiert
+
+
+ ); + } +} diff --git a/js/components/StandingsTable.js b/js/components/StandingsTable.js new file mode 100644 index 0000000..c82d8ad --- /dev/null +++ b/js/components/StandingsTable.js @@ -0,0 +1,96 @@ +import React from 'react'; +import { + Button, + Card, + CardBody, + Collapse, + Table +} from 'reactstrap'; + +import {rangedmap} from '../utils/rangedmap'; + +export class StandingsTable extends React.Component { + constructor(props) { + super(props); + + this.state = { + showFullTable: false + }; + this.toggleShowFullTable = this.toggleShowFullTable.bind(this); + } + + render() { + const performances = this.props.data.group_phase_performances; + + return ( + + +

Aktuelle Rangliste

+ + + + + + + + + + + { rangedmap(performances, (team, index) => ( + + ), 0, 3) } + + + { rangedmap(performances, (team, index) => ( + + ), 3) } + + + + + + +
#Team NameMatch DifferenzPunkt Differenz
+ +
+
+
+ ); + } + + toggleShowFullTable() { + this.setState({showFullTable: !this.state.showFullTable}); + } +} + +class TeamRow extends React.Component { + constructor(props) { + super(props); + } + + render() { + return ( + + { this.props.teamToShow.rank } + { this.props.teamToShow.team_name } + { this.props.teamToShow.win_loss_differential } + { this.props.teamToShow.point_differential } + + ); + } +} + +class TableButton extends React.Component { + render() { + const {isFullTableShown} = this.props; + + if (isFullTableShown) { + return ; + } else { + return ; + } + } +} diff --git a/js/components/TournamentBigImage.js b/js/components/TournamentBigImage.js new file mode 100644 index 0000000..ed304f0 --- /dev/null +++ b/js/components/TournamentBigImage.js @@ -0,0 +1,22 @@ +import {Container, ListGroup, ListGroupItem} from 'reactstrap'; +import React from 'react'; + +export function TournamentBigImage(props) { + return (
+

{props.name}

+ + + +
); +} + +function TournamentProperties(props) { + return ( + {props.description && {props.description}} + + {props.isPublic ? 'Das Turnier ist öffentlich.' : 'Das Turnier ist privat.'} + + Turnier-Code: {props.code} + von {props.ownerUsername} + ); +} diff --git a/js/components/TournamentStatusBar.js b/js/components/TournamentStatusBar.js new file mode 100644 index 0000000..0fd018a --- /dev/null +++ b/js/components/TournamentStatusBar.js @@ -0,0 +1,29 @@ +import Navbar from 'react-bootstrap/Navbar'; +import {Container} from 'reactstrap'; +import React from 'react'; + +export function TournamentStatusBar(props) { + return ( + + {props.children} + + ); +} + +export function TournamentStatusBarButton(props) { + return ( + {props.children} + ); +} + +export function EditButton(props) { + const {tournamentId, isOwner, isSignedIn} = props; + + if (isSignedIn && isOwner) { + return ( + Turnier bearbeiten + ); + } else { + return null; + } +} diff --git a/js/redux/tournamentStatistics.js b/js/redux/tournamentStatistics.js new file mode 100644 index 0000000..4004982 --- /dev/null +++ b/js/redux/tournamentStatistics.js @@ -0,0 +1,81 @@ +export const actionTypesTournamentStatistics = { + 'REQUEST_TOURNAMENT_STATISTICS': 'REQUEST_TOURNAMENT_STATISTICS', + 'INT_REQUEST_TOURNAMENT_STATISTICS': 'INT_REQUEST_TOURNAMENT_STATISTICS', + + 'REQUEST_TOURNAMENT_STATISTICS_SUCCESS': 'REQUEST_TOURNAMENT_STATISTICS_SUCCESS', + + 'REHYDRATE': 'TOURNAMENTINFO_REHYDRATE', + 'CLEAR': 'TOURNAMENTINFO_CLEAR' +}; + +export const defaultStateTournamentStatistics = { + code: '', + description: '', + id: -1, + name: '', + owner_username: '', + isPublic: '', + + statistics_available: false, + + most_dominant_team: {}, + least_dominant_team: {}, + group_phase_performances: [] +}; + + +export function transformTournamentInfoToStatistics(data) { + return { + code: data.code, + description: data.description, + id: data.id, + name: data.name, + ownerUsername: data.owner_username, + isPublic: data.public + }; +} + +export function transformTournamentStatsToStatistics(data) { + if (statisticsUnavailable(data)) { + return { + statistics_available: false, + most_dominant_team: {}, + least_dominant_team: {}, + group_phase_performances: [] + }; + } + + const statistics = { + statistics_available: true, + most_dominant_team: { + points_made: data.most_dominant_score.scored_points, + points_received: data.most_dominant_score.received_points, + team_name: data.most_dominant_score.team.name + }, + least_dominant_team: { + points_made: data.least_dominant_score.scored_points, + points_received: data.least_dominant_score.received_points, + team_name: data.least_dominant_score.team.name + }, + group_phase_performances: [] + }; + + for (let i = 0; i < data.group_scores.length; i++) { + const score = data.group_scores[i]; + + statistics.group_phase_performances[i] = { + win_loss_differential: score.group_points, + point_differential: score.scored_points - score.received_points, + rank: i + 1, + team_name: score.team.name + }; + } + + return statistics; +} + +function statisticsUnavailable(data) { + return data === {} || data.most_dominant_score === null || + data.least_dominant_score === null || data.group_scores === []; +} + diff --git a/js/utils/rangedmap.js b/js/utils/rangedmap.js new file mode 100644 index 0000000..adedcb6 --- /dev/null +++ b/js/utils/rangedmap.js @@ -0,0 +1,4 @@ + +export function rangedmap(arr, func, start, end) { + return arr.slice(start, end).map((element, index) => func(element, start + index)); +} diff --git a/pages/_app.js b/pages/_app.js index ac91bd6..6bc4dcb 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -15,8 +15,8 @@ class TurniereApp extends App { render() { const {Component, pageProps, reduxStore} = this.props; return ( - - + + diff --git a/pages/tournament-statistics.js b/pages/tournament-statistics.js new file mode 100644 index 0000000..0beb23b --- /dev/null +++ b/pages/tournament-statistics.js @@ -0,0 +1,88 @@ +import Head from 'next/head'; +import React from 'react'; +import {connect} from 'react-redux'; +import {Col, Container, Row} from 'reactstrap'; + +import {TurniereNavigation} from '../js/components/Navigation'; +import {StandingsTable} from '../js/components/StandingsTable'; +import {DominanceShower} from '../js/components/DominanceShower'; +import {Footer} from '../js/components/Footer'; +import {requestTournamentStatistics} from '../js/api'; +import {EditButton, TournamentStatusBar, TournamentStatusBarButton} from '../js/components/TournamentStatusBar'; +import Navbar from 'react-bootstrap/Navbar'; +import {TournamentBigImage} from '../js/components/TournamentBigImage'; + +class StatisticsTournamentPage extends React.Component { + static async getInitialProps({query}) { + return {query}; + } + + componentDidMount() { + requestTournamentStatistics(this.props.query.code, () => {}, () => {}); + } + + render() { + const {tournamentStatistics} = this.props; + + return ( +
+ + {tournamentStatistics.name}: turnie.re + + + + + + {tournamentStatistics.name} + + + zurück zum Turnier + + + +
+ +
+
+
+ ); + } +} + +function StatisticsView(props) { + if (props.tournamentStatistics.statistics_available) { + return (
+ + + + + + + + + + + + + +
); + } else { + return ( +

Statistiken sind für dieses Turnier leider nicht verfügbar.

+
); + } +} + +function mapTournamentStatisticsToProps(state) { + const {tournamentStatistics} = state; + const {isSignedIn, username} = state.userinfo; + return {tournamentStatistics, isSignedIn, username}; +} + +export default connect( + mapTournamentStatisticsToProps +)(StatisticsTournamentPage); diff --git a/pages/tournament.js b/pages/tournament.js index 5496d38..341adfe 100644 --- a/pages/tournament.js +++ b/pages/tournament.js @@ -1,7 +1,6 @@ import Head from 'next/head'; import React from 'react'; import {connect} from 'react-redux'; -import {Container, ListGroup, ListGroupItem} from 'reactstrap'; import Navbar from 'react-bootstrap/Navbar'; @@ -16,6 +15,8 @@ import '../static/css/tournament.css'; import {getTournament} from '../js/redux/tournamentApi'; import {PlayoffStages} from '../js/components/PlayoffStages'; import GroupStage from '../js/components/GroupStage'; +import {TournamentBigImage} from '../js/components/TournamentBigImage'; +import {EditButton, TournamentStatusBar, TournamentStatusBarButton} from '../js/components/TournamentStatusBar'; class PrivateTournamentPage extends React.Component { render() { @@ -38,36 +39,21 @@ class PrivateTournamentPage extends React.Component { } function StatusBar(props) { - return ( - - - {props.tournament.name} - - - - ); + return ( + + {props.tournament.name} + + + + ); } - -function TournamentBigImage(props) { - return (
-

{props.name}

- - - -
); +function StatisticsButton(props) { + return ( + Statistiken + ); } -function TournamentProperties(props) { - return ( - {props.description && {props.description}} - - {props.isPublic ? 'Das Turnier ist öffentlich.' : 'Das Turnier ist privat.'} - - Turnier-Code: {props.code} - von {props.ownerUsername} - ); -} function mapStateToTournamentPageProperties(state) { const {isSignedIn, username} = state.userinfo; @@ -76,18 +62,6 @@ function mapStateToTournamentPageProperties(state) { const TournamentPage = connect(mapStateToTournamentPageProperties)(PrivateTournamentPage); -function EditButton(props) { - const {id, isOwner, isSignedIn} = props; - - if (isSignedIn && isOwner) { - return ( - Turnier bearbeiten - ); - } else { - return null; - } -} - class Main extends React.Component { static async getInitialProps({query}) { return {query}; diff --git a/server.js b/server.js index 6c90888..8772603 100644 --- a/server.js +++ b/server.js @@ -27,6 +27,12 @@ app.prepare() app.render(req, res, actualPage, queryParam); }); + server.get('/t/:code/statistics', (req, res) => { + const actualPage = '/tournament-statistics'; + const queryParam = {code: req.params.code}; + app.render(req, res, actualPage, queryParam); + }); + server.get('*', (req, res) => { return handle(req, res); });