Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div class="container-fluid">
  <div class="row">
    <div class="col-md-12" id="header">
      <h1>Recipe Box</h1>
    </div>
    
    <div class="col-md-12" id="main">
    </div>
    
  </div>
</div>
              
            
!

CSS

              
                $danger: #d9534f;
$warning: #f0ad4e;
$success: #5cb85c;
$ingFieldWidth: 68%;

.form-group .container {
  width: 100%;
}

.form-group .row {
  margin-bottom: 10px;
}

.form-group .edit-ingredient,
.form-group .add-ingredient {
  display: inline-block;
  width: $ingFieldWidth;
}

.form-group .remove-ing {
  margin: 0 15px;
}

.form-group i:before {
  line-height: 2.4285;
  display: inline-block;
}

.form-group i:hover {
  cursor: pointer;
}

.form-group .remove-ing {
  color: $danger;
}

.form-group .add-ing {
  color: $success;
}

.warning {
  background-color: $warning;
  border-top-left-radius: 6px;
  border-top-right-radius: 6px;
  color: #fff;
}

#dlt-btns {
  margin-top: 50px;
}

#dlt-btns button {
  float: none;
}
              
            
!

JS

              
                /* Requires markdown-it markdown parser */
var md = window.markdownit();
/* Needed components from reactbootstrap */
var Button = ReactBootstrap.Button;
var ButtonToolbar = ReactBootstrap.ButtonToolbar;
var Accordion = ReactBootstrap.Accordion;
var Panel = ReactBootstrap.Panel;
var Modal = ReactBootstrap.Modal;
var FormGroup = ReactBootstrap.FormGroup;
var FormControl = ReactBootstrap.FormControl;
var ControlLabel = ReactBootstrap.ControlLabel;
var Row = ReactBootstrap.Row;
var Col = ReactBootstrap.Col;
var Grid = ReactBootstrap.Grid;
var HelpBlock = ReactBootstrap.HelpBlock;

/**React Components **/
var RecipeBox = React.createClass({
  getInitialState: function() {
    return {
      jcRB: JSON.parse(this.props.data)     
    };
  },
  handleEditRecipe: function(data) {
    this.setState({jcRB: data});
    
    /* Send new data to storage */
    var newStorage = JSON.stringify(this.state.jcRB);
    localStorage.setItem('jcRB', newStorage)
  },
  handleDeleteRecipe: function(recID) {
    /* Update the component to trigger UI update */
    this.state.jcRB.splice(recID, 1);
    this.setState({jcRB: this.state.jcRB})

    /* Send new data to storage */
    var newStorage = JSON.stringify(this.state.jcRB);
    localStorage.setItem('jcRB', newStorage)
  },
  render: function() {
    return (
      <div id="recipe-box">
        <RecipeList data={this.state.jcRB} handleEditRecipe={this.handleEditRecipe} handleDeleteRecipe={this.handleDeleteRecipe} />
        <div id="controls">
          <RecipeControls data={this.state.jcRB} handleEditRecipe={this.handleEditRecipe} />
        </div>
      </div>
    );
  }
});

var RecipeList = React.createClass({
  getInitialState: function() {
    return {
      showEditModal: false,
      showDeleteModal: false,
      recipe: ''
    }
  },
  editRecipe: function(e) {
    var recipeID = e.target.id.substr(12);
    this.setState({
      showEditModal: true,
      showDeleteModal: false,
      recipe: recipeID
    });
  },
  deleteRecipe: function(e) {
    var recipeID = e.target.id.substr(14);
    this.setState({
      showEditModal: false,
      showDeleteModal: true,
      recipe: recipeID
    });
  },
  processDeleteRecipe: function() {
    var recipeID = this.state.recipe;
    this.props.handleDeleteRecipe(this.state.recipe);
    this.setState({
      showEditModal: false,
      showDeleteModal: false,
      recipe: ''
    });
  },
  saveRecipe: function(id, newData) {
    /* Replace correct recipe and pass new data up to parent */
    var newList = this.props.data;
    newList[id] = newData;
    this.props.handleEditRecipe(newList);
    this.setState({ 
      showEditModal: false,
      showDeleteModal: false,
      recipe: ''
    });
  },
  closeModal: function() {
    this.setState({ 
      showEditModal: false,
      showDeleteModal: false,
      recipe: ''
    });
  },
  getName: function() {
    if ( this.state.recipe == '' ) {return};
    return this.props.data[this.state.recipe].name;  
  },
  render: function() {
    /* Get individual recipe data contained in storage */
    var theRecipes = this.props.data.map(function (data, index) {
      
      /* Split ingredient array into <li> objects */
      var theIngredients = function(data) {
        return(
          data.ingredients.map(function(ing, index) {
            return <li key={index} id={"ingredient-" + index}>{ing}</li>
          })
        )
      };
      
      var theDirections = function(data) {
        return {__html: md.render(data.directions)};
      };

      /* Return RecipeList component HTML */
      return (
          <Panel key={index} header={data.name} eventKey={index}>
            <h4>Ingredients</h4>
              <ul>
                {theIngredients(data)}
              </ul>
              <hr />
              <h4>Directions</h4>
              <div id="recipe-direction" dangerouslySetInnerHTML={theDirections(data)} />
              <hr />
              <ButtonToolbar id="recipe-btns">
                <Button id={"edit-recipe-" + index} className="edit-recipe" onClick={this.editRecipe}><i className="fa fa-pencil" aria-hidden="true"></i> Edit</Button>
                <Button bsStyle="danger" id={"delete-recipe-" + index} className="delete-recipe" onClick={this.deleteRecipe}><i className="fa fa-trash" aria-hidden="true"></i> Delete</Button>
              </ButtonToolbar>
          </Panel>
        );
      }, this);
 
    return (
      <Accordion>
        {theRecipes}
        
        <Modal show={this.state.showEditModal} onHide={this.closeModal}>
          <Modal.Header closeButton>
            <Modal.Title>Edit Recipe</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            <ModalContent data={this.props.data[this.state.recipe]} modalSaveRecipe={this.saveRecipe} recipeId={this.state.recipe} closeModal={this.closeModal} />
          </Modal.Body>
        </Modal>
        
        <Modal show={this.state.showDeleteModal} onHide={this.closeModal}>
          <Modal.Header className="warning" closeButton>
            <Modal.Title>Delete Recipe</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            <p className="text-center">Are you sure you want to permanently delete:<br/><strong>{this.getName()}</strong> ?</p>
            <ButtonToolbar id="dlt-btns" className="text-center">
              <Button bsStyle="warning" onClick={this.processDeleteRecipe}>Yes</Button>
              <Button onClick={this.closeModal}>No</Button>
            </ButtonToolbar>
          </Modal.Body>
        </Modal>
      </Accordion>
    );
  }
});

var ModalContent = React.createClass({
  getInitialState() {
    return this.props.data;
  },
  handleChange: function(e, addNewIng, dltIng) {
    /* Update ingredient values */
    var theName = document.getElementById('name').value;
    var theIngredients = [];
    var iCount = document.getElementsByClassName('edit-ingredient').length;
    for (var i=0; i<iCount; i++) {
      theIngredients.push(document.getElementsByClassName('edit-ingredient')[i].value);
    }
    var theDirection = document.getElementById('directions').value;
    
    /* Add new ingredient to array */
    if ( addNewIng ) {
      theIngredients.push('');
    }
    
    /* Delete specific ingredient from array */
    if ( dltIng ) {
      var delId = e.target.id.substr(4);
      theIngredients.splice(delId, 1);
    }
    
    /* Set state and re-render form */
    this.setState({ 
      name: theName,
      ingredients: theIngredients,
      directions: theDirection
    });
  },
  getIngredients: function(data) {
    return(
        data.ingredients.map(function(ing, index) {
          return (
            <Row key={index}>
              <FormGroup controlId="theIngredient">
                <Col xs={12}>
                  <FormControl type="text" className="edit-ingredient" value={ing} onChange={this.handleChange} /> <i className="fa fa-minus remove-ing" id={"del-" + index} onClick={this.removeThisIng} aria-hidden="true"></i>
                  {this.lastIng(index)}
                </Col>
              </FormGroup>
            </Row>
          ); 
        }, this)
      );
  },
  lastIng: function(index) {
    if (index == this.state.ingredients.length-1) {
      return <i className="fa fa-plus add-ing" onClick={this.addNewIng} aria-hidden="true"></i>;
    }
      return;
  },
  removeThisIng: function(e) {
    this.handleChange(e, false, true);
  },
  addNewIng: function(e) {
    this.handleChange(e, true, false);
  },
  saveData: function() {
    var theId = this.props.recipeId;
    this.props.modalSaveRecipe(theId, this.state);
  },
  render: function() {
    return(
          <form>
            <FormGroup controlId="name">
              <ControlLabel>Name</ControlLabel>
              <Grid>
                <Row>
                  <Col xs={12}>
                <FormControl type="text" value={this.state.name} onChange={this.handleChange} />
                  </Col>
                </Row>
              </Grid>
            </FormGroup>
            <FormGroup controlId="editIngredients">
              <ControlLabel>Ingredients</ControlLabel>
                <Grid>
                  {this.getIngredients(this.state)}
                </Grid>
            </FormGroup>
            <hr />
            <FormGroup controlId="directions">
              <ControlLabel>Directions</ControlLabel>
              <Grid>
                <Row>
                  <Col xs={12}>
                <FormControl componentClass="textarea" value={this.state.directions} onChange={this.handleChange} />
                <HelpBlock>*markdown is supported</HelpBlock>
                  </Col>
                </Row>
              </Grid>
            </FormGroup>
            <Grid>
              <Row>
                <Col xs={12}>
                  <ButtonToolbar>
                    <Button bsStyle="success" onClick={this.saveData}>Save</Button>
                    <Button onClick={this.props.closeModal}>Close</Button>
                  </ButtonToolbar>
                </Col>
              </Row>
            </Grid>
          </form>
    );
  }
});

var RecipeControls = React.createClass({
  getInitialState: function() {
    return {
      showAddModal: false
    };
  },
  showAddModal: function() {
    this.setState({
      showAddModal: true  
    });
  },
  closeModal: function() {
    this.setState({
      showAddModal: false
    });
  },
  saveRecipe: function(newRecipe) {
    var newRecList = this.props.data;
    newRecList.push(newRecipe);
    this.props.handleEditRecipe(newRecList);
    this.setState({
      showAddModal: false
    })
  },
  render: function() {
    return (
      <div>
        <Button bsStyle="primary" onClick={this.showAddModal}><i className="fa fa-plus" aria-hidden="true"></i> Add Recipe</Button>

        <Modal show={this.state.showAddModal} onHide={this.closeModal}>
          <Modal.Header closeButton>
            <Modal.Title>Add Recipe</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            <ModalAddRecipe saveNewRecipe={this.saveRecipe} closeModal={this.closeModal} />
          </Modal.Body>
        </Modal>
      </div>
      
    );
  }
});

var ModalAddRecipe = React.createClass({
  getInitialState: function() {
    return {
      name: '',
      ingredients: [''],
      directions: ''
    };
  },
  getIngredients: function(data) {
    return(
        data.map(function(ing, index) {
          return (
            <Row key={index}>
              <FormGroup controlId="theIngredient">
                <Col xs={12}>
                  <FormControl type="text" className="add-ingredient" value={ing} onChange={this.handleChange} /> <i className="fa fa-minus remove-ing" id={"del-" + index} onClick={this.removeThisIng} aria-hidden="true"></i>{this.lastIng(index)}
                </Col>
              </FormGroup>
            </Row>
          ); 
        }, this)
      );
  },
  lastIng: function(index) {
    if (index == this.state.ingredients.length-1) {
      return <i className="fa fa-plus add-ing" onClick={this.addNewIng} aria-hidden="true"></i>;
    }
      return;
  },
  handleChange: function(e, addNewIng, dltIng) {
    /* Update ingredient values */
    var theName = document.getElementById('name').value;
    var theIngredients = [];
    var iCount = document.getElementsByClassName('add-ingredient').length;
    for (var i=0; i<iCount; i++) {
      theIngredients.push(document.getElementsByClassName('add-ingredient')[i].value);
    }
    var theDirection = document.getElementById('directions').value;
    
    /* Add new ingredient to array */
    if ( addNewIng ) {
      theIngredients.push('');
    }
    
    /* Delete specific ingredient from array */
    if ( dltIng ) {
      var delId = e.target.id.substr(4);
      theIngredients.splice(delId, 1);
    }
    
    /* Set state and re-render form */
    this.setState({ 
      name: theName,
      ingredients: theIngredients,
      directions: theDirection
    });
  },
  removeThisIng: function(e) {
    this.handleChange(e, false, true);
  },
  addNewIng: function(e) {
    this.handleChange(e, true, false);
  },
  saveData: function() {
    var newRecipe = this.state;
    if (newRecipe.name == '' ) {
      newRecipe.name = 'Undefined';
    }
    this.props.saveNewRecipe(newRecipe);
  },
  render: function() {
    return(
          <form>
            <FormGroup controlId="name">
              <ControlLabel>Name</ControlLabel>
              <Grid>
                <Row>
                  <Col xs={12}>
                <FormControl type="text" value={this.state.name} onChange={this.handleChange} />
                  </Col>
                </Row>
              </Grid>
            </FormGroup>
            <FormGroup controlId="editIngredients">
              <ControlLabel>Ingredients</ControlLabel>
                <Grid>
                  {this.getIngredients(this.state.ingredients)}
                </Grid>
            </FormGroup>
            <hr />
            <FormGroup controlId="directions">
              <ControlLabel>Directions</ControlLabel>
              <Grid>
                <Row>
                  <Col xs={12}>
                    <FormControl componentClass="textarea" value={this.state.directions} onChange={this.handleChange} />
                    <HelpBlock>*markdown is supported</HelpBlock>
                  </Col>
                </Row>
              </Grid>
            </FormGroup>
            <Grid>
              <Row>
                <Col xs={12}>
                  <ButtonToolbar>
                    <Button bsStyle="success" onClick={this.saveData}>Save</Button>
                    <Button onClick={this.props.closeModal}>Close</Button>
                  </ButtonToolbar>
                </Col>
              </Row>
            </Grid>
          </form>
    );
  }
});


/** 
* Check for browser localStorage capability.
* Returns True or False as needed.
**/
function storageAvailable(type) {
	try {
		var storage = window[type],
			x = '__storage_test__';
		storage.setItem(x, x);
		storage.removeItem(x);
		return true;
	}
	catch(e) {
		return false;
	}
}

/* Set default data if localStorage is initially null */
function setDefaultData() {
  var data = [
               {
                 name: 'Fall Off The Bone Ribs',
                 ingredients: ['1 rack of pork baby back ribs','juice of one lemon','1/4 cup of your favorite dry-rub','1/2 cup of your favorite bbq sauce'],
                 directions: '1. Preheat oven to 300° F.\n2. Remove excess fat from ribs. Peel the silver skin off the back of the ribs - lift with a sharp knife and grab with a paper towel to remove.\n3. Cut ribs apart into individual pieces.\n4. Rub ribs all over with lemon juice.\n5. Coat ribs with dry rub. Place meat side down in large baking pan, & cover tightly with foil, shiny side out.\n6. Bake in the oven for 2 1/2 hours.\n7. Remove from oven & pour off liquid.\n8. Brush bbq sauce over all sides of ribs.\n\n#### Finishing\n**Grill**: To finish ribs on the grill, remove from the pan and place ribs on the grill (I use a basket over direct but low heat) basting and turning a few times for about 10 minutes.\n\n**Oven**: To finish ribs in the oven, set oven to broil and return ribs to the same oven rack, basting and broiling about 5 minutes per side, watching so they don’t burn. They will be so tender, it’s best to turn them using gloved hands.'
               },
               {
                 name: 'Grilled Caprese Chicken',
                 ingredients: ['4 boneless skinless chicken breasts','1/3 cup olive oil','juice of 2 lemons','2 cloves garlic','6 large basil leaves','1 tsp salt','1/4 tsp white pepper','3/4 cup balsamic vinegar','4 slices of mozzarella cheese, 1/2 inch thick','3 tomoatoes, sliced','3 basil leaves, chopped'],
                 directions: '1. Place olive oil, lemon juice, garlic, basil, salt, and white pepper in a blender or food processor. Blend until mixture is pureed.\n2.Place chicken breasts in resealable plastic bag, pour blended mixture over chicken. Seal bag and marinate in refigerator for 1-2 hours.\n3. Place balsamic vinegar into small sauce pan and bring to a boil. Reduce head and simmer for 8-10 minutes. Once a syrupy like consistency is achieved, remove from head and set aside.\n4. Prehead grill to medium. Remove chicken from marinade and place onto grill. Cook 8-10 minutes per side depending on thickness.\n4. Toward end of cooking place a slice of mozzarella and 1-2 slices of tomato on each chicken breast. Cook for 2 additional minutes.\n5. Plate chicken breasts, drizzle balsamic reduction over chicken, and garnish with chopped basil leaves.'
               }
              ];
  var defaultData = JSON.stringify(data);
  localStorage.setItem('jcRB', defaultData);
}

/** 
* If browser has localStorage capability, run render React components. 
* Otherwise, display error message to user.
**/
if (storageAvailable('localStorage')) {
  if ( localStorage.getItem('jcRB') == null || localStorage.getItem('jcRB') == '[]' ) {
    setDefaultData();
    //TODO Allow user to selectively reload default data or start with empty recipe box
  }
  ReactDOM.render(
    <RecipeBox data={localStorage.getItem('jcRB')} />,
    document.getElementById('main')
  );
} else {
  document.getElementById('main').innerHTML = '<h2>Sorry, but your browser doesn\'t seem to support local data storage. Please enable and try again.</h2>';
}
              
            
!
999px

Console