Skip to content

Sofie's recipe library #23

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

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a13dbee
created files
ssofiejohansson Feb 24, 2025
318f4e8
added html layout and some styling on cards and form
ssofiejohansson Feb 24, 2025
7716179
added js
ssofiejohansson Feb 26, 2025
6844c9f
changed input types and linked js code
ssofiejohansson Feb 27, 2025
d68ecfb
text size edits
ssofiejohansson Feb 27, 2025
d82ff01
removed media queries and added different layout to grid to make it r…
ssofiejohansson Feb 28, 2025
1a3514f
edit sort text style
ssofiejohansson Feb 28, 2025
a814e7f
added mockup recipe array and added all button
ssofiejohansson Mar 5, 2025
15320d6
script is working for sorting and filtering
ssofiejohansson Mar 6, 2025
c4bd32f
added random function button and changed the responsive styling
ssofiejohansson Mar 6, 2025
27482ce
changed grid size and some styling
ssofiejohansson Mar 7, 2025
6f3a06b
Using API! Filter/sorting works OK
ssofiejohansson Mar 12, 2025
c1605a1
descriptions clarified
ssofiejohansson Mar 12, 2025
e3d5a3b
added loading message and instructions
ssofiejohansson Mar 13, 2025
f617f05
cleaned up code in html and css mainly
ssofiejohansson Mar 13, 2025
5b870f7
added more comments/cleaned up css
ssofiejohansson Mar 14, 2025
28e5f9c
trying to work out error msg
ssofiejohansson Mar 14, 2025
eb65232
changed grid size
ssofiejohansson Mar 16, 2025
503512d
changed margin style, added more recipes
ssofiejohansson Mar 18, 2025
59128a5
added animation on btn and changed mobile view for the form
ssofiejohansson Mar 19, 2025
187bc7d
changed style on instructions button
ssofiejohansson Mar 19, 2025
1c6360f
added more recipes
ssofiejohansson Mar 19, 2025
6a7c7f5
minor styling edits
ssofiejohansson Mar 19, 2025
d6e68e0
edit grid layout
ssofiejohansson Mar 19, 2025
7ce27df
added local storage and fixed error message 402
ssofiejohansson Mar 21, 2025
a7c3b5f
cleaned up code / added comments
ssofiejohansson Mar 21, 2025
6e1489a
added project info in readme file
ssofiejohansson Mar 21, 2025
6487bc4
edit readme
ssofiejohansson Mar 21, 2025
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.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
# js-project-recipe-library

Link to netlify: https://stirring-yeot-236514.netlify.app/

My second project in the JS bootcamp.

The Recipe Library App is a web app that helps users find recipes based on different filters and sorting options. It pulls real recipe data from the Spoonacular API and dynamically updates the display as users make selections. The app includes a random recipe option, messages when no results are found and a fully responsive design. To improve performance and user experience, I added features like multi-filter functionality, local storage caching, a loading state and interactive recipe instructions.
Copy link

Choose a reason for hiding this comment

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

It's great that you included a brief description!

137 changes: 137 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<!-- Using some fontawesome icons -->
<script
src="https://kit.fontawesome.com/beb1bc6f21.js"
crossorigin="anonymous"
></script>
<link
rel="stylesheet"
href="./style/style.css"
>
<title>Project 2 - Recipe Library</title>
</head>

<body>
<header>
<a href="./index.html">
<h1>Recipe Library</h1>
</a>
</header>

<main>

<!-- Diet Filter Form -->
<form id="dietForm">
<div class="form">
<p>Filter on Diet</p>
<label class="button btn-filter">
<input
type="radio"
name="diet"
value="all"
> 🍽️ All
</label>
<label class="button btn-filter">
<input
type="radio"
name="diet"
value="vegan"
> 🌱 Vegan
</label>
<label class="button btn-filter">
<input
type="radio"
name="diet"
value="vegetarian"
> 🥕 Vegetarian
</label>
<label class="button btn-filter">
<input
type="radio"
name="diet"
value="glutenFree"
> 🌾 Gluten-Free
</label>
<label class="button btn-filter">
<input
type="radio"
name="diet"
value="dairyFree"
> 🧀 Dairy-Free
</label>
</div>

<!-- Sort Filter Form -->
<div class="form">
<p>Sort by Time</p>
<label class="button btn-sort">
<input
type="radio"
name="timeFilter"
value="ascending"
> 🕑 A quick one
</label>
<label class="button btn-sort">
<input
type="radio"
name="timeFilter"
value="descending"
> ⏳ I got time!
</label>
</div>
</form>

<!-- Random recipe button-->
<div class="form">
<p>In need of Inspo?</p>
<button
class="button btn-random animationBtn"
id="randomBtn"
>✨ Surprise me!</button>
</div>

<!-- If an error happens, the message shows here -->
<p
id="errorMessage"
class="form"
></p>

<!-- Loading message showing while fetching for recipes with som animation -->
<div id="loadingMessage">
<div id="loadingText">
<p class="animation">⏳</p>
<h4>Hang on! We are loading some super yummy recipes just for you ✨</h4>
</div>
</div>

<!-- One <section> per recipe displays here -->
<section
id="cards"
class="card-parent"
>
</section>

</main>

<!-- Icons and links-->
<footer>
Copy link

Choose a reason for hiding this comment

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

Fun to have a footer, looks cute.

<div class="icons">
<p>Project by:
<a href="https://github.com/ssofiejohansson"><i class="fa-brands fa-github"></i></a>
<a href="https://www.linkedin.com/in/sofie-j-011155310/"><i class="fa-brands fa-linkedin"></i></a>
</p>
</div>
</footer>

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

</html>
Binary file added js/.DS_Store
Binary file not shown.
176 changes: 176 additions & 0 deletions js/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// This is where I store recipes globally
let allRecipes = []
Copy link

Choose a reason for hiding this comment

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

General comment for the javascript: I think the code is well structured and the naming of functions etc makes it easy to understand what the different parts of the code do.

let filteredRecipes = []

// Fetch from API
const fetchRecipes = () => {

const URL = 'https://api.spoonacular.com/recipes/random?number=24&apiKey=427f448f971e4dcea73ae654c0850b2a'

const loadingMessage = document.querySelector('#loadingMessage')
const errorMessage = document.getElementById('errorMessage')

// Show loading message while fetching
loadingMessage.style.display = 'block'

errorMessage.innerHTML = ''

fetch(URL)
.then(response => response.json())
.then(data => {
// If API limit is reached, show error message
if (data.code === 402) {
errorMessage.innerHTML = `
<h2>Uh oh! You've reached the daily limit of recipe requests. ✨</h2>
<p>Here are some saved recipes for you, but come back tomorrow if you want to get more!</p>`
throw new Error("You've reached the daily limit of recipe requests.")
}

allRecipes = data.recipes
filteredRecipes = [...allRecipes]

// Save the recipes in local storage
localStorage.setItem('recipes', JSON.stringify(allRecipes))

displayRecipes(filteredRecipes)
})
.catch((error) => {
console.error(error.message)

// If API runs out, try loading from local storage
const cachedRecipes = localStorage.getItem('recipes')
if (cachedRecipes) {
allRecipes = JSON.parse(cachedRecipes)
filteredRecipes = [...allRecipes]
displayRecipes(filteredRecipes)
} else {
// Show error message when no cached recipes exist
errorMessage.innerHTML = `
<h2>Uh oh! There is a problem when loading recipes. Come back tomorrow. ✨</h2>
<p>${error.message}</p>`
}
})
.finally(() => {
// Hide loading message when request finish so its not showing all the time
loadingMessage.style.display = 'none'
})
}
Copy link

Choose a reason for hiding this comment

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

Nice to have some data to fall back on when the daily quota is reached. Looks clean too.



//Display fetched recipes in my card container
const displayRecipes = (recipesToDisplay) => {
const cardContainer = document.querySelector('#cards')
cardContainer.innerHTML = ''

recipesToDisplay.forEach(recipe => {

let dietLabels = []

//checks what diet the recipe is and adds it to the dietLabels array above
if (recipe.vegan) dietLabels.push("Vegan")
if (recipe.vegetarian) dietLabels.push("Vegetarian")
if (recipe.glutenFree) dietLabels.push("Gluten Free")
if (recipe.dairyFree) dietLabels.push("Dairy Free")

cardContainer.innerHTML += `
<article class="card">
<div class="card-image">
<img
src="${recipe.image}"
alt="Recipe image"
/>
</div>
<h2>${recipe.title}</h2>
<hr>
<div class="instructions">
<h3 class="title">Diet:</h3>
<p>${dietLabels.length > 0 ? dietLabels.join(", ") : "No specific diet"}</p>
</div>
<div class="instructions">
<h3 class="title">Time:</h3>
<p>${recipe.readyInMinutes} min</p>
</div>
<hr>
<div class="ingredients">
<h4 class="title">Ingredient list:</h4>
<ul>
${recipe.extendedIngredients.map(ingredient =>
`<li>${ingredient.original}</li>`).join("")}
</ul>
</div>
<button class="button btn-random instructionsBtn">⬇️ Recipe instructions</button>
<div class="instructionList" style="display: none;">
<p>${recipe.instructions}</p>
</div>
<hr>
<a
href="${recipe.sourceUrl}"
target="_blank"
class="recipe-link"
>🔗 View Full Recipe</a>
</article>`
})
}

// Function to filter recipes based on diet
const filterByDiet = (filterType) => {
if (filterType === "all") {
filteredRecipes = [...allRecipes]
} else {
filteredRecipes = allRecipes.filter(recipe => recipe[filterType] === true)
}

if (filteredRecipes.length === 0) {
document.querySelector('#cards').innerHTML = `<p>❌ No ${filterType} recipes found. Why don't you pick another one?✨ </p>`
} else {
displayRecipes(filteredRecipes)
}
}

// Function to sort recipes by readyInMinutes
const sortByReadyTime = (order) => {
if (order === 'ascending') {
filteredRecipes.sort((a, b) => a.readyInMinutes - b.readyInMinutes)
} else {
filteredRecipes.sort((a, b) => b.readyInMinutes - a.readyInMinutes)
}

displayRecipes(filteredRecipes)
}

//click button to show instructions for recipes
document.addEventListener('click', (event) => {
if (event.target.classList.contains('instructionsBtn')) {
const instructionDiv = event.target.nextElementSibling

if (instructionDiv && instructionDiv.classList.contains('instructionList')) {
instructionDiv.style.display = instructionDiv.style.display === 'none' ? 'block' : 'none'
}
}
})

// Function to display a random recipe
const surpriseMe = () => {
if (allRecipes.length > 0) {
const randomRecipe = allRecipes[Math.floor(Math.random() * allRecipes.length)]
displayRecipes([randomRecipe])
}
}

// All event listeners
document.querySelectorAll('.btn-filter input').forEach(button => {
button.addEventListener("change", (event) => {
filterByDiet(event.target.value)
})
})

document.querySelectorAll('.btn-sort input').forEach(button => {
button.addEventListener("change", (event) => {
sortByReadyTime(event.target.value)
})
})

document.querySelector('#randomBtn').addEventListener("click", surpriseMe)

// Display recipes from the start
fetchRecipes()
Loading