diff --git a/js/apis/GooglePlacesApi.js b/js/apis/GooglePlacesApi.js index 08b93a8..3500541 100644 --- a/js/apis/GooglePlacesApi.js +++ b/js/apis/GooglePlacesApi.js @@ -1,7 +1,7 @@ // @flow import { type GooglePlaceObj } from '../records/PlaceRecord'; import { GoogleAPIKey } from '../constants/AppConstants'; -// import { concat } from 'ramda'; +import { memoize } from 'ramda'; const placesDetailUrl = `https://maps.googleapis.com/maps/api/place/details/json?key=${GoogleAPIKey}`; const photosUrl = `https://maps.googleapis.com/maps/api/place/photo?key=${GoogleAPIKey}`; @@ -16,27 +16,40 @@ type GoogleFindPlaceResponse = { const milesToMeters = miles => Math.ceil(miles > 0 ? miles / 0.00062137 : 0); -export const getPlaceDetails = async ({ - distance, - placeId, -}: { - distance: number, - placeId: ?string, -}): Promise => { - if (!placeId || typeof placeId !== 'string') { - throw new Error('placeid looks wrong'); +/** + * always return a promise and swallow exceptions so as not to break the stream + */ +const safeWrapAjax = ajaxFn => + memoize((...args) => { + return ajaxFn(...args).catch(error => { + console.log('ERROR: ', error); //eslint-disable-line no-console + return null; + }); + }); + +export const getPlaceDetails = safeWrapAjax( + async ({ + distance, + placeId, + }: { + distance: number, + placeId: ?string, + }): Promise => { + if (!placeId || typeof placeId !== 'string') { + throw new Error('placeid looks wrong'); + } + + const res = await fetch(`${placesDetailUrl}&placeid=${placeId}`); + + const { error_message, result }: GooglePlaceDetailsResponse = await res.json(); + + if (error_message) { + throw new Error(error_message); + } + + return { ...result, distance }; } - - const res = await fetch(`${placesDetailUrl}&placeid=${placeId}`); - - const { error_message, result }: GooglePlaceDetailsResponse = await res.json(); - - if (error_message) { - throw new Error(error_message); - } - - return { ...result, distance }; -}; +); export const getURLForPhotoReference = (opts?: { maxheight?: number, maxwidth?: number } = {}) => ( photoReference: string @@ -59,50 +72,52 @@ export const getURLForPhotoReference = (opts?: { maxheight?: number, maxwidth?: // return (await fetch(reqUrl)).toJson(); // }; -export const findNearbyPlaces = async ({ - location, - search, - radius, -}: { - location?: Position, - search?: string, - radius: number, -}): Promise => { - if (!location) { - return null; - } - - const { - coords: { latitude, longitude }, - } = location; - - const keyword = search ? `keyword=${encodeURIComponent(search)}` : ''; - const loc = `location=${latitude},${longitude}`; - const rad = `radius=${milesToMeters(radius)}`; - const reqUrl = `${placesFromTextUrl}&${keyword}&${loc}&${rad}&type=restaurant`; - - let error; - let places; - try { - const response: GoogleFindPlaceResponse = await (await fetch(reqUrl)).json(); - - // if (response.next_page_token) { - // const page = await getPageResults({ pageToken: response.next_page_token }); - // return concat(response.results, page.results); - // } - - if (response.status !== 'OK') { - throw new Error('google find places request failed'); +export const findNearbyPlaces = safeWrapAjax( + async ({ + location, + search, + radius, + }: { + location?: Position, + search?: string, + radius: number, + }): Promise => { + if (!location) { + return null; } - places = response.results; - } catch (err) { - console.log(error); // eslint-disable-line no-console - error = err; + const { + coords: { latitude, longitude }, + } = location; + + const keyword = search ? `keyword=${encodeURIComponent(search)}` : ''; + const loc = `location=${latitude},${longitude}`; + const rad = `radius=${milesToMeters(radius)}`; + const reqUrl = `${placesFromTextUrl}&${keyword}&${loc}&${rad}&type=restaurant`; + + let error; + let places; + try { + const response: GoogleFindPlaceResponse = await (await fetch(reqUrl)).json(); + + // if (response.next_page_token) { + // const page = await getPageResults({ pageToken: response.next_page_token }); + // return concat(response.results, page.results); + // } + + if (response.status !== 'OK') { + throw new Error('google find places request failed'); + } + + places = response.results; + } catch (err) { + console.log(error); // eslint-disable-line no-console + error = err; + } + return { + error, + location, + places, + }; } - return { - error, - location, - places, - }; -}; +); diff --git a/js/enhancers/placeEnhancers.js b/js/enhancers/placeEnhancers.js index 2d38001..4020525 100644 --- a/js/enhancers/placeEnhancers.js +++ b/js/enhancers/placeEnhancers.js @@ -2,10 +2,26 @@ import withProps from 'recompose/withProps'; import mapPropsStream from 'recompose/mapPropsStream'; import compose from 'recompose/compose'; -import Places$ from '../streams/PlacesStream'; +import Places$, { emitter as emitPlace } from '../streams/PlacesStream'; import FoodItems$ from '../streams/FoodItemsStream'; import { path } from 'ramda'; import { List } from 'immutable'; +import { buildPlaceRecord } from '../records/PlaceRecord'; +import { getPlaceDetails } from '../apis/GooglePlacesApi'; + +export const fetchPlaceDetails = async ({ + distance, + placeId, +}: { + distance: number, + placeId: ?string, +}) => { + const place = await getPlaceDetails({ + distance, + placeId, + }); + emitPlace(buildPlaceRecord(place)); +}; export const withPlaces = mapPropsStream(props$ => props$.combineLatest(Places$, (props, places) => { @@ -29,6 +45,7 @@ export const withPlace = compose( return { ...props, place, + fetchPlaceDetails, }; }) ); diff --git a/js/pages/PlaceDetail.js b/js/pages/PlaceDetail.js index dca745f..bba6354 100644 --- a/js/pages/PlaceDetail.js +++ b/js/pages/PlaceDetail.js @@ -1,8 +1,8 @@ // @flow -import React, { Component } from 'react'; +import React from 'react'; import { View, Image, ScrollView, Linking } from 'react-native'; import theme from '../ui-theme'; -import { compose, pure } from 'recompose'; +import { compose, pure, withState, withHandlers, lifecycle } from 'recompose'; import typeof PlaceRecord from '../records/PlaceRecord'; import { withPlace, @@ -27,112 +27,100 @@ const contentTileStyle = { paddingRight: 20, }; -export class PlaceDetail extends Component { - static displayName = 'PlaceDetail'; +type Props = { + place: ?PlaceRecord, + foodItems: ?List, + currentImage: number, + setCurrentImage: (idx: number) => void, + viewOnMap: () => void, + fetchPlaceDetails: (arg: { distance: number, placeId: string }) => Promise, +}; - props: { - place: ?PlaceRecord, - foodItems: ?List, - }; - - state: { - currentImage: number, - }; - - state = { - currentImage: 0, - }; - - changeCurrentImage = (currentImage: number) => this.setState({ currentImage }); - - // TODO placeholder implementation until we get a backend - addToFaves = () => - this.setState(prevState => ({ - isFavorite: !prevState.isFavorite, - })); - - viewOnMap = () => { - const { place } = this.props; - if (!place) { - return; - } - const { latitude, longitude } = place; - const url = `geo:${latitude}, ${longitude}`; - - Linking.canOpenURL(url) - .then(supported => { - if (supported) { - return Linking.openURL(url); - } - }) - .catch(err => console.error('Failed to open geo URL', err)); // eslint-disable-line - }; - - // todo: need to get the item from the stream by this id - render() { - const { place, foodItems } = this.props; - - if (!place) { - return ; - } - - const { photos } = place; - const { currentImage } = this.state; - - return ( - - - {photos.size === 1 && } - {photos.size > 1 && ( - - {photos.map(uri => ( - - ))} - - )} - - - - - {place.address} - - - - - - - - Products - {!!foodItems && - foodItems.map(foodItem => ( - - ))} - - - ); +const PlaceDetail = ({ place, foodItems, currentImage, setCurrentImage, viewOnMap }: Props) => { + if (!place) { + return ; } -} -const enhance = compose(pure, withPlaceIdFromRoute, withPlace, withFoodItemsForPlace); + const { photos } = place; -export default enhance(PlaceDetail); + return ( + + + {photos.size === 1 && } + {photos.size > 1 && ( + + {photos.map(uri => ( + + ))} + + )} + + + + + {place.address} + + {}} + color={style.actionIconColor} + /> + + + + Products + {!!foodItems && + foodItems.map(foodItem => ( + + ))} + + + ); +}; + +export default compose( + pure, + withPlaceIdFromRoute, + withPlace, + withFoodItemsForPlace, + withState('currentImage', 'setCurrentImage', 0), + withHandlers({ + viewOnMap: (props: Props) => () => { + const { place } = props; + if (!place) { + return; + } + const { latitude, longitude } = place; + const url = `geo:${latitude}, ${longitude}`; + + Linking.canOpenURL(url) + .then(supported => { + if (supported) { + return Linking.openURL(url); + } + }) + .catch(err => console.error('Failed to open geo URL', err)); // eslint-disable-line + }, + }), + lifecycle({ + componentDidMount() { + this.props.fetchPlaceDetails({ + distance: this.props.place.distance, + placeId: this.props.place.id, + }); + }, + }) +)(PlaceDetail); diff --git a/js/records/PlaceRecord.js b/js/records/PlaceRecord.js index 395e8cd..6061ece 100644 --- a/js/records/PlaceRecord.js +++ b/js/records/PlaceRecord.js @@ -96,43 +96,40 @@ const getPhotoUrls = pipe( map(getURLForPhotoReference({ maxheight: 600 })) ); -export const buildPlaceRecord = memoizeWith( - (place: ?GooglePlaceObj) => place && place.place_id, - (place: ?GooglePlaceObj) => { - if (!place) { - return; - } - - const { - place_id, - name, - formatted_address, - geometry: { location = {} } = {}, - formatted_phone_number, - url, - opening_hours = {}, - distance, - } = place; - - const photos = fromJS(getPhotoUrls(place)); - const thumb = getThumb(place); - - return new PlaceRecord({ - id: place_id, - name: name, - address: formatted_address, - latitude: location.lat, - longitude: location.lng, - distance, - phoneNumber: formatted_phone_number, - googleMapsUrl: url, - photos, - thumb, - hours: fromJS(opening_hours.weekday_text), - periods: fromJS(opening_hours.periods), - openNow: opening_hours.open_now, - }); +export const buildPlaceRecord = (place: ?GooglePlaceObj) => { + if (!place) { + return; } -); + + const { + place_id, + name, + formatted_address, + geometry: { location = {} } = {}, + formatted_phone_number, + url, + opening_hours = {}, + distance, + } = place; + + const photos = fromJS(getPhotoUrls(place)); + const thumb = getThumb(place); + + return new PlaceRecord({ + id: place_id, + name: name, + address: formatted_address, + latitude: location.lat, + longitude: location.lng, + distance, + phoneNumber: formatted_phone_number, + googleMapsUrl: url, + photos, + thumb, + hours: fromJS(opening_hours.weekday_text), + periods: fromJS(opening_hours.periods), + openNow: opening_hours.open_now, + }); +}; export default PlaceRecord; diff --git a/js/streams/PlacesStream.js b/js/streams/PlacesStream.js index 47a3a31..6fc636b 100644 --- a/js/streams/PlacesStream.js +++ b/js/streams/PlacesStream.js @@ -3,7 +3,7 @@ import { ReplaySubject, Observable } from 'rxjs'; import { buildPlaceRecord } from '../records/PlaceRecord'; import { Map } from 'immutable'; import { findNearbyPlaces, getPlaceDetails } from '../apis/GooglePlacesApi'; -import { memoize, path } from 'ramda'; +import { path } from 'ramda'; import { type GooglePlaceObj } from '../records/PlaceRecord'; import { setById } from '../helpers/ImmutableHelpers'; import location$ from './LocationStream'; @@ -15,21 +15,6 @@ import geodist from 'geodist'; const getGeoDist = begin => end => geodist(begin, end, { exact: true }); -/** - * always return a promise and swallow exceptions so as not to break the stream - */ -const safeWrapAjax = ajaxFn => - memoize((...args) => { - return ajaxFn(...args).catch(error => { - console.log('ERROR: ', error); //eslint-disable-line no-console - return null; - }); - }); - -const safeFindNearbyPlaces = safeWrapAjax(findNearbyPlaces); - -const safeGetPlaceDetails = safeWrapAjax(getPlaceDetails); - const placesSubject = new ReplaySubject(); export function emitter(val?: ?PlaceRecord) { @@ -40,7 +25,7 @@ filter$.subscribe(() => emitter(null)); foodItems$ .mergeMap((foodItems = Map()) => Observable.from(foodItems.toArray())) - .mergeMap(safeGetPlaceDetails) + .mergeMap(getPlaceDetails) .map(buildPlaceRecord) .subscribe(emitter); @@ -48,7 +33,7 @@ location$ .combineLatest(filter$) .debounceTime(200) .mergeMap(([location, filter]: [?Position, FilterRecord]) => { - return safeFindNearbyPlaces({ + return findNearbyPlaces({ location, radius: filter.radius, search: filter.search,