Merge branch 'favorites'
This commit is contained in:
commit
0e56691fbc
|
|
@ -9,3 +9,9 @@ variables:
|
|||
include:
|
||||
- project: 'turniere/turniere-infra'
|
||||
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.
|
||||
|
||||
# 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,48 +4,73 @@ import React, {Component} from 'react';
|
|||
import {getGroup} from '../redux/tournamentApi';
|
||||
import {notify} from 'react-notify-toast';
|
||||
import {sortMatchesByPositionAscending} from '../utils/sorting';
|
||||
import {FaChevronDown} from 'react-icons/fa6';
|
||||
|
||||
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});
|
||||
this.groupRefs = this.props.groups.reduce((acc, group) => {
|
||||
acc[group.id] = React.createRef();
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (<div className='py-2 px-1'>
|
||||
<h1 className='custom-font'>
|
||||
<span className='px-2'>Gruppenphase</span>
|
||||
<ShowMatchesToggleButton show={this.state.showMatches} toggle={this.toggleShowMatches}/>
|
||||
</h1>
|
||||
<Row className='mt-3 gx-0'>
|
||||
{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>);
|
||||
return (
|
||||
<div className='py-2 px-1'>
|
||||
<h1 className='custom-font'>
|
||||
<span className='px-2'>Gruppenphase</span>
|
||||
</h1>
|
||||
<Row className='mt-3 gx-0'>
|
||||
{this.props.groups.map(group => (
|
||||
<Group
|
||||
group={group}
|
||||
key={group.id}
|
||||
isSignedIn={this.props.isSignedIn}
|
||||
isOwner={this.props.isOwner}
|
||||
groupRef={this.groupRefs[group.id]}
|
||||
/>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function ShowMatchesToggleButton(props) {
|
||||
return (<Button onClick={props.toggle} className='float-right default-font-family'>
|
||||
{props.show ? 'Spiele ausblenden' : 'Spiele anzeigen'}
|
||||
</Button>);
|
||||
function ShowMatchesToggleChevron(props) {
|
||||
const toggleClass = props.show ? 'rotate' : '';
|
||||
return (
|
||||
<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 {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = props.group;
|
||||
this.state = {
|
||||
...props.group,
|
||||
showMatches: false
|
||||
};
|
||||
this.reload = this.reload.bind(this);
|
||||
this.handleToggle = this.handleToggle.bind(this);
|
||||
this.onReloadSuccess = this.onReloadSuccess.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() {
|
||||
getGroup(this.state.id, this.onReloadSuccess, this.onReloadError);
|
||||
}
|
||||
|
|
@ -59,25 +84,46 @@ export class Group extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
return (<Col className='minw-25 py-2'>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<h3 className='custom-font'>Gruppe {this.state.number}</h3>
|
||||
<Collapse isOpen={this.props.showMatches}>
|
||||
{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}/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>);
|
||||
const teamIds = new Set();
|
||||
this.state.matches.forEach(match => {
|
||||
teamIds.add(match.team1.id);
|
||||
teamIds.add(match.team2.id);
|
||||
});
|
||||
const teamIdsString = Array.from(teamIds).join(',');
|
||||
return (
|
||||
<Col className="minw-25 py-2">
|
||||
<Card ref={this.props.groupRef} data-teams={teamIdsString}>
|
||||
<CardBody className="position-relative">
|
||||
<h3 className="custom-font">
|
||||
Gruppe {this.state.number}
|
||||
</h3>
|
||||
<ShowMatchesToggleChevron
|
||||
show={this.state.showMatches}
|
||||
toggle={this.handleToggle}
|
||||
/>
|
||||
<Collapse isOpen={this.state.showMatches}>
|
||||
{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}/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function GroupScoresTable(props) {
|
||||
return (<Table className='mt-4' striped size='sm' responsive>
|
||||
<thead>
|
||||
return (
|
||||
<Table className="mt-4" striped size="sm" responsive>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Team</th>
|
||||
|
|
@ -85,20 +131,23 @@ function GroupScoresTable(props) {
|
|||
<th><span title="Becherdifferenz">Dif.</span></th>
|
||||
<th><span title="Getroffene Becher (Geworfen)">Gew.</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</thead>
|
||||
<tbody>
|
||||
{props.scores.map(groupScore => <GroupScoresTableRow score={groupScore} key={groupScore.id}/>)}
|
||||
</tbody>
|
||||
</Table>);
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function GroupScoresTableRow(props) {
|
||||
return (<tr>
|
||||
<td>{props.score.position}</td>
|
||||
<td>{props.score.team.name}</td>
|
||||
<td>{props.score.group_points}</td>
|
||||
<td>{props.score.difference_in_points}</td>
|
||||
<td>{props.score.scored_points}</td>
|
||||
</tr>);
|
||||
const teamId = `favorite-team-groupstage-${props.score.team.id}`;
|
||||
return (
|
||||
<tr id={teamId}>
|
||||
<td>{props.score.position}</td>
|
||||
<td>{props.score.team.name}</td>
|
||||
<td>{props.score.group_points}</td>
|
||||
<td>{props.score.difference_in_points}</td>
|
||||
<td>{props.score.scored_points}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,6 @@ import {useRouter} from 'next/router';
|
|||
import React from 'react';
|
||||
|
||||
class LinkButtonComponent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.defaultProps = {
|
||||
outline: true,
|
||||
color: 'secondary'
|
||||
};
|
||||
}
|
||||
|
||||
handleClick(e) {
|
||||
e.preventDefault();
|
||||
this.props.router.push(this.props.href);
|
||||
|
|
@ -30,9 +22,7 @@ LinkButtonComponent.defaultProps = {
|
|||
color: 'secondary'
|
||||
};
|
||||
|
||||
// export default withRouter(LinkButton);
|
||||
|
||||
export function LinkButton(props) {
|
||||
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 ?
|
||||
<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'>
|
||||
<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">
|
||||
<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>
|
||||
<span className="badge bg-secondary align-items-center">
|
||||
{groupInformation}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export class PlayoffStages extends Component {
|
|||
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}
|
||||
level={getLevelName(stage.level)} matches={stage.matches} stageLevel={stage.level}
|
||||
key={stage.level}/>)}
|
||||
</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';
|
||||
|
||||
export function Stage(props) {
|
||||
const {isSignedIn, isOwner, updateNextStage} = props;
|
||||
const {isSignedIn, isOwner, updateNextStage, stageLevel} = 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>)))}
|
||||
<Col className='minw-25' key={match.id}>
|
||||
<Match
|
||||
match={match}
|
||||
isSignedIn={isSignedIn}
|
||||
isOwner={isOwner}
|
||||
onFinish={updateNextStage}
|
||||
stageLevel={stageLevel}
|
||||
/>
|
||||
</Col>)))}
|
||||
</Row>
|
||||
</Container>
|
||||
</div>);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,13 @@ export function getTournamentMatches(tournamentId, successCallback, errorCallbac
|
|||
}
|
||||
getRequest(getState(), '/tournaments/' + tournamentId + '/matches' + matchFilter)
|
||||
.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);
|
||||
}
|
||||
|
|
@ -67,7 +73,8 @@ function convertTournament(apiTournament) {
|
|||
isPublic: apiTournament.public,
|
||||
ownerUsername: apiTournament.owner_username,
|
||||
groupStage: groupStage,
|
||||
playoffStages: playoffStages
|
||||
playoffStages: playoffStages,
|
||||
teams: apiTournament.teams
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-notify-toast": "^0.5.1",
|
||||
"react-pose": "^4.0.10",
|
||||
"react-redux": "^8.0.2",
|
||||
|
|
|
|||
|
|
@ -141,8 +141,8 @@ class CreateTournamentForm extends React.Component {
|
|||
</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.'}
|
||||
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>
|
||||
</Collapse>
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ function MainBottomSummary() {
|
|||
<div className="col-lg-6">
|
||||
<h2>Ich habe einen Turniercode bekommen. Was nun?</h2>
|
||||
<p>
|
||||
Der Turniercode führt dich direkt zu einem Turnier. Gebe dafür den Code <a className="text-success"
|
||||
href="#turniercode-form">oben</a> ein,
|
||||
Der Turniercode führt dich direkt zu einem Turnier. Gebe dafür den Code
|
||||
<a className="text-success" href="#turniercode-form">oben</a> ein,
|
||||
dann wirst du sofort weitergeleitet. </p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ import {Footer} from '../js/components/Footer';
|
|||
|
||||
import TournamentList from '../js/components/TournamentList';
|
||||
import RequireLogin from '../js/components/RequireLogin';
|
||||
import { LinkButton } from '../js/components/LinkButton';
|
||||
import {LinkButton} from '../js/components/LinkButton';
|
||||
|
||||
class PrivateTournamentsPage extends React.Component {
|
||||
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">
|
||||
<Head>
|
||||
<title>Private Turniere: turnie.re</title>
|
||||
|
|
@ -42,10 +42,10 @@ function PrivateTournamentsPageContent(props) {
|
|||
</Container>
|
||||
<Container className="pb-5 pt-3">
|
||||
<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 &&
|
||||
<LinkButton href="/create" outline={false} color='success'>neues Turnier erstellen</LinkButton>
|
||||
<LinkButton href="/create" outline={false} color="success">neues Turnier erstellen</LinkButton>
|
||||
}
|
||||
</ButtonGroup>
|
||||
</Container>
|
||||
|
|
@ -57,7 +57,7 @@ class PrivateTournamentsCard extends React.Component {
|
|||
return (<Card className="shadow">
|
||||
<CardBody>
|
||||
<h1 className="custom-font">Private Turniere</h1>
|
||||
<TournamentList type='private'/>
|
||||
<TournamentList type="private"/>
|
||||
</CardBody>
|
||||
</Card>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {ErrorPageComponent} from '../js/components/ErrorComponents';
|
|||
import {getTournament} from '../js/redux/tournamentApi';
|
||||
import {
|
||||
Col,
|
||||
Container, Navbar, NavbarBrand, NavItem, Row, Spinner
|
||||
Container, Row, Spinner
|
||||
} from 'reactstrap';
|
||||
import {QRCodeSVG} from 'qrcode.react';
|
||||
import {Group} from '../js/components/GroupStage';
|
||||
|
|
@ -14,22 +14,22 @@ function FullscreenPage(props) {
|
|||
let logo;
|
||||
if (props.showLogo) {
|
||||
logo = <Col>
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
<img height='300' width='300' src='/static/images/bpwstr_logo.png'></img>
|
||||
</div>
|
||||
</Col>;
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
<img height="300" width="300" src="/static/images/bpwstr_logo.png"></img>
|
||||
</div>
|
||||
</Col>;
|
||||
} else {
|
||||
logo = <div />;
|
||||
logo = <div/>;
|
||||
}
|
||||
return (<div>
|
||||
<Container className='fs-5' fluid>
|
||||
<Row className='row-cols-4'>
|
||||
{props.groups.map(group => <Col className='mb-2'><Group group={group} key={group.id}/></Col>)}
|
||||
<Container className="fs-5" fluid>
|
||||
<Row className="row-cols-4">
|
||||
{props.groups.map(group => <Col className="mb-2"><Group group={group} key={group.id}/></Col>)}
|
||||
<Col>
|
||||
<div className="d-flex justify-content-center align-items-center">
|
||||
<QRCodeSVG
|
||||
className='shadow mx-auto'
|
||||
value='https://qr.bpwstr.de/2'
|
||||
className="shadow mx-auto"
|
||||
value="https://qr.bpwstr.de/2"
|
||||
size="300"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -40,13 +40,6 @@ function FullscreenPage(props) {
|
|||
</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 {
|
||||
static async getInitialProps({query}) {
|
||||
|
|
@ -122,9 +115,9 @@ class Main extends React.Component {
|
|||
<Head>
|
||||
<title>Vollbild-Ansicht: turnie.re</title>
|
||||
</Head>
|
||||
<Container className='p-5 text-center text-secondary'>
|
||||
<Spinner size='sm'/>
|
||||
<span className='ml-3'>lade Vollbild-Ansicht</span>
|
||||
<Container className="p-5 text-center text-secondary">
|
||||
<Spinner size="sm"/>
|
||||
<span className="ml-3">lade Vollbild-Ansicht</span>
|
||||
</Container>
|
||||
</div>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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,
|
||||
Col, Container, DropdownItem, DropdownMenu, DropdownToggle, Navbar, NavbarBrand, Row, UncontrolledDropdown,
|
||||
Spinner
|
||||
} from 'reactstrap';
|
||||
import {Match} from '../js/components/Match';
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import {EditButton, TournamentStatusBar} from '../js/components/TournamentStatus
|
|||
import {LinkButton} from '../js/components/LinkButton';
|
||||
import {LoadingPage} from '../js/components/LoadingPage';
|
||||
import {getTournament} from '../js/redux/tournamentApi';
|
||||
import {FavoriteBar} from '../js/components/FavoriteBar';
|
||||
import {ScrollToTopButton} from '../js/components/ScrollToTopButton';
|
||||
|
||||
class PrivateTournamentPage extends React.Component {
|
||||
render() {
|
||||
|
|
@ -22,17 +24,21 @@ class PrivateTournamentPage extends React.Component {
|
|||
const {isSignedIn, username} = this.props;
|
||||
const isOwner = username === ownerUsername;
|
||||
|
||||
return (<div className='pb-5'>
|
||||
<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}/>
|
||||
return (
|
||||
<div className='pb-5'>
|
||||
<TournamentBigImage {...this.props.tournament}/>
|
||||
<StatusBar tournament={this.props.tournament} isOwner={isOwner} isSignedIn={isSignedIn}/>
|
||||
<FavoriteBar teams={this.props.tournament.teams}/>
|
||||
<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>
|
||||
<ScrollToTopButton />
|
||||
</div>
|
||||
</div>);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +48,6 @@ function StatusBar(props) {
|
|||
<EditButton tournamentId={props.tournament.id} isOwner={props.isOwner} isSignedIn={props.isSignedIn}/>
|
||||
<StatisticsButton tournamentId={props.tournament.id}/>
|
||||
<FullscreenButton tournamentId={props.tournament.id}/>
|
||||
<LinkButton href={'/t/' + props.tournament.id + '/fullscreen-groups'}>Vollbild-Ansicht Gruppen</LinkButton>
|
||||
</ButtonGroup>
|
||||
</TournamentStatusBar>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,16 @@
|
|||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.favorites {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.minw-25 {
|
||||
min-width: 350px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
|
@ -27,6 +31,49 @@
|
|||
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 {
|
||||
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"
|
||||
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:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
|
|
|||
Loading…
Reference in New Issue