Skip to content
This repository was archived by the owner on Oct 1, 2025. It is now read-only.

Commit 4232bbe

Browse files
committed
feat: Completed Work in Forkify App Second Section
1 parent d2ac2ab commit 4232bbe

File tree

8 files changed

+217
-28
lines changed

8 files changed

+217
-28
lines changed

10-forkify/src/js/controller.js

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import 'core-js/stable';
22
import 'regenerator-runtime/runtime';
33

44
import * as model from './model.js';
5+
import { getHash } from './helpers.js';
56
import recipeView from './views/recipeView.js';
67
import searchView from './views/searchView.js';
78
import resultsView from './views/resultsView.js';
89
import paginationView from './views/paginationView.js';
10+
import bookmarksView from './views/bookmarksView.js';
911

1012
// https://forkify-api.herokuapp.com/v2
1113

@@ -15,14 +17,18 @@ const controlRecipe = async function ()
1517
{
1618
try
1719
{
18-
// Get Hash
19-
const id = window.location.hash.slice(1);
20+
const id = getHash();
2021

2122
if (!id) return;
2223

2324
// Render Spinner while the Data is being Fetched
2425
recipeView.renderSpinner();
2526

27+
// Update Selected Recipe from Search Results
28+
resultsView.update(model.getSearchResultsPage());
29+
// Update Bookmarks
30+
bookmarksView.update(model.state.bookmarks);
31+
2632
// Fetching Data
2733
await model.loadRecipe(id);
2834
const { recipe } = model.state;
@@ -41,6 +47,9 @@ const controlSearchResults = async function ()
4147
{
4248
try
4349
{
50+
// Render Spinner while the Search is being Fetched
51+
resultsView.renderSpinner();
52+
4453
// Get Search Query
4554
const query = searchView.getQuery();
4655

@@ -51,7 +60,7 @@ const controlSearchResults = async function ()
5160
console.log(model.state.search);
5261

5362
// Render Search Results
54-
resultsView.render(model.getSearchResultsPage(1));
63+
resultsView.render(model.getSearchResultsPage());
5564
// Render Initial Pagination Buttons
5665
paginationView.render(model.state.search);
5766
}
@@ -70,9 +79,45 @@ const controlPagination = function (goToPage)
7079
paginationView.render(model.state.search);
7180
};
7281

82+
const controlServings = function (newServings)
83+
{
84+
// Update Recipe Servings
85+
model.updateServings(newServings);
86+
87+
// Update Recipe
88+
recipeView.update(model.state.recipe);
89+
};
90+
91+
const controlBookmark = function ()
92+
{
93+
// Add/Remove Bookmark
94+
model.state.recipe.bookmarked ? model.delBookmark(model.state.recipe.id) : model.addBookmark(model.state.recipe);
95+
96+
// Update Recipe
97+
console.log(model.state.recipe);
98+
recipeView.update(model.state.recipe);
99+
100+
// Render Bookmarks
101+
bookmarksView.render(model.state.bookmarks);
102+
};
103+
104+
const controlInitBookmarks = function ()
105+
{
106+
bookmarksView.render(model.state.bookmarks);
107+
};
108+
109+
const clearBookmarks = function ()
110+
{
111+
localStorage.clear('bookmarks');
112+
};
113+
// clearBookmarks()
114+
73115
const init = function ()
74116
{
117+
bookmarksView.addHandlerRender(controlInitBookmarks);
75118
recipeView.addHandlerRender(controlRecipe);
119+
recipeView.addHandlerUpdateServings(controlServings);
120+
recipeView.addHandlerBookmark(controlBookmark);
76121
searchView.addHandlerSearch(controlSearchResults);
77122
paginationView.addHandlerPagination(controlPagination);
78123
};

10-forkify/src/js/helpers.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ export const timeout = function (s)
1111
});
1212
};
1313

14+
15+
export const getHash = function ()
16+
{
17+
// Get Hash ID
18+
return window.location.hash.slice(1);
19+
};
20+
1421
export const getJSON = async function (url)
1522
{
1623
try

10-forkify/src/js/model.js

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ import { API_URL, RESULTS_PER_PAGE } from './config';
44
import { getJSON } from './helpers.js';
55

66
export const state = {
7+
bookmarks: [],
78
recipe: {},
89
search: {
910
query: '',
1011
results: [],
1112
resultsPerPage: RESULTS_PER_PAGE,
12-
page: 1
1313
}
1414
};
1515

@@ -34,6 +34,9 @@ export const loadRecipe = async function (id)
3434
cookingTime: recipe.cooking_time,
3535
ingredients: recipe.ingredients
3636
};
37+
38+
state.recipe.bookmarked = state.bookmarks.some(bookmark => bookmark.id === id);
39+
3740
console.log(state.recipe);
3841
}
3942
catch (err)
@@ -48,6 +51,7 @@ export const loadSearchResults = async function (query)
4851
try
4952
{
5053
const data = await getJSON(`${ API_URL }?search=${ query }`);
54+
state.search.page = 1;
5155

5256
state.search.query = query;
5357
state.search.results = data.data.recipes.map(recipe =>
@@ -75,4 +79,55 @@ export const getSearchResultsPage = function (page = state.search.page)
7579
const end = page * state.search.resultsPerPage;
7680

7781
return state.search.results.slice(start, end);
78-
};
82+
};
83+
84+
export const updateServings = function (newServings)
85+
{
86+
const oldServings = state.recipe.servings;
87+
state.recipe.ingredients.map(ing =>
88+
{
89+
ing.quantity = ing.quantity * newServings / oldServings;
90+
});
91+
92+
state.recipe.servings = newServings;
93+
};
94+
95+
const storeBookmarks = function ()
96+
{
97+
localStorage.setItem('bookmarks', JSON.stringify(state.bookmarks));
98+
};
99+
100+
export const addBookmark = function (recipe)
101+
{
102+
// Add Bookmark
103+
state.bookmarks.push(recipe);
104+
105+
// Mark Current Recipe as Bookmarked
106+
if (recipe.id !== state.recipe.id) return;
107+
108+
state.recipe.bookmarked = true;
109+
storeBookmarks();
110+
};
111+
112+
export const delBookmark = function (id)
113+
{
114+
// Delete Bookmark
115+
const index = state.bookmarks.findIndex(bookmark => bookmark.id === id);
116+
state.bookmarks.splice(index, 1);
117+
118+
// Mark Current Recipe as Not Bookmarked
119+
if (id !== state.recipe.id) return;
120+
121+
state.recipe.bookmarked = false;
122+
storeBookmarks();
123+
};
124+
125+
const init = function ()
126+
{
127+
const storage = localStorage.getItem('bookmarks');
128+
129+
if (!storage) return;
130+
131+
state.bookmarks = JSON.parse(storage);
132+
};
133+
init();
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import PreviewView from "./previewView.js";
2+
3+
class BookmarksView extends PreviewView
4+
{
5+
_parentElement = document.querySelector('.bookmarks__list');
6+
_errorMessage = 'No bookmarks yet, Find a nice bookmark and bookmark it';
7+
_successMessage = '';
8+
9+
addHandlerRender (handler)
10+
{
11+
window.addEventListener('load', handler);
12+
}
13+
14+
_generateMarkup ()
15+
{
16+
return this._data.map(preview => this._generateMarkupPreview(preview)).join('\n');
17+
}
18+
}
19+
20+
export default new BookmarksView();
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import View from "./view";
2+
3+
import { getHash } from "../helpers";
4+
5+
export default class PreviewView extends View
6+
{
7+
_generateMarkupPreview (preview)
8+
{
9+
const id = getHash();
10+
11+
return `
12+
<li class="preview">
13+
<a class="preview__link ${ preview.id === id ? 'preview__link--active' : '' }" href="#${ preview.id }">
14+
<figure class="preview__fig">
15+
<img src="${ preview.image }" alt="${ preview.title }" />
16+
</figure>
17+
<div class="preview__data">
18+
<h4 class="preview__title">${ preview.title }</h4>
19+
<p class="preview__publisher">${ preview.publisher }</p>
20+
</div>
21+
</a>
22+
</li>
23+
`;
24+
}
25+
}

10-forkify/src/js/views/recipeView.js

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,31 @@ class RecipeView extends View
1515
['hashchange', 'load'].forEach(eventName => window.addEventListener(eventName, handler));
1616
}
1717

18+
addHandlerUpdateServings (handler)
19+
{
20+
this._parentElement.addEventListener('click', event =>
21+
{
22+
const btn = event.target.closest('.btn--update-servings');
23+
24+
if (!btn) return;
25+
26+
const updateTo = +btn.dataset.update;
27+
if (updateTo > 0) handler(updateTo);
28+
});
29+
}
30+
31+
addHandlerBookmark (handler)
32+
{
33+
this._parentElement.addEventListener('click', event =>
34+
{
35+
const btn = event.target.closest('.btn--bookmark');
36+
37+
if (!btn) return;
38+
39+
handler();
40+
});
41+
}
42+
1843
_generateMarkupIngredient (ing)
1944
{
2045
return `
@@ -57,12 +82,12 @@ class RecipeView extends View
5782
<span class="recipe__info-text">servings</span>
5883
5984
<div class="recipe__info-buttons">
60-
<button class="btn--tiny btn--increase-servings">
85+
<button data-update="${ this._data.servings - 1 }" class="btn--tiny btn--update-servings">
6186
<svg>
6287
<use href="${ icons }#icon-minus-circle"></use>
6388
</svg>
6489
</button>
65-
<button class="btn--tiny btn--increase-servings">
90+
<button data-update="${ this._data.servings + 1 }" class="btn--tiny btn--update-servings">
6691
<svg>
6792
<use href="${ icons }#icon-plus-circle"></use>
6893
</svg>
@@ -72,9 +97,9 @@ class RecipeView extends View
7297
7398
<div class="recipe__user-generated">
7499
</div>
75-
<button class="btn--round">
100+
<button class="btn--round btn--bookmark">
76101
<svg class="">
77-
<use href="${ icons }#icon-bookmark-fill"></use>
102+
<use href="${ icons }#icon-bookmark${ this._data.bookmarked ? '-fill' : '' }"></use>
78103
</svg>
79104
</button>
80105
</div>
Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import View from './view';
1+
import PreviewView from './previewView.js';
22

33
import icons from 'url:../../img/icons.svg';
44

5-
class ResultsView extends View
5+
class ResultsView extends PreviewView
66
{
77
_parentElement = document.querySelector('.results');
88
_errorMessage = 'No recipes found that matched your query';
@@ -12,23 +12,6 @@ class ResultsView extends View
1212
{
1313
return this._data.map(preview => this._generateMarkupPreview(preview)).join('\n');
1414
}
15-
16-
_generateMarkupPreview (preview)
17-
{
18-
return `
19-
<li class="preview">
20-
<a class="preview__link" href="#${ preview.id }">
21-
<figure class="preview__fig">
22-
<img src="${ preview.image }" alt="Test" />
23-
</figure>
24-
<div class="preview__data">
25-
<h4 class="preview__title">${ preview.title }</h4>
26-
<p class="preview__publisher">${ preview.publisher }</p>
27-
</div>
28-
</a>
29-
</li>
30-
`;
31-
}
3215
}
3316

3417
export default new ResultsView();

10-forkify/src/js/views/view.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,39 @@ export default class View
1010

1111
this._data = data;
1212
const markup = this._generateMarkup();
13+
1314
this._clear();
1415
this._parentElement.insertAdjacentHTML('afterbegin', markup);
1516
}
1617

18+
update (data)
19+
{
20+
// if (!data || (Array.isArray(data) && data.length === 0)) return this.renderError();
21+
22+
this._data = data;
23+
const newMarkup = this._generateMarkup();
24+
25+
const newDOM = document.createRange().createContextualFragment(newMarkup);
26+
const newElements = Array.from(newDOM.querySelectorAll('*'));
27+
const currElements = Array.from(this._parentElement.querySelectorAll('*'));
28+
29+
newElements.forEach((newEl, i) =>
30+
{
31+
const currEl = currElements.at(i);
32+
33+
// Checks if Nodes are Different from Each Other
34+
if (newEl.isEqualNode(currEl)) return;
35+
36+
// Checks if it's First Child is Text
37+
if (newEl.firstChild?.nodeValue.trim() !== '')
38+
// Update Changed Text
39+
currEl.textContent = newEl.textContent;
40+
41+
// Update Node Attributes
42+
Array.from(newEl.attributes).forEach(atr => currEl.setAttribute(atr.name, atr.value));
43+
});
44+
}
45+
1746
_clear ()
1847
{
1948
this._parentElement.innerHTML = '';

0 commit comments

Comments
 (0)