diff --git a/.babelrc b/.babelrc index 1d53eae..aae63c3 100644 --- a/.babelrc +++ b/.babelrc @@ -1,15 +1,18 @@ { - "presets": [[ - "@babel/preset-env", - { - "targets": { - "node": "10" + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "node": "10" + } } - } - ], "@babel/preset-react"], + ], + "@babel/preset-react" + ], "plugins": [ - "@babel/plugin-proposal-class-properties" + "@babel/plugin-proposal-class-properties", + "@babel/plugin-proposal-optional-chaining", + "@babel/plugin-proposal-nullish-coalescing-operator" ] } - - diff --git a/.env-sample b/.env-example similarity index 67% rename from .env-sample rename to .env-example index 286bc38..147f7c8 100644 --- a/.env-sample +++ b/.env-example @@ -1 +1,2 @@ REACT_APP_BACKEND_URL = backend_url; +URL="https://url" diff --git a/.gitignore b/.gitignore index d9cd4cd..cbcfe7e 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,4 @@ debug.log* .coveralls.yml .env - +.dccache diff --git a/jest.config.js b/jest.config.js index 8e03884..1aa66de 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,14 +1,14 @@ module.exports = { - snapshotSerializers: ['enzyme-to-json/serializer'], - setupFilesAfterEnv: ['./src/setupTest.js'], + snapshotSerializers: ["enzyme-to-json/serializer"], + setupFilesAfterEnv: ["./src/setupTest.js"], verbose: true, - coverageDirectory: './coverage', + coverageDirectory: "./coverage", moduleNameMapper: { - '\\.(css|less|scss)$': 'identity-obj-proxy', - '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': - '/src/tests/__mocks__/fileMock.js', + "\\.(css|less|scss)$": "identity-obj-proxy", + "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": + "/src/tests/__mocks__/fileMock.js", }, - setupFiles: ['./src/setupTest.js'], - testPathIgnorePatterns: ['redux'], - coveragePathIgnorePatterns: ['redux'], + setupFiles: ["./src/setupTest.js"], + testPathIgnorePatterns: ["redux"], + coveragePathIgnorePatterns: ["redux"], }; diff --git a/package.json b/package.json index 124d386..fe332d4 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,17 @@ "dependencies": { "@babel/core": "7.5.4", "@babel/plugin-proposal-class-properties": "^7.12.1", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", + "@babel/plugin-proposal-optional-chaining": "^7.12.7", "@babel/plugin-syntax-class-properties": "^7.12.1", "@babel/polyfill": "^7.12.1", "@babel/preset-env": "7.5.4", "@babel/preset-react": "7.0.0", + "@fortawesome/fontawesome-svg-core": "^1.2.32", + "@fortawesome/free-brands-svg-icons": "^5.15.1", + "@fortawesome/free-regular-svg-icons": "^5.15.1", + "@fortawesome/free-solid-svg-icons": "^5.15.1", + "@fortawesome/react-fontawesome": "^0.1.12", "axios": "^0.21.0", "babel-loader": "8.0.6", "babel-plugin-transform-class-properties": "^6.24.1", @@ -61,6 +68,7 @@ "@storybook/addon-essentials": "^6.0.28", "@storybook/addon-links": "^6.0.28", "@storybook/react": "^6.0.28", + "@testing-library/react-hooks": "^3.4.2", "babel-jest": "^26.6.3", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.5", @@ -69,6 +77,7 @@ "identity-obj-proxy": "^3.0.0", "jest": "^26.6.3", "moxios": "^0.4.0", + "jest-enzyme": "^7.1.2", "node-sass": "^5.0.0", "react-is": "^17.0.1", "react-test-renderer": "16.8.6", diff --git a/redux/actions/alerts.js b/redux/actions/alerts.js new file mode 100644 index 0000000..536866e --- /dev/null +++ b/redux/actions/alerts.js @@ -0,0 +1,7 @@ +import { SET_ERROR, SET_SUCCESS } from "./types"; + +export const success = (payload) => (dispatch) => + dispatch({ type: SET_SUCCESS, payload }); + +export const resError = (payload) => (dispatch) => + dispatch({ type: SET_ERROR, payload }); diff --git a/redux/actions/roles.js b/redux/actions/roles.js new file mode 100644 index 0000000..7cd8745 --- /dev/null +++ b/redux/actions/roles.js @@ -0,0 +1,67 @@ +import { GET_ROLES, CREATE_ROLE, DELETE_ROLE, UPDATE_ROLE } from "./types"; +import { creating, updating, deleting, resolved } from "./statuses"; +import { success, resError } from "./alerts"; +import axiosBase from "../axiosBase"; + +export const getRoles = () => async (dispatch) => { + try { + const res = await axiosBase.get("/role"); + dispatch({ + type: GET_ROLES, + payload: res.data, + }); + dispatch(success({ message: res.data.message })); + } catch (error) { + const { data } = error.response; + dispatch(resError({ error: data.error || data.message || data.msg })); + } +}; + +export const createRole = (role) => async (dispatch) => { + dispatch(creating()); + try { + const res = await axiosBase.post("/role/register", role); + dispatch({ + type: CREATE_ROLE, + payload: res.data, + }); + dispatch(resolved()); + dispatch(success({ message: res.data.msg })); + } catch (error) { + dispatch(resolved()); + dispatch(resError({ error: error.response.data.error })); + } +}; + +export const updateRole = (id, role) => async (dispatch) => { + dispatch(updating()); + try { + const res = await axiosBase.patch(`/role/${id}`, role); + dispatch({ + type: UPDATE_ROLE, + payload: res.data, + }); + dispatch(resolved()); + dispatch(success({ message: res.data.message })); + } catch (error) { + dispatch(resolved()); + dispatch(resError({ error: error.response.data.error })); + } +}; + +export const deleteRole = (role) => async (dispatch) => { + dispatch(deleting()); + try { + const res = await axiosBase.delete(`/role/${role.id}`); + dispatch({ + type: DELETE_ROLE, + payload: res.data, + id: role.id, + }); + dispatch(resolved()); + dispatch(success({ message: res.data.message })); + } catch (error) { + dispatch(resolved()); + dispatch(resError({ error: error.response.data.error })); + } +}; diff --git a/redux/actions/statuses.js b/redux/actions/statuses.js new file mode 100644 index 0000000..f429e0f --- /dev/null +++ b/redux/actions/statuses.js @@ -0,0 +1,17 @@ +import { + CREATING_ROLE, + ASSIGNING_ROLE, + DELETING_ROLE, + UPDATING_ROLE, + SET_RESOLVED, +} from "./types"; + +export const creating = () => (dispatch) => dispatch({ type: CREATING_ROLE }); + +export const updating = () => (dispatch) => dispatch({ type: UPDATING_ROLE }); + +export const assigning = () => (dispatch) => dispatch({ type: ASSIGNING_ROLE }); + +export const deleting = () => (dispatch) => dispatch({ type: DELETING_ROLE }); + +export const resolved = () => (dispatch) => dispatch({ type: SET_RESOLVED }); diff --git a/redux/actions/types.js b/redux/actions/types.js index 8ce3455..87dd439 100644 --- a/redux/actions/types.js +++ b/redux/actions/types.js @@ -3,3 +3,18 @@ export const SET_LOADING = "SET_LOADING"; export const SET_RESPONSE = "SET_RESPONSE"; export const SET_TOKEN = "SET_TOKEN"; export const SET_AUTH_STATUS = "SET_AUTH_STATUS"; + +export const GET_ROLES = "GET_ROLES"; +export const CREATE_ROLE = "CREATE_ROLE"; +export const UPDATE_ROLE = "UPDATE_ROLE"; +export const DELETE_ROLE = "DELETE_ROLE"; +export const ASSIGN_ROLE = "ASSIGN_ROLE"; +export const GET_USERS = "GET_USERS"; + +export const CREATING_ROLE = "CREATING_ROLE"; +export const UPDATING_ROLE = "UPDATING_ROLE"; +export const DELETING_ROLE = "DELETING_ROLE"; +export const ASSIGNING_ROLE = "ASSIGNING_ROLE"; +export const SET_RESOLVED = "SET_RESOLVED"; + +export const SET_SUCCESS = "SET_SUCCESS"; diff --git a/redux/actions/users.js b/redux/actions/users.js new file mode 100644 index 0000000..bb22302 --- /dev/null +++ b/redux/actions/users.js @@ -0,0 +1,38 @@ +import { GET_USERS, ASSIGN_ROLE, SET_ERROR } from "./types"; +import { assigning, resolved } from "./statuses"; +import { success, resError } from "./alerts"; +import axiosBase from "../axiosBase"; + +export const getUsers = () => async (dispatch) => { + try { + const res = await axiosBase.get("/users"); + dispatch({ + type: GET_USERS, + payload: res.data, + }); + } catch (error) { + const { data } = error.response; + dispatch(resError({ error: data.error || data.message || data.msg })); + } +}; + +export const assignRole = ({ userRole, email }) => async (dispatch) => { + dispatch(assigning()); + try { + const res = await axiosBase.post("/role/assign", { + userRole, + email, + }); + const usersRes = await axiosBase.get("/users"); + + dispatch({ + type: ASSIGN_ROLE, + payload: { users: usersRes.data.users, msg: res.data.message }, + }); + dispatch(success({ message: res.data.msg })); + dispatch(resolved()); + } catch (error) { + dispatch(resError({ error: error.response.data?.error })); + dispatch(resolved()); + } +}; diff --git a/redux/axiosBase.js b/redux/axiosBase.js new file mode 100644 index 0000000..fcc29ec --- /dev/null +++ b/redux/axiosBase.js @@ -0,0 +1,14 @@ +import axios from "axios"; + +const url = process.env.URL; +const token = localStorage.getItem("token"); + +const axiosBase = axios.create({ + baseURL: url, + headers: { + "x-auth-token": token, + "Content-Type": "application/json", + }, +}); + +export default axiosBase; diff --git a/redux/reducers/alerts.js b/redux/reducers/alerts.js new file mode 100644 index 0000000..cee991b --- /dev/null +++ b/redux/reducers/alerts.js @@ -0,0 +1,17 @@ +import { SET_ERROR, SET_SUCCESS } from "../actions/types"; + +const initialState = { + message: "", + color: "", +}; + +export default (state = initialState, action) => { + switch (action.type) { + case SET_SUCCESS: + return { ...state, message: action.payload.message, color: "success" }; + case SET_ERROR: + return { ...state, message: action.payload.error, color: "danger" }; + default: + return state; + } +}; diff --git a/redux/reducers/index.js b/redux/reducers/index.js index 00bc84b..055394f 100644 --- a/redux/reducers/index.js +++ b/redux/reducers/index.js @@ -1,9 +1,13 @@ -import { combineReducers } from 'redux'; -import auth from './auth'; -import signup from './signup'; -import profile from './profile'; -import profileErrors from './profile/errors'; -import linemanager from './profile/linemanager'; +import { combineReducers } from "redux"; +import auth from "./auth"; +import signup from "./signup"; +import profile from "./profile"; +import profileErrors from "./profile/errors"; +import linemanager from "./profile/linemanager"; +import roles from "./roles"; +import users from "./users"; +import statuses from "./statuses"; +import alerts from "./alerts"; // function that contains all reducer objects. const allReducers = combineReducers({ @@ -12,6 +16,11 @@ const allReducers = combineReducers({ profile, profileErrors, linemanager, + roles, + statuses, + users, + alerts, + signup, }); export default allReducers; diff --git a/redux/reducers/roles.js b/redux/reducers/roles.js new file mode 100644 index 0000000..cbb0df8 --- /dev/null +++ b/redux/reducers/roles.js @@ -0,0 +1,51 @@ +import { + GET_ROLES, + CREATE_ROLE, + DELETE_ROLE, + UPDATE_ROLE, +} from "../actions/types"; + +const initialState = { + roles: [], + msg: "", +}; + +export default (state = initialState, action) => { + switch (action.type) { + case GET_ROLES: + return { + ...state, + roles: action.payload.roles, + msg: action.payload.message, + }; + + case CREATE_ROLE: + return { + ...state, + roles: [...state.roles, action.payload.role], + msg: action.payload.message, + }; + + case UPDATE_ROLE: + const { id } = action.payload.role; + const updated = state.roles.map((role) => { + if (role.id === id) { + return { ...action.payload.role }; + } else return role; + }); + + return { + roles: updated, + msg: action.payload.message, + }; + + case DELETE_ROLE: + const remains = state.roles.filter((role) => role.id !== action.id); + return { + roles: remains, + msg: action.payload.message, + }; + default: + return state; + } +}; diff --git a/redux/reducers/statuses.js b/redux/reducers/statuses.js new file mode 100644 index 0000000..d992af1 --- /dev/null +++ b/redux/reducers/statuses.js @@ -0,0 +1,31 @@ +import { + CREATING_ROLE, + ASSIGNING_ROLE, + DELETING_ROLE, + UPDATING_ROLE, + SET_RESOLVED, +} from "../actions/types"; + +const initialState = { + creating: false, + updating: false, + assigning: false, + deleting: false, +}; + +export default (state = initialState, action) => { + switch (action.type) { + case CREATING_ROLE: + return { ...state, creating: true }; + case ASSIGNING_ROLE: + return { ...state, assigning: true }; + case DELETING_ROLE: + return { ...state, deleting: true }; + case UPDATING_ROLE: + return { ...state, updating: true }; + case SET_RESOLVED: + return initialState; + default: + return state; + } +}; diff --git a/redux/reducers/users.js b/redux/reducers/users.js new file mode 100644 index 0000000..3697683 --- /dev/null +++ b/redux/reducers/users.js @@ -0,0 +1,20 @@ +import { GET_USERS, ASSIGN_ROLE } from "../actions/types"; + +const initialState = { + users: [], + msg: "", +}; + +export default (state = initialState, action) => { + switch (action.type) { + case GET_USERS: + case ASSIGN_ROLE: + return { + ...state, + users: action.payload.users, + msg: action.payload.message, + }; + default: + return state; + } +}; diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..1b65a34 --- /dev/null +++ b/src/App.css @@ -0,0 +1,6 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: Arial, Helvetica, sans-serif; +} diff --git a/src/App.js b/src/App.js index 97d1b8d..af16839 100644 --- a/src/App.js +++ b/src/App.js @@ -1,39 +1,43 @@ -import React from 'react'; -import Signup from './Views/Signup'; -import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; -import './App.scss'; -import 'jquery/dist/jquery.min.js'; -import 'bootstrap/dist/js/bootstrap.min.js'; -import 'bootstrap/dist/css/bootstrap.min.css'; -import 'bootstrap/dist/css/bootstrap.css'; -import { Container } from 'reactstrap'; +import React from "react"; +import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; +import { Container } from "reactstrap"; +import { library } from "@fortawesome/fontawesome-svg-core"; +import { fab } from "@fortawesome/free-brands-svg-icons"; +import { faTrashAlt, faEdit } from "@fortawesome/free-solid-svg-icons"; +import Home from "./components/HomePage"; +import Login from "./components/Login"; +import Signup from "./Views/Signup"; +import ViewProfile from "./components/profile/View"; +import CompleteProfile from "./components/profile/Complete"; +import UpdateProfile from "./components/profile/Update"; +import Navigation from "./components/Navigation"; +import Footer from "./components/Footer"; +import Roles from "./views/Roles/Roles"; -import './App.scss'; +import "./App.scss"; +import "jquery/dist/jquery.min.js"; +import "bootstrap/dist/js/bootstrap.min.js"; +import "bootstrap/dist/css/bootstrap.min.css"; -import Home from './components/HomePage'; -import Login from './components/Login'; -import ViewProfile from './components/profile/View'; -import CompleteProfile from './components/profile/Complete'; -import UpdateProfile from './components/profile/Update'; -import Navigation from './components/Navigation'; -import Footer from './components/Footer'; +library.add(fab, faTrashAlt, faEdit); const App = () => ( -
+
- - - - + + + + - + +