├── .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 | 
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------