├── .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 |
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 | Line # |
15 | Description |
16 | Qty |
17 | Unit |
18 | Price($) |
19 | Total($) |
20 |
21 | { listLineItems(selectedOrder) }
22 |
23 |
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 |
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 |
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 |
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 | };
--------------------------------------------------------------------------------