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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# js-project-recipe-library
https://saras-js-project-recipe-library.netlify.app/
82 changes: 82 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<!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" />
</head>

<body>
<main>
<h1>Recipe Library</h1>

<!-- ====== FILTERS / SORT / LUCKY CONTROLS ====== -->

Choose a reason for hiding this comment

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

This is a really neat way to organize your code!

Copy link
Author

Choose a reason for hiding this comment

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

Thank you:)

<div class="controls">

<!-- FILTER SECTION -->
<section class="filters" id="cuisineFilter">
<h2 class="section-title">Filter on kitchen</h2>
<div class="filter-buttons">
<label class="filter-option">
<input checked type="radio" name="cuisine" value="all" />

Choose a reason for hiding this comment

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

Smart to use radio-buttons! 💡

All
</label>
<label class="filter-option">
<input type="radio" name="cuisine" value="asian" />
Asian
</label>
<label class="filter-option">
<input type="radio" name="cuisine" value="italian" />
Italian
</label>
<label class="filter-option">
<input type="radio" name="cuisine" value="mexican" />
Mexican
</label>
<label class="filter-option">
<input type="radio" name="cuisine" value="middle eastern" />
Middle Eastern
</label>
<label class="filter-option">
<input type="radio" name="cuisine" value="nordic" />
Nordic
</label>
</div>
</section>

<!-- SORT SECTION -->
<section class="sort" id="sortOnTime">
<h2 class="section-title">Sort on time</h2>
<div class="sort-buttons">
<label class="sort-button">
<input checked type="radio" name="order" value="desc" />
Descending
</label>
<label class="sort-button">
<input type="radio" name="order" value="asc" />
Ascending
</label>
</div>
</section>

<!-- FEELING LUCKY SECTION -->
<section class="lucky">
<h2 class="section-title">Feeling lucky?</h2>
<div class="lucky-button">
<button id="random-button" class="random-button">Surprise me!</button>
</div>
</section>
</div>

<!-- ====== RECIPE CARDS GRID ====== -->
<section
class="cards"
id="cardsContainer"
aria-label="Recipe cards grid">
</section>
</main>

<script src="script.js"></script>
</body>
</html>
170 changes: 170 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
const cardsContainer = document.getElementById('cardsContainer')
const cuisineFilter = document.getElementById('cuisineFilter')
const sortSection = document.getElementById('sortOnTime')
const randomButton = document.getElementById('random-button')

const API_KEY = 'c0e5e1580c434758805e8f05530e9b26'
const API_KEY2 = 'd4c7b9ec0f33432ea2aa56c816a17159'

const CACHE_KEY = 'recipes'
const CACHE_DURATION = 24 * 60 * 60 * 1000

let recipesArray = []

const params = new URLSearchParams({
number: 30,
cuisine: 'Asian,Italian,Mexican,Middle Eastern',
diet: 'vegan,vegetarian,gluten free',
type: 'breakfast,main course,dessert',
sort: 'popularity',
addRecipeInformation: true,
instructionsRequired: true,
fillIngredients: true,
apiKey: API_KEY
})
Comment on lines +14 to +24

Choose a reason for hiding this comment

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

Nice that you have used URLSearchParams! Super useful and clever


const URL = `https://api.spoonacular.com/recipes/complexSearch?${params.toString()}`

const showCardsContainer = (recipesArray) => {
cardsContainer.innerHTML = ''

if (!recipesArray || recipesArray.length === 0) {
cardsContainer.innerHTML = `
<div class='empty-message'>
<h2>Oops 😅</h2>
<p>We couldn't find any recipes for your filter...<br>
Try changing it or click <strong>Surprise me!</strong> 🎉</p>
</div>
Comment on lines +35 to +37

Choose a reason for hiding this comment

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

nice empty state msg and nudge to try the random button :)

`
return
}

recipesArray.forEach((recipe) => {
cardsContainer.innerHTML += `
<div class='card'>
<img src='${recipe.image}' alt='${recipe.title}' />
<div class='card-content'>
<h3>${recipe.title}</h3>
<div class='divider'></div>
<p><strong>Cuisine:</strong>

Choose a reason for hiding this comment

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

The use of is intuitive since it might be read out loud of accessibility reasons

<span class='cuisine-value'>
${recipe?.cuisines?.length ? recipe.cuisines.join(', ') : 'N/A'}
</span>
Comment on lines +51 to +52

Choose a reason for hiding this comment

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

good fallback

</p>
<p><strong>Time:</strong>
<span class='time-value'>${recipe.readyInMinutes} min</span>
</p>
<div class='divider'></div>
<p class='ingredients-header'>Ingredients:</p>
<ul>
${recipe?.extendedIngredients
.map(({ name }) => `<li>${name}</li>`)

Choose a reason for hiding this comment

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

It looked nice to have the list with dots

.join('')}
</ul>
</div>
</div>
`
})
}

const fetchData = async () => {
const cachedData = localStorage.getItem(CACHE_KEY)

if (cachedData) {
try {
const { recipes, timestamp } = JSON.parse(cachedData)

Choose a reason for hiding this comment

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

What does the timestamp do?

const isExpired = Date.now() - timestamp > CACHE_DURATION
recipesArray = recipes
Comment on lines +73 to +77

Choose a reason for hiding this comment

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

smart, nice use of a cache "timer" too


if (!isExpired) {
showCardsContainer(recipes)
return
}
} catch (error) {
console.error('Error parsing cached data:', error)
localStorage.removeItem(CACHE_KEY)
}
}

try {
const res = await fetch(URL)

if (res.status === 402) {
cardsContainer.innerHTML = `
<div class='empty-message'>
<h2>Daily API limit reached 🚫</h2>
<p>Please try again later.</p>
</div>`;
Comment on lines +92 to +97

Choose a reason for hiding this comment

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

return;
}

if (!res.ok) {
cardsContainer.innerHTML = `
<div class='empty-message'>
<h2>Oops 😅</h2>
<p>No recipes found...</p>
</div>
`
throw new Error(`HTTP error! Status: ${res.status}`)
}

const data = await res.json()
const recipes = data.results
recipesArray = recipes

localStorage.setItem(
CACHE_KEY,
JSON.stringify({
recipes,
timestamp: Date.now()
})
)

showCardsContainer(recipes)
} catch (error) {
console.error('Error occurred:', error)
}
}

fetchData()

const filterOnCuisine = () => {
const selected = cuisineFilter.querySelector('input[name="cuisine"]:checked')
const selectedCuisine = selected ? selected.value : 'all'

const filteredRecipes =
selectedCuisine === 'all'
? recipesArray
: recipesArray.filter((recipe) =>
recipe.cuisines
.map((cuisine) => cuisine.toLowerCase())
.includes(selectedCuisine.toLowerCase())
Comment on lines +140 to +141

Choose a reason for hiding this comment

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

Smart to use toLowerCase if anything has a capital letter somewhere, to make sure nothings missed

)

showCardsContainer(filteredRecipes)
}

const sortOnTime = () => {
const selected = sortSection.querySelector('input[name="order"]:checked')
const order = selected ? selected.value : 'desc'

const sorted = [...recipesArray].sort((a, b) => {
return order === 'asc'
? a.readyInMinutes - b.readyInMinutes
: b.readyInMinutes - a.readyInMinutes
})

showCardsContainer(sorted)
}
Comment on lines +147 to +158

Choose a reason for hiding this comment

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

nice job! If you want to keep the sorting even when you change the cuisine filters you could maybe save the sorting rule in a global variable. Just something to take with you going forward.


const getRandomRecipe = () => {
const randomIndex = Math.floor(Math.random() * recipesArray.length)
const randomRecipe = [recipesArray[randomIndex]]

showCardsContainer(randomRecipe)
}

cuisineFilter.addEventListener('change', filterOnCuisine)
sortSection.addEventListener('change', sortOnTime)
randomButton.addEventListener('click', getRandomRecipe)

Loading