Pen Settings

HTML

CSS

CSS Base

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

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

Auto Save

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.

Vue

              
                <template>
<main>
  <h1 id="title">RECIPE BOX</h1>

  <section id="actions">
    <select id="recipes" size="3" v-model="SELECTED" v-show="RECIPES_NAMES.length">
      <option v-for="name in RECIPES_NAMES">{{ name }}</option>
    </select>
    <div class="actions">
      <button type="button" class="bi bi-trash3-fill" @click="deleteRecipe" title="Delete Recipe" :disabled="!RECIPES_NAMES.length" />
      <button type="button" class="bi bi-pencil-fill" @click="openModal(SELECTED, [...RECIPES[SELECTED].ingredients], [...RECIPES[SELECTED].directions], 'edit')" title="Edit Recipe" :disabled="!SELECTED" />
      <button type="button" class="bi bi-plus-lg" @click="openModal()" title="Create Recipe" />
      <button type="button" class="bi bi-arrow-clockwise" @click="resetRecipes" title="Reset Recipes" />
    </div>
  </section>

  <section class="box" v-for="(arr, name) in RECIPES[SELECTED]" v-show="arr.length > 0">
    <h2 class="section-title">{{ name }}</h2>
    <component :is="name == 'directions' ? 'ol' : 'ul'" :class="name">
      <li v-for="i in arr">{{ i }}</li>
    </component>
  </section>
</main>


<Transition>
  <aside id="modal" v-show="MODAL_OPENED" ref="MODAL_WINDOW">
    <form id="modal-content" @submit.prevent="handleForm">
      <div class="box">
        <h2 class="section-title">Recipe</h2>
        <input type="text" ref="RECIPE_NAME_INPUT" v-model="FORM.recipe" placeholder="Recipe Name" required>
      </div>
      <div class="box" v-for="(arr, name) in { ingredients: FORM.ingredients, directions: FORM.directions }">
        <h2 class="section-title">{{ name }}</h2>
        <ul v-if="name == 'ingredients'" :class="name">
          <li v-for="(ingredient, i) in arr">
            <input type="text" v-model="arr[i]"/>
          </li>
        </ul>
        <ol :class="name" v-else>
          <li v-for="(direction, i) in arr">
            <textarea v-model="arr[i]" rows="5"/>
          </li>
        </ol>
        <div class="actions">
          <button type="button" class="bi bi-plus-lg" @click="arr.push('')" />
          <button type="button" class="bi bi-dash-lg" @click="arr.pop()" :disabled="!arr.length" />
        </div>
      </div>
      <div class="actions">
        <button type="submit">{{ FORM.action }}</button>
        <button type="button" @click="closeModal">Close</button>
      </div>
    </form>
  </aside>
</Transition>
</template>



<script>
import { ref, computed, watch, nextTick } from "vue";


export default {
  setup() {
    const INITIAL_RECIPES = {
      "Classic Waffles": {
        ingredients: [
          "2 cups all-purpose flour",
          "1 teaspoon salt",
          "4 teaspoons baking powder",
          "2 tablespoons white sugar",
          "2 eggs",
          "1 ½ cups warm milk",
          "⅓ cup butter, melted",
          "1 teaspoon vanilla extract"
        ],
        directions: [
          "In a large bowl, mix together flour, salt, baking powder and sugar; set aside. Preheat waffle iron to desired temperature.",
          "In a separate bowl, beat the eggs. Stir in the milk, butter and vanilla. Pour the milk mixture into the flour mixture; beat until blended.",
          "Ladle the batter into a preheated waffle iron. Cook the waffles until golden and crisp. Serve immediately."
        ]
      },
      "Chocolate Oatmeal Cookies": {
        ingredients: [
          "1 cup all-purpose flour",
          "3 tablespoons unsweetened cocoa powder",
          "1 teaspoon baking powder",
          "½ teaspoon baking soda",
          "½ teaspoon salt",
          "½ teaspoon ground cinnamon",
          "½ cup margarine",
          "½ cup brown sugar",
          "½ cup white sugar",
          "1 egg",
          "1 teaspoon vanilla extract",
          "1 ¼ cups rolled oats",
          "½ cup semisweet chocolate chips"
        ],
        directions: [
          "Preheat oven to 350 degrees F (175 degrees C). Grease cookie sheets. Stir together the flour, cocoa, baking powder, baking soda, salt and cinnamon; set aside.",
          "In a large bowl, cream together the margarine, brown sugar and white sugar. Beat in the egg and vanilla. Stir in the dry ingredients using a wooden spoon. Mix in the oats and chocolate chips. Drop by tablespoonfuls onto cookie sheets, leaving 2 inches between cookies.",
          "Bake for 8 to 10 minutes in the preheated oven, or until lightly browned. Allow cookies to cool on baking sheet for 5 minutes before removing to a wire rack to cool completely."
        ]
      },
      "Spatchcock Chicken": {
        ingredients: [
          "2 (3 1/2) pound whole chickens, wingtips removed",
          "2 teaspoons salt",
          "1 teaspoon dried tarragon",
          "1 teaspoon paprika",
          "¼ teaspoon black pepper",
          "4 teaspoons olive oil",
          "2 lemons, thinly sliced and seeded"
        ],
        directions: [
          "Preheat oven to 450 degrees F (230 degrees C). Line a large rimmed baking sheet with foil.",
          "Place chicken, breast side down, on a work surface. Starting at the tail end, cut along both sides of backbone with kitchen shears. Remove backbone. Grabbing hold of both sides of the chicken, open it like a book. Turn breast side up. Push down on each side of breast with your hands until you hear it crack. Flatten chicken and transfer to one short end of the prepared baking sheet. Repeat with the second chicken.",
          "Combine salt, tarragon, paprika, and pepper in a small bowl. Stir in oil. Run your fingers under chicken skin and rub tarragon paste under skin. Slide lemon slices under skin, in a single layer.",
          "Roast until skin is crisp and an instant-read thermometer inserted into thickest part of breast reads 165 degrees F, about 35 minutes. Let stand 5 minutes before cutting each chicken into 8 pieces."
        ]
      }
    },
          //
          KEY = "_SSbit01_recipes",
          RECIPES = ref({}),
          RECIPES_NAMES = computed(() => Object.keys(RECIPES.value)),
          SELECTED = ref("");
    
    try {
      if (typeof localStorage === "object" && navigator.cookieEnabled) {
        if (localStorage[KEY]) {
          const local = JSON.parse(localStorage[KEY]);
          RECIPES.value = local.recipes;
          SELECTED.value = local.selected;
        } else {
          RECIPES.value = { ...INITIAL_RECIPES }
          SELECTED.value = RECIPES_NAMES.value[0];
          localStorage[KEY] = JSON.stringify({
            recipes: RECIPES.value,
            selected: SELECTED.value
          });
        }
        watch([RECIPES.value, SELECTED], ([recipes, selected]) => {
          localStorage[KEY] = JSON.stringify({ recipes, selected })
        });
      } else {
        throw Error("LocalStorage not available");
      }
    } catch({ message }) {
      alert(message);
      RECIPES.value = { ...INITIAL_RECIPES }
      SELECTED.value = RECIPES_NAMES.value[0];
    }
    //
    
    //
    function deleteRecipe() {
      if (confirm(`Are you sure you want to delete the ${SELECTED.value} recipe?`)) {
        delete RECIPES.value[SELECTED.value];
        SELECTED.value = RECIPES_NAMES.value[0];
      }
    }
    
    function resetRecipes() {
      if (confirm("Are you sure you want to restore default recipes?")) {
        for (const i in RECIPES.value) delete RECIPES.value[i];
        Object.assign(RECIPES.value, INITIAL_RECIPES);
        SELECTED.value = RECIPES_NAMES.value[0];
      }
    }
    //
    
    // Modal
    const MODAL_WINDOW = ref(null),
          MODAL_OPENED = ref(false),
          RECIPE_NAME_INPUT = ref(null),
          FORM = ref({
            recipe: "",
            ingredients: [""],
            directions: [""],
            action: "add"
          });
    
    async function openModal(recipe = "", ingredients = [""], directions = [""], action = "add") {
      FORM.value = { recipe, ingredients, directions, action }
      MODAL_OPENED.value = true;
      document.body.style.overflow = "hidden";
      await nextTick();
      MODAL_WINDOW.value?.scrollTo(0, 0);
    }
    
    function closeModal() {
      MODAL_OPENED.value = false;
      document.body.style.overflow = "auto";
    }
    
    function handleForm() {
      const { value: { recipe, ingredients, directions, action } } = FORM;
      if (action == "edit") {
        delete RECIPES.value[SELECTED.value];
      }
      if (!RECIPES.value[recipe] || confirm("There's already a recipe with that name. Do you want to replace it?")) {
        RECIPES.value[recipe] = {
          ingredients: ingredients.filter(i => i != ""),
          directions: directions.filter(d => d != "")
        }
        SELECTED.value = recipe;
        closeModal();
      }
    }
    //
    
    return {
      RECIPES,
      RECIPES_NAMES,
      SELECTED,
      deleteRecipe,
      resetRecipes,
      MODAL_WINDOW,
      MODAL_OPENED,
      RECIPE_NAME_INPUT,
      FORM,
      openModal,
      closeModal,
      handleForm
    }
  }
}
</script>



<style lang="sass">
@import url("https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css")

body
  font-family: Avenir, Helvetica, Arial, sans-serif
  background-color: SandyBrown
  margin: 0
  
main
  display: grid
  gap: 1.5em
  color: white
  background-color: FireBrick
  max-width: 50em
  $px: .5em
  padding: 1em $px 2em $px
  border: medium solid DarkRed
  box-shadow: 4px 4px 1rem black
  margin: 0 auto

.v-
  &enter-active, &leave-active
    transition-duration: .2s
  &enter-from, &leave-to
    opacity: 0
  
.actions
  display: flex
  gap: .25em
  flex-wrap: wrap
  > button
    flex: 1
    font-size: inherit
    text-transform: capitalize
    font-variant: small-caps
    font-weight: bold
    text-shadow: 0 0 4px Black
    padding: .5em
    border: none
    border-radius: 4px
    box-shadow: 0 0 4px Black
    transition-property: color, background-color, text-shadow
    transition-duration: .2s
    &:enabled
      color: Cornsilk
      background-color: rgba(30, 30, 40, .8)
      cursor: pointer
      &:hover
        background-color: rgba(30, 45, 60, .75)
      &:active
        background-color: rgba(40, 60, 80, .75)
        text-shadow: 0 0 2px gold
    &:disabled
      cursor: not-allowed
      color: Black
  
.box
  display: grid
  background-color: rgba(0, 0, 0, .75)
  padding: 1em .75em
  border-radius: 8px
  box-shadow: 0 0 4px
  ul
    display: grid
    gap: .5em
    padding-left: 1em
    margin-top: .5em
    margin-bottom: 0
  li
    line-height: 1.5
    &::marker
      color: rgb(180, 240, 255)
    
.section-title
  font-variant: small-caps
  text-transform: capitalize
  text-align: center
  background-color: rgba(0, 0, 0, .4)
  max-width: max-content
  padding: .2em .4em
  border: thin solid DarkSlateGray
  border-radius: 4px
  $mx: auto
  margin: 0 $mx .5em $mx
  
.directions
  display: grid
  gap: 1em
  list-style: none
  padding: 0
  margin: 0
  > li
    counter-increment: count
    &::before
      content: "Step "counter(count)
      display: block
      text-decoration: underline
      color: rgb(180, 240, 255)
      font-variant: small-caps
      font-size: 1.5em
      font-style: italic
      margin-bottom: .25rem
  
#title
  text-align: center
  text-shadow: 1px 1px 4px black
  font-style: italic
  text-decoration: underline
  text-decoration-style: double
  margin: 0
  
#actions
  font-size: 1.4em
  
#recipes
  overflow: auto
  width: 100%
  font-size: inherit
  border: thick solid LightSlateGray
  border-radius: 8px
  box-shadow: 0 0 4px Black
  margin-bottom: .5em
  > option
    padding: .2em
  
#modal
  position: fixed
  z-index: 1
  left: 0
  top: 0
  width: 100%
  height: 100%
  background-color: rgba(0, 0, 0, .5)
  backdrop-filter: blur(.2em)
  display: flex
  box-sizing: border-box
  $px: 0
  padding: 1em $px 2.5em $px
  overflow: auto
  overscroll-behavior: none
  &-content
    display: grid
    gap: 1em
    color: white
    background-color: rgba(0, 0, 0, .5)
    width: 35em
    padding: .5em
    border: thin solid
    border-radius: 6px
    margin: auto
    .actions
      margin-top: 1em
      > button
        font-size: 1.4em
    > .box
      > ul
        list-style: none
        padding: 0
      input, textarea
        font: inherit
        box-sizing: border-box
        width: 100%
        padding: .4em
        border: thin solid transparent
        border-radius: 4px
        margin: auto
        transition: border-color .2s, box-shadow .2s
        &:focus
          border-color: Blue
          box-shadow: 0 0 4px 3px DodgerBlue
          outline: none
      textarea
        resize: vertical
</style>
              
            
!
999px

Console