diff --git a/config/webpack.config.prod.js b/config/webpack.config.prod.js
index f63d0f6d05637a81ad015c92b22c3db630029168..0eb878fac2adeac89ac2653e839dfe7ae96d9771 100644
--- a/config/webpack.config.prod.js
+++ b/config/webpack.config.prod.js
@@ -135,7 +135,7 @@ module.exports = {
test: /\.scss$/,
loader: ExtractTextPlugin.extract(
'style',
- 'css?modules&importLoaders=1&-autoprefixer!postcss!resolve-url!sass'
+ 'css?importLoaders=1!postcss!sass'
)
},
// JSON is not enabled by default in Webpack but both Node and Browserify
diff --git a/package.json b/package.json
index db99d7e53b78b7a5278452c44ebbc926f0ed24b0..e7a62217c17665ca333eeda568c64cf576ba81ea 100644
--- a/package.json
+++ b/package.json
@@ -55,6 +55,7 @@
"whatwg-fetch": "1.0.0"
},
"dependencies": {
+ "bpl": "git+https://7f8bbb4b0a383ad905fee5b0f7cbc0c22533b556:x-oauth-basic@github.com/Blocp/bpl.git",
"react": "^15.3.2",
"react-dom": "^15.3.2",
"react-redux": "^4.4.5",
@@ -62,8 +63,8 @@
"react-router-redux": "^4.0.7",
"redux": "^3.6.0",
"redux-promise": "^0.5.3",
- "whatwg-fetch": "^1.0.0",
- "bpl": "git+https://7f8bbb4b0a383ad905fee5b0f7cbc0c22533b556:x-oauth-basic@github.com/Blocp/bpl.git"
+ "redux-saga": "^0.14.2",
+ "whatwg-fetch": "^1.0.0"
},
"scripts": {
"start": "node scripts/start.js",
diff --git a/src/components/BuildingListTable/index.js b/src/components/BuildingListTable/index.js
index 8e2adfc4d4e4d777d9b2b4fd85838aa881d7a608..275586a57bb33be90305b7f800dd98be87ff72a1 100644
--- a/src/components/BuildingListTable/index.js
+++ b/src/components/BuildingListTable/index.js
@@ -1,6 +1,6 @@
import React, { PropTypes } from 'react';
import { Link } from 'react-router';
-import './styles.css';
+import './styles.scss';
export default function BuildingListTable({ buildings }) {
if (!buildings || buildings.length === 0) {
diff --git a/src/components/BuildingListTable/styles.css b/src/components/BuildingListTable/styles.scss
similarity index 100%
rename from src/components/BuildingListTable/styles.css
rename to src/components/BuildingListTable/styles.scss
diff --git a/src/components/BuildingOverview/index.js b/src/components/BuildingOverview/index.js
index f07ec448da96a4cf0c9f62c90820789780d6fbf8..ee12d28f37ab02c5914cd55652649f6797fadcb4 100644
--- a/src/components/BuildingOverview/index.js
+++ b/src/components/BuildingOverview/index.js
@@ -1,11 +1,7 @@
import React, { PropTypes } from 'react';
+import './styles.scss';
export default function BuildingOverview({ building }) {
- if (Object.keys(building).length === 0) {
- // TODO add loading icon?
- return
...
;
- }
-
return (
diff --git a/src/components/BuildingOverview/styles.css b/src/components/BuildingOverview/styles.scss
similarity index 100%
rename from src/components/BuildingOverview/styles.css
rename to src/components/BuildingOverview/styles.scss
diff --git a/src/components/SideBarDetail/index.js b/src/components/SideBarDetail/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..fb62883ea388069d22fa287c021c4634b11a6f16
--- /dev/null
+++ b/src/components/SideBarDetail/index.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import './styles.scss';
+
+export default function SideBarDetail() {
+ return (
+
+
Sidebar
+
+ );
+}
diff --git a/src/containers/BuildingDetail/styles.css b/src/components/SideBarDetail/styles.scss
similarity index 100%
rename from src/containers/BuildingDetail/styles.css
rename to src/components/SideBarDetail/styles.scss
diff --git a/src/components/TurkHit/defaultForm.js b/src/components/TurkHit/defaultForm.js
new file mode 100644
index 0000000000000000000000000000000000000000..b8fa47c87c48c8304d0a37a71cce5bffbbe46256
--- /dev/null
+++ b/src/components/TurkHit/defaultForm.js
@@ -0,0 +1,16 @@
+// blocpower_id and address will be added from component
+const defaultTurkHit = {
+ instructions_text: 'We need to get building dimensions for buildings using Google Earth. Read the instructions below on what measurements need to be taken.',
+ instructions_url: 'http://beta.blocpower.us/dimensions/BuildingDimensionsInstructions.pdf',
+ worksheet_url: 'http://beta.blocpower.us/dimensions/BuildingDimensionsTemplate.xlsx',
+ max_assignments: '1',
+ title: 'Measure building dimensions',
+ description: 'Use google earth to measure building\'s dimensions, windows, and doors',
+ keywords: 'building, dimensions, google earth',
+ duration: '300',
+ reward: '5',
+ MIN_FILE_BYTES: 1,
+ MAX_FILE_BYTES: 10,
+};
+
+export default defaultTurkHit;
diff --git a/src/components/TurkHit/index.js b/src/components/TurkHit/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..e549d09d36d2b580afac77ce83548a1344c9b273
--- /dev/null
+++ b/src/components/TurkHit/index.js
@@ -0,0 +1,45 @@
+import React, { PropTypes } from 'react';
+import defaultForm from './defaultForm';
+import './styles.scss';
+
+export default function TurkHit({ createHit, address, blocpower_id, hit }) {
+ let hitStatus = ;
+ if (!hit.error.message && hit.status !== '') {
+ hitStatus = (
+
+
HIT Id: {hit.id}
+
HIT Status: {hit.status}
+
+ );
+ } else {
+ hitStatus = (
+
+ );
+ }
+
+ return (
+
+
+ {hitStatus}
+
+ );
+}
+
+TurkHit.propTypes = {
+ createHit: PropTypes.func,
+ address: PropTypes.string,
+ blocpower_id: PropTypes.number,
+ hit: PropTypes.shape({
+ id: PropTypes.string,
+ status: PropTypes.string,
+ loading: PropTypes.boolean,
+ error: PropTypes.boolean,
+ }),
+};
diff --git a/src/components/TurkHit/styles.scss b/src/components/TurkHit/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/containers/BuildingDetail/actions.js b/src/containers/BuildingDetail/actions.js
index e31836f2e28ba5c6b227162885cf70f4f3376e15..356c341109ee1bbf3f0e08918911efc0c633941a 100644
--- a/src/containers/BuildingDetail/actions.js
+++ b/src/containers/BuildingDetail/actions.js
@@ -1,25 +1,93 @@
import 'whatwg-fetch';
-import { FETCH_BUILDING_DETAIL } from './constants';
+import {
+ LOAD_BUILDING_DETAIL,
+ LOAD_BUILDING_DETAIL_SUCCEES,
+ LOAD_BUILDING_DETAIL_ERROR,
+ CREATE_HIT,
+ CREATE_HIT_SUCCEES,
+ CREATE_HIT_ERROR,
+} from './constants';
-const ROOT_URL = `${process.env.REACT_APP_BUILDING_SERVICE}/building/`;
-const HEADERS = new Headers({ 'x-blocpower-app-key': process.env.REACT_APP_KEY });
+/**
+ * Load building details, this action starts the request saga
+ *
+ * @param blocPowerID The current blocPowerID
+ * @returns {object} An action object with a type of LOAD_BUILDING_DETAIL
+ * passing the building detail
+ */
+export function loadBuildingDetail(blocPowerID) {
+ return {
+ type: LOAD_BUILDING_DETAIL,
+ payload: blocPowerID,
+ };
+}
-const init = {
- method: 'GET',
- headers: HEADERS,
- mode: 'cors',
- cache: 'default',
-};
+/**
+ * Dispatched when the building details are loaded by the request saga
+ *
+ * @param {object} buildingDetail The building data
+ * @returns {object} An action object with a type of
+ * LOAD_BUILDING_DETAIL_SUCCEES
+ */
+export function buildingDetailLoaded(buildingDetail) {
+ return {
+ type: LOAD_BUILDING_DETAIL_SUCCEES,
+ payload: buildingDetail,
+ };
+}
-function fetchBuildingDetail(blocPowerID) {
+/**
+ * Dispatched when loading the building detail fails
+ *
+ * @param {object} error The error
+ *
+ * @return {object} An action object with a type of
+ * LOAD_BUILDING_DETAIL_ERROR passing the error
+ */
+export function buildingDetailLoadingError(error) {
return {
- type: FETCH_BUILDING_DETAIL,
- payload: fetch(`${ROOT_URL}${blocPowerID}`, init).then(response =>
- response.json()
- ),
+ type: LOAD_BUILDING_DETAIL_ERROR,
+ error,
};
}
-/* eslint-disable import/prefer-default-export */
-export { fetchBuildingDetail };
-/* eslint-enable */
+/**
+ * Create mturk HIT, this action starts the request saga
+ *
+ * @param {object} formData The mturk form data
+ * @returns {object} An action object with a type of CREATE_HIT
+ */
+export function createHit(formData) {
+ return {
+ type: CREATE_HIT,
+ payload: formData,
+ };
+}
+
+/**
+ * Dispatched when the mturk HIT was successfully created
+ *
+ * @param {object} hitData The created hit data
+ * @returns {object} An action object with a type of CREATE_HIT_SUCCEES
+ */
+export function createHitSuccess(hitData) {
+ return {
+ type: CREATE_HIT_SUCCEES,
+ payload: hitData,
+ };
+}
+
+/**
+ * Dispatched when creating the mturk HIT fails
+ *
+ * @param {object} error The error
+ *
+ * @return {object} An action object with a type of CREATE_HIT_ERROR
+ * passing the error
+ */
+export function createHitError(error) {
+ return {
+ type: CREATE_HIT_ERROR,
+ error,
+ };
+}
diff --git a/src/containers/BuildingDetail/constants.js b/src/containers/BuildingDetail/constants.js
index 1d22cced45a63a673f33fa0c59af1268e4305d08..873b9080d77454308cb96f389e1c36077d58b7c8 100644
--- a/src/containers/BuildingDetail/constants.js
+++ b/src/containers/BuildingDetail/constants.js
@@ -1,3 +1,6 @@
-/* eslint-disable import/prefer-default-export */
-export const FETCH_BUILDING_DETAIL = 'FETCH_BUILDING_DETAIL';
-/* eslint-enable */
+export const LOAD_BUILDING_DETAIL = 'LOAD_BUILDING_DETAIL';
+export const LOAD_BUILDING_DETAIL_SUCCEES = 'LOAD_BUILDING_DETAIL_SUCCEES';
+export const LOAD_BUILDING_DETAIL_ERROR = 'LOAD_BUILDING_DETAIL_ERROR';
+export const CREATE_HIT = 'CREATE_HIT';
+export const CREATE_HIT_SUCCEES = 'CREATE_HIT_SUCCEES';
+export const CREATE_HIT_ERROR = 'CREATE_HIT_ERROR';
diff --git a/src/containers/BuildingDetail/index.js b/src/containers/BuildingDetail/index.js
index 1339a137870f454ab7c333962cd9db75a7c76fb3..d8789ee3e335cac29379c66d09d107569f1b5818 100644
--- a/src/containers/BuildingDetail/index.js
+++ b/src/containers/BuildingDetail/index.js
@@ -1,34 +1,87 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+
+import { loadBuildingDetail, createHit } from './actions';
+import './styles.scss';
+import SideBarDetail from '../../components/SideBarDetail';
+import TurkHit from '../../components/TurkHit';
-import { fetchBuildingDetail } from './actions';
-import './styles.css';
-import BuildingOverview from '../../components/BuildingOverview';
class BuildingDetail extends Component {
componentDidMount() {
- this.props.fetchBuildingDetail(this.props.params.blocPowerID);
+ this.props.loadBuildingDetail(this.props.params.blocPowerID);
}
render() {
- return ;
+ let mainContent = ;
+ let hit = ;
+
+ if (this.props.buildingDetail.overview.error) {
+ mainContent = (
+
+ {this.props.buildingDetail.overview.error.message}
+
+ );
+ } else {
+ mainContent = React.cloneElement(this.props.children, {
+ building: this.props.buildingDetail.overview,
+ });
+
+ hit = (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {mainContent}
+ {hit}
+
+
+ );
}
}
BuildingDetail.propTypes = {
buildingDetail: PropTypes.shape({
- address: PropTypes.string,
- bbl: PropTypes.number,
- blocpower_id: PropTypes.number,
- borough: PropTypes.string,
- zipcode: PropTypes.number,
+ overview: PropTypes.shape({
+ address: PropTypes.string,
+ bbl: PropTypes.number,
+ blocpower_id: PropTypes.number,
+ borough: PropTypes.string,
+ zipcode: PropTypes.number,
+ loading: PropTypes.boolean,
+ error: PropTypes.boolean,
+ }),
+ hit: PropTypes.shape({
+ id: PropTypes.string,
+ status: PropTypes.string,
+ loading: PropTypes.boolean,
+ error: PropTypes.boolean,
+ }),
}),
params: PropTypes.objectOf(PropTypes.string),
- fetchBuildingDetail: PropTypes.func,
+ loadBuildingDetail: PropTypes.func,
+ createHit: PropTypes.func,
+ children: React.PropTypes.element,
};
+function mapDispatchToProps(dispatch) {
+ return bindActionCreators({ loadBuildingDetail, createHit }, dispatch);
+}
+
function mapStateToProps({ buildingDetail }) {
return { buildingDetail };
}
-export default connect(mapStateToProps, { fetchBuildingDetail })(BuildingDetail);
+export default connect(mapStateToProps, mapDispatchToProps)(BuildingDetail);
diff --git a/src/containers/BuildingDetail/reducer.js b/src/containers/BuildingDetail/reducer.js
index a49aa9c3e76794afdf24c61b3f372ab5beb96ca5..457c6ed739417d42b920af95be052ddff1bdb7b4 100644
--- a/src/containers/BuildingDetail/reducer.js
+++ b/src/containers/BuildingDetail/reducer.js
@@ -1,9 +1,87 @@
-import { FETCH_BUILDING_DETAIL } from './constants';
+import {
+ LOAD_BUILDING_DETAIL,
+ LOAD_BUILDING_DETAIL_SUCCEES,
+ LOAD_BUILDING_DETAIL_ERROR,
+ CREATE_HIT,
+ CREATE_HIT_SUCCEES,
+ CREATE_HIT_ERROR,
+} from './constants';
-export default function (state = {}, action) {
+const initState = {
+ overview: {
+ loading: false,
+ error: false,
+ },
+ hit: {
+ id: '',
+ status: '',
+ loading: false,
+ error: false,
+ },
+};
+
+export default function (state = initState, action) {
switch (action.type) {
- case FETCH_BUILDING_DETAIL:
- return action.payload.data;
+ case LOAD_BUILDING_DETAIL:
+ return {
+ ...state,
+ overview: {
+ ...state.overview,
+ loading: true,
+ error: false,
+ },
+ };
+
+ case LOAD_BUILDING_DETAIL_SUCCEES:
+ return {
+ ...state,
+ overview: {
+ ...state.overview,
+ ...action.payload.data,
+ loading: false,
+ },
+ };
+
+ case LOAD_BUILDING_DETAIL_ERROR:
+ return {
+ ...state,
+ overview: {
+ ...state.overview,
+ loading: true,
+ error: action.error,
+ },
+ };
+
+ case CREATE_HIT:
+ return {
+ ...state,
+ hit: {
+ ...state.hit,
+ loading: true,
+ error: false,
+ },
+ };
+
+ case CREATE_HIT_SUCCEES:
+ return {
+ ...state,
+ hit: {
+ ...state.hit,
+ id: action.payload.data.hit_id,
+ status: 'Submitted',
+ loading: false,
+ },
+ };
+
+ case CREATE_HIT_ERROR:
+ return {
+ ...state,
+ hit: {
+ ...state.hit,
+ loading: false,
+ error: action.error,
+ },
+ };
default:
return state;
diff --git a/src/containers/BuildingDetail/sagas.js b/src/containers/BuildingDetail/sagas.js
new file mode 100644
index 0000000000000000000000000000000000000000..319b938a11d3e9726622bc22fca5aaeac2f45a31
--- /dev/null
+++ b/src/containers/BuildingDetail/sagas.js
@@ -0,0 +1,72 @@
+import { call, put, takeEvery } from 'redux-saga/effects';
+import request from '../../utils/request';
+
+import {
+ LOAD_BUILDING_DETAIL,
+ CREATE_HIT,
+} from './constants';
+
+import {
+ buildingDetailLoaded,
+ buildingDetailLoadingError,
+ createHitSuccess,
+ createHitError,
+} from './actions';
+
+const BUILDING_SERVICE_URL = `${process.env.REACT_APP_BUILDING_SERVICE}`;
+const HEADERS = new Headers({
+ 'x-blocpower-app-key': process.env.REACT_APP_KEY,
+});
+
+/**
+ * Detail page request/response handler
+ *
+ * @param {object} action blocPowerId of building
+ */
+function* getBuildingDetail(action) {
+ const blocPowerId = action.payload;
+ const data = yield call(
+ request,
+ `${BUILDING_SERVICE_URL}/building/${blocPowerId}`, {
+ method: 'GET',
+ headers: HEADERS,
+ }
+ );
+
+ if (!data.err) {
+ yield put(buildingDetailLoaded(data));
+ } else {
+ yield put(buildingDetailLoadingError(data.err));
+ }
+}
+
+/**
+ * Mechanical Turk HIT creation request/response handler
+ *
+ * @param {object} action Form data of mechanical turk job
+ */
+function* createHit(action) {
+ const turkHitFormData = action.payload;
+ const data = yield call(request, `${BUILDING_SERVICE_URL}/turkhit/`, {
+ method: 'POST',
+ headers: HEADERS,
+ body: JSON.stringify(turkHitFormData),
+ });
+
+ if (!data.err) {
+ yield put(createHitSuccess(data));
+ } else {
+ yield put(createHitError(data.err));
+ }
+}
+
+/**
+ * Watches for LOAD_BUILDING_DETAIL & CREATE_HIT actions and calls
+ * the appropriate handler function.
+ */
+function* buildingDetailWatcher() {
+ yield takeEvery(LOAD_BUILDING_DETAIL, getBuildingDetail);
+ yield takeEvery(CREATE_HIT, createHit);
+}
+
+export default buildingDetailWatcher;
diff --git a/src/containers/BuildingDetail/styles.scss b/src/containers/BuildingDetail/styles.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/containers/BuildingList/index.js b/src/containers/BuildingList/index.js
index a06bf3c16044b56d4f4ce03cdd850582d44ed8bf..3e775c91a0cb3d502c8e8cc1f3c25f7e892d490a 100644
--- a/src/containers/BuildingList/index.js
+++ b/src/containers/BuildingList/index.js
@@ -1,11 +1,10 @@
-
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { fetchBuildings, searchTerm } from './actions';
import BuildingListTable from '../../components/BuildingListTable';
-import './styles.css';
+import './styles.scss';
import { SearchSVG } from '../../components/bpl';
// TODO remove this address
diff --git a/src/containers/BuildingList/reducer.js b/src/containers/BuildingList/reducer.js
index 6c804205b1a62101965efb48e8b8e2c9695de06d..13acd7e523fec1da413e9c3b9d1c88e9f2602256 100644
--- a/src/containers/BuildingList/reducer.js
+++ b/src/containers/BuildingList/reducer.js
@@ -3,7 +3,7 @@ import { FETCH_BUILDINGS, SEARCH_TERM } from './constants';
export default function (state = {}, action) {
switch (action.type) {
case FETCH_BUILDINGS:
- return { ...state, buildings: action.payload.data };
+ return { ...state, buildings: action.payload.buildings };
case SEARCH_TERM:
return { ...state, term: action.payload };
diff --git a/src/containers/BuildingList/styles.css b/src/containers/BuildingList/styles.scss
similarity index 100%
rename from src/containers/BuildingList/styles.css
rename to src/containers/BuildingList/styles.scss
diff --git a/src/reducer.js b/src/reducers.js
similarity index 83%
rename from src/reducer.js
rename to src/reducers.js
index af7f5c9bc2e0136f0d58a670692dad81c301be83..3568c2e97cbcec614ecf80980febf3020f658e45 100644
--- a/src/reducer.js
+++ b/src/reducers.js
@@ -4,10 +4,8 @@ import { routerReducer } from 'react-router-redux';
import BuildingListReducer from './containers/BuildingList/reducer';
import BuildingDetailReducer from './containers/BuildingDetail/reducer';
-const rootReducer = combineReducers({
+export default combineReducers({
routing: routerReducer,
buildingList: BuildingListReducer,
buildingDetail: BuildingDetailReducer,
});
-
-export default rootReducer;
diff --git a/src/routes.js b/src/routes.js
index 281d3477251de40f9bf4ba2b338efe6626371810..24075922b7870f7f5c4d6cb70c3df84afcde0029 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -2,13 +2,16 @@ import React from 'react';
import { Route, IndexRoute, IndexRedirect } from 'react-router';
import BuildingList from './containers/BuildingList';
import BuildingDetail from './containers/BuildingDetail';
+import BuildingOverview from './components/BuildingOverview';
-module.exports = (
+export default (
-
+
+
+
);
diff --git a/src/sagas.js b/src/sagas.js
new file mode 100644
index 0000000000000000000000000000000000000000..644286f84cd58aca32892992442cbcdb1562dde0
--- /dev/null
+++ b/src/sagas.js
@@ -0,0 +1,7 @@
+import buildingDetailSaga from './containers/BuildingDetail/sagas';
+
+export default function* rootSaga() {
+ yield [
+ buildingDetailSaga(),
+ ];
+}
diff --git a/src/store.js b/src/store.js
index d397ae7334ebba491d3df136b4c1f77eba69606b..83daf1d4ddd0e9eeb25c01875956ebdabcedf56a 100644
--- a/src/store.js
+++ b/src/store.js
@@ -2,7 +2,10 @@ import { createStore, applyMiddleware, compose } from 'redux';
import ReduxPromise from 'redux-promise';
import { browserHistory } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux';
-import rootReducer from './reducer';
+import createSagaMiddleware from 'redux-saga';
+
+import rootReducer from './reducers';
+import rootSaga from './sagas';
/* eslint-disable no-underscore-dangle */
const composeEnhancers =
@@ -12,15 +15,18 @@ const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
// Specify here name, actionsBlacklist, actionsCreators and other options
}) : compose;
+/* eslint-enable */
+
+const sagaMiddleware = createSagaMiddleware();
const enhancer = composeEnhancers(
- applyMiddleware(ReduxPromise),
+ applyMiddleware(sagaMiddleware, ReduxPromise),
// other store enhancers if any
);
-/* eslint-enable */
const store = createStore(rootReducer, enhancer);
-
const history = syncHistoryWithStore(browserHistory, store);
+sagaMiddleware.run(rootSaga);
+
export { store, history };
diff --git a/src/utils/request.js b/src/utils/request.js
new file mode 100644
index 0000000000000000000000000000000000000000..fae139a806cc767f217d043fe43d63f267d10ed2
--- /dev/null
+++ b/src/utils/request.js
@@ -0,0 +1,35 @@
+import 'whatwg-fetch';
+
+
+/**
+ * Checks if a network request came back fine, and throws an error if not
+ *
+ * @param {object} response A response from a network request
+ *
+ * @return {object|undefined} Returns either the response, or throws an error
+ */
+function checkStatus(response) {
+ if (response.ok) {
+ return response;
+ }
+
+ const error = new Error(response.statusText);
+ error.response = response;
+ throw error;
+}
+
+/**
+ * Requests a URL, returning a promise
+ *
+ * @param {string} url The URL we want to request
+ * @param {object} [options] The options we want to pass to "fetch"
+ *
+ * @return {object} An object containing either "data" or "err"
+ */
+export default function request(url, options) {
+ return fetch(url, options)
+ .then(checkStatus)
+ .then(response => response.json())
+ .then(data => ({ data }))
+ .catch(err => ({ err }));
+}