Buduję przykładową aplikację Receptury: Forkidify , w którym używam JavaScript, NPM, Babel, WebPack i używam niestandardowego API, aby pobrać dane.

adres URL API

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

Przykład wyszukiwania

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

uzyskać przykład

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

Rzecz, którą wyświetla elementy receptur na ekranie z wymaganymi składnikami dla tego konkretnego przepisu, również znajdują się dwa przyciski + i - , które służą do dodawania porcji i na podstawie tego rozmiaru obsługi i wymaganych składników zmian.

Poniżej znajdują się zrzuty ekranu i pliki kodu do lepszego zrozumienia:

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);
});

reveview.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);
    });
};

receptura.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);
};

Zrzuty ekranu

Strona lądowania

Landing Page

Wprowadzanie zapytania wyszukiwania jako Pizza

Entering search query as pizza

Wyniki zapytania wyszukiwania

Results of the search query

Wybieranie receptury z listy i pokazuje domyślne wartości składników i porcji

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

Wyświetlanie wartości domyślnych dla porcji i składników, ale + i - przycisk powinien zwiększyć lub zmniejszyć składniki i porcje, chociaż zwiększa i zmniejsza liczbę porcji zgodnie z wyborem, ale w składnikach pokazuje "?" .

increasing and decreasing servings

jakiekolwiek rozwiązanie proszę?

-1
Mohit Kumar Sharma 22 lipiec 2020, 14:36

1 odpowiedź

Najlepsza odpowiedź

Znalazłem rozwiązanie, problem był w receptura Wcześniej w tej metodzie napisałem

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

Zmieniłem to do:

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

I zadziałało.

0
Mohit Kumar Sharma 22 lipiec 2020, 12:36