diff --git a/.babelrc b/.babelrc index 5557047..0938dbc 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,12 @@ { - "presets": ["@babel/preset-env","@babel/preset-react"], - "plugins": ["transform-class-properties"] + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ], + "plugins": [ + "transform-class-properties", + "@babel/plugin-proposal-class-properties", + "@babel/plugin-transform-regenerator", + "@babel/plugin-transform-runtime" + ] } diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..80ed262 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,10 @@ +version: "2" +checks: + method-lines: + config: + threshold: 40 + method-complexity: + config: + threshold: 6 +exclude_patterns: +- "**/__tests__/" diff --git a/.eslintrc.json b/.eslintrc.json index 2617ee6..0c443aa 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -33,6 +33,9 @@ } ], "react/no-array-index-key": 0, - "jsx-a11y/anchor-is-valid": 0 + "jsx-a11y/anchor-is-valid": 0, + "no-shadow": 0, + "no-nested-ternary": 0, + "react/jsx-props-no-spreading": 0 } } diff --git a/package.json b/package.json index 467f554..7e7ccc5 100644 --- a/package.json +++ b/package.json @@ -34,14 +34,19 @@ "homepage": "https://github.com/Stackup-Rwanda/stackup2-barefoot-frontend#readme", "dependencies": { "@babel/core": "^7.9.6", + "@babel/plugin-transform-runtime": "^7.10.1", + "@babel/polyfill": "^7.10.1", + "@babel/preset-env": "^7.10.2", "@babel/preset-react": "^7.9.4", "@material-ui/core": "^4.10.0", "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.55", "@storybook/addon-actions": "^5.3.18", "@storybook/addon-links": "^5.3.18", "@storybook/addons": "^5.3.18", "@storybook/react": "^5.3.18", "@testing-library/react": "^10.0.4", + "axios": "^0.19.2", "babel-eslint": "^10.1.0", "babel-jest": "^26.0.1", "babel-loader": "^8.1.0", @@ -59,23 +64,28 @@ "jest": "^26.0.1", "jest-html-reporters": "^1.2.1", "jest-transform-stub": "^2.0.0", + "less-loader": "^6.1.0", "mini-css-extract-plugin": "^0.9.0", "prop-types": "^15.7.2", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-loader-spinner": "^3.1.14", "react-redux": "^7.2.0", "react-router-dom": "^5.2.0", + "react-slick": "^0.26.1", "react-test-renderer": "^16.13.1", "redux": "^4.0.5", "redux-devtools-extension": "^2.13.8", "redux-mock-store": "^1.5.4", "redux-thunk": "^2.3.0", + "slick-carousel": "^1.8.1", "style-loader": "^1.2.1", "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.11.0" }, "devDependencies": { + "dotenv-webpack": "^1.8.0", "eslint": "^6.8.0", "eslint-config-airbnb": "^18.1.0", "eslint-plugin-import": "^2.20.2", diff --git a/src/__tests__/App.test.js b/src/__tests__/App.test.js index 3edf03d..6aa16bc 100644 --- a/src/__tests__/App.test.js +++ b/src/__tests__/App.test.js @@ -4,32 +4,39 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { Provider } from 'react-redux'; import renderer from 'react-test-renderer'; +import { cleanup } from '@testing-library/react'; import configureStore from 'redux-mock-store'; import App from '../entry/App'; -import LandingPage from '../views/LandingPage/LandingPage'; -import reducer from '../reducers/reducer'; -import firstMessage from '../actions/actions'; +// import LandingPage from '../views/LandingPage/LandingPage'; +// import reducer from '../reducers/reducer'; +// import firstMessage from '../actions/actions'; -const mockStore = configureStore([]); -const store = mockStore({ - message: 'Welcome', -}); -store.dispatch = jest.fn(); -const component = renderer.create( - - - , -); +// const mockStore = configureStore([]); +// const store = mockStore({ +// message: 'Welcome', +// }); +// store.dispatch = jest.fn(); +// const component = renderer.create( +// +// +// , +// ); describe('App tests', () => { - it('Will prove that the app is rendered from App component', () => { - const appRender = shallow(); - expect(appRender.contains()); - }); + afterEach(cleanup); - it('should return welcome when no action provided', () => { - expect(reducer(undefined, {})).toEqual({ message: 'Welcome' }); - }); - it('should return Redux when action is provided with value', () => { - expect(reducer(undefined, { ...firstMessage, value: 'Redux' })).toEqual({ message: 'Redux' }); + it('Should match the App component snapshot', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); }); + // it('Will prove that the app is rendered from App component', () => { + // const appRender = shallow(); + // expect(appRender.contains()); + // }); + + // it('should return welcome when no action provided', () => { + // expect(reducer(undefined, {})).toEqual({ message: 'Welcome' }); + // }); + // it('should return Redux when action is provided with value', () => { + // expect(reducer(undefined, { ...firstMessage, value: 'Redux' })).toEqual({ message: 'Redux' }); + // }); }); diff --git a/src/__tests__/RequestsTablePage.test.js b/src/__tests__/RequestsTablePage.test.js new file mode 100644 index 0000000..ef685d7 --- /dev/null +++ b/src/__tests__/RequestsTablePage.test.js @@ -0,0 +1,62 @@ +/* eslint-disable no-undef */ +import React from 'react'; +import { mount } from 'enzyme'; +import { cleanup } from '@testing-library/react'; +import thunk from 'redux-thunk'; +import { Provider } from 'react-redux'; +import configureStore from 'redux-mock-store'; +import { BrowserRouter as Router } from 'react-router-dom'; +import RequestsTablePage from '../views/RequestsTablePage/RequestsTablePage'; +import requests from './data/data'; + +const mockStore = configureStore([]); +const store = mockStore( + { + loading: true, + tripRequests: [], + error: '', + }, +); +store.dispatch = jest.fn(); + +const fetchTripRequests = jest.fn(); +const props = { + requests, + fetchTripRequests, +}; + +describe('', () => { + afterEach(cleanup); + it('Should return true if the RequestsTablePage component exists', () => { + const tree = mount( + + + + + , + ); + expect(tree.find('Card').exists()).toBe(true); + }); + + it('Should render props for the RequestsTablePage component', () => { + const tree = mount( + + + + + , + ); + expect(tree.find('StyledTableCell').exists()).toBe(true); + }); + + it('Should match the RequestsTablePage snapshot', () => { + const tree = mount( + + + + + , + ); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/__tests__/SectionHeader.test.js b/src/__tests__/SectionHeader.test.js new file mode 100644 index 0000000..2a05537 --- /dev/null +++ b/src/__tests__/SectionHeader.test.js @@ -0,0 +1,12 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { cleanup } from '@testing-library/react'; +import SectionHeader from '../components/SectionHeader/SectionHeader'; + +describe('', () => { + afterEach(cleanup); + it('Should match the SectionHeader component snapshot', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/__tests__/__snapshots__/NavBar.test.js.snap b/src/__tests__/__snapshots__/NavBar.test.js.snap deleted file mode 100644 index 56d5cf0..0000000 --- a/src/__tests__/__snapshots__/NavBar.test.js.snap +++ /dev/null @@ -1,50 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` Should match the NavBar component snapshot 1`] = ` -
-
-
-
-

- Barefoot -

-

- Nomad -

-
- -
-
-
-`; diff --git a/src/__tests__/__snapshots__/SectionHeader.test.js.snap b/src/__tests__/__snapshots__/SectionHeader.test.js.snap new file mode 100644 index 0000000..f7b4ae4 --- /dev/null +++ b/src/__tests__/__snapshots__/SectionHeader.test.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Should match the SectionHeader component snapshot 1`] = ` +

+`; diff --git a/src/__tests__/data/data.js b/src/__tests__/data/data.js new file mode 100644 index 0000000..9ff6949 --- /dev/null +++ b/src/__tests__/data/data.js @@ -0,0 +1,32 @@ +const requests = { + tripRequests: [ + { + id: 1, + trips: [ + { + travelDate: '2020-09-15', + travelFrom: 'Kigali', + travelTo: 'Durban', + }, + ], + travelReason: 'Africa Tech Summit', + accommodation: true, + status: 'pending', + }, + { + id: 2, + trips: [ + { + travelDate: '2020-09-10', + travelFrom: 'Kigali', + travelTo: 'Dubai', + }, + ], + travelReason: 'Africa Tech Summit', + accommodation: true, + status: 'pending', + }, + ], +}; + +export default requests; diff --git a/src/actions/actions.js b/src/actions/actions.js deleted file mode 100644 index 780bf61..0000000 --- a/src/actions/actions.js +++ /dev/null @@ -1,5 +0,0 @@ -const firstMessage = { - type: 'FIRST_MESSAGE', -}; - -export default firstMessage; diff --git a/src/assets/styles/sass/index.scss b/src/assets/styles/sass/index.scss index 8501e4f..b741745 100644 --- a/src/assets/styles/sass/index.scss +++ b/src/assets/styles/sass/index.scss @@ -137,7 +137,7 @@ body { } .testimony-item { - display: flex; + display: flex !important; flex-direction: row; justify-content: center; align-items: center; @@ -169,6 +169,10 @@ body { font-size: 14pt; } +.testimony-description { + margin-left: 2px; +} + .testimony-author { font-size: 10pt; } diff --git a/src/components/RequestsTable/RequestsTable.js b/src/components/RequestsTable/RequestsTable.js new file mode 100644 index 0000000..b32638b --- /dev/null +++ b/src/components/RequestsTable/RequestsTable.js @@ -0,0 +1,121 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { withStyles, makeStyles } from '@material-ui/core/styles'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TableRow from '@material-ui/core/TableRow'; +import RemoveCircleOutline from '@material-ui/icons/RemoveCircleOutline'; +import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; +import ActionsMenu from '../../views/ActionsMenu/ActionsMenu'; + +const StyledTableCell = withStyles((theme) => ({ + head: { + backgroundColor: theme.palette.secondary.contrastText, + color: theme.palette.primary.dark, + }, + body: { + fontSize: 14, + }, +}))(TableCell); + +const StyledTableRow = withStyles((theme) => ({ + root: { + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover, + }, + }, +}))(TableRow); + +const useStyles = makeStyles({ + table: { + minWidth: 700, + }, + tableCell: { + fontSize: 12, + }, + tableIcons: { + fontSize: 16, + }, + noData: { + fontSize: 14, + color: '#777777', + backgroundColor: '#FFFFFF', + textAlign: 'center', + }, +}); + +const requesterActions = [ + { + id: 1, + name: 'Edit', + }, + { + id: 2, + name: 'Book Accommodation', + }, + { + id: 3, + name: 'Rate Accommodation', + }, + { + id: 4, + name: 'Like/Dislike Accommodation', + }, +]; + +const RequestsTable = ({ requests }) => { + const classes = useStyles(); + + return ( + + + + + Date + Departure + Destination + Reason + Accommodation + Status + + + + + {requests.length > 0 ? (requests.map((row) => ( + + + {row.trips[0].travelDate} + + {row.trips[0].travelFrom} + {row.trips[0].travelTo} + {row.travelReason} + {row.accommodation ? 'Yes' : 'No'} + + {row.status === 'pending' ? : } + + + + + + )) + ) : ( + + + Oops! Looks like you have not yet made any requests. + + + )} + +
+
+ ); +}; + +RequestsTable.propTypes = { + requests: PropTypes.arrayOf().isRequired, +}; + +export default RequestsTable; diff --git a/src/components/SectionHeader/SectionHeader.js b/src/components/SectionHeader/SectionHeader.js new file mode 100644 index 0000000..b59d777 --- /dev/null +++ b/src/components/SectionHeader/SectionHeader.js @@ -0,0 +1,31 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; + +const useStyles = makeStyles({ + title: { + color: '#484848', + backgroundColor: '#00a7990f', + padding: 24, + fontSize: 14, + fontFamily: 'Roboto', + fontWeight: 500, + }, +}); + +const SectionHeader = ({ title }) => { + const classes = useStyles(); + + return ( + + {title} + + ); +}; + +SectionHeader.propTypes = { + title: PropTypes.string.isRequired, +}; + +export default SectionHeader; diff --git a/src/entry/App.js b/src/entry/App.js index 2f18277..74b5f39 100644 --- a/src/entry/App.js +++ b/src/entry/App.js @@ -5,6 +5,7 @@ import CssBaseline from '@material-ui/core/CssBaseline'; import theme from '../assets/styles/theme'; import LandingPage from '../views/LandingPage/LandingPage'; import NavBar from '../views/NavBar/NavBar'; +import RequestsTablePage from '../views/RequestsTablePage/RequestsTablePage'; const App = () => ( @@ -12,7 +13,8 @@ const App = () => ( - + + diff --git a/src/entry/index.js b/src/entry/index.js index cdff617..ce45dad 100644 --- a/src/entry/index.js +++ b/src/entry/index.js @@ -1,14 +1,14 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { createStore } from 'redux'; import { Provider } from 'react-redux'; -import { composeWithDevTools } from 'redux-devtools-extension'; import App from './App'; import '../assets/styles/sass/index.scss'; import '../assets/styles/css/index.css'; -import reducer from '../reducers/reducer'; +import store from '../redux/store'; -const store = createStore(reducer, composeWithDevTools()); - -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render( + + + , document.getElementById('root'), +); diff --git a/src/reducers/reducer.js b/src/reducers/reducer.js deleted file mode 100644 index 0fbe134..0000000 --- a/src/reducers/reducer.js +++ /dev/null @@ -1,17 +0,0 @@ -import firstMessage from '../actions/actions'; - -const initialState = { - message: 'Welcome', -}; -const reducer = (state = initialState, action) => { - switch (action.type) { - case firstMessage.type: - return { - message: `${action.value}`, - }; - default: - return state; - } -}; - -export default reducer; diff --git a/src/redux/rootReducer.js b/src/redux/rootReducer.js new file mode 100644 index 0000000..f0d7080 --- /dev/null +++ b/src/redux/rootReducer.js @@ -0,0 +1,8 @@ +import { combineReducers } from 'redux'; +import tripRequestsReducer from './tripRequests/tripRequestsReducer'; + +const rootReducer = combineReducers({ + tripRequests: tripRequestsReducer, +}); + +export default rootReducer; diff --git a/src/redux/store.js b/src/redux/store.js new file mode 100644 index 0000000..2bbb5bc --- /dev/null +++ b/src/redux/store.js @@ -0,0 +1,11 @@ +import { createStore, applyMiddleware } from 'redux'; +import thunk from 'redux-thunk'; +import { composeWithDevTools } from 'redux-devtools-extension'; +import rootReducer from './rootReducer'; + +const store = createStore( + rootReducer, + composeWithDevTools(applyMiddleware(thunk)), +); + +export default store; diff --git a/src/redux/tripRequests/__tests__/tripRequestsActions.test.js b/src/redux/tripRequests/__tests__/tripRequestsActions.test.js new file mode 100644 index 0000000..e69de29 diff --git a/src/redux/tripRequests/__tests__/tripRequestsReducer.test.js b/src/redux/tripRequests/__tests__/tripRequestsReducer.test.js new file mode 100644 index 0000000..e69de29 diff --git a/src/redux/tripRequests/tripRequestsActions.js b/src/redux/tripRequests/tripRequestsActions.js new file mode 100644 index 0000000..cb5a68b --- /dev/null +++ b/src/redux/tripRequests/tripRequestsActions.js @@ -0,0 +1,46 @@ +import axios from 'axios'; +import { + FETCH_TRIP_REQUESTS_REQUEST, + FETCH_TRIP_REQUESTS_SUCCESS, + FETCH_TRIP_REQUESTS_FAILURE, +} from './tripRequestsTypes'; + +export const fetchTripRequestsRequest = () => ({ + type: FETCH_TRIP_REQUESTS_REQUEST, +}); + +export const fetchTripRequestsSuccess = (tripRequests) => ({ + type: FETCH_TRIP_REQUESTS_SUCCESS, + payload: tripRequests, +}); + +export const fetchTripRequestsFailure = (error) => ({ + type: FETCH_TRIP_REQUESTS_FAILURE, + payload: error, +}); + +const api = axios.create({ + baseURL: process.env.REACT_APP_API_URL, + headers: { + Authorization: `Bearer ${process.env.REACT_APP_SOME_TOKEN}`, + 'Content-Type': 'application/json', + }, + method: 'GET', +}); + +export const fetchTripRequests = () => async (dispatch) => { + try { + dispatch(fetchTripRequestsRequest); + const res = await api.get('/trips'); + const tripRequests = res.data.data.foundRequests; + dispatch(fetchTripRequestsSuccess(tripRequests)); + } catch (error) { + if (error.response.status === 400) { + const errorMessage = error.response.data.error; + dispatch(fetchTripRequestsFailure(errorMessage)); + } else { + const tripRequests = []; + dispatch(fetchTripRequestsSuccess(tripRequests)); + } + } +}; diff --git a/src/redux/tripRequests/tripRequestsReducer.js b/src/redux/tripRequests/tripRequestsReducer.js new file mode 100644 index 0000000..46f6bca --- /dev/null +++ b/src/redux/tripRequests/tripRequestsReducer.js @@ -0,0 +1,36 @@ +import { + FETCH_TRIP_REQUESTS_REQUEST, + FETCH_TRIP_REQUESTS_SUCCESS, + FETCH_TRIP_REQUESTS_FAILURE, +} from './tripRequestsTypes'; + +const initialState = { + loading: true, + tripRequests: [], + error: '', +}; + +const tripRequestsReducer = (state = initialState, { type, payload }) => { + switch (type) { + case FETCH_TRIP_REQUESTS_REQUEST: + return { + ...state, + loading: true, + }; + case FETCH_TRIP_REQUESTS_SUCCESS: + return { + loading: false, + tripRequests: payload, + error: '', + }; + case FETCH_TRIP_REQUESTS_FAILURE: + return { + loading: false, + tripRequests: [], + error: payload, + }; + default: return state; + } +}; + +export default tripRequestsReducer; diff --git a/src/redux/tripRequests/tripRequestsTypes.js b/src/redux/tripRequests/tripRequestsTypes.js new file mode 100644 index 0000000..5a2b7dd --- /dev/null +++ b/src/redux/tripRequests/tripRequestsTypes.js @@ -0,0 +1,3 @@ +export const FETCH_TRIP_REQUESTS_REQUEST = 'FETCH_TRIP_REQUESTS_REQUEST'; +export const FETCH_TRIP_REQUESTS_SUCCESS = 'FETCH_TRIP_REQUESTS_SUCCESS'; +export const FETCH_TRIP_REQUESTS_FAILURE = 'FETCH_TRIP_REQUESTS_FAILURE'; diff --git a/src/views/ActionsMenu/ActionsMenu.js b/src/views/ActionsMenu/ActionsMenu.js new file mode 100644 index 0000000..d990bc7 --- /dev/null +++ b/src/views/ActionsMenu/ActionsMenu.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button'; +import Menu from '@material-ui/core/Menu'; +import MenuItem from '@material-ui/core/MenuItem'; +import MoreVert from '@material-ui/icons/MoreVert'; + +const useStyles = makeStyles({ + tableIcons: { + fontSize: 16, + }, + item: { + fontSize: 12, + color: '#484848', + }, +}); + +const ActionsMenu = ({ actions }) => { + const [anchorEl, setAnchorEl] = React.useState(null); + + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const classes = useStyles(); + + return ( +

+ + + { + actions.map((action) => ( + + {action.name} + + )) + } + +
+ ); +}; + +ActionsMenu.propTypes = { + actions: PropTypes.arrayOf().isRequired, +}; + +export default ActionsMenu; diff --git a/src/views/ActionsMenu/__tests__/ActionsMenu.test.js b/src/views/ActionsMenu/__tests__/ActionsMenu.test.js new file mode 100644 index 0000000..62e28e6 --- /dev/null +++ b/src/views/ActionsMenu/__tests__/ActionsMenu.test.js @@ -0,0 +1,32 @@ +/* eslint-disable no-undef */ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { cleanup } from '@testing-library/react'; +import ActionsMenu from '../ActionsMenu'; + +const requesterActions = [ + { + id: 1, + name: 'Edit', + }, + { + id: 2, + name: 'Book Accommodation', + }, + { + id: 3, + name: 'Rate Accommodation', + }, + { + id: 4, + name: 'Like/Dislike Accommodation', + }, +]; + +describe('', () => { + afterEach(cleanup); + it('Should match the ActionsMenu component snapshot', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/views/Footer/__tests__/Footer.test.js b/src/views/Footer/__tests__/Footer.test.js new file mode 100644 index 0000000..b960928 --- /dev/null +++ b/src/views/Footer/__tests__/Footer.test.js @@ -0,0 +1,13 @@ +/* eslint-disable no-undef */ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { cleanup } from '@testing-library/react'; +import Footer from '../Footer'; + +describe('