-
Notifications
You must be signed in to change notification settings - Fork 60
Mikaelas js-project-recipe-library #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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/ |
| 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> | ||
|
|
||
| </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> | ||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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! 👏 |
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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!