Merge branch 'master' into mark_advancing_teams
This commit is contained in:
commit
65088a435b
|
|
@ -9,3 +9,9 @@ variables:
|
||||||
include:
|
include:
|
||||||
- project: 'turniere/turniere-infra'
|
- project: 'turniere/turniere-infra'
|
||||||
file: '/ci/pipeline.yaml'
|
file: '/ci/pipeline.yaml'
|
||||||
|
|
||||||
|
eslint:
|
||||||
|
stage: test
|
||||||
|
script:
|
||||||
|
- yarn install
|
||||||
|
- yarn eslint .
|
||||||
|
|
|
||||||
|
|
@ -39,3 +39,7 @@ $ docker build -t turniere-frontend .
|
||||||
```
|
```
|
||||||
|
|
||||||
The built container exposes port 80.
|
The built container exposes port 80.
|
||||||
|
|
||||||
|
# Todo
|
||||||
|
|
||||||
|
Timer in topnav
|
||||||
|
|
@ -0,0 +1,192 @@
|
||||||
|
import React, {useState, useEffect, useRef} from 'react';
|
||||||
|
import {Button, ButtonGroup, Input} from 'reactstrap';
|
||||||
|
import {FaHeartCirclePlus, FaRegHeart, FaHeart, FaArrowTurnDown} from 'react-icons/fa6';
|
||||||
|
|
||||||
|
export function FavoriteBar({teams}) {
|
||||||
|
const [favorite, setFavorite] = useState(null);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isPulsing, setIsPulsing] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const headingRef = useRef(null);
|
||||||
|
const favoriteBarRef = useRef(null);
|
||||||
|
const scrollButtonRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const savedFavorite = localStorage.getItem('favoriteTeam');
|
||||||
|
if (savedFavorite) {
|
||||||
|
const team = teams.find(team => team.id === parseInt(savedFavorite, 10));
|
||||||
|
if (team) {
|
||||||
|
setFavorite(team);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [teams]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isVisible && favoriteBarRef.current) {
|
||||||
|
favoriteBarRef.current.style.maxHeight = `${favoriteBarRef.current.scrollHeight}px`;
|
||||||
|
} else if (favoriteBarRef.current) {
|
||||||
|
favoriteBarRef.current.style.maxHeight = '0';
|
||||||
|
}
|
||||||
|
}, [isVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (favoriteBarRef.current) {
|
||||||
|
favoriteBarRef.current.style.maxHeight = `${favoriteBarRef.current.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
const toggleFavorite = team => {
|
||||||
|
if (favorite && favorite.id === team.id) {
|
||||||
|
setFavorite(null);
|
||||||
|
localStorage.removeItem('favoriteTeam');
|
||||||
|
} else {
|
||||||
|
setFavorite(team);
|
||||||
|
localStorage.setItem('favoriteTeam', team.id);
|
||||||
|
setIsPulsing(true);
|
||||||
|
headingRef.current.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||||
|
if (scrollButtonRef.current) {
|
||||||
|
scrollButtonRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setIsVisible(false); // Close the favorite menu
|
||||||
|
};
|
||||||
|
|
||||||
|
function findLowestPlayoffParticipation(favoriteId) {
|
||||||
|
const matchesWithFavoriteParticipation = document.querySelectorAll(`[data-team-level-ids*='-${favoriteId}']`);
|
||||||
|
let lowestMatch = null; // lowest means lowest stage number > latest game of the favorite
|
||||||
|
let lowestStageNum = Infinity;
|
||||||
|
|
||||||
|
// Iterate over each element to find the match with the lowest stage number
|
||||||
|
matchesWithFavoriteParticipation.forEach(el => {
|
||||||
|
const dataTeamLevelIds = el.getAttribute('data-team-level-ids').split(',');
|
||||||
|
dataTeamLevelIds.forEach(pair => {
|
||||||
|
const [stage, teamId] = pair.split('-').map(Number);
|
||||||
|
if (teamId === favorite.id && stage < lowestStageNum) {
|
||||||
|
lowestStageNum = stage;
|
||||||
|
lowestMatch = el;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return lowestMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findScrollToGroup(favoriteId) {
|
||||||
|
// Look for group elements that contain the favorite team's ID
|
||||||
|
const groupElements = document.querySelectorAll('[data-teams]');
|
||||||
|
const scrollToNotFound = null;
|
||||||
|
|
||||||
|
groupElements.forEach(groupEl => {
|
||||||
|
const teamIds = groupEl.getAttribute('data-teams').split(',').map(id => parseInt(id, 10));
|
||||||
|
if (teamIds.includes(favoriteId)) {
|
||||||
|
return groupEl;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return scrollToNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToFavorite = () => {
|
||||||
|
if (!favorite) {
|
||||||
|
return; // Exit if there is no favorite team selected
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowestMatch = findLowestPlayoffParticipation(favorite.id);
|
||||||
|
|
||||||
|
let scrollTo;
|
||||||
|
if (lowestMatch) {
|
||||||
|
scrollTo = lowestMatch;
|
||||||
|
} else {
|
||||||
|
scrollTo = findScrollToGroup(favorite.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scrollTo) {
|
||||||
|
console.error('No match or group found for the favorite team');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scrollTimeout;
|
||||||
|
const handleScroll = () => {
|
||||||
|
clearTimeout(scrollTimeout);
|
||||||
|
scrollTimeout = setTimeout(() => {
|
||||||
|
setIsPulsing(false);
|
||||||
|
scrollTo.classList.add('scroll-to-highlight');
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollTo.classList.remove('scroll-to-highlight');
|
||||||
|
}, 2000);
|
||||||
|
window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollTo.scrollIntoView({behavior: 'smooth', block: 'center'}); // Smoothly scroll to the target element
|
||||||
|
|
||||||
|
// Add a scroll event listener to start the highlighting after scrolling only
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedTeams = [...teams].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
const filteredTeams = sortedTeams.filter(team => team.name.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="favorites border-bottom py-2 px-1">
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<h1 className="custom-font m-2 px-2" ref={headingRef}>Favorit:</h1>
|
||||||
|
<p className="m-2">{favorite ? favorite.name : ''}</p>
|
||||||
|
<ButtonGroup className="m-2">
|
||||||
|
<Button
|
||||||
|
title="{isVisible ? 'Favoriten schließen' : 'Favoriten öffnen'}"
|
||||||
|
onClick={() => setIsVisible(!isVisible)}
|
||||||
|
>
|
||||||
|
<FaHeartCirclePlus/>
|
||||||
|
</Button>
|
||||||
|
{favorite && (
|
||||||
|
<Button
|
||||||
|
title="Zum aktuellen Spiel des Favoriten springen"
|
||||||
|
onClick={scrollToFavorite}
|
||||||
|
className={isPulsing ? 'pulse-animation' : ''}
|
||||||
|
innerRef={scrollButtonRef}
|
||||||
|
>
|
||||||
|
<FaArrowTurnDown/>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
<div className={`favorite-bar ${isVisible ? 'visible' : ''}`} ref={favoriteBarRef}>
|
||||||
|
{sortedTeams.length > 5 && (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Team suchen..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
|
className="mb-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
{filteredTeams.map(team => (
|
||||||
|
<div
|
||||||
|
key={team.id}
|
||||||
|
onClick={() => toggleFavorite(team)}
|
||||||
|
style={{display: 'flex', alignItems: 'center', cursor: 'pointer'}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleFavorite(team);
|
||||||
|
}}
|
||||||
|
style={{marginRight: '10px'}}
|
||||||
|
>
|
||||||
|
{favorite && favorite.id === team.id ? <FaHeart/> : <FaRegHeart/>}
|
||||||
|
</Button>
|
||||||
|
<span>{team.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,56 +4,74 @@ import React, {Component} from 'react';
|
||||||
import {getGroup} from '../redux/tournamentApi';
|
import {getGroup} from '../redux/tournamentApi';
|
||||||
import {notify} from 'react-notify-toast';
|
import {notify} from 'react-notify-toast';
|
||||||
import {sortMatchesByPositionAscending} from '../utils/sorting';
|
import {sortMatchesByPositionAscending} from '../utils/sorting';
|
||||||
|
import {FaChevronDown} from 'react-icons/fa6';
|
||||||
|
|
||||||
export default class GroupStage extends Component {
|
export default class GroupStage extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {showMatches: this.props.showMatches};
|
this.groupRefs = this.props.groups.reduce((acc, group) => {
|
||||||
this.toggleShowMatches = this.toggleShowMatches.bind(this);
|
acc[group.id] = React.createRef();
|
||||||
}
|
return acc;
|
||||||
|
}, {});
|
||||||
toggleShowMatches() {
|
|
||||||
this.setState({showMatches: !this.state.showMatches});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (<div className='py-2 px-1'>
|
return (
|
||||||
<h1 className='custom-font'>
|
<div className='py-2 px-1'>
|
||||||
<span className='px-2'>Gruppenphase</span>
|
<h1 className='custom-font'>
|
||||||
<ShowMatchesToggleButton show={this.state.showMatches} toggle={this.toggleShowMatches}/>
|
<span className='px-2'>Gruppenphase</span>
|
||||||
</h1>
|
</h1>
|
||||||
<Row className='mt-3 gx-0'>
|
<Row className='mt-3 gx-0'>
|
||||||
{this.props.groups.map(group => (
|
{this.props.groups.map(group => (
|
||||||
<Group
|
<Group
|
||||||
group={group}
|
group={group}
|
||||||
key={group.id}
|
key={group.id}
|
||||||
isSignedIn={this.props.isSignedIn}
|
isSignedIn={this.props.isSignedIn}
|
||||||
isOwner={this.props.isOwner}
|
isOwner={this.props.isOwner}
|
||||||
showMatches={this.state.showMatches}
|
groupRef={this.groupRefs[group.id]}
|
||||||
teams={this.props.teams}
|
teams={this.props.teams}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Row>
|
</Row>
|
||||||
</div>);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ShowMatchesToggleButton(props) {
|
function ShowMatchesToggleChevron(props) {
|
||||||
return (<Button onClick={props.toggle} className='float-right default-font-family'>
|
const toggleClass = props.show ? 'rotate' : '';
|
||||||
{props.show ? 'Spiele ausblenden' : 'Spiele anzeigen'}
|
return (
|
||||||
</Button>);
|
<Button
|
||||||
|
color="link"
|
||||||
|
onClick={props.toggle}
|
||||||
|
className="position-absolute top-0 end-0 m-2 mt-1 button-no-focus"
|
||||||
|
title={props.show ? 'Matches ausblenden' : 'Matches einblenden'}
|
||||||
|
>
|
||||||
|
<FaChevronDown className={`my-chevron ${toggleClass}`}/>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class Group extends Component {
|
export class Group extends Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = props.group;
|
this.state = {
|
||||||
|
...props.group,
|
||||||
|
showMatches: false
|
||||||
|
};
|
||||||
this.reload = this.reload.bind(this);
|
this.reload = this.reload.bind(this);
|
||||||
|
this.handleToggle = this.handleToggle.bind(this);
|
||||||
this.onReloadSuccess = this.onReloadSuccess.bind(this);
|
this.onReloadSuccess = this.onReloadSuccess.bind(this);
|
||||||
this.onReloadError = this.onReloadError.bind(this);
|
this.onReloadError = this.onReloadError.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleToggle() {
|
||||||
|
this.setState(prevState => ({showMatches: !prevState.showMatches}));
|
||||||
|
if (this.props.groupRef.current) {
|
||||||
|
this.props.groupRef.current.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
reload() {
|
reload() {
|
||||||
getGroup(this.state.id, this.onReloadSuccess, this.onReloadError);
|
getGroup(this.state.id, this.onReloadSuccess, this.onReloadError);
|
||||||
}
|
}
|
||||||
|
|
@ -67,31 +85,45 @@ export class Group extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (<Col className='minw-25 py-2'>
|
const teamIds = new Set();
|
||||||
<Card>
|
this.state.matches.forEach(match => {
|
||||||
<CardBody>
|
teamIds.add(match.team1.id);
|
||||||
<h3 className='custom-font'>Gruppe {this.state.number}</h3>
|
teamIds.add(match.team2.id);
|
||||||
<Collapse isOpen={this.props.showMatches}>
|
});
|
||||||
{this.state.matches.sort(sortMatchesByPositionAscending()).map(match => (
|
const teamIdsString = Array.from(teamIds).join(',');
|
||||||
<Match
|
return (
|
||||||
match={match}
|
<Col className="minw-25 py-2">
|
||||||
isSignedIn={this.props.isSignedIn}
|
<Card ref={this.props.groupRef} data-teams={teamIdsString}>
|
||||||
isOwner={this.props.isOwner}
|
<CardBody className="position-relative">
|
||||||
onChange={this.reload}
|
<h3 className="custom-font">
|
||||||
key={match.id}
|
Gruppe {this.state.number}
|
||||||
/>
|
</h3>
|
||||||
))}
|
<ShowMatchesToggleChevron
|
||||||
</Collapse>
|
show={this.state.showMatches}
|
||||||
<GroupScoresTable scores={this.state.scores} teams={this.props.teams} />
|
toggle={this.handleToggle}
|
||||||
</CardBody>
|
/>
|
||||||
</Card>
|
<Collapse isOpen={this.state.showMatches}>
|
||||||
</Col>);
|
{this.state.matches.sort(sortMatchesByPositionAscending()).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} teams={this.props.teams}/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function GroupScoresTable(props) {
|
function GroupScoresTable(props) {
|
||||||
return (
|
return (
|
||||||
<Table className='mt-4' striped size='sm' responsive>
|
<Table className="mt-4" striped size="sm" responsive>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
|
|
@ -117,9 +149,10 @@ function GroupScoresTable(props) {
|
||||||
function GroupScoresTableRow(props) {
|
function GroupScoresTableRow(props) {
|
||||||
const advancingTeam = props.teams?.find(team => team.id === props.score.team.id && team.advancing_from_group_stage);
|
const advancingTeam = props.teams?.find(team => team.id === props.score.team.id && team.advancing_from_group_stage);
|
||||||
const rowClass = advancingTeam ? 'table-success' : '';
|
const rowClass = advancingTeam ? 'table-success' : '';
|
||||||
|
const teamId = `favorite-team-groupstage-${props.score.team.id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className={rowClass}>
|
<tr className={rowClass} id={teamId}>
|
||||||
<td>{props.score.position}</td>
|
<td>{props.score.position}</td>
|
||||||
<td>{props.score.team.name}</td>
|
<td>{props.score.team.name}</td>
|
||||||
<td>{props.score.group_points}</td>
|
<td>{props.score.group_points}</td>
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,6 @@ import {useRouter} from 'next/router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
class LinkButtonComponent extends React.Component {
|
class LinkButtonComponent extends React.Component {
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.defaultProps = {
|
|
||||||
outline: true,
|
|
||||||
color: 'secondary'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick(e) {
|
handleClick(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.router.push(this.props.href);
|
this.props.router.push(this.props.href);
|
||||||
|
|
@ -30,9 +22,7 @@ LinkButtonComponent.defaultProps = {
|
||||||
color: 'secondary'
|
color: 'secondary'
|
||||||
};
|
};
|
||||||
|
|
||||||
// export default withRouter(LinkButton);
|
|
||||||
|
|
||||||
export function LinkButton(props) {
|
export function LinkButton(props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (<LinkButtonComponent {...props} router={router} />);
|
return (<LinkButtonComponent {...props} router={router}/>);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,12 +122,27 @@ export class Match extends React.Component {
|
||||||
const groupInformation = this.state.match.group ?
|
const groupInformation = this.state.match.group ?
|
||||||
<div className="mb-2 mt-2">Gr. {this.state.match.group.number}</div> :
|
<div className="mb-2 mt-2">Gr. {this.state.match.group.number}</div> :
|
||||||
'';
|
'';
|
||||||
|
let team1Id; let team2Id;
|
||||||
|
if (this.props.stageLevel !== undefined) {
|
||||||
|
team1Id = `${this.props.stageLevel}-${this.props.match.team1.id}`;
|
||||||
|
team2Id = `${this.props.stageLevel}-${this.props.match.team2.id}`;
|
||||||
|
} else {
|
||||||
|
team1Id = undefined;
|
||||||
|
team2Id = undefined;
|
||||||
|
}
|
||||||
return (<div className='mb-3'>
|
return (<div className='mb-3'>
|
||||||
<Card className={'shadow-sm match '} onClick={this.toggleModal}>
|
<Card
|
||||||
|
className={'shadow-sm match'}
|
||||||
|
onClick={this.toggleModal}
|
||||||
|
data-team-level-ids={`${team1Id},${team2Id}`}
|
||||||
|
>
|
||||||
<div className="d-flex flex-row">
|
<div className="d-flex flex-row">
|
||||||
<CardBody className={borderClass + ' border py-2 ' + cardClass + ' ' + styles.match_bg}>
|
<CardBody className={borderClass + ' border py-2 ' + cardClass + ' ' + styles.match_bg}>
|
||||||
<MatchTable match={this.state.match} borderColor={borderClass}/>
|
<MatchTable
|
||||||
|
match={this.state.match}
|
||||||
|
stageLevel={this.props.stageLevel}
|
||||||
|
borderColor={borderClass}
|
||||||
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
<span className="badge bg-secondary align-items-center">
|
<span className="badge bg-secondary align-items-center">
|
||||||
{groupInformation}
|
{groupInformation}
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ export class PlayoffStages extends Component {
|
||||||
return (<div>
|
return (<div>
|
||||||
{this.props.playoffStages.map(stage => <Stage isSignedIn={this.props.isSignedIn}
|
{this.props.playoffStages.map(stage => <Stage isSignedIn={this.props.isSignedIn}
|
||||||
isOwner={this.props.isOwner} updateNextStage={() => this.updateNextStage(stage.id)}
|
isOwner={this.props.isOwner} updateNextStage={() => this.updateNextStage(stage.id)}
|
||||||
level={getLevelName(stage.level)} matches={stage.matches}
|
level={getLevelName(stage.level)} matches={stage.matches} stageLevel={stage.level}
|
||||||
key={stage.level}/>)}
|
key={stage.level}/>)}
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import React, {useState, useEffect} from 'react';
|
||||||
|
import {Button} from 'reactstrap';
|
||||||
|
import {FaCircleArrowUp} from 'react-icons/fa6';
|
||||||
|
|
||||||
|
export function ScrollToTopButton() {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (window.scrollY > 1.5 * window.innerHeight) {
|
||||||
|
setIsVisible(true);
|
||||||
|
} else {
|
||||||
|
setIsVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const scrollToTop = () => {
|
||||||
|
window.scrollTo({top: 0, behavior: 'smooth'});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={scrollToTop}
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '20px',
|
||||||
|
right: '20px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: '50px',
|
||||||
|
height: '50px',
|
||||||
|
zIndex: 999,
|
||||||
|
transition: 'opacity 0.5s ease-in-out',
|
||||||
|
opacity: isVisible ? 0.7 : 0,
|
||||||
|
pointerEvents: isVisible ? 'auto' : 'none',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaCircleArrowUp style={{fontSize: '36px'}}/>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,15 +3,22 @@ import {Match} from './Match';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export function Stage(props) {
|
export function Stage(props) {
|
||||||
const {isSignedIn, isOwner, updateNextStage} = props;
|
const {isSignedIn, isOwner, updateNextStage, stageLevel} = props;
|
||||||
|
|
||||||
return (<div>
|
return (<div>
|
||||||
<Container className='py-5'>
|
<Container className='py-5'>
|
||||||
<h1 className='custom-font'>{props.level}</h1>
|
<h1 className='custom-font'>{props.level}</h1>
|
||||||
<Row>
|
<Row>
|
||||||
{props.matches.map((match => (
|
{props.matches.map((match => (
|
||||||
<Col className='minw-25' key={match.id}><Match match={match} isSignedIn={isSignedIn}
|
<Col className='minw-25' key={match.id}>
|
||||||
isOwner={isOwner} onFinish={updateNextStage}/></Col>)))}
|
<Match
|
||||||
|
match={match}
|
||||||
|
isSignedIn={isSignedIn}
|
||||||
|
isOwner={isOwner}
|
||||||
|
onFinish={updateNextStage}
|
||||||
|
stageLevel={stageLevel}
|
||||||
|
/>
|
||||||
|
</Col>)))}
|
||||||
</Row>
|
</Row>
|
||||||
</Container>
|
</Container>
|
||||||
</div>);
|
</div>);
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,13 @@ export function getTournamentMatches(tournamentId, successCallback, errorCallbac
|
||||||
}
|
}
|
||||||
getRequest(getState(), '/tournaments/' + tournamentId + '/matches' + matchFilter)
|
getRequest(getState(), '/tournaments/' + tournamentId + '/matches' + matchFilter)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
successCallback(response.status, response.data.sort((a, b) => a.position > b.position).map(match => convertMatch(match)));
|
successCallback(
|
||||||
|
response.status, response.data.sort(
|
||||||
|
(a, b) => a.position > b.position
|
||||||
|
).map(
|
||||||
|
match => convertMatch(match)
|
||||||
|
)
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch(errorCallback);
|
.catch(errorCallback);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
"qrcode.react": "^3.1.0",
|
"qrcode.react": "^3.1.0",
|
||||||
"react": "^18.1.0",
|
"react": "^18.1.0",
|
||||||
"react-dom": "^18.1.0",
|
"react-dom": "^18.1.0",
|
||||||
|
"react-icons": "^5.5.0",
|
||||||
"react-notify-toast": "^0.5.1",
|
"react-notify-toast": "^0.5.1",
|
||||||
"react-pose": "^4.0.10",
|
"react-pose": "^4.0.10",
|
||||||
"react-redux": "^8.0.2",
|
"react-redux": "^8.0.2",
|
||||||
|
|
|
||||||
|
|
@ -141,8 +141,8 @@ class CreateTournamentForm extends React.Component {
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<WarningPopup
|
<WarningPopup
|
||||||
text={'Füge bitte noch ' + (this.state.groupAdvance - this.state.teams.length)
|
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.'}
|
' Team(s) hinzu, um ' + this.state.groupAdvance + ' Team(s) im Playoff zu haben.'}
|
||||||
shown={this.state.teams.length < this.state.groupAdvance}/>
|
shown={this.state.teams.length < this.state.groupAdvance}/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</Collapse>
|
</Collapse>
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,8 @@ function MainBottomSummary() {
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<h2>Ich habe einen Turniercode bekommen. Was nun?</h2>
|
<h2>Ich habe einen Turniercode bekommen. Was nun?</h2>
|
||||||
<p>
|
<p>
|
||||||
Der Turniercode führt dich direkt zu einem Turnier. Gebe dafür den Code <a className="text-success"
|
Der Turniercode führt dich direkt zu einem Turnier. Gebe dafür den Code
|
||||||
href="#turniercode-form">oben</a> ein,
|
<a className="text-success" href="#turniercode-form">oben</a> ein,
|
||||||
dann wirst du sofort weitergeleitet. </p>
|
dann wirst du sofort weitergeleitet. </p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,11 @@ import {Footer} from '../js/components/Footer';
|
||||||
|
|
||||||
import TournamentList from '../js/components/TournamentList';
|
import TournamentList from '../js/components/TournamentList';
|
||||||
import RequireLogin from '../js/components/RequireLogin';
|
import RequireLogin from '../js/components/RequireLogin';
|
||||||
import { LinkButton } from '../js/components/LinkButton';
|
import {LinkButton} from '../js/components/LinkButton';
|
||||||
|
|
||||||
class PrivateTournamentsPage extends React.Component {
|
class PrivateTournamentsPage extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (<RequireLogin loginMessage='Sie müssen angemeldet sein, um Ihre privaten Turniere zu sehen.'>
|
return (<RequireLogin loginMessage="Sie müssen angemeldet sein, um Ihre privaten Turniere zu sehen.">
|
||||||
<div className="main generic-fullpage-bg">
|
<div className="main generic-fullpage-bg">
|
||||||
<Head>
|
<Head>
|
||||||
<title>Private Turniere: turnie.re</title>
|
<title>Private Turniere: turnie.re</title>
|
||||||
|
|
@ -42,10 +42,10 @@ function PrivateTournamentsPageContent(props) {
|
||||||
</Container>
|
</Container>
|
||||||
<Container className="pb-5 pt-3">
|
<Container className="pb-5 pt-3">
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<LinkButton href="/list" outline={false} color='primary'>zu den öffentlichen Turnieren</LinkButton>
|
<LinkButton href="/list" outline={false} color="primary">zu den öffentlichen Turnieren</LinkButton>
|
||||||
{
|
{
|
||||||
props.isSignedIn &&
|
props.isSignedIn &&
|
||||||
<LinkButton href="/create" outline={false} color='success'>neues Turnier erstellen</LinkButton>
|
<LinkButton href="/create" outline={false} color="success">neues Turnier erstellen</LinkButton>
|
||||||
}
|
}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
@ -57,7 +57,7 @@ class PrivateTournamentsCard extends React.Component {
|
||||||
return (<Card className="shadow">
|
return (<Card className="shadow">
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<h1 className="custom-font">Private Turniere</h1>
|
<h1 className="custom-font">Private Turniere</h1>
|
||||||
<TournamentList type='private'/>
|
<TournamentList type="private"/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>);
|
</Card>);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {ErrorPageComponent} from '../js/components/ErrorComponents';
|
||||||
import {getTournament} from '../js/redux/tournamentApi';
|
import {getTournament} from '../js/redux/tournamentApi';
|
||||||
import {
|
import {
|
||||||
Col,
|
Col,
|
||||||
Container, Navbar, NavbarBrand, NavItem, Row, Spinner
|
Container, Row, Spinner
|
||||||
} from 'reactstrap';
|
} from 'reactstrap';
|
||||||
import {QRCodeSVG} from 'qrcode.react';
|
import {QRCodeSVG} from 'qrcode.react';
|
||||||
import {Group} from '../js/components/GroupStage';
|
import {Group} from '../js/components/GroupStage';
|
||||||
|
|
@ -15,21 +15,21 @@ function FullscreenPage(props) {
|
||||||
if (props.showLogo) {
|
if (props.showLogo) {
|
||||||
logo = <Col>
|
logo = <Col>
|
||||||
<div className="d-flex justify-content-center align-items-center">
|
<div className="d-flex justify-content-center align-items-center">
|
||||||
<img height='300' width='300' src='/static/images/bpwstr_logo.png'></img>
|
<img height="300" width="300" src="/static/images/bpwstr_logo.png"></img>
|
||||||
</div>
|
</div>
|
||||||
</Col>;
|
</Col>;
|
||||||
} else {
|
} else {
|
||||||
logo = <div />;
|
logo = <div/>;
|
||||||
}
|
}
|
||||||
return (<div>
|
return (<div>
|
||||||
<Container className='fs-5' fluid>
|
<Container className="fs-5" fluid>
|
||||||
<Row className='row-cols-4'>
|
<Row className="row-cols-4">
|
||||||
{props.groups.map(group => <Col className='mb-2'><Group group={group} key={group.id} teams={props.teams}/></Col>)}
|
{props.groups.map(group => <Col className="mb-2"><Group group={group} key={group.id} teams={props.teams}/></Col>)}
|
||||||
<Col>
|
<Col>
|
||||||
<div className="d-flex justify-content-center align-items-center">
|
<div className="d-flex justify-content-center align-items-center">
|
||||||
<QRCodeSVG
|
<QRCodeSVG
|
||||||
className='shadow mx-auto'
|
className="shadow mx-auto"
|
||||||
value='https://qr.bpwstr.de/2'
|
value="https://qr.bpwstr.de/2"
|
||||||
size="300"
|
size="300"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -40,13 +40,6 @@ function FullscreenPage(props) {
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
function FullscreenPageHeader(props) {
|
|
||||||
return (<Navbar color='light' className='mb-4 border-bottom py-0'>
|
|
||||||
<NavbarBrand>{props.title}</NavbarBrand>
|
|
||||||
{props.page}/{props.maxPage}
|
|
||||||
</Navbar>);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Main extends React.Component {
|
class Main extends React.Component {
|
||||||
static async getInitialProps({query}) {
|
static async getInitialProps({query}) {
|
||||||
|
|
@ -122,9 +115,9 @@ class Main extends React.Component {
|
||||||
<Head>
|
<Head>
|
||||||
<title>Vollbild-Ansicht: turnie.re</title>
|
<title>Vollbild-Ansicht: turnie.re</title>
|
||||||
</Head>
|
</Head>
|
||||||
<Container className='p-5 text-center text-secondary'>
|
<Container className="p-5 text-center text-secondary">
|
||||||
<Spinner size='sm'/>
|
<Spinner size="sm"/>
|
||||||
<span className='ml-3'>lade Vollbild-Ansicht</span>
|
<span className="ml-3">lade Vollbild-Ansicht</span>
|
||||||
</Container>
|
</Container>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||||
import {ErrorPageComponent} from '../js/components/ErrorComponents';
|
import {ErrorPageComponent} from '../js/components/ErrorComponents';
|
||||||
import {getTournamentMatches, getTournamentMeta} from '../js/redux/tournamentApi';
|
import {getTournamentMatches, getTournamentMeta} from '../js/redux/tournamentApi';
|
||||||
import {
|
import {
|
||||||
Col, Container, DropdownItem, DropdownMenu, DropdownToggle, Navbar, NavbarBrand, NavItem, Row, UncontrolledDropdown,
|
Col, Container, DropdownItem, DropdownMenu, DropdownToggle, Navbar, NavbarBrand, Row, UncontrolledDropdown,
|
||||||
Spinner
|
Spinner
|
||||||
} from 'reactstrap';
|
} from 'reactstrap';
|
||||||
import {Match} from '../js/components/Match';
|
import {Match} from '../js/components/Match';
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import {EditButton, TournamentStatusBar} from '../js/components/TournamentStatus
|
||||||
import {LinkButton} from '../js/components/LinkButton';
|
import {LinkButton} from '../js/components/LinkButton';
|
||||||
import {LoadingPage} from '../js/components/LoadingPage';
|
import {LoadingPage} from '../js/components/LoadingPage';
|
||||||
import {getTournament} from '../js/redux/tournamentApi';
|
import {getTournament} from '../js/redux/tournamentApi';
|
||||||
|
import {FavoriteBar} from '../js/components/FavoriteBar';
|
||||||
|
import {ScrollToTopButton} from '../js/components/ScrollToTopButton';
|
||||||
|
|
||||||
class PrivateTournamentPage extends React.Component {
|
class PrivateTournamentPage extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
|
|
@ -22,22 +24,26 @@ class PrivateTournamentPage extends React.Component {
|
||||||
const {isSignedIn, username} = this.props;
|
const {isSignedIn, username} = this.props;
|
||||||
const isOwner = username === ownerUsername;
|
const isOwner = username === ownerUsername;
|
||||||
|
|
||||||
return (<div className='pb-5'>
|
return (
|
||||||
<TournamentBigImage {...this.props.tournament}/>
|
<div className='pb-5'>
|
||||||
<StatusBar tournament={this.props.tournament} isOwner={isOwner} isSignedIn={isSignedIn}/>
|
<TournamentBigImage {...this.props.tournament}/>
|
||||||
<div className='stages'>
|
<StatusBar tournament={this.props.tournament} isOwner={isOwner} isSignedIn={isSignedIn}/>
|
||||||
{groupStage != null &&
|
<FavoriteBar teams={this.props.tournament.teams}/>
|
||||||
<div><GroupStage
|
<div className='stages'>
|
||||||
groups={groupStage.groups}
|
{groupStage != null &&
|
||||||
isSignedIn={isSignedIn}
|
<div><GroupStage
|
||||||
isOwner={isOwner}
|
groups={groupStage.groups}
|
||||||
showMatches={playoffStages !== null}
|
isSignedIn={isSignedIn}
|
||||||
teams={this.props.tournament.teams}
|
isOwner={isOwner}
|
||||||
/></div>}
|
showMatches={playoffStages !== null}
|
||||||
<PlayoffStages playoffStages={playoffStages} isSignedIn={isSignedIn}
|
teams={this.props.tournament.teams}
|
||||||
isOwner={isOwner}/>
|
/></div>}
|
||||||
|
<PlayoffStages playoffStages={playoffStages} isSignedIn={isSignedIn}
|
||||||
|
isOwner={isOwner}/>
|
||||||
|
</div>
|
||||||
|
<ScrollToTopButton />
|
||||||
</div>
|
</div>
|
||||||
</div>);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,7 +53,6 @@ function StatusBar(props) {
|
||||||
<EditButton tournamentId={props.tournament.id} isOwner={props.isOwner} isSignedIn={props.isSignedIn}/>
|
<EditButton tournamentId={props.tournament.id} isOwner={props.isOwner} isSignedIn={props.isSignedIn}/>
|
||||||
<StatisticsButton tournamentId={props.tournament.id}/>
|
<StatisticsButton tournamentId={props.tournament.id}/>
|
||||||
<FullscreenButton tournamentId={props.tournament.id}/>
|
<FullscreenButton tournamentId={props.tournament.id}/>
|
||||||
<LinkButton href={'/t/' + props.tournament.id + '/fullscreen-groups'}>Vollbild-Ansicht Gruppen</LinkButton>
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</TournamentStatusBar>);
|
</TournamentStatusBar>);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,16 @@
|
||||||
background-color: #f8f8f8;
|
background-color: #f8f8f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.favorites {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
.minw-25 {
|
.minw-25 {
|
||||||
min-width: 350px;
|
min-width: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.match:hover {
|
.match:hover {
|
||||||
box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important;
|
box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .15) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.match:hover > div {
|
.match:hover > div {
|
||||||
|
|
@ -27,6 +31,49 @@
|
||||||
width: 11rem;
|
width: 11rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-animation {
|
||||||
|
animation: pulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-to-highlight {
|
||||||
|
animation: pulse 1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-bar {
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.7s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-bar.visible {
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-chevron {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
color: gray;
|
||||||
|
}
|
||||||
|
.my-chevron.rotate {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
.button-no-focus:focus:not(:focus-visible),
|
||||||
|
.button-no-focus:active {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.hide-cursor {
|
.hide-cursor {
|
||||||
cursor: none;
|
cursor: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5828,6 +5828,11 @@ react-fast-compare@^3.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb"
|
||||||
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==
|
||||||
|
|
||||||
|
react-icons@^5.5.0:
|
||||||
|
version "5.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.5.0.tgz#8aa25d3543ff84231685d3331164c00299cdfaf2"
|
||||||
|
integrity sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==
|
||||||
|
|
||||||
react-is@^16.13.1:
|
react-is@^16.13.1:
|
||||||
version "16.13.1"
|
version "16.13.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue