create food item save button and stream

This commit is contained in:
Bart Akeley 2018-02-04 13:14:37 -06:00
parent a03df04154
commit e5e76ce881
6 changed files with 177 additions and 48 deletions

View file

@ -2,6 +2,7 @@
import { memoize } from 'ramda';
import FilterRecord from '../records/FilterRecord';
import { BASE_URL } from '../constants/AppConstants';
import FoodItemRecord from '../records/FoodItemRecord';
export type FoodItemsFilter = {
radius?: number,
@ -26,42 +27,56 @@ export type FoodItemsForLocation = {
fooditems: Array<RawFoodItem>,
};
export const getFoodItems = memoize(async ({ loc, filter }: { loc: Position, filter: FilterRecord }): Promise<
FoodItemsForLocation
> => {
const { coords: { latitude: lat, longitude: lng } } = loc;
const { orderby, categories, radius } = filter;
export const getFoodItems = memoize(
async ({
loc,
filter,
}: {
loc: Position,
filter: FilterRecord,
}): Promise<FoodItemsForLocation> => {
const { coords: { latitude: lat, longitude: lng } } = loc;
const { orderby, categories, radius } = filter;
try {
return fetch(`http://${BASE_URL}/fooditems`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
lat,
lng,
orderby,
filter: {
...(categories ? { categories } : {}),
radius,
try {
return fetch(`http://${BASE_URL}/fooditems`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}),
})
.then(res => res.json())
.then(json => ({
...json,
body: JSON.stringify({
lat,
lng,
orderby,
filter: {
...(categories ? { categories } : {}),
radius,
},
}),
})
.then(res => res.json())
.then(json => ({
...json,
loading: false,
error: null,
}));
} catch (error) {
console.error(error); // eslint-disable-line no-console
return {
orderby: 'distance',
filter: {},
fooditems: [],
loading: false,
error: null,
}));
} catch (error) {
console.error(error); // eslint-disable-line no-console
return {
orderby: 'distance',
filter: {},
fooditems: [],
loading: false,
error: error,
};
error: error,
};
}
}
});
);
export const createFoodItem = (foodItem: FoodItemRecord) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(foodItem);
}, 1000);
});
};

View file

@ -0,0 +1,35 @@
// @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';
const FoodItemSaveBtn = ({
saveFoodItem,
loading,
}: {
saveFoodItem?: Function,
loading: boolean,
}) => {
const textStyle = {
color: 'white',
marginRight: 20,
fontWeight: 'bold',
opacity: loading ? 0.7 : 1,
};
return (
<TouchableOpacity onPress={loading ? null : saveFoodItem}>
<Text style={textStyle}>SAVE</Text>
</TouchableOpacity>
);
};
export default compose(
withCreateFoodItem,
mapProps(({ saveFoodItem, loading }) => ({
saveFoodItem,
loading,
}))
)(FoodItemSaveBtn);

View file

@ -5,6 +5,7 @@ import { type RoutingContextFlowTypes, routingContextPropTypes } from '../routes
import { path } from 'ramda';
import queryString from 'query-string';
import { getSearch } from '../helpers/RouteHelpers';
import FoodItemSaveBtn from '../components/FoodItemSaveBtn';
type Props = {
title: ?string,
@ -48,6 +49,9 @@ class TopToolbar extends Component {
case '/list': {
return viewMode === 'map' ? 'list' : 'map';
}
case '/createFoodItem': {
return <FoodItemSaveBtn />;
}
default: {
return '';
}

View file

@ -0,0 +1,41 @@
// @flow
import mapPropsStream from 'recompose/mapPropsStream';
import CreateFoodItem$, { emitter } from '../streams/CreateFoodItemStream';
import FoodItemRecord from '../records/FoodItemRecord';
import { createFoodItem } from '../apis/FoodItemsApi';
// todo real implementation somewhere
const isFoodItemValid = () => true;
export const withCreateFoodItem = 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 saveFoodItem = async () => {
if (isFoodItemValid(foodItem)) {
try {
setLoading(true);
await createFoodItem(foodItem);
} catch (error) {
// todo else surface a toast notification
setError(error);
} finally {
setLoading(false);
}
}
};
return {
...props,
foodItem,
loading,
error,
setFoodItem,
saveFoodItem,
};
});
});

View file

@ -12,6 +12,8 @@ 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 Spinner from 'react-native-loading-spinner-overlay';
type GooglePlaceObject = {
placeID: string,
@ -31,18 +33,19 @@ const fieldNameStyle = {
paddingLeft: 10,
};
const Field = ({ onPress, text = '' }: { onPress?: Function, text: string }) =>
const Field = ({ onPress, text = '' }: { onPress?: Function, text: string }) => (
<TouchableOpacity style={fieldStyle} onPress={onPress}>
<Text style={fieldNameStyle}>
{text}
</Text>
<Text style={fieldNameStyle}>{text}</Text>
<Divider />
</TouchableOpacity>;
</TouchableOpacity>
);
const openPlaceModal = (onChoosePlace: (place: GooglePlaceObject) => void) => () => {
RNGooglePlaces.openAutocompleteModal({ type: 'establishment' }).then(onChoosePlace).catch(error => {
throw error;
});
RNGooglePlaces.openAutocompleteModal({ type: 'establishment' })
.then(onChoosePlace)
.catch(error => {
throw error;
});
};
type Props = {
@ -56,12 +59,23 @@ type Props = {
updatePlace: (place: GooglePlaceObject) => void,
addImage: (uri: string) => void,
setImagePreview: (index?: number) => void,
loading: boolean,
};
const CreateFoodItem = (props: Props) => {
const { foodItem, toggleNameModal, setPropOfFoodItem, place, updatePlace, addImage, setImagePreview } = props;
const {
foodItem,
toggleNameModal,
setPropOfFoodItem,
place,
updatePlace,
addImage,
setImagePreview,
loading,
} = props;
return (
<View style={{ ...theme.page.container, backgroundColor: 'white', padding: 10 }}>
<Spinner visible={loading} />
<Field text={foodItem.name || 'Name'} onPress={toggleNameModal} />
<Field text={place.name || 'Place'} onPress={openPlaceModal(updatePlace)} />
<CategoryPicker
@ -77,9 +91,9 @@ const CreateFoodItem = (props: Props) => {
/>
<Divider />
<ImagePicker onCreateNew={addImage}>
{foodItem.images.map((image, index) =>
{foodItem.images.map((image, index) => (
<ImageThumb key={index} uri={image} onPress={() => setImagePreview(index)} />
)}
))}
</ImagePicker>
</View>
);
@ -147,10 +161,11 @@ const updatePlace = ({ setPlace, foodItem, setFoodItem }: Props) => ({
);
};
const toggleNameModal = ({ nameModalOpen, setNameModalOpen }) => () => setNameModalOpen(!nameModalOpen);
const toggleNameModal = ({ nameModalOpen, setNameModalOpen }) => () =>
setNameModalOpen(!nameModalOpen);
export default compose(
withState('foodItem', 'setFoodItem', new FoodItemRecord()),
withCreateFoodItem,
withState('place', 'setPlace', new PlaceRecord()),
withState('nameModalOpen', 'setNameModalOpen', false),
withState('imagePreview', 'setImagePreview', -1),

View file

@ -0,0 +1,19 @@
// @flow
import { ReplaySubject } from 'rxjs';
import FoodItemRecord from '../records/FoodItemRecord';
type CreateFoodItemState = {
foodItem: FoodItemRecord,
loading: boolean,
error?: ?Error,
};
const multicaster: ReplaySubject<CreateFoodItemState> = new ReplaySubject();
export function emitter(val: CreateFoodItemState) {
multicaster.next(val);
}
emitter({ foodItem: new FoodItemRecord(), loading: false, error: null });
export default multicaster;