├── .eslintrc.json ├── .gitignore ├── README.md ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── components │ ├── Auth.js │ ├── GetUser.js │ ├── NavBar │ │ ├── View.js │ │ └── index.js │ ├── RecipeCard.js │ └── RecipeList.js ├── index.js ├── modules │ ├── index.js │ ├── recipe │ │ ├── actions.js │ │ ├── api.js │ │ ├── reducers.js │ │ └── sagas.js │ └── user │ │ ├── actions.js │ │ ├── api.js │ │ ├── reducers.js │ │ └── sagas.js ├── routes │ ├── AddRecipe │ │ ├── View.js │ │ └── index.js │ ├── Home │ │ ├── View.js │ │ └── index.js │ ├── LoginPage │ │ ├── View.js │ │ └── index.js │ ├── MyRecipes │ │ ├── View.js │ │ └── index.js │ ├── SignupPage │ │ ├── View.js │ │ └── index.js │ ├── SingleRecipe │ │ ├── View.js │ │ └── index.js │ └── index.js └── store.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "plugins": [ 5 | "react", 6 | "jsx-a11y", 7 | "import" 8 | ], 9 | "globals": { 10 | "document": true, 11 | "describe": true, 12 | "expect": true, 13 | "it": true, 14 | "window": true, 15 | "FileReader": true, 16 | "URLSearchParams": true, 17 | "localStorage": true 18 | }, 19 | "rules": { 20 | "react/jsx-filename-extension": 0, 21 | "jsx-a11y/label-has-for": 0 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-menu-monkey-client 2 | 3 | This is recipe box website created using Feathersjs for the backend and React for the frontend. The website displays dishes that users can click on to find recipes and pictures. New users can create an acccount which grants them access to adding new recipes. 4 | 5 | ![Front page screenshot](http://i.imgur.com/4G6gOAL.png) 6 | 7 | # How to run it 8 | 9 | Start [server](https://github.com/benawad/feathersjs-menu-monkey-backend) then: 10 | 11 | `git clone https://github.com/benawad/react-menu-monkey-client.git` 12 | 13 | `cd react-menu-monkey-client` 14 | 15 | `npm install` 16 | 17 | `npm start` 18 | 19 | # Learn how it was made 20 | 21 | Check out the [YouTube tutorial](https://www.youtube.com/watch?v=nR0kxhbI09I). 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "menu_monkey_client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "devDependencies": { 6 | "eslint": "^3.19.0", 7 | "eslint-config-airbnb": "^14.1.0", 8 | "eslint-plugin-import": "^2.2.0", 9 | "eslint-plugin-jsx-a11y": "^5.0.0", 10 | "eslint-plugin-react": "^7.0.0", 11 | "react-scripts": "0.9.5" 12 | }, 13 | "dependencies": { 14 | "aria": "^0.2.1", 15 | "feathers-authentication-client": "^0.3.2", 16 | "feathers-client": "^2.2.0", 17 | "feathers-localstorage": "^1.0.0", 18 | "feathers-rest": "^1.7.2", 19 | "history": "^4.6.1", 20 | "prop-types": "^15.5.8", 21 | "react": "^15.5.4", 22 | "react-dom": "^15.5.4", 23 | "react-redux": "^5.0.4", 24 | "react-router": "^4.1.1", 25 | "react-router-dom": "^4.1.1", 26 | "react-router-redux": "^4.0.8", 27 | "redux": "^3.6.0", 28 | "redux-actions": "^2.0.2", 29 | "redux-saga": "^0.15.3", 30 | "semantic-ui-react": "^0.68.2", 31 | "superagent": "^3.5.2" 32 | }, 33 | "scripts": { 34 | "start": "react-scripts start", 35 | "build": "react-scripts build", 36 | "test": "react-scripts test --env=jsdom", 37 | "eject": "react-scripts eject", 38 | "lint": "eslint src", 39 | "lint:fix": "eslint src --fix" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/benawad/react-menu-monkey-client/67a6c51fb216b1cba085330011dac923606acf21/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | React App 18 | 19 | 20 |
21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/Auth.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import { requestAuth } from '../modules/user/actions'; 7 | 8 | export default function requireAuthentication(Component) { 9 | class AuthenticatedComponent extends React.Component { 10 | 11 | componentWillMount() { 12 | this.props.requestAuth(() => { 13 | const redirectAfterLogin = this.props.location.pathname; 14 | this.props.history.push(`/login?next=${redirectAfterLogin}`); 15 | }); 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 | { Object.values(this.props.user).length ? 22 | : 23 |

...loading

24 | } 25 |
26 | ); 27 | } 28 | } 29 | 30 | AuthenticatedComponent.defaultProps = { 31 | user: {}, 32 | requestAuth: () => ({}), 33 | }; 34 | 35 | AuthenticatedComponent.propTypes = { 36 | user: PropTypes.shape({ 37 | _id: PropTypes.string, 38 | email: PropTypes.string, 39 | }), 40 | requestAuth: PropTypes.func, 41 | location: PropTypes.shape({ 42 | pathname: PropTypes.string.isRequired, 43 | }).isRequired, 44 | history: PropTypes.shape({ 45 | push: PropTypes.func, 46 | }).isRequired, 47 | }; 48 | 49 | const mapStateToProps = state => ({ 50 | user: state.user, 51 | }); 52 | 53 | function mapDispatchToProps(dispatch) { 54 | return bindActionCreators({ 55 | requestAuth, 56 | }, dispatch); 57 | } 58 | 59 | return connect(mapStateToProps, mapDispatchToProps)(AuthenticatedComponent); 60 | } 61 | -------------------------------------------------------------------------------- /src/components/GetUser.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import PropTypes from 'prop-types'; 5 | 6 | import { requestAuth } from '../modules/user/actions'; 7 | 8 | export default function getUser(Component) { 9 | class AuthenticatedComponent extends React.Component { 10 | 11 | componentWillMount() { 12 | this.props.requestAuth(() => {}); 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 | ); 19 | } 20 | } 21 | 22 | AuthenticatedComponent.defaultProps = { 23 | requestAuth: () => ({}), 24 | }; 25 | 26 | AuthenticatedComponent.propTypes = { 27 | requestAuth: PropTypes.func, 28 | }; 29 | 30 | const mapStateToProps = () => ({}); 31 | 32 | function mapDispatchToProps(dispatch) { 33 | return bindActionCreators({ 34 | requestAuth, 35 | }, dispatch); 36 | } 37 | 38 | return connect(mapStateToProps, mapDispatchToProps)(AuthenticatedComponent); 39 | } 40 | -------------------------------------------------------------------------------- /src/components/NavBar/View.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Menu } from 'semantic-ui-react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const loggedIn = (email, requestLogout, history) => ( 6 | 7 | history.push('/profile/recipes')}> 8 | {email} 9 | 10 | 11 | { requestLogout(); history.push('/'); }}> 12 | logout 13 | 14 | 15 | ); 16 | 17 | const loggedOut = history => ( 18 | 19 | history.push('/signup')}> 20 | sign up 21 | 22 | 23 | history.push('/login')}> 24 | login 25 | 26 | 27 | ); 28 | 29 | const NavBar = ({ user, requestLogout, history }) => ( 30 | 31 | history.push('/recipes/add')}> 32 | Add Recipe 33 | 34 | { 35 | Object.values(user).length 36 | ? loggedIn(user.email, requestLogout, history) 37 | : loggedOut(history) 38 | } 39 | 40 | ); 41 | 42 | NavBar.propTypes = { 43 | user: PropTypes.shape({ 44 | _id: PropTypes.string, 45 | email: PropTypes.string, 46 | }), 47 | requestLogout: PropTypes.func, 48 | history: PropTypes.shape({ 49 | push: PropTypes.func, 50 | }).isRequired, 51 | }; 52 | 53 | export default NavBar; 54 | -------------------------------------------------------------------------------- /src/components/NavBar/index.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | 4 | import View from './View'; 5 | import { requestLogout } from '../../modules/user/actions'; 6 | 7 | function mapStateToProps(state) { 8 | return { 9 | user: state.user, 10 | }; 11 | } 12 | 13 | function mapDispatchToProps(dispatch) { 14 | return bindActionCreators({ 15 | requestLogout, 16 | }, dispatch); 17 | } 18 | 19 | export default connect(mapStateToProps, mapDispatchToProps)(View); 20 | -------------------------------------------------------------------------------- /src/components/RecipeCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card } from 'semantic-ui-react'; 3 | import { withRouter } from 'react-router-dom'; 4 | import PropTypes from 'prop-types'; 5 | 6 | const RecipeCard = ({ _id, name, imageUrl, description, history }) => ( 7 | history.push(`/view/${_id}`)} 14 | key={_id} 15 | /> 16 | ); 17 | 18 | RecipeCard.defaultProps = { 19 | history: {}, 20 | }; 21 | 22 | RecipeCard.propTypes = { 23 | _id: PropTypes.string.isRequired, 24 | name: PropTypes.string.isRequired, 25 | imageUrl: PropTypes.string.isRequired, 26 | description: PropTypes.string.isRequired, 27 | history: PropTypes.shape({ 28 | push: PropTypes.func, 29 | }), 30 | }; 31 | 32 | export default withRouter(RecipeCard); 33 | -------------------------------------------------------------------------------- /src/components/RecipeList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card } from 'semantic-ui-react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import RecipeCard from './RecipeCard'; 6 | 7 | const RecipeList = ({ recipes }) => ( 8 | 9 | {recipes.map(recipe => RecipeCard(recipe))} 10 | 11 | ); 12 | 13 | RecipeList.propTypes = { 14 | recipes: PropTypes.arrayOf(PropTypes.shape({ 15 | _id: PropTypes.string.isRequired, 16 | name: PropTypes.string.isRequired, 17 | imageUrl: PropTypes.string.isRequired, 18 | description: PropTypes.string.isRequired, 19 | })).isRequired, 20 | }; 21 | 22 | export default RecipeList; 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | 5 | import store from './store'; 6 | import Routes from './routes'; 7 | 8 | const app = ( 9 | 10 | 11 | 12 | ); 13 | 14 | ReactDOM.render( 15 | app, 16 | document.getElementById('root'), 17 | ); 18 | -------------------------------------------------------------------------------- /src/modules/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import superagent from 'superagent'; 4 | import feathers from 'feathers-client'; 5 | import rest from 'feathers-rest/client'; 6 | import auth from 'feathers-authentication-client'; 7 | import { fork, all } from 'redux-saga/effects'; 8 | 9 | import { currRecipe, myRecipes, recipes } from './recipe/reducers'; 10 | import * as recipeSagas from './recipe/sagas'; 11 | import { user } from './user/reducers'; 12 | import * as userSagas from './user/sagas'; 13 | 14 | /* 15 | userSagas = { 16 | loginSaga: () =>, 17 | signupSaga: () =>, 18 | } 19 | */ 20 | 21 | export const rootReducer = combineReducers({ 22 | currRecipe, 23 | myRecipes, 24 | recipes, 25 | user, 26 | router: routerReducer, 27 | }); 28 | 29 | export function* rootSaga() { 30 | yield all([ 31 | ...Object.values(userSagas), 32 | ...Object.values(recipeSagas), 33 | ].map(fork)); 34 | } 35 | 36 | const host = 'http://localhost:3030'; 37 | export const app = feathers() 38 | .configure(rest(host).superagent(superagent)) 39 | .configure(feathers.hooks()) 40 | .configure(auth({ storage: window.localStorage })); 41 | 42 | export const usersService = app.service('users'); 43 | export const recipesService = app.service('recipes'); 44 | -------------------------------------------------------------------------------- /src/modules/recipe/actions.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | 3 | export const REQUEST_RECENT_RECIPES = 'REQUEST_RECENT_RECIPES'; 4 | export const RECEIVE_RECENT_RECIPES = 'RECEIVE_RECENT_RECIPES'; 5 | export const REQUEST_CREATE_RECIPE = 'REQUEST_CREATE_RECIPE'; 6 | export const REQUEST_RECIPE = 'REQUEST_RECIPE'; 7 | export const RECEIVE_RECIPE = 'RECEIVE_RECIPE'; 8 | export const REQUEST_MY_RECIPES = 'REQUEST_MY_RECIPES'; 9 | export const RECEIVE_MY_RECIPES = 'RECEIVE_MY_RECIPES'; 10 | 11 | export const requestRecentRecipes = createAction(REQUEST_RECENT_RECIPES); 12 | export const receiveRecentRecipes = createAction(RECEIVE_RECENT_RECIPES); 13 | export const requestCreateRecipe = createAction(REQUEST_CREATE_RECIPE); 14 | export const requestRecipe = createAction(REQUEST_RECIPE); 15 | export const receiveRecipe = createAction(RECEIVE_RECIPE); 16 | export const requestMyRecipes = createAction(REQUEST_MY_RECIPES); 17 | export const receiveMyRecipes = createAction(RECEIVE_MY_RECIPES); 18 | -------------------------------------------------------------------------------- /src/modules/recipe/api.js: -------------------------------------------------------------------------------- 1 | import { recipesService } from '../index'; 2 | 3 | export async function findRecipes(payload) { 4 | try { 5 | return await recipesService.find(payload); 6 | } catch (err) { 7 | return []; 8 | } 9 | } 10 | 11 | export async function getRecipe(payload) { 12 | try { 13 | return await recipesService.get(payload); 14 | } catch (err) { 15 | return {}; 16 | } 17 | } 18 | 19 | export async function createRecipe(payload) { 20 | try { 21 | return await recipesService.create(payload); 22 | } catch (err) { 23 | return {}; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modules/recipe/reducers.js: -------------------------------------------------------------------------------- 1 | import { handleAction } from 'redux-actions'; 2 | import { 3 | receiveRecipe, 4 | receiveMyRecipes, 5 | receiveRecentRecipes, 6 | } from './actions'; 7 | 8 | export const currRecipe = handleAction(receiveRecipe, { 9 | next(state, { payload }) { 10 | return payload; 11 | }, 12 | }, {}); 13 | 14 | export const myRecipes = handleAction(receiveMyRecipes, { 15 | next(state, { payload }) { 16 | return payload; 17 | }, 18 | }, []); 19 | 20 | export const recipes = handleAction(receiveRecentRecipes, { 21 | next(state, { payload }) { 22 | return payload; 23 | }, 24 | }, []); 25 | -------------------------------------------------------------------------------- /src/modules/recipe/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeEvery, call, put } from 'redux-saga/effects'; 2 | import { createRecipe, findRecipes, getRecipe } from './api'; 3 | 4 | import { 5 | receiveRecentRecipes, 6 | receiveRecipe, 7 | receiveMyRecipes, 8 | REQUEST_RECIPE, 9 | REQUEST_RECENT_RECIPES, 10 | REQUEST_MY_RECIPES, 11 | REQUEST_CREATE_RECIPE, 12 | } from './actions'; 13 | 14 | function* callRecentRecipes({ payload }) { 15 | const recipes = yield call(findRecipes, payload); 16 | yield put(receiveRecentRecipes(recipes.data)); 17 | } 18 | 19 | export function* recentRecipesSaga() { 20 | yield takeEvery(REQUEST_RECENT_RECIPES, callRecentRecipes); 21 | } 22 | 23 | function* callCreateRecipe({ payload: { redirect, data } }) { 24 | const recipe = yield call(createRecipe, data); 25 | // eslint-disable-next-line 26 | redirect(recipe._id); 27 | } 28 | 29 | export function* addRecipeSaga() { 30 | yield takeEvery(REQUEST_CREATE_RECIPE, callCreateRecipe); 31 | } 32 | 33 | function* callRecipe({ payload }) { 34 | const recipe = yield call(getRecipe, payload); 35 | yield put(receiveRecipe(recipe)); 36 | } 37 | 38 | export function* recipeSaga() { 39 | yield takeEvery(REQUEST_RECIPE, callRecipe); 40 | } 41 | 42 | function* callMyRecipes({ payload }) { 43 | const myRecipes = yield call(findRecipes, payload); 44 | yield put(receiveMyRecipes(myRecipes.data)); 45 | } 46 | 47 | export function* myRecipesSaga() { 48 | yield takeEvery(REQUEST_MY_RECIPES, callMyRecipes); 49 | } 50 | -------------------------------------------------------------------------------- /src/modules/user/actions.js: -------------------------------------------------------------------------------- 1 | import { createAction } from 'redux-actions'; 2 | // action type 3 | // request* REQUEST_* 4 | // receive* RECEIVE_* 5 | 6 | export const REQUEST_SIGNUP = 'REQUEST_SIGNUP'; 7 | export const REQUEST_LOGIN = 'REQUEST_LOGIN'; 8 | export const REQUEST_LOGOUT = 'REQUEST_LOGOUT'; 9 | export const RECEIVE_LOGOUT = 'RECEIVE_LOGOUT'; 10 | export const REQUEST_AUTH = 'REQUEST_AUTH'; 11 | export const RECEIVE_AUTH = 'RECEIVE_AUTH'; 12 | 13 | export const requestSignup = createAction(REQUEST_SIGNUP); 14 | export const requestLogin = createAction(REQUEST_LOGIN); 15 | export const requestAuth = createAction(REQUEST_AUTH); 16 | export const requestLogout = createAction(REQUEST_LOGOUT); 17 | export const receiveLogout = createAction(RECEIVE_LOGOUT); 18 | export const receiveAuth = createAction(RECEIVE_AUTH); 19 | -------------------------------------------------------------------------------- /src/modules/user/api.js: -------------------------------------------------------------------------------- 1 | import { app, usersService } from '../index'; 2 | 3 | export async function signup(payload) { 4 | try { 5 | return await usersService.create(payload); 6 | } catch (err) { 7 | return {}; 8 | } 9 | } 10 | 11 | /* 12 | payload = { 13 | email: 'bob', 14 | password: 'mypass', 15 | }; 16 | */ 17 | export async function login(payload) { 18 | try { 19 | return await app.authenticate({ 20 | strategy: 'local', 21 | ...payload, 22 | }); 23 | } catch (err) { 24 | return {}; 25 | } 26 | } 27 | 28 | export function logout() { 29 | return app.logout(); 30 | } 31 | 32 | export async function auth() { 33 | try { 34 | const token = localStorage.getItem('feathers-jwt'); 35 | const payload = await app.passport.verifyJWT(token); 36 | const user = await usersService.get(payload.userId); 37 | return user; 38 | } catch (err) { 39 | return {}; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/modules/user/reducers.js: -------------------------------------------------------------------------------- 1 | import { handleAction, combineActions } from 'redux-actions'; 2 | import { 3 | receiveAuth, 4 | receiveLogout, 5 | } from './actions'; 6 | 7 | // eslint-disable-next-line 8 | export const user = handleAction(combineActions(receiveAuth, receiveLogout), { 9 | next(state, action) { 10 | return action.payload; 11 | }, 12 | }, {}); 13 | -------------------------------------------------------------------------------- /src/modules/user/sagas.js: -------------------------------------------------------------------------------- 1 | import { takeEvery, call, put } from 'redux-saga/effects'; 2 | 3 | import { logout, signup, login, auth } from './api'; 4 | import { 5 | receiveAuth, 6 | receiveLogout, 7 | requestAuth, 8 | REQUEST_LOGIN, 9 | REQUEST_LOGOUT, 10 | REQUEST_SIGNUP, 11 | REQUEST_AUTH, 12 | } from './actions'; 13 | 14 | 15 | function* callLogin({ payload: { data, redirect } }) { 16 | yield call(login, data); 17 | yield put(requestAuth()); 18 | redirect(); 19 | } 20 | 21 | export function* loginSaga() { 22 | yield takeEvery(REQUEST_LOGIN, callLogin); 23 | } 24 | 25 | function* callSignup({ payload: { redirect, data } }) { 26 | yield call(signup, data); 27 | redirect(); 28 | } 29 | 30 | export function* signupSaga() { 31 | yield takeEvery(REQUEST_SIGNUP, callSignup); 32 | } 33 | 34 | function* callLogout() { 35 | yield call(logout); 36 | yield put(receiveLogout({})); 37 | } 38 | 39 | export function* logoutSaga() { 40 | yield takeEvery(REQUEST_LOGOUT, callLogout); 41 | } 42 | 43 | function* callAuth({ payload }) { 44 | const user = yield call(auth); 45 | yield put(receiveAuth(user)); 46 | if (!Object.values(user).length) { 47 | payload(); 48 | } 49 | } 50 | 51 | export function* authSaga() { 52 | yield takeEvery(REQUEST_AUTH, callAuth); 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/routes/AddRecipe/View.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Form, Message } from 'semantic-ui-react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | export default class AddRecipe extends Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { nameError: false, error: false, name: '', ingredients: '', description: '' }; 10 | } 11 | 12 | handleImageChange = (e) => { 13 | e.preventDefault(); 14 | 15 | const reader = new FileReader(); 16 | const file = e.target.files[0]; 17 | 18 | reader.onloadend = () => { 19 | this.setState({ 20 | file, 21 | imagePreviewUrl: reader.result, 22 | }); 23 | }; 24 | 25 | reader.readAsDataURL(file); 26 | } 27 | 28 | handleSubmit = (e) => { 29 | const name = this.state.name.trim(); 30 | if (name === '') { 31 | this.setState({ nameError: true, error: true }); 32 | } else if (this.state.imagePreviewUrl === undefined) { 33 | this.setState({ error: true }); 34 | } else { 35 | this.props.requestCreateRecipe({ 36 | redirect: id => this.props.history.push(`/view/${id}`), 37 | data: { 38 | name, 39 | description: this.state.description, 40 | ingredients: this.state.ingredients.split('\n'), 41 | imageUrl: this.state.imagePreviewUrl, 42 | }, 43 | }); 44 | } 45 | e.preventDefault(); 46 | } 47 | 48 | handleDescriptionChange = (e) => { 49 | this.setState({ description: e.target.value }); 50 | } 51 | 52 | handleIngredientChange = (e) => { 53 | this.setState({ ingredients: e.target.value }); 54 | } 55 | 56 | render() { 57 | return ( 58 |
59 | 60 | 61 | this.setState({ name: e.target.value })} placeholder="Recipe Name" /> 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 80 | 81 | 82 | ); 83 | } 84 | } 85 | 86 | AddRecipe.defaultProps = { 87 | requestCreateRecipe: () => ({}), 88 | }; 89 | 90 | AddRecipe.propTypes = { 91 | requestCreateRecipe: PropTypes.func, 92 | history: PropTypes.shape({ 93 | push: PropTypes.func, 94 | }).isRequired, 95 | }; 96 | -------------------------------------------------------------------------------- /src/routes/AddRecipe/index.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | 4 | import View from './View'; 5 | import { requestCreateRecipe } from '../../modules/recipe/actions'; 6 | 7 | function mapStateToProps() { 8 | return { 9 | }; 10 | } 11 | 12 | function mapDispatchToProps(dispatch) { 13 | return bindActionCreators({ 14 | requestCreateRecipe, 15 | }, dispatch); 16 | } 17 | 18 | export default connect(mapStateToProps, mapDispatchToProps)(View); 19 | -------------------------------------------------------------------------------- /src/routes/Home/View.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Divider } from 'semantic-ui-react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import RecipeList from '../../components/RecipeList'; 6 | 7 | export default class Home extends Component { 8 | 9 | componentWillMount() { 10 | this.props.requestRecentRecipes({ 11 | query: { $sort: { createdAt: -1 } }, 12 | }); 13 | } 14 | 15 | render() { 16 | return ( 17 |
18 | 19 | 20 |
21 | ); 22 | } 23 | } 24 | 25 | Home.defaultProps = { 26 | requestRecentRecipes: () => ({}), 27 | recipes: [], 28 | }; 29 | 30 | Home.propTypes = { 31 | requestRecentRecipes: PropTypes.func, 32 | recipes: PropTypes.arrayOf(PropTypes.shape({ 33 | _id: PropTypes.string.isRequired, 34 | name: PropTypes.string.isRequired, 35 | imageUrl: PropTypes.string.isRequired, 36 | description: PropTypes.string.isRequired, 37 | })), 38 | }; 39 | -------------------------------------------------------------------------------- /src/routes/Home/index.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | 4 | import View from './View'; 5 | import { requestRecentRecipes } from '../../modules/recipe/actions'; 6 | 7 | function mapStateToProps(state) { 8 | return { 9 | recipes: state.recipes, 10 | }; 11 | } 12 | 13 | function mapDispatchToProps(dispatch) { 14 | return bindActionCreators({ 15 | requestRecentRecipes, 16 | }, dispatch); 17 | } 18 | 19 | export default connect(mapStateToProps, mapDispatchToProps)(View); 20 | -------------------------------------------------------------------------------- /src/routes/LoginPage/View.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Form } from 'semantic-ui-react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | export default class LoginPage extends Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { username: '', password: '' }; 10 | } 11 | 12 | handleSubmit = (e) => { 13 | this.props.requestLogin({ 14 | redirect: () => { 15 | const params = new URLSearchParams(this.props.location.search); 16 | const next = params.get('next'); 17 | if (next) { 18 | this.props.history.push(next); 19 | } else { 20 | this.props.history.push('/'); 21 | } 22 | }, 23 | data: { 24 | email: this.state.username, 25 | password: this.state.password, 26 | }, 27 | }); 28 | e.preventDefault(); 29 | this.setState({ username: '', password: '' }); 30 | } 31 | 32 | render() { 33 | return ( 34 |
35 | 36 | 37 | this.setState({ username: e.target.value })} value={this.state.username} placeholder="Email" /> 38 | 39 | 40 | 41 | this.setState({ password: e.target.value })} value={this.state.password} placeholder="Password" type="password" /> 42 | 43 | 44 |
45 | ); 46 | } 47 | } 48 | 49 | LoginPage.defaultProps = { 50 | requestLogin: () => ({}), 51 | location: {}, 52 | }; 53 | 54 | LoginPage.propTypes = { 55 | requestLogin: PropTypes.func, 56 | location: PropTypes.shape({ 57 | search: PropTypes.string, 58 | }), 59 | history: PropTypes.shape({ 60 | push: PropTypes.func, 61 | }).isRequired, 62 | }; 63 | -------------------------------------------------------------------------------- /src/routes/LoginPage/index.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | 4 | import View from './View'; 5 | import { requestLogin } from '../../modules/user/actions'; 6 | 7 | function mapStateToProps() { 8 | return {}; 9 | } 10 | 11 | function mapDispatchToProps(dispatch) { 12 | return bindActionCreators({ 13 | requestLogin, 14 | }, dispatch); 15 | } 16 | 17 | export default connect(mapStateToProps, mapDispatchToProps)(View); 18 | -------------------------------------------------------------------------------- /src/routes/MyRecipes/View.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import RecipeList from '../../components/RecipeList'; 5 | 6 | export default class MyRecipes extends Component { 7 | 8 | componentWillMount() { 9 | /* eslint-disable */ 10 | this.props.requestMyRecipes({ 11 | query: { 12 | ownerId: this.props.user._id, 13 | }, 14 | }); 15 | /* eslint-enable */ 16 | } 17 | 18 | render() { 19 | return ( 20 | 21 | ); 22 | } 23 | } 24 | 25 | MyRecipes.defaultProps = { 26 | requestMyRecipes: () => ({}), 27 | user: {}, 28 | myRecipes: [], 29 | }; 30 | 31 | MyRecipes.propTypes = { 32 | requestMyRecipes: PropTypes.func, 33 | user: PropTypes.shape({ 34 | _id: PropTypes.string, 35 | email: PropTypes.string, 36 | }), 37 | myRecipes: PropTypes.arrayOf(PropTypes.shape({ 38 | _id: PropTypes.string.isRequired, 39 | name: PropTypes.string.isRequired, 40 | imageUrl: PropTypes.string.isRequired, 41 | description: PropTypes.string.isRequired, 42 | })), 43 | }; 44 | -------------------------------------------------------------------------------- /src/routes/MyRecipes/index.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | 4 | import View from './View'; 5 | import { requestMyRecipes } from '../../modules/recipe/actions'; 6 | 7 | function mapStateToProps(state) { 8 | return { 9 | myRecipes: state.myRecipes, 10 | }; 11 | } 12 | 13 | function mapDispatchToProps(dispatch) { 14 | return bindActionCreators({ 15 | requestMyRecipes, 16 | }, dispatch); 17 | } 18 | 19 | export default connect(mapStateToProps, mapDispatchToProps)(View); 20 | -------------------------------------------------------------------------------- /src/routes/SignupPage/View.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Button, Form } from 'semantic-ui-react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | export default class SignupPage extends Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = { username: '', password: '' }; 10 | } 11 | 12 | handleSubmit = (e) => { 13 | this.props.requestSignup({ 14 | redirect: () => this.props.history.push('/login'), 15 | data: { 16 | email: this.state.username, 17 | password: this.state.password, 18 | }, 19 | }); 20 | e.preventDefault(); 21 | this.setState({ username: '', password: '' }); 22 | } 23 | 24 | render() { 25 | return ( 26 |
27 | 28 | 29 | this.setState({ username: e.target.value })} value={this.state.username} placeholder="Email" /> 30 | 31 | 32 | 33 | this.setState({ password: e.target.value })} value={this.state.password} placeholder="Password" type="password" /> 34 | 35 | 36 |
37 | ); 38 | } 39 | } 40 | 41 | SignupPage.defaultProps = { 42 | requestSignup: () => ({}), 43 | }; 44 | 45 | SignupPage.propTypes = { 46 | requestSignup: PropTypes.func, 47 | history: PropTypes.shape({ 48 | push: PropTypes.func, 49 | }).isRequired, 50 | }; 51 | -------------------------------------------------------------------------------- /src/routes/SignupPage/index.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | 4 | import View from './View'; 5 | import { requestSignup } from '../../modules/user/actions'; 6 | 7 | function mapStateToProps() { 8 | return {}; 9 | } 10 | 11 | function mapDispatchToProps(dispatch) { 12 | return bindActionCreators({ 13 | requestSignup, 14 | }, dispatch); 15 | } 16 | 17 | export default connect(mapStateToProps, mapDispatchToProps)(View); 18 | -------------------------------------------------------------------------------- /src/routes/SingleRecipe/View.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Header, Image, Segment, Container } from 'semantic-ui-react'; 3 | import PropTypes from 'prop-types'; 4 | 5 | export default class SingleRecipe extends Component { 6 | 7 | componentWillMount() { 8 | this.props.requestRecipe(this.props.match.params.recipeId); 9 | } 10 | 11 | render() { 12 | const recipe = this.props.currRecipe; 13 | if (!Object.prototype.hasOwnProperty.call(recipe, 'name')) { 14 | return (

Loading...

); 15 | } 16 | 17 | return ( 18 |
19 |
{recipe.name}
20 | 21 | 22 | { /* eslint-disable */ } 23 | {recipe.ingredients.map((ing, i) => {ing})} 24 | 25 | 26 | {recipe.description.split('\n').map((d, i) =>

{d}

)} 27 | { /* eslint-enable */ } 28 |
29 |
30 | ); 31 | } 32 | } 33 | 34 | SingleRecipe.defaultProps = { 35 | requestRecipe: () => ({}), 36 | currRecipe: {}, 37 | }; 38 | 39 | SingleRecipe.propTypes = { 40 | requestRecipe: PropTypes.func, 41 | currRecipe: PropTypes.shape({ 42 | _id: PropTypes.string.isRequired, 43 | name: PropTypes.string.isRequired, 44 | imageUrl: PropTypes.string.isRequired, 45 | description: PropTypes.string.isRequired, 46 | }), 47 | match: PropTypes.shape({ 48 | params: PropTypes.shape({ 49 | recipeId: PropTypes.string, 50 | }), 51 | }).isRequired, 52 | }; 53 | 54 | -------------------------------------------------------------------------------- /src/routes/SingleRecipe/index.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | 4 | import View from './View'; 5 | import { requestRecipe } from '../../modules/recipe/actions'; 6 | 7 | function mapStateToProps(state) { 8 | return { 9 | currRecipe: state.currRecipe, 10 | }; 11 | } 12 | 13 | function mapDispatchToProps(dispatch) { 14 | return bindActionCreators({ 15 | requestRecipe, 16 | }, dispatch); 17 | } 18 | 19 | export default connect(mapStateToProps, mapDispatchToProps)(View); 20 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | BrowserRouter, 4 | Route, 5 | Link, 6 | } from 'react-router-dom'; 7 | import { Header, Container } from 'semantic-ui-react'; 8 | 9 | import Home from './Home'; 10 | import SingleRecipe from './SingleRecipe'; 11 | import LoginPage from './LoginPage'; 12 | import SignupPage from './SignupPage'; 13 | import AddRecipe from './AddRecipe'; 14 | import MyRecipes from './MyRecipes'; 15 | import requireAuthentication from '../components/Auth'; 16 | import getUser from '../components/GetUser'; 17 | import NavBar from '../components/NavBar'; 18 | 19 | export default () => ( 20 | 21 | 22 |
23 | Menu Monkey 24 |
25 | ()} /> 26 | 27 | ()} /> 28 | ()} /> 29 | ()} /> 30 | 31 | 32 |
33 |
34 | ); 35 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import createSagaMiddleware from 'redux-saga'; 3 | 4 | import { rootReducer, rootSaga } from './modules'; 5 | 6 | const sagaMiddleware = createSagaMiddleware(); 7 | 8 | export default createStore(rootReducer, {}, applyMiddleware(sagaMiddleware)); 9 | 10 | sagaMiddleware.run(rootSaga); 11 | --------------------------------------------------------------------------------