diff --git a/js/components/GroupStage.js b/js/components/GroupStage.js index 7208f9f..2c9791e 100644 --- a/js/components/GroupStage.js +++ b/js/components/GroupStage.js @@ -16,9 +16,9 @@ export default class GroupStage extends Component { } render() { - return (
+ return (

- Gruppenphase + Gruppenphase

@@ -35,7 +35,7 @@ function ShowMatchesToggleButton(props) { ); } -class Group extends Component { +export class Group extends Component { constructor(props) { super(props); this.state = props.group; @@ -57,7 +57,7 @@ class Group extends Component { } render() { - return ( + return (

Gruppe {this.state.number}

@@ -78,9 +78,9 @@ function GroupScoresTable(props) { Team - Punkte - Becher geworfen - Becher kassiert + Pkt. + Gew. + Kas. @@ -94,7 +94,7 @@ function GroupScoresTableRow(props) { return ( {props.score.team.name} {props.score.group_points} - {props.score.received_points} {props.score.scored_points} + {props.score.received_points} ); } diff --git a/js/components/TournamentBigImage.js b/js/components/TournamentBigImage.js index a7609ed..1e74603 100644 --- a/js/components/TournamentBigImage.js +++ b/js/components/TournamentBigImage.js @@ -3,7 +3,7 @@ import React from 'react'; export function TournamentBigImage(props) { return (
-

{props.name}

+

{props.name}

diff --git a/js/redux/tournamentApi.js b/js/redux/tournamentApi.js index 84e1085..572e325 100644 --- a/js/redux/tournamentApi.js +++ b/js/redux/tournamentApi.js @@ -25,6 +25,27 @@ export function getStage(stageId, successCallback, errorCallback) { .catch(errorCallback); } +export function getTournamentMeta(tournamentId, successCallback, errorCallback) { + getRequest(getState(), '/tournaments/' + tournamentId + '?simple=true') + .then(response => { + successCallback(response.status, response.data); + }) + .catch(errorCallback); +} + +export function getTournamentMatches(tournamentId, successCallback, errorCallback, matchState=null) { + let matchFilter = ''; + if (matchState) { + matchFilter = '?state=' + matchState; + } + getRequest(getState(), '/tournaments/' + tournamentId + '/matches' + matchFilter) + .then(response => { + successCallback(response.status, response.data.sort((a, b) => a.position > b.position).map(match => convertMatch(match))); + }) + .catch(errorCallback); +} + + function convertTournament(apiTournament) { let groupStage = null; const playoffStages = []; @@ -51,7 +72,7 @@ function convertTournament(apiTournament) { function convertPlayoffStage(apiStage) { return { - id: apiStage.id, level: apiStage.level, matches: apiStage.matches.map(match => convertMatch(match, false)) + id: apiStage.id, level: apiStage.level, matches: apiStage.matches.sort((a, b) => a.position > b.position).map(match => convertMatch(match, false)) }; } @@ -59,8 +80,8 @@ function convertGroup(apiGroup) { return { id: apiGroup.id, number: apiGroup.number, - scores: apiGroup.group_scores, - matches: apiGroup.matches.map(match => convertMatch(match, true)) + scores: apiGroup.group_scores.sort((a, b) => b.group_points > a.group_points), + matches: apiGroup.matches.sort((a, b) => a.position > b.position).map(match => convertMatch(match, true)) }; } diff --git a/package.json b/package.json index d732053..2c46b03 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "eslint-config-google": "^0.14.0", "express": "^4.18.1", "next": "^12.1.6", + "qrcode.react": "^3.1.0", "react": "^18.1.0", "react-dom": "^18.1.0", "react-notify-toast": "^0.5.1", diff --git a/pages/tournament-fullscreen-groups.js b/pages/tournament-fullscreen-groups.js new file mode 100644 index 0000000..1b8f7b4 --- /dev/null +++ b/pages/tournament-fullscreen-groups.js @@ -0,0 +1,151 @@ +import Head from 'next/head'; +import React from 'react'; +import {ErrorPageComponent} from '../js/components/ErrorComponents'; +import {getTournament} from '../js/redux/tournamentApi'; +import { + Col, + Container, Navbar, NavbarBrand, NavItem, Row, Spinner +} from 'reactstrap'; +import {QRCodeSVG} from 'qrcode.react'; +import {Group} from '../js/components/GroupStage'; + + +function FullscreenPage(props) { + let logo; + if (props.showLogo) { + logo = +
+ +
+ ; + } else { + logo =
; + } + return (
+ + + {props.groups.map(group => )} + +
+ +
+ + {logo} +
+
+
); +} + +function FullscreenPageHeader(props) { + return ( + {props.title} + {props.page}/{props.maxPage} + ); +} + + +class Main extends React.Component { + static async getInitialProps({query}) { + return {query}; + } + + constructor(props) { + super(props); + this.groupsPerPage = 11; + this.pages = 2; + this.page = 0; + this.backgroundColors = ['#a8e6cf', '#dcedc1', '#ffd3b6']; + + this.state = { + groups: [], tournament: null, loadedTournament: false, loadingStatus: null, page: 0, + showLogo: false, backgroundColor: 'white' + }; + this.onTournamentRequestSuccess = this.onTournamentRequestSuccess.bind(this); + this.onTournamentRequestError = this.onTournamentRequestError.bind(this); + this.increasePage = this.increasePage.bind(this); + } + + componentDidMount() { + this.updateTournament(); + const intervalIdPage = setInterval(this.increasePage, 3000); + this.setState({intervalIdPage: intervalIdPage}); + } + + increasePage() { + if (this.page >= this.pages) { + this.page = 0; + } else { + this.page = this.page + 1; + } + this.updateTournament(); + } + + componentWillUnmount() { + clearInterval(this.state.intervalIdPage); + } + + updateTournament() { + getTournament(this.props.query.code, this.onTournamentRequestSuccess, this.onTournamentRequestError); + } + + onTournamentRequestSuccess(requestStatus, tournament) { + // filter groups by page + const groups = tournament.groupStage.groups; + const start = this.page * this.groupsPerPage; + const end = (this.page + 1) * this.groupsPerPage; + + this.setState({ + loadingStatus: requestStatus, tournament: tournament, + groups: groups.slice(start, end), loadedTournament: true, + page: this.page, showLogo: this.page == this.pages, + backgroundColor: this.backgroundColors[this.page] + }); + } + + onTournamentRequestError(error) { + if (error.response) { + this.setState({loadingStatus: error.response.status, loadedTournament: true}); + } else { + this.setState({loadingStatus: -1, loadedTournament: true}); + } + } + + render() { + const {groups, tournament, loadedTournament, loadingStatus, page, showLogo, backgroundColor} = this.state; + if (!loadedTournament) { + return (
+ + Vollbild-Ansicht: turnie.re + + + + lade Vollbild-Ansicht + +
); + } + if (loadingStatus === 200) { + return (
+ + {tournament.name}: turnie.re + + + +
); + } else { + return ; + } + } +} + +export default Main; diff --git a/pages/tournament-fullscreen.js b/pages/tournament-fullscreen.js index 3df8089..16204bf 100644 --- a/pages/tournament-fullscreen.js +++ b/pages/tournament-fullscreen.js @@ -1,20 +1,179 @@ import Head from 'next/head'; import React from 'react'; +import {ErrorPageComponent} from '../js/components/ErrorComponents'; +import {getTournamentMatches, getTournamentMeta} from '../js/redux/tournamentApi'; +import { + Col, Container, DropdownItem, DropdownMenu, DropdownToggle, Navbar, NavbarBrand, NavItem, Row, UncontrolledDropdown, + Spinner +} from 'reactstrap'; +import {Match} from '../js/components/Match'; -class FullscreenTournamentPage extends React.Component { + +function FullscreenPage(props) { + return (
+ + +
); +} + +function Matches(props) { + let matches; + if (props.matches == null) { + matches = (
+ + lade Matches +
); + } else if (props.matches.length === 0) { + matches = (
keine Matches
); + } else { + matches = ( + {props.matches.map(match => )} + ); + } + return (
+ {matches} +
); +} + +function FilterDropdown(props) { + return ( + Match-Filter: + + {props.selected.label} + + + {Object.keys(matchFilters).map(matchFilter => props.select(matchFilters[matchFilter])}> + {matchFilters[matchFilter].label} + )} + + ); +} + + +function FullscreenPageHeader(props) { + return ( + + {props.title} + + Turnier-Code: {props.code} + + ); +} + +const matchFilters = { + 'all': {backend: null, label: 'alle'}, + 'in_progress': {backend: 'in_progress', label: 'laufend'}, + 'upcoming': {backend: 'upcoming', label: 'kommend'}, + 'not_started': {backend: 'not_started', label: 'bereit zum Starten'}, + 'finished': {backend: 'finished', label: 'beendet'}, + 'single_team': {backend: 'single_team', label: 'ohne Gegner'}, + 'not_ready': {backend: 'not_ready', label: 'noch nicht festgelegt'} +}; + +class Main extends React.Component { static async getInitialProps({query}) { return {query}; } + constructor(props) { + super(props); + + this.state = { + tournamentMeta: null, matches: [], matchFilter: matchFilters.all, loadedMeta: false, loadedMatches: false + }; + this.onTournamentRequestSuccess = this.onTournamentRequestSuccess.bind(this); + this.onTournamentRequestError = this.onTournamentRequestError.bind(this); + this.onTournamentMatchesRequestSuccess = this.onTournamentMatchesRequestSuccess.bind(this); + this.onTournamentMatchesRequestError = this.onTournamentMatchesRequestError.bind(this); + this.updateMatches = this.updateMatches.bind(this); + this.selectFilter = this.selectFilter.bind(this); + } + + selectFilter(filter) { + this.setState({matchFilter: filter, loadedMatches: false}); + this.updateMatches(); + } + + componentDidMount() { + const tournamentId = this.props.query.code; + getTournamentMeta(tournamentId, this.onTournamentRequestSuccess, this.onTournamentRequestError); + this.updateMatches(); + const intervalId = setInterval(this.updateMatches, 3000); + this.setState({intervalId: intervalId}); + } + + componentWillUnmount() { + clearInterval(this.state.intervalId); + } + + updateMatches() { + const tournamentId = this.props.query.code; + getTournamentMatches(tournamentId, this.onTournamentMatchesRequestSuccess, this.onTournamentMatchesRequestError, + this.state.matchFilter.backend); + } + + + onTournamentRequestSuccess(requestStatus, tournament) { + this.setState({metaStatus: requestStatus, tournamentMeta: tournament, loadedMeta: true}); + } + + onTournamentRequestError(error) { + if (error.response) { + this.setState({metaStatus: error.response.status, loadedMeta: true}); + } else { + this.setState({metaStatus: -1, loadedMeta: true}); + } + } + + onTournamentMatchesRequestSuccess(requestStatus, matches) { + this.setState({matchesStatus: requestStatus, matches: matches, loadedMatches: true}); + } + + onTournamentMatchesRequestError(error) { + if (error.response) { + this.setState({matchesStatus: error.response.status, loadedMatches: true}); + } else { + this.setState({matchesStatus: -1, loadedMatches: true}); + } + } + + render() { - return (
- - Turnie.re - Turnieranzeige (Vollbild) - -

Turnieranzeige (Vollbild)

-

Code: {this.props.query.code}

-
); + const {metaStatus, matchesStatus, tournamentMeta, matches} = this.state; + const filter = { + selected: this.state.matchFilter, select: this.selectFilter + }; + if (!this.state.loadedMeta) { + return (
+ + Vollbild-Ansicht: turnie.re + + + + lade Vollbild-Ansicht + +
); + } + if (!this.state.loadedMatches) { + return (
+ + {tournamentMeta.name}: turnie.re + + +
); + } + if (metaStatus === 200 && matchesStatus === 200) { + return (
+ + {tournamentMeta.name}: turnie.re + + +
); + } else { + return ; + } } } -export default FullscreenTournamentPage; +export default Main; diff --git a/pages/tournament-statistics.js b/pages/tournament-statistics.js index d0ddf18..6f9682e 100644 --- a/pages/tournament-statistics.js +++ b/pages/tournament-statistics.js @@ -1,7 +1,7 @@ import Head from 'next/head'; import React from 'react'; import {connect} from 'react-redux'; -import {ButtonGroup, Col, Container, Row, NavbarBrand} from 'reactstrap'; +import {ButtonGroup, Col, Container, Row} from 'reactstrap'; import {TurniereNavigation} from '../js/components/Navigation'; import {StandingsTable} from '../js/components/StandingsTable'; @@ -44,6 +44,9 @@ class StatisticsTournamentPage extends React.Component { zurück zum Turnier + + Turnier-Vollbild-Ansicht +
diff --git a/pages/tournament.js b/pages/tournament.js index 333904a..b5b656c 100644 --- a/pages/tournament.js +++ b/pages/tournament.js @@ -41,6 +41,8 @@ function StatusBar(props) { + + Vollbild-Ansicht Gruppen ); } @@ -51,6 +53,12 @@ function StatisticsButton(props) { ); } +function FullscreenButton(props) { + return ( + Vollbild-Ansicht + ); +} + function mapStateToTournamentPageProperties(state) { const {isSignedIn, username} = state.userinfo; diff --git a/public/static/css/everypage.css b/public/static/css/everypage.css index 0a2c339..1741101 100644 --- a/public/static/css/everypage.css +++ b/public/static/css/everypage.css @@ -17,7 +17,7 @@ } .big-image { - background: url("/static/images/landingpage-background.jpg") no-repeat center; + background: url("/static/images/bpwstr_banner.jpg") no-repeat center; background-size: cover; padding: 3vw 5vw; text-align: center; diff --git a/public/static/css/tournament.css b/public/static/css/tournament.css index 63b476a..5855f3f 100644 --- a/public/static/css/tournament.css +++ b/public/static/css/tournament.css @@ -11,7 +11,7 @@ } .minw-25 { - min-width: 25%; + min-width: 350px; } .match:hover { diff --git a/public/static/images/bpwstr_banner.jpg b/public/static/images/bpwstr_banner.jpg new file mode 100644 index 0000000..1f3dc92 Binary files /dev/null and b/public/static/images/bpwstr_banner.jpg differ diff --git a/public/static/images/bpwstr_logo.png b/public/static/images/bpwstr_logo.png new file mode 100644 index 0000000..8081fc3 Binary files /dev/null and b/public/static/images/bpwstr_logo.png differ diff --git a/server.js b/server.js index 7dc0784..d17d936 100644 --- a/server.js +++ b/server.js @@ -21,6 +21,12 @@ app.prepare() app.render(req, res, actualPage, queryParam); }); + server.get('/t/:code/fullscreen-groups', (req, res) => { + const actualPage = '/tournament-fullscreen-groups'; + const queryParam = {code: req.params.code}; + app.render(req, res, actualPage, queryParam); + }); + server.get('/t/:code/edit', (req, res) => { const actualPage = '/tournament-edit'; const queryParam = {code: req.params.code}; diff --git a/yarn.lock b/yarn.lock index b2bd013..2a8a372 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5715,6 +5715,11 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +qrcode.react@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8" + integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q== + qs@6.10.3: version "6.10.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"