Merge remote-tracking branch 'origin/master'

This commit is contained in:
Erick Clark 2018-05-02 13:24:41 -05:00
commit a98fcc247c
23 changed files with 259 additions and 105 deletions

View file

@ -91,7 +91,7 @@ android {
applicationId "com.aretherecookies"
minSdkVersion 16
targetSdkVersion 22
versionCode 1
versionCode 6
versionName "1.0"
ndk {
abiFilters "armeabi-v7a", "x86"

View file

@ -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">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> -->
<uses-sdk
android:minSdkVersion="16"

Binary file not shown.

Binary file not shown.

View file

@ -1,13 +1,13 @@
//@flow
import React, { Component } from 'react';
import { View } from 'react-native';
import { View, StatusBar } from 'react-native';
import { ThemeProvider } from 'react-native-material-ui';
import theme from './ui-theme';
import DrawerMenu from './pages/DrawerMenu';
import rxjsconfig from 'recompose/rxjsObservableConfig';
import setObservableConfig from 'recompose/setObservableConfig';
import TopToolbar from './components/TopToolbar';
import { NativeRouter, Route, Redirect, AndroidBackButton } from 'react-router-native';
import { NativeRouter, Route, Redirect, AndroidBackButton, Switch } from 'react-router-native';
import List from './pages/List';
import FoodItemDetail from './pages/FoodItemDetail';
import PlaceDetail from './pages/PlaceDetail';
@ -15,6 +15,7 @@ import Drawer from 'react-native-drawer';
import CreateFoodItem from './pages/CreateFoodItem';
import AuthManager from './AuthManager';
import LoginPage from './pages/LoginPage';
import LandingPage from './pages/LandingPage';
setObservableConfig(rxjsconfig);
@ -42,9 +43,12 @@ export default class App extends Component {
openDrawerOffset={100}
content={<DrawerMenu onCloseDrawer={this.closeDrawer} />}
tweenDuration={150}>
<StatusBar backgroundColor={theme.statusBarColor} />
<Redirect from="/" to="/landing" />
<Switch>
<Route path="/landing" component={LandingPage} />
<View style={theme.page.container}>
<TopToolbar toggleSideMenu={this.openDrawer} />
<Redirect from="/" to="/list/food" />
<Route path="/list/:type" component={List} />
<Route path="/foodItem/:id" component={FoodItemDetail} />
<Route path="/place/:id" component={PlaceDetail} />
@ -59,6 +63,7 @@ export default class App extends Component {
}}
/>
</View>
</Switch>
</Drawer>
</ThemeProvider>
</AndroidBackButton>

View file

@ -27,7 +27,7 @@ export type RawFoodItem = {
export type FoodItemsForLocation = {
orderby: string,
filter: FoodItemsFilter,
fooditems: Array<RawFoodItem>,
fooditems: ?Array<RawFoodItem>,
};
export const getFoodItems = memoize(
@ -38,7 +38,9 @@ export const getFoodItems = memoize(
loc: Position,
filter: FilterRecord,
}): Promise<FoodItemsForLocation> => {
const { coords: { latitude: lat, longitude: lng } } = loc;
const {
coords: { latitude: lat, longitude: lng },
} = loc;
const { orderby, categories, radius } = filter;
try {

27
js/apis/PositionApi.js Normal file
View file

@ -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);
};

View file

@ -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<FoodItemRecord>) => {
if (!filter) {
@ -27,7 +28,8 @@ const sortByDistance = (foodItemsSeq: SetSeq<FoodItemRecord>) => {
return foodItemsSeq.sort((left, right) => left.distance - right.distance);
};
const intoArray = (foodItemsSeq: SetSeq<FoodItemRecord>) => foodItemsSeq.toArray();
const intoArray = (foodItemsSeq: SetSeq<FoodItemRecord>) =>
foodItemsSeq ? foodItemsSeq.toArray() : [];
class FoodItemList extends Component {
static displayName = 'FoodItemList';
@ -37,16 +39,21 @@ class FoodItemList extends Component {
limit?: number,
foodItemsSeq: SetSeq<FoodItemRecord>,
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 <View style={{ flexShrink: 2 }}>{items.map(renderFoodItem)}</View>;
}
}
const enhance = compose(pure, withFoodItemsAsSeq);
export default enhance(FoodItemList);
export default pure(FoodItemList);

View file

@ -42,7 +42,7 @@ export const SubText = ({ children, style = {} }: { children?: string, style?: O
};
export const TileBox = ({ children, style = {} }: { children?: any, style?: Object }) => (
<View style={{ height: 70, flexDirection: 'row', backgroundColor: 'white', alignItems: 'center', ...style }}>
<View style={{ flexDirection: 'row', backgroundColor: 'white', alignItems: 'center', paddingTop: 15, paddingBottom: 15, ...style }}>
{children}
</View>
);

View file

@ -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}
>
<View style={{ height: 70, flexDirection: 'row', backgroundColor: 'white' }}>
<View>
<TileBox>
<Thumbnail thumb={place.thumb} />
<View style={{ paddingTop: 5 }}>
<View>
<StrongText>{`${place.name} - ${distance} mi`}</StrongText>
<SubText>{getCategoriesText(foodItems)}</SubText>
<SubText>{getHoursText(place.hours)}</SubText>
</View>
</TileBox>
</View>
</Link>
);

View file

@ -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) => (
<View style={style}>
<Picker selectedValue={selected} onValueChange={debounce(onValueChange)} style={{ flex: 1 }}>
<Picker selectedValue={selected} onValueChange={debounce(onValueChange)} style={{ flex: 1, }}>
{CHOICES.map(renderItem(selected))}
</Picker>
</View>

View file

@ -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<number, FoodItemRecord>, foodItemId: number }) => {
withProps((props: { foodItemsMap: ?Map<number, FoodItemRecord>, 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<number, FoodItemRecord> }) => {
const foodItemsByPlace = props.foodItemsMap.groupBy(foodItem => foodItem.placeId);
withProps((props: { foodItemsMap: ?Map<number, FoodItemRecord> }) => {
if (!props.foodItemsMap) {
return {};
}
return {
foodItemsByPlace,
foodItemsByPlace: props.foodItemsMap.groupBy(foodItem => foodItem.placeId),
};
})
);

View file

@ -69,29 +69,29 @@ const FilterModal = (props: Props) => {
})}
<View
style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={{ fontSize: 15 }}>Sort By</Text>
<Text>Sort By</Text>
<OrderbyPicker
selected={orderby}
onValueChange={updateOrderby}
style={{ height: 40, width: 150 }}
style={theme.modalDropDown}
/>
</View>
<View
style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
<Text style={{ fontSize: 15 }}>Search Radius</Text>
<Text>Search Radius</Text>
<RadiusPicker
selected={radius}
onValueChange={updateRadius}
style={{ height: 40, width: 150 }}
style={theme.modalDropDown}
/>
</View>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end' }}>
<View style={{ width: 100, flexDirection: 'row', justifyContent: 'space-between' }}>
<View style={{ width: 150, flexDirection: 'row', justifyContent: 'flex-end' }}>
<TouchableOpacity onPress={onClose}>
<Text style={{ color: palette.accentColor }}>Cancel</Text>
<Text style={theme.modalButton}>CANCEL</Text>
</TouchableOpacity>
<TouchableOpacity onPress={applyChanges}>
<Text style={{ color: palette.accentColor }}>Apply</Text>
<Text style={theme.modalButton}>APPLY</Text>
</TouchableOpacity>
</View>
</View>

View file

@ -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 }) => () => {

View file

@ -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) => (
<FoodItemTile key={foodItem.id} foodItem={foodItem} />
);
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<FoodItemRecord>,
};
addFoodItem = () => {
@ -45,12 +49,13 @@ export default class FoodList extends Component {
};
render() {
const { isFilterModalOpen } = this.props;
const { isFilterModalOpen, foodItemsSeq } = this.props;
return (
<View style={theme.page.container}>
<Spinner visible={!foodItemsSeq} />
<ScrollView>
<FoodItemList renderFoodItem={renderFoodItem} />
<FoodItemList foodItemsSeq={foodItemsSeq} renderFoodItem={renderFoodItem} />
</ScrollView>
<FilterModal isVisible={isFilterModalOpen} onClose={this.toggleFilterModal} />
<ActionButton
@ -66,3 +71,5 @@ export default class FoodList extends Component {
);
}
}
export default withFoodItemsAsSeq(FoodList);

40
js/pages/LandingPage.js Normal file
View file

@ -0,0 +1,40 @@
// @flow
import React from 'react';
import { View, Text, Image } from 'react-native';
import atcCookieImage from '../../static/atc-cookie-logo.png';
import { Link } from 'react-router-native';
import RouterButton from 'react-router-native-button';
import theme from '../ui-theme';
const LandingPage = () => {
return (
<View
style={{
flexDirection: 'column',
alignItems: 'center',
padding: 20,
justifyContent: 'space-between',
flex: 1,
height: '100%',
}}>
<Image source={atcCookieImage} style={{ height: 275 }} resizeMode="contain" />
<Text style={{ fontSize: 24, textAlign: 'center' }}>
We need to use your location to bring you the best experience possible.
</Text>
<View style={{ width: 275 }}>
<RouterButton
to="/list/food?positionBy=location"
replace
title="OK, fine"
color={theme.palette.primaryColor}
/>
</View>
<Link to="/list/food?positionBy=zip" replace>
<Text style={{ color: theme.palette.primaryColor, padding: 20 }}>I'd rather not.</Text>
</Link>
</View>
);
};
export default LandingPage;

View file

@ -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)}>
<Food tabLabel="FOOD" />
<Places tabLabel="PLACES" />
</ScrollableTabView>

View file

@ -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) => {
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<string, FoodItemRecord>, manualUpdates) => {
if (foodItemMap) {
return foodItemMap.mergeDeep(manualUpdates);
}
})
.combineLatest(
Quantity$,
(
foodItems: Map<string, FoodItemRecord>,
foodItems: ?Map<string, FoodItemRecord>,
quantitiesFromStream: Map<string, QuantityFragment>
) => {
if (foodItems) {
return foodItems.mergeDeepWith(
(foodItem, foodItemQuantities) => foodItem.merge(foodItemQuantities),
quantitiesFromStream
);
}
}
)
.combineLatest(
Image$,
(foodItems: Map<string, FoodItemRecord>, latestFromImages$: Map<string, ImageFragment>) =>
// $FlowFixMe this type is incompatible with the expected param type of object type
foodItems.mergeDeepWith((foodItem: FoodItemRecord, imageFragment: ImageFragment) => {
(foodItems: ?Map<string, FoodItemRecord>, latestFromImages$: Map<string, ImageFragment>) => {
if (foodItems) {
return foodItems.mergeDeepWith(
// $FlowFixMe
(foodItem: FoodItemRecord, imageFragment: ImageFragment) => {
return foodItem.set('images', imageFragment.images);
}, latestFromImages$)
},
latestFromImages$
);
}
}
);

View file

@ -1,16 +1,12 @@
// @flow
import { Observable, type Observer } from 'rxjs';
import { ReplaySubject } from 'rxjs';
export default Observable.create((obs: Observer<Position>): Observable<Position> => {
// $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<Position> = new ReplaySubject();
export function emitter(val: ?Position) {
multicaster.next(val);
}
);
});
emitter(null);
export default multicaster;

View file

@ -22,7 +22,7 @@ const safeGetPlaceDetails = memoize((placeId: string): Promise<?GooglePlaceObj>
});
const Places$ = foodItems$
.mergeMap((foodItems: Map<string, FoodItemRecord>): Observable<string> => {
.mergeMap((foodItems: Map<string, FoodItemRecord> = Map()): Observable<string> => {
return Observable.from(foodItems.toArray().map(foodItem => foodItem.placeId));
})
.distinct()

View file

@ -7,15 +7,18 @@ import { COLOR } from 'react-native-material-ui';
export const primaryColor = '#6d5354';
export default {
palette: {
export const palette = {
primaryColor,
accentColor: '#0E6E9E',
disabledColor: COLOR.grey500,
facebook: '#3B5998',
google: '#DB4437',
errorColor: '#B92D00',
},
};
export default {
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,

View file

@ -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",

BIN
static/atc-cookie-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB