Update code

- Implement pagination
- Updating recipe servings
- Upating Algorithm
This commit is contained in:
2023-08-15 15:49:18 +07:00
parent 7d2f9b16c9
commit 57f41be9fd
8 changed files with 180 additions and 14 deletions
+1
View File
@@ -1,2 +1,3 @@
export const API_URL = 'https://forkify-api.herokuapp.com/api/v2/recipes'; export const API_URL = 'https://forkify-api.herokuapp.com/api/v2/recipes';
export const TIMEOUT_SEC = 10; export const TIMEOUT_SEC = 10;
export const RES_PER_PAGE = 10;
+32 -6
View File
@@ -2,13 +2,14 @@ import * as model from './model.js';
import recipeView from './views/recipeView.js'; import recipeView from './views/recipeView.js';
import searchView from './views/searchView.js'; import searchView from './views/searchView.js';
import resultsView from './views/resultsView.js'; import resultsView from './views/resultsView.js';
import paginationView from './views/paginationView.js';
import 'core-js/stable'; import 'core-js/stable';
import 'regenerator-runtime/runtime'; import 'regenerator-runtime/runtime';
if (module.hot) { // if (module.hot) {
module.hot.accept(); // module.hot.accept();
} // }
const controlRecipes = async function () { const controlRecipes = async function () {
try { try {
@@ -17,6 +18,9 @@ const controlRecipes = async function () {
if (!id) return; if (!id) return;
recipeView.renderSpinner(); recipeView.renderSpinner();
// 0) Update results view to mark selected search results
resultsView.update(model.getSearchResultsPage());
// 1)Loading recipe // 1)Loading recipe
await model.loadRecipe(id); await model.loadRecipe(id);
@@ -27,7 +31,7 @@ const controlRecipes = async function () {
} }
}; };
const controlSearchResults = async () => { const controlSearchResults = async function () {
try { try {
resultsView.renderSpinner(); resultsView.renderSpinner();
@@ -39,15 +43,37 @@ const controlSearchResults = async () => {
await model.loadSearchResults(query); await model.loadSearchResults(query);
// 3) Render results // 3) Render results
resultsView.render(model.state.search.results); resultsView.render(model.getSearchResultsPage());
// 4) Render initial pagination buttons
paginationView.render(model.state.search);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
}; };
const init = () => { const controlPagination = goToPage => {
// 1) Render new results
resultsView.render(model.getSearchResultsPage(goToPage));
// 2) Render new initial pagination buttons
paginationView.render(model.state.search);
};
const controlServings = function (newServings) {
// Update the recipe servings (in state)
model.updateServings(newServings);
// Update the recipe view
// recipeView.render(model.state.recipe);
recipeView.update(model.state.recipe);
};
const init = function () {
recipeView.addHandleRender(controlRecipes); recipeView.addHandleRender(controlRecipes);
recipeView.addHandlerUpdateServings(controlServings);
searchView.addHandlerSearch(controlSearchResults); searchView.addHandlerSearch(controlSearchResults);
paginationView.addHandlerClick(controlPagination);
}; };
init(); init();
-2
View File
@@ -14,8 +14,6 @@ export const getJSON = async function (url) {
const res = await Promise.race([fetch(url), timeout(TIMEOUT_SEC)]); const res = await Promise.race([fetch(url), timeout(TIMEOUT_SEC)]);
const data = await res.json(); const data = await res.json();
console.log(data);
if (!res.ok) throw new Error(`${data.message} (${res.status})`); if (!res.ok) throw new Error(`${data.message} (${res.status})`);
return data; return data;
+20 -3
View File
@@ -1,5 +1,5 @@
import { async } from 'regenerator-runtime'; import { async } from 'regenerator-runtime';
import { API_URL } from './config'; import { API_URL, RES_PER_PAGE } from './config';
import { getJSON } from './helpers'; import { getJSON } from './helpers';
export const state = { export const state = {
@@ -7,6 +7,8 @@ export const state = {
search: { search: {
query: '', query: '',
results: [], results: [],
page: 1,
resultsPerPage: RES_PER_PAGE,
}, },
}; };
@@ -26,8 +28,6 @@ export const loadRecipe = async function (id) {
cookingTime: recipe.cooking_time, cookingTime: recipe.cooking_time,
ingredients: recipe.ingredients, ingredients: recipe.ingredients,
}; };
console.log(state.recipe);
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -52,3 +52,20 @@ export const loadSearchResults = async function (query) {
throw error; throw error;
} }
}; };
export const getSearchResultsPage = function (page = state.search.page) {
if (page) state.search.page = page;
const start = (page - 1) * state.search.resultsPerPage;
const end = page * state.search.resultsPerPage;
return state.search.results.slice(start, end);
};
export const updateServings = function (newServings) {
state.recipe.ingredients.forEach(ing => {
ing.quantity = (ing.quantity * newServings) / state.recipe.servings;
});
state.recipe.servings = newServings;
};
+28
View File
@@ -13,6 +13,34 @@ export default class View {
this._parentElement.insertAdjacentHTML('afterbegin', markup); this._parentElement.insertAdjacentHTML('afterbegin', markup);
} }
update(data) {
this._data = data;
const newMarkup = this._generateMarkup();
const newDom = document.createRange().createContextualFragment(newMarkup);
const newElements = Array.from(newDom.querySelectorAll('*'));
const curElements = Array.from(this._parentElement.querySelectorAll('*'));
newElements.forEach((newEl, i) => {
const curEl = curElements[i];
console.log(curEl, newEl.isEqualNode(curEl));
// Update change TEXT
if (
!newEl.isEqualNode(curEl) &&
newEl.firstChild.nodeValue.trim() !== ''
) {
curEl.textContent = newEl.textContent;
}
// Updated changes ATTRIBUTES
if (!newEl.isEqualNode(curEl))
Array.from(newEl.attributes).forEach(attr =>
curEl.setAttribute(attr.name, attr.value)
);
});
}
_clear() { _clear() {
this._parentElement.innerHTML = ''; this._parentElement.innerHTML = '';
} }
+79
View File
@@ -0,0 +1,79 @@
import View from './View.js';
import icons from '../../img/icons.svg';
class PaginationView extends View {
_parentElement = document.querySelector('.pagination');
addHandlerClick(handler) {
this._parentElement.addEventListener('click', function (e) {
const btn = e.target.closest('.btn--inline');
if (!btn) return;
const goToPage = +btn.dataset.goto;
handler(goToPage);
});
}
_generateMarkup() {
const curPage = this._data.page;
const numPages = Math.ceil(
this._data.results.length / this._data.resultsPerPage
);
// Page 1 and there are other pages
if (curPage === 1 && numPages > 1) {
return `
<button data-goto="${
curPage + 1
}" class="btn--inline pagination__btn--next">
<span>Page ${curPage + 1}</span>
<svg class="search__icon">
<use href="${icons}#icon-arrow-right"></use>
</svg>
</button>
`;
}
// Last page
if (curPage === numPages && numPages > 1) {
return `
<button data-goto="${
curPage - 1
}" class="btn--inline pagination__btn--prev">
<svg class="search__icon">
<use href="${icons}#icon-arrow-left"></use>
</svg>
<span>Page ${curPage - 1}</span>
</button>
`;
}
// Other pages
if (this._data.page < numPages) {
return `
<button data-goto="${
curPage - 1
}" class="btn--inline pagination__btn--prev">
<svg class="search__icon">
<use href="${icons}#icon-arrow-left"></use>
</svg>
<span>Page ${curPage - 1}</span>
</button>
<button data-goto="${
curPage + 1
}" class="btn--inline pagination__btn--next">
<span>Page ${curPage + 1}</span>
<svg class="search__icon">
<use href="${icons}#icon-arrow-right"></use>
</svg>
</button>
`;
}
// Page 1 and there are No other pages
return '';
}
}
export default new PaginationView();
+15 -2
View File
@@ -11,6 +11,15 @@ class RecipeView extends View {
['hashchange', 'load'].forEach(ev => window.addEventListener(ev, handler)); ['hashchange', 'load'].forEach(ev => window.addEventListener(ev, handler));
} }
addHandlerUpdateServings(handler) {
this._parentElement.addEventListener('click', function (e) {
const btn = e.target.closest('.btn--update-servings');
if (!btn) return;
const { updateTo } = btn.dataset;
if (+updateTo > 0) handler(+updateTo);
});
}
_generateMarkup() { _generateMarkup() {
return ` return `
<figure class="recipe__fig"> <figure class="recipe__fig">
@@ -42,12 +51,16 @@ class RecipeView extends View {
<span class="recipe__info-text">servings</span> <span class="recipe__info-text">servings</span>
<div class="recipe__info-buttons"> <div class="recipe__info-buttons">
<button class="btn--tiny btn--increase-servings"> <button class="btn--tiny btn--update-servings" data-update-to="${
this._data.servings - 1
}">
<svg> <svg>
<use href="${icons}#icon-minus-circle"></use> <use href="${icons}#icon-minus-circle"></use>
</svg> </svg>
</button> </button>
<button class="btn--tiny btn--increase-servings"> <button class="btn--tiny btn--update-servings" data-update-to="${
this._data.servings + 1
}">
<svg> <svg>
<use href="${icons}#icon-plus-circle"></use> <use href="${icons}#icon-plus-circle"></use>
</svg> </svg>
+5 -1
View File
@@ -11,9 +11,13 @@ class ResultsView extends View {
} }
_generateMarkupPreview(data) { _generateMarkupPreview(data) {
const id = window.location.hash.slice(1);
return ` return `
<li class="preview"> <li class="preview">
<a class="preview__link preview__link" href="#${data.id}"> <a class="preview__link ${
data.id === id ? 'preview__link--active' : ''
}" href="#${data.id}">
<figure class="preview__fig"> <figure class="preview__fig">
<img src="${data.image}" alt="${data.title}" /> <img src="${data.image}" alt="${data.title}" />
</figure> </figure>