diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..68b32ea5e Binary files /dev/null and b/.DS_Store differ 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/index.html b/index.html new file mode 100644 index 000000000..96776501f --- /dev/null +++ b/index.html @@ -0,0 +1,69 @@ + + + + + + + + Recipe Library + + + + + +
+

Recipe Library

+
+ +
+ + +
+

Filter on kitchen

+ + + + + +
+ +
+

Sort on time

+ + +
+ +
+

Preferences

+ + + +
+ + +
+

Surprise me!

+ +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 000000000..d4df2a9c9 --- /dev/null +++ b/script.js @@ -0,0 +1,219 @@ +// ------ 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 div") + +// ------ API key & URL ------ // +const apiKey = '0cc881e89fc0422eac77c85260da365d' +const URL = `https://api.spoonacular.com/recipes/random?number=10&apiKey=${apiKey}` + +// ------ Global variables ------ // +let allMeals = [] +let currentPreference = "" + + +// ------ Buttons ------ // +buttonGroups.forEach(group => { + group.addEventListener("click", e => { + 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 as backup if API fails ------ // +function loadFromLocalStorage() { + const storedRecipes = localStorage.getItem("recipes") + if (storedRecipes) { + allMeals = JSON.parse(storedRecipes) + + const filtered = filterByPreference(allMeals) + renderMeals(filtered) + return true + } + return false +} + + +// ----Fetch data from Spoonacular API + local storage + error messages--- // + +const fetchData = () => { + //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 + + // saves recipes to localStorage for backup + localStorage.setItem("recipes", JSON.stringify(allMeals)) + + // filter preferences + const filtered = filterByPreference(allMeals) + renderMeals(filtered) + }) + // error control + messages + .catch(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 🫡

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

Something went wrong, 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) + ) +} + + +// ------ Recipe cards ------ // +function renderMeals(meals) { + recipeCard.innerHTML = "" + + 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, 5).map(ing => ing.name) + : [] + + const cardHTML = ` +
+ ${meal.title} +

${meal.title}

+

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

+

Ready in: ${meal.readyInMinutes} min

+

Ingredients:

+ +
+ ` + recipeCard.insertAdjacentHTML('beforeend', cardHTML) + }) +} + + +// ------ Filter by kitchen/cuisine ------ // +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") { + const filtered = filterByPreference(allMeals) + renderMeals(filtered) + } else { + const filteredByCuisine = allMeals.filter(meal => + meal.cuisines && meal.cuisines.includes(filter) + ) + const finalFiltered = filterByPreference(filteredByCuisine) + renderMeals(finalFiltered) + } + }) +}) + + +// ------ Filter by cooking time ------ // +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) + const filtered = filterByPreference(quickMeals) + renderMeals(filtered) + } else if (sortType === "slow cook's") { + const slowMeals = allMeals.filter(meal => meal.readyInMinutes > 20) + const filtered = filterByPreference(slowMeals) + renderMeals(filtered) + } else { + const filtered = filterByPreference(allMeals) + renderMeals(filtered) + } + }) +}) + + +// ------ Get random recipes ------ // +randomButton.addEventListener("click", () => { + if (allMeals.length === 0) { + recipeCard.innerHTML = '

No recipe match, sorry 🫣

' + return + } + + filterButtons.forEach(b => b.classList.remove("selected")) + sortButtons.forEach(b => b.classList.remove("selected")) + + randomButton.classList.add("selected") + + const randomMeal = allMeals[Math.floor(Math.random() * allMeals.length)] + renderMeals([randomMeal]) +}) + + +// ------ Preference: dropdown toggle ------ // +function toggleDropdown() { + document.getElementById("dropdownMenu").classList.toggle("show") +} + + +// ------ Preference: dropdown selection ------ // +preferenceOptions.forEach(option => { + option.addEventListener("click", () => { + preferenceOptions.forEach(o => o.classList.remove("selected")) + option.classList.add("selected") + currentPreference = option.dataset.value.toLowerCase() + const filtered = filterByPreference(allMeals) + renderMeals(filtered) + }) +}) + + +// ------ 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 ------ // +window.onload = () => { + fetchData() +} + + diff --git a/style.css b/style.css new file mode 100644 index 000000000..d82726b45 --- /dev/null +++ b/style.css @@ -0,0 +1,244 @@ +/* mobile first*/ +body { + background: #FAFBFF; + +} + +h1 { + 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; + margin-top: 64px; + +} + +.grid-parent { + display: grid; + grid-template-columns: 1fr; + gap: 20px; + margin-top: 64px; +} + +/* filter and sorting section */ +h2 { + 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 { + 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 { + grid-row: 2; + /*for the lay out in mobile first*/ +} + +.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; +} + +.random-button button { + background-color: #0018A4; + color: #FFECEA; + border: none; + cursor: pointer; + border-radius: 50px; + padding: 8px 16px 8px 16px; + gap: 10px; +} + +.random-button button.selected { + background-color: #CCFFE2; + color: #FF6589; +} + +.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 div { + padding: 10px 16px; + display: block; + text-decoration: none; + color: #0018A4; + border-radius: 50px; +} + +.show { + display: block; +} + +.preferences-content div.selected { + background-color: #FF6589; +} + + +/*recipe cards*/ + +#recipe-card { + margin-left: 13%; +} + +.card { + width: 200px; + height: auto; + display: inline-block; + border-radius: 16px; + border: 1px solid #E9E9E9; + padding: 5px; + margin-top: 40px; +} + +.card img { + width: 100%; + height: 150px; + object-fit: cover; + 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) { + + body { + margin-left: 64px; + margin-right: 64px; + } + + /* layout desktop */ + + .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 1; + grid-row: auto; + } + + .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; + grid-row: span 2; + } + + /*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; + } + + /*preference button + content*/ + .preferences button:hover { + border: 1px solid #FF6589; + } + + .preferences-content div:hover { + background-color: #FF6589; + cursor: pointer; + } + + /*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; + } + +} \ No newline at end of file