Merge pull request #49 from turniere/ticket/TURNIERE-233

Ticket/turniere 233: Add page content loading indicators
This commit is contained in:
betanummeric 2019-10-25 12:30:15 +02:00 committed by GitHub
commit 475e00157c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 102 additions and 24 deletions

View File

@ -2,7 +2,9 @@ stages:
- build - build
- deploy - deploy
kaniko: kaniko:
tags:
- docker
stage: build stage: build
image: image:
name: gcr.io/kaniko-project/executor:debug name: gcr.io/kaniko-project/executor:debug
@ -11,7 +13,9 @@ kaniko:
- echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
- /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:latest --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:latest --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
kubernetes: kubernetes:
tags:
- turniere-deploy
stage: deploy stage: deploy
only: only:
- master - master

View File

@ -344,6 +344,7 @@ const reducerTournamentStatistics = (state = defaultStateTournamentStatistics, a
storeOptionalToken(error.response); storeOptionalToken(error.response);
} }
action.parameters.errorCallback(); action.parameters.errorCallback();
return Object.assign({}, state, {loaded: true});
}); });
return state; return state;
case actionTypesTournamentStatistics.INT_REQUEST_TOURNAMENT_STATISTICS: case actionTypesTournamentStatistics.INT_REQUEST_TOURNAMENT_STATISTICS:
@ -361,11 +362,12 @@ const reducerTournamentStatistics = (state = defaultStateTournamentStatistics, a
storeOptionalToken(error.response); storeOptionalToken(error.response);
} }
action.parameters.errorCallback(); action.parameters.errorCallback();
return Object.assign({}, state, {loaded: true});
}); });
return Object.assign({}, state, action.parameters.tournamentInfo); return Object.assign({}, state, action.parameters.tournamentInfo, {loaded: true});
case actionTypesTournamentStatistics.REQUEST_TOURNAMENT_STATISTICS_SUCCESS: case actionTypesTournamentStatistics.REQUEST_TOURNAMENT_STATISTICS_SUCCESS:
action.parameters.successCallback(); action.parameters.successCallback();
return Object.assign({}, state, action.parameters.tournamentStatistics); return Object.assign({}, state, action.parameters.tournamentStatistics, {loaded: true});
default: return state; default: return state;
} }
}; };

View File

@ -0,0 +1,23 @@
import Head from 'next/head';
import {TurniereNavigation} from './Navigation';
import {Container} from 'reactstrap';
import {Footer} from './Footer';
import React from 'react';
export function LoadingPage(props) {
return (<div>
<Head>
<title>{props.title}</title>
</Head>
<TurniereNavigation/>
<Container>
<div className='text-center'>
<img src='/static/images/logo-questionmark.png' alt='' className='loading-logo'/>
</div>
<div className='py-5 text-center w-100 h1 custom-font'>
{props.text}
</div>
</Container>
<Footer/>
</div>);
}

View File

@ -1,35 +1,51 @@
import React from 'react'; import React from 'react';
import {requestTournamentList} from '../api'; import {requestTournamentList} from '../api';
import {Spinner} from 'react-bootstrap';
export default class TournamentList extends React.Component { export default class TournamentList extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
tournaments: [] tournaments: [],
loaded: false
}; };
} }
componentDidMount() { componentDidMount() {
requestTournamentList(this.props.type, tournaments => { requestTournamentList(this.props.type, tournaments => {
this.setState({ this.setState({
tournaments: tournaments tournaments: tournaments,
loaded: true
}); });
}, () => {}); }, () => {
this.setState({loaded: true});
});
} }
render() { render() {
if (this.state.tournaments.length === 0) { if (!this.state.loaded) {
return <p className="text-center border-light font-italic text-secondary border-top border-bottom p-1">keine return (<EmptyList>
Turniere vorhanden</p>; <Spinner animation='border' role='status' size='sm'/>
} else { <span className='ml-3'>lade Turnier-Liste</span>
return this.state.tournaments.map(item => ( </EmptyList>);
// The code should be item.code but the api just supports it this way by now
<TournamentListEntry name={item.name} code={item.id} key={item.id}/>
));
} }
if (this.state.tournaments.length === 0) {
return <EmptyList>keine Turniere vorhanden</EmptyList>;
}
return this.state.tournaments.map(item => (
// The code should be item.code but the api just supports it this way by now
<TournamentListEntry name={item.name} code={item.id} key={item.id}/>
));
} }
} }
function EmptyList(props) {
return (<p className="text-center border-light font-italic text-secondary border-top border-bottom p-1">
{props.children}
</p>);
}
function TournamentListEntry(props) { function TournamentListEntry(props) {
return ( return (
<a className="w-100 d-inline-block mt-2 text-left btn btn-outline-primary" href={'/t/' + props.code}> <a className="w-100 d-inline-block mt-2 text-left btn btn-outline-primary" href={'/t/' + props.code}>

View File

@ -16,6 +16,7 @@ export const defaultStateTournamentStatistics = {
owner_username: '', owner_username: '',
isPublic: '', isPublic: '',
loaded: false,
statistics_available: false, statistics_available: false,
most_dominant_team: {}, most_dominant_team: {},

View File

@ -19,6 +19,7 @@ import 'bootstrap/dist/css/bootstrap.min.css';
import '../static/css/everypage.css'; import '../static/css/everypage.css';
import '../static/css/index.css'; import '../static/css/index.css';
import {LoadingPage} from '../js/components/LoadingPage';
class EditTournamentPage extends React.Component { class EditTournamentPage extends React.Component {
static async getInitialProps({query}) { static async getInitialProps({query}) {
@ -29,26 +30,31 @@ class EditTournamentPage extends React.Component {
super(props); super(props);
this.state = { this.state = {
validCode: false validCode: false,
loaded: false
}; };
} }
componentDidMount() { componentDidMount() {
requestTournament(this.props.query.code, () => { requestTournament(this.props.query.code, () => {
this.setState({validCode: true}); this.setState({validCode: true, loaded: true});
if (this._edittournamentcontent != null) { if (this._edittournamentcontent != null) {
this._edittournamentcontent.notifyOfContentUpdate(); this._edittournamentcontent.notifyOfContentUpdate();
} }
}, () => { }, () => {
this.setState({validCode: false}); this.setState({validCode: false, loaded: true});
}); });
} }
render() { render() {
const {validCode} = this.state; const {validCode, loaded} = this.state;
const {tournamentname, ownerUsername, isSignedIn, username} = this.props; const {tournamentname, ownerUsername, isSignedIn, username} = this.props;
if (!loaded) {
return <LoadingPage title='turnie.re' text='Turnier-Einstellungen werden geladen...'/>;
}
return (<UserRestrictor> return (<UserRestrictor>
<Option condition={validCode && isSignedIn && ownerUsername === username}> <Option condition={validCode && isSignedIn && ownerUsername === username}>
<div className='pb-5'> <div className='pb-5'>

View File

@ -11,6 +11,7 @@ import {requestTournamentStatistics} from '../js/api';
import {EditButton, TournamentStatusBar, TournamentStatusBarButton} from '../js/components/TournamentStatusBar'; import {EditButton, TournamentStatusBar, TournamentStatusBarButton} from '../js/components/TournamentStatusBar';
import Navbar from 'react-bootstrap/Navbar'; import Navbar from 'react-bootstrap/Navbar';
import {TournamentBigImage} from '../js/components/TournamentBigImage'; import {TournamentBigImage} from '../js/components/TournamentBigImage';
import {LoadingPage} from '../js/components/LoadingPage';
class StatisticsTournamentPage extends React.Component { class StatisticsTournamentPage extends React.Component {
static async getInitialProps({query}) { static async getInitialProps({query}) {
@ -24,6 +25,10 @@ class StatisticsTournamentPage extends React.Component {
render() { render() {
const {tournamentStatistics} = this.props; const {tournamentStatistics} = this.props;
if (!tournamentStatistics.loaded) {
return <LoadingPage title='turnie.re' text='Statistiken zum Turnier werden geladen...'/>;
}
return ( return (
<div> <div>
<Head> <Head>

View File

@ -12,11 +12,12 @@ import 'bootstrap/dist/css/bootstrap.min.css';
import '../static/css/everypage.css'; import '../static/css/everypage.css';
import '../static/css/tournament.css'; import '../static/css/tournament.css';
import {getTournament} from '../js/redux/tournamentApi';
import {PlayoffStages} from '../js/components/PlayoffStages'; import {PlayoffStages} from '../js/components/PlayoffStages';
import GroupStage from '../js/components/GroupStage'; import GroupStage from '../js/components/GroupStage';
import {TournamentBigImage} from '../js/components/TournamentBigImage'; import {TournamentBigImage} from '../js/components/TournamentBigImage';
import {EditButton, TournamentStatusBar, TournamentStatusBarButton} from '../js/components/TournamentStatusBar'; import {EditButton, TournamentStatusBar, TournamentStatusBarButton} from '../js/components/TournamentStatusBar';
import {LoadingPage} from '../js/components/LoadingPage';
import {getTournament} from '../js/redux/tournamentApi';
class PrivateTournamentPage extends React.Component { class PrivateTournamentPage extends React.Component {
render() { render() {
@ -71,7 +72,8 @@ class Main extends React.Component {
super(props); super(props);
this.state = { this.state = {
tournament: null tournament: null,
loaded: false
}; };
this.onTournamentRequestSuccess = this.onTournamentRequestSuccess.bind(this); this.onTournamentRequestSuccess = this.onTournamentRequestSuccess.bind(this);
this.onTournamentRequestError = this.onTournamentRequestError.bind(this); this.onTournamentRequestError = this.onTournamentRequestError.bind(this);
@ -82,14 +84,14 @@ class Main extends React.Component {
} }
onTournamentRequestSuccess(requestStatus, tournament) { onTournamentRequestSuccess(requestStatus, tournament) {
this.setState({status: requestStatus, tournament: tournament}); this.setState({status: requestStatus, tournament: tournament, loaded: true});
} }
onTournamentRequestError(error) { onTournamentRequestError(error) {
if (error.response) { if (error.response) {
this.setState({status: error.response.status}); this.setState({status: error.response.status, loaded: true});
} else { } else {
this.setState({status: -1}); this.setState({status: -1, loaded: true});
} }
} }
@ -109,6 +111,9 @@ class Main extends React.Component {
<Footer/> <Footer/>
</div>); </div>);
} else { } else {
if (!this.state.loaded) {
return <LoadingPage title='turnie.re' text='Turnier wird geladen...'/>;
}
return <ErrorPageComponent code={status}/>; return <ErrorPageComponent code={status}/>;
} }
} }

View File

@ -72,3 +72,19 @@ footer {
background-size: cover; background-size: cover;
min-height: 100vh; min-height: 100vh;
} }
.loading-logo {
animation: blink .8s ease-in-out infinite;
}
@keyframes blink {
0% {
filter: grayscale(100%);
}
70% {
filter: grayscale(0%);
}
100% {
filter: grayscale(100%);
}
}