places map view

This commit is contained in:
Bart Akeley 2017-09-09 13:10:55 -05:00
parent 72f210d29b
commit 46d24f9668
9 changed files with 207 additions and 96 deletions

View file

@ -3,28 +3,45 @@ import { View } from 'react-native';
import React from 'react';
import { pure } from 'recompose';
import { List } from 'immutable';
import FoodItemRecord from '../records/FoodItemRecord';
import typeof PlaceRecord from '../records/PlaceRecord';
import { Link } from 'react-router-native';
import { Thumbnail, StrongText, SubText } from './ItemTile';
import { routeWithTitle } from '../helpers/RouteHelpers';
import { getCategories } from '../helpers/CategoryHelpers';
import { type Map } from 'immutable';
const appendString = (left: string, right: string) => `${left}${right}`;
const getHoursText = (hours: List<string>) => {
if (!hours) {
return 'not listed';
return 'hours not listed';
}
// Note: js Date return 0-Sun/6-Sat where google return 0-Mon/6-Sun
// Immutable.List is smart enough to know that a negative index is counted from the last index
return hours.get(new Date().getDay() - 1);
const today = new Date().getDay() - 1;
return hours.get(today);
};
const getCategoriesText = (foodItems: Map<string, FoodItemRecord>) => {
const categoriesFromFoodItems = getCategories(foodItems);
return categoriesFromFoodItems.interpose(', ').reduce(appendString, '');
};
import theme from '../ui-theme';
type PlaceTileProps = { place: PlaceRecord, distance: number, categories: List<string> };
export default pure(({ place, distance = 999, categories = new List() }: PlaceTileProps) => {
type PlaceTileProps = {
place: PlaceRecord,
foodItems: Map<string, FoodItemRecord>,
};
export default pure(({ place, foodItems }: PlaceTileProps) => {
if (!place) {
return <View />;
}
const distance = foodItems.first().distance || 999;
return (
<Link
to={routeWithTitle(`/place/${place.id || ''}`, place.name)}
@ -33,15 +50,9 @@ export default pure(({ place, distance = 999, categories = new List() }: PlaceTi
<View style={{ height: 70, flexDirection: 'row', backgroundColor: 'white' }}>
<Thumbnail thumb={place.thumb} />
<View style={{ paddingTop: 5 }}>
<StrongText>
{`${place.name} - ${distance} mi`}
</StrongText>
<SubText>
{`${categories.interpose(', ').reduce((str, token) => str + token, '')}`}
</SubText>
<SubText>
{getHoursText(place.hours)}
</SubText>
<StrongText>{`${place.name} - ${distance} mi`}</StrongText>
<SubText>{getCategoriesText(foodItems)}</SubText>
<SubText>{getHoursText(place.hours)}</SubText>
</View>
</View>
</Link>

View file

@ -1,13 +1,11 @@
// @flow
import withProps from 'recompose/withProps';
import compose from 'recompose/compose';
// import mapProps from 'recompose/mapProps';
import { path } from 'ramda';
import mapPropsStream from 'recompose/mapPropsStream';
import FoodItems$ from '../streams/FoodItemsStream';
import typeof FoodItemRecord from '../records/FoodItemRecord';
import { Map } from 'immutable';
import { getCategoryText } from '../helpers/CategoryHelpers';
export const withFoodItems = mapPropsStream(props$ =>
props$.combineLatest(FoodItems$, (props, foodItems) => {
@ -58,21 +56,3 @@ export const withFoodItemsGroupedByPlace = compose(
};
})
);
export const withCategories = withProps((props: { foodItems: Map<string, FoodItemRecord> }) => {
const categories = props.foodItems.toSet().map(foodItem => foodItem.category).map(getCategoryText).toList();
return {
...props,
categories,
};
});
export const withDistance = withProps((props: { foodItems: Map<string, FoodItemRecord> }) => {
const distance = props.foodItems.first().get('distance', 999);
return {
...props,
distance,
};
});

View file

@ -0,0 +1,16 @@
// @flow
import { compose, withState } from 'recompose';
import { path } from 'ramda';
import { withLocation } from './locationEnhancers';
export const withRegionState = compose(
withLocation,
withState('region', 'onRegionChange', ({ location }) => {
return {
latitude: path(['coords', 'latitude'], location),
longitude: path(['coords', 'longitude'], location),
latitudeDelta: 0.07,
longitudeDelta: 0.07,
};
})
);

View file

@ -0,0 +1,33 @@
// @flow
import { getContext, mapProps } from 'recompose';
import { shape, func, string } from 'prop-types';
import { path } from 'ramda';
export const withRouterContext = getContext({
router: shape({
route: shape({
location: shape({
state: shape({
viewMode: string,
}),
}),
}),
history: shape({
push: func.isRequired,
}).isRequired,
}).isRequired,
});
export const withViewMode = mapProps((props: Object) => {
return {
...props,
viewMode: path(['router', 'route', 'location', 'state', 'viewMode'], props),
};
});
export const withPushRoute = mapProps((props: Object) => {
return {
...props,
pushRoute: path(['router', 'history', 'push'], props),
};
});

View file

@ -1,5 +1,11 @@
// @flow
import { type Category, CATEGORY_BEVERAGES, CATEGORY_DESSERTS, CATEGORY_ENTREES } from '../records/FoodItemRecord';
import FoodItemRecord, {
type Category,
CATEGORY_BEVERAGES,
CATEGORY_DESSERTS,
CATEGORY_ENTREES,
} from '../records/FoodItemRecord';
import { type Map } from 'immutable';
export const getCategoryText = (category: Category) => {
switch (category) {
@ -13,3 +19,11 @@ export const getCategoryText = (category: Category) => {
return 'Other';
}
};
export const getCategories = (foodItems: Map<string, FoodItemRecord>) => {
return foodItems
.toSet()
.map(foodItem => foodItem.get('category'))
.map(getCategoryText)
.toList();
};

View file

@ -1,31 +1,13 @@
// @flow
import { shape, func, string } from 'prop-types';
import { compose, branch, getContext, mapProps } from 'recompose';
import { path } from 'ramda';
import { compose, branch } from 'recompose';
import FoodList from './FoodList';
import FoodMap from './FoodMap';
import { withRouterContext, withViewMode, withPushRoute } from '../enhancers/routeEnhancers';
export default compose(
getContext({
router: shape({
route: shape({
location: shape({
state: shape({
viewMode: string,
}),
}),
}),
history: shape({
push: func.isRequired,
}).isRequired,
}).isRequired,
}),
mapProps(({ router }) => {
return {
viewMode: path(['route', 'location', 'state', 'viewMode'], router),
pushRoute: path(['history', 'push'], router),
};
}),
withRouterContext,
withViewMode,
withPushRoute,
branch(({ viewMode }) => {
return viewMode === 'map';
}, FoodMap)

View file

@ -1,41 +1,14 @@
// @flow
import React, { Component } from 'react';
import { View, ScrollView } from 'react-native';
import theme from '../ui-theme';
import PlaceTile from '../components/PlaceTile';
import { compose } from 'recompose';
import { withFoodItemsGroupedByPlace, withCategories, withDistance } from '../enhancers/foodItemEnhancers';
import { withPlace } from '../enhancers/placeEnhancers';
import { type Map } from 'immutable';
import typeof FoodItemRecord from '../records/FoodItemRecord';
import PlacesList from './PlacesList';
import PlacesMap from './PlacesMap';
import { compose, branch } from 'recompose';
import { withRouterContext, withViewMode, withPushRoute } from '../enhancers/routeEnhancers';
const withPlaceProps = compose(withCategories, withDistance, withPlace);
const PlacesList = withFoodItemsGroupedByPlace(
({ foodItemsByPlace }: { foodItemsByPlace: Map<string, Map<string, FoodItemRecord>> }) => {
return (
<View>
{foodItemsByPlace
.map((foodItems: Map<string, FoodItemRecord>, placeId: string) => {
const EnhancedPlaceTile = withPlaceProps(PlaceTile);
return <EnhancedPlaceTile key={placeId} placeId={placeId} foodItems={foodItems} />;
})
.toList()}
</View>
);
}
);
export default class Places extends Component {
static displayName = 'Places';
render() {
return (
<View style={theme.page.container}>
<ScrollView>
<PlacesList />
</ScrollView>
</View>
);
}
}
export default compose(
withRouterContext,
withViewMode,
withPushRoute,
branch(({ viewMode }) => {
return viewMode === 'map';
}, PlacesMap)
)(PlacesList);

30
js/pages/PlacesList.js Normal file
View file

@ -0,0 +1,30 @@
// @flow
import React from 'react';
import { ScrollView } from 'react-native';
import PlaceTile from '../components/PlaceTile';
import { compose } from 'recompose';
import { withFoodItemsGroupedByPlace } from '../enhancers/foodItemEnhancers';
import { withPlaces } from '../enhancers/placeEnhancers';
import { type Map, List } from 'immutable';
import typeof FoodItemRecord from '../records/FoodItemRecord';
import typeof PlaceRecord from '../records/PlaceRecord';
type Props = {
foodItemsByPlace: Map<string, Map<string, FoodItemRecord>>,
places: Map<string, PlaceRecord>,
};
const PlacesList = ({ foodItemsByPlace, places }: Props) => {
return (
<ScrollView>
{places
.map((place: PlaceRecord, placeId: string) => {
const foodItems = foodItemsByPlace.get(placeId, new List());
return <PlaceTile key={placeId} place={place} foodItems={foodItems} />;
})
.toList()}
</ScrollView>
);
};
export default compose(withFoodItemsGroupedByPlace, withPlaces)(PlacesList);

72
js/pages/PlacesMap.js Normal file
View file

@ -0,0 +1,72 @@
// @flow
import React from 'react';
import typeof FoodItemRecord from '../records/FoodItemRecord';
import { compose, renderComponent } from 'recompose';
import { Map } from 'immutable';
import MapView from 'react-native-maps';
import { routeWithTitle } from '../helpers/RouteHelpers';
import { withPlaces } from '../enhancers/placeEnhancers';
import PlaceTile from '../components/PlaceTile';
import { withFoodItemsGroupedByPlace } from '../enhancers/foodItemEnhancers';
import { withRegionState } from '../enhancers/mapViewEnhancers';
import typeof PlaceRecord from '../records/PlaceRecord';
type Region = {
latitude: number,
longitude: number,
latitudeDelta: number,
longitudeDelta: number,
};
type Props = {
places: Map<string, PlaceRecord>,
foodItemsByPlace: Map<string, Map<string, FoodItemRecord>>,
location: Location,
region: Region,
onRegionChange: Region => void,
pushRoute: () => {},
};
// const PlacesMap = ({ places, region, onRegionChange, pushRoute }: Props) => {
const PlacesMap = ({ places, foodItemsByPlace, region, onRegionChange, pushRoute }: Props) => {
return (
<MapView
initialRegion={region}
region={region}
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}
onRegionChange={onRegionChange}
>
{places
.map((place: PlaceRecord, placeId: string) => {
const foodItems = foodItemsByPlace.get(placeId, new Map());
const firstFoodItem = foodItems.first();
if (!firstFoodItem) {
return;
}
return (
<MapView.Marker
key={placeId}
title={place.name}
coordinate={{
latitude: firstFoodItem.latitude,
longitude: firstFoodItem.longitude,
}}
>
<MapView.Callout
onPress={() => {
pushRoute(routeWithTitle(`/place/${placeId || ''}`, place.name));
}}
>
<PlaceTile place={place} foodItems={foodItems} />
</MapView.Callout>
</MapView.Marker>
);
})
.toList()}
</MapView>
);
};
export default compose(renderComponent, withFoodItemsGroupedByPlace, withPlaces, withRegionState)(PlacesMap);