-
Notifications
You must be signed in to change notification settings - Fork 32
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
recipe library #26
base: main
Are you sure you want to change the base?
recipe library #26
Changes from all commits
4aca285
fa426b4
726a0e4
3d422b2
98ee5de
0587009
ea626fe
43ec1f8
ecc9627
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 |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"liveServer.settings.port": 5503 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,8 @@ | ||
# js-project-recipe-library | ||
|
||
To do: | ||
[] Put filter and sort in a form | ||
[]Show a message if the API quota is reached. | ||
[] Use local storage caching to reduce API requests. | ||
[] Show a loading state while fetching data. | ||
[] Fix the css |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
<title>Recipe Library</title> | ||
<link rel="stylesheet" href="./style.css"> | ||
<link rel="preconnect" href="https://fonts.googleapis.com"> | ||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&family=Playfair+Display:ital,wght@0,400..900;1,400..900&display=swap" rel="stylesheet"> | ||
</head> | ||
|
||
<body> | ||
<header> | ||
<h1>Recipe Library</h1> | ||
</header> | ||
|
||
<main> | ||
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. Would this be considered "MAIN". I don't know the answer just wondering form my own sake. I put mine in the header for some reason. |
||
<section class="filter-container" id="filter-container"> | ||
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. I don't really know why really but they said that it is best practice to put sorting and filtering in forms. |
||
<form class="recipe-filter-form" id="recipe-filter-form"> | ||
<div class="filer-on-diet" id="filter-on-diet"> | ||
<h2>Filter on diet</h2> | ||
<select id="diet-filter" name="diet"> | ||
<option value="">All Diets</option> | ||
<option value="vegetarian">Vegetarian</option> | ||
<option value="vegan">Vegan</option> | ||
</select> | ||
</div> | ||
|
||
<div class="sort-recipes-ingredients" id="sort-recipes-ingredients"> | ||
<h2>Sort on ingredients</h2> | ||
<div class="sort-options" id="sort-options-ingredients"> | ||
<button type="button" class="sort-btn-ingredients" id="btn-descending-ingredients">Descending</button> | ||
<button type="button" class="sort-btn-ingredients" id="btn-ascending-ingredients">Ascending</button> | ||
</div> | ||
</div> | ||
|
||
<div class="sort-recipes" id="sort-recipes"> | ||
<h2>Sort on time</h2> | ||
<div class="sort-options" id="sort-options-time"> | ||
<button type="button" class="sort-btn" id="btn-descending">Descending</button> | ||
<button type="button" class="sort-btn" id="btn-ascending">Ascending</button> | ||
</div> | ||
</div> | ||
|
||
<div class="random-recipe"> | ||
<h2>What should I eat today?</h2> | ||
<button type="button" class="random-btn" id="btn-random">Get recipe</button> | ||
</div> | ||
</form> | ||
</section> | ||
|
||
<section class="recipes-container" id="recipes-container"> | ||
</section> | ||
</main> | ||
|
||
<script src="./script.js"></script> | ||
|
||
</div> | ||
</body> | ||
</html> |
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. Super clean, easy to read and great end results with the JS! I have nothing I can find that could be improved🙌 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
const recipesContainer = document.getElementById('recipes-container') | ||
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. I really liked how you put all your DOM Elements at the top. Mine is scattered across all my different functions but I think this is nicer and I also think you are doing it the correct way! |
||
const recipeForm = document.getElementById('recipe-filter-form') | ||
const dietFilter = document.getElementById('diet-filter') | ||
const btnAscendingIngredients = document.getElementById('btn-ascending-ingredients') | ||
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. I like your naming. I always put the name before the "btn" but I am once again thinking that your way is the superior way! |
||
const btnDescendingIngredients = document.getElementById('btn-descending-ingredients') | ||
const btnDescending = document.getElementById('btn-descending') | ||
const btnAscending = document.getElementById('btn-ascending') | ||
const btnRandom = document.getElementById('btn-random') | ||
const CACHE_KEY = 'spoonacular_recipes_cache' | ||
const CACHE_EXPIRY = 24 * 60 * 60 * 1000 | ||
|
||
const baseUrl = "https://api.spoonacular.com/recipes/random" | ||
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 that you divided it up the url for easier management. |
||
const apiKey = "110e75fc870c4091a4fd4bf706e6efc8" | ||
const numRecipes = 50 | ||
const url = `${baseUrl}?apiKey=${apiKey}&number=${numRecipes}` | ||
|
||
let allRecipes = [] | ||
let currentRecipes = [] | ||
let lastSortAction = [] | ||
|
||
const displayError = (message) => { | ||
recipesContainer.innerHTML = `<p class="error-message">${message}</p>` | ||
} | ||
|
||
const displayLoading = () => { | ||
recipesContainer.innerHTML = `<p class="loading-message">Loading recipes... Please wait.</p>` | ||
} | ||
|
||
const createRecipeCard = (recipe) => { | ||
const ingredientListItems = recipe.extendedIngredients | ||
.map(ingredient => `<li>${ingredient.original}</li>`) | ||
.join('') | ||
return ` | ||
<div class="recipe-card" id="${recipe.id}"> | ||
<img src="${recipe.image}" alt="${recipe.title}" class="recipe-img"> | ||
<h2>${recipe.title}</h2> | ||
<hr> | ||
<p class="time-info"><span class="bold">Time:</span> ${recipe.readyInMinutes} minutes</p> | ||
<hr> | ||
<div class="card-buttons"> | ||
<div class ="ingredient-list"> | ||
<button class="ingredient-btn" onclick="toggleIngredients('ingredients-${recipe.id}')">Ingredients</button> | ||
<ul id="ingredients-${recipe.id}" class="hidden-ingredients">${ingredientListItems}</ul> | ||
</div> | ||
<button class="instructions-btn" onclick="showInstructions(${recipe.id})">Show Instructions</button> | ||
</div> | ||
</div>` | ||
} | ||
|
||
const displayRecipes = (recipes) => { | ||
recipesContainer.innerHTML = recipes.map(createRecipeCard).join('') | ||
} | ||
|
||
const toggleIngredients = (ingredientsId) => { | ||
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. So great that you got that to work so great! I am a bit jealous of this function. |
||
const ingredientsList = document.getElementById(ingredientsId) | ||
if (ingredientsList.style.display === 'none' || ingredientsList.style.display === '') { | ||
ingredientsList.style.display = 'block' | ||
} else { | ||
ingredientsList.style.display = 'none' | ||
} | ||
} | ||
|
||
const showInstructions = (recipeId) => { | ||
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. Also this one! Awesome work! |
||
const recipe = allRecipes.find(r => r.id === recipeId) | ||
|
||
if (!recipe) { | ||
displayError('Instructions not found. Please try again.') | ||
return | ||
} | ||
const recipeCard = document.getElementById(recipeId) | ||
const backButton = `<button class="back-button" onclick="displayRecipes(currentRecipes)">Back</button>` | ||
|
||
if (recipe.instructions) { | ||
recipeCard.innerHTML = `<h3>Instructions:</h3><p>${recipe.instructions}</p>${backButton}` | ||
} else { | ||
recipeCard.innerHTML = `<p class="instructions-list">Instructions not available.</p>${backButton}` | ||
} | ||
} | ||
|
||
const filterRecipesByDiet = (diet) => { | ||
if (!diet) { | ||
return allRecipes | ||
} | ||
return allRecipes.filter(recipe => recipe.hasOwnProperty(diet) && recipe[diet] === true) | ||
} | ||
|
||
const sortRecipes = (recipes, key, order = 'asc') => { | ||
if (!recipes || recipes.length === 0) { | ||
return [] | ||
} | ||
const sortedRecipes = [...recipes] | ||
|
||
sortedRecipes.sort((a, b) => { | ||
const valueA = key === 'extendedIngredients' ? a[key].length : a[key] | ||
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. I like how you used a ternary operator to make it a bit more compact |
||
const valueB = key === 'extendedIngredients' ? b[key].length : b[key] | ||
|
||
if (order === 'asc') { | ||
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. Could you have used a ternary operator here aswell? |
||
return valueA - valueB | ||
} else { | ||
return valueB - valueA | ||
} | ||
}) | ||
return sortedRecipes | ||
} | ||
|
||
const showRandomRecipe = () => { | ||
if(allRecipes.length === 0){ | ||
displayError('No recipes found. Please try again.') | ||
return | ||
} | ||
const randomIndex = Math.floor(Math.random() * allRecipes.length) | ||
const randomRecipe = allRecipes[randomIndex] | ||
currentRecipes = [randomRecipe] | ||
displayRecipes(currentRecipes) | ||
} | ||
|
||
const applyFilterAndSort = () => { | ||
const diet = dietFilter.value | ||
|
||
if (!diet) { | ||
currentRecipes = [...allRecipes] | ||
} | ||
|
||
let filteredRecipes = filterRecipesByDiet(diet) | ||
|
||
if (filteredRecipes.length === 0) { | ||
displayError('No recipes found matching the selected diet. Please try again.') | ||
currentRecipes = [] | ||
return | ||
} | ||
if (lastSortAction === 'ingredients-asc') { | ||
filteredRecipes = sortRecipes(filteredRecipes, 'extendedIngredients', 'asc') | ||
} else if (lastSortAction === 'ingredients-desc') { | ||
filteredRecipes = sortRecipes(filteredRecipes, 'extendedIngredients', 'desc') | ||
} else if (lastSortAction === 'time-asc') { | ||
filteredRecipes = sortRecipes(filteredRecipes, 'readyInMinutes', 'asc') | ||
} else if (lastSortAction === 'time-desc') { | ||
filteredRecipes = sortRecipes(filteredRecipes, 'readyInMinutes', 'desc') | ||
} | ||
currentRecipes = filteredRecipes | ||
displayRecipes(currentRecipes) | ||
} | ||
|
||
const saveToCache = (data) => { | ||
const cacheData = { | ||
timestamp: new Date().getTime(), | ||
recipes: data.recipes | ||
} | ||
localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData)) | ||
} | ||
|
||
const getFromCache = () => { | ||
const cachedData = localStorage.getItem(CACHE_KEY) | ||
if (!cachedData) return null | ||
|
||
const parsedData = JSON.parse(cachedData) | ||
const now = new Date().getTime() | ||
|
||
if (now - parsedData.timestamp > CACHE_EXPIRY) { | ||
localStorage.removeItem(CACHE_KEY) | ||
return null | ||
} | ||
|
||
return parsedData | ||
} | ||
|
||
|
||
dietFilter.addEventListener('change', applyFilterAndSort) | ||
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. Nice structure again with the eventlisteners at the end! |
||
|
||
btnAscendingIngredients.addEventListener('click', () => { | ||
lastSortAction = 'ingredients-asc' | ||
applyFilterAndSort() | ||
}) | ||
|
||
btnDescendingIngredients.addEventListener('click', () => { | ||
lastSortAction = 'ingredients-desc' | ||
applyFilterAndSort() | ||
}) | ||
|
||
btnAscending.addEventListener('click', () => { | ||
lastSortAction = 'time-asc' | ||
applyFilterAndSort() | ||
}) | ||
|
||
btnDescending.addEventListener('click', () => { | ||
lastSortAction = 'time-desc' | ||
applyFilterAndSort() | ||
}) | ||
|
||
btnRandom.addEventListener('click', showRandomRecipe) | ||
|
||
const fetchRecipes = () => { | ||
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. Is it a good idé to have this in the end. Probably is I just don't know. You have done everythings else super great so I am once again thinking you did it the correct way. |
||
|
||
displayLoading () | ||
|
||
const cachedData = getFromCache() | ||
if (cachedData) { | ||
allRecipes = cachedData.recipes | ||
currentRecipes = cachedData.recipes | ||
displayRecipes(currentRecipes) | ||
return | ||
} | ||
|
||
fetch (url) | ||
.then(response => { | ||
if (!response.ok) { | ||
throw new Error(`HTTP error! Status: ${error.message}`) | ||
} | ||
return response.json() | ||
}) | ||
.then ((data) => { | ||
allRecipes = data.recipes | ||
currentRecipes = data.recipes | ||
displayRecipes(currentRecipes) | ||
|
||
saveToCache(data) | ||
|
||
}) | ||
.catch(error => { | ||
if (error.message.includes('quota')) { | ||
displayError(`${error.message} You've reached the limit for new recipes today.`) | ||
} else { | ||
displayError(`Error fetching data: ${error.message}`) | ||
} | ||
}) | ||
} | ||
|
||
fetchRecipes() |
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.
More or less flawless html! Clean code, good naming and nothing unnecessary. Great work.
The only thing worth think about in the future is that it might be a good Idé to put sorting and filtering in forms