Update function

- Implement error and success message.
- Implement search results.
This commit is contained in:
2023-08-14 16:38:37 +07:00
parent 6894474d12
commit 7d2f9b16c9
9 changed files with 204 additions and 71 deletions
-11
View File
@@ -86,8 +86,6 @@
<div class="search-results"> <div class="search-results">
<ul class="results"> <ul class="results">
<a href="#5ed6604591c37cdc054bcd09">Receipt 1</a>
<a href="#5ed6604591c37cdc054bca5d">Receipt 2</a>
<!-- <!--
<li class="preview"> <li class="preview">
<a class="preview__link preview__link--active" href="#23456"> <a class="preview__link preview__link--active" href="#23456">
@@ -151,15 +149,6 @@
</svg> </svg>
</div> --> </div> -->
<!-- <div class="error">
<div>
<svg>
<use href="src/img/icons.svg#icon-alert-triangle"></use>
</svg>
</div>
<p>No recipes found for your query. Please try again!</p>
</div> -->
<!-- <!--
<figure class="recipe__fig"> <figure class="recipe__fig">
<img src="src/img/test-1.jpg" alt="Tomato" class="recipe__img" /> <img src="src/img/test-1.jpg" alt="Tomato" class="recipe__img" />
+2
View File
@@ -0,0 +1,2 @@
export const API_URL = 'https://forkify-api.herokuapp.com/api/v2/recipes';
export const TIMEOUT_SEC = 10;
+30 -15
View File
@@ -1,20 +1,14 @@
import * as model from './model.js'; 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 resultsView from './views/resultsView.js';
import 'core-js/stable'; import 'core-js/stable';
import 'regenerator-runtime/runtime'; import 'regenerator-runtime/runtime';
const timeout = function (s) { if (module.hot) {
return new Promise(function (_, reject) { module.hot.accept();
setTimeout(function () { }
reject(new Error(`Request took too long! Timeout after ${s} second`));
}, s * 1000);
});
};
// https://forkify-api.herokuapp.com/v2
///////////////////////////////////////
const controlRecipes = async function () { const controlRecipes = async function () {
try { try {
@@ -29,10 +23,31 @@ const controlRecipes = async function () {
// 2) Rendering recipe // 2) Rendering recipe
recipeView.render(model.state.recipe); recipeView.render(model.state.recipe);
} catch (error) { } catch (error) {
alert(error); recipeView.renderError(error);
} }
}; };
['hashchange', 'load'].forEach(ev => const controlSearchResults = async () => {
window.addEventListener(ev, controlRecipes) try {
); resultsView.renderSpinner();
// 1) Get search query
const query = searchView.getQuery();
if (!query) return;
// 2) Load search results
await model.loadSearchResults(query);
// 3) Render results
resultsView.render(model.state.search.results);
} catch (error) {
console.error(error);
}
};
const init = () => {
recipeView.addHandleRender(controlRecipes);
searchView.addHandlerSearch(controlSearchResults);
};
init();
+25
View File
@@ -0,0 +1,25 @@
import { async } from 'regenerator-runtime';
import { TIMEOUT_SEC } from './config';
const timeout = function (s) {
return new Promise(function (_, reject) {
setTimeout(function () {
reject(new Error(`Request took too long! Timeout after ${s} second`));
}, s * 1000);
});
};
export const getJSON = async function (url) {
try {
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;
} catch (error) {
throw error;
}
};
+30 -7
View File
@@ -1,15 +1,18 @@
import { async } from 'regenerator-runtime';
import { API_URL } from './config';
import { getJSON } from './helpers';
export const state = { export const state = {
recipe: {}, recipe: {},
search: {
query: '',
results: [],
},
}; };
export const loadRecipe = async function (id) { export const loadRecipe = async function (id) {
try { try {
const res = await fetch( const data = await getJSON(`${API_URL}/${id}`);
`https://forkify-api.herokuapp.com/api/v2/recipes/${id}`
);
const data = await res.json();
if (!res.ok) throw new Error(`${data.message} (${res.status})`);
const { recipe } = data.data; const { recipe } = data.data;
@@ -26,6 +29,26 @@ export const loadRecipe = async function (id) {
console.log(state.recipe); console.log(state.recipe);
} catch (error) { } catch (error) {
alert(error); throw error;
}
};
export const loadSearchResults = async function (query) {
try {
state.search.query = query;
const data = await getJSON(`${API_URL}?search=${query}`);
state.search.results = data.data.recipes.map(rec => {
return {
id: rec.id,
title: rec.title,
publisher: rec.publisher,
image: rec.image_url,
};
});
} catch (error) {
console.error(`${error} 💥💥💥`);
throw error;
} }
}; };
+47
View File
@@ -0,0 +1,47 @@
import icons from '../../img/icons.svg';
export default class View {
_data;
render(data) {
if (!data || (Array.isArray(data) && data.length === 0))
return this.renderError();
this._data = data;
const markup = this._generateMarkup();
this._clear();
this._parentElement.insertAdjacentHTML('afterbegin', markup);
}
_clear() {
this._parentElement.innerHTML = '';
}
renderSpinner = function () {
const markup = `
<div class="spinner">
<svg>
<use href="${icons}#icon-loader"></use>
</svg>
</div>
`;
this._clear();
this._parentElement.insertAdjacentHTML('afterbegin', markup);
};
renderError(message = this._errorMessage) {
const markup = `
<div class="message">
<div>
<svg>
<use href="${icons}#icon-alert-triangle"></use>
</svg>
</div>
<p>${message}</p>
</div>
`;
this._clear();
this._parentElement.insertAdjacentHTML('afterbegin', markup);
}
}
+18 -38
View File
@@ -1,41 +1,24 @@
import View from './View';
import icons from '../../img/icons.svg'; import icons from '../../img/icons.svg';
import fractional from 'fractional'; import fractional from 'fractional';
class RecipeView { class RecipeView extends View {
#parentElement = document.querySelector('.recipe'); _parentElement = document.querySelector('.recipe');
#data; _errorMessage = 'We could not find that recipe. Please try another one!';
_message = '';
render(data) { addHandleRender(handler) {
this.#data = data; ['hashchange', 'load'].forEach(ev => window.addEventListener(ev, handler));
const markup = this.#generateMarkup();
this.#clear();
this.#parentElement.insertAdjacentHTML('afterbegin', markup);
} }
#clear() { _generateMarkup() {
this.#parentElement.innerHTML = '';
}
renderSpinner = function () {
const markup = `
<div class="spinner">
<svg>
<use href="${icons}#icon-loader"></use>
</svg>
</div>
`;
this.#parentElement.innerHTML = '';
this.#parentElement.insertAdjacentHTML('afterbegin', markup);
};
#generateMarkup() {
return ` return `
<figure class="recipe__fig"> <figure class="recipe__fig">
<img src="${this.#data.image}" alt="${ <img src="${this._data.image}" alt="${
this.#data.title this._data.title
}" class="recipe__img" /> }" class="recipe__img" />
<h1 class="recipe__title"> <h1 class="recipe__title">
<span>${this.#data.title}</span> <span>${this._data.title}</span>
</h1> </h1>
</figure> </figure>
@@ -45,7 +28,7 @@ class RecipeView {
<use href="${icons}#icon-clock"></use> <use href="${icons}#icon-clock"></use>
</svg> </svg>
<span class="recipe__info-data recipe__info-data--minutes">${ <span class="recipe__info-data recipe__info-data--minutes">${
this.#data.cookingTime this._data.cookingTime
}</span> }</span>
<span class="recipe__info-text">minutes</span> <span class="recipe__info-text">minutes</span>
</div> </div>
@@ -54,7 +37,7 @@ class RecipeView {
<use href="${icons}#icon-users"></use> <use href="${icons}#icon-users"></use>
</svg> </svg>
<span class="recipe__info-data recipe__info-data--people">${ <span class="recipe__info-data recipe__info-data--people">${
this.#data.servings this._data.servings
}</span> }</span>
<span class="recipe__info-text">servings</span> <span class="recipe__info-text">servings</span>
@@ -73,9 +56,6 @@ class RecipeView {
</div> </div>
<div class="recipe__user-generated"> <div class="recipe__user-generated">
<svg>
<use href="${icons}#icon-user"></use>
</svg>
</div> </div>
<button class="btn--round"> <button class="btn--round">
<svg class=""> <svg class="">
@@ -87,7 +67,7 @@ class RecipeView {
<div class="recipe__ingredients"> <div class="recipe__ingredients">
<h2 class="heading--2">Recipe ingredients</h2> <h2 class="heading--2">Recipe ingredients</h2>
<ul class="recipe__ingredient-list"> <ul class="recipe__ingredient-list">
${this.#data.ingredients.map(this.#generateMarkupIngredient).join('')} ${this._data.ingredients.map(this._generateMarkupIngredient).join('')}
</ul> </ul>
</div> </div>
@@ -96,25 +76,25 @@ class RecipeView {
<p class="recipe__directions-text"> <p class="recipe__directions-text">
This recipe was carefully designed and tested by This recipe was carefully designed and tested by
<span class="recipe__publisher">${ <span class="recipe__publisher">${
this.#data.publisher this._data.publisher
}</span>. Please check out }</span>. Please check out
directions at their website. directions at their website.
</p> </p>
<a <a
class="btn--small recipe__btn" class="btn--small recipe__btn"
href="h${this.#data.sourceUrl}" href="h${this._data.sourceUrl}"
target="_blank" target="_blank"
> >
<span>Directions</span> <span>Directions</span>
<svg class="search__icon"> <svg class="search__icon">
<use href="src/img/icons.svg#icon-arrow-right"></use> <use href="${icons}#icon-arrow-right"></use>
</svg> </svg>
</a> </a>
</div> </div>
`; `;
} }
#generateMarkupIngredient(ing) { _generateMarkupIngredient(ing) {
return ` return `
<li class="recipe__ingredient"> <li class="recipe__ingredient">
<svg class="recipe__icon"> <svg class="recipe__icon">
+30
View File
@@ -0,0 +1,30 @@
import View from './View.js';
import icons from '../../img/icons.svg';
class ResultsView extends View {
_parentElement = document.querySelector('.results');
_errorMessage = 'No recipes found for your query! Please try again!';
_message = '';
_generateMarkup() {
return this._data.map(this._generateMarkupPreview).join('');
}
_generateMarkupPreview(data) {
return `
<li class="preview">
<a class="preview__link preview__link" href="#${data.id}">
<figure class="preview__fig">
<img src="${data.image}" alt="${data.title}" />
</figure>
<div class="preview__data">
<h4 class="preview__title">${data.title}</h4>
<p class="preview__publisher">${data.publisher}</p>
</div>
</a>
</li>
`;
}
}
export default new ResultsView();
+22
View File
@@ -0,0 +1,22 @@
class SearchView {
_parentEl = document.querySelector('.search');
getQuery() {
const query = this._parentEl.querySelector('.search__field').value;
this._clearInput();
return query;
}
_clearInput() {
this._parentEl.querySelector('.search__field').value = '';
}
addHandlerSearch(handler) {
this._parentEl.addEventListener('submit', function (e) {
e.preventDefault();
handler();
});
}
}
export default new SearchView();