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 is required to process package imports. If you need a different preprocessor remove all packages first.

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

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.

Editor Settings

Code Indentation

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

Visit your global Editor Settings.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

HTML

            
              <div id="app"></div>
            
          
!

CSS

            
              // Colors

$danger: crimson;

// Defaults

$defaultFont: 'Cabin', sans-serif;
$defaultBorder: 1px dashed tomato;
$hand-drawn-border-radius: 255px 15px 225px 15px/15px 225px 15px 255px;

html {
  font-size: 20px;
  font-family: $defaultFont;
  box-sizing: border-box;
}

*, *:before, *:after {
  box-sizing: inherit;
}

h1 {
  font-size: 3em;
}
h2 {
  font-size: 2em;
}
h3 {
  font-size: 1.5em;
}
h4 {
  font-size: 1.25em;
}

label {
  display: block;
}

textarea {
  display: block;
}

input[type="text"],
textarea {
  width: 100%;
  font-family: inherit;
  padding: 6px;
}

button[role=button],
input[type=submit] {
  cursor: pointer;
}

.container {
  max-width: 700px;
  margin: 1em auto;
  padding: .5em;
}

// Buttons

@mixin button($color, $padding) {
  padding: $padding;
  display: inherit;
  text-decoration: none;
  border: 1px solid black;
  border-radius: $hand-drawn-border-radius;
  font-family: $defaultFont;
  background-color: $color;
  color: white;
  font-size: 20px;
  margin-right: 1px;
  transition: background-color 0.4s ease;
  &:last-child {
    margin-left: 0;
  }
  &:hover {
    background-color: darken($color, 10);
  }
}

@mixin button-small($color, $padding) {
  @include button($color, $padding);
  font-size: 14px;
}

// ------------
// Linear Icons
// ------------

.lnr {
  display: inline-block;
  fill: currentColor;
  width: 1em;
  height: 1em;
  vertical-align: -0.05em;
}

// ------------
// Generic form stuff
// ------------

.has-error input,
.has-error textarea {
  border: 2px solid $danger;
}

.alert {
  font-size: .75em;
  padding: .5em;
  color: white;
  background-color: $danger;
}

// ------------
// Header
// ------------

.header {
  margin: 0 0 1rem 0;
  display: flex;
  justify-content: space-between;
  align-items: center;
  &__btn-outer {
    display: inline-block;
  }
  
  &__btn-inner {
    @include button(tomato, 10px 15px);
    
    &.active {
      background-color: gray;
    }
  }
  
  &:after {
    clear: both;
  }
}

// ------------
// Recipe list
// ------------

.recipe-list {
  &__item-outer {
    margin-bottom: .5em;
  }
  &__item-inner {
    font-size: 1.5em;
    text-decoration: none;
    color: tomato;
    border-bottom: $defaultBorder;
    transition: all 0.4s ease;
    
    &:hover {
      color: green;
      border-color: green;
    }
  }
}

// ------------
// Recipe page
// ------------

.recipe {
  border: $defaultBorder;
  padding: 1.5em;
  &__section {
    margin-top: 1em;
  }
  &__action-btn-group {

  }
  &__action-btn {
    @include button-small(gray, 5px 10px);
    display: inline;
  }
  &__header {
    font-size: 2em;
    margin: 0 0 0.5em 0;
    border-bottom: 2px solid tomato;
  }
  &__ingredient-item {
    line-height: 1.2em;
  }
}

// ------------
// Recipe form
// ------------

.recipe-form{
  
  &__group {
    margin-bottom: 1em;
  }
  
  &__control {
    font-size: 1rem;
    display: block;
    margin-top: 4px;
    outline: none;
    transition: all 0.4s ease;
    border-radius: $hand-drawn-border-radius;
    border: 2px solid gray;
    padding: 6px 10px;
    &:focus {
      border: 2px solid tomato;
    }
  }
  
  &__btn {
    @include button(gray, 10px 15px);
  }
}
            
          
!

JS

            
              const { IndexRoute, Router, Route, IndexLink, Link, hashHistory} = ReactRouter;

let mockRecipes = [
  {
    id: 'pumpkin-pie',
    name: 'Pumpkin pie',
    ingredients: 'Pumpkin\nSugar\nCrust',
    directions: 'Bake in oven for 30 mins at 350 degrees.'
  }, {
    id: 'cheese-pizza',
    name: 'Cheese pizza',
    ingredients: 'Cheese\nPizza crust\nMarinara',
    directions: 'Bake in the oven for 12 mins at 450 degrees.'
  }, {
    id: 'black-bean-burger',
    name: 'Black bean burger',
    ingredients: 'Black beans\nFlaxseeds, ground\nMustard\nNutritional yeast',
    directions: 'Saute in lightly oiled skillet over medium-high heat for 7 mins. Flip at 3.5 mins.'
  }
];

function replaceAll(str, find, replace) {
  return str.replace(new RegExp(find, 'g'), replace);
}

function generateId(recipe) {
  return replaceAll(recipe.name, ' ', '-').toLowerCase().trim();
}

// Mock API for testing without local storage
const mockApi = {
  recipes: mockRecipes,
  getAll() {
    return [...this.recipes];
  },
  
  getRecipe(id) {
    let recipe;
    
    if(typeof id != "string") {
      console.warn("Recipe ID must be a string");
    }
    recipe = this.recipes.find(recipe => recipe.id === id);
    
    if(!recipe) {
      throw new Error("Recipe not found.");
    }
    
    return Object.assign({}, recipe);
  },
  
  deleteRecipe(id) {
    if(typeof id != "string") {
      console.warn("Recipe ID must be a string");
    }
    
    const existingRecipeIndex = recipes.findIndex(r => r.id == id);
    
    this.recipes = [
      ...this.recipes.slice(0, existingRecipeIndex),
      ...this.recipes.slice(existingRecipeIndex + 1)
    ];
  },

  saveRecipe(recipe) {
    recipe = Object.assign({}, recipe);
    
    if(recipe.id) {
      const existingRecipeIndex = recipes.findIndex(r => r.id == recipe.id);
      this.recipes = [
        ...this.recipes.slice(0, existingRecipeIndex),
        recipe,
        ...this.recipes.slice(existingRecipeIndex + 1)
      ];
    } else {
      recipe = Object.assign(recipe, {id: generateId(recipe)})
      this.recipes = [
        ...this.recipes,
        recipe
      ];
    }
    return Object.assign({}, recipe);
  }
};

const LOCAL_STORAGE_KEY = '_kme211_recipes';

const localStorageApi = {
  recipes: [],
  getAll() {
    let recipes = localStorage.getItem(LOCAL_STORAGE_KEY);
    if(!recipes || !recipes.length) {
      localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(mockRecipes));
      recipes = [].concat(mockRecipes);
    } else {
      recipes = JSON.parse(recipes);
    }
    
    this.recipes = recipes;
    return this.recipes;
  },
  
  saveRecipe(recipe) {
    recipe = Object.assign({}, recipe);
    
    if(recipe.id) {
      const existingRecipeIndex = this.recipes.findIndex(r => r.id == recipe.id);
      this.recipes = [
        ...this.recipes.slice(0, existingRecipeIndex),
        recipe,
        ...this.recipes.slice(existingRecipeIndex + 1)
      ];
    } else {
      recipe = Object.assign(recipe, {id: generateId(recipe)})
      this.recipes = [
        ...this.recipes,
        recipe
      ];
    }
    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.recipes))
    return Object.assign({}, recipe);
  },
  
  getRecipe(id) {
    let recipe;
    if(typeof id != "string") {
      console.warn("Recipe ID must be a string");
    }
    recipe = this.recipes.find(recipe => recipe.id === id);
    
    if(!recipe) {
      throw new Error("Recipe not found.");
    }
    
    return Object.assign({}, recipe);
  },
  
  deleteRecipe(id) {
    if(typeof id != "string") {
      console.warn("Recipe ID must be a string");
    }
    
    const existingRecipeIndex = this.recipes.findIndex(r => r.id == id);
    
    this.recipes = [
      ...this.recipes.slice(0, existingRecipeIndex),
      ...this.recipes.slice(existingRecipeIndex + 1)
    ];
    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.recipes))
  }
};

// Set which API to use
const api = localStorageApi;

// Helper to remove any HTML from form inputs
const formHelpers = {
  stripHTML(string) {
    var re = /(<([^>]+)>)/gi;
    return string.replace(re, "")
  }
};

const Button = ({value, onClick, iconName, classes}) => {
  return (
    <button className={classes} onClick={onClick} role="button"><span className={"lnr lnr-" + iconName}></span>{value}</button>
  );
}

const TextInput = ({name, label, onChange, value, error}) => {
  let wrapperClass = 'recipe-form__group';
  if(error && error.length > 0) {
    wrapperClass += " " + 'has-error';
  }
  
  return (
    <div className={wrapperClass}>
      <div className="field">
        <label htmlFor={name}>{label}</label>
        <input
          type="text"
          name={name}
          className="recipe-form__control"
          value={value}
          onChange={onChange}/>
        {error && <div className="alert alert-danger">{error}</div>}
      </div>
    </div>
  );
}

const TextArea = ({name, label, onChange, value, error}) => {
  let wrapperClass = 'recipe-form__group';
  if(error && error.length > 0) {
    wrapperClass += " " + 'has-error';
  }
  
  return (
    <div className={wrapperClass}>
      <div className="recipe-form__field">
        <label htmlFor={name}>{label}</label>
        <textarea
          name={name}
          className="recipe-form__control"
          value={value}
          rows={value.split('\n').length}
          onChange={onChange}>
        </textarea>
        {error && <div className="alert alert-danger">{error}</div>}
      </div>
    </div>
  );
}

TextInput.propTypes = {
  name: React.PropTypes.string.isRequired,
  label: React.PropTypes.string.isRequired,
  onChange: React.PropTypes.func.isRequired,
  value: React.PropTypes.string.isRequired,
  error: React.PropTypes.string
};

const RecipeForm = ({recipe, onSave, onChange, saving, errors}) => {
  return (
    <form className="recipe-form">
      <TextInput
        name="name"
        label="Name"
        value={recipe.name}
        onChange={onChange}
        error={errors.name}/>
      
      <TextArea 
        name="ingredients"
        label="Ingredients"
        value={recipe.ingredients}
        onChange={onChange}
        error={errors.ingredients}/>
      
      <TextArea 
        name="directions"
        label="Directions"
        value={recipe.directions}
        onChange={onChange}
        error={errors.directions}/>
      
      <input
        type="submit"
        disabled={saving}
        value={saving ? 'Saving...' : 'Save'}
        className="recipe-form__btn"
        onClick={onSave}/>
    </form>
  );
}

class ManageRecipePage extends React.Component {
  constructor(props, context) {
    super(props, context);

    const recipeId = this.props.params.id;
    let recipe = {name: '', ingredients: '', directions: ''};
    if(recipeId) {
      recipe = api.getRecipe(recipeId);
    }
    
    this.state = {
      recipe: recipe,
      errors: {},
      saving: false
    };
    
    this.updateRecipeState = this.updateRecipeState.bind(this);
    this.saveRecipe = this.saveRecipe.bind(this);
  }
  
  recipeFormIsValid() {
    let formIsValid = true;
    let errors = {};

    if(this.state.recipe.name.length < 5) {
      errors.name = 'Name must be at least 5 characters.';
      formIsValid = false;
    }

    if(this.state.recipe.ingredients.split('\n').length < 2) {
      errors.ingredients = 'You must have at least 2 ingredients separated by line breaks.';
      formIsValid = false;
    }
    
    this.setState({errors: errors});
    return formIsValid;
  }
  
  redirect(id) {
    this.setState({saving: false});
    this.context.router.push('/recipe/' + id);
  }

  saveRecipe(event) {
    event.preventDefault();
    
    if(!this.recipeFormIsValid()) {
      return;
    }
    
    this.setState({saving: true});
    
    const cleanRecipe = {};
    for(let key in this.state.recipe) {
      cleanRecipe[key] = formHelpers.stripHTML(this.state.recipe[key]);
    }
    let savedRecipe = api.saveRecipe(cleanRecipe);
    this.redirect(savedRecipe.id);
  }
  
  updateRecipeState(event) {
    const field = event.target.name;
    let recipe = this.state.recipe;
    recipe[field] = event.target.value;
    return this.setState({recipe: recipe});
  }
  
  render() {
    return (
      <div>
        <RecipeForm 
          recipe={this.state.recipe}
          onChange={this.updateRecipeState}
          onSave={this.saveRecipe}
          errors={this.state.errors}
          saving={this.state.saving} />
      </div>
    );
  }
}

ManageRecipePage.contextTypes = {
  router: React.PropTypes.object
};

const Recipe = ({id, name, onDelete, ingredients, directions}) => {
  return (
    <div className="recipe">
      <h3 className="recipe__header">{name}</h3>
      
      <div className="recipe__action-btn-group" role="group" aria-label="actions">
        <Link to={"/edit/" + id} className="recipe__action-btn"><span className="lnr lnr-pencil"></span> Edit</Link>
        <Button 
          value="Delete" 
          onClick={onDelete} 
          iconName="trash"
          classes="recipe__action-btn"/>
      </div>
      
      <section className="recipe__section">
      <h4 className="recipe__header">Ingredients</h4>
      <ul className="recipe__ingredient-list">
        {ingredients.split('\n').map((ingredient, index) => {
          return <li 
                   className="recipe__ingredient-item"
                   key={index}>
            {ingredient}
          </li>
        })}
      </ul>
      </section>
      
      {directions && <section className="recipe__section">
        <h4 className="recipe__header">Directions</h4>
        <p className="recipe__directions">{directions}</p>
      </section>}
      
    </div>
  );
}

Recipe.propTypes = {
  id: React.PropTypes.string.isRequired,
  name: React.PropTypes.string.isRequired,
  onDelete: React.PropTypes.func.isRequired,
  ingredients: React.PropTypes.string.isRequired,
  directions: React.PropTypes.string.isRequired
};

class RecipePage  extends React.Component {
  
  constructor(props, context) {
    super(props, context);
    const id = props.params.id;
    let recipe;
    try {
      recipe = api.getRecipe(id);
      console.log('recipe', recipe)
    } catch(e) {
      console.error(e);
      this.redirect();
    }
    
    this.state = {
      recipe: recipe
    }
    this.deleteRecipe = this.deleteRecipe.bind(this, id);
  }
  
  redirect() {
    this.context.router.push('/');
  }
  
  deleteRecipe(id) {
    api.deleteRecipe(id);
    this.redirect();
  }
  
  render() {
    const {id, name, ingredients, directions} = this.state.recipe;
    return (
      <Recipe
        id={id}
        name={name}
        ingredients={ingredients}
        directions={directions}
        onDelete={this.deleteRecipe} />
    ); 
  }
}

RecipePage.contextTypes = {
  router: React.PropTypes.object
};

const RecipesList = ({ recipes }) => {
  return (
    <ul className="recipe-list">
    {recipes.map(recipe =>
        <li 
          key={recipe.id}
          className="recipe-list__item-outer">
                   <Link 
                     to={"/recipe/" + recipe.id}
                     className="recipe-list__item-inner">
                     {recipe.name}
                   </Link>
        </li>
     )}
     {recipes.length === 0 && <p>No recipes found. You should add some!</p>}
    </ul>
    
  );
}

RecipesList.propTypes = {
  recipes: React.PropTypes.array.isRequired
};

class RecipesPage extends React.Component {
  constructor(props, context) {
    super(props, context);
    this.state = {
      recipes: []
    };
  }
  
  componentDidMount() {
    this.setState({
      recipes:  api.getAll()
    });
  }
  
  render() {
    const {recipes} = this.state;
    return (
      <RecipesList recipes={recipes} />
    );
  }
}

const NavLink = (props) => {
  return (
    <Link {...props} activeClassName="active"/>
  );
}

const NavIndexLink = (props) => {
  return (
    <IndexLink {...props} activeClassName="active"/>
  );
}

const Header = () => {
  return (
    <header className="header">
      <h1 className="header__title">Recipe Box</h1>
      <ul role="nav" className="header__nav">
        <li className="header__btn-outer">
          <NavIndexLink 
              to="/" 
              className="header__btn-inner">
            Recipes
          </NavIndexLink>
        </li>
        <li className="header__btn-outer">
          <NavLink 
              to="/add"
              className="header__btn-inner">
          Add new recipe
          </NavLink>
        </li>
      </ul>
    </header>
  );
}

class App extends React.Component {
  constructor(props, context) {
    super(props, context);
  }
  
  render() {
    return (
      <div className="container">
        <Header/>
        {this.props.children}
      </div>
    );
  }
}

ReactDOM.render((
  <Router history={hashHistory}>
    <Route path="/" component={App}>
      <IndexRoute component={RecipesPage}/>
      <Route path="/recipe/:id" component={RecipePage}/>
      <Route path="/edit/:id" component={ManageRecipePage}/>
      <Route path="/add" component={ManageRecipePage}/>
    </Route>
  </Router>
), document.getElementById('app'));
            
          
!
999px

Console