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
61 changes: 61 additions & 0 deletions index.html
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="styles.css" />
</head>
<body>
<div class="container">
<h1>Recipe Library</h1>

<section id="controls">
<div>
<h2>Filter by cuisine</h2>
<div class="controls__container filter">
<button
onclick="setActiveFilter('all')"
id="filter__all"
class="btn">
All
</button>
<button
onclick="setActiveFilter('italian')"
id="filter__italian"
class="btn">
Italy
</button>
<button
onclick="setActiveFilter('american')"
id="filter__american"
class="btn">
USA
</button>
<button
onclick="setActiveFilter('chinese')"
id="filter__chinese"
class="btn">
China
</button>
</div>
</div>

<div>
<h2>Sort by time</h2>
<div class="controls__container sort">
<button onclick="setActiveSort('desc')" id="sort__desc" class="btn">
Descending
</button>
<button onclick="setActiveSort('asc')" id="sort__asc" class="btn">
Ascending
</button>
</div>
</div>
</section>

<section id="recipe__grid"></section>
</div>
<script src="script.js"></script>
</body>
</html>
135 changes: 135 additions & 0 deletions script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// --- State ---
let cuisineQueryParam = ""; // e.g., "&cuisine=italian"
let sortQueryParam = "&sort=time&sortDirection=desc"; // default by time, descending
let activeFilterBtn = null;
let activeSortBtn = null;

const API_KEY = "3f2445631f2d458b92fe40f9832bcc51";

// --- Marks the clicked button as active and removes the active class from the previously clicked one. ---
function markActive(newBtn, prevBtnRefName) {
if (newBtn === null) return;
if (window[prevBtnRefName]) {
window[prevBtnRefName].classList.remove("active");
}
newBtn.classList.add("active");
window[prevBtnRefName] = newBtn;
}

//Fetches updated recipes and sends them to the UI rendering function (showCase()).
function updateAndRender() {
showCase(fetchRecipes());
}

// --- UI Actions Handles what happens when the user clicks a filter button (like “Italian” or “Mexican”).---
function setActiveFilter(filter) {
// Set query param
if (filter === "all") {
cuisineQueryParam = "";
} else {
cuisineQueryParam = "&cuisine=" + encodeURIComponent(filter);
}
// Mark active button
const btn = document.getElementById(`filter__${filter}`);
markActive(btn, "activeFilterBtn");

// Refresh results
updateAndRender();
}

function setActiveSort(direction) {
// Validate: asc | desc
const dir = direction === "asc" ? "asc" : "desc";

Choose a reason for hiding this comment

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

nice to use ternary operators here.

sortQueryParam = `&sort=time&sortDirection=${dir}`;

const btn = document.getElementById(`sort__${dir}`);

Choose a reason for hiding this comment

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

clever to use the variable to get the correct element from the DOM

markActive(btn, "activeSortBtn");

// Refresh results
updateAndRender();
}

// --- Data Fetch recipe from spoonacular API, based on filter and sort settings ---
async function fetchRecipes() {
const URL = `https://api.spoonacular.com/recipes/complexSearch?fillIngredients=true&addRecipeInformation=true&number=12&apiKey=${API_KEY}${cuisineQueryParam}${sortQueryParam}`;

try {
const response = await fetch(URL);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
// data.results is an array of recipe objects
return data.results || [];
} catch (err) {
console.error(err);
// Graceful fallback
return [];
}
}

// --- Format time, and minustes ---
function formatTime(totalMinutes) {
const h = Math.floor(totalMinutes / 60);
const m = totalMinutes % 60;
if (h && m) return `${h}h ${m}m`;
if (h) return `${h}h`;
return `${m}m`;
}
Comment on lines +72 to +78

Choose a reason for hiding this comment

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

nice


// Display recepie card in HTML Grid/ CSS style on it
async function showCase(recipesPromise) {
const recipeGrid = document.getElementById("recipe__grid");
recipeGrid.innerHTML = `<div id="loading">Loading…</div>`;

const recipes = await recipesPromise;

if (!recipes.length) {
recipeGrid.innerHTML = `<p>No recipes found. Try a different filter.</p>`;
return;
}

recipeGrid.innerHTML = "";

recipes.forEach((recipe) => {
const recipeCard = document.createElement("div");
recipeCard.className = "recipe__card";

const cuisines =
Array.isArray(recipe.cuisines) && recipe.cuisines.length
? recipe.cuisines.join(", ")
: "—";

const ingredients = Array.isArray(recipe.extendedIngredients)
? recipe.extendedIngredients.map((ing) => ing.original)
: [];

const ingredientList = document.createElement("ul");
ingredientList.className = "recipe__ingredients";
ingredients.forEach((text) => {
const li = document.createElement("li");
li.textContent = text;
ingredientList.appendChild(li);
});
// html structor
recipeCard.innerHTML = `
<img src="${recipe.image}" alt="${recipe.title}" class="recipe__image" />
<h3 class="recipe__title">${recipe.title}</h3>
<p class="recipe__meta"><b>Cuisine:</b> ${cuisines}</p>
<p class="recipe__meta"><b>Time:</b> ${formatTime(
recipe.readyInMinutes || 0
)}</p>
<h4 class="recipe__subtitle">Ingredients:</h4>
`;

recipeCard.appendChild(ingredientList);
recipeGrid.appendChild(recipeCard);
});
}
// initialization (auto-run- on load)
// --- Init (after DOM is ready because script is at the end of body) ---
(function init() {
// Set defaults visually & load first page
setActiveFilter("all");
setActiveSort("desc");
})();
105 changes: 105 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
* {
margin: 0;
}

h1 {
color: blue;
font-size: 3.5rem;
margin-top: 50px;
margin-bottom: 50px;
}

h2 {
margin: 25px 0 25px;
}

#controls {
display: flex;
justify-content: space-between;
max-width: 900px;
flex-flow: row wrap;
}

.controls__container {
display: flex;
justify-content: start;
gap: 30px;
flex-wrap: wrap;
}

.controls__container > button {
color: #0018a4;
border: none;

Choose a reason for hiding this comment

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

To avoid the "jumping" of the buttons when you hover over them you could change the border: none; to border: 2px solid transparent;

padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
cursor: pointer;
border-radius: 30px;
}
.controls__container.filter > button {
background-color: #ccffe2;
}
.controls__container.filter > button.active {
color: white;
background-color: #0018a4;
}
.controls__container.filter > button:hover {
border: 2px blue solid;
}

.controls__container.sort > button {
background-color: #ffecea;
}
.controls__container.sort > button.active {
background-color: #ff6589;
}
.controls__container.sort > button:hover {
border: 2px blue solid;
background-color: #ff6589;
}

#recipe__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
margin-top: 2rem;
column-gap: 10px;
row-gap: 20px;
}

.recipe__card {
display: flex;
flex-direction: column;
padding: 1rem;
/* border: 2px grey solid; */
border-radius: 10px;
width: 300px;
gap: 10px;
height: fit-content;
box-shadow: 1px 2px 3px 2px #dadada;
}

.recipe__card > img {
border-radius: 20px;
align-self: center;
max-width: 100%;
margin: 16px;
}

.recipe__card > h3,
h4 {
border-bottom: 1px grey solid;
padding-bottom: inherit;
}

.recipe__card > ul {
list-style: none;
padding-inline-start: 0px;
}

.container {
/* padding: 20px; */
width: 90dvw;
margin: 0 auto;
}