From 4aca285b7edb6663414f1e9db074c8d540e9d889 Mon Sep 17 00:00:00 2001 From: Ida Hellgren Date: Thu, 13 Mar 2025 15:13:45 +0100 Subject: [PATCH 1/9] Added: css-file, html-file, javascript-file --- .vscode/settings.json | 3 + index.html | 57 ++++++++++++++ script.js | 172 ++++++++++++++++++++++++++++++++++++++++++ style.css | 159 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 391 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 index.html create mode 100644 script.js create mode 100644 style.css diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..155422b0 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5503 +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 00000000..0fcc7525 --- /dev/null +++ b/index.html @@ -0,0 +1,57 @@ + + + + + + Recipe Library + + + + + + + +
+

Recipe Library

+
+ +
+
+
+

Filter on diet

+ +
+ +
+

Sort on ingredients

+
+ + +
+
+
+

Sort on time

+
+ + +
+
+
+

What should I eat today?

+ +
+
+ +
+
+
+ + + + + + \ No newline at end of file diff --git a/script.js b/script.js new file mode 100644 index 00000000..905b089e --- /dev/null +++ b/script.js @@ -0,0 +1,172 @@ +const recipesContainer = document.getElementById('recipes-container') +const dietFilter = document.getElementById('diet-filter') +const btnAscendingIngredients = document.getElementById('btn-ascending-ingredients') +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 baseUrl = "https://api.spoonacular.com/recipes/random" +const apiKey = "110e75fc870c4091a4fd4bf706e6efc8" +const url = `${baseUrl}/?apiKey=${apiKey}&number=50` + +let allRecipes = [] + +const fetchRecipes = () => { + fetch (url) + .then((response) => response.json ()) + .then ((data) => { + console.log(data) + allRecipes = data.recipes + displayRecipes(allRecipes) + + }) + .catch(error => { + console.error ('Error fetching data', error) + }) +} + +const displayRecipes = (allRecipes) => { + recipesContainer.innerHTML = '' + + allRecipes.forEach(recipe => { + console.log(recipe) + let ingredientListItems = '' + recipe.extendedIngredients.forEach(ingredient => { + ingredientListItems += `
  • ${ingredient.name}
  • ` + }) + + recipesContainer.innerHTML += `
    + ${recipe.title} +

    ${recipe.title}

    +
    +

    Time: ${recipe.readyInMinutes} minutes

    +
    +
    +

    Ingredients:

    +
      ${ingredientListItems}
    +
    + +
    ` +}) +} + +function toggleIngredients(ingredientsId) { + 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) => { + const recipe = allRecipes.find(r => r.id === recipeId) + + if (recipe) { + const recipeCard = document.getElementById(recipeId) + + if (recipe.instructions) { + recipeCard.innerHTML = `

    Instructions:

    ${recipe.instructions}

    + ` + } else { + recipeCard.innerHTML = `

    Instructions not available.

    + ` + } + } +} + +const filterRecipesByDiet = (diet) => { + let filteredRecipes + if (diet === '') { + filteredRecipes = allRecipes + } else if (diet === 'vegetarian') { + filteredRecipes = allRecipes.filter(recipe => recipe.vegetarian === true) + } else if (diet === 'vegan') { + filteredRecipes = allRecipes.filter(recipe => recipe.vegan === true) + } + + if (filteredRecipes.length === 0) { + recipesContainer.innerHTML = `

    No recipes found. Try another filter.

    `; + } else { + return filteredRecipes; + } +} + +const sortOnIngredients = (recipes, order) => { + let sortedRecipesIngredients = [...recipes] + + sortedRecipesIngredients.sort ((a, b) => { + if (order === 'asc') { + return a.extendedIngredients.length - b.extendedIngredients.length + } else { + return b.extendedIngredients.length - a.extendedIngredients.length + } + }) + + displayRecipes(sortedRecipesIngredients) +} + + +const sortOnTime = (recipes, order) => { + let sortedRecipesTime = [...recipes] + sortedRecipesTime.sort((a, b) => { + if (order === 'asc') { + return a.readyInMinutes - b.readyInMinutes + } else { + return b.readyInMinutes - a.readyInMinutes + } + }) + displayRecipes(sortedRecipesTime) +} + + +const showRandomRecipe = () => { + if(allRecipes.length === 0) return + const randomIndex = Math.floor(Math.random() * allRecipes.length) + const randomRecipe = allRecipes[randomIndex] + displayRecipes([randomRecipe]) +} + +dietFilter.addEventListener('change', () => { + const diet = dietFilter.value + const filteredRecipes = filterRecipesByDiet(diet) + if (filteredRecipes) { + displayRecipes (filteredRecipes) + } +}) + +btnAscendingIngredients.addEventListener('click', () => { + const diet = dietFilter.value + const filteredRecipes = filterRecipesByDiet (diet) + if (filteredRecipes) { + sortOnIngredients(filteredRecipes, 'asc') + } +}) + +btnDescendingIngredients.addEventListener('click', () => { + const diet = dietFilter.value + const filteredRecipes = filterRecipesByDiet (diet) + if (filteredRecipes) { + sortOnIngredients(filteredRecipes, 'desc') + } +}) + +btnAscending.addEventListener('click', () => { + const diet = dietFilter.value; + const filteredRecipes = filterRecipesByDiet(diet); + if(filteredRecipes){ + sortOnTime(filteredRecipes, 'asc'); + } +}) + +btnDescending.addEventListener('click', () => { + const diet = dietFilter.value; + const filteredRecipes = filterRecipesByDiet(diet); + if(filteredRecipes){ + sortOnTime(filteredRecipes, 'desc'); + } +}) + +btnRandom.addEventListener('click', showRandomRecipe) + +fetchRecipes() \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 00000000..4b671cb0 --- /dev/null +++ b/style.css @@ -0,0 +1,159 @@ +/* Base styles */ +* { + margin: 0; + padding: 10; + box-sizing: border-box; + font-family: "Montserrat", sans-serif; +} + +body { + background-color: #fafbff; + margin: 10px; +} + +header { + margin-bottom: 30px; + margin-top: 30px; +} + +h1 { + color: #0018A4; + font-size: 64px; + font-weight: 700; + margin-left: 10px; +} + +h2 { + font-size: 18px; + font-weight: 700; + margin-bottom: 16px; + margin-top: 16px; +} + +p { + font-size: 16px; + font-weight: 400; +} + +.cuisine-info { + margin-top: 5px; + margin-bottom: 5px; +} + +.time-info { + margin-bottom: 5px; +} + +.bold { + font-weight: 700; +} + +.ingredients{ + font-weight: 700; + margin-top: 5px; +} + + +.filter-container { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 30px; + margin-left: 10px; +} + +button { + padding: 8px 16px; + border: none; + border-radius: 50px; + border: 2px solid transparent; + font-weight: 500; + font-size: 16px; + color: #0018A4; + cursor: pointer; + transition: all 0.3s ease; +} + +button:hover { + border: 2px solid #0018A4; + } + +.sort-btn { + background-color: #FFECEA; + } + +.random-btn { + background-color: #CCFFE2; +} + +.recipes-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); + gap: 16px; +} + +.recipe-card { + display: flex; + flex-direction: column; + background-color: white; + border: 1px solid grey; + border-radius: 10px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + padding: 16px; + width: 290px; + justify-self: center; + transition: ease-in-out 0.3s; +} + +.recipe-card:hover { + transform: translateY(-5px); +} + +.recipe-img { + width: 100%; + height: 200px; + object-fit: cover; +} + +ul { + list-style: none; + padding: 0; + margin-bottom: 5px; + margin-top: 5px; +} + +li { + line-height: 25px; +} + +.instructions-btn { +margin-top: auto; +} + +select { + padding: 8px 16px; + border: none; + border-radius: 50px; + border: 2px solid transparent; + font-weight: 500; + font-size: 16px; + color: #0018A4; + background-color: rgb(209, 243, 255); + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + } + + select:focus { + outline: none; + } + + select:hover { + outline: none; + border: 2px solid #0018A4; + } + + .hidden-ingredients { + display: none; +} + \ No newline at end of file From fa426b447b6229b1f68f3f5bc340d8d828993a30 Mon Sep 17 00:00:00 2001 From: Ida Hellgren Date: Thu, 13 Mar 2025 15:53:16 +0100 Subject: [PATCH 2/9] Changed the ingredient toggle to a button --- script.js | 2 +- style.css | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/script.js b/script.js index 905b089e..1ca8143c 100644 --- a/script.js +++ b/script.js @@ -42,7 +42,7 @@ const displayRecipes = (allRecipes) => {

    Time: ${recipe.readyInMinutes} minutes


    -

    Ingredients:

    +
      ${ingredientListItems}
    diff --git a/style.css b/style.css index 4b671cb0..7ec39ac4 100644 --- a/style.css +++ b/style.css @@ -35,6 +35,11 @@ p { font-weight: 400; } +.ingredient-btn{ + margin-top: 10px; + margin-bottom: 10px; +} + .cuisine-info { margin-top: 5px; margin-bottom: 5px; From 726a0e4d29ccd9422351ab8230b60c23b8cac264 Mon Sep 17 00:00:00 2001 From: Ida Hellgren Date: Fri, 14 Mar 2025 16:22:43 +0100 Subject: [PATCH 3/9] css changes and error-handling --- README.md | 10 +++++ script.js | 15 +++++-- style.css | 126 ++++++++++++++++++++++++++++++++---------------------- 3 files changed, 95 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 58f1a8a6..598acb66 100644 --- a/README.md +++ b/README.md @@ -1 +1,11 @@ # js-project-recipe-library + +To do: +[] Put filter and sort in a form +[]Display empty state if no matching recipes. +[]Show a message if the API quota is reached. +[] Update DOM when filter/sorting changes. +[] Use local storage caching to reduce API requests. +[] Show a loading state while fetching data. +[] Fix the css +[] fix filter in sort btns \ No newline at end of file diff --git a/script.js b/script.js index 1ca8143c..aed7a0ef 100644 --- a/script.js +++ b/script.js @@ -5,12 +5,18 @@ const btnDescendingIngredients = document.getElementById('btn-descending-ingredi const btnDescending = document.getElementById('btn-descending') const btnAscending = document.getElementById('btn-ascending') const btnRandom = document.getElementById('btn-random') + const baseUrl = "https://api.spoonacular.com/recipes/random" const apiKey = "110e75fc870c4091a4fd4bf706e6efc8" -const url = `${baseUrl}/?apiKey=${apiKey}&number=50` +const numRecipes = 50; +const url = `${baseUrl}/?apiKey=${apiKey}&number=${numRecipes}` let allRecipes = [] +const displayError = (message) => { + recipesContainer.innerHTML = `

    ${message}

    ` +} + const fetchRecipes = () => { fetch (url) .then((response) => response.json ()) @@ -21,7 +27,7 @@ const fetchRecipes = () => { }) .catch(error => { - console.error ('Error fetching data', error) + alert ('Error fetching data', error) }) } @@ -29,7 +35,6 @@ const displayRecipes = (allRecipes) => { recipesContainer.innerHTML = '' allRecipes.forEach(recipe => { - console.log(recipe) let ingredientListItems = '' recipe.extendedIngredients.forEach(ingredient => { ingredientListItems += `
  • ${ingredient.name}
  • ` @@ -41,16 +46,18 @@ const displayRecipes = (allRecipes) => {

    Time: ${recipe.readyInMinutes} minutes


    +
      ${ingredientListItems}
    +
    ` }) } -function toggleIngredients(ingredientsId) { +const toggleIngredients = (ingredientsId) => { const ingredientsList = document.getElementById(ingredientsId); if (ingredientsList.style.display === 'none' || ingredientsList.style.display === '') { ingredientsList.style.display = 'block'; diff --git a/style.css b/style.css index 7ec39ac4..206cb088 100644 --- a/style.css +++ b/style.css @@ -1,19 +1,20 @@ /* Base styles */ -* { +*, +*::before, +*::after { margin: 0; - padding: 10; + padding: 0; box-sizing: border-box; - font-family: "Montserrat", sans-serif; } body { + font-family: "Montserrat", sans-serif; background-color: #fafbff; margin: 10px; } header { - margin-bottom: 30px; - margin-top: 30px; + margin: 30px 0; } h1 { @@ -26,57 +27,69 @@ h1 { h2 { font-size: 18px; font-weight: 700; - margin-bottom: 16px; - margin-top: 16px; + margin: 16px 0; } p { font-size: 16px; - font-weight: 400; } -.ingredient-btn{ - margin-top: 10px; - margin-bottom: 10px; +.bold { + font-weight: 700; } -.cuisine-info { - margin-top: 5px; - margin-bottom: 5px; +.filter-container { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 30px; + margin-left: 10px; } -.time-info { - margin-bottom: 5px; +.recipes-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); + gap: 16px; } -.bold { - font-weight: 700; +.recipe-card { + display: flex; + flex-direction: column; + background-color: white; + border: 1px solid grey; + border-radius: 10px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + padding: 16px; + transition: ease-in-out 0.3s; } -.ingredients{ - font-weight: 700; - margin-top: 5px; +.recipe-card:hover { + transform: translateY(-5px); } +.recipe-img { + width: 100%; + height: 200px; + object-fit: cover; +} -.filter-container { +.card-buttons{ display: flex; - flex-wrap: wrap; - gap: 20px; - margin-bottom: 30px; - margin-left: 10px; + flex-direction: column; + gap: 10px; + margin-top: auto; } button { padding: 8px 16px; - border: none; - border-radius: 50px; border: 2px solid transparent; + border-radius: 50px; font-weight: 500; font-size: 16px; color: #0018A4; cursor: pointer; transition: all 0.3s ease; + background-color: transparent; } button:hover { @@ -87,39 +100,50 @@ button:hover { background-color: #FFECEA; } +.sort-btn:active { + background-color: black; + } + + .random-btn { background-color: #CCFFE2; } -.recipes-container { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(290px, 1fr)); - gap: 16px; +hr { + margin-bottom: 10px; } -.recipe-card { - display: flex; - flex-direction: column; - background-color: white; - border: 1px solid grey; - border-radius: 10px; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); - padding: 16px; - width: 290px; - justify-self: center; - transition: ease-in-out 0.3s; +.ingredient-btn, .instructions-btn{ + width: 100%; } -.recipe-card:hover { - transform: translateY(-5px); +.cuisine-info { + margin-top: 5px; + margin-bottom: 5px; } -.recipe-img { - width: 100%; - height: 200px; - object-fit: cover; +.time-info { + margin-bottom: 5px; } +.bold { + font-weight: 700; +} + +.ingredients{ + font-weight: 700; + margin-top: 5px; +} + + + + + + + + + + ul { list-style: none; padding: 0; @@ -131,9 +155,7 @@ li { line-height: 25px; } -.instructions-btn { -margin-top: auto; -} + select { padding: 8px 16px; From 3d422b2e884a7f5f16f5b332329f7deabe9566a1 Mon Sep 17 00:00:00 2001 From: Ida Hellgren Date: Sat, 15 Mar 2025 13:01:44 +0100 Subject: [PATCH 4/9] new structure to the javascript file, trying to keep it DRY --- index.html | 6 +- script.js | 213 ++++++++++++++++++++++++++++++----------------------- style.css | 10 +++ 3 files changed, 132 insertions(+), 97 deletions(-) diff --git a/index.html b/index.html index 0fcc7525..8396c5c8 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,7 @@

    Recipe Library

    -
    +

    Filter on diet

    - - - - -
    - -
    -

    Sort on ingredients

    -
    - - +
    +
    +

    Filter on diet

    + +
    + +
    +

    Sort on ingredients

    +
    + + +
    +
    + +
    +

    Sort on time

    +
    + + +
    -
    -
    -

    Sort on time

    -
    - - + +
    +

    What should I eat today?

    +
    -
    -
    -

    What should I eat today?

    - -
    +
    diff --git a/script.js b/script.js index 33306168..9e832fdc 100644 --- a/script.js +++ b/script.js @@ -1,4 +1,5 @@ const recipesContainer = document.getElementById('recipes-container') +const recipeForm = document.getElementById('recipe-filter-form') const dietFilter = document.getElementById('diet-filter') const btnAscendingIngredients = document.getElementById('btn-ascending-ingredients') const btnDescendingIngredients = document.getElementById('btn-descending-ingredients') diff --git a/style.css b/style.css index e36b67a5..f6904029 100644 --- a/style.css +++ b/style.css @@ -34,7 +34,7 @@ p { } -.filter-container { +.recipe-filter-form { display: flex; flex-wrap: wrap; gap: 20px; From 43ec1f8790c7cb39ac3fd86294d1205a265e8adf Mon Sep 17 00:00:00 2001 From: Ida Hellgren Date: Fri, 21 Mar 2025 10:53:50 +0100 Subject: [PATCH 8/9] replaced null with [] on row 19 --- script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script.js b/script.js index 9e832fdc..5b719f6d 100644 --- a/script.js +++ b/script.js @@ -16,7 +16,7 @@ const url = `${baseUrl}?apiKey=${apiKey}&number=${numRecipes}` let allRecipes = [] let currentRecipes = [] -let lastSortAction = null +let lastSortAction = [] const displayError = (message) => { recipesContainer.innerHTML = `

    ${message}

    ` From ecc9627d81610e20a00573fd79015fd0050f6569 Mon Sep 17 00:00:00 2001 From: Ida Hellgren Date: Fri, 21 Mar 2025 10:59:24 +0100 Subject: [PATCH 9/9] updated the textfor informing the user that the daily quota is reached --- script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script.js b/script.js index 5b719f6d..5ca0fe43 100644 --- a/script.js +++ b/script.js @@ -218,7 +218,7 @@ const fetchRecipes = () => { }) .catch(error => { if (error.message.includes('quota')) { - displayError(`${error.message} You might want to update your API key or subscribe to a higher tier plan.`) + displayError(`${error.message} You've reached the limit for new recipes today.`) } else { displayError(`Error fetching data: ${error.message}`) }