install npm modules: npm install
run web app: open command line and type ‘npm run start’
We implement our files according to MVC paradigm:
We first create a recipe view file
src/js/views/recipeViews.js
We use fractional module to calculate integer display of fractions: npm install fractional –save
This is so that we can apply the effects to ingredient.count and ingredient.unit
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import { elements } from './base' import { Fraction } from 'fractional' const formatCount = count => { console.log(count) if (count) { // 2.5 --> 2 1/2 const result = count.toString().split('.').map(el => parseInt(el, 10)) console.log('result', result) if (!result[1]) return result[0] else if (result[0] == 0) { const fr = new Fraction(count.toFixed(1)) return `${fr.numerator}/${fr.denominator}` } else { const fr = new Fraction(count.toFixed(1) - result[0]) return `${result[0]} ${fr.numerator}/${fr.denominator}` } } return '?'; } const createIngredient = ingredient => { return `<li class="recipe__item"> <svg class="recipe__icon"> <use href="img/icons.svg#icon-check"></use> </svg> <div class="recipe__count">${formatCount(ingredient.count)}</div> <div class="recipe__ingredient"> <span class="recipe__unit">${ingredient.unit}</span> pasta </div> </li>` } export const clearResults = () => { elements.recipe.innerHTML = ''; } |
renderRecipe is used in our controller (index.js) to display html when we click on an item. We use the html from the template, and then add data to it via the recipe object.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
export const renderRecipe = recipe => { const markup = ` <figure class="recipe__fig"> <img src="${recipe.image}" alt="${recipe.title}" class="recipe__img"> <h1 class="recipe__title"> <span>${recipe.title}</span> </h1> </figure> <div class="recipe__details"> <div class="recipe__info"> <svg class="recipe__info-icon"> <use href="img/icons.svg#icon-stopwatch"></use> </svg> <span class="recipe__info-data recipe__info-data--minutes">${recipe.time}</span> <span class="recipe__info-text"> minutes</span> </div> <div class="recipe__info"> <svg class="recipe__info-icon"> <use href="img/icons.svg#icon-man"></use> </svg> <span class="recipe__info-data recipe__info-data--people">${recipe.servings}</span> <span class="recipe__info-text"> servings</span> <div class="recipe__info-buttons"> <button class="btn-tiny"> <svg> <use href="img/icons.svg#icon-circle-with-minus"></use> </svg> </button> <button class="btn-tiny"> <svg> <use href="img/icons.svg#icon-circle-with-plus"></use> </svg> </button> </div> </div> <button class="recipe__love"> <svg class="header__likes"> <use href="img/icons.svg#icon-heart-outlined"></use> </svg> </button> </div> <div class="recipe__ingredients"> <ul class="recipe__ingredient-list"> ${recipe.ingredients.map(el => createIngredient(el)).join(' ')} </ul> <button class="btn-small recipe__btn"> <svg class="search__icon"> <use href="img/icons.svg#icon-shopping-cart"></use> </svg> <span>Add to shopping list</span> </button> </div> <div class="recipe__directions"> <h2 class="heading-2">How to cook it</h2> <p class="recipe__directions-text"> This recipe was carefully designed and tested by <span class="recipe__by">${recipe.author}</span>. Please check out directions at their website. </p> <a class="btn-small recipe__btn" href="${recipe.url}" target="_blank" rel="noopener noreferrer"> <span>Directions</span> <svg class="search__icon"> <use href="img/icons.svg#icon-triangle-right"></use> </svg> </a> </div> `; elements.recipe.insertAdjacentHTML('afterbegin', markup) }; |
As you can see, when we fetched data for the recipe successfully, we then render the recipe.
src/js/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import Search from './models/Search' import Recipe from './models/Recipes' import * as searchView from './views/searchView' import * as recipeView from './views/recipeView' import { elements, renderLoader, clearLoader } from './views/base' const state = {} // other code ////// recipe ///////////// const controlRecipe = async () => { const id = window.location.hash.replace('#', ''); // entire url, then get the hash if (id) { // prepare ui for changes renderLoader(elements.recipe); // create new recipe object state.recipe = new Recipe(id) searchView.highlightSelected(id) try { await state.recipe.getRecipe(); state.recipe.parseIngredients(); state.recipe.calcTime() state.recipe.calcServings(); clearLoader(); recipeView.clearResults(); recipeView.renderRecipe(state.recipe) // render recipe here } catch (err) { console.log(err) } } }; ['hashchange', 'load'].forEach(event => window.addEventListener(event, controlRecipe)) |
src/js/models/Recipes.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
import axios from 'axios' export default class Recipe { constructor(id) { this.id = id; } async getRecipe() { try { const res = await axios(`https://forkify-api.herokuapp.com/api/get?rId=${this.id}`); this.title = res.data.recipe.title; this.author = res.data.recipe.publisher; this.image = res.data.recipe.image_url; this.url = res.data.recipe.source_url; this.ingredients = res.data.recipe.ingredients; console.log(res) } catch (err) { console.log(error) } } calcTime() { // assuming that we need 15 min for each 3 ingredients const numIng = this.ingredients.length; const periods = Math.ceil(numIng/3); this.time = periods * 15; } calcServings() { this.servings = 4; } // change ingredient long names to short parseIngredients() { const unitsLong = ['tablespoons', 'tablespoon', 'ounce', 'ounces', 'teaspoon', 'teaspoons', 'cups', 'pounds']; const unitsShort = ['tbsp', 'tbsp', 'oz', 'oz', 'tsp', 'tsp', 'cup', 'pound']; const newIngredients = this.ingredients.map( el => { // 1) uniform units let ingredient = el.toLowerCase(); unitsLong.forEach((unit, i) => { ingredient = ingredient.replace(unit, unitsShort[i]); }) // 2) remove parenthesis ingredient = ingredient.replace(/ *\([^)]*\) */g, ' ') // 3) parse ingredients into count, unit, and ingredient const arrIng = ingredient.split(' '); const unitIndex = arrIng.findIndex(el2 => unitsShort.includes(el2)); let objIng; if (unitIndex > -1) { // there is a unit // Ex. 4 1/2 cups, arrCount is [4, 1/2] ---> "4+1/2" ---> 4.5 // Ex. 4 cups, arrCunt is [4] const arrCount = arrIng.slice(0, unitIndex); let count; if (arrCount.length === 1) { count = eval(arrIng[0].replace('-', '+')); } else { count = eval(arrIng.slice(0, unitIndex).join('+')); } objIng = { count, unit: arrIng[unitIndex], ingredient: arrIng.slice(unitIndex+1).join(' ') }; } else if (parseInt(arrIng[0], 10)) { // there is NO unit, but 1st element is number objIng = { count: parseInt(arrIng[0], 10), unit: '', ingredient: arrIng.slice(1).join(' ') } } else if (unitIndex === -1) { // there is no unit and no 1st position objIng = { count: 1, unit: '', ingredient }; } return objIng; }); this.ingredients = newIngredients } } |