diff --git a/android/app/build.gradle b/android/app/build.gradle index e8b35a5..937c2de 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -91,7 +91,7 @@ android { applicationId "com.aretherecookies" minSdkVersion 16 targetSdkVersion 22 - versionCode 1 + versionCode 6 versionName "1.0" ndk { abiFilters "armeabi-v7a", "x86" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6af0fe5..2640918 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,13 +2,13 @@ xmlns:tools="http://schemas.android.com/tools" package="com.aretherecookies" android:versionCode="1" - android:versionName="1.0"> + android:versionName="1.0.1"> - + } tweenDuration={150}> - - - - - - - - { - if (!AuthManager.isLoggedIn()) { - return ; - } - return ; - }} - /> - + + + + + + + + + + + { + if (!AuthManager.isLoggedIn()) { + return ; + } + return ; + }} + /> + + diff --git a/js/apis/FoodItemsApi.js b/js/apis/FoodItemsApi.js index e12174d..c04eea0 100644 --- a/js/apis/FoodItemsApi.js +++ b/js/apis/FoodItemsApi.js @@ -27,7 +27,7 @@ export type RawFoodItem = { export type FoodItemsForLocation = { orderby: string, filter: FoodItemsFilter, - fooditems: Array, + fooditems: ?Array, }; export const getFoodItems = memoize( @@ -38,7 +38,9 @@ export const getFoodItems = memoize( loc: Position, filter: FilterRecord, }): Promise => { - const { coords: { latitude: lat, longitude: lng } } = loc; + const { + coords: { latitude: lat, longitude: lng }, + } = loc; const { orderby, categories, radius } = filter; try { diff --git a/js/apis/PositionApi.js b/js/apis/PositionApi.js new file mode 100644 index 0000000..803e577 --- /dev/null +++ b/js/apis/PositionApi.js @@ -0,0 +1,27 @@ +// @flow +import { emitter } from '../streams/LocationStream'; + +export const getCurrentPosition = () => { + // $FlowFixMe: maximumAge not found on object literal + navigator.geolocation.getCurrentPosition( + (pos: Position) => emitter(pos), + err => { + throw err; + }, + { + enableHighAccuracy: false, + timeout: 2000, + } + ); +}; + +// TODO actually implement geolocation for zipcode into lat/lng +export const getPositionFromZip = () => { + const dummyPos: any = { + coords: { + latitude: 30.267, + longitude: -97.7485, + }, + }; + emitter(dummyPos); +}; diff --git a/js/components/FoodItemList.js b/js/components/FoodItemList.js index c9854de..4d6b6e3 100644 --- a/js/components/FoodItemList.js +++ b/js/components/FoodItemList.js @@ -3,11 +3,12 @@ import React, { Component } from 'react'; import { View } from 'react-native'; import { type SetSeq } from 'immutable'; import FoodItemRecord from '../records/FoodItemRecord'; -import { pure, compose } from 'recompose'; -import { withFoodItemsAsSeq } from '../enhancers/foodItemEnhancers'; +import { pure } from 'recompose'; import R from 'ramda'; -const matchString = R.memoize((match = '', str = '') => str.toLowerCase().includes(match.toLowerCase())); +const matchString = R.memoize((match = '', str = '') => + str.toLowerCase().includes(match.toLowerCase()) +); const filterBy = (filter?: string = '') => (foodItemsSeq: SetSeq) => { if (!filter) { @@ -27,7 +28,8 @@ const sortByDistance = (foodItemsSeq: SetSeq) => { return foodItemsSeq.sort((left, right) => left.distance - right.distance); }; -const intoArray = (foodItemsSeq: SetSeq) => foodItemsSeq.toArray(); +const intoArray = (foodItemsSeq: SetSeq) => + foodItemsSeq ? foodItemsSeq.toArray() : []; class FoodItemList extends Component { static displayName = 'FoodItemList'; @@ -37,16 +39,21 @@ class FoodItemList extends Component { limit?: number, foodItemsSeq: SetSeq, renderFoodItem: (foodItem: typeof FoodItemRecord) => Component<*, *, *>, + foodItemsLoading: boolean, }; render() { const { filter, foodItemsSeq, renderFoodItem, limit } = this.props; + + if (!foodItemsSeq) { + return null; + } + const getItems = R.compose(intoArray, limitBy(limit), sortByDistance, filterBy(filter)); const items = getItems(foodItemsSeq); + return {items.map(renderFoodItem)}; } } -const enhance = compose(pure, withFoodItemsAsSeq); - -export default enhance(FoodItemList); +export default pure(FoodItemList); diff --git a/js/components/ItemTile.js b/js/components/ItemTile.js index 7f1be29..b03dded 100644 --- a/js/components/ItemTile.js +++ b/js/components/ItemTile.js @@ -42,7 +42,7 @@ export const SubText = ({ children, style = {} }: { children?: string, style?: O }; export const TileBox = ({ children, style = {} }: { children?: any, style?: Object }) => ( - + {children} ); diff --git a/js/components/PlaceTile.js b/js/components/PlaceTile.js index baf0dab..af1c59d 100644 --- a/js/components/PlaceTile.js +++ b/js/components/PlaceTile.js @@ -6,7 +6,7 @@ import { List } from 'immutable'; import FoodItemRecord from '../records/FoodItemRecord'; import typeof PlaceRecord from '../records/PlaceRecord'; import { Link } from 'react-router-native'; -import { Thumbnail, StrongText, SubText } from './ItemTile'; +import { TileBox, Thumbnail, StrongText, SubText } from './ItemTile'; import { routeWithTitle } from '../helpers/RouteHelpers'; import { getCategories } from '../helpers/CategoryHelpers'; import { type Map } from 'immutable'; @@ -47,13 +47,15 @@ export default pure(({ place, foodItems }: PlaceTileProps) => { to={routeWithTitle(`/place/${place.id || ''}`, place.name)} underlayColor={theme.itemTile.pressHighlightColor} > - - - - {`${place.name} - ${distance} mi`} - {getCategoriesText(foodItems)} - {getHoursText(place.hours)} - + + + + + {`${place.name} - ${distance} mi`} + {getCategoriesText(foodItems)} + {getHoursText(place.hours)} + + ); diff --git a/js/components/RadiusPicker.js b/js/components/RadiusPicker.js index 62d40c4..0fae8a9 100644 --- a/js/components/RadiusPicker.js +++ b/js/components/RadiusPicker.js @@ -19,7 +19,7 @@ const renderItem = (selected: number) => (item: number) => ( type radiusPickerProps = { selected: number, onValueChange: Function, style: Object }; const radiusPicker = pure(({ selected, onValueChange, style }: radiusPickerProps) => ( - + {CHOICES.map(renderItem(selected))} diff --git a/js/enhancers/foodItemEnhancers.js b/js/enhancers/foodItemEnhancers.js index 7b711d0..0ef1aed 100644 --- a/js/enhancers/foodItemEnhancers.js +++ b/js/enhancers/foodItemEnhancers.js @@ -11,7 +11,7 @@ export const withFoodItems = mapPropsStream(props$ => props$.combineLatest(FoodItems$, (props, foodItems) => { return { ...props, - foodItemsMap: foodItems, + foodItemsMap: foodItems && foodItems, }; }) ); @@ -20,7 +20,7 @@ export const withFoodItemsAsSeq = mapPropsStream(props$ => props$.combineLatest(FoodItems$, (props, foodItems) => { return { ...props, - foodItemsSeq: foodItems.valueSeq(), + foodItemsSeq: foodItems && foodItems.valueSeq(), }; }) ); @@ -33,10 +33,9 @@ export const withFoodItemIdFromRoute = withProps((props: { match: { params: { id export const withFoodItem = compose( withFoodItems, withFoodItemIdFromRoute, - withProps((props: { foodItemsMap: Map, foodItemId: number }) => { + withProps((props: { foodItemsMap: ?Map, foodItemId: number }) => { const { foodItemsMap, foodItemId } = props; - const foodItem = foodItemsMap.get(foodItemId); - return { foodItem }; + return { foodItem: foodItemsMap && foodItemsMap.get(foodItemId) }; }) ); @@ -49,10 +48,12 @@ export const withFoodItemPlaceId = withProps((props: { foodItem: FoodItemRecord export const withFoodItemsGroupedByPlace = compose( withFoodItems, - withProps((props: { foodItemsMap: Map }) => { - const foodItemsByPlace = props.foodItemsMap.groupBy(foodItem => foodItem.placeId); + withProps((props: { foodItemsMap: ?Map }) => { + if (!props.foodItemsMap) { + return {}; + } return { - foodItemsByPlace, + foodItemsByPlace: props.foodItemsMap.groupBy(foodItem => foodItem.placeId), }; }) ); diff --git a/js/modals/FilterModal.js b/js/modals/FilterModal.js index a52ebc0..062f00c 100644 --- a/js/modals/FilterModal.js +++ b/js/modals/FilterModal.js @@ -69,29 +69,29 @@ const FilterModal = (props: Props) => { })} - Sort By + Sort By - Search Radius + Search Radius - + - Cancel + CANCEL - Apply + APPLY diff --git a/js/pages/FoodItemDetail.js b/js/pages/FoodItemDetail.js index 7ccc76e..e34e157 100644 --- a/js/pages/FoodItemDetail.js +++ b/js/pages/FoodItemDetail.js @@ -77,7 +77,7 @@ const contentTileStyle = { }; type Props = { - foodItem: FoodItemRecord, + foodItem: ?FoodItemRecord, place: PlaceRecord, currentImage: number, quantityModalOpen: boolean, @@ -196,12 +196,18 @@ export default compose( withState('imagesLoading', 'setImagesLoading', true), withHandlers({ addPhoto: ({ addImage, foodItem, setImagesLoading }: Props) => async () => { + if (!foodItem) { + return; + } const imageUri = await openImagePicker(); setImagesLoading(true); await addImage({ foodItemId: foodItem.id, imageUri }); setImagesLoading(false); }, updateAmount: ({ updateQuantity, foodItem }: Props) => (quantity: Quantity) => { + if (!foodItem) { + return; + } updateQuantity({ foodItemId: foodItem.id, quantity }); }, toggleQuantityModal: ({ quantityModalOpen, setQuantityModalOpen }) => () => { diff --git a/js/pages/FoodList.js b/js/pages/FoodList.js index 5b4169d..de04db0 100644 --- a/js/pages/FoodList.js +++ b/js/pages/FoodList.js @@ -7,6 +7,9 @@ import { routeWithTitle } from '../helpers/RouteHelpers'; import FoodItemList from '../components/FoodItemList'; import typeof FoodItemRecord from '../records/FoodItemRecord'; import FilterModal from '../modals/FilterModal'; +import { withFoodItemsAsSeq } from '../enhancers/foodItemEnhancers'; +import { type SetSeq } from 'immutable'; +import Spinner from 'react-native-loading-spinner-overlay'; import theme from '../ui-theme'; @@ -14,13 +17,14 @@ const renderFoodItem = (foodItem: FoodItemRecord) => ( ); -export default class FoodList extends Component { +class FoodList extends Component { static displayName = 'FoodList'; props: { pushRoute: Object => void, isFilterModalOpen: boolean, toggleFilterModal: (val: boolean) => void, + foodItemsSeq: SetSeq, }; addFoodItem = () => { @@ -45,12 +49,13 @@ export default class FoodList extends Component { }; render() { - const { isFilterModalOpen } = this.props; + const { isFilterModalOpen, foodItemsSeq } = this.props; return ( + - + { + return ( + + + + We need to use your location to bring you the best experience possible. + + + + + + I'd rather not. + + + ); +}; + +export default LandingPage; diff --git a/js/pages/List.js b/js/pages/List.js index 118c189..b3f2978 100644 --- a/js/pages/List.js +++ b/js/pages/List.js @@ -5,10 +5,16 @@ import Places from './Places'; import ScrollableTabView from 'react-native-scrollable-tab-view'; import theme from '../ui-theme.js'; import { type RoutingContextFlowTypes, routingContextPropTypes } from '../routes'; +import { getSearch } from '../helpers/RouteHelpers'; +import { getCurrentPosition, getPositionFromZip } from '../apis/PositionApi'; const tabs = ['food', 'places']; -const getTabIndex = ({ match: { params: { type = '' } } }: RoutingContextFlowTypes): number => { +const getTabIndex = ({ + match: { + params: { type = '' }, + }, +}: RoutingContextFlowTypes): number => { return tabs.indexOf(type) || 0; }; @@ -28,6 +34,17 @@ class List extends Component { tabView: TabView; + // TODO convert this component to SFC using recompose + // also, figure out a less lifecyle-y way to do this request + componentDidMount() { + const { positionBy = 'zip' } = getSearch(this.context); + if (positionBy === 'location') { + getCurrentPosition(); + } else { + getPositionFromZip(); + } + } + updateTabRoute = ({ i }: { i: number }) => { const currentTab = getTabIndex(this.props); @@ -68,8 +85,7 @@ class List extends Component { tabBarInactiveTextColor={theme.topTabs.selectedTextColor} prerenderingSiblingsNumber={Infinity} onChangeTab={this.updateTabRoute} - initialPage={getTabIndex(this.props)} - > + initialPage={getTabIndex(this.props)}> diff --git a/js/streams/FoodItemsStream.js b/js/streams/FoodItemsStream.js index 1c7ee74..ee315a1 100644 --- a/js/streams/FoodItemsStream.js +++ b/js/streams/FoodItemsStream.js @@ -27,34 +27,51 @@ const manualUpdate$ = foodItemSubject.scan( Map() ); -const fetchedFoodItems$ = location$ - .combineLatest(Filter$) - .mergeMap(([loc, filter]: [Position, FilterRecord]) => getFoodItems({ loc, filter })) - .map(({ fooditems = [] }: FoodItemsForLocation) => { - return fooditems.map(createFoodItem).reduce(setById, new Map()); +const fetchedFoodItems$ = Filter$.combineLatest(location$) + .mergeMap(([filter, loc]: [Position, FilterRecord]) => { + if (loc) { + return getFoodItems({ loc, filter }); + } + return Promise.resolve({}); + }) + .map(({ fooditems }: FoodItemsForLocation) => { + if (fooditems) { + return fooditems.map(createFoodItem).reduce(setById, new Map()); + } + return null; }); export default fetchedFoodItems$ .combineLatest(manualUpdate$, (foodItemMap: Map, manualUpdates) => { - return foodItemMap.mergeDeep(manualUpdates); + if (foodItemMap) { + return foodItemMap.mergeDeep(manualUpdates); + } }) .combineLatest( Quantity$, ( - foodItems: Map, + foodItems: ?Map, quantitiesFromStream: Map ) => { - return foodItems.mergeDeepWith( - (foodItem, foodItemQuantities) => foodItem.merge(foodItemQuantities), - quantitiesFromStream - ); + if (foodItems) { + return foodItems.mergeDeepWith( + (foodItem, foodItemQuantities) => foodItem.merge(foodItemQuantities), + quantitiesFromStream + ); + } } ) .combineLatest( Image$, - (foodItems: Map, latestFromImages$: Map) => - // $FlowFixMe this type is incompatible with the expected param type of object type - foodItems.mergeDeepWith((foodItem: FoodItemRecord, imageFragment: ImageFragment) => { - return foodItem.set('images', imageFragment.images); - }, latestFromImages$) + (foodItems: ?Map, latestFromImages$: Map) => { + if (foodItems) { + return foodItems.mergeDeepWith( + // $FlowFixMe + (foodItem: FoodItemRecord, imageFragment: ImageFragment) => { + return foodItem.set('images', imageFragment.images); + }, + latestFromImages$ + ); + } + } ); diff --git a/js/streams/LocationStream.js b/js/streams/LocationStream.js index 4d6541b..bd3ec1e 100644 --- a/js/streams/LocationStream.js +++ b/js/streams/LocationStream.js @@ -1,16 +1,12 @@ // @flow -import { Observable, type Observer } from 'rxjs'; +import { ReplaySubject } from 'rxjs'; -export default Observable.create((obs: Observer): Observable => { - // $FlowFixMe: property maximumAge not found on object literal - navigator.geolocation.getCurrentPosition( - (pos: Position) => obs.next(pos), - err => { - throw err; - }, - { - enableHighAccuracy: false, - timeout: 2000, - } - ); -}); +const multicaster: ReplaySubject = new ReplaySubject(); + +export function emitter(val: ?Position) { + multicaster.next(val); +} + +emitter(null); + +export default multicaster; diff --git a/js/streams/PlacesStream.js b/js/streams/PlacesStream.js index 6aa8c19..6c168c7 100644 --- a/js/streams/PlacesStream.js +++ b/js/streams/PlacesStream.js @@ -22,7 +22,7 @@ const safeGetPlaceDetails = memoize((placeId: string): Promise }); const Places$ = foodItems$ - .mergeMap((foodItems: Map): Observable => { + .mergeMap((foodItems: Map = Map()): Observable => { return Observable.from(foodItems.toArray().map(foodItem => foodItem.placeId)); }) .distinct() diff --git a/js/ui-theme.js b/js/ui-theme.js index 5aa4454..40eab0d 100644 --- a/js/ui-theme.js +++ b/js/ui-theme.js @@ -7,15 +7,18 @@ import { COLOR } from 'react-native-material-ui'; export const primaryColor = '#6d5354'; +export const palette = { + primaryColor, + accentColor: '#0E6E9E', + disabledColor: COLOR.grey500, + facebook: '#3B5998', + google: '#DB4437', + errorColor: '#B92D00', +}; + export default { - palette: { - primaryColor, - accentColor: '#0E6E9E', - disabledColor: COLOR.grey500, - facebook: '#3B5998', - google: '#DB4437', - errorColor: '#B92D00', - }, + statusBarColor: '#412A2B', + palette: palette, toolbar: { titleText: { color: COLOR.white }, leftElement: { color: COLOR.white }, @@ -26,6 +29,11 @@ export default { elevation: 0, }, }, + actionButton: { + speedDialActionIcon: { + backgroundColor: '#48A0CC', + }, + }, checkbox: { icon: { color: '#0E6E9E', @@ -45,14 +53,29 @@ export default { selectedTextColor: 'rgba(255, 255, 255, 0.7)', backgroundColor: primaryColor, }, + modalButton: { + fontSize: 14, + fontWeight: '500', + color: palette.accentColor, + paddingLeft: 30, + }, + modalDropDown: { + /* fontSize: 16, + fontWeight: 'bold', + color: 'black', */ + height: 40, + width: 150, + }, itemTile: { thumbnailSize: 50, - thumbnailColor: COLOR.grey500, + thumbnailColor: COLOR.grey400, itemNameStyle: { + fontSize: 16, color: COLOR.black, }, itemPlaceStyle: { - color: COLOR.grey500, + color: COLOR.grey700, + paddingTop: 3, }, availableCountStyle: { fontSize: 25, diff --git a/package.json b/package.json index 4b3c530..e35ed47 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,17 @@ { "name": "aretherecookies", +<<<<<<< HEAD "version": "1.6.0", +======= + "version": "1.0.1", +>>>>>>> master "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" + "android:dev": "react-native run-android && react-native start", + "android:release": "cd android && ./gradlew assembleRelease" }, "dependencies": { "babel-preset-es2015": "^6.24.0", diff --git a/static/atc-cookie-logo.png b/static/atc-cookie-logo.png new file mode 100644 index 0000000..a23251a Binary files /dev/null and b/static/atc-cookie-logo.png differ