diff --git a/api/api.js b/api/api.js new file mode 100644 index 0000000..c3760ce --- /dev/null +++ b/api/api.js @@ -0,0 +1,35 @@ +var express = require('express'); +var path = require('path'); + +var app = express(); + +var isProduction = process.env.NODE_ENV === 'production'; +var port = isProduction ? process.env.PORT : 3001; + +var bodyParser = require('body-parser'); +app.use(bodyParser.json({ type: 'application/json' })) + +app.use(function(req, res, next) { + res.header('Access-Control-Allow-Origin', 'http://localhost:3000'); + res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + + next(); +}); + +app.post('/api/login', function(req, res) { + const credentials = req.body; + if(credentials.user==='admin' && credentials.password==='password'){ + res.json({'user': credentials.user, 'role': 'ADMIN'}); + }else{ + res.status('500').send({'message' : 'Invalid user/password'}); + } +}); + +app.post('/api/logout', function(req, res) { + res.json({'user': 'admin', 'role': 'ADMIN'}); +}); + +app.listen(port, function () { + console.log('Server running on port ' + port); +}); \ No newline at end of file diff --git a/package.json b/package.json index 75b6086..19daf49 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build:webpack": "NODE_ENV=production webpack --config webpack.config.prod.js", "build": "npm run clean && npm run build:webpack", "start": "npm run clean && node devServer.js", + "api": "node ./api/api.js", "lint": "eslint src" }, "repository": { @@ -32,6 +33,7 @@ }, "homepage": "https://github.com/jsdmc/react-redux-router-crud-boilerplate", "dependencies": { + "axios": "^0.7.0", "classnames": "^2.1.5", "history": "^1.12.0", "react": "^0.14.0-rc1", diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx new file mode 100644 index 0000000..77a8dd8 --- /dev/null +++ b/src/components/Header/Header.jsx @@ -0,0 +1,54 @@ +import React, { Component } from 'react'; +// import { Link } from 'react-router'; +import './Header.scss'; + +export default class Header extends Component { + onLogoutClick() { + event.preventDefault(); + // this.props.handleLogout(); + } + + render() { + return ( + + ); + } +} \ No newline at end of file diff --git a/src/components/Header/Header.scss b/src/components/Header/Header.scss new file mode 100644 index 0000000..705437d --- /dev/null +++ b/src/components/Header/Header.scss @@ -0,0 +1,15 @@ +:global { + .navbar-brand { + position:relative; + padding-left: 60; + } + + .nav .header_fa { + font-size: 1em; + margin-right: 0.5em; + } + + .navbar-default .navbar-nav>li>a:focus { + outline: none; + } +} \ No newline at end of file diff --git a/src/components/index.js b/src/components/index.js index 2a8e075..b2cc997 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -5,6 +5,7 @@ * */ +export Header from './Header/Header'; export AutoCounter from './AutoCounter/AutoCounter'; export Counter from './Counter'; export SmartLink from './SmartLink/SmartLink'; diff --git a/src/containers/CoreLayout.js b/src/containers/CoreLayout.js index ac95f8b..04b51ba 100644 --- a/src/containers/CoreLayout.js +++ b/src/containers/CoreLayout.js @@ -1,5 +1,5 @@ import React, { Component, PropTypes } from 'react'; -import { SmartLink } from '../components'; +import { Header, SmartLink } from '../components'; import '../styles/main.scss'; export default class CoreLayout extends Component { @@ -9,36 +9,9 @@ export default class CoreLayout extends Component { } render() { - const navTop = () => ( - - ); - return (
- { navTop() } +
diff --git a/src/containers/LoginPage/LoginPage.jsx b/src/containers/LoginPage/LoginPage.jsx new file mode 100644 index 0000000..76b37d3 --- /dev/null +++ b/src/containers/LoginPage/LoginPage.jsx @@ -0,0 +1,90 @@ +import React, { Component, PropTypes } from 'react'; +import { connect } from 'react-redux'; +import { pushState } from 'redux-router'; +import { login } from '../../redux/modules/auth'; + +import './LoginPage.scss'; + +class Login extends Component { + + componentWillReceiveProps(nextProps) { + console.log('Login: componentWillReceiveProps: '+nextProps); + console.log('user: '+this.props.user+'\tnextUser='+nextProps.user); + + if (nextProps.user) { + // logged in, let's show home + this.dispatch(pushState(null, '/counter')); + } + } + + handleLogin(event) { + event.preventDefault(); + const username = this.refs.username; // need for getDOMNode() call going away in React 0.14 + const password = this.refs.password; + this.props.dispatch(login(username.value, password.value)); + // username.value = ''; + // password.value = ''; + } + + render(){ + const {user, loginError} = this.props; + return( +
+
+
+
+
+

Please Log in

+
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + { + !user && loginError && +
+ {loginError.message}. Hint: use admin/password to log in. +
+ } + + + +
+
+
+
+ ); + } +} + +Login.propTypes = { + user: PropTypes.string, + loginError: PropTypes.object, + dispatch: PropTypes.func.isRequired, + routerState: PropTypes.object.isRequired +}; + +function mapStateToProps(state){ + const { auth, router } = state; + if(auth){ + return {user: auth.user, loginError: auth.loginError, routerState: router}; + }else{ + return {user: null, routerState: router}; + } +} + +export default connect(mapStateToProps)(Login); diff --git a/src/containers/LoginPage/LoginPage.scss b/src/containers/LoginPage/LoginPage.scss new file mode 100644 index 0000000..f4aefa6 --- /dev/null +++ b/src/containers/LoginPage/LoginPage.scss @@ -0,0 +1,29 @@ +:global{ + .panel-signin { + margin-top: 25%; + } + + .form-signin { + max-width: auto; + padding: 15px; + margin: 0 auto; + + .checkbox { + margin-bottom: 10px; + } + + .form-control:focus { + z-index: 2; + } + input[type="text"] { + margin-bottom: 0px; + } + input[type="password"] { + margin-bottom: 0px; + } + } + + .input-group { + margin-bottom: 10px; + } +} \ No newline at end of file diff --git a/src/containers/index.js b/src/containers/index.js index 717d106..6871317 100644 --- a/src/containers/index.js +++ b/src/containers/index.js @@ -1,2 +1,3 @@ export CoreLayout from './CoreLayout' -export MoviesPage from './MoviesPage/MoviesPage' \ No newline at end of file +export MoviesPage from './MoviesPage/MoviesPage' +export LoginPage from './LoginPage/LoginPage' \ No newline at end of file diff --git a/src/index.html b/src/index.html index 4666891..8f69c16 100644 --- a/src/index.html +++ b/src/index.html @@ -2,9 +2,20 @@ React Transform Boilerplate - + + + + + + + + + + + + +
diff --git a/src/redux/middleware/promiseMiddleware.js b/src/redux/middleware/promiseMiddleware.js new file mode 100644 index 0000000..935a8d9 --- /dev/null +++ b/src/redux/middleware/promiseMiddleware.js @@ -0,0 +1,27 @@ +export default function promiseMiddleware() { + return next => action => { + const { promise, type, ...rest } = action; + + if (!promise) { + return next(action); + } + + const [REQUEST, SUCCESS, FAILURE] = types; + + next({ ...rest, type: REQUEST }); + + return promise + .then(res => { + next({ ...rest, res, type: SUCCESS }); + + return true; + }) + .catch(error => { + next({ ...rest, error, type: FAILURE }); + + // Another benefit is being able to log all failures here + console.log(error); + return false; + }); + }; +} \ No newline at end of file diff --git a/src/redux/modules/auth.js b/src/redux/modules/auth.js new file mode 100644 index 0000000..3e41518 --- /dev/null +++ b/src/redux/modules/auth.js @@ -0,0 +1,154 @@ +import axios from 'axios'; + +//--------------------------- Action constants -------------------------- + +// names for actions can be more specific +const LOGIN_REQUEST = 'LOGIN_REQUEST'; +const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; +const LOGIN_FAILURE = 'LOGIN_FAILURE'; + +const LOGOUT_REQUEST = 'LOGOUT_REQUEST'; +const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; +const LOGOUT_FAILURE = 'LOGOUT_FAILURE'; + +//--------------------------- Reducer function -------------------------- + +const initialState = { + // user: null, + // password: null, + // userRole: null, + // loggingIn: false, + // loggingOut: false, + // loginError: null, +}; + +export default function auth(state = initialState, action = {}) { + switch (action.type) { + case LOGIN_REQUEST: + return Object.assign({}, state, {loggingIn: true}); + + case LOGIN_SUCCESS: + return Object.assign({}, state, { + loggingIn: false, user: action.user, role: action.role}); + + case LOGIN_FAILURE: + return { + ...state, + loggingIn: false, + user: null, + role: null, + loginError: action.error + }; + case LOGOUT_REQUEST: + return { + ...state, + loggingOut: true + }; + case LOGOUT_SUCCESS: + return { + ...state, + loggingOut: false, + user: null, + userRole: null, + loginError: null + }; + case LOGOUT_FAILURE: + return { + ...state, + loggingOut: false, + logoutError: action.error + }; + default: + return state; + } +} + +//--------------------------- Action functions -------------------------- + +function loginRequest(user) { + return { + type: LOGIN_REQUEST, + user: user + }; +} + +function loginSuccess(user, data) { + return { + type: LOGIN_SUCCESS, + user: data.user, + role: data.role + }; +} + +function loginFailure(user, error) { + return { + type: LOGIN_FAILURE, + user: user, + error: error + }; +} + +export function login(user, password) { + return (dispatch) => { + + dispatch(loginRequest(user)); + + return axios + .post('http://localhost:3001/api/login', { user: user, password: password }) + .then(response => dispatch(loginSuccess(user, response.data))) + .catch(function (error){ + const response=error.response; + if(response===undefined){ + dispatch(loginFailure(user, error)); + }else{ + error.status = response.status; + error.statusText = response.statusText; + error.message = response.message; + dispatch(loginFailure(user, error)); + } + }); + }; +} + +function logoutRequest(user) { + return { + type: LOGOUT_REQUEST, + user + }; +} + +function logoutSuccess(user) { + return { + type: LOGOUT_SUCCESS, + user + }; +} + +function logoutFailure(user, error) { + return { + type: LOGOUT_FAILURE, + user, + error + }; +} + +export function logout(user) { + return dispatch => { + + dispatch(logoutRequest(user)); + + return axios.post('http://localhost:3001/api/logout', { user: user }) + .then(response => dispatch(logoutSuccess(response.data))) + .catch(function (error){ + const response=error.response; + if(response===undefined){ + dispatch(logoutFailure(user, error)); + }else{ + error.status = response.status; + error.statusText = response.statusText; + error.message = response.message; + dispatch(logout(user, error)); + } + }); + }; +} \ No newline at end of file diff --git a/src/redux/modules/counter.js b/src/redux/modules/counter.js index 0a66f58..e87eb5f 100644 --- a/src/redux/modules/counter.js +++ b/src/redux/modules/counter.js @@ -1,8 +1,8 @@ -// actions constants +//--------------------------- Action constants -------------------------- export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; export const DECREMENT_COUNTER = 'DECREMENT_COUNTER'; -// reducer +//--------------------------- Reducer function -------------------------- export default function counter(state = 0, action) { switch (action.type) { case INCREMENT_COUNTER: @@ -14,7 +14,7 @@ export default function counter(state = 0, action) { } } -// actions functions +//--------------------------- Action functions -------------------------- export function increment() { return { type: INCREMENT_COUNTER diff --git a/src/redux/modules/reducer.js b/src/redux/modules/reducer.js index d9e1feb..f951449 100644 --- a/src/redux/modules/reducer.js +++ b/src/redux/modules/reducer.js @@ -1,9 +1,11 @@ import { combineReducers } from 'redux'; import counter from './counter'; +import auth from './auth'; import { routerStateReducer as router } from 'redux-router'; export default combineReducers({ + auth, counter, router }); diff --git a/src/routes.js b/src/routes.js index 420d04e..a57f143 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,13 +1,14 @@ import React from 'react'; import { Route, Redirect } from 'react-router'; -import { CoreLayout, MoviesPage } from './containers'; +import { CoreLayout, MoviesPage, LoginPage } from './containers'; import { Counter, AutoCounter } from './components'; export default ( - - - - - + + + + + + ); \ No newline at end of file diff --git a/webpack.config.dev.js b/webpack.config.dev.js index 2ee8d4e..abdb523 100644 --- a/webpack.config.dev.js +++ b/webpack.config.dev.js @@ -12,6 +12,9 @@ module.exports = { './src/index' ] }, + resolve : { + extensions : ['', '.js', '.jsx'] + }, output: { path: path.join(__dirname, 'dist'), //"entry" keys will be a bundle names diff --git a/webpack.config.prod.js b/webpack.config.prod.js index 0055225..58bf596 100644 --- a/webpack.config.prod.js +++ b/webpack.config.prod.js @@ -10,6 +10,9 @@ module.exports = { './src/index.js', './src/index.html' ], + resolve : { + extensions : ['', '.js', '.jsx'] + }, output: { path: path.join(__dirname, 'dist'), filename: 'bundle.js',