From 84888e88194b5679f7bc7ac6ed02d85ca55ec155 Mon Sep 17 00:00:00 2001 From: Frida Engman Date: Thu, 25 Sep 2025 17:23:58 +0300 Subject: [PATCH 1/7] laying foundation --- index.html | 60 +++++++++++++++++++++++++ script.js | 15 +++++++ style.css | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 index.html create mode 100644 script.js create mode 100644 style.css diff --git a/index.html b/index.html new file mode 100644 index 000000000..e09087d82 --- /dev/null +++ b/index.html @@ -0,0 +1,60 @@ + + + + + + + + Test project 3 + + + + + +
+

Recipe Library

+
+
+ +
+

Filter on kitchen

+ + + + +
+ +
+

Sort on time

+ + +
+ +
+

1 place holder +

+
+
+ 2 place holder +
+
+ 3 place holder +
+
+ 4 place holder +
+ +
+ + + + + + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 000000000..e6c11f7ad --- /dev/null +++ b/script.js @@ -0,0 +1,15 @@ +// //function for buttons +const buttonGroups = document.querySelectorAll(".filter-and-buttons, .sorting-and-buttons") + +buttonGroups.forEach(group => { + const buttons = group.querySelectorAll("button") + + buttons.forEach(button => { + button.addEventListener("click", () => { + buttons.forEach(b => b.classList.remove("selected")); + button.classList.add("selected"); + }); + }); +}); + + diff --git a/style.css b/style.css new file mode 100644 index 000000000..9ae587327 --- /dev/null +++ b/style.css @@ -0,0 +1,126 @@ +/*mobile first*/ + +body { + background: #FAFBFF; + align-items: center; + margin-left: 64px; + margin-right: 64px; +} + +h1 { + font-family: sans-serif; + font-style: bold; + font-size: 64px; + color: #0018A4; + margin-top: 64px; + +} + +.grid-parent { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; + margin-top: 64px; +} + +.filter-and-buttons { + grid-column: span 2; +} + +.sorting-and-buttons { + grid-column: span 2; +} + +h2 { + font-family: sans-serif; + font-weight: 700; + font-style: bold; + font-size: 22px; + color: black; +} + +.filter-and-buttons button { + background-color: #CCFFE2; + color: #0018A4; + border: none; + cursor: pointer; + border-radius: 50px; + padding: 8px 16px 8px 16px; + gap: 10px; +} + + +.filter-and-buttons button.selected { + background-color: #0018A4; + color: white; +} + + +.sorting-and-buttons button { + background-color: #FFECEA; + color: #0018A4; + border: none; + cursor: pointer; + border-radius: 50px; + padding: 8px 16px 8px 16px; + gap: 10px; +} + +.sorting-and-buttons button.selected { + background-color: #FF6589; + color: white; +} + +.grid-card { + border: 1px solid #E9E9E9; + border-radius: 16px; + grid-column: span 4; +} + + +/* responsive tablet and desktop */ +@media (min-width: 667px) { + + .grid-parent { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; + margin-top: 64px; + } + + .filter-and-buttons { + grid-column: span 1; + } + + .sorting-and-buttons { + grid-column: span 3; + } + + .filter-and-buttons button:hover { + border: 1px solid #0018A4; + } + + + .sorting-and-buttons button:hover { + border: 1px solid #0018A4; + background-color: #FF6589; + color: white; + } + + .grid-card:hover { + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); + border: 2px solid #0018A4; + border-radius: 16px; + padding: 16px 16px 24px 16px; + gap: 16px; + + } + + .grid-card { + border: 1px solid #E9E9E9; + border-radius: 16px; + grid-column: span 1; + grid-row: span 2; + } + +} \ No newline at end of file From 9a428d4b1a24c8b3a5d74170c0e9c78e56ee11d4 Mon Sep 17 00:00:00 2001 From: Frida Engman Date: Fri, 26 Sep 2025 13:02:02 +0300 Subject: [PATCH 2/7] fixing mobile first button layout --- index.html | 4 ++-- style.css | 52 ++++++++++++++++++++++++++++++---------------------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/index.html b/index.html index e09087d82..1ab7a9219 100644 --- a/index.html +++ b/index.html @@ -22,6 +22,7 @@

Recipe Library

+

Filter on kitchen

@@ -37,8 +38,7 @@

Sort on time

-

1 place holder -

+ 1 place holder
2 place holder diff --git a/style.css b/style.css index 9ae587327..65d221ed1 100644 --- a/style.css +++ b/style.css @@ -3,12 +3,10 @@ body { background: #FAFBFF; align-items: center; - margin-left: 64px; - margin-right: 64px; } h1 { - font-family: sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-style: bold; font-size: 64px; color: #0018A4; @@ -18,25 +16,19 @@ h1 { .grid-parent { display: grid; - grid-template-columns: repeat(4, 1fr); + grid-template-columns: 1fr; gap: 20px; margin-top: 64px; } -.filter-and-buttons { - grid-column: span 2; -} - -.sorting-and-buttons { - grid-column: span 2; -} - +/* filter and sorting section */ h2 { - font-family: sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-weight: 700; font-style: bold; font-size: 22px; color: black; + margin-bottom: 10px; } .filter-and-buttons button { @@ -49,12 +41,15 @@ h2 { gap: 10px; } - .filter-and-buttons button.selected { background-color: #0018A4; color: white; } +.sorting-and-buttons { + grid-row: 2; + /*for the lay out in mobile first*/ +} .sorting-and-buttons button { background-color: #FFECEA; @@ -71,16 +66,27 @@ h2 { color: white; } + +/* recipe cards */ + .grid-card { border: 1px solid #E9E9E9; border-radius: 16px; grid-column: span 4; + height: 400px; } /* responsive tablet and desktop */ @media (min-width: 667px) { + body { + margin-left: 64px; + margin-right: 64px; + } + + /* layout desktop */ + .grid-parent { display: grid; grid-template-columns: repeat(4, 1fr); @@ -94,13 +100,22 @@ h2 { .sorting-and-buttons { grid-column: span 3; + grid-row: auto; } + .grid-card { + border: 1px solid #E9E9E9; + border-radius: 16px; + grid-column: span 1; + grid-row: span 2; + } + + /*effects desktop*/ + .filter-and-buttons button:hover { border: 1px solid #0018A4; } - .sorting-and-buttons button:hover { border: 1px solid #0018A4; background-color: #FF6589; @@ -116,11 +131,4 @@ h2 { } - .grid-card { - border: 1px solid #E9E9E9; - border-radius: 16px; - grid-column: span 1; - grid-row: span 2; - } - } \ No newline at end of file From cdde9d928d7486fad63326f2043fdadfb9ec6c17 Mon Sep 17 00:00:00 2001 From: Frida Engman Date: Tue, 7 Oct 2025 17:53:39 +0300 Subject: [PATCH 3/7] java-script for recipe cards --- .DS_Store | Bin 0 -> 6148 bytes index.html | 47 +++++++++++------- script.js | 140 +++++++++++++++++++++++++++++++++++++++++++++++++++-- style.css | 119 +++++++++++++++++++++++++++++++++++++++------ 4 files changed, 272 insertions(+), 34 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..06ecffd19fc506a302aad2fc5637dcba89f61419 GIT binary patch literal 6148 zcmeHKK~BRk5FD2xwIHD$IpzaI>JLJd9yo9SK7g830u(4BMS@%IJb{03siY_F~l)mkm3eQ+@tD5d&6&3 zK=$qcw|GE_Io9^?tXdSaYB6KK9F7b6$CnxFLtJ8tQ{*@&rrhyzgYns6)F|-8xgL2! zQrw|#Gs4Lh#uBeQOLN8-s5^`|GE#jh)_^0%Y%nn1!F^}^`ikNJUbq6TfGhAX6kyI4 z8yqTn=?b_4uE0tG`936c!OXBxjGqn`@d`lfvpE{;^1CRU#4s~#6xl-yN+l{a*cBrv zo$ZOmWrmHS(h=K@ - Test project 3 + Recipe Library @@ -20,40 +20,53 @@

Recipe Library

+

Filter on kitchen

- - - + + + +

Sort on time

- - + +
-
- 1 place holder -
-
- 2 place holder -
-
- 3 place holder +
+

Preferences

+ + +
-
- 4 place holder + + +
+

Surprise me!

+
-
+
+ +
+
+ diff --git a/script.js b/script.js index e6c11f7ad..d42374e2a 100644 --- a/script.js +++ b/script.js @@ -1,8 +1,17 @@ -// //function for buttons -const buttonGroups = document.querySelectorAll(".filter-and-buttons, .sorting-and-buttons") +// elements from html +const buttonGroups = document.querySelectorAll(".filter-and-buttons, .sorting-and-buttons, .random-button"); +const recipeCard = document.getElementById('recipe-card'); +// API key + url +const apiKey = '0cc881e89fc0422eac77c85260da365d'; +const URL = `https://api.spoonacular.com/recipes/random?number=10&apiKey=${apiKey}`; + +// recipe data holder +let allMeals = []; + +// buttons buttonGroups.forEach(group => { - const buttons = group.querySelectorAll("button") + const buttons = group.querySelectorAll("button"); buttons.forEach(button => { button.addEventListener("click", () => { @@ -12,4 +21,129 @@ buttonGroups.forEach(group => { }); }); +// API, fetch recipe data +const fetchData = () => { + fetch(URL) + .then(response => response.json()) + .then(data => { + allMeals = data.recipes; + renderMeals(allMeals); + }) + .catch(error => { + console.error('Error:', error); + recipeCard.innerHTML = '

No recipe, sorry!

'; + }); +}; + +// cards +function renderMeals(meals) { + recipeCard.innerHTML = ''; + + meals.forEach(meal => { + const ingredients = meal.extendedIngredients + ? meal.extendedIngredients.slice(0, 4).map(ing => ing.name) + : []; + + const cardHTML = ` +
+ ${meal.title} +

${meal.title}

+

Dish Type: ${meal.dishTypes?.[0] || 'Unknown'}

+

Country (Cuisine): ${meal.cuisines?.[0] || 'Unknown'}

+

Ready in: ${meal.readyInMinutes} min

+

Ingredients:

+
    + ${ingredients.map(ing => `
  • ${ing}
  • `).join('')} +
+
+ `; + recipeCard.insertAdjacentHTML('beforeend', cardHTML); + }); +} + +// filtering on kitchen +const filterButtons = document.querySelectorAll(".filter-and-buttons button"); + +filterButtons.forEach(button => { + button.addEventListener("click", () => { + filterButtons.forEach(b => b.classList.remove("selected")); + button.classList.add("selected"); + + const filter = button.textContent.trim(); + + if (filter === "All") { + renderMeals(allMeals); + } else { + const filtered = allMeals.filter(meal => + meal.cuisines && meal.cuisines.includes(filter) + ); + renderMeals(filtered); + } + }); +}); + +//filtering on sorting + +const sortButtons = document.querySelectorAll(".sorting-and-buttons button"); + +sortButtons.forEach(button => { + button.addEventListener("click", () => { + sortButtons.forEach(b => b.classList.remove("selected")) + button.classList.add("selected") + + const sortType = button.textContent.trim().toLowerCase() + + if (sortType === "quick meals") { + const quickMeals = allMeals.filter(meal => meal.readyInMinutes <= 20) + renderMeals(quickMeals) + } else if (sortType === "slow cook's") { + const longMeals = allMeals.filter(meal => meal.readyInMinutes > 20) + renderMeals(longMeals) + } else { + renderMeals(allMeals) + } + }) +}) + +// random recipe generator +const randomButton = document.querySelector(".random-button button") + +if (randomButton) { + randomButton.addEventListener("click", () => { + filterButtons.forEach(b => b.classList.remove("selected")) + sortButtons.forEach(b => b.classList.remove("selected")) + const allFilterBtn = Array.from(filterButtons).find(b => b.textContent.trim() === "All") + if (allFilterBtn) allFilterBtn.classList.add("selected") + const allSortBtn = Array.from(sortButtons).find(b => b.textContent.trim().toLowerCase() === "all") + if (allSortBtn) allSortBtn.classList.add("selected") + + fetchData() + }) +} + + +function toggleDropdown() { + document.getElementById("dropdownMenu").classList.toggle("show"); +} + +window.onclick = function (event) { + if (!event.target.matches('button')) { + let dropdowns = document.getElementsByClassName("preferences-content"); + for (let i = 0; i < dropdowns.length; i++) { + let openDropdown = dropdowns[i]; + if (openDropdown.classList.contains('show')) { + openDropdown.classList.remove('show'); + } + } + } +} + + +//loading recipes from start +window.onload = fetchData + + + + + diff --git a/style.css b/style.css index 65d221ed1..c08984fea 100644 --- a/style.css +++ b/style.css @@ -1,8 +1,7 @@ -/*mobile first*/ - +/* mobile first*/ body { background: #FAFBFF; - align-items: center; + } h1 { @@ -66,16 +65,80 @@ h2 { color: white; } +.random-button button { + background-color: #0018A4; + color: #FFECEA; + border: none; + cursor: pointer; + border-radius: 50px; + padding: 8px 16px 8px 16px; + gap: 10px; +} -/* recipe cards */ +.random-button button.selected { + background-color: #CCFFE2; + color: #FF6589; +} -.grid-card { - border: 1px solid #E9E9E9; +.preferences button { + background-color: #0018A4; + color: #FFECEA; + border: none; + cursor: pointer; + border-radius: 50px; + padding: 8px 16px 8px 16px; + gap: 10px; + position: relative; + display: inline-block; +} + +.preferences-content { + display: none; + position: absolute; + background-color: #FFECEA; + min-width: 160px; + box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.2); + border-radius: 20px; +} + +.preferences-content a { + padding: 10px 16px; + display: block; + text-decoration: none; + color: #0018A4; + border-radius: 50px; +} + + +.show { + display: block; +} + + + +/*recipe cards*/ + +#recipe-card { + margin-left: 13%; +} + +.card { + width: 200px; + height: auto; + display: inline-block; + /* vertical-align: text-top; */ border-radius: 16px; - grid-column: span 4; - height: 400px; + border: 1px solid #E9E9E9; + padding: 5px; + margin-top: 40px; } +.card img { + width: 100%; + height: 150px; + object-fit: cover; + border-radius: 10px; +} /* responsive tablet and desktop */ @media (min-width: 667px) { @@ -92,6 +155,7 @@ h2 { grid-template-columns: repeat(4, 1fr); gap: 20px; margin-top: 64px; + } .filter-and-buttons { @@ -99,11 +163,20 @@ h2 { } .sorting-and-buttons { - grid-column: span 3; + grid-column: span 1; grid-row: auto; } - .grid-card { + .random-button { + grid-column: span 1; + grid-row: auto; + } + + #recipe-card { + margin-left: 0%; + } + + .card { border: 1px solid #E9E9E9; border-radius: 16px; grid-column: span 1; @@ -112,23 +185,41 @@ h2 { /*effects desktop*/ + /*filter button*/ .filter-and-buttons button:hover { border: 1px solid #0018A4; } + /*sorting button*/ .sorting-and-buttons button:hover { border: 1px solid #0018A4; background-color: #FF6589; color: white; } - .grid-card:hover { + /*preference button*/ + .preferences button:hover { + border: 1px solid #FF6589; + } + + .preferences-content a:hover { + background-color: #FF6589; + } + + /*random button*/ + .random-button button { + transition: transform 0.4s ease; + } + + .random-button button:hover { + transform: rotate(360deg); + } + + /*recipe cards*/ + .card:hover { box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); border: 2px solid #0018A4; border-radius: 16px; - padding: 16px 16px 24px 16px; - gap: 16px; - } } \ No newline at end of file From c3491f2a4ea95e9d94fbc7441a94ef17c765aacb Mon Sep 17 00:00:00 2001 From: Frida Engman Date: Wed, 8 Oct 2025 17:57:29 +0300 Subject: [PATCH 4/7] adds preference selector --- .DS_Store | Bin 6148 -> 6148 bytes index.html | 14 ++-- script.js | 185 +++++++++++++++++++++++++++++++++++------------------ style.css | 6 +- 4 files changed, 129 insertions(+), 76 deletions(-) diff --git a/.DS_Store b/.DS_Store index 06ecffd19fc506a302aad2fc5637dcba89f61419..68b32ea5eb5463a941b983b6f4f1f35201247e41 100644 GIT binary patch delta 66 zcmZoMXfc=|#>B`mu~2NHo+2a5#DLw5ER%Vd_HJfnW@6pgV8ghXor9kPsA97s^LOUS U{34bd3_!rhz`(RQKx7Lu03#RB)qu~2NHo+2ab#DLw4KQJ;fvQFk<+^gEnz`&sQ9}Ivj1_nNcVulii zN`@SUR0chUWFW7Y!Eo~?MoHGq>>T_YKyx-fWcRecipe Library

Filter on kitchen

- + - +
@@ -46,9 +46,9 @@

Preferences

id="dropdownMenu" class="preferences-content" > - Gluten Free - Vegetarian - Vegan + + + @@ -58,10 +58,6 @@

Preferences

Surprise me!

- - - -
diff --git a/script.js b/script.js index d42374e2a..1462af67d 100644 --- a/script.js +++ b/script.js @@ -1,91 +1,131 @@ -// elements from html -const buttonGroups = document.querySelectorAll(".filter-and-buttons, .sorting-and-buttons, .random-button"); -const recipeCard = document.getElementById('recipe-card'); +// Elements +const buttonGroups = document.querySelectorAll(".filter-and-buttons, .sorting-and-buttons, .random-button") +const recipeCard = document.getElementById('recipe-card') +const filterButtons = document.querySelectorAll(".filter-and-buttons button") +const sortButtons = document.querySelectorAll(".sorting-and-buttons button") +const randomButton = document.querySelector(".random-button button") +const preferenceOptions = document.querySelectorAll("#dropdownMenu option") + +// API key & URL +const apiKey = '0cc881e89fc0422eac77c85260da365d' +const URL = `https://api.spoonacular.com/recipes/random?number=12&apiKey=${apiKey}` -// API key + url -const apiKey = '0cc881e89fc0422eac77c85260da365d'; -const URL = `https://api.spoonacular.com/recipes/random?number=10&apiKey=${apiKey}`; +// Global data holders +let allMeals = [] +let currentPreference = "" -// recipe data holder -let allMeals = []; -// buttons +// Button visual selection buttonGroups.forEach(group => { - const buttons = group.querySelectorAll("button"); + group.addEventListener("click", e => { + if (e.target.tagName !== "BUTTON") return + group.querySelectorAll("button").forEach(b => b.classList.remove("selected")) + e.target.classList.add("selected") + }) +}) + - buttons.forEach(button => { - button.addEventListener("click", () => { - buttons.forEach(b => b.classList.remove("selected")); - button.classList.add("selected"); - }); - }); -}); +// Data from localStorage +function loadFromLocalStorage() { + const storedRecipes = localStorage.getItem("recipes") + if (storedRecipes) { + console.log("Recipes loaded from localStorage") + allMeals = JSON.parse(storedRecipes) -// API, fetch recipe data + const filtered = filterByPreference(allMeals) + renderMeals(filtered) + return true + } + return false +} + + +// Fetch data from Spoonacular API const fetchData = () => { fetch(URL) .then(response => response.json()) .then(data => { - allMeals = data.recipes; - renderMeals(allMeals); + allMeals = data.recipes + console.log("Fetched recipes:", allMeals) + + // Save recipes to localStorage + localStorage.setItem("recipes", JSON.stringify(allMeals)) + + // Filter and display + const filtered = filterByPreference(allMeals) + renderMeals(filtered) }) .catch(error => { - console.error('Error:', error); - recipeCard.innerHTML = '

No recipe, sorry!

'; - }); -}; + console.error('Error fetching recipes:', error) + recipeCard.innerHTML = '

No recipes found. Please try again.

' + }) +} + + +// Filter recipes on diet/ preference +function filterByPreference(meals) { + if (!currentPreference || currentPreference === "all") return meals + + return meals.filter(meal => + meal.diets && meal.diets.includes(currentPreference) + ) +} -// cards + +// Recipe cards function renderMeals(meals) { - recipeCard.innerHTML = ''; + recipeCard.innerHTML = "" + + if (meals.length === 0) { + recipeCard.innerHTML = '

No recipes match your filters.

' + return + } meals.forEach(meal => { const ingredients = meal.extendedIngredients ? meal.extendedIngredients.slice(0, 4).map(ing => ing.name) - : []; + : [] const cardHTML = `
${meal.title}

${meal.title}

-

Dish Type: ${meal.dishTypes?.[0] || 'Unknown'}

-

Country (Cuisine): ${meal.cuisines?.[0] || 'Unknown'}

+

Cuisine: ${meal.cuisines?.[0] || 'Unknown'}

Ready in: ${meal.readyInMinutes} min

Ingredients:

    ${ingredients.map(ing => `
  • ${ing}
  • `).join('')}
- `; - recipeCard.insertAdjacentHTML('beforeend', cardHTML); - }); + ` + recipeCard.insertAdjacentHTML('beforeend', cardHTML) + }) } -// filtering on kitchen -const filterButtons = document.querySelectorAll(".filter-and-buttons button"); +// Filter by kitchen/cuisine filterButtons.forEach(button => { button.addEventListener("click", () => { - filterButtons.forEach(b => b.classList.remove("selected")); - button.classList.add("selected"); + filterButtons.forEach(b => b.classList.remove("selected")) + button.classList.add("selected") - const filter = button.textContent.trim(); + const filter = button.textContent.trim() if (filter === "All") { - renderMeals(allMeals); + const filtered = filterByPreference(allMeals) + renderMeals(filtered) } else { - const filtered = allMeals.filter(meal => + const filteredByCuisine = allMeals.filter(meal => meal.cuisines && meal.cuisines.includes(filter) - ); - renderMeals(filtered); + ) + const finalFiltered = filterByPreference(filteredByCuisine) + renderMeals(finalFiltered) } - }); -}); - -//filtering on sorting + }) +}) -const sortButtons = document.querySelectorAll(".sorting-and-buttons button"); +// Filter by cooking time sortButtons.forEach(button => { button.addEventListener("click", () => { sortButtons.forEach(b => b.classList.remove("selected")) @@ -95,25 +135,29 @@ sortButtons.forEach(button => { if (sortType === "quick meals") { const quickMeals = allMeals.filter(meal => meal.readyInMinutes <= 20) - renderMeals(quickMeals) + const filtered = filterByPreference(quickMeals) + renderMeals(filtered) } else if (sortType === "slow cook's") { - const longMeals = allMeals.filter(meal => meal.readyInMinutes > 20) - renderMeals(longMeals) + const slowMeals = allMeals.filter(meal => meal.readyInMinutes > 20) + const filtered = filterByPreference(slowMeals) + renderMeals(filtered) } else { - renderMeals(allMeals) + const filtered = filterByPreference(allMeals) + renderMeals(filtered) } }) }) -// random recipe generator -const randomButton = document.querySelector(".random-button button") +// Get random recipes if (randomButton) { randomButton.addEventListener("click", () => { filterButtons.forEach(b => b.classList.remove("selected")) sortButtons.forEach(b => b.classList.remove("selected")) + const allFilterBtn = Array.from(filterButtons).find(b => b.textContent.trim() === "All") if (allFilterBtn) allFilterBtn.classList.add("selected") + const allSortBtn = Array.from(sortButtons).find(b => b.textContent.trim().toLowerCase() === "all") if (allSortBtn) allSortBtn.classList.add("selected") @@ -122,28 +166,41 @@ if (randomButton) { } +// Preference dropdown toggle function toggleDropdown() { - document.getElementById("dropdownMenu").classList.toggle("show"); + document.getElementById("dropdownMenu").classList.toggle("show") } + +// Preference selection +preferenceOptions.forEach(option => { + option.addEventListener("click", () => { + currentPreference = option.value.toLowerCase() + const filtered = filterByPreference(allMeals) + renderMeals(filtered) + toggleDropdown() + }) +}) + + +// Close dropdown if clicked outside window.onclick = function (event) { if (!event.target.matches('button')) { - let dropdowns = document.getElementsByClassName("preferences-content"); + let dropdowns = document.getElementsByClassName("preferences-content") for (let i = 0; i < dropdowns.length; i++) { - let openDropdown = dropdowns[i]; + let openDropdown = dropdowns[i] if (openDropdown.classList.contains('show')) { - openDropdown.classList.remove('show'); + openDropdown.classList.remove('show') } } } } -//loading recipes from start -window.onload = fetchData - - - - - - +// Initial page load +window.onload = () => { + const hasCache = loadFromLocalStorage() + if (!hasCache) { + fetchData() + } +} diff --git a/style.css b/style.css index c08984fea..b5d17b2b6 100644 --- a/style.css +++ b/style.css @@ -101,7 +101,7 @@ h2 { border-radius: 20px; } -.preferences-content a { +.preferences-content option { padding: 10px 16px; display: block; text-decoration: none; @@ -126,7 +126,6 @@ h2 { width: 200px; height: auto; display: inline-block; - /* vertical-align: text-top; */ border-radius: 16px; border: 1px solid #E9E9E9; padding: 5px; @@ -202,8 +201,9 @@ h2 { border: 1px solid #FF6589; } - .preferences-content a:hover { + .preferences-content option:hover { background-color: #FF6589; + cursor: pointer; } /*random button*/ From e3963978c140b1336d245ebedfa2fe98c819b416 Mon Sep 17 00:00:00 2001 From: Frida Engman Date: Thu, 9 Oct 2025 17:20:51 +0300 Subject: [PATCH 5/7] added comments and error messages --- index.html | 6 +-- script.js | 123 ++++++++++++++++++++++++++++++----------------------- style.css | 27 ++++++++++-- 3 files changed, 95 insertions(+), 61 deletions(-) diff --git a/index.html b/index.html index 1d5faffb1..96776501f 100644 --- a/index.html +++ b/index.html @@ -46,9 +46,9 @@

Preferences

id="dropdownMenu" class="preferences-content" > - - - +
Gluten Free
+
Vegetarian
+
Vegan
diff --git a/script.js b/script.js index 1462af67d..91b346a0c 100644 --- a/script.js +++ b/script.js @@ -1,31 +1,31 @@ -// Elements +// ------ Elements ------ // const buttonGroups = document.querySelectorAll(".filter-and-buttons, .sorting-and-buttons, .random-button") const recipeCard = document.getElementById('recipe-card') const filterButtons = document.querySelectorAll(".filter-and-buttons button") const sortButtons = document.querySelectorAll(".sorting-and-buttons button") const randomButton = document.querySelector(".random-button button") -const preferenceOptions = document.querySelectorAll("#dropdownMenu option") +const preferenceOptions = document.querySelectorAll("#dropdownMenu div") -// API key & URL +// ------ API key & URL ------ // const apiKey = '0cc881e89fc0422eac77c85260da365d' -const URL = `https://api.spoonacular.com/recipes/random?number=12&apiKey=${apiKey}` +const URL = `https://api.spoonacular.com/recipes/random?number=10&apiKey=${apiKey}` -// Global data holders +// ------ Global variables ------ // let allMeals = [] let currentPreference = "" -// Button visual selection +// ------ Buttons ------ // buttonGroups.forEach(group => { group.addEventListener("click", e => { - if (e.target.tagName !== "BUTTON") return + if (e.target.tagName !== "BUTTON") return // to avoid accidental clicks group.querySelectorAll("button").forEach(b => b.classList.remove("selected")) e.target.classList.add("selected") }) }) -// Data from localStorage +// ------ Data from localStorage as backup if API fails ------ // function loadFromLocalStorage() { const storedRecipes = localStorage.getItem("recipes") if (storedRecipes) { @@ -40,29 +40,50 @@ function loadFromLocalStorage() { } -// Fetch data from Spoonacular API +// ----Fetch data from Spoonacular API + local storage + error messages--- // + const fetchData = () => { - fetch(URL) - .then(response => response.json()) + //message when loading recipes + recipeCard.innerHTML = '

Loading recipes...

' + + return fetch(URL) + .then(response => { + if (response.status === 402) { + //error message if api-limit reached + throw new Error("API limit reached 😑") + } + + return response.json() + }) + .then(data => { allMeals = data.recipes console.log("Fetched recipes:", allMeals) - // Save recipes to localStorage + // saves recipes to localStorage for backup localStorage.setItem("recipes", JSON.stringify(allMeals)) - // Filter and display + // filter preferences const filtered = filterByPreference(allMeals) renderMeals(filtered) }) + // error control + messages .catch(error => { console.error('Error fetching recipes:', error) - recipeCard.innerHTML = '

No recipes found. Please try again.

' + + if (error.message === "API limit reached 😑") { + // backup, loading from local storage + if (!loadFromLocalStorage()) { + recipeCard.innerHTML = '

API quota reached and no saved recipes found. Please try again tomorrow 🫡.

' + } + } else { + // something else went wrong + recipeCard.innerHTML = '

Something went wrong- please try again 🫣

' + } }) } - -// Filter recipes on diet/ preference +// ------ Filter recipes on diet/ preference ------// function filterByPreference(meals) { if (!currentPreference || currentPreference === "all") return meals @@ -72,18 +93,18 @@ function filterByPreference(meals) { } -// Recipe cards +// ------ Recipe cards ------ // function renderMeals(meals) { recipeCard.innerHTML = "" - if (meals.length === 0) { - recipeCard.innerHTML = '

No recipes match your filters.

' + if (meals.length === 0) { //no matching recipes - message + recipeCard.innerHTML = '

No recipe match, sorry 🫣

' return } meals.forEach(meal => { const ingredients = meal.extendedIngredients - ? meal.extendedIngredients.slice(0, 4).map(ing => ing.name) + ? meal.extendedIngredients.slice(0, 5).map(ing => ing.name) : [] const cardHTML = ` @@ -103,7 +124,7 @@ function renderMeals(meals) { } -// Filter by kitchen/cuisine +// ------ Filter by kitchen/cuisine ------ // filterButtons.forEach(button => { button.addEventListener("click", () => { filterButtons.forEach(b => b.classList.remove("selected")) @@ -125,7 +146,7 @@ filterButtons.forEach(button => { }) -// Filter by cooking time +// ------ Filter by cooking time ------ // sortButtons.forEach(button => { button.addEventListener("click", () => { sortButtons.forEach(b => b.classList.remove("selected")) @@ -149,58 +170,52 @@ sortButtons.forEach(button => { }) -// Get random recipes -if (randomButton) { - randomButton.addEventListener("click", () => { - filterButtons.forEach(b => b.classList.remove("selected")) - sortButtons.forEach(b => b.classList.remove("selected")) +// ------ Get random recipes ------ // +randomButton.addEventListener("click", () => { + if (allMeals.length === 0) { + recipeCard.innerHTML = '

No recipe match, sorry 🫣

' + return + } - const allFilterBtn = Array.from(filterButtons).find(b => b.textContent.trim() === "All") - if (allFilterBtn) allFilterBtn.classList.add("selected") + filterButtons.forEach(b => b.classList.remove("selected")) + sortButtons.forEach(b => b.classList.remove("selected")) - const allSortBtn = Array.from(sortButtons).find(b => b.textContent.trim().toLowerCase() === "all") - if (allSortBtn) allSortBtn.classList.add("selected") + randomButton.classList.add("selected") - fetchData() - }) -} + const randomMeal = allMeals[Math.floor(Math.random() * allMeals.length)] + renderMeals([randomMeal]) +}) -// Preference dropdown toggle +// ------ Preference: dropdown toggle ------ // function toggleDropdown() { document.getElementById("dropdownMenu").classList.toggle("show") } -// Preference selection +// ------ Preference: dropdown selection ------ // preferenceOptions.forEach(option => { option.addEventListener("click", () => { - currentPreference = option.value.toLowerCase() + preferenceOptions.forEach(o => o.classList.remove("selected")) + option.classList.add("selected") + currentPreference = option.dataset.value.toLowerCase() const filtered = filterByPreference(allMeals) renderMeals(filtered) - toggleDropdown() }) }) -// Close dropdown if clicked outside -window.onclick = function (event) { - if (!event.target.matches('button')) { - let dropdowns = document.getElementsByClassName("preferences-content") - for (let i = 0; i < dropdowns.length; i++) { - let openDropdown = dropdowns[i] - if (openDropdown.classList.contains('show')) { - openDropdown.classList.remove('show') - } - } +// ------ To close dropdown if click anywhere on the page ------ // +window.addEventListener('click', e => { + if (!e.target.closest('.preferences')) { + document.getElementById("dropdownMenu").classList.remove("show") } -} +}) -// Initial page load +// ------ Initial page load ------ // window.onload = () => { - const hasCache = loadFromLocalStorage() - if (!hasCache) { - fetchData() - } + fetchData() } + + diff --git a/style.css b/style.css index b5d17b2b6..d82726b45 100644 --- a/style.css +++ b/style.css @@ -101,7 +101,7 @@ h2 { border-radius: 20px; } -.preferences-content option { +.preferences-content div { padding: 10px 16px; display: block; text-decoration: none; @@ -109,11 +109,13 @@ h2 { border-radius: 50px; } - .show { display: block; } +.preferences-content div.selected { + background-color: #FF6589; +} /*recipe cards*/ @@ -139,6 +141,23 @@ h2 { border-radius: 10px; } +.error { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-style: bold; + font-size: 40px; + color: #0018A4; + text-align: center; +} + +.loading { + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + font-style: bold; + font-size: 40px; + color: #0018A4; + text-align: center; + +} + /* responsive tablet and desktop */ @media (min-width: 667px) { @@ -196,12 +215,12 @@ h2 { color: white; } - /*preference button*/ + /*preference button + content*/ .preferences button:hover { border: 1px solid #FF6589; } - .preferences-content option:hover { + .preferences-content div:hover { background-color: #FF6589; cursor: pointer; } From 216c8d674679b08b34670338e26f6109718a8680 Mon Sep 17 00:00:00 2001 From: Frida Engman Date: Thu, 9 Oct 2025 17:42:18 +0300 Subject: [PATCH 6/7] cleaned up console logs --- README.md | 1 + script.js | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 58f1a8a66..e9fcc3586 100644 --- a/README.md +++ b/README.md @@ -1 +1,2 @@ # js-project-recipe-library +netlify url: https://js-project-recipe-library-frida.netlify.app/ \ No newline at end of file diff --git a/script.js b/script.js index 91b346a0c..4f6d6222a 100644 --- a/script.js +++ b/script.js @@ -29,7 +29,6 @@ buttonGroups.forEach(group => { function loadFromLocalStorage() { const storedRecipes = localStorage.getItem("recipes") if (storedRecipes) { - console.log("Recipes loaded from localStorage") allMeals = JSON.parse(storedRecipes) const filtered = filterByPreference(allMeals) @@ -58,7 +57,6 @@ const fetchData = () => { .then(data => { allMeals = data.recipes - console.log("Fetched recipes:", allMeals) // saves recipes to localStorage for backup localStorage.setItem("recipes", JSON.stringify(allMeals)) @@ -69,16 +67,16 @@ const fetchData = () => { }) // error control + messages .catch(error => { - console.error('Error fetching recipes:', error) + if (error.message === "API limit reached 😑") { // backup, loading from local storage if (!loadFromLocalStorage()) { - recipeCard.innerHTML = '

API quota reached and no saved recipes found. Please try again tomorrow 🫡.

' + recipeCard.innerHTML = '

API quota reached and no saved recipes found.. Please try again tomorrow 🫡.

' } } else { // something else went wrong - recipeCard.innerHTML = '

Something went wrong- please try again 🫣

' + recipeCard.innerHTML = '

Something went wrong, please try again 🫣

' } }) } From 848e4f3b9c7b009690dab5f0f48c85da561651c6 Mon Sep 17 00:00:00 2001 From: Frida Engman Date: Thu, 9 Oct 2025 17:47:44 +0300 Subject: [PATCH 7/7] chaged typo --- script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script.js b/script.js index 4f6d6222a..d4df2a9c9 100644 --- a/script.js +++ b/script.js @@ -72,7 +72,7 @@ const fetchData = () => { if (error.message === "API limit reached 😑") { // backup, loading from local storage if (!loadFromLocalStorage()) { - recipeCard.innerHTML = '

API quota reached and no saved recipes found.. Please try again tomorrow 🫡.

' + recipeCard.innerHTML = '

API quota reached and no saved recipes found.. Please try again tomorrow 🫡

' } } else { // something else went wrong