mirror of
https://github.com/Nezumi-2711/practice-js.git
synced 2026-09-22 05:32:05 +00:00
Update function
- Implement error and success message. - Implement search results.
This commit is contained in:
-11
@@ -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" />
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const API_URL = 'https://forkify-api.herokuapp.com/api/v2/recipes';
|
||||||
|
export const TIMEOUT_SEC = 10;
|
||||||
+30
-15
@@ -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();
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user