Add api calls with persistent user data

Added login and registration api calls to the function pool,
while the application state is persistet through localStorage.
Also (if signed in) the username will be displayed in the
navigation bar of the website.
This commit is contained in:
JP1998 2018-11-29 16:19:02 +01:00
parent f19b2d399c
commit 91b532b6cd
9 changed files with 3039 additions and 59 deletions

View File

@ -10,6 +10,9 @@ import {
NavItem,
NavLink
} from 'reactstrap';
import { connect } from 'react-redux'
import React from "react";
export function BigImage(props) {
@ -26,6 +29,7 @@ export class TurniereNavigation extends React.Component {
super(props);
this.toggle = this.toggle.bind(this);
this.state = {
collapsed: true
};
@ -68,15 +72,48 @@ function Betabadge() {
return (<Badge color="danger" className="mr-2">BETA</Badge>);
}
function LoginLogoutButtons() {
return (
<ButtonGroup className="nav-item">
<Button href="/login" className="btn navbar-btn btn-outline-success my-2 my-sm-0 px-5">Login</Button>
<Button href="/register" className="btn navbar-btn btn-outline-success my-2 my-sm-0 px-5">Registrieren</Button>
</ButtonGroup>
);
class InvisibleLoginLogoutButtons extends React.Component {
logoutClick() {
console.log("Logged out.");
/*
/users/sign_out <- DELETE Token invalidieren
/users/validate_token <- GET Token valide?
*/
}
render() {
const { isSignedIn, username, logout } = this.props
if(isSignedIn) {
return (
<ButtonGroup className="nav-item">
<Button href="/profile" className="btn navbar-btn btn-outline-success my-2 my-sm-0 px-5">{ username }</Button>
<Button onClick={this.logoutClick} className="btn navbar-btn btn-outline-success my-2 my-sm-0 px-5">Logout</Button>
</ButtonGroup>
);
} else {
return (
<ButtonGroup className="nav-item">
<Button href="/login" className="btn navbar-btn btn-outline-success my-2 my-sm-0 px-5">Login</Button>
<Button href="/register" className="btn navbar-btn btn-outline-success my-2 my-sm-0 px-5">Registrieren</Button>
</ButtonGroup>
);
}
}
}
const mapStateToLoginLogoutButtonProperties = (state) => {
const { isSignedIn, username } = state.userinfo;
return { isSignedIn, username }
}
const LoginLogoutButtons = connect(
mapStateToLoginLogoutButtonProperties
)(InvisibleLoginLogoutButtons)
export function Footer() {
return (
<footer className="footer mt-5 bg-dark text-light">

203
js/api.js Normal file
View File

@ -0,0 +1,203 @@
import { createStore, applyMiddleware, combineReducers } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'
import { errorMessages } from './constants'
const axios = require('axios');
const api_url = 'https://api.turnie.re';
const actiontypes_userinfo = {
'REGISTER' : 'REGISTER',
'REGISTER_RESULT_SUCCESS' : 'REGISTER_RESULT_SUCCESS',
'REGISTER_RESULT_ERROR' : 'REGISTER_RESULT_ERROR',
'LOGIN' : 'LOGIN',
'LOGIN_RESULT_SUCCESS' : 'LOGIN_RESULT_SUCCESS',
'LOGIN_RESULT_ERROR' : 'LOGIN_RESULT_ERROR',
'REHYDRATE' : 'USERINFO_REHYDRATE',
}
const defaultstate_userinfo = {
isSignedIn : false,
username : null,
error : false,
errorMessages : [],
accesstoken : null,
client : null,
expiry : null,
uid : null
}
const reducer_userinfo = (state = defaultstate_userinfo, action) => {
switch(action.type) {
case actiontypes_userinfo.REGISTER:
axios.post(api_url + '/users', {
'username' : action.parameters.username,
'email' : action.parameters.email,
'password' : action.parameters.password
}).then((resp) => {
__store.dispatch({
type : actiontypes_userinfo.REGISTER_RESULT_SUCCESS
})
}).catch((error) => {
if (error.response) {
__store.dispatch({
'type' : actiontypes_userinfo.REGISTER_RESULT_ERROR,
'parameters' : {
'errorMessages' : error.response.data.errors.full_messages
}
})
} else {
__store.dispatch({
'type' : actiontypes_userinfo.REGISTER_RESULT_ERROR,
'parameters' : {
'errorMessages' : [
errorMessages['registration_errorunknown']['en']
]
}
})
}
});
return Object.assign({}, state, {});
case actiontypes_userinfo.REGISTER_RESULT_SUCCESS:
return Object.assign({}, state, {
error : false,
errorMessages : []
});
case actiontypes_userinfo.REGISTER_RESULT_ERROR:
return Object.assign({}, state, {
error : true,
errorMessages : action.parameters.errorMessages
});
case actiontypes_userinfo.LOGIN:
axios.post(api_url + '/users/sign_in', {
email : action.parameters.email,
password : action.parameters.password
}).then((resp) => {
console.log(resp);
__store.dispatch({
type : actiontypes_userinfo.LOGIN_RESULT_SUCCESS,
parameters : {
username : resp.data.data.username,
accesstoken : resp.headers['access-token'],
client : resp.headers['client'],
expiry : resp.headers['expiry'],
uid : resp.headers['uid']
}
});
}).catch((error) => {
if(error.response) {
console.log(error.response);
__store.dispatch({
'type' : actiontypes_userinfo.LOGIN_RESULT_ERROR,
'parameters' : {
'errorMessages' : error.response.data.errors
}
})
} else {
console.log(error);
console.log(errorMessages['login_errorunknown']['en']);
__store.dispatch({
type : actiontypes_userinfo.LOGIN_RESULT_ERROR,
parameters : [
errorMessages['login_errorunknown']['en']
]
});
}
});
return Object.assign({}, state, {});
case actiontypes_userinfo.LOGIN_RESULT_SUCCESS:
return Object.assign({}, state, {
isSignedIn : true,
error : false,
errorMessages : [],
username : action.parameters.username,
accesstoken : action.parameters.accesstoken,
client : action.parameters.client,
expiry : action.parameters.expiry,
uid : action.parameters.uid
});
case actiontypes_userinfo.LOGIN_RESULT_ERROR:
return Object.assign({}, state, {
error : true,
errorMessages : action.parameters.errorMessages
});
case actiontypes_userinfo.REHYDRATE:
return Object.assign({}, state, action.parameters);
default: return state;
}
}
const reducers = {
userinfo: reducer_userinfo
}
const default_applicationstate = {
userinfo : defaultstate_userinfo
}
var __store;
export function initializeStore(initialState = default_applicationstate) {
__store = createStore(
combineReducers(reducers),
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware))
);
__store.subscribe(() => {
localStorage.setItem('reduxState', JSON.stringify(__store.getState()))
});
return __store;
}
export function verifyCredentials() {
rehydrateApplicationState();
}
export function register(username, email, password) {
__store.dispatch({
type: actiontypes_userinfo.REGISTER,
parameters: {
username: username,
email: email,
password: password
}
});
}
export function login(email, password) {
__store.dispatch({
type: actiontypes_userinfo.LOGIN,
parameters: {
email: email,
password: password
}
})
}
export function getState() {
return __store.getState();
}
function rehydrateApplicationState() {
const persistedState = localStorage.getItem('reduxState') ?
JSON.parse(localStorage.getItem('reduxState')) :
undefined;
if(persistedState) {
__store.dispatch({
type : actiontypes_userinfo.REHYDRATE,
parameters : Object.assign({}, persistedState.userinfo, {})
});
}
}

View File

@ -0,0 +1,9 @@
import React from "react";
export function BigImage(props) {
return (
<div className="big-image">
<h1 className="display-1">{props.text}</h1>
</div>
);
}

11
js/constants.js Normal file
View File

@ -0,0 +1,11 @@
export const errorMessages = {
registration_errorunknown : {
en : 'An unknown error prevented a successful registration.'
},
login_errorunknown : {
en : 'An unknown error prevented a successful login.'
}
}

View File

@ -0,0 +1,50 @@
import React from 'react'
import { initializeStore } from '../api'
const isServer = typeof window === 'undefined'
const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__'
function getOrCreateStore (initialState) {
// Always make a new store if server, otherwise state is shared between requests
if (isServer) {
return initializeStore(initialState)
}
// Create store if unavailable on the client and set it on the window object
if (!window[__NEXT_REDUX_STORE__]) {
window[__NEXT_REDUX_STORE__] = initializeStore(initialState)
}
return window[__NEXT_REDUX_STORE__]
}
export default (App) => {
return class AppWithRedux extends React.Component {
static async getInitialProps (appContext) {
// Get or Create the store with `undefined` as initialState
// This allows you to set a custom default initialState
const reduxStore = getOrCreateStore()
// Provide the store to getInitialProps of pages
appContext.ctx.reduxStore = reduxStore
let appProps = {}
if (typeof App.getInitialProps === 'function') {
appProps = await App.getInitialProps(appContext)
}
return {
...appProps,
initialReduxState: reduxStore.getState()
}
}
constructor (props) {
super(props)
this.reduxStore = getOrCreateStore(props.initialReduxState)
}
render () {
return <App {...this.props} reduxStore={this.reduxStore} />
}
}
}

View File

@ -20,7 +20,11 @@
"next": "^7.0.2",
"react": "^16.6.1",
"react-dom": "^16.6.1",
"reactstrap": "^6.5.0"
"react-redux": "^5.1.1",
"reactstrap": "^6.5.0",
"redux": "^4.0.1",
"redux-devtools-extension": "^2.13.7",
"redux-thunk": "^2.3.0"
},
"devDependencies": {
"react-editable-list": "0.0.3"

25
pages/_app.js Normal file
View File

@ -0,0 +1,25 @@
import App, {Container} from 'next/app'
import React from 'react'
import { Provider } from 'react-redux'
import withReduxStore from '../js/redux/reduxStoreBinder'
import { verifyCredentials } from '../js/api'
class TurniereApp extends App {
componentDidMount() {
verifyCredentials();
}
render () {
const {Component, pageProps, reduxStore} = this.props
return (
<Container>
<Provider store={reduxStore}>
<Component {...pageProps} />
</Provider>
</Container>
)
}
}
export default withReduxStore(TurniereApp)

View File

@ -5,11 +5,13 @@ import {Alert, Button, Card, CardBody} from 'reactstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import {BigImage, Footer, TurniereNavigation} from '../js/CommonComponents.js'
import { BigImage, Footer, TurniereNavigation } from '../js/CommonComponents.js'
import '../static/everypage.css'
import '../static/css/index.css'
import { connect } from 'react-redux'
function Main() {
return (
<div className="main">
@ -164,14 +166,21 @@ function PromotedLinkCreateTournament() {
</Card>);
}
export default () => (
<div>
<Head>
<title>turnie.re</title>
</Head>
<TurniereNavigation/>
<BigImage text="Einfach Turniere organisieren"/>
<Main/>
<Footer/>
</div>
);
class Index extends React.Component {
render () {
return (
<div>
<Head>
<title>turnie.re</title>
</Head>
<TurniereNavigation/>
<BigImage text="Einfach Turniere organisieren"/>
<Main/>
<Footer/>
</div>
);
}
}
export default connect()(Index);

2710
yarn.lock

File diff suppressed because it is too large Load Diff