diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..1744b88 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,38 @@ +{ + "env": { + "browser": true, + "es6": true, + "node": true + }, + "plugins": [ + "react" + ], + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "rules": { + "react/jsx-uses-react": "error", + "react/jsx-uses-vars": "error", + "indent": [ + "error", + 4 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ] + } +} diff --git a/.hound.yml b/.hound.yml new file mode 100644 index 0000000..fc4f352 --- /dev/null +++ b/.hound.yml @@ -0,0 +1,8 @@ +fail_on_violations: true + +jshint: + enabled: false + +eslint: + enabled: true + config_file: .eslintrc.json diff --git a/js/CommonComponents.js b/js/CommonComponents.js index b8407ae..408ab37 100644 --- a/js/CommonComponents.js +++ b/js/CommonComponents.js @@ -11,9 +11,11 @@ import { NavLink } from 'reactstrap'; -import { connect } from 'react-redux' +import { connect } from 'react-redux'; -import React from "react"; +import React from 'react'; + +import { logout } from './api'; export function BigImage(props) { return ( @@ -74,23 +76,14 @@ function Betabadge() { 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 + const { isSignedIn, username } = this.props; if(isSignedIn) { return ( - + ); } else { @@ -107,12 +100,12 @@ class InvisibleLoginLogoutButtons extends React.Component { const mapStateToLoginLogoutButtonProperties = (state) => { const { isSignedIn, username } = state.userinfo; - return { isSignedIn, username } -} + return { isSignedIn, username }; +}; const LoginLogoutButtons = connect( mapStateToLoginLogoutButtonProperties -)(InvisibleLoginLogoutButtons) +)(InvisibleLoginLogoutButtons); export function Footer() { return ( diff --git a/js/EditableStringList.js b/js/EditableStringList.js index a7cfda0..64ef704 100644 --- a/js/EditableStringList.js +++ b/js/EditableStringList.js @@ -1,5 +1,5 @@ -import React from "react"; -import {Alert, Button, Input, InputGroup, InputGroupAddon} from "reactstrap"; +import React from 'react'; +import { Alert, Button, Input, InputGroup, InputGroupAddon } from 'reactstrap'; export default class EditableStringList extends React.Component { constructor(props) { @@ -31,17 +31,14 @@ export default class EditableStringList extends React.Component { if ((typeof this.state.entries !== 'undefined') && this.state.entries.length > 0) { return (
- - {this.state.entries.map((text) => )} + + {this.state.entries.map((text) => )}
); } else { return (
- + {this.props.placeholder}
); @@ -64,8 +61,7 @@ class StringInput extends React.Component { render() { return ( - { + { if (e.key === 'Enter') { this.submit(); return false; @@ -73,10 +69,10 @@ class StringInput extends React.Component { }}/> + onClick={() => this.submit()}>{this.props.addButtonText} - ) + ); } submit() { diff --git a/js/api.js b/js/api.js index 9f45781..3c5db29 100644 --- a/js/api.js +++ b/js/api.js @@ -1,26 +1,32 @@ -import { createStore, applyMiddleware, combineReducers } from 'redux' -import { composeWithDevTools } from 'redux-devtools-extension' -import thunkMiddleware from 'redux-thunk' -import { errorMessages } from './constants' +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', + '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', + 'LOGIN' : 'LOGIN', + 'LOGIN_RESULT_SUCCESS' : 'LOGIN_RESULT_SUCCESS', + 'LOGIN_RESULT_ERROR' : 'LOGIN_RESULT_ERROR', + + 'LOGOUT' : 'LOGOUT', - 'STORE_AUTH_HEADERS' : 'STORE_AUTH_HEADERS', + 'VERIFY_CREDENTIALS' : 'VERIFY_CREDENTIALS', + 'VERIFY_CREDENTIALS_SUCCESS' : 'VERIFY_CREDENTIALS_SUCCESS', + 'VERIFY_CREDENTIALS_ERROR' : 'VERIFY_CREDENTIALS_ERROR', - 'REHYDRATE' : 'USERINFO_REHYDRATE', -} + 'STORE_AUTH_HEADERS' : 'STORE_AUTH_HEADERS', + + 'REHYDRATE' : 'USERINFO_REHYDRATE', + 'CLEAR' : 'USERINFO_CLEAR', +}; const defaultstate_userinfo = { isSignedIn : false, @@ -32,33 +38,32 @@ const defaultstate_userinfo = { client : null, expiry : null, uid : null -} +}; -export function postRequest(url, data) { +export function postRequest(state, url, data) { return axios.post(api_url + url, data, { - headers : generateHeaders() + headers : generateHeaders(state) }); } -export function getRequest(url, data) { - return axios.get(api_url + url, data, { - headers : generateHeaders() +export function getRequest(state, url) { + return axios.get(api_url + url, { + headers : generateHeaders(state) }); } -export function deleteRequest(url, data) { - return axios.delete(api_url + url, data, { - headers : generateHeaders() +export function deleteRequest(state, url) { + return axios.delete(api_url + url, { + headers : generateHeaders(state) }); } -function generateHeaders() { - var userinfostate = __store.getState().userinfo; - if(userinfostate.isSignedIn) { +function generateHeaders(state) { + if(state.isSignedIn) { return { - 'access-token' : userinfostate.accesstoken, - 'client' : userinfostate.client, - 'uid' : userinfostate.uid + 'access-token' : state.accesstoken, + 'client' : state.client, + 'uid' : state.uid }; } else { return {}; @@ -70,136 +75,162 @@ function storeOptionalToken(response) { __store.dispatch({ type : actiontypes_userinfo.STORE_AUTH_HEADERS, parameters : { - accesstoken : resp.headers['access-token'], - client : resp.headers['client'], - expiry : resp.headers['expiry'], - uid : resp.headers['uid'] + accesstoken : response.headers['access-token'], + client : response.headers['client'], + expiry : response.headers['expiry'], + uid : response.headers['uid'] } - }) + }); } } function checkForAuthenticationHeaders(response) { if(response.headers) { const requiredHeaders = [ - 'access-token', 'client', 'uid', 'expiry', // TODO: Add last header that is required (I don't remember it right now lol) + 'access-token', 'client', 'uid', 'expiry' ]; for(var i = 0; i < requiredHeaders.length; i++) { if(!response.headers[requiredHeaders[i]]) { return false; } } - return false; + return true; } return false; } const reducer_userinfo = (state = defaultstate_userinfo, action) => { switch(action.type) { - case actiontypes_userinfo.REGISTER: - postRequest('/users', { - 'username' : action.parameters.username, - 'email' : action.parameters.email, - 'password' : action.parameters.password - }).then((resp) => { + case actiontypes_userinfo.REGISTER: + postRequest(state, '/users', { + 'username' : action.parameters.username, + 'email' : action.parameters.email, + 'password' : action.parameters.password + }).then((resp) => { + __store.dispatch({ + type : actiontypes_userinfo.REGISTER_RESULT_SUCCESS + }); + storeOptionalToken(resp); + }).catch((error) => { + if (error.response) { __store.dispatch({ - type : actiontypes_userinfo.REGISTER_RESULT_SUCCESS - }); - storeOptionalToken(resp); - }).catch((error) => { - if (error.response) { - __store.dispatch({ - 'type' : actiontypes_userinfo.REGISTER_RESULT_ERROR, - 'parameters' : { - 'errorMessages' : error.response.data.errors.full_messages - } - }); - storeOptionalToken(error.response); - } 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: - postRequest('/users/sign_in', { - email : action.parameters.email, - password : action.parameters.password - }).then((resp) => { - __store.dispatch({ - type : actiontypes_userinfo.LOGIN_RESULT_SUCCESS, - parameters : { - username : resp.data.data.username, + 'type' : actiontypes_userinfo.REGISTER_RESULT_ERROR, + 'parameters' : { + 'errorMessages' : error.response.data.errors.full_messages } }); - storeOptionalToken(resp); - }).catch((error) => { - if(error.response) { - __store.dispatch({ - 'type' : actiontypes_userinfo.LOGIN_RESULT_ERROR, - 'parameters' : { - 'errorMessages' : error.response.data.errors - } - }); - storeOptionalToken(error.response); - } else { - __store.dispatch({ - 'type' : actiontypes_userinfo.LOGIN_RESULT_ERROR, - 'parameters' : { - 'errorMessages' : [ errorMessages['login_errorunknown']['en'] ] - } - }); + storeOptionalToken(error.response); + } 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: + postRequest(state, '/users/sign_in', { + email : action.parameters.email, + password : action.parameters.password + }).then((resp) => { + __store.dispatch({ + type : actiontypes_userinfo.LOGIN_RESULT_SUCCESS, + parameters : { + username : resp.data.data.username, } }); - return Object.assign({}, state, {}); - case actiontypes_userinfo.LOGIN_RESULT_SUCCESS: - return Object.assign({}, state, { - isSignedIn : true, - error : false, - errorMessages : [], - username : action.parameters.username, - }); - case actiontypes_userinfo.LOGIN_RESULT_ERROR: - return Object.assign({}, state, { - error : true, - errorMessages : action.parameters.errorMessages - }); - case actiontypes_userinfo.STORE_AUTH_HEADERS: - return Object.assign({}, state, { - accesstoken : action.parameters.accesstoken, - client : action.parameters.client, - expiry : action.parameters.expiry, - uid : action.parameters.uid - }); - case actiontypes_userinfo.REHYDRATE: - return Object.assign({}, state, action.parameters); - default: return state; + storeOptionalToken(resp); + }).catch((error) => { + if(error.response) { + __store.dispatch({ + 'type' : actiontypes_userinfo.LOGIN_RESULT_ERROR, + 'parameters' : { + 'errorMessages' : error.response.data.errors + } + }); + storeOptionalToken(error.response); + } else { + __store.dispatch({ + 'type' : actiontypes_userinfo.LOGIN_RESULT_ERROR, + 'parameters' : { + 'errorMessages' : [ 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, + }); + case actiontypes_userinfo.LOGIN_RESULT_ERROR: + return Object.assign({}, state, { + error : true, + errorMessages : action.parameters.errorMessages + }); + case actiontypes_userinfo.LOGOUT: + deleteRequest(state, '/users/sign_out').then(() => { + __store.dispatch({ type : actiontypes_userinfo.CLEAR }); + }).catch(() => { + __store.dispatch({ type : actiontypes_userinfo.CLEAR }); + }); + return Object.assign({}, state, {}); + case actiontypes_userinfo.STORE_AUTH_HEADERS: + return Object.assign({}, state, { + accesstoken : action.parameters.accesstoken, + client : action.parameters.client, + expiry : action.parameters.expiry, + uid : action.parameters.uid + }); + case actiontypes_userinfo.VERIFY_CREDENTIALS: + getRequest(state, '/users/validate_token').then((resp) => { + storeOptionalToken(resp); + }).catch(() => { + __store.dispatch({ type: actiontypes_userinfo.CLEAR }); + }); + return Object.assign({}, state, {}); + case actiontypes_userinfo.REHYDRATE: + return Object.assign({}, state, action.parameters); + case actiontypes_userinfo.CLEAR: + return Object.assign({}, state, { + isSignedIn : false, + username : null, + error : false, + errorMessages : [], + + accesstoken : null, + client : null, + expiry : null, + uid : null + }); + default: return state; } -} +}; const reducers = { userinfo: reducer_userinfo -} +}; const default_applicationstate = { userinfo : defaultstate_userinfo -} +}; var __store; @@ -210,7 +241,7 @@ export function initializeStore(initialState = default_applicationstate) { composeWithDevTools(applyMiddleware(thunkMiddleware)) ); __store.subscribe(() => { - localStorage.setItem('reduxState', JSON.stringify(__store.getState())) + localStorage.setItem('reduxState', JSON.stringify(__store.getState())); }); return __store; } @@ -218,7 +249,9 @@ export function initializeStore(initialState = default_applicationstate) { export function verifyCredentials() { rehydrateApplicationState(); - // TODO: Actually perform a verification of the loaded credentials + if(__store.getState().userinfo.isSignedIn) { + __store.dispatch({ type: actiontypes_userinfo.VERIFY_CREDENTIALS }); + } } export function register(username, email, password) { @@ -239,7 +272,11 @@ export function login(email, password) { email: email, password: password } - }) + }); +} + +export function logout() { + __store.dispatch({ type : actiontypes_userinfo.LOGOUT }); } export function getState() { diff --git a/js/components/BigImage.js b/js/components/BigImage.js index e442ab7..b89f6ff 100644 --- a/js/components/BigImage.js +++ b/js/components/BigImage.js @@ -1,4 +1,4 @@ -import React from "react"; +import React from 'react'; export function BigImage(props) { return ( diff --git a/js/constants.js b/js/constants.js index a0bc2e2..80ed3d7 100644 --- a/js/constants.js +++ b/js/constants.js @@ -7,5 +7,5 @@ export const errorMessages = { login_errorunknown : { en : 'An unknown error prevented a successful login.' } -} +}; diff --git a/js/redux/reduxStoreBinder.js b/js/redux/reduxStoreBinder.js index e685a1f..179d2a0 100644 --- a/js/redux/reduxStoreBinder.js +++ b/js/redux/reduxStoreBinder.js @@ -1,20 +1,20 @@ -import React from 'react' -import { initializeStore } from '../api' +import React from 'react'; +import { initializeStore } from '../api'; -const isServer = typeof window === 'undefined' -const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__' +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) + 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) + window[__NEXT_REDUX_STORE__] = initializeStore(initialState); } - return window[__NEXT_REDUX_STORE__] + return window[__NEXT_REDUX_STORE__]; } export default (App) => { @@ -22,29 +22,29 @@ export default (App) => { 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() + const reduxStore = getOrCreateStore(); // Provide the store to getInitialProps of pages - appContext.ctx.reduxStore = reduxStore + appContext.ctx.reduxStore = reduxStore; - let appProps = {} + let appProps = {}; if (typeof App.getInitialProps === 'function') { - appProps = await App.getInitialProps(appContext) + appProps = await App.getInitialProps(appContext); } return { ...appProps, initialReduxState: reduxStore.getState() - } + }; } constructor (props) { - super(props) - this.reduxStore = getOrCreateStore(props.initialReduxState) + super(props); + this.reduxStore = getOrCreateStore(props.initialReduxState); } render () { - return + return ; } - } -} + }; +}; diff --git a/next.config.js b/next.config.js index f8bd78a..f332296 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,3 @@ -const withCSS = require('@zeit/next-css') -module.exports = withCSS() \ No newline at end of file +const withCSS = require('@zeit/next-css'); +module.exports = withCSS(); \ No newline at end of file diff --git a/package.json b/package.json index cbe7f53..50ad571 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "redux-thunk": "^2.3.0" }, "devDependencies": { + "eslint": "^5.9.0", + "eslint-plugin-react": "^7.11.1", "react-editable-list": "0.0.3" } } diff --git a/pages/_app.js b/pages/_app.js index 9edc4f4..3bf91cd 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -1,25 +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' +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(); - } + componentDidMount() { + verifyCredentials(); + } - render () { - const {Component, pageProps, reduxStore} = this.props - return ( - - - - - - ) - } + render () { + const {Component, pageProps, reduxStore} = this.props; + return ( + + + + + + ); + } } -export default withReduxStore(TurniereApp) +export default withReduxStore(TurniereApp); diff --git a/pages/_error.js b/pages/_error.js index 300cb4a..202f1ca 100644 --- a/pages/_error.js +++ b/pages/_error.js @@ -1,15 +1,15 @@ -import Head from 'next/head' -import React from 'react' -import {Footer, TurniereNavigation} from "../js/CommonComponents"; +import Head from 'next/head'; +import React from 'react'; +import {Footer, TurniereNavigation} from '../js/CommonComponents'; import 'bootstrap/dist/css/bootstrap.min.css'; -import {Container} from "reactstrap"; -import '../static/everypage.css' -import '../static/css/error.css' +import {Container} from 'reactstrap'; +import '../static/everypage.css'; +import '../static/css/error.css'; export default class Error extends React.Component { static getInitialProps({ res, err }) { const statusCode = res ? res.statusCode : err ? err.statusCode : 400; - return { statusCode } + return { statusCode }; } render() { @@ -22,7 +22,7 @@ export default class Error extends React.Component {