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
Binary file added .DS_Store
Binary file not shown.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
# js-project-recipe-library

https://marina-recipelibrary.netlify.app/
Binary file added images/padthai.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/pizza.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/risotto.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/tacos.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<!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>Project Recipe Library</title>
</head>

<body>
<div class="outer-container">
<header>
<h1>Recipe Library</h1>
</header>

<section class="upper-box">
<div class="filter">
<p>Filter on kitchen</p>
<button class="btn-kitchen">All</button>
<button class="btn-kitchen">Asian</button>
<button class="btn-kitchen">European</button>
<button class="btn-kitchen">Mexican</button>
<button class="btn-kitchen">American</button>
<button class="btn-kitchen">Southern</button>
<button class="btn-kitchen">Mediterranean</button>
</div>

<div class="sorting">
<p>Sort on time</p>
<button class="btn-sorting">Descending</button>
<button class="btn-sorting">Ascending</button>
</div>
<div class="random">
<p>Random</p>
<button class="btn-random">🎲 Surprise Me!</button>
</div>
</section>

<main class="container" id="container"></main>
</div>
<script src="script.js"></script>

</body>

</html>
137 changes: 137 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
//=======GLOBAL VARIABLES=======//

const apiKey = "f7aa1eb0bfab45cdae5e9f2f21292092"
const container = document.getElementById("container")
const buttons = document.querySelectorAll(".btn-kitchen")
const sortButtons = document.querySelectorAll(".btn-sorting")
const randomButton = document.querySelector(".btn-random")

let selectedCuisine = "All"
let currentRecipes = []

//======= FUNCTIONS for UI =======//

//Buttons, changes color when active//
function makesButtonInteractive(groupClass) {

Choose a reason for hiding this comment

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

Either stick to functions declared like:
function myFunctionName () {}
or
const myFunctionName = () => {}
don't mix

const buttons = document.querySelectorAll(groupClass)

buttons.forEach((btn) => {
btn.addEventListener("click", () => {
buttons.forEach(b => b.classList.remove("active"))
btn.classList.add("active")
})
})
}

//Shows recipes in container//
const showRecipe = (recipeArray) => {
container.innerHTML = "" //Resetting the container before filling it//

recipeArray.forEach(recipe => {
const cuisineText = recipe.cuisines.length ? recipe.cuisines.join(", ") : "N/A"

Choose a reason for hiding this comment

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

smart!


const timeText = recipe.readyInMinutes ? `${recipe.readyInMinutes} min` : "N/A"

container.innerHTML += `
<div class="card">
<img src="${recipe.image}" alt="${recipe.title}">
<p><strong>${recipe.title}</strong></p>
<hr class="divider">

<div class="info-row">
<span><strong>Cuisine:</strong> ${cuisineText}</span>
<span><strong>Time: </strong> ${timeText}</span>
</div>

<hr class="divider">
<p><strong>Ingredients:</strong></p>
<ul>
${recipe.extendedIngredients
?.slice(0, 5)
.map(i => `<li>${i.original}</li>`)
.join("") || "No ingredients"}
</ul>
</div>
`;
});
};

//======= EVENTLISTENERS =======//

//Filterbuttons//
buttons.forEach(button => {
button.addEventListener("click", () => {
selectedCuisine = button.textContent.trim() //trim removes invisible spaces at the beginning or end of a text

Choose a reason for hiding this comment

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

good safety to trim

fetchRecipes()
})
})

//Sortingbuttons//
sortButtons.forEach(button => {
button.addEventListener("click", () => {
const chosenSort = button.textContent.trim() //Ascending/descending//
if (!currentRecipes.length) return

const sortedRecipes = [...currentRecipes] //coping the latest list from the API

sortedRecipes.sort((a, b) => {
const timeA = a.readyInMinutes || 0
const timeB = b.readyInMinutes || 0
return chosenSort === "Ascending" ? timeA - timeB : timeB - timeA
})

showRecipe(sortedRecipes)
})
})

//Randombutton//
randomButton.addEventListener("click", () => {
if (!currentRecipes.length) {
container.innerHTML =
"<p> No recipes to show for this cuisine right now...</p>"
return
}

const randomIndex = Math.floor(Math.random() * currentRecipes.length)
const randomRecipe = currentRecipes[randomIndex]

showRecipe([randomRecipe])
})

//======= FETCH from API =======//

async function fetchRecipes() {
container.innerHTML = "<p>⏳ Loading recipes...</p>"

Choose a reason for hiding this comment

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

loading state! ⭐


try {
let url = `https://api.spoonacular.com/recipes/random?number=30&apiKey=${apiKey}&addRecipeInformation=true&addRecipeInstructions=true`

if (selectedCuisine !== "All") {
url += `&cuisine=${selectedCuisine}` //Use cuisines here instead of tags, to get sorted by cuisines//
}

const response = await fetch(url)
if (!response.ok) throw new Error("API-limit reached or network error")

const data = await response.json()

currentRecipes = data.recipes
.filter(recipe => recipe.cuisines && recipe.cuisines.length > 0) //Takes away recipes without cuisine
.filter(recipe => selectedCuisine === "All" || recipe.cuisines.includes(selectedCuisine)) //Filter on chosen cuisine

if (currentRecipes.length === 0) {
container.innerHTML = `<p>No recipes were found for this "${selectedCuisine}" 😕</p>`
} else {
showRecipe(currentRecipes)
}

} catch (err) {
container.innerHTML = `<p>⚠️ ${err.message}</p>`
}
}

//======= SETUP =======//

makesButtonInteractive(".btn-kitchen") //The function runs on both groups//
makesButtonInteractive(".btn-sorting")
fetchRecipes() //fetches all recipes when page loads
198 changes: 198 additions & 0 deletions style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
*,
/*html important to avoid side-scroll*/
body,
html {
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
box-sizing: border-box;
overflow-x: hidden;
}

.outer-container {
display: grid;
}

h1 {
width: 350px;
height: 70px;
margin: 50px 0 0 10px;
color: #0018A4;
font-size: 45px;
font-weight: bold;
line-height: 100%;
}


.upper-box {
display: flex;
flex-direction: column;
gap: 20px;
margin: 10px;
padding: 10px;
font-size: 20px;
font-weight: bold;
}

.btn-kitchen {
padding: 8px 16px 8px 16px;
margin-top: 10px;
background-color: #CCFFE2;
border-radius: 50px;
font-size: 16px;
border: none;
color: #0018A4;
}

.btn-kitchen.active {
background-color: #0018A4;
color: white;
}

.btn-kitchen:hover {
border: 2px solid #0018A4;
}

.btn-sorting {
padding: 8px 16px 8px 16px;
margin-top: 10px;
background-color: #FFECEA;
border-radius: 50px;
font-size: 16px;
border: none;
color: #0018A4;
}

.btn-sorting.active {
background-color: #FF6589;
color: white;
}

.btn-sorting:hover {
border: 2px solid #2d46d5;
}
Comment on lines +71 to +73

Choose a reason for hiding this comment

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

To avoid the "hopping" of the buttons when you hover over them you could add the same border on all the buttons but transparent color:

border: 2px solid transparent;

So the border is there all the time but only visible on hover. I hope that makes sense?


.btn-random {
width: 150px;
padding: 8px 16px 8px 16px;
margin-top: 10px;
background-color: rgb(212, 232, 240);
border-radius: 50px;
font-size: 14px;
border: none;
color: #0018A4;
}

.btn-random.active {
background-color: #0018A4;
color: white;
}

.btn-random:hover {
border: 2px solid #2d46d5;
}
Comment on lines +91 to +93

Choose a reason for hiding this comment

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

If you want to refactor the code and keep it more DRY you could stack similar styling together.
since this is exactly the same styling as for btn-sorting:hover
not neccesary just something to take with you


.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, max-content));
width: 100%;
max-width: 320px;
justify-content: center;
gap: 20px;
padding: 10px;
margin: 0 auto;
}

.card {
font-size: 22px;
display: flex;
max-width: 300px;
flex-direction: column;
border: 1px solid #e9e9e9;
border-radius: 16px;
padding: 16px 16px 24px 16px;
}

.card:hover {
transform: scale(1.03);
box-shadow: 0px 0px 30px 0px #0018A433;
border: 2px solid #0018A4;
}

.card img {
object-fit: cover;
width: 100%;
height: 200px;
border-radius: 12px;
margin-bottom: 15px;
}

.card ul {
list-style-type: none;
font-size: 16px;
margin-top: 10px;
}

p {
font-size: 18px;
}

.info-row {
font-size: 16px;
}

.info-row span {
display: block;
padding: 5px;
}

ul li {
padding: 5px;
}

.divider {
width: 95%;
height: 0;
border: 1px solid #e9e9e9;
margin: 10px 0 10px 0;
}

/* //==== TABLET ====// */
@media (min-width:600px) {
h1 {
font-size: 50px;
margin: 50px 0 0 20px;
}

.container {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
justify-content: center;
width: 100%;
max-width: 900px;
margin: 0 auto;
gap: 50px;
}
}

/* //==== DESKTOP ====// */
@media (min-width: 1024px) {
.outer-container {
width: 100%;
padding-left: 40px;
box-sizing: border-box;
}

h1,
.upper-box {
margin-left: 0;
text-align: left;
}

.container {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
justify-content: start;
margin-left: 0;
gap: 30px;
max-width: 1600px;
}
}