Merge branch 'master' into ticket/TURNIERE-156

This commit is contained in:
Jonny 2019-05-02 08:52:18 +02:00 committed by GitHub
commit b30a3a759e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 309 additions and 94 deletions

View File

@ -30,7 +30,7 @@ const actiontypes_userinfo = {
'STORE_AUTH_HEADERS' : 'STORE_AUTH_HEADERS', 'STORE_AUTH_HEADERS' : 'STORE_AUTH_HEADERS',
'REHYDRATE' : 'USERINFO_REHYDRATE', 'REHYDRATE' : 'USERINFO_REHYDRATE',
'CLEAR' : 'USERINFO_CLEAR', 'CLEAR' : 'USERINFO_CLEAR'
}; };
const defaultstate_userinfo = { const defaultstate_userinfo = {
@ -70,6 +70,16 @@ const defaultstate_tournamentinfo = {
teams : [] teams : []
}; };
const actiontypes_tournamentlist = {
'FETCH': 'FETCH',
'FETCH_SUCCESS': 'FETCH_SUCCESS',
'REHYDRATE': 'REHYDRATE'
};
const defaultstate_tournamentlist = {
tournaments: []
};
export function postRequest(state, url, data) { export function postRequest(state, url, data) {
return axios.post(api_url + url, data, { return axios.post(api_url + url, data, {
headers : generateHeaders(state) headers : generateHeaders(state)
@ -186,7 +196,8 @@ const reducer_userinfo = (state = defaultstate_userinfo, action) => {
__store.dispatch({ __store.dispatch({
type : actiontypes_userinfo.LOGIN_RESULT_SUCCESS, type : actiontypes_userinfo.LOGIN_RESULT_SUCCESS,
parameters : { parameters : {
username : resp.data.data.username, username : resp.data.username,
successCallback: action.parameters.successCallback
} }
}); });
storeOptionalToken(resp); storeOptionalToken(resp);
@ -210,6 +221,7 @@ const reducer_userinfo = (state = defaultstate_userinfo, action) => {
}); });
return Object.assign({}, state, {}); return Object.assign({}, state, {});
case actiontypes_userinfo.LOGIN_RESULT_SUCCESS: case actiontypes_userinfo.LOGIN_RESULT_SUCCESS:
action.parameters.successCallback(action.parameters.username);
return Object.assign({}, state, { return Object.assign({}, state, {
isSignedIn : true, isSignedIn : true,
error : false, error : false,
@ -223,6 +235,7 @@ const reducer_userinfo = (state = defaultstate_userinfo, action) => {
}); });
case actiontypes_userinfo.LOGOUT: case actiontypes_userinfo.LOGOUT:
deleteRequest(action.state, '/users/sign_out').then(() => { deleteRequest(action.state, '/users/sign_out').then(() => {
action.parameters.successCallback();
__store.dispatch({ type : actiontypes_userinfo.CLEAR }); __store.dispatch({ type : actiontypes_userinfo.CLEAR });
}).catch(() => { }).catch(() => {
__store.dispatch({ type : actiontypes_userinfo.CLEAR }); __store.dispatch({ type : actiontypes_userinfo.CLEAR });
@ -322,14 +335,40 @@ const reducer_tournamentinfo = (state = defaultstate_tournamentinfo, action) =>
} }
}; };
const reducer_tournamentlist = (state = defaultstate_tournamentlist, action) => {
switch (action.type) {
case actiontypes_tournamentlist.FETCH:
getRequest(action.state, '/tournaments?type=' + action.parameters.type).then((resp) => {
__store.dispatch({
type: actiontypes_tournamentlist.FETCH_SUCCESS,
parameters: resp.data
});
storeOptionalToken(resp);
action.parameters.successCallback(resp.data);
}).catch((error) => {
if(error.response) {
storeOptionalToken(error.response);
}
action.parameters.errorCallback();
});
return state;
case actiontypes_tournamentlist.FETCH_SUCCESS:
return Object.assign({}, state, {tournaments: action.parameters});
default:
return state;
}
};
const reducers = { const reducers = {
userinfo: reducer_userinfo, userinfo: reducer_userinfo,
tournamentinfo: reducer_tournamentinfo tournamentinfo: reducer_tournamentinfo,
tournamentlist: reducer_tournamentlist
}; };
const default_applicationstate = { const default_applicationstate = {
userinfo : defaultstate_userinfo, userinfo : defaultstate_userinfo,
tournamentinfo: defaultstate_tournamentinfo tournamentinfo: defaultstate_tournamentinfo,
tournamentlist: defaultstate_tournamentlist
}; };
var __store; var __store;
@ -372,20 +411,24 @@ export function register(username, email, password) {
}); });
} }
export function login(email, password) { export function login(email, password, successCallback) {
__store.dispatch({ __store.dispatch({
type: actiontypes_userinfo.LOGIN, type: actiontypes_userinfo.LOGIN,
parameters: { parameters: {
email: email, email: email,
password: password password: password,
successCallback: successCallback
}, },
state: __store.getState() state: __store.getState()
}); });
} }
export function logout() { export function logout(successCallback) {
__store.dispatch({ __store.dispatch({
type : actiontypes_userinfo.LOGOUT, type : actiontypes_userinfo.LOGOUT,
parameters: {
successCallback: successCallback
},
state: __store.getState() state: __store.getState()
}); });
} }
@ -431,6 +474,18 @@ export function getState() {
return __store.getState(); return __store.getState();
} }
export function requestTournamentList(type, successCallback, errorCallback) {
__store.dispatch({
type: actiontypes_tournamentlist.FETCH,
parameters: {
type: type,
successCallback: successCallback,
errorCallback: errorCallback
},
state: __store.getState()
});
}
function rehydrateApplicationState() { function rehydrateApplicationState() {
const persistedState = localStorage.getItem('reduxState') ? const persistedState = localStorage.getItem('reduxState') ?
JSON.parse(localStorage.getItem('reduxState')) : JSON.parse(localStorage.getItem('reduxState')) :
@ -445,6 +500,10 @@ function rehydrateApplicationState() {
type : actiontypes_tournamentinfo.REHYDRATE, type : actiontypes_tournamentinfo.REHYDRATE,
parameters : Object.assign({}, persistedState.tournamentinfo) parameters : Object.assign({}, persistedState.tournamentinfo)
}); });
__store.dispatch({
type : actiontypes_tournamentlist.REHYDRATE,
parameters : Object.assign({}, persistedState.tournamentlist)
});
applicationHydrated = true; applicationHydrated = true;
} }
} }

View File

@ -5,6 +5,9 @@ import Router from 'next/router';
import { login } from '../api'; import { login } from '../api';
import '../../static/css/errormessages.css';
import {notify} from 'react-notify-toast';
export function Login(props) { export function Login(props) {
return ( return (
<Container className="py-5"> <Container className="py-5">
@ -28,7 +31,7 @@ class LoginErrorList extends React.Component {
const { error, errorMessages } = this.props; const { error, errorMessages } = this.props;
if(error) { if(error) {
return ( return (
<ul className='text-danger mt-3'> <ul className='mt-3 error-box'>
{ errorMessages.map((message, index) => { errorMessages.map((message, index) =>
<li key={index}> <li key={index}>
{message} {message}
@ -81,7 +84,7 @@ class LoginForm extends React.Component {
tryLogin(event) { tryLogin(event) {
event.preventDefault(); event.preventDefault();
login(this.state.email, this.state.password); login(this.state.email, this.state.password, (username) => notify.show('Willkommen, ' + username + '!', 'success', 2500));
} }
render() { render() {

View File

@ -14,6 +14,7 @@ import { connect } from 'react-redux';
import React from 'react'; import React from 'react';
import { logout } from '../api'; import { logout } from '../api';
import {notify} from 'react-notify-toast';
export class TurniereNavigation extends React.Component { export class TurniereNavigation extends React.Component {
@ -40,11 +41,7 @@ export class TurniereNavigation extends React.Component {
<Betabadge/> <Betabadge/>
<NavbarToggler onClick={this.toggle} /> <NavbarToggler onClick={this.toggle} />
<Collapse isOpen={!this.state.collapsed} navbar> <Collapse isOpen={!this.state.collapsed} navbar>
<Nav navbar className="mr-auto"> <NavLinks/>
<Navlink target="/create" text="Turnier erstellen"/>
<Navlink target="/list" text="Öffentliche Turniere"/>
<Navlink target="/faq" text="FAQ"/>
</Nav>
<LoginLogoutButtons/> <LoginLogoutButtons/>
</Collapse> </Collapse>
</Navbar> </Navbar>
@ -60,12 +57,28 @@ function Navlink(props) {
); );
} }
class SmartNavLinks extends React.Component {
render() {
return (<Nav navbar className="mr-auto">
<Navlink target="/create" text="Turnier erstellen"/>
<Navlink target="/list" text="Öffentliche Turniere"/>
{this.props.isSignedIn && <Navlink target="/private" text="Private Turniere"/>}
<Navlink target="/faq" text="FAQ"/>
</Nav>);
}
}
function Betabadge() { function Betabadge() {
return (<Badge color="danger" className="mr-2">BETA</Badge>); return (<Badge color="danger" className="mr-2">BETA</Badge>);
} }
class InvisibleLoginLogoutButtons extends React.Component { class InvisibleLoginLogoutButtons extends React.Component {
logout(){
logout(() => notify.show('Du bist jetzt abgemeldet.', 'success', 2500));
}
render() { render() {
const { isSignedIn, username } = this.props; const { isSignedIn, username } = this.props;
@ -73,7 +86,7 @@ class InvisibleLoginLogoutButtons extends React.Component {
return ( return (
<ButtonGroup className="nav-item"> <ButtonGroup className="nav-item">
<Button outline color="success" href="/profile" className="navbar-btn my-2 my-sm-0 px-5">{ username }</Button> <Button outline color="success" href="/profile" className="navbar-btn my-2 my-sm-0 px-5">{ username }</Button>
<Button outline color="success" onClick={logout.bind(this)} className="navbar-btn my-2 my-sm-0 px-5">Logout</Button> <Button outline color="success" onClick={this.logout.bind(this)} className="navbar-btn my-2 my-sm-0 px-5">Logout</Button>
</ButtonGroup> </ButtonGroup>
); );
} else { } else {
@ -87,12 +100,15 @@ class InvisibleLoginLogoutButtons extends React.Component {
} }
} }
const mapStateToLoginLogoutButtonProperties = (state) => { const mapStateToUserinfo = (state) => {
const { isSignedIn, username } = state.userinfo; const { isSignedIn, username } = state.userinfo;
return { isSignedIn, username }; return { isSignedIn, username };
}; };
const LoginLogoutButtons = connect( const LoginLogoutButtons = connect(
mapStateToLoginLogoutButtonProperties mapStateToUserinfo
)(InvisibleLoginLogoutButtons); )(InvisibleLoginLogoutButtons);
const NavLinks = connect(
mapStateToUserinfo
)(SmartNavLinks);

View File

@ -0,0 +1,39 @@
import React from 'react';
import {requestTournamentList} from '../api';
export default class TournamentList extends React.Component {
constructor(props) {
super(props);
this.state = {
tournaments: []
};
}
componentDidMount() {
requestTournamentList(this.props.type, tournaments => {
this.setState({
tournaments: tournaments
});
}, () => {});
}
render() {
if (this.state.tournaments.length === 0) {
return <p className="text-center border-light font-italic text-secondary border-top border-bottom p-1">keine
Turniere vorhanden</p>;
} else {
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 TournamentListEntry(props) {
return (
<a className="w-100 d-inline-block mt-2 text-left btn btn-outline-primary" href={'/t/' + props.code}>
{props.name}
</a>
);
}

View File

@ -217,7 +217,8 @@ class CreateTournamentForm extends React.Component {
'name': this.state.name, 'name': this.state.name,
'description': this.state.description, 'description': this.state.description,
'public': this.state.public, 'public': this.state.public,
'teams': this.createTeamArray(this.state.teams) 'group_stage': this.state.groupPhaseEnabled,
'teams': createTeamArray(this.state.groupPhaseEnabled, this.state.groups, this.state.teams)
}, () => { }, () => {
notify.show('Das Turnier wurde erfolgreich erstellt.', 'success', 5000); notify.show('Das Turnier wurde erfolgreich erstellt.', 'success', 5000);
}, () => { }, () => {
@ -225,13 +226,41 @@ class CreateTournamentForm extends React.Component {
}); });
} }
createTeamArray(teamnames) { }
let result = [];
/**
for(let i = 0; i < teamnames.length; i++) { * This method creates an array of team objects that conform to the currently
result[i] = { 'name': teamnames[i] }; * api specs available at https://apidoc.turnie.re/
} *
* @param {boolean} groupphase Whether a group phase is to be created
return result; * @param {string[][]} groups The teams split into the groups that are
} * to be used in the group phase of the tournament. Please note that
* according to the api every team can only occur once (not enforced
* by this method) and that every team from {@param teams} will have
* to be in one of the groups (also not enforced by this method, but
* might lead to inconsistencies)
* @param {string[]} teams An array containing all names of the teams
* that are to be created for the tournament
* @return {Object[]} an array of teams that can be directly sent to the
* backend
*/
function createTeamArray(groupphase, groups, teams) {
let result = [];
if(groupphase) {
for(let groupNumber = 0; groupNumber < groups.length; groupNumber++) {
for(let groupMember = 0; groupMember < groups[groupNumber].length; groupMember++) {
result[result.length] = {
'name': groups[groupNumber][groupMember],
'group': groupNumber
};
}
}
} else {
for(let i = 0; i < teams.length; i++) {
result[i] = { 'name': teams[i] };
}
}
return result;
} }

View File

@ -1,21 +1,15 @@
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import { import {Card, CardBody, Container} from 'reactstrap';
Card,
CardBody,
Container
} from 'reactstrap';
import { TurniereNavigation } from '../js/components/Navigation'; import {TurniereNavigation} from '../js/components/Navigation';
import { Footer } from '../js/components/Footer'; import {Footer} from '../js/components/Footer';
import {
getRequest,
getState
} from '../js/api';
import '../static/everypage.css'; import '../static/everypage.css';
import TournamentList from '../js/components/TournamentList';
import {connect} from 'react-redux';
export default class ListPage extends React.Component { export default class PublicTournamentsPage extends React.Component {
render() { render() {
return ( return (
@ -25,7 +19,7 @@ export default class ListPage extends React.Component {
</Head> </Head>
<TurniereNavigation/> <TurniereNavigation/>
<div> <div>
<TournamentList/> <PublicTournamentPageContent/>
</div> </div>
<Footer/> <Footer/>
</div> </div>
@ -33,55 +27,38 @@ export default class ListPage extends React.Component {
} }
} }
class TournamentList extends React.Component { function mapStateToProperties(state) {
constructor(props) { const {isSignedIn} = state.userinfo;
super(props); return {isSignedIn};
this.state = { }
error: null,
isLoaded: false,
items: []
};
}
componentDidMount() { const PublicTournamentPageContent = connect(
getRequest(getState(), '/tournaments?type=public') mapStateToProperties,
.then( )(PublicTournaments);
response => {
this.setState({
isLoaded: true,
items: response.data
});
},
error => {
this.setState({
isLoaded: true,
error
});
}
);
}
render() { function PublicTournaments(props) {
return ( if (props.isSignedIn) {
<Container className="py-5"> return (<div>
<Card className="shadow"> <Container className='pt-5'>
<CardBody> <PublicTournamentsCard/>
<h1 className="custom-font">Öffentliche Turniere</h1>
{this.state.items.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}/>
))}
</CardBody>
</Card>
</Container> </Container>
); <Container className="pb-5 pt-3">
<a href='/private' className="btn btn-success shadow">zu den privaten Turnieren</a>
</Container>
</div>);
} else {
return (<Container className='py-5'>
<PublicTournamentsCard/>
</Container>);
} }
} }
function TournamentListEntry(props) { function PublicTournamentsCard() {
return ( return (<Card className="shadow">
<a className="w-100 d-inline-block mt-2 text-left btn btn-outline-primary" href={ '/t/' + props.code }> <CardBody>
{props.name} <h1 className="custom-font">Öffentliche Turniere</h1>
</a> <TournamentList type='public'/>
); </CardBody>
</Card>);
} }

83
pages/private.js Normal file
View File

@ -0,0 +1,83 @@
import Head from 'next/head';
import React from 'react';
import {connect} from 'react-redux';
import {Card, CardBody, Container,} from 'reactstrap';
import {TurniereNavigation} from '../js/components/Navigation';
import {Footer} from '../js/components/Footer';
import {Option, UserRestrictor} from '../js/components/UserRestrictor';
import {Login} from '../js/components/Login';
import '../static/everypage.css';
import TournamentList from '../js/components/TournamentList';
class PrivateTournamentsPage extends React.Component {
render() {
const {isSignedIn} = this.props;
return (
<UserRestrictor>
<Option condition={isSignedIn}>
<div className="main generic-fullpage-bg">
<Head>
<title>Private Turniere: turnie.re</title>
</Head>
<TurniereNavigation/>
<PrivateTournamentsPageContent/>
<Footer/>
</div>
</Option>
<Option condition={true}>
<div className="main generic-fullpage-bg">
<Head>
<title>Anmeldung</title>
</Head>
<TurniereNavigation/>
<div>
<Login
hint="Sie müssen angemeldet sein, um diesen Inhalt anzuzeigen!"/>
</div>
<Footer/>
</div>
</Option>
</UserRestrictor>
);
}
}
function mapStateToProperties(state) {
const {isSignedIn} = state.userinfo;
return {isSignedIn};
}
const PrivateTournamentListPage = connect(
mapStateToProperties,
)(PrivateTournamentsPage);
export default PrivateTournamentListPage;
function PrivateTournamentsPageContent() {
return (<div>
<Container className="pt-5">
<PrivateTournamentsCard/>
</Container>
<Container className="pb-5 pt-3">
<a href='/list' className="btn btn-success shadow">zu den öffentlichen Turnieren</a>
</Container>
</div>);
}
class PrivateTournamentsCard extends React.Component {
render() {
return (
<Card className="shadow">
<CardBody>
<h1 className="custom-font">Private Turniere</h1>
<TournamentList type='private'/>
</CardBody>
</Card>
);
}
}

View File

@ -17,10 +17,10 @@ import { TurniereNavigation } from '../js/components/Navigation';
import { Footer } from '../js/components/Footer'; import { Footer } from '../js/components/Footer';
import { register } from '../js/api'; import { register } from '../js/api';
import '../static/css/errormessages.css';
import '../static/everypage.css'; import '../static/everypage.css';
export default class RegisterPage extends React.Component { export default class RegisterPage extends React.Component {
render() { render() {
return ( return (
<div className="main generic-fullpage-bg"> <div className="main generic-fullpage-bg">
@ -62,11 +62,9 @@ class RegisterErrorList extends React.Component {
const { error, errorMessages } = this.props; const { error, errorMessages } = this.props;
if(error) { if(error) {
return ( return (
<ul className='text-danger mt-3'> <ul className="mt-3 error-box">
{ errorMessages.map((message, index) => { errorMessages.map((message, index) =>
<li key={index}> <li key={index}>{message}</li>
{message}
</li>
) } ) }
</ul> </ul>
); );
@ -86,7 +84,6 @@ const VisibleRegisterErrorList = connect(
)(RegisterErrorList); )(RegisterErrorList);
class RegisterForm extends React.Component { class RegisterForm extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);

View File

@ -0,0 +1,12 @@
.error-box {
border: 2px solid #dc3545;
border-radius: 4px;
padding: 8px 16px;
}
.error-box > li {
color: #dc3545;
margin-right: 36px;
list-style-type: none;
}