diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..671105b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,2 @@ +printWidth: 100; +parser: flow; diff --git a/js/apis/FoodItemsApi.js b/js/apis/FoodItemsApi.js index 80cc879..0c0d8e6 100644 --- a/js/apis/FoodItemsApi.js +++ b/js/apis/FoodItemsApi.js @@ -1,111 +1,60 @@ // @flow +import { memoize } from 'ramda'; -// TODO open a websoket and create observable from it -const DUMMY_DATA = [ - { - id: 1, - name: 'Big John Cookies', - placeId: 'ChIJf5QJFBK1RIYRjjfxZz9Z0O0', - latitude: 30.270667599999996, - longitude: -97.7532464, - distance: 0.5, - quantity: 'alot', - category: 'desserts', - images: [ - 'https://s-media-cache-ak0.pinimg.com/564x/e7/5f/08/e75f08b00c0bc7f2b01b3d1a636389f6.jpg', - 'http://images.media-allrecipes.com/userphotos/560x315/1107530.jpg', - ], - thumbImage: 'http://images.media-allrecipes.com/userphotos/560x315/1107530.jpg', - }, - { - id: 2, - name: 'Jelly Filled Donuts', - placeId: 'ChIJ72if-Qe1RIYRCzMucGEEdBA', - latitude: 30.263963300000004, - longitude: -97.742308, - distance: 1.0, - quantity: 'few', - category: 'desserts', - images: ['https://timenewsfeed.files.wordpress.com/2013/06/jellydonut.jpg'], - thumbImage: 'https://timenewsfeed.files.wordpress.com/2013/06/jellydonut.jpg', - }, - { - id: 3, - name: 'Spelt Brownies', - placeId: 'ChIJG44vBQi1RIYRvWGHdYUolZY', - latitude: 30.265019100000004, - longitude: -97.7419085, - distance: 1.7, - quantity: 'few', - category: 'desserts', - images: ['http://www.momshealthyeats.com/wp-content/uploads/2011/11/spelt-fudge-brownie.jpg'], - thumbImage: 'http://www.momshealthyeats.com/wp-content/uploads/2011/11/spelt-fudge-brownie.jpg', - }, - { - id: 4, - name: 'Oatmeal Raisin Cookies', - placeId: 'ChIJf5QJFBK1RIYRjjfxZz9Z0O0', - latitude: 30.270667599999996, - longitude: -97.7532464, - distance: 0.5, - quantity: 'few', - category: 'desserts', - images: ['http://cookingontheside.com/wp-content/uploads/2009/04/oatmeal_raisin_cookies_stack-400.jpg'], - thumbImage: 'http://cookingontheside.com/wp-content/uploads/2009/04/oatmeal_raisin_cookies_stack-400.jpg', - }, - { - id: 5, - name: 'Donuts with Sprinkles', - placeId: 'ChIJ72if-Qe1RIYRCzMucGEEdBA', - latitude: 30.263963300000004, - longitude: -97.742308, - distance: 1.0, - quantity: 'few', - category: 'desserts', - images: ['https://dannivee.files.wordpress.com/2013/10/img_1950.jpg'], - thumbImage: 'https://dannivee.files.wordpress.com/2013/10/img_1950.jpg', - }, - { - id: 6, - name: 'Powdered Donuts', - placeId: 'ChIJ72if-Qe1RIYRCzMucGEEdBA', - latitude: 30.263963300000004, - longitude: -97.742308, - distance: 1.0, - quantity: 'few', - category: 'desserts', - images: [ - 'http://3.bp.blogspot.com/-NUKSXr1qLHs/UQmsaEFgbTI/AAAAAAAAA_Y/l4psfVl4a5A/s1600/white-powdered-sugar-doughnuts-tracie-kaska.jpg', - ], - thumbImage: - 'http://3.bp.blogspot.com/-NUKSXr1qLHs/UQmsaEFgbTI/AAAAAAAAA_Y/l4psfVl4a5A/s1600/white-powdered-sugar-doughnuts-tracie-kaska.jpg', - }, - { - id: 7, - name: 'Snickerdoodles', - placeId: 'ChIJr3szW6a1RIYRkM7LRpnBIO0', - latitude: 30.266898599999998, - longitude: -97.73798459999999, - quantity: 'few', - category: 'desserts', - images: ['http://josephcphillips.com/wp-content/uploads/2015/02/snickerdoodles2.jpg'], - thumbImage: 'http://josephcphillips.com/wp-content/uploads/2015/02/snickerdoodles2.jpg', - }, - { - id: 8, - name: 'Pizza', - placeId: 'ChIJr3szW6a1RIYRkM7LRpnBIO0', - latitude: 30.266898599999998, - longitude: -97.73798459999999, - quantity: 'few', - category: 'entrees', - images: ['http://www.foodandhealth.co.uk/wp-content/uploads/2016/05/Hot-stone-Vegan-Pizza.jpg'], - thumbImage: 'http://www.foodandhealth.co.uk/wp-content/uploads/2016/05/Hot-stone-Vegan-Pizza.jpg', - }, -]; - -/* eslint-disable no-unused-vars */ -export const getFoodItemsForLocation = (location: Location) => { - return Promise.resolve(DUMMY_DATA); +export type FoodItemsFilter = { + radius?: number, }; -/* eslint-enable no-unused-vars */ + +export type RawFoodItem = { + id: string, + name: string, + place_id: string, + category: string, + images: string, + thumbimage: string, + latitude: number, + longitude: number, + distance: number, +}; + +export type FoodItemsForLocation = { + orderby: string, + filter: FoodItemsFilter, + fooditems: Array, +}; + +export const getFoodItemsForLocation = memoize( + async ({ pos: { coords: { latitude, longitude } } }: { pos: Position }): Promise => { + try { + return fetch('http://192.168.122.1:3000/fooditems', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + lat: latitude, + lng: longitude, + orderby: 'distance', + filter: { + radius: 10, + }, + }), + }) + .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: error, + }; + } + } +); diff --git a/js/components/FoodItemTile.js b/js/components/FoodItemTile.js index 7f4de1e..3ea16c1 100644 --- a/js/components/FoodItemTile.js +++ b/js/components/FoodItemTile.js @@ -8,30 +8,36 @@ import theme from '../ui-theme'; import { Link } from 'react-router-native'; import { routeWithTitle } from '../helpers/RouteHelpers'; import { TileBox, StrongText, SubText, Thumbnail } from './ItemTile'; +import { withPlace } from '../enhancers/placeEnhancers'; -export default pure(({ foodItem, place = {} }: { foodItem: FoodItemRecord, place: PlaceRecord }) => { - if (!foodItem || !place) { - return ; - } - - return ( - - - - - - - {foodItem.name || ''} - - - {`${place.name || ''} - ${foodItem.distance} mi`} - - - - - - ); +const PlaceNameAndDistance = withPlace(({ place = {}, distance = 999.9 }: { place: PlaceRecord, distance: number }) => { + return {`${place.name || 'Loading...'} - ${parseFloat(distance).toFixed(1)} mi`}; }); + +export default pure( + ({ foodItem }: { foodItem: FoodItemRecord }) => { + if (!foodItem) { + return ; + } + + return ( + + + + + + {foodItem.name || ''} + + + + + + ); + } +); diff --git a/js/enhancers/foodItemEnhancers.js b/js/enhancers/foodItemEnhancers.js index 8ae538d..7b711d0 100644 --- a/js/enhancers/foodItemEnhancers.js +++ b/js/enhancers/foodItemEnhancers.js @@ -26,8 +26,8 @@ export const withFoodItemsAsSeq = mapPropsStream(props$ => ); export const withFoodItemIdFromRoute = withProps((props: { match: { params: { id: string } } }) => { - const id = path(['match', 'params', 'id'], props) || 0; - return { foodItemId: +id }; + const id: string = path(['match', 'params', 'id'], props) || ''; + return { foodItemId: id }; }); export const withFoodItem = compose( diff --git a/js/pages/FoodList.js b/js/pages/FoodList.js index 6a4ae58..755c204 100644 --- a/js/pages/FoodList.js +++ b/js/pages/FoodList.js @@ -6,13 +6,10 @@ import { ActionButton } from 'react-native-material-ui'; import { routeWithTitle } from '../helpers/RouteHelpers'; import FoodItemList from '../components/FoodItemList'; import typeof FoodItemRecord from '../records/FoodItemRecord'; -import { withPlaceForFoodItem } from '../enhancers/placeEnhancers'; import theme from '../ui-theme'; -const FoodItemWithPlace = withPlaceForFoodItem(FoodItemTile); - -const renderFoodItem = (foodItem: FoodItemRecord) => ; +const renderFoodItem = (foodItem: FoodItemRecord) => ; export default class FoodList extends Component { static displayName = 'FoodList'; diff --git a/js/records/FoodItemRecord.js b/js/records/FoodItemRecord.js index 11b6d5c..61bea5e 100644 --- a/js/records/FoodItemRecord.js +++ b/js/records/FoodItemRecord.js @@ -1,5 +1,6 @@ //@flow import { fromJS, List, Record } from 'immutable'; +import { type RawFoodItem } from '../apis/FoodItemsApi'; export const QUANTITY_NONE: 'none' = 'none'; export const QUANTITY_FEW: 'few' = 'few'; @@ -20,7 +21,7 @@ export type Category = export const CATEGORIES = [CATEGORY_BEVERAGES, CATEGORY_DESSERTS, CATEGORY_ENTREES, CATEGORY_OTHER]; export type FoodItem = { - id: ?number, + id: ?string, name: string, placeId: ?number, latitude: number, @@ -34,7 +35,7 @@ export type FoodItem = { }; const FoodRecordDefaults: FoodItem = { - id: null, + id: '', name: '', placeId: null, latitude: 0, @@ -49,10 +50,12 @@ const FoodRecordDefaults: FoodItem = { const FoodItemRecord = Record(FoodRecordDefaults, 'FoodItemRecord'); -export const createFoodItem = (foodItemRaw: Object) => { +export const createFoodItem = (foodItemRaw: RawFoodItem) => { return new FoodItemRecord({ ...foodItemRaw, - images: fromJS(foodItemRaw.images), + images: fromJS((foodItemRaw.images || '').split(',')), + placeId: foodItemRaw.place_id, + thumbImage: foodItemRaw.thumbimage, }); }; diff --git a/js/streams/FoodItemsStream.js b/js/streams/FoodItemsStream.js index 73eb0de..5d50a82 100644 --- a/js/streams/FoodItemsStream.js +++ b/js/streams/FoodItemsStream.js @@ -3,8 +3,10 @@ import { createFoodItem } from '../records/FoodItemRecord'; import { setById } from '../helpers/ImmutableHelpers'; import { Map } from 'immutable'; import location$ from './LocationStream'; -import { getFoodItemsForLocation } from '../apis/FoodItemsApi'; +import { getFoodItemsForLocation, type FoodItemsForLocation } from '../apis/FoodItemsApi'; -export default location$.mergeMap(getFoodItemsForLocation).map(foodItems => { - return foodItems.map(createFoodItem).reduce(setById, new Map()); -}); +export default location$ + .mergeMap(pos => getFoodItemsForLocation({ pos })) + .map(({ fooditems = [] }: FoodItemsForLocation) => { + return fooditems.map(createFoodItem).reduce(setById, new Map()); + }); diff --git a/js/streams/PlacesStream.js b/js/streams/PlacesStream.js index fc7825d..58cfedb 100644 --- a/js/streams/PlacesStream.js +++ b/js/streams/PlacesStream.js @@ -16,18 +16,18 @@ import { setById } from '../helpers/ImmutableHelpers'; */ const safeGetPlaceDetails = memoize((placeId: string): Promise => { return getPlaceDetails(placeId).catch(error => { - console.log(error); // eslint-disable-line no-console + console.error(error); // eslint-disable-line no-console return null; }); }); -export default foodItems$ - .map((foodItems: Map): Array => { - return foodItems.map(foodItem => foodItem.placeId).toArray(); +const Places$ = foodItems$ + .mergeMap((foodItems: Map): Observable => { + return Observable.from(foodItems.toArray().map(foodItem => foodItem.placeId)); }) - .mergeMap((placeIds: Array): Observable>> => { - return Observable.forkJoin(placeIds.map(safeGetPlaceDetails)); - }) - .map((places: Array): Map => { - return places.map(buildPlaceRecord).reduce(setById, new Map()); - }); + .distinct() + .mergeMap(safeGetPlaceDetails) + .map(buildPlaceRecord) + .scan(setById, new Map()); + +export default Places$;