mirror of
https://gitlab.com/wheres-the-tp/ui-mobile.git
synced 2026-01-25 07:24:56 -06:00
places map view
This commit is contained in:
parent
72f210d29b
commit
46d24f9668
9 changed files with 207 additions and 96 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
|
|
|||
16
js/enhancers/mapViewEnhancers.js
Normal file
16
js/enhancers/mapViewEnhancers.js
Normal 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,
|
||||
};
|
||||
})
|
||||
);
|
||||
33
js/enhancers/routeEnhancers.js
Normal file
33
js/enhancers/routeEnhancers.js
Normal 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),
|
||||
};
|
||||
});
|
||||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
30
js/pages/PlacesList.js
Normal 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
72
js/pages/PlacesMap.js
Normal 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);
|
||||
Loading…
Add table
Reference in a new issue