Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# js-project-recipe-library
netlify url: https://js-project-recipe-library-frida.netlify.app/
69 changes: 69 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<link
rel="stylesheet"
href="style.css"
>
<title>Recipe Library</title>
</head>

<body>


<div class="header">
<h1>Recipe Library</h1>
</div>
Comment on lines +20 to +22

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use the

instead of div if it is a header


<section class="grid-parent">


<div class="filter-and-buttons">
<h2> Filter on kitchen </h2>
<button> All </button>
<button> Vietnamese </button>
<button> American </button>
<button> Mediterranean </button>

</div>

<div class="sorting-and-buttons">
<h2> Sort on time</h2>
<button> Quick meals </button>
<button> Slow cook's </button>
</div>

<div class="preferences">
<h2> Preferences </h2>
<button onclick="toggleDropdown()"> Choose here</button>
<div
id="dropdownMenu"
class="preferences-content"
>
<div data-value="Gluten Free">Gluten Free</div>
<div data-value="Vegetarian">Vegetarian</div>
<div data-value="Vegan">Vegan</div>
</div>

</div>


<div class="random-button">
<h2> Surprise me!</h2>
<button> Random </button>
</div>
</section>

<section id=recipe-card>
</section>

<script src="script.js"></script>
</body>

</html>
219 changes: 219 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
@@ -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")
Comment on lines +22 to +23

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure but maybe it could work to use .toggle("className") here

})
})


// ------ Data from localStorage as backup if API fails ------ //
function loadFromLocalStorage() {
const storedRecipes = localStorage.getItem("recipes")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = '<p class="loading">Loading recipes...</p>'

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 = '<p class="error">API quota reached and no saved recipes found.. Please try again tomorrow 🫡</p>'
}
} else {
// something else went wrong
recipeCard.innerHTML = '<p class="error"> Something went wrong, please try again 🫣</p>'
}
})
}

// ------ 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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stick to either arrow functions like youve done in
const fetchData = () => {...}
or
function renderMeals(meals) {...}

dont mix

recipeCard.innerHTML = ""

if (meals.length === 0) { //no matching recipes - message
recipeCard.innerHTML = '<p class="error">No recipe match, sorry 🫣</p>'
Comment on lines +98 to +99

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good fallback!

return
}

meals.forEach(meal => {
const ingredients = meal.extendedIngredients
? meal.extendedIngredients.slice(0, 5).map(ing => ing.name)
: []

const cardHTML = `
<div class="card">
<img src="${meal.image}" alt="${meal.title}">
<h3>${meal.title}</h3>
<p><strong>Cuisine:</strong> ${meal.cuisines?.[0] || 'Unknown'}</p>
<p><strong>Ready in:</strong> ${meal.readyInMinutes} min</p>
<p><strong>Ingredients:</strong></p>
<ul>
${ingredients.map(ing => `<li>${ing}</li>`).join('')}
</ul>
</div>
`
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 = '<p class="error">No recipe match, sorry 🫣</p>'
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()
}


Loading