2
\$\begingroup\$

I have completed a coding challenge from Free Code Camp. The challenge's user stories are as follows:

  • I can create recipes that have names and ingredients.
  • I can see an index view where the names of all the recipes are visible.
  • I can click into any of those recipes to view it.
  • I can edit these recipes.
  • I can delete these recipes.
  • All new recipes I add are saved in my browser's local storage. If I refresh the page, these recipes will still be there.

Also, a rule was to use React and SASS (The FCC founder said that SCSS is acceptable).

This was my first attempt at using Redux. As a result, some things in the app are experimental. For example, the reducer works well and returns a new state each time - but I'm sure the code here could be better written.

Also, in the RecipeList component, I found myself confused at how I should use the component's state, or draw data from the Redux store. Some thoughts on this are most welcome.

As was recommended to me on a different code review, I used the BEM pattern for my SCSS.

I used Vex for the modals. This resulted in some pretty ugly code. For example, html tags within a string (which I think looks really confusing in the vicinity of JSX). But the functionality is fine, I believe. Would React-Bootstrap be a better option in this case?

Here is a link to a Codepen of the app

Javascript:

const initial = () => {
   let recipesInit = {
      recipes: [
         {id: 1, title: 'Pao de queijo', ings: '100ml milk, 100ml vegetable oil, Pinch of salt, 250g tapioca flour, 125g parmesan or vegetarian alternative, grated, 1 free-range egg'},
         {id: 2, title: 'Monastery Gyuvetch', ings: '2 lbs beef, 4 tomatoes, chopped, 1/2 lbs mushrooms, 1 cup rice, 1 onion, chopped, 15 olives, whole, a bunch of parsley, 2 tbsp vegetable oil, 1 tbsp butter, 1 tbsp sugar, 2 1/2 cups beef stock, black pepper, paprika and salt'},
         {id: 3, title: 'Hobaktteok', ings: '½ cup cooked butternut squash, ¼ cup honey, 2 tablespoon freshly squeezed lemon, 2 cups rice flour (frozen rice flour sold at a Korean grocery store or make your own), lemon zest from 1 lemon, 1 tablespoon water, ¼ teaspoon salt,2 tablespoons sugar'}
      ],
      currentlySelected: 1
   };

   var retrievedObject = localStorage.getItem('recipesObject');
   let parsedRetrievedObject = JSON.parse(retrievedObject);

   const noRecipesInLocalStorage = parsedRetrievedObject === null || parsedRetrievedObject.recipes.length == 0;

   if(noRecipesInLocalStorage) {
      return recipesInit;
   } else {
      return parsedRetrievedObject;
   }
};

// Reducer
const recipeReducer = (state = initial(), action) => {
   let newState = {};
   let newRecipes = [];

   switch (action.type) {
   case 'ADD_RECIPE':
      console.log(action);
      newRecipes = [...state.recipes, action];
      newState = Object.assign({}, state, {recipes: newRecipes});
      localStorage.setItem('recipesObject', JSON.stringify(newState));
      return newState;

   case 'DELETE_RECIPE':
      console.log(action);
      newRecipes = [
         ...state.recipes.slice(0, action.id),
         ...state.recipes.slice(action.id + 1)
      ];
      newState = Object.assign({}, state, {recipes: newRecipes});
      localStorage.setItem('recipesObject', JSON.stringify(newState));
      return newState;

   case 'CHANGE_SELECTED_RECIPE':
      console.log(action);
      return Object.assign({}, state, {currentlySelected: action.currentlySelected});

   case 'EDIT_RECIPE':
      console.log(action);
      newRecipes = [
         ...state.recipes.slice(0, action.id),
         action,
         ...state.recipes.slice(action.id + 1)
      ];
      newState = Object.assign({}, state, {recipes: newRecipes} );
      localStorage.setItem('recipesObject', JSON.stringify(newState));
      return newState;

   default:
      return state;

   }
};

const {createStore} = Redux;
const store = createStore(recipeReducer);

const RecipeList = React.createClass({
   getInitialState(){
      return {
         currentlySelected: store.getState().currentlySelected
      };
   },

   editPrompt() {
      let current = store.getState().recipes[this.state.currentlySelected];

      vex.close();

      vex.dialog.open({
         message: 'Edit Recipe',
         input: `<input name="editTitle" type="text" value="${current.title}" />
               <textarea name="editIngs" rows="7">${current.ings}</textarea>`,
         buttons: [
            $.extend({}, vex.dialog.buttons.NO, {
               text: 'Cancel',
               click: () => vex.close()
            }),
            $.extend({}, vex.dialog.buttons.YES, {
               text: 'OK'
            })
         ],
         callback: data => {
            if(data){
               store.dispatch({
                  type: 'EDIT_RECIPE',
                  id: this.state.currentlySelected,
                  title: data.editTitle,
                  ings: data.editIngs
               });
            }
         }
      });
   },

   infoPrompt(i) {
      store.dispatch({
         type: 'CHANGE_SELECTED_RECIPE',
         currentlySelected: i
      });

      this.setState({currentlySelected: i}, () => {
         let current = store.getState().recipes[this.state.currentlySelected];

         vex.dialog.open({
            message: `<h1>${current.title}</h1> \n ${current.ings}`,
            input: '',
            buttons: [
               $.extend({}, vex.dialog.buttons.YES, {
                  text: 'Cancel'
               }), $.extend({}, vex.dialog.buttons.NO, {
                  text: 'Edit',
                  click: () => this.editPrompt()
               })
            ]
         });
      });
   },

   render(){
      return (
         <ul className="col-xs-12">{
            this.props.recipes.map((recipe, i) => {
               return (<li className="jumbotron list__jumbo row" key={i}>
                  <div>
                     <span className="list__close-button col-xs-1" onClick={ () => store.dispatch({id: i, type: 'DELETE_RECIPE'}) }>&times;</span>
                  </div>
                  <div>
                     <span className="list__title col-xs-11" onClick={this.infoPrompt.bind(this, i)}>{recipe.title}</span>
                  </div>
               </li>);
            })
         }</ul>
      );
   }
});

const Header = (props) => {
   return (
      <div className="header-main">
         <h5 className="header__mainTitle"><a href="http://github.com/alanbuchanan">My Tasty Recipes</a></h5>
         <div className="header__addButton-container">
            <a onClick={props.save} target="_blank"><span className="glyphicon glyphicon-plus-sign"></span></a>
         </div>
      </div>
   );
};

const RecipeBox = React.createClass({
   savePrompt() {
      vex.dialog.open({
         message: 'Add a Recipe',
         input: `<input name="userTitle" type="text" placeholder="Name"/>
            <textarea name="userIngs" type="text" rows="7" placeholder="Ingredients/Cooking instructions"/>`,

         callback: data => {
            if(data) {
               store.dispatch({
                  type: 'ADD_RECIPE',
                  id: Date.now(),
                  title: data.userTitle,
                  ings: data.userIngs
               });
            }
         }
      });
   },

   render() {
      return (
         <div>
            <Header save={this.savePrompt}/>
            <RecipeList recipes={store.getState().recipes}/>
         </div>
      );
   }
});

const render = () => ReactDOM.render(<RecipeBox />, document.getElementById('root'));
store.subscribe(render);
render();

SCSS:

@import url(https://fonts.googleapis.com/css?family=Chewy);
@import url(https://fonts.googleapis.com/css?family=Bad+Script);
$bg-color: #eee;
$header-title-color: #d4d4d4 !important;
$add-button-color: #118DF0;
$jumbo-bg-color: #69938e;
$jumbo-font-color: #fff;
$close-btn-color: #cccccc;
$header-main-title-font: 'Chewy', cursive;
$list-title-font: 'Bad Script', cursive;

@mixin main-transition() {
    -webkit-transition: opacity .12s ease-in-out !important;
    -moz-transition: opacity .12s ease-in-out !important;
    -ms-transition: opacity .12s ease-in-out !important;
    -o-transition: opacity .12s ease-in-out !important;
    transition: opacity .12s ease-in-out !important;
    &:hover {
        opacity: 0.9;
    }
}

body {
    background-color: $bg-color;
}

ul {
    list-style-type: none;
    position: relative;
}

textarea {
    resize: none;
}

a {
    color: inherit;
    text-decoration: none;
    &:hover, &:active, &:visited, &:link {
        color: inherit;
        text-decoration: none;
    }
}

.header-main {
    height: 60px;
    padding: 10px;
}

.header__mainTitle {
    position: relative;
    float: left;
    font-family: $header-main-title-font;
    color: $header-title-color;
    font-size: 23px;
}
.header__addButton-container {
    position: relative;
    float: right;
}

.btn-circle-sm {
  width: 35px;
  height: 35px;
  text-align: center;
  padding: 2px 0;
  font-size: 20px;
  line-height: 1.65;
  border-radius: 30px;
}

.glyphicon {
    width: 100%;
    height:0;
    padding-bottom: 100%;
    border-radius: 50%;
    cursor: pointer;
    opacity: 1;
    font-size: 38px;
    color: $add-button-color;
    @include main-transition();

}

.list__jumbo {
    padding: 10px;
    padding-left: 30px !important;
    cursor: pointer;
    background-color: $jumbo-bg-color !important;
    color: $jumbo-font-color !important;
    min-height: 100px;
    margin: 0 auto;
    position: relative;
    margin-bottom: 10px;
    @include main-transition();
}

.list__title {
    margin-top: 10px;
    font-size: 40px;
    font-family: $list-title-font;
    display: inline-block;
    min-height: 100%
}

.list__close-button {
    margin-top: 10px;
    color: $close-btn-color;
}

@media screen and (min-width: 992px){
    .container {
        width: 750px
    }
}
\$\endgroup\$
4
\$\begingroup\$

I see some Redux conventions misconceptions:

  1. The reducer purpose is to change the state, I see that you also copy state to localStorage.

  2. You should use actionCreators to save data in localStorage. In actionCreator, you save data and then - dispatch action to reducer.

  3. Any of the React components with Redux don't need to have state. You should pass all callbacks and properties via props. And use the Redux's connect function to the root component.

  4. You don't need to use store.getState() in the React component views. You should read your data or state values from props.

  5. Not fully defined the state of application (now I see only data in state): you have selected/not-selected recipe - this is state, you have modal opened/closed - this is also state.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.