mirror of
https://gitlab.com/wheres-the-tp/ui-mobile.git
synced 2026-01-25 09:44:55 -06:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
4e934d9b6b
8 changed files with 182 additions and 48 deletions
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
35
js/components/FoodItemSaveBtn.js
Normal file
35
js/components/FoodItemSaveBtn.js
Normal 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);
|
||||
|
|
@ -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 '';
|
||||
}
|
||||
|
|
|
|||
41
js/enhancers/createFoodItemEnhancers.js
Normal file
41
js/enhancers/createFoodItemEnhancers.js
Normal 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,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
19
js/streams/CreateFoodItemStream.js
Normal file
19
js/streams/CreateFoodItemStream.js
Normal 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;
|
||||
|
|
@ -20,6 +20,7 @@
|
|||
"react-native-facebook-login": "^1.6.0",
|
||||
"react-native-google-places": "^2.1.0",
|
||||
"react-native-image-picker": "^0.26.3",
|
||||
"react-native-loading-spinner-overlay": "^0.5.2",
|
||||
"react-native-looped-carousel": "^0.1.5",
|
||||
"react-native-maps": "0.15.1",
|
||||
"react-native-material-ui": "^1.7.0",
|
||||
|
|
|
|||
|
|
@ -3721,6 +3721,10 @@ react-native-image-picker@^0.26.3:
|
|||
version "0.26.7"
|
||||
resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-0.26.7.tgz#ad2ee957f7f6cc01396893ea03d84cb2adb2e376"
|
||||
|
||||
react-native-loading-spinner-overlay@^0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/react-native-loading-spinner-overlay/-/react-native-loading-spinner-overlay-0.5.2.tgz#b7bcd277476d596615fd7feee601789f9bdc7acc"
|
||||
|
||||
react-native-looped-carousel@^0.1.5:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/react-native-looped-carousel/-/react-native-looped-carousel-0.1.7.tgz#9e81aec732039250568e367383d0bd70ba4173e9"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue