- {tableData.length > 10 &&
-
Displaying {displaying.start} - {displaying.end} of {tableData.length} available drones:
-
}
- {/* displaying end */}
-
- {/* table end */}
+
+
+ {drones.total ?
+ (
+
+
+
Displaying {displayFrom} - {displayTo} of {drones.total} {dronesTypeText}:
+
- {tableData.length > 10 &&
-
-
Show
-
-
+
+
+
+
+ Image
+
+ {
+ updateDroneTable({sortBy: sortBy === '-serialNumber' ? 'serialNumber' : '-serialNumber'});
+ }}
+ >
+ Drone Serial Number
+
+
+
+ {
+ updateDroneTable({sortBy: sortBy === '-name' ? 'name' : '-name'});
+ }}
+ >
+ Drone Name
+
+
+
+ {
+ updateDroneTable({sortBy: sortBy === '-type' ? 'type' : '-type'});
+ }}
+ >
+ Drone Type
+
+
+
+ {
+ updateDroneTable({sortBy: sortBy === '-mileage' ? 'mileage' : '-mileage'});
+ }}
+ >
+ Mileage
+
+
+
+
+
+
+ {drones.items.map((drone) => (
+
+
+
+ {drone.thumbnailUrl ?
+ (
+
+ ) : (
+
+ )
+ }
+
+
+ {drone.serialNumber}
+ {drone.name}
+ {drone.type}
+ {drone.mileage}
+
+
+
+
+ ))}
+
+
-
per page
-
- }
- {/* show-per-page end */}
-
+
+
+ {
+ updateDroneTable({limit: value});
+ }}
+ />
+
+
+
{
+ updateDroneTable({offset: Math.ceil(selected * limit)});
+ }}
+ />
+
+
+
+
+ ) : (
+
{noDronesText}
+ )
+ }
);
}
}
-
MyDronesTable.propTypes = {
- tableData: PropTypes.array.isRequired,
- items: PropTypes.object.isRequired,
- itemPerPage: PropTypes.func.isRequired,
- displayingHandle: PropTypes.func.isRequired,
- displaying: PropTypes.object.isRequired,
-
+ currentTab: PropTypes.string.isRequired,
+ updateDroneTable: PropTypes.func.isRequired,
+ availableDrones: PropTypes.object.isRequired,
+ onMissionDrones: PropTypes.object.isRequired,
+ offset: PropTypes.number.isRequired,
+ limit: PropTypes.number.isRequired,
+ sortBy: PropTypes.string.isRequired,
+ deleteDrone: PropTypes.func.isRequired,
};
export default CSSModules(MyDronesTable, styles);
diff --git a/src/routes/MyDrone/components/MyDronesTable/MyDronesTable.scss b/src/routes/MyDrone/components/MyDronesTable/MyDronesTable.scss
index 4c1c6ce..5210a09 100644
--- a/src/routes/MyDrone/components/MyDronesTable/MyDronesTable.scss
+++ b/src/routes/MyDrone/components/MyDronesTable/MyDronesTable.scss
@@ -1,15 +1,76 @@
.my-drones-table {
- padding: 20px;
position: relative;
}
+.react-table {
+ margin: 25px 18px 0;
+}
+
+.table {
+ width: 100%;
+
+ td,
+ th {
+ padding: 0;
+ }
+}
+
+.thead {
+ background-color: #1e526c;
+}
+
+.th-inner {
+ color: #fff;
+ font-size: 14px;
+ font-weight: 400;
+ padding: 14px 27px 16px;
+ text-align: left;
+}
+
+.th-inner--sort-asc,
+.th-inner--sort-desc {
+ @extend .th-inner;
+ cursor: pointer;
+
+ &:after {
+ background: url('https://github.com/icon-sort-desc.png') no-repeat;
+ content: '';
+ display: inline-block;
+ height: 7px;
+ margin-left: 8px;
+ width: 11px;
+ }
+}
+
+.th-inner--sort-asc {
+ &:after {
+ transform: rotate(180deg);
+ }
+}
+
+.tr {
+ border-bottom: 1px solid #e7e8ea;
+}
+
+.td-inner {
+ font-size: 14px;
+ padding: 16px 27px;
+
+ > a {
+ color: #3b73b9;
+ }
+}
+
+.td-inner_date {
+ white-space: nowrap;
+}
+
.table-head {
display: flex;
- padding-bottom: 20px;
- padding-top: 5px;
.display {
font-size: 14px;
color: #131313;
+ margin: 30px 20px 0;
}
.filter-btn {
margin-left: auto;
@@ -33,106 +94,103 @@
}
}
-.show-per-page {
- position: absolute;
- display: flex;
- align-items: center;
- bottom: 27px;
- left: 20px;
+.navigation {
+ margin: 25px 20px;
}
-.perPage-select {
- width: 52px;
- margin: 0 10px;
+.navigation:after {
+ clear: both;
+ content: '';
+ display: table;
}
-:global {
- .Dropdown-root {
- position: relative;
- }
- .Dropdown-control {
- position: relative;
- overflow: hidden;
- background-color: white;
- border: 1px solid #e3e3e3;
- border-radius: 5px;
- box-sizing: border-box;
- color: #282828;
- cursor: default;
- outline: none;
- padding: 2px 12px 2px 10px;
- transition: all 200ms ease;
- }
+.pagination {
+ float: right;
+}
- .Dropdown-control:hover {
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
- }
+.perpage {
+ float: left;
+}
- .Dropdown-arrow {
- background: url("https://github.com/styles/img/icon-dropdown-caret-sm.png") no-repeat;
- width: 15px;
- height: 9px;
- border: none;
- position: absolute;
- top: 10px;
- right: 3px;
- }
+.actions {
+ display: flex;
+ justify-content: center;
+ align-items: baseline;
- .is-open .Dropdown-arrow {
- border-color: transparent transparent #999;
- border-width: 0 5px 5px;
- }
+ > li {
+ list-style: none;
+ color: #babfca;
+ font-size: 11px;
+ font-weight: bold;
+ height: 50px;
+ cursor: pointer;
+ margin-left: 40px;
- .Dropdown-menu {
- background-color: white;
- border: 1px solid #ccc;
- box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
- box-sizing: border-box;
- margin-top: -1px;
- max-height: 200px;
- overflow-y: auto;
- position: absolute;
- top: 100%;
- width: 100%;
- z-index: 1000;
- -webkit-overflow-scrolling: touch;
- }
+ &:first-child {
+ margin-left: 0;
+ }
+ }
+}
- .Dropdown-menu .Dropdown-group > .Dropdown-title{
- padding: 8px 10px;
- color: rgba(51, 51, 51, 1);
- font-weight: bold;
- text-transform: capitalize;
- }
+%action_link {
+ color: #babfca;
+ display: block;
+ font-size: 11px;
+ font-weight: bold;
+ text-align: center;
- .Dropdown-option {
- box-sizing: border-box;
- color: rgba(51, 51, 51, 0.8);
- cursor: pointer;
- display: block;
- padding: 8px 10px;
- }
+ &:hover {
+ color: #babfca;
+ }
+}
- .Dropdown-option:last-child {
- border-bottom-right-radius: 2px;
- border-bottom-left-radius: 2px;
- }
+.view-detail,
+.edit,
+.delete {
+ @extend %action_link;
+}
- .Dropdown-option:hover {
- background-color: #f2f9fc;
- color: #333;
- }
- .Dropdown-option.is-selected {
- background-color: #f2f9fc;
- color: #333;
- }
+%action_icon {
+ background-position: 0 0;
+ background-repeat: no-repeat;
+ content: '';
+ display: block;
+ margin: 0 auto;
+}
- .Dropdown-noresults {
- box-sizing: border-box;
- color: #ccc;
- cursor: default;
- display: block;
- padding: 8px 10px;
- }
+.view-detail:before {
+ @extend %action_icon;
+
+ width: 27px;
+ height: 17px;
+ background: url('https://github.com/icon-view-detail.png');
+}
+
+.edit:before {
+ @extend %action_icon;
+
+ width: 20px;
+ height: 20px;
+ background: url('https://github.com/icon-edit-row.png');
+}
+
+.delete:before {
+ @extend %action_icon;
+
+ width: 15px;
+ height: 22px;
+ background: url('https://github.com/icon-delete-row.png');
+}
+
+.no-drones {
+ padding: 30px 20px;
+}
+
+.no-image {
+ background: url('https://github.com/icon-drone-black.png') no-repeat center;
+ opacity: 0.5;
+ border: 1px solid rgb(77, 77, 77);
+ height: 95px;
+ width: 148px;
}
diff --git a/src/routes/MyDrone/components/MyDronesTabs/MyDronesTabs.jsx b/src/routes/MyDrone/components/MyDronesTabs/MyDronesTabs.jsx
index e1ef7bd..4ed638a 100644
--- a/src/routes/MyDrone/components/MyDronesTabs/MyDronesTabs.jsx
+++ b/src/routes/MyDrone/components/MyDronesTabs/MyDronesTabs.jsx
@@ -1,54 +1,35 @@
import React, {PropTypes} from 'react';
import CSSModules from 'react-css-modules';
-import {Tab, Tabs, TabList, TabPanel} from 'react-tabs';
import styles from './MyDronesTabs.scss';
-import MyDronesTable from '../MyDronesTable';
-
-Tabs.setUseDefaultStyles(false);
/*
* MyDronesTabs
*/
-
-export const MyDronesTabs = ({availableDrones, onMissionDrones, itemPerPage, items, displayingHandle, displaying}) => (
-
-
-
- Available({availableDrones.length} )
- On Mission({onMissionDrones.length} )
-
-
-
-
-
-
-
-
-
-
-
+export const MyDronesTabs = ({currentTab, updateDroneTable, availableDrones, onMissionDrones}) => (
+
+
{
+ currentTab !== 'available' && updateDroneTable({currentTab: 'available'});
+ }}
+ >Available ({availableDrones.total})
+
+
{
+ currentTab !== 'onMission' && updateDroneTable({currentTab: 'onMission'});
+ }}
+ >On Mission ({onMissionDrones.total})
+
+
);
MyDronesTabs.propTypes = {
- availableDrones: PropTypes.array.isRequired,
- onMissionDrones: PropTypes.array.isRequired,
- items: PropTypes.object.isRequired,
- itemPerPage: PropTypes.func.isRequired,
- displayingHandle: PropTypes.func.isRequired,
- displaying: PropTypes.object.isRequired,
+ currentTab: PropTypes.string.isRequired,
+ updateDroneTable: PropTypes.func.isRequired,
+ availableDrones: PropTypes.object.isRequired,
+ onMissionDrones: PropTypes.object.isRequired,
};
diff --git a/src/routes/MyDrone/components/MyDronesTabs/MyDronesTabs.scss b/src/routes/MyDrone/components/MyDronesTabs/MyDronesTabs.scss
index ea175db..240402b 100644
--- a/src/routes/MyDrone/components/MyDronesTabs/MyDronesTabs.scss
+++ b/src/routes/MyDrone/components/MyDronesTabs/MyDronesTabs.scss
@@ -1,5 +1,31 @@
.my-drones-tabs {
- :global {
+ border-bottom: 1px solid #e7e8ea;
+ padding-left: 15px;
+ overflow: hidden;
+}
+
+.tab {
+ background-color: #e7e8ea;
+ border-radius: 5px 5px 0 0;
+ cursor: pointer;
+ float: left;
+ height: 46px;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 45px;
+ margin-left: 7px;
+ text-align: center;
+ width: 150px;
+ &:first-child {
+ margin-left: 0;
}
}
+
+.tab_active {
+ background-color: #315b95;
+ color: #fff;
+ cursor: default;
+
+ @extend .tab;
+}
diff --git a/src/routes/MyDrone/components/ProviderMap/ProviderMap.jsx b/src/routes/MyDrone/components/ProviderMap/ProviderMap.jsx
index 30df989..7fdde1c 100644
--- a/src/routes/MyDrone/components/ProviderMap/ProviderMap.jsx
+++ b/src/routes/MyDrone/components/ProviderMap/ProviderMap.jsx
@@ -3,8 +3,25 @@ import CSSModules from 'react-css-modules';
import MapLegends from '../MapLegends';
import styles from './ProviderMap.scss';
-const getImage = (name) => `${window.location.origin}/img/${name}`;
+const statusToImage = {
+ 'idle-busy': 'icon-error-drone.png',
+ 'in-motion': 'icon-booked-drone.png',
+ 'idle-ready': 'icon-standby-drone.png',
+};
+const getMarkerIcon = (status) => `${window.location.origin}/img/${statusToImage[status]}`;
+
+const mapConfig = {
+ zoom: 13,
+ center: {
+ lat: -6.202180076671433,
+ lng: 106.83877944946289,
+ },
+ mapTypeControl: false,
+ zoomControl: false,
+ streetViewControl: false,
+ clickableIcons: false,
+};
/*
* ProviderMap
@@ -13,51 +30,31 @@ const getImage = (name) => `${window.location.origin}/img/${name}`;
class ProviderMap extends React.Component {
componentDidMount() {
- const {myDrons} = this.props;
-
- this.map = new google.maps.Map(this.node, {
- zoom: 7,
- center: myDrons[0],
- mapTypeControl: false,
- zoomControl: false,
- streetViewControl: false,
- });
-
- this.start = new google.maps.Marker({
- icon: getImage('icon-standby-drone.png'),
- position: myDrons[0],
- map: this.map,
- });
-
- this.end = new google.maps.Marker({
- icon: getImage('icon-booked-drone.png'),
- position: myDrons[1],
- map: this.map,
- });
-
- this.drone = new google.maps.Marker({
- icon: getImage('icon-error-drone.png'),
- position: myDrons[2],
- map: this.map,
- });
-
- this.start = new google.maps.Marker({
- icon: getImage('icon-standby-drone.png'),
- position: myDrons[3],
- map: this.map,
- });
-
- this.end = new google.maps.Marker({
- icon: getImage('icon-booked-drone.png'),
- position: myDrons[4],
- map: this.map,
- });
-
- this.drone = new google.maps.Marker({
- icon: getImage('icon-error-drone.png'),
- position: myDrons[5],
- map: this.map,
- });
+ this.map = new google.maps.Map(this.node, mapConfig);
+ this.droneMarkers = [];
+
+ // add all markers to the map
+ for (const droneCurrentLocation of this.props.dronesCurrentLocations) {
+ if (droneCurrentLocation.currentLocation.length >= 2) {
+ const droneMarker = new google.maps.Marker({
+ icon: getMarkerIcon(droneCurrentLocation.status),
+ position: {
+ lng: droneCurrentLocation.currentLocation[0],
+ lat: droneCurrentLocation.currentLocation[1],
+ },
+ map: this.map,
+ });
+ this.droneMarkers.push(droneMarker);
+ }
+ }
+
+ // zoom map to fit all markers
+ const markersBounds = new google.maps.LatLngBounds();
+ for (const droneMarker of this.droneMarkers) {
+ markersBounds.extend(droneMarker.getPosition());
+ }
+ this.map.setCenter(markersBounds.getCenter());
+ this.map.fitBounds(markersBounds);
}
shouldComponentUpdate() { // eslint-disable-line lodash/prefer-constant
@@ -76,7 +73,7 @@ class ProviderMap extends React.Component {
}
ProviderMap.propTypes = {
- myDrons: PropTypes.array.isRequired,
+ dronesCurrentLocations: PropTypes.array.isRequired,
};
diff --git a/src/routes/MyDrone/containers/MyDroneContainer.js b/src/routes/MyDrone/containers/MyDroneContainer.js
index 92d5f83..251190f 100644
--- a/src/routes/MyDrone/containers/MyDroneContainer.js
+++ b/src/routes/MyDrone/containers/MyDroneContainer.js
@@ -1,6 +1,12 @@
-import {connect} from 'react-redux';
+import {asyncConnect} from 'redux-connect';
+import {actions} from '../modules/MyDrone';
+
import MyDroneView from '../components/MyDroneView';
+const resolve = [{
+ promise: ({store}) => store.dispatch(actions.load()),
+}];
+
const mapState = (state) => state.myDrone;
-export default connect(mapState, {})(MyDroneView);
+export default asyncConnect(resolve, mapState, actions)(MyDroneView);
diff --git a/src/routes/MyDrone/containers/MyDronesTableContainer.js b/src/routes/MyDrone/containers/MyDronesTableContainer.js
new file mode 100644
index 0000000..84c9dc4
--- /dev/null
+++ b/src/routes/MyDrone/containers/MyDronesTableContainer.js
@@ -0,0 +1,7 @@
+import {connect} from 'react-redux';
+import {actions} from '../modules/MyDrone';
+import MyDronesTable from '../components/MyDronesTable';
+
+const mapState = (state) => state.myDrone;
+
+export default connect(mapState, actions)(MyDronesTable);
diff --git a/src/routes/MyDrone/containers/MyDronesTabsContainer.js b/src/routes/MyDrone/containers/MyDronesTabsContainer.js
index 5403c48..f41c629 100644
--- a/src/routes/MyDrone/containers/MyDronesTabsContainer.js
+++ b/src/routes/MyDrone/containers/MyDronesTabsContainer.js
@@ -1,16 +1,7 @@
import {connect} from 'react-redux';
+import {actions} from '../modules/MyDrone';
import MyDronesTabs from '../components/MyDronesTabs';
-import {itemPerPageAction, displayedRowsAction} from '../modules/MyDrone';
-
const mapState = (state) => state.myDrone;
-const mapDispatchToProps = (dispatch) => ({
- itemPerPage: (items) => {
- dispatch(itemPerPageAction(items));
- },
- displayingHandle: (items) => {
- dispatch(displayedRowsAction(items));
- },
-});
-export default connect(mapState, mapDispatchToProps)(MyDronesTabs);
+export default connect(mapState, actions)(MyDronesTabs);
diff --git a/src/routes/MyDrone/modules/MyDrone.js b/src/routes/MyDrone/modules/MyDrone.js
index 2ab9a65..e669f19 100644
--- a/src/routes/MyDrone/modules/MyDrone.js
+++ b/src/routes/MyDrone/modules/MyDrone.js
@@ -1,191 +1,127 @@
-import {handleActions, createAction} from 'redux-actions';
-import Reactable from 'reactable';
-
-const unsafe = Reactable.unsafe;
-
-const rowActions =
- '
';
-const getImage = () => `${window.location.origin}/img/`;
+import {handleActions} from 'redux-actions';
+import _ from 'lodash';
+import APIService from 'services/APIService';
+import {toastr} from 'react-redux-toastr';
+
+// ------------------------------------
+// Constants
+// ------------------------------------
+export const LOADED = 'MyDrone/LOADED';
+export const UPDATE_DRONE_TABLE = 'MyDrone/UPDATE_DRONE_TABLE';
+export const SET_LIMIT = 'MyDrone/SET_LIMIT';
+export const SET_OFFSET = 'MyDrone/SET_OFFSET';
+export const SET_SORT_BY = 'MyDrone/SET_SORT_BY';
+export const SET_CURRENT_TAB = 'MyDrone/SET_CURRENT_TAB';
// ------------------------------------
// Actions
// ------------------------------------
-export const itemPerPageAction = createAction('CHANGE_ITEM_SIZE');
-export const displayedRowsAction = createAction('DISPLAYED_ROWS');
+export const load = () => async(dispatch, getState) => {
+ const query = _.pick(getState().myDrone, ['limit', 'offset', 'sortBy']);
-export const sendRequest = (values) => new Promise((resolve) => {
- alert(JSON.stringify(values, null, 2));
- resolve();
-});
+ const dronesCurrentLocations = await APIService.fetchDronesCurrentLocations();
+ const availableDrones = await APIService.searchProviderDrones({...query, statuses: 'idle-ready'});
+ const onMissionDrones = await APIService.searchProviderDrones({...query, statuses: 'idle-busy,in-motion'});
+
+ dispatch({
+ type: LOADED,
+ payload: {
+ dronesCurrentLocations,
+ availableDrones,
+ onMissionDrones,
+ },
+ });
+};
+
+export const updateDroneTable = (filter) => async(dispatch, getState) => {
+ const prevState = getState().myDrone;
+ const newState = {...prevState, ...filter};
+ const {currentTab, limit, sortBy} = newState;
+ let {offset} = newState;
+
+ if (_.has(filter, 'currentTab')) {
+ // reset page to 0 when change tab
+ offset = 0;
+ dispatch({type: SET_OFFSET, payload: offset});
+
+ dispatch({type: SET_CURRENT_TAB, payload: currentTab});
+ }
+ if (_.has(filter, 'limit')) {
+ // adjust page number (offset) when change per page quantity (limit)
+ offset = Math.floor(prevState.offset / limit);
+ dispatch({type: SET_OFFSET, payload: offset});
+
+ dispatch({type: SET_LIMIT, payload: limit});
+ }
+ if (_.has(filter, 'offset')) {
+ dispatch({type: SET_OFFSET, payload: offset});
+ }
+ if (_.has(filter, 'sortBy')) {
+ dispatch({type: SET_SORT_BY, payload: sortBy});
+ }
+
+ const query = {limit, offset, sortBy};
+ const payload = {};
+
+ if (currentTab === 'available') {
+ payload.availableDrones = await APIService.searchProviderDrones({...query, statuses: 'idle-ready'});
+ } else if (currentTab === 'onMission') {
+ payload.onMissionDrones = await APIService.searchProviderDrones({...query, statuses: 'idle-busy,in-motion'});
+ }
+
+ dispatch({type: UPDATE_DRONE_TABLE, payload});
+};
+export const deleteDrone = (id) => async(dispatch, getState) => {
+ const currentState = getState().myDrone;
+ const query = _.pick(currentState, ['limit', 'offset', 'sortBy']);
+ const currentTab = currentState.currentTab;
+ const payload = {};
+ const totalDronsOnCurrentTab = currentTab === 'available' ? currentState.availableDrones.total : currentState.onMissionDrones.total;
+
+ await APIService.deleteProviderDrone(id);
+
+ toastr.success('Drone deleted');
+
+ // if we delete the last drone on the page on the current tab, switch page to previous one
+ if (totalDronsOnCurrentTab === query.offset + 1) {
+ query.offset = Math.max(query.offset - query.limit, 0);
+ dispatch({type: SET_OFFSET, payload: query.offset});
+ }
+
+ if (currentTab === 'available') {
+ payload.availableDrones = await APIService.searchProviderDrones({...query, statuses: 'idle-ready'});
+ } else if (currentTab === 'onMission') {
+ payload.onMissionDrones = await APIService.searchProviderDrones({...query, statuses: 'idle-busy,in-motion'});
+ }
+
+ dispatch({type: UPDATE_DRONE_TABLE, payload});
+};
export const actions = {
- itemPerPageAction,
- displayedRowsAction,
+ load,
+ updateDroneTable,
+ deleteDrone,
};
// ------------------------------------
// Reducer
// ------------------------------------
export default handleActions({
- [itemPerPageAction]: (state, action) => ({
- ...state, items: action.payload,
- }),
- [displayedRowsAction]: (state, action) => ({
- ...state, displaying: action.payload,
+ [LOADED]: (state, action) => ({
+ ...state, ...action.payload,
}),
+ [SET_LIMIT]: (state, action) => ({...state, limit: action.payload}),
+ [SET_OFFSET]: (state, action) => ({...state, offset: action.payload}),
+ [SET_SORT_BY]: (state, action) => ({...state, sortBy: action.payload}),
+ [SET_CURRENT_TAB]: (state, action) => ({...state, currentTab: action.payload}),
+ [UPDATE_DRONE_TABLE]: (state, action) => ({...state, ...action.payload}),
}, {
- // initial data
- items: {value: 10, label: '10'},
- displaying: {start: 1, end: 10, currentPage: 0},
- myDrons: [
- {
- lat: -6.195168,
- lng: 106.446533,
- status: 'Stand By',
- },
- {
- lat: -5.145657,
- lng: 104.47998,
- status: 'Booked',
- },
- {
- lat: -7.079088,
- lng: 107.215576,
- status: 'Error',
- },
- {
- lat: -6.500899,
- lng: 107.797852,
- status: 'Stand By',
- },
- {
- lat: -6.937333,
- lng: 108.643799,
- status: 'Booked',
- },
- {
- lat: -7.591218,
- lng: 108.028564,
- status: 'Error',
- },
- {
- lat: -5.462896,
- lng: 107.775879,
- status: 'Error',
- },
- ],
- availableDrones: [
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type gorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type corem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type xorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type sorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type worem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type iorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type korem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type rorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type morem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type gorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type corem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type xorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type sorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type worem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type iorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type korem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type rorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type morem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type gorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type corem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type xorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type sorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type worem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type iorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type korem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type rorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type morem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type gorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type corem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type xorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type sorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type worem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type lorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type iorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type korem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type rorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type morem', Mileage: '999.99 miles', '': unsafe(rowActions)},
-
- ],
- onMissionDrones: [
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type korem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type rorem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- {Image: unsafe(`
`), 'Drone Serial Number': '123456789ABC', 'Drone Name': 'Drone name lorem ipsum', 'Drone Type': 'Drone type morem', Mileage: '999.99 miles', '': unsafe(rowActions)},
- ],
+ currentTab: 'available',
+ limit: 10,
+ offset: 0,
+ sortBy: 'serialNumber',
+ dronesCurrentLocations: [],
+ availableDrones: {total: 0, items: []},
+ onMissionDrones: {total: 0, items: []},
});
diff --git a/src/routes/MyRequest/components/MyRequestView.jsx b/src/routes/MyRequest/components/MyRequestView.jsx
index 766a63d..a971b3a 100644
--- a/src/routes/MyRequest/components/MyRequestView.jsx
+++ b/src/routes/MyRequest/components/MyRequestView.jsx
@@ -2,9 +2,11 @@ import React, {PropTypes} from 'react';
import CSSModules from 'react-css-modules';
import Tabs from 'components/Tabs';
import Pagination from 'components/Pagination';
+import SelectPerPage from 'components/SelectPerPage';
import styles from './MyRequestView.scss';
import MyRequestFilter from './MyRequestFilter';
import MyRequestItemsContainer from '../containers/MyRequestItemsContainer';
+import _ from 'lodash';
const tabList = [{
name: 'New/Pending (5)',
@@ -16,7 +18,7 @@ const tabList = [{
name: 'Completed (3)',
}];
-export const MyRequestView = ({activeTab}) => (
+export const MyRequestView = ({activeTab, requestItems, limit, offset}) => (
Requests
@@ -35,15 +37,32 @@ export const MyRequestView = ({activeTab}) => (
}}
/>
-
);
MyRequestView.propTypes = {
activeTab: PropTypes.number,
+ requestItems: PropTypes.array.isRequired,
+ limit: PropTypes.number.isRequired,
+ offset: PropTypes.number.isRequired,
};
export default CSSModules(MyRequestView, styles);
diff --git a/src/routes/MyRequest/components/MyRequestView.scss b/src/routes/MyRequest/components/MyRequestView.scss
index bf02f33..69e68e2 100644
--- a/src/routes/MyRequest/components/MyRequestView.scss
+++ b/src/routes/MyRequest/components/MyRequestView.scss
@@ -48,3 +48,20 @@
}
+.navigation {
+ margin: 25px 20px;
+}
+
+.navigation:after {
+ clear: both;
+ content: '';
+ display: table;
+}
+
+.pagination {
+ float: right;
+}
+
+.perpage {
+ float: left;
+}
diff --git a/src/routes/MyRequest/modules/MyRequest.js b/src/routes/MyRequest/modules/MyRequest.js
index e178900..eb3ddb8 100644
--- a/src/routes/MyRequest/modules/MyRequest.js
+++ b/src/routes/MyRequest/modules/MyRequest.js
@@ -21,6 +21,8 @@ export const actions = {
// ------------------------------------
export default handleActions({
}, {
+ limit: 10,
+ offset: 0,
requestItems: [{
status: 'new',
requestId: '123ASDD',
diff --git a/src/routes/PilotChecklist/components/PilotChecklistForm/PilotChecklistForm.jsx b/src/routes/PilotChecklist/components/PilotChecklistForm/PilotChecklistForm.jsx
new file mode 100644
index 0000000..ec821f8
--- /dev/null
+++ b/src/routes/PilotChecklist/components/PilotChecklistForm/PilotChecklistForm.jsx
@@ -0,0 +1,100 @@
+import React, {PropTypes, Component} from 'react';
+import {reduxForm} from 'redux-form';
+import Button from 'components/Button';
+import Radiobox from 'components/Radiobox';
+import TextareaField from 'components/TextareaField';
+import CSSModules from 'react-css-modules';
+import styles from './PilotChecklistForm.scss';
+import _ from 'lodash';
+
+export class PilotChecklistForm extends Component {
+ constructor(props) {
+ super(props);
+
+ this.onButtonClick = this.onButtonClick.bind(this);
+ PilotChecklistForm.pressedButton = null;
+ }
+
+ onButtonClick(name) {
+ PilotChecklistForm.pressedButton = name;
+ this.props.handleSubmit((values) => this.props.save({...values, pressedButton: name}))();
+ }
+
+ render() {
+ const {questions, fields, missionStatus} = this.props;
+ const isReadonly = _.includes(['completed', 'in-progress'], missionStatus);
+ const hasErrors = _.find(fields.answers, (answerRow) => answerRow.answer.error || answerRow.note.error);
+
+ return (
+
+ );
+ }
+}
+
+PilotChecklistForm.propTypes = {
+ questions: PropTypes.array.isRequired,
+ fields: PropTypes.object.isRequired,
+ handleSubmit: PropTypes.func.isRequired,
+ missionStatus: PropTypes.string.isRequired,
+ save: PropTypes.func.isRequired,
+};
+
+const fields = [
+ 'answers[].answer',
+ 'answers[].note',
+];
+
+/**
+ * Validate function for redux form
+ * @param {Object} values values to validate
+ * @return {Object} errors
+ */
+const validate = (values) => {
+ const errors = {};
+
+ if (PilotChecklistForm.pressedButton === 'saveload') {
+ errors.answers = _.map(values.answers, (answerRow) => {
+ let err;
+
+ if (_.isNil(answerRow.answer)) {
+ err = {answer: 'Answer is required'};
+ } else if (answerRow.answer === 'no') {
+ err = {answer: 'Answer cannot be "No"'};
+ } else if (answerRow.answer === 'note' && (!_.isString(answerRow.note) || answerRow.note.trim() === '')) {
+ err = {note: 'You have to provide a "Note", when you chose "No, but proceed with caution"'};
+ }
+
+ return err;
+ });
+ }
+
+ return errors;
+};
+
+export default reduxForm({form: 'pilotChecklist', fields, validate})(CSSModules(PilotChecklistForm, styles));
diff --git a/src/routes/PilotChecklist/components/PilotChecklistForm/PilotChecklistForm.scss b/src/routes/PilotChecklist/components/PilotChecklistForm/PilotChecklistForm.scss
new file mode 100644
index 0000000..6272302
--- /dev/null
+++ b/src/routes/PilotChecklist/components/PilotChecklistForm/PilotChecklistForm.scss
@@ -0,0 +1,62 @@
+.pilot-checklist-form {
+ margin: 0 auto;
+ max-width: 1000px;
+}
+
+.question {
+ border-bottom: 1px solid #d5d5d5;
+ margin-bottom: 30px;
+ padding-bottom: 34px;
+
+ &:last-child {
+ border-bottom: 0;
+ margin-bottom: 0;
+ }
+}
+
+.radioboxes {
+ display: flex;
+}
+
+.radiobox {
+ margin-right: 50px;
+}
+
+.note {
+ margin-top: 10px;
+}
+
+.note-label {
+ display: block;
+ margin-bottom: 8px;
+}
+
+.actions {
+ border-top: 1px solid #d5d5d5;
+ padding-bottom: 30px;
+ text-align: right;
+
+ button {
+ margin-left: 12px;
+ }
+}
+
+.error {
+ color: #f00;
+ height: 40px;
+ line-height: 40px;
+}
+
+.note-label-wrap {
+ display: flex;
+
+ .error {
+ height: auto;
+ line-height: inherit;
+ margin-left: 30px;
+ }
+}
+
+.global-error {
+ height: 40px;
+}
diff --git a/src/routes/PilotChecklist/components/PilotChecklistForm/index.js b/src/routes/PilotChecklist/components/PilotChecklistForm/index.js
new file mode 100644
index 0000000..93af7bb
--- /dev/null
+++ b/src/routes/PilotChecklist/components/PilotChecklistForm/index.js
@@ -0,0 +1,3 @@
+import PilotChecklistForm from './PilotChecklistForm';
+
+export default PilotChecklistForm;
diff --git a/src/routes/PilotChecklist/components/PilotChecklistView.jsx b/src/routes/PilotChecklist/components/PilotChecklistView.jsx
new file mode 100644
index 0000000..e977a98
--- /dev/null
+++ b/src/routes/PilotChecklist/components/PilotChecklistView.jsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import CSSModules from 'react-css-modules';
+import styles from './PilotChecklistView.scss';
+import PilotChecklistForm from '../containers/PilotChecklistFormContainer';
+
+export const PilotChecklistView = () => (
+
+
+
+
Flight Checklist
+
+
+
+
+);
+
+PilotChecklistView.propTypes = {
+};
+
+export default CSSModules(PilotChecklistView, styles);
diff --git a/src/routes/PilotChecklist/components/PilotChecklistView.scss b/src/routes/PilotChecklist/components/PilotChecklistView.scss
new file mode 100644
index 0000000..f9be2bd
--- /dev/null
+++ b/src/routes/PilotChecklist/components/PilotChecklistView.scss
@@ -0,0 +1,37 @@
+.pilot-checklist-view {
+ background-color: transparent;
+
+ :global {
+
+ }
+}
+
+.wrap {
+ padding: 0 30px 35px;
+}
+
+.header {
+ border-bottom: 1px solid #d5d5d5;
+ display: flex;
+ margin-bottom: 19px;
+ justify-content: space-between;
+ padding-bottom: 17px;
+ padding-top: 21px;
+ position: relative;
+}
+
+.title {
+ color: #333333;
+ font-size: 24px;
+ font-weight: 600;
+ line-height: 32px;
+ margin: 0;
+ padding: 0;
+}
+
+.panel {
+ background-color: #fff;
+ border: 1px solid #e0e0e0;
+ border-radius: 3px;
+ padding: 19px 24px;
+}
diff --git a/src/routes/PilotChecklist/containers/BreadcrumbItemContainer.js b/src/routes/PilotChecklist/containers/BreadcrumbItemContainer.js
new file mode 100644
index 0000000..206227f
--- /dev/null
+++ b/src/routes/PilotChecklist/containers/BreadcrumbItemContainer.js
@@ -0,0 +1,11 @@
+import {connect} from 'react-redux';
+
+// we use global BreadcrumbItem component to display breadcrumb item,
+// just pass a title property here
+import BreadcrumbItem from 'components/BreadcrumbItem';
+
+const mapState = (state) => ({
+ title: state.pilotChecklist.missionName,
+});
+
+export default connect(mapState, {})(BreadcrumbItem);
diff --git a/src/routes/PilotChecklist/containers/PilotChecklistContainer.js b/src/routes/PilotChecklist/containers/PilotChecklistContainer.js
new file mode 100644
index 0000000..7b65831
--- /dev/null
+++ b/src/routes/PilotChecklist/containers/PilotChecklistContainer.js
@@ -0,0 +1,12 @@
+import {asyncConnect} from 'redux-connect';
+import {actions} from '../modules/PilotChecklist';
+
+import PilotChecklistView from '../components/PilotChecklistView';
+
+const resolve = [{
+ promise: ({store, params}) => store.dispatch(actions.load(params.id)),
+}];
+
+const mapState = (state) => state.pilotChecklist;
+
+export default asyncConnect(resolve, mapState, actions)(PilotChecklistView);
diff --git a/src/routes/PilotChecklist/containers/PilotChecklistFormContainer.js b/src/routes/PilotChecklist/containers/PilotChecklistFormContainer.js
new file mode 100644
index 0000000..cb1bae3
--- /dev/null
+++ b/src/routes/PilotChecklist/containers/PilotChecklistFormContainer.js
@@ -0,0 +1,33 @@
+import {connect} from 'react-redux';
+import {actions} from '../modules/PilotChecklist';
+import _ from 'lodash';
+
+import PilotChecklistForm from '../components/PilotChecklistForm';
+
+/**
+ * Create initial values for the checklist form
+ * it takes into account that we can have not all answers,
+ * but form requires full quantity of elements, so we create empty answers when need
+ *
+ * @param {Array} questions list of all questions
+ * @param {Array} answers) list of answers, could be not for all questions
+ * @return {Object} initialValues for the form
+ */
+const answersToInitialValues = (questions, answers) => ({
+ answers: _.map(questions, (question) => (
+ {
+ ...{answer: undefined, note: undefined}, // eslint-disable-line no-undefined
+ ..._.find(answers, {question: question.id}),
+ }
+ )),
+});
+
+const mapState = (state) => ({
+ questions: state.pilotChecklist.questions,
+ initialValues: answersToInitialValues(state.pilotChecklist.questions, state.pilotChecklist.answers),
+ missionStatus: state.pilotChecklist.missionStatus,
+});
+
+export default connect(mapState, {
+ save: actions.save,
+})(PilotChecklistForm);
diff --git a/src/routes/PilotChecklist/index.js b/src/routes/PilotChecklist/index.js
new file mode 100644
index 0000000..602408e
--- /dev/null
+++ b/src/routes/PilotChecklist/index.js
@@ -0,0 +1,18 @@
+import {injectReducer} from '../../store/reducers';
+import React from 'react';
+import BreadcrumbItem from './containers/BreadcrumbItemContainer';
+
+export default (store) => ({
+ name: 'Flight Checklist',
+ path: 'pilot-checklist/:id',
+ getComponent(nextState, cb) {
+ require.ensure([], (require) => {
+ const PilotChecklist = require('./containers/PilotChecklistContainer').default;
+ const reducer = require('./modules/PilotChecklist').default;
+
+ injectReducer(store, {key: 'pilotChecklist', reducer});
+ cb(null, PilotChecklist);
+ }, 'PilotChecklist');
+ },
+ prettifyParam: () => React.createElement(BreadcrumbItem), // eslint-disable-line react/display-name
+});
diff --git a/src/routes/PilotChecklist/modules/PilotChecklist.js b/src/routes/PilotChecklist/modules/PilotChecklist.js
new file mode 100644
index 0000000..9991388
--- /dev/null
+++ b/src/routes/PilotChecklist/modules/PilotChecklist.js
@@ -0,0 +1,75 @@
+import {handleActions} from 'redux-actions';
+import APIService from 'services/APIService';
+import {toastr} from 'react-redux-toastr';
+import _ from 'lodash';
+
+// ------------------------------------
+// Constants
+// ------------------------------------
+export const LOADED = 'PilotChecklist/LOADED';
+export const UPDATED = 'PilotChecklist/UPDATED';
+
+// ------------------------------------
+// Actions
+// ------------------------------------
+export const load = (missionId) => async(dispatch) => {
+ const response = await APIService.getPilotChecklist(missionId);
+ const answers = response.pilotChecklist ? response.pilotChecklist.answers : [];
+
+ dispatch({type: LOADED, payload: {..._.pick(response, ['missionStatus', 'missionName', 'questions']), answers, missionId}});
+};
+
+export const save = (values) => async (dispatch, getState) => {
+ const questions = getState().pilotChecklist.questions;
+ // send to server only not empty answers and not empty answer properties
+ const notEmptyAnswers = [];
+ _.forEach(values.answers, (answerRow, index) => {
+ const notEmptyAnswer = {};
+ const hasAnswer = !!answerRow.answer;
+ const hasNote = _.isString(answerRow.note) && answerRow.note.trim() !== '';
+
+ if (hasAnswer || hasNote) {
+ hasAnswer && (notEmptyAnswer.answer = answerRow.answer);
+ hasNote && (notEmptyAnswer.note = answerRow.note);
+ // add question id to the answer
+ notEmptyAnswer.question = questions[index].id;
+ notEmptyAnswers.push(notEmptyAnswer);
+ }
+ });
+
+ const response = await APIService.updatePilotChecklist(getState().pilotChecklist.missionId, {
+ answers: notEmptyAnswers,
+ load: values.pressedButton === 'saveload',
+ });
+ dispatch({
+ type: UPDATED,
+ payload: {
+ missionStatus: response.missionStatus,
+ answers: response.pilotChecklist.answers,
+ },
+ });
+ if (values.pressedButton === 'saveload') {
+ toastr.success('Checklist saved and mission loaded');
+ } else {
+ toastr.success('Checklist saved');
+ }
+};
+
+export const actions = {
+ load,
+ save,
+};
+
+// ------------------------------------
+// Reducer
+// ------------------------------------
+export default handleActions({
+ [LOADED]: (state, {payload}) => ({...state, ...payload}),
+ [UPDATED]: (state, {payload}) => ({...state, ...payload}),
+}, {
+ missionId: '',
+ missionStatus: '',
+ missionName: '',
+ questions: [],
+ answers: [],
+});
diff --git a/src/routes/PilotMissions/components/PilotMissionsView.jsx b/src/routes/PilotMissions/components/PilotMissionsView.jsx
new file mode 100644
index 0000000..32a5329
--- /dev/null
+++ b/src/routes/PilotMissions/components/PilotMissionsView.jsx
@@ -0,0 +1,54 @@
+import React, {PropTypes} from 'react';
+import CSSModules from 'react-css-modules';
+import {Link} from 'react-router';
+import StatusLabel from 'components/StatusLabel';
+import Table from 'components/Table';
+import styles from './PilotMissionsView.scss';
+
+const columns = [{
+ header: 'Mission Name',
+ accessor: 'missionName',
+ render: (prop) =>
{prop.value}, // eslint-disable-line react/display-name
+ sortable: true,
+}, {
+ header: 'Status',
+ accessor: 'status',
+ render: (prop) =>
, // eslint-disable-line react/display-name
+ sortable: true,
+}];
+
+export const PilotMissionsView = ({missions, load, offset, limit, total, sortBy}) => (
+
+
+
+
Pilot Missions
+
+
+ {missions.length ? (
+
+ ) : (
+
No missions found.
+ )}
+
+
+
+);
+
+PilotMissionsView.propTypes = {
+ missions: PropTypes.array.isRequired,
+ load: PropTypes.func.isRequired,
+ offset: PropTypes.number.isRequired,
+ limit: PropTypes.number.isRequired,
+ total: PropTypes.number.isRequired,
+ sortBy: PropTypes.string.isRequired,
+};
+
+export default CSSModules(PilotMissionsView, styles);
diff --git a/src/routes/PilotMissions/components/PilotMissionsView.scss b/src/routes/PilotMissions/components/PilotMissionsView.scss
new file mode 100644
index 0000000..52c6656
--- /dev/null
+++ b/src/routes/PilotMissions/components/PilotMissionsView.scss
@@ -0,0 +1,52 @@
+.pilot-missions-view {
+ background-color: transparent;
+
+ :global {
+
+ }
+}
+
+.wrap {
+ padding: 0 30px 35px;
+}
+
+.header {
+ border-bottom: 1px solid #d5d5d5;
+ display: flex;
+ margin-bottom: 19px;
+ justify-content: space-between;
+ padding-bottom: 17px;
+ padding-top: 21px;
+ position: relative;
+}
+
+.title {
+ color: #333333;
+ font-size: 24px;
+ font-weight: 600;
+ line-height: 32px;
+ margin: 0;
+ padding: 0;
+}
+
+.panel {
+ background-color: #fff;
+ border: 1px solid #e0e0e0;
+ border-radius: 3px;
+ padding: 19px 24px;
+}
+
+.create-btn {
+ background: #315b95;
+ border: none;
+ color: white;
+ display: inline-block;
+ font-weight: bold;
+ min-width: 115px;
+ margin-left: 10px;
+ padding: 13px 10px;
+
+ &:hover {
+ color: #fff;
+ }
+}
diff --git a/src/routes/PilotMissions/containers/PilotMissionsContainer.js b/src/routes/PilotMissions/containers/PilotMissionsContainer.js
new file mode 100644
index 0000000..4fb6f8a
--- /dev/null
+++ b/src/routes/PilotMissions/containers/PilotMissionsContainer.js
@@ -0,0 +1,12 @@
+import {asyncConnect} from 'redux-connect';
+import {actions} from '../modules/PilotMissions';
+
+import PilotMissionsView from '../components/PilotMissionsView';
+
+const resolve = [{
+ promise: ({store}) => store.dispatch(actions.load()),
+}];
+
+const mapState = (state) => ({...state.pilotMissions});
+
+export default asyncConnect(resolve, mapState, actions)(PilotMissionsView);
diff --git a/src/routes/PilotMissions/index.js b/src/routes/PilotMissions/index.js
new file mode 100644
index 0000000..0da6e08
--- /dev/null
+++ b/src/routes/PilotMissions/index.js
@@ -0,0 +1,15 @@
+import {injectReducer} from '../../store/reducers';
+
+export default (store) => ({
+ name: 'Pilot Missions',
+ path: 'pilot-missions',
+ getComponent(nextState, cb) {
+ require.ensure([], (require) => {
+ const PilotMissions = require('./containers/PilotMissionsContainer').default;
+ const reducer = require('./modules/PilotMissions').default;
+
+ injectReducer(store, {key: 'pilotMissions', reducer});
+ cb(null, PilotMissions);
+ }, 'PilotMissions');
+ },
+});
diff --git a/src/routes/PilotMissions/modules/PilotMissions.js b/src/routes/PilotMissions/modules/PilotMissions.js
new file mode 100644
index 0000000..0f651b5
--- /dev/null
+++ b/src/routes/PilotMissions/modules/PilotMissions.js
@@ -0,0 +1,39 @@
+import {handleActions} from 'redux-actions';
+import APIService from 'services/APIService';
+import _ from 'lodash';
+
+// ------------------------------------
+// Constants
+// ------------------------------------
+export const LOADED = 'PilotMissions/LOADED';
+
+// ------------------------------------
+// Actions
+// ------------------------------------
+export const load = (params) => async(dispatch, getState) => {
+ const allParams = {..._.pick(getState().pilotMissions, ['offset', 'limit', 'sortBy']), ...params};
+ if (!allParams.sortBy) {
+ delete allParams.sortBy;
+ }
+
+ const respond = await APIService.fetchPilotMissions(allParams);
+
+ dispatch({type: LOADED, payload: {missions: respond.items, total: respond.total, ...params}});
+};
+
+export const actions = {
+ load,
+};
+
+// ------------------------------------
+// Reducer
+// ------------------------------------
+export default handleActions({
+ [LOADED]: (state, {payload}) => ({...state, ...payload}),
+}, {
+ offset: 0,
+ limit: 10,
+ total: 0,
+ sortBy: 'missionName',
+ missions: [],
+});
diff --git a/src/routes/ResetPassword/components/ResetPasswordView.jsx b/src/routes/ResetPassword/components/ResetPasswordView.jsx
new file mode 100644
index 0000000..3fe1671
--- /dev/null
+++ b/src/routes/ResetPassword/components/ResetPasswordView.jsx
@@ -0,0 +1,80 @@
+import React, {Component} from 'react';
+import CSSModules from 'react-css-modules';
+import styles from './ResetPasswordView.scss';
+import TextField from '../../../components/TextField';
+import FormField from '../../../components/FormField';
+import Button from '../../../components/Button';
+import {reduxForm} from 'redux-form';
+import {sendRequest} from '../modules/ResetPassword';
+import {browserHistory} from 'react-router';
+import {toastr} from 'react-redux-toastr';
+
+class ResetPasswordView extends Component {
+
+ /**
+ * This function is called when the form is submitted
+ * This is triggered by handleSubmit
+ */
+ onSubmit(data) {
+ sendRequest(data).then(() => {
+ toastr.success('', 'Password reset successfuly, kindly login again');
+ browserHistory.push('/');
+ }).catch((reason) => {
+ const message = reason.response.body.error || 'something went wrong, please try again';
+ toastr.error(message);
+ });
+ }
+
+ render() {
+ const {fields, handleSubmit, location: {query: {token}}} = this.props;
+ const _self = this;
+ return (
+
+ );
+ }
+}
+
+ResetPasswordView.propTypes = {
+ fields: React.PropTypes.object.isRequired,
+ location: React.PropTypes.object.isRequired,
+ handleSubmit: React.PropTypes.func.isRequired,
+};
+
+const form = reduxForm({
+ form: 'resetPasswordForm',
+ fields: ['password', 'email'],
+ validate(values) {
+ const errors = {};
+ if (!values.password) {
+ errors.password = 'required';
+ }
+ if (!values.email) {
+ errors.email = 'required';
+ }
+
+ return errors;
+ },
+});
+
+export default form(CSSModules(ResetPasswordView, styles));
diff --git a/src/routes/ResetPassword/components/ResetPasswordView.scss b/src/routes/ResetPassword/components/ResetPasswordView.scss
new file mode 100644
index 0000000..b008991
--- /dev/null
+++ b/src/routes/ResetPassword/components/ResetPasswordView.scss
@@ -0,0 +1,48 @@
+.reset-password-form {
+ padding: 50px 0 14px;
+ margin: 0 300px;
+ height: calc(100vh - 60px - 42px - 50px); // header height - breadcrumb height - footer height
+ h4 {
+ font-weight: bold;
+ font-size: 20px;
+ color: #525051;
+ margin-top: 40px;
+ border-top: 1px solid #e7e8ea;
+ padding-top: 25px;
+ }
+ :global {
+ .form-field {
+ width: 100%;
+ &.error {
+ color: #ff3100;
+ > div:first-child {
+ border: 1px solid #ff3100;
+ }
+ }
+ }
+ }
+}
+.row {
+ display: flex;
+ margin-bottom: 22px;
+ label {
+ display: block;
+ flex: 0 0 20%;
+ align-self: center;
+ font-size: 14px;
+ color: #343434;
+ font-weight: bold;
+ }
+
+ .input-with-label {
+ flex: 0 0 20%;
+ display: flex;
+ align-items: center;
+ .input {
+ flex: 0 0 66%;
+ }
+ }
+}
+.actions {
+ text-align: right;
+}
diff --git a/src/routes/ResetPassword/containers/ResetPasswordContainer.js b/src/routes/ResetPassword/containers/ResetPasswordContainer.js
new file mode 100644
index 0000000..f6309c7
--- /dev/null
+++ b/src/routes/ResetPassword/containers/ResetPasswordContainer.js
@@ -0,0 +1,12 @@
+import {asyncConnect} from 'redux-connect';
+import {actions} from '../modules/ResetPassword';
+
+import ResetPasswordView from '../components/ResetPasswordView';
+
+const resolve = [{
+ promise: () => Promise.resolve(),
+}];
+
+const mapState = (state) => state.resetPassword;
+
+export default asyncConnect(resolve, mapState, actions)(ResetPasswordView);
diff --git a/src/routes/ResetPassword/index.js b/src/routes/ResetPassword/index.js
new file mode 100644
index 0000000..eeb7dbb
--- /dev/null
+++ b/src/routes/ResetPassword/index.js
@@ -0,0 +1,20 @@
+import {injectReducer} from '../../store/reducers';
+
+export default (store) => ({
+ path: 'reset-password',
+ name: 'Reset password', /* Breadcrumb name */
+ staticName: true,
+ getComponent(nextState, cb) {
+ require.ensure([], (require) => {
+ const Dashboard = require('./containers/ResetPasswordContainer').default;
+ const reducer = require('./modules/ResetPassword').default;
+
+ injectReducer(store, {key: 'resetPassword', reducer});
+ if (!nextState.location.query.token) {
+ cb(new Error('Invalid route invocation'));
+ } else {
+ cb(null, Dashboard);
+ }
+ }, 'ResetPassword');
+ },
+});
diff --git a/src/routes/ResetPassword/modules/ResetPassword.js b/src/routes/ResetPassword/modules/ResetPassword.js
new file mode 100644
index 0000000..a5bcb9e
--- /dev/null
+++ b/src/routes/ResetPassword/modules/ResetPassword.js
@@ -0,0 +1,23 @@
+import {handleActions} from 'redux-actions';
+import APIService from 'services/APIService';
+// ------------------------------------
+// Actions
+// ------------------------------------
+
+export const actions = {
+};
+
+export const sendRequest = (values) => new Promise((resolve, reject) => {
+ APIService.resetPassword(values).then((result) => {
+ resolve(result);
+ }).catch((reason) => {
+ reject(reason);
+ });
+});
+
+// ------------------------------------
+// Reducer
+// ------------------------------------
+export default handleActions({
+}, {
+});
diff --git a/src/routes/index.js b/src/routes/index.js
index b89d765..5556b6b 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -22,14 +22,28 @@ import AvailablePackagesRoute from './AvailablePackages';
import AdminDashboard from './Admin/AdminDashboard';
import NoFlyZones from './Admin/NoFlyZones';
import ProviderDetailsRoute from './ProviderDetails';
+import ResetPasswordRoute from './ResetPassword';
+import PilotMissionsRoute from './PilotMissions';
+import PilotChecklistRoute from './PilotChecklist';
+import {defaultAuth0Service} from '../services/AuthService';
+
+import {onSocialLoginSuccessAction} from 'store/modules/global';
export const createRoutes = (store) => ({
path: '/',
name: 'CoreLayout',
indexRoute: {
onEnter: (nextState, replace, cb) => {
- replace('/dashboard');
- cb();
+ // parse the hash if present
+ if (nextState.location.hash) {
+ defaultAuth0Service.parseHash(nextState.location.hash).then(() => {
+ store.dispatch(onSocialLoginSuccessAction());
+ cb();
+ });
+ } else {
+ replace('/dashboard');
+ cb();
+ }
},
},
childRoutes: [
@@ -59,10 +73,12 @@ export const createRoutes = (store) => ({
BrowseProviderRoute(store),
DroneDetailsRoute(store),
AvailablePackagesRoute(store),
+ ProviderDetailsRoute(store),
+ PilotMissionsRoute(store),
+ PilotChecklistRoute(store),
],
},
- ProviderDetailsRoute(store),
-
+ ResetPasswordRoute(store),
// admin routes
{
path: 'admin',
diff --git a/src/services/APIService.js b/src/services/APIService.js
index 783359b..fc22738 100644
--- a/src/services/APIService.js
+++ b/src/services/APIService.js
@@ -453,36 +453,6 @@ const statusDetail = {
},
};
-/*
- As there is no Authorization implemented in the project.
- Here I've hardcoded automatic registering and authorization of a dumb user to make requests to the server.
- This should be removed when real authorizatin is implemented.
- */
-const testUser = {
- firstName: 'test',
- lastName: 'test',
- email: 'kj2h34jh23424h2l34h324ljh1@khj4k234hl234hjl.com',
- phone: '42',
- password: 'qwerty',
-};
-
-const register = () => request
- .post(`${config.api.basePath}/api/v1/register`)
- .send(testUser)
- .set('Content-Type', 'application/json')
- .end();
-
-const authorize = () => request
- .post(`${config.api.basePath}/api/v1/login`)
- .set('Content-Type', 'application/json')
- .send(_.pick(testUser, 'email', 'password'))
- .end();
-
-const regAndAuth = () => authorize().then(
- authorize,
- () => register().then(authorize),
-);
-
export default class APIService {
static fetchMyRequestStatus(filterByStatus) {
return (new Promise((resolve) => {
@@ -500,69 +470,54 @@ export default class APIService {
})).then(() => statusDetail[id]);
}
- static fetchMissionList() {
- return regAndAuth().then((authRes) => {
- const accessToken = authRes.body.accessToken;
-
- return request
- .get(`${config.api.basePath}/api/v1/missions`)
- .set('Authorization', `Bearer ${accessToken}`)
- .end()
- .then((res) => res.body.items.map((item) => ({
+ static fetchMissionList(params) {
+ const token = this.accessToken;
+ return request
+ .get(`${config.api.basePath}/api/v1/missions`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .query(params)
+ .end()
+ .then((res) => ({
+ total: res.body.total,
+ items: res.body.items.map((item) => ({
...item,
- downloadLink: `${config.api.basePath}/api/v1/missions/${item.id}/download?token=${accessToken}`,
- })));
- });
+ downloadLink: `${config.api.basePath}/api/v1/missions/${item.id}/download?token=${token}`,
+ })),
+ }));
}
static getMission(id) {
- return regAndAuth().then((authRes) => {
- const accessToken = authRes.body.accessToken;
-
- return request
- .get(`${config.api.basePath}/api/v1/missions/${id}`)
- .set('Authorization', `Bearer ${accessToken}`)
- .end()
- .then((res) => res.body);
- });
+ return request
+ .get(`${config.api.basePath}/api/v1/missions/${id}`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .end()
+ .then((res) => res.body);
}
static createMission(values) {
- return regAndAuth().then((authRes) => {
- const accessToken = authRes.body.accessToken;
-
- return request
- .post(`${config.api.basePath}/api/v1/missions`)
- .set('Authorization', `Bearer ${accessToken}`)
- .send(values)
- .end()
- .then((res) => res.body);
- });
+ return request
+ .post(`${config.api.basePath}/api/v1/missions`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .send(values)
+ .end()
+ .then((res) => res.body);
}
static updateMission(id, values) {
- return regAndAuth().then((authRes) => {
- const accessToken = authRes.body.accessToken;
-
- return request
- .put(`${config.api.basePath}/api/v1/missions/${id}`)
- .set('Authorization', `Bearer ${accessToken}`)
- .send(values)
- .end()
- .then((res) => res.body);
- });
+ return request
+ .put(`${config.api.basePath}/api/v1/missions/${id}`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .send(values)
+ .end()
+ .then((res) => res.body);
}
static deleteMission(id) {
- return regAndAuth().then((authRes) => {
- const accessToken = authRes.body.accessToken;
-
- return request
- .del(`${config.api.basePath}/api/v1/missions/${id}`)
- .set('Authorization', `Bearer ${accessToken}`)
- .end()
- .then((res) => res.body);
- });
+ return request
+ .del(`${config.api.basePath}/api/v1/missions/${id}`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .end()
+ .then((res) => res.body);
}
/**
@@ -641,4 +596,178 @@ export default class APIService {
.del(`${config.api.basePath}/api/v1/nfz/${id}`)
.end();
}
+
+ /**
+ * Reset the user password
+ * @param {Object} entity the client request payload
+ */
+ static resetPassword(entity) {
+ return request
+ .post(`${config.api.basePath}/api/v1/reset-password`)
+ .set('Content-Type', 'application/json')
+ .send(entity)
+ .end();
+ }
+
+ /**
+ * Send the forgot password link to user's email account
+ * @param {Object} entity the client request payload
+ */
+ static forgotPassword(entity) {
+ return request
+ .post(`${config.api.basePath}/api/v1/forgot-password`)
+ .set('Content-Type', 'application/json')
+ .send(entity)
+ .end();
+ }
+
+ /**
+ * Get all drones current locations of the current provider
+ * @return {Array} list of drones current locations
+ */
+ static fetchDronesCurrentLocations() {
+ return request
+ .get(`${config.api.basePath}/api/v1/provider/drones/current-locations`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .end()
+ .then((res) => res.body);
+ }
+
+ /**
+ * Search for the current provider drones
+ * @param {Object} params
+ * @param {Number} params.limit the limit
+ * @param {Number} params.offset the offset
+ * @returns {{total: Number, items: Array}} the result
+ */
+ static searchProviderDrones(params) {
+ return request
+ .get(`${config.api.basePath}/api/v1/provider/drones`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .query(params)
+ .end()
+ .then((res) => res.body);
+ }
+
+ /**
+ * Delete a drone of the current provider
+ * @param {String} id drone id
+ */
+ static deleteProviderDrone(id) {
+ return request
+ .del(`${config.api.basePath}/api/v1/provider/drones/${id}`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .end();
+ }
+
+ /**
+ * Get provider drone data
+ * @param {String} id drone id
+ * @return {Object} drone object
+ */
+ static fetchProviderDrone(id) {
+ return request
+ .get(`${config.api.basePath}/api/v1/provider/drones/${id}`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .end()
+ .then((res) => res.body);
+ }
+
+ /**
+ * Create provider drone
+ * @param {Object} drone drone object
+ * @return {Object} drone object
+ */
+ static createProviderDrone(drone) {
+ return request
+ .post(`${config.api.basePath}/api/v1/provider/drones`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .send(drone)
+ .end()
+ .then((res) => res.body);
+ }
+
+ /**
+ * Update provider drone
+ * @param {Object} drone drone object
+ * @return {Object} drone object
+ */
+ static updateProviderDrone(id, drone) {
+ return request
+ .put(`${config.api.basePath}/api/v1/provider/drones/${id}`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .send(drone)
+ .end()
+ .then((res) => res.body);
+ }
+
+ /**
+ * Get provider drone's missions
+ * (they are sorted by startedAt, newer first)
+ * @param {String} id drone id
+ * @return {Array} mission list
+ */
+ static fetchProviderDroneMissions(id, params) {
+ return request
+ .get(`${config.api.basePath}/api/v1/provider/drones/${id}/missions`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .query(params)
+ .end()
+ .then((res) => res.body);
+ }
+
+ /**
+ * Get provider drone mission quantities for a month
+ * @param {String} id drone id
+ * @return {Array} mission quantities
+ */
+ static fetchProviderDroneMonthMissions(id, month) {
+ return request
+ .get(`${config.api.basePath}/api/v1/provider/drones/${id}/missions/monthly-count?month=${month}`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .end()
+ .then((res) => res.body);
+ }
+
+ /**
+ * Get pilot checklist by mission id
+ * @param {String} id mission id
+ */
+ static getPilotChecklist(id) {
+ return request
+ .get(`${config.api.basePath}/api/v1/pilot/checklist/${id}/`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .end()
+ .then((res) => res.body);
+ }
+
+ /**
+ * Update pilot checklist by mission id
+ * @param {String} id mission id
+ * @param {Object} checklist checklist object
+ */
+ static updatePilotChecklist(id, checklist) {
+ return request
+ .put(`${config.api.basePath}/api/v1/pilot/checklist/${id}/`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .send(checklist)
+ .end()
+ .then((res) => res.body);
+ }
+
+ /**
+ * Fetch pilot missions
+ * @param {Object} params params
+ * @param {Number} params.limit the limit
+ * @param {Number} params.offset the offset
+ * @param {String} params.sortBy sort by property name
+ */
+ static fetchPilotMissions(params) {
+ return request
+ .get(`${config.api.basePath}/api/v1/pilot/missions`)
+ .set('Authorization', `Bearer ${this.accessToken}`)
+ .query(params)
+ .end()
+ .then((res) => res.body);
+ }
}
diff --git a/src/services/AuthService.js b/src/services/AuthService.js
new file mode 100644
index 0000000..4480b44
--- /dev/null
+++ b/src/services/AuthService.js
@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) 2016 Topcoder Inc, All rights reserved.
+ */
+
+/**
+ * auth0 Authentication service for the app.
+ *
+ * @author TCSCODER
+ * @version 1.0.0
+ */
+
+import Auth0 from 'auth0-js';
+import config from '../config';
+import UserApi from '../api/User';
+
+const userApi = new UserApi(config.api.basePath);
+const idTokenKey = 'id_token';
+
+class AuthService {
+
+ /**
+ * Default constructor
+ * @param {String} clientId the auth0 client id
+ * @param {String} domain the auth0 domain
+ */
+ constructor(clientId, domain) {
+ this.auth0 = new Auth0({
+ clientID: clientId,
+ domain,
+ responseType: 'token',
+ callbackURL: config.AUTH0_CALLBACK,
+ });
+ this.login = this.login.bind(this);
+ this.parseHash = this.parseHash.bind(this);
+ this.loggedIn = this.loggedIn.bind(this);
+ this.logout = this.logout.bind(this);
+ this.getProfile = this.getProfile.bind(this);
+ this.getHeader = this.getHeader.bind(this);
+ }
+
+ /**
+ * Redirects the user to appropriate social network for oauth2 authentication
+ *
+ * @param {Object} params any params to pass to auth0 client
+ * @param {Function} onError function to execute on error
+ */
+ login(params, onError) {
+ // redirects the call to auth0 instance
+ this.auth0.login(params, onError);
+ }
+
+ /**
+ * Parse the hash fragment of url
+ * This method will actually parse the token
+ * will create a user profile if not already present and save the id token in local storage
+ * if there is some error delete the access token
+ * @param {String} hash the hash fragment
+ */
+ parseHash(hash) {
+ const _self = this;
+ const authResult = _self.auth0.parseHash(hash);
+ if (authResult && authResult.idToken) {
+ _self.setToken(authResult.idToken);
+ return new Promise((resolve) => {
+ _self.getProfile((error, profile) => {
+ if (error) {
+ // remove the id token
+ _self.removeToken();
+ throw error;
+ } else {
+ return userApi.registerSocialUser(profile.name, profile.email, _self.getToken()).then(
+ (authResult2) => {
+ localStorage.setItem('userInfo', JSON.stringify(authResult2));
+ resolve(authResult2);
+ }).catch((reason) => {
+ // remove the id token
+ _self.removeToken();
+ throw reason;
+ });
+ }
+ });
+ });
+ }
+ return Promise.reject(new Error('Social login failure'));
+ }
+
+ /**
+ * Check if the user is logged in
+ * @param {String} hash the hash fragment
+ */
+ loggedIn() {
+ // Checks if there is a saved token and it's still valid
+ return !!this.getToken();
+ }
+
+ /**
+ * Set the id token to be stored in local storage
+ * @param {String} idToken the token to store
+ */
+ setToken(idToken) {
+ // Saves user token to localStorage
+ localStorage.setItem(idTokenKey, idToken);
+ }
+
+ /**
+ * Get the stored id token from local storage
+ */
+ getToken() {
+ // Retrieves the user token from localStorage
+ return localStorage.getItem(idTokenKey);
+ }
+
+ /**
+ * Remove the id token from local storage
+ */
+ removeToken() {
+ // Clear user token and profile data from localStorage
+ localStorage.removeItem(idTokenKey);
+ }
+
+ /**
+ * Logout the user from the application, delete the id token
+ */
+ logout() {
+ this.removeToken();
+ }
+
+ /**
+ * Get the authorization header for API access
+ */
+ getHeader() {
+ return {
+ Authorization: `Bearer ${this.getToken()}`,
+ };
+ }
+
+ /**
+ * Get the profile of currently logged in user
+ *
+ * @param {callback} the callback function to call after operation finishes
+ * @return {Object} the profile of logged in user
+ */
+ getProfile(callback) {
+ this.auth0.getProfile(this.getToken(), callback);
+ }
+}
+
+const defaultAuth0Service = new AuthService(config.AUTH0_CLIENT_ID, config.AUTH0_CLIENT_DOMAIN);
+
+export {AuthService as default, defaultAuth0Service};
diff --git a/src/static/assets/drone_image_01.jpg b/src/static/assets/drone_image_01.jpg
new file mode 100644
index 0000000..2e239a5
Binary files /dev/null and b/src/static/assets/drone_image_01.jpg differ
diff --git a/src/static/assets/drone_specification_01.jpg b/src/static/assets/drone_specification_01.jpg
new file mode 100644
index 0000000..8c489ea
Binary files /dev/null and b/src/static/assets/drone_specification_01.jpg differ
diff --git a/src/static/assets/drone_specification_01.pdf b/src/static/assets/drone_specification_01.pdf
new file mode 100644
index 0000000..4dd6c21
Binary files /dev/null and b/src/static/assets/drone_specification_01.pdf differ
diff --git a/src/static/assets/drone_thumb_01.jpg b/src/static/assets/drone_thumb_01.jpg
new file mode 100644
index 0000000..a6544bd
Binary files /dev/null and b/src/static/assets/drone_thumb_01.jpg differ
diff --git a/src/static/img/myDrones/drone-lg.png b/src/static/img/myDrones/drone-lg.png
index 2283147..448d226 100644
Binary files a/src/static/img/myDrones/drone-lg.png and b/src/static/img/myDrones/drone-lg.png differ
diff --git a/src/static/img/myDrones/drone-spec.png b/src/static/img/myDrones/drone-spec.png
index 6f08ad2..d645448 100644
Binary files a/src/static/img/myDrones/drone-spec.png and b/src/static/img/myDrones/drone-spec.png differ
diff --git a/src/static/img/myDrones/my-drone-1.png b/src/static/img/myDrones/my-drone-1.png
index 02d3b2b..eb58cc1 100644
Binary files a/src/static/img/myDrones/my-drone-1.png and b/src/static/img/myDrones/my-drone-1.png differ
diff --git a/src/static/img/myDrones/my-drone-2.png b/src/static/img/myDrones/my-drone-2.png
index b9402c3..5a3b4d9 100644
Binary files a/src/static/img/myDrones/my-drone-2.png and b/src/static/img/myDrones/my-drone-2.png differ
diff --git a/src/static/img/myDrones/my-drone-3.png b/src/static/img/myDrones/my-drone-3.png
index b347080..846771d 100644
Binary files a/src/static/img/myDrones/my-drone-3.png and b/src/static/img/myDrones/my-drone-3.png differ
diff --git a/src/static/img/myDrones/my-drone-4.png b/src/static/img/myDrones/my-drone-4.png
index 2722254..622dbce 100644
Binary files a/src/static/img/myDrones/my-drone-4.png and b/src/static/img/myDrones/my-drone-4.png differ
diff --git a/src/store/modules/global.js b/src/store/modules/global.js
index 0e0357c..5c825d5 100644
--- a/src/store/modules/global.js
+++ b/src/store/modules/global.js
@@ -3,38 +3,49 @@ import {browserHistory} from 'react-router';
import UserApi from 'api/User.js';
import config from '../../config';
+import APIService from 'services/APIService';
+
const userApi = new UserApi(config.api.basePath);
+//------------------------------------------------------------------------------
+// Constants
+
+const LOGIN_ACTION_FAILURE = 'LOGIN_ACTION_FAILURE';
+const LOGIN_ACTION_SUCCESS = 'LOGIN_ACTION_SUCCESS';
+
+const LOGIN_REDIRECT = {
+ admin: '/admin',
+ consumer: '/browse-provider',
+ pilot: '/pilot-missions',
+ provider: '/dashboard',
+};
+
+const LOGOUT_ACTION = 'LOGOUT_ACTION';
+const USER_INFO_KEY = 'userInfo';
+
// ------------------------------------
// Actions
// ------------------------------------
+
+// TODO: Any use of these local variables should be eliminated!
+// Their current usage should be entirely replaced using the redux state,
+// and action payloads!
let isLogged = false;
let hasError = false;
let errorText = '';
+let userInfo = {};
-export const sendLoginRequest = (values) => new Promise((resolve) => {
- userApi.login(values.email, values.password).then((authResult) => {
- isLogged = true;
- hasError = false;
- if (authResult.user.role === 'consumer') {
- browserHistory.push('/browse-provider');
- } else if (authResult.user.role === 'provider') {
- browserHistory.push('/dashboard');
- } else if (authResult.user.role === 'admin') {
- browserHistory.push('/admin');
- } else if (authResult.user.role === 'pilot') {
- browserHistory.push('/pilot');
- }
- }).catch((err) => {
- isLogged = false;
- hasError = true;
- errorText = JSON.parse(err.responseText);
- });
- resolve();
-});
+function loadUserInfo() {
+ userInfo = localStorage.getItem(USER_INFO_KEY);
+ if (userInfo) {
+ userInfo = JSON.parse(userInfo);
+ APIService.accessToken = userInfo.accessToken;
+ }
+ return userInfo;
+}
export const sendSignupRequest = (values) => new Promise((resolve) => {
- userApi.register('name', values.email, values.password).then(() => {
+ userApi.register(values.firstName, values.lastName, values.email, values.password).then(() => {
isLogged = true;
hasError = false;
browserHistory.push('/browse-provider');
@@ -48,14 +59,37 @@ export const sendSignupRequest = (values) => new Promise((resolve) => {
export const toggleNotification = createAction('TOGGLE_NOTIFICATION');
-export const loginAction = createAction('LOGIN_ACTION');
+export const loginAction = (data) => (dispatch) => {
+ userApi.login(data.email, data.password).then((res) => {
+ localStorage.setItem(USER_INFO_KEY, JSON.stringify(res));
+ dispatch({type: LOGIN_ACTION_SUCCESS});
+ browserHistory.push(LOGIN_REDIRECT[res.user.role]);
+ }).catch((failure) => {
+ dispatch({
+ type: LOGIN_ACTION_FAILURE,
+ payload: JSON.parse(failure.response).error,
+ });
+ });
+};
+
+export const onSocialLoginSuccessAction = () => (dispatch) => {
+ dispatch({type: LOGIN_ACTION_SUCCESS});
+ browserHistory.push(LOGIN_REDIRECT[loadUserInfo().user.role]);
+};
+
+export const logoutAction = () => (dispatch) => {
+ browserHistory.push('/home');
+ dispatch({
+ type: LOGOUT_ACTION,
+ });
+};
export const signupAction = createAction('SIGNUP_ACTION');
export const actions = {
- toggleNotification, loginAction,
+ toggleNotification, loginAction, logoutAction,
};
-// console.log(loginAction(true))
+
// ------------------------------------
// Reducer
// ------------------------------------
@@ -63,24 +97,45 @@ export default handleActions({
[toggleNotification]: (state, action) => ({
...state, toggleNotif: action.payload,
}),
- [loginAction]: (state) => ({
- ...state, loggedUser: isLogged, hasError, errorText,
+ [LOGIN_ACTION_FAILURE]: (state, action) => ({
+ ...state,
+ loggedUser: false,
+ hasError: true,
+ errorText: action.payload,
+ user: {},
+ }),
+ [LOGIN_ACTION_SUCCESS]: (state) => ({
+ ...state,
+ loggedUser: true,
+ hasError: false,
+ errorText: '',
+ user: (loadUserInfo() ? loadUserInfo().user : {}),
}),
+ [LOGOUT_ACTION]: (state) => {
+ localStorage.removeItem(USER_INFO_KEY);
+ APIService.accessToken = '';
+ isLogged = false;
+ return ({
+ ...state,
+ loggedUser: false,
+ hasError,
+ errorText,
+ user: {},
+ });
+ },
[signupAction]: (state) => ({
- ...state, loggedUser: isLogged, hasError, errorText,
+ ...state, loggedUser: isLogged, hasError, errorText, user: (loadUserInfo() ? loadUserInfo().user : {}),
}),
}, {
toggleNotif: false,
- loggedUser: false,
+ loggedUser: Boolean(loadUserInfo()),
location: 'Jakarta, Indonesia',
selectedCategory: 'Category',
categories: [
{name: 'Category1'},
{name: 'Category2'},
],
- user: {
- name: 'John Doe',
- },
+ user: loadUserInfo() ? loadUserInfo().user : {},
notifications: [
{
id: 1,
diff --git a/src/store/reducers.js b/src/store/reducers.js
index 7b0c5a5..9c93e85 100644
--- a/src/store/reducers.js
+++ b/src/store/reducers.js
@@ -5,12 +5,44 @@ import {reducer as form} from 'redux-form';
import {reducer as toastr} from 'react-redux-toastr';
import global from './modules/global';
import searchNFZ from './modules/searchNFZ';
+import _ from 'lodash';
+
+/**
+ * Normalize form field of a number type
+ * @param {Mixed} value current field value
+ * @param {Mixed} previousValue previous field value
+ * @return {Mixed} resulting field value
+ */
+const normalizeFloat = (value, previousValue) => (
+ _.isString(value) && !value.match(/^\d*(\.\d*)?$/) ? previousValue : value
+);
+
+/**
+ * Normalize form field of an integer type
+ * @param {Mixed} value current field value
+ * @return {Mixed} resulting field value
+ */
+const normalizeInteger = (value) => (
+ _.isString(value) ? value.replace(/[^\d]/g, '') : value
+);
export const makeRootReducer = (asyncReducers) => combineReducers({
router,
global,
searchNFZ,
- form,
+ form: form.normalize({
+ editDrones: {
+ numberOfRotors: normalizeInteger,
+ minSpeed: normalizeFloat,
+ maxSpeed: normalizeFloat,
+ maxFlightTime: normalizeFloat,
+ maxCargoWeight: normalizeFloat,
+ maxAltitude: normalizeFloat,
+ cameraResolution: normalizeFloat,
+ videoResolution: normalizeFloat,
+ mileage: normalizeFloat,
+ },
+ }),
reduxAsyncConnect,
...asyncReducers,
toastr,
diff --git a/src/styles/img/icon-pagination-next.png b/src/styles/img/icon-pagination-next.png
new file mode 100644
index 0000000..7237964
Binary files /dev/null and b/src/styles/img/icon-pagination-next.png differ
diff --git a/src/styles/img/icon-pagination-prev.png b/src/styles/img/icon-pagination-prev.png
new file mode 100644
index 0000000..43275ae
Binary files /dev/null and b/src/styles/img/icon-pagination-prev.png differ
diff --git a/src/styles/img/icon-select-arrow-small.png b/src/styles/img/icon-select-arrow-small.png
new file mode 100644
index 0000000..9a4c127
Binary files /dev/null and b/src/styles/img/icon-select-arrow-small.png differ