From 54f5bf2893a9231da70de9aaff2742bc0a34ad6f Mon Sep 17 00:00:00 2001 From: Bart Akeley Date: Sun, 11 Feb 2018 09:53:53 -0600 Subject: [PATCH] create food item save button wired up to backend --- js/apis/FetchApi.js | 1 + js/apis/FoodItemsApi.js | 35 +++++++++++-------------- js/apis/QuantityApi.js | 2 -- js/components/FoodItemSaveBtn.js | 28 ++++++++++++++------ js/enhancers/createFoodItemEnhancers.js | 28 +++++++++----------- js/enhancers/routeEnhancers.js | 17 ++++++++---- js/pages/CreateFoodItem.js | 4 +-- js/streams/FoodItemsStream.js | 23 +++++++++++++++- js/streams/LocationStream.js | 1 + 9 files changed, 87 insertions(+), 52 deletions(-) diff --git a/js/apis/FetchApi.js b/js/apis/FetchApi.js index cd3bce9..2ada38f 100644 --- a/js/apis/FetchApi.js +++ b/js/apis/FetchApi.js @@ -17,6 +17,7 @@ export const fetchRequest = async ({ method, headers: { 'Content-Type': 'application/json', + ...AuthManager.getAuthHeader(), ...headers, }, body: JSON.stringify(body), diff --git a/js/apis/FoodItemsApi.js b/js/apis/FoodItemsApi.js index bb59d3e..0ead1f5 100644 --- a/js/apis/FoodItemsApi.js +++ b/js/apis/FoodItemsApi.js @@ -1,8 +1,8 @@ // @flow import { memoize } from 'ramda'; import FilterRecord from '../records/FilterRecord'; -import { BASE_URL } from '../constants/AppConstants'; import FoodItemRecord from '../records/FoodItemRecord'; +import { fetchRequest } from './FetchApi'; export type FoodItemsFilter = { radius?: number, @@ -39,12 +39,10 @@ export const getFoodItems = memoize( const { orderby, categories, radius } = filter; try { - return fetch(`http://${BASE_URL}/fooditems`, { + return fetchRequest({ + endpoint: '/fooditems', method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ + body: { lat, lng, orderby, @@ -52,14 +50,12 @@ export const getFoodItems = memoize( ...(categories ? { categories } : {}), radius, }, - }), - }) - .then(res => res.json()) - .then(json => ({ - ...json, - loading: false, - error: null, - })); + }, + }).then(json => ({ + ...json, + loading: false, + error: null, + })); } catch (error) { console.error(error); // eslint-disable-line no-console return { @@ -73,10 +69,11 @@ export const getFoodItems = memoize( } ); -export const createFoodItem = (foodItem: FoodItemRecord) => { - return new Promise(resolve => { - setTimeout(() => { - resolve(foodItem); - }, 1000); +export const createFoodItem = async (foodItem: FoodItemRecord) => { + const res = await fetchRequest({ + endpoint: '/addfooditem', + method: 'POST', + body: foodItem, }); + return res; }; diff --git a/js/apis/QuantityApi.js b/js/apis/QuantityApi.js index 415d6d4..466dc41 100644 --- a/js/apis/QuantityApi.js +++ b/js/apis/QuantityApi.js @@ -1,7 +1,6 @@ // @flow import type { Quantity } from '../constants/QuantityConstants'; import type { QuantityResponse } from '../constants/QuantityConstants'; -import AuthManager from '../AuthManager'; import { fetchRequest } from './FetchApi'; export const setQuantity = ({ @@ -14,7 +13,6 @@ export const setQuantity = ({ return fetchRequest({ method: 'POST', endpoint: '/quantity', - headers: { ...AuthManager.getAuthHeader() }, body: { foodItemId, quantity, diff --git a/js/components/FoodItemSaveBtn.js b/js/components/FoodItemSaveBtn.js index 5f718c1..63440a5 100644 --- a/js/components/FoodItemSaveBtn.js +++ b/js/components/FoodItemSaveBtn.js @@ -1,9 +1,10 @@ // @flow import React from 'react'; import { Text, TouchableOpacity } from 'react-native'; -import { withCreateFoodItem } from '../enhancers/createFoodItemEnhancers'; -import compose from 'recompose/compose'; -import mapProps from 'recompose/mapProps'; +import { withCreateFoodItemState } from '../enhancers/createFoodItemEnhancers'; +import { withReplaceRoute } from '../enhancers/routeEnhancers'; +import { compose, onlyUpdateForKeys, withHandlers } from 'recompose'; +import { routeWithTitle } from '../helpers/RouteHelpers'; const FoodItemSaveBtn = ({ saveFoodItem, @@ -27,9 +28,20 @@ const FoodItemSaveBtn = ({ }; export default compose( - withCreateFoodItem, - mapProps(({ saveFoodItem, loading }) => ({ - saveFoodItem, - loading, - })) + withCreateFoodItemState, + withReplaceRoute, + withHandlers({ + saveFoodItem: ({ saveFoodItem, setLoading, setError, replaceRoute }) => async () => { + try { + setLoading(true); + const { id, name } = await saveFoodItem(); + replaceRoute(routeWithTitle(`/foodItem/${id || ''}`, name)); + } catch (error) { + setError(error); + } finally { + setLoading(false); + } + }, + }), + onlyUpdateForKeys(['loading']) )(FoodItemSaveBtn); diff --git a/js/enhancers/createFoodItemEnhancers.js b/js/enhancers/createFoodItemEnhancers.js index 88b32c0..433d3f3 100644 --- a/js/enhancers/createFoodItemEnhancers.js +++ b/js/enhancers/createFoodItemEnhancers.js @@ -1,31 +1,27 @@ // @flow import mapPropsStream from 'recompose/mapPropsStream'; -import CreateFoodItem$, { emitter } from '../streams/CreateFoodItemStream'; -import FoodItemRecord from '../records/FoodItemRecord'; +import CreateFoodItem$, { emitter as emitCreateItemState } from '../streams/CreateFoodItemStream'; +import { emitter as emitFoodItemsState } from '../streams/FoodItemsStream'; import { createFoodItem } from '../apis/FoodItemsApi'; +import FoodItemRecord, { createFoodItem as buildFoodItem } from '../records/FoodItemRecord'; // todo real implementation somewhere const isFoodItemValid = () => true; -export const withCreateFoodItem = mapPropsStream(props$ => { +export const withCreateFoodItemState = mapPropsStream(props$ => { return props$.combineLatest(CreateFoodItem$, (props, state) => { const { foodItem, loading, error } = state; - const setFoodItem = (foodItem: FoodItemRecord) => emitter({ ...state, foodItem }); - const setLoading = (loading: boolean) => emitter({ ...state, loading }); - const setError = (error: Error) => emitter({ ...state, error }); + const setFoodItem = (foodItem: FoodItemRecord) => emitCreateItemState({ ...state, foodItem }); + const setLoading = (loading: boolean) => emitCreateItemState({ ...state, loading }); + const setError = (error: Error) => emitCreateItemState({ ...state, error }); const saveFoodItem = async () => { if (isFoodItemValid(foodItem)) { - try { - setLoading(true); - await createFoodItem(foodItem); - } catch (error) { - // todo else surface a toast notification - setError(error); - } finally { - setLoading(false); - } + const res = await createFoodItem(foodItem); + const createdFoodItem = buildFoodItem(res[0]); + emitFoodItemsState(createdFoodItem); + return createdFoodItem; } }; @@ -36,6 +32,8 @@ export const withCreateFoodItem = mapPropsStream(props$ => { error, setFoodItem, saveFoodItem, + setLoading, + setError, }; }); }); diff --git a/js/enhancers/routeEnhancers.js b/js/enhancers/routeEnhancers.js index a37ed60..4549cc0 100644 --- a/js/enhancers/routeEnhancers.js +++ b/js/enhancers/routeEnhancers.js @@ -1,5 +1,5 @@ // @flow -import { getContext, mapProps } from 'recompose'; +import { getContext, withProps, compose } from 'recompose'; import { shape, func, string } from 'prop-types'; import { path } from 'ramda'; import { getSearch } from '../helpers/RouteHelpers'; @@ -21,16 +21,23 @@ export const withRouterContext = getContext({ }).isRequired, }); -export const withViewMode = mapProps((props: Object) => { +export const withViewMode = withProps((props: Object) => { return { - ...props, viewMode: getSearch(props).viewMode, }; }); -export const withPushRoute = mapProps((props: Object) => { +export const withPushRoute = withProps((props: Object) => { return { - ...props, pushRoute: path(['router', 'history', 'push'], props), }; }); + +export const withReplaceRoute = compose( + withRouterContext, + withProps((props: Object) => { + return { + replaceRoute: path(['router', 'history', 'replace'], props), + }; + }) +); diff --git a/js/pages/CreateFoodItem.js b/js/pages/CreateFoodItem.js index bda12fb..c29a468 100644 --- a/js/pages/CreateFoodItem.js +++ b/js/pages/CreateFoodItem.js @@ -12,7 +12,7 @@ import { compose, branch, withState, withHandlers, renderComponent, mapProps } f import RNGooglePlaces from 'react-native-google-places'; import CategoryPicker from '../components/CategoryPicker'; import { ImageThumb, ImagePicker } from '../components/ImagePicker'; -import { withCreateFoodItem } from '../enhancers/createFoodItemEnhancers'; +import { withCreateFoodItemState } from '../enhancers/createFoodItemEnhancers'; import Spinner from 'react-native-loading-spinner-overlay'; type GooglePlaceObject = { @@ -165,7 +165,7 @@ const toggleNameModal = ({ nameModalOpen, setNameModalOpen }) => () => setNameModalOpen(!nameModalOpen); export default compose( - withCreateFoodItem, + withCreateFoodItemState, withState('place', 'setPlace', new PlaceRecord()), withState('nameModalOpen', 'setNameModalOpen', false), withState('imagePreview', 'setImagePreview', -1), diff --git a/js/streams/FoodItemsStream.js b/js/streams/FoodItemsStream.js index 41d456c..fd409f6 100644 --- a/js/streams/FoodItemsStream.js +++ b/js/streams/FoodItemsStream.js @@ -1,4 +1,5 @@ //@flow +import { ReplaySubject } from 'rxjs'; import FoodItemRecord, { createFoodItem } from '../records/FoodItemRecord'; import { setById } from '../helpers/ImmutableHelpers'; import { Map } from 'immutable'; @@ -9,11 +10,31 @@ import FilterRecord from '../records/FilterRecord'; import Quantity$ from './QuantityStream'; import type { QuantityFragment } from '../constants/QuantityConstants'; -export default location$ +const foodItemSubject: ReplaySubject = new ReplaySubject(); + +export function emitter(val?: ?FoodItemRecord) { + foodItemSubject.next(val); +} + +emitter(null); + +const manualUpdate$ = foodItemSubject.scan( + (foodItemMap: Map, foodItem: FoodItemRecord) => { + return foodItem ? foodItemMap.set(foodItem.id, foodItem) : foodItemMap; + }, + Map() +); + +const fetchedFoodItems$ = location$ .combineLatest(Filter$) .mergeMap(([loc, filter]: [Position, FilterRecord]) => getFoodItems({ loc, filter })) .map(({ fooditems = [] }: FoodItemsForLocation) => { return fooditems.map(createFoodItem).reduce(setById, new Map()); + }); + +export default fetchedFoodItems$ + .combineLatest(manualUpdate$, (foodItemMap: Map, manualUpdates) => { + return foodItemMap.mergeDeep(manualUpdates); }) .combineLatest( Quantity$, diff --git a/js/streams/LocationStream.js b/js/streams/LocationStream.js index b767c2f..0eb5bdc 100644 --- a/js/streams/LocationStream.js +++ b/js/streams/LocationStream.js @@ -9,6 +9,7 @@ export default Observable.create((obs: Observer): Observable }, { enableHighAccuracy: false, + maximumAge: 10000, timeout: 1000, } );