Skip to content

Commit 4daa22e

Browse files
authored
Add files via upload
1 parent 47c6024 commit 4daa22e

11 files changed

+910
-0
lines changed

View.js

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import icons from 'url:../../img/icons.svg';
2+
3+
export default class View {
4+
// we won't create any instance of this view
5+
// we'll only use it as a parent class of the other child views
6+
_data;
7+
8+
render(data) {
9+
if (!data || (Array.isArray(data) && data.length === 0))
10+
return this.renderError();
11+
12+
//this is part of the public API
13+
this._data = data;
14+
const markup = this._generateMarkup();// every view that renders something to the UI needs this method
15+
this._clear();
16+
this._parentElement.insertAdjacentHTML('afterbegin', markup);
17+
}
18+
// the two properties and the render method are something that all the views will have in common
19+
_clear() {
20+
// this method will be available for all views with #parentElement
21+
this._parentElement.innerHTML = ''; //get rid of existing content
22+
}
23+
24+
renderSpinner() {
25+
// this will be a public method that the controller can call as it starts fetching data
26+
const markup = `
27+
<div class="spinner">
28+
<svg>
29+
<use href="${icons}#icon-loader"></use>
30+
</svg>
31+
</div>
32+
`;
33+
this._clear();
34+
this._parentElement.insertAdjacentHTML('afterbegin', markup);
35+
}
36+
renderError(message = this._errorMessage) {
37+
const markup = `
38+
<div class="error">
39+
<div>
40+
<svg>
41+
<use href="${icons}#icon-alert-triangle"></use>
42+
</svg>
43+
</div>
44+
<p>${message}</p>
45+
</div>`;
46+
47+
this._clear();
48+
this._parentElement.insertAdjacentHTML('afterbegin', markup);
49+
}
50+
// implement a method for success messages; we don't need it yet
51+
renderMessage(message = this._message) {
52+
const markup = `
53+
<div class="message">
54+
<div>
55+
<svg>
56+
<use href="${icons}#icon-smile"></use>
57+
</svg>
58+
</div>
59+
<p>${message}</p>
60+
</div>`;
61+
62+
this._clear();
63+
this._parentElement.insertAdjacentHTML('afterbegin', markup);
64+
}
65+
}

config.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// here we'll put all the variables that should be constants and reused across the project
2+
// this will allow us to easily configure our project by simply changing some of the data in this file
3+
// we'll only put variables that are important for defining some important data about the application e.g., API url when getting search data and when uploading a recipe to the server
4+
export const API_URL = 'https://forkify-api.herokuapp.com/api/v2/recipes/';// uppercase as this is a constant that will esssentially never change
5+
export const TIMEOUT_SEC = 10;
6+
export const RES_PER_PAGE = 10;

controller.js

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import * as model from './model.js';
2+
import recipeView from './views/recipeView.js';
3+
import searchView from './views/searchView.js';
4+
import resultsView from './views/resultsView.js';
5+
import paginationView from './views/paginationView.js';
6+
7+
// to make sure our app is supported by old browsers:
8+
import 'core-js/stable'; // polyfill everything else
9+
import 'regenerator-runtime/runtime'; //polyfill async await
10+
// import recipeView from './views/recipeView.js';
11+
12+
// if (module.hot) {
13+
// module.hot.accept();
14+
// }
15+
16+
const controlRecipes = async function () {
17+
try {
18+
// id
19+
const id = window.location.hash.slice();
20+
21+
// guard clause
22+
if (!id) return;
23+
// listen for the event of the entire page loading
24+
// render spinner
25+
recipeView.renderSpinner(); //you can do this with all other views
26+
27+
// load recipe
28+
await model.loadRecipe(id); //1. we receive data here
29+
// loadRecipe is an async function and will return a promise; we thus have to await that promise before we can move on to execution
30+
// this is an example of one async function calling another async function
31+
// const recipe = model.state.recipe
32+
33+
// console.log(recipe);
34+
// 2) Rendering recipe
35+
recipeView.render(model.state.recipe); // 2. we render data here
36+
// 'render' is a very common name for render e.g., in React
37+
// const recipeView = new recipeView(model.state.recipe)// also possible
38+
// render method will accept the data from recipe and store it in recipeView object
39+
40+
// TEST
41+
controlServings()
42+
} catch (err) {
43+
recipeView.renderError();
44+
}
45+
};
46+
47+
const controlSearchResulsts = async () => {
48+
try {
49+
resultsView.renderSpinner();
50+
console.log(resultsView);
51+
// 1) Get search query
52+
const query = searchView.getQuery();
53+
if (!query) return;
54+
55+
// 2) Load search results
56+
await model.loadSearchResults(query);
57+
58+
// 3) Render results
59+
// resultsView.render(model.state.search.results)
60+
resultsView.render(model.getSearchResultsPage());
61+
62+
// 4) Render initial pagination buttons
63+
paginationView.render(model.state.search); // pass in the whole object
64+
// console.log(model.getSearchResultsPage(1));
65+
} catch (err) {
66+
console.error(err);
67+
}
68+
};
69+
70+
// Publisher-subscriber pattern in practice
71+
// init function is called when the program starts which immediately calls the addHandlerRender function from the view
72+
// this is possible because the controller does in fact import both the view and the model
73+
// as we call addHandlerRender, we pass in controlRecipes as an argument
74+
// essentially, we subscribe controlRecipes to addHandlerRender
75+
// at this point, the two functions are essentially finally connected
76+
// addHandlerRender thus listens for events (addEventListener), and uses controlRecipes as a callback
77+
// as soon as the publisher publishes an event, the subscriber will get called
78+
// this will allow keeping the handler in the controller and the listener in the view
79+
80+
// analyze controlSearchResulsts and controlPagination; they're quite similar and inter-twined
81+
82+
const controlPagination = goToPage => {
83+
// 1) Render new results
84+
// resultsView.render(model.state.search.results)
85+
resultsView.render(model.getSearchResultsPage(goToPage));
86+
//this works because render will overwrite the markup that was there previously; because of the clear() method
87+
88+
// 2) Render new pagination buttons
89+
paginationView.render(model.state.search);
90+
};
91+
92+
const controlServings = () => {
93+
// update the recipe servings (in state)
94+
model.updateServings(8)
95+
// update the recipe view; we'll simply re-render the recipe instead of manually changing the quantitity elements
96+
recipeView.render(model.state.recipe);
97+
}
98+
99+
const init = () => {
100+
recipeView.addHandlerRender(controlRecipes);
101+
102+
// the event is listened for in addHandlerRender but handled here
103+
searchView.addHandlerSearch(controlSearchResulsts);
104+
paginationView.addHandlerclick(controlPagination);
105+
};
106+
init();

helpers.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// this will contain functions that we reuse over and over again in our project
2+
import { TIMEOUT_SEC } from "./config";
3+
4+
const timeout = function (s) {
5+
return new Promise(function (_, reject) {
6+
setTimeout(function () {
7+
reject(new Error(`Request took too long! Timeout after ${s} second`));
8+
}, s * 1000);
9+
});
10+
};
11+
12+
export const getJSON = async url => {
13+
try {// will do fetching and converting to JSON all in one step
14+
const res = await Promise.race([fetch(url), timeout(TIMEOUT_SEC)]);// there'll be a race between this fetch function and the timeout function
15+
// as soon as a promise in the race rejects or fulfills, that promise becomes the winner
16+
const data = await res.json(); // json returns another promise which we would have to await again
17+
18+
if (!res.ok) throw new Error(`${data.message} (${res.status})`);
19+
return data;//data will be the resolved value of the promise that getJSON returns
20+
} catch(err) {
21+
// rethrow the error to catch it somewhere else
22+
throw err;
23+
// now the promise that's being returned from getJSON will reject
24+
// we've propagated the error down from one async function to the other by rethrowing the error
25+
}
26+
};

0 commit comments

Comments
 (0)