├── .gitignore ├── README.md ├── controllers ├── EdiRoute.controller.js └── UserRoute.controller.js ├── models ├── EdiDoc.js └── User.js ├── package.json ├── passport.js ├── public ├── favicon.ico ├── index.html └── manifest.json ├── routes ├── EdiRoute.js └── UserRoute.js ├── server.js ├── src ├── App.js ├── App.test.js ├── actions │ ├── authActions.js │ ├── orderActions.js │ ├── orderSearchActions.js │ └── types.js ├── components │ ├── Home │ │ ├── Home.js │ │ └── index.js │ ├── ListAllOrders │ │ ├── ListAllOrders.css │ │ ├── ListAllOrders.js │ │ └── index.js │ ├── Login │ │ ├── Login.js │ │ └── index.js │ ├── OrderErrorMsg │ │ ├── OrderErrorMsg.js │ │ └── index.js │ ├── OrderModal │ │ ├── OrderModal.js │ │ ├── OrderModalBody.js │ │ ├── OrderModalBuyerShipRow.js │ │ ├── OrderModalDocInfoRow.js │ │ ├── OrderModalFooter.js │ │ ├── OrderModalHeader.js │ │ ├── OrderModalLineItemInfo.js │ │ ├── OrderModalRefIdRow.js │ │ ├── OrderModalShipMethodRow.js │ │ └── index.js │ ├── OrderTable │ │ ├── OrderTable.js │ │ ├── OrderTableBody.js │ │ ├── OrderTableFooter.js │ │ ├── OrderTableHeader.js │ │ ├── OrderTablePagination.js │ │ ├── OrderTableRowsPerPageToggle.js │ │ └── index.js │ ├── Register │ │ ├── Register.js │ │ └── index.js │ ├── SearchForm │ │ ├── SearchForm.css │ │ ├── SearchForm.js │ │ └── index.js │ ├── SearchOrders │ │ ├── SearchOrders.js │ │ ├── index.js │ │ └── searchOrders.css │ ├── SearchResultMetadata │ │ ├── SearchResultMetadata.js │ │ └── index.js │ └── TopNavbar │ │ ├── TopNavbar.js │ │ └── index.js ├── containers │ ├── ListAllOrdersContainer │ │ ├── ListAllOrdersContainer.js │ │ └── index.js │ ├── LoginContainer │ │ ├── LoginContainer.js │ │ └── index.js │ ├── ProtectedRouteContainer │ │ ├── ProtectedRouteContainer.js │ │ └── index.js │ ├── RegisterContainer │ │ ├── RegisterContainer.js │ │ └── index.js │ ├── SearchOrdersContainer │ │ ├── SearchOrdersContainer.js │ │ └── index.js │ └── TopNavbarContainer │ │ ├── NavbarContainer.js │ │ └── index.js ├── index.css ├── index.js ├── is-empty.js ├── reducers │ ├── authentication │ │ ├── authErrorReducer.js │ │ └── authReducer.js │ ├── index.js │ └── orders │ │ ├── orderReducer.js │ │ └── orderSearchReducer.js ├── serviceWorker.js ├── setAuthToken.js ├── store.js └── utils │ └── utils.js ├── validation ├── is-empty.js ├── login.js └── register.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # webstorm project data 26 | .idea/ 27 | 28 | # ENV vars 29 | .env 30 | 31 | /server/config/DB.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React EDI Web Portal 2 | This project was inspired due to a lack of a user friendly way to view incoming purchase order data sent over 3 | a B2B X12 EDI document exchange. 4 | 5 | #### Backstory 6 | A company that I worked for had recently setup an automated B2B EDI document exchange which included these documents: 7 | - (850) Purchase Orders 8 | - (855) Purchase Order Acknowledgements 9 | - (856) Advanced Ship Notice 10 | - (810) Invoice 11 | 12 | All of these document transactions were sent directly into a SQL database, and the B2B partnership moved to production 13 | status before a tool was made for our customer service team to easily be able to view the incoming purchase order data 14 | 15 | It was rather inconvenient to have our CS team use a SQL database explorer - these typically included much more functionality 16 | than what was needed, along with various risks of misinterpretting the data or altering the table definitions 17 | 18 | What we needed was a minimal, read-only web portal for our CS team to view the incoming purchase order data for individual 19 | orders - and this is what I aimed to provide with this app! 20 | 21 | ## See it in Action! 22 | https://aca-final-project-edi-viewer.herokuapp.com 23 | 24 | Feel free to register an account and browse through the purchase orders (all sensitive data has been scrubbed) 25 | 26 | ## Features 27 | - Lists all incoming purchase orders, and when an order is clicked a modal is displayed containing relevant order information 28 | - Search function that can search based on: order #, date, SKU, or name 29 | - A calendar button next to search field to select a date to search 30 | - Can select how many rows per page are displayed 31 | - Pagination that adjusts to number of rows per page selected 32 | - A sticky table header on scroll down when viewing 50 or 100 orders per page 33 | - User authentication based on JWT 34 | 35 | ## Technologies Used 36 | - React + React Router v4 + Redux + Redux Thunk 37 | - Node 38 | - Express 39 | - MongoDB 40 | - Mongoose 41 | - Passport.js 42 | - JWT Authentication 43 | - Bcrypt.js 44 | - Reactstrap 45 | - Bootstrap CSS 46 | - Classnames 47 | - Validator.js 48 | - Deployed on Heroku -------------------------------------------------------------------------------- /controllers/EdiRoute.controller.js: -------------------------------------------------------------------------------- 1 | // import the EdiRoute mongodb model 2 | const EdiDoc = require("../models/EdiDoc"); 3 | 4 | module.exports = { 5 | 6 | // returns all orders 7 | index: (req, res) => { 8 | EdiDoc.find({}).exec() 9 | .then((docs) => { 10 | return res.json({ docs, success: true }); 11 | }) 12 | .catch(err => { 13 | return res.json({ error: err, success: false }); 14 | }); 15 | }, 16 | 17 | // returns paginated orders 18 | page: (req, res) => { 19 | // config pagination 20 | const perPage = Number(req.query.limit) || 20; 21 | const currPage = Number(req.params.page) || 1; 22 | console.log('curr page backend', currPage); 23 | 24 | const queryOpts = { 25 | sort: { "Luma Order Number": -1 }, 26 | lean: true, 27 | page: currPage, 28 | limit: perPage 29 | }; 30 | 31 | EdiDoc.paginate({}, queryOpts) 32 | .then(result => { 33 | return res.json({ success: true, result }); 34 | }) 35 | .catch(err => { 36 | return res.json({ success: false, error: err }); 37 | }); 38 | }, 39 | 40 | // search route with pagination 41 | search: (req, res) => { 42 | const searchTerm = String(req.params.searchTerm).trim().toUpperCase(); 43 | const perPage = Number(req.query.limit) || 20; 44 | const currPage = Number(req.query.page) || 1; 45 | 46 | const queryOpts = { 47 | sort: { "Luma Order Number": -1 }, 48 | lean: true, 49 | page: currPage, 50 | limit: perPage 51 | }; 52 | 53 | EdiDoc.paginate({ "Search": searchTerm }, queryOpts) 54 | .then(result => { 55 | return res.json({ success: true, result }); 56 | }) 57 | .catch(err => { 58 | return res.json({ success: false, error: err }); 59 | }); 60 | } 61 | 62 | }; -------------------------------------------------------------------------------- /controllers/UserRoute.controller.js: -------------------------------------------------------------------------------- 1 | const gravatar = require("gravatar"); 2 | const bcrypt = require("bcryptjs"); 3 | const jwt = require("jsonwebtoken"); 4 | const passport = require("passport"); 5 | const validateRegisterInput = require("../validation/register"); 6 | const validateLoginInput = require("../validation/login"); 7 | 8 | // import User mongodb model 9 | const User = require("../models/User"); 10 | 11 | module.exports = { 12 | 13 | // POST - /register route 14 | register: (req, res) => { 15 | // use the validateRegisterInput fn to process the data in req.body and return any errors and if input isValid 16 | const { errors, isValid } = validateRegisterInput(req.body); 17 | // if invalid input, return the errors in JSON format 18 | if (!isValid) { 19 | return res.status(400).json(errors); 20 | } 21 | 22 | User.findOne({ 23 | email: req.body.email 24 | }).then(user => { 25 | // if found a user, return error saying user already has registered 26 | if (user) { 27 | return res.status(400).json({ 28 | email: 'Email already exists' 29 | }); 30 | } 31 | else { 32 | // create gravatar based on email text 33 | const avatar = gravatar.url(req.body.email, { 34 | s: '200', 35 | r: 'pg', 36 | d: 'mm' 37 | }); 38 | // create new User model, hash+salt the password, and save to DB 39 | const newUser = new User({ 40 | name: req.body.name, 41 | email: req.body.email, 42 | password: req.body.password, 43 | avatar 44 | }); 45 | // encrypt the password 46 | bcrypt.genSalt(10, (err, salt) => { 47 | if (err) console.error("There was an error encrypting password", err); 48 | else { 49 | bcrypt.hash(newUser.password, salt, (err, hash) => { 50 | if (err) console.error("There was an error encrypting password", err); 51 | else { 52 | newUser.password = hash; 53 | // save newUser to DB 54 | newUser.save() 55 | .then(user => { 56 | // on successful save, return the user json obj 57 | return res.json(user); 58 | }); 59 | } 60 | }); 61 | } 62 | }); 63 | } 64 | }); 65 | }, 66 | 67 | // POST - /login route 68 | login: (req, res) => { 69 | // use the validateLoginInput fn to process the data in req.body and return any errors and if input isValid 70 | const { errors, isValid } = validateLoginInput(req.body); 71 | // if invalid input, return errors in json obj 72 | if (!isValid) { 73 | return res.status(400).json(errors); 74 | } 75 | 76 | const email = req.body.email; 77 | const password = req.body.password; 78 | 79 | User.findOne({ email }) 80 | .then(user => { 81 | // if user not found, then return errors 82 | if (!user) { 83 | errors.email = 'User not found'; 84 | return res.status(400).json(errors); 85 | } 86 | 87 | bcrypt.compare(password, user.password) 88 | .then(isMatch => { 89 | if (isMatch) { 90 | const payload = { 91 | id: user.id, 92 | name: user.name, 93 | avatar: user.avatar 94 | }; 95 | 96 | // create and sign the JWT token 97 | jwt.sign(payload, 'secret', { 98 | expiresIn: 3600 99 | }, (err, token) => { 100 | if (err) console.error("There is an error with token", err); 101 | else { 102 | // return the jwt token 103 | return res.json({ 104 | success: true, 105 | token: `Bearer ${token}` 106 | }); 107 | } 108 | }); 109 | } 110 | // else passwords do NOT match 111 | else { 112 | errors.password = 'Incorrect password'; 113 | return res.status(400).json(errors); 114 | } 115 | }); 116 | }); 117 | }, 118 | 119 | // GET route - user can only access this route if they have a JWT token stored, otherwise it will redirect to login page 120 | // this is used for protected routes 121 | userInfo: (req, res) => { 122 | return res.json({ 123 | id: req.user.id, 124 | name: req.user.name, 125 | email: req.user.email 126 | }); 127 | } 128 | 129 | }; -------------------------------------------------------------------------------- /models/EdiDoc.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const mongoosePaginate = require('mongoose-paginate'); 3 | const Schema = mongoose.Schema; 4 | 5 | // we need to turn strict mode off so we don't need to define the extremely deeply nested nature of the edi docs 6 | const EdiSchema = new Schema({}, { strict: false, collection: 'ediDocs' }); 7 | EdiSchema.plugin(mongoosePaginate); 8 | 9 | module.exports = mongoose.model("ediDocs", EdiSchema); -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | // User schema for mongodb 5 | const UserSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: true 9 | }, 10 | email: { 11 | type: String, 12 | required: true 13 | }, 14 | password: { 15 | type: String, 16 | required: true 17 | }, 18 | avatar: { 19 | type: String 20 | }, 21 | date: { 22 | type: Date, 23 | default: Date.now() 24 | } 25 | }, { 26 | collection: 'users' 27 | }); 28 | 29 | module.exports = mongoose.model('Users', UserSchema); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-edi-viewer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "engines": { 6 | "node": "8.11.3" 7 | }, 8 | "dependencies": { 9 | "axios": "^0.18.0", 10 | "bcryptjs": "^2.4.3", 11 | "body-parser": "^1.18.3", 12 | "bootstrap": "^4.1.3", 13 | "classnames": "^2.2.6", 14 | "concurrently": "^4.1.0", 15 | "dotenv": "^6.2.0", 16 | "express": "^4.16.4", 17 | "gravatar": "^1.8.0", 18 | "jsonwebtoken": "^8.4.0", 19 | "jwt-decode": "^2.2.0", 20 | "moment": "^2.23.0", 21 | "mongoose": "^5.3.13", 22 | "mongoose-paginate": "^5.0.3", 23 | "passport": "^0.4.0", 24 | "passport-jwt": "^4.0.0", 25 | "prop-types": "^15.6.2", 26 | "react": "^16.6.3", 27 | "react-datepicker": "^2.0.0", 28 | "react-dom": "^16.6.3", 29 | "react-paginate": "^6.0.0", 30 | "react-redux": "^5.1.1", 31 | "react-router-dom": "^4.3.1", 32 | "react-scripts": "2.1.1", 33 | "react-stickynode": "^2.1.0", 34 | "react-transition-group": "^2.5.0", 35 | "reactstrap": "^7.0.2", 36 | "redux": "^4.0.1", 37 | "redux-thunk": "^2.3.0", 38 | "validator": "^10.9.0" 39 | }, 40 | "scripts": { 41 | "start": "node server.js", 42 | "start:dev": "concurrently --names \"server.,react..\" -c \"blue.dim,magenta.dim\" --prefix \"{time}..{name}{index}\" \"nodemon server.js\" \"yarn run start\"", 43 | "start:server": "nodemon server.js", 44 | "start:react": "react-scripts start", 45 | "build": "react-scripts build", 46 | "test": "react-scripts test", 47 | "eject": "react-scripts eject", 48 | "heroku-postbuild": "yarn install --only=dev && yarn install && yarn run build" 49 | }, 50 | "eslintConfig": { 51 | "extends": "react-app" 52 | }, 53 | "browserslist": [ 54 | ">0.2%", 55 | "not dead", 56 | "not ie <= 11", 57 | "not op_mini all" 58 | ], 59 | "devDependencies": { 60 | "nodemon": "^1.18.6" 61 | }, 62 | "proxy": "http://localhost:5000" 63 | } 64 | -------------------------------------------------------------------------------- /passport.js: -------------------------------------------------------------------------------- 1 | const JWTStrategy = require("passport-jwt").Strategy; 2 | const ExtractJWT = require("passport-jwt").ExtractJwt; 3 | // import Users mongoose model 4 | const User = require("./models/User"); 5 | 6 | const opts = {}; 7 | 8 | opts.jwtFromRequest = ExtractJWT.fromAuthHeaderAsBearerToken(); 9 | opts.secretOrKey = process.env.SECRET || "supersecretdevsecret"; 10 | 11 | module.exports = passport => { 12 | passport.use(new JWTStrategy(opts, (jwt_payload, done) => { 13 | User.findById(jwt_payload.id) 14 | .then(user => { 15 | if (user) return done(null, user); 16 | 17 | return done(null, false); 18 | }) 19 | .catch(err => console.error(err)); 20 | })); 21 | }; -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwaltrip/React-EDI-Viewer/b3a588a6f6baa633070bc4a72030a492060d3399/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | 23 | Lumaprints EDI Viewer 24 | 25 | 26 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "EDI Viewer", 3 | "name": "Lumaprints EDI Web Portal", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /routes/EdiRoute.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const ediController = require("../controllers/EdiRoute.controller"); 4 | 5 | // returns all orders 6 | router.get('/', ediController.index); 7 | 8 | // returns paginated orders 9 | router.get('/:page', ediController.page); 10 | 11 | // search route with pagination 12 | router.get('/search/:searchTerm', ediController.search); 13 | 14 | module.exports = router; -------------------------------------------------------------------------------- /routes/UserRoute.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const jwt = require("jsonwebtoken"); 4 | const passport = require("passport"); 5 | 6 | const userRouteController = require("../controllers/UserRoute.controller"); 7 | 8 | // POST - /register route 9 | router.post('/register', userRouteController.register); 10 | 11 | // POST - /login route 12 | router.post('/login', userRouteController.login); 13 | 14 | // GET route - user can only access this route if they have a JWT token stored, otherwise it will redirect to login page 15 | // this is used for protected routes 16 | router.get('/me', passport.authenticate('jwt', { session: false }), userRouteController.userInfo); 17 | 18 | module.exports = router; -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | require("dotenv").config(); 3 | const express = require("express"); 4 | const bodyParser = require("body-parser"); 5 | const mongoose = require("mongoose"); 6 | const passport = require("passport"); 7 | 8 | const app = express(); 9 | 10 | // initialize passport 11 | app.use(passport.initialize()); 12 | require("./passport")(passport); 13 | 14 | // setup mongodb connection 15 | mongoose.Promise = global.Promise; 16 | mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true }).then( 17 | () => { console.log("Database is connected") }, 18 | (err) => { console.log("Cannot connect to the database"+ err) } 19 | ); 20 | 21 | // import routes 22 | const userRoutes = require("./routes/UserRoute"); 23 | const ediRoutes = require("./routes/EdiRoute"); 24 | 25 | // setup middleware 26 | app.use(bodyParser.urlencoded({ extended: false })); 27 | app.use(bodyParser.json()); 28 | 29 | // setup express to serve the static index.html built by react 30 | app.use(express.static(path.join(__dirname, "build"))); 31 | 32 | // set the backend server port 33 | const port = process.env.PORT || 5000; 34 | 35 | // setup routes 36 | app.use('/api/users', userRoutes); 37 | app.use('/edi', ediRoutes); 38 | 39 | // a catchall route if any API calls aren't used, then serve the index.html built by react 40 | // this needs to be after all other routes 41 | app.get("*", (req, res) => { 42 | res.sendFile(path.join(__dirname, "build", "index.html")); 43 | }); 44 | 45 | app.listen(port, () => { 46 | console.log(`Backend server running and listening on port ${port}`); 47 | }); 48 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | // import react router 3 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 4 | 5 | // import bootstrap css 6 | import 'bootstrap/dist/css/bootstrap.min.css'; 7 | 8 | import { Container } from 'reactstrap'; 9 | import TopNavbar from './containers/TopNavbarContainer'; 10 | import Register from "./containers/RegisterContainer"; 11 | import Login from "./containers/LoginContainer"; 12 | import Home from "./components/Home"; 13 | import ListAllOrders from './containers/ListAllOrdersContainer'; 14 | import SearchOrders from './containers/SearchOrdersContainer'; 15 | import ProtectedRoute from './containers/ProtectedRouteContainer'; 16 | 17 | class App extends Component { 18 | render() { 19 | return ( 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | export default App; 39 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/actions/authActions.js: -------------------------------------------------------------------------------- 1 | import { GET_AUTH_ERRORS, SET_CURRENT_USER } from "./types"; 2 | import setAuthToken from '../setAuthToken'; 3 | import jwt_decode from 'jwt-decode'; 4 | import axios from 'axios'; 5 | 6 | export const registerUser = (user, history) => dispatch => { 7 | axios.post('/api/users/register', user) 8 | .then(res => history.push('/login')) 9 | .catch(err => { 10 | dispatch({ 11 | type: GET_AUTH_ERRORS, 12 | payload: err.response.data 13 | }); 14 | }); 15 | }; 16 | 17 | export const loginUser = (user) => dispatch => { 18 | axios.post('/api/users/login', user) 19 | .then(res => { 20 | const { token } = res.data; 21 | // set token in localStorage 22 | localStorage.setItem('jwtToken', token); 23 | // set token to be in all axios headers 24 | setAuthToken(token); 25 | // decode the token 26 | const decoded = jwt_decode(token); 27 | dispatch(setCurrentUser(decoded)); 28 | }) 29 | .catch(err => { 30 | dispatch({ 31 | type: GET_AUTH_ERRORS, 32 | payload: err.response.data 33 | }); 34 | }); 35 | }; 36 | 37 | export const setCurrentUser = decoded => { 38 | return { 39 | type: SET_CURRENT_USER, 40 | payload: decoded 41 | }; 42 | }; 43 | 44 | export const logoutUser = (history) => dispatch => { 45 | // remove JWT token from localStorage 46 | localStorage.removeItem('jwtToken'); 47 | // remove JWT token from axios Authorization headers 48 | setAuthToken(false); 49 | // set current user back to empty object 50 | dispatch(setCurrentUser({})); 51 | // redirect to login page 52 | if (history) { 53 | history.push('/login'); 54 | } 55 | }; -------------------------------------------------------------------------------- /src/actions/orderActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_ORDERS_BEGIN, 3 | FETCH_ORDERS_SUCCESS, 4 | FETCH_ORDERS_FAILURE, 5 | SET_CURRENT_ORDER, 6 | SET_ORDER_ROWS_PER_PAGE, 7 | SET_CURRENT_PAGE 8 | } from "./types"; 9 | import axios from 'axios'; 10 | 11 | /* 12 | * BEGIN FETCH ORDERS ACTION 13 | * */ 14 | 15 | export const fetchOrdersBegin = () => ({ 16 | type: FETCH_ORDERS_BEGIN 17 | }); 18 | 19 | export const fetchOrdersSuccess = (orders) => ({ 20 | type: FETCH_ORDERS_SUCCESS, 21 | payload: { 22 | data: orders.data.result.docs, 23 | currentPage: orders.data.result.page, 24 | perPage: orders.data.result.limit, 25 | totalPages: orders.data.result.pages, 26 | totalResults: orders.data.result.total, 27 | } 28 | }); 29 | 30 | export const fetchOrdersFailure = (error) => ({ 31 | type: FETCH_ORDERS_FAILURE, 32 | payload: { error: error.response.statusText } 33 | }); 34 | 35 | export const fetchOrders = (currPage = 1, perPage = 20) => dispatch => { 36 | dispatch(fetchOrdersBegin()); 37 | 38 | axios(`/edi/${currPage}/?limit=${perPage}`) 39 | .then(orders => { 40 | if (orders.data.success) { 41 | return dispatch(fetchOrdersSuccess(orders)); 42 | } else { 43 | return dispatch(fetchOrdersFailure(orders.data.error)); 44 | } 45 | }) 46 | .catch(error => dispatch(fetchOrdersFailure(error))); 47 | 48 | }; 49 | 50 | /* 51 | * END FETCH ORDERS ACTION 52 | * */ 53 | 54 | /* 55 | * BEGIN SET CURRENT ORDER ACTION 56 | * */ 57 | 58 | export const setCurrentOrderSuccess = (order) => ({ 59 | type: SET_CURRENT_ORDER, 60 | payload: { order } 61 | }); 62 | 63 | export const setCurrentOrder = (order) => dispatch => { 64 | return new Promise(resolve => { 65 | dispatch(setCurrentOrderSuccess(order)); 66 | 67 | resolve(); 68 | }) 69 | }; 70 | 71 | /* 72 | * END SET CURRENT ORDER ACTION 73 | * */ 74 | 75 | /* 76 | * BEGIN SET NUMBER OF ROWS PER PAGE ACTION 77 | * */ 78 | 79 | export const setRowsPerPageSuccess = (perPage) => ({ 80 | type: SET_ORDER_ROWS_PER_PAGE, 81 | payload: { perPage } 82 | }); 83 | 84 | export const setRowsPerPage = (perPage) => dispatch => { 85 | return new Promise(resolve => { 86 | dispatch(setRowsPerPageSuccess(perPage)); 87 | 88 | resolve(); 89 | }); 90 | }; 91 | 92 | /* 93 | * END SET NUMBER OF ROWS PER PAGE ACTION 94 | * */ 95 | 96 | /* 97 | * BEGIN SET CURRENT PAGE ACTION 98 | * */ 99 | 100 | export const setCurrentPageSuccess = (currPage) => ({ 101 | type: SET_CURRENT_PAGE, 102 | payload: { currPage } 103 | }); 104 | 105 | export const setCurrentPage = (currPage) => dispatch => { 106 | return new Promise(resolve => { 107 | dispatch(setCurrentPageSuccess(currPage)); 108 | 109 | resolve(); 110 | }); 111 | }; 112 | 113 | /* 114 | * END SET CURRENT PAGE ACTION 115 | * */ -------------------------------------------------------------------------------- /src/actions/orderSearchActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_SEARCH_ORDERS_BEGIN, 3 | FETCH_SEARCH_ORDERS_SUCCESS, 4 | FETCH_SEARCH_ORDERS_FAILURE, 5 | SET_SEARCH_CURRENT_ORDER, 6 | SET_SEARCH_ORDER_ROWS_PER_PAGE, 7 | SET_SEARCH_CURRENT_PAGE, 8 | SET_SEARCH_TERM 9 | } from "./types"; 10 | import axios from 'axios'; 11 | 12 | /* 13 | * BEGIN FETCH SEARCH ORDERS ACTION 14 | * */ 15 | 16 | export const fetchSearchOrdersBegin = () => ({ 17 | type: FETCH_SEARCH_ORDERS_BEGIN 18 | }); 19 | 20 | export const fetchSearchOrdersSuccess = (orders) => ({ 21 | type: FETCH_SEARCH_ORDERS_SUCCESS, 22 | payload: { 23 | data: orders.data.result.docs, 24 | currentPage: orders.data.result.page, 25 | perPage: orders.data.result.limit, 26 | totalPages: orders.data.result.pages, 27 | totalResults: orders.data.result.total, 28 | } 29 | }); 30 | 31 | export const fetchSearchOrdersFailure = (error) => ({ 32 | type: FETCH_SEARCH_ORDERS_FAILURE, 33 | payload: { error: error.response.statusText } 34 | }); 35 | 36 | export const fetchSearchOrders = (searchTerm, currPage, perPage) => dispatch => { 37 | dispatch(fetchSearchOrdersBegin()); 38 | 39 | axios(`/edi/search/${searchTerm}/?limit=${perPage}&page=${currPage}`) 40 | .then(orders => { 41 | if (orders.data.success) { 42 | return dispatch(fetchSearchOrdersSuccess(orders)); 43 | } else { 44 | return dispatch(fetchSearchOrdersFailure(orders.data.error)); 45 | } 46 | }) 47 | .catch(err => dispatch(fetchSearchOrdersFailure(err))); 48 | }; 49 | 50 | /* 51 | * END FETCH SEARCH ORDERS ACTION 52 | * */ 53 | 54 | /* 55 | * BEGIN SET SEARCH CURRENT ORDER ACTION 56 | * */ 57 | 58 | export const setSearchCurrentOrderSuccess = (order) => ({ 59 | type: SET_SEARCH_CURRENT_ORDER, 60 | payload: { order } 61 | }); 62 | 63 | export const setSearchCurrentOrder = (order) => dispatch => { 64 | return new Promise(resolve => { 65 | dispatch(setSearchCurrentOrderSuccess(order)); 66 | 67 | resolve(); 68 | }); 69 | }; 70 | 71 | /* 72 | * END SET SEARCH CURRENT ORDER ACTION 73 | * */ 74 | 75 | /* 76 | * BEGIN SET SEARCH NUMBER OF ROWS PER PAGE ACTION 77 | * */ 78 | 79 | export const setSearchRowsPerPageSuccess = (perPage) => ({ 80 | type: SET_SEARCH_ORDER_ROWS_PER_PAGE, 81 | payload: { perPage } 82 | }); 83 | 84 | export const setSearchRowsPerPage = (perPage) => dispatch => { 85 | return new Promise(resolve => { 86 | dispatch(setSearchRowsPerPageSuccess(perPage)); 87 | 88 | resolve(); 89 | }) 90 | }; 91 | 92 | /* 93 | * END SET SEARCH NUMBER OF ROWS PER PAGE ACTION 94 | * */ 95 | 96 | /* 97 | * BEGIN SET SEARCH CURRENT PAGE ACTION 98 | * */ 99 | 100 | export const setSearchCurrentPageSuccess = (currPage) => ({ 101 | type: SET_SEARCH_CURRENT_PAGE, 102 | payload: { currPage } 103 | }); 104 | 105 | export const setSearchCurrentPage = (currPage) => dispatch => { 106 | return new Promise(resolve => { 107 | dispatch(setSearchCurrentPageSuccess(currPage)); 108 | 109 | resolve(); 110 | }) 111 | }; 112 | 113 | /* 114 | * END SET SEARCH CURRENT PAGE ACTION 115 | * */ 116 | 117 | export const setSearchTermSuccess = (searchTerm) => ({ 118 | type: SET_SEARCH_TERM, 119 | payload: { searchTerm } 120 | }); 121 | 122 | export const setSearchTerm = (searchTerm) => dispatch => { 123 | dispatch(setSearchTermSuccess(searchTerm)); 124 | }; -------------------------------------------------------------------------------- /src/actions/types.js: -------------------------------------------------------------------------------- 1 | // these define the different action types to be used in redux 2 | 3 | // authentication types 4 | export const GET_AUTH_ERRORS = 'GET_AUTH_ERRORS'; 5 | export const SET_CURRENT_USER = 'SET_CURRENT_USER'; 6 | 7 | // order types 8 | export const FETCH_ORDERS_BEGIN = 'FETCH_ORDERS_BEGIN'; 9 | export const FETCH_ORDERS_SUCCESS = 'FETCH_ORDERS_SUCCESS'; 10 | export const FETCH_ORDERS_FAILURE = 'FETCH_ORDERS_FAILURE'; 11 | 12 | export const SET_CURRENT_ORDER = 'SET_CURRENT_ORDER'; 13 | export const SET_ORDER_ROWS_PER_PAGE = 'SET_ORDER_ROWS_PER_PAGE'; 14 | export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE'; 15 | 16 | export const FETCH_SEARCH_ORDERS_BEGIN = 'FETCH_SEARCH_ORDERS_BEGIN'; 17 | export const FETCH_SEARCH_ORDERS_SUCCESS = 'FETCH_SEARCH_ORDERS_SUCCESS'; 18 | export const FETCH_SEARCH_ORDERS_FAILURE = 'FETCH_SEARCH_ORDERS_FAILURE'; 19 | 20 | export const SET_SEARCH_TERM = 'SET_SEARCH_TERM'; 21 | export const SET_SEARCH_CURRENT_ORDER = 'SET_SEARCH_CURRENT_ORDER'; 22 | export const SET_SEARCH_ORDER_ROWS_PER_PAGE = 'SET_SEARCH_ORDER_ROWS_PER_PAGE'; 23 | export const SET_SEARCH_CURRENT_PAGE = 'SET_SEARCH_CURRENT_PAGE'; -------------------------------------------------------------------------------- /src/components/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Redirect } from 'react-router-dom'; 4 | import { connect } from 'react-redux'; 5 | 6 | const Home = (props) => { 7 | if (props.auth.isAuthenticated) { 8 | return ; 9 | } else { 10 | return ; 11 | } 12 | }; 13 | 14 | Home.propTypes = { 15 | auth: PropTypes.object.isRequired 16 | }; 17 | 18 | const mapStateToProps = state => ({ 19 | auth: state.auth 20 | }); 21 | 22 | export default connect( 23 | mapStateToProps 24 | )(Home); -------------------------------------------------------------------------------- /src/components/Home/index.js: -------------------------------------------------------------------------------- 1 | import Home from './Home'; 2 | 3 | export default Home; -------------------------------------------------------------------------------- /src/components/ListAllOrders/ListAllOrders.css: -------------------------------------------------------------------------------- 1 | .break-disabled > a { 2 | color: #6c757d; 3 | pointer-events: none; 4 | cursor: auto; 5 | background-color: #fff; 6 | position: relative; 7 | display: block; 8 | padding: .5rem .75rem; 9 | margin-left: -1px; 10 | line-height: 1.25; 11 | border: 1px solid #dee2e6; 12 | } 13 | 14 | .order-skeleton { 15 | /*background-image: linear-gradient(gray 80%, transparent 0);*/ 16 | color: #e7e7e7; 17 | pointer-events: none; 18 | cursor: auto; 19 | } 20 | 21 | .order-row { 22 | cursor: pointer; 23 | } 24 | 25 | .line-item-header { 26 | font-size: 11pt; 27 | font-weight: bold; 28 | line-height: 2 !important; 29 | margin-bottom: 0.5rem !important; 30 | } 31 | 32 | .line-item-head { 33 | font-weight: bold; 34 | vertical-align: middle !important; 35 | } 36 | 37 | .header-sticky { 38 | font-weight: bold; 39 | color: #fff; 40 | background-color: #212529; 41 | } 42 | 43 | .header-sticky > div { 44 | padding: .3rem; 45 | } 46 | 47 | .prev-next-label > a { 48 | padding-left: 1rem; 49 | padding-right: 1rem; 50 | } -------------------------------------------------------------------------------- /src/components/ListAllOrders/ListAllOrders.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import moment from 'moment'; 4 | import { range } from '../../utils/utils'; 5 | import { Container, Row, Col } from 'reactstrap'; 6 | 7 | import './ListAllOrders.css'; 8 | 9 | import OrderTable from '../OrderTable'; 10 | import OrderModal from '../OrderModal'; 11 | import OrderErrorMsg from '../OrderErrorMsg'; 12 | 13 | class ListAllOrders extends Component { 14 | 15 | static propTypes = { 16 | fetchOrders: PropTypes.func.isRequired, 17 | setCurrentOrder: PropTypes.func.isRequired, 18 | setRowsPerPage: PropTypes.func.isRequired, 19 | setCurrentPage: PropTypes.func.isRequired, 20 | orders: PropTypes.array.isRequired, 21 | currentPage: PropTypes.number.isRequired, 22 | perPage: PropTypes.number.isRequired, 23 | totalPages: PropTypes.number.isRequired, 24 | totalResults: PropTypes.number.isRequired, 25 | selectedOrder: PropTypes.object, 26 | isLoading: PropTypes.bool.isRequired, 27 | error: PropTypes.object, 28 | match: PropTypes.object.isRequired, 29 | history: PropTypes.object.isRequired, 30 | }; 31 | 32 | static defaultProps = { 33 | selectedOrder: {}, 34 | error: {} 35 | }; 36 | 37 | state = { modal: false }; 38 | 39 | componentDidMount() { 40 | const { id } = this.props.match.params; 41 | 42 | if (id === this.props.currentPage) { 43 | this.props.fetchOrders(this.props.currentPage, this.props.perPage); 44 | } else { 45 | this.props.fetchOrders(Number(id), this.props.perPage); 46 | } 47 | } 48 | 49 | handlePageClick = (data) => { 50 | this.props.setCurrentPage(data.selected + 1).then(() => { 51 | // update router url 52 | this.props.history.push(`/orders/${this.props.currentPage}`); 53 | // fetch next page data 54 | this.props.fetchOrders(this.props.currentPage, this.props.perPage); 55 | }); 56 | }; 57 | 58 | listOrders = (orders, perPage, currPage, totalOrders, setCurrentOrder) => { 59 | const startIdx = totalOrders - (perPage * currPage-1); 60 | const idxRange = range(perPage, startIdx).reverse(); 61 | 62 | return orders.map((order, idx) => { 63 | return ( 64 | setCurrentOrder(order)}> 65 | {idxRange[idx]} 66 | {order["Filename"]} 67 | {order["Luma Order Number"]} 68 | {order["Partner Po Number"]} 69 | {moment(order["Transaction Set Data"]["Purchase Order Date"]).format("YYYY-MM-DD")} 70 | 71 | ); 72 | }); 73 | }; 74 | 75 | listOrdersSkeleton = (perPage, totalResults) => { 76 | let numRows; 77 | if (totalResults === 0) { numRows = perPage; } 78 | else { numRows = (perPage > totalResults) ? totalResults : perPage; } 79 | 80 | const idxRange = range(numRows); 81 | 82 | return idxRange.map((order, idx) => { 83 | return ( 84 | 85 | ██ 86 | 87 | █████████████████████████████████████ 88 | 89 | 90 | █████ 91 | 92 | 93 | ██████ 94 | 95 | 96 | ███████ 97 | 98 | 99 | ); 100 | }); 101 | }; 102 | 103 | listLineItems = (order) => { 104 | let lineItems = []; 105 | 106 | for (let i=0; i 117 | {lineItemID} 118 | 119 | 120 | SKU: 121 | {sku} 122 | 123 | 124 | Item Desc: 125 | {itemDesc} 126 | 127 | 128 | {qty} 129 | {unitM} 130 | {itemCostEa} 131 | {itemCostThruQty} 132 | 133 | ); 134 | 135 | lineItems.push(currLineItem); 136 | } 137 | 138 | return lineItems; 139 | }; 140 | 141 | toggleModal = () => { 142 | this.setState({ modal: !this.state.modal }); 143 | }; 144 | 145 | setCurrentOrder = (order) => { 146 | this.props.setCurrentOrder(order).then(() => { 147 | this.toggleModal(); 148 | }); 149 | }; 150 | 151 | handlePerPageSelect = (perPage) => { 152 | this.props.setRowsPerPage(perPage).then(() => { 153 | this.props.fetchOrders(this.props.currentPage, this.props.perPage); 154 | }); 155 | }; 156 | 157 | render() { 158 | let errorMsg; 159 | if (this.props.error) { 160 | errorMsg = ; 161 | } 162 | 163 | return ( 164 | 165 | {errorMsg} 166 |

Orders

167 | 168 | 182 | 183 | {/* Order Details Modal */} 184 | 190 |
191 | ); 192 | } 193 | } 194 | 195 | export default ListAllOrders; -------------------------------------------------------------------------------- /src/components/ListAllOrders/index.js: -------------------------------------------------------------------------------- 1 | import ListAllOrders from './ListAllOrders'; 2 | 3 | export default ListAllOrders; -------------------------------------------------------------------------------- /src/components/Login/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | 5 | class Login extends Component { 6 | 7 | state = { 8 | email: '', 9 | password: '', 10 | errors: {} 11 | }; 12 | 13 | static propTypes = { 14 | loginUser: PropTypes.func.isRequired, 15 | auth: PropTypes.object.isRequired, 16 | errors: PropTypes.object.isRequired, 17 | history: PropTypes.object.isRequired, 18 | }; 19 | 20 | componentDidMount() { 21 | // if user is authenticated, then redirect them to homepage 22 | if (this.props.auth.isAuthenticated) { 23 | this.props.history.push('/'); 24 | } 25 | } 26 | 27 | componentWillReceiveProps(nextProps) { 28 | // if user is authenticated, then redirect them to homepage 29 | if (nextProps.auth.isAuthenticated) { 30 | this.props.history.push('/'); 31 | } 32 | // if there are errors in the loginUser redux action, add errors to props 33 | if (nextProps.errors) { 34 | this.setState({ 35 | errors: nextProps.errors 36 | }); 37 | } 38 | } 39 | 40 | handleInputChange = e => { 41 | this.setState({ 42 | [e.target.name]: e.target.value 43 | }); 44 | }; 45 | 46 | handleSubmit = e => { 47 | e.preventDefault(); 48 | 49 | const user = { 50 | email: this.state.email, 51 | password: this.state.password 52 | }; 53 | 54 | // call redux action loginUser 55 | this.props.loginUser(user); 56 | }; 57 | 58 | render() { 59 | const { errors } = this.state; 60 | 61 | return ( 62 |
63 |

Login

64 |
65 |
66 |
67 | 77 | {errors.email && (
{errors.email}
)} 78 |
79 |
80 | 90 | {errors.password && (
{errors.password}
)} 91 |
92 |
93 | 94 |
95 |
96 |
97 |
98 | ); 99 | } 100 | } 101 | 102 | export default Login; -------------------------------------------------------------------------------- /src/components/Login/index.js: -------------------------------------------------------------------------------- 1 | import Login from './Login'; 2 | 3 | export default Login; -------------------------------------------------------------------------------- /src/components/OrderErrorMsg/OrderErrorMsg.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Alert } from 'reactstrap'; 4 | 5 | const OrderErrorMsg = ({ message }) => ( 6 | Error: {message} 7 | ); 8 | 9 | OrderErrorMsg.displayName = 'OrderErrorMsg'; 10 | OrderErrorMsg.propTypes = { 11 | message: PropTypes.string, 12 | }; 13 | 14 | OrderErrorMsg.defaultProps = { 15 | message: '', 16 | }; 17 | 18 | export default OrderErrorMsg; -------------------------------------------------------------------------------- /src/components/OrderErrorMsg/index.js: -------------------------------------------------------------------------------- 1 | import OrderErrorMsg from './OrderErrorMsg'; 2 | 3 | export default OrderErrorMsg; -------------------------------------------------------------------------------- /src/components/OrderModal/OrderModal.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Modal } from 'reactstrap'; 4 | import OrderModalHeader from './OrderModalHeader'; 5 | import OrderModalBody from './OrderModalBody'; 6 | import OrderModalFooter from './OrderModalFooter'; 7 | 8 | const OrderModal = ({ isOpen, toggleModal, listLineItems, selectedOrder }) => ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | 16 | OrderModal.propTypes = { 17 | isOpen: PropTypes.bool.isRequired, 18 | toggleModal: PropTypes.func.isRequired, 19 | listLineItems: PropTypes.func.isRequired, 20 | selectedOrder: PropTypes.object, 21 | }; 22 | 23 | OrderModal.defaultProps = { 24 | selectedOrder: {} 25 | }; 26 | OrderModal.displayName = 'OrderModal'; 27 | 28 | export default OrderModal; -------------------------------------------------------------------------------- /src/components/OrderModal/OrderModalBody.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ModalBody, Container } from 'reactstrap'; 4 | import OrderModalDocInfoRow from './OrderModalDocInfoRow'; 5 | import OrderModalRefIdRow from './OrderModalRefIdRow'; 6 | import OrderModalShipMethodRow from './OrderModalShipMethodRow'; 7 | import OrderModalBuyerShipRow from './OrderModalBuyerShipRow'; 8 | import OrderModalLineItemInfo from './OrderModalLineItemInfo'; 9 | 10 | const OrderModalBody = ({ selectedOrder, listLineItems }) => ( 11 | 12 | { 13 | selectedOrder && ( 14 | 15 | {/* Document Information */} 16 | 17 | {/* Reference Identificaiton + Datetime Reference */} 18 | 19 | {/* Shipping Method */} 20 | 21 | {/* Buyer/Shipping Details */} 22 | 23 | {/* Line item info */} 24 | 25 | 26 | ) 27 | } 28 | 29 | ); 30 | 31 | OrderModalBody.propTypes = { 32 | listLineItems: PropTypes.func.isRequired, 33 | selectedOrder: PropTypes.object, 34 | }; 35 | 36 | OrderModalBody.defaultProps = { 37 | selectedOrder: {} 38 | }; 39 | OrderModalBody.displayName = 'OrderModalBody'; 40 | 41 | export default OrderModalBody; -------------------------------------------------------------------------------- /src/components/OrderModal/OrderModalBuyerShipRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Row, Col, Container } from 'reactstrap'; 4 | 5 | const OrderModalBuyerShipRow = ({ selectedOrder }) => ( 6 | 7 | {/* LEFT */} 8 | 9 | 10 | Buyer Details 11 | 12 | {/* Buyer Address */} 13 | 14 | Bill-to-Party 15 | {selectedOrder["Buyer Data"]["Buyer Name"]} 16 | 17 | { selectedOrder["Buyer Data"]["Buyer Address Line 2"] ? 18 | selectedOrder["Buyer Data"]["Buyer Address Line 1"] + ', ' + selectedOrder["Buyer Data"]["Buyer Address Line 2"] : 19 | selectedOrder["Buyer Data"]["Buyer Address Line 1"] 20 | } 21 | 22 | 23 | {selectedOrder["Buyer Data"]["Buyer City"] + ', ' + selectedOrder["Buyer Data"]["Buyer State"] + ' ' + selectedOrder["Buyer Data"]["Buyer Zip"] + ', ' + selectedOrder["Buyer Data"]["Buyer Country"]} 24 | 25 | {/* Buyer Contact Info */} 26 | Contact Info 27 | 28 | 29 | Email: 30 | {selectedOrder["Buyer Data"]["Buyer Email"]} 31 | 32 | 33 | Phone: 34 | {selectedOrder["Buyer Data"]["Buyer Telephone"]} 35 | 36 | 37 | 38 | 39 | 40 | {/* RIGHT */} 41 | 42 | 43 | Shipping Details 44 | 45 | {/* Shipping Address */} 46 | 47 | Ship To 48 | {selectedOrder["Shipping Data"]["Shipping Name"]} 49 | 50 | { selectedOrder["Shipping Data"]["Shipping Address Line 2"] ? 51 | selectedOrder["Shipping Data"]["Shipping Address Line 1"] + ', ' + selectedOrder["Shipping Data"]["Shipping Address Line 2"] : 52 | selectedOrder["Shipping Data"]["Shipping Address Line 1"] 53 | } 54 | 55 | 56 | {selectedOrder["Shipping Data"]["Shipping City"] + ', ' + selectedOrder["Shipping Data"]["Shipping State"] + ' ' + selectedOrder["Shipping Data"]["Shipping Zip"] + ', ' + selectedOrder["Shipping Data"]["Ship Country"]} 57 | 58 | {/* Shipping Contact Info */} 59 | Contact Info 60 | 61 | 62 | Email: 63 | {selectedOrder["Shipping Data"]["Shipping Email"]} 64 | 65 | 66 | Phone: 67 | {selectedOrder["Shipping Data"]["Shipping Telephone"]} 68 | 69 | 70 | 71 | 72 | 73 | ); 74 | 75 | OrderModalBuyerShipRow.propTypes = { 76 | selectedOrder: PropTypes.object.isRequired, 77 | }; 78 | 79 | OrderModalBuyerShipRow.displayName = 'OrderModalBuyerShipRow'; 80 | 81 | export default OrderModalBuyerShipRow; -------------------------------------------------------------------------------- /src/components/OrderModal/OrderModalDocInfoRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import moment from "moment"; 4 | import { Row, Col } from 'reactstrap'; 5 | 6 | const OrderModalDocInfoRow = ({ selectedOrder }) => ( 7 | 8 | {/* Doc Info - LEFT */} 9 | 10 | 11 | Document Information 12 | 13 | 14 | Lumaprints Order #: 15 | {selectedOrder["Luma Order Number"]} 16 | 17 | 18 | Partner Order #: 19 | {selectedOrder["Partner Po Number"]} 20 | 21 | 22 | PO Date: 23 | {moment(selectedOrder["Transaction Set Data"]["Purchase Order Date"]).format("YYYY-MM-DD")} 24 | 25 | 26 | 27 | {/* Doc Info - RIGHT */} 28 | 29 | 30 |   31 | 32 | 33 | Purchase Order Type: 34 | {selectedOrder["Transaction Set Data"]["Purchase Order Type Code"][0]} 35 | 36 | 37 | Transaction Purpose: 38 | {selectedOrder["Transaction Set Data"]["Transaction Set Purpose Code"][0]} 39 | 40 | 41 | 42 | ); 43 | 44 | OrderModalDocInfoRow.propTypes = { 45 | selectedOrder: PropTypes.object.isRequired, 46 | }; 47 | 48 | OrderModalDocInfoRow.displayName = 'OrderModalDocInfoRow'; 49 | 50 | export default OrderModalDocInfoRow; -------------------------------------------------------------------------------- /src/components/OrderModal/OrderModalFooter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ModalFooter, Button } from 'reactstrap'; 4 | 5 | const OrderModalFooter = ({ toggleModal }) => ( 6 | 7 | 8 | 9 | ); 10 | 11 | OrderModalFooter.propTypes = { 12 | toggleModal: PropTypes.func.isRequired, 13 | }; 14 | 15 | OrderModalFooter.displayName = 'OrderModalFooter'; 16 | 17 | export default OrderModalFooter; -------------------------------------------------------------------------------- /src/components/OrderModal/OrderModalHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ModalHeader } from 'reactstrap'; 4 | 5 | const OrderModalHeader = ({ selectedOrder, toggleModal }) => ( 6 | 7 | { selectedOrder && (Lumaprints Purchase Order #: {selectedOrder["Luma Order Number"]}) } 8 | 9 | ); 10 | 11 | OrderModalHeader.propTypes = { 12 | toggleModal: PropTypes.func.isRequired, 13 | selectedOrder: PropTypes.object, 14 | }; 15 | 16 | OrderModalHeader.defaultProps = { 17 | selectedOrder: {} 18 | }; 19 | 20 | OrderModalHeader.displayName = 'OrderModalHeader'; 21 | 22 | export default OrderModalHeader; -------------------------------------------------------------------------------- /src/components/OrderModal/OrderModalLineItemInfo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Row, Col, Container, Table } from 'reactstrap'; 4 | 5 | const OrderModalLineItemInfo = ({ selectedOrder, listLineItems }) => ( 6 | 7 | 8 | 9 | Line Item Information 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | { listLineItems(selectedOrder) } 22 | 23 |
Line #DescriptionQtyUnitPrice($)Total($)
24 | 25 | 26 | 27 | Line Count: 28 | {selectedOrder["Transaction Set Data"]["Num Line Items"]} 29 | 30 | 31 | 32 |
33 |
34 | ); 35 | 36 | OrderModalLineItemInfo.propTypes = { 37 | listLineItems: PropTypes.func.isRequired, 38 | selectedOrder: PropTypes.object, 39 | }; 40 | 41 | OrderModalLineItemInfo.defaultProps = { 42 | selectedOrder: {} 43 | }; 44 | 45 | OrderModalLineItemInfo.displayName = 'OrderModalLineItemInfo'; 46 | 47 | export default OrderModalLineItemInfo; -------------------------------------------------------------------------------- /src/components/OrderModal/OrderModalRefIdRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import moment from "moment"; 4 | import { Row, Col } from 'reactstrap'; 5 | 6 | const OrderModalRefIdRow = ({ selectedOrder }) => ( 7 | 8 | {/* Ref ID - LEFT */} 9 | 10 | 11 | Reference Identification 12 | 13 | 14 | Vendor ID #: 15 | {selectedOrder["Transaction Set Data"]["Beginning Segment for Purchase Order"]["Vendor ID Number"]} 16 | 17 | 18 | Customer Order #: 19 | {selectedOrder["Transaction Set Data"]["Beginning Segment for Purchase Order"]["Order Number"]} 20 | 21 | 22 | Customer Ref #: 23 | {selectedOrder["Transaction Set Data"]["Beginning Segment for Purchase Order"]["Customer Reference Number"]} 24 | 25 | 26 | 27 | {/* Date/Time Ref - RIGHT */} 28 | 29 | 30 | Date/Time Reference 31 | 32 | 33 | Customer Order Date: 34 | {moment(selectedOrder["Transaction Set Data"]["DateTime References"]["Order"]).format("YYYY-MM-DD")} 35 | 36 | 37 | Requested Ship: 38 | {moment(selectedOrder["Transaction Set Data"]["DateTime References"]["Requested Ship"]).format("YYYY-MM-DD")} 39 | 40 | 41 | Delivery Requested: 42 | {moment(selectedOrder["Transaction Set Data"]["DateTime References"]["Delivery Requested"]).format("YYYY-MM-DD")} 43 | 44 | 45 | 46 | ); 47 | 48 | OrderModalRefIdRow.propTypes = { 49 | selectedOrder: PropTypes.object.isRequired, 50 | }; 51 | 52 | OrderModalRefIdRow.displayName = 'OrderModalRefIdRow'; 53 | 54 | export default OrderModalRefIdRow; -------------------------------------------------------------------------------- /src/components/OrderModal/OrderModalShipMethodRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Row, Col, Container } from 'reactstrap'; 4 | 5 | const OrderModalShipMethodRow = ({ selectedOrder }) => ( 6 | 7 | 8 | 9 | Shipping Details (Routing Sequence/Transit Time) 10 | 11 | 12 | Ship ID Code Qualifier: 13 | {selectedOrder["Transaction Set Data"]["Shipping Method ID Code Qualifier"][0]} 14 | 15 | 16 | Ship ID Code/Route: 17 | {selectedOrder["Transaction Set Data"]["Shipping Method ID Code"] + ' ' + selectedOrder["Transaction Set Data"]["Shipping Routing Method"]} 18 | 19 | 20 | 21 | ); 22 | 23 | OrderModalShipMethodRow.propTypes = { 24 | selectedOrder: PropTypes.object.isRequired, 25 | }; 26 | 27 | OrderModalShipMethodRow.displayName = 'OrderModalShipMethodRow'; 28 | 29 | export default OrderModalShipMethodRow; -------------------------------------------------------------------------------- /src/components/OrderModal/index.js: -------------------------------------------------------------------------------- 1 | import OrderModal from './OrderModal'; 2 | import OrderModalBody from './OrderModalBody'; 3 | import OrderModalBuyerShipRow from './OrderModalBuyerShipRow'; 4 | import OrderModalDocInfoRow from './OrderModalDocInfoRow'; 5 | import OrderModalFooter from './OrderModalFooter'; 6 | import OrderModalHeader from './OrderModalHeader'; 7 | import OrderModalLineItemInfo from './OrderModalLineItemInfo'; 8 | import OrderModalRefIdRow from './OrderModalRefIdRow'; 9 | import OrderModalShipMethodRow from './OrderModalShipMethodRow'; 10 | 11 | export { 12 | OrderModalBody, 13 | OrderModalBuyerShipRow, 14 | OrderModalDocInfoRow, 15 | OrderModalFooter, 16 | OrderModalHeader, 17 | OrderModalLineItemInfo, 18 | OrderModalRefIdRow, 19 | OrderModalShipMethodRow 20 | } 21 | 22 | export default OrderModal; -------------------------------------------------------------------------------- /src/components/OrderTable/OrderTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Sticky from 'react-stickynode'; 4 | import OrderTableHeader from './OrderTableHeader'; 5 | import OrderTableBody from './OrderTableBody'; 6 | import OrderTableFooter from './OrderTableFooter'; 7 | 8 | const OrderTable = ({ isLoading, 9 | orders, 10 | perPage, 11 | totalPages, 12 | totalOrders, 13 | currPage, 14 | initialPage, 15 | listOrders, 16 | listOrdersSkeleton, 17 | setCurrentOrder, 18 | onPerPageSelect, 19 | onPageClick }) => ( 20 |
21 | {/* Order Table Sticky Header (onScroll) */} 22 | 23 | 24 | {/* Order Table Body */} 25 | 35 | 36 | {/* Table footer - contains pagination and ordersPerPage select */} 37 | 44 |
45 | ); 46 | 47 | OrderTable.propTypes = { 48 | isLoading: PropTypes.bool.isRequired, 49 | orders: PropTypes.array, 50 | perPage: PropTypes.number.isRequired, 51 | totalPages: PropTypes.number.isRequired, 52 | totalOrders: PropTypes.number.isRequired, 53 | currPage: PropTypes.number.isRequired, 54 | initialPage: PropTypes.number, 55 | listOrders: PropTypes.func.isRequired, 56 | listOrdersSkeleton: PropTypes.func.isRequired, 57 | setCurrentOrder: PropTypes.func.isRequired, 58 | onPerPageSelect: PropTypes.func.isRequired, 59 | onPageClick: PropTypes.func.isRequired, 60 | }; 61 | 62 | OrderTable.defaultProps = { 63 | orders: [], 64 | initialPage: 0 65 | }; 66 | 67 | OrderTable.displayName = 'OrderTable'; 68 | 69 | export default OrderTable; -------------------------------------------------------------------------------- /src/components/OrderTable/OrderTableBody.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Table } from 'reactstrap'; 4 | 5 | const OrderTableBody = ({ isLoading, listOrdersSkeleton, listOrders, orders, perPage, currPage, totalOrders, setCurrentOrder }) => ( 6 | 7 | 8 | { isLoading ? listOrdersSkeleton(perPage, totalOrders) : listOrders(orders, perPage, currPage, totalOrders, setCurrentOrder) } 9 | 10 |
11 | ); 12 | 13 | OrderTableBody.propTypes = { 14 | isLoading: PropTypes.bool.isRequired, 15 | orders: PropTypes.array, 16 | perPage: PropTypes.number.isRequired, 17 | totalOrders: PropTypes.number.isRequired, 18 | currPage: PropTypes.number.isRequired, 19 | listOrders: PropTypes.func.isRequired, 20 | listOrdersSkeleton: PropTypes.func.isRequired, 21 | setCurrentOrder: PropTypes.func.isRequired, 22 | }; 23 | 24 | OrderTableBody.defaultProps = { 25 | orders: [] 26 | }; 27 | 28 | OrderTableBody.displayName = 'OrderTableBody'; 29 | 30 | export default OrderTableBody; -------------------------------------------------------------------------------- /src/components/OrderTable/OrderTableFooter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Row, Col } from 'reactstrap'; 4 | import OrderTableRowsPerPageToggle from './OrderTableRowsPerPageToggle'; 5 | import OrderTablePagination from './OrderTablePagination'; 6 | 7 | const OrderTableFooter = ({ perPage, totalPages, initialPage, handlePerPageSelect, handlePageClick }) => ( 8 | 9 | 10 | {/* Rows per page button group */} 11 | 15 | {/* pagination centered next to button group */} 16 | 21 | 22 | 23 | ); 24 | 25 | OrderTableFooter.propTypes = { 26 | perPage: PropTypes.number.isRequired, 27 | totalPages: PropTypes.number.isRequired, 28 | initialPage: PropTypes.number, 29 | handlePerPageSelect: PropTypes.func.isRequired, 30 | handlePageClick: PropTypes.func.isRequired, 31 | }; 32 | 33 | OrderTableFooter.defaultProps = { 34 | initialPage: 0 35 | }; 36 | 37 | OrderTableFooter.displayName = 'OrderTableFooter'; 38 | 39 | export default OrderTableFooter; -------------------------------------------------------------------------------- /src/components/OrderTable/OrderTableHeader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Container } from 'reactstrap'; 3 | 4 | const OrderTableHeader = () => ( 5 | 6 | 7 |
#
8 |
Filename
9 |
Luma Order Number
10 |
Partner Order Number
11 |
Date Placed
12 |
13 |
14 | ); 15 | 16 | OrderTableHeader.displayName = 'OrderTableHeader'; 17 | 18 | export default OrderTableHeader; -------------------------------------------------------------------------------- /src/components/OrderTable/OrderTablePagination.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Col } from 'reactstrap'; 4 | import ReactPaginate from 'react-paginate'; 5 | 6 | const OrderTablePagination = ({ totalPages, initialPage, handlePageClick }) => ( 7 | 8 | 30 | 31 | ); 32 | 33 | OrderTablePagination.propTypes = { 34 | totalPages: PropTypes.number.isRequired, 35 | initialPage: PropTypes.number, 36 | handlePageClick: PropTypes.func.isRequired, 37 | }; 38 | 39 | OrderTablePagination.defaultProps = { 40 | initialPage: 0 41 | }; 42 | 43 | OrderTablePagination.displayName = 'OrderTablePagination'; 44 | 45 | export default OrderTablePagination; -------------------------------------------------------------------------------- /src/components/OrderTable/OrderTableRowsPerPageToggle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Row, Col, ButtonGroup, Button } from 'reactstrap'; 4 | 5 | const OrderTableRowsPerPageToggle = ({ perPage, onPerPageSelect }) => ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | rows per page 15 | 16 | ); 17 | 18 | OrderTableRowsPerPageToggle.propTypes = { 19 | perPage: PropTypes.number.isRequired, 20 | onPerPageSelect: PropTypes.func.isRequired, 21 | }; 22 | 23 | OrderTableRowsPerPageToggle.displayName = 'OrderTableRowsPerPageToggle'; 24 | 25 | export default OrderTableRowsPerPageToggle; -------------------------------------------------------------------------------- /src/components/OrderTable/index.js: -------------------------------------------------------------------------------- 1 | import OrderTable from './OrderTable'; 2 | import OrderTableBody from './OrderTableBody'; 3 | import OrderTableFooter from './OrderTableFooter'; 4 | import OrderTableHeader from './OrderTableHeader'; 5 | import OrderTablePagination from './OrderTablePagination'; 6 | import OrderTableRowsPerPageToggle from './OrderTableRowsPerPageToggle'; 7 | 8 | export { 9 | OrderTableBody, 10 | OrderTableFooter, 11 | OrderTableHeader, 12 | OrderTablePagination, 13 | OrderTableRowsPerPageToggle 14 | } 15 | 16 | export default OrderTable; 17 | -------------------------------------------------------------------------------- /src/components/Register/Register.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | 5 | class Register extends Component { 6 | 7 | state = { 8 | name: '', 9 | email: '', 10 | password: '', 11 | password_confirm: '', 12 | errors: {} 13 | }; 14 | 15 | static propTypes = { 16 | registerUser: PropTypes.func.isRequired, 17 | auth: PropTypes.object.isRequired, 18 | errors: PropTypes.object.isRequired, 19 | history: PropTypes.object.isRequired, 20 | }; 21 | 22 | componentDidMount() { 23 | // if user is logged in, then they should not be able to access the Register page 24 | // redirect them to the homepage 25 | if (this.props.auth.isAuthenticated) { 26 | this.props.history.push('/'); 27 | } 28 | } 29 | 30 | componentWillReceiveProps(nextProps) { 31 | // if user is logged in, then they should not be able to access the Register page 32 | // redirect them to the homepage 33 | if (nextProps.auth.isAuthenticated) { 34 | this.props.history.push('/'); 35 | } 36 | // if there were any errors in registering the user, they will be added as props 37 | if (nextProps.errors) { 38 | this.setState({ 39 | errors: nextProps.errors 40 | }); 41 | } 42 | } 43 | 44 | handleInputChange = e => { 45 | this.setState({ 46 | [e.target.name]: e.target.value 47 | }); 48 | }; 49 | 50 | handleSubmit = e => { 51 | e.preventDefault(); 52 | 53 | const user = { 54 | name: this.state.name, 55 | email: this.state.email, 56 | password: this.state.password, 57 | password_confirm: this.state.password_confirm 58 | }; 59 | 60 | // call redux action registerUser 61 | this.props.registerUser(user, this.props.history); 62 | }; 63 | 64 | render() { 65 | const { errors } = this.state; 66 | 67 | return ( 68 |
69 |

Registration

70 |
71 |
72 |
73 | 83 | {errors.name && (
{errors.name}
)} 84 |
85 |
86 | 96 | {errors.email && (
{errors.email}
)} 97 |
98 |
99 | 109 | {errors.password && (
{errors.password}
)} 110 |
111 |
112 | 122 | {errors.password_confirm && (
{errors.password_confirm}
)} 123 |
124 |
125 | 126 |
127 |
128 |
129 |
130 | ); 131 | } 132 | } 133 | 134 | export default Register; -------------------------------------------------------------------------------- /src/components/Register/index.js: -------------------------------------------------------------------------------- 1 | import Register from './Register'; 2 | 3 | export default Register; -------------------------------------------------------------------------------- /src/components/SearchForm/SearchForm.css: -------------------------------------------------------------------------------- 1 | .datepicker-container { 2 | position: absolute; 3 | z-index: 1000; 4 | } -------------------------------------------------------------------------------- /src/components/SearchForm/SearchForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import DatePicker from 'react-datepicker'; 4 | import moment from 'moment'; 5 | 6 | import "react-datepicker/dist/react-datepicker.css"; 7 | import "./SearchForm.css"; 8 | 9 | class SearchForm extends Component { 10 | 11 | state = { 12 | searchText: '', 13 | isOpen: false, 14 | startDate: new Date() 15 | }; 16 | 17 | handleTextChange = (e) => { 18 | e.preventDefault(); 19 | this.setState({ [e.target.name]: e.target.value }); 20 | }; 21 | 22 | handleSubmit = (e) => { 23 | e.preventDefault(); 24 | 25 | const tmpSearchText = this.state.searchText; 26 | // redirect to search page 27 | this.setState({ searchText: '' }, () => { 28 | this.props.history.push(`/orders/search/${tmpSearchText}`); 29 | }); 30 | }; 31 | 32 | toggleDatePicker = (e) => { 33 | e.preventDefault(); 34 | this.setState({ isOpen: !this.state.isOpen }); 35 | }; 36 | 37 | handleDatepickerChange = (date) => { 38 | const formattedDate = moment(date).format("MM-DD-YYYY"); 39 | this.setState({ searchText: formattedDate, isOpen: false }); 40 | }; 41 | 42 | render() { 43 | return ( 44 |
45 |
46 |
47 |
48 | 51 |
52 | 62 |
63 | 69 |
70 |
71 | { 72 | this.state.isOpen && ( 73 | 79 | ) 80 | } 81 |
82 |
83 | ); 84 | } 85 | } 86 | 87 | export default withRouter(SearchForm); -------------------------------------------------------------------------------- /src/components/SearchForm/index.js: -------------------------------------------------------------------------------- 1 | import SearchForm from './SearchForm'; 2 | 3 | export default SearchForm; -------------------------------------------------------------------------------- /src/components/SearchOrders/SearchOrders.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import moment from 'moment'; 4 | import { range } from '../../utils/utils'; 5 | import { Container, Row, Col } from 'reactstrap'; 6 | 7 | import './searchOrders.css'; 8 | 9 | import OrderTable from '../OrderTable'; 10 | import OrderModal from '../OrderModal'; 11 | import OrderErrorMsg from '../OrderErrorMsg'; 12 | import SearchResultMetadata from '../SearchResultMetadata'; 13 | 14 | 15 | class SearchOrders extends Component { 16 | 17 | static propTypes = { 18 | fetchSearchOrders: PropTypes.func.isRequired, 19 | setSearchCurrentOrder: PropTypes.func.isRequired, 20 | setSearchRowsPerPage: PropTypes.func.isRequired, 21 | setSearchCurrentPage: PropTypes.func.isRequired, 22 | setSearchTerm: PropTypes.func.isRequired, 23 | orders: PropTypes.array.isRequired, 24 | currentPage: PropTypes.number.isRequired, 25 | perPage: PropTypes.number.isRequired, 26 | totalPages: PropTypes.number.isRequired, 27 | totalResults: PropTypes.number.isRequired, 28 | selectedOrder: PropTypes.object, 29 | isLoading: PropTypes.bool.isRequired, 30 | searchTerm: PropTypes.string, 31 | error: PropTypes.object, 32 | match: PropTypes.object.isRequired, 33 | history: PropTypes.object.isRequired, 34 | }; 35 | 36 | static defaultProps = { 37 | selectedOrder: {}, 38 | searchTerm: '', 39 | error: {} 40 | }; 41 | 42 | state = { modal: false }; 43 | 44 | async componentDidMount() { 45 | const { searchTerm } = this.props.match.params; 46 | 47 | await this.props.setSearchTerm(searchTerm); 48 | this.props.fetchSearchOrders(searchTerm, this.props.currentPage, this.props.perPage); 49 | } 50 | 51 | async componentWillReceiveProps(nextProps) { 52 | const { searchTerm } = this.props.match.params; 53 | 54 | if (nextProps.match.params.searchTerm !== searchTerm) { 55 | await this.props.setSearchTerm(nextProps.match.params.searchTerm); 56 | this.props.fetchSearchOrders(nextProps.match.params.searchTerm, this.props.currentPage, this.props.perPage); 57 | } 58 | } 59 | 60 | handlePageClick = (data) => { 61 | const { searchTerm } = this.props.match.params; 62 | 63 | this.props.setSearchCurrentPage(data.selected + 1).then(() => { 64 | // fetch next page data 65 | this.props.fetchSearchOrders(searchTerm, this.props.currentPage, this.props.perPage); 66 | }); 67 | }; 68 | 69 | listOrders = (orders, perPage, currPage, totalOrders, setCurrentOrder) => { 70 | const startIdx = totalOrders - (perPage * currPage-1); 71 | const idxRange = range(perPage, startIdx).reverse(); 72 | 73 | return orders.map((order, idx) => { 74 | return ( 75 | setCurrentOrder(order)}> 76 | {idxRange[idx]} 77 | {order["Filename"]} 78 | {order["Luma Order Number"]} 79 | {order["Partner Po Number"]} 80 | {moment(order["Transaction Set Data"]["Purchase Order Date"]).format("YYYY-MM-DD")} 81 | 82 | ); 83 | }); 84 | }; 85 | 86 | listOrdersSkeleton = (perPage, totalResults) => { 87 | const numRows = (perPage > totalResults) ? totalResults : perPage; 88 | const idxRange = range(numRows); 89 | 90 | return idxRange.map((order, idx) => { 91 | return ( 92 | 93 | ██ 94 | 95 | █████████████████████████████████████ 96 | 97 | 98 | █████ 99 | 100 | 101 | ██████ 102 | 103 | 104 | ███████ 105 | 106 | 107 | ); 108 | }); 109 | }; 110 | 111 | listLineItems = (order) => { 112 | let lineItems = []; 113 | 114 | for (let i=0; i 125 | {lineItemID} 126 | 127 | 128 | SKU: 129 | {sku} 130 | 131 | 132 | Item Desc: 133 | {itemDesc} 134 | 135 | 136 | {qty} 137 | {unitM} 138 | {itemCostEa} 139 | {itemCostThruQty} 140 | 141 | ); 142 | 143 | lineItems.push(currLineItem); 144 | } 145 | 146 | return lineItems; 147 | }; 148 | 149 | toggleModal = () => { 150 | this.setState({ modal: !this.state.modal }); 151 | }; 152 | 153 | setCurrentOrder = (order) => { 154 | this.props.setSearchCurrentOrder(order).then(() => { 155 | this.toggleModal(); 156 | }); 157 | }; 158 | 159 | handlePerPageSelect = (perPage) => { 160 | const { searchTerm } = this.props.match.params; 161 | 162 | this.props.setSearchRowsPerPage(perPage).then(() => { 163 | this.props.fetchSearchOrders(searchTerm, this.props.currentPage, this.props.perPage); 164 | }); 165 | }; 166 | 167 | render() { 168 | let errorMsg; 169 | if (this.props.error) { 170 | errorMsg = ; 171 | } 172 | 173 | return ( 174 | 175 | {errorMsg} 176 |

Search Results

177 | 178 | 179 | 180 | 193 | 194 | {/* Order Details Modal */} 195 | 201 |
202 | ); 203 | } 204 | } 205 | 206 | export default SearchOrders; -------------------------------------------------------------------------------- /src/components/SearchOrders/index.js: -------------------------------------------------------------------------------- 1 | import SearchOrders from './SearchOrders'; 2 | 3 | export default SearchOrders; -------------------------------------------------------------------------------- /src/components/SearchOrders/searchOrders.css: -------------------------------------------------------------------------------- 1 | .break-disabled > a { 2 | color: #6c757d; 3 | pointer-events: none; 4 | cursor: auto; 5 | background-color: #fff; 6 | position: relative; 7 | display: block; 8 | padding: .5rem .75rem; 9 | margin-left: -1px; 10 | line-height: 1.25; 11 | border: 1px solid #dee2e6; 12 | } 13 | 14 | .order-skeleton { 15 | /*background-image: linear-gradient(gray 80%, transparent 0);*/ 16 | color: #e7e7e7; 17 | pointer-events: none; 18 | cursor: auto; 19 | } 20 | 21 | .order-row { 22 | cursor: pointer; 23 | } 24 | 25 | .line-item-header { 26 | font-size: 11pt; 27 | font-weight: bold; 28 | line-height: 2 !important; 29 | margin-bottom: 0.5rem !important; 30 | } 31 | 32 | .line-item-head { 33 | font-weight: bold; 34 | vertical-align: middle !important; 35 | } 36 | 37 | .header-sticky { 38 | font-weight: bold; 39 | color: #fff; 40 | background-color: #212529; 41 | } 42 | 43 | .header-sticky > div { 44 | padding: .3rem; 45 | } 46 | 47 | .prev-next-label > a { 48 | padding-left: 1rem; 49 | padding-right: 1rem; 50 | } -------------------------------------------------------------------------------- /src/components/SearchResultMetadata/SearchResultMetadata.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Alert } from 'reactstrap'; 4 | 5 | const SearchResultMetadata = ({ searchTerm, numResults }) => { 6 | // determine the color of the alert 7 | let alertColor; 8 | if (numResults === 0) alertColor = 'danger'; 9 | else alertColor = 'success'; 10 | 11 | return ( 12 |
13 | 14 | Search term: "{searchTerm}" returned {numResults} results 15 | 16 |
17 | ); 18 | }; 19 | 20 | SearchResultMetadata.propTypes = { 21 | searchTerm: PropTypes.string.isRequired, 22 | numResults: PropTypes.number.isRequired, 23 | }; 24 | 25 | SearchResultMetadata.displayName = 'SearchResultMetadata'; 26 | 27 | export default SearchResultMetadata; -------------------------------------------------------------------------------- /src/components/SearchResultMetadata/index.js: -------------------------------------------------------------------------------- 1 | import SearchResultMetadata from './SearchResultMetadata'; 2 | 3 | export default SearchResultMetadata; -------------------------------------------------------------------------------- /src/components/TopNavbar/TopNavbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router-dom'; 4 | import { 5 | Collapse, 6 | Navbar, 7 | NavbarToggler, 8 | NavbarBrand, 9 | Nav, 10 | NavItem, 11 | NavLink, 12 | UncontrolledDropdown, 13 | DropdownToggle, 14 | DropdownMenu, 15 | DropdownItem } from 'reactstrap'; 16 | 17 | import SearchForm from "../SearchForm"; 18 | 19 | class TopNavbar extends Component { 20 | 21 | static propTypes = { 22 | logoutUser: PropTypes.func.isRequired, 23 | auth: PropTypes.object.isRequired 24 | }; 25 | 26 | state = { 27 | isOpen: false 28 | }; 29 | 30 | onLogout = e => { 31 | e.preventDefault(); 32 | this.props.logoutUser(this.props.history); 33 | }; 34 | 35 | toggle = () => { 36 | this.setState({ isOpen: !this.state.isOpen }); 37 | }; 38 | 39 | render() { 40 | const { isAuthenticated, user } = this.props.auth; 41 | 42 | // order link next to NavbarBrand 43 | // only rendered if authenticated 44 | const orderLink = ( 45 | 46 | Orders 47 | 48 | ); 49 | 50 | // search form, logout 51 | // only rendered if authenticated 52 | const authLinks = ( 53 |
    54 | {/* the inline search form w/ date picker */} 55 | 56 | 57 | 58 | 59 | {user.name} 66 | {user.name} 67 | 68 | 69 | 70 | Logout 71 | 72 | 73 | 74 |
75 | ); 76 | 77 | // navbar links when logged out 78 | const guestLinks = ( 79 |
    80 | 81 | Register 82 | 83 | 84 | Login 85 | 86 |
87 | ); 88 | 89 | return ( 90 | 91 | Lumaprints EDI Viewer 92 | 93 | 94 | 97 | 100 | 101 | 102 | ); 103 | } 104 | } 105 | 106 | export default TopNavbar; -------------------------------------------------------------------------------- /src/components/TopNavbar/index.js: -------------------------------------------------------------------------------- 1 | import TopNavbar from './TopNavbar'; 2 | 3 | export default TopNavbar; -------------------------------------------------------------------------------- /src/containers/ListAllOrdersContainer/ListAllOrdersContainer.js: -------------------------------------------------------------------------------- 1 | import ListAllOrders from '../../components/ListAllOrders'; 2 | import { withRouter } from 'react-router-dom'; 3 | // redux imports 4 | import { connect } from "react-redux"; 5 | import { 6 | fetchOrders, 7 | setCurrentOrder, 8 | setRowsPerPage, 9 | setCurrentPage 10 | } from "../../actions/orderActions"; 11 | 12 | const mapStateToProps = state => ({ 13 | orders: state.orderData.orders, 14 | currentPage: state.orderData.currentPage, 15 | perPage: state.orderData.perPage, 16 | totalPages: state.orderData.totalPages, 17 | totalResults: state.orderData.totalResults, 18 | selectedOrder: state.orderData.selectedOrder, 19 | isLoading: state.orderData.isLoading, 20 | error: state.orderData.error, 21 | }); 22 | 23 | const mapDispatchToProps = dispatch => ({ 24 | fetchOrders: (currPage, perPage) => dispatch(fetchOrders(currPage, perPage)), 25 | setCurrentOrder: (order) => dispatch(setCurrentOrder(order)), 26 | setRowsPerPage: (perPage) => dispatch(setRowsPerPage(perPage)), 27 | setCurrentPage: (currPage) => dispatch(setCurrentPage(currPage)) 28 | }); 29 | 30 | export default connect( 31 | mapStateToProps, 32 | mapDispatchToProps 33 | )(withRouter(ListAllOrders)); -------------------------------------------------------------------------------- /src/containers/ListAllOrdersContainer/index.js: -------------------------------------------------------------------------------- 1 | import ListAllOrdersContainer from './ListAllOrdersContainer'; 2 | 3 | export default ListAllOrdersContainer; -------------------------------------------------------------------------------- /src/containers/LoginContainer/LoginContainer.js: -------------------------------------------------------------------------------- 1 | import Login from '../../components/Login'; 2 | import { connect } from 'react-redux'; 3 | import { withRouter } from 'react-router-dom'; 4 | import { loginUser } from "../../actions/authActions"; 5 | 6 | const mapStateToProps = state => ({ 7 | auth: state.auth, 8 | errors: state.authErrors 9 | }); 10 | 11 | export default connect( 12 | mapStateToProps, 13 | { loginUser } 14 | )(withRouter(Login)); -------------------------------------------------------------------------------- /src/containers/LoginContainer/index.js: -------------------------------------------------------------------------------- 1 | import LoginContainer from './LoginContainer'; 2 | 3 | export default LoginContainer; -------------------------------------------------------------------------------- /src/containers/ProtectedRouteContainer/ProtectedRouteContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { Route, Redirect } from 'react-router-dom'; 5 | 6 | // higher order component that ensures user is authenticated before rendering route component 7 | // if not logged in, redirect to login page 8 | const ProtectedRoute = ({ component: Component, auth, ...rest }) => ( 9 | ( 10 | auth.isAuthenticated ? 11 | ( ) : 12 | ( ) 16 | )} /> 17 | ); 18 | 19 | ProtectedRoute.displayName = 'ProtectedRoute'; 20 | ProtectedRoute.propTypes = { 21 | auth: PropTypes.object.isRequired, 22 | }; 23 | 24 | const mapStateToProps = state => ({ 25 | auth: state.auth 26 | }); 27 | 28 | export default connect( 29 | mapStateToProps 30 | )(ProtectedRoute); -------------------------------------------------------------------------------- /src/containers/ProtectedRouteContainer/index.js: -------------------------------------------------------------------------------- 1 | import ProtectedRoute from './ProtectedRouteContainer'; 2 | 3 | export default ProtectedRoute; 4 | -------------------------------------------------------------------------------- /src/containers/RegisterContainer/RegisterContainer.js: -------------------------------------------------------------------------------- 1 | import Register from '../../components/Register'; 2 | import { connect } from 'react-redux'; 3 | import { withRouter } from 'react-router-dom'; 4 | import { registerUser } from "../../actions/authActions"; 5 | 6 | const mapStateToProps = state => ({ 7 | auth: state.auth, 8 | errors: state.authErrors 9 | }); 10 | 11 | export default connect( 12 | mapStateToProps, 13 | { registerUser } 14 | )(withRouter(Register)); -------------------------------------------------------------------------------- /src/containers/RegisterContainer/index.js: -------------------------------------------------------------------------------- 1 | import RegisterContainer from './RegisterContainer'; 2 | 3 | export default RegisterContainer; -------------------------------------------------------------------------------- /src/containers/SearchOrdersContainer/SearchOrdersContainer.js: -------------------------------------------------------------------------------- 1 | import SearchOrders from '../../components/SearchOrders'; 2 | import { withRouter } from 'react-router-dom'; 3 | // redux imports 4 | import { connect } from "react-redux"; 5 | import { 6 | fetchSearchOrders, 7 | setSearchCurrentOrder, 8 | setSearchRowsPerPage, 9 | setSearchCurrentPage, 10 | setSearchTerm 11 | } from "../../actions/orderSearchActions"; 12 | 13 | const mapStateToProps = state => ({ 14 | orders: state.searchOrderData.orders, 15 | currentPage: state.searchOrderData.currentPage, 16 | perPage: state.searchOrderData.perPage, 17 | totalPages: state.searchOrderData.totalPages, 18 | totalResults: state.searchOrderData.totalResults, 19 | selectedOrder: state.searchOrderData.selectedOrder, 20 | isLoading: state.searchOrderData.isLoading, 21 | error: state.searchOrderData.error, 22 | searchTerm: state.searchOrderData.searchTerm, 23 | }); 24 | 25 | const mapDispatchToProps = dispatch => ({ 26 | fetchSearchOrders: (searchTerm, currPage, perPage) => dispatch(fetchSearchOrders(searchTerm, currPage, perPage)), 27 | setSearchCurrentOrder: (order) => dispatch(setSearchCurrentOrder(order)), 28 | setSearchRowsPerPage: (perPage) => dispatch(setSearchRowsPerPage(perPage)), 29 | setSearchCurrentPage: (currPage) => dispatch(setSearchCurrentPage(currPage)), 30 | setSearchTerm: (searchTerm) => dispatch(setSearchTerm(searchTerm)) 31 | }); 32 | 33 | export default connect( 34 | mapStateToProps, 35 | mapDispatchToProps 36 | )(withRouter(SearchOrders)); -------------------------------------------------------------------------------- /src/containers/SearchOrdersContainer/index.js: -------------------------------------------------------------------------------- 1 | import SearchOrdersContainer from './SearchOrdersContainer'; 2 | 3 | export default SearchOrdersContainer; -------------------------------------------------------------------------------- /src/containers/TopNavbarContainer/NavbarContainer.js: -------------------------------------------------------------------------------- 1 | import TopNavbar from '../../components/TopNavbar'; 2 | import { withRouter } from "react-router-dom"; 3 | import { connect } from 'react-redux'; 4 | import { logoutUser } from '../../actions/authActions'; 5 | 6 | const mapStateToProps = state => ({ 7 | auth: state.auth 8 | }); 9 | 10 | const mapDispatchToProps = dispatch => ({ 11 | logoutUser: (history) => dispatch(logoutUser(history)) 12 | }); 13 | 14 | export default connect( 15 | mapStateToProps, 16 | mapDispatchToProps 17 | )(withRouter(TopNavbar)); -------------------------------------------------------------------------------- /src/containers/TopNavbarContainer/index.js: -------------------------------------------------------------------------------- 1 | import Navbar from './NavbarContainer'; 2 | 3 | export default Navbar; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | // import provider to connect App to redux store 7 | import { Provider } from 'react-redux'; 8 | // import the redux store to be used in the Provider component 9 | import store from './store'; 10 | // authentication imports 11 | import jwt_decode from 'jwt-decode'; 12 | import setAuthToken from './setAuthToken'; 13 | import { setCurrentUser, logoutUser } from "./actions/authActions"; 14 | 15 | // when app starts, check localStorage if jwtToken is set, if so, setCurrentUser 16 | // then check jwtToken expiration date, if expired, then logout user, redirect to /login 17 | if (localStorage.jwtToken) { 18 | setAuthToken(localStorage.jwtToken); 19 | const decoded = jwt_decode(localStorage.jwtToken); 20 | store.dispatch(setCurrentUser(decoded)); 21 | 22 | // check if token is expired 23 | // if so, redirect to login 24 | const currentTime = Date.now() / 1000; 25 | if (decoded.exp < currentTime) { 26 | store.dispatch(logoutUser()); 27 | window.location.href = '/login'; 28 | } 29 | } 30 | 31 | ReactDOM.render( 32 | 33 | 34 | , 35 | document.getElementById('root')); 36 | 37 | // If you want your app to work offline and load faster, you can change 38 | // unregister() to register() below. Note this comes with some pitfalls. 39 | // Learn more about service workers: http://bit.ly/CRA-PWA 40 | serviceWorker.unregister(); 41 | -------------------------------------------------------------------------------- /src/is-empty.js: -------------------------------------------------------------------------------- 1 | // helper function to check if the passed value is undefined, null, or objects or strings length = 0 2 | const isEmpty = (value) => { 3 | return ( 4 | value === undefined || 5 | value === null || 6 | (typeof value === 'object' && Object.keys(value).length === 0) || 7 | (typeof value === 'string' && value.trim().length === 0) 8 | ); 9 | }; 10 | 11 | export default isEmpty; -------------------------------------------------------------------------------- /src/reducers/authentication/authErrorReducer.js: -------------------------------------------------------------------------------- 1 | import { GET_AUTH_ERRORS } from "../../actions/types"; 2 | 3 | const initialState = {}; 4 | 5 | export default function(state = initialState, action) { 6 | switch(action.type) { 7 | case GET_AUTH_ERRORS: 8 | return action.payload; 9 | default: 10 | return state; 11 | } 12 | }; -------------------------------------------------------------------------------- /src/reducers/authentication/authReducer.js: -------------------------------------------------------------------------------- 1 | import { SET_CURRENT_USER } from "../../actions/types"; 2 | import isEmpty from '../../is-empty'; 3 | 4 | const initialState = { 5 | isAuthenticated: false, 6 | user: {} 7 | }; 8 | 9 | export default function(state = initialState, action) { 10 | switch(action.type) { 11 | case SET_CURRENT_USER: 12 | return { 13 | ...state, 14 | isAuthenticated: !isEmpty(action.payload), 15 | user: action.payload 16 | }; 17 | default: 18 | return state; 19 | } 20 | }; -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | // this creates and exports the rootReducer 2 | // all reducers should be imported here, and added as key:value pairs in the combineReducers fn 3 | import { combineReducers } from 'redux'; 4 | import authErrorReducer from "./authentication/authErrorReducer"; 5 | import authReducer from "./authentication/authReducer"; 6 | import ordersReducer from './orders/orderReducer'; 7 | import orderSearchReducer from './orders/orderSearchReducer'; 8 | 9 | // export rootReducer 10 | export default combineReducers({ 11 | auth: authReducer, 12 | authErrors: authErrorReducer, 13 | orderData: ordersReducer, 14 | searchOrderData: orderSearchReducer 15 | }); 16 | -------------------------------------------------------------------------------- /src/reducers/orders/orderReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_ORDERS_BEGIN, 3 | FETCH_ORDERS_SUCCESS, 4 | FETCH_ORDERS_FAILURE, 5 | SET_CURRENT_ORDER, 6 | SET_ORDER_ROWS_PER_PAGE, 7 | SET_CURRENT_PAGE 8 | } from "../../actions/types"; 9 | 10 | const initialState = { 11 | orders: [], 12 | currentPage: 1, 13 | totalPages: 0, 14 | totalResults: 0, 15 | perPage: 20, 16 | selectedOrder: null, 17 | isLoading: true, 18 | error: null 19 | }; 20 | 21 | export default function orderReducer(state = initialState, action) { 22 | const { payload } = action; 23 | 24 | switch(action.type) { 25 | case FETCH_ORDERS_BEGIN: 26 | return { 27 | ...state, 28 | isLoading: true, 29 | error: null 30 | }; 31 | case FETCH_ORDERS_SUCCESS: 32 | return { 33 | ...state, 34 | isLoading: false, 35 | orders: payload.data, 36 | currentPage: payload.currentPage, 37 | perPage: payload.perPage, 38 | totalPages: payload.totalPages, 39 | totalResults: payload.totalResults 40 | }; 41 | case FETCH_ORDERS_FAILURE: 42 | return { 43 | ...state, 44 | isLoading: false, 45 | error: payload.error 46 | }; 47 | case SET_CURRENT_ORDER: 48 | return { 49 | ...state, 50 | selectedOrder: payload.order 51 | }; 52 | case SET_ORDER_ROWS_PER_PAGE: 53 | return { 54 | ...state, 55 | perPage: payload.perPage 56 | }; 57 | case SET_CURRENT_PAGE: 58 | return { 59 | ...state, 60 | isLoading: true, 61 | currentPage: payload.currPage 62 | }; 63 | default: 64 | return state; 65 | } 66 | } -------------------------------------------------------------------------------- /src/reducers/orders/orderSearchReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | FETCH_SEARCH_ORDERS_BEGIN, 3 | FETCH_SEARCH_ORDERS_SUCCESS, 4 | FETCH_SEARCH_ORDERS_FAILURE, 5 | SET_SEARCH_CURRENT_ORDER, 6 | SET_SEARCH_ORDER_ROWS_PER_PAGE, 7 | SET_SEARCH_CURRENT_PAGE, SET_SEARCH_TERM 8 | } from "../../actions/types"; 9 | 10 | const initialState = { 11 | orders: [], 12 | searchTerm: '', 13 | currentPage: 1, 14 | totalPages: 0, 15 | totalResults: 0, 16 | perPage: 20, 17 | selectedOrder: null, 18 | isLoading: true, 19 | error: null 20 | }; 21 | 22 | export default function orderSearchReducer(state = initialState, action) { 23 | const { payload } = action; 24 | 25 | switch (action.type) { 26 | case FETCH_SEARCH_ORDERS_BEGIN: 27 | return { 28 | ...state, 29 | isLoading: true, 30 | error: null 31 | }; 32 | case FETCH_SEARCH_ORDERS_SUCCESS: 33 | return { 34 | ...state, 35 | orders: payload.data, 36 | currentPage: payload.currentPage, 37 | perPage: payload.perPage, 38 | totalPages: payload.totalPages, 39 | totalResults: payload.totalResults, 40 | isLoading: false, 41 | }; 42 | case FETCH_SEARCH_ORDERS_FAILURE: 43 | return { 44 | ...state, 45 | isLoading: false, 46 | error: payload.error 47 | }; 48 | case SET_SEARCH_CURRENT_ORDER: 49 | return { 50 | ...state, 51 | selectedOrder: payload.order 52 | }; 53 | case SET_SEARCH_ORDER_ROWS_PER_PAGE: 54 | return { 55 | ...state, 56 | perPage: payload.perPage 57 | }; 58 | case SET_SEARCH_CURRENT_PAGE: 59 | return { 60 | ...state, 61 | isLoading: true, 62 | currentPage: payload.currPage 63 | }; 64 | case SET_SEARCH_TERM: 65 | return { 66 | ...state, 67 | searchTerm: payload.searchTerm 68 | }; 69 | default: 70 | return state; 71 | } 72 | } -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/setAuthToken.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | // if JWT token is present, set the Authorization header to always include the token 4 | // else delete the Authorization token if it's present 5 | const setAuthToken = token => { 6 | if (token) { 7 | axios.defaults.headers.common['Authorization'] = token; 8 | } else { 9 | delete axios.defaults.headers.common['Authorization']; 10 | } 11 | }; 12 | 13 | export default setAuthToken; -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | // this creates the initial redux store that is passed to the Provider component in React 2 | // this is setup to connect to the Redux Chrome Dev tools extension 3 | import { createStore, applyMiddleware, compose } from 'redux'; 4 | import thunk from 'redux-thunk'; 5 | import rootReducer from './reducers'; 6 | 7 | // initial state object that reducer states are added to 8 | const initialState = {}; 9 | 10 | // add any additional middleware to this array 11 | const middleware = [thunk]; 12 | 13 | const store = createStore( 14 | rootReducer, 15 | initialState, 16 | compose( 17 | applyMiddleware(...middleware), 18 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 19 | ) 20 | ); 21 | 22 | export default store; -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | export const range = (size, startAt = 0) => { 2 | return [...Array(size).keys()].map(i => i + startAt); 3 | }; -------------------------------------------------------------------------------- /validation/is-empty.js: -------------------------------------------------------------------------------- 1 | // helper function to check if the passed value is undefined, null, or objects or strings length = 0 2 | const isEmpty = (value) => { 3 | return ( 4 | value === undefined || 5 | value === null || 6 | (typeof value === 'object' && Object.keys(value).length === 0) || 7 | (typeof value === 'string' && value.trim().length === 0) 8 | ); 9 | }; 10 | 11 | module.exports = isEmpty; -------------------------------------------------------------------------------- /validation/login.js: -------------------------------------------------------------------------------- 1 | const Validator = require("validator"); 2 | const isEmpty = require("./is-empty"); 3 | 4 | module.exports = function validateLoginInput(data) { 5 | let errors = {}; 6 | data.email = !isEmpty(data.email) ? data.email : ''; 7 | data.password = !isEmpty(data.password) ? data.password : ''; 8 | 9 | // checks if email field is a properly formatted email 10 | if (!Validator.isEmail(data.email)) { 11 | errors.email = 'Email is invalid'; 12 | } 13 | // checks if email field is empty 14 | if (Validator.isEmpty(data.email)) { 15 | errors.email = 'Email is required'; 16 | } 17 | // checks if password is between 6-30 chars 18 | if (!Validator.isLength(data.password, { min: 6, max: 30 })) { 19 | errors.password = 'Password must have at least 6 chars'; 20 | } 21 | // checks if password is empty 22 | if (Validator.isEmpty(data.password)) { 23 | errors.password = 'Password is required'; 24 | } 25 | 26 | // return object that contains any errors (if there are any), and boolean value if isValid 27 | return { 28 | errors, 29 | isValid: isEmpty(errors) 30 | }; 31 | }; -------------------------------------------------------------------------------- /validation/register.js: -------------------------------------------------------------------------------- 1 | const Validator = require("validator"); 2 | const isEmpty = require("./is-empty"); 3 | 4 | module.exports = function validateRegisterInput(data) { 5 | let errors = {}; 6 | data.name = !isEmpty(data.name) ? data.name : ''; 7 | data.email = !isEmpty(data.email) ? data.email : ''; 8 | data.password = !isEmpty(data.password) ? data.password : ''; 9 | data.password_confirm = !isEmpty(data.password_confirm) ? data.password_confirm : ''; 10 | 11 | // check name input field length is between 2-30 chars 12 | if (!Validator.isLength(data.name, { min: 2, max: 30 })) { 13 | errors.name = 'Name must be between 2 to 30 chars'; 14 | } 15 | // check name field is not empty 16 | if (Validator.isEmpty(data.name)) { 17 | errors.name = 'Name field is required'; 18 | } 19 | // check email field is a properly formatted email string 20 | if (!Validator.isEmail(data.email)) { 21 | errors.email = 'Email is invalid'; 22 | } 23 | // check email field is not empty 24 | if (Validator.isEmpty(data.email)) { 25 | errors.email = 'Email field is required'; 26 | } 27 | // check password field length is between 6-30 chars 28 | if (!Validator.isLength(data.password, { min:6, max: 30 })) { 29 | errors.password = 'Password must have 6 chars'; 30 | } 31 | // check password field is not empty 32 | if (Validator.isEmpty(data.password)) { 33 | errors.password = 'Password field is required'; 34 | } 35 | // check password confirm field is between 6-30 chars 36 | if (!Validator.isLength(data.password_confirm, { min: 6, max: 30 })) { 37 | errors.password_confirm = 'Password must have 6 chars'; 38 | } 39 | // check password and password confirm fields are equal 40 | if (!Validator.equals(data.password, data.password_confirm)) { 41 | errors.password_confirm = 'Password and Confirm Password must match'; 42 | } 43 | // check password confirm field is not empty 44 | if (Validator.isEmpty(data.password_confirm)) { 45 | errors.password_confirm = 'Confirm password is required'; 46 | } 47 | 48 | // return object that contains any errors (if there are any), and boolean value if isValid 49 | return { 50 | errors, 51 | isValid: isEmpty(errors) 52 | }; 53 | }; --------------------------------------------------------------------------------