Skip to content

Instantly share code, notes, and snippets.

@chemok78
Created December 28, 2016 04:25
Show Gist options
  • Save chemok78/b0df2551019dc2950c3e91e54f052b2d to your computer and use it in GitHub Desktop.
Save chemok78/b0df2551019dc2950c3e91e54f052b2d to your computer and use it in GitHub Desktop.
React JS Online Recipe Box
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'));
<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>
.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