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.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# js-project-recipe-library

https://mikaelas-js-project-recipe-library.netlify.app/
Binary file added images/focaccia.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
91 changes: 91 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<!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>

<h1>Recipe Library</h1>

<div class="filter-menu">
<div class="filters">
<div class="filter-box">
<h2>Filter on dish</h2>

<div class="filter-buttons filter-dish" id="filterDishType">
<button class="active">All</button>
<button>Starter</button>
<button>Main Course</button>
<button>Dessert</button>
<button>Side Dish</button>
<button>Lunch</button>
<button>Dinner</button>
</div>
Comment on lines +21 to +28
Copy link

@KausarShangareeva KausarShangareeva Oct 14, 2025

Choose a reason for hiding this comment

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

+ Nice work! The button structure is clear and easy to follow!


</div>

<div class="filter-box">
<h2>Filter on kitchen</h2>

<div class="filter-buttons filter-cuisine" id="filterCuisine">
<button class="active">All</button>
<button>African</button>
<button>Asian</button>
<button>American</button>
<button>Cajun</button>
<button>European</button>
<button>Latin American</button>
<button>Middle Eastern</button>
</div>

<div class="filter-box">
<h2>Gluten free?</h2>

<div class="filter-buttons filter-gluten-free">
<button id="glutenFreeButton">No</button>
</div>

</div>
</div>
</div>


<div>

<div class="sort-box">
<h2>Sort on time</h2>

<div class="sort-button" id="sortRecipe">
<button>Fastest recipes first</button>
<button>Slowest recipes first</button>
</div>

</div>

<div class="random-box">
<h2>Can't decide?</h2>

<div class="random-button">
<button id="randomRecipe">Random recipe</button>
</div>

</div>

</div>
</div>

<section class="card-grid" id="card">

<div class="card" id="loadingCard">Loading...</div>

</section>

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

</html>
257 changes: 257 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
const API_KEY = '0c53a9868a9e4217a3adc272f97c6cf8';
const URL = `https://api.spoonacular.com/recipes/complexSearch?number=60&fillIngredients=true&type=starter,main%20course,dessert,side%20dish,lunch,dinner&cuisine=African,Asian,American,Cajun,European,Latin%20American,Middle%20Eastern&addRecipeInformation=true&apiKey=${API_KEY}`;

const card = document.getElementById('card')
const loadingCard = document.getElementById('loadingCard')
const filterDishButtons = document.querySelectorAll('.filter-dish button');
const filterCuisineButtons = document.querySelectorAll('.filter-cuisine button');
const filterGlutenFreeButton = document.querySelector('.filter-gluten-free button');
const sortButtons = document.querySelectorAll('.sort-button button');
const randomButton =document.querySelector('.random-button button');

const displayRecipes = recipeCard => {
card.innerHTML = ''

if (!recipeCard || recipeCard.length === 0) {
card.innerHTML = `
<div class='card'>
<h3>Inga recept hittades</h3>
<p>Prova att filtrera på något annat.</p>
</div>`;
Comment on lines +17 to +20

Choose a reason for hiding this comment

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

stick to english?

return;}

recipeCard.forEach(recipe => {
card.innerHTML += `
<a target='_blank' href='${recipe.spoonacularSourceUrl}'>
<div class='card'>
<img src='${recipe.image}' alt='${recipe.title}'>

<h3><b>${recipe.title}</b></h3>

<hr>

<p><b>Dish type:</b> ${recipe.dishTypes.join(', ') || 'Unknown Dish Type'}</p>
<p><b>Cuisine:</b> ${recipe.cuisines.join(', ') || 'Unknown Cuisine'}</p>
<p><b>Time:</b> ${recipe.readyInMinutes || 'N/A'} minutes</p>
Comment on lines +33 to +35

Choose a reason for hiding this comment

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

good fallback


<hr>

<h4>Ingredients</h4>

<p class='ingredients-list'>${recipe.extendedIngredients
? recipe.extendedIngredients.map(ingredient => ingredient.original).join(',<br> ')
: 'No ingredients listed'}
</p>
</div>
</a>
`
})
}

const localRecipes = [
{
title: 'Focaccia Bread',
image: 'images/focaccia.jpg',
dishTypes: ['bread', 'side dish'],
cuisines: ['Italian', 'European'],
readyInMinutes: 45,
extendedIngredients: [
{ original: 'flour' },
{ original: 'olive oil' },
{ original: 'yeast' },
{ original: 'salt' }
]
Comment on lines +51 to +63

Choose a reason for hiding this comment

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

The recipe data is well-structured and easy to read. I like how each recipe object is consistent in shape, making it straightforward to work with for filtering, sorting, or displaying. The use of extendedIngredients as an array of objects is a smart choice for flexibility

},
{
title: 'Veggie Tacos',
image: 'images/tacos.jpg',
dishTypes: ['taco', 'main course'],
cuisines: ['Mexican', 'Latin American'],
readyInMinutes: 25,
extendedIngredients: [
{ original: 'tortillas' },
{ original: 'beans' },
{ original: 'avocado' },
{ original: 'lime' }
]
},
{
title: 'Ramen Soup',
image: 'images/ramen.jpg',
dishTypes: ['soup', 'main course'],
cuisines: ['Asian'],
readyInMinutes: 30,
extendedIngredients: [
{ original: 'noodles' },
{ original: 'egg' },
{ original: 'broth' },
{ original: 'spring onions' }
]
},
{
title: 'Pad Thai',
image: 'images/padthai.jpg',
dishTypes: ['main course'],
cuisines: ['Asian'],
readyInMinutes: 30,
extendedIngredients: [
{ original: 'rice noodles' },
{ original: 'shrimp' },
{ original: 'bean sprouts' },
{ original: 'peanuts' },
{ original: 'lime' }
]
},
{
title: 'Caesar Salad',
image: 'images/caesarsalad.jpg',
dishTypes: ['salad', 'side dish'],
cuisines: ['American'],
readyInMinutes: 15,
extendedIngredients: [
{ original: 'romaine lettuce' },
{ original: 'croutons' },
{ original: 'parmesan' },
{ original: 'caesar dressing' }
]
},
{
title: 'Chicken Curry',
image: 'images/chickencurry.jpg',
dishTypes: ['main course'],
cuisines: ['Asian'],
readyInMinutes: 40,
extendedIngredients: [
{ original: 'chicken' },
{ original: 'coconut milk' },
{ original: 'curry paste' },
{ original: 'onion' },
{ original: 'garlic' }
]
}
];

let currentRecipes = []
let currentSortOrder = null;

const simplifyButtonText = text => text.trim().toLowerCase() || '';

const getActiveFilters = (buttons) => {
const active = [...buttons]
.filter(button => button.classList.contains('active'))
.map(button => simplifyButtonText(button.textContent));
if (active.length === 0 || active.includes('all')) return null;
return new Set(active);
}

const applyFilters = () => {
let filteredRecipes = currentRecipes;

const dishTypes = getActiveFilters(filterDishButtons);
const cuisines = getActiveFilters(filterCuisineButtons);
const glutenFree = filterGlutenFreeButton.classList.contains('active');

if (dishTypes) {
filteredRecipes = filteredRecipes.filter(recipe => recipe.dishTypes.some(type => dishTypes.has(simplifyButtonText(type))));
} if (cuisines) {
filteredRecipes = filteredRecipes.filter(recipe => recipe.cuisines.some(cuisine => cuisines.has(simplifyButtonText(cuisine))));
} if (glutenFree) {
filteredRecipes = filteredRecipes.filter(recipe => recipe.glutenFree);
} if (currentSortOrder === 'fast') {
filteredRecipes = filteredRecipes.sort((a, b) => a.readyInMinutes - b.readyInMinutes);
} else if (currentSortOrder === 'slow') {
filteredRecipes = filteredRecipes.sort((a, b) => b.readyInMinutes - a.readyInMinutes);
}
Comment on lines +154 to +164

Choose a reason for hiding this comment

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

Great work here! 👏 I like how clearly the filtering logic is structured, and the chaining of conditions makes it easy to follow. The code is readable and straightforward, which is always a big plus. For the sorting section, maybe extract the comparator into a small helper function to avoid repeating readyInMinutes logic. Not critical, but it could help if you add more sort options later

Nice job keeping it concise! ✅

displayRecipes(filteredRecipes);
}

const handleError = error => {
alert("The API request limit has been reached. But don't worry, you can still view some local recipes!");
currentRecipes = localRecipes;
displayRecipes(localRecipes);
}
Comment on lines +168 to +172

Choose a reason for hiding this comment

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


fetch(URL)
.then(response => response.json())
.then(data => {
currentRecipes = data.results;
displayRecipes(currentRecipes);
})
.catch(handleError);

filterDishButtons.forEach(button => {
button.addEventListener('click', () => {
const isAll = button.textContent.trim().toLowerCase() === 'all';

if (isAll) {
filterDishButtons.forEach(button => button.classList.remove('active'));
button.classList.add('active');
applyFilters();
} else {
const allButton = Array.from(filterDishButtons).find(button => button.textContent.trim().toLowerCase() === 'all');
if (allButton) allButton.classList.remove('active');

button.classList.toggle('active');

const anyActive = Array.from(filterDishButtons).some(button => button.classList.contains('active'));
if (!anyActive && allButton) {
allButton.classList.add('active');
}
applyFilters();
}
});
});

filterCuisineButtons.forEach(button => {
button.addEventListener('click', () => {
const isAll = button.textContent.trim().toLowerCase() === 'all';

if (isAll) {
filterCuisineButtons.forEach(button => button.classList.remove('active'));
button.classList.add('active');
applyFilters();
} else {
const allButton = Array.from(filterCuisineButtons).find(button => button.textContent.trim().toLowerCase() === 'all');
if (allButton) allButton.classList.remove('active');

button.classList.toggle('active');

const anyActive = Array.from(filterCuisineButtons).some(button => button.classList.contains('active'));
if (!anyActive && allButton) {
allButton.classList.add('active');
}
applyFilters();
}
});
});

filterGlutenFreeButton.addEventListener('click', () => {
filterGlutenFreeButton.classList.toggle('active');
filterGlutenFreeButton.textContent = filterGlutenFreeButton.classList.contains('active') ? 'Yes' : 'No';
applyFilters();
});

sortButtons.forEach(button => {
button.addEventListener('click', () => {
sortButtons.forEach(button => button.classList.remove('active'));
button.classList.add('active');

const sortOrder = button.textContent.trim().toLowerCase();

currentSortOrder = sortOrder.includes('fastest') ? 'fast' : 'slow';

applyFilters();
});
});
Comment on lines +234 to +245

Choose a reason for hiding this comment

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

The event handling is clear, and I like how the active class is managed—makes the UI state obvious and consistent. The trim().toLowerCase() approach is a nice touch to avoid issues with whitespace or capitalization )


randomButton.addEventListener('click', () => {
randomButton.classList.toggle('active');
randomButton.textContent = randomButton.classList.contains('active') ? 'Back to All' : 'Random recipe';
const randomRecipe = currentRecipes[Math.floor(Math.random() * currentRecipes.length)];

if (randomButton.classList.contains('active')) {
displayRecipes([randomRecipe]);
} else {
displayRecipes(currentRecipes);
}
});
Comment on lines +247 to +257

Choose a reason for hiding this comment

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

The toggle logic is clear, and I like how the button text updates dynamically based on its state—it makes the UX intuitive. Using Math.floor(Math.random() * currentRecipes.length) is a straightforward way to pick a random recipe, and it’s easy to follow. Great job! 👏

Loading