mirror of
https://gitlab.com/wheres-the-tp/ui-mobile.git
synced 2026-01-25 05:54:56 -06:00
filter state management bits
This commit is contained in:
parent
ba8eb7c1e3
commit
e15c5b4df6
24 changed files with 305 additions and 170 deletions
|
|
@ -1,2 +0,0 @@
|
|||
printWidth: 100;
|
||||
parser: flow;
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
// @flow
|
||||
import { memoize } from 'ramda';
|
||||
import FilterRecord from '../records/FilterRecord';
|
||||
|
||||
const BASE_URL = 'aretherecookies.herokuapp.com';
|
||||
// const BASE_URL = '192.168.1.6:3000';
|
||||
|
||||
export type FoodItemsFilter = {
|
||||
radius?: number,
|
||||
|
|
@ -25,38 +27,42 @@ export type FoodItemsForLocation = {
|
|||
fooditems: Array<RawFoodItem>,
|
||||
};
|
||||
|
||||
export const getFoodItemsForLocation = memoize(
|
||||
async ({ pos: { coords: { latitude, longitude } } }: { pos: Position }): Promise<FoodItemsForLocation> => {
|
||||
try {
|
||||
return fetch(`https://${BASE_URL}/fooditems`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
export const getFoodItems = memoize(async ({ loc, filter }: { loc: Position, filter: FilterRecord }): Promise<
|
||||
FoodItemsForLocation
|
||||
> => {
|
||||
const { coords: { latitude: lat, longitude: lng } } = loc;
|
||||
const { orderby, categories, radius } = filter;
|
||||
|
||||
try {
|
||||
return fetch(`http://${BASE_URL}/fooditems`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
lat,
|
||||
lng,
|
||||
orderby,
|
||||
filter: {
|
||||
...(categories ? { categories } : {}),
|
||||
radius,
|
||||
},
|
||||
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: [],
|
||||
}),
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(json => ({
|
||||
...json,
|
||||
loading: false,
|
||||
error: error,
|
||||
};
|
||||
}
|
||||
error: null,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
return {
|
||||
orderby: 'distance',
|
||||
filter: {},
|
||||
fooditems: [],
|
||||
loading: false,
|
||||
error: error,
|
||||
};
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { getCategoryText } from '../helpers/CategoryHelpers';
|
|||
import { Picker, View } from 'react-native';
|
||||
import theme from '../ui-theme';
|
||||
import { pure } from 'recompose';
|
||||
import { type Category, CATEGORIES } from '../records/FoodItemRecord';
|
||||
import { type Category, CATEGORIES } from '../constants/CategoryConstants';
|
||||
import debounce from '../helpers/debounce';
|
||||
|
||||
const { picker: { color: selectedColor } } = theme;
|
||||
|
|
@ -13,16 +13,17 @@ const defaultColor = 'black';
|
|||
const getItemColor = (selected, current) => (selected === current ? selectedColor : defaultColor);
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
const renderItem = (selected: Category) => (item: Category) =>
|
||||
<Picker.Item key={item} label={getCategoryText(item)} value={item} color={getItemColor(selected, item)} />;
|
||||
const renderItem = (selected: Category) => (item: Category) => (
|
||||
<Picker.Item key={item} label={getCategoryText(item)} value={item} color={getItemColor(selected, item)} />
|
||||
);
|
||||
|
||||
type categoryPickerProps = { selected: Category, onValueChange: Function, style: Object };
|
||||
const categoryPicker = pure(({ selected, onValueChange, style }: categoryPickerProps) =>
|
||||
const categoryPicker = pure(({ selected, onValueChange, style }: categoryPickerProps) => (
|
||||
<View style={style}>
|
||||
<Picker selectedValue={selected} onValueChange={debounce(onValueChange)} style={{ flex: 1 }}>
|
||||
{CATEGORIES.map(renderItem(selected))}
|
||||
</Picker>
|
||||
</View>
|
||||
);
|
||||
));
|
||||
|
||||
export default categoryPicker;
|
||||
|
|
|
|||
49
js/components/FilterModal.js
Normal file
49
js/components/FilterModal.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { View, TouchableOpacity, Text } from 'react-native';
|
||||
import CheckBox from 'react-native-checkbox';
|
||||
import Modal from './Modal';
|
||||
// import { Icon } from 'react-native-material-ui';
|
||||
import { withFilter } from '../enhancers/filterEnhancers';
|
||||
import typeof FilterRecord from '../records/FilterRecord';
|
||||
import { CATEGORIES, type Category } from '../constants/CategoryConstants';
|
||||
import { getCategoryText } from '../helpers/CategoryHelpers';
|
||||
// import { Set } from 'immutable';
|
||||
|
||||
type Props = {
|
||||
isVisible: boolean,
|
||||
onClose: () => void,
|
||||
filter: FilterRecord,
|
||||
setFilter: (f: FilterRecord) => void,
|
||||
};
|
||||
|
||||
const FilterModal = withFilter(({ isVisible, onClose, filter, setFilter }: Props) => {
|
||||
const { orderby, categories, radius } = filter;
|
||||
|
||||
const toggleCategory = category => checked => {
|
||||
setFilter(
|
||||
filter.update('categories', categories => {
|
||||
return checked ? categories.delete(category) : categories.add(category);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isVisible={isVisible}>
|
||||
<TouchableOpacity onPress={onClose}>
|
||||
<Text style={{ fontSize: 30, fontWeight: 'bold' }}>Filters</Text>
|
||||
{CATEGORIES.map((category: Category) => (
|
||||
<CheckBox
|
||||
key={category}
|
||||
style={{ margin: 20 }}
|
||||
checked={categories.has(category)}
|
||||
onChange={toggleCategory(category)}
|
||||
label={getCategoryText(category)}
|
||||
/>
|
||||
))}
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default FilterModal;
|
||||
|
|
@ -43,11 +43,7 @@ class FoodItemList extends Component {
|
|||
const { filter, foodItemsSeq, renderFoodItem, limit } = this.props;
|
||||
const getItems = R.compose(intoArray, limitBy(limit), sortByDistance, filterBy(filter));
|
||||
const items = getItems(foodItemsSeq);
|
||||
return (
|
||||
<View style={{ flexShrink: 2 }}>
|
||||
{items.map(renderFoodItem)}
|
||||
</View>
|
||||
);
|
||||
return <View style={{ flexShrink: 2 }}>{items.map(renderFoodItem)}</View>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,31 +6,31 @@ import { pure } from 'recompose';
|
|||
import theme from '../ui-theme';
|
||||
import { Icon } from 'react-native-material-ui';
|
||||
|
||||
export default pure(({ children, isVisible }) =>
|
||||
<Modal isVisible={isVisible}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'white',
|
||||
padding: 4,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'stretch',
|
||||
borderRadius: 4,
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
export default pure(({ children, isVisible }) => {
|
||||
return (
|
||||
<Modal isVisible={isVisible} backdropColor="black" backdropOpacity={1.0}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'column',
|
||||
backgroundColor: 'white',
|
||||
padding: 4,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'stretch',
|
||||
borderRadius: 4,
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export const TextButton = ({ text, onPress, style = {} }: { text: string, onPress: () => void, style: Object }) => {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
<View style={{ flexShrink: 0, ...style }}>
|
||||
<Text style={{ color: theme.palette.accentColor, padding: 12, fontSize: 17 }}>
|
||||
{text}
|
||||
</Text>
|
||||
<Text style={{ color: theme.palette.accentColor, padding: 12, fontSize: 17 }}>{text}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ type PlaceTileProps = {
|
|||
};
|
||||
|
||||
export default pure(({ place, foodItems }: PlaceTileProps) => {
|
||||
if (!place) {
|
||||
if (!place || foodItems.size === 0) {
|
||||
return <View />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { getQuantityText } from '../helpers/QuantityHelpers';
|
|||
import { Picker, View } from 'react-native';
|
||||
import theme from '../ui-theme';
|
||||
import { pure } from 'recompose';
|
||||
import { type Quantity, QUANTITIES } from '../records/FoodItemRecord';
|
||||
import { type Quantity, QUANTITIES } from '../constants/QuantityConstants';
|
||||
import debounce from '../helpers/debounce';
|
||||
|
||||
const { picker: { color: selectedColor } } = theme;
|
||||
|
|
@ -13,21 +13,22 @@ const defaultColor = 'black';
|
|||
const getItemColor = (selected, current) => (selected === current ? selectedColor : defaultColor);
|
||||
|
||||
/* eslint-disable react/display-name */
|
||||
const renderQuantityItem = (selectedQuantity: Quantity) => (quantity: Quantity) =>
|
||||
const renderQuantityItem = (selectedQuantity: Quantity) => (quantity: Quantity) => (
|
||||
<Picker.Item
|
||||
key={quantity}
|
||||
label={getQuantityText(quantity)}
|
||||
value={quantity}
|
||||
color={getItemColor(selectedQuantity, quantity)}
|
||||
/>;
|
||||
/>
|
||||
);
|
||||
|
||||
type QuantityPickerProps = { quantity: Quantity, onValueChange: Function, style: Object };
|
||||
const QuantityPicker = pure(({ quantity, onValueChange, style }: QuantityPickerProps) =>
|
||||
const QuantityPicker = pure(({ quantity, onValueChange, style }: QuantityPickerProps) => (
|
||||
<View style={style}>
|
||||
<Picker selectedValue={quantity} onValueChange={debounce(onValueChange)} style={{ flex: 1 }}>
|
||||
{QUANTITIES.map(renderQuantityItem(quantity))}
|
||||
</Picker>
|
||||
</View>
|
||||
);
|
||||
));
|
||||
|
||||
export default QuantityPicker;
|
||||
|
|
|
|||
11
js/constants/CategoryConstants.js
Normal file
11
js/constants/CategoryConstants.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// @flow
|
||||
export const CATEGORY_BEVERAGES: 'beverages' = 'beverages';
|
||||
export const CATEGORY_DESSERTS: 'desserts' = 'desserts';
|
||||
export const CATEGORY_ENTREES: 'entrees' = 'entrees';
|
||||
export const CATEGORY_OTHER: 'other' = 'other';
|
||||
export type Category =
|
||||
| typeof CATEGORY_BEVERAGES
|
||||
| typeof CATEGORY_DESSERTS
|
||||
| typeof CATEGORY_ENTREES
|
||||
| typeof CATEGORY_OTHER;
|
||||
export const CATEGORIES = [CATEGORY_BEVERAGES, CATEGORY_DESSERTS, CATEGORY_ENTREES, CATEGORY_OTHER];
|
||||
7
js/constants/QuantityConstants.js
Normal file
7
js/constants/QuantityConstants.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
// @flow
|
||||
export const QUANTITY_NONE: 'none' = 'none';
|
||||
export const QUANTITY_FEW: 'few' = 'few';
|
||||
export const QUANTITY_MANY: 'many' = 'many';
|
||||
export const QUANTITY_LOTS: 'lots' = 'lots';
|
||||
export type Quantity = typeof QUANTITY_NONE | typeof QUANTITY_FEW | typeof QUANTITY_MANY | typeof QUANTITY_LOTS;
|
||||
export const QUANTITIES = [QUANTITY_NONE, QUANTITY_FEW, QUANTITY_LOTS, QUANTITY_MANY];
|
||||
13
js/enhancers/filterEnhancers.js
Normal file
13
js/enhancers/filterEnhancers.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// @flow
|
||||
import mapPropsStream from 'recompose/mapPropsStream';
|
||||
import Filter$, { emitter } from '../streams/FilterStream';
|
||||
|
||||
export const withFilter = mapPropsStream(props$ => {
|
||||
return props$.combineLatest(Filter$, (props, filter) => {
|
||||
return {
|
||||
...props,
|
||||
filter,
|
||||
setFilter: emitter,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
@ -1,10 +1,6 @@
|
|||
// @flow
|
||||
import FoodItemRecord, {
|
||||
type Category,
|
||||
CATEGORY_BEVERAGES,
|
||||
CATEGORY_DESSERTS,
|
||||
CATEGORY_ENTREES,
|
||||
} from '../records/FoodItemRecord';
|
||||
import FoodItemRecord from '../records/FoodItemRecord';
|
||||
import { type Category, CATEGORY_BEVERAGES, CATEGORY_DESSERTS, CATEGORY_ENTREES } from '../constants/CategoryConstants';
|
||||
import { type Map } from 'immutable';
|
||||
|
||||
export const getCategoryText = (category: Category) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { type Quantity } from '../records/FoodItemRecord';
|
||||
import { type Quantity } from '../constants/QuantityConstants';
|
||||
|
||||
export const getQuantityText = (quantity: Quantity) => {
|
||||
switch (quantity) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { compose, branch } from 'recompose';
|
||||
import { compose, branch, withState } from 'recompose';
|
||||
import FoodList from './FoodList';
|
||||
import FoodMap from './FoodMap';
|
||||
import { withRouterContext, withViewMode, withPushRoute } from '../enhancers/routeEnhancers';
|
||||
|
|
@ -8,6 +8,7 @@ export default compose(
|
|||
withRouterContext,
|
||||
withViewMode,
|
||||
withPushRoute,
|
||||
withState('isFilterModalOpen', 'toggleFilterModal', false),
|
||||
branch(({ viewMode }) => {
|
||||
return viewMode === 'map';
|
||||
}, FoodMap)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import React, { Component } from 'react';
|
|||
import { Image, View } from 'react-native';
|
||||
import theme from '../ui-theme';
|
||||
import { StrongText, SubText } from '../components/ItemTile';
|
||||
import { type FoodItem, type Quantity, QUANTITY_MANY } from '../records/FoodItemRecord';
|
||||
import { type FoodItem } from '../records/FoodItemRecord';
|
||||
import { type Quantity, QUANTITY_MANY } from '../constants/QuantityConstants';
|
||||
import typeof PlaceRecord from '../records/PlaceRecord';
|
||||
import { compose, pure } from 'recompose';
|
||||
import IconButton from '../components/IconButton';
|
||||
|
|
@ -70,14 +71,16 @@ export class FoodItemDetail extends Component {
|
|||
return (
|
||||
<View style={{ ...theme.page.container }}>
|
||||
<View style={{ flex: 3 }}>
|
||||
{viewableImages.size === 1 &&
|
||||
<Image style={stretchedStyle} source={{ uri: viewableImages.get(0) }} />}
|
||||
{viewableImages.size > 1 &&
|
||||
{viewableImages.size === 1 && (
|
||||
<Image style={stretchedStyle} source={{ uri: viewableImages.get(0) }} />
|
||||
)}
|
||||
{viewableImages.size > 1 && (
|
||||
<Carousel autoplay={false} onAnimateNextPage={this.changeCurrentImage} style={stretchedStyle}>
|
||||
{viewableImages.map(uri =>
|
||||
{viewableImages.map(uri => (
|
||||
<Image key={uri} style={{ flex: 1, resizeMode: 'stretch' }} source={{ uri }} />
|
||||
)}
|
||||
</Carousel>}
|
||||
))}
|
||||
</Carousel>
|
||||
)}
|
||||
<CountBadge currentImage={this.state.currentImage + 1} totalCount={viewableImages.size} />
|
||||
</View>
|
||||
<View style={{ flex: 1, marginBottom: 10, ...contentTileStyle }}>
|
||||
|
|
@ -86,12 +89,8 @@ export class FoodItemDetail extends Component {
|
|||
underlayColor={theme.itemTile.pressHighlightColor}
|
||||
>
|
||||
<View style={{ marginTop: 15 }}>
|
||||
<StrongText>
|
||||
{place.name}
|
||||
</StrongText>
|
||||
<SubText>
|
||||
{place.address}
|
||||
</SubText>
|
||||
<StrongText>{place.name}</StrongText>
|
||||
<SubText>{place.address}</SubText>
|
||||
</View>
|
||||
</Link>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { ActionButton } from 'react-native-material-ui';
|
|||
import { routeWithTitle } from '../helpers/RouteHelpers';
|
||||
import FoodItemList from '../components/FoodItemList';
|
||||
import typeof FoodItemRecord from '../records/FoodItemRecord';
|
||||
import FilterModal from '../components/FilterModal';
|
||||
|
||||
import theme from '../ui-theme';
|
||||
|
||||
|
|
@ -16,6 +17,8 @@ export default class FoodList extends Component {
|
|||
|
||||
props: {
|
||||
pushRoute: Object => void,
|
||||
isFilterModalOpen: boolean,
|
||||
toggleFilterModal: (val: boolean) => void,
|
||||
};
|
||||
|
||||
addFoodItem = () => {
|
||||
|
|
@ -23,13 +26,40 @@ export default class FoodList extends Component {
|
|||
this.props.pushRoute(newRoute);
|
||||
};
|
||||
|
||||
toggleFilterModal = () => {
|
||||
const { isFilterModalOpen, toggleFilterModal } = this.props;
|
||||
toggleFilterModal(!isFilterModalOpen);
|
||||
};
|
||||
|
||||
onActionPressed = (action: string) => {
|
||||
switch (action) {
|
||||
case 'add':
|
||||
this.addFoodItem();
|
||||
break;
|
||||
case 'filter':
|
||||
this.toggleFilterModal();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isFilterModalOpen } = this.props;
|
||||
|
||||
return (
|
||||
<View style={theme.page.container}>
|
||||
<ScrollView>
|
||||
<FoodItemList renderFoodItem={renderFoodItem} />
|
||||
</ScrollView>
|
||||
<ActionButton icon="add" onPress={this.addFoodItem} />
|
||||
<FilterModal isVisible={isFilterModalOpen} onClose={this.toggleFilterModal} />
|
||||
<ActionButton
|
||||
actions={[
|
||||
{ icon: 'add', label: 'Add New', name: 'add' },
|
||||
{ icon: 'filter-list', label: 'Filter', name: 'filter' },
|
||||
]}
|
||||
icon="add"
|
||||
transition="speedDial"
|
||||
onPress={this.onActionPressed}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
12
js/records/FilterRecord.js
Normal file
12
js/records/FilterRecord.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// @flow
|
||||
import { Record } from 'immutable';
|
||||
import { CATEGORIES } from '../constants/CategoryConstants';
|
||||
import { Set } from 'immutable';
|
||||
|
||||
const FilterRecord = Record({
|
||||
orderby: 'distance',
|
||||
categories: Set(CATEGORIES),
|
||||
radius: 20,
|
||||
});
|
||||
|
||||
export default FilterRecord;
|
||||
|
|
@ -1,24 +1,8 @@
|
|||
//@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';
|
||||
export const QUANTITY_MANY: 'many' = 'many';
|
||||
export const QUANTITY_LOTS: 'lots' = 'lots';
|
||||
export type Quantity = typeof QUANTITY_NONE | typeof QUANTITY_FEW | typeof QUANTITY_MANY | typeof QUANTITY_LOTS;
|
||||
export const QUANTITIES = [QUANTITY_NONE, QUANTITY_FEW, QUANTITY_LOTS, QUANTITY_MANY];
|
||||
|
||||
export const CATEGORY_BEVERAGES: 'beverages' = 'beverages';
|
||||
export const CATEGORY_DESSERTS: 'desserts' = 'desserts';
|
||||
export const CATEGORY_ENTREES: 'entrees' = 'entrees';
|
||||
export const CATEGORY_OTHER: 'other' = 'other';
|
||||
export type Category =
|
||||
| typeof CATEGORY_BEVERAGES
|
||||
| typeof CATEGORY_DESSERTS
|
||||
| typeof CATEGORY_ENTREES
|
||||
| typeof CATEGORY_OTHER;
|
||||
export const CATEGORIES = [CATEGORY_BEVERAGES, CATEGORY_DESSERTS, CATEGORY_ENTREES, CATEGORY_OTHER];
|
||||
import { type Category, CATEGORY_DESSERTS } from '../constants/CategoryConstants';
|
||||
import { type Quantity, QUANTITY_MANY } from '../constants/QuantityConstants';
|
||||
|
||||
export type FoodItem = {
|
||||
id: ?string,
|
||||
|
|
|
|||
13
js/streams/FilterStream.js
Normal file
13
js/streams/FilterStream.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// @flow
|
||||
import { Subject, Observable, ReplaySubject } from 'rxjs';
|
||||
import FilterRecord from '../records/FilterRecord';
|
||||
|
||||
const multicaster = new ReplaySubject();
|
||||
|
||||
export const emitter = val => multicaster.next(val);
|
||||
|
||||
// Observable.from([new FilterRecord()]).subscribe(multicaster);
|
||||
|
||||
emitter(new FilterRecord());
|
||||
|
||||
export default multicaster;
|
||||
|
|
@ -3,10 +3,13 @@ import { createFoodItem } from '../records/FoodItemRecord';
|
|||
import { setById } from '../helpers/ImmutableHelpers';
|
||||
import { Map } from 'immutable';
|
||||
import location$ from './LocationStream';
|
||||
import { getFoodItemsForLocation, type FoodItemsForLocation } from '../apis/FoodItemsApi';
|
||||
import { getFoodItems, type FoodItemsForLocation } from '../apis/FoodItemsApi';
|
||||
import FilterSubject from './FilterStream';
|
||||
import FilterRecord from '../records/FilterRecord';
|
||||
|
||||
export default location$
|
||||
.mergeMap(pos => getFoodItemsForLocation({ pos }))
|
||||
.combineLatest(FilterSubject)
|
||||
.mergeMap(([loc, filter]: [Position, FilterRecord]) => getFoodItems({ loc, filter }))
|
||||
.map(({ fooditems = [] }: FoodItemsForLocation) => {
|
||||
return fooditems.map(createFoodItem).reduce(setById, new Map());
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { getPlaceDetails } from '../apis/PlaceDetailsApi';
|
|||
import { memoize } from 'ramda';
|
||||
import { Observable } from 'rxjs';
|
||||
import typeof FoodItemRecord from '../records/FoodItemRecord';
|
||||
import PlaceRecord, { type GooglePlaceObj } from '../records/PlaceRecord';
|
||||
import { type GooglePlaceObj } from '../records/PlaceRecord';
|
||||
import { setById } from '../helpers/ImmutableHelpers';
|
||||
|
||||
/**
|
||||
|
|
|
|||
91
package.json
91
package.json
|
|
@ -1,45 +1,50 @@
|
|||
{
|
||||
"name": "AreThereCookies",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node node_modules/react-native/local-cli/cli.js start",
|
||||
"test": "jest",
|
||||
"lint": "flow && eslint js",
|
||||
"android": "react-native run-android"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-preset-es2015": "^6.24.0",
|
||||
"immutable": "^3.8.1",
|
||||
"ramda": "^0.24.1",
|
||||
"react": "~15.4.1",
|
||||
"react-native": "~0.42.0",
|
||||
"react-native-drawer": "^2.3.0",
|
||||
"react-native-google-places": "^2.1.0",
|
||||
"react-native-image-picker": "^0.26.3",
|
||||
"react-native-looped-carousel": "^0.1.5",
|
||||
"react-native-maps": "0.15.1",
|
||||
"react-native-material-ui": "^1.7.0",
|
||||
"react-native-modal": "^2.2.0",
|
||||
"react-native-scrollable-tab-view": "^0.7.4",
|
||||
"react-native-vector-icons": "^4.0.0",
|
||||
"react-router-native": "^4.0.0",
|
||||
"recompose": "^0.23.4",
|
||||
"rxjs": "^5.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-jest": "18.0.0",
|
||||
"babel-preset-react-native": "1.9.1",
|
||||
"eslint": "^3.14.1",
|
||||
"eslint-plugin-react": "^6.9.0",
|
||||
"eslint-plugin-react-native": "^2.2.1",
|
||||
"flow-bin": "0.38",
|
||||
"jest": "18.1.0",
|
||||
"jshint": "^2.9.4",
|
||||
"react-test-renderer": "15.4.2"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "react-native"
|
||||
}
|
||||
"name": "aretherecookies",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node node_modules/react-native/local-cli/cli.js start",
|
||||
"test": "jest",
|
||||
"lint": "flow && eslint js",
|
||||
"android": "react-native run-android"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-preset-es2015": "^6.24.0",
|
||||
"immutable": "^3.8.1",
|
||||
"ramda": "^0.24.1",
|
||||
"react": "~15.4.1",
|
||||
"react-native": "~0.42.0",
|
||||
"react-native-checkbox": "^2.0.0",
|
||||
"react-native-drawer": "^2.3.0",
|
||||
"react-native-google-places": "^2.1.0",
|
||||
"react-native-image-picker": "^0.26.3",
|
||||
"react-native-looped-carousel": "^0.1.5",
|
||||
"react-native-maps": "0.15.1",
|
||||
"react-native-material-ui": "^1.7.0",
|
||||
"react-native-modal": "^2.2.0",
|
||||
"react-native-scrollable-tab-view": "^0.7.4",
|
||||
"react-native-vector-icons": "^4.0.0",
|
||||
"react-router-native": "^4.0.0",
|
||||
"recompose": "^0.23.4",
|
||||
"rxjs": "^5.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^7.1.1",
|
||||
"babel-jest": "18.0.0",
|
||||
"babel-preset-react-native": "1.9.1",
|
||||
"eslint": "^3.14.1",
|
||||
"eslint-plugin-react": "^6.9.0",
|
||||
"eslint-plugin-react-native": "^2.2.1",
|
||||
"flow-bin": "0.38",
|
||||
"jest": "18.1.0",
|
||||
"jshint": "^2.9.4",
|
||||
"react-test-renderer": "15.4.2"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "react-native"
|
||||
},
|
||||
"prettier": {
|
||||
"lineWidth": 100,
|
||||
"parser": "flow"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
SELECT
|
||||
*,
|
||||
ST_AsGeoJSON(loc) as location,
|
||||
ST_Distance(
|
||||
loc,
|
||||
ST_GeogFromText('SRID=4326;POINT(-97.7286718 30.3033267)')
|
||||
) / 1609 as distance
|
||||
FROM food_items
|
||||
WHERE
|
||||
ST_DWithin(
|
||||
loc,
|
||||
ST_GeogFromText('SRID=4326;POINT(-97.7286718 30.3033267)'),
|
||||
10 * 1609
|
||||
)
|
||||
f.id AS id,
|
||||
f.name AS name,
|
||||
f.place_id AS place_id,
|
||||
f.category AS category,
|
||||
f.images AS images,
|
||||
f.thumbImage AS thumbImage,
|
||||
ST_AsGeoJSON(f.loc) AS location,
|
||||
ST_Distance(f.loc, ST_SetSRID(ST_Point(-97.7286718, 30.3033267),4326)::geography) / 1609 AS distance,
|
||||
q.quantity AS quantity,
|
||||
q.date AS lastUpdated
|
||||
FROM food_items f
|
||||
LEFT OUTER JOIN (
|
||||
SELECT food_item_id, quantity, MAX(date) AS date FROM quantities GROUP BY food_item_id, quantity
|
||||
) q
|
||||
ON f.id = q.food_item_id
|
||||
WHERE ST_DWithin(loc, ST_SetSRID(ST_Point(-97.7286718, 30.3033267),4326)::geography, 20 * 1609)
|
||||
AND f.category IN ('desserts', 'beverages', 'entrees', 'other')
|
||||
ORDER BY distance ASC;
|
||||
|
|
|
|||
|
|
@ -3479,7 +3479,7 @@ promise@^7.1.1:
|
|||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
prop-types@15.5.10, prop-types@^15.5.4, prop-types@^15.5.8:
|
||||
prop-types@15.5.10, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8:
|
||||
version "15.5.10"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"
|
||||
dependencies:
|
||||
|
|
@ -3545,6 +3545,12 @@ react-native-animatable@^1.2.0:
|
|||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-animatable/-/react-native-animatable-1.2.0.tgz#fd279c6ee4b49161c6cc3b951ed7765b35a73467"
|
||||
|
||||
react-native-checkbox@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-checkbox/-/react-native-checkbox-2.0.0.tgz#453bbfd2e055a21e69ebe7842414a055d50ff449"
|
||||
dependencies:
|
||||
prop-types "^15.5.10"
|
||||
|
||||
react-native-drawer@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-drawer/-/react-native-drawer-2.3.0.tgz#a0369ec80ff0b61c9f152dbdea91fe76c843113a"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue