rename food to product

This commit is contained in:
Bart Akeley 2020-04-17 22:50:23 -05:00
parent a11150e960
commit 96e24881fc
46 changed files with 622 additions and 646 deletions

View file

@ -8,9 +8,9 @@ import { StatusBar } from 'react-native';
import { ThemeContext, getTheme } from 'react-native-material-ui';
import { NativeRouter, Route, Redirect, AndroidBackButton, Switch } from 'react-router-native';
import Nav from './pages/Nav';
import FoodItemDetail from './pages/FoodItemDetail';
import ProductDetail from './pages/ProductDetail';
import PlaceDetail from './pages/PlaceDetail';
import CreateFoodItem from './pages/CreateFoodItem';
import CreateProduct from './pages/CreateProduct';
import LoginPage from './pages/LoginPage';
import LandingPage from './pages/LandingPage';
import ZipcodePage from './pages/ZipcodePage';
@ -34,12 +34,12 @@ export default class App extends Component {
<Switch>
<Route path="/landing" component={LandingPage} />
<Route path="/list/:type" component={Nav} />
<Route path="/foodItem/:id" component={FoodItemDetail} />
<Route path="/product/:id" component={ProductDetail} />
<Route path="/place/:id" component={PlaceDetail} />
<Route path="/login" component={LoginPage} />
<Route path="/logout" component={LoginPage} />
<Route path="/zipcode" component={ZipcodePage} />
<Route path="/createFoodItem" component={CreateFoodItem} />
<Route path="/createProduct" component={CreateProduct} />
</Switch>
</AppContainer>
</ThemeContext.Provider>

View file

@ -21,24 +21,24 @@ export const fetchFaves = withLoggedInEmail(async email => {
});
});
export const putFaves = withLoggedInEmail(async (email, foodItemIds) => {
export const putFaves = withLoggedInEmail(async (email, productIds) => {
return fetchRequest({
endpoint: '/fave',
method: 'PUT',
body: {
email,
foodItemIds,
productIds,
},
});
});
export const deleteFaves = withLoggedInEmail(async (email, foodItemIds) => {
export const deleteFaves = withLoggedInEmail(async (email, productIds) => {
return fetchRequest({
endpoint: '/fave',
method: 'DELETE',
body: {
email,
foodItemIds,
productIds,
},
});
});

View file

@ -2,19 +2,19 @@
import type { ImageRaw } from '../records/ImageRecord';
import { fetchRequest, fetchRequestBinary } from './FetchApi';
export const getImages = (foodItemId: string): Promise<Array<ImageRaw>> => {
export const getImages = (productId: string): Promise<Array<ImageRaw>> => {
return fetchRequest({
endpoint: `/images/${foodItemId}`,
endpoint: `/images/${productId}`,
method: 'GET',
});
};
export const addImage = async ({
foodItemId,
productId,
username,
imageUri,
}: {
foodItemId: string,
productId: string,
username: string,
imageUri: string,
}) => {
@ -30,7 +30,7 @@ export const addImage = async ({
body.append('username', username);
const res = await fetchRequestBinary({
endpoint: `/images/${foodItemId}`,
endpoint: `/images/${productId}`,
body,
});

View file

@ -1,16 +1,15 @@
import { memoizeWith, pathOr, map, nth } from 'ramda';
import FilterRecord from '../records/FilterRecord';
import FoodItemRecord from '../records/FoodItemRecord';
import { pathOr, map, nth } from 'ramda';
import ProductRecord from '../records/ProductRecord';
import AuthManager from '../AuthManager';
import { addImage } from './ImagesApi';
import { fetchRequest } from './FetchApi';
import debounce from '../helpers/debounce';
export type FoodItemsFilter = {
export type ProductsFilter = {
radius?: number,
};
export type RawFoodItem = {
export type RawProduct = {
id: string,
name: string,
placeid: string,
@ -23,30 +22,30 @@ export type RawFoodItem = {
lastupdated: number,
};
export type FoodItemsForLocation = {
export type ProductsForLocation = {
orderby: string,
filter: FoodItemsFilter,
fooditems: ?Array<RawFoodItem>,
filter: ProductsFilter,
products: ?Array<RawProduct>,
};
export const getFoodItems = debounce(async ({ filter }) => {
export const getProducts = debounce(async ({ filter }) => {
const { search } = filter;
if (!search) {
return {
fooditems: [],
products: [],
loading: false,
error: null,
};
}
try {
const { fooditems } = await fetchRequest({
endpoint: `/fooditems/${encodeURIComponent(search)}`,
const { products } = await fetchRequest({
endpoint: `/products/${encodeURIComponent(search)}`,
});
return {
fooditems,
products,
loading: false,
error: null,
};
@ -55,14 +54,14 @@ export const getFoodItems = debounce(async ({ filter }) => {
return {
orderby: 'distance',
filter: {},
fooditems: [],
products: [],
loading: false,
error: error,
};
}
}, 500);
export const createFoodItem = async (foodItem: FoodItemRecord) => {
export const createProduct = async (product: ProductRecord) => {
if (!AuthManager.user) {
throw new Error('You must be logged in to create food items');
}
@ -70,14 +69,14 @@ export const createFoodItem = async (foodItem: FoodItemRecord) => {
const username = AuthManager.user.name;
const res = await fetchRequest({
endpoint: '/addfooditem',
endpoint: '/addproduct',
method: 'POST',
body: foodItem,
body: product,
});
const addImageUri = (imageUri: string) => addImage({ foodItemId: res.id, imageUri, username });
const addImageUri = (imageUri: string) => addImage({ productId: res.id, imageUri, username });
const images = await Promise.all(map(addImageUri, foodItem.images.toArray()));
const images = await Promise.all(map(addImageUri, product.images.toArray()));
const thumbimage = pathOr('', ['url'], nth(0, images));

View file

@ -4,17 +4,17 @@ import type { QuantityResponse } from '../constants/QuantityConstants';
import { fetchRequest } from './FetchApi';
export const setQuantity = ({
foodItemId,
productId,
quantity,
}: {
foodItemId: string,
productId: string,
quantity: Quantity,
}): Promise<QuantityResponse> => {
return fetchRequest({
method: 'POST',
endpoint: '/quantity',
body: {
foodItemId,
productId,
quantity,
},
});

View file

@ -1,56 +0,0 @@
import React from 'react';
import { View } from 'react-native';
import { pure } from 'recompose';
import FoodItemRecord from '../records/FoodItemRecord';
import typeof PlaceRecord from '../records/PlaceRecord';
import theme from '../ui-theme';
import { Link } from 'react-router-native';
import { routeWithTitle } from '../helpers/RouteHelpers';
import { TileBox, StrongText, SubText, Thumbnail, QuantityLine } from './ItemTile';
import { withPlace } from '../enhancers/placeEnhancers';
const PlaceNameAndDistance = withPlace(
({ place, distance = 999.9 }: { place: ?PlaceRecord, distance: number }) => {
return (
<SubText>{`${(place && place.name) || 'Loading...'} - ${parseFloat(distance).toFixed(
1
)} mi`}</SubText>
);
}
);
const EmptyFoodItemTile = () => {
return (
<View>
<TileBox>
<Thumbnail thumb={null} />
<View>
<StrongText>Loading...</StrongText>
</View>
</TileBox>
</View>
);
};
export default pure(({ foodItem }: { foodItem: FoodItemRecord }) => {
if (!foodItem) {
return <EmptyFoodItemTile />;
}
return (
<Link
to={routeWithTitle(`/foodItem/${foodItem.id || ''}`, foodItem.name)}
underlayColor={theme.itemTile.pressHighlightColor}>
<View>
<TileBox>
<Thumbnail thumb={foodItem.thumbImage} />
<View>
<StrongText>{foodItem.name || ''}</StrongText>
<PlaceNameAndDistance placeId={foodItem.placeId} distance={foodItem.distance} />
<QuantityLine quantity={foodItem.quantity} lastupdated={foodItem.lastupdated} />
</View>
</TileBox>
</View>
</Link>
);
});

View file

@ -1,7 +1,7 @@
import { View } from 'react-native';
import React from 'react';
import { pure } from 'recompose';
import FoodItemRecord from '../records/FoodItemRecord';
import ProductRecord from '../records/ProductRecord';
import typeof PlaceRecord from '../records/PlaceRecord';
import { Link } from 'react-router-native';
import { TileBox, Thumbnail, StrongText, SubText } from './ItemTile';
@ -13,8 +13,8 @@ import { type Map } from 'immutable';
const getHoursText = (place: PlaceRecord) => (place.openNow ? 'Open now' : 'Closed');
// const getCategoriesText = (foodItems: Map<string, FoodItemRecord>) => {
// const categories = getCategories(foodItems);
// const getCategoriesText = (products: Map<string, ProductRecord>) => {
// const categories = getCategories(products);
// if (categories.size < 1) {
// return 'Nothing listed yet';
// }
@ -25,7 +25,7 @@ import theme from '../ui-theme';
type PlaceTileProps = {
place: PlaceRecord,
foodItems: Map<string, FoodItemRecord>,
products: Map<string, ProductRecord>,
};
export default pure(({ place }: PlaceTileProps) => {
@ -38,7 +38,7 @@ export default pure(({ place }: PlaceTileProps) => {
<Thumbnail thumb={place.thumb} />
<View>
<StrongText>{place.name}</StrongText>
{/* <SubText>{getCategoriesText(foodItems)}</SubText> */}
{/* <SubText>{getCategoriesText(products)}</SubText> */}
<SubText>
{getHoursText(place)} - {parseFloat(place.distance).toFixed(1)} mi
</SubText>

View file

@ -1,20 +1,20 @@
// @flow
import { withCreateFoodItemState } from '../enhancers/createFoodItemEnhancers';
import { withCreateProductState } from '../enhancers/createProductEnhancers';
import { withReplaceRoute } from '../enhancers/routeEnhancers';
import { compose, onlyUpdateForKeys, withHandlers } from 'recompose';
import { routeWithTitle } from '../helpers/RouteHelpers';
import TopSaveButton from './TopSaveButton';
export default compose(
withCreateFoodItemState,
withCreateProductState,
withReplaceRoute,
withHandlers({
onSave: ({ saveFoodItem, setLoading, setError, replaceRoute }) => async () => {
onSave: ({ saveProduct, setLoading, setError, replaceRoute }) => async () => {
setError(null);
setLoading(true);
try {
const { id, name } = await saveFoodItem();
replaceRoute(routeWithTitle(`/foodItem/${id || ''}`, name));
const { id, name } = await saveProduct();
replaceRoute(routeWithTitle(`/product/${id || ''}`, name));
} catch (error) {
setError(error);
} finally {

View file

@ -0,0 +1,54 @@
import React from 'react';
import { View } from 'react-native';
import { pure } from 'recompose';
import ProductRecord from '../records/ProductRecord';
import typeof PlaceRecord from '../records/PlaceRecord';
import theme from '../ui-theme';
import { Link } from 'react-router-native';
import { routeWithTitle } from '../helpers/RouteHelpers';
import { TileBox, StrongText, SubText, Thumbnail, QuantityLine } from './ItemTile';
import { withPlace } from '../enhancers/placeEnhancers';
const PlaceNameAndDistance = withPlace(({ place }) => {
return (
<SubText>{`${(place && place.name) || 'Loading...'} - ${parseFloat(place.distance).toFixed(
1
)} mi`}</SubText>
);
});
const EmptyProductTile = () => {
return (
<View>
<TileBox>
<Thumbnail thumb={null} />
<View>
<StrongText>Loading...</StrongText>
</View>
</TileBox>
</View>
);
};
export default pure(({ product }: { product: ProductRecord }) => {
if (!product) {
return <EmptyProductTile />;
}
return (
<Link
to={routeWithTitle(`/product/${product.id || ''}`, product.name)}
underlayColor={theme.itemTile.pressHighlightColor}>
<View>
<TileBox>
<Thumbnail thumb={product.thumbImage} />
<View>
<StrongText>{product.name || ''}</StrongText>
<PlaceNameAndDistance placeId={product.placeId} placeType={product.placeType} />
<QuantityLine quantity={product.quantity} lastupdated={product.lastupdated} />
</View>
</TileBox>
</View>
</Link>
);
});

View file

@ -2,7 +2,7 @@
import React, { Component } from 'react';
import { View } from 'react-native';
import { type SetSeq } from 'immutable';
import FoodItemRecord from '../records/FoodItemRecord';
import ProductRecord from '../records/ProductRecord';
import { compose, pure } from 'recompose';
import { pipe } from 'ramda';
import { withFilter } from '../enhancers/filterEnhancers';
@ -14,20 +14,20 @@ type homomorph<T> = T => T;
type Props = {
filter: FilterRecord,
foodItemsSeq: SetSeq<FoodItemRecord>,
children: (foodItem: FoodItemRecord) => Component<*, *, *>,
productsSeq: SetSeq<ProductRecord>,
children: (product: ProductRecord) => Component<*, *, *>,
};
const sortByDistance = (foodItemsSeq: SetSeq<FoodItemRecord>): SetSeq<FoodItemRecord> => {
return foodItemsSeq.sort((left, right) => left.distance - right.distance);
const sortByDistance = (productsSeq: SetSeq<ProductRecord>): SetSeq<ProductRecord> => {
return productsSeq.sort((left, right) => left.distance - right.distance);
};
const sortByLastUpdated = (foodItemsSeq: SetSeq<FoodItemRecord>): SetSeq<FoodItemRecord> => {
return foodItemsSeq.sort((left, right) => right.lastupdated - left.lastupdated);
const sortByLastUpdated = (productsSeq: SetSeq<ProductRecord>): SetSeq<ProductRecord> => {
return productsSeq.sort((left, right) => right.lastupdated - left.lastupdated);
};
const sortByQuantity = (foodItemsSeq: SetSeq<FoodItemRecord>): SetSeq<FoodItemRecord> => {
return foodItemsSeq.sort((left, right) => {
const sortByQuantity = (productsSeq: SetSeq<ProductRecord>): SetSeq<ProductRecord> => {
return productsSeq.sort((left, right) => {
const quantityCompare = compareQuantity(left.quantity, right.quantity);
return quantityCompare === 0 ? left.distance - right.distance : quantityCompare;
});
@ -44,20 +44,20 @@ const getSortBy = (filter: FilterRecord): homomorph<*> => {
}
};
const intoArray = (foodItemsSeq: SetSeq<FoodItemRecord>): Array<FoodItemRecord> =>
foodItemsSeq ? foodItemsSeq.toArray() : [];
const intoArray = (productsSeq: SetSeq<ProductRecord>): Array<ProductRecord> =>
productsSeq ? productsSeq.toArray() : [];
const FoodItemList = (props: Props) => {
const { filter, foodItemsSeq, children } = props;
const ProductList = (props: Props) => {
const { filter, productsSeq, children } = props;
if (!foodItemsSeq) {
if (!productsSeq) {
return null;
}
const items = pipe(
getSortBy(filter),
intoArray
)(foodItemsSeq);
)(productsSeq);
return <View style={{ flexShrink: 2 }}>{items.map(children)}</View>;
};
@ -65,4 +65,4 @@ const FoodItemList = (props: Props) => {
export default compose(
pure,
withFilter
)(FoodItemList);
)(ProductList);

View file

@ -5,7 +5,7 @@ import { Toolbar, Icon } from 'react-native-material-ui';
import { path, cond, test } from 'ramda';
import queryString from 'query-string';
import { getSearch, getViewMode } from '../helpers/RouteHelpers';
import FoodItemSaveBtn from '../components/FoodItemSaveBtn';
import ProductSaveBtn from '../components/ProductSaveBtn';
import { withFilter } from '../enhancers/filterEnhancers';
import FilterRecord from '../records/FilterRecord';
import { palette } from '../ui-theme';
@ -56,7 +56,7 @@ class TopToolbar extends Component {
setFilter(filter.set('search', ''));
};
onChangeText = (search) => {
onChangeText = search => {
const { setFilter, filter } = this.props;
setFilter(filter.set('search', search));
};
@ -64,8 +64,8 @@ class TopToolbar extends Component {
getRightElement = () => {
const route = path(['router', 'route', 'location', 'pathname'], this.props);
if (/^\/createFoodItem/.test(route)) {
return <FoodItemSaveBtn />;
if (/^\/createProduct/.test(route)) {
return <ProductSaveBtn />;
}
};

View file

@ -1,34 +1,34 @@
import mapPropsStream from 'recompose/mapPropsStream';
import CreateFoodItem$, { emitter as emitCreateItemState } from '../streams/CreateFoodItemStream';
import { emitter as emitFoodItemsState } from '../streams/FoodItemsStream';
import { createFoodItem } from '../apis/FoodItemsApi';
import FoodItemRecord, { createFoodItem as buildFoodItem } from '../records/FoodItemRecord';
import CreateProduct$, { emitter as emitCreateItemState } from '../streams/CreateProductStream';
import { emitter as emitProductsState } from '../streams/ProductsStream';
import { createProduct } from '../apis/ProductsApi';
import ProductRecord, { createProduct as buildProduct } from '../records/ProductRecord';
import Snackbar from 'react-native-snackbar';
export const withCreateFoodItemState = mapPropsStream((props$) => {
return props$.combineLatest(CreateFoodItem$, (props, state) => {
const { foodItem, loading, error } = state;
export const withCreateProductState = mapPropsStream(props$ => {
return props$.combineLatest(CreateProduct$, (props, state) => {
const { product, loading, error } = state;
const setFoodItem = (foodItem: FoodItemRecord) => emitCreateItemState({ ...state, foodItem });
const setProduct = (product: ProductRecord) => emitCreateItemState({ ...state, product });
const setLoading = (loading: boolean) => emitCreateItemState({ ...state, loading });
const setError = (error: Error) => emitCreateItemState({ ...state, error });
const saveFoodItem = async () => {
const saveProduct = async () => {
try {
// insert new item into db and cast it into FoodItemRecord
const newItem = buildFoodItem(await createFoodItem(foodItem));
// insert new item into db and cast it into ProductRecord
const newItem = buildProduct(await createProduct(product));
Snackbar.show({
title: foodItem.name + ' added',
title: product.name + ' added',
backgroundColor: 'black',
color: 'white',
});
// notify food items state of new item
emitFoodItemsState(newItem);
emitProductsState(newItem);
// clear the create item form to default empty record
setFoodItem(new FoodItemRecord());
setProduct(new ProductRecord());
// allow caller to see what was created
return newItem;
@ -48,11 +48,11 @@ export const withCreateFoodItemState = mapPropsStream((props$) => {
return {
...props,
foodItem,
product,
loading,
error,
setFoodItem,
saveFoodItem,
setProduct,
saveProduct,
setLoading,
setError,
};

View file

@ -9,9 +9,9 @@ const fetchAndEmitFaves = async () => {
emitFaves(faves);
};
const buildTempFave = foodItemId =>
const buildTempFave = productId =>
fromJS({
food_item_id: foodItemId,
food_item_id: productId,
date: Date.now(),
});
@ -27,19 +27,19 @@ export const withFaves = mapPropsStream(props$ =>
console.log(err); // eslint-disable-line no-console
}
},
addFave: async foodItemId => {
addFave: async productId => {
try {
await putFaves([foodItemId]);
emitFave(buildTempFave(foodItemId));
await putFaves([productId]);
emitFave(buildTempFave(productId));
} catch (err) {
console.log(err); // eslint-disable-line no-console
}
},
deleteFave: async foodItemId => {
deleteFave: async productId => {
try {
await deleteFaves([foodItemId]);
await deleteFaves([productId]);
const idx = faves.findIndex(fave => get(fave, 'id') === foodItemId);
const idx = faves.findIndex(fave => get(fave, 'id') === productId);
if (idx >= 0) {
emitFaves(faves.delete(idx));
}

View file

@ -1,87 +0,0 @@
// @flow
import withProps from 'recompose/withProps';
import compose from 'recompose/compose';
import mapPropsStream from 'recompose/mapPropsStream';
import FoodItems$ from '../streams/FoodItemsStream';
import typeof FoodItemRecord from '../records/FoodItemRecord';
import { Map, Set } from 'immutable';
import { pipe, path, toLower, equals } from 'ramda';
type FindPredicate<A> = (left: A) => (right: A) => Boolean;
const getName: (f: FoodItemRecord) => String = pipe(path(['name']), toLower);
const matchesName: FindPredicate<FoodItemRecord> = left => right => {
return equals(getName(left), getName(right));
};
const addIfNotExisting = (predicate: FindPredicate<FoodItemRecord>) => (
set: Set<FoodItemRecord>,
item: FoodItemRecord
) => {
return !set.find(predicate(item)) ? set.add(item) : set;
};
const intoSet = reducer => items => {
return items ? items.reduce(reducer, new Set()) : new Set();
};
export const withFoodItems = mapPropsStream(props$ =>
props$.combineLatest(FoodItems$, (props, foodItems) => {
return {
...props,
foodItemsMap: foodItems,
};
})
);
export const withFoodItemsAsSeq = mapPropsStream(props$ =>
props$.combineLatest(FoodItems$, (props, foodItems) => {
return {
...props,
foodItemsSeq: foodItems && foodItems.valueSeq(),
};
})
);
export const withFoodItemIdFromRoute = withProps((props: { match: { params: { id: string } } }) => {
const id: string = path(['match', 'params', 'id'], props) || '';
return { foodItemId: id };
});
export const withFoodItem = compose(
withFoodItems,
withFoodItemIdFromRoute,
withProps((props: { foodItemsMap: ?Map<number, FoodItemRecord>, foodItemId: number }) => {
const { foodItemsMap, foodItemId } = props;
return { foodItem: foodItemsMap && foodItemsMap.get(foodItemId) };
})
);
export const withFoodItemPlaceId = withProps((props: { foodItem: FoodItemRecord }) => {
const { foodItem } = props;
return {
placeId: foodItem && foodItem.placeId,
};
});
export const withFoodItemsGroupedByPlace = compose(
withFoodItems,
withProps((props: { foodItemsMap: ?Map<number, FoodItemRecord> }) => {
if (!props.foodItemsMap) {
return {};
}
return {
foodItemsByPlace: props.foodItemsMap.groupBy(foodItem => foodItem.placeId),
};
})
);
export const withUniqueFoodItems = compose(
withFoodItemsAsSeq,
withProps(({ foodItemsSeq }) => {
return {
foodItemsSeq: intoSet(addIfNotExisting(matchesName))(foodItemsSeq),
};
})
);

View file

@ -1,39 +1,39 @@
// @flow
import { withProps } from "recompose";
import { emit } from "../streams/ImagesStream";
import { addImage, getImages } from "../apis/ImagesApi";
import AuthManager from "../AuthManager";
import { map } from "ramda";
import { withProps } from 'recompose';
import { emit } from '../streams/ImagesStream';
import { addImage, getImages } from '../apis/ImagesApi';
import AuthManager from '../AuthManager';
import { map } from 'ramda';
export const withImages = withProps({
addImage: async ({ foodItemId, imageUri }) => {
addImage: async ({ productId, imageUri }) => {
try {
if (!AuthManager.user) {
throw new Error("You need to be logged in to add images");
throw new Error('You need to be logged in to add images');
}
const username = AuthManager.user.name;
const { url, date } = await addImage({ foodItemId, username, imageUri });
const { url, date } = await addImage({ productId, username, imageUri });
emit({
url,
username,
date,
food_item_id: foodItemId
food_item_id: productId,
});
} catch (error) {
// TODO error handling in the UI
console.error(error); // eslint-disable-line
}
},
getImages: async ({ foodItemId }) => {
getImages: async ({ productId }) => {
try {
const images = await getImages(foodItemId);
const images = await getImages(productId);
map(emit, images);
} catch (error) {
// TODO error handling in the UI
console.error(error); // eslint-disable-line
}
}
},
});

View file

@ -1,24 +1,16 @@
// @flow
import withProps from 'recompose/withProps';
import mapPropsStream from 'recompose/mapPropsStream';
import compose from 'recompose/compose';
import Places$, { emitter as emitPlace } from '../streams/PlacesStream';
import FoodItems$ from '../streams/FoodItemsStream';
import Products$ from '../streams/ProductsStream';
import { path } from 'ramda';
import { List } from 'immutable';
import { List, getIn } from 'immutable';
import { buildPlaceRecord } from '../records/PlaceRecord';
import { getPlaceDetails } from '../apis/GooglePlacesApi';
import { getSearch } from '../helpers/RouteHelpers';
import { withRouterContext } from './routeEnhancers';
import { find } from 'rxjs/operator/find';
export const fetchPlaceDetails = async ({
distance,
placeId,
}: {
distance: number,
placeId: ?string,
}) => {
export const fetchPlaceDetails = async ({ distance, placeId }) => {
const place = await getPlaceDetails({
distance,
placeId,
@ -41,7 +33,6 @@ export const withPlaceIdFromRoute = withProps((props: { match: { params: { id: s
});
export const withPlaceId = compose(
withPlaces,
withRouterContext,
withProps(props => {
const placeId = props.placeId || getSearch(props).placeId;
@ -59,40 +50,34 @@ export const withPlaceActions = withProps(() => {
export const withPlace = compose(
withPlaceId,
withPlaces,
withProps(({ placeId, places }) => {
if (!placeId) {
return {
place: null,
fetchPlaceDetails,
};
}
return {
place: places && places.get(placeId),
fetchPlaceDetails,
};
withProps(({ placeId, places, placeType }) => {
const place =
placeId && placeType && getIn(places, [placeType], []).find(place => place.id === placeId);
return { place, fetchPlaceDetails };
})
);
export const withPlaceForFoodItem = compose(
export const withPlaceForProduct = compose(
withPlaces,
withProps(({ places, foodItem }) => {
if (!foodItem || !foodItem.placeType) {
withProps(({ places, product }) => {
if (!product || !product.placeType) {
return { place: null };
}
const place = places.find(place => place.placeType === foodItem.placeType);
const place = getIn(places, [product.placeType, 0]);
return { place };
})
);
export const withFoodItemsForPlace = mapPropsStream(props$ =>
props$.combineLatest(FoodItems$, (props, foodItemsMap) => {
export const withProductsForPlace = mapPropsStream(props$ =>
props$.combineLatest(Products$, (props, productsMap) => {
const placeId = props.placeId;
const foodItems = foodItemsMap
? foodItemsMap.toList().filter(foodItem => placeId === foodItem.placeId)
const products = productsMap
? productsMap.toList().filter(product => placeId === product.placeId)
: List();
return {
...props,
foodItems,
products,
};
})
);

View file

@ -0,0 +1,90 @@
// @flow
import withProps from 'recompose/withProps';
import compose from 'recompose/compose';
import mapPropsStream from 'recompose/mapPropsStream';
import Products$ from '../streams/ProductsStream';
import typeof ProductRecord from '../records/ProductRecord';
import { Map, Set } from 'immutable';
import { pipe, path, toLower, equals } from 'ramda';
type FindPredicate<A> = (left: A) => (right: A) => Boolean;
const getName: (f: ProductRecord) => String = pipe(
path(['name']),
toLower
);
const matchesName: FindPredicate<ProductRecord> = left => right => {
return equals(getName(left), getName(right));
};
const addIfNotExisting = (predicate: FindPredicate<ProductRecord>) => (
set: Set<ProductRecord>,
item: ProductRecord
) => {
return !set.find(predicate(item)) ? set.add(item) : set;
};
const intoSet = reducer => items => {
return items ? items.reduce(reducer, new Set()) : new Set();
};
export const withProducts = mapPropsStream(props$ =>
props$.combineLatest(Products$, (props, products) => {
return {
...props,
productsMap: products,
};
})
);
export const withProductsAsSeq = mapPropsStream(props$ =>
props$.combineLatest(Products$, (props, products) => {
return {
...props,
productsSeq: products && products.valueSeq(),
};
})
);
export const withProductIdFromRoute = withProps((props: { match: { params: { id: string } } }) => {
const id: string = path(['match', 'params', 'id'], props) || '';
return { productId: id };
});
export const withProduct = compose(
withProducts,
withProductIdFromRoute,
withProps((props: { productsMap: ?Map<number, ProductRecord>, productId: number }) => {
const { productsMap, productId } = props;
return { product: productsMap && productsMap.get(productId) };
})
);
export const withProductPlaceId = withProps((props: { product: ProductRecord }) => {
const { product } = props;
return {
placeId: product && product.placeId,
};
});
export const withProductsGroupedByPlace = compose(
withProducts,
withProps((props: { productsMap: ?Map<number, ProductRecord> }) => {
if (!props.productsMap) {
return {};
}
return {
productsByPlace: props.productsMap.groupBy(product => product.placeId),
};
})
);
export const withUniqueProducts = compose(
withProductsAsSeq,
withProps(({ productsSeq }) => {
return {
productsSeq: intoSet(addIfNotExisting(matchesName))(productsSeq),
};
})
);

View file

@ -1,18 +1,18 @@
//@flow
import { withProps } from "recompose";
import { setQuantity } from "../apis/QuantityApi";
import { emit as emitQuantity } from "../streams/QuantityStream";
import type { Quantity, QuantityResponse } from "../constants/QuantityConstants";
import { nth } from "ramda";
import { withProps } from 'recompose';
import { setQuantity } from '../apis/QuantityApi';
import { emit as emitQuantity } from '../streams/QuantityStream';
import type { Quantity, QuantityResponse } from '../constants/QuantityConstants';
import { nth } from 'ramda';
export const withUpdateQuantity = withProps({
updateQuantity: async ({ foodItemId, quantity }: { foodItemId: string, quantity: Quantity }) => {
updateQuantity: async ({ productId, quantity }: { productId: string, quantity: Quantity }) => {
try {
const newQuantity: QuantityResponse = nth(0, await setQuantity({ foodItemId, quantity }));
const newQuantity: QuantityResponse = nth(0, await setQuantity({ productId, quantity }));
emitQuantity(newQuantity);
} catch (error) {
// todo - error states in food item detail page
console.error(error); // eslint-disable-line
}
}
},
});

View file

@ -1,5 +1,5 @@
// @flow
import FoodItemRecord from '../records/FoodItemRecord';
import ProductRecord from '../records/ProductRecord';
import {
type Category,
CATEGORY_BEVERAGES,
@ -21,9 +21,9 @@ export const getCategoryText = (category: Category) => {
}
};
export const getCategories = (foodItems: Map<string, FoodItemRecord>) => {
return foodItems
.map(foodItem => foodItem.get('category'))
export const getCategories = (products: Map<string, ProductRecord>) => {
return products
.map(product => product.get('category'))
.toSet()
.map(getCategoryText)
.toList();

View file

@ -1,28 +1,32 @@
import { memoizeWith, identity } from "ramda";
import { memoizeWith, identity } from 'ramda';
export const getZoomBox = memoizeWith(identity, (foodItemsMap, coords) =>
foodItemsMap.reduce(
(prev, foodItem) => {
const minLat = !prev.minLat || prev.minLat > foodItem.latitude ? foodItem.latitude : prev.minLat;
export const getZoomBox = memoizeWith(identity, (productsMap, coords) =>
productsMap.reduce(
(prev, product) => {
const minLat =
!prev.minLat || prev.minLat > product.latitude ? product.latitude : prev.minLat;
const maxLat = !prev.maxLat || prev.maxLat < foodItem.latitude ? foodItem.latitude : prev.maxLat;
const maxLat =
!prev.maxLat || prev.maxLat < product.latitude ? product.latitude : prev.maxLat;
const minLng = !prev.minLng || prev.minLng > foodItem.longitude ? foodItem.longitude : prev.minLng;
const minLng =
!prev.minLng || prev.minLng > product.longitude ? product.longitude : prev.minLng;
const maxLng = !prev.maxLng || prev.maxLng < foodItem.longitude ? foodItem.longitude : prev.maxLng;
const maxLng =
!prev.maxLng || prev.maxLng < product.longitude ? product.longitude : prev.maxLng;
return {
minLat,
maxLat,
minLng,
maxLng
maxLng,
};
},
{
minLat: coords.latitude,
maxLat: coords.latitude,
minLng: coords.longitude,
maxLng: coords.longitude
maxLng: coords.longitude,
}
)
);

View file

@ -1,6 +1,6 @@
// @flow
import React, { PureComponent } from 'react';
import typeof FoodItemRecord from '../records/FoodItemRecord';
import typeof ProductRecord from '../records/ProductRecord';
import { CATEGORIES } from '../constants/CategoryConstants';
import { getCategoryText } from '../helpers/CategoryHelpers';
import FullScreenModal from './FullScreenModal';
@ -10,7 +10,7 @@ import PickerItemRow from '../components/PickerItemRow';
type Props = {
onClose: () => void,
onUpdate: (c: Category) => void,
foodItem: FoodItemRecord,
product: ProductRecord,
};
class CategoryModal extends PureComponent {
@ -24,13 +24,13 @@ class CategoryModal extends PureComponent {
};
render() {
const { onClose, foodItem } = this.props;
const { onClose, product } = this.props;
return (
<FullScreenModal title="Category" onClose={onClose}>
{CATEGORIES.map(category => (
<PickerItemRow
key={category}
isSelected={foodItem && foodItem.category === category}
isSelected={product && product.category === category}
text={getCategoryText(category)}
onPress={() => this.updateAndClose(category)}
/>

View file

@ -50,7 +50,7 @@ export const wrapModalComponent = ({ component, onCloseProp, onUpdateProp }) =>
pure,
mapProps((props: Props) => {
return {
foodItem: props.foodItem,
product: props.product,
onClose: props[onCloseProp],
onUpdate: props[onUpdateProp],
};

View file

@ -1,20 +1,19 @@
// @flow
import React, { Component } from 'react';
import { TouchableOpacity, ScrollView, SafeAreaView } from 'react-native';
import { TextButton } from './Modal';
import FoodItemList from '../components/FoodItemList';
import FoodItemRecord from '../records/FoodItemRecord';
import ProductList from '../components/ProductsList';
import ProductRecord from '../records/ProductRecord';
import { StrongText } from '../components/ItemTile';
import { Toolbar } from 'react-native-material-ui';
import FilterRecord from '../records/FilterRecord';
import { compose, pure } from 'recompose';
import { withUniqueFoodItems } from '../enhancers/foodItemEnhancers';
import { withUniqueProducts } from '../enhancers/productsEnhancers';
import { Set } from 'immutable';
type Props = {
onClose: () => void,
onUpdate: (name: string) => void,
foodItemsSeq: ?Set<FoodItemRecord>,
productsSeq: ?Set<ProductRecord>,
};
class NameModal extends Component {
@ -45,7 +44,7 @@ class NameModal extends Component {
};
render() {
const { onClose, foodItemsSeq } = this.props;
const { onClose, productsSeq } = this.props;
const { filter } = this.state;
return (
<SafeAreaView
@ -80,17 +79,15 @@ class NameModal extends Component {
/>
)}
<ScrollView style={{ paddingLeft: 78 }}>
<FoodItemList filter={filter} limit={10} foodItemsSeq={foodItemsSeq}>
{(foodItem: FoodItemRecord) => (
<TouchableOpacity
key={foodItem.id}
onPress={() => this.updateAndClose(foodItem.name)}>
<ProductList filter={filter} limit={10} productsSeq={productsSeq}>
{(product: ProductRecord) => (
<TouchableOpacity key={product.id} onPress={() => this.updateAndClose(product.name)}>
<StrongText style={{ paddingTop: 20, paddingBottom: 20 }}>
{foodItem.name}
{product.name}
</StrongText>
</TouchableOpacity>
)}
</FoodItemList>
</ProductList>
</ScrollView>
</SafeAreaView>
);
@ -99,5 +96,5 @@ class NameModal extends Component {
export default compose(
pure,
withUniqueFoodItems
withUniqueProducts
)(NameModal);

View file

@ -3,17 +3,17 @@ import React from 'react';
import { getQuantityDropdownText } from '../helpers/QuantityHelpers';
import { type Quantity, QUANTITIES } from '../constants/QuantityConstants';
import FullScreenModal from './FullScreenModal';
import { typeof FoodItem } from '../records/FoodItemRecord';
import { typeof Product } from '../records/ProductRecord';
import PickerItemRow from '../components/PickerItemRow';
type Props = {
onClose: () => void,
foodItem: FoodItem,
product: Product,
onUpdate: (q: Quantity) => void,
};
const QuantityModal = (props: Props) => {
const { foodItem, onUpdate, onClose } = props;
const { product, onUpdate, onClose } = props;
const onPress = (q: Quantity) => () => {
onUpdate(q);
@ -26,7 +26,7 @@ const QuantityModal = (props: Props) => {
<PickerItemRow
key={quantity}
text={getQuantityDropdownText(quantity)}
isSelected={quantity === foodItem.quantity}
isSelected={quantity === product.quantity}
onPress={onPress(quantity)}
/>
))}

View file

@ -1,11 +1,10 @@
// @flow
import React from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import theme from '../ui-theme';
import { Divider } from 'react-native-material-ui';
import FoodItemRecord from '../records/FoodItemRecord';
import ProductRecord from '../records/ProductRecord';
import PlaceRecord from '../records/PlaceRecord';
import NameModal from '../modals/FoodItemNameModal';
import NameModal from '../modals/ProductNameModal';
import CategoryModal from '../modals/CategoryModal';
import ImagePreviewModal from '../modals/ImagePreviewModal';
import {
@ -19,8 +18,8 @@ import {
} from 'recompose';
import RNGooglePlaces from 'react-native-google-places';
import { ImageThumb, ImagePicker } from '../components/ImagePicker';
import { withCreateFoodItemState } from '../enhancers/createFoodItemEnhancers';
import { withPlaceForFoodItem, withPlaceId, withPlaceActions } from '../enhancers/placeEnhancers';
import { withCreateProductState } from '../enhancers/createProductEnhancers';
import { withPlaceForProduct, withPlaceId, withPlaceActions } from '../enhancers/placeEnhancers';
import Spinner from 'react-native-loading-spinner-overlay';
import { openImagePicker } from '../helpers/ImagePickerHelpers';
import { IndexedSeq } from 'immutable';
@ -32,10 +31,10 @@ import { wrapModalComponent } from '../modals/FullScreenModal';
import { withAuthRedirect } from '../enhancers/authEnhancers';
type Props = {
foodItem: typeof FoodItemRecord,
setPropOfFoodItem: Function,
product: typeof ProductRecord,
setPropOfProduct: Function,
toggleNameModal: Function,
setFoodItem: Function,
setProduct: Function,
setModalsVisible: Function,
place: ?PlaceRecord,
updatePlace: (place: GooglePlaceObject) => void,
@ -44,7 +43,7 @@ type Props = {
loading: boolean,
setLoading: (arg: boolean) => void,
emitPlace: (place: Object) => void,
withFoodItemsAsSeq: IndexedSeq<FoodItemRecord>,
withProductsAsSeq: IndexedSeq<ProductRecord>,
toggleCategoryModal: () => void,
toggleQuantityModal: () => void,
};
@ -89,9 +88,9 @@ const openPlaceModal = (onChoosePlace: (place: GooglePlaceObject) => void) => ()
RNGooglePlaces.openAutocompleteModal({ type: 'establishment' }).then(onChoosePlace);
};
const CreateFoodItem = (props: Props) => {
const CreateProduct = (props: Props) => {
const {
foodItem,
product,
toggleNameModal,
updatePlace,
addImage,
@ -105,20 +104,20 @@ const CreateFoodItem = (props: Props) => {
return (
<View style={{ ...theme.page.container, backgroundColor: 'white', padding: 10 }}>
<Spinner visible={loading} />
<Field text={foodItem.name} onPress={toggleNameModal} placeholder="Name" />
<Field text={product.name} onPress={toggleNameModal} placeholder="Name" />
<Field text={place && place.name} onPress={openPlaceModal(updatePlace)} placeholder="Place" />
<Field
text={foodItem.category && getCategoryText(foodItem.category)}
text={product.category && getCategoryText(product.category)}
onPress={toggleCategoryModal}
placeholder="Category"
/>
<Field
text={foodItem.quantity && getQuantityDropdownText(foodItem.quantity)}
text={product.quantity && getQuantityDropdownText(product.quantity)}
onPress={toggleQuantityModal}
placeholder="Quantity"
/>
<ImagePicker onCreateNew={addImage}>
{foodItem.images.map((imageURI: string) => (
{product.images.map((imageURI: string) => (
<ImageThumb key={imageURI} uri={imageURI} onPress={() => setImagePreview(imageURI)} />
))}
</ImagePicker>
@ -128,31 +127,31 @@ const CreateFoodItem = (props: Props) => {
const ImagePreviewComp = compose(
renderComponent,
mapProps(({ foodItem, setFoodItem, setImagePreview, imagePreview }) => {
mapProps(({ product, setProduct, setImagePreview, imagePreview }) => {
return {
onClose: () => setImagePreview(-1),
onDelete: () => {
setFoodItem(foodItem.deleteIn(['images', imagePreview]));
setProduct(product.deleteIn(['images', imagePreview]));
setImagePreview(null);
},
imageSrc: foodItem.images.get(imagePreview),
imageSrc: product.images.get(imagePreview),
};
})
)(ImagePreviewModal);
const setPropOfFoodItem = ({ foodItem, setFoodItem }: Props) => (prop: string) => (value: any) => {
setFoodItem(foodItem.set(prop, value));
const setPropOfProduct = ({ product, setProduct }: Props) => (prop: string) => (value: any) => {
setProduct(product.set(prop, value));
};
const addImage = ({ foodItem, setFoodItem }: Props) => async () => {
const addImage = ({ product, setProduct }: Props) => async () => {
const { path } = await openImagePicker();
Snackbar.show({
title: 'Photo added.',
});
setFoodItem(foodItem.update('images', images => images.add(path)));
setProduct(product.update('images', images => images.add(path)));
};
const updatePlace = ({ foodItem, setFoodItem, emitPlace }: Props) => placeDetails => {
const updatePlace = ({ product, setProduct, emitPlace }: Props) => placeDetails => {
const { placeID, latitude, longitude } = placeDetails;
emitPlace(
@ -162,8 +161,8 @@ const updatePlace = ({ foodItem, setFoodItem, emitPlace }: Props) => placeDetail
})
);
setFoodItem(
foodItem.merge({
setProduct(
product.merge({
placeId: placeID,
latitude,
longitude,
@ -173,16 +172,16 @@ const updatePlace = ({ foodItem, setFoodItem, emitPlace }: Props) => placeDetail
export default compose(
withAuthRedirect,
withCreateFoodItemState,
withCreateProductState,
withPlaceId,
withPlaceForFoodItem,
withPlaceForProduct,
withPlaceActions,
withState('nameModalOpen', 'setNameModalOpen', false),
withState('imagePreview', 'setImagePreview', null),
withState('categoryModalOpen', 'setCategoryModalOpen', false),
withState('quantityModalOpen', 'setQuantityModalOpen', false),
withHandlers({
setPropOfFoodItem,
setPropOfProduct,
addImage,
updatePlace,
toggleNameModal: ({ nameModalOpen, setNameModalOpen }) => () => {
@ -196,14 +195,14 @@ export default compose(
},
}),
withHandlers({
setQuantity: ({ setPropOfFoodItem }) => setPropOfFoodItem('quantity'),
setCategory: ({ setPropOfFoodItem }) => setPropOfFoodItem('category'),
setName: ({ setPropOfFoodItem }) => setPropOfFoodItem('name'),
setQuantity: ({ setPropOfProduct }) => setPropOfProduct('quantity'),
setCategory: ({ setPropOfProduct }) => setPropOfProduct('category'),
setName: ({ setPropOfProduct }) => setPropOfProduct('name'),
}),
lifecycle({
componentDidMount() {
const { placeId, setFoodItem } = this.props;
setFoodItem(new FoodItemRecord({ placeId }));
const { placeId, setProduct } = this.props;
setProduct(new ProductRecord({ placeId }));
},
}),
branch(
@ -231,4 +230,4 @@ export default compose(
onUpdateProp: 'setQuantity',
})
)
)(CreateFoodItem);
)(CreateProduct);

View file

@ -2,12 +2,12 @@
import React from 'react';
import { View, ScrollView, RefreshControl } from 'react-native';
import { compose, pure, withState, withHandlers, lifecycle } from 'recompose';
import { withFoodItems } from '../enhancers/foodItemEnhancers';
import { withProducts } from '../enhancers/productsEnhancers';
import { Map, get } from 'immutable';
import typeof FoodItemRecord from '../records/FoodItemRecord';
import typeof ProductRecord from '../records/ProductRecord';
import { withFaves } from '../enhancers/favesEnhancer';
import type { Faves, Fave } from '../streams/FavesStream';
import FoodItemTile from '../components/FoodItemTile';
import ProductTile from '../components/ProductTile';
import { withAuthRedirect } from '../enhancers/authEnhancers';
type Props = {
@ -26,7 +26,7 @@ const FavesList = ({ faves, isRefreshing, onPulldown }: Props) => {
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onPulldown} />}>
{faves &&
faves.map((fave: Faves, index: number) => {
return <FoodItemTile key={fave.id || index} foodItem={fave} />;
return <ProductTile key={fave.id || index} product={fave} />;
})}
</ScrollView>
</View>

View file

@ -6,7 +6,7 @@ import { compose, renderComponent, withState, withProps } from 'recompose';
import { Map, get } from 'immutable';
import MapView from 'react-native-maps';
import { routeWithTitle } from '../helpers/RouteHelpers';
import FoodItemTile from '../components/FoodItemTile';
import ProductTile from '../components/ProductTile';
import { getZoomBox } from '../helpers/CoordinatesHelpers';
import { withFaves } from '../enhancers/favesEnhancer';
import { type Faves } from '../streams/FavesStream';
@ -47,9 +47,9 @@ const FavesMap = ({ faves, region, onRegionChange, pushRoute, initialRegion }: P
}}>
<MapView.Callout
onPress={() => {
pushRoute(routeWithTitle(`/foodItem/${get(fave, 'id', '')}`, get(fave, 'name')));
pushRoute(routeWithTitle(`/product/${get(fave, 'id', '')}`, get(fave, 'name')));
}}>
<FoodItemTile foodItem={fave} />
<ProductTile product={fave} />
</MapView.Callout>
</MapView.Marker>
))}
@ -58,8 +58,8 @@ const FavesMap = ({ faves, region, onRegionChange, pushRoute, initialRegion }: P
);
};
const withInitialRegionProp = withProps(({ foodItemsMap = Map(), location }) => {
const zoomBox = getZoomBox(foodItemsMap, location.coords);
const withInitialRegionProp = withProps(({ productsMap = Map(), location }) => {
const zoomBox = getZoomBox(productsMap, location.coords);
return {
initialRegion: {

View file

@ -1,7 +1,7 @@
// @flow
import React from 'react';
import { View, SafeAreaView } from 'react-native';
import Food from './Food';
import Food from './Products';
import Places from './Places';
import { BottomNavigation } from 'react-native-material-ui';
import { getLocation } from '../apis/PositionApi';
@ -60,7 +60,7 @@ const Nav = (props: Props) => {
key="add"
icon="add-circle"
label="Add"
onPress={pushRoute(`/createFoodItem?backto=${location.pathname}`)}
onPress={pushRoute(`/createProduct?backto=${location.pathname}`)}
style={{
container: {
minWidth: 40,

View file

@ -4,18 +4,14 @@ import { View, Text, Image, ScrollView } from 'react-native';
import theme, { palette } from '../ui-theme';
import { compose, pure, withState, withHandlers, lifecycle } from 'recompose';
import typeof PlaceRecord from '../records/PlaceRecord';
import {
withPlace,
withPlaceIdFromRoute,
withFoodItemsForPlace,
} from '../enhancers/placeEnhancers';
import { withPlace, withPlaceIdFromRoute, withProductsForPlace } from '../enhancers/placeEnhancers';
import Carousel from 'react-native-looped-carousel';
import CountBadge from '../components/CountBadge';
import { StrongText } from '../components/ItemTile';
import IconButton from '../components/IconButton';
import { type List } from 'immutable';
import typeof FoodItemRecord from '../records/FoodItemRecord';
import FoodItemTile from '../components/FoodItemTile';
import typeof ProductRecord from '../records/ProductRecord';
import ProductTile from '../components/ProductTile';
import { openUrl } from '../helpers/linkHelpers';
import { routeWithQuery } from '../helpers/RouteHelpers';
import { Link } from 'react-router-native';
@ -32,7 +28,7 @@ const contentTileStyle = {
type Props = {
place: ?PlaceRecord,
foodItems: ?List<FoodItemRecord>,
products: ?List<ProductRecord>,
currentImage: number,
setCurrentImage: (idx: number) => void,
viewOnMap: () => void,
@ -41,7 +37,7 @@ type Props = {
};
const PlaceDetail = (props: Props) => {
const { place, foodItems, currentImage, setCurrentImage, viewOnMap, phoneCall } = props;
const { place, products, currentImage, setCurrentImage, viewOnMap, phoneCall } = props;
if (!place) {
return <View />;
@ -56,7 +52,7 @@ const PlaceDetail = (props: Props) => {
{photos.size === 1 && <Image style={stretchedStyle} source={{ uri: photos.first() }} />}
{photos.size > 1 && (
<Carousel autoplay={false} onAnimateNextPage={setCurrentImage} style={stretchedStyle}>
{photos.map((uri) => (
{photos.map(uri => (
<Image key={uri} style={{ flex: 1, resizeMode: 'stretch' }} source={{ uri }} />
))}
</Carousel>
@ -84,14 +80,14 @@ const PlaceDetail = (props: Props) => {
</View>
<View style={{ padding: 15, ...contentTileStyle }}>
<StrongText>Products</StrongText>
{!!foodItems &&
foodItems.map((foodItem) => (
<FoodItemTile key={foodItem.id} foodItem={foodItem} place={place} />
{!!products &&
products.map(product => (
<ProductTile key={product.id} product={product} place={place} />
))}
{!foodItems ||
(!foodItems.size && (
{!products ||
(!products.size && (
<Link
to={routeWithQuery('/createFoodItem', {
to={routeWithQuery('/createProduct', {
routeTitle: 'Add a Food Item',
placeId: place.id,
})}>
@ -116,7 +112,7 @@ export default compose(
pure,
withPlaceIdFromRoute,
withPlace,
withFoodItemsForPlace,
withProductsForPlace,
withState('currentImage', 'setCurrentImage', 0),
withHandlers({
viewOnMap: (props: Props) => () => {

View file

@ -3,15 +3,15 @@ import React from 'react';
import { View, ScrollView, RefreshControl } from 'react-native';
import PlaceTile from '../components/PlaceTile';
import { compose, pure, withState, withHandlers, lifecycle } from 'recompose';
// import { withFoodItemsGroupedByPlace } from '../enhancers/foodItemEnhancers';
// import { withProductsGroupedByPlace } from '../enhancers/productEnhancers';
import { withPlaces } from '../enhancers/placeEnhancers';
import { Map, List } from 'immutable';
import typeof FoodItemRecord from '../records/FoodItemRecord';
import typeof ProductRecord from '../records/ProductRecord';
import typeof PlaceRecord from '../records/PlaceRecord';
import { withRouterContext } from '../enhancers/routeEnhancers';
type Props = {
foodItemsByPlace: Map<string, Map<string, FoodItemRecord>>,
productsByPlace: Map<string, Map<string, ProductRecord>>,
places: ?Map<string, PlaceRecord>,
onRefresh: () => Promise<any>,
onPulldown: () => {},
@ -21,7 +21,7 @@ type Props = {
const byDistance = (left: PlaceRecord, right: PlaceRecord) => left.distance - right.distance;
const PlacesList = ({ foodItemsByPlace = Map(), places, isRefreshing, onPulldown }: Props) => {
const PlacesList = ({ productsByPlace = Map(), places, isRefreshing, onPulldown }: Props) => {
const refreshing = isRefreshing || !places;
return (
<View>
@ -31,8 +31,8 @@ const PlacesList = ({ foodItemsByPlace = Map(), places, isRefreshing, onPulldown
places
.sort(byDistance)
.map((place: PlaceRecord, placeId: string) => {
// const foodItems = foodItemsByPlace.get(placeId, new List());
return <PlaceTile key={placeId} place={place} foodItems={[]} />;
// const products = productsByPlace.get(placeId, new List());
return <PlaceTile key={placeId} place={place} products={[]} />;
})
.toList()}
</ScrollView>
@ -42,7 +42,7 @@ const PlacesList = ({ foodItemsByPlace = Map(), places, isRefreshing, onPulldown
export default compose(
pure,
// withFoodItemsGroupedByPlace,
// withProductsGroupedByPlace,
withPlaces,
withRouterContext,
withState('isRefreshing', 'setRefreshing', false),

View file

@ -1,14 +1,14 @@
// @flow
import React from 'react';
import { View } from 'react-native';
import typeof FoodItemRecord from '../records/FoodItemRecord';
import typeof ProductRecord from '../records/ProductRecord';
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 { withProductsGroupedByPlace } from '../enhancers/productsEnhancers';
import { withRegionState } from '../enhancers/mapViewEnhancers';
import typeof PlaceRecord from '../records/PlaceRecord';
@ -21,7 +21,7 @@ type Region = {
type Props = {
places: Map<string, PlaceRecord>,
foodItemsByPlace: Map<string, Map<string, FoodItemRecord>>,
productsByPlace: Map<string, Map<string, ProductRecord>>,
location: Location,
region: Region,
onRegionChange: Region => void,
@ -30,7 +30,7 @@ type Props = {
const PlacesMap = ({
places = Map(),
foodItemsByPlace = Map(),
productsByPlace = Map(),
region,
onRegionChange,
pushRoute,
@ -44,10 +44,10 @@ const PlacesMap = ({
onRegionChange={onRegionChange}>
{places
.map((place: PlaceRecord, placeId: string) => {
const foodItems = foodItemsByPlace.get(placeId, new Map());
const firstFoodItem = foodItems.first();
const products = productsByPlace.get(placeId, new Map());
const firstProduct = products.first();
if (!firstFoodItem) {
if (!firstProduct) {
return;
}
@ -56,14 +56,14 @@ const PlacesMap = ({
key={placeId}
title={place.name}
coordinate={{
latitude: firstFoodItem.latitude,
longitude: firstFoodItem.longitude,
latitude: firstProduct.latitude,
longitude: firstProduct.longitude,
}}>
<MapView.Callout
onPress={() => {
pushRoute(routeWithTitle(`/place/${placeId || ''}`, place.name));
}}>
<PlaceTile place={place} foodItems={foodItems} />
<PlaceTile place={place} products={products} />
</MapView.Callout>
</MapView.Marker>
);
@ -76,7 +76,7 @@ const PlacesMap = ({
export default compose(
renderComponent,
withFoodItemsGroupedByPlace,
withProductsGroupedByPlace,
withPlaces,
withRegionState
)(PlacesMap);

View file

@ -3,7 +3,7 @@ import React from 'react';
import { Button, Image, Text, View } from 'react-native';
import theme from '../ui-theme';
import { StrongText, SubText } from '../components/ItemTile';
import typeof FoodItemRecord from '../records/FoodItemRecord';
import typeof ProductRecord from '../records/ProductRecord';
import { type Quantity } from '../constants/QuantityConstants';
import typeof PlaceRecord from '../records/PlaceRecord';
import {
@ -15,8 +15,8 @@ import {
lifecycle,
} from 'recompose';
import IconButton from '../components/IconButton';
import { withFoodItem } from '../enhancers/foodItemEnhancers';
import { withPlaceForFoodItem } from '../enhancers/placeEnhancers';
import { withProduct } from '../enhancers/productsEnhancers';
import { withPlaceForProduct } from '../enhancers/placeEnhancers';
import Carousel from 'react-native-looped-carousel';
import CountBadge from '../components/CountBadge';
import { routeWithTitle, loginWithBackto } from '../helpers/RouteHelpers';
@ -36,11 +36,11 @@ import { withFaves } from '../enhancers/favesEnhancer';
import debounce from '../helpers/debounce';
import { withCurrentPath, withReplaceRoute } from '../enhancers/routeEnhancers';
const { foodItemDetails: style } = theme;
const { productDetails: style } = theme;
const stretchedStyle = { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 };
const FoodItemImages = ({
const ProductImages = ({
currentImage,
visibleImages,
changeCurrentImage,
@ -90,26 +90,26 @@ const contentTileStyle = {
};
type Props = {
foodItem: ?FoodItemRecord,
foodItemId: string,
product: ?ProductRecord,
productId: string,
place: PlaceRecord,
currentImage: number,
quantityModalOpen: boolean,
isAuthed: boolean,
changeCurrentImage: (index: number) => void,
updateAmount: (quantity: Quantity) => void,
updateQuantity: (arg: { foodItemId: string, quantity: Quantity }) => void,
updateQuantity: (arg: { productId: string, quantity: Quantity }) => void,
toggleQuantityModal: () => void,
addPhoto: () => void,
addImage: (arg: { foodItemId: string, imageUri: string }) => Promise<void>,
addImage: (arg: { productId: string, imageUri: string }) => Promise<void>,
addToFaves: () => void,
isFave: boolean,
deleteFromFaves: () => void,
};
export const FoodItemDetail = (props: Props) => {
export const ProductDetail = (props: Props) => {
const {
foodItem,
product,
place,
currentImage,
changeCurrentImage,
@ -123,15 +123,15 @@ export const FoodItemDetail = (props: Props) => {
deleteFromFaves,
} = props;
if (!foodItem || !place) {
if (!product || !place) {
return <Spinner />;
}
return (
<View style={{ ...theme.page.container }}>
<FoodItemImages
<ProductImages
currentImage={currentImage}
visibleImages={foodItem.images.filter(identity)}
visibleImages={product.images.filter(identity)}
changeCurrentImage={changeCurrentImage}
/>
<View style={{ ...contentTileStyle }}>
@ -153,10 +153,10 @@ export const FoodItemDetail = (props: Props) => {
justifyContent: 'space-between',
}}>
<Text style={{ fontSize: 42, color: 'black' }}>
{getQuantityLabelText(foodItem.quantity)}
{getQuantityLabelText(product.quantity)}
</Text>
<SubText>
Last updated at {moment(foodItem.lastupdated).format('h:mm A on MMM D, YYYY')}
Last updated at {moment(product.lastupdated).format('h:mm A on MMM D, YYYY')}
</SubText>
{isAuthed && (
<Button
@ -168,7 +168,7 @@ export const FoodItemDetail = (props: Props) => {
{!isAuthed && (
<RouterButton
title="Log in to update quantity"
to={loginWithBackto(`/foodItem/${foodItem.id}?loginAction=open-quantity-modal`)}
to={loginWithBackto(`/product/${product.id}?loginAction=open-quantity-modal`)}
color={theme.actionButton.speedDialActionIcon.backgroundColor}
/>
)}
@ -204,15 +204,15 @@ export const FoodItemDetail = (props: Props) => {
)}
</View>
{quantityModalOpen && (
<QuantityModal foodItem={foodItem} onUpdate={updateAmount} onClose={toggleQuantityModal} />
<QuantityModal product={product} onUpdate={updateAmount} onClose={toggleQuantityModal} />
)}
</View>
);
};
export default compose(
withFoodItem,
withPlaceForFoodItem,
withProduct,
withPlaceForProduct,
withUpdateQuantity,
withAuthed,
withFaves,
@ -221,33 +221,33 @@ export default compose(
withState('currentImage', 'changeCurrentImage', 0),
withState('quantityModalOpen', 'setQuantityModalOpen', false),
withHandlers({
updateAmount: ({ updateQuantity, foodItem }: Props) => async (quantity: Quantity) => {
if (!foodItem) {
updateAmount: ({ updateQuantity, product }: Props) => async (quantity: Quantity) => {
if (!product) {
return;
}
await updateQuantity({ foodItemId: foodItem.id, quantity });
Snackbar.show({ title: 'Food updated.', backgroundColor: 'black', color: 'white' });
await updateQuantity({ productId: product.id, quantity });
Snackbar.show({ title: 'Product updated.', backgroundColor: 'black', color: 'white' });
},
toggleQuantityModal: ({ quantityModalOpen, setQuantityModalOpen }) =>
debounce(() => {
setQuantityModalOpen(!quantityModalOpen);
}, 500),
addToFaves: ({ addFave, foodItemId, isAuthed, replaceRoute }) =>
addToFaves: ({ addFave, productId, isAuthed, replaceRoute }) =>
debounce(() => {
if (!isAuthed) {
replaceRoute(loginWithBackto(`/foodItem/${foodItemId}?loginAction=add-to-faves`));
replaceRoute(loginWithBackto(`/product/${productId}?loginAction=add-to-faves`));
} else {
addFave(foodItemId);
addFave(productId);
}
}, 500),
deleteFromFaves: ({ deleteFave, foodItemId }) => () => deleteFave(foodItemId),
deleteFromFaves: ({ deleteFave, productId }) => () => deleteFave(productId),
}),
withProps(props => ({
isFave: props.faves && !!props.faves.find(fave => get(fave, 'id') === props.foodItemId),
isFave: props.faves && !!props.faves.find(fave => get(fave, 'id') === props.productId),
loginAction: pathOr('', [1], /loginAction=(.+)(&?.*$)/.exec(props.currentPath)),
})),
onlyUpdateForKeys([
'foodItem',
'product',
'place',
'quantityModalOpen',
// 'imagesLoading',
@ -267,4 +267,4 @@ export default compose(
}
},
})
)(FoodItemDetail);
)(ProductDetail);

View file

@ -1,30 +1,19 @@
// @flow
import React from 'react';
import { View, Text, ScrollView, RefreshControl } from 'react-native';
import FoodItemTile from '../components/FoodItemTile';
import FoodItemList from '../components/FoodItemList';
import typeof FoodItemRecord from '../records/FoodItemRecord';
import { withFoodItemsAsSeq } from '../enhancers/foodItemEnhancers';
import { type SetSeq } from 'immutable';
import ProductTile from '../components/ProductTile';
import ProductsList from '../components/ProductsList';
import { withProductsAsSeq } from '../enhancers/productsEnhancers';
import { compose, pure, withState, withHandlers, lifecycle } from 'recompose';
import { withFilter } from '../enhancers/filterEnhancers';
import { withRouterContext } from '../enhancers/routeEnhancers';
import FullScreenMessage from '../components/FullScreenMessage';
import { withFaves } from '../enhancers/favesEnhancer';
type Props = {
foodItemsSeq: SetSeq<FoodItemRecord>,
isRefreshing: boolean,
onRefresh: () => Promise<any>,
onPulldown: () => {},
isFilterDirty: boolean,
};
const ProductList = props => {
const { productsSeq, isRefreshing, onPulldown, isFilterDirty } = props;
const FoodList = (props: Props) => {
const { foodItemsSeq, isRefreshing, onPulldown, isFilterDirty } = props;
const refreshing = isRefreshing || !foodItemsSeq;
const showNoResults = !isRefreshing && isFilterDirty && foodItemsSeq && !foodItemsSeq.size;
const refreshing = isRefreshing || !productsSeq;
const showNoResults = !isRefreshing && isFilterDirty && productsSeq && !productsSeq.size;
return (
<View style={{ flex: 1 }}>
@ -38,9 +27,9 @@ const FoodList = (props: Props) => {
<ScrollView
style={{ flex: 1 }}
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onPulldown} />}>
<FoodItemList foodItemsSeq={foodItemsSeq}>
{(foodItem: FoodItemRecord) => <FoodItemTile key={foodItem.id} foodItem={foodItem} />}
</FoodItemList>
<ProductsList productsSeq={productsSeq}>
{product => <ProductTile key={product.id} product={product} />}
</ProductsList>
</ScrollView>
)}
</View>
@ -50,7 +39,7 @@ const FoodList = (props: Props) => {
export default compose(
pure,
withRouterContext,
withFoodItemsAsSeq,
withProductsAsSeq,
withFilter,
withFaves,
withState('isRefreshing', 'setRefreshing', false),
@ -72,4 +61,4 @@ export default compose(
this.props.onPulldown();
},
})
)(FoodList);
)(ProductList);

View file

@ -1,7 +1,6 @@
// @flow
import { compose, branch } from 'recompose';
import FoodList from './FoodList';
import FoodMap from './FoodMap';
import ProductList from './ProductList';
import ProductMap from './ProductsMap';
import { withRouterContext, withViewMode, withPushRoute } from '../enhancers/routeEnhancers';
export default compose(
@ -10,5 +9,5 @@ export default compose(
withPushRoute,
branch(({ viewMode }) => {
return viewMode === 'map';
}, FoodMap)
)(FoodList);
}, ProductMap)
)(ProductList);

View file

@ -1,42 +1,24 @@
// @flow
import React from 'react';
import { View, Image } from 'react-native';
import typeof FoodItemRecord from '../records/FoodItemRecord';
import { withFoodItems } from '../enhancers/foodItemEnhancers';
import { withProducts } from '../enhancers/productsEnhancers';
import { withLocation } from '../enhancers/locationEnhancers';
import { compose, renderComponent, withState, withProps } from 'recompose';
import { Map } from 'immutable';
import MapView, { Marker } from 'react-native-maps';
import { routeWithTitle } from '../helpers/RouteHelpers';
import FoodItemTile from '../components/FoodItemTile';
import ProductTile from '../components/ProductTile';
import { getZoomBox } from '../helpers/CoordinatesHelpers';
import LOCATION_DOT from '../../static/location-dot.png';
type Region = {
latitude: number,
longitude: number,
latitudeDelta: number,
longitudeDelta: number,
};
type Props = {
foodItemsMap: Map<string, FoodItemRecord>,
location: Location,
initialRegion: Region,
region: Region,
onRegionChange: (Region) => void,
pushRoute: () => {},
};
const FoodMap = ({
foodItemsMap,
const ProductsMap = ({
productsMap,
region,
onRegionChange,
pushRoute,
initialRegion,
location,
}: Props) => {
if (!foodItemsMap) {
}) => {
if (!productsMap) {
return null;
}
@ -46,21 +28,21 @@ const FoodMap = ({
region={region || initialRegion}
style={{ flex: 1 }}
onRegionChangeComplete={onRegionChange}>
{foodItemsMap
.map((foodItem, id) => {
{productsMap
.map((product, id) => {
return (
<MapView.Marker
key={id}
title={foodItem.name}
title={product.name}
coordinate={{
latitude: foodItem.latitude || 0,
longitude: foodItem.longitude || 0,
latitude: product.latitude || 0,
longitude: product.longitude || 0,
}}>
<MapView.Callout
onPress={() => {
pushRoute(routeWithTitle(`/foodItem/${foodItem.id || ''}`, foodItem.name));
pushRoute(routeWithTitle(`/product/${product.id || ''}`, product.name));
}}>
<FoodItemTile foodItem={foodItem} />
<ProductTile product={product} />
</MapView.Callout>
</MapView.Marker>
);
@ -76,8 +58,8 @@ const FoodMap = ({
);
};
const withInitialRegionProp = withProps(({ foodItemsMap = Map(), location }) => {
const zoomBox = getZoomBox(foodItemsMap, location.coords);
const withInitialRegionProp = withProps(({ productsMap = Map(), location }) => {
const zoomBox = getZoomBox(productsMap, location.coords);
return {
initialRegion: {
@ -91,8 +73,8 @@ const withInitialRegionProp = withProps(({ foodItemsMap = Map(), location }) =>
export default compose(
renderComponent,
withFoodItems,
withProducts,
withLocation,
withState('region', 'onRegionChange', null),
withInitialRegionProp
)(FoodMap);
)(ProductsMap);

View file

@ -11,7 +11,7 @@ const ImageRecord = Record({
url: '',
username: '',
date: Date.now(),
foodItemId: '',
productId: '',
});
export type ImageFragment = { id: string, images: OrderedSet<typeof ImageRecord> };
@ -19,7 +19,7 @@ export type ImageFragment = { id: string, images: OrderedSet<typeof ImageRecord>
export const buildImageRecord = (imageRaw: ImageRaw) => {
return new ImageRecord({
...imageRaw,
foodItemId: imageRaw.food_item_id,
productId: imageRaw.food_item_id,
});
};

View file

@ -1,11 +1,10 @@
import { Set, Record } from 'immutable';
import { type RawFoodItem } from '../apis/FoodItemsApi';
import { type RawProduct } from '../apis/ProductsApi';
import { type Category } from '../constants/CategoryConstants';
import { type Quantity } from '../constants/QuantityConstants';
import ImageRecord, { buildImageRecord } from '../records/ImageRecord';
import { map, pathOr } from 'ramda';
import ImageRecord, { buildImageRecord } from './ImageRecord';
export type FoodItem = {
export type Product = {
id: ?string,
name: string,
placeId: ?number,
@ -20,7 +19,7 @@ export type FoodItem = {
lastupdated: number,
};
const FoodRecordDefaults: FoodItem = {
const FoodRecordDefaults: Product = {
id: '',
name: '',
placeId: null,
@ -36,19 +35,19 @@ const FoodRecordDefaults: FoodItem = {
lastupdated: 0,
};
const FoodItemRecord = Record(FoodRecordDefaults, 'FoodItemRecord');
const ProductRecord = Record(FoodRecordDefaults, 'ProductRecord');
export const createFoodItem = (foodItemRaw: ?RawFoodItem) => {
if (!foodItemRaw) {
return foodItemRaw;
export const createProduct = (productRaw: ?RawProduct) => {
if (!productRaw) {
return productRaw;
}
return new FoodItemRecord({
...foodItemRaw,
placeType: foodItemRaw.placeType,
thumbImage: foodItemRaw.thumbimage,
// images: new Set(map(buildImageRecord, pathOr([], ['images'], foodItemRaw))),
images: new Set([buildImageRecord({ url: foodItemRaw.thumbimage })]),
return new ProductRecord({
...productRaw,
placeType: productRaw.placeType,
thumbImage: productRaw.thumbimage,
// images: new Set(map(buildImageRecord, pathOr([], ['images'], productRaw))),
images: new Set([buildImageRecord({ url: productRaw.thumbimage })]),
});
};
export default FoodItemRecord;
export default ProductRecord;

View file

@ -1,19 +0,0 @@
// @flow
import { ReplaySubject } from 'rxjs';
import FoodItemRecord from '../records/FoodItemRecord';
type CreateFoodItemState = {
foodItem: FoodItemRecord,
loading: boolean,
error?: ?Error,
};
const multicaster: ReplaySubject<CreateFoodItemState> = new ReplaySubject();
export function emitter(val: CreateFoodItemState) {
multicaster.next(val);
}
emitter({ foodItem: new FoodItemRecord(), loading: false, error: null });
export default multicaster;

View file

@ -0,0 +1,19 @@
// @flow
import { ReplaySubject } from 'rxjs';
import ProductRecord from '../records/ProductRecord';
type CreateProductState = {
product: ProductRecord,
loading: boolean,
error?: ?Error,
};
const multicaster: ReplaySubject<CreateProductState> = new ReplaySubject();
export function emitter(val: CreateProductState) {
multicaster.next(val);
}
emitter({ product: new ProductRecord(), loading: false, error: null });
export default multicaster;

View file

@ -2,7 +2,7 @@
import { BehaviorSubject } from 'rxjs';
import { Record, List, get } from 'immutable';
import filter$ from './FilterStream';
import foodItems$ from './FoodItemsStream';
import products$ from './ProductsStream';
import { test } from 'ramda';
export type Fave = {
@ -22,20 +22,20 @@ export function emitOne(fave: ?Fave) {
}
export default observable
.combineLatest(foodItems$)
.map(([faves, foodItems]) => {
return faves && faves.map(fave => get(foodItems, get(fave, 'food_item_id')));
.combineLatest(products$)
.map(([faves, products]) => {
return faves && faves.map(fave => get(products, get(fave, 'food_item_id')));
})
.combineLatest(filter$)
.map(([faveFoodItems, filter]) => {
.map(([faveProducts, filter]) => {
const filterTest = filter.search && test(new RegExp(filter.search, 'i'));
return (
faveFoodItems &&
faveFoodItems.filter(faveFoodItem => {
if (!faveFoodItem) {
faveProducts &&
faveProducts.filter(faveProduct => {
if (!faveProduct) {
return false;
}
return !filterTest || filterTest(faveFoodItem.name);
return !filterTest || filterTest(faveProduct.name);
})
);
});

View file

@ -1,68 +0,0 @@
//@flow
import { BehaviorSubject, Observable } from 'rxjs';
import FoodItemRecord, { createFoodItem, type FoodItem } from '../records/FoodItemRecord';
import { setById } from '../helpers/ImmutableHelpers';
import { Map, type Record } from 'immutable';
import location$ from './LocationStream';
import { getFoodItems, type FoodItemsForLocation } from '../apis/FoodItemsApi';
import Filter$ from './FilterStream';
import Quantity$ from './QuantityStream';
import type { QuantityFragment } from '../constants/QuantityConstants';
import { type ImageFragment } from '../records/ImageRecord';
import Image$ from './ImagesStream';
const foodItemSubject: BehaviorSubject<FoodItemRecord> = new BehaviorSubject();
export function emitter(val?: ?FoodItemRecord) {
foodItemSubject.next(val);
}
emitter(null);
const manualUpdate$ = foodItemSubject.scan(
(foodItemMap: Map<string, FoodItemRecord>, foodItem: FoodItemRecord) => {
return foodItem ? foodItemMap.set(foodItem.id, foodItem) : foodItemMap;
},
Map()
);
const fetchedFoodItems$ = Filter$.combineLatest(location$)
.debounceTime(200)
.mergeMap(([filter, loc]) => {
if (!loc) {
return Promise.resolve({});
}
return getFoodItems({ filter, loc });
})
.map(({ fooditems }: FoodItemsForLocation) => {
if (fooditems) {
return fooditems.map(createFoodItem).reduce(setById, new Map());
}
return null;
});
export default fetchedFoodItems$
.combineLatest(manualUpdate$, (foodItemMap: Map<string, FoodItemRecord>, manualUpdates) => {
if (foodItemMap) {
return foodItemMap.mergeDeep(manualUpdates);
}
})
.combineLatest(
Quantity$,
(
foodItems: ?Map<string, FoodItemRecord>,
quantitiesFromStream: Map<string, QuantityFragment>
) => {
if (foodItems) {
return foodItems.mergeDeep(quantitiesFromStream);
}
}
)
.combineLatest(
Image$,
(foodItems: ?Map<string, FoodItemRecord>, latestFromImages$: Map<String, ImageFragment>) => {
if (foodItems) {
return foodItems.mergeDeep(latestFromImages$);
}
}
);

View file

@ -13,18 +13,15 @@ export function emit(val: ?ImageRaw) {
// force our observable to emit an initial empty map so that food items will load
emit(null);
export default observable.scan(
(imagesByFoodItemId: Map<string, ImageFragment>, image: ImageRaw) => {
export default observable.scan((imagesByProductId: Map<string, ImageFragment>, image: ImageRaw) => {
if (!image || !image.food_item_id) {
return imagesByFoodItemId;
return imagesByProductId;
}
return imagesByFoodItemId.update(image.food_item_id, ({ images = OrderedSet() } = {}) => {
return imagesByProductId.update(image.food_item_id, ({ images = OrderedSet() } = {}) => {
return {
id: image.food_item_id,
images: images.add(buildImageRecord(image)),
};
});
},
new Map()
);
}, new Map());

View file

@ -1,14 +1,14 @@
import { ReplaySubject, Observable } from 'rxjs';
import { ReplaySubject } from 'rxjs';
import { buildPlaceRecord } from '../records/PlaceRecord';
import { Map } from 'immutable';
import { findNearbyPlaces, getPlaceDetails } from '../apis/GooglePlacesApi';
import { findNearbyPlaces } from '../apis/GooglePlacesApi';
import { path } from 'ramda';
import { type GooglePlaceObj } from '../records/PlaceRecord';
import { setById } from '../helpers/ImmutableHelpers';
import location$ from './LocationStream';
import filter$ from './FilterStream';
import FilterRecord from '../records/FilterRecord';
import foodItems$ from './FoodItemsStream';
// import products$ from './ProductsStream';
import PlaceRecord from '../records/PlaceRecord';
import geodist from 'geodist';
@ -22,9 +22,9 @@ export function emitter(val?: ?PlaceRecord) {
filter$.subscribe(() => emitter(null));
// foodItems$
// .mergeMap((foodItems = Map()) => Observable.from(foodItems.toArray()))
// .mergeMap(([foodItemId, foodItem]) => getPlaceDetails(foodItem))
// products$
// .mergeMap((products = Map()) => Observable.from(products.toArray()))
// .mergeMap(([productId, product]) => getPlaceDetails(product))
// .map(buildPlaceRecord)
// .subscribe(emitter);
@ -63,9 +63,22 @@ location$
})
.subscribe(places => places && places.map(emitter));
export default placesSubject.scan((places, place) => {
export default placesSubject.scan(
(typeToPlace, place) => {
if (!place) {
return null;
return typeToPlace;
}
return setById(places || new Map(), place);
});
const { placeType } = place;
if (Object.keys(typeToPlace).includes(placeType)) {
return {
...typeToPlace,
[placeType]: [...typeToPlace[placeType], place],
};
}
return typeToPlace;
},
{ HEB: [], WholeFoods: [] }
);

View file

@ -0,0 +1,81 @@
//@flow
import { BehaviorSubject } from 'rxjs';
import ProductRecord, { createProduct } from '../records/ProductRecord';
import { setById } from '../helpers/ImmutableHelpers';
import { Map, set, getIn } from 'immutable';
import location$ from './LocationStream';
import { getProducts, type ProductsForLocation } from '../apis/ProductsApi';
import Filter$ from './FilterStream';
import Quantity$ from './QuantityStream';
import type { QuantityFragment } from '../constants/QuantityConstants';
// import { type ImageFragment } from '../records/ImageRecord';
// import Image$ from './ImagesStream';
import places$ from './PlacesStream';
const productSubject: BehaviorSubject<ProductRecord> = new BehaviorSubject();
export function emitter(val?: ?ProductRecord) {
productSubject.next(val);
}
emitter(null);
const manualUpdate$ = productSubject.scan(
(productMap: Map<string, ProductRecord>, product: ProductRecord) => {
return product ? productMap.set(product.id, product) : productMap;
},
Map()
);
const fetchedProducts$ = Filter$.combineLatest(location$)
.debounceTime(200)
.mergeMap(([filter, loc]) => {
if (!loc) {
return Promise.resolve({});
}
return getProducts({ filter, loc });
})
.map(({ products }: ProductsForLocation) => {
if (products) {
return products.map(createProduct).reduce(setById, new Map());
}
return null;
});
export default fetchedProducts$
.combineLatest(manualUpdate$, (productMap: Map<string, ProductRecord>, manualUpdates) => {
if (productMap) {
return productMap.mergeDeep(manualUpdates);
}
})
.combineLatest(
Quantity$,
(
products: ?Map<string, ProductRecord>,
quantitiesFromStream: Map<string, QuantityFragment>
) => {
if (products) {
return products.mergeDeep(quantitiesFromStream);
}
}
)
.combineLatest(places$, (products, places) => {
if (!places || !products) {
return products;
}
const getPlaceIdForNearest = placeType => getIn(places, [placeType, 0, 'id'], '');
return products.map(product =>
set(product, 'placeId', getPlaceIdForNearest(product.placeType))
);
});
// )
// .combineLatest(
// Image$,
// (products: ?Map<string, ProductRecord>, latestFromImages$: Map<String, ImageFragment>) => {
// if (products) {
// return products.mergeDeep(latestFromImages$);
// }
// }
// );

View file

@ -12,13 +12,16 @@ export function emit(val: ?QuantityResponse) {
// force our observable to emit an initial empty map so that food items will load
emit(null);
export default observable.scan((quantitiesByFoodItemId: Map<string, QuantityFragment>, quantity?: QuantityResponse) => {
export default observable.scan(
(quantitiesByProductId: Map<string, QuantityFragment>, quantity?: QuantityResponse) => {
if (!quantity) {
return quantitiesByFoodItemId;
return quantitiesByProductId;
}
return quantitiesByFoodItemId.set(quantity.food_item_id, {
return quantitiesByProductId.set(quantity.food_item_id, {
quantity: quantity.quantity,
lastupdated: quantity.date,
});
}, new Map());
},
new Map()
);

View file

@ -96,7 +96,7 @@ export default {
defaultColor: '#000000',
selectedColor: '#017C9A',
},
foodItemDetails: {
productDetails: {
actionIconColor: '#017C9A',
},
placeDetails: {