From 57f41be9fd318290165821465b490a450017852d Mon Sep 17 00:00:00 2001 From: Loi Phan Date: Tue, 15 Aug 2023 15:49:18 +0700 Subject: [PATCH] Update code - Implement pagination - Updating recipe servings - Upating Algorithm --- src/js/config.js | 1 + src/js/controller.js | 38 +++++++++++++--- src/js/helpers.js | 2 - src/js/model.js | 23 ++++++++-- src/js/views/View.js | 28 ++++++++++++ src/js/views/paginationView.js | 79 ++++++++++++++++++++++++++++++++++ src/js/views/recipeView.js | 17 +++++++- src/js/views/resultsView.js | 6 ++- 8 files changed, 180 insertions(+), 14 deletions(-) create mode 100644 src/js/views/paginationView.js diff --git a/src/js/config.js b/src/js/config.js index 540732e..2f8e4c6 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -1,2 +1,3 @@ export const API_URL = 'https://forkify-api.herokuapp.com/api/v2/recipes'; export const TIMEOUT_SEC = 10; +export const RES_PER_PAGE = 10; diff --git a/src/js/controller.js b/src/js/controller.js index b22ff2d..edb7699 100644 --- a/src/js/controller.js +++ b/src/js/controller.js @@ -2,13 +2,14 @@ import * as model from './model.js'; import recipeView from './views/recipeView.js'; import searchView from './views/searchView.js'; import resultsView from './views/resultsView.js'; +import paginationView from './views/paginationView.js'; import 'core-js/stable'; import 'regenerator-runtime/runtime'; -if (module.hot) { - module.hot.accept(); -} +// if (module.hot) { +// module.hot.accept(); +// } const controlRecipes = async function () { try { @@ -17,6 +18,9 @@ const controlRecipes = async function () { if (!id) return; recipeView.renderSpinner(); + // 0) Update results view to mark selected search results + resultsView.update(model.getSearchResultsPage()); + // 1)Loading recipe await model.loadRecipe(id); @@ -27,7 +31,7 @@ const controlRecipes = async function () { } }; -const controlSearchResults = async () => { +const controlSearchResults = async function () { try { resultsView.renderSpinner(); @@ -39,15 +43,37 @@ const controlSearchResults = async () => { await model.loadSearchResults(query); // 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) { 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.addHandlerUpdateServings(controlServings); searchView.addHandlerSearch(controlSearchResults); + paginationView.addHandlerClick(controlPagination); }; init(); diff --git a/src/js/helpers.js b/src/js/helpers.js index 0b3a8d3..15e7232 100644 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -14,8 +14,6 @@ export const getJSON = async function (url) { const res = await Promise.race([fetch(url), timeout(TIMEOUT_SEC)]); const data = await res.json(); - console.log(data); - if (!res.ok) throw new Error(`${data.message} (${res.status})`); return data; diff --git a/src/js/model.js b/src/js/model.js index 005ea20..8b25ead 100644 --- a/src/js/model.js +++ b/src/js/model.js @@ -1,5 +1,5 @@ import { async } from 'regenerator-runtime'; -import { API_URL } from './config'; +import { API_URL, RES_PER_PAGE } from './config'; import { getJSON } from './helpers'; export const state = { @@ -7,6 +7,8 @@ export const state = { search: { query: '', results: [], + page: 1, + resultsPerPage: RES_PER_PAGE, }, }; @@ -26,8 +28,6 @@ export const loadRecipe = async function (id) { cookingTime: recipe.cooking_time, ingredients: recipe.ingredients, }; - - console.log(state.recipe); } catch (error) { throw error; } @@ -52,3 +52,20 @@ export const loadSearchResults = async function (query) { 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; +}; diff --git a/src/js/views/View.js b/src/js/views/View.js index e5fa4da..3b50d4d 100644 --- a/src/js/views/View.js +++ b/src/js/views/View.js @@ -13,6 +13,34 @@ export default class View { 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() { this._parentElement.innerHTML = ''; } diff --git a/src/js/views/paginationView.js b/src/js/views/paginationView.js new file mode 100644 index 0000000..5cd5a6a --- /dev/null +++ b/src/js/views/paginationView.js @@ -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 ` + + `; + } + + // Last page + if (curPage === numPages && numPages > 1) { + return ` + + `; + } + + // Other pages + if (this._data.page < numPages) { + return ` + + + `; + } + + // Page 1 and there are No other pages + return ''; + } +} + +export default new PaginationView(); diff --git a/src/js/views/recipeView.js b/src/js/views/recipeView.js index 14cf122..dff3827 100644 --- a/src/js/views/recipeView.js +++ b/src/js/views/recipeView.js @@ -11,6 +11,15 @@ class RecipeView extends View { ['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() { return `
@@ -42,12 +51,16 @@ class RecipeView extends View { servings
- -