quantities round trip api and stream

make POST requests to /quantity to update the quantity of an existing food item - then mixin the new quantity with the existing food items observable
This commit is contained in:
Bart Akeley 2017-11-26 19:52:16 -06:00
parent 70892af248
commit 9221b7f9b5
10 changed files with 105 additions and 19 deletions

View file

@ -1,9 +1,7 @@
// @flow // @flow
import { memoize } from 'ramda'; import { memoize } from 'ramda';
import FilterRecord from '../records/FilterRecord'; import FilterRecord from '../records/FilterRecord';
import { BASE_URL } from '../constants/AppConstants';
const BASE_URL = 'aretherecookies.herokuapp.com';
// const BASE_URL = '192.168.1.6:3000';
export type FoodItemsFilter = { export type FoodItemsFilter = {
radius?: number, radius?: number,

27
js/apis/QuantityApi.js Normal file
View file

@ -0,0 +1,27 @@
// @flow
import type { Quantity } from '../constants/QuantityConstants';
import { BASE_URL } from '../constants/AppConstants';
import type { QuantityResponse } from '../constants/QuantityConstants';
export const setQuantity = ({
foodItemId,
quantity,
}: {
foodItemId: string,
quantity: Quantity,
}): ?Promise<QuantityResponse> => {
try {
return fetch(`http://${BASE_URL}/quantity`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
foodItemId,
quantity,
}),
}).then(res => res.json());
} catch (error) {
console.log(error); //eslint-disable-line
}
};

View file

@ -0,0 +1,4 @@
// @flow
export const BASE_URL = 'aretherecookies.herokuapp.com';
// export const BASE_URL = '192.168.1.6:3000';

View file

@ -3,5 +3,15 @@ export const QUANTITY_NONE: 'none' = 'none';
export const QUANTITY_FEW: 'few' = 'few'; export const QUANTITY_FEW: 'few' = 'few';
export const QUANTITY_MANY: 'many' = 'many'; export const QUANTITY_MANY: 'many' = 'many';
export const QUANTITY_LOTS: 'lots' = 'lots'; export const QUANTITY_LOTS: 'lots' = 'lots';
export type Quantity = typeof QUANTITY_NONE | typeof QUANTITY_FEW | typeof QUANTITY_MANY | typeof QUANTITY_LOTS;
export const QUANTITIES = [QUANTITY_NONE, QUANTITY_FEW, QUANTITY_LOTS, QUANTITY_MANY]; export const QUANTITIES = [QUANTITY_NONE, QUANTITY_FEW, QUANTITY_LOTS, QUANTITY_MANY];
export type Quantity = typeof QUANTITY_NONE | typeof QUANTITY_FEW | typeof QUANTITY_MANY | typeof QUANTITY_LOTS;
export type QuantityResponse = {
food_item_id: string,
quantity: Quantity,
date: number,
};
export type QuantityFragment = { quantity: Quantity, lastupdated: number };

View file

@ -0,0 +1,13 @@
//@flow
import { withProps } from 'recompose';
import { setQuantity } from '../apis/QuantityApi';
import { emit } from '../streams/QuantityStream';
import type { Quantity, QuantityResponse } from '../constants/QuantityConstants';
import { nth } from 'ramda';
export const withUpdateQuantity = withProps({
updateQuantity: async ({ foodItemId, quantity }: { foodItemId: string, quantity: Quantity }) => {
const newQuantity: QuantityResponse = nth(0, await setQuantity({ foodItemId, quantity }));
emit(newQuantity);
},
});

View file

@ -3,6 +3,10 @@ import { type List, type Map } from 'immutable';
export const pushInto = <T>(list: List<T>, item: T): List<T> => list.push(item); export const pushInto = <T>(list: List<T>, item: T): List<T> => list.push(item);
export function merge(a: Map<any, Object>, b: Map<any, Object>) {
return a.merge(b);
}
export const setById = (map: Map<string, any>, item: ?{ id?: string }): Map<string, any> => { export const setById = (map: Map<string, any>, item: ?{ id?: string }): Map<string, any> => {
if (!item || !item.id) { if (!item || !item.id) {
return map; return map;

View file

@ -3,8 +3,8 @@ import React, { Component } from 'react';
import { Image, View } from 'react-native'; import { Image, View } from 'react-native';
import theme from '../ui-theme'; import theme from '../ui-theme';
import { StrongText, SubText } from '../components/ItemTile'; import { StrongText, SubText } from '../components/ItemTile';
import { type FoodItem } from '../records/FoodItemRecord'; import typeof FoodItemRecord from '../records/FoodItemRecord';
import { type Quantity, QUANTITY_MANY } from '../constants/QuantityConstants'; import { type Quantity } from '../constants/QuantityConstants';
import typeof PlaceRecord from '../records/PlaceRecord'; import typeof PlaceRecord from '../records/PlaceRecord';
import { compose, pure } from 'recompose'; import { compose, pure } from 'recompose';
import IconButton from '../components/IconButton'; import IconButton from '../components/IconButton';
@ -16,6 +16,7 @@ import QuantityPicker from '../components/QuantityPicker';
import { routeWithTitle } from '../helpers/RouteHelpers'; import { routeWithTitle } from '../helpers/RouteHelpers';
import { Link } from 'react-router-native'; import { Link } from 'react-router-native';
import moment from 'moment'; import moment from 'moment';
import { withUpdateQuantity } from '../enhancers/quantityEnhancers';
const { foodItemDetails: style } = theme; const { foodItemDetails: style } = theme;
@ -31,19 +32,18 @@ export class FoodItemDetail extends Component {
static displayName = 'FoodItemDetail'; static displayName = 'FoodItemDetail';
props: { props: {
foodItem: FoodItem, foodItem: FoodItemRecord,
place: PlaceRecord, place: PlaceRecord,
updateQuantity: ({ foodItemId: string, quantity: Quantity }) => void,
}; };
state: { state: {
isFavorite: boolean, isFavorite: boolean,
quantity: Quantity,
currentImage: number, currentImage: number,
}; };
state = { state = {
currentImage: 0, currentImage: 0,
quantity: QUANTITY_MANY,
}; };
// TODO placeholder implementation until we get a backend // TODO placeholder implementation until we get a backend
@ -52,7 +52,10 @@ export class FoodItemDetail extends Component {
isFavorite: !prevState.isFavorite, isFavorite: !prevState.isFavorite,
})); }));
updateAmount = (quantity: Quantity) => this.setState({ quantity }); updateAmount = (quantity: Quantity) => {
const { updateQuantity, foodItem: { id: foodItemId } } = this.props;
updateQuantity({ foodItemId, quantity });
};
// TODO // TODO
addPhoto = () => {}; addPhoto = () => {};
@ -102,11 +105,11 @@ export class FoodItemDetail extends Component {
}} }}
> >
<QuantityPicker <QuantityPicker
quantity={this.state.quantity || quantity} quantity={quantity}
onValueChange={this.updateAmount} onValueChange={this.updateAmount}
style={{ flex: 1, marginBottom: 10 }} style={{ flex: 1, marginBottom: 10 }}
/> />
<SubText>Last updated at {moment(foodItem.lastupdated).format('H:mm A on MMM D, YYYY')}</SubText> <SubText>Last updated at {moment(foodItem.lastupdated).format('h:mm A on MMM D, YYYY')}</SubText>
</View> </View>
<View style={{ flex: 2, ...contentTileStyle }}> <View style={{ flex: 2, ...contentTileStyle }}>
<IconButton <IconButton
@ -127,6 +130,6 @@ export class FoodItemDetail extends Component {
} }
} }
const enhance = compose(pure, withFoodItem, withPlaceForFoodItem); const enhance = compose(pure, withFoodItem, withPlaceForFoodItem, withUpdateQuantity);
export default enhance(FoodItemDetail); export default enhance(FoodItemDetail);

View file

@ -1,15 +1,20 @@
//@flow //@flow
import { createFoodItem } from '../records/FoodItemRecord'; import FoodItemRecord, { createFoodItem } from '../records/FoodItemRecord';
import { setById } from '../helpers/ImmutableHelpers'; import { setById } from '../helpers/ImmutableHelpers';
import { Map } from 'immutable'; import { Map } from 'immutable';
import location$ from './LocationStream'; import location$ from './LocationStream';
import { getFoodItems, type FoodItemsForLocation } from '../apis/FoodItemsApi'; import { getFoodItems, type FoodItemsForLocation } from '../apis/FoodItemsApi';
import FilterSubject from './FilterStream'; import Filter$ from './FilterStream';
import FilterRecord from '../records/FilterRecord'; import FilterRecord from '../records/FilterRecord';
import Quantity$ from './QuantityStream';
import type { QuantityFragment } from '../constants/QuantityConstants';
export default location$ export default location$
.combineLatest(FilterSubject) .combineLatest(Filter$)
.mergeMap(([loc, filter]: [Position, FilterRecord]) => getFoodItems({ loc, filter })) .mergeMap(([loc, filter]: [Position, FilterRecord]) => getFoodItems({ loc, filter }))
.map(({ fooditems = [] }: FoodItemsForLocation) => { .map(({ fooditems = [] }: FoodItemsForLocation) => {
return fooditems.map(createFoodItem).reduce(setById, new Map()); return fooditems.map(createFoodItem).reduce(setById, new Map());
})
.combineLatest(Quantity$, (foodItems: Map<string, FoodItemRecord>, quantities: Map<string, QuantityFragment>) => {
return foodItems.mergeDeepWith((foodItem, quantity) => foodItem.merge(quantity), quantities);
}); });

View file

@ -0,0 +1,24 @@
//@flow
import { ReplaySubject } from 'rxjs';
import type { QuantityResponse, QuantityFragment } from '../constants/QuantityConstants';
import { Map } from 'immutable';
const observable: ReplaySubject<QuantityResponse> = new ReplaySubject();
export function emit(val: ?QuantityResponse) {
observable.next(val);
}
// force our observable to emit an initial empty map so that food items will load
emit(null);
export default observable.scan((quantitiesByFoodItemId: Map<string, QuantityFragment>, quantity?: QuantityResponse) => {
if (!quantity) {
return quantitiesByFoodItemId;
}
return quantitiesByFoodItemId.set(quantity.food_item_id, {
quantity: quantity.quantity,
lastupdated: quantity.date,
});
}, new Map());

View file

@ -10,9 +10,7 @@ SELECT
q.quantity AS quantity, q.quantity AS quantity,
q.date AS lastUpdated q.date AS lastUpdated
FROM food_items f FROM food_items f
LEFT OUTER JOIN ( LEFT OUTER JOIN latest_quantities q
SELECT food_item_id, quantity, MAX(date) AS date FROM quantities GROUP BY food_item_id, quantity
) q
ON f.id = q.food_item_id ON f.id = q.food_item_id
WHERE ST_DWithin(loc, ST_SetSRID(ST_Point(-97.7286718, 30.3033267),4326)::geography, 20 * 1609) WHERE ST_DWithin(loc, ST_SetSRID(ST_Point(-97.7286718, 30.3033267),4326)::geography, 20 * 1609)
AND f.category IN ('desserts', 'beverages', 'entrees', 'other') AND f.category IN ('desserts', 'beverages', 'entrees', 'other')