Merge branch 'master' into ticket/TURNIERE-156
This commit is contained in:
commit
b30a3a759e
73
js/api.js
73
js/api.js
|
|
@ -30,7 +30,7 @@ const actiontypes_userinfo = {
|
|||
'STORE_AUTH_HEADERS' : 'STORE_AUTH_HEADERS',
|
||||
|
||||
'REHYDRATE' : 'USERINFO_REHYDRATE',
|
||||
'CLEAR' : 'USERINFO_CLEAR',
|
||||
'CLEAR' : 'USERINFO_CLEAR'
|
||||
};
|
||||
|
||||
const defaultstate_userinfo = {
|
||||
|
|
@ -70,6 +70,16 @@ const defaultstate_tournamentinfo = {
|
|||
teams : []
|
||||
};
|
||||
|
||||
const actiontypes_tournamentlist = {
|
||||
'FETCH': 'FETCH',
|
||||
'FETCH_SUCCESS': 'FETCH_SUCCESS',
|
||||
'REHYDRATE': 'REHYDRATE'
|
||||
};
|
||||
|
||||
const defaultstate_tournamentlist = {
|
||||
tournaments: []
|
||||
};
|
||||
|
||||
export function postRequest(state, url, data) {
|
||||
return axios.post(api_url + url, data, {
|
||||
headers : generateHeaders(state)
|
||||
|
|
@ -186,7 +196,8 @@ const reducer_userinfo = (state = defaultstate_userinfo, action) => {
|
|||
__store.dispatch({
|
||||
type : actiontypes_userinfo.LOGIN_RESULT_SUCCESS,
|
||||
parameters : {
|
||||
username : resp.data.data.username,
|
||||
username : resp.data.username,
|
||||
successCallback: action.parameters.successCallback
|
||||
}
|
||||
});
|
||||
storeOptionalToken(resp);
|
||||
|
|
@ -210,6 +221,7 @@ const reducer_userinfo = (state = defaultstate_userinfo, action) => {
|
|||
});
|
||||
return Object.assign({}, state, {});
|
||||
case actiontypes_userinfo.LOGIN_RESULT_SUCCESS:
|
||||
action.parameters.successCallback(action.parameters.username);
|
||||
return Object.assign({}, state, {
|
||||
isSignedIn : true,
|
||||
error : false,
|
||||
|
|
@ -223,6 +235,7 @@ const reducer_userinfo = (state = defaultstate_userinfo, action) => {
|
|||
});
|
||||
case actiontypes_userinfo.LOGOUT:
|
||||
deleteRequest(action.state, '/users/sign_out').then(() => {
|
||||
action.parameters.successCallback();
|
||||
__store.dispatch({ type : actiontypes_userinfo.CLEAR });
|
||||
}).catch(() => {
|
||||
__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 = {
|
||||
userinfo: reducer_userinfo,
|
||||
tournamentinfo: reducer_tournamentinfo
|
||||
tournamentinfo: reducer_tournamentinfo,
|
||||
tournamentlist: reducer_tournamentlist
|
||||
};
|
||||
|
||||
const default_applicationstate = {
|
||||
userinfo : defaultstate_userinfo,
|
||||
tournamentinfo: defaultstate_tournamentinfo
|
||||
tournamentinfo: defaultstate_tournamentinfo,
|
||||
tournamentlist: defaultstate_tournamentlist
|
||||
};
|
||||
|
||||
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({
|
||||
type: actiontypes_userinfo.LOGIN,
|
||||
parameters: {
|
||||
email: email,
|
||||
password: password
|
||||
password: password,
|
||||
successCallback: successCallback
|
||||
},
|
||||
state: __store.getState()
|
||||
});
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
export function logout(successCallback) {
|
||||
__store.dispatch({
|
||||
type : actiontypes_userinfo.LOGOUT,
|
||||
parameters: {
|
||||
successCallback: successCallback
|
||||
},
|
||||
state: __store.getState()
|
||||
});
|
||||
}
|
||||
|
|
@ -431,6 +474,18 @@ export function 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() {
|
||||
const persistedState = localStorage.getItem('reduxState') ?
|
||||
JSON.parse(localStorage.getItem('reduxState')) :
|
||||
|
|
@ -445,6 +500,10 @@ function rehydrateApplicationState() {
|
|||
type : actiontypes_tournamentinfo.REHYDRATE,
|
||||
parameters : Object.assign({}, persistedState.tournamentinfo)
|
||||
});
|
||||
__store.dispatch({
|
||||
type : actiontypes_tournamentlist.REHYDRATE,
|
||||
parameters : Object.assign({}, persistedState.tournamentlist)
|
||||
});
|
||||
applicationHydrated = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@ import Router from 'next/router';
|
|||
|
||||
import { login } from '../api';
|
||||
|
||||
import '../../static/css/errormessages.css';
|
||||
import {notify} from 'react-notify-toast';
|
||||
|
||||
export function Login(props) {
|
||||
return (
|
||||
<Container className="py-5">
|
||||
|
|
@ -28,7 +31,7 @@ class LoginErrorList extends React.Component {
|
|||
const { error, errorMessages } = this.props;
|
||||
if(error) {
|
||||
return (
|
||||
<ul className='text-danger mt-3'>
|
||||
<ul className='mt-3 error-box'>
|
||||
{ errorMessages.map((message, index) =>
|
||||
<li key={index}>
|
||||
{message}
|
||||
|
|
@ -81,7 +84,7 @@ class LoginForm extends React.Component {
|
|||
|
||||
tryLogin(event) {
|
||||
event.preventDefault();
|
||||
login(this.state.email, this.state.password);
|
||||
login(this.state.email, this.state.password, (username) => notify.show('Willkommen, ' + username + '!', 'success', 2500));
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { connect } from 'react-redux';
|
|||
import React from 'react';
|
||||
|
||||
import { logout } from '../api';
|
||||
import {notify} from 'react-notify-toast';
|
||||
|
||||
export class TurniereNavigation extends React.Component {
|
||||
|
||||
|
|
@ -40,11 +41,7 @@ export class TurniereNavigation extends React.Component {
|
|||
<Betabadge/>
|
||||
<NavbarToggler onClick={this.toggle} />
|
||||
<Collapse isOpen={!this.state.collapsed} navbar>
|
||||
<Nav navbar className="mr-auto">
|
||||
<Navlink target="/create" text="Turnier erstellen"/>
|
||||
<Navlink target="/list" text="Öffentliche Turniere"/>
|
||||
<Navlink target="/faq" text="FAQ"/>
|
||||
</Nav>
|
||||
<NavLinks/>
|
||||
<LoginLogoutButtons/>
|
||||
</Collapse>
|
||||
</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() {
|
||||
return (<Badge color="danger" className="mr-2">BETA</Badge>);
|
||||
}
|
||||
|
||||
class InvisibleLoginLogoutButtons extends React.Component {
|
||||
|
||||
logout(){
|
||||
logout(() => notify.show('Du bist jetzt abgemeldet.', 'success', 2500));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isSignedIn, username } = this.props;
|
||||
|
||||
|
|
@ -73,7 +86,7 @@ class InvisibleLoginLogoutButtons extends React.Component {
|
|||
return (
|
||||
<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" 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>
|
||||
);
|
||||
} else {
|
||||
|
|
@ -87,12 +100,15 @@ class InvisibleLoginLogoutButtons extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
const mapStateToLoginLogoutButtonProperties = (state) => {
|
||||
const mapStateToUserinfo = (state) => {
|
||||
const { isSignedIn, username } = state.userinfo;
|
||||
return { isSignedIn, username };
|
||||
};
|
||||
|
||||
const LoginLogoutButtons = connect(
|
||||
mapStateToLoginLogoutButtonProperties
|
||||
mapStateToUserinfo
|
||||
)(InvisibleLoginLogoutButtons);
|
||||
|
||||
const NavLinks = connect(
|
||||
mapStateToUserinfo
|
||||
)(SmartNavLinks);
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -217,7 +217,8 @@ class CreateTournamentForm extends React.Component {
|
|||
'name': this.state.name,
|
||||
'description': this.state.description,
|
||||
'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);
|
||||
}, () => {
|
||||
|
|
@ -225,13 +226,41 @@ class CreateTournamentForm extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
createTeamArray(teamnames) {
|
||||
let result = [];
|
||||
|
||||
for(let i = 0; i < teamnames.length; i++) {
|
||||
result[i] = { 'name': teamnames[i] };
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method creates an array of team objects that conform to the currently
|
||||
* api specs available at https://apidoc.turnie.re/
|
||||
*
|
||||
* @param {boolean} groupphase Whether a group phase is to be created
|
||||
* @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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,16 @@
|
|||
import Head from 'next/head';
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
Container
|
||||
} from 'reactstrap';
|
||||
import Head from 'next/head';
|
||||
import React from 'react';
|
||||
import {Card, CardBody, Container} from 'reactstrap';
|
||||
|
||||
import { TurniereNavigation } from '../js/components/Navigation';
|
||||
import { Footer } from '../js/components/Footer';
|
||||
import {
|
||||
getRequest,
|
||||
getState
|
||||
} from '../js/api';
|
||||
import {TurniereNavigation} from '../js/components/Navigation';
|
||||
import {Footer} from '../js/components/Footer';
|
||||
|
||||
import '../static/everypage.css';
|
||||
import TournamentList from '../js/components/TournamentList';
|
||||
import {connect} from 'react-redux';
|
||||
|
||||
export default class PublicTournamentsPage extends React.Component {
|
||||
|
||||
export default class ListPage extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="main generic-fullpage-bg">
|
||||
|
|
@ -25,7 +19,7 @@ export default class ListPage extends React.Component {
|
|||
</Head>
|
||||
<TurniereNavigation/>
|
||||
<div>
|
||||
<TournamentList/>
|
||||
<PublicTournamentPageContent/>
|
||||
</div>
|
||||
<Footer/>
|
||||
</div>
|
||||
|
|
@ -33,55 +27,38 @@ export default class ListPage extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
class TournamentList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: null,
|
||||
isLoaded: false,
|
||||
items: []
|
||||
};
|
||||
}
|
||||
function mapStateToProperties(state) {
|
||||
const {isSignedIn} = state.userinfo;
|
||||
return {isSignedIn};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
getRequest(getState(), '/tournaments?type=public')
|
||||
.then(
|
||||
response => {
|
||||
this.setState({
|
||||
isLoaded: true,
|
||||
items: response.data
|
||||
});
|
||||
},
|
||||
error => {
|
||||
this.setState({
|
||||
isLoaded: true,
|
||||
error
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
const PublicTournamentPageContent = connect(
|
||||
mapStateToProperties,
|
||||
)(PublicTournaments);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Container className="py-5">
|
||||
<Card className="shadow">
|
||||
<CardBody>
|
||||
<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>
|
||||
function PublicTournaments(props) {
|
||||
if (props.isSignedIn) {
|
||||
return (<div>
|
||||
<Container className='pt-5'>
|
||||
<PublicTournamentsCard/>
|
||||
</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) {
|
||||
return (
|
||||
<a className="w-100 d-inline-block mt-2 text-left btn btn-outline-primary" href={ '/t/' + props.code }>
|
||||
{props.name}
|
||||
</a>
|
||||
);
|
||||
function PublicTournamentsCard() {
|
||||
return (<Card className="shadow">
|
||||
<CardBody>
|
||||
<h1 className="custom-font">Öffentliche Turniere</h1>
|
||||
<TournamentList type='public'/>
|
||||
</CardBody>
|
||||
</Card>);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,10 +17,10 @@ import { TurniereNavigation } from '../js/components/Navigation';
|
|||
import { Footer } from '../js/components/Footer';
|
||||
import { register } from '../js/api';
|
||||
|
||||
import '../static/css/errormessages.css';
|
||||
import '../static/everypage.css';
|
||||
|
||||
export default class RegisterPage extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="main generic-fullpage-bg">
|
||||
|
|
@ -62,11 +62,9 @@ class RegisterErrorList extends React.Component {
|
|||
const { error, errorMessages } = this.props;
|
||||
if(error) {
|
||||
return (
|
||||
<ul className='text-danger mt-3'>
|
||||
<ul className="mt-3 error-box">
|
||||
{ errorMessages.map((message, index) =>
|
||||
<li key={index}>
|
||||
{message}
|
||||
</li>
|
||||
<li key={index}>{message}</li>
|
||||
) }
|
||||
</ul>
|
||||
);
|
||||
|
|
@ -86,7 +84,6 @@ const VisibleRegisterErrorList = connect(
|
|||
)(RegisterErrorList);
|
||||
|
||||
class RegisterForm extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue