mirror of
https://gitlab.com/wheres-the-tp/ui-mobile.git
synced 2026-01-25 13:14:56 -06:00
fetch place details on mount
This commit is contained in:
parent
1bec0f4565
commit
2d6eb7e787
5 changed files with 230 additions and 228 deletions
|
|
@ -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<GooglePlaceObj> => {
|
||||
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<GooglePlaceObj> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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,
|
||||
};
|
||||
};
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<FoodItemRecord>,
|
||||
currentImage: number,
|
||||
setCurrentImage: (idx: number) => void,
|
||||
viewOnMap: () => void,
|
||||
fetchPlaceDetails: (arg: { distance: number, placeId: string }) => Promise<void>,
|
||||
};
|
||||
|
||||
props: {
|
||||
place: ?PlaceRecord,
|
||||
foodItems: ?List<FoodItemRecord>,
|
||||
};
|
||||
|
||||
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 <View />;
|
||||
}
|
||||
|
||||
const { photos } = place;
|
||||
const { currentImage } = this.state;
|
||||
|
||||
return (
|
||||
<ScrollView style={theme.page.container}>
|
||||
<View style={{ height: 200 }}>
|
||||
{photos.size === 1 && <Image style={stretchedStyle} source={{ uri: photos.first() }} />}
|
||||
{photos.size > 1 && (
|
||||
<Carousel
|
||||
autoplay={false}
|
||||
onAnimateNextPage={this.changeCurrentImage}
|
||||
style={stretchedStyle}>
|
||||
{photos.map(uri => (
|
||||
<Image key={uri} style={{ flex: 1, resizeMode: 'stretch' }} source={{ uri }} />
|
||||
))}
|
||||
</Carousel>
|
||||
)}
|
||||
<CountBadge currentImage={currentImage + 1} totalCount={photos.size} />
|
||||
</View>
|
||||
<View style={{ height: 50, marginBottom: 10, ...contentTileStyle }}>
|
||||
<View style={{ marginTop: 15 }}>
|
||||
<StrongText>{place.address}</StrongText>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexBasis: 75,
|
||||
...contentTileStyle,
|
||||
marginBottom: 10,
|
||||
paddingTop: 5,
|
||||
paddingBottom: 10,
|
||||
}}>
|
||||
<IconButton
|
||||
glyph="stars"
|
||||
text="Add to Faves"
|
||||
onPress={this.addToFaves}
|
||||
color={style.actionIconColor}
|
||||
/>
|
||||
<IconButton
|
||||
glyph="map"
|
||||
text="View on a Map"
|
||||
onPress={this.viewOnMap}
|
||||
color={style.actionIconColor}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 2, ...contentTileStyle, paddingTop: 10 }}>
|
||||
<StrongText>Products</StrongText>
|
||||
{!!foodItems &&
|
||||
foodItems.map(foodItem => (
|
||||
<FoodItemTile key={foodItem.id} foodItem={foodItem} place={place} />
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
const PlaceDetail = ({ place, foodItems, currentImage, setCurrentImage, viewOnMap }: Props) => {
|
||||
if (!place) {
|
||||
return <View />;
|
||||
}
|
||||
}
|
||||
|
||||
const enhance = compose(pure, withPlaceIdFromRoute, withPlace, withFoodItemsForPlace);
|
||||
const { photos } = place;
|
||||
|
||||
export default enhance(PlaceDetail);
|
||||
return (
|
||||
<ScrollView style={theme.page.container}>
|
||||
<View style={{ height: 200, flexGrow: 0 }}>
|
||||
{photos.size === 1 && <Image style={stretchedStyle} source={{ uri: photos.first() }} />}
|
||||
{photos.size > 1 && (
|
||||
<Carousel autoplay={false} onAnimateNextPage={setCurrentImage} style={stretchedStyle}>
|
||||
{photos.map(uri => (
|
||||
<Image key={uri} style={{ flex: 1, resizeMode: 'stretch' }} source={{ uri }} />
|
||||
))}
|
||||
</Carousel>
|
||||
)}
|
||||
<CountBadge currentImage={currentImage + 1} totalCount={photos.size} />
|
||||
</View>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
...contentTileStyle,
|
||||
marginBottom: 10,
|
||||
paddingTop: 5,
|
||||
paddingBottom: 10,
|
||||
}}>
|
||||
<View style={{ marginTop: 15, marginBottom: 20 }}>
|
||||
<StrongText>{place.address}</StrongText>
|
||||
</View>
|
||||
<IconButton
|
||||
glyph="favorite"
|
||||
text="Add to Faves"
|
||||
onPress={() => {}}
|
||||
color={style.actionIconColor}
|
||||
/>
|
||||
<IconButton
|
||||
glyph="map"
|
||||
text="View on a Map"
|
||||
onPress={viewOnMap}
|
||||
color={style.actionIconColor}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ ...contentTileStyle, paddingTop: 10 }}>
|
||||
<StrongText>Products</StrongText>
|
||||
{!!foodItems &&
|
||||
foodItems.map(foodItem => (
|
||||
<FoodItemTile key={foodItem.id} foodItem={foodItem} place={place} />
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue