Created
December 28, 2016 04:25
-
-
Save chemok78/b0df2551019dc2950c3e91e54f052b2d to your computer and use it in GitHub Desktop.
React JS Online Recipe Box
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Storage.prototype.setObj = function(key,obj){ | |
//Extend local storage prototype so we can store objects | |
return this.setItem(key, JSON.stringify(obj)); | |
}; | |
Storage.prototype.getObj = function(key){ | |
return JSON.parse(this.getItem(key)); | |
} | |
//Save ReactBootstrap components in global variables | |
var Button = ReactBootstrap.Button; | |
var ButtonToolbar = ReactBootstrap.ButtonToolbar; | |
var Modal = ReactBootstrap.Modal; | |
var OverlayTrigger = ReactBootstrap.OverlayTrigger; | |
var FormGroup = ReactBootstrap.FormGroup; | |
var FormControl = ReactBootstrap.FormControl; | |
var HelpBlock = ReactBootstrap.HelpBlock; | |
var ControlLabel = ReactBootstrap.ControlLabel; | |
var HelpBlock = ReactBootstrap.HelpBlock; | |
var App = React.createClass({ | |
//Parent component to render all elements and hold state | |
getInitialState(){ | |
return{ | |
showModal:false, | |
//App parent container controls whether modal is visible or not | |
//passed down to AddRecipe component with this.props.showModal (true or false) | |
recipeKeys: [ ], | |
//Array of recipe names as single source of truth for all components. Passed down to all child components | |
recipes: [ ], | |
//Array of recipe objects as single source of truth for all components. Passed down to all child components | |
initialFormInput: {name: "", ingredients: []} | |
} | |
}, | |
deleteRecipe: function(recipe){ | |
//function to delete a recipe, takes recipe name as parameter | |
//setState here | |
var allKeys = this.state.recipeKeys.slice(); | |
var allRecipes = this.state.recipes.slice(); | |
var indexKey = allKeys.indexOf(recipe); | |
allKeys.splice(indexKey,1); | |
var allRecipesReduced = allRecipes.filter(function(item){ | |
return item.name !== recipe; | |
}); | |
this.setState({recipeKeys: allKeys, recipes: allRecipesReduced}, function(){ | |
//edit LocalStorage here | |
localStorage.removeItem(recipe); | |
localStorage.setObj("recipeKeys", allKeys); | |
}); | |
}, | |
addRecipeKey: function(recipe){ | |
//passed down to AddRecipe to add the recipe key to App Parent Component | |
var allKeys = this.state.recipeKeys.slice(); | |
//get a copy of the recipeKeys from state | |
var allRecipes = this.state.recipes.slice(); | |
//get a copy of recipes array of objects from state | |
if(allKeys.indexOf(recipe.name) === -1){ | |
//check if the recipe name is in the state already | |
allKeys.push(recipe.name); | |
allRecipes.push(recipe); | |
localStorage.setObj("recipeKeys", allKeys); | |
//add new recipe key to localStorage recipeKeys item | |
localStorage.setObj(recipe.name, recipe); | |
this.setState({recipeKeys: allKeys, recipes: allRecipes, initialFormInput:{name: "", ingredients:[]}}); | |
} else { | |
localStorage.setObj(recipe.name, recipe); | |
var index = allKeys.indexOf(recipe.name); | |
allRecipes[index] = recipe; | |
this.setState({recipeKeys: allKeys, recipes: allRecipes, initialFormInput:{name: "", ingredients: []}}); | |
} | |
}, | |
componentDidMount: function(){ | |
var dummyRecipes = [ | |
{ | |
"name": "Pizza", | |
"ingredients": ["Dough", "Tomato", "Cheese"] | |
}, | |
{ | |
"name": "Sushi", | |
"ingredients": ["Rice", "Seaweed", "Tuna"] | |
} | |
] | |
if(localStorage.getItem("recipeKeys") === null){ | |
//add dummy recipes and recipeKeys array if localStorage is empty | |
localStorage.setObj("recipeKeys", ["Pizza", "Sushi"]); | |
dummyRecipes.forEach(function(item){ | |
localStorage.setObj(item.name, item); | |
}); | |
this.setState({recipeKeys: ["Pizza", "Sushi"], recipes: dummyRecipes}); | |
} else { | |
var recipeKeys = localStorage.getObj("recipeKeys"); | |
var recipes = []; | |
//generate an array of recipes from local storage | |
recipeKeys.forEach(function(item){ | |
var recipeObject = localStorage.getObj(item); | |
recipes.push(recipeObject); | |
}); | |
this.setState({recipeKeys: recipeKeys, recipes: recipes}); | |
} | |
}, //ComponentDidMount | |
open: function(){ | |
//Passed down to AddRecipeButton with this.props.openModal | |
this.setState({showModal:true}); | |
}, | |
close: function(){ | |
//Passed down to AddRecipe with this.props.closeModal | |
this.setState({showModal:false, initialFormInput:{name: "", ingredients:[]}}); | |
}, | |
editRecipe: function(recipe){ | |
this.setState({initialFormInput: recipe}, function(){ | |
this.open(); | |
}); | |
}, | |
render: function(){ | |
return( | |
<div className="container"> | |
<h1 className="text-center" id="title">Recipe Box</h1> | |
<br/> | |
<RecipeList recipes = {this.state.recipes} deleteRecipe = {this.deleteRecipe} editRecipe={this.editRecipe} /> | |
{/*App passes this.state.recipes array to RecipeList*/} | |
{/*RecipeList passes item recipe Object to RecipeItem*/} | |
{/*RecipeItem passes ingredient string to IngredientItem*/} | |
<AddRecipeButton openModal = {this.open}/> | |
<AddRecipe closeModal = {this.close} showModal={this.state.showModal} addRecipeKey = {this.addRecipeKey} initialFormInput = {this.state.initialFormInput}/> | |
</div> | |
) | |
} | |
});//App Component | |
var RecipeList = function (props) { | |
//Recipe Component that receives display Array as props | |
//Loop through display Array and render a Recipe component that takes item as a props | |
//this.props.deleteRecipe is passed in from App Parent component | |
return ( | |
<ul className="list-group"> | |
{ | |
props.recipes.map( (item,index) => <RecipeItem recipe={item} deleteRecipe = {props.deleteRecipe} editRecipe={props.editRecipe}/> ) | |
} | |
</ul> | |
); | |
}; | |
var RecipeItem = React.createClass({ | |
//every Recipe component has it's own state | |
//receives a Recipe object as props | |
//Every RecipeItem renders Ingredient components | |
//this.props.deleteRecipe is passed in from RecipeList parent component | |
getInitialState: function(){ | |
return {displayIngredients: false} | |
}, | |
toggleRecipe: function() { | |
this.setState({displayIngredients: !this.state.displayIngredients}) | |
}, | |
render: function() { | |
return( | |
<li className="list-group-item recipeItem"> | |
<h3 onClick={this.toggleRecipe} className="text-center recipeTitle">{this.props.recipe.name}</h3> | |
<div style={{display: this.state.displayIngredients ? 'block' : 'none'}}> | |
<br/> | |
<ul className="list-group" > | |
{this.props.recipe.ingredients.map((item) => <IngredientItem ingredient={item} />)} | |
</ul> | |
<ButtonToolbar> | |
<DeleteRecipeButton deleteRecipe = {this.props.deleteRecipe} recipeName={this.props.recipe.name}/> | |
<EditRecipeButton editRecipe = {this.props.editRecipe} recipe={this.props.recipe}/> | |
</ButtonToolbar> | |
</div> | |
</li> | |
) | |
} | |
}); | |
var IngredientItem = function(props){ | |
return ( | |
<li className="list-group-item ingredientItem"> | |
<p className="text-center">{props.ingredient}</p> | |
</li> | |
) | |
}; | |
var DeleteRecipeButton = React.createClass({ | |
render: function(){ | |
return <Button bsStyle="danger" bsSize="small" onClick={() => this.props.deleteRecipe(this.props.recipeName)}>Delete</Button> | |
} | |
}); | |
var AddRecipeButton = React.createClass({ | |
render: function(){ | |
return ( | |
<Button bsStyle="primary" bsSize="large" onClick={this.props.openModal}> | |
Add Recipe | |
</Button> | |
) | |
} | |
}); | |
var EditRecipeButton = React.createClass({ | |
render: function(){ | |
return ( | |
<Button bsStyle="default" bsSize="small" onClick={() => this.props.editRecipe(this.props.recipe)}>Edit</Button> | |
) | |
} | |
}); | |
var AddRecipe = React.createClass({ | |
//Form in modal to add recipe | |
getInitialState(){ | |
//set initial state for showModal to false | |
return { | |
name: this.props.initialFormInput.name, | |
ingredients: this.props.initialFormInput.ingredients | |
}; | |
}, | |
getValidationStateName(){ | |
var length = this.state.name.length; | |
if(length > 0) { | |
return "success"; | |
} else { | |
return "error"; | |
} | |
}, | |
getValidationStateIngredients(){ | |
var length = this.state.ingredients.length; | |
if(length > 0){ | |
return "success"; | |
} else { | |
return "error"; | |
} | |
}, | |
handleInput: function(key,e){ | |
//method to handle update state with user input field | |
//key is the input field | |
//e is the event object onClick | |
var input = e.target.value; | |
if(key === "ingredients"){ | |
input = e.target.value.split(","); | |
} | |
var update = {}; | |
//create object to merge with state | |
update[key] = input; | |
//set the value of input field to update object | |
this.setState(update); | |
}, | |
handleSubmit(){ | |
var recipe = {name: this.state.name, ingredients: this.state.ingredients}; | |
this.props.addRecipeKey(recipe); | |
//Call addRecipeKey method from App parent component | |
this.props.closeModal(); | |
this.setState({name: "", ingredients: []}); | |
//reset the state of AddRecipe component, so when the form opens again it's blank | |
}, | |
componentWillReceiveProps: function(nextProps){ | |
this.setState({name: nextProps.initialFormInput.name, ingredients: nextProps.initialFormInput.ingredients}); | |
}, | |
render: function(){ | |
return ( | |
<div> | |
<Modal show={this.props.showModal} onHide={this.props.closeModal}> | |
<Modal.Header closeButton> | |
<Modal.Title>Add a Recipe Here</Modal.Title> | |
</Modal.Header> | |
<Modal.Body> | |
<form> | |
<FormGroup controlId="formNameText" validationState = {this.getValidationStateName()}> | |
<ControlLabel>Recipe</ControlLabel> | |
<FormControl | |
type="text" | |
placeholder="Give your recipe a name" | |
value={this.state.name} | |
onInput={this.handleInput.bind(this,'name')} | |
/> | |
<FormControl.Feedback /> | |
</FormGroup> | |
<br/> | |
<FormGroup controlId="formIngredientsTextarea" validationState = {this.getValidationStateIngredients()}> | |
<ControlLabel>Ingredients</ControlLabel> | |
<FormControl | |
componentClass="textarea" | |
placeholder="Insert your ingredients, separated by a comma" | |
value={this.state.ingredients} | |
onInput={this.handleInput.bind(this,'ingredients')} | |
/> | |
<FormControl.Feedback /> | |
<hr/> | |
</FormGroup> | |
<Button bsStyle="primary" onClick={this.handleSubmit}>Submit</Button> | |
</form> | |
</Modal.Body> | |
</Modal> | |
</div> | |
) | |
} | |
});//AddRecipeButton | |
ReactDOM.render(<App />, document.getElementById('app')); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<meta charset="utf-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<link href="https://fonts.googleapis.com/css?family=Bitter" rel="stylesheet"> | |
<link href="https://fonts.googleapis.com/css?family=Rubik" rel="stylesheet"> | |
</head> | |
<body> | |
<div class="container"> | |
<div id="app"> | |
</div> | |
</div> | |
</body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.container { | |
margin-top:20px; | |
} | |
body { | |
font-family: 'Rubik', sans-serif; | |
background-image: url("https://images.unsplash.com/photo-1424847651672-bf20a4b0982b"); | |
background-size:cover; | |
} | |
.recipeItem { | |
border-color: #7f8c8d; | |
opacity: 0.95; | |
} | |
#title { | |
color:white; | |
} | |
.recipeTitle { | |
color: #c0392b; | |
} | |
.ingredientItem{ | |
color: #2980b9; | |
border-color: #34495e; | |
opacity: 1.0; | |
margin-left:10%; | |
margin-right:10%; | |
} | |
.btn-toolbar{ | |
text-align: center; | |
margin-bottom: 20px; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment