mirror of
https://gitlab.com/wheres-the-tp/ui-mobile.git
synced 2026-01-25 05:54:56 -06:00
Resolve "Fave food items list"
This commit is contained in:
parent
514240112f
commit
586200a654
24 changed files with 502 additions and 269 deletions
52
js/App.js
52
js/App.js
|
|
@ -1,26 +1,27 @@
|
||||||
//@flow
|
//@flow
|
||||||
import React, { Component } from "react";
|
import rxjsconfig from 'recompose/rxjsObservableConfig';
|
||||||
import { StatusBar } from "react-native";
|
import setObservableConfig from 'recompose/setObservableConfig';
|
||||||
import { ThemeContext, getTheme } from "react-native-material-ui";
|
|
||||||
import rxjsconfig from "recompose/rxjsObservableConfig";
|
|
||||||
import setObservableConfig from "recompose/setObservableConfig";
|
|
||||||
import { NativeRouter, Route, Redirect, AndroidBackButton, Switch } from "react-router-native";
|
|
||||||
import Nav from "./pages/Nav";
|
|
||||||
import FoodItemDetail from "./pages/FoodItemDetail";
|
|
||||||
import PlaceDetail from "./pages/PlaceDetail";
|
|
||||||
import CreateFoodItem from "./pages/CreateFoodItem";
|
|
||||||
import AuthManager from "./AuthManager";
|
|
||||||
import LoginPage from "./pages/LoginPage";
|
|
||||||
import LandingPage from "./pages/LandingPage";
|
|
||||||
import ZipcodePage from "./pages/ZipcodePage";
|
|
||||||
import { AppContainer } from "./components/AppContainer";
|
|
||||||
|
|
||||||
import theme from "./ui-theme";
|
|
||||||
|
|
||||||
setObservableConfig(rxjsconfig);
|
setObservableConfig(rxjsconfig);
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
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 PlaceDetail from './pages/PlaceDetail';
|
||||||
|
import CreateFoodItem from './pages/CreateFoodItem';
|
||||||
|
import LoginPage from './pages/LoginPage';
|
||||||
|
import LandingPage from './pages/LandingPage';
|
||||||
|
import ZipcodePage from './pages/ZipcodePage';
|
||||||
|
import { AppContainer } from './components/AppContainer';
|
||||||
|
|
||||||
|
import theme from './ui-theme';
|
||||||
|
|
||||||
|
console.disableYellowBox = true;
|
||||||
|
|
||||||
export default class App extends Component {
|
export default class App extends Component {
|
||||||
static displayName = "App";
|
static displayName = 'App';
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -29,7 +30,7 @@ export default class App extends Component {
|
||||||
<ThemeContext.Provider value={getTheme(theme)}>
|
<ThemeContext.Provider value={getTheme(theme)}>
|
||||||
<AppContainer>
|
<AppContainer>
|
||||||
<StatusBar backgroundColor={theme.statusBarColor} />
|
<StatusBar backgroundColor={theme.statusBarColor} />
|
||||||
<Redirect from="/" to="/landing" />
|
<Redirect from="/" to="/landing" replace />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path="/landing" component={LandingPage} />
|
<Route path="/landing" component={LandingPage} />
|
||||||
<Route path="/list/:type" component={Nav} />
|
<Route path="/list/:type" component={Nav} />
|
||||||
|
|
@ -38,16 +39,7 @@ export default class App extends Component {
|
||||||
<Route path="/login" component={LoginPage} />
|
<Route path="/login" component={LoginPage} />
|
||||||
<Route path="/logout" component={LoginPage} />
|
<Route path="/logout" component={LoginPage} />
|
||||||
<Route path="/zipcode" component={ZipcodePage} />
|
<Route path="/zipcode" component={ZipcodePage} />
|
||||||
<Route
|
<Route path="/createFoodItem" component={CreateFoodItem} />
|
||||||
path="/createFoodItem"
|
|
||||||
render={props => {
|
|
||||||
if (!AuthManager.isLoggedIn()) {
|
|
||||||
const search = props.location.search + "&" || "?";
|
|
||||||
return <Redirect to={`/login${search}`} push />;
|
|
||||||
}
|
|
||||||
return <CreateFoodItem />;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Switch>
|
</Switch>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ export type AUTH_PROVIDER =
|
||||||
| typeof AUTH_PROVIDER_AUTH0;
|
| typeof AUTH_PROVIDER_AUTH0;
|
||||||
|
|
||||||
export type User = {
|
export type User = {
|
||||||
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
token: string,
|
token: string,
|
||||||
tokenExpires: number,
|
tokenExpires: number,
|
||||||
|
|
@ -21,6 +22,11 @@ export type User = {
|
||||||
provider: AUTH_PROVIDER,
|
provider: AUTH_PROVIDER,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EmailAndPassword = {
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
};
|
||||||
|
|
||||||
class AuthManager {
|
class AuthManager {
|
||||||
user: ?User;
|
user: ?User;
|
||||||
|
|
||||||
|
|
@ -28,15 +34,17 @@ class AuthManager {
|
||||||
return this.user && this.user.token;
|
return this.user && this.user.token;
|
||||||
};
|
};
|
||||||
|
|
||||||
getAuthHeader = () => {
|
getEmail = () => {
|
||||||
const { token, provider } = this.user || {};
|
return this.user && this.user.email;
|
||||||
|
};
|
||||||
|
|
||||||
switch (provider) {
|
getId = () => {
|
||||||
case AUTH_PROVIDER_FACEBOOK:
|
return this.user && this.user.id;
|
||||||
return { authorization: `facebook-token ${token}` };
|
};
|
||||||
default:
|
|
||||||
return {};
|
getAuthHeader = () => {
|
||||||
}
|
const { token } = this.user || {};
|
||||||
|
return { authorization: `auth0-token ${token || ''}` };
|
||||||
};
|
};
|
||||||
|
|
||||||
isLoggedIn = () => {
|
isLoggedIn = () => {
|
||||||
|
|
@ -54,17 +62,19 @@ class AuthManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setUser({
|
this.setUser({
|
||||||
|
email: auth0User.email,
|
||||||
token: auth0User.accessToken,
|
token: auth0User.accessToken,
|
||||||
tokenExpires: +moment().add(auth0User.expiresIn, 'seconds'),
|
tokenExpires: +moment().add(auth0User.expiresIn, 'seconds'),
|
||||||
|
id: auth0User.idToken,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
setUser = ({ token, tokenExpires }) => {
|
setUser = user => {
|
||||||
if (!token || +moment(tokenExpires) <= +moment()) {
|
if (!user.token || +moment(user.tokenExpires) <= +moment()) {
|
||||||
this.user = null;
|
this.user = null;
|
||||||
AsyncStorage.removeItem('user');
|
AsyncStorage.removeItem('user');
|
||||||
} else {
|
} else {
|
||||||
this.user = { token, tokenExpires };
|
this.user = user;
|
||||||
AsyncStorage.setItem('user', JSON.stringify(this.user));
|
AsyncStorage.setItem('user', JSON.stringify(this.user));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -73,8 +83,8 @@ class AuthManager {
|
||||||
const user = JSON.parse(await AsyncStorage.getItem('user'));
|
const user = JSON.parse(await AsyncStorage.getItem('user'));
|
||||||
if (user) {
|
if (user) {
|
||||||
this.setUser(user);
|
this.setUser(user);
|
||||||
return this.user;
|
|
||||||
}
|
}
|
||||||
|
return this.isLoggedIn();
|
||||||
};
|
};
|
||||||
|
|
||||||
authenticate = async ({ email, password }) => {
|
authenticate = async ({ email, password }) => {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
// @flow
|
// @flow
|
||||||
import Auth0 from 'react-native-auth0';
|
import Auth0 from 'react-native-auth0';
|
||||||
|
import { type EmailAndPassword } from '../AuthManager';
|
||||||
|
|
||||||
let auth0Instance = null;
|
let auth0Instance = null;
|
||||||
|
|
||||||
export type Auth0User = {
|
export type Auth0User = {
|
||||||
|
email: string,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
expiresIn: number,
|
expiresIn: number,
|
||||||
|
idToken: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAuth0Instance = () => {
|
const getAuth0Instance = () => {
|
||||||
|
|
@ -18,13 +21,16 @@ const getAuth0Instance = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loginAuth0 = async ({ email: username, password }): Promise<?Auth0User> => {
|
export const loginAuth0 = async ({ email, password }: EmailAndPassword): Promise<?Auth0User> => {
|
||||||
const creds = await getAuth0Instance().auth.passwordRealm({
|
const creds = await getAuth0Instance().auth.passwordRealm({
|
||||||
username,
|
username: email,
|
||||||
password,
|
password,
|
||||||
realm: 'Username-Password-Authentication',
|
realm: 'Username-Password-Authentication',
|
||||||
});
|
});
|
||||||
return creds;
|
return {
|
||||||
|
email,
|
||||||
|
...creds,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createAuth0User = async ({ email, username, password }) => {
|
export const createAuth0User = async ({ email, username, password }) => {
|
||||||
|
|
|
||||||
44
js/apis/FavesApi.js
Normal file
44
js/apis/FavesApi.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { fetchRequest } from './FetchApi';
|
||||||
|
import AuthManager from '../AuthManager';
|
||||||
|
|
||||||
|
const withLoggedInEmail = fn => async (...args) => {
|
||||||
|
await AuthManager.checkIsAuthed();
|
||||||
|
|
||||||
|
const email = AuthManager.getEmail();
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
throw new Error('you must be logged in to view favorites');
|
||||||
|
}
|
||||||
|
|
||||||
|
return fn(email, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchFaves = withLoggedInEmail(async email => {
|
||||||
|
return fetchRequest({
|
||||||
|
endpoint: `/faves`,
|
||||||
|
method: 'POST',
|
||||||
|
body: { email },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const putFaves = withLoggedInEmail(async (email, foodItemIds) => {
|
||||||
|
return fetchRequest({
|
||||||
|
endpoint: '/fave',
|
||||||
|
method: 'PUT',
|
||||||
|
body: {
|
||||||
|
email,
|
||||||
|
foodItemIds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteFaves = withLoggedInEmail(async (email, foodItemIds) => {
|
||||||
|
return fetchRequest({
|
||||||
|
endpoint: '/fave',
|
||||||
|
method: 'DELETE',
|
||||||
|
body: {
|
||||||
|
email,
|
||||||
|
foodItemIds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,26 +1,26 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { BASE_URL } from "../constants/AppConstants";
|
import { BASE_URL } from '../constants/AppConstants';
|
||||||
import AuthManager from "../AuthManager";
|
import AuthManager from '../AuthManager';
|
||||||
|
|
||||||
export const fetchRequest = async ({
|
export const fetchRequest = async ({
|
||||||
endpoint,
|
endpoint,
|
||||||
method,
|
method,
|
||||||
headers = {},
|
headers = {},
|
||||||
body
|
body,
|
||||||
}: {
|
}: {
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
method: string,
|
method: string,
|
||||||
headers?: Object,
|
headers?: Object,
|
||||||
body?: Object
|
body?: Object,
|
||||||
}) => {
|
}) => {
|
||||||
const res = await fetch(`https://${BASE_URL}${endpoint}`, {
|
const res = await fetch(`http://${BASE_URL}${endpoint}`, {
|
||||||
method,
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
'Content-Type': 'application/json',
|
||||||
...AuthManager.getAuthHeader(),
|
...AuthManager.getAuthHeader(),
|
||||||
...headers
|
...headers,
|
||||||
},
|
},
|
||||||
...(body ? { body: JSON.stringify(body) } : {})
|
...(body ? { body: JSON.stringify(body) } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 401) {
|
if (res.status === 401) {
|
||||||
|
|
@ -36,10 +36,10 @@ export const fetchRequest = async ({
|
||||||
|
|
||||||
export const fetchRequestBinary = async ({ endpoint, body }: { endpoint: string, body: any }) => {
|
export const fetchRequestBinary = async ({ endpoint, body }: { endpoint: string, body: any }) => {
|
||||||
return fetch(`http://${BASE_URL}${endpoint}`, {
|
return fetch(`http://${BASE_URL}${endpoint}`, {
|
||||||
method: "POST",
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
...AuthManager.getAuthHeader()
|
...AuthManager.getAuthHeader(),
|
||||||
},
|
},
|
||||||
body
|
body,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,22 @@ const PlaceNameAndDistance = withPlace(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const EmptyFoodItemTile = () => {
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<TileBox>
|
||||||
|
<Thumbnail thumb={null} />
|
||||||
|
<View>
|
||||||
|
<StrongText>Loading...</StrongText>
|
||||||
|
</View>
|
||||||
|
</TileBox>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default pure(({ foodItem }: { foodItem: FoodItemRecord }) => {
|
export default pure(({ foodItem }: { foodItem: FoodItemRecord }) => {
|
||||||
if (!foodItem) {
|
if (!foodItem) {
|
||||||
return <View />;
|
return <EmptyFoodItemTile />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { View, TextInput, TouchableOpacity, SafeAreaView } from 'react-native';
|
import { View, TextInput, TouchableOpacity, SafeAreaView } from 'react-native';
|
||||||
import { Toolbar, Icon } from 'react-native-material-ui';
|
import { Toolbar, Icon } from 'react-native-material-ui';
|
||||||
import { path, pipe, not, cond, isNil, compose, always } from 'ramda';
|
import { path, cond, test } from 'ramda';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
import { getSearch, getViewMode } from '../helpers/RouteHelpers';
|
import { getSearch, getViewMode } from '../helpers/RouteHelpers';
|
||||||
import FoodItemSaveBtn from '../components/FoodItemSaveBtn';
|
import FoodItemSaveBtn from '../components/FoodItemSaveBtn';
|
||||||
|
|
@ -31,19 +31,11 @@ class TopToolbar extends Component {
|
||||||
const { router } = this.props;
|
const { router } = this.props;
|
||||||
const { history } = router;
|
const { history } = router;
|
||||||
|
|
||||||
const notNil = compose(
|
if (history.length > 1) {
|
||||||
not,
|
history.goBack();
|
||||||
isNil
|
} else {
|
||||||
);
|
history.replace('/list/food');
|
||||||
|
}
|
||||||
const goBackTo = pipe(
|
|
||||||
path(['route', 'location', 'search']),
|
|
||||||
queryString.parse,
|
|
||||||
path(['backto']),
|
|
||||||
cond([[notNil, history.replace], [always(true), history.goBack]])
|
|
||||||
);
|
|
||||||
|
|
||||||
goBackTo(router);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
toggleMapList = () => {
|
toggleMapList = () => {
|
||||||
|
|
@ -86,10 +78,15 @@ class TopToolbar extends Component {
|
||||||
|
|
||||||
const showSearch = /^\/list/.test(route);
|
const showSearch = /^\/list/.test(route);
|
||||||
const showSearchClear = filter.search.length > 0;
|
const showSearchClear = filter.search.length > 0;
|
||||||
const searchPlaceholder = /\/food/.test(route) ? 'Search Food...' : 'Search Places...';
|
|
||||||
|
const searchPlaceholder = cond([
|
||||||
|
[test(/\/food/), () => 'Search Food...'],
|
||||||
|
[test(/\/places/), () => 'Search Places...'],
|
||||||
|
[test(/\/faves/), () => 'Search favorites'],
|
||||||
|
])(route);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{backgroundColor: palette.primaryColor}}>
|
<View style={{ backgroundColor: palette.primaryColor }}>
|
||||||
<SafeAreaView>
|
<SafeAreaView>
|
||||||
{!showSearch && (
|
{!showSearch && (
|
||||||
<Toolbar
|
<Toolbar
|
||||||
|
|
@ -100,7 +97,11 @@ class TopToolbar extends Component {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showSearch && (
|
{showSearch && (
|
||||||
<View style={{ flexDirection: 'row', padding: 10 }}>
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
padding: 10,
|
||||||
|
}}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
// @public
|
// @public
|
||||||
export const BASE_URL = 'aretherecookies.herokuapp.com';
|
// export const BASE_URL = 'aretherecookies.herokuapp.com';
|
||||||
|
|
||||||
// @home
|
// @home
|
||||||
// export const BASE_URL = '192.168.1.8:3000';
|
// export const BASE_URL = '192.168.1.8:3000';
|
||||||
|
|
||||||
// @stouthaus
|
// @stouthaus
|
||||||
// export const BASE_URL = '192.168.1.191:3000';
|
export const BASE_URL = '192.168.1.181:3000';
|
||||||
|
|
||||||
export const GoogleAPIKey = 'AIzaSyBfMm1y6JayCbXrQmgAG1R3ka4ZOJno_5E';
|
export const GoogleAPIKey = 'AIzaSyBfMm1y6JayCbXrQmgAG1R3ka4ZOJno_5E';
|
||||||
|
|
|
||||||
27
js/enhancers/authEnhancers.js
Normal file
27
js/enhancers/authEnhancers.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import mapPropsStream from 'recompose/mapPropsStream';
|
||||||
|
import { compose, lifecycle } from 'recompose';
|
||||||
|
import AuthManager from '../AuthManager';
|
||||||
|
import { withRouterContext } from './routeEnhancers';
|
||||||
|
import { getPathname, loginWithBackto } from '../helpers/RouteHelpers';
|
||||||
|
|
||||||
|
export const withAuthed = mapPropsStream(props$ =>
|
||||||
|
props$.combineLatest(AuthManager.checkIsAuthed(), (props, isAuthed) => {
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
isAuthed,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const withAuthRedirect = compose(
|
||||||
|
withRouterContext,
|
||||||
|
withAuthed,
|
||||||
|
lifecycle({
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.props.isAuthed) {
|
||||||
|
const pathname = getPathname(this.props);
|
||||||
|
this.props.router.history.replace(loginWithBackto(pathname));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
52
js/enhancers/favesEnhancer.js
Normal file
52
js/enhancers/favesEnhancer.js
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
//@flow
|
||||||
|
import mapPropsStream from 'recompose/mapPropsStream';
|
||||||
|
import Faves$, { emit as emitFaves, emitOne as emitFave } from '../streams/FavesStream';
|
||||||
|
import { fetchFaves, putFaves, deleteFaves } from '../apis/FavesApi';
|
||||||
|
import { fromJS, get } from 'immutable';
|
||||||
|
|
||||||
|
const fetchAndEmitFaves = async () => {
|
||||||
|
const faves = fromJS(await fetchFaves());
|
||||||
|
emitFaves(faves);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildTempFave = foodItemId =>
|
||||||
|
fromJS({
|
||||||
|
food_item_id: foodItemId,
|
||||||
|
date: Date.now(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const withFaves = mapPropsStream(props$ =>
|
||||||
|
props$.combineLatest(Faves$, (props, faves) => {
|
||||||
|
return {
|
||||||
|
...props,
|
||||||
|
faves,
|
||||||
|
getFaves: async () => {
|
||||||
|
try {
|
||||||
|
await fetchAndEmitFaves();
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addFave: async foodItemId => {
|
||||||
|
try {
|
||||||
|
await putFaves([foodItemId]);
|
||||||
|
emitFave(buildTempFave(foodItemId));
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteFave: async foodItemId => {
|
||||||
|
try {
|
||||||
|
await deleteFaves([foodItemId]);
|
||||||
|
|
||||||
|
const idx = faves.findIndex(fave => get(fave, 'id') === foodItemId);
|
||||||
|
if (idx >= 0) {
|
||||||
|
emitFaves(faves.delete(idx));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err); // eslint-disable-line no-console
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
@ -9,10 +9,7 @@ import { pipe, path, toLower, equals } from 'ramda';
|
||||||
|
|
||||||
type FindPredicate<A> = (left: A) => (right: A) => Boolean;
|
type FindPredicate<A> = (left: A) => (right: A) => Boolean;
|
||||||
|
|
||||||
const getName: (f: FoodItemRecord) => String = pipe(
|
const getName: (f: FoodItemRecord) => String = pipe(path(['name']), toLower);
|
||||||
path(['name']),
|
|
||||||
toLower
|
|
||||||
);
|
|
||||||
|
|
||||||
const matchesName: FindPredicate<FoodItemRecord> = left => right => {
|
const matchesName: FindPredicate<FoodItemRecord> = left => right => {
|
||||||
return equals(getName(left), getName(right));
|
return equals(getName(left), getName(right));
|
||||||
|
|
@ -33,7 +30,7 @@ export const withFoodItems = mapPropsStream(props$ =>
|
||||||
props$.combineLatest(FoodItems$, (props, foodItems) => {
|
props$.combineLatest(FoodItems$, (props, foodItems) => {
|
||||||
return {
|
return {
|
||||||
...props,
|
...props,
|
||||||
foodItemsMap: foodItems && foodItems,
|
foodItemsMap: foodItems,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// @flow
|
|
||||||
import { getContext, withProps, compose } from 'recompose';
|
import { getContext, withProps, compose } from 'recompose';
|
||||||
import { shape, func, string } from 'prop-types';
|
import { shape, func, string } from 'prop-types';
|
||||||
import { path } from 'ramda';
|
import { path, pathOr } from 'ramda';
|
||||||
import { getViewMode } from '../helpers/RouteHelpers';
|
import { getViewMode } from '../helpers/RouteHelpers';
|
||||||
|
|
||||||
export type routeMatch = {
|
export type routeMatch = {
|
||||||
|
|
@ -66,3 +65,14 @@ export const withReplaceRoute = compose(
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const withCurrentPath = compose(
|
||||||
|
withRouterContext,
|
||||||
|
withProps(props => {
|
||||||
|
const { pathname, search } = pathOr({}, ['router', 'route', 'location'], props);
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentPath: `${pathname}${search || '?'}`,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { curry, pipe, pathOr } from 'ramda';
|
import { curry, pipe, pathOr, path } from 'ramda';
|
||||||
import queryString from 'query-string';
|
import queryString from 'query-string';
|
||||||
|
|
||||||
|
const PARAM_BACK_TO = 'backto';
|
||||||
|
|
||||||
type RouteTo = {
|
type RouteTo = {
|
||||||
pathname: string,
|
pathname: string,
|
||||||
search?: string,
|
search?: string,
|
||||||
|
|
@ -18,8 +20,8 @@ export const routeWithQuery = curry(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const routeWithTitle = curry(
|
export const routeWithTitle = curry((pathname: string, routeTitle: string): RouteTo =>
|
||||||
(pathname: string, routeTitle: string): RouteTo => routeWithQuery(pathname, { routeTitle })
|
routeWithQuery(pathname, { routeTitle })
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getSearch = pipe(
|
export const getSearch = pipe(
|
||||||
|
|
@ -27,7 +29,10 @@ export const getSearch = pipe(
|
||||||
queryString.parse
|
queryString.parse
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getViewMode = pipe(
|
export const getViewMode = pipe(getSearch, pathOr('list', ['viewMode']));
|
||||||
getSearch,
|
|
||||||
pathOr('list', ['viewMode'])
|
export const getBackTo = pipe(getSearch, path([PARAM_BACK_TO]));
|
||||||
);
|
|
||||||
|
export const loginWithBackto = (backto: string) => `/login?${PARAM_BACK_TO}=${backto}`;
|
||||||
|
|
||||||
|
export const getPathname = pathOr('/list/food', ['router', 'route', 'location', 'pathname']);
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,24 @@
|
||||||
// @flow
|
// @flow
|
||||||
export default <T>(fn: Function, delay?: number = 0): (() => Promise<T>) => {
|
export default <T>(fn: Function, delay?: number = 100): (() => Promise<T>) => {
|
||||||
let timeoutId;
|
let cacheTimeout;
|
||||||
|
let cachedResult;
|
||||||
return (...args) => {
|
return (...args) => {
|
||||||
if (timeoutId) {
|
if (cacheTimeout) {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(cacheTimeout);
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
timeoutId = setTimeout(() => {
|
if (!cachedResult) {
|
||||||
|
cachedResult = new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
resolve(fn(...args));
|
resolve(fn(...args));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
reject(err);
|
reject(err);
|
||||||
} finally {
|
|
||||||
timeoutId = null;
|
|
||||||
}
|
}
|
||||||
}, delay);
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
cacheTimeout = setTimeout(() => (cachedResult = null), delay);
|
||||||
|
|
||||||
|
return cachedResult;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import { getCategoryText } from '../helpers/CategoryHelpers';
|
||||||
import { getQuantityDropdownText } from '../helpers/QuantityHelpers';
|
import { getQuantityDropdownText } from '../helpers/QuantityHelpers';
|
||||||
import QuantityModal from '../modals/QuantityModal';
|
import QuantityModal from '../modals/QuantityModal';
|
||||||
import { wrapModalComponent } from '../modals/FullScreenModal';
|
import { wrapModalComponent } from '../modals/FullScreenModal';
|
||||||
|
import { withAuthRedirect } from '../enhancers/authEnhancers';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
foodItem: typeof FoodItemRecord,
|
foodItem: typeof FoodItemRecord,
|
||||||
|
|
@ -171,6 +172,7 @@ const updatePlace = ({ foodItem, setFoodItem, emitPlace }: Props) => placeDetail
|
||||||
};
|
};
|
||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
|
withAuthRedirect,
|
||||||
withCreateFoodItemState,
|
withCreateFoodItemState,
|
||||||
withPlaceId,
|
withPlaceId,
|
||||||
withPlaceForFoodItem,
|
withPlaceForFoodItem,
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { Text, View } from 'react-native';
|
|
||||||
import theme from '../ui-theme';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
route: Object,
|
|
||||||
navigator: Object,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class Faves extends Component {
|
|
||||||
static displayName = 'Faves';
|
|
||||||
|
|
||||||
props: Props;
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<View style={theme.page.container}>
|
|
||||||
<Text>Faves!</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
58
js/pages/FavesPage.js
Normal file
58
js/pages/FavesPage.js
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
// @flow
|
||||||
|
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 { Map, get } from 'immutable';
|
||||||
|
import typeof FoodItemRecord from '../records/FoodItemRecord';
|
||||||
|
import { withFaves } from '../enhancers/favesEnhancer';
|
||||||
|
import type { Faves, Fave } from '../streams/FavesStream';
|
||||||
|
import FoodItemTile from '../components/FoodItemTile';
|
||||||
|
import { withAuthRedirect } from '../enhancers/authEnhancers';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
faves: Faves,
|
||||||
|
onRefresh: () => Promise<any>,
|
||||||
|
onPulldown: () => {},
|
||||||
|
isRefreshing: boolean,
|
||||||
|
viewMode: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FavesList = ({ faves, isRefreshing, onPulldown }: Props) => {
|
||||||
|
const refreshing = isRefreshing || !faves;
|
||||||
|
return (
|
||||||
|
<View>
|
||||||
|
<ScrollView
|
||||||
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onPulldown} />}>
|
||||||
|
{faves &&
|
||||||
|
faves.map((fave: Faves, index: number) => {
|
||||||
|
return <FoodItemTile key={fave.id || index} foodItem={fave} />;
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
pure,
|
||||||
|
withAuthRedirect,
|
||||||
|
withFaves,
|
||||||
|
withState('isRefreshing', 'setRefreshing', true),
|
||||||
|
withHandlers({
|
||||||
|
onPulldown: ({ setRefreshing, getFaves }) => async () => {
|
||||||
|
try {
|
||||||
|
setRefreshing(true);
|
||||||
|
await getFaves();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error); // eslint-disable-line no-console
|
||||||
|
} finally {
|
||||||
|
setRefreshing(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
lifecycle({
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.onPulldown();
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)(FavesList);
|
||||||
|
|
@ -19,21 +19,24 @@ import { withFoodItem } from '../enhancers/foodItemEnhancers';
|
||||||
import { withPlaceForFoodItem } from '../enhancers/placeEnhancers';
|
import { withPlaceForFoodItem } from '../enhancers/placeEnhancers';
|
||||||
import Carousel from 'react-native-looped-carousel';
|
import Carousel from 'react-native-looped-carousel';
|
||||||
import CountBadge from '../components/CountBadge';
|
import CountBadge from '../components/CountBadge';
|
||||||
import { routeWithTitle } from '../helpers/RouteHelpers';
|
import { routeWithTitle, loginWithBackto } from '../helpers/RouteHelpers';
|
||||||
import { Link } from 'react-router-native';
|
import { Link } from 'react-router-native';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { withUpdateQuantity } from '../enhancers/quantityEnhancers';
|
import { withUpdateQuantity } from '../enhancers/quantityEnhancers';
|
||||||
import { getQuantityLabelText } from '../helpers/QuantityHelpers';
|
import { getQuantityLabelText } from '../helpers/QuantityHelpers';
|
||||||
import AuthManager from '../AuthManager';
|
|
||||||
import RouterButton from 'react-router-native-button';
|
import RouterButton from 'react-router-native-button';
|
||||||
import QuantityModal from '../modals/QuantityModal';
|
import QuantityModal from '../modals/QuantityModal';
|
||||||
import { withImages } from '../enhancers/imagesEnhancers';
|
import { withImages } from '../enhancers/imagesEnhancers';
|
||||||
import { openImagePicker } from '../helpers/ImagePickerHelpers';
|
import { openImagePicker } from '../helpers/ImagePickerHelpers';
|
||||||
import { identity } from 'ramda';
|
import { identity, pathOr } from 'ramda';
|
||||||
import Spinner from 'react-native-loading-spinner-overlay';
|
import Spinner from 'react-native-loading-spinner-overlay';
|
||||||
import ImageRecord from '../records/ImageRecord';
|
import ImageRecord from '../records/ImageRecord';
|
||||||
import { List } from 'immutable';
|
import { List, get } from 'immutable';
|
||||||
import Snackbar from 'react-native-snackbar';
|
import Snackbar from 'react-native-snackbar';
|
||||||
|
import { withAuthed } from '../enhancers/authEnhancers';
|
||||||
|
import { withFaves } from '../enhancers/favesEnhancer';
|
||||||
|
import debounce from '../helpers/debounce';
|
||||||
|
import { withCurrentPath, withReplaceRoute } from '../enhancers/routeEnhancers';
|
||||||
|
|
||||||
const { foodItemDetails: style } = theme;
|
const { foodItemDetails: style } = theme;
|
||||||
|
|
||||||
|
|
@ -94,7 +97,7 @@ type Props = {
|
||||||
place: PlaceRecord,
|
place: PlaceRecord,
|
||||||
currentImage: number,
|
currentImage: number,
|
||||||
quantityModalOpen: boolean,
|
quantityModalOpen: boolean,
|
||||||
isLoggedIn: boolean,
|
isAuthed: boolean,
|
||||||
changeCurrentImage: (index: number) => void,
|
changeCurrentImage: (index: number) => void,
|
||||||
updateAmount: (quantity: Quantity) => void,
|
updateAmount: (quantity: Quantity) => void,
|
||||||
updateQuantity: (arg: { foodItemId: string, quantity: Quantity }) => void,
|
updateQuantity: (arg: { foodItemId: string, quantity: Quantity }) => void,
|
||||||
|
|
@ -103,6 +106,9 @@ type Props = {
|
||||||
addImage: (arg: { foodItemId: string, imageUri: string }) => Promise<void>,
|
addImage: (arg: { foodItemId: string, imageUri: string }) => Promise<void>,
|
||||||
imagesLoading: boolean,
|
imagesLoading: boolean,
|
||||||
setImagesLoading: (loading: boolean) => void,
|
setImagesLoading: (loading: boolean) => void,
|
||||||
|
addToFaves: () => void,
|
||||||
|
isFave: boolean,
|
||||||
|
deleteFromFaves: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FoodItemDetail = (props: Props) => {
|
export const FoodItemDetail = (props: Props) => {
|
||||||
|
|
@ -111,12 +117,15 @@ export const FoodItemDetail = (props: Props) => {
|
||||||
place,
|
place,
|
||||||
currentImage,
|
currentImage,
|
||||||
changeCurrentImage,
|
changeCurrentImage,
|
||||||
isLoggedIn,
|
isAuthed,
|
||||||
addPhoto,
|
addPhoto,
|
||||||
toggleQuantityModal,
|
toggleQuantityModal,
|
||||||
quantityModalOpen,
|
quantityModalOpen,
|
||||||
updateAmount,
|
updateAmount,
|
||||||
imagesLoading,
|
imagesLoading,
|
||||||
|
addToFaves,
|
||||||
|
isFave,
|
||||||
|
deleteFromFaves,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (!foodItem || !place) {
|
if (!foodItem || !place) {
|
||||||
|
|
@ -155,34 +164,48 @@ export const FoodItemDetail = (props: Props) => {
|
||||||
<SubText>
|
<SubText>
|
||||||
Last updated at {moment(foodItem.lastupdated).format('h:mm A on MMM D, YYYY')}
|
Last updated at {moment(foodItem.lastupdated).format('h:mm A on MMM D, YYYY')}
|
||||||
</SubText>
|
</SubText>
|
||||||
{isLoggedIn && (
|
{isAuthed && (
|
||||||
<Button
|
<Button
|
||||||
title="Update quantity"
|
title="Update quantity"
|
||||||
onPress={toggleQuantityModal}
|
onPress={toggleQuantityModal}
|
||||||
color={theme.palette.primaryColor}
|
color={theme.palette.primaryColor}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isLoggedIn && (
|
{!isAuthed && (
|
||||||
<RouterButton
|
<RouterButton
|
||||||
title="Log in to update quantity"
|
title="Log in to update quantity"
|
||||||
to={'/login'}
|
to={loginWithBackto(`/foodItem/${foodItem.id}?loginAction=open-quantity-modal`)}
|
||||||
color={theme.palette.primaryColor}
|
color={theme.palette.primaryColor}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flex: 1, ...contentTileStyle }}>
|
<View style={{ flex: 1, ...contentTileStyle }}>
|
||||||
{/* <IconButton
|
{isAuthed && isFave ? (
|
||||||
glyph="stars"
|
<IconButton
|
||||||
text="Add to Faves"
|
glyph="favorite"
|
||||||
onPress={this.addToFaves}
|
text="Favorited"
|
||||||
color={style.actionIconColor}
|
onPress={deleteFromFaves}
|
||||||
/> */}
|
color="#990000"
|
||||||
{isLoggedIn && (
|
style={{ marginBottom: 10 }}
|
||||||
|
textStyle={{ fontSize: 20, color: '#555555' }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
glyph="favorite"
|
||||||
|
text="Add to Faves"
|
||||||
|
onPress={addToFaves}
|
||||||
|
color={style.actionIconColor}
|
||||||
|
style={{ marginBottom: 10 }}
|
||||||
|
textStyle={{ fontSize: 20, color: '#555555' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isAuthed && (
|
||||||
<IconButton
|
<IconButton
|
||||||
glyph="insert-photo"
|
glyph="insert-photo"
|
||||||
text="Add Photo"
|
text="Add Photo"
|
||||||
onPress={addPhoto}
|
onPress={addPhoto}
|
||||||
color={style.actionIconColor}
|
color={style.actionIconColor}
|
||||||
|
textStyle={{ fontSize: 20, color: '#555555' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -198,9 +221,10 @@ export default compose(
|
||||||
withPlaceForFoodItem,
|
withPlaceForFoodItem,
|
||||||
withUpdateQuantity,
|
withUpdateQuantity,
|
||||||
withImages,
|
withImages,
|
||||||
withProps(() => ({
|
withAuthed,
|
||||||
isLoggedIn: AuthManager.isLoggedIn(),
|
withFaves,
|
||||||
})),
|
withCurrentPath,
|
||||||
|
withReplaceRoute,
|
||||||
withState('currentImage', 'changeCurrentImage', 0),
|
withState('currentImage', 'changeCurrentImage', 0),
|
||||||
withState('quantityModalOpen', 'setQuantityModalOpen', false),
|
withState('quantityModalOpen', 'setQuantityModalOpen', false),
|
||||||
withState('imagesLoading', 'setImagesLoading', true),
|
withState('imagesLoading', 'setImagesLoading', true),
|
||||||
|
|
@ -230,21 +254,51 @@ export default compose(
|
||||||
await updateQuantity({ foodItemId: foodItem.id, quantity });
|
await updateQuantity({ foodItemId: foodItem.id, quantity });
|
||||||
Snackbar.show({ title: 'Food updated.', backgroundColor: 'black', color: 'white' });
|
Snackbar.show({ title: 'Food updated.', backgroundColor: 'black', color: 'white' });
|
||||||
},
|
},
|
||||||
toggleQuantityModal: ({ quantityModalOpen, setQuantityModalOpen }) => () => {
|
toggleQuantityModal: ({ quantityModalOpen, setQuantityModalOpen }) =>
|
||||||
setQuantityModalOpen(!quantityModalOpen);
|
debounce(() => {
|
||||||
},
|
setQuantityModalOpen(!quantityModalOpen);
|
||||||
|
}, 500),
|
||||||
|
addToFaves: ({ addFave, foodItemId, isAuthed, replaceRoute }) =>
|
||||||
|
debounce(() => {
|
||||||
|
if (!isAuthed) {
|
||||||
|
replaceRoute(loginWithBackto(`/foodItem/${foodItemId}?loginAction=add-to-faves`));
|
||||||
|
} else {
|
||||||
|
addFave(foodItemId);
|
||||||
|
}
|
||||||
|
}, 500),
|
||||||
|
deleteFromFaves: ({ deleteFave, foodItemId }) => () => deleteFave(foodItemId),
|
||||||
}),
|
}),
|
||||||
|
withProps(props => ({
|
||||||
|
isFave: props.faves && !!props.faves.find(fave => get(fave, 'id') === props.foodItemId),
|
||||||
|
loginAction: pathOr('', [1], /loginAction=(.+)(&?.*$)/.exec(props.currentPath)),
|
||||||
|
})),
|
||||||
onlyUpdateForKeys([
|
onlyUpdateForKeys([
|
||||||
'foodItem',
|
'foodItem',
|
||||||
'place',
|
'place',
|
||||||
'currentImage',
|
'currentImage',
|
||||||
'quantityModalOpen',
|
'quantityModalOpen',
|
||||||
'imagesLoading',
|
'imagesLoading',
|
||||||
'isLoggedIn',
|
'isAuthed',
|
||||||
|
'faves',
|
||||||
]),
|
]),
|
||||||
lifecycle({
|
lifecycle({
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { getImages, foodItemId, setImagesLoading } = this.props;
|
const {
|
||||||
|
getImages,
|
||||||
|
foodItemId,
|
||||||
|
setImagesLoading,
|
||||||
|
loginAction,
|
||||||
|
setQuantityModalOpen,
|
||||||
|
addToFaves,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (loginAction === 'open-quantity-modal') {
|
||||||
|
setQuantityModalOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loginAction === 'add-to-faves') {
|
||||||
|
addToFaves();
|
||||||
|
}
|
||||||
|
|
||||||
getImages({
|
getImages({
|
||||||
foodItemId,
|
foodItemId,
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import { View, Text, ScrollView, RefreshControl } from "react-native";
|
import { View, Text, ScrollView, RefreshControl } from 'react-native';
|
||||||
import FoodItemTile from "../components/FoodItemTile";
|
import FoodItemTile from '../components/FoodItemTile';
|
||||||
import FoodItemList from "../components/FoodItemList";
|
import FoodItemList from '../components/FoodItemList';
|
||||||
import typeof FoodItemRecord from "../records/FoodItemRecord";
|
import typeof FoodItemRecord from '../records/FoodItemRecord';
|
||||||
import { withFoodItemsAsSeq } from "../enhancers/foodItemEnhancers";
|
import { withFoodItemsAsSeq } from '../enhancers/foodItemEnhancers';
|
||||||
import { type SetSeq } from "immutable";
|
import { type SetSeq } from 'immutable';
|
||||||
import { compose, pure, withState, withHandlers, lifecycle } from "recompose";
|
import { compose, pure, withState, withHandlers, lifecycle } from 'recompose';
|
||||||
import { withFilter } from "../enhancers/filterEnhancers";
|
import { withFilter } from '../enhancers/filterEnhancers';
|
||||||
import { withRouterContext } from "../enhancers/routeEnhancers";
|
import { withRouterContext } from '../enhancers/routeEnhancers';
|
||||||
import FullScreenMessage from "../components/FullScreenMessage";
|
import FullScreenMessage from '../components/FullScreenMessage';
|
||||||
|
import { withFaves } from '../enhancers/favesEnhancer';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
foodItemsSeq: SetSeq<FoodItemRecord>,
|
foodItemsSeq: SetSeq<FoodItemRecord>,
|
||||||
isRefreshing: boolean,
|
isRefreshing: boolean,
|
||||||
onRefresh: () => Promise<any>,
|
onRefresh: () => Promise<any>,
|
||||||
onPulldown: () => {},
|
onPulldown: () => {},
|
||||||
isFilterDirty: boolean
|
isFilterDirty: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
const FoodList = (props: Props) => {
|
const FoodList = (props: Props) => {
|
||||||
|
|
@ -36,8 +37,7 @@ const FoodList = (props: Props) => {
|
||||||
{!showNoResults && (
|
{!showNoResults && (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onPulldown} />}
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onPulldown} />}>
|
||||||
>
|
|
||||||
<FoodItemList foodItemsSeq={foodItemsSeq}>
|
<FoodItemList foodItemsSeq={foodItemsSeq}>
|
||||||
{(foodItem: FoodItemRecord) => <FoodItemTile key={foodItem.id} foodItem={foodItem} />}
|
{(foodItem: FoodItemRecord) => <FoodItemTile key={foodItem.id} foodItem={foodItem} />}
|
||||||
</FoodItemList>
|
</FoodItemList>
|
||||||
|
|
@ -52,23 +52,24 @@ export default compose(
|
||||||
withRouterContext,
|
withRouterContext,
|
||||||
withFoodItemsAsSeq,
|
withFoodItemsAsSeq,
|
||||||
withFilter,
|
withFilter,
|
||||||
withState("isRefreshing", "setRefreshing", false),
|
withFaves,
|
||||||
|
withState('isRefreshing', 'setRefreshing', false),
|
||||||
withHandlers({
|
withHandlers({
|
||||||
onPulldown: ({ setRefreshing, onRefresh, router }) => async () => {
|
onPulldown: ({ setRefreshing, onRefresh, getFaves, router }) => async () => {
|
||||||
try {
|
try {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
await onRefresh();
|
await Promise.all([onRefresh(), getFaves()]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error); // eslint-disable-line no-console
|
console.error(error); // eslint-disable-line no-console
|
||||||
router.history.push("/zipcode");
|
router.history.push('/zipcode');
|
||||||
} finally {
|
} finally {
|
||||||
setTimeout(() => setRefreshing(false), 1000);
|
setTimeout(() => setRefreshing(false), 1000);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}),
|
}),
|
||||||
lifecycle({
|
lifecycle({
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.onPulldown();
|
this.props.onPulldown();
|
||||||
}
|
},
|
||||||
})
|
})
|
||||||
)(FoodList);
|
)(FoodList);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { withRouterContext } from '../enhancers/routeEnhancers';
|
||||||
import AuthManager from '../AuthManager';
|
import AuthManager from '../AuthManager';
|
||||||
import theme from '../ui-theme';
|
import theme from '../ui-theme';
|
||||||
import { Divider, Icon } from 'react-native-material-ui';
|
import { Divider, Icon } from 'react-native-material-ui';
|
||||||
|
import { getBackTo } from '../helpers/RouteHelpers';
|
||||||
|
|
||||||
const auth0ErrToStr = message => {
|
const auth0ErrToStr = message => {
|
||||||
if (typeof message === 'string') {
|
if (typeof message === 'string') {
|
||||||
|
|
@ -78,7 +79,11 @@ const LoginPageComponent = ({
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
}}>
|
}}>
|
||||||
{error && <Text style={{ fontSize: 16, color: '#721c24', backgroundColor: '#f8d7da', padding: 16,}}>{error}</Text>}
|
{error && (
|
||||||
|
<Text style={{ fontSize: 16, color: '#721c24', backgroundColor: '#f8d7da', padding: 16 }}>
|
||||||
|
{error}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<Text style={{ fontSize: 42, fontWeight: '300', margin: 16 }}>Sign In</Text>
|
<Text style={{ fontSize: 42, fontWeight: '300', margin: 16 }}>Sign In</Text>
|
||||||
|
|
||||||
|
|
@ -91,7 +96,7 @@ const LoginPageComponent = ({
|
||||||
onChangeText={setEmail}
|
onChangeText={setEmail}
|
||||||
style={{
|
style={{
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
paddingLeft: 0
|
paddingLeft: 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
@ -108,7 +113,7 @@ const LoginPageComponent = ({
|
||||||
onChangeText={setUsername}
|
onChangeText={setUsername}
|
||||||
style={{
|
style={{
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
paddingLeft: 0
|
paddingLeft: 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
@ -126,7 +131,7 @@ const LoginPageComponent = ({
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
style={{
|
style={{
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
paddingLeft: 0
|
paddingLeft: 0,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
@ -134,7 +139,7 @@ const LoginPageComponent = ({
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={{ flexDirection: 'row', padding: 16, paddingBottom: 0, paddingTop: 32 }}>
|
<View style={{ flexDirection: 'row', padding: 16, paddingBottom: 0, paddingTop: 32 }}>
|
||||||
<View style={{ flex: 1, flexDirection: 'column', }}>
|
<View style={{ flex: 1, flexDirection: 'column' }}>
|
||||||
<Button title={submitTitle} onPress={submitAction} color={theme.palette.primaryColor} />
|
<Button title={submitTitle} onPress={submitAction} color={theme.palette.primaryColor} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -160,7 +165,7 @@ const LoginPageComponent = ({
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
padding:16,
|
padding: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}>
|
}}>
|
||||||
|
|
@ -188,10 +193,11 @@ export default compose(
|
||||||
withState('username', 'setUsername', ''),
|
withState('username', 'setUsername', ''),
|
||||||
withState('isNewUser', 'setIsNewUser', false),
|
withState('isNewUser', 'setIsNewUser', false),
|
||||||
withHandlers({
|
withHandlers({
|
||||||
checkAuth: ({ router }) => async () => {
|
checkAuth: props => async () => {
|
||||||
const isLoggedIn = await AuthManager.checkIsAuthed();
|
const isLoggedIn = await AuthManager.checkIsAuthed();
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
router.history.goBack();
|
const backTo = getBackTo(props);
|
||||||
|
return backTo ? props.router.history.replace(backTo) : props.router.history.goBack();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,15 @@ import { pathOr } from 'ramda';
|
||||||
import uiTheme from '../ui-theme';
|
import uiTheme from '../ui-theme';
|
||||||
import ProfilePage from './ProfilePage';
|
import ProfilePage from './ProfilePage';
|
||||||
import AuthManager from '../AuthManager';
|
import AuthManager from '../AuthManager';
|
||||||
|
import FavesPage from './FavesPage';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activeTab: string,
|
activeTab: string,
|
||||||
router: routerContext,
|
router: routerContext,
|
||||||
setRoute: (route: string) => () => void,
|
setRoute: (route: string) => () => void,
|
||||||
|
location: {
|
||||||
|
pathname: string,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const Nav = (props: Props) => {
|
const Nav = (props: Props) => {
|
||||||
|
|
@ -25,7 +29,8 @@ const Nav = (props: Props) => {
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
{activeTab === 'food' && <Food onRefresh={getLocation} />}
|
{activeTab === 'food' && <Food onRefresh={getLocation} />}
|
||||||
{activeTab === 'places' && <Places onRefresh={getLocation} />}
|
{activeTab === 'places' && <Places onRefresh={getLocation} />}
|
||||||
{activeTab === 'profile' && <ProfilePage isLoggedIn={AuthManager.isLoggedIn()} />}
|
{activeTab === 'faves' && <FavesPage />}
|
||||||
|
{activeTab === 'profile' && <ProfilePage />}
|
||||||
</View>
|
</View>
|
||||||
<SafeAreaView>
|
<SafeAreaView>
|
||||||
<BottomNavigation active={activeTab}>
|
<BottomNavigation active={activeTab}>
|
||||||
|
|
@ -63,10 +68,10 @@ const Nav = (props: Props) => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<BottomNavigation.Action
|
<BottomNavigation.Action
|
||||||
key="fave"
|
key="faves"
|
||||||
icon="favorite"
|
icon="favorite"
|
||||||
label="Faves"
|
label="Faves"
|
||||||
onPress={setRoute('/list/food')}
|
onPress={setRoute('/list/faves')}
|
||||||
style={{
|
style={{
|
||||||
container: {
|
container: {
|
||||||
minWidth: 40,
|
minWidth: 40,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { withRouterContext } from '../enhancers/routeEnhancers';
|
||||||
import { getLocation } from '../apis/PositionApi';
|
import { getLocation } from '../apis/PositionApi';
|
||||||
import Snackbar from 'react-native-snackbar';
|
import Snackbar from 'react-native-snackbar';
|
||||||
import AsyncStorage from '@react-native-community/async-storage';
|
import AsyncStorage from '@react-native-community/async-storage';
|
||||||
|
import { withAuthed } from '../enhancers/authEnhancers';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
zipcode: ?string,
|
zipcode: ?string,
|
||||||
|
|
@ -21,9 +22,10 @@ type Props = {
|
||||||
router: {
|
router: {
|
||||||
history: {
|
history: {
|
||||||
replace: (route: string) => void,
|
replace: (route: string) => void,
|
||||||
|
push: (route: string) => void,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
isLoggedIn: boolean,
|
isAuthed: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProfilePage = (props: Props) => {
|
export const ProfilePage = (props: Props) => {
|
||||||
|
|
@ -35,13 +37,21 @@ export const ProfilePage = (props: Props) => {
|
||||||
error,
|
error,
|
||||||
toggleGPS,
|
toggleGPS,
|
||||||
router: { history },
|
router: { history },
|
||||||
isLoggedIn,
|
isAuthed,
|
||||||
} = props;
|
} = props;
|
||||||
const usingGPS = zipcode === 'usegps';
|
const usingGPS = zipcode === 'usegps';
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, flexDirection: 'column', }}>
|
<View style={{ flex: 1, flexDirection: 'column' }}>
|
||||||
<Text style={{ fontSize: 42, marginBottom: 8, color: '000', padding: 16 }}>Profile</Text>
|
<Text style={{ fontSize: 42, marginBottom: 8, color: '#000', padding: 16 }}>Profile</Text>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between', borderColor: '#ccc', borderTopWidth: 1, borderBottomWidth: 1, padding: 16 }}>
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
borderColor: '#ccc',
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
padding: 16,
|
||||||
|
}}>
|
||||||
<Text style={{ fontSize: 16 }}>Use My Location</Text>
|
<Text style={{ fontSize: 16 }}>Use My Location</Text>
|
||||||
<Switch value={usingGPS} onValueChange={toggleGPS} />
|
<Switch value={usingGPS} onValueChange={toggleGPS} />
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -70,7 +80,7 @@ export const ProfilePage = (props: Props) => {
|
||||||
)}
|
)}
|
||||||
{!!error && <Text style={{ fontSize: 16, color: 'red' }}>{error}</Text>}
|
{!!error && <Text style={{ fontSize: 16, color: 'red' }}>{error}</Text>}
|
||||||
<View style={{ marginTop: 16, padding: 8 }}>
|
<View style={{ marginTop: 16, padding: 8 }}>
|
||||||
{isLoggedIn && (
|
{isAuthed && (
|
||||||
<IconButton
|
<IconButton
|
||||||
glyph="exit-to-app"
|
glyph="exit-to-app"
|
||||||
text="Logout"
|
text="Logout"
|
||||||
|
|
@ -79,7 +89,7 @@ export const ProfilePage = (props: Props) => {
|
||||||
textStyle={{ color: palette.accentColor }}
|
textStyle={{ color: palette.accentColor }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isLoggedIn && (
|
{!isAuthed && (
|
||||||
<IconButton
|
<IconButton
|
||||||
glyph="exit-to-app"
|
glyph="exit-to-app"
|
||||||
text="Login"
|
text="Login"
|
||||||
|
|
@ -140,5 +150,6 @@ export default compose(
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.getZipcode();
|
this.props.getZipcode();
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
withAuthed
|
||||||
)(ProfilePage);
|
)(ProfilePage);
|
||||||
|
|
|
||||||
83
js/routes.js
83
js/routes.js
|
|
@ -1,83 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from 'react';
|
|
||||||
import { shape, func, string, arrayOf } from 'prop-types';
|
|
||||||
import Food from './pages/Food';
|
|
||||||
import Places from './pages/Places';
|
|
||||||
import Faves from './pages/Faves';
|
|
||||||
import FoodItemDetail from './pages/FoodItemDetail';
|
|
||||||
import PlaceDetail from './pages/PlaceDetail';
|
|
||||||
|
|
||||||
export type Route = {
|
|
||||||
title: string,
|
|
||||||
component: React.Component<any, any, any>,
|
|
||||||
icon: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
food: {
|
|
||||||
title: 'Food',
|
|
||||||
component: Food,
|
|
||||||
icon: 'local-pizza',
|
|
||||||
},
|
|
||||||
places: {
|
|
||||||
title: 'Places',
|
|
||||||
component: Places,
|
|
||||||
icon: 'restaurant',
|
|
||||||
},
|
|
||||||
faves: {
|
|
||||||
title: 'Faves',
|
|
||||||
component: Faves,
|
|
||||||
icon: 'stars',
|
|
||||||
},
|
|
||||||
foodDetail: {
|
|
||||||
title: 'Food Details',
|
|
||||||
component: FoodItemDetail,
|
|
||||||
icon: 'local-pizza',
|
|
||||||
},
|
|
||||||
placeDetail: {
|
|
||||||
title: 'Place Details',
|
|
||||||
component: PlaceDetail,
|
|
||||||
icon: 'domain',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const routingContextPropTypes = {
|
|
||||||
router: shape({
|
|
||||||
history: shape({
|
|
||||||
push: func.isRequired,
|
|
||||||
replace: func.isRequired,
|
|
||||||
goBack: func.isRequired,
|
|
||||||
entries: arrayOf(
|
|
||||||
shape({
|
|
||||||
pathname: string.isRequired,
|
|
||||||
search: string.isRequired,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RoutingContextFlowTypes = {
|
|
||||||
router: {
|
|
||||||
history: {
|
|
||||||
push: Function,
|
|
||||||
replace: Function,
|
|
||||||
goBack: Function,
|
|
||||||
entries: Array<{
|
|
||||||
pathname: string,
|
|
||||||
search: string,
|
|
||||||
}>,
|
|
||||||
},
|
|
||||||
route: {
|
|
||||||
location: {
|
|
||||||
pathname: string,
|
|
||||||
state: ?Object,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
match: {
|
|
||||||
params: {
|
|
||||||
type?: string,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
41
js/streams/FavesStream.js
Normal file
41
js/streams/FavesStream.js
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
//@flow
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { Record, List, get } from 'immutable';
|
||||||
|
import filter$ from './FilterStream';
|
||||||
|
import foodItems$ from './FoodItemsStream';
|
||||||
|
import { test } from 'ramda';
|
||||||
|
|
||||||
|
export type Fave = {
|
||||||
|
food_item_id: string,
|
||||||
|
date: number,
|
||||||
|
};
|
||||||
|
export type Faves = List<Record>;
|
||||||
|
|
||||||
|
const observable: BehaviorSubject<Faves> = new BehaviorSubject(null);
|
||||||
|
|
||||||
|
export function emit(fave: ?Faves) {
|
||||||
|
observable.next(fave);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitOne(fave: ?Fave) {
|
||||||
|
observable.first().subscribe(currentFaves => observable.next(currentFaves.push(fave)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observable
|
||||||
|
.combineLatest(foodItems$)
|
||||||
|
.map(([faves, foodItems]) => {
|
||||||
|
return faves && faves.map(fave => get(foodItems, get(fave, 'food_item_id')));
|
||||||
|
})
|
||||||
|
.combineLatest(filter$)
|
||||||
|
.map(([faveFoodItems, filter]) => {
|
||||||
|
const filterTest = filter.search && test(new RegExp(filter.search, 'i'));
|
||||||
|
return (
|
||||||
|
faveFoodItems &&
|
||||||
|
faveFoodItems.filter(faveFoodItem => {
|
||||||
|
if (!faveFoodItem) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !filterTest || filterTest(faveFoodItem.name);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue