mirror of
https://gitlab.com/wheres-the-tp/ui-mobile.git
synced 2026-01-25 06:14:55 -06:00
Merge branch '33-faves' into 'master'
Resolve "Fave food items list" Closes #33 See merge request aretherecookies/ui-mobile!26
This commit is contained in:
commit
221703c4f0
24 changed files with 502 additions and 269 deletions
52
js/App.js
52
js/App.js
|
|
@ -1,26 +1,27 @@
|
|||
//@flow
|
||||
import React, { Component } from "react";
|
||||
import { StatusBar } from "react-native";
|
||||
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";
|
||||
|
||||
import rxjsconfig from 'recompose/rxjsObservableConfig';
|
||||
import setObservableConfig from 'recompose/setObservableConfig';
|
||||
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 {
|
||||
static displayName = "App";
|
||||
static displayName = 'App';
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
|
@ -29,7 +30,7 @@ export default class App extends Component {
|
|||
<ThemeContext.Provider value={getTheme(theme)}>
|
||||
<AppContainer>
|
||||
<StatusBar backgroundColor={theme.statusBarColor} />
|
||||
<Redirect from="/" to="/landing" />
|
||||
<Redirect from="/" to="/landing" replace />
|
||||
<Switch>
|
||||
<Route path="/landing" component={LandingPage} />
|
||||
<Route path="/list/:type" component={Nav} />
|
||||
|
|
@ -38,16 +39,7 @@ export default class App extends Component {
|
|||
<Route path="/login" component={LoginPage} />
|
||||
<Route path="/logout" component={LoginPage} />
|
||||
<Route path="/zipcode" component={ZipcodePage} />
|
||||
<Route
|
||||
path="/createFoodItem"
|
||||
render={props => {
|
||||
if (!AuthManager.isLoggedIn()) {
|
||||
const search = props.location.search + "&" || "?";
|
||||
return <Redirect to={`/login${search}`} push />;
|
||||
}
|
||||
return <CreateFoodItem />;
|
||||
}}
|
||||
/>
|
||||
<Route path="/createFoodItem" component={CreateFoodItem} />
|
||||
</Switch>
|
||||
</AppContainer>
|
||||
</ThemeContext.Provider>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export type AUTH_PROVIDER =
|
|||
| typeof AUTH_PROVIDER_AUTH0;
|
||||
|
||||
export type User = {
|
||||
id: string,
|
||||
name: string,
|
||||
token: string,
|
||||
tokenExpires: number,
|
||||
|
|
@ -21,6 +22,11 @@ export type User = {
|
|||
provider: AUTH_PROVIDER,
|
||||
};
|
||||
|
||||
export type EmailAndPassword = {
|
||||
email: string,
|
||||
password: string,
|
||||
};
|
||||
|
||||
class AuthManager {
|
||||
user: ?User;
|
||||
|
||||
|
|
@ -28,15 +34,17 @@ class AuthManager {
|
|||
return this.user && this.user.token;
|
||||
};
|
||||
|
||||
getAuthHeader = () => {
|
||||
const { token, provider } = this.user || {};
|
||||
getEmail = () => {
|
||||
return this.user && this.user.email;
|
||||
};
|
||||
|
||||
switch (provider) {
|
||||
case AUTH_PROVIDER_FACEBOOK:
|
||||
return { authorization: `facebook-token ${token}` };
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
getId = () => {
|
||||
return this.user && this.user.id;
|
||||
};
|
||||
|
||||
getAuthHeader = () => {
|
||||
const { token } = this.user || {};
|
||||
return { authorization: `auth0-token ${token || ''}` };
|
||||
};
|
||||
|
||||
isLoggedIn = () => {
|
||||
|
|
@ -54,17 +62,19 @@ class AuthManager {
|
|||
}
|
||||
|
||||
this.setUser({
|
||||
email: auth0User.email,
|
||||
token: auth0User.accessToken,
|
||||
tokenExpires: +moment().add(auth0User.expiresIn, 'seconds'),
|
||||
id: auth0User.idToken,
|
||||
});
|
||||
};
|
||||
|
||||
setUser = ({ token, tokenExpires }) => {
|
||||
if (!token || +moment(tokenExpires) <= +moment()) {
|
||||
setUser = user => {
|
||||
if (!user.token || +moment(user.tokenExpires) <= +moment()) {
|
||||
this.user = null;
|
||||
AsyncStorage.removeItem('user');
|
||||
} else {
|
||||
this.user = { token, tokenExpires };
|
||||
this.user = user;
|
||||
AsyncStorage.setItem('user', JSON.stringify(this.user));
|
||||
}
|
||||
};
|
||||
|
|
@ -73,8 +83,8 @@ class AuthManager {
|
|||
const user = JSON.parse(await AsyncStorage.getItem('user'));
|
||||
if (user) {
|
||||
this.setUser(user);
|
||||
return this.user;
|
||||
}
|
||||
return this.isLoggedIn();
|
||||
};
|
||||
|
||||
authenticate = async ({ email, password }) => {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
// @flow
|
||||
import Auth0 from 'react-native-auth0';
|
||||
import { type EmailAndPassword } from '../AuthManager';
|
||||
|
||||
let auth0Instance = null;
|
||||
|
||||
export type Auth0User = {
|
||||
email: string,
|
||||
accessToken: string,
|
||||
expiresIn: number,
|
||||
idToken: string,
|
||||
};
|
||||
|
||||
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({
|
||||
username,
|
||||
username: email,
|
||||
password,
|
||||
realm: 'Username-Password-Authentication',
|
||||
});
|
||||
return creds;
|
||||
return {
|
||||
email,
|
||||
...creds,
|
||||
};
|
||||
};
|
||||
|
||||
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
|
||||
import { BASE_URL } from "../constants/AppConstants";
|
||||
import AuthManager from "../AuthManager";
|
||||
import { BASE_URL } from '../constants/AppConstants';
|
||||
import AuthManager from '../AuthManager';
|
||||
|
||||
export const fetchRequest = async ({
|
||||
endpoint,
|
||||
method,
|
||||
headers = {},
|
||||
body
|
||||
body,
|
||||
}: {
|
||||
endpoint: string,
|
||||
method: string,
|
||||
headers?: Object,
|
||||
body?: Object
|
||||
body?: Object,
|
||||
}) => {
|
||||
const res = await fetch(`https://${BASE_URL}${endpoint}`, {
|
||||
const res = await fetch(`http://${BASE_URL}${endpoint}`, {
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
...AuthManager.getAuthHeader(),
|
||||
...headers
|
||||
...headers,
|
||||
},
|
||||
...(body ? { body: JSON.stringify(body) } : {})
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
|
|
@ -36,10 +36,10 @@ export const fetchRequest = async ({
|
|||
|
||||
export const fetchRequestBinary = async ({ endpoint, body }: { endpoint: string, body: any }) => {
|
||||
return fetch(`http://${BASE_URL}${endpoint}`, {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
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 }) => {
|
||||
if (!foodItem) {
|
||||
return <View />;
|
||||
return <EmptyFoodItemTile />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import React, { Component } from 'react';
|
||||
import { View, TextInput, TouchableOpacity, SafeAreaView } from 'react-native';
|
||||
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 { getSearch, getViewMode } from '../helpers/RouteHelpers';
|
||||
import FoodItemSaveBtn from '../components/FoodItemSaveBtn';
|
||||
|
|
@ -31,19 +31,11 @@ class TopToolbar extends Component {
|
|||
const { router } = this.props;
|
||||
const { history } = router;
|
||||
|
||||
const notNil = compose(
|
||||
not,
|
||||
isNil
|
||||
);
|
||||
|
||||
const goBackTo = pipe(
|
||||
path(['route', 'location', 'search']),
|
||||
queryString.parse,
|
||||
path(['backto']),
|
||||
cond([[notNil, history.replace], [always(true), history.goBack]])
|
||||
);
|
||||
|
||||
goBackTo(router);
|
||||
if (history.length > 1) {
|
||||
history.goBack();
|
||||
} else {
|
||||
history.replace('/list/food');
|
||||
}
|
||||
};
|
||||
|
||||
toggleMapList = () => {
|
||||
|
|
@ -86,10 +78,15 @@ class TopToolbar extends Component {
|
|||
|
||||
const showSearch = /^\/list/.test(route);
|
||||
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 (
|
||||
<View style={{backgroundColor: palette.primaryColor}}>
|
||||
<View style={{ backgroundColor: palette.primaryColor }}>
|
||||
<SafeAreaView>
|
||||
{!showSearch && (
|
||||
<Toolbar
|
||||
|
|
@ -100,7 +97,11 @@ class TopToolbar extends Component {
|
|||
/>
|
||||
)}
|
||||
{showSearch && (
|
||||
<View style={{ flexDirection: 'row', padding: 10 }}>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
padding: 10,
|
||||
}}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
// @flow
|
||||
|
||||
// @public
|
||||
export const BASE_URL = 'aretherecookies.herokuapp.com';
|
||||
// export const BASE_URL = 'aretherecookies.herokuapp.com';
|
||||
|
||||
// @home
|
||||
// export const BASE_URL = '192.168.1.8:3000';
|
||||
|
||||
// @stouthaus
|
||||
// export const BASE_URL = '192.168.1.191:3000';
|
||||
export const BASE_URL = '192.168.1.181:3000';
|
||||
|
||||
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;
|
||||
|
||||
const getName: (f: FoodItemRecord) => String = pipe(
|
||||
path(['name']),
|
||||
toLower
|
||||
);
|
||||
const getName: (f: FoodItemRecord) => String = pipe(path(['name']), toLower);
|
||||
|
||||
const matchesName: FindPredicate<FoodItemRecord> = left => right => {
|
||||
return equals(getName(left), getName(right));
|
||||
|
|
@ -33,7 +30,7 @@ export const withFoodItems = mapPropsStream(props$ =>
|
|||
props$.combineLatest(FoodItems$, (props, foodItems) => {
|
||||
return {
|
||||
...props,
|
||||
foodItemsMap: foodItems && foodItems,
|
||||
foodItemsMap: foodItems,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// @flow
|
||||
import { getContext, withProps, compose } from 'recompose';
|
||||
import { shape, func, string } from 'prop-types';
|
||||
import { path } from 'ramda';
|
||||
import { path, pathOr } from 'ramda';
|
||||
import { getViewMode } from '../helpers/RouteHelpers';
|
||||
|
||||
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
|
||||
import { curry, pipe, pathOr } from 'ramda';
|
||||
import { curry, pipe, pathOr, path } from 'ramda';
|
||||
import queryString from 'query-string';
|
||||
|
||||
const PARAM_BACK_TO = 'backto';
|
||||
|
||||
type RouteTo = {
|
||||
pathname: string,
|
||||
search?: string,
|
||||
|
|
@ -18,8 +20,8 @@ export const routeWithQuery = curry(
|
|||
}
|
||||
);
|
||||
|
||||
export const routeWithTitle = curry(
|
||||
(pathname: string, routeTitle: string): RouteTo => routeWithQuery(pathname, { routeTitle })
|
||||
export const routeWithTitle = curry((pathname: string, routeTitle: string): RouteTo =>
|
||||
routeWithQuery(pathname, { routeTitle })
|
||||
);
|
||||
|
||||
export const getSearch = pipe(
|
||||
|
|
@ -27,7 +29,10 @@ export const getSearch = pipe(
|
|||
queryString.parse
|
||||
);
|
||||
|
||||
export const getViewMode = pipe(
|
||||
getSearch,
|
||||
pathOr('list', ['viewMode'])
|
||||
);
|
||||
export const getViewMode = pipe(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
|
||||
export default <T>(fn: Function, delay?: number = 0): (() => Promise<T>) => {
|
||||
let timeoutId;
|
||||
export default <T>(fn: Function, delay?: number = 100): (() => Promise<T>) => {
|
||||
let cacheTimeout;
|
||||
let cachedResult;
|
||||
return (...args) => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
if (cacheTimeout) {
|
||||
clearTimeout(cacheTimeout);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
|
||||
if (!cachedResult) {
|
||||
cachedResult = new Promise((resolve, reject) => {
|
||||
try {
|
||||
resolve(fn(...args));
|
||||
} catch (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 QuantityModal from '../modals/QuantityModal';
|
||||
import { wrapModalComponent } from '../modals/FullScreenModal';
|
||||
import { withAuthRedirect } from '../enhancers/authEnhancers';
|
||||
|
||||
type Props = {
|
||||
foodItem: typeof FoodItemRecord,
|
||||
|
|
@ -171,6 +172,7 @@ const updatePlace = ({ foodItem, setFoodItem, emitPlace }: Props) => placeDetail
|
|||
};
|
||||
|
||||
export default compose(
|
||||
withAuthRedirect,
|
||||
withCreateFoodItemState,
|
||||
withPlaceId,
|
||||
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 Carousel from 'react-native-looped-carousel';
|
||||
import CountBadge from '../components/CountBadge';
|
||||
import { routeWithTitle } from '../helpers/RouteHelpers';
|
||||
import { routeWithTitle, loginWithBackto } from '../helpers/RouteHelpers';
|
||||
import { Link } from 'react-router-native';
|
||||
import moment from 'moment';
|
||||
import { withUpdateQuantity } from '../enhancers/quantityEnhancers';
|
||||
import { getQuantityLabelText } from '../helpers/QuantityHelpers';
|
||||
import AuthManager from '../AuthManager';
|
||||
import RouterButton from 'react-router-native-button';
|
||||
import QuantityModal from '../modals/QuantityModal';
|
||||
import { withImages } from '../enhancers/imagesEnhancers';
|
||||
import { openImagePicker } from '../helpers/ImagePickerHelpers';
|
||||
import { identity } from 'ramda';
|
||||
import { identity, pathOr } from 'ramda';
|
||||
import Spinner from 'react-native-loading-spinner-overlay';
|
||||
import ImageRecord from '../records/ImageRecord';
|
||||
import { List } from 'immutable';
|
||||
import { List, get } from 'immutable';
|
||||
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;
|
||||
|
||||
|
|
@ -94,7 +97,7 @@ type Props = {
|
|||
place: PlaceRecord,
|
||||
currentImage: number,
|
||||
quantityModalOpen: boolean,
|
||||
isLoggedIn: boolean,
|
||||
isAuthed: boolean,
|
||||
changeCurrentImage: (index: number) => void,
|
||||
updateAmount: (quantity: Quantity) => void,
|
||||
updateQuantity: (arg: { foodItemId: string, quantity: Quantity }) => void,
|
||||
|
|
@ -103,6 +106,9 @@ type Props = {
|
|||
addImage: (arg: { foodItemId: string, imageUri: string }) => Promise<void>,
|
||||
imagesLoading: boolean,
|
||||
setImagesLoading: (loading: boolean) => void,
|
||||
addToFaves: () => void,
|
||||
isFave: boolean,
|
||||
deleteFromFaves: () => void,
|
||||
};
|
||||
|
||||
export const FoodItemDetail = (props: Props) => {
|
||||
|
|
@ -111,12 +117,15 @@ export const FoodItemDetail = (props: Props) => {
|
|||
place,
|
||||
currentImage,
|
||||
changeCurrentImage,
|
||||
isLoggedIn,
|
||||
isAuthed,
|
||||
addPhoto,
|
||||
toggleQuantityModal,
|
||||
quantityModalOpen,
|
||||
updateAmount,
|
||||
imagesLoading,
|
||||
addToFaves,
|
||||
isFave,
|
||||
deleteFromFaves,
|
||||
} = props;
|
||||
|
||||
if (!foodItem || !place) {
|
||||
|
|
@ -155,34 +164,48 @@ export const FoodItemDetail = (props: Props) => {
|
|||
<SubText>
|
||||
Last updated at {moment(foodItem.lastupdated).format('h:mm A on MMM D, YYYY')}
|
||||
</SubText>
|
||||
{isLoggedIn && (
|
||||
{isAuthed && (
|
||||
<Button
|
||||
title="Update quantity"
|
||||
onPress={toggleQuantityModal}
|
||||
color={theme.palette.primaryColor}
|
||||
/>
|
||||
)}
|
||||
{!isLoggedIn && (
|
||||
{!isAuthed && (
|
||||
<RouterButton
|
||||
title="Log in to update quantity"
|
||||
to={'/login'}
|
||||
to={loginWithBackto(`/foodItem/${foodItem.id}?loginAction=open-quantity-modal`)}
|
||||
color={theme.palette.primaryColor}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<View style={{ flex: 1, ...contentTileStyle }}>
|
||||
{/* <IconButton
|
||||
glyph="stars"
|
||||
text="Add to Faves"
|
||||
onPress={this.addToFaves}
|
||||
color={style.actionIconColor}
|
||||
/> */}
|
||||
{isLoggedIn && (
|
||||
{isAuthed && isFave ? (
|
||||
<IconButton
|
||||
glyph="favorite"
|
||||
text="Favorited"
|
||||
onPress={deleteFromFaves}
|
||||
color="#990000"
|
||||
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
|
||||
glyph="insert-photo"
|
||||
text="Add Photo"
|
||||
onPress={addPhoto}
|
||||
color={style.actionIconColor}
|
||||
textStyle={{ fontSize: 20, color: '#555555' }}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
|
@ -198,9 +221,10 @@ export default compose(
|
|||
withPlaceForFoodItem,
|
||||
withUpdateQuantity,
|
||||
withImages,
|
||||
withProps(() => ({
|
||||
isLoggedIn: AuthManager.isLoggedIn(),
|
||||
})),
|
||||
withAuthed,
|
||||
withFaves,
|
||||
withCurrentPath,
|
||||
withReplaceRoute,
|
||||
withState('currentImage', 'changeCurrentImage', 0),
|
||||
withState('quantityModalOpen', 'setQuantityModalOpen', false),
|
||||
withState('imagesLoading', 'setImagesLoading', true),
|
||||
|
|
@ -230,21 +254,51 @@ export default compose(
|
|||
await updateQuantity({ foodItemId: foodItem.id, quantity });
|
||||
Snackbar.show({ title: 'Food updated.', backgroundColor: 'black', color: 'white' });
|
||||
},
|
||||
toggleQuantityModal: ({ quantityModalOpen, setQuantityModalOpen }) => () => {
|
||||
setQuantityModalOpen(!quantityModalOpen);
|
||||
},
|
||||
toggleQuantityModal: ({ quantityModalOpen, setQuantityModalOpen }) =>
|
||||
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([
|
||||
'foodItem',
|
||||
'place',
|
||||
'currentImage',
|
||||
'quantityModalOpen',
|
||||
'imagesLoading',
|
||||
'isLoggedIn',
|
||||
'isAuthed',
|
||||
'faves',
|
||||
]),
|
||||
lifecycle({
|
||||
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({
|
||||
foodItemId,
|
||||
|
|
|
|||
|
|
@ -1,22 +1,23 @@
|
|||
// @flow
|
||||
import React from "react";
|
||||
import { View, Text, ScrollView, RefreshControl } from "react-native";
|
||||
import FoodItemTile from "../components/FoodItemTile";
|
||||
import FoodItemList from "../components/FoodItemList";
|
||||
import typeof FoodItemRecord from "../records/FoodItemRecord";
|
||||
import { withFoodItemsAsSeq } from "../enhancers/foodItemEnhancers";
|
||||
import { type SetSeq } from "immutable";
|
||||
import { compose, pure, withState, withHandlers, lifecycle } from "recompose";
|
||||
import { withFilter } from "../enhancers/filterEnhancers";
|
||||
import { withRouterContext } from "../enhancers/routeEnhancers";
|
||||
import FullScreenMessage from "../components/FullScreenMessage";
|
||||
import React from 'react';
|
||||
import { View, Text, ScrollView, RefreshControl } from 'react-native';
|
||||
import FoodItemTile from '../components/FoodItemTile';
|
||||
import FoodItemList from '../components/FoodItemList';
|
||||
import typeof FoodItemRecord from '../records/FoodItemRecord';
|
||||
import { withFoodItemsAsSeq } from '../enhancers/foodItemEnhancers';
|
||||
import { type SetSeq } from 'immutable';
|
||||
import { compose, pure, withState, withHandlers, lifecycle } from 'recompose';
|
||||
import { withFilter } from '../enhancers/filterEnhancers';
|
||||
import { withRouterContext } from '../enhancers/routeEnhancers';
|
||||
import FullScreenMessage from '../components/FullScreenMessage';
|
||||
import { withFaves } from '../enhancers/favesEnhancer';
|
||||
|
||||
type Props = {
|
||||
foodItemsSeq: SetSeq<FoodItemRecord>,
|
||||
isRefreshing: boolean,
|
||||
onRefresh: () => Promise<any>,
|
||||
onPulldown: () => {},
|
||||
isFilterDirty: boolean
|
||||
isFilterDirty: boolean,
|
||||
};
|
||||
|
||||
const FoodList = (props: Props) => {
|
||||
|
|
@ -36,8 +37,7 @@ const FoodList = (props: Props) => {
|
|||
{!showNoResults && (
|
||||
<ScrollView
|
||||
style={{ flex: 1 }}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onPulldown} />}
|
||||
>
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onPulldown} />}>
|
||||
<FoodItemList foodItemsSeq={foodItemsSeq}>
|
||||
{(foodItem: FoodItemRecord) => <FoodItemTile key={foodItem.id} foodItem={foodItem} />}
|
||||
</FoodItemList>
|
||||
|
|
@ -52,23 +52,24 @@ export default compose(
|
|||
withRouterContext,
|
||||
withFoodItemsAsSeq,
|
||||
withFilter,
|
||||
withState("isRefreshing", "setRefreshing", false),
|
||||
withFaves,
|
||||
withState('isRefreshing', 'setRefreshing', false),
|
||||
withHandlers({
|
||||
onPulldown: ({ setRefreshing, onRefresh, router }) => async () => {
|
||||
onPulldown: ({ setRefreshing, onRefresh, getFaves, router }) => async () => {
|
||||
try {
|
||||
setRefreshing(true);
|
||||
await onRefresh();
|
||||
await Promise.all([onRefresh(), getFaves()]);
|
||||
} catch (error) {
|
||||
console.error(error); // eslint-disable-line no-console
|
||||
router.history.push("/zipcode");
|
||||
router.history.push('/zipcode');
|
||||
} finally {
|
||||
setTimeout(() => setRefreshing(false), 1000);
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
lifecycle({
|
||||
componentDidMount() {
|
||||
this.props.onPulldown();
|
||||
}
|
||||
},
|
||||
})
|
||||
)(FoodList);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { withRouterContext } from '../enhancers/routeEnhancers';
|
|||
import AuthManager from '../AuthManager';
|
||||
import theme from '../ui-theme';
|
||||
import { Divider, Icon } from 'react-native-material-ui';
|
||||
import { getBackTo } from '../helpers/RouteHelpers';
|
||||
|
||||
const auth0ErrToStr = message => {
|
||||
if (typeof message === 'string') {
|
||||
|
|
@ -78,7 +79,11 @@ const LoginPageComponent = ({
|
|||
alignItems: 'center',
|
||||
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>
|
||||
|
||||
|
|
@ -91,7 +96,7 @@ const LoginPageComponent = ({
|
|||
onChangeText={setEmail}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
paddingLeft: 0
|
||||
paddingLeft: 0,
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
|
|
@ -108,7 +113,7 @@ const LoginPageComponent = ({
|
|||
onChangeText={setUsername}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
paddingLeft: 0
|
||||
paddingLeft: 0,
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
|
|
@ -126,7 +131,7 @@ const LoginPageComponent = ({
|
|||
onChangeText={setPassword}
|
||||
style={{
|
||||
fontSize: 16,
|
||||
paddingLeft: 0
|
||||
paddingLeft: 0,
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
|
|
@ -134,7 +139,7 @@ const LoginPageComponent = ({
|
|||
</View>
|
||||
|
||||
<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} />
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -160,7 +165,7 @@ const LoginPageComponent = ({
|
|||
style={{
|
||||
flexDirection: 'row',
|
||||
marginTop: 0,
|
||||
padding:16,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
|
|
@ -188,10 +193,11 @@ export default compose(
|
|||
withState('username', 'setUsername', ''),
|
||||
withState('isNewUser', 'setIsNewUser', false),
|
||||
withHandlers({
|
||||
checkAuth: ({ router }) => async () => {
|
||||
checkAuth: props => async () => {
|
||||
const isLoggedIn = await AuthManager.checkIsAuthed();
|
||||
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 ProfilePage from './ProfilePage';
|
||||
import AuthManager from '../AuthManager';
|
||||
import FavesPage from './FavesPage';
|
||||
|
||||
type Props = {
|
||||
activeTab: string,
|
||||
router: routerContext,
|
||||
setRoute: (route: string) => () => void,
|
||||
location: {
|
||||
pathname: string,
|
||||
},
|
||||
};
|
||||
|
||||
const Nav = (props: Props) => {
|
||||
|
|
@ -25,7 +29,8 @@ const Nav = (props: Props) => {
|
|||
<View style={{ flex: 1 }}>
|
||||
{activeTab === 'food' && <Food onRefresh={getLocation} />}
|
||||
{activeTab === 'places' && <Places onRefresh={getLocation} />}
|
||||
{activeTab === 'profile' && <ProfilePage isLoggedIn={AuthManager.isLoggedIn()} />}
|
||||
{activeTab === 'faves' && <FavesPage />}
|
||||
{activeTab === 'profile' && <ProfilePage />}
|
||||
</View>
|
||||
<SafeAreaView>
|
||||
<BottomNavigation active={activeTab}>
|
||||
|
|
@ -63,10 +68,10 @@ const Nav = (props: Props) => {
|
|||
}}
|
||||
/>
|
||||
<BottomNavigation.Action
|
||||
key="fave"
|
||||
key="faves"
|
||||
icon="favorite"
|
||||
label="Faves"
|
||||
onPress={setRoute('/list/food')}
|
||||
onPress={setRoute('/list/faves')}
|
||||
style={{
|
||||
container: {
|
||||
minWidth: 40,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { withRouterContext } from '../enhancers/routeEnhancers';
|
|||
import { getLocation } from '../apis/PositionApi';
|
||||
import Snackbar from 'react-native-snackbar';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import { withAuthed } from '../enhancers/authEnhancers';
|
||||
|
||||
type Props = {
|
||||
zipcode: ?string,
|
||||
|
|
@ -21,9 +22,10 @@ type Props = {
|
|||
router: {
|
||||
history: {
|
||||
replace: (route: string) => void,
|
||||
push: (route: string) => void,
|
||||
},
|
||||
},
|
||||
isLoggedIn: boolean,
|
||||
isAuthed: boolean,
|
||||
};
|
||||
|
||||
export const ProfilePage = (props: Props) => {
|
||||
|
|
@ -35,13 +37,21 @@ export const ProfilePage = (props: Props) => {
|
|||
error,
|
||||
toggleGPS,
|
||||
router: { history },
|
||||
isLoggedIn,
|
||||
isAuthed,
|
||||
} = props;
|
||||
const usingGPS = zipcode === 'usegps';
|
||||
return (
|
||||
<View style={{ flex: 1, flexDirection: 'column', }}>
|
||||
<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={{ flex: 1, flexDirection: 'column' }}>
|
||||
<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,
|
||||
}}>
|
||||
<Text style={{ fontSize: 16 }}>Use My Location</Text>
|
||||
<Switch value={usingGPS} onValueChange={toggleGPS} />
|
||||
</View>
|
||||
|
|
@ -70,7 +80,7 @@ export const ProfilePage = (props: Props) => {
|
|||
)}
|
||||
{!!error && <Text style={{ fontSize: 16, color: 'red' }}>{error}</Text>}
|
||||
<View style={{ marginTop: 16, padding: 8 }}>
|
||||
{isLoggedIn && (
|
||||
{isAuthed && (
|
||||
<IconButton
|
||||
glyph="exit-to-app"
|
||||
text="Logout"
|
||||
|
|
@ -79,7 +89,7 @@ export const ProfilePage = (props: Props) => {
|
|||
textStyle={{ color: palette.accentColor }}
|
||||
/>
|
||||
)}
|
||||
{!isLoggedIn && (
|
||||
{!isAuthed && (
|
||||
<IconButton
|
||||
glyph="exit-to-app"
|
||||
text="Login"
|
||||
|
|
@ -140,5 +150,6 @@ export default compose(
|
|||
componentDidMount() {
|
||||
this.props.getZipcode();
|
||||
},
|
||||
})
|
||||
}),
|
||||
withAuthed
|
||||
)(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