Estoy creando una aplicación de recetas de muestra llamada: Forkify en la que estoy usando javascript, npm, babel, webpack y estoy usando una API personalizada para recuperar los datos.

URL de API

https://forkify-api.herokuapp.com/

Ejemplo de búsqueda

https://forkify-api.herokuapp.com/api/search?q=pizza

Obtenga un ejemplo

https://forkify-api.herokuapp.com/api/get?rId=47746

Lo que muestra los elementos de la receta en la pantalla con los ingredientes requeridos para esa receta en particular, también hay dos botones + y - que se utilizan para agregar porciones y en base de eso cambia el tamaño de la porción y los ingredientes requeridos.

A continuación se muestran las capturas de pantalla y los archivos de código para una mejor comprensión:

index.js

/*
Global state of the app
- search object
- current recipe object
- shopping list object
- liked recipe
*/

import Search from "./models/Search";
import Recipe from "./models/Recipe";
import * as searchView from "./views/searchView";
import * as recipeView from "./views/recipeView";
import { elements, renderLoader, clearLoader } from "./views/base";
const state = {};

/* SEARCH CONTROLLER */

const controlSearch = async () => {
    // 1. Get query from the view.
    const query = searchView.getInput(); //TODO

    if (query) {
        // 2. New search object and add it to state.
        state.search = new Search(query);

        // 3. Prepare UI for results.
        searchView.clearInput();
        searchView.clearResults();
        renderLoader(elements.searchRes);

        try {
            // 4. Search for recipes.
            await state.search.getResults();

            // 5. Render results on UI.
            clearLoader();
            searchView.renderResults(state.search.result);

        } catch (error) {
            alert("Something wrong with the search...");
            clearLoader();
        }
    }
}

elements.searchForm.addEventListener("submit", e => {
    e.preventDefault();
    controlSearch();
});

elements.searchResPages.addEventListener("click", e => {
    const btn = e.target.closest(".btn-inline");
    if (btn) {
        const goToPage = parseInt(btn.dataset.goto, 10);
        searchView.clearResults();
        searchView.renderResults(state.search.result, goToPage);
    }
});

/*
RECIPE CONTROLLER
*/
const controlRecipe = async () => {
    // Get ID from URL
    const id = window.location.hash.replace("#", "");
    console.log(id);
    if (id) {
        // Prepare UI for changes
        recipeView.clearRecipe();
        renderLoader(elements.recipe);      // passing parent

        // highlight selected search item
        if (state.search) searchView.highlightSelected(id);

        // Create new recipe object
        state.recipe = new Recipe(id);

        try {
            // Get recipe data and parse ingredients
            await state.recipe.getRecipe();
            state.recipe.parseIngredients();

            // Calculate servings and time
            state.recipe.calcTime();
            console.log(state.recipe.ingredients);
            state.recipe.calcServings();

            // Render recipe
            clearLoader();
            recipeView.renderRecipe(state.recipe);      // to put recipe
        } catch (error) {
            // console.log(error);
            alert("Error processing recipe !");
        }
    }
};
["hashchange", "load"].forEach(event => window.addEventListener(event, controlRecipe));

// CODE FOR "+" "-" BUTTON IN RECIPE
// handling recipe button clicks
elements.recipe.addEventListener("click", e => {

    // btn-decrease * means button decrease "-" with any child
    // it will be true if there is button decrease or button decrease with any child
    if (e.target.matches(".btn-decrease, .btn-decrease *")) {

        // decrease button "-" is clicked
        if(state.recipe.servings>1) {
            state.recipe.updateServings("dec");
            recipeView.updateServingsIngredients(state.recipe);
        }
    }
    else if (e.target.matches(".btn-increase, .btn-increase *")) {

        // increase button is clicked
        state.recipe.updateServings("inc");
        recipeView.updateServingsIngredients(state.recipe);
    }
    console.log(state.recipe);
});

RecetaView.js

import { elements } from "./base";
import { Fraction } from "fractional";

export const clearRecipe = () => {
    elements.recipe.innerHTML = "";
};

// To format the decimal number.
const formatCount = count => {
    if (count) {
        // count = 2.5 --> 5/2 or 2 1/2
        // count = 0.5 --> 1/2
        const [int, dec] = count.toString().split(".").map(el => parseInt(el, 10));
        if (!dec) return count;
        if (int === 0) {
            const fr = new Fraction(count);
            return `${fr.numerator}/${fr.denominator}`;
        }
        else {
            // to show fraction of integer part and decimal part separately ex : 2.5 = 2 1/2
            const fr = new Fraction(count - int);
            return `${int} ${fr.numerator}/${fr.denominator}`;
        }
    }
    return "?";
};

const createIngredient = ingredient => `
        <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>
            ${ingredient.ingredient}
        </div>
        </li>
`;

export const renderRecipe = recipe => {
    const markup = `
        <figure class="recipe__fig">
        <img src="${recipe.img}" 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 btn-decrease">
                    <svg>
                        <use href="img/icons.svg#icon-circle-with-minus"></use>
                    </svg>
                </button>
                <button class="btn-tiny btn-increase">
                    <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">
            <span>Directions</span>
            <svg class="search__icon">
                <use href="img/icons.svg#icon-triangle-right"></use>
            </svg>

        </a>
    </div>
    `;
    elements.recipe.insertAdjacentHTML("afterbegin", markup);
}

export const updateServingsIngredients = recipe => {

    // update counts
    document.querySelector(".recipe__info-data--people").textContent = recipe.servings;


    // update ingredients
    const countElements = Array.from(document.querySelectorAll(".recipe__count"));
    countElements.forEach((el, i) => {
        el.textContent = formatCount(recipe.ingredients[i].count);
    });
};

Recipe.js

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/search?q=${this.query}`);
            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.img = res.data.recipe.image_url;
            this.url = res.data.recipe.source_url;
            this.ingredients = res.data.recipe.ingredients;
        } catch (error) {
            console.log(error);
            alert("Something went wrong :(");
        }
    }
    calcTime() {
        // Assuming that we need 15 minutes for each 3 ingredients
        const numIng = this.ingredients.length;
        const periods = Math.ceil(numIng / 3);
        this.time = periods * 15;
    }

    calcServings() {
        this.servings = 4;
    }

    parseIngredients() {
        const unitsLong = ["tablespoons", "tablespoon", "ounces", "ounce", "teaspoons", "teaspoon", "cups", "pounds"];
        const unitsShort = ["tbsp", "tbsp", "oz", "oz", "tsp", "tsp", "cup", "pound"];
        const units = [...unitsShort, "kg", "g"];
        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 ingredients
            const arrIng = ingredient.split(" ");
            const unitIndex = arrIng.findIndex(el2 => units.includes(el2));

            let objIng;

            if (unitIndex > -1) {
                // there is a unit
                // Example 4 1/2 cups, arrCount is [4 , 1/2] --> eval("4+1/2") = 4.5
                // Example 4 cups \, arrCount 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 numberin 1st position
                objIng = {
                    count: 1,
                    unit: "",
                    ingredient
                }
            }
            // return ingredient;
            return objIng;
        });
        this.ingredients = newIngredients;
    }
    updateServings(type) {

        // servings
        const newServings = type === "dec" ? this.servings - 1 : this.servings + 1;

        // ingredients
        this.ingredients.forEach(ing => {
            ing.count *= (newServings.count / this.servings);
        });
        this.servings = newServings;
    }
};

searchView.js

/*
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
export const ID = 23;
*/



import { elements } from "./base";

export const getInput = () => elements.searchInput.value;

export const clearInput = () => {
    elements.searchInput.value = "";
};

export const clearResults = () => {
    elements.searchResList.innerHTML = "";
    elements.searchResPages.innerHTML = "";
};

export const highlightSelected = id => {
    const resultsArr = Array.from(document.querySelectorAll(".results__link"));
    resultsArr.forEach(el => {
        el.classList.remove("results__link--active");
    });
    document.querySelector(`a[href="#${id}"]`).classList.add("results__link--active");
}

/*  EXAMPLE CODE
"pasta with tomato and spinach"
acc:0/acc+curr.length=5 /newTitle =['pasta']
acc:5/acc+curr.length=9 /newTitle =['pasta','with']
acc:9/acc+curr.length=15 /newTitle =['pasta','with','tomato']
acc:15/acc+curr.length=18 /newTitle =['pasta','with','tomato']
acc:18/acc+curr.length=25 /newTitle =['pasta','with','tomato']
*/

const limitRecipeTitle = (title, limit = 17) => {
    const newTitle = [];
    if (title.length > limit) {
        title.split(" ").reduce((acc, curr) => {
            if (acc + curr.length <= limit) {
                newTitle.push(curr);
            }
            return acc + curr.length;
        }, 0);
        // return the results
        return `${newTitle.join(' ')}...`;
    }
    return title;
};

const renderRecipe = recipe => {
    const markup = `
    <li>
        <a class="results__link" href="#${recipe.recipe_id}">
            <figure class="results__fig">
                <img src="${recipe.image_url}" alt="${recipe.title}">
            </figure>
            <div class="results__data">
                <h4 class="results__name">${limitRecipeTitle(recipe.title)}</h4>
                 <p class="results__author">${recipe.publisher}</p>
            </div>
        </a>
    </li>
    `;
    elements.searchResList.insertAdjacentHTML("beforeend", markup);
};

// type: "prev" or "next"
const createButton = (page, type) => `
<button class="btn-inline results__btn--${type}" data-goto=${type === "prev" ? page - 1 : page + 1}>
<span>Page ${ type === "prev" ? page - 1 : page + 1}</span>
<svg class="search__icon">
    <use href="img/icons.svg#icon-triangle-${ type === "prev" ? "left" : "right"}"></use>
</svg>
</button>
`


const renderButtons = (page, numResults, resPerPage) => {
    const pages = Math.ceil(numResults / resPerPage);
    let button;
    if (page === 1 && pages > 1) {
        // Only button to go to next page.
        button = createButton(page, "next");
    }
    else if (page < pages) {
        // Both buttons
        button = `
        ${createButton(page, "prev")}
        ${createButton(page, "next")}
        `;
    }
    else if (page === pages && pages > 1) {
        // Only button to go to previous page.
        button = createButton(page, "prev");
    }
    elements.searchResPages.insertAdjacentHTML("afterbegin", button);
}

export const renderResults = (recipes = [], page = 1, resPerPage = 10) => {
    // render results of current page
    const start = (page - 1) * resPerPage;
    const end = page * resPerPage;
    // recipes.slice(start,end).forEach(renderRecipe);
    recipes.slice(start, end).forEach(renderRecipe);


    // render pagination buttons
    renderButtons(page, recipes.length, resPerPage);
};

Search.js

import axios from "axios";
// import {proxy} from "../config";
export default class Search{
    constructor(query){
        this.query=query;
    }

    async getResults() {
        try{
        const res = await axios(`https://forkify-api.herokuapp.com/api/search?q=${this.query}`);
        this.result = res.data.recipes;
        }
        catch(error){
            alert(error);
        }
    };

}

base.js

export const elements = {
    searchForm: document.querySelector(".search"),
    searchInput: document.querySelector(".search__field"),
    searchRes: document.querySelector(".results"),
    searchResList: document.querySelector(".results__list"),
    searchResPages: document.querySelector(".results__pages"),
    recipe:document.querySelector(".recipe")
};

export const elementStrings = {
    loader: "loader"
};

export const renderLoader = parent => {
    const loader = `
    <div class="${elementStrings.loader}">
        <svg>
            <use href="img/icons.svg#icon-cw">
            </use>
        </svg>
    </div>
    `;
    parent.insertAdjacentHTML("afterbegin", loader);
};

export const clearLoader = () => {
    const loader = document.querySelector(`.${elementStrings.loader}`);
    if (loader) loader.parentElement.removeChild(loader);
};

Capturas de pantalla

Página de destino

Landing Page

Ingresando la consulta de búsqueda como pizza

Entering search query as pizza

Resultados de la consulta de búsqueda

Results of the search query

Seleccionar Receta de la lista y mostrar valores predeterminados de ingredientes y porciones

Selecting Recipe from the list and showing default values of ingredients and servings

Muestra valores predeterminados para porciones e ingredientes pero el botón + y - debería aumentar o disminuir los ingredientes y porciones, aunque aumenta y disminuye el número de porciones según la selección pero en ingredientes muestra '?'

increasing and decreasing servings

¿Alguna solución, por favor?

-1
Mohit Kumar Sharma 22 jul. 2020 a las 14:36

1 respuesta

La mejor respuesta

Encontré la solución, el problema estaba en el archivo Recipe.js : última parte donde se declaró el método updateServings (type) . Anteriormente en ese método escribí

ing.count *= (newServings.count / this.servings);

Lo cambié a:

ing.count *= (newServings / this.servings);

Y funcionó.

0
Mohit Kumar Sharma 22 jul. 2020 a las 12:36